结论
核心拼接路径兼容,但结束语义不完全一致。
两边都通过 SSE 返回 chat.completion.chunk,并把工具参数放在 choices[].delta.tool_calls[].function.arguments 中分块返回。客户端可以用同一套累计逻辑拼接参数。
主要差异:自定义接口工具调用结束时返回 finish_reason: "stop";OpenRouter DeepSeek 返回 finish_reason: "tool_calls"。如果要对齐 DeepSeek/OpenAI-compatible 工具调用语义,建议把工具调用结束 chunk 改成 tool_calls。
实测数据源
| 接口 | JSONL 文件 | 总 SSE JSON chunk | Tool argument chunk | 模型 |
|---|---|---|---|---|
| 自定义接口 | tmp/streaming_tool_chunks.jsonl | 317 | 252 | qwen3.5-122b-int4 |
| OpenRouter DeepSeek | tmp/openrouter-deepseek-v4-flash-streaming-tool-chunks.jsonl | 461 | 434 | deepseek/deepseek-v4-flash-20260423 |
协议层对齐情况
| 检查项 | 自定义接口 | OpenRouter DeepSeek | 判断 |
|---|---|---|---|
| SSE chunk 对象 | object: chat.completion.chunk | object: chat.completion.chunk | 一致 |
| 工具参数路径 | choices[].delta.tool_calls[].function.arguments | choices[].delta.tool_calls[].function.arguments | 一致 |
| 参数是否分块 | 252 个参数 chunk | 434 个参数 chunk | 一致,均为真实分块 |
| 工具调用终止原因 | stop | tool_calls | 不一致 |
| 额外字段 | prompt_token_ids, token_ids, stop_reason | provider, native_finish_reason, usage | 实现差异,可兼容忽略 |
关键 chunk 对比
自定义接口:第一个 tool chunk
{
"tool_calls": [
{
"id": "chatcmpl-tool-8ec795451c9063ff",
"type": "function",
"index": 0,
"function": {
"name": "write_markdown_story",
"arguments": "{"
}
}
]
}
OpenRouter DeepSeek:第一个 tool chunk
{
"content": null,
"role": "assistant",
"tool_calls": [
{
"index": 0,
"id": "call_db8531364ef5436b8eb2cb0a",
"type": "function",
"function": {
"name": "write_markdown_story",
"arguments": ""
}
}
]
}
自定义接口:最后一个 tool chunk
{
"tool_calls": [
{
"index": 0,
"function": {
"arguments": ""
}
}
]
}
OpenRouter DeepSeek:最后一个 tool chunk
{
"content": null,
"role": "assistant",
"tool_calls": [
{
"index": 0,
"function": {
"arguments": "}"
}
}
]
}
Finish chunk 对比
自定义接口
[
{
"index": 0,
"delta": {
"tool_calls": [
{
"index": 0,
"function": {
"arguments": ""
}
}
]
},
"logprobs": null,
"finish_reason": "stop",
"stop_reason": null,
"token_ids": null
}
]
OpenRouter DeepSeek
[
{
"index": 0,
"delta": {
"content": "",
"role": "assistant",
"reasoning": null
},
"finish_reason": "tool_calls",
"native_finish_reason": "tool_calls"
},
{
"index": 0,
"delta": {
"content": "",
"role": "assistant"
},
"finish_reason": "tool_calls",
"native_finish_reason": "tool_calls"
}
]
字段分布
自定义接口 delta keys
{
"role": 1,
"content": 1,
"reasoning": 64,
"tool_calls": 252
}
OpenRouter DeepSeek delta keys
{
"content": 461,
"role": 461,
"reasoning": 26,
"reasoning_details": 25,
"tool_calls": 434
}
建议
- 客户端解析层可以按同一套逻辑累计
delta.tool_calls[].function.arguments,这部分已实测兼容。 - 服务端若要贴近 DeepSeek/OpenAI-compatible 工具调用语义,工具调用结束时应返回
finish_reason: "tool_calls"。 - 客户端不要依赖每个 chunk 都有
role或content。OpenRouter DeepSeek 基本每个 chunk 都带,自定义接口不是。 - 客户端应忽略未知顶层字段和 choice 字段,例如
provider、native_finish_reason、token_ids。
参考规范
DeepSeek Chat Completion 文档说明:stream=true 时会以 data-only SSE 发送 partial deltas,并用 data: [DONE] 结束;工具定义通过 tools 和 tool_choice 提供;finish_reason 包含 tool_calls,用于表示模型调用了工具。