From f4ea1211204a1eb734b8deac30393e08146475d3 Mon Sep 17 00:00:00 2001 From: Yuxin Qiao <104957188+Yuxin-Qiao@users.noreply.github.com> Date: Wed, 12 Aug 2026 12:21:59 +0800 Subject: [PATCH 1/2] fix(openai-chat): forward service tier --- src/adapters/openai-chat.ts | 4 ++++ tests/openai-chat-hardening.test.ts | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/src/adapters/openai-chat.ts b/src/adapters/openai-chat.ts index 99b62af5e..09bfddb34 100644 --- a/src/adapters/openai-chat.ts +++ b/src/adapters/openai-chat.ts @@ -744,6 +744,10 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd messages, stream: parsed.stream, }; + // Preserve a caller-selected service tier for OpenAI-compatible chat gateways. The + // request pipeline deliberately does not inject fast mode for this adapter, but dropping + // an explicit value here makes the Responses parser's serviceTier projection ineffective. + if (parsed.options.serviceTier !== undefined) body.service_tier = parsed.options.serviceTier; if (modelInList(provider.reasoningSplitModels, parsed.modelId)) body.reasoning_split = true; const maxTokens = resolveMaxTokens(provider, parsed); const openRouterRouting = resolveOpenRouterRouting(provider, parsed.modelId); diff --git a/tests/openai-chat-hardening.test.ts b/tests/openai-chat-hardening.test.ts index b78c3102b..b22007a59 100644 --- a/tests/openai-chat-hardening.test.ts +++ b/tests/openai-chat-hardening.test.ts @@ -302,6 +302,16 @@ describe("openai-chat credential hardening", () => { expect(body).not.toHaveProperty("prompt_cache_key"); }); + test("preserves a caller-supplied service tier for the outbound chat body", () => { + const adapter = createOpenAIChatAdapter(provider()); + const req = parsed(); + req.options.serviceTier = "priority"; + + const body = JSON.parse(adapter.buildRequest(req).body); + + expect(body.service_tier).toBe("priority"); + }); + test("canonical Kimi Coding Plan routes forward Codex prompt_cache_key", () => { for (const [providerName, authMode] of [ ["kimi", "oauth"], From 197c82ae992fa0a9e6d53ac4459a4b2c2a0d01a8 Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Wed, 12 Aug 2026 23:36:30 +0900 Subject: [PATCH 2/2] fix(openai-chat): gate the chat service tier behind a provider opt-in MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up on top of @Yuxin-Qiao's forwarding commit. The forwarding itself is right: the Responses parser records a caller-supplied service_tier and this adapter dropped it, so the value never reached the wire. But 66 registry providers share the openai-chat adapter, and applyServiceTierGate only strips for openai-responses, so forwarding unconditionally would put an OpenAI-specific field on every one of them. Strict gateways reject unknown body fields, which turns a caller-supplied tier into an upstream 400 on routes that work today. prompt_cache_key directly below already solves this exact problem with an opt-in flag, and its comment names the same hazard. service_tier now follows that precedent: chatServiceTier on the registry entry, threaded through derive and router the way promptCacheKey is. supportsServiceTier is deliberately not reused — it governs the Responses wire through applyServiceTierGate and means something different. No registry entry opts in yet; that wants per-provider evidence that the gateway documents the parameter, which is a separate change. --- src/adapters/openai-chat.ts | 10 +++++++++- src/providers/derive.ts | 2 ++ src/providers/registry.ts | 6 ++++++ src/router.ts | 1 + src/types.ts | 9 +++++++++ tests/openai-chat-hardening.test.ts | 25 +++++++++++++++++++++++-- 6 files changed, 50 insertions(+), 3 deletions(-) diff --git a/src/adapters/openai-chat.ts b/src/adapters/openai-chat.ts index 09bfddb34..370706cb1 100644 --- a/src/adapters/openai-chat.ts +++ b/src/adapters/openai-chat.ts @@ -747,7 +747,15 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd // Preserve a caller-selected service tier for OpenAI-compatible chat gateways. The // request pipeline deliberately does not inject fast mode for this adapter, but dropping // an explicit value here makes the Responses parser's serviceTier projection ineffective. - if (parsed.options.serviceTier !== undefined) body.service_tier = parsed.options.serviceTier; + // + // Opt-in, like `prompt_cache_key` directly below: `service_tier` is an OpenAI-specific + // extension and 66 registry providers share this adapter, several of which reject + // unknown body fields. Forwarding unconditionally would turn a caller-supplied + // `service_tier` into an upstream 400 on those routes. `supportsServiceTier` is the + // Responses-wire flag (applyServiceTierGate) and deliberately does not gate this path. + if (provider.chatServiceTier && parsed.options.serviceTier !== undefined) { + body.service_tier = parsed.options.serviceTier; + } if (modelInList(provider.reasoningSplitModels, parsed.modelId)) body.reasoning_split = true; const maxTokens = resolveMaxTokens(provider, parsed); const openRouterRouting = resolveOpenRouterRouting(provider, parsed.modelId); diff --git a/src/providers/derive.ts b/src/providers/derive.ts index 452eed56d..0aef21218 100644 --- a/src/providers/derive.ts +++ b/src/providers/derive.ts @@ -239,6 +239,7 @@ export function providerConfigSeed(entry: ProviderRegistryEntry): OcxProviderCon ...(entry.noPenaltyModels ? { noPenaltyModels: [...entry.noPenaltyModels] } : {}), ...(entry.parallelToolCalls !== undefined ? { parallelToolCalls: entry.parallelToolCalls } : {}), ...(entry.promptCacheKey !== undefined ? { promptCacheKey: entry.promptCacheKey } : {}), + ...(entry.chatServiceTier !== undefined ? { chatServiceTier: entry.chatServiceTier } : {}), ...(entry.responsesPath !== undefined ? { responsesPath: entry.responsesPath } : {}), ...(entry.statelessResponses !== undefined ? { statelessResponses: entry.statelessResponses } : {}), ...(entry.requiresAdjacentResponsesToolResults !== undefined @@ -419,6 +420,7 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig if (!prov.noPenaltyModels && seed.noPenaltyModels) prov.noPenaltyModels = [...seed.noPenaltyModels]; if (prov.parallelToolCalls === undefined && seed.parallelToolCalls !== undefined) prov.parallelToolCalls = seed.parallelToolCalls; if (prov.promptCacheKey === undefined && seed.promptCacheKey !== undefined) prov.promptCacheKey = seed.promptCacheKey; + if (prov.chatServiceTier === undefined && seed.chatServiceTier !== undefined) prov.chatServiceTier = seed.chatServiceTier; // Fill-only: a hand-edited path must survive, and a config saved before the registry // learned this route still gets backfilled. if (prov.responsesPath === undefined && seed.responsesPath !== undefined) prov.responsesPath = seed.responsesPath; diff --git a/src/providers/registry.ts b/src/providers/registry.ts index 42eef9369..6f0801119 100644 --- a/src/providers/registry.ts +++ b/src/providers/registry.ts @@ -237,6 +237,12 @@ export interface ProviderRegistryEntry { parallelToolCalls?: boolean; /** Opt this provider into forwarding prompt_cache_key (OpenAI-specific; strict backends reject it). */ promptCacheKey?: boolean; + /** + * Opt-in: forward `service_tier` on the `/chat/completions` wire. Same hazard as + * `promptCacheKey` — an OpenAI-specific extension that strict gateways reject. Distinct from + * `supportsServiceTier`, which governs the Responses wire. + */ + chatServiceTier?: boolean; autoToolChoiceOnlyModels?: string[]; preserveReasoningContentModels?: string[]; requiresReasoningPlaceholderModels?: string[]; diff --git a/src/router.ts b/src/router.ts index 435148f3f..b01fee043 100644 --- a/src/router.ts +++ b/src/router.ts @@ -350,6 +350,7 @@ export function routedProviderConfig(providerName: string, provider: OcxProvider // opt-in, while an explicit user `false` keeps overriding registry `true`. ...(provider.parallelToolCalls === undefined && registryEntry.parallelToolCalls !== undefined ? { parallelToolCalls: registryEntry.parallelToolCalls } : {}), ...(provider.promptCacheKey === undefined && registryEntry.promptCacheKey !== undefined ? { promptCacheKey: registryEntry.promptCacheKey } : {}), + ...(provider.chatServiceTier === undefined && registryEntry.chatServiceTier !== undefined ? { chatServiceTier: registryEntry.chatServiceTier } : {}), ...(provider.reasoningWireFormat === undefined && registryEntry.reasoningWireFormat !== undefined ? { reasoningWireFormat: registryEntry.reasoningWireFormat } : {}), diff --git a/src/types.ts b/src/types.ts index 3dc5c3354..6de3fae29 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1438,6 +1438,15 @@ export interface OcxProviderConfig { * fields. Default off; only enable for providers that document this parameter. */ promptCacheKey?: boolean; + /** + * Opt-in: forward `service_tier` to the upstream `/chat/completions` body. + * OpenAI-specific extension with the same hazard as `promptCacheKey` — strict backends + * reject unknown fields, and 66 registry providers share the `openai-chat` adapter, so a + * caller-supplied `service_tier` would otherwise turn working requests into upstream 400s. + * `supportsServiceTier` is the Responses-wire flag and does not apply here. + * Default off; only enable for providers that document this parameter on the chat wire. + */ + chatServiceTier?: boolean; /** * Provider-local passthrough SSE repair for broken openai-responses gateways that reuse exact * placeholder message/reasoning ids or omit the terminal id after a stable added event. diff --git a/tests/openai-chat-hardening.test.ts b/tests/openai-chat-hardening.test.ts index b22007a59..61de2920d 100644 --- a/tests/openai-chat-hardening.test.ts +++ b/tests/openai-chat-hardening.test.ts @@ -302,8 +302,8 @@ describe("openai-chat credential hardening", () => { expect(body).not.toHaveProperty("prompt_cache_key"); }); - test("preserves a caller-supplied service tier for the outbound chat body", () => { - const adapter = createOpenAIChatAdapter(provider()); + test("preserves a caller-supplied service tier when the provider opts in", () => { + const adapter = createOpenAIChatAdapter(provider({ chatServiceTier: true })); const req = parsed(); req.options.serviceTier = "priority"; @@ -312,6 +312,27 @@ describe("openai-chat credential hardening", () => { expect(body.service_tier).toBe("priority"); }); + // `service_tier` is an OpenAI-specific extension and this adapter serves 66 registry + // providers, several of which reject unknown body fields. Forwarding it by default would + // turn a caller-supplied tier into an upstream 400 on those routes, so absence of the + // opt-in must mean the field is dropped — the same contract `prompt_cache_key` uses. + test("drops a caller-supplied service tier when the provider has not opted in", () => { + for (const p of [provider(), provider({ chatServiceTier: false })]) { + const req = parsed(); + req.options.serviceTier = "priority"; + + const body = JSON.parse(createOpenAIChatAdapter(p).buildRequest(req).body); + + expect(body).not.toHaveProperty("service_tier"); + } + }); + + test("an opted-in provider without a caller tier still sends no service_tier", () => { + const body = JSON.parse(createOpenAIChatAdapter(provider({ chatServiceTier: true })).buildRequest(parsed()).body); + + expect(body).not.toHaveProperty("service_tier"); + }); + test("canonical Kimi Coding Plan routes forward Codex prompt_cache_key", () => { for (const [providerName, authMode] of [ ["kimi", "oauth"],