Skip to content
This page is a translation of the authoritative Chinese source and may lag behind.View the original

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)

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

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:

PositionShapeCondition path
Message top-level content blockmessages[].content[] is directly an image blockmessages.*.content.*.type equals image
Tool result nestingmessages[].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:

ApproachEffectSuitable for
switch-route (this page)Image-bearing requests redirect to the vision model; image information is preservedImage content is valuable to the answer
filter-content-typesStrips images; the original model serves as usualOnly 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.