From 9616d1e8ba5cd0c497e0cd9792ac212c0a29f28d Mon Sep 17 00:00:00 2001 From: Sahan <57447079+sahancava@users.noreply.github.com> Date: Sun, 15 Mar 2026 04:36:52 -0400 Subject: [PATCH] fix: Disable strict mode tools for non-native openai-completions compatible APIs (#45497) Merged via squash. Prepared head SHA: 20fe05fe747821455c020521e5c2072b368713d8 Co-authored-by: sahancava <57447079+sahancava@users.noreply.github.com> Co-authored-by: frankekn <4488090+frankekn@users.noreply.github.com> Reviewed-by: @frankekn --- CHANGELOG.md | 1 + src/agents/model-compat.test.ts | 43 ++++++++++++++++++++++++++++++++- src/agents/model-compat.ts | 25 ++++++++++++------- 3 files changed, 59 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 09a1af818b4..965d13eb4d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ Docs: https://docs.openclaw.ai - Plugins/install precedence: keep bundled plugins ahead of auto-discovered globals by default, but let an explicitly installed plugin record win its own duplicate-id tie so installed channel plugins load from `~/.openclaw/extensions` after `openclaw plugins install`. - macOS/canvas actions: keep unattended local agent actions on trusted in-app canvas surfaces only, and stop exposing the deep-link fallback key to arbitrary page scripts. Thanks @vincentkoc. - Agents/compaction: extend the enclosing run deadline once while compaction is actively in flight, and abort the underlying SDK compaction on timeout/cancel so large-session compactions stop freezing mid-run. (#46889) Thanks @asyncjason. +- Models/openai-completions: default non-native OpenAI-compatible providers to omit tool-definition `strict` fields unless users explicitly opt back in, so tool calling keeps working on providers that reject that option. (#45497) Thanks @sahancava. ### Fixes diff --git a/src/agents/model-compat.test.ts b/src/agents/model-compat.test.ts index 56b9c16203c..733d9a2f47f 100644 --- a/src/agents/model-compat.test.ts +++ b/src/agents/model-compat.test.ts @@ -28,6 +28,10 @@ function supportsUsageInStreaming(model: Model): boolean | undefined { ?.supportsUsageInStreaming; } +function supportsStrictMode(model: Model): boolean | undefined { + return (model.compat as { supportsStrictMode?: boolean } | undefined)?.supportsStrictMode; +} + function createTemplateModel(provider: string, id: string): Model { return { id, @@ -94,6 +98,13 @@ function expectSupportsUsageInStreamingForcedOff(overrides?: Partial> expect(supportsUsageInStreaming(normalized)).toBe(false); } +function expectSupportsStrictModeForcedOff(overrides?: Partial>): void { + const model = { ...baseModel(), ...overrides }; + delete (model as { compat?: unknown }).compat; + const normalized = normalizeModelCompat(model as Model); + expect(supportsStrictMode(normalized)).toBe(false); +} + function expectResolvedForwardCompat( model: Model | undefined, expected: { provider: string; id: string }, @@ -226,6 +237,17 @@ describe("normalizeModelCompat", () => { }); }); + it("forces supportsStrictMode off for z.ai models", () => { + expectSupportsStrictModeForcedOff(); + }); + + it("forces supportsStrictMode off for custom openai-completions provider", () => { + expectSupportsStrictModeForcedOff({ + provider: "custom-cpa", + baseUrl: "https://cpa.example.com/v1", + }); + }); + it("forces supportsDeveloperRole off for Qwen proxy via openai-completions", () => { expectSupportsDeveloperRoleForcedOff({ provider: "qwen-proxy", @@ -283,6 +305,18 @@ describe("normalizeModelCompat", () => { const normalized = normalizeModelCompat(model); expect(supportsDeveloperRole(normalized)).toBe(false); expect(supportsUsageInStreaming(normalized)).toBe(false); + expect(supportsStrictMode(normalized)).toBe(false); + }); + + it("respects explicit supportsStrictMode true on non-native endpoints", () => { + const model = { + ...baseModel(), + provider: "custom-cpa", + baseUrl: "https://proxy.example.com/v1", + compat: { supportsStrictMode: true }, + }; + const normalized = normalizeModelCompat(model); + expect(supportsStrictMode(normalized)).toBe(true); }); it("does not mutate caller model when forcing supportsDeveloperRole off", () => { @@ -296,16 +330,23 @@ describe("normalizeModelCompat", () => { expect(normalized).not.toBe(model); expect(supportsDeveloperRole(model)).toBeUndefined(); expect(supportsUsageInStreaming(model)).toBeUndefined(); + expect(supportsStrictMode(model)).toBeUndefined(); expect(supportsDeveloperRole(normalized)).toBe(false); expect(supportsUsageInStreaming(normalized)).toBe(false); + expect(supportsStrictMode(normalized)).toBe(false); }); it("does not override explicit compat false", () => { const model = baseModel(); - model.compat = { supportsDeveloperRole: false, supportsUsageInStreaming: false }; + model.compat = { + supportsDeveloperRole: false, + supportsUsageInStreaming: false, + supportsStrictMode: false, + }; const normalized = normalizeModelCompat(model); expect(supportsDeveloperRole(normalized)).toBe(false); expect(supportsUsageInStreaming(normalized)).toBe(false); + expect(supportsStrictMode(normalized)).toBe(false); }); }); diff --git a/src/agents/model-compat.ts b/src/agents/model-compat.ts index 72deb0c655f..46e37733aec 100644 --- a/src/agents/model-compat.ts +++ b/src/agents/model-compat.ts @@ -54,9 +54,10 @@ export function normalizeModelCompat(model: Model): Model { // The `developer` role and stream usage chunks are OpenAI-native behaviors. // Many OpenAI-compatible backends reject `developer` and/or emit usage-only - // chunks that break strict parsers expecting choices[0]. For non-native - // openai-completions endpoints, force both compat flags off — unless the - // user has explicitly opted in via their model config. + // chunks that break strict parsers expecting choices[0]. Additionally, the + // `strict` boolean inside tools validation is rejected by several providers + // causing tool calls to be ignored. For non-native openai-completions endpoints, + // default these compat flags off unless explicitly opted in. const compat = model.compat ?? undefined; // When baseUrl is empty the pi-ai library defaults to api.openai.com, so // leave compat unchanged and let default native behavior apply. @@ -64,13 +65,14 @@ export function normalizeModelCompat(model: Model): Model { if (!needsForce) { return model; } - - // Respect explicit user overrides: if the user has set a compat flag to - // true in their model definition, they know their endpoint supports it. const forcedDeveloperRole = compat?.supportsDeveloperRole === true; const forcedUsageStreaming = compat?.supportsUsageInStreaming === true; - - if (forcedDeveloperRole && forcedUsageStreaming) { + const targetStrictMode = compat?.supportsStrictMode ?? false; + if ( + compat?.supportsDeveloperRole !== undefined && + compat?.supportsUsageInStreaming !== undefined && + compat?.supportsStrictMode !== undefined + ) { return model; } @@ -82,7 +84,12 @@ export function normalizeModelCompat(model: Model): Model { ...compat, supportsDeveloperRole: forcedDeveloperRole || false, supportsUsageInStreaming: forcedUsageStreaming || false, + supportsStrictMode: targetStrictMode, } - : { supportsDeveloperRole: false, supportsUsageInStreaming: false }, + : { + supportsDeveloperRole: false, + supportsUsageInStreaming: false, + supportsStrictMode: false, + }, } as typeof model; }