# 模型、提供方与提示词路由

> 小米平台的加密回调登录与免费通道退场，xAI 与 Codex 登录，模态与附件的新规则，按模型家族挑选系统提示词，以及模型层源码的其他改动。

- 作者：David（道雾轩）
- 专栏：MiMo Code 源码解读（https://daiw.org/manual/mimo-code.md）
- 最后更新：2026-09-17
- 原文：https://daiw.org/manual/mimo-code/providers
- 转载与引用：请注明出处并附原文链接（https://daiw.org/about/copyright）

MiMo Code 的模型层沿用 OpenCode 的骨架：models.dev 目录加上环境变量、凭据、插件与配置，层层叠成 Provider 状态；`transform.ts` 抹平各家在消息与参数上的差异；登录走插件的 `auth` 钩子。这些机制见[提供方目录](https://daiw.org/manual/opencode/provider-catalog)、[提供方适配层](https://daiw.org/manual/opencode/provider-transform)与[认证与提供方插件](https://daiw.org/manual/opencode/auth-and-plugins)，那几篇对照的是 OpenCode v1.18.30，与 MiMo 的 v1.14.19 底座在细节上有出入。本篇只看 MiMo 往里加了什么：自家平台的登录与免费通道、xAI 与 Codex 两个登录插件、模型能力相关的新规则，以及系统提示词怎样按模型家族挑选。

## 怎么用

| 场景 | 做法 |
| --- | --- |
| TUI 登录 | `/login` 与 `/connect` 打开同一个对话框：小米（推荐）、Codex、从 Claude Code 导入，其后才是其余提供方 |
| 命令行登录 | `mimo providers login`（别名 `mimo auth login`）先让你在 MiMo 与其他提供方之间选择，`-p xiaomi` 直接进入小米登录；`mimo auth whoami` 显示已登录的 uid |
| 登出 | `/logout` 只删除 `xiaomi` 的凭据 |
| 声明模态 | `/modalities` 勾选当前模型支持的输入（image、audio、video、pdf），写入全局配置 |
| 模型分组 | `model_groups` 定义分组，agent、actor、workflow 里写模型的地方可以用组名代替 `provider/model`；`vision_model` 指定看图任务用的模型 |
| 提供方与模型选项 | 提供方级 `only_configured_models`、`headerTimeout`、`chunkTimeout`；模型级 `voice_design`、`voice_clone`、`cachePromptTTL` |
| 环境变量 | `MIMOCODE_MIMO_ONLY`、`MIMOCODE_DISABLE_PROVIDER_ENV`、`MIMOCODE_CODEX_MODE`、`MIMOCODE_ENABLE_DYNAMIC_SYSTEM_PROMPT`、`MIMOCODE_MAX_PROMPT_IMAGES`、`MIMOCODE_MAX_PROMPT_IMAGE_SIZE`、`MIMO_PLATFORM_URL` |

两个斜杠命令登记在 `packages/opencode/src/cli/cmd/tui/app.tsx:826` 与 `app.tsx:838`，登出在 `app.tsx:849`；命令行入口在 `packages/opencode/src/cli/cmd/providers.ts:533`。

## 小米平台：加密回调换来一把 API key

内置插件表的头两项是 MiMo 新加的 `MimoAuthPlugin` 与 `AnthropicProxyPlugin`，其后是上游原有的 Codex、Copilot 等，中间插入 MiMo 自己的 `XaiAuthPlugin`（`packages/opencode/src/plugin/index.ts:142`）。

`MimoAuthPlugin`（`packages/opencode/src/plugin/mimo.ts:86`）有三个钩子：`config` 把 `xiaomi` 注册为配置提供方，没登录也能出现在列表里，名称与 API 地址留给模型目录；`auth.loader` 把登录时拿到的 `base_url` 设成该提供方的 `baseURL`（`mimo.ts:102`）；`chat.headers` 给发往 `xiaomi` 的请求加上 `X-Mimo-Source` 头，值为 `mimocode-cli`（`mimo.ts:198`）。

唯一的登录方法叫“浏览器登录”，类型标为 `oauth`，但拿回来的不是 access token，而是一把普通的 API key：

1. 生成一对 X25519 密钥（`mimo.ts:26`），在本机随机端口起一个 HTTP 服务（`mimo.ts:114`）。
2. 拼出 `https://platform.xiaomimimo.com/authorize` 的地址（域名可用 `MIMO_PLATFORM_URL` 覆盖），参数带上公钥 `pk`、回调地址与 `key_name`（`mimo.ts:76`）。`key_name` 形如 `mimo-code-cli-key-` 加 8 位十六进制，首次生成后存进数据目录的 `mimo-key-name` 文件复用（`mimo.ts:15`）。自动打开的地址回调本机端口；交给界面展示的是另一个版本，回调平台自己的 `/authorize/code/callback` 页面，浏览器不在本机时由用户复制授权码粘回来。
3. 平台把结果加密后放进回调参数 `u`，本机服务解密成功就把浏览器重定向到平台的成功页，最多等 5 分钟（`mimo.ts:130`）。

密文格式写在注释里（`mimo.ts:36`）：

```ts
const encrypted = Buffer.from(encryptedBase64, "base64url")
// Format: ephemeralPublicKey(32 bytes) + nonce(12 bytes) + ciphertext + tag(16 bytes)
const ephemeralPub = encrypted.subarray(0, 32)
const nonce = encrypted.subarray(32, 44)
const ciphertextAndTag = encrypted.subarray(44)
const tag = ciphertextAndTag.subarray(ciphertextAndTag.length - 16)
const ciphertext = ciphertextAndTag.subarray(0, ciphertextAndTag.length - 16)
```

随后用本机私钥与平台的临时公钥做 ECDH，对共享密钥取 SHA-256 作为 AES-256-GCM 的密钥解密（`mimo.ts:51`）。明文是 `sk`、`uid`、`url` 三个字段：`sk` 存为 `type: "api"` 的密钥，`uid` 与 `url` 进 `metadata`（`mimo.ts:175`）。粘贴的授权码走同一个解密函数。

两个前端各包了一层。TUI 的 `MimoOAuthFlow`（`packages/opencode/src/cli/cmd/tui/component/dialog-mimo-login.tsx:171`）挂载时就开始等浏览器回调，输入框同时接受粘贴，成功后若目录里有 `mimo-v2.5-pro` 就把它设为当前模型（`dialog-mimo-login.tsx:196`）。命令行的 `mimoLogin`（`providers.ts:262`）让浏览器回调与标准输入赛跑，粘贴的码解不开可以重试，共 3 次（`providers.ts:285`）。开源版还留了一个 `src/ext` 登录扩展点（`providers.ts:242`），但仓库里没有这个目录。

同文件的 `AnthropicProxyPlugin`（`mimo.ts:205`）只在 `anthropic` 既有已保存的凭据、又配置了 `baseURL` 时生效：删掉请求头里的 `anthropic-beta`，并在 SSE 流里读到 `message_stop` 事件后主动关闭流，不再等上游断开连接。“从 Claude Code 导入”会写入这个 `baseURL`，见[从 Claude Code、Codex 与 OpenCode 迁移](https://daiw.org/manual/mimo-code/imports)。

## 免费通道 mimo-auto 的退场

MiMo 曾提供免登录的 `mimo/mimo-auto` 通道。开源代码里找不到 `mimo` 提供方本身的定义，只有几处按 ID 的特判：干净安装时 TUI 优先把它选为默认模型（`packages/opencode/src/cli/cmd/tui/context/local.tsx:216`）；配置里声明了 `mimo-auto` 时强制把它标为支持图片输入（`packages/opencode/src/provider/provider.ts:1424`）；首次使用前要先在 `DialogAgreement` 里同意用户协议（`packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx:1130`）；遇到限流排队而重试时，最多每 24 小时弹一次 `DialogTokenPlan` 推荐订阅 Token Plan（`packages/opencode/src/cli/cmd/tui/routes/session/index.tsx:461`）。

退场时间写死在 `packages/opencode/src/util/free-api-sunset.ts:1`：

```ts
export const FREE_API_SUNSET_AT = Date.parse("2026-07-26T10:00:00.000Z")
```

换算成北京时间是 7 月 26 日 18:00，早于 v0.1.14 的发布日期，所以这个版本里下面三层都已生效：

- **服务端**：`getLanguage` 与 `getSpeech` 遇到 `mimo/mimo-auto` 直接抛错（`provider.ts:1805`、`provider.ts:1866`）。
- **TUI 提交**：`shouldBlockFreeApiRequest` 拦下发给模型的输入并弹窗，提示用 `/login` 登录或配置第三方 API；只在本地执行的斜杠命令与 shell 模式放行（`prompt/index.tsx:1113`）。
- **显示**：模型名去掉“限免至 7 月 26 日”的说明。`createFreeApiSunsetSignal` 启动时按剩余毫秒数设一个定时器，TUI 开着跨过这个时间点也会即时切换（`packages/opencode/src/cli/cmd/tui/util/free-api-sunset.ts:19`）。

服务端抛出的错误原文是：

> MiMo free API service has ended. Sign in or configure a third-party API.

与此同时，上游 `opencode` 提供方（OpenCode Zen）的免费模型被一律隐藏：未认证时整个提供方为空，认证后也只剩付费模型（`provider.ts:309`）。上游 v1.14.19 的逻辑正相反，没有密钥时删掉收费模型，用 `apiKey: "public"` 开放免费模型。

## xAI 与 Codex 登录

v1.14.19 没有 xAI 登录插件，`packages/opencode/src/plugin/xai.ts` 是 MiMo 加的，提供三种方式（`xai.ts:594`）：

| 方式 | 做法 |
| --- | --- |
| 浏览器 | PKCE 授权码流程，复用 Grok CLI 公开的 client_id，回调固定在 `127.0.0.1:56121`；注释说明 xAI 按注册的地址校验 redirect_uri |
| 无头 | RFC 8628 设备码：`authorization_pending` 时按间隔加 3 秒余量再查，`slow_down` 时间隔加 5 秒，截止时间取服务端的 `expires_in`，缺省 5 分钟 |
| API key | 手动输入 |

loader 返回占位 key 与自定义 `fetch`：存储的过期时间或 JWT 里的 `exp` 距现在不足 120 秒就刷新（`xai.ts:534`），并发请求共用同一次刷新，新 token 写回 `auth.json`，写失败也不影响本次请求。

Codex（ChatGPT 订阅）沿用上游的 `packages/opencode/src/plugin/codex.ts`，改动很少：上游在 OAuth 登录后只保留名字含 codex 或列在 8 个型号白名单里的模型，MiMo 删掉了这段过滤，`openai` 目录里的模型全部保留，价格一律清零（`codex.ts:372`）；刷新 token 时传入请求的 `AbortSignal`；错误页对错误信息做 HTML 转义；两个方法改名为 `Codex via ChatGPT Pro/Plus (browser)` 与 `Codex via ChatGPT Pro/Plus (headless)`。TUI 登录框里的 Codex 条目直接调用 `openai` 提供方的第 0 个方法（`dialog-mimo-login.tsx:48`）。

## 窗口、模态与附件

**窗口与输出上限**。在配置里声明、目录里又查不到的模型，`limit.context` 缺省为 `DEFAULT_CONTEXT_WINDOW`，即 1000000，并记一条警告提示去配置（`provider.ts:1391`），上游缺省为 0。输出上限看 `usesLargeModelDefaults`：提供方是 `mimo` 或 `xiaomi`，或模型 ID 含 claude、gpt、mimo 的，缺省 128000（`packages/opencode/src/provider/transform.ts:28`）。`maxOutputTokens` 对这类模型直接返回 128000，不再与模型声明的 `limit.output` 取小（`transform.ts:1850`）；其余模型仍是上游的“目录值与 32000 取小”。

**模态**。`/modalities`（`packages/opencode/src/cli/cmd/tui/component/dialog-modalities.tsx:66`）把勾选结果写进全局配置里该模型的 `modalities.input`，输出模态照抄现有声明，然后重建实例。需要看图时由 `getVisionModel` 选模型：配置了 `vision_model` 且可用就用它，否则在所有支持图片输入的模型里先挑 `mimo`、`xiaomi` 自家的，再按输入单价从低到高（`provider.ts:1965`、`provider.ts:1122`）。

`packages/opencode/src/provider/capability-registry.ts` 名字像一张模型能力总表，其实只服务 MCP 的 `sampling/createMessage`。服务器发来的采样请求可能带图片或音频，它把“模型声明的输入模态”与“SDK 适配包能否序列化这种媒体”两道闸门取交，适配包一侧分 supported、unsupported、unknown 三态，未知一律不选，单个媒体上限 20 MiB（`capability-registry.ts:54`）。调用方在 `packages/opencode/src/mcp/sampling.ts:624`，见 [MCP：采样与工具检索](https://daiw.org/manual/mimo-code/mcp)。

**图片体积**。`transform.ts` 在发送前新增了 `limitImages`：只有 Anthropic、Bedrock，以及经网关、OpenRouter、Copilot 转发的 Claude 模型才套用上限（`transform.ts:794`），单图解码后不超过 4500000 字节、最长边不超过 2000 像素（`packages/opencode/src/provider/image.ts:7`）。超限的 PNG、JPEG 逐级缩小、降低质量，重新编码成 JPEG；仍然超限或格式无法解码，就换成一段占位文本。历史消息里的图片同样处理，一张超大截图不会让之后的每次请求都失败。`MIMOCODE_MAX_PROMPT_IMAGES` 限制每次请求带的图片张数（丢弃最旧的），`MIMOCODE_MAX_PROMPT_IMAGE_SIZE` 覆盖字节上限。

**工具结果里的附件**。上游用一个布尔值判断 SDK 是否支持在工具结果里放媒体，不支持就把全部媒体挪进一条补充的 user 消息。MiMo 改为逐个附件决定去向（`packages/opencode/src/session/tool-attachment.ts:80`）：

```ts
export function routeToolAttachment(input: {
  model: Provider.Model
  attachment: ToolAttachment
  allowNative: boolean
}): ToolAttachmentRoute {
  if (!modelAcceptsMime(input.model, input.attachment.mime)) return "placeholder"
  if (input.allowNative && providerAcceptsNative(input.model, input.attachment)) return "native"
  if (providerAcceptsSynthetic(input.model, input.attachment)) return "synthetic"
  return "placeholder"
}
```

- **native**：附件留在工具结果里。只有 Anthropic、Bedrock（图片与 PDF）和 Gemini 3（另加音频、视频）走这条路，而且必须是内联的 data URL（`tool-attachment.ts:64`）。
- **synthetic**：附件搬进紧随其后的一条 user 消息，开头是常量 `SYNTHETIC_ATTACHMENT_PROMPT`，再逐个注明来自哪次工具调用（`packages/opencode/src/session/message-v2.ts:1065`）。音频只对 `@ai-sdk/openai-compatible`（wav、mp3、mpeg）与 Google 系开放，视频只对 Google 系开放（`tool-attachment.ts:43`）。
- **placeholder**：模型不接受这种 MIME，或两条路都走不通，就换成一句说明文字，告诉模型附件已保留、但无法发给当前模型或提供方（`tool-attachment.ts:142`）。

失败的工具调用不走 native（`message-v2.ts:1017`）。

## 系统提示词按模型家族挑选

主提示词由 `SystemPrompt.provider` 决定（`packages/opencode/src/session/system.ts:35`）：

```ts
export function provider(model: Provider.Model, harness?: HarnessMode) {
  if (usesGPTToolset(model.id, harness, model.api.id, model.family)) return [PROMPT_GPT]
  const prompt = (id: string) => {
    // An explicit process override can route GPT here; keep its prompt aligned
    // with the default tool schema instead of advertising Codex-only tools.
    if (isGPTModel(id)) return PROMPT_DEFAULT
    if (id.includes("gpt-4") || id.includes("o1") || id.includes("o3")) return PROMPT_BEAST
    if (id.includes("gemini-")) return PROMPT_GEMINI
    if (id.includes("claude")) return PROMPT_ANTHROPIC
    if (id.toLowerCase().includes("trinity")) return PROMPT_TRINITY
    if (id.toLowerCase().includes("kimi")) return PROMPT_KIMI
    if (id.toLowerCase().includes("deepseek")) return PROMPT_DEEPSEEK
    if (id.toLowerCase().includes("glm")) return PROMPT_GLM
    if (id.toLowerCase().includes("minimax")) return PROMPT_MINIMAX
  }
  return [prompt(model.id) ?? prompt(model.api.id) ?? PROMPT_DEFAULT]
}
```

与上游 v1.14.19 相比有三处不同：

1. **先判 Codex 模式**。`usesGPTToolset`（`packages/opencode/src/tool/gpt.ts:41`）对 ID 含 gpt 的模型（gpt-oss 除外）返回真；`MIMOCODE_CODEX_MODE=true` 或会话首条消息带 `harness: "codex"` 能把其他模型也拉进来，`MIMOCODE_CODEX_MODE=false` 则全部关掉（`gpt.ts:11`）。命中后用 `gpt.txt`，并换用 GPT 专属工具集（`packages/opencode/src/session/prompt.ts:1509`）；这份 `gpt.txt` 在 MiMo 里重写成了自称 Codex 的提示词，上游单独的 `codex.txt` 被删掉。Codex 模式被强制关闭时 GPT 模型落到 `default.txt`，因此 `beast.txt` 只剩 ID 含 o1、o3 而不含 gpt 的模型会用到。
2. **先看 `model.id` 再看 `model.api.id`**。上游只看 `api.id`。
3. **新增三个家族**：deepseek、glm、minimax。

| 文件 | 行数 | 与上游的关系 |
| --- | --- | --- |
| `default.txt` | 170 | 重写（上游 105 行），加入 agent 模式、权限、任务与子智能体、技能、记忆等 MiMo 概念 |
| `gpt.txt` | 226 | 重写为 Codex 风格（上游 107 行） |
| `anthropic.txt` | 39 | 重写为精简版（上游 105 行） |
| `deepseek.txt`、`minimax.txt`、`glm.txt` | 131、140、51 | 新增 |
| `beast.txt` | 155 | 自称改为 MiMoCode，末尾加一段自主运行的安全边界 |
| `kimi.txt`、`gemini.txt`、`trinity.txt` | 95、155、97 | 基本只改自称，另有几句随工具名与路径规则调整 |

新增的三份有些值得留意的痕迹：`deepseek.txt` 与 `minimax.txt` 的工作流段落都要求在 `packages/opencode` 目录下跑 `bun typecheck`，`deepseek.txt` 的编码规范一节还列出 Drizzle schema 字段用 snake_case 这类规则（`packages/opencode/src/session/prompt/deepseek.txt:86`），与仓库根目录 `AGENTS.md` 的风格指南同源，这些规则会随系统提示词发给用户的任何项目；`minimax.txt` 的第 126、127 行是重复的同一句；`glm.txt` 写到工具调用约定就结束了，没有任务与子智能体两节。另有一份 `default.old.txt`，代码里没有引用。

环境信息默认不发。`SystemPrompt.environment` 开头就检查 `MIMOCODE_ENABLE_DYNAMIC_SYSTEM_PROMPT`，未设置时返回空数组（`system.ts:79`），调用方同样受这个开关控制（`prompt.ts:4444`）。打开后分两种写法：命中 `anthropic.txt` 的模型拿到一段带当前分支、主分支、`git status` 与最近两次提交的环境说明，按会话创建时间缓存（`system.ts:81`）；其余模型拿到以 MiMo Code Agent 自称开头的 `<env>` 块，模型不支持图片时再附一段 `<vision-capability>`，列出最多 3 个可以交给子智能体看图的模型（`system.ts:139`）。两种写法都附带一句要求回复语言跟随用户的指令。

## provider.ts 与 transform.ts 的其他改动

| 改动 | 出处 |
| --- | --- |
| `openai` 提供方默认等响应头 5 分钟；所有提供方的 SSE 分块间隔缺省 8 分钟（上游不设），注释说是为 mimo-v2.5-pro 在 MiMo Router 上重建上下文后的冷启动首字延迟留余量 | `provider.ts:45` |
| `xiaomi` 的模型 ID 同时含 mimo 与 ptc 两段时改走 Responses API；`xiaomi` 走 `@ai-sdk/openai-compatible` 时换用内置的 copilot SDK，把 `exec` 工具声明成 Responses 的自由格式工具，见 [exec 与 QuickJS 沙箱](https://daiw.org/manual/mimo-code/exec-sandbox) | `provider.ts:334`、`provider.ts:1734` |
| `getSmallModel` 在未配置 `small_model` 时解析 `lite` 分组；`ultra`、`standard`、`lite` 三个内置层级未配置时回落到默认模型，所以什么都不配时标题生成这类轻量调用就用默认模型，不再像上游那样按 haiku、flash、nano 自动挑；其他未知组名报错并给出相近的组名 | `provider.ts:1951`、`provider.ts:1910` |
| `defaultModel` 遇到配置里失效的 `model` 不再直接返回，最后兜底时只挑支持工具调用、有文本输入、窗口非零的模型 | `provider.ts:1992` |
| 提供方设 `only_configured_models` 后，`models` 充当隐式白名单 | `provider.ts:1573` |
| `MIMOCODE_DISABLE_PROVIDER_ENV`（`MIMOCODE_MIMO_ONLY` 会连带打开）时不再凭环境变量点亮提供方 | `provider.ts:1440` |
| 配置里新声明、走 `@ai-sdk/openai-compatible` 的 deepseek 模型默认把推理放进 `reasoning_content` 回传 | `provider.ts:1377` |
| 新增 `getSpeech` 与按模态推断的 `modelKind`，供[内置 LLM 服务](https://daiw.org/manual/mimo-code/llm-server)的语音接口使用 | `provider.ts:1865`、`provider.ts:2099` |
| 发送前先补齐空内容，再保证请求以 user 消息结尾（必要时追加一条内容为 `Continue.` 的 user 消息），两步的先后顺序写成了注释里的契约 | `transform.ts:995` |
| 缓存断点改为“最后一条 system 加最后两条消息”，工具表的最后一项也打断点；只有确认支持内联标记的提供方才打 | `transform.ts:554`、`transform.ts:266` |
| `@ai-sdk/openai`、`@ai-sdk/azure` 的工具显式设 `strict: false`；所有提供方的工具 schema 先把根部的 `anyOf` 拍平成单个 object | `transform.ts:1107`、`transform.ts:2000` |
| MiMo 网关把内容审核与风控拦截放在 HTTP 400 里，`error.ts` 把错误码 421、441 翻译成可读信息 | `packages/opencode/src/provider/error.ts:170` |

请求前缀与缓存命中的整体安排见[压缩、修剪与请求前缀](https://daiw.org/manual/mimo-code/compaction-and-prune)。

下一篇：[内置 LLM 服务](https://daiw.org/manual/mimo-code/llm-server)——每个 MiMo 实例都挂着一套 OpenAI 兼容接口，技能和子进程凭临时令牌借用模型，而拿不到真实密钥。
