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; }