fix: normalize finish_reason to 'tool_calls' when tool calls are present (#379)

Some upstream providers (e.g. Antigravity) return non-standard finish_reason
values like 'other' instead of the OpenAI-standard 'tool_calls' when the
model invokes tools. This causes downstream consumers (e.g. OpenClaw) to
fail to execute tool calls, breaking agentic sub-agent workflows.

Changes:
- nonStreamingHandler: post-translation guard that normalizes finish_reason
  to 'tool_calls' when message.tool_calls is present
- sseToJsonHandler: accumulate tool_calls from streaming deltas in
  parseSSEToOpenAIResponse; extract function_call items from Responses API
  output in handleForcedSSEToJson
- openai-responses translator: use toolCallIndex to choose between
  'tool_calls' and 'stop' in flush and response.completed events

Tested: 7 scenarios (non-stream text, single/multiple tool calls, stream
text/tool calls, multi-turn tool conversation, tools present but unused)
This commit is contained in:
Liam
2026-03-23 10:35:25 +08:00
committed by GitHub
parent b8918c0c1c
commit 01e4a28f0a
3 changed files with 51 additions and 5 deletions

View File

@@ -364,6 +364,7 @@ export function openaiResponsesToOpenAIResponse(chunk, state) {
// Flush: send final chunk with finish_reason
if (!state.finishReasonSent && state.started) {
state.finishReasonSent = true;
const hasToolCalls = state.toolCallIndex > 0;
return {
id: state.chatId || `chatcmpl-${Date.now()}`,
object: "chat.completion.chunk",
@@ -372,7 +373,7 @@ export function openaiResponsesToOpenAIResponse(chunk, state) {
choices: [{
index: 0,
delta: {},
finish_reason: "stop"
finish_reason: hasToolCalls ? "tool_calls" : "stop"
}]
};
}
@@ -505,7 +506,9 @@ export function openaiResponsesToOpenAIResponse(chunk, state) {
if (!state.finishReasonSent) {
state.finishReasonSent = true;
state.finishReason = "stop"; // Mark for usage injection in stream.js
const hasToolCalls = state.toolCallIndex > 0;
const resolvedFinishReason = hasToolCalls ? "tool_calls" : "stop";
state.finishReason = resolvedFinishReason; // Mark for usage injection in stream.js
const finalChunk = {
id: state.chatId,
@@ -515,7 +518,7 @@ export function openaiResponsesToOpenAIResponse(chunk, state) {
choices: [{
index: 0,
delta: {},
finish_reason: "stop"
finish_reason: resolvedFinishReason
}]
};