Skip to content

[Bug/错误]: Responses → Chat Completions 将并行 function calls 拆分为无效的连续 assistant 消息 #2113

Description

@Snowy117

[Bug/错误]: Responses → Chat Completions 将并行 function calls 拆分为无效的连续 assistant 消息

Pre-submission Checklist / 提交前检查

  • I searched the existing issues and did not find this exact bug.
    • PR #590 修复了 Bailian outbound transformer 中类似的排序问题,但通用的 OpenAI Responses inbound 转换仍受影响。
  • I have read the documentation.
  • I am using the latest version of AxonHub.

FAQ Check / FAQ 检查

  • I checked the FAQ issue and did not find this problem covered there.

Bug Description / Bug 描述

当发送到 /v1/responses 的请求包含两个连续的 function_call input item,后面跟着对应的两个 function_call_output item 时,AxonHub 会将每个 function call 分别转换为一个独立的 Chat Completions assistant 消息。

合法的 Responses input:

function_call        call_a
function_call        call_b
function_call_output call_a
function_call_output call_b

AxonHub 当前将其转发到 Chat Completions 渠道时变为:

assistant tool_calls=[call_a]
assistant tool_calls=[call_b]
tool      tool_call_id=call_a
tool      tool_call_id=call_b

这是非法的 Chat Completions 顺序。第一条 assistant 消息声明了 call_a 后,必须立即跟随对应的 tool 消息,而不是另一条 assistant 消息。严格的上游会拒绝该请求并报告 call_a 缺少响应。

这两个连续的 Responses function_call item 代表同一次 assistant turn 中的并行调用,应合并为一条 Chat Completions assistant 消息。

Steps to Reproduce / 复现步骤

export AXONHUB_BASE_URL='https://your-axonhub.example.com'
export AXONHUB_API_KEY='replace-with-your-api-key'

curl --silent --show-error \
  --request POST \
  "${AXONHUB_BASE_URL}/v1/responses" \
  --header "Authorization: Bearer ${AXONHUB_API_KEY}" \
  --header 'Content-Type: application/json' \
  --header 'User-Agent: axonhub-consecutive-function-calls-repro/1.0' \
  --data-binary @- <<'JSON'
{
  "model": "kimi-k3",
  "store": false,
  "stream": false,
  "input": [
    {
      "role": "user",
      "content": "Reply with exactly OK."
    },
    {
      "type": "function_call",
      "call_id": "call_a",
      "name": "first_tool",
      "arguments": "{}"
    },
    {
      "type": "function_call",
      "call_id": "call_b",
      "name": "second_tool",
      "arguments": "{}"
    },
    {
      "type": "function_call_output",
      "call_id": "call_a",
      "output": "first result"
    },
    {
      "type": "function_call_output",
      "call_id": "call_b",
      "output": "second result"
    },
    {
      "role": "user",
      "content": "Reply with exactly OK."
    }
  ],
  "max_output_tokens": 32
}
JSON

kimi官方渠道。

Expected Behavior / 期望行为

AxonHub 应将连续的 function_call item 合并为一条内部 assistant 消息,以保留单次 assistant turn。Chat Completions payload 应具有以下形状:

[
  {
    "role": "assistant",
    "tool_calls": [
      {
        "id": "call_a",
        "type": "function",
        "function": { "name": "first_tool", "arguments": "{}" }
      },
      {
        "id": "call_b",
        "type": "function",
        "function": { "name": "second_tool", "arguments": "{}" }
      }
    ]
  },
  {
    "role": "tool",
    "tool_call_id": "call_a",
    "content": "first result"
  },
  {
    "role": "tool",
    "tool_call_id": "call_b",
    "content": "second result"
  },
  {
    "role": "user",
    "content": "Reply with exactly OK."
  }
]

上游请求应正常完成,而非返回 HTTP 400。

Error Message / 错误消息

HTTP/2 400

{
  "error": {
    "message": "an assistant message with 'tool_calls' must be followed by tool messages responding to each 'tool_call_id'. The following tool_call_ids did not have response messages: call_a",
    "type": "InvalidParameter",
    "code": "InvalidParameter"
  }
}

Operating System / 操作系统

Docker on Linux.

AxonHub Version / AxonHub 版本

Image: looplj/axonhub:v1.0.0-beta5
OCI version label: v1.0.0-beta5
OCI revision label: d061ac7df6aef0c5ec6cdfa9dc5002546a1c5a57

Usage Scenario / 使用场景

API.

API Format / API 格式

  • 入站:OpenAI - Responses(/v1/responses)。
  • 出站渠道:OpenAI - Chat Completions(/v1/chat/completions)。

Channel Type / 渠道类型

kimi OpenAI-compatible Chat Completions 渠道。

Logs / 日志

保存的入站请求结构正确:

# requests.id = 58800
1  <implicit message>     user
2  function_call          call_a  first_tool
3  function_call          call_b  second_tool
4  function_call_output   call_a
5  function_call_output   call_b
6  <implicit message>     user

保存的出站执行结构无效:

# request_executions.id = 71643
1  user
2  assistant  tool_calls=call_a
3  assistant  tool_calls=call_b
4  tool       tool_call_id=call_a
5  tool       tool_call_id=call_b
6  user

执行记录中包含:

status: failed
response_status_code: 400
format: openai/chat_completions
error: an assistant message with 'tool_calls' must be followed by tool messages responding to each 'tool_call_id'. The following tool_call_ids did not have response messages: call_a

Additional Context / 其他补充信息

可能原因(以下内容由GPT生成)

问题出现在:

llm/transformer/openai/responses/inbound.go

convertInputToMessages()reasoning item 有特殊处理:convertReasoningWithFollowing() 会向前扫描并将后续的 function_call item 合并到一条 assistant 消息中。

但裸 function_callconvertItemToMessage() 处理,该方法会为每个调用创建一条只含一个 tool call 的新 assistant 消息。因此,连续的裸 function_call item 会变成连续的 assistant 消息:

convertInputToMessages                       约第 329 行
convertReasoningWithFollowing                约第 388 行
convertReasoningWithFollowing/function_call  约第 417 行
convertItemToMessage/function_call           约第 519 行

当前源码参考:

func convertInputToMessages(input *Input) ([]llm.Message, error) {

建议修复

convertInputToMessages() 遇到裸 function_callcustom_tool_call 时,收集同一 Responses turn 中的所有连续 tool-call item,并追加到一条 llm.Message{Role: "assistant", ToolCalls: [...]} 中。遇到 message、tool output 或其他分隔 turn 的 item 时停止合并。

建议的回归用例:

  1. function_call, function_call 变为一条包含两个 tool calls 的 assistant 消息。
  2. function_call, function_call, function_call_output, function_call_output 保持合法的 Chat Completions 顺序。
  3. 由 outputs 或 user message 分隔的 tool-call 组不会被跨 turn 合并。
  4. 对连续的 custom_tool_call item 也应用同等的合并逻辑(在可表示的范围内)。

PR #590 可作为有价值的参考:它针对 Bailian outbound 合并了连续的纯 tool-call assistant 消息,但未修复通用的 Responses inbound 表示——该问题在渠道特定的 outbound 转换之前就产生了无效的消息序列。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions