diff --git a/open-sse/config/providerRegistry.ts b/open-sse/config/providerRegistry.ts index 02a19bfc1e..9f56748fd7 100644 --- a/open-sse/config/providerRegistry.ts +++ b/open-sse/config/providerRegistry.ts @@ -12,8 +12,21 @@ export interface RegistryModel { id: string; name: string; targetFormat?: string; + unsupportedParams?: readonly string[]; } +// Reasoning models reject temperature, top_p, penalties, logprobs, n. +// Frozen to prevent accidental mutation (shared across all model entries). +const REASONING_UNSUPPORTED: readonly string[] = Object.freeze([ + "temperature", + "top_p", + "frequency_penalty", + "presence_penalty", + "logprobs", + "top_logprobs", + "n", +]); + export interface RegistryOAuth { clientIdEnv?: string; clientIdDefault?: string; @@ -426,8 +439,11 @@ export const REGISTRY: Record = { { id: "gpt-4o", name: "GPT-4o" }, { id: "gpt-4o-mini", name: "GPT-4o Mini" }, { id: "gpt-4-turbo", name: "GPT-4 Turbo" }, - { id: "o1", name: "O1" }, - { id: "o1-mini", name: "O1 Mini" }, + { id: "o1", name: "O1", unsupportedParams: REASONING_UNSUPPORTED }, + { id: "o1-mini", name: "O1 Mini", unsupportedParams: REASONING_UNSUPPORTED }, + { id: "o1-pro", name: "O1 Pro", unsupportedParams: REASONING_UNSUPPORTED }, + { id: "o3", name: "O3", unsupportedParams: REASONING_UNSUPPORTED }, + { id: "o3-mini", name: "O3 Mini", unsupportedParams: REASONING_UNSUPPORTED }, ], }, @@ -1060,6 +1076,43 @@ export function getRegisteredProviders(): string[] { return Object.keys(REGISTRY); } +// Precomputed map: modelId → unsupportedParams (O(1) lookup instead of O(N×M) scan). +// Built once at module load from all registry entries. +const _unsupportedParamsMap = new Map(); +for (const entry of Object.values(REGISTRY)) { + for (const model of entry.models) { + if (model.unsupportedParams && !_unsupportedParamsMap.has(model.id)) { + _unsupportedParamsMap.set(model.id, model.unsupportedParams); + } + } +} + +/** + * Get unsupported parameters for a specific model. + * Uses O(1) precomputed lookup. Also handles prefixed model IDs + * (e.g., "openai/o3" → strips prefix and looks up "o3"). + * Returns empty array if no restrictions are defined. + */ +export function getUnsupportedParams(provider: string, modelId: string): readonly string[] { + // 1. Check current provider's registry (exact match) + const entry = getRegistryEntry(provider); + const modelEntry = entry?.models.find((m) => m.id === modelId); + if (modelEntry?.unsupportedParams) return modelEntry.unsupportedParams; + + // 2. O(1) lookup in precomputed map (handles cross-provider routing) + const cached = _unsupportedParamsMap.get(modelId); + if (cached) return cached; + + // 3. Handle prefixed model IDs (e.g., "openai/o3" → "o3") + if (modelId.includes("/")) { + const bareId = modelId.split("/").pop() || ""; + const bare = _unsupportedParamsMap.get(bareId); + if (bare) return bare; + } + + return []; +} + /** * Get provider category: "oauth" or "apikey" * Used by the resilience layer to apply different cooldown/backoff profiles. diff --git a/open-sse/handlers/chatCore.ts b/open-sse/handlers/chatCore.ts index 1919add060..820692030a 100644 --- a/open-sse/handlers/chatCore.ts +++ b/open-sse/handlers/chatCore.ts @@ -13,6 +13,7 @@ import { refreshWithRetry } from "../services/tokenRefresh.ts"; import { createRequestLogger } from "../utils/requestLogger.ts"; import { getModelTargetFormat, PROVIDER_ID_TO_ALIAS } from "../config/providerModels.ts"; import { resolveModelAlias } from "../services/modelDeprecation.ts"; +import { getUnsupportedParams } from "../config/providerRegistry.ts"; import { createErrorResult, parseUpstreamError, formatProviderError } from "../utils/error.ts"; import { HTTP_STATUS } from "../config/constants.ts"; import { handleBypassRequest } from "../utils/bypassHandler.ts"; @@ -53,7 +54,9 @@ export function shouldUseNativeCodexPassthrough({ }): boolean { if (provider !== "codex") return false; if (sourceFormat !== FORMATS.OPENAI_RESPONSES) return false; - return String(endpointPath || "").toLowerCase().endsWith("/responses"); + return String(endpointPath || "") + .toLowerCase() + .endsWith("/responses"); } /** @@ -287,6 +290,21 @@ export async function handleChatCore({ // Update model in body translatedBody.model = model; + // Strip unsupported parameters for reasoning models (o1, o3, etc.) + const unsupported = getUnsupportedParams(provider, model); + if (unsupported.length > 0) { + const stripped: string[] = []; + for (const param of unsupported) { + if (Object.hasOwn(translatedBody, param)) { + stripped.push(param); + delete translatedBody[param]; + } + } + if (stripped.length > 0) { + log?.warn?.("PARAMS", `Stripped unsupported params for ${model}: ${stripped.join(", ")}`); + } + } + // Get executor for this provider const executor = getExecutor(provider);