From fe8f06db4e608f74445c37ea0642b21def20f542 Mon Sep 17 00:00:00 2001 From: Alishahed Date: Fri, 20 Mar 2026 22:44:36 -0700 Subject: [PATCH] fix(agent): forward configured tool choice into stream params Ensure agent model params can force tool_choice through the embedded runner so OpenAI-compatible providers actually receive the required tool policy. Made-with: Cursor --- .../extra-params.openai.test.ts | 33 +++++++++++++++++++ .../extra-params.test-support.ts | 2 ++ src/agents/pi-embedded-runner/extra-params.ts | 8 +++++ 3 files changed, 43 insertions(+) diff --git a/src/agents/pi-embedded-runner/extra-params.openai.test.ts b/src/agents/pi-embedded-runner/extra-params.openai.test.ts index f7f033f5827..9bfaf84bb2b 100644 --- a/src/agents/pi-embedded-runner/extra-params.openai.test.ts +++ b/src/agents/pi-embedded-runner/extra-params.openai.test.ts @@ -1,5 +1,6 @@ import type { Model } from "@mariozechner/pi-ai"; import { afterEach, describe, expect, it } from "vitest"; +import type { OpenClawConfig } from "../../config/config.js"; import { captureEnv } from "../../test-utils/env.js"; import { runExtraParamsCase } from "./extra-params.test-support.js"; @@ -93,3 +94,35 @@ describe("extra-params: OpenAI attribution", () => { }); }); }); + +describe("extra-params: tool choice forwarding", () => { + it("forwards tool_choice from agent model params into stream options", () => { + const cfg = { + agents: { + defaults: { + models: { + "openai/gpt-5.4": { + params: { + tool_choice: "required", + }, + }, + }, + }, + }, + } satisfies OpenClawConfig; + + const capture = runExtraParamsCase({ + applyModelId: "gpt-5.4", + applyProvider: "openai", + cfg, + model: { + api: "openai-completions", + provider: "openai", + id: "gpt-5.4", + } as Model<"openai-completions">, + payload: {}, + }); + + expect((capture.options as { toolChoice?: string } | undefined)?.toolChoice).toBe("required"); + }); +}); diff --git a/src/agents/pi-embedded-runner/extra-params.test-support.ts b/src/agents/pi-embedded-runner/extra-params.test-support.ts index ae4fdb9edc3..d43d83e481b 100644 --- a/src/agents/pi-embedded-runner/extra-params.test-support.ts +++ b/src/agents/pi-embedded-runner/extra-params.test-support.ts @@ -5,6 +5,7 @@ import { applyExtraParamsToAgent } from "./extra-params.js"; export type ExtraParamsCapture> = { headers?: Record; + options?: SimpleStreamOptions; payload: TPayload; }; @@ -32,6 +33,7 @@ export function runExtraParamsCase< const baseStreamFn: StreamFn = (model, _context, options) => { captured.headers = options?.headers; + captured.options = options; options?.onPayload?.(params.payload, model); return {} as ReturnType; }; diff --git a/src/agents/pi-embedded-runner/extra-params.ts b/src/agents/pi-embedded-runner/extra-params.ts index 8ed61d4aeff..7be59f6f9c9 100644 --- a/src/agents/pi-embedded-runner/extra-params.ts +++ b/src/agents/pi-embedded-runner/extra-params.ts @@ -90,6 +90,14 @@ function createStreamFnWithExtraParams( if (typeof extraParams.maxTokens === "number") { streamParams.maxTokens = extraParams.maxTokens; } + const resolvedToolChoice = resolveAliasedParamValue( + [extraParams], + "tool_choice", + "toolChoice", + ); + if (resolvedToolChoice !== undefined) { + streamParams.toolChoice = resolvedToolChoice as CacheRetentionStreamOptions["toolChoice"]; + } const transport = extraParams.transport; if (transport === "sse" || transport === "websocket" || transport === "auto") { streamParams.transport = transport;