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 里全部写入错误的模型标识。
通过 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 都适用,不需要分支。
把「请求时发的 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 侧,不要丢。