> Raw Markdown twin (generated at build time from the source Markdown). Rendered page: https://docs.gatellm.io/en/howto/route-image-requests-to-vision-model · Doc index: https://docs.gatellm.io/en/llms.txt


# Route image-bearing requests to a vision model

When a text model (a model that does not support vision) receives an image-bearing request, the upstream rejects it with a 400. A typical trigger scenario: coding-agent clients (Claude Code, etc.) replay the entire conversation history — once any turn in the history contained an image (even if it was just a tool reading a screenshot), every subsequent turn re-sends that image, and the text model errors out on every turn from then on.

The fix: add a `switch-route` rule (a mode of the request-body rule) to that model. It detects images before protocol translation based on the **client** request shape, and on a match redirects this request to the vision model; requests without images proceed to the original model as usual. The predicate only does structural checks and does not read the image binary content, so a large body with base64 is not read entirely into memory.

## Prerequisites

- There is already a text model route (below, the "original text model"), and clients report a 400 on image-bearing requests.
- A vision-capable target model (below, the "vision model") is already configured. The `switch-route` target **supports cross-protocol**: the client speaks Anthropic while the vision model goes through OpenAI Chat Completions — that works too.
- The client does not need authorization for the vision model: the redirect target is treated as an admin-designated internal route, and the gateway does not re-validate the caller's group model ACL for it (the model ACL is only validated once at request ingress against the client's original model).

## Configuration steps (console)

Console → Upstreams & Models → find the **original text model** → Edit → **Request body rules** → **+ Add rule**:

1. In the **Mode** dropdown select `switch-route`; the row expands two controls.
2. **Switch to model (use_model)**: select the vision model from the dropdown.
3. **Match conditions (when)**: add predicates one by one, each filling in the **field path** (written according to the **client protocol**'s shape); leaving "equals" blank means "the field existing is a match", filling a value means "value equality is a match"; multiple predicates are OR-ed together.

The actual console UI (the screenshot is the English UI; the Chinese UI corresponds to "Request payload rules / Switch to model (use_model) / Match conditions when / + Add condition"):

![Request Payload Rules: the Mode dropdown expanded, switch-route selected (last item highlighted)](/images/usecases/switch-route-mode-dropdown.png)

*After selecting `switch-route` for the mode, the rule row expands the "Switch to model (use_model)" and "Match conditions when" control groups; leaving "equals" blank is an existence check.*

![Switch to model (use_model): the dropdown lists all models configured in the gateway, with the target vision model selected; add match conditions one by one below](/images/usecases/switch-route-use-model-dropdown.png)

*The `use_model` dropdown only lists models already configured in the gateway — if the target vision model is not created yet, you cannot select it here; create the model first. The name is the full name shown in the list, possibly with an upstream prefix.*

Save to take effect (configuration hot-reloads, no restart needed).

> ⚠️ The rule must be placed on **the model configuration that the client request actually hits**. If the client model name is a load balancer (LB), place the rule on the node models behind the LB — the node the LB resolves to runs with its own request-body rules.

## How to write the condition path

The position of the image in the request body **depends on the client protocol**, and there is one easily-missed deep position for Anthropic clients. Using three common client protocols as examples:

### Anthropic client (/v1/messages)

Anthropic's image block is `{"type": "image", "source": {...}}` and can appear at **two depths**:

| Position | Shape | Condition path |
|------|------|----------|
| Message top-level content block | `messages[].content[]` is directly an image block | `messages.*.content.*.type` equals `image` |
| Tool result nesting | `messages[].content[]` is a `tool_result` block, and the image block is inside its `content[]` | `messages.*.content.*.content.*.type` equals `image` |

**Add both conditions.** Configuring only the top-level one is a common mistake: after a coding agent reads an image file through a tool, the image enters the history as a `tool_result` content block, one level deeper than the top level — the top-level condition cannot match it, and the image-bearing request still hits the text model and keeps getting a 400.

### OpenAI Chat Completions client (/v1/chat/completions)

The image block is `{"type": "image_url", ...}` and only appears at the top level of user messages (the content of tool-role messages is a string, with no nested images):

- `messages.*.content.*.image_url`, leave "equals" blank (existence check)

### DashScope client

- `input.messages.*.content.*.image`, leave "equals" blank (existence check)

When multiple client protocols coexist, list each protocol's path in `when` (OR semantics; any one matching triggers the switch).

## JSON shape

The `request_payload` rule corresponding to the console configuration above (Anthropic client, appended to the original text model's `request_payload` array, coexisting with existing rules):

```json
{
  "mode": "switch-route",
  "when": [
    { "path": "messages.*.content.*.type", "eq": "image" },
    { "path": "messages.*.content.*.content.*.type", "eq": "image" }
  ],
  "use_model": "qwen-vl-max"
}
```

## Verification

After saving, replay an image-bearing request (or use a minimal case: a user message containing one image block), and look at that request in the console logs viewer:

- The **Model** column should show the vision model (not the original text model), the upstream protocol is the vision model's protocol, and the status code is 200.
- Send another plain-text request; the **Model** column should still be the original text model — confirming nothing else was affected.

## Behavior semantics and caveats

- **Routing is per-request, not per-turn.** Once an image enters the conversation history, every subsequent turn's request carries it and every turn matches the condition and redirects to the vision model. This is expected behavior: if the context has an image, a vision model is needed to understand it.
- **The redirect carries the full history.** On a match, the entire history is translated for the vision model (including the plain-text turns in it), billed at the vision model's rate.
- **The predicate only does structural checks.** The `*` in the path is an array wildcard (any element matching suffices); leaving "equals" blank is an existence check. The predicate does not read string values' content nor binaries, and disk spooling of large bodies stays enabled.

## Alternative: don't switch models, just strip images

If the business can accept "not seeing images as long as there is no error", switch to the `filter-content-types` rule instead: the gateway strips image blocks from the request and injects a piece of prompt text in place (customizable), and the request is legally sent to the text model. The two approaches are mutually exclusive — pick one:

| Approach | Effect | Suitable for |
|------|------|------|
| `switch-route` (this page) | Image-bearing requests redirect to the vision model; image information is preserved | Image content is valuable to the answer |
| `filter-content-types` | Strips images; the original model serves as usual | Only want to eliminate errors, don't care about image content |

**Next**: [Request rewrite vs routing: which to pick](/en/practices/routing-and-transform.md) for the selection boundaries among the three rewrite approaches; [Upstream & model fields](/en/reference/upstreams-models-fields.md) for the full list of `request_payload` modes.
