-
Notifications
You must be signed in to change notification settings - Fork 1.1k
feat(router): opt-in blocked model redirection at shared routing layer #2854
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -33,6 +33,7 @@ import { | |
| } from "./providers/openai-tiers"; | ||
| import { decodeRoutedModelIdOrThrow, encodeRoutedModelId } from "./providers/slug-codec"; | ||
| import { resolveModelAlias } from "./providers/default-aliases"; | ||
| import { resolveBlockedModelRedirect } from "./lib/shadow-call"; | ||
| import { getStaleCached } from "./codex/model-cache"; | ||
| import { codexAccountNamespaceEntries } from "./codex/account-namespaces"; | ||
| import { | ||
|
|
@@ -506,19 +507,23 @@ function isBareOpenAiFamilyModel(modelId: string): boolean { | |
| } | ||
|
|
||
| function routeResult( | ||
| config: OcxConfig | undefined, | ||
| providerName: string, | ||
| provider: OcxProviderConfig, | ||
| modelId: string, | ||
| routeKind: RouteDecisionKind, | ||
| routeReason: string, | ||
| ): RouteResult { | ||
| const redirected = resolveBlockedModelRedirect(config, modelId); | ||
| const effectiveModelId = redirected ?? modelId; | ||
|
Comment on lines
+517
to
+518
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a redirect points to a model owned by another provider—for example, Useful? React with 👍 / 👎. |
||
| const effectiveRouteReason = redirected ? "blocked-model-redirect" : routeReason; | ||
|
Comment on lines
+517
to
+519
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a policy or combo selects a blocked model, this redirect runs in the recursive concrete route, but Useful? React with 👍 / 👎. |
||
| const codexAccountMode = providerCodexAccountMode(providerName, provider); | ||
| return { | ||
| providerName, | ||
| provider: routedProviderConfig(providerName, provider), | ||
| modelId, | ||
| modelId: effectiveModelId, | ||
| routeKind, | ||
| routeReason, | ||
| routeReason: effectiveRouteReason, | ||
| ...(codexAccountMode ? { codexAccountMode } : {}), | ||
| }; | ||
| } | ||
|
|
@@ -621,7 +626,7 @@ function routeModelInternal( | |
| throw new NoEnabledOpenAiProviderError(nativeModelId); | ||
| } | ||
| return { | ||
| ...routeResult(OPENAI_CODEX_PROVIDER_ID, provider, nativeModelId, "explicit-account", "account-namespace"), | ||
| ...routeResult(config, OPENAI_CODEX_PROVIDER_ID, provider, nativeModelId, "explicit-account", "account-namespace"), | ||
| // Exact account injection uses the pool credential machinery even when the canonical | ||
| // provider is globally Direct. The fixed id bypasses pool selection entirely. | ||
| codexAccountMode: "pool", | ||
|
|
@@ -666,7 +671,7 @@ function routeModelInternal( | |
| // itself a known model (e.g. orcarouter/auto). Route it whole instead of stripping to the | ||
| // remainder, which would send a bare `auto` the upstream cannot resolve. | ||
| if (known.includes(modelId)) { | ||
| return routeResult(provName, prov, modelId, "explicit-provider", "explicit-provider-namespace"); | ||
| return routeResult(config, provName, prov, modelId, "explicit-provider", "explicit-provider-namespace"); | ||
| } | ||
| // Codex-facing alias ids (`provider/vendor-model`) decode back to the native | ||
| // slash id via an exact known-id lookup; raw full-slash selectors keep working. | ||
|
|
@@ -676,6 +681,7 @@ function routeModelInternal( | |
| ? decoded | ||
| : resolveModelAlias(config, prov, known, requestedModel) ?? decoded; | ||
| return routeResult( | ||
| config, | ||
| provName, | ||
| prov, | ||
| nativeModel, | ||
|
|
@@ -689,15 +695,15 @@ function routeModelInternal( | |
| if (isBareOpenAiFamilyModel(modelId)) { | ||
| const provider = config.providers[OPENAI_CODEX_PROVIDER_ID]; | ||
| if (provider && provider.disabled !== true) { | ||
| return routeResult(OPENAI_CODEX_PROVIDER_ID, provider, modelId, "native", "native-family"); | ||
| return routeResult(config, OPENAI_CODEX_PROVIDER_ID, provider, modelId, "native", "native-family"); | ||
| } | ||
| throw new NoEnabledOpenAiProviderError(modelId); | ||
| } | ||
|
|
||
| for (const [provName, prov] of activeProviderEntries(config)) { | ||
| if (prov.defaultModel === modelId | ||
| || (typeof prov.defaultModel === "string" && encodeRoutedModelId(prov.defaultModel) === modelId)) { | ||
| return routeResult(provName, prov, prov.defaultModel as string, "explicit-provider", "configured-default-model"); | ||
| return routeResult(config, provName, prov, prov.defaultModel as string, "explicit-provider", "configured-default-model"); | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -708,7 +714,7 @@ function routeModelInternal( | |
| if (prov.models && Array.isArray(prov.models)) { | ||
| const hit = (prov.models as string[]).find(id => id === modelId || encodeRoutedModelId(id) === modelId); | ||
| if (hit !== undefined) { | ||
| return routeResult(provName, prov, hit, "explicit-provider", "configured-model-list"); | ||
| return routeResult(config, provName, prov, hit, "explicit-provider", "configured-model-list"); | ||
| } | ||
| } | ||
| } | ||
|
|
@@ -728,7 +734,7 @@ function routeModelInternal( | |
| } | ||
| if (aliasMatches[0]) { | ||
| const match = aliasMatches[0]; | ||
| return routeResult(match.provider, config.providers[match.provider], match.model, "explicit-provider", "model-alias"); | ||
| return routeResult(config, match.provider, config.providers[match.provider], match.model, "explicit-provider", "model-alias"); | ||
| } | ||
|
|
||
| if (config.defaultProvider === LEGACY_CHATGPT_PROVIDER_ID) { | ||
|
|
@@ -737,7 +743,7 @@ function routeModelInternal( | |
| if (hasOwnProvider(config.providers, config.defaultProvider)) { | ||
| const defaultProv = config.providers[config.defaultProvider]; | ||
| if (defaultProv.disabled === true) throw new Error(`Default provider is disabled: ${config.defaultProvider}`); | ||
| return routeResult(config.defaultProvider, defaultProv, modelId, "default-provider", "default-provider"); | ||
| return routeResult(config, config.defaultProvider, defaultProv, modelId, "default-provider", "default-provider"); | ||
| } | ||
|
|
||
| throw new Error(`No provider configured for model: ${modelId}`); | ||
|
|
@@ -786,7 +792,7 @@ function routeByKnownModelPattern(config: OcxConfig, modelId: string): RouteResu | |
| ); | ||
| if (matchingProvider) { | ||
| const [provName, prov] = matchingProvider; | ||
| return routeResult(provName, prov, modelId, "explicit-provider", "model-pattern"); | ||
| return routeResult(config, provName, prov, modelId, "explicit-provider", "model-pattern"); | ||
| } | ||
| // Deliberately no "first provider with an Anthropic adapter" fallback here. Picking by | ||
| // object insertion order, without checking `models`, `selectedModels`, `disabledModels` or | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -436,17 +436,25 @@ export interface OcxConfig { | |
| * commit messages, skill orchestration) to a user-chosen model. Default intercepted | ||
| * source models: gpt-5.4-mini (older clients) and gpt-5.6-luna (Codex 0.145.0+). | ||
| * Opt-in; disabled by default. Matching requests preserve their configured reasoning effort. | ||
| * All requests for configured shadow source models are intercepted regardless of request kind, | ||
| * except when the replacement intersects the same provider+model source set. | ||
| */ | ||
| shadowCallIntercept?: { | ||
| /** When true, requests for known shadow/helper source models are rewritten to the configured model. */ | ||
| enabled?: boolean; | ||
| /** Replacement model id (e.g. "gpt-5.5"). */ | ||
| model?: string; | ||
| /** Optional override of intercepted source-model prefixes (default: gpt-5.4-mini, gpt-5.6-luna). */ | ||
| sourceModels?: string[]; | ||
| }; | ||
| * All requests for configured shadow source models are intercepted regardless of request kind, | ||
| * except when the replacement intersects the same provider+model source set. | ||
| */ | ||
| shadowCallIntercept?: { | ||
| /** When true, requests for known shadow/helper source models are rewritten to the configured model. */ | ||
| enabled?: boolean; | ||
| /** Replacement model id (e.g. "gpt-5.5"). */ | ||
| model?: string; | ||
| /** Optional override of intercepted source-model prefixes (default: gpt-5.4-mini, gpt-5.6-luna). */ | ||
| sourceModels?: string[]; | ||
| }; | ||
| /** | ||
| * Optional map of blocked model IDs to their replacement model IDs. | ||
| * When configured, incoming requests targeting a blocked model (including | ||
| * account-namespaced and concrete routes) are redirected to the replacement | ||
| * model at the shared routing layer with routeReason "blocked-model-redirect". | ||
| * Unset or omitted by default. | ||
| */ | ||
| blockedModelRedirects?: Record<string, string>; | ||
|
Comment on lines
+450
to
+457
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This adds a user-facing, opt-in configuration key, but the commit contains no AGENTS.md reference: AGENTS.md:L340-L341 Useful? React with 👍 / 👎. |
||
| /** | ||
| * 3-state multi-agent surface override: | ||
| * - "v1": force ALL models to v1 surface (override upstream pins) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For a hand-edited mapping such as
{ "gpt-5.6-terra": "" }or a whitespace-only target, this schema accepts the value;routeResultthen produces an empty/invalid upstream model ID, and an empty value also retains the original route reason because it is falsy. Validate both keys and values with trimming and a non-empty constraint so malformed optional configuration degrades safely rather than breaking matching requests.Useful? React with 👍 / 👎.