返回博客

DeepSeek Anthropic 兼容层的模型名谎言:res.model 不可信

在一个工作流编辑器里,我给每个 LLM 节点都展示了当前使用的模型名称。某天环境变量明明配的是 DEEPSEEK_MODEL=deepseek-v4-flash,UI 上却稳定显示 claude-opus-4-7——感觉像在看别人的账单。

现象

  • 项目用 @anthropic-ai/sdk,只是把 baseURL 换成了 https://api.deepseek.com/anthropic
  • 请求体里 model 字段发的是 deepseek-v4-flash
  • 拿到响应后用 res.model 记录到日志 / 渲染到 UI
  • 结果日志和 UI 一致显示 claude-opus-4-7(或 claude-3-opus-*
  • 调用的是 DeepSeek、钱也从 DeepSeek key 扣,但"模型名"完全是 Claude 家的字符串

根因

DeepSeek 在 /anthropic 路径下实现了一套 Anthropic Messages API 的形状兼容,包括请求体结构、响应结构——乃至 model 字段的回显格式。Anthropic 官方 API 会把你发的 claude-3-opus-20240229 原封不动回显回来;DeepSeek 的兼容层为了"看起来像 Anthropic",也在 model 字段里回填了一个 claude-* 风格的字符串,而不是你实际发过去的 deepseek-v4-flash

不是 Bug,是蓄意的形状模仿。但一旦你把 res.model 当作权威来源,就会在日志、监控、UI 里全部写入错误的模型标识。

坑:把 res.model 当模型权威来源

通过 Anthropic 兼容层调用 DeepSeek 时,res.model 返回的是 claude-* 风格字符串,与你发送的 model 参数完全不同。任何依赖 res.model 的日志、数据库记录、UI 展示都会显示错误信息。

同样的问题存在于其他 Anthropic-shape 代理(vLLM with Anthropic adapter、LiteLLM Anthropic 模式、各类第三方网关)。

正解

权威的模型名是你发出去的那个,不是响应里回来的那个。做法是在 SDK 的薄包装层里固定住 model,下游统一消费包装层的字段而非原始响应:

class LLMClient {
  constructor(
    private sdk: Anthropic,
    public readonly provider: 'deepseek' | 'anthropic',
    public readonly model: string,   // ← 发出去的 · 权威来源
  ) {}
 
  async chat(messages: MessageParam[], systemPrompt?: string) {
    const res = await this.sdk.messages.create({
      model: this.model,
      messages,
      system: systemPrompt,
      max_tokens: 8192,
    });
    return {
      text: res.content[0].type === 'text' ? res.content[0].text : '',
      requestedModel: this.model,   // ← 展示 / 持久化用这个
      serverModel: res.model,       // ← 仅用于 debug,不对外暴露
      inputTokens: res.usage.input_tokens,
      outputTokens: res.usage.output_tokens,
    };
  }
}

在执行层 / 数据库写入时:

return {
  provider: client.provider,
  model: client.model,        // ← UI 和日志显示这个
  serverModel: result.serverModel, // ← 保留在数据模型里,别删
  text: result.text,
  tokens: result.inputTokens + result.outputTokens,
};

对于 Anthropic 官方端点,res.model 是诚实的,serverModel 会和 requestedModel 一致——所以同一套包装对两个 provider 都适用,不需要分支。

正解:信任 client.model,而不是 res.model

把「请求时发的 model 参数」固定在客户端包装层,输出结构里用 requestedModel(或直接 client.model)做权威字段;把 res.model 降级为仅供 debug 的 serverModel 保留,但不暴露给 UI 和业务日志。

实测验证

// 发送
const res = await sdk.messages.create({
  model: 'deepseek-v4-flash',
  messages: [{ role: 'user', content: 'hello' }],
  max_tokens: 16,
});
 
console.log(res.model);    // → "claude-opus-4-7"   ← 谎言
console.log(client.model); // → "deepseek-v4-flash" ← 真相

一句话外卖

兼容层的 res.model 可以是谎言;真相永远是你发出去的那个 model 参数——存在 client 侧,不要丢。