-
Notifications
You must be signed in to change notification settings - Fork 909
feat(providers): add Responses terminal repair escape hatch for custom providers #2362
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
base: dev
Are you sure you want to change the base?
Changes from all commits
55679ce
ab2cf6c
49dee2d
218719c
65f9791
1a0a10b
6154677
f9d7bc2
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 |
|---|---|---|
|
|
@@ -100,6 +100,9 @@ differing backup and rewrites known legacy namespaced selected ids to bare ids. | |
| | `modelSupportsReasoningSummaries?` | `Record<string, boolean>` | Set a model to `false` to stop advertising summaries and strip summary-delivery fields. | | ||
| | `modelReasoningSummaryDelivery?` | `Record<string, "sequential" \| "sequential_cutoff" \| "concurrent" \| "concurrent_cutoff">` | Per-model Responses delivery enum; rewrites an existing delivery field. | | ||
| | `modelAdapters?` | `Record<string, string>` | Per-model `openai-chat` or `openai-responses` wire override for mixed-wire gateways. Explicit entries beat registry defaults. The OpenCode Go preset selects Responses for `gpt-5.6-luna` while leaving sibling models on their documented wires; DeepSeek can select native Responses for `deepseek-v4-flash`; and GitHub Copilot declares Responses-only defaults for its GPT-5 family (`gpt-5.3-codex`, `gpt-5.4`, `gpt-5.4-mini`, `gpt-5.5`, `gpt-5.6-luna`, `gpt-5.6-sol`, `gpt-5.6-terra`) because those models reject `/chat/completions` for agent traffic. Models without a built-in default (for example `gpt-5.4-nano`) can be opted in here. Single-wire upstream pins and canonical ChatGPT forward reject overrides. | | ||
| | `modelResponsesCompatibility?` | `Record<string, "terminal-repair">` | Case-insensitive per-model opt-in for the Responses terminal-repair policy on custom providers. A matching model gets the 500 ms default grace unless `modelResponsesTerminalRepair` supplies an explicit grace. The effective model wire must be `openai-responses`; canonical ChatGPT forward rejects this key. | | ||
| | `modelResponsesTerminalRepair?` | `Record<string, number \| { graceMs: number }>` | Case-insensitive per-model terminal-repair grace in milliseconds. It overrides the compatibility default and takes precedence over `responsesTerminalRepair`; positive values are floored and capped at 60 seconds. Invalid or ambiguous case-folded entries fail closed at resolution. | | ||
| | `responsesTerminalRepair?` | `"terminal-repair" \| number \| { graceMs: number }` | Provider-level terminal-repair fallback for models using the `openai-responses` wire. The string selects the 500 ms default; numeric/object values set the grace and are capped at 60 seconds. It is considered only after compatibility and explicit per-model settings, and is rejected on the canonical ChatGPT forward provider. | | ||
| | xAI Responses opt-in (dashboard) | switch | For `xai` only, atomically sets or clears the `grok-4.5` and `grok-4.6` `modelAdapters` entries. A hand-edited single entry appears as mixed until the next switch write normalizes both. Other overrides and tier behavior are unchanged. | | ||
| | `modelPreferHostedTools?` | `Record<string,string[]>` | Exact-model opt-in for non-forward Responses gateways that reserve a hosted-tool namespace. Currently accepts only `["image_generation"]`; a matching model must use the `openai-responses` wire and support that hosted tool. It removes colliding client `image_gen` declarations and rewrites their selectors to preserve caller tool choice. For OpenAI API virtual `-pro` models, the selected public ID is matched first and the resolved base wire-model ID is a fallback. `modelAdapters` resolves the public ID first, then the base ID; the second resolution determines the final wire. Other models retain normal alias behavior. | | ||
| | `reasoningEffortMap?` | `Record<string, string>` | Provider-wide wire aliases for reasoning labels. | | ||
|
|
@@ -132,6 +135,36 @@ differing backup and rewrites known legacy namespaced selected ids to bare ids. | |
| | `unsafeAllowNativeLocalExec?` | `boolean` | Cursor legacy boolean, equivalent to `nativeLocalExec: "on"` only when the newer field is unset. | | ||
| | `nativeLocalExec?` | `"off" \| "codex-sandbox" \| "on"` | Cursor local-exec policy. `off` is default; `codex-sandbox` currently fails closed like `off`. | | ||
|
|
||
| ### Responses terminal-repair policy | ||
|
|
||
| These three keys are overlapping controls for custom providers that need a bounded repair when a | ||
| native Responses stream does not deliver its terminal event. For each requested model, the | ||
| effective adapter (the provider adapter or its `modelAdapters` override) must be | ||
| `openai-responses`; Chat Completions and other wires never opt in. Model matching is | ||
| case-insensitive. | ||
|
|
||
| Resolution uses this precedence: | ||
|
|
||
| 1. A matching `modelResponsesCompatibility` entry opts the model into terminal repair. Its | ||
| default grace is 500 ms, unless a matching `modelResponsesTerminalRepair` entry supplies an | ||
| explicit grace. | ||
| 2. Otherwise, a matching `modelResponsesTerminalRepair` entry supplies the per-model grace. | ||
| 3. Otherwise, `responsesTerminalRepair` supplies the provider-level fallback. | ||
|
|
||
| Grace values are positive finite milliseconds, and the runtime floors them and caps any result at | ||
| 60 seconds. Config validation rejects malformed values and rejects all three keys on the canonical | ||
| ChatGPT forward provider (matching by adapter, authMode, and normalized baseUrl configuration, not provider name). The runtime resolver is defense in depth: an invalid or ambiguous | ||
| case-folded per-model entry is not selected, so resolution fails closed instead of choosing an | ||
| arbitrary entry. | ||
|
|
||
| #### Decision Log: why three overlapping knobs? | ||
|
|
||
| `modelResponsesCompatibility` provides a readable opt-in with a safe default, while | ||
| `modelResponsesTerminalRepair` handles models that need a different grace period. The | ||
| provider-level `responsesTerminalRepair` covers a gateway whose Responses models share one policy. | ||
| Keeping all three preserves simple compatibility migration without giving a broad default priority | ||
| over an explicit per-model choice. | ||
|
|
||
|
Comment on lines
+138
to
+167
Contributor
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. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: #!/bin/bash
set -euo pipefail
fd -a 'providers\.md$' docs-site/src/content/docs | sort
rg -n -i -C 2 'terminal.?repair|Responses' docs-site/src/content/docsRepository: lidge-jun/opencodex Length of output: 50378 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- provider-reference files ---'
fd -a -i 'providers\.md$' docs-site/src/content/docs | sort
printf '%s\n' '--- terminal-repair matches in target locale provider pages ---'
for locale in ja ko ru zh-cn; do
file=$(fd -a -i 'providers\.md$' "docs-site/src/content/docs/$locale" | head -n 1 || true)
if [ -n "$file" ]; then
echo "### $file"
rg -n -i -C 4 'terminal.?repair|responsescompatibility|responsesTerminalRepair|modelResponses' "$file" || true
else
echo "### no provider page for $locale"
fi
done
printf '%s\n' '--- English policy context ---'
sed -n '130,175p' docs-site/src/content/docs/reference/configuration/providers.mdRepository: lidge-jun/opencodex Length of output: 5135 🏁 Script executed: #!/bin/bash
set -euo pipefail
for file in \
docs-site/src/content/docs/ja/reference/configuration/providers.md \
docs-site/src/content/docs/ko/reference/configuration/providers.md \
docs-site/src/content/docs/ru/reference/configuration/providers.md \
docs-site/src/content/docs/zh-cn/reference/configuration/providers.md; do
echo "### $file"
wc -l "$file"
rg -n -i -C 3 'terminal.?repair|responsescompatibility|responsesTerminalRepair|modelResponses|modelAdapters' "$file" || true
doneRepository: lidge-jun/opencodex Length of output: 9021 🏁 Script executed: #!/bin/bash
set -euo pipefail
for file in \
docs-site/src/content/docs/ja/reference/configuration/providers.md \
docs-site/src/content/docs/ko/reference/configuration/providers.md \
docs-site/src/content/docs/ru/reference/configuration/providers.md \
docs-site/src/content/docs/zh-cn/reference/configuration/providers.md; do
echo "### $file"
sed -n '120,190p' "$file"
doneRepository: lidge-jun/opencodex Length of output: 19955 Add the terminal-repair policy to the localized provider-reference pages.
🤖 Prompt for AI AgentsSource: Path instructions |
||
| ### FastWire B1 capability migration | ||
|
|
||
| Fast capability and arbitrary Chat caller-tier forwarding are independent after FastWire B1. The | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -72,7 +72,7 @@ import { | |
| type FastWire, | ||
| type ProviderCostOverlay, | ||
| } from "./types"; | ||
| import { OPENAI_CODEX_PROVIDER_ID } from "./providers/openai-tiers"; | ||
| import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "./providers/openai-tiers"; | ||
| import { modelAutoCompactTokenLimitsConfigError } from "./providers/auto-compact-budget"; | ||
| import { fastWireDeclarationError, hasFastWireCapabilityConflict } from "./providers/fastwire"; | ||
| import { | ||
|
|
@@ -709,6 +709,92 @@ export function modelPreferHostedToolsConfigError( | |
| return null; | ||
| } | ||
|
|
||
| /** | ||
| * Validate a provider's per-model wire override map (#404). | ||
| * | ||
| * Rejects, rather than silently ignoring, configurations the resolver would refuse: | ||
| * a value outside the allowed wires, a model the upstream pins to one wire, and any | ||
| * override on a canonical forward provider (where switching wires would drop the | ||
| * caller's forwarded credential). Silently dropping them would leave the user | ||
| * believing an override is in effect. | ||
| */ | ||
| export function modelResponsesCompatibilityConfigError( | ||
| value: unknown, | ||
| field = "modelResponsesCompatibility", | ||
| providerName?: string, | ||
| provider?: { adapter?: unknown; authMode?: unknown; baseUrl?: unknown }, | ||
| ): string | null { | ||
| if (value === undefined) return null; | ||
| if (!value || typeof value !== "object" || Array.isArray(value)) return `${field} must be a plain object`; | ||
| const prototype = Object.getPrototypeOf(value); | ||
| if (prototype !== Object.prototype && prototype !== null) return `${field} must be a plain object with own properties`; | ||
| const entries = Object.entries(value); | ||
| if (entries.length > 0 && provider && isCanonicalOpenAiForwardProvider(provider as OcxProviderConfig)) { | ||
| return `${field} is not supported on the canonical ChatGPT forward provider`; | ||
| } | ||
| const seen = new Set<string>(); | ||
| for (const [key, entry] of entries) { | ||
| const lower = key.toLowerCase(); | ||
| if (seen.has(lower)) { | ||
| return `${field} contains duplicate case-insensitive model id "${key}"`; | ||
| } | ||
| seen.add(lower); | ||
| if (!key.trim() || key !== key.trim()) return `${field} keys must be nonblank trimmed model ids`; | ||
| if (entry !== "terminal-repair") { | ||
| return `${field}.${key} must be "terminal-repair"`; | ||
| } | ||
| } | ||
|
Comment on lines
+736
to
+746
Contributor
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. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Reject duplicate case-folded model keys during validation. Lines 735-740 and Lines 758-764 accept both Track normalized keys in both validators and reject duplicates before persistence. Add candidate-validation and management-path tests for both maps. Also applies to: 758-764 🤖 Prompt for AI Agents |
||
| return null; | ||
| } | ||
|
|
||
| export function modelResponsesTerminalRepairConfigError( | ||
| value: unknown, | ||
| field = "modelResponsesTerminalRepair", | ||
| providerName?: string, | ||
| provider?: { adapter?: unknown; authMode?: unknown; baseUrl?: unknown }, | ||
| ): string | null { | ||
| if (value === undefined) return null; | ||
| if (!value || typeof value !== "object" || Array.isArray(value)) return `${field} must be a plain object`; | ||
| const prototype = Object.getPrototypeOf(value); | ||
| if (prototype !== Object.prototype && prototype !== null) return `${field} must be a plain object with own properties`; | ||
| const entries = Object.entries(value); | ||
| if (entries.length > 0 && provider && isCanonicalOpenAiForwardProvider(provider as OcxProviderConfig)) { | ||
| return `${field} is not supported on the canonical ChatGPT forward provider`; | ||
| } | ||
| const seen = new Set<string>(); | ||
| for (const [key, entry] of entries) { | ||
| const lower = key.toLowerCase(); | ||
| if (seen.has(lower)) { | ||
| return `${field} contains duplicate case-insensitive model id "${key}"`; | ||
| } | ||
| seen.add(lower); | ||
| if (!key.trim() || key !== key.trim()) return `${field} keys must be nonblank trimmed model ids`; | ||
| const grace = typeof entry === "number" ? entry : (typeof entry === "object" && entry ? (entry as { graceMs?: unknown }).graceMs : null); | ||
| if (typeof grace !== "number" || !Number.isFinite(grace) || Math.floor(grace) <= 0) { | ||
| return `${field}.${key} must be a positive number of milliseconds or { graceMs: number }`; | ||
| } | ||
|
Comment on lines
+772
to
+775
Contributor
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. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Reject grace values that floor to zero. Lines 760-763 and Lines 779-782 accept a positive fractional value such as Require Also applies to: 779-782 🤖 Prompt for AI Agents |
||
| } | ||
| return null; | ||
| } | ||
|
|
||
| export function responsesTerminalRepairConfigError( | ||
| value: unknown, | ||
| field = "responsesTerminalRepair", | ||
| providerName?: string, | ||
| provider?: { adapter?: unknown; authMode?: unknown; baseUrl?: unknown }, | ||
| ): string | null { | ||
| if (value === undefined) return null; | ||
| if (provider && isCanonicalOpenAiForwardProvider(provider as OcxProviderConfig)) { | ||
| return `${field} is not supported on the canonical ChatGPT forward provider`; | ||
| } | ||
| if (value === "terminal-repair") return null; | ||
| const grace = typeof value === "number" ? value : (typeof value === "object" && value ? (value as { graceMs?: unknown }).graceMs : null); | ||
| if (typeof grace !== "number" || !Number.isFinite(grace) || Math.floor(grace) <= 0) { | ||
| return `${field} must be "terminal-repair", a positive number of milliseconds, or { graceMs: number }`; | ||
| } | ||
| return null; | ||
| } | ||
|
|
||
| const CODEX_ACCOUNT_NAMESPACES_RECORD_ERROR = | ||
| "codexAccountNamespaces must be a plain object mapping account selectors to Codex account ids"; | ||
| const CODEX_ACCOUNT_NAMESPACE_KEY_ERROR = | ||
|
|
@@ -1081,6 +1167,45 @@ const configSchema = z.object({ | |
| message: modelAdaptersError, | ||
| }); | ||
| } | ||
| const compatError = modelResponsesCompatibilityConfigError( | ||
| (provider as { modelResponsesCompatibility?: unknown }).modelResponsesCompatibility, | ||
| "modelResponsesCompatibility", | ||
| name, | ||
| provider, | ||
| ); | ||
| if (compatError) { | ||
| ctx.addIssue({ | ||
| code: "custom", | ||
| path: ["providers", redactSecretString(name), "modelResponsesCompatibility"], | ||
| message: compatError, | ||
| }); | ||
| } | ||
| const modelRepairError = modelResponsesTerminalRepairConfigError( | ||
| (provider as { modelResponsesTerminalRepair?: unknown }).modelResponsesTerminalRepair, | ||
| "modelResponsesTerminalRepair", | ||
| name, | ||
| provider, | ||
| ); | ||
| if (modelRepairError) { | ||
| ctx.addIssue({ | ||
| code: "custom", | ||
| path: ["providers", redactSecretString(name), "modelResponsesTerminalRepair"], | ||
| message: modelRepairError, | ||
| }); | ||
| } | ||
| const repairError = responsesTerminalRepairConfigError( | ||
| (provider as { responsesTerminalRepair?: unknown }).responsesTerminalRepair, | ||
| "responsesTerminalRepair", | ||
| name, | ||
| provider, | ||
| ); | ||
| if (repairError) { | ||
| ctx.addIssue({ | ||
| code: "custom", | ||
| path: ["providers", redactSecretString(name), "responsesTerminalRepair"], | ||
| message: repairError, | ||
| }); | ||
| } | ||
| const preferHostedToolsError = modelPreferHostedToolsConfigError( | ||
| (provider as { modelPreferHostedTools?: unknown }).modelPreferHostedTools, | ||
| "modelPreferHostedTools", | ||
|
|
||
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.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Document configuration-based canonical-forward exclusion.
The runtime excludes a provider when its
adapter,authMode, and normalizedbaseUrlidentify the canonical ChatGPT forward route. It does not use the provider name. State this rule so a renamed canonical provider is not incorrectly documented as eligible for terminal repair.As per path instructions: canonical OpenAI/ChatGPT forward providers must be excluded “matching by provider configuration rather than provider-name heuristics.”
🤖 Prompt for AI Agents
Source: Path instructions