diff --git a/changelog.d/fixes/9545-gpt56-effort-tools.md b/changelog.d/fixes/9545-gpt56-effort-tools.md new file mode 100644 index 0000000000..7cc1a77bb3 --- /dev/null +++ b/changelog.d/fixes/9545-gpt56-effort-tools.md @@ -0,0 +1 @@ +- fix(providers): strip provider prefix in getModelTargetFormat to route GPT-5.6 models to /v1/responses (#9545) diff --git a/open-sse/config/providerModels.ts b/open-sse/config/providerModels.ts index 4051e00e93..75099bad83 100644 --- a/open-sse/config/providerModels.ts +++ b/open-sse/config/providerModels.ts @@ -12,27 +12,27 @@ export const PROVIDER_MODELS: Record = new Proxy( {} as Record, { get(_, prop) { - if (typeof prop === 'symbol') return undefined; + if (typeof prop === "symbol") return undefined; return Reflect.get(initModels(), prop, _models); }, has(_, prop) { - if (typeof prop === 'symbol') return false; + if (typeof prop === "symbol") return false; return Reflect.has(initModels(), prop); }, ownKeys() { return Reflect.ownKeys(initModels()); }, getOwnPropertyDescriptor(_, prop) { - if (typeof prop === 'symbol') return undefined; + if (typeof prop === "symbol") return undefined; return Object.getOwnPropertyDescriptor(initModels(), prop); }, set(_, prop, value) { - if (typeof prop === 'symbol') return false; + if (typeof prop === "symbol") return false; (initModels() as Record)[prop] = value; return true; }, deleteProperty(_, prop) { - if (typeof prop === 'symbol') return false; + if (typeof prop === "symbol") return false; return Reflect.deleteProperty(initModels(), prop); }, } @@ -41,27 +41,27 @@ export const PROVIDER_ID_TO_ALIAS: Record = new Proxy( {} as Record, { get(_, prop) { - if (typeof prop === 'symbol') return undefined; + if (typeof prop === "symbol") return undefined; return Reflect.get(initAliases(), prop, _aliases); }, has(_, prop) { - if (typeof prop === 'symbol') return false; + if (typeof prop === "symbol") return false; return Reflect.has(initAliases(), prop); }, ownKeys() { return Reflect.ownKeys(initAliases()); }, getOwnPropertyDescriptor(_, prop) { - if (typeof prop === 'symbol') return undefined; + if (typeof prop === "symbol") return undefined; return Object.getOwnPropertyDescriptor(initAliases(), prop); }, set(_, prop, value) { - if (typeof prop === 'symbol') return false; + if (typeof prop === "symbol") return false; (initAliases() as Record)[prop] = value; return true; }, deleteProperty(_, prop) { - if (typeof prop === 'symbol') return false; + if (typeof prop === "symbol") return false; return Reflect.deleteProperty(initAliases(), prop); }, } @@ -116,7 +116,13 @@ export function findModelName(aliasOrId: string, modelId: string): string { export function getModelTargetFormat(aliasOrId: string, modelId: string): string | null { const models = PROVIDER_MODELS[aliasOrId]; - const found = models?.find((m) => m.id === modelId); + // Strip provider prefix if present: "openai/gpt-5.6-luna" → "gpt-5.6-luna" + const prefix = aliasOrId + "/"; + const bareModelId = + typeof modelId === "string" && modelId.startsWith(prefix) + ? modelId.slice(prefix.length) + : modelId; + const found = models?.find((m) => m.id === bareModelId); if (found?.targetFormat) return found.targetFormat; // #5842: OpenAI "*-pro" reasoning models (o1-pro, gpt-5.x-pro) are only served by // the native /v1/responses endpoint — /v1/chat/completions 404s ("only supported @@ -124,7 +130,7 @@ export function getModelTargetFormat(aliasOrId: string, modelId: string): string // covers dynamically-synced ids that post-date the catalog (same spirit as the gh // executor's /codex/i routing, 9router#102). Scoped to the openai alias so other // providers shipping *-pro ids keep their own endpoint semantics. - if (aliasOrId === "openai" && /-pro$/i.test(modelId)) return "openai-responses"; + if (aliasOrId === "openai" && /-pro$/i.test(bareModelId)) return "openai-responses"; return null; } diff --git a/tests/unit/9545-gpt56-reasoning-tools.test.ts b/tests/unit/9545-gpt56-reasoning-tools.test.ts new file mode 100644 index 0000000000..4899e59f1f --- /dev/null +++ b/tests/unit/9545-gpt56-reasoning-tools.test.ts @@ -0,0 +1,29 @@ +import { describe, it } from "node:test"; +import assert from "node:assert"; + +describe("Issue #9545 — GPT-5.6 URL routing + reasoning_effort with tools", () => { + it("getModelTargetFormat should resolve gpt-5.6-luna with and without provider prefix", async () => { + const { getModelTargetFormat } = await import("../../open-sse/config/providerModels.ts"); + assert.strictEqual(getModelTargetFormat("openai", "gpt-5.6-luna"), "openai-responses"); + assert.strictEqual(getModelTargetFormat("openai", "openai/gpt-5.6-luna"), "openai-responses"); + }); + + it("getModelTargetFormat should resolve non-prefixed models unchanged", async () => { + const { getModelTargetFormat } = await import("../../open-sse/config/providerModels.ts"); + assert.strictEqual(getModelTargetFormat("openai", "gpt-4o"), null); + assert.strictEqual(getModelTargetFormat("openai", "gpt-5.5-pro"), "openai-responses"); + assert.strictEqual(getModelTargetFormat("openai", "openai/gpt-5.5-pro"), "openai-responses"); + }); + + it("stripGpt5ReasoningWhenTools should not strip when targetFormat=openai-responses", async () => { + const { stripGpt5ReasoningWhenTools } = + await import("../../open-sse/services/gpt5SamplingGuard.ts"); + const body = { + model: "gpt-5.6-luna", + tools: [{ type: "function", function: { name: "test" } }], + reasoning_effort: "high", + }; + const r = stripGpt5ReasoningWhenTools(body, "openai", "gpt-5.6-luna", "openai-responses", null); + assert.strictEqual(r.reasoning_effort, "high"); + }); +});