跳转至

第三方代理 API 兼容性问题

商汤日日新接入Claude Code

问题根因

Claude Code 在 v2.1.63 之后 改用了新的 thinking 参数格式:

1
2
3
4
5
6
7
8
{
  "thinking": {
    "type": "adaptive" // ← 新版本用这个
  },
  "output_config": {
    "effort": "high"
  }
}

但商汤日日新的代理 API 只认 ["enabled", "disabled", "auto"] 这三个值,不支持 adaptive,所以直接报 400 错误。

解决方案(按推荐顺序)

方案 1:在 Claude Code 里关闭 Thinking Mode(最快)

在 Claude Code 交互界面里按 Alt + T(Mac 是 Option + T),把思考模式关掉。

或者在 settings.json 中全局配置:

{
  "alwaysThinkingEnabled": false,
  "env": {
    "ANTHROPIC_AUTH_TOKEN": "sk-**********************",
    "ANTHROPIC_BASE_URL": "https://token.sensenova.cn",
    "ANTHROPIC_MODEL": "deepseek-v4-flash",
    "ANTHROPIC_DEFAULT_SONNET_MODEL": "deepseek-v4-flash",
    "ANTHROPIC_DEFAULT_HAIKU_MODEL": "deepseek-v4-flash",
    "ANTHROPIC_DEFAULT_OPUS_MODEL": "deepseek-v4-flash",
    "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1",
    "DISABLE_AUTOUPDATER": "1",
    "CLAUDE_CODE_NO_UPDATE": "1",
    "MAX_THINKING_TOKENS": "0"
  },
  "permissions": {
    "allow": ["Read", "Edit", "Write", ...]
  },
  "theme": "dark"
}

方案 2:降级 Claude Code 到兼容版本

npm uninstall -g @anthropic-ai/claude-code
npm install -g @anthropic-ai/claude-code@2.1.154

v2.1.154 及之前的版本不会注入 adaptive thinking 类型,与第三方代理端点兼容。

方案 3:删掉 effortLevel 配置

settings.json移除 "effortLevel": "high" 这一行。

方案 4:换用原生 API

直接用 DeepSeek 官方 API(https://api.deepseek.com/anthropic)或换回 Anthropic 官方 API(https://api.anthropic.com)。

关于客户端角色的澄清

Claude Code 客户端本身只是一个工具——它不会推理、不会分析、没有智能。它就像一双手脚,帮你读文件、写代码、执行命令,把用户的话打包成 API 请求发给模型,把模型的回答展示给你看。

之前说的"客户端思考"指的是 Claude Code 客户端在 API 请求里主动加一个 thinking 参数,这个行为是 Claude Code v2.1.63 之后才加的。第三方代理(如商汤日日新)不认识 type: "adaptive",直接报 400 错误。

配置生效后: alwaysThinkingEnabled: false + MAX_THINKING_TOKENS: "0" 告诉客户端:闭嘴,别在请求里乱加参数,老老实实当传话筒。

TokenRhythm 接入方案

背景

tokenrhythm.studio 只兼容标准 Anthropic Messages API 格式,校验严格;而 Claude Code 会强制发送一些它不支持的 header 和请求体字段,直接对接就会 400。

最终方案:在本地跑一个代理,Claude Code 连本地代理,代理清洗请求后再转发到 tokenrhythm.studio,响应流式透传返回。

1
2
3
4
Claude Code ──> 本地代理 (127.0.0.1:3456) ──> https://tokenrhythm.studio ──> DeepSeek 模型
                    │ 清洗请求                    ▲
                    └────────────────────────────┘
                      响应流式透传(SSE)

三个问题与解决方案

# 报错信息 根因 解决方案
1 400 尚未验证或不支持的 anthropic-beta:claude-code-20250219 Claude Code 强制携带 anthropic-beta 头,第三方平台不认识 代理剥离 anthropic-beta 请求头
2 400 messages.N.role: Invalid option 请求体 messages 里混入 system/developer 等非法 role system/developer 消息提取合并到顶层 system 字段,其余非法 role 强制改为 user
3 400 messages.N.content.N.thinking 长度不足(长对话约 30 轮后触发) Claude Code 在消息里插入 thinking 类型 content block,DeepSeek 不支持 删除 thinking / redacted_thinking content block 及相关字段,过滤后为空的 content 补占位

代理核心逻辑

请求处理流程:

接收请求 → 缓冲完整请求体(data/end 事件)
        → 清洗 headers
            ├─ 丢弃 anthropic-beta
            ├─ host 改写为 tokenrhythm.studio
            └─ 丢弃旧 content-length(稍后按新 body 重算)
        → sanitizeBody() 清洗 JSON 请求体
            ├─ 修复非法 role(见问题 2)
            ├─ 删除 thinking 相关内容(见问题 3)
            └─ 解析失败则原样透传,不阻断
        → 重新计算 content-length
        → https.request 转发到 tokenrhythm.studio:443
        → proxyRes.pipe(clientRes) 流式透传(保持 SSE 正常)

关键设计点:

  • 零依赖:只用 Node 内置 http / https,无 package.json、无 node_modules,Node ≥ 18 即可运行
  • sanitizeBody(bodyStr) 纯函数:返回 { json, modified, logInfo },JSON 解析失败时原样透传
  • 日志:每个请求打印 [时间] METHOD URL;发生清洗时打印 → 修复项1 | 修复项2
  • 安全:只监听 127.0.0.1,不暴露公网

完整代码

const http = require("http");
const https = require("https");

const TARGET_HOST = "tokenrhythm.studio";
const PORT = process.env.PORT || 3456;

/**
 * 清洗请求体:
 * 1. 将 system/developer role 提取到顶层 system 字段
 * 2. 删除 thinking / redacted_thinking content blocks
 * 3. 非法 role 强制改为 user
 * 4. 删除顶层 thinking 参数
 */
function sanitizeBody(bodyStr) {
  try {
    const json = JSON.parse(bodyStr);
    let modified = false;
    let logInfo = [];

    if (json.messages && Array.isArray(json.messages)) {
      const systemParts = [];
      const cleanMessages = [];

      for (let i = 0; i < json.messages.length; i++) {
        const msg = json.messages[i];
        const role = msg.role;

        // 修复非法 role:system/developer → 提取到顶层 system
        if (role === "system" || role === "developer") {
          const content =
            typeof msg.content === "string"
              ? msg.content
              : JSON.stringify(msg.content);
          systemParts.push(content);
          logInfo.push(`moved messages[${i}] (role=${role}) → system`);
          modified = true;
          continue;
        } else if (role !== "user" && role !== "assistant") {
          msg.role = "user";
          logInfo.push(
            role
              ? `forced messages[${i}] role="${role}" → "user"`
              : `fixed messages[${i}] missing role → "user"`,
          );
          modified = true;
        }

        // 删除 thinking / redacted_thinking content blocks
        if (msg.content && Array.isArray(msg.content)) {
          const originalLen = msg.content.length;
          const cleanContent = [];

          for (let j = 0; j < msg.content.length; j++) {
            const block = msg.content[j];
            if (
              block.type === "thinking" ||
              block.type === "redacted_thinking"
            ) {
              logInfo.push(
                `removed messages[${i}].content[${j}] (type=${block.type})`,
              );
              modified = true;
              continue;
            }
            if (block.thinking !== undefined) {
              delete block.thinking;
              modified = true;
            }
            if (block.signature !== undefined) {
              delete block.signature;
              modified = true;
            }
            cleanContent.push(block);
          }

          if (cleanContent.length === 0 && originalLen > 0) {
            cleanContent.push({ type: "text", text: "" });
            logInfo.push(`added placeholder text for messages[${i}]`);
          }
          msg.content = cleanContent;
        }

        cleanMessages.push(msg);
      }

      if (systemParts.length > 0) {
        const existing = json.system || "";
        json.system = existing
          ? existing + "\n\n" + systemParts.join("\n\n")
          : systemParts.join("\n\n");
      }
      json.messages = cleanMessages;
    }

    // 删除顶层 thinking 参数
    if (json.thinking !== undefined) {
      delete json.thinking;
      logInfo.push("removed top-level thinking param");
      modified = true;
    }

    return { json, modified, logInfo };
  } catch (e) {
    return { json: null, modified: false, logInfo: [] };
  }
}

const server = http.createServer((clientReq, clientRes) => {
  const timestamp = new Date().toISOString();
  console.log(`[${timestamp}] ${clientReq.method} ${clientReq.url}`);

  const chunks = [];
  clientReq.on("data", (chunk) => chunks.push(chunk));
  clientReq.on("end", () => {
    let body = Buffer.concat(chunks);
    let bodyStr = body.toString();

    // ===== 修复 1:剥离 anthropic-beta header =====
    const headers = {};
    let logInfo = [];

    for (const [key, value] of Object.entries(clientReq.headers)) {
      const lowerKey = key.toLowerCase();
      if (lowerKey === "anthropic-beta") {
        logInfo.push("stripped anthropic-beta");
        continue;
      }
      if (lowerKey === "host") {
        headers[key] = TARGET_HOST;
        continue;
      }
      if (lowerKey === "content-length") continue;
      headers[key] = value;
    }

    // ===== 修复 2 & 3:修正 body 中的非法 role 和 thinking blocks =====
    const result = sanitizeBody(bodyStr);
    if (result.json) {
      bodyStr = JSON.stringify(result.json);
      body = Buffer.from(bodyStr);
      logInfo = logInfo.concat(result.logInfo);
    }

    headers["content-length"] = body.length;

    if (logInfo.length > 0) {
      console.log(`  → ${logInfo.join(" | ")}`);
    }

    // 转发
    const options = {
      hostname: TARGET_HOST,
      port: 443,
      path: clientReq.url,
      method: clientReq.method,
      headers: headers,
    };

    const proxyReq = https.request(options, (proxyRes) => {
      clientRes.writeHead(proxyRes.statusCode, proxyRes.headers);
      proxyRes.pipe(clientRes);
    });

    proxyReq.on("error", (err) => {
      console.error(`  → Error: ${err.message}`);
      if (!clientRes.headersSent) {
        clientRes.writeHead(502, { "Content-Type": "application/json" });
        clientRes.end(
          JSON.stringify({ error: "Proxy Error", message: err.message }),
        );
      }
    });

    proxyReq.write(body);
    proxyReq.end();
  });
});

server.listen(PORT, "127.0.0.1", () => {
  console.log(`========================================`);
  console.log(` Anthropic Request Fixer Proxy `);
  console.log(`========================================`);
  console.log(`Local:    http://127.0.0.1:${PORT}`);
  console.log(`Target:   https://${TARGET_HOST}`);
  console.log(`Fixes:    strip beta | fix roles | remove thinking blocks`);
  console.log(`========================================\n`);
});

使用方法

1. 启动代理:

# 默认端口 3456
node TokenRhythm-anthropic-fixer-proxy.js

或者保存为脚本文件(.bat)

运行脚本,只能开一个对话窗口,不能多个窗口调用这个API,因为这个脚本的端口是固定的

1
2
3
4
5
6
7
8
@echo off
title TokenRhythm Proxy Fixer183
echo ========================================
echo Starting TokenRhythm Anthropic Fixer...
echo ========================================
cd /d "%~dp0"
:: 启动第一个实例(183)
start "TokenRhythm Proxy 3456 (183)" cmd /c "set PORT=3456 && node TokenRhythm-anthropic-fixer-proxy.js"

2. 配置 Claude Code:

ANTHROPIC_BASE_URL 指向本地端口:

"ANTHROPIC_BASE_URL": "http://127.0.0.1:3456"

改完重启 Claude Code。

3. 验证:

可正常对话

start "TokenRhythm Proxy 3457 (VPN)" cmd /c "set PORT=3457 && set HTTP_PROXY=http://127.0.0.1:7890 && set HTTPS_PROXY=http://127.0.0.1:7890 && node TokenRhythm-anthropic-fixer-proxy.js"

文件清单

文件 说明
TokenRhythm-anthropic-fixer-proxy.js 代理脚本(唯一在用版本,含全部三项修复)
TokenRhythm-anthropic-fixer-proxy183.bat 启动入口:端口 3456,国内直连
TokenRhythm-anthropic-fixer-proxy176.bat 启动入口:端口 3457,VPN