> ## Documentation Index
> Fetch the complete documentation index at: https://docs.morphllm.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Model Router

> Classifies prompt difficulty, ambiguity, and domain for automatic model selection

Not every prompt needs a \$15/M-token model. A "fix this typo" request and a "design an event sourcing system" request look identical to your API call, but one costs 10x more than it should.

The Morph Router classifies prompt difficulty, ambiguity, and domain in a single \~180ms call, then tells you which model to send it to. Trained on millions of coding prompts. \$0.005 per request.

**Pricing**: \$0.005/request | **Max input**: 65,536 tokens

<Frame>
  <img src="https://mintcdn.com/morph-555d6c14/eYc0Iv7sw-Cjqxn8/images/router-model-radar-fable-deepseek.svg?fit=max&auto=format&n=eYc0Iv7sw-Cjqxn8&q=85&s=fe4a2fcc2415389aa0941d43b44c19d0" alt="Radar chart comparing candidate model routing profiles" width="1200" height="760" data-path="images/router-model-radar-fable-deepseek.svg" />
</Frame>

## Quick Start

Ask the router which model to use, then call it:

<Tabs>
  <Tab title="cURL">
    ```bash theme={null}

    curl -s -X POST "https://api.morphllm.com/v1/router/multimodel" \
      -H "Authorization: Bearer YOUR_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "input": "Add error handling to this function",
        "allowed_providers": ["anthropic"]
      }'
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    // Ask the router which model to use
    const res = await fetch("https://api.morphllm.com/v1/router/multimodel", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.MORPH_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        input: "Add error handling to this function",
        allowed_providers: ["anthropic"],
      }),
    });
    const { model } = await res.json(); // call this model next
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    import requests

    MORPH_API_KEY = "YOUR_API_KEY"

    # Ask the router which model to use
    resp = requests.post(
        "https://api.morphllm.com/v1/router/multimodel",
        headers={
            "Authorization": f"Bearer {MORPH_API_KEY}",
            "Content-Type": "application/json",
        },
        json={
            "input": "Add error handling to this function",
            "allowed_providers": ["anthropic"],
        },
    )
    model = resp.json()["model"]  # call this model next
    ```
  </Tab>
</Tabs>

Two endpoints. [`/v1/router/multimodel`](#router-multimodel) hands Morph your model list and returns the one to call. [`/v1/router/classify`](#router-classify) returns the raw classifier labels and leaves the mapping to you.

<h2 id="router-multimodel">
  /router/multimodel
</h2>

Hand the router your candidate models (or whole providers) plus a policy. It classifies the prompt and returns the single best model to call, with no mapping table to maintain.

**Request**

| Field               | Type      | Description                                                                                                                                          |
| ------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `input`             | string    | The prompt to route (required).                                                                                                                      |
| `allowed_models`    | string\[] | Restrict selection to these exact models, e.g. `["kimi-k3", "claude-haiku-4-5-20251001"]`. Optional.                                                 |
| `allowed_providers` | string\[] | Restrict selection to these providers (`openai`, `anthropic`, `gemini`, `deepseek`, `zai`, `moonshot`). Optional.                                    |
| `policy`            | string    | `"balanced"` (default), `"cost_efficient"`, `"capability_heavy"`, or `"domain_skills"`.                                                              |
| `default_model`     | string    | Fallback returned as-is when the prompt is too ambiguous to size (`needs_info`). Must satisfy the allowed filter, or the call returns 400. Optional. |

`allowed_models` and `allowed_providers` are unioned: a model qualifies if it matches either. Leaving both empty considers the whole catalog, which is the right way to explore the API and the wrong way to run it in production. Two or three candidates is the shape that holds up. See [Best practices](#best-practices).

**Model catalog**

| Provider    | Models                                                                                |
| ----------- | ------------------------------------------------------------------------------------- |
| `openai`    | `gpt-5.5`                                                                             |
| `anthropic` | `claude-haiku-4-5-20251001`, `claude-sonnet-4-6`, `claude-opus-4-8`, `claude-fable-5` |
| `gemini`    | `gemini-3.5-flash`, `gemini-3.1-pro-preview`                                          |
| `deepseek`  | `deepseek-v4-flash`, `deepseek-v4-pro`                                                |
| `zai`       | `glm-5.2`                                                                             |
| `moonshot`  | `kimi-k3`                                                                             |

**Policies**

Selection scores every candidate on how far it sits from the request's difficulty and ambiguity tier, whether it covers the domain, and what it costs. The policy sets the weights:

| Policy               | Difficulty | Ambiguity | Domain | Cost | Behavior                                                                                                 |
| -------------------- | ---------- | --------- | ------ | ---- | -------------------------------------------------------------------------------------------------------- |
| `balanced` (default) | 30         | 60        | 0      | 0    | Capability-first and cost-blind. Among models that cover the request equally, the more capable one wins. |
| `cost_efficient`     | 25         | 40        | 0      | 2    | Same coverage test, cheaper tie-break. Drops a tier rather than pay for headroom.                        |
| `capability_heavy`   | 30         | 70        | 80     | 0    | Domain-aware and cost-blind. The strongest in-domain model for the request.                              |
| `domain_skills`      | 30         | 70        | 95     | 3    | Domain-aware and cost-aware. The cheapest in-domain specialist that still covers the tier.               |

`balanced` and `cost_efficient` have domain weight 0, so they return the same model for every domain at a given difficulty and ambiguity.

**Where each request lands**

With the full catalog allowed under `balanced`:

|              | Ambiguity `low`     | Ambiguity `med`     | Ambiguity `high` |
| ------------ | ------------------- | ------------------- | ---------------- |
| **`easy`**   | `deepseek-v4-flash` | `claude-sonnet-4-6` | `claude-fable-5` |
| **`medium`** | `glm-5.2`           | `glm-5.2`           | `claude-fable-5` |
| **`hard`**   | `glm-5.2`           | `claude-opus-4-8`   | `claude-fable-5` |

GLM-5.2 owns the medium tier outright and takes hard work at low ambiguity. Opus keeps hard x med-ambiguity. High ambiguity always escalates to Fable 5, the strongest model for prompts whose intent is unclear.

Switch to `cost_efficient` and five cells move: medium x low drops to `deepseek-v4-pro`, hard x med drops to `glm-5.2`, and the high-ambiguity column above `easy` goes to `kimi-k3`, which covers the same cells as Fable 5 at less than half the price. Restrict to `allowed_providers: ["anthropic"]` and you get Sonnet everywhere except hard x med (Opus) and high ambiguity (Fable 5).

<Tabs>
  <Tab title="cURL">
    ```bash theme={null}
    curl -s -X POST "https://api.morphllm.com/v1/router/multimodel" \
      -H "Authorization: Bearer YOUR_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "input": "Add error handling to this function",
        "allowed_models": ["claude-haiku-4-5-20251001", "kimi-k3"],
        "policy": "balanced",
        "default_model": "kimi-k3"
      }'
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    import requests

    MORPH_API_KEY = "YOUR_API_KEY"

    resp = requests.post(
        "https://api.morphllm.com/v1/router/multimodel",
        headers={
            "Authorization": f"Bearer {MORPH_API_KEY}",
            "Content-Type": "application/json",
        },
        json={
            "input": "Add error handling to this function",
            "allowed_models": ["claude-haiku-4-5-20251001", "kimi-k3"],
            "policy": "balanced",
            "default_model": "kimi-k3",
        },
    )
    model = resp.json()["model"]  # call this model next
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    const res = await fetch("https://api.morphllm.com/v1/router/multimodel", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.MORPH_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        input: "Add error handling to this function",
        allowed_models: ["claude-haiku-4-5-20251001", "kimi-k3"],
        policy: "balanced",
        default_model: "kimi-k3",
      }),
    });

    const { model } = await res.json(); // call this model next
    ```
  </Tab>
</Tabs>

**Response**

```json theme={null}
{
  "model": "claude-haiku-4-5-20251001",
  "provider": "anthropic",
  "difficulty": "easy",
  "confidence": 0.93,
  "ambiguity": "low",
  "ambiguity_confidence": 0.88,
  "domain": "coding",
  "domain_confidence": 0.91
}
```

`model` is what you call next. The classifier signals are echoed back so you can act on them too, e.g. show a "let's clarify" prompt when `difficulty` is `needs_info`. `ambiguity` and `domain` are present only when those heads cleared their threshold; treat a missing field as "no signal." If the prompt resolves to `needs_info` and you passed a `default_model`, that model is returned as-is.

<h2 id="best-practices">
  Best practices
</h2>

Routing saves money by moving requests off models they don't need. It loses money when the switching itself costs more than the tier difference. Four rules keep it on the right side of that line.

### Route between two or three models, not ten

Every extra candidate is another prefix cache, another failure mode, and another surface to evaluate. Two tiers, cheap and strong, capture most of the available savings. A third earns its place only when it owns a cell the other two are genuinely bad at.

Passing the whole catalog looks like more optimization and is usually less: it maximizes how often the model changes, which is the thing that costs you (see the next rule). In production, pin a set:

```json theme={null}
{ "allowed_models": ["claude-haiku-4-5-20251001", "kimi-k3"], "default_model": "kimi-k3" }
```

### Be cache-aware: a model switch is a full re-prefill

The router returns a model per call, but calling it per turn is usually wrong. Switching models mid-session invalidates the upstream KV prefix cache, so the next turn re-prefills the entire conversation from scratch at the full input rate.

The size of that mistake: on Morph's own models, cached input is $0.22/M against $1.10/M uncached, an 80% discount you forfeit on every switch. A 60k-token agent session that "saves" money by moving from a $15/M model to a $6/M model pays for 60k tokens of fresh prefill to do it, and can come out behind.

Classify at session and task boundaries. Not every turn.

### Know your cache-breaking events

A switch is free when the cache was already cold, and expensive when it wasn't. These are the moments that decide which:

| Event                                               | Cache impact                      | What to do                                   |
| --------------------------------------------------- | --------------------------------- | -------------------------------------------- |
| New session, first turn                             | Nothing cached yet                | The best place to route. The switch is free. |
| Context compaction                                  | Prefix rewritten anyway           | Re-route here. You already lost the cache.   |
| Model or provider switch                            | Full re-prefill on the new model  | Only at a boundary you're already paying for |
| Editing an earlier message                          | Everything after it invalidated   | Append, don't rewrite                        |
| Timestamp, UUID, or request id in the system prompt | Kills every hit after it          | Move variable content last                   |
| Reordered or regenerated tool definitions           | Prefix diverges at the tool block | Serialize tools deterministically            |
| `cache_ttl` expiry                                  | Prefix stops hitting entirely     | Raise the TTL or accept the recompute        |

Details on all of these: [Prompt Caching](/sdk/components/caching).

### Pin the model once the context is expensive

Past roughly 60k tokens, prefill dominates any per-token rate difference. Stop re-classifying and hold whatever the session is already on. Morph's own [Claude Code proxy](#claude-code) ships this as a context lock: once a turn's context passes the threshold it skips the classify call entirely and keeps the route, so the prefix cache keeps hitting.

If your context is the problem, shrink it rather than re-route around it. [Compact](/sdk/components/compact) cuts 50-70% and is itself a clean boundary to re-route on.

<h2 id="labels">
  Labels
</h2>

The classifier heads return these labels. `/v1/router/multimodel` maps them for you; `/v1/router/classify` hands them over raw.

**Difficulty**

| Label        | What it means                                                      | Example mapping                     |
| ------------ | ------------------------------------------------------------------ | ----------------------------------- |
| `easy`       | Trivial change, any model handles it                               | Haiku, DeepSeek Flash, Gemini Flash |
| `medium`     | Moderate complexity, benefits from a capable model                 | Sonnet, GLM-5.2, GPT-5.5            |
| `hard`       | Complex task, needs a strong model                                 | Opus, Kimi K3, Gemini Pro           |
| `needs_info` | Ambiguous prompt: difficulty didn't clear the confidence threshold | Your default model                  |

**Ambiguity**

| Label  | What it means                          |
| ------ | -------------------------------------- |
| `low`  | Well-specified request                 |
| `med`  | Some detail missing                    |
| `high` | Underspecified; may need clarification |

**Domain**

| Label     | What it means              |
| --------- | -------------------------- |
| `general` | General-purpose prompt     |
| `summary` | Summarization / extraction |
| `coding`  | Code generation or editing |
| `design`  | Design / architecture      |
| `data`    | Data / analytics           |

<h2 id="router-classify">
  /router/classify
</h2>

Runs the requested classifier heads against your prompt and returns the raw labels. Use this when you already have a model mapping you trust and only want the signals.

**Request**

| Field     | Type      | Description                                                                                     |
| --------- | --------- | ----------------------------------------------------------------------------------------------- |
| `input`   | string    | The prompt to classify (required).                                                              |
| `classes` | string\[] | Which heads to run: `"difficulty"`, `"ambiguity"`, `"domain"`. Optional, defaults to all three. |

<Tabs>
  <Tab title="cURL">
    ```bash theme={null}
    curl -s -X POST "https://api.morphllm.com/v1/router/classify" \
      -H "Authorization: Bearer YOUR_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "input": "Add error handling to this function",
        "classes": ["difficulty", "ambiguity", "domain"]
      }'
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    import requests

    MORPH_API_KEY = "YOUR_API_KEY"

    resp = requests.post(
        "https://api.morphllm.com/v1/router/classify",
        headers={
            "Authorization": f"Bearer {MORPH_API_KEY}",
            "Content-Type": "application/json",
        },
        json={
            "input": "Add error handling to this function",
            "classes": ["difficulty", "ambiguity", "domain"],
        },
    )
    classifications = resp.json()["classifications"]
    difficulty = classifications["difficulty"]["label"]
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    const res = await fetch("https://api.morphllm.com/v1/router/classify", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.MORPH_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        input: "Add error handling to this function",
        classes: ["difficulty", "ambiguity", "domain"],
      }),
    });

    const { classifications } = await res.json();
    const difficulty = classifications.difficulty.label;
    ```
  </Tab>
</Tabs>

**Response**

```json theme={null}
{
  "classifications": {
    "difficulty": { "class_id": 0, "label": "easy",   "confidence": 0.93, "meets_threshold": true },
    "ambiguity":  { "class_id": 0, "label": "low",    "confidence": 0.88, "meets_threshold": true },
    "domain":     { "class_id": 2, "label": "coding", "confidence": 0.91, "meets_threshold": true }
  }
}
```

Each head returns `label`, `class_id`, `confidence`, and `meets_threshold` (whether confidence cleared the head's threshold). When `difficulty` does **not** meet its threshold, treat it as `needs_info`: the prompt is too ambiguous to size confidently.

## Production example

Route once per session, then reuse the decision for every turn in it. This is the shape that actually saves money, because the model only changes when the cache was going to be cold anyway.

<Tabs>
  <Tab title="TypeScript">
    ```typescript theme={null}
    import OpenAI from 'openai';

    const openai = new OpenAI();
    const CANDIDATES = ["claude-haiku-4-5-20251001", "kimi-k3"];
    const sessionModel = new Map<string, string>();

    async function modelForSession(sessionId: string, firstTurn: string) {
      const cached = sessionModel.get(sessionId);
      if (cached) return cached; // don't re-route mid-session: the prefix cache is warm

      const res = await fetch("https://api.morphllm.com/v1/router/multimodel", {
        method: "POST",
        headers: {
          Authorization: `Bearer ${process.env.MORPH_API_KEY}`,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          input: firstTurn,
          allowed_models: CANDIDATES,
          policy: "balanced",
          default_model: "kimi-k3",
        }),
      });

      const { model } = await res.json();
      sessionModel.set(sessionId, model);
      return model;
    }

    async function handleTurn(sessionId: string, messages: any[]) {
      const model = await modelForSession(sessionId, messages[0].content);
      return await openai.chat.completions.create({ model, messages });
    }

    // "Add a TODO comment"           → easy → claude-haiku-4-5-20251001
    // "Design event sourcing system" → hard → kimi-k3
    // Every later turn in that session reuses the same model, cache intact.
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    import os
    import requests
    from openai import OpenAI

    openai = OpenAI()
    MORPH_API_KEY = os.environ["MORPH_API_KEY"]
    CANDIDATES = ["claude-haiku-4-5-20251001", "kimi-k3"]
    _session_model: dict[str, str] = {}


    def model_for_session(session_id: str, first_turn: str) -> str:
        if session_id in _session_model:
            return _session_model[session_id]  # cache is warm, don't switch

        resp = requests.post(
            "https://api.morphllm.com/v1/router/multimodel",
            headers={
                "Authorization": f"Bearer {MORPH_API_KEY}",
                "Content-Type": "application/json",
            },
            json={
                "input": first_turn,
                "allowed_models": CANDIDATES,
                "policy": "balanced",
                "default_model": "kimi-k3",
            },
            timeout=5,
        )
        model = resp.json()["model"]
        _session_model[session_id] = model
        return model


    def handle_turn(session_id: str, messages: list[dict]):
        model = model_for_session(session_id, messages[0]["content"])
        return openai.chat.completions.create(model=model, messages=messages)
    ```
  </Tab>
</Tabs>

Wrap the router call in a try/catch and fall back to a safe default model if it ever fails. `default_model` already covers the `needs_info` case, not a transport failure.

<h2 id="claude-code">
  Integrate with Claude Code
</h2>

Route every Claude Code turn through the router with no change to how developers work. A local proxy sits between Claude Code and Anthropic (`ANTHROPIC_BASE_URL` points at it), classifies each turn, and picks the cheapest Claude model, and reasoning effort, that can handle it, following your org's routing policy.

Requires macOS or Linux, Node 22+, and the `claude` CLI.

**1. Install** with your Morph API key:

```bash theme={null}
curl -fsSL https://morphllm.com/router/install.sh | MORPH_API_KEY=sk-... bash
```

The installer pulls the latest routing runtime, verifies its sha256, installs it under `~/.morph/ccr-router`, and gives you a `morph-claude` command. Re-running the one-liner upgrades in place.

**2. Authenticate upstream.** By default the proxy uses your Claude Pro/Max subscription: run `claude login` once. To use your org's Anthropic key instead, add it to the install:

```bash theme={null}
curl -fsSL https://morphllm.com/router/install.sh | MORPH_API_KEY=sk-... ANTHROPIC_API_KEY=sk-ant-org-... bash
```

**3. Run** `morph-claude` instead of `claude`:

```bash theme={null}
morph-claude              # Claude Code, routed per-turn by your org policy
morph-claude uninstall    # full removal, restores your pre-install config
```

Each turn is classified on difficulty, ambiguity, and domain, matched against your org's routing matrix to a `{model, effort}`, then clamped to the models that user is permitted. One decision per turn, 1.5s classify timeout; if the classify ever fails, routing fails open to your default model.

The proxy is cache-aware by default. Once a turn's context passes the context lock threshold (60k tokens out of the box) it stops classifying and holds the session's current model, so the upstream prefix cache keeps hitting instead of being thrown away for a cheaper per-token rate that no longer pays for itself.

**Set the policy.** Admins configure the routing matrix and per-user permissions in the dashboard under **Administration → Model Router**. Edits reach every developer within the hour, with no redeploy and no reinstall. The **Analytics** tab shows the model mix, turn volume, and estimated savings vs sending every turn to Opus. Prefer to own the policy? Point the proxy at a local `router-matrix.json` or an endpoint you host via `MORPH_MATRIX_FILE` / `MORPH_MATRIX_URL`.

<Note>
  Routing metrics are metadata-only. No prompt or completion text ever leaves the machine. Set `MORPH_METRICS_DISABLED=1` to send nothing at all.
</Note>

## Edge / Cloudflare Workers

`fetch` is available natively at the edge, so you can call the router from a Cloudflare Worker, Vercel Edge Function, or Deno with no SDK:

```typescript theme={null}
export default {
  async fetch(request: Request, env: Env) {
    const { input } = await request.json();

    const res = await fetch("https://api.morphllm.com/v1/router/multimodel", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${env.MORPH_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ input, allowed_providers: ["anthropic"] }),
    });
    const { model } = await res.json();

    return Response.json({ model });
  }
};
```

<Note>
  The `@morphllm/morphsdk/edge` build ships a `RawRouter` helper, but it targets the legacy [`/router/raw`](#deprecated-endpoints) endpoint. For the current endpoints, call them directly with `fetch` as shown above.
</Note>

## API Reference

Both endpoints are `POST https://api.morphllm.com/...` with an `Authorization: Bearer YOUR_API_KEY` header.

<Tabs>
  <Tab title="/router/multimodel">
    ```
    POST /v1/router/multimodel

    Request:
    {
      "input": "string",                       // required
      "allowed_models": ["kimi-k3"],           // optional
      "allowed_providers": ["anthropic"],      // optional: "openai" | "anthropic" | "gemini"
                                               //         | "deepseek" | "zai" | "moonshot"
      "policy": "balanced",                    // "balanced" (default) | "cost_efficient" | "capability_heavy" | "domain_skills"
      "default_model": "claude-sonnet-4-6"     // optional, returned as-is on needs_info; 400 if outside the allow filter
    }

    Response:
    {
      "model": "claude-haiku-4-5-20251001",
      "provider": "anthropic",
      "difficulty": "easy",
      "confidence": 0.93,
      "ambiguity": "low",              // present only when the head clears its threshold
      "ambiguity_confidence": 0.88,
      "domain": "coding",
      "domain_confidence": 0.91
    }
    ```
  </Tab>

  <Tab title="/router/classify">
    ```
    POST /v1/router/classify

    Request:
    {
      "input": "string",                                 // required
      "classes": ["difficulty", "ambiguity", "domain"]   // optional, defaults to all three
    }

    Response:
    {
      "classifications": {
        "difficulty": { "class_id": 0, "label": "easy",   "confidence": 0.93, "meets_threshold": true },
        "ambiguity":  { "class_id": 0, "label": "low",    "confidence": 0.88, "meets_threshold": true },
        "domain":     { "class_id": 2, "label": "coding", "confidence": 0.91, "meets_threshold": true }
      }
    }
    ```
  </Tab>
</Tabs>

Inputs over 65,536 tokens return `413`. An unknown `classes` value or a `default_model` outside the allow filter returns `400`.

## When to Use

**Use the router when:**

* Processing varied user requests (simple typo fixes to complex architecture tasks)
* You want to minimize API costs without manually classifying prompts
* Building cost-conscious AI products with mixed complexity workloads

**Skip the router when:**

* All tasks need the same model tier (e.g., always Opus for agentic coding)
* The \~180ms routing latency matters more than cost savings
* You need deterministic model selection for testing or compliance

## Performance

* **Latency**: \~180ms average, one call per routing decision
* **Parallel**: Can run in parallel with other work
* **HTTP/2**: Connection reuse for subsequent calls

***

<h2 id="deprecated-endpoints">
  Deprecated endpoints
</h2>

<Warning>
  `/v1/router/raw` and `/v1/router/{provider}` are superseded by [`/v1/router/classify`](#router-classify) and [`/v1/router/multimodel`](#router-multimodel). They remain **fully supported for backward compatibility**, so existing integrations keep working with no changes, but new code should use the endpoints above. The provider endpoints will be removed in a future release.
</Warning>

### /router/raw

Returns just a difficulty label. Use [`/v1/router/classify`](#router-classify) instead for new code.

<Tabs>
  <Tab title="cURL">
    ```bash theme={null}
    curl -s -X POST "https://api.morphllm.com/v1/router/raw" \
      -H "Authorization: Bearer YOUR_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "input": "Add error handling to this function",
        "mode": "balanced"
      }'
    ```

    Returns: `{ "difficulty": "easy", "confidence": 0.93 }`
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    import requests

    MORPH_API_KEY = "YOUR_API_KEY"

    resp = requests.post(
        "https://api.morphllm.com/v1/router/raw",
        headers={
            "Authorization": f"Bearer {MORPH_API_KEY}",
            "Content-Type": "application/json",
        },
        json={"input": "Add error handling to this function", "mode": "balanced"},
    )
    difficulty = resp.json()["difficulty"]
    ```
  </Tab>

  <Tab title="TypeScript SDK">
    ```typescript theme={null}
    import { MorphClient } from '@morphllm/morphsdk';

    const morph = new MorphClient({ apiKey: "YOUR_API_KEY" });

    const { difficulty } = await morph.routers.raw.classify({
      input: 'Add error handling to this function',
      mode: 'balanced', // 'balanced' (default) | 'aggressive'
    });
    ```
  </Tab>
</Tabs>

**Modes** — `balanced` (default) balances cost and quality; `aggressive` optimizes harder for cost, pushing more prompts to `easy`. Returns `difficulty` (`easy` | `medium` | `hard` | `needs_info`).

For edge environments (Cloudflare Workers, Vercel Edge, Deno), use `@morphllm/morphsdk/edge`:

```typescript theme={null}
import { RawRouter } from '@morphllm/morphsdk/edge';

export default {
  async fetch(request: Request, env: Env) {
    const { input } = await request.json();
    const router = new RawRouter({ apiKey: env.MORPH_API_KEY });
    const { difficulty } = await router.classify({ input });
    return Response.json({ difficulty });
  }
};
```

### /router/{provider}

Returns a provider-specific model name directly instead of a difficulty label. Registered for `openai`, `anthropic`, and `gemini` only; there is no `/v1/router/zai`, `/v1/router/deepseek`, or `/v1/router/moonshot`. Use [`/v1/router/multimodel`](#router-multimodel) with `allowed_providers` instead: it does the same model selection with control over the candidate set and policy.

Under the hood these now call the multimodel router constrained to that provider, so they keep working with no changes on your side.

```bash theme={null}
curl -s -X POST "https://api.morphllm.com/v1/router/anthropic" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"input": "your task", "mode": "balanced"}'
```

Returns: `{ "model": "claude-haiku-4-5-20251001", "confidence": 0.93 }`

The SDK still exposes `morph.routers.anthropic.selectModel()`, `morph.routers.openai.selectModel()`, and `morph.routers.gemini.selectModel()` for backwards compatibility. Migrate to [`/v1/router/multimodel`](#router-multimodel).

## See Also

* [Prompt Caching](/sdk/components/caching) — what a model switch costs you, and how to keep hits
* [Compact](/sdk/components/compact) — shrink context instead of routing around it
* [Enterprise Model Routing](/guides/enterprise-model-routing) — org-wide policy for Claude Code
* [Open Source Models](/sdk/components/fast-models) — the models Morph serves directly
