> 原始 Markdown 孪生体（构建期从源 Markdown 生成）。渲染页：https://docs.gatellm.io/zh-CN/howto/route-image-requests-to-vision-model · 文档索引：https://docs.gatellm.io/zh-CN/llms.txt


# 把含图片的请求路由到视觉模型

文本模型（不支持视觉的模型）收到带图片的请求，上游会以 400 拒收。典型触发场景：coding agent 类客户端（Claude Code 等）会重放整段对话历史，一旦历史里某轮出现过图片——哪怕只是工具读过一张截图——后续每一轮都会带着这张图重发，文本模型从此轮轮报错。

解法：给该模型加一条 `switch-route` 规则（请求体规则的一种模式）。它在协议翻译**前**按**客户端**请求形状检测图片，命中即把本次请求改投到视觉模型；不含图片的请求照常走原模型。判定只做结构检查、不读图片二进制内容，含 base64 的大请求体不会被整段读入内存。

## 前置

- 已有一个文本模型路由（下称「原文本模型」），客户端反馈带图请求 400。
- 已配好一个支持视觉的目标模型（下称「视觉模型」）。`switch-route` 的目标**支持跨协议**：客户端说 Anthropic、视觉模型走 OpenAI Chat Completions，也可以。
- 客户端无需授权视觉模型：改投目标视为管理员指定的内部路由，网关不对它重新校验调用方分组的模型 ACL（模型 ACL 只在请求入口对客户端原始模型校验一次）。

## 配置步骤（控制台）

控制台 → 上游与模型 → 找到**原文本模型** → 编辑 → **请求体规则** → **+ 添加规则**：

1. **模式**下拉选 `switch-route`，该行展开两个控件。
2. **切换到模型（use_model）**：下拉选视觉模型。
3. **命中条件（when）**：逐条添加判定，每条填**字段路径**（按**客户端协议**的形状写）；「等于」留空表示「字段存在即命中」，填值表示「值相等才命中」；多条之间是**或**的关系。

控制台实际界面（截图为英文 UI，中文 UI 对应「请求负载规则 / 切换到模型 (use_model) / 命中条件 when / + 添加条件」）：

![Request Payload Rules：模式下拉展开，选中 switch-route（列表末项高亮）](/images/usecases/switch-route-mode-dropdown.png)

*模式选 `switch-route` 后，规则行展开「Switch to model (use_model)」与「Match conditions when」两组控件；「等于」留空即存在性检查。*

![Switch to model (use_model)：下拉列出网关已配置的全部模型，选中目标视觉模型；下方逐条添加命中条件](/images/usecases/switch-route-use-model-dropdown.png)

*`use_model` 下拉只列网关里已配置的模型——目标视觉模型没建好时这里选不到，先建好模型。名字以列表里的完整名称为准，可能带上游前缀。*

保存即生效（配置热重载，无需重启）。

> ⚠️ 规则要挂在**客户端请求实际命中的那条模型配置**上。如果客户端模型名是一个负载均衡器（LB），规则挂在 LB 背后的节点模型上——LB 解析出的节点会带着自己的请求体规则执行。

## 条件路径怎么写

图片在请求体里的位置**取决于客户端协议**，且对 Anthropic 客户端有一个容易漏的深层位置。以三类常见客户端协议为例：

### Anthropic 客户端（/v1/messages）

Anthropic 的图片块是 `{"type": "image", "source": {...}}`，可能出现在**两个深度**：

| 位置 | 形状 | 条件路径 |
|------|------|----------|
| 消息顶层内容块 | `messages[].content[]` 直接是 image 块 | `messages.*.content.*.type` 等于 `image` |
| 工具结果嵌套 | `messages[].content[]` 是 `tool_result` 块，其 `content[]` 里才是 image 块 | `messages.*.content.*.content.*.type` 等于 `image` |

**两个条件都要加。** 只配顶层那条是常见错误：coding agent 用工具读过图片文件后，图片以 `tool_result` 内容块的形式进入历史，比顶层深一层——顶层条件匹配不到它，带图请求照样打到文本模型继续 400。

### OpenAI Chat Completions 客户端（/v1/chat/completions）

图片块是 `{"type": "image_url", ...}`，只出现在 user 消息顶层（tool 角色消息的 content 是字符串，没有嵌套图片）：

- `messages.*.content.*.image_url`，「等于」留空（存在性检查）

### DashScope 客户端

- `input.messages.*.content.*.image`，「等于」留空（存在性检查）

多种客户端协议并存时，把各协议的路径都并列进 `when`（OR 语义，任一命中即切换）。

## JSON 形态

上面的控制台配置对应的 `request_payload` 规则（Anthropic 客户端，追加进原文本模型的 `request_payload` 数组，与已有规则共存）：

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

## 验证

配置保存后，重放一条带图片的请求（或用最小用例：user 消息含一个 image 块），到控制台日志查看器看这条请求：

- **模型**列应显示视觉模型（而非原文本模型），上游协议是视觉模型的协议，状态码 200。
- 再发一条纯文本请求，**模型**列应仍是原文本模型——确认未误伤。

## 行为语义与注意事项

- **按请求粒度路由，不是按轮次。** 图片一旦进入对话历史，后续每一轮请求都会带着它、每一轮都会命中条件改投视觉模型。这是预期行为：上下文里有图，就需要视觉模型来理解。
- **改投会带上全部历史。** 命中后整段历史被翻译给视觉模型（包括其中的纯文本轮次），按视觉模型计价。
- **谓词只做结构检查。** 路径里的 `*` 是数组通配（任一元素命中即可）；「等于」留空是存在性检查。谓词不读字符串值的内容、不读二进制，大 body 的磁盘暂存保持开启。

## 备选方案：不切模型，直接剥图

如果业务上「看不见图也行、只要别报错」，改用 `filter-content-types` 规则：网关把图片块从请求里剥掉、在原位注入一段提示文本（可自定义），请求合法地送给文本模型。两种方案互斥，取其一：

| 方案 | 效果 | 适用 |
|------|------|------|
| `switch-route`（本页） | 带图请求改投视觉模型，图片信息保留 | 图片内容对回答有价值 |
| `filter-content-types` | 剥掉图片，原模型照常服务 | 只想消除报错，不在乎图片内容 |

**下一步**：[请求改写与路由：怎么选](/zh-CN/practices/routing-and-transform.md) 看三种改写手段的选型边界；[上游与模型字段](/zh-CN/reference/upstreams-models-fields.md) 看 `request_payload` 全部模式一览。
