diff --git a/CHANGELOG.md b/CHANGELOG.md index 9adb4c86acd..47fb63e5c39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ Docs: https://docs.openclaw.ai - Discord/gateway startup: treat plain-text and transient `/gateway/bot` metadata fetch failures as transient startup errors so Discord gateway boot no longer crashes on unhandled rejections. (#44397) Thanks @jalehman. - Gateway/session reset: preserve `lastAccountId` and `lastThreadId` across gateway session resets so replies keep routing back to the same account and thread after `/reset`. (#44773) Thanks @Lanfei. - Agents/memory bootstrap: load only one root memory file, preferring `MEMORY.md` and using `memory.md` as a fallback, so case-insensitive Docker mounts no longer inject duplicate memory context. (#26054) Thanks @Lanfei. +- Agents/OpenAI-compatible compat overrides: respect explicit user `models[].compat` opt-ins for non-native `openai-completions` endpoints so usage-in-streaming capability overrides no longer get forced off when the endpoint actually supports them. (#44432) Thanks @cheapestinference. ## 2026.3.12 diff --git a/src/agents/model-compat.test.ts b/src/agents/model-compat.test.ts index fc52ee2205e..56b9c16203c 100644 --- a/src/agents/model-compat.test.ts +++ b/src/agents/model-compat.test.ts @@ -251,7 +251,7 @@ describe("normalizeModelCompat", () => { }); }); - it("overrides explicit supportsDeveloperRole true on non-native endpoints", () => { + it("respects explicit supportsDeveloperRole true on non-native endpoints", () => { const model = { ...baseModel(), provider: "custom-cpa", @@ -259,10 +259,10 @@ describe("normalizeModelCompat", () => { compat: { supportsDeveloperRole: true }, }; const normalized = normalizeModelCompat(model); - expect(supportsDeveloperRole(normalized)).toBe(false); + expect(supportsDeveloperRole(normalized)).toBe(true); }); - it("overrides explicit supportsUsageInStreaming true on non-native endpoints", () => { + it("respects explicit supportsUsageInStreaming true on non-native endpoints", () => { const model = { ...baseModel(), provider: "custom-cpa", @@ -270,6 +270,18 @@ describe("normalizeModelCompat", () => { compat: { supportsUsageInStreaming: true }, }; const normalized = normalizeModelCompat(model); + expect(supportsUsageInStreaming(normalized)).toBe(true); + }); + + it("still forces flags off when not explicitly set by user", () => { + const model = { + ...baseModel(), + provider: "custom-cpa", + baseUrl: "https://proxy.example.com/v1", + }; + delete (model as { compat?: unknown }).compat; + const normalized = normalizeModelCompat(model); + expect(supportsDeveloperRole(normalized)).toBe(false); expect(supportsUsageInStreaming(normalized)).toBe(false); }); diff --git a/src/agents/model-compat.ts b/src/agents/model-compat.ts index 7bad084fe57..72deb0c655f 100644 --- a/src/agents/model-compat.ts +++ b/src/agents/model-compat.ts @@ -55,17 +55,22 @@ 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. + // openai-completions endpoints, force both compat flags off — unless the + // user has explicitly opted in via their model config. 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. - // Note: explicit true values are intentionally overridden for non-native - // endpoints for safety. const needsForce = baseUrl ? !isOpenAINativeEndpoint(baseUrl) : false; if (!needsForce) { return model; } - if (compat?.supportsDeveloperRole === false && compat?.supportsUsageInStreaming === false) { + + // 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) { return model; } @@ -73,7 +78,11 @@ export function normalizeModelCompat(model: Model): Model { return { ...model, compat: compat - ? { ...compat, supportsDeveloperRole: false, supportsUsageInStreaming: false } + ? { + ...compat, + supportsDeveloperRole: forcedDeveloperRole || false, + supportsUsageInStreaming: forcedUsageStreaming || false, + } : { supportsDeveloperRole: false, supportsUsageInStreaming: false }, } as typeof model; }