diff --git a/open-sse/config/providerRegistry.ts b/open-sse/config/providerRegistry.ts index 83b86ca860..aa753b701d 100644 --- a/open-sse/config/providerRegistry.ts +++ b/open-sse/config/providerRegistry.ts @@ -407,6 +407,7 @@ export const REGISTRY: Record = { tokenUrl: "https://auth.openai.com/oauth/token", }, models: [ + { id: "gpt-5.5", name: "GPT 5.5", ...GPT_5_5_CODEX_CAPABILITIES }, { id: "gpt-5.5-xhigh", name: "GPT 5.5 (xHigh)", ...GPT_5_5_CODEX_CAPABILITIES }, { id: "gpt-5.5-high", name: "GPT 5.5 (High)", ...GPT_5_5_CODEX_CAPABILITIES }, { id: "gpt-5.5-medium", name: "GPT 5.5 (Medium)", ...GPT_5_5_CODEX_CAPABILITIES }, diff --git a/open-sse/services/model.ts b/open-sse/services/model.ts index 84bda681ee..017c42ea1b 100644 --- a/open-sse/services/model.ts +++ b/open-sse/services/model.ts @@ -74,8 +74,15 @@ for (const [aliasOrId, models] of Object.entries(PROVIDER_MODELS)) { } const KNOWN_MODEL_IDS = new Set(MODEL_TO_PROVIDERS.keys()); const CODEX_PREFERRED_UNPREFIXED_MODELS = new Set(["gpt-5.5"]); +const CODEX_PREFERRED_UNPREFIXED_MODEL_ALIASES = new Map([["gpt-5.5", "gpt-5.5-medium"]]); export const CODEX_NATIVE_UNPREFIXED_MODELS = new Set(["codex-auto-review"]); +interface ProviderConnectionLike { + provider?: unknown; + isActive?: unknown; + is_active?: unknown; +} + /** * Resolve provider alias to provider ID */ @@ -127,6 +134,68 @@ function hasKnownProviderModel(providerOrAlias, modelId) { return canonicalModel !== modelId && models.some((entry) => entry?.id === canonicalModel); } +function hasCodexPreferredUnprefixedModel(modelId) { + const canonicalModel = CODEX_PREFERRED_UNPREFIXED_MODEL_ALIASES.get(modelId); + if (!canonicalModel) return false; + + const providerAlias = PROVIDER_ID_TO_ALIAS.codex || "codex"; + const models = PROVIDER_MODELS[providerAlias] || PROVIDER_MODELS.codex || []; + return models.some((entry) => entry?.id === canonicalModel); +} + +function resolveInferredProviderModel(provider, modelId) { + const codexPreferredModel = CODEX_PREFERRED_UNPREFIXED_MODEL_ALIASES.get(modelId); + if (provider === "codex" && codexPreferredModel) { + return codexPreferredModel; + } + return resolveProviderModelAlias(provider, modelId); +} + +function getInferredProvidersForModel(modelId) { + const providers = [...(MODEL_TO_PROVIDERS.get(modelId) || [])]; + + if ( + CODEX_PREFERRED_UNPREFIXED_MODELS.has(modelId) && + hasCodexPreferredUnprefixedModel(modelId) && + !providers.includes("codex") + ) { + providers.push("codex"); + } + + return providers; +} + +function isProviderConnectionActive(connection: ProviderConnectionLike) { + if (connection.isActive !== undefined) { + return connection.isActive !== false && connection.isActive !== 0; + } + if (connection.is_active !== undefined) { + return connection.is_active !== false && connection.is_active !== 0; + } + return false; +} + +function getProviderIdFromConnection(connection: unknown) { + if (!connection || typeof connection !== "object") return null; + const record = connection as ProviderConnectionLike; + if (typeof record.provider !== "string" || !record.provider) return null; + if (!isProviderConnectionActive(record)) return null; + return resolveProviderAlias(record.provider); +} + +async function getActiveProviderSet() { + try { + const { getProviderConnections } = await import("@/lib/localDb"); + const conns = (await getProviderConnections()) as unknown[]; + const providers = conns + .map(getProviderIdFromConnection) + .filter((provider): provider is string => Boolean(provider)); + return new Set(providers); + } catch { + return null; + } +} + function shouldTreatAsExactModelId(modelStr) { if (!modelStr || typeof modelStr !== "string" || !modelStr.includes("/")) return false; if (!KNOWN_MODEL_IDS.has(modelStr)) return false; @@ -276,7 +345,7 @@ function parseAliasTarget(target) { } async function resolveModelByProviderInference(modelId, extendedContext) { - const providers = MODEL_TO_PROVIDERS.get(modelId) || []; + const providers = getInferredProvidersForModel(modelId); const nonOpenAIProviders = providers.filter((p) => p !== "openai"); @@ -288,10 +357,24 @@ async function resolveModelByProviderInference(modelId, extendedContext) { }; } - if (providers.includes("codex") && CODEX_PREFERRED_UNPREFIXED_MODELS.has(modelId)) { + const activeProviders = await getActiveProviderSet(); + + const activeCandidates = activeProviders ? providers.filter((p) => activeProviders.has(p)) : []; + + if (activeCandidates.length === 1) { + const provider = activeCandidates[0]; + const canonicalModel = resolveInferredProviderModel(provider, modelId); + return { provider, model: canonicalModel, extendedContext }; + } + + if ( + activeProviders?.has("codex") && + providers.includes("codex") && + CODEX_PREFERRED_UNPREFIXED_MODELS.has(modelId) + ) { return { provider: "codex", - model: modelId, + model: resolveInferredProviderModel("codex", modelId), extendedContext, }; } @@ -305,24 +388,15 @@ async function resolveModelByProviderInference(modelId, extendedContext) { }; } - let activeProviders: Set | null = null; - try { - const { getProviderConnections } = await import("@/lib/localDb"); - const conns = await getProviderConnections(); - activeProviders = new Set(conns.filter((c: any) => c.is_active).map((c: any) => c.provider)); - } catch { - // DB unavailable - } - const eligibleProviders = activeProviders - ? nonOpenAIProviders.filter((p) => activeProviders!.has(p)) + ? nonOpenAIProviders.filter((p) => activeProviders.has(p)) : nonOpenAIProviders; const candidatesToUse = eligibleProviders.length > 0 ? eligibleProviders : nonOpenAIProviders; if (candidatesToUse.length === 1) { const provider = candidatesToUse[0]; - const canonicalModel = resolveProviderModelAlias(provider, modelId); + const canonicalModel = resolveInferredProviderModel(provider, modelId); return { provider, model: canonicalModel, extendedContext }; } diff --git a/tests/unit/chat-helpers.test.ts b/tests/unit/chat-helpers.test.ts index e0b2668423..9ac870459e 100644 --- a/tests/unit/chat-helpers.test.ts +++ b/tests/unit/chat-helpers.test.ts @@ -113,6 +113,35 @@ test("resolveModelOrError keeps non-Codex gpt-5.5 Responses requests on OpenAI", assert.equal(result.model, "gpt-5.5"); }); +test("resolveModelOrError routes bare gpt-5.5 to Codex medium when Codex is the only active account", async () => { + await seedConnection("codex"); + + const result = await resolveModelOrError( + "gpt-5.5", + { model: "gpt-5.5", input: "hello" }, + "/v1/responses", + { "user-agent": "OpenAI/Node" } + ); + + assert.equal(result.provider, "codex"); + assert.equal(result.model, "gpt-5.5-medium"); + assert.equal(result.targetFormat, "openai-responses"); +}); + +test("resolveModelOrError keeps bare gpt-5.5 on OpenAI when OpenAI is the only active account", async () => { + await seedConnection("openai"); + + const result = await resolveModelOrError( + "gpt-5.5", + { model: "gpt-5.5", input: "hello" }, + "/v1/responses", + { "user-agent": "OpenAI/Node" } + ); + + assert.equal(result.provider, "openai"); + assert.equal(result.model, "gpt-5.5"); +}); + test("checkPipelineGates blocks providers with an open circuit breaker", async () => { const breaker = getCircuitBreaker("openai"); breaker.state = STATE.OPEN;