Skip to content

[Bug] Claude /v1/messages 转换后出现连续 user 角色,导致 Gemini API 返回 400 INVALID_ARGUMENT #200

@zhaiiker

Description

@zhaiiker

版本号

v1.3.1

请求接口

Anthropic

模型名称

gemini-3.1-pro-preview

客户端/工具

Claude

问题描述

Issue Body

环境信息

  • 项目: iBUHub/AIStudioToAPI
  • 版本: v1.3.1
  • 接口: POST /v1/messages(Anthropic Claude 兼容格式)
  • 目标模型: gemini-3.1-pro-preview
  • 客户端: Claude Code CLI
  • 复现日期: 2026-06-05

问题描述

通过本项目的 Claude 兼容接口发起带 多轮 tool 调用 的请求时,Google API 返回:

400 INVALID_ARGUMENT
{"error":{"code":400,"message":"Request contains an invalid argument.","status":"INVALID_ARGUMENT"}}

服务端日志示例:

[INFO] [Adapter] Starting translation of Claude request format to Google format...
[INFO] [Adapter] Converted 27 Claude tool(s) to Gemini format
[DEBUG] [Registry] Found WebSocket connection for authIndex=28
[INFO] [Context#28] Received request: POST /v1beta/models/gemini-3.1-pro-preview:streamGenerateContent
[INFO] [Context#28] Request processing failed: Google API returned error: 400 INVALID_ARGUMENT
[ERROR] ❌ [Request] Claude real stream failed. Status code: 400

请求在 Adapter 转换完成、转发到 Gemini 之前 结构已不合法,属于 FormatConverter.translateClaudeToGoogle() 的转换逻辑问题。

复现步骤

  1. 部署 AIStudioToAPI,配置可用 auth 与 gemini-3.1-pro-preview 模型。
  2. 使用 Claude Code CLI(或任何会发送 Anthropic Messages API 格式的客户端)连接本项目的 /v1/messages
  3. 发起包含以下特征的对话:
    • messages 数组中存在 role: "system" 的消息(Claude Code 会把 skills 列表放在这里);
    • 多轮 tool_use / tool_result(如 Bash 工具调用);
    • tool_result 之后紧跟普通 user 文本消息(如多次「继续」)。
  4. 观察服务端日志,请求在转发至 streamGenerateContent 后返回 400 INVALID_ARGUMENT

期望行为

Claude 格式请求应被正确转换为符合 Gemini API 要求的 contents 结构(user / model 角色交替),并成功获得模型响应。

实际行为

转换后的 Gemini 请求包含 连续的 user 角色,触发 Google API 400 INVALID_ARGUMENT

根因分析

问题位于 src/core/FormatConverter.jstranslateClaudeToGoogle(),主要有两点:

1. messages 中的 role: "system" 未被过滤(P0)

OpenAI 适配路径会过滤 system 消息并合并到 systemInstruction

// translateOpenAIToGoogle — 正确处理
const conversationMessages = openaiBody.messages.filter(msg => msg.role !== "system");

但 Claude 适配路径 只处理顶层 claudeBody.system,不处理 messages 里的 role: "system"。这些消息在写入 googleContents 时被映射为 user

// translateClaudeToGoogle — 第 2298-2302 行
role: message.role === "assistant" ? "model" : "user",  // system 也变成 user

Claude Code 会在 messages 中插入 skills 的 system 消息,导致对话开头出现:

user(用户消息)→ user(system/skills)→ model(tool_use)→ ...

2. tool_result 与普通 user 文本未合并(P0)

flushToolParts() 将工具结果写成一条 user 消息;若下一条仍是 user 文本(如「继续」),会再推入一条 user,形成:

user(functionResponse)→ user(继续/文本)

Gemini API 要求 contentsusermodel 严格交替,连续 user 会导致 INVALID_ARGUMENT

3. Claude tool_result 数组内容处理不完整(P1,次要)

OpenAI 路径对数组型 tool 响应有完整规范化(保证 functionResponse.response 为 object):

// translateOpenAIToGoogle — 第 574-621 行
if (Array.isArray(responseContent)) { ... responseContent = { result: JSON.stringify(...) }; }

Claude 路径对 Array.isArray(toolResult.content) 仅提取 type === "text" 的块。若内容为原始 JSON 数组(如 docker inspect 返回 [{ "Id": "...", ... }]),会被转成 { "result": "" }工具结果内容丢失(不一定直接 400,但行为异常)。

本地验证

在 v1.3.1 代码上构造最小复现 payload,转换后 contents 角色序列为:

user → user → model → user → user

出现 两处连续 user,与上述分析一致。

建议修复方向

  1. 对齐 OpenAI 路径:在 translateClaudeToGoogle() 中过滤 messagesrole === "system" 的消息,合并进 systemInstruction(或追加到已有 systemInstruction)。
  2. 合并相邻同角色 contents:转换结束后,将连续 user(或连续 model)合并为单条消息;至少处理「tool_result flush 后紧跟 user 文本」的场景。
  3. 复用 OpenAI 的数组 tool 响应规范化逻辑到 Claude 的 tool_result 分支。
  4. 补充单元测试:覆盖 system-in-messages、tool_result + 后续文本、非 text 数组 tool_result 三类场景。

相关代码位置

文件 函数/行号 说明
src/core/FormatConverter.js translateClaudeToGoogle() ~2066 Claude 转换主逻辑
src/core/FormatConverter.js ~2120-2129 仅处理 claudeBody.system
src/core/FormatConverter.js ~2298-2302 system 被映射为 user
src/core/FormatConverter.js ~2134-2142, 2216-2223 tool_result flush 逻辑
src/core/FormatConverter.js translateOpenAIToGoogle() ~534-544 可参考的 system 过滤实现
src/core/FormatConverter.js translateOpenAIToGoogle() ~574-621 可参考的数组 tool 响应处理

补充说明

  • scripts/client/build.js 中有针对 thinkingLevel / thoughtSignature 的 INVALID_ARGUMENT 兜底注释,但无法修复 contents 角色交替 这一结构性问题。
  • test/ 目录目前缺少 translateClaudeToGoogle 相关测试,建议一并补充以防回归。

日志片段(节选)

[INFO] [Adapter] Converted 27 Claude tool(s) to Gemini format
[INFO] [Context#28] Received request: POST /v1beta/models/gemini-3.1-pro-preview:streamGenerateContent
[INFO] [Context#28] Request processing failed: Google API returned error: 400 INVALID_ARGUMENT
[ERROR] ❌ [Request] Claude real stream failed. Status code: 400, message: Proxy browser error: Google API returned error: 400 INVALID_ARGUMENT

感谢维护!如需更多日志或完整 Final Gemini Request DEBUG 输出,我可以继续提供。

日志信息 (关键)

1. 错误摘要

[INFO] [Entrypoint] Received a request: POST /v1/messages
[INFO] [Auth] API Key verification passed (from: x.x.x.x)
[INFO] [Request] Claude generation request - account rotation count: 1/10 (Current account: 28), request ID: req_1780673184457_v27089l7j
[INFO] [Adapter] Starting translation of Claude request format to Google format...
[INFO] [Adapter] Converted 27 Claude tool(s) to Gemini format
[INFO] [Adapter] Claude to Google translation complete.
[INFO] [Context#28] Received request: POST /v1beta/models/gemini-3.1-pro-preview:streamGenerateContent
[INFO] [Context#28] Request processing failed: Google API returned error: 400 INVALID_ARGUMENT
[ERROR] [Request] Claude real stream failed. Status code: 400, message: Proxy browser error: Google API returned error: 400 INVALID_ARGUMENT

2. 入站 Claude 请求特征(节选)

{
  "model": "gemini-3.1-pro-preview",
  "max_tokens": 32000,
  "stream": true,
  "messages": [
    {
      "role": "user",
      "content": [
        { "type": "text", "text": "<system-reminder>...[truncated]...</system-reminder>" },
        { "type": "text", "text": "用户任务内容(已省略)" }
      ]
    },
    {
      "role": "system",
      "content": "The following skills are available for use with the Skill tool: ... [skills 列表,已省略]"
    },
    { "role": "assistant", "content": [{ "type": "tool_use", "id": "toolu_xxx", "name": "Bash", "input": { "command": "..." } }] },
    { "role": "user", "content": [{ "type": "tool_result", "tool_use_id": "toolu_xxx", "content": "...", "is_error": false }] },
    "... 多轮 tool_use / tool_result ...",
    {
      "role": "user",
      "content": [
        { "type": "tool_result", "tool_use_id": "toolu_yyy", "content": "[{...docker inspect JSON 数组...}]" },
        { "type": "text", "text": "继续\n" },
        "... 多次「继续」..."
      ]
    },
    {
      "role": "system",
      "content": "The task tools haven't been used recently. ... [harness 提醒,已省略]"
    }
  ],
  "system": [
    { "type": "text", "text": "You are Claude Code, Anthropic's official CLI for Claude." },
    { "type": "text", "text": "... [system prompt,已省略] ..." }
  ],
  "tools": "[27 个 Claude Code 工具定义,含 Bash/Agent/Edit 等,已省略]"
}

关键点:

  • messages 数组内存在 role: "system"(skills 列表、harness 提醒),不在顶层 system 字段。
  • 对话含 多轮 tool_use / tool_result
  • 最后一个 user 消息同时包含 tool_result 与多条 text(「继续」)。

3. 转换后的 Gemini 请求(问题证据)

以下为 LOG_LEVEL=DEBUG[Adapter] Debug: Final Gemini Request结构摘要(长文本已截断):

{
  "contents": [
    {
      "role": "user",
      "parts": [
        { "text": "<system-reminder>...[truncated]...</system-reminder>" },
        { "text": "用户任务内容" }
      ]
    },
    {
      "role": "user",
      "parts": [
        { "text": "The following skills are available for use with the Skill tool: ..." }
      ]
    },
    {
      "role": "model",
      "parts": [
        {
          "functionCall": { "name": "Bash", "args": { "command": "..." } },
          "thoughtSignature": "context_engineering_is_the_way_to_go"
        }
      ]
    },
    {
      "role": "user",
      "parts": [
        { "functionResponse": { "name": "Bash", "response": { "result": "..." } } }
      ]
    },
    "... 多组 model(user functionCall) + user(functionResponse) 交替 ...",
    {
      "role": "user",
      "parts": [
        { "functionResponse": { "name": "Bash", "response": { "result": "" } } },
        { "text": "继续\n" },
        { "text": "继续\n" },
        "... 更多「继续」..."
      ]
    },
    {
      "role": "user",
      "parts": [
        { "text": "The task tools haven't been used recently. ..." }
      ]
    }
  ],
  "systemInstruction": {
    "role": "user",
    "parts": [{ "text": "You are Claude Code... [truncated]" }]
  },
  "generationConfig": {
    "maxOutputTokens": 32000
  },
  "tools": [
    {
      "functionDeclarations": [
        { "name": "Agent", "parameters": { "type": "OBJECT", "...": "..." } },
        { "name": "Bash", "parameters": { "type": "OBJECT", "...": "..." } },
        "... 共 27 个 ..."
      ]
    }
  ],
  "safetySettings": "[...]"
}

转换后 contents 角色序列(按条数):

user → user → model → user → model → user → ... → user → user
       ^^^^                              ^^^^^^^^^^^^
   skills 被写成 user              连续 user(含 harness 提醒)

即存在 至少两处连续 user 角色,不符合 Gemini contents 要求的 user / model 交替。


4. 根因对照(供维护者参考)

Claude 入站 当前转换结果 OpenAI 路径行为
messages[].role === "system" 写入 contentsrole: "user" 过滤并合并到 systemInstruction
tool_result 后紧跟 user 文本 分成两条 user content OpenAI tool 消息会合并到同一 user
tool_result.content 为 JSON 数组 非 text 块时可能变成 { "result": "" } OpenAI 路径有完整数组规范化

5. 账号切换副作用(非根因,但会放大失败体验)

[WARN] [Auth] Failure threshold reached (1/1)! Preparing to switch account...
[INFO] [Auth] Successfully switched to account #29, counters reset.

单次 400 INVALID_ARGUMENT 即触发账号切换(FAILURE_THRESHOLD=1),导致上下文池 rebalance、WebSocket 重连等额外开销。


6. 复现条件小结

  1. AIStudioToAPI v1.3.1
  2. Claude Code CLI → POST /v1/messages
  3. 模型:gemini-3.1-pro-preview
  4. 对话含:messagesrole:system + 多轮 tools + 末尾 user 文本
  5. 观察:Final Gemini Request 出现连续 user,随后 Google API 400 INVALID_ARGUMENT

Metadata

Metadata

Assignees

No one assigned

    Labels

    🐛 BugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions