fix(agents): clean 41328 compat strict-mode scope

This commit is contained in:
Şahan 2026-03-13 18:47:03 -04:00 committed by Frank Yang
parent a2d73be3a4
commit 4efb622cbe
2 changed files with 58 additions and 10 deletions

View File

@ -28,6 +28,10 @@ function supportsUsageInStreaming(model: Model<Api>): boolean | undefined {
?.supportsUsageInStreaming;
}
function supportsStrictMode(model: Model<Api>): boolean | undefined {
return (model.compat as { supportsStrictMode?: boolean } | undefined)?.supportsStrictMode;
}
function createTemplateModel(provider: string, id: string): Model<Api> {
return {
id,
@ -94,6 +98,13 @@ function expectSupportsUsageInStreamingForcedOff(overrides?: Partial<Model<Api>>
expect(supportsUsageInStreaming(normalized)).toBe(false);
}
function expectSupportsStrictModeForcedOff(overrides?: Partial<Model<Api>>): void {
const model = { ...baseModel(), ...overrides };
delete (model as { compat?: unknown }).compat;
const normalized = normalizeModelCompat(model as Model<Api>);
expect(supportsStrictMode(normalized)).toBe(false);
}
function expectResolvedForwardCompat(
model: Model<Api> | 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);
});
});

View File

@ -54,9 +54,10 @@ export function normalizeModelCompat(model: Model<Api>): Model<Api> {
// 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<Api>): Model<Api> {
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<Api>): Model<Api> {
...compat,
supportsDeveloperRole: forcedDeveloperRole || false,
supportsUsageInStreaming: forcedUsageStreaming || false,
supportsStrictMode: targetStrictMode,
}
: { supportsDeveloperRole: false, supportsUsageInStreaming: false },
: {
supportsDeveloperRole: false,
supportsUsageInStreaming: false,
supportsStrictMode: false,
},
} as typeof model;
}