Skip to content

Commit 27a2b4e

Browse files
committed
fix(providers): address Codex review on Copilot Responses wire defaults
Move the per-model wire map onto PROVIDER_REGISTRY. A second provider fact table next to the Copilot seed it had to stay aligned with is exactly the drift-prone split src/AGENTS.md forbids. `modelWireDefaults` is registry-only metadata, deliberately not part of ProviderConfigSeed: seeding it into a saved config would freeze today's map on disk and make it indistinguishable from an explicit user `modelAdapters` entry, which must always win. A registry invariant test now pins every default to a seeded model and an allowed wire. Keep sampling params on routed Responses gateways. Both compat handlers stripped max_output_tokens for ANY openai-responses route, though the intent — stated in claude-messages.ts as "routed providers keep them" — was the native ChatGPT forward path only. Gate it on `authMode === "forward"`, matching the adapter's own stripUnsupportedForwardParams condition so the two layers agree. temperature/top_p/stop/user stay stripped on every Responses route: `stop` is not a Responses parameter, reasoning models reject temperature/top_p, and noTemperatureModels is honored only by the openai-chat adapter, so there is no per-model filter on this wire. Restore credential recovery for routed Responses gateways. Moving a model onto this wire moves it from the normal adapter loop into the passthrough branch, which formats non-2xx immediately — so a Copilot model would surface a 401 a token refresh fixes, or a 429 a healthy pool key absorbs, while its chat-wire siblings on the same provider still recover. Add a bounded recovery before the error formatting: one forced OAuth refresh, then one replay per remaining pool key. Nothing has streamed and the body is a replayable string, the same precondition the transient-5xx retry relies on. The canonical ChatGPT forward provider is excluded so its own pool-retry path is untouched. Also feed registry `modelWireDefaults` and configured `modelAdapters` keys into knownModelIdsForProvider, so a provider that names a model only in `modelAdapters` is selectable as `provider/model` and appears in the bare-id selector hint. Docs: fix the heading nesting, and carry the gpt-5.4 text-only caveat and the /chat/completions failure message into the ko/ja/zh-cn/ru locales. Full suite: 6069 pass, 6 fail — all six also fail on the unmodified base (claude-desktop-cli x2, cli-help service usage, provider-workspace-auth GUI grep, decodeSchtasksOutput) except cli-restore-back, which passes in isolation and times out only under full-suite load.
1 parent ae0f9c4 commit 27a2b4e

15 files changed

Lines changed: 458 additions & 105 deletions

File tree

docs-site/src/content/docs/guides/providers.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ ocx logout <provider>
9090
| `cursor` | `cursor` | `https://api2.cursor.sh` | Experimental PKCE login, live HTTP/2 transport, and account-filtered model discovery. |
9191
| `github-copilot` | `openai-chat` | `https://api.githubcopilot.com` | Experimental. GitHub device flow + `copilot_internal` exchange (VS Code OAuth client). Requires an active Copilot subscription; not an official third-party API. The catalog is mixed-wire — see [GitHub Copilot model wires](#github-copilot-model-wires). |
9292

93-
#### GitHub Copilot model wires
93+
### GitHub Copilot model wires
9494

9595
Copilot's catalog is not uniformly served by `/chat/completions`. The provider adapter stays
9696
`openai-chat`, which is correct for the Claude, Gemini, GPT-3.5/4 and `gpt-5-mini` entries, but the

docs-site/src/content/docs/ja/guides/providers.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -199,8 +199,12 @@ Amazon Bedrock ネイティブ API のような、これらの実装のいずれ
199199
サポートします(非公式ブリッジ — VS Code 公開クライアント ID でログイン後、短期 Copilot API トークンに
200200
交換し、有効な Copilot サブスクリプションが必要で GitHub ポリシー変更でブロックされる可能性あり)。Copilot の
201201
カタログは wire が混在します — `gpt-5.3-codex``gpt-5.4``gpt-5.4-mini``gpt-5.5``gpt-5.6-luna`
202-
`gpt-5.6-sol``gpt-5.6-terra` は Responses API でのみ応答するため、opencodex が自動的に
203-
`openai-responses` へルーティングします。明示的な `modelAdapters` の指定が常に優先されます。
202+
`gpt-5.6-sol``gpt-5.6-terra` は Chat Completions では
203+
`model "…" is not accessible via the /chat/completions endpoint` で失敗するため、opencodex が自動的に
204+
`openai-responses` へルーティングします。`gpt-5.4` はテキストのみの Chat Completions リクエストなら
205+
成功しますが、Codex が function tool と reasoning effort を伴う実リクエストを送ると失敗するため、
206+
既定では Responses のままです。その他のモデルは `openai-chat` のままです。明示的な
207+
`modelAdapters` の指定が常に優先されます。
204208
`gpt-5.6-sol` のような bare な OpenAI 名は canonical `openai` provider 専用なので、
205209
`github-copilot/gpt-5.6-sol` のように provider を付けて選択してください。GitLab Duo は Bearer
206210
**サブスクリプショントークン**(通常の API キーではない)で認証します。**Cloudflare AI

docs-site/src/content/docs/ko/guides/providers.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -200,8 +200,11 @@ Amazon Bedrock 네이티브 API처럼 이 구현 중 어느 것과도 맞지 않
200200
지원합니다(비공식 브리지 — VS Code 공개 클라이언트 id로 로그인 후 단기 Copilot API 토큰으로
201201
교환하며, 활성 Copilot 구독이 필요하고 GitHub 정책 변경으로 막힐 수 있음). Copilot 카탈로그는
202202
wire가 섞여 있습니다 — `gpt-5.3-codex`, `gpt-5.4`, `gpt-5.4-mini`, `gpt-5.5`, `gpt-5.6-luna`,
203-
`gpt-5.6-sol`, `gpt-5.6-terra`는 Responses API로만 응답하므로 opencodex가 자동으로
204-
`openai-responses`로 라우팅합니다. 명시적인 `modelAdapters` 항목이 항상 우선합니다.
203+
`gpt-5.6-sol`, `gpt-5.6-terra`는 Chat Completions에서
204+
`model "…" is not accessible via the /chat/completions endpoint`로 실패하므로 opencodex가 자동으로
205+
`openai-responses`로 라우팅합니다. `gpt-5.4`는 텍스트 전용 Chat Completions 요청은 성공하지만
206+
Codex가 function tool과 reasoning effort를 함께 보내면 실패하므로 기본값은 Responses 유지입니다.
207+
나머지 모델은 `openai-chat` 그대로입니다. 명시적인 `modelAdapters` 항목이 항상 우선합니다.
205208
`gpt-5.6-sol` 같은 bare OpenAI 이름은 canonical `openai` provider 전용이므로
206209
`github-copilot/gpt-5.6-sol`처럼 provider를 붙여 선택해야 합니다. GitLab Duo는 Bearer
207210
**구독 토큰**(일반 API 키가 아님)으로 인증합니다. **Cloudflare AI

docs-site/src/content/docs/ru/guides/providers.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ ocx logout <provider>
9191
| `kiro` | `kiro` | `https://runtime.us-east-1.kiro.dev` | Первый вход импортирует существующую сессию после установки Kiro CLI (`curl -fsSL https://cli.kiro.dev/install | bash`) и входа через `kiro-cli login`. **Добавить аккаунт** выполняет выход из `kiro-cli`, запускает новый вход через браузер, переключает аккаунт самого `kiro-cli` и сохраняет метаданные профиля отдельно для каждого аккаунта. Существующие аккаунты OpenCodex сохраняются; при отмене или сбое восстанавливается предыдущая сессия `kiro-cli`. |
9292
| `google-antigravity` | `google` | `https://daily-cloudcode-pa.googleapis.com` | Google OAuth поверх протокола Cloud Code Assist. |
9393
| `cursor` | `cursor` | `https://api2.cursor.sh` | Экспериментальный PKCE-вход, живой транспорт HTTP/2 и обнаружение моделей с фильтрацией по аккаунту. |
94-
| `github-copilot` | `openai-chat` | `https://api.githubcopilot.com` | Экспериментально. Device flow GitHub + обмен `copilot_internal` (OAuth-клиент VS Code). Требуется активная подписка Copilot; это не официальный сторонний API. Каталог смешанный по wire: `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` отвечают только на Responses API, и opencodex маршрутизирует их через `openai-responses` автоматически. Явная запись `modelAdapters` всегда важнее. Голые имена OpenAI зарезервированы за каноническим провайдером `openai`, поэтому выбирайте модель как `github-copilot/gpt-5.6-sol`. |
94+
| `github-copilot` | `openai-chat` | `https://api.githubcopilot.com` | Экспериментально. Device flow GitHub + обмен `copilot_internal` (OAuth-клиент VS Code). Требуется активная подписка Copilot; это не официальный сторонний API. Каталог смешанный по wire: `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` падают на Chat Completions с `model "…" is not accessible via the /chat/completions endpoint`, поэтому opencodex автоматически маршрутизирует их через `openai-responses`. У `gpt-5.4` текстовый запрос к Chat Completions проходит, но реальный запрос Codex с function tools и reasoning effort — нет, поэтому по умолчанию он остаётся на Responses. Остальные модели каталога остаются на `openai-chat`. Явная запись `modelAdapters` всегда важнее. Голые имена OpenAI зарезервированы за каноническим провайдером `openai`, поэтому выбирайте модель командой `codex -m github-copilot/gpt-5.6-sol`. |
9595

9696
Для канонических пресетов Kimi Coding Plan (вход через аккаунт `kimi` и API-ключ `kimi-code`)
9797
opencodex передаёт в запрос Chat Completions только стабильный `prompt_cache_key`, предоставленный

docs-site/src/content/docs/zh-cn/guides/providers.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,10 @@ GPT-5.6 Sol/Terra/Luna 会预置在提供商的回退列表中,因此即使实
186186
`ocx login github-copilot` 使用 GitHub 设备流 OAuth 登录(非官方桥接 — 使用 VS Code 公开客户端 id
187187
登录后换取短期 Copilot API 令牌,需要有效的 Copilot 订阅,GitHub 政策收紧时可能失效)。Copilot 目录混用
188188
多种 wire —— `gpt-5.3-codex``gpt-5.4``gpt-5.4-mini``gpt-5.5``gpt-5.6-luna``gpt-5.6-sol`
189-
`gpt-5.6-terra` 仅在 Responses API 上响应,opencodex 会自动将它们路由到 `openai-responses`
189+
`gpt-5.6-terra` 在 Chat Completions 上会失败并返回
190+
`model "…" is not accessible via the /chat/completions endpoint`,opencodex 会自动将它们路由到
191+
`openai-responses``gpt-5.4` 的纯文本 Chat Completions 请求可以成功,但 Codex 携带 function tools
192+
和 reasoning effort 的真实请求会失败,因此默认仍走 Responses。其余模型保持 `openai-chat`
190193
显式的 `modelAdapters` 配置始终优先。`gpt-5.6-sol` 这类裸 OpenAI 名称保留给 canonical `openai`
191194
provider,因此必须使用 `github-copilot/gpt-5.6-sol` 这样带 provider 前缀的选择器。GitLab Duo
192195
使用 Bearer **订阅令牌**(而非普通 API 密钥)进行认证。

src/providers/model-wire-defaults.ts

Lines changed: 0 additions & 79 deletions
This file was deleted.

src/providers/registry.ts

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,16 @@ export interface ProviderRegistryEntry {
123123
defaultModel?: string;
124124
models?: string[];
125125
liveModels?: boolean;
126+
/**
127+
* Per-model wire defaults for a preset whose upstream catalog is mixed-wire, keyed by
128+
* native model id. Values must be OpenAI-shaped wires (MODEL_ADAPTER_OVERRIDE_ALLOWED).
129+
*
130+
* Registry-only metadata, deliberately not part of `ProviderConfigSeed`: seeding it into
131+
* a saved config would freeze today's map on disk and make it indistinguishable from an
132+
* explicit user `modelAdapters` entry, which must always win over the default.
133+
* Read through `providerModelWireDefault` at resolve time instead.
134+
*/
135+
modelWireDefaults?: Record<string, string>;
126136
modelDiscovery?: ProviderModelDiscoverySpec;
127137
contextWindow?: number;
128138
modelContextWindows?: Record<string, number>;
@@ -1190,8 +1200,6 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
11901200
liveModels: true,
11911201
// Seed refreshed 2026-07-30 from a live 37-entry Copilot catalog; embeddings and the
11921202
// internal `trajectory-compaction` entry are omitted because they are not chat models.
1193-
// The GPT-5.3+ entries are Responses-only on this upstream — see
1194-
// `builtinModelWireDefault` in providers/model-wire-defaults.ts.
11951203
models: [
11961204
"claude-haiku-4.5", "claude-opus-4.5", "claude-opus-4.6", "claude-opus-4.7",
11971205
"claude-opus-4.8", "claude-sonnet-4.5", "claude-sonnet-4.6", "claude-sonnet-5",
@@ -1200,6 +1208,22 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
12001208
"gpt-5.3-codex", "gpt-5.4", "gpt-5.4-mini", "gpt-5.5",
12011209
"gpt-5.6-luna", "gpt-5.6-sol", "gpt-5.6-terra",
12021210
],
1211+
// Copilot's catalog is mixed-wire: `openai-chat` above is right for the Claude, Gemini,
1212+
// GPT-3.5/4 and gpt-5-mini entries, but these answer only on the Responses API and fail
1213+
// chat completions with `model "…" is not accessible via the /chat/completions endpoint`.
1214+
// gpt-5.4 hides it behind a passing smoke test — a text-only chat request succeeds and
1215+
// only a real Codex request fails, because Codex supplies function tools plus a reasoning
1216+
// effort. Verified 2026-07-30 with real `codex exec` runs, not minimal text probes:
1217+
// presence in GET /models proves neither. Recheck when Copilot changes models.
1218+
modelWireDefaults: {
1219+
"gpt-5.3-codex": "openai-responses",
1220+
"gpt-5.4": "openai-responses",
1221+
"gpt-5.4-mini": "openai-responses",
1222+
"gpt-5.5": "openai-responses",
1223+
"gpt-5.6-luna": "openai-responses",
1224+
"gpt-5.6-sol": "openai-responses",
1225+
"gpt-5.6-terra": "openai-responses",
1226+
},
12031227
defaultModel: "gpt-4o",
12041228
note: "Experimental unofficial Copilot bridge. Logs in via GitHub device flow using the public VS Code OAuth client id, then exchanges for a short-lived Copilot API token (copilot_internal). Requires an active Copilot subscription. GitHub may tighten or revoke this path; do not send confidential material you would not paste into Copilot Chat.",
12051229
},
@@ -1246,6 +1270,35 @@ export function providerMatchesRegistryTransport(
12461270
return normalizedProviderEndpoint(provider.baseUrl) === normalizedProviderEndpoint(entry.baseUrl);
12471271
}
12481272

1273+
/** Copilot publishes dated snapshots of a family (`gpt-4o-2024-05-13`), so match those too. */
1274+
const DATED_SNAPSHOT_SUFFIX = /-\d{4}-\d{2}-\d{2}$/;
1275+
1276+
/**
1277+
* The wire a model should use when the user has configured no `modelAdapters` entry for it,
1278+
* or undefined when the provider-wide adapter already applies.
1279+
*
1280+
* Distinct from the hard pins in `types.ts`: a pin means the upstream speaks exactly one wire
1281+
* and a user override must not apply. This is a default — an explicit `modelAdapters` entry
1282+
* always wins, including one naming the provider-wide adapter to opt a model back out.
1283+
*
1284+
* `currentAdapter` is the provider's configured adapter: a default only nudges a provider
1285+
* between OpenAI-shaped wires, so a provider a user moved to some other adapter is left alone.
1286+
* A provider re-pointed away from the preset transport keeps nothing from the preset either.
1287+
*/
1288+
export function providerModelWireDefault(
1289+
id: string,
1290+
provider: Pick<OcxProviderConfig, "baseUrl" | "adapter"> & Partial<Pick<OcxProviderConfig, "authMode">>,
1291+
modelId: string,
1292+
allowedWires: ReadonlySet<string>,
1293+
): string | undefined {
1294+
if (!allowedWires.has(provider.adapter)) return undefined;
1295+
const defaults = getProviderRegistryEntry(id)?.modelWireDefaults;
1296+
if (!defaults || !providerMatchesRegistryTransport(id, provider)) return undefined;
1297+
const model = modelId.trim().toLowerCase();
1298+
const wire = defaults[model] ?? defaults[model.replace(DATED_SNAPSHOT_SUFFIX, "")];
1299+
return wire !== undefined && allowedWires.has(wire) ? wire : undefined;
1300+
}
1301+
12491302
/**
12501303
* Effective Codex account mode for a provider. For canonical `openai`, a valid persisted
12511304
* `codexAccountMode` on the provider config wins and a missing/invalid value defaults to

src/router.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ export function knownModelIdsForProvider(provName: string, prov: OcxProviderConf
5353
registry?.modelDefaultReasoningEfforts,
5454
registry?.modelReasoningEffortMap,
5555
registry?.modelMaxOutputTokens,
56+
registry?.modelWireDefaults,
57+
// A user who names a model in `modelAdapters` has asserted it exists on this provider,
58+
// so it is selectable as `provider/model` even when no seed or cache lists it.
59+
prov.modelAdapters,
5660
]) {
5761
for (const id of Object.keys(map ?? {})) ids.add(id);
5862
}

src/server/adapter-resolve.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { createResponsesPassthroughAdapter } from "../adapters/openai-responses"
99
import type { OcxProviderConfig } from "../types";
1010
import { isWirePinnedModel, MODEL_ADAPTER_OVERRIDE_ALLOWED, pinnedWireAdapter } from "../types";
1111
import { isCanonicalOpenAiForwardProvider } from "../providers/openai-tiers";
12-
import { builtinModelWireDefault } from "../providers/model-wire-defaults";
12+
import { providerModelWireDefault } from "../providers/registry";
1313

1414
/**
1515
* Resolve the wire a single model should use: a hard pin first, then a configured
@@ -31,7 +31,7 @@ export function resolveWireProtocolOverride(providerName: string, modelId: strin
3131
// provider-wide adapter, which is how a user opts a model out of a built-in default.
3232
const requested = configured && MODEL_ADAPTER_OVERRIDE_ALLOWED.has(configured)
3333
? configured
34-
: builtinModelWireDefault(providerName, modelId, providerConfig.adapter);
34+
: providerModelWireDefault(providerName, providerConfig, modelId, MODEL_ADAPTER_OVERRIDE_ALLOWED);
3535
if (requested
3636
&& MODEL_ADAPTER_OVERRIDE_ALLOWED.has(requested)
3737
&& requested !== providerConfig.adapter

src/server/chat-completions.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,15 @@ export async function handleChatCompletions(
8888
directRoute = route.codexAccountMode === "direct";
8989
// ChatGPT backend rejects store:true and unsupported sampling knobs.
9090
internalBody.store = false;
91-
delete internalBody.max_output_tokens;
91+
// Forward mode is the only route that 400s on max_output_tokens, and the adapter's
92+
// own stripUnsupportedForwardParams already drops it there on the same condition.
93+
// A routed Responses gateway (Copilot, api.openai.com, ...) honors the field, and
94+
// dropping it there silently ignores the caller's max_tokens.
95+
if (route.provider.authMode === "forward") delete internalBody.max_output_tokens;
96+
// temperature/top_p/stop/user stay stripped on every Responses route: `stop` is not
97+
// a Responses parameter at all, reasoning models reject temperature/top_p, and the
98+
// passthrough adapter relays the body verbatim — `noTemperatureModels` and friends
99+
// are honored only by the openai-chat adapter, so there is no per-model filter here.
92100
delete internalBody.temperature;
93101
delete internalBody.top_p;
94102
delete internalBody.stop;

0 commit comments

Comments
 (0)