diff --git a/src/lib/ai/coach/__tests__/tools-loop-always-reasoning-gateway.test.ts b/src/lib/ai/coach/__tests__/tools-loop-always-reasoning-gateway.test.ts index b5071cc0c..9b7a56d34 100644 --- a/src/lib/ai/coach/__tests__/tools-loop-always-reasoning-gateway.test.ts +++ b/src/lib/ai/coach/__tests__/tools-loop-always-reasoning-gateway.test.ts @@ -312,6 +312,13 @@ describe("#1276: a gateway model that always reasons", () => { }, ); + it("runs a tool call the server reports with finish_reason stop", async () => { + toolFinish = "stop"; + const out = await turn("medium"); + expect(executeCoachTool).toHaveBeenCalledTimes(1); + expect(out.result.content).toBe(ANSWER); + }); + it("fails as reasoning_only, never with the thought, when no answer ever comes", async () => { parser = "raw"; reasoningWords = 20_000; diff --git a/src/lib/ai/openai-client.ts b/src/lib/ai/openai-client.ts index 4f94b2035..20dce6fea 100644 --- a/src/lib/ai/openai-client.ts +++ b/src/lib/ai/openai-client.ts @@ -554,7 +554,12 @@ export class OpenAIClient implements AIProvider { model: this.config.model, providerType: this.type, ...(toolCalls ? { toolCalls } : {}), - finishReason: mapFinishReason(choice?.finish_reason), + // #1276 — the tool calls themselves decide, as on the Anthropic and + // Codex wires: vLLM, Ollama and LiteLLM can put `stop` beside them, + // and the loop runs calls only on `tool_calls`. + finishReason: toolCalls + ? "tool_calls" + : mapFinishReason(choice?.finish_reason), ...(params.reasoning ? { reasoning: {