模型与账号行为
模型名如何路由到渠道,目录如何发现和缓存,能力和推理强度如何决定请求体,可用性探测,以及失败时如何换账号、重试、续期和记录额度。
本页讲网关在一个请求背后做的决定:把模型名交给哪个渠道,这个模型能做什么,reasoning_effort 最终变成了什么,失败时换不换账号、等不等、重不重试。日常使用不需要了解这些;当结果和预期不一样时,这里是答案。
模型名如何路由
请求里的 model 按下面的顺序解析:
-
写成
渠道/模型(如kilo/some-model),且渠道是一个正在运行的渠道 id:直接交给它,斜杠后面的部分原样作为上游模型名。 -
只写模型 id:按内置的渠道顺序(
cline、opencode、kilo、gemini、zcode、minimax、trae、qoder……)找第一个在目录中列出这个 id 的渠道。 -
都没找到,但只有一个渠道在运行:原样转发给它。
-
否则返回
404 model_not_found:Model '…' not found. Use `provider/model` or see /v1/models.
请求没有带 model 时使用 server.default_model;也没有设置时返回 model is required。
同一个 id 出现在多个渠道时,/v1/models 只列出第一个渠道的那一条。想指定渠道,就用 渠道/模型 的写法。
模型目录
每个渠道的模型列表来自上游自己的列表接口,能力信息来自 Hoshimi 内置的能力表和账号中的声明。
- 缓存 5 分钟:同一渠道的列表在 5 分钟内不会重复拉取。
- 防止异常缩水:新拉到的列表不到上一次的一半时,先保留旧列表一个周期,并在日志里警告
the catalogue came back at N of M models (likely a partial answer);再过一个周期仍然如此,才接受新列表。 - 失败时保留旧的:拉取失败时保留上一次成功的目录,30 秒后再试。从未成功过的渠道使用它内置的后备列表;后备列表只是历史数据,不代表这些模型现在可用。
- 跨重启保留:每次成功的目录保存在数据目录的
catalog-cache/中,启动时先恢复(只恢复 7 天以内、账号和选项完全相同的快照),然后立即在后台刷新。
网页界面的 模型 页和 Playground 通过 GET /api/v1/catalog/stream(SSE)加载目录:先发出已缓存的结果,再按渠道逐个发出新结果。拉取最多同时进行 8 个;一轮最多等 20 秒,没完成的渠道标记为超时,已经收到的模型保留。标准的 /v1/models 仍然是普通 JSON。
能力信息
管理接口 GET /api/v1/models 给出每个模型的能力:
| 字段 | 含义 |
|---|---|
vision | 能否读图 |
reasoning | 是否推理(思考) |
tools | 是否支持工具调用;null 表示不知道 |
context_window、max_output | 上下文长度和最大输出;没有资料时为 131072 和 32768 |
can_disable_thinking | 能否关闭思考 |
region_sensitive | 可用性是否取决于出口地区 |
free | 是否免费:true、false 或 null(不知道) |
efforts、effort_default | 这个模型自己的推理强度档位和默认档位 |
probe | 最近一次可用性探测的结果 |
unavailable | 是否正被网关记为失效(见下文) |
能力的来源,优先级从高到低:
- 渠道内置的能力表,以及账号或渠道选项中的声明(如
agg的model_specs,见 agg 聚合渠道); - 上游列表本身带的元数据(部分渠道);
- models.dev,但只补充没有任何声明的模型的
vision和reasoning两项。
models.dev 不提供上下文长度、输出上限和推理档位:同一个 id 在不同站点、不同套餐下的限制不同,这些必须描述你实际使用的那个端点。Hoshimi 内置了一份离线快照,联网后在后台拉取最新数据,只影响之后的查询,不会拖慢请求;拉取失败时保留已有信息。
free 是关于上游价格或访问方式的证据,不保证额度无限;没有证据时就是 null。本地模型不会自动标为免费。各站点的判断依据见 pi-free 迁移与差异。
推理强度
只有调用方明确给出档位时,网关才会改写请求:
- Chat Completions 的
reasoning_effort,或 Responses 风格的reasoning.effort; - 没有给出档位时请求体原样发送,调用方自己设置的
max_tokens保持不变。Playground 的 自动(服务端默认) 不发送档位。 - 原生 Anthropic Messages 请求(直接转发到支持 Messages 的上游时)保留调用方自己的 thinking 和输出设置,不经过这里的规则。
给出档位后,按模型分三种情况:
| 模型 | 处理方式 |
|---|---|
有自己的档位菜单(efforts 不为空) | 选出档位,把这个模型声明的补丁合并进请求体(例如写入 reasoning_effort,或 thinking、output_config.effort);max_tokens 设为模型输出上限与调用方上限中较小的一个 |
| 推理模型但没有菜单 | 用输出上限控制思考量:light 2,048、balanced 8,192、deep 用满模型的输出上限 |
| 不推理的模型 | 不改写 |
档位名称的对应:
| 调用方发送 | 无菜单模型(阶梯) | 有菜单的模型 |
|---|---|---|
minimal、low | light | 菜单中有同名档位就用它;否则 minimal 换成 low |
medium | balanced | 菜单中有就用它 |
high | deep | 菜单中有就用它 |
xhigh、max | deep | 菜单中有同名档位就用它;否则 xhigh 换成 max |
light、balanced、deep | 原样 | 分别换成 low、medium、high |
- 菜单模型经过上面的对应后仍然找不到档位时,使用它的默认档位;阶梯模型收到不认识的档位时使用
balanced。 - 菜单模型请求中原有的
reasoning_effort、reasoning字段,如果不在补丁里,会被移除;max_completion_tokens统一换成max_tokens。 - 不能关闭思考的模型(
can_disable_thinking为false),阶梯上限加倍,因为思考和回答共用同一个上限。 - 阶梯的实际上限还会被模型的输出上限和调用方自己更小的
max_tokens进一步压低,但最低 512;所以明确选择档位时,低于 512 的调用方上限会被提高到 512。菜单模型最低为 1。 - 阶梯模型的
reasoning_effort和reasoning字段会被移除,因为上游并不执行它们,只有max_tokens起作用。
各站点的菜单写法不同:例如 Kilo 用 reasoning 对象,MiniMax 和 ZCode 用 output_config.effort,CodeArts 用 thinking.type,Raccoon 用 extra_body.thinking,Gemini Code Assist 的 gemini-3.8-flash 提供 low、medium、high、tiered(由模型自己决定)。不要假设所有模型都有同样的档位,用 GET /api/v1/models 查看每个 id 的 efforts 和每一档对应的上限。
可用性探测
有些渠道的模型列表只是一串 id,说不出这个出口实际能用哪些模型。探测用最小的请求(发送 ping,最多 64 个 token)逐个问一遍。
在 模型 页点 探测,或调用管理接口:
# 探测默认的渠道
curl -X POST http://127.0.0.1:10600/api/v1/probe -H "Authorization: Bearer <工具访问密钥>"
# 只探测指定的模型
curl -X POST http://127.0.0.1:10600/api/v1/probe \
-H "Authorization: Bearer <工具访问密钥>" -H "Content-Type: application/json" \
-d '{"models": ["kilo/<model-id>", "cline/<model-id>"]}'
# 读取结果
curl http://127.0.0.1:10600/api/v1/probe -H "Authorization: Bearer <工具访问密钥>"- 不指定模型时探测
opencode、nvidia和agg这几个列表无法说明可用性的渠道。Kilo 的免费列表已经由上游筛选过,不在默认范围内。 - 探测在后台进行,同时最多 2 个,每个最多等 45 秒。已有一轮在进行时,再次发起不会叠加,返回
started: false。 - 无法解析的模型名放在返回的
unresolved中。 - 结果保存在
hoshimi.db中,重启后仍在,并附在 模型 页和GET /api/v1/models的每一行上。
| 结果 | 界面显示 | 含义 |
|---|---|---|
available | 可用 | 模型产生了内容或 token |
region-blocked | 地区受限 | 这个出口所在的地区被拒绝 |
unavailable | 不可用 | 针对这个模型的拒绝 |
throttled | 限流 | 额度不足,没能问到模型 |
unknown | 未知 | 凭据、网络或服务器出错,无法判断模型本身 |
只有 unavailable 是关于模型本身的结论;地区、额度和未知的结果都不说明模型不存在。Playground 为图片寻找描述模型时,会跳过 unavailable 和 region-blocked 的模型。
记住失效的模型
正常请求中,网关也会记住上游说“没有这个模型”的情况:
- HTTP
404或410,或400、405、406、415、418、422、451的响应正文明确说模型不存在(如model not found、does not exist、不存在、已下线),算作一次“这个模型失效”。 - 正文同时提到额度、限流、余额、未登录、凭据等账号问题时不算;
401、402、429和5xx无论正文说什么都不算。 - 渠道中被问到的账号都这样回答,才记一次。
记下之后:
- 从第一次记录起 10 分钟内,对这个模型的请求直接返回
503 model_unavailable(… is temporarily unavailable: the station stopped answering for it.),不再访问上游; - 10 分钟内累计 3 次,变为本次运行期间一直失效;
- 模型重新正常回答时清除记录;
POST /api/v1/models/unavailable/reset或重启服务器清除所有记录。
这份记录只在内存中,与保存在数据库里的探测结果不同。被记为失效的模型在 模型 页和 GET /api/v1/models 中标为 unavailable,但不一定从列表中消失。
账号轮换与重试
一个渠道有多个账号时,请求从第一个可用的账号开始;跳过需要重新登录和正在暂停轮换的账号。失败后按失败的类型决定下一步:
| 失败类型 | 典型的上游回答 | 网关的做法 |
|---|---|---|
| 凭据被拒 | 401,或正文是凭据问题的 403 | 先尝试刷新这个账号的令牌,刷新成功就用它重试;否则换下一个账号 |
| 权限不足 | 正文不是凭据问题的 403 | 不换账号,直接返回 |
| 额度或限流 | 402、429,或正文提到额度、限流、余额不足 | 把这个账号暂停轮换,然后换下一个账号 |
| 策略拦截 | CodeBuddy 业务码 11140,或“安全审核”等 | 暂停这个账号 30 分钟,不刷新令牌,换下一个账号 |
| 模型饱和 | CodeBuddy 业务码 14003(只提供 SWITCH_MODEL),或“模型繁忙”等 | 不换账号,直接把上游的说明返回给调用方:所有账号都会遇到同样的情况 |
| 模型不存在 | 404、410 等(见上一节) | 继续问其他账号;全部如此时记为失效 |
| 可恢复的错误 | 408、409、425、500、502、503、504、507、Cloudflare 521–527、Anthropic 529,以及不在任何表中的状态码 | 换下一个账号 |
| 请求本身的问题 | 400、405、406、415、418、422、451 | 不换账号,直接返回:换哪个账号都一样 |
规则:
- 一个请求最多尝试
server.max_account_retries(默认 3)个不同的账号。 - 额度暂停的时长:优先用响应头
Retry-After,其次用正文中写明的重置时间(如将在 2026-09-11 18:08:17 UTC+8 重置、reset at … UTC+8,按其中的时区换算),都没有时暂停 1 小时。已有的暂停不会被缩短。 - 账号都问过了,而最后一次失败是可恢复的错误(不是凭据、额度、策略或模型不存在)时,等一会儿再从头试一轮,最多两次。等待时间优先用
Retry-After或正文中的重置时间,最长 8 秒;Retry-After: 0表示立即重试。没有时间时随机等待(上限从 0.25 秒起翻倍,最长 8 秒)。 - 已经向调用方输出了内容的请求,绝不会自动重放,以免同一段回答出现两次。
- 渠道里没有任何可以尝试的账号(都需要登录或都在暂停中)时,返回
502:Provider '…' exhausted all accounts.
暂停轮换的账号和原因显示在 渠道 页(暂停中,如 被限流、被策略阻止)和 GET /api/v1/credits 的 cooled 中。暂停记录只在内存中,重启后清空。
凭据续期
带有刷新令牌的账号由续期定时器维护(server.auto_refresh):
- 启动 30 秒后第一轮,之后每 30 分钟一轮;
- 每轮续期 1 小时内到期的账号;没有记录过期时间的账号每轮都续期;
- 续期成功后,新的令牌只写回
config.json中对应的那一个账号; - 上游拒绝刷新令牌本身(如
invalid_grant,或没有说明原因的401)时,账号被标为 需要登录,不再参与轮换,直到重新登录;其他失败下一轮再试。单纯的403不会让账号作废。
请求中遇到凭据被拒时也会立即续期一次。续期状态和手动续期:
curl http://127.0.0.1:10600/api/v1/refresh -H "Authorization: Bearer <工具访问密钥>"
curl -X POST http://127.0.0.1:10600/api/v1/refresh \
-H "Authorization: Bearer <工具访问密钥>" -H "Content-Type: application/json" \
-d '{"provider": "cline", "force": true}'force 表示不管是否快到期都续期;标为需要登录的账号即使 force 也会跳过。网页上对应 渠道 页的 立即续期 和 续期所有账户。
余额、签到与被动额度
余额和签到
GET /api/v1/credits 读取各账号的余额、暂停轮换的账号和签到状态;POST /api/v1/credits/claim(可带 {"provider": "…"})立即签到;GET /api/v1/credits/checkins 列出签到历史。没有余额或签到接口的渠道会被跳过。
开启 server.auto_checkin 时,启动 30 秒后进行第一轮签到,之后每 24 小时一轮。签到按北京时间(UTC+8)的日历日计算;当天已有账号签到成功(或站点回答今天已签过)后,同一天不再重复。签到逐个账号依次进行,不并发,以免触发站点的风控。每次结果(定时的和手动的)都记入 365 天的历史。
被动额度
上游的每个回答都可能带着额度信息。网关在推理请求的响应头里读取剩余量和上限,不额外发请求。按下面的顺序,取第一对能解析的:
| 剩余量 | 上限 |
|---|---|
x-ratelimit-remaining-requests | x-ratelimit-limit-requests |
x-ratelimit-remaining | x-ratelimit-limit |
ratelimit-remaining-requests | ratelimit-limit-requests |
ratelimit-remaining | ratelimit-limit |
x-ratelimit-remaining-requests-day | x-ratelimit-limit-requests-day |
x-ratelimit-remaining-day | x-ratelimit-limit-day |
- 数值解析是严格的:
10 requests不会被读成 10。剩余量和上限两个头都在、却没有一对能解析时,记一次“表头无法解析”。 - 百分比限制在 0–100;读数超过 5 分钟视为过时。
- 每个渠道另有四个计数:响应总数、
401/403、429、5xx。 - 渠道 页显示读数:10% 及以下为红色,25% 及以下为琥珀色,过时的读数标为 (旧)。
- 读数按渠道(不是按账号)记录,只在内存中,记录失败也不会影响推理。管理接口只返回表头名称,不返回原始值。