OpenClaw 深度解析(五):模型与提供商系统

场景:用户想在 Claude 和 Kimi 之间切换

你在用 OpenClaw 处理日常工作,大多数时候用 claude-sonnet-4-6,但对于某些中文任务你更喜欢 Kimi(月之暗面)。你在配置文件里同时保存了两个提供商的密钥,并且希望能用 anthropic/claude-sonnet-4-6 或者 kimi-coding/k2p5 来指定它们,还想在 Claude 触发速率限制时自动回退到备用模型。

这个场景暴露了五个具体问题:

  1. 寻址 :系统怎么知道 kimi-coding/k2p5 是"月之暗面,k2p5 模型"?
  2. 认证:每个提供商的 API 密钥存在哪、怎么取?
  3. 自动发现:不需要在配置里把每个提供商手写一遍吗?
  4. 回退:主模型失败后,系统怎么自动切换?
  5. 扩展: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-6parseModelRef 负责解析这个字符串:

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;
}

这让用户在配置里写 doubaobytedance 都能正确路由到 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-messagesopenai-compatiblegoogle-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 流程。这需要:

  1. 一个交互式引导用户完成 OAuth 的 auth 方法
  2. 认证成功后,往配置写入 models.providers.minimax-portalconfigPatch
  3. 写入认证档案(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 是什么。

相关推荐
冬奇Lab8 小时前
一天一个开源项目(第42篇):OpenFang - 用 Rust 构建的 Agent 操作系统,16 层安全与 7 个自主 Hands
人工智能·rust·开源
IT_陈寒8 小时前
SpringBoot性能飙升200%?这5个隐藏配置你必须知道!
前端·人工智能·后端
yiyu07168 小时前
3分钟搞懂深度学习AI:反向传播:链式法则的归责游戏
人工智能·深度学习
机器之心9 小时前
OpenClaw绝配!GPT-5.4问世,AI能力开始大一统,就是太贵
人工智能·openai
机器之心9 小时前
海外华人15人团队打造,统一理解与生成的图像模型,超越Nano banana登顶图像编辑
人工智能·openai
用户552796026059 小时前
在老版本 HPC 系统上运行 Antigravity(反重力)
人工智能
Axinyp10 小时前
Windows WSL2 安装 OpenClaw 踩坑指南
人工智能
恋猫de小郭10 小时前
你用的 Claude 可能是虚假 Claude ,论文数据告诉你,Shadow API 中的欺骗性模型声明
前端·人工智能·ai编程