场景:用户想在 Claude 和 Kimi 之间切换
你在用 OpenClaw 处理日常工作,大多数时候用 claude-sonnet-4-6,但对于某些中文任务你更喜欢 Kimi(月之暗面)。你在配置文件里同时保存了两个提供商的密钥,并且希望能用 anthropic/claude-sonnet-4-6 或者 kimi-coding/k2p5 来指定它们,还想在 Claude 触发速率限制时自动回退到备用模型。
这个场景暴露了五个具体问题:
- 寻址 :系统怎么知道
kimi-coding/k2p5是"月之暗面,k2p5 模型"? - 认证:每个提供商的 API 密钥存在哪、怎么取?
- 自动发现:不需要在配置里把每个提供商手写一遍吗?
- 回退:主模型失败后,系统怎么自动切换?
- 扩展:MiniMax 等新提供商是怎么通过 Plugin SDK 接入的?
一、模型地址:ModelRef
所有的模型操作都从一个基本单元开始:
typescript
// src/agents/model-selection.ts
export type ModelRef = {
provider: string; // "anthropic" | "kimi-coding" | "ollama" | ...
model: string; // "claude-sonnet-4-6" | "k2p5" | "llama3.2" | ...
};
模型在所有地方都以 provider/model 格式书写,例如 anthropic/claude-sonnet-4-6。parseModelRef 负责解析这个字符串:
typescript
export function parseModelRef(raw: string, defaultProvider: string): ModelRef | null {
const slash = raw.indexOf("/");
if (slash === -1) {
// 没有斜杠:使用默认提供商(anthropic)
return normalizeModelRef(defaultProvider, raw);
}
const provider = raw.slice(0, slash);
const model = raw.slice(slash + 1);
return normalizeModelRef(provider, model);
}
默认值 (src/agents/defaults.ts):
typescript
export const DEFAULT_PROVIDER = "anthropic";
export const DEFAULT_MODEL = "claude-opus-4-6";
当用户写 model: claude-sonnet-4-6(不带斜杠),系统自动补全为 anthropic/claude-sonnet-4-6 并给出弃用警告,引导用户使用完整格式。
提供商名称归一化
不同用户可能用不同的名称指代同一个提供商,normalizeProviderId 处理所有已知别名:
typescript
export function normalizeProviderId(provider: string): string {
const normalized = provider.trim().toLowerCase();
if (normalized === "z.ai" || normalized === "z-ai") return "zai";
if (normalized === "qwen") return "qwen-portal";
if (normalized === "kimi-code") return "kimi-coding";
if (normalized === "bedrock" || normalized === "aws-bedrock") return "amazon-bedrock";
if (normalized === "bytedance" || normalized === "doubao") return "volcengine";
return normalized;
}
这让用户在配置里写 doubao 或 bytedance 都能正确路由到 VolcEngine 提供商,而不用关心内部 ID。
二、提供商配置:静态声明与自动发现
静态配置
用户在 openclaw.yml 里为每个提供商声明:
yaml
models:
providers:
anthropic:
apiKey: ANTHROPIC_API_KEY # 环境变量名
kimi-coding:
baseUrl: https://api.kimi.com/coding/
apiKey: KIMI_CODING_API_KEY
api: anthropic-messages # 协议类型
models:
- id: k2p5
name: "Kimi K2.5"
contextWindow: 262144
每个 ProviderConfig 包含:
apiKey:可以是实际密钥字符串、环境变量名(系统自动读取)、或 OAuth 占位符baseUrl:API 端点api:协议类型(anthropic-messages、openai-compatible、google-ai等)models:该提供商的模型列表,含上下文窗口、推理能力、输入类型等元数据
隐式自动发现
但要求用户手写每个提供商太繁琐了。resolveImplicitProviders() 在 Gateway 启动时自动扫描:
typescript
export async function resolveImplicitProviders(params: {
agentDir: string;
explicitProviders?: Record<string, ProviderConfig> | null;
}): Promise<ModelsConfig["providers"]> {
const providers: Record<string, ProviderConfig> = {};
const authStore = ensureAuthProfileStore(params.agentDir, { allowKeychainPrompt: false });
// 检查 MiniMax:有 API Key 环境变量或认证档案就自动激活
const minimaxKey = resolveEnvApiKeyVarName("minimax")
?? resolveApiKeyFromProfiles({ provider: "minimax", store: authStore });
if (minimaxKey) {
providers.minimax = { ...buildMinimaxProvider(), apiKey: minimaxKey };
}
// 检查 Kimi:同样逻辑
const kimiCodingKey = resolveEnvApiKeyVarName("kimi-coding")
?? resolveApiKeyFromProfiles({ provider: "kimi-coding", store: authStore });
if (kimiCodingKey) {
providers["kimi-coding"] = { ...buildKimiCodingProvider(), apiKey: kimiCodingKey };
}
// ... 同样的模式处理 20+ 个提供商
}
这意味着用户只需要设置 KIMI_CODING_API_KEY 环境变量,OpenClaw 下次启动时就会自动找到 Kimi 并注入它的完整模型列表------不需要改任何配置。
合并策略:merge 模式
当隐式发现和显式配置同时存在时,models.mode = "merge"(默认值)将两者合并:
typescript
function mergeProviderModels(implicit: ProviderConfig, explicit: ProviderConfig): ProviderConfig {
// 合并规则:
// - 显式配置的用户字段(cost、headers、compat)保留
// - 从内置目录刷新 input、contextWindow、maxTokens(避免用户手动维护)
// - reasoning 字段:用户显式设置的优先;否则采用内置默认值
return {
...explicitModel,
input: implicitModel.input,
reasoning: "reasoning" in explicitModel ? explicitModel.reasoning : implicitModel.reasoning,
contextWindow: implicitModel.contextWindow,
maxTokens: implicitModel.maxTokens,
};
}
这个设计解决了一个实际痛点:当 Anthropic 发布新模型时,用户不需要手动更新配置里的 contextWindow 值,内置目录会自动刷新------但用户自定义的 cost 覆盖和 headers 注入依然生效。
三、模型目录与认证档案
模型目录(Model Catalog)
ModelCatalogEntry 是对"一个可用模型"的完整描述:
typescript
type ModelCatalogEntry = {
id: string; // "claude-sonnet-4-6"
name: string; // "Claude Sonnet 4.6"
provider: string; // "anthropic"
contextWindow?: number; // 200000
reasoning?: boolean; // 是否支持扩展思考
input?: Array<"text" | "image">; // 支持的输入类型
};
目录通过 @mariozechner/pi-coding-agent SDK 的 ModelRegistry 获取------这是 OpenClaw 底层的推理 SDK,内置了 Anthropic、OpenAI、Google 等主流提供商的最新模型列表。自定义提供商的模型会追加到这个目录之后。
认证档案(Auth Profiles)
为了支持多账户 和速率限制轮转 ,OpenClaw 不直接存储 API Key,而是使用认证档案(Auth Profile)系统。
每个 Profile 是这样的结构:
typescript
type ApiKeyCredential = { type: "api_key"; provider: string; apiKey: string };
type OAuthCredential = { type: "oauth"; provider: string; access: string; refresh: string; expires: number };
type TokenCredential = { type: "token"; provider: string; token: string };
type AuthProfileCredential = ApiKeyCredential | OAuthCredential | TokenCredential;
档案存储在 ~/.openclaw/agents/<agentId>/auth.json。用户可以为同一个提供商配置多个档案(例如两个 Anthropic 账户)。
冷却机制 :当一个 Profile 触发 429(速率限制)时,系统调用 markAuthProfileCooldown(),为该 Profile 设置冷却期。下次请求时,resolveAuthProfileOrder() 会优先选择不在冷却期的 Profile------这实现了多账户之间的自动轮转,无需用户手动干预。
scss
请求 → resolveAuthProfileOrder() → 选 Profile(跳过冷却中的)
↓
触发 429? → markAuthProfileCooldown()
成功? → markAuthProfileGood() + markAuthProfileUsed()
四、模型选择与别名
模型配置块:既是白名单也是别名表
agents.defaults.models 是一个双重用途的配置块:
yaml
agents:
defaults:
model: anthropic/claude-sonnet-4-6
models:
anthropic/claude-sonnet-4-6:
alias: sonnet # 给这个模型起一个短名字
kimi-coding/k2p5:
alias: kimi
anthropic/claude-haiku-4-5:
alias: haiku
- 当
models非空时 :只有列出的模型才被允许使用------这是一个白名单 。任何未在列表中的provider/model会被拒绝("model not allowed: ...")。 alias字段 :允许用户用kimi代替完整的kimi-coding/k2p5。别名解析在每次模型引用时都会检查。
白名单的例外:显式配置的 fallback 列表绕过白名单。这是合理的------用户既然配置了回退链,就是在明确授权这些模型。
模型别名系统
typescript
export function buildModelAliasIndex(params: {
cfg: OpenClawConfig;
defaultProvider: string;
}): ModelAliasIndex {
const byAlias = new Map<string, { alias: string; ref: ModelRef }>();
// ...
// 填充:alias "sonnet" → { provider: "anthropic", model: "claude-sonnet-4-6" }
}
解析时优先检查别名:
typescript
export function resolveModelRefFromString(params: {
raw: string;
defaultProvider: string;
aliasIndex?: ModelAliasIndex;
}): { ref: ModelRef; alias?: string } | null {
if (!params.raw.includes("/")) {
// 没有斜杠:先查别名表
const aliasMatch = params.aliasIndex?.byAlias.get(params.raw.toLowerCase());
if (aliasMatch) return { ref: aliasMatch.ref, alias: aliasMatch.alias };
}
return { ref: parseModelRef(params.raw, params.defaultProvider) };
}
所以在 Telegram 消息里发 /model kimi 就能切换到 kimi-coding/k2p5,不需要记住完整路径。
五、回退链:主模型失败时的自动降级
问题:主模型触发速率限制或宕机
配置 fallback 链:
yaml
agents:
defaults:
model:
primary: anthropic/claude-sonnet-4-6
fallbacks:
- anthropic/claude-haiku-4-5 # 先降级到小模型
- kimi-coding/k2p5 # 再试 Kimi
resolveFallbackCandidates() 按顺序构建候选列表,去重并过滤无效项。实际的轮转逻辑在 pi-embedded-runner.ts 的外层重试循环里------我们在上一篇分析 Agent 引擎时已经看到了这个结构:
typescript
// 简化版本
const candidates = resolveFallbackCandidates({ cfg, provider, model });
for (const candidate of candidates) {
try {
return await runWithModel(candidate); // 成功则返回
} catch (err) {
if (isFailoverError(err)) continue; // 可降级的错误:试下一个
if (isFallbackAbortError(err)) throw; // 用户主动中止:不降级
}
}
throw new Error(`All fallbacks failed: ...`);
探针机制(Probe)
冷却中的主模型不会被永久跳过。shouldProbePrimaryDuringCooldown 实现了一个**探针(probe)**逻辑:
- 每隔 30 秒,即使主模型仍在冷却期,也允许发一次探针请求
- 如果冷却过期时间在未来 2 分钟内,提前开始探针
- 探针成功 → 主模型恢复,
markAuthProfileGood()
这保证了速率限制解除后系统能快速恢复到主模型,而不是永远停留在降级状态。
六、通过 Plugin SDK 注册新提供商
问题:第三方提供商如何接入?
以 MiniMax 为例。它的 OAuth 流程不是标准 API Key,而是需要一个带浏览器跳转的 Device Code 流程。这需要:
- 一个交互式引导用户完成 OAuth 的
auth方法 - 认证成功后,往配置写入
models.providers.minimax-portal的configPatch - 写入认证档案(access token + refresh token)
这些都通过 ProviderPlugin 类型和 api.registerProvider() 实现:
typescript
// extensions/minimax-portal-auth/index.ts
const minimaxPortalPlugin = {
id: "minimax-portal-auth",
configSchema: emptyPluginConfigSchema(),
register(api: OpenClawPluginApi) {
api.registerProvider({
id: "minimax-portal",
label: "MiniMax",
docsPath: "/providers/minimax",
aliases: ["minimax"],
auth: [
{
id: "oauth",
label: "MiniMax OAuth (Global)",
kind: "device_code", // 触发 Device Code 流程
run: createOAuthHandler("global"),
},
{
id: "oauth-cn",
label: "MiniMax OAuth (CN)",
kind: "device_code",
run: createOAuthHandler("cn"),
},
],
});
},
};
ProviderAuthMethod.run 返回的 ProviderAuthResult 包含:
typescript
return {
// 写入认证档案:access token + refresh token
profiles: [{
profileId: "minimax-portal:default",
credential: { type: "oauth", access: "...", refresh: "...", expires: ... },
}],
// 写入 openclaw.yml 的 configPatch:提供商配置 + 模型列表
configPatch: {
models: {
providers: {
"minimax-portal": {
baseUrl: "https://api.minimax.io/anthropic",
apiKey: "minimax-oauth", // OAuth 占位符
api: "anthropic-messages",
models: [{ id: "MiniMax-M2.1", ... }, { id: "MiniMax-M2.5", ... }],
},
},
},
agents: { defaults: { models: {
"minimax-portal/MiniMax-M2.1": { alias: "minimax-m2.1" },
"minimax-portal/MiniMax-M2.5": { alias: "minimax-m2.5" },
}}},
},
// 引导用户立即切换到这个模型
defaultModel: "minimax-portal/MiniMax-M2.5",
};
用户运行 openclaw login 选择 MiniMax,完成 OAuth 跳转后,这一切自动写入------从此 minimax-m2.5 别名就可以使用了。
七、reasoning 字段与思考等级
部分模型(如 Claude Sonnet 4.6 的 extended thinking 模式)支持"慢思考"------花更多 token 推理后再给出答案。OpenClaw 用 ThinkLevel 统一管理这个维度:
typescript
export type ThinkLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
resolveThinkingDefault 从模型目录读取 reasoning: true 字段,自动将支持推理的模型的默认思考等级设为 "low":
typescript
export function resolveThinkingDefault(params: {
cfg: OpenClawConfig;
provider: string;
model: string;
catalog?: ModelCatalogEntry[];
}): ThinkLevel {
const configured = params.cfg.agents?.defaults?.thinkingDefault;
if (configured) return configured;
const candidate = params.catalog?.find(
(entry) => entry.provider === params.provider && entry.id === params.model,
);
// 有推理能力的模型默认开启 low 等级
return candidate?.reasoning ? "low" : "off";
}
这让用户不用手动配置"这个模型需要开启 thinking"------模型目录里的能力声明自动决定了默认行为。
小结
模型与提供商系统的核心是一套多层次的寻址与路由机制:
| 层次 | 机制 | 作用 |
|---|---|---|
| 地址层 | ModelRef = { provider, model } |
统一的"提供商/模型"坐标 |
| 归一化层 | normalizeProviderId() + normalizeProviderModelId() |
处理用户输入的多样性 |
| 别名层 | ModelAliasIndex |
允许用户用短名字代替完整路径 |
| 目录层 | ModelCatalogEntry[] |
声明模型的能力(上下文、推理、输入类型) |
| 发现层 | resolveImplicitProviders() |
扫描环境变量/认证档案,自动激活提供商 |
| 合并层 | mergeProviderModels() |
内置目录刷新元数据,保留用户自定义字段 |
| 认证层 | Auth Profiles + 冷却机制 | 多账户轮转,速率限制自动规避 |
| 回退层 | resolveFallbackCandidates() + 探针 |
主模型失败时自动降级,冷却恢复后探针恢复 |
| 扩展层 | ProviderPlugin + api.registerProvider() |
第三方提供商通过 Plugin SDK 注入 OAuth 流程和模型配置 |
下一篇,我们将进入 OpenClaw 的节点系统与 Canvas------探索 Pi 框架中的"节点"概念如何支撑多 Agent 协作,以及 Canvas 是什么。