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-routetarget 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:
- In the Mode dropdown select
switch-route; the row expands two controls. - Switch to model (use_model): select the vision model from the dropdown.
- 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"):

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.

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):
{
"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 for the selection boundaries among the three rewrite approaches; Upstream & model fields for the full list of request_payload modes.
