From ac7dfcb8f7540c96cea9d6d3eb3f7164a7d6b150 Mon Sep 17 00:00:00 2001 From: chilung Date: Sat, 22 Aug 2026 08:34:00 +0000 Subject: [PATCH 1/5] feat(providers): add Responses terminal repair escape hatch for custom providers (closes #1809) --- src/providers/registry.ts | 43 +++++++++++++- src/types/provider.ts | 14 +++++ tests/deepseek-inbound-wire.test.ts | 87 +++++++++++++++++++++++++++++ 3 files changed, 141 insertions(+), 3 deletions(-) diff --git a/src/providers/registry.ts b/src/providers/registry.ts index 9fda85ba63..c721564729 100644 --- a/src/providers/registry.ts +++ b/src/providers/registry.ts @@ -2877,15 +2877,52 @@ export function providerModelResponsesUpstreamStreaming( return entry.modelResponsesUpstreamStreaming[modelId.trim().toLowerCase()]; } -/** Resolve a registry-only terminal-repair policy for native Responses streams. */ +/** + * Resolve terminal-repair policy for native Responses streams (supports registry presets + * and custom-provider configuration overrides, issue #1809). + */ export function providerModelResponsesTerminalRepair( id: string, - provider: Pick & Partial>, + provider: Pick & Partial>, modelId: string, ): ResponsesTerminalRepairPolicy | undefined { + const modelKey = modelId.trim().toLowerCase(); + const effectiveAdapter = provider.modelAdapters?.[modelId] ?? provider.modelAdapters?.[modelKey] ?? provider.adapter; + + // Custom provider opt-in: effective wire must be openai-responses + if (effectiveAdapter === "openai-responses") { + // 1. Check explicit modelResponsesCompatibility + const compat = provider.modelResponsesCompatibility?.[modelId] ?? provider.modelResponsesCompatibility?.[modelKey]; + if (compat === "terminal-repair") { + const raw = provider.modelResponsesTerminalRepair?.[modelId] ?? provider.modelResponsesTerminalRepair?.[modelKey]; + const grace = typeof raw === "number" ? raw : (typeof raw === "object" && raw ? raw.graceMs : 500); + const graceMs = Math.floor(grace ?? 500); + return { graceMs: Number.isFinite(graceMs) && graceMs > 0 ? graceMs : 500 }; + } + + // 2. Check explicit modelResponsesTerminalRepair + const rawModel = provider.modelResponsesTerminalRepair?.[modelId] ?? provider.modelResponsesTerminalRepair?.[modelKey]; + if (rawModel !== undefined) { + const grace = typeof rawModel === "number" ? rawModel : (typeof rawModel === "object" && rawModel ? rawModel.graceMs : undefined); + const graceMs = Math.floor(grace ?? 0); + if (Number.isFinite(graceMs) && graceMs > 0) return { graceMs }; + } + + // 3. Check provider-level responsesTerminalRepair + if (provider.responsesTerminalRepair !== undefined) { + if (provider.responsesTerminalRepair === "terminal-repair") return { graceMs: 500 }; + const grace = typeof provider.responsesTerminalRepair === "number" + ? provider.responsesTerminalRepair + : (typeof provider.responsesTerminalRepair === "object" && provider.responsesTerminalRepair ? provider.responsesTerminalRepair.graceMs : undefined); + const graceMs = Math.floor(grace ?? 0); + if (Number.isFinite(graceMs) && graceMs > 0) return { graceMs }; + } + } + + // Fall back to registry-defined policy const entry = getProviderRegistryEntry(id); if (!entry?.modelResponsesTerminalRepair || !providerMatchesRegistryTransport(id, provider)) return undefined; - const policy = entry.modelResponsesTerminalRepair[modelId.trim().toLowerCase()]; + const policy = entry.modelResponsesTerminalRepair[modelKey]; const graceMs = Math.floor(policy?.graceMs ?? 0); if (!Number.isFinite(graceMs) || graceMs <= 0) return undefined; return { graceMs }; diff --git a/src/types/provider.ts b/src/types/provider.ts index 3dfca58ddc..9edb67f25e 100644 --- a/src/types/provider.ts +++ b/src/types/provider.ts @@ -204,6 +204,20 @@ export interface OcxProviderConfig { * `ocxr1` envelopes are still stripped because no upstream can decrypt them. */ preserveResponsesReasoningContent?: boolean; + /** + * Optional per-model Responses compatibility escape hatch for custom providers (issue #1809). + * Keyed by the upstream model id (case-insensitive). + * "terminal-repair" opts into the bounded terminal repair state machine (default 500ms grace). + */ + modelResponsesCompatibility?: Record; + /** + * Explicit per-model terminal-repair grace period for native Responses streams (in ms). + */ + modelResponsesTerminalRepair?: Record; + /** + * Provider-level default terminal-repair grace period for native Responses streams. + */ + responsesTerminalRepair?: { graceMs: number } | number | "terminal-repair"; /** * Explicit opt-in for a relay that genuinely fronts OpenAI and can decode native * compaction blobs. Absent or false degrades foreign blobs to an opaque note. diff --git a/tests/deepseek-inbound-wire.test.ts b/tests/deepseek-inbound-wire.test.ts index 1f298bacce..ef6a568266 100644 --- a/tests/deepseek-inbound-wire.test.ts +++ b/tests/deepseek-inbound-wire.test.ts @@ -1014,4 +1014,91 @@ describe("stateless Responses upstreams get no stateful parameters", () => { expect(input.some(item => item.call_id === "call_orphan")).toBe(false); expect(input.some(item => item.type === "message")).toBe(true); }); + + describe("Custom provider Responses terminal repair escape hatch (#1809)", () => { + test("custom provider opts into default 500ms terminal repair via modelResponsesCompatibility", () => { + const customProv = { + adapter: "openai-responses", + baseUrl: "https://custom-gateway.test/v1", + modelResponsesCompatibility: { + "my-model": "terminal-repair" as const, + }, + }; + expect(providerModelResponsesTerminalRepair("custom-gateway", customProv, "my-model")).toEqual({ graceMs: 500 }); + expect(providerModelResponsesTerminalRepair("custom-gateway", customProv, "MY-MODEL")).toEqual({ graceMs: 500 }); + expect(providerModelResponsesTerminalRepair("custom-gateway", customProv, "other-model")).toBeUndefined(); + }); + + test("custom provider specifies explicit graceMs via modelResponsesTerminalRepair", () => { + const customProv = { + adapter: "openai-responses", + baseUrl: "https://custom-gateway.test/v1", + modelResponsesTerminalRepair: { + "model-num": 1500, + "model-obj": { graceMs: 2000 }, + }, + }; + expect(providerModelResponsesTerminalRepair("custom-gateway", customProv, "model-num")).toEqual({ graceMs: 1500 }); + expect(providerModelResponsesTerminalRepair("custom-gateway", customProv, "model-obj")).toEqual({ graceMs: 2000 }); + expect(providerModelResponsesTerminalRepair("custom-gateway", customProv, "unconfigured")).toBeUndefined(); + }); + + test("custom provider specifies provider-level responsesTerminalRepair", () => { + const customProvString = { + adapter: "openai-responses", + baseUrl: "https://custom-gateway.test/v1", + responsesTerminalRepair: "terminal-repair" as const, + }; + expect(providerModelResponsesTerminalRepair("custom-gateway", customProvString, "any-model")).toEqual({ graceMs: 500 }); + + const customProvNumber = { + adapter: "openai-responses", + baseUrl: "https://custom-gateway.test/v1", + responsesTerminalRepair: 750, + }; + expect(providerModelResponsesTerminalRepair("custom-gateway", customProvNumber, "any-model")).toEqual({ graceMs: 750 }); + }); + + test("rejects repair for non-responses wires even when compatibility is set", () => { + const chatProv = { + adapter: "openai-chat", + baseUrl: "https://custom-gateway.test/v1", + modelResponsesCompatibility: { + "my-model": "terminal-repair" as const, + }, + }; + expect(providerModelResponsesTerminalRepair("custom-gateway", chatProv, "my-model")).toBeUndefined(); + }); + + test("respects per-model modelAdapters overrides", () => { + const hybridProv = { + adapter: "openai-chat", + baseUrl: "https://custom-gateway.test/v1", + modelAdapters: { + "responses-model": "openai-responses", + }, + modelResponsesCompatibility: { + "responses-model": "terminal-repair" as const, + "chat-model": "terminal-repair" as const, + }, + }; + expect(providerModelResponsesTerminalRepair("custom-gateway", hybridProv, "responses-model")).toEqual({ graceMs: 500 }); + expect(providerModelResponsesTerminalRepair("custom-gateway", hybridProv, "chat-model")).toBeUndefined(); + }); + + test("fails closed on non-positive or invalid grace values", () => { + const invalidProv = { + adapter: "openai-responses", + baseUrl: "https://custom-gateway.test/v1", + modelResponsesTerminalRepair: { + "zero-grace": 0, + "neg-grace": -500, + "nan-grace": NaN, + }, + }; + expect(providerModelResponsesTerminalRepair("custom-gateway", invalidProv, "zero-grace")).toBeUndefined(); + expect(providerModelResponsesTerminalRepair("custom-gateway", invalidProv, "neg-grace")).toBeUndefined(); + expect(providerModelResponsesTerminalRepair("custom-gateway", invalidProv, "nan-grace")).toBeUndefined(); + }); + }); }); From c6af33f15b6a4183eaa5d15fdabb1f43bdfd6ca4 Mon Sep 17 00:00:00 2001 From: chilung Date: Sat, 22 Aug 2026 08:44:02 +0000 Subject: [PATCH 2/5] fix(providers): normalize configured model keys in terminal repair lookup (#1809) --- src/providers/registry.ts | 19 +++++++++++++++---- tests/deepseek-inbound-wire.test.ts | 9 ++++++--- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/providers/registry.ts b/src/providers/registry.ts index c721564729..0a5ca05381 100644 --- a/src/providers/registry.ts +++ b/src/providers/registry.ts @@ -2877,6 +2877,17 @@ export function providerModelResponsesUpstreamStreaming( return entry.modelResponsesUpstreamStreaming[modelId.trim().toLowerCase()]; } +function lookupCaseInsensitive(map: Record | undefined, key: string): T | undefined { + if (!map) return undefined; + if (Object.prototype.hasOwnProperty.call(map, key)) return map[key]; + const lowerKey = key.trim().toLowerCase(); + if (Object.prototype.hasOwnProperty.call(map, lowerKey)) return map[lowerKey]; + for (const [k, v] of Object.entries(map)) { + if (k.trim().toLowerCase() === lowerKey) return v; + } + return undefined; +} + /** * Resolve terminal-repair policy for native Responses streams (supports registry presets * and custom-provider configuration overrides, issue #1809). @@ -2887,21 +2898,21 @@ export function providerModelResponsesTerminalRepair( modelId: string, ): ResponsesTerminalRepairPolicy | undefined { const modelKey = modelId.trim().toLowerCase(); - const effectiveAdapter = provider.modelAdapters?.[modelId] ?? provider.modelAdapters?.[modelKey] ?? provider.adapter; + const effectiveAdapter = lookupCaseInsensitive(provider.modelAdapters, modelId) ?? provider.adapter; // Custom provider opt-in: effective wire must be openai-responses if (effectiveAdapter === "openai-responses") { // 1. Check explicit modelResponsesCompatibility - const compat = provider.modelResponsesCompatibility?.[modelId] ?? provider.modelResponsesCompatibility?.[modelKey]; + const compat = lookupCaseInsensitive(provider.modelResponsesCompatibility, modelId); if (compat === "terminal-repair") { - const raw = provider.modelResponsesTerminalRepair?.[modelId] ?? provider.modelResponsesTerminalRepair?.[modelKey]; + const raw = lookupCaseInsensitive(provider.modelResponsesTerminalRepair, modelId); const grace = typeof raw === "number" ? raw : (typeof raw === "object" && raw ? raw.graceMs : 500); const graceMs = Math.floor(grace ?? 500); return { graceMs: Number.isFinite(graceMs) && graceMs > 0 ? graceMs : 500 }; } // 2. Check explicit modelResponsesTerminalRepair - const rawModel = provider.modelResponsesTerminalRepair?.[modelId] ?? provider.modelResponsesTerminalRepair?.[modelKey]; + const rawModel = lookupCaseInsensitive(provider.modelResponsesTerminalRepair, modelId); if (rawModel !== undefined) { const grace = typeof rawModel === "number" ? rawModel : (typeof rawModel === "object" && rawModel ? rawModel.graceMs : undefined); const graceMs = Math.floor(grace ?? 0); diff --git a/tests/deepseek-inbound-wire.test.ts b/tests/deepseek-inbound-wire.test.ts index ef6a568266..f26a86332d 100644 --- a/tests/deepseek-inbound-wire.test.ts +++ b/tests/deepseek-inbound-wire.test.ts @@ -1021,11 +1021,12 @@ describe("stateless Responses upstreams get no stateful parameters", () => { adapter: "openai-responses", baseUrl: "https://custom-gateway.test/v1", modelResponsesCompatibility: { - "my-model": "terminal-repair" as const, + "My-Model": "terminal-repair" as const, }, }; expect(providerModelResponsesTerminalRepair("custom-gateway", customProv, "my-model")).toEqual({ graceMs: 500 }); expect(providerModelResponsesTerminalRepair("custom-gateway", customProv, "MY-MODEL")).toEqual({ graceMs: 500 }); + expect(providerModelResponsesTerminalRepair("custom-gateway", customProv, "My-Model")).toEqual({ graceMs: 500 }); expect(providerModelResponsesTerminalRepair("custom-gateway", customProv, "other-model")).toBeUndefined(); }); @@ -1034,12 +1035,14 @@ describe("stateless Responses upstreams get no stateful parameters", () => { adapter: "openai-responses", baseUrl: "https://custom-gateway.test/v1", modelResponsesTerminalRepair: { - "model-num": 1500, - "model-obj": { graceMs: 2000 }, + "Model-Num": 1500, + "Model-Obj": { graceMs: 2000 }, }, }; expect(providerModelResponsesTerminalRepair("custom-gateway", customProv, "model-num")).toEqual({ graceMs: 1500 }); + expect(providerModelResponsesTerminalRepair("custom-gateway", customProv, "MODEL-NUM")).toEqual({ graceMs: 1500 }); expect(providerModelResponsesTerminalRepair("custom-gateway", customProv, "model-obj")).toEqual({ graceMs: 2000 }); + expect(providerModelResponsesTerminalRepair("custom-gateway", customProv, "MODEL-OBJ")).toEqual({ graceMs: 2000 }); expect(providerModelResponsesTerminalRepair("custom-gateway", customProv, "unconfigured")).toBeUndefined(); }); From 31c6347ba107b6a6ec1af1722ff45d0f5c9b1a58 Mon Sep 17 00:00:00 2001 From: chilung Date: Sat, 22 Aug 2026 09:51:24 +0000 Subject: [PATCH 3/5] fix(providers): fail closed on invalid explicit grace values in compatibility mode (#1809) --- src/providers/registry.ts | 10 +++++++--- tests/deepseek-inbound-wire.test.ts | 16 ++++++++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/providers/registry.ts b/src/providers/registry.ts index 0a5ca05381..dfbe74b516 100644 --- a/src/providers/registry.ts +++ b/src/providers/registry.ts @@ -2906,9 +2906,13 @@ export function providerModelResponsesTerminalRepair( const compat = lookupCaseInsensitive(provider.modelResponsesCompatibility, modelId); if (compat === "terminal-repair") { const raw = lookupCaseInsensitive(provider.modelResponsesTerminalRepair, modelId); - const grace = typeof raw === "number" ? raw : (typeof raw === "object" && raw ? raw.graceMs : 500); - const graceMs = Math.floor(grace ?? 500); - return { graceMs: Number.isFinite(graceMs) && graceMs > 0 ? graceMs : 500 }; + if (raw !== undefined) { + const grace = typeof raw === "number" ? raw : (typeof raw === "object" && raw ? raw.graceMs : undefined); + const graceMs = Math.floor(grace ?? 0); + if (!Number.isFinite(graceMs) || graceMs <= 0) return undefined; + return { graceMs }; + } + return { graceMs: 500 }; } // 2. Check explicit modelResponsesTerminalRepair diff --git a/tests/deepseek-inbound-wire.test.ts b/tests/deepseek-inbound-wire.test.ts index f26a86332d..a92b1ba8a4 100644 --- a/tests/deepseek-inbound-wire.test.ts +++ b/tests/deepseek-inbound-wire.test.ts @@ -1098,10 +1098,26 @@ describe("stateless Responses upstreams get no stateful parameters", () => { "neg-grace": -500, "nan-grace": NaN, }, + modelResponsesCompatibility: { + "compat-zero": "terminal-repair" as const, + "compat-neg": "terminal-repair" as const, + "compat-nan": "terminal-repair" as const, + }, + }; + const invalidCompatProv = { + ...invalidProv, + modelResponsesTerminalRepair: { + "compat-zero": 0, + "compat-neg": -500, + "compat-nan": NaN, + }, }; expect(providerModelResponsesTerminalRepair("custom-gateway", invalidProv, "zero-grace")).toBeUndefined(); expect(providerModelResponsesTerminalRepair("custom-gateway", invalidProv, "neg-grace")).toBeUndefined(); expect(providerModelResponsesTerminalRepair("custom-gateway", invalidProv, "nan-grace")).toBeUndefined(); + expect(providerModelResponsesTerminalRepair("custom-gateway", invalidCompatProv, "compat-zero")).toBeUndefined(); + expect(providerModelResponsesTerminalRepair("custom-gateway", invalidCompatProv, "compat-neg")).toBeUndefined(); + expect(providerModelResponsesTerminalRepair("custom-gateway", invalidCompatProv, "compat-nan")).toBeUndefined(); }); }); }); From 5c22fd991fd3d21a3fa849794e453ac038663dfb Mon Sep 17 00:00:00 2001 From: chilung Date: Sat, 22 Aug 2026 10:36:14 +0000 Subject: [PATCH 4/5] feat(providers): wire terminal repair config validation and safeConfigDTO (#1809) --- src/config.ts | 74 +++++++++++++++++++++++++++++ src/server/auth-cors.ts | 12 +++++ tests/deepseek-inbound-wire.test.ts | 42 ++++++++++++++++ 3 files changed, 128 insertions(+) diff --git a/src/config.ts b/src/config.ts index dcf34313a4..a8e176029e 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1104,6 +1104,50 @@ export function modelAdapterRecordConfigError( return null; } +export function modelResponsesCompatibilityConfigError( + value: unknown, + field = "modelResponsesCompatibility", +): string | null { + if (value === undefined) return null; + if (!value || typeof value !== "object" || Array.isArray(value)) return `${field} must be a plain object`; + for (const [key, entry] of Object.entries(value)) { + 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"`; + } + } + return null; +} + +export function modelResponsesTerminalRepairConfigError( + value: unknown, + field = "modelResponsesTerminalRepair", +): string | null { + if (value === undefined) return null; + if (!value || typeof value !== "object" || Array.isArray(value)) return `${field} must be a plain object`; + for (const [key, entry] of Object.entries(value)) { + 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) || grace <= 0) { + return `${field}.${key} must be a positive number of milliseconds or { graceMs: number }`; + } + } + return null; +} + +export function responsesTerminalRepairConfigError( + value: unknown, + field = "responsesTerminalRepair", +): string | null { + if (value === undefined) return null; + 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) || 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 = @@ -1473,6 +1517,36 @@ const configSchema = z.object({ message: modelAdaptersError, }); } + const compatError = modelResponsesCompatibilityConfigError( + (provider as { modelResponsesCompatibility?: unknown }).modelResponsesCompatibility, + ); + if (compatError) { + ctx.addIssue({ + code: "custom", + path: ["providers", redactSecretString(name), "modelResponsesCompatibility"], + message: compatError, + }); + } + const modelRepairError = modelResponsesTerminalRepairConfigError( + (provider as { modelResponsesTerminalRepair?: unknown }).modelResponsesTerminalRepair, + ); + if (modelRepairError) { + ctx.addIssue({ + code: "custom", + path: ["providers", redactSecretString(name), "modelResponsesTerminalRepair"], + message: modelRepairError, + }); + } + const repairError = responsesTerminalRepairConfigError( + (provider as { responsesTerminalRepair?: unknown }).responsesTerminalRepair, + ); + if (repairError) { + ctx.addIssue({ + code: "custom", + path: ["providers", redactSecretString(name), "responsesTerminalRepair"], + message: repairError, + }); + } const preferHostedToolsError = modelPreferHostedToolsConfigError( (provider as { modelPreferHostedTools?: unknown }).modelPreferHostedTools, "modelPreferHostedTools", diff --git a/src/server/auth-cors.ts b/src/server/auth-cors.ts index 77ffa085c2..2b3fa90b6c 100644 --- a/src/server/auth-cors.ts +++ b/src/server/auth-cors.ts @@ -4,6 +4,9 @@ import { apiKeyTransportConfigError, booleanRecordConfigError, modelAdapterRecordConfigError, + modelResponsesCompatibilityConfigError, + modelResponsesTerminalRepairConfigError, + responsesTerminalRepairConfigError, modelPreferHostedToolsConfigError, codexAutoStartEnabled, nonBlankStringArrayConfigError, @@ -614,6 +617,12 @@ export function providerManagementConfigError(name: unknown, provider: unknown): if (reasoningSummaryDeliveryError) return `provider ${name} ${reasoningSummaryDeliveryError}`; const modelAdaptersError = modelAdapterRecordConfigError(raw.modelAdapters, "modelAdapters", name, typed); if (modelAdaptersError) return `provider ${name} ${modelAdaptersError}`; + const compatError = modelResponsesCompatibilityConfigError(raw.modelResponsesCompatibility); + if (compatError) return `provider ${name} ${compatError}`; + const modelRepairError = modelResponsesTerminalRepairConfigError(raw.modelResponsesTerminalRepair); + if (modelRepairError) return `provider ${name} ${modelRepairError}`; + const repairError = responsesTerminalRepairConfigError(raw.responsesTerminalRepair); + if (repairError) return `provider ${name} ${repairError}`; const preferHostedToolsError = modelPreferHostedToolsConfigError( raw.modelPreferHostedTools, "modelPreferHostedTools", @@ -709,6 +718,9 @@ export function safeConfigDTO(config: OcxConfig): unknown { "modelMaxOutputTokens", "openRouterRouting", "modelOpenRouterRouting", + "modelResponsesCompatibility", + "modelResponsesTerminalRepair", + "responsesTerminalRepair", "reasoningEfforts", "modelReasoningEfforts", "reasoningWireFormat", diff --git a/tests/deepseek-inbound-wire.test.ts b/tests/deepseek-inbound-wire.test.ts index a92b1ba8a4..2e40e03cc7 100644 --- a/tests/deepseek-inbound-wire.test.ts +++ b/tests/deepseek-inbound-wire.test.ts @@ -12,6 +12,8 @@ * assert the captured upstream URL, which is externally observable. */ import { afterEach, beforeEach, describe, expect, test } from "bun:test"; +import { safeConfigDTO, providerManagementConfigError } from "../src/server/auth-cors"; +import type { OcxConfig } from "../src/types"; import { enrichProviderFromRegistry, providerConfigSeed } from "../src/providers/derive"; import { getProviderRegistryEntry, @@ -1119,5 +1121,45 @@ describe("stateless Responses upstreams get no stateful parameters", () => { expect(providerModelResponsesTerminalRepair("custom-gateway", invalidCompatProv, "compat-neg")).toBeUndefined(); expect(providerModelResponsesTerminalRepair("custom-gateway", invalidCompatProv, "compat-nan")).toBeUndefined(); }); + + test("safeConfigDTO preserves terminal-repair configuration keys", () => { + const config: OcxConfig = { + providers: { + "custom-gw": { + adapter: "openai-responses", + baseUrl: "https://custom-gateway.test/v1", + modelResponsesCompatibility: { "my-model": "terminal-repair" }, + modelResponsesTerminalRepair: { "my-model": 1500 }, + responsesTerminalRepair: { graceMs: 800 }, + }, + }, + } as unknown as OcxConfig; + const dto = safeConfigDTO(config) as { providers: Record> }; + expect(dto.providers["custom-gw"].modelResponsesCompatibility).toEqual({ "my-model": "terminal-repair" }); + expect(dto.providers["custom-gw"].modelResponsesTerminalRepair).toEqual({ "my-model": 1500 }); + expect(dto.providers["custom-gw"].responsesTerminalRepair).toEqual({ graceMs: 800 }); + }); + + test("providerManagementConfigError validates terminal-repair configuration", () => { + expect(providerManagementConfigError("custom-gw", { + adapter: "openai-responses", + baseUrl: "https://custom-gateway.test/v1", + modelResponsesCompatibility: { "my-model": "terminal-repair" }, + modelResponsesTerminalRepair: { "my-model": 1500 }, + responsesTerminalRepair: 800, + })).toBeNull(); + + expect(providerManagementConfigError("custom-gw", { + adapter: "openai-responses", + baseUrl: "https://custom-gateway.test/v1", + modelResponsesCompatibility: { "my-model": "invalid" }, + })).toContain('modelResponsesCompatibility.my-model must be "terminal-repair"'); + + expect(providerManagementConfigError("custom-gw", { + adapter: "openai-responses", + baseUrl: "https://custom-gateway.test/v1", + responsesTerminalRepair: -500, + })).toContain('responsesTerminalRepair must be "terminal-repair", a positive number'); + }); }); }); From 2e3a9aafbf61cd8fa64c4a88aa19241d0441d69c Mon Sep 17 00:00:00 2001 From: chilung Date: Sat, 22 Aug 2026 11:42:27 +0000 Subject: [PATCH 5/5] fix(providers): harden custom terminal repair resolver against forward mode and key collisions (#1809) --- src/config.ts | 25 ++++++++++++- src/providers/registry.ts | 56 +++++++++++++++++++--------- tests/deepseek-inbound-wire.test.ts | 57 +++++++++++++++++++++++++++++ 3 files changed, 119 insertions(+), 19 deletions(-) diff --git a/src/config.ts b/src/config.ts index a8e176029e..4c047082a5 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1107,10 +1107,18 @@ export function modelAdapterRecordConfigError( 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`; - for (const [key, entry] of Object.entries(value)) { + 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`; + } + for (const [key, entry] of entries) { 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"`; @@ -1122,10 +1130,18 @@ export function modelResponsesCompatibilityConfigError( 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`; - for (const [key, entry] of Object.entries(value)) { + 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`; + } + for (const [key, entry] of entries) { 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) || grace <= 0) { @@ -1138,8 +1154,13 @@ export function modelResponsesTerminalRepairConfigError( 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) || grace <= 0) { diff --git a/src/providers/registry.ts b/src/providers/registry.ts index dfbe74b516..06b8cedc41 100644 --- a/src/providers/registry.ts +++ b/src/providers/registry.ts @@ -19,6 +19,7 @@ import { } from "../adapters/cursor/discovery"; import { COMMAND_CODE_MODEL_REASONING_EFFORTS } from "./command-code-efforts"; import { isCanonicalOpenRouterTarget } from "./openrouter-routing"; +import { isCanonicalOpenAiForwardProvider } from "./openai-tiers"; export type ProviderAuthKind = "forward" | "oauth" | "key" | "local"; export type MetadataModelIdNormalize = "case-insensitive"; @@ -2877,15 +2878,24 @@ export function providerModelResponsesUpstreamStreaming( return entry.modelResponsesUpstreamStreaming[modelId.trim().toLowerCase()]; } +const DEFAULT_TERMINAL_REPAIR_GRACE_MS = 500; +const MAX_TERMINAL_REPAIR_GRACE_MS = 60_000; + function lookupCaseInsensitive(map: Record | undefined, key: string): T | undefined { if (!map) return undefined; - if (Object.prototype.hasOwnProperty.call(map, key)) return map[key]; - const lowerKey = key.trim().toLowerCase(); - if (Object.prototype.hasOwnProperty.call(map, lowerKey)) return map[lowerKey]; + const target = key.trim().toLowerCase(); + if (!target) return undefined; + let matchedValue: T | undefined = undefined; + let matchCount = 0; for (const [k, v] of Object.entries(map)) { - if (k.trim().toLowerCase() === lowerKey) return v; + if (k.trim().toLowerCase() === target) { + matchedValue = v; + matchCount++; + } } - return undefined; + // If multiple keys case-fold to the same target (e.g. "My-Model" and "my-model"), reject as ambiguous + if (matchCount > 1) return undefined; + return matchedValue; } /** @@ -2897,6 +2907,11 @@ export function providerModelResponsesTerminalRepair( provider: Pick & Partial>, modelId: string, ): ResponsesTerminalRepairPolicy | undefined { + // Canonical ChatGPT forward traffic must never undergo synthetic terminal repair + if (isCanonicalOpenAiForwardProvider(provider as OcxProviderConfig)) { + return undefined; + } + const modelKey = modelId.trim().toLowerCase(); const effectiveAdapter = lookupCaseInsensitive(provider.modelAdapters, modelId) ?? provider.adapter; @@ -2907,30 +2922,37 @@ export function providerModelResponsesTerminalRepair( if (compat === "terminal-repair") { const raw = lookupCaseInsensitive(provider.modelResponsesTerminalRepair, modelId); if (raw !== undefined) { - const grace = typeof raw === "number" ? raw : (typeof raw === "object" && raw ? raw.graceMs : undefined); - const graceMs = Math.floor(grace ?? 0); + const grace = typeof raw === "number" ? raw : (typeof raw === "object" && raw && "graceMs" in raw ? (raw as { graceMs?: unknown }).graceMs : undefined); + const graceMs = Math.floor(typeof grace === "number" ? grace : 0); if (!Number.isFinite(graceMs) || graceMs <= 0) return undefined; - return { graceMs }; + return { graceMs: Math.min(graceMs, MAX_TERMINAL_REPAIR_GRACE_MS) }; } - return { graceMs: 500 }; + return { graceMs: DEFAULT_TERMINAL_REPAIR_GRACE_MS }; } // 2. Check explicit modelResponsesTerminalRepair const rawModel = lookupCaseInsensitive(provider.modelResponsesTerminalRepair, modelId); if (rawModel !== undefined) { - const grace = typeof rawModel === "number" ? rawModel : (typeof rawModel === "object" && rawModel ? rawModel.graceMs : undefined); - const graceMs = Math.floor(grace ?? 0); - if (Number.isFinite(graceMs) && graceMs > 0) return { graceMs }; + const grace = typeof rawModel === "number" ? rawModel : (typeof rawModel === "object" && rawModel && "graceMs" in rawModel ? (rawModel as { graceMs?: unknown }).graceMs : undefined); + const graceMs = Math.floor(typeof grace === "number" ? grace : 0); + if (Number.isFinite(graceMs) && graceMs > 0) { + return { graceMs: Math.min(graceMs, MAX_TERMINAL_REPAIR_GRACE_MS) }; + } + // Explicit model-level setting exists but is non-positive/invalid: fail closed, do not fall back to provider default + return undefined; } // 3. Check provider-level responsesTerminalRepair if (provider.responsesTerminalRepair !== undefined) { - if (provider.responsesTerminalRepair === "terminal-repair") return { graceMs: 500 }; + if (provider.responsesTerminalRepair === "terminal-repair") return { graceMs: DEFAULT_TERMINAL_REPAIR_GRACE_MS }; const grace = typeof provider.responsesTerminalRepair === "number" ? provider.responsesTerminalRepair - : (typeof provider.responsesTerminalRepair === "object" && provider.responsesTerminalRepair ? provider.responsesTerminalRepair.graceMs : undefined); - const graceMs = Math.floor(grace ?? 0); - if (Number.isFinite(graceMs) && graceMs > 0) return { graceMs }; + : (typeof provider.responsesTerminalRepair === "object" && provider.responsesTerminalRepair && "graceMs" in provider.responsesTerminalRepair ? (provider.responsesTerminalRepair as { graceMs?: unknown }).graceMs : undefined); + const graceMs = Math.floor(typeof grace === "number" ? grace : 0); + if (Number.isFinite(graceMs) && graceMs > 0) { + return { graceMs: Math.min(graceMs, MAX_TERMINAL_REPAIR_GRACE_MS) }; + } + return undefined; } } @@ -2940,7 +2962,7 @@ export function providerModelResponsesTerminalRepair( const policy = entry.modelResponsesTerminalRepair[modelKey]; const graceMs = Math.floor(policy?.graceMs ?? 0); if (!Number.isFinite(graceMs) || graceMs <= 0) return undefined; - return { graceMs }; + return { graceMs: Math.min(graceMs, MAX_TERMINAL_REPAIR_GRACE_MS) }; } /** diff --git a/tests/deepseek-inbound-wire.test.ts b/tests/deepseek-inbound-wire.test.ts index 2e40e03cc7..8dc234b5e2 100644 --- a/tests/deepseek-inbound-wire.test.ts +++ b/tests/deepseek-inbound-wire.test.ts @@ -13,6 +13,11 @@ */ import { afterEach, beforeEach, describe, expect, test } from "bun:test"; import { safeConfigDTO, providerManagementConfigError } from "../src/server/auth-cors"; +import { + modelResponsesCompatibilityConfigError, + modelResponsesTerminalRepairConfigError, + responsesTerminalRepairConfigError, +} from "../src/config"; import type { OcxConfig } from "../src/types"; import { enrichProviderFromRegistry, providerConfigSeed } from "../src/providers/derive"; import { @@ -1095,6 +1100,7 @@ describe("stateless Responses upstreams get no stateful parameters", () => { const invalidProv = { adapter: "openai-responses", baseUrl: "https://custom-gateway.test/v1", + responsesTerminalRepair: 750, modelResponsesTerminalRepair: { "zero-grace": 0, "neg-grace": -500, @@ -1122,6 +1128,45 @@ describe("stateless Responses upstreams get no stateful parameters", () => { expect(providerModelResponsesTerminalRepair("custom-gateway", invalidCompatProv, "compat-nan")).toBeUndefined(); }); + test("canonical ChatGPT forward provider never undergoes terminal repair", () => { + const canonicalOpenAi = { + adapter: "openai-responses", + authMode: "forward" as const, + baseUrl: "https://chatgpt.com/backend-api/codex", + responsesTerminalRepair: "terminal-repair" as const, + modelResponsesTerminalRepair: { "gpt-5": 1000 }, + modelResponsesCompatibility: { "gpt-5": "terminal-repair" as const }, + }; + expect(providerModelResponsesTerminalRepair("openai", canonicalOpenAi, "gpt-5")).toBeUndefined(); + }); + + test("duplicate case-folded keys fail closed on ambiguity", () => { + const conflictProv = { + adapter: "openai-responses", + baseUrl: "https://custom-gateway.test/v1", + modelResponsesTerminalRepair: { + "My-Model": 500, + "my-model": 1500, + }, + }; + expect(providerModelResponsesTerminalRepair("custom-gateway", conflictProv, "My-Model")).toBeUndefined(); + expect(providerModelResponsesTerminalRepair("custom-gateway", conflictProv, "my-model")).toBeUndefined(); + expect(providerModelResponsesTerminalRepair("custom-gateway", conflictProv, "MY-MODEL")).toBeUndefined(); + }); + + test("clamps grace period to maximum 60,000 ms", () => { + const hugeProv = { + adapter: "openai-responses", + baseUrl: "https://custom-gateway.test/v1", + modelResponsesTerminalRepair: { + "huge-model": 120_000, + "max-safe": Number.MAX_SAFE_INTEGER, + }, + }; + expect(providerModelResponsesTerminalRepair("custom-gateway", hugeProv, "huge-model")).toEqual({ graceMs: 60_000 }); + expect(providerModelResponsesTerminalRepair("custom-gateway", hugeProv, "max-safe")).toEqual({ graceMs: 60_000 }); + }); + test("safeConfigDTO preserves terminal-repair configuration keys", () => { const config: OcxConfig = { providers: { @@ -1160,6 +1205,18 @@ describe("stateless Responses upstreams get no stateful parameters", () => { baseUrl: "https://custom-gateway.test/v1", responsesTerminalRepair: -500, })).toContain('responsesTerminalRepair must be "terminal-repair", a positive number'); + + const canonicalOpenAi = { + adapter: "openai-responses", + authMode: "forward", + baseUrl: "https://chatgpt.com/backend-api/codex", + }; + expect(responsesTerminalRepairConfigError("terminal-repair", "responsesTerminalRepair", "openai", canonicalOpenAi)) + .toContain("responsesTerminalRepair is not supported on the canonical ChatGPT forward provider"); + expect(modelResponsesCompatibilityConfigError({ "gpt-5": "terminal-repair" }, "modelResponsesCompatibility", "openai", canonicalOpenAi)) + .toContain("modelResponsesCompatibility is not supported on the canonical ChatGPT forward provider"); + expect(modelResponsesTerminalRepairConfigError({ "gpt-5": 500 }, "modelResponsesTerminalRepair", "openai", canonicalOpenAi)) + .toContain("modelResponsesTerminalRepair is not supported on the canonical ChatGPT forward provider"); }); }); });