Skip to content

修复DeepSeek Anthropic 端点在 thinking 模式下,多轮对话返回 HTTP 400 - #73

Open
ZirrorDmage wants to merge 2 commits into
SuperJJ007:mainfrom
ZirrorDmage:main
Open

修复DeepSeek Anthropic 端点在 thinking 模式下,多轮对话返回 HTTP 400#73
ZirrorDmage wants to merge 2 commits into
SuperJJ007:mainfrom
ZirrorDmage:main

Conversation

@ZirrorDmage

Copy link
Copy Markdown

根因

DeepSeek 与 Anthropic 对 thinking 块的多轮处理语义不同:

Anthropic API DeepSeek API
thinking 是否需回传 不需要(Claude Science 自动剥离) tool_use 存在时必须回传
默认 thinking 状态 由请求控制 默认 enabled

当 Claude Science 在多轮对话中按 Anthropic 规范剥离了 assistant 消息中的 thinking 块后,DeepSeek 检测到 thinking 启用但历史中 tool_use 关联的 thinking 缺失 → 400。

迭代记录

v1 — 初始分析(被否决)

对照 DeepSeek 官方文档后发现,代码中 auto → adaptive 的映射和 strip_ignored_params 均非 Anthropic 端点所需。保留以下函数并部署:

函数 作用
strip_thinking_from_history 无 tool_use → 剥离 thinking;有 tool_use → 保留
map_effort budget_tokensoutput_config.effort
strip_ignored_params 移除 temperaturetop_ptop_k

验证结果

  • strip_ignored_params 删除的温度/采样参数 DeepSeek 自己会处理(传了不报错,忽略即可)→ 移除

v2 — 精简为最小修复(被否决)

仅保留 strip_thinking_from_historyauto 透传。

验证结果:有 tool_use 时,Science 在到达 Gateway 之前已经剥离 thinking。Gateway 看到 tool_use → 保留(不做任何事),但 thinking 块早已缺失 → 400 复现

关键发现:Science 按 Anthropic 规范无条件剥离所有 thinking 块,包括含 tool_use 的消息。Gateway 无法"保留"已经不存在的数据。

v3 — 链断裂检测(当前方案)

放弃主动剥离/保留逻辑,改为被动检测:

  • 移除 strip_thinking_from_history — DeepSeek 官方文档明确无 tool_use 时 API 自己会忽略 thinking,无需 Gateway 干预
  • 新增 thinking_chain_broken — 仅检测 tool_use 存在但 thinking 缺失的场景,此时链已断裂,必须设 disabled 避免 400
  • 保留 auto 透传

这是最小可行方案:只在已经必报 400 的边界场景下干预,不影响正常单轮和多轮对话。

最终改动

新增 thinking_chain_broken() 函数,按 DeepSeek 官方文档规则检测 thinking 链是否断裂:

  • assistant 消息中包含 tool_use 但缺少 thinking → 链断裂(Science 已剥离)
  • 链断裂时,当前请求设 thinking: disabled,避免 400

同时移除原代码中 auto → adaptive 的 thinking type 映射。DeepSeek Anthropic 端点原生接受标准 Anthropic thinking 取值,无需 provider 特定重映射。

改动范围

单文件:desktop/gateway/src/policy.rs

normalize_thinking

-    if body.get("thinking")... == Some("auto") {
-        thinking.insert("type", "adaptive");
-    }
+    // Claude Science sends "auto"; DeepSeek's Anthropic endpoint
+    // handles it natively — no provider-specific remapping needed.

新增 thinking_chain_broken

fn thinking_chain_broken(messages: &Value) -> bool {
    // 遍历历史 assistant 消息
    // 有 tool_use 但无 thinking → 链断裂 → true
    // 其他情况 → false
}

transform_request 调用

     normalize_thinking(&mut body);
+    if thinking_chain_broken(&body["messages"]) {
+        body["thinking"] = serde_json::json!({"type": "disabled"});
+    }
     serde_json::to_vec(&body)

行为矩阵

场景 thinking 状态 结果
单轮对话 保持 auto 正常
多轮纯文本 保持 auto,thinking 原样透传 正常(API 会忽略)
多轮 tool_use + thinking 完整 保持 auto 正常
多轮 tool_use + thinking 缺失 disabled 本轮放弃 thinking,避免 400

后续方向

Gateway 可在响应阶段缓存 DeepSeek 返回的 thinking 块,在下一轮请求中检测缺失时补回,从而实现完整的 thinking 连续性和更接近 DeepSeek 官方示例的行为。当前修复为最小可行方案。

参考

  DeepSeek 的 Anthropic 端点对 reasoning_content 的回传有两条规则:
  - assistant 消息中包含 tool_use 时,后续所有轮次必须回传 reasoning_content
  - assistant 消息中无 tool_use 时,reasoning_content 可忽略

  但 Claude Science 按 Anthropic 原生规范无条件剥离所有thinking 块,导致第二轮请求中 thinking 块缺失,DeepSeek 返回 HTTP 400:

    "The content[].thinking in the thinking mode must be passed back to the API."

  新增 `strip_thinking_from_history()`,在请求发送前按DeepSeek官方文档的规则处理历史消息中的 thinking 块:

  - 有 tool_use → 保留 thinking(API 要求必须回传,否则 400)
  - 无 tool_use → 剥离 thinking(API 忽略,且 Science 已丢弃)

  同时移除 `normalize_thinking` 中 `auto → adaptive` 的映射。
  DeepSeek Anthropic 端点原生接受标准 Anthropic thinking 取值,无需 provider 特定的重映射。

  参考:
  - https://api-docs.deepseek.com/zh-cn/guides/anthropic_api
  - https://api-docs.deepseek.com/zh-cn/guides/thinking_mode
# DeepSeek Thinking 多轮对话修复

## 问题

DeepSeek Anthropic 端点在 thinking 模式下,多轮对话返回 HTTP 400:

```
The content[].thinking in the thinking mode must be passed back to the API.
```

## 根因

DeepSeek 与 Anthropic 对 thinking 块的多轮处理语义不同:

| | Anthropic API | DeepSeek API |
|---|---|---|
| thinking 是否需回传 | 不需要(Claude Science 自动剥离) | tool_use 存在时**必须回传** |
| 默认 thinking 状态 | 由请求控制 | **默认 enabled** |

当 Claude Science 在多轮对话中按 Anthropic 规范剥离了 assistant 消息中的 thinking 块后,DeepSeek 检测到 thinking 启用但历史中 tool_use 关联的 thinking 缺失 → 400。

## 迭代记录

### v1 — 初始分析(被否决)

对照 DeepSeek 官方文档后发现,代码中 `auto → adaptive` 的映射和 `strip_ignored_params` 均非 Anthropic 端点所需。保留以下函数并部署:

| 函数 | 作用 |
|---|---|
| `strip_thinking_from_history` | 无 tool_use → 剥离 thinking;有 tool_use → 保留 |
| `map_effort` | `budget_tokens` → `output_config.effort` |
| `strip_ignored_params` | 移除 `temperature`、`top_p`、`top_k` 等 |

**验证结果**:

- `map_effort` 设置 `effort=high` 导致 reasoning 消耗大量 token,产生空响应 → **移除**
- `strip_ignored_params` 删除的温度/采样参数 DeepSeek 自己会处理(传了不报错,忽略即可)→ **移除**

### v2 — 精简为最小修复(被否决)

仅保留 `strip_thinking_from_history` 和 `auto` 透传。

**验证结果**:有 tool_use 时,Science 在到达 Gateway **之前**已经剥离 thinking。Gateway 看到 tool_use → 保留(不做任何事),但 thinking 块早已缺失 → **400 复现**。

关键发现:Science 按 Anthropic 规范**无条件**剥离所有 thinking 块,包括含 tool_use 的消息。Gateway 无法"保留"已经不存在的数据。

### v3 — 链断裂检测(当前方案)

放弃主动剥离/保留逻辑,改为被动检测:

- 移除 `strip_thinking_from_history` — DeepSeek 官方文档明确无 tool_use 时 API 自己会忽略 thinking,无需 Gateway 干预
- 新增 `thinking_chain_broken` — 仅检测 tool_use 存在但 thinking 缺失的场景,此时链已断裂,必须设 `disabled` 避免 400
- 保留 `auto` 透传

这是最小可行方案:只在**已经必报 400** 的边界场景下干预,不影响正常单轮和多轮对话。

## 最终改动

新增 `thinking_chain_broken()` 函数,按 DeepSeek 官方文档规则检测 thinking 链是否断裂:

- assistant 消息中包含 `tool_use` 但缺少 `thinking` → 链断裂(Science 已剥离)
- 链断裂时,当前请求设 `thinking: disabled`,避免 400

同时移除原代码中 `auto → adaptive` 的 thinking type 映射。DeepSeek Anthropic 端点原生接受标准 Anthropic thinking 取值,无需 provider 特定重映射。

## 改动范围

单文件:`desktop/gateway/src/policy.rs`

### normalize_thinking

```diff
-    if body.get("thinking")... == Some("auto") {
-        thinking.insert("type", "adaptive");
-    }
+    // Claude Science sends "auto"; DeepSeek's Anthropic endpoint
+    // handles it natively — no provider-specific remapping needed.
```

### 新增 thinking_chain_broken

```rust
fn thinking_chain_broken(messages: &Value) -> bool {
    // 遍历历史 assistant 消息
    // 有 tool_use 但无 thinking → 链断裂 → true
    // 其他情况 → false
}
```

### transform_request 调用

```diff
     normalize_thinking(&mut body);
+    if thinking_chain_broken(&body["messages"]) {
+        body["thinking"] = serde_json::json!({"type": "disabled"});
+    }
     serde_json::to_vec(&body)
```

## 行为矩阵

| 场景 | thinking 状态 | 结果 |
|---|---|---|
| 单轮对话 | 保持 `auto` | 正常 |
| 多轮纯文本 | 保持 `auto`,thinking 原样透传 | 正常(API 会忽略) |
| 多轮 tool_use + thinking 完整 | 保持 `auto` | 正常 |
| 多轮 tool_use + thinking 缺失 | → `disabled` | 本轮放弃 thinking,避免 400 |

## 后续方向

Gateway 可在响应阶段缓存 DeepSeek 返回的 thinking 块,在下一轮请求中检测缺失时补回,从而实现完整的 thinking 连续性和更接近 DeepSeek 官方示例的行为。当前修复为最小可行方案。

## 参考

- [DeepSeek Anthropic API 文档](https://api-docs.deepseek.com/zh-cn/guides/anthropic_api)
- [DeepSeek 思考模式文档](https://api-docs.deepseek.com/zh-cn/guides/thinking_mode)
- BUG-083-DEEPSEEK-THINKING
@ZirrorDmage ZirrorDmage changed the title 修复DeepSeek Anthropic 端点在 thinking 模式下,多轮对话返回 HTTP 400 #70 修复DeepSeek Anthropic 端点在 thinking 模式下,多轮对话返回 HTTP 400 Aug 1, 2026
@ZirrorDmage
ZirrorDmage marked this pull request as ready for review August 1, 2026 07:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant