diff --git a/apps/gateway/src/chat/tools/parse-provider-response.spec.ts b/apps/gateway/src/chat/tools/parse-provider-response.spec.ts index 94a4570cc1..8ded2810c3 100644 --- a/apps/gateway/src/chat/tools/parse-provider-response.spec.ts +++ b/apps/gateway/src/chat/tools/parse-provider-response.spec.ts @@ -824,6 +824,68 @@ describe("parseProviderResponse", () => { expect(result.finishReason).toBe("upstream_error"); }); + + it("maps 'end_turn' finish reason to 'stop' for groq", () => { + const json = { + choices: [ + { + message: { content: "Hello", role: "assistant" }, + finish_reason: "end_turn", + }, + ], + usage: { + prompt_tokens: 10, + completion_tokens: 5, + total_tokens: 15, + }, + }; + + const result = parseProviderResponse( + "groq", + "llama-3.3-70b-versatile", + json, + ); + + expect(result.finishReason).toBe("stop"); + }); + + it("maps 'tool_use' finish reason to 'tool_calls' for together-ai", () => { + const json = { + choices: [ + { + message: { + role: "assistant", + content: null, + tool_calls: [ + { + id: "call_1", + type: "function", + function: { + name: "get_weather", + arguments: '{"city":"San Francisco"}', + }, + }, + ], + }, + finish_reason: "tool_use", + }, + ], + usage: { + prompt_tokens: 10, + completion_tokens: 5, + total_tokens: 15, + }, + }; + + const result = parseProviderResponse( + "together-ai", + "deepseek-ai/DeepSeek-V3", + json, + ); + + expect(result.finishReason).toBe("tool_calls"); + expect(result.toolResults).toHaveLength(1); + }); }); describe("refusal finish reason", () => { diff --git a/apps/gateway/src/chat/tools/parse-provider-response.ts b/apps/gateway/src/chat/tools/parse-provider-response.ts index 33b92c60b0..b721f0e72f 100644 --- a/apps/gateway/src/chat/tools/parse-provider-response.ts +++ b/apps/gateway/src/chat/tools/parse-provider-response.ts @@ -1074,7 +1074,10 @@ export function parseProviderResponse( reasoningContent = hasReasoning ? aggregatedReasoning : null; finishReason = allChoices[0]?.finish_reason ?? null; - if (finishReason === "abort") { + // Map non-standard finish reasons to OpenAI-compatible values + if (finishReason === "end_turn") { + finishReason = "stop"; + } else if (finishReason === "abort") { logger.warn("Upstream sent abort finish_reason", { provider: usedProvider, model: usedModel, @@ -1087,6 +1090,8 @@ export function parseProviderResponse( // "abort" is an upstream-initiated interruption, not a client // cancellation, so it counts as an upstream error. finishReason = "upstream_error"; + } else if (finishReason === "tool_use") { + finishReason = "tool_calls"; } // ZAI-specific fix for incorrect finish_reason in tool response scenarios