fix(ai): run gateway tool calls whatever finish reason comes beside them

The Coach loop runs a round's tool calls only when the round finished with
"tool_calls". The Anthropic and Codex clients already derive that from the
calls themselves; the OpenAI-compatible client copied the server's
finish_reason, and vLLM, Ollama and LiteLLM can report "stop" beside a
parsed tool call. The loop then took the round as the answer, the call never
ran, and what the person saw was whatever text came with it.

A reply that carries tool calls now reports "tool_calls", as on the other
wires.

Refs #1276
This commit is contained in:
Marc-André Bombeck committed 2026-10-24 18:20:35 +02:00
1 parent 652dda050c
commit c9673883b1
2 files changed
+13 -1

No files matched your search

@@ -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;
+6 -1
View File
@@ -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: {