fix(providers): route GitHub Copilot Responses-only models off chat completions - #746
fix(providers): route GitHub Copilot Responses-only models off chat completions#746mushikingh wants to merge 5 commits into
Conversation
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughGitHub Copilot now maps mixed model catalogs to ChangesGitHub Copilot mixed-wire routing
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Client
participant Router
participant AdapterResolver
participant CopilotResponses
participant CredentialRecovery
Client->>Router: select github-copilot/model
Router->>AdapterResolver: resolve model adapter
AdapterResolver->>CopilotResponses: route Responses-only model
CopilotResponses-->>CredentialRecovery: return 401 or 429
CredentialRecovery->>CopilotResponses: refresh or rotate and replay
CopilotResponses-->>Client: return response or final failure
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs-site/src/content/docs/guides/providers.md`:
- Line 93: Change the “GitHub Copilot model wires” heading from an h4 to an h3
so it is correctly nested beneath the surrounding “## 2. Account login (OAuth)”
section and satisfies the heading hierarchy.
In `@docs-site/src/content/docs/ja/guides/providers.md`:
- Around line 201-204: Update the model-routing descriptions in
docs-site/src/content/docs/ja/guides/providers.md lines 201-204 and
docs-site/src/content/docs/ko/guides/providers.md lines 201-204 to state that
gpt-5.4 remains routed to Responses by default, while text-only Chat Completions
are supported; retain the requirement for Responses when real Codex requests use
function tools and reasoning, without changing the handling of the other listed
models.
In `@docs-site/src/content/docs/ru/guides/providers.md`:
- Line 94: The Copilot provider guidance is incomplete in both translated pages.
Update docs-site/src/content/docs/ru/guides/providers.md lines 94-94 and
docs-site/src/content/docs/zh-cn/guides/providers.md lines 187-191 to include
the /chat/completions failure message, the gpt-5.4 text-only caveat, remaining
openai-chat behavior, wire-default details, and the manual selection command
codex -m github-copilot/...; keep each translation synchronized with the actual
CLI/API behavior.
In `@src/router.ts`:
- Around line 312-318: Update knownModelIdsForProvider, used by
providersServingModelId, to include the keys from prov.modelAdapters in the
returned model-ID set alongside existing sources. Add a router test covering a
provider configured only with modelAdapters, verifying its adapted model appears
among bare-model selector alternatives.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 6da0615d-686e-4480-a737-ce612a5682da
📒 Files selected for processing (14)
docs-site/src/content/docs/guides/providers.mddocs-site/src/content/docs/ja/guides/providers.mddocs-site/src/content/docs/ko/guides/providers.mddocs-site/src/content/docs/ko/reference/configuration.mddocs-site/src/content/docs/reference/configuration.mddocs-site/src/content/docs/ru/guides/providers.mddocs-site/src/content/docs/zh-cn/guides/providers.mdsrc/providers/model-wire-defaults.tssrc/providers/registry.tssrc/router.tssrc/server/adapter-resolve.tstests/adapter-resolve.test.tstests/github-copilot-wire-defaults.test.tstests/router.test.ts
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ae0f9c41dc
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
Codex review — all four findings verified and addressed (
|
| Failing test | Branch | Base |
|---|---|---|
claude-desktop-cli — persisted profile |
fail | fail |
claude-desktop-cli — import rejects invalid |
fail | fail |
cli-help — service usage remove alias |
fail | fail (expectation predates repair) |
provider-workspace-auth — logout/delete states |
fail | fail (greps GUI for setToast(...), now showActionFeedback(...)) |
decodeSchtasksOutput — UTF-16LE BOM |
fail | fail |
cli-restore-back — ambiguous cleanup |
fail (5017ms timeout) | passes in isolation — 5s-timeout flake under load |
None are caused by this PR, and I did not touch GUI or Windows-scheduler code.
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/server/chat-completions.ts (1)
86-103: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDuplicated Responses-wire sampling-param stripping across two handlers — extract a shared helper.
src/server/chat-completions.tsandsrc/server/claude-messages.tsboth carry an identical block that conditionally stripsmax_output_tokens(forward-only) and unconditionally stripstemperature/top_p/stop/userwhenroute.provider.adapter === "openai-responses". This PR itself had to patch both copies in lockstep to fix the Copilot routed-gateway bug — proof that keeping this logic duplicated risks the two call sites silently diverging again the next time a Responses-wire nuance needs handling.
src/server/chat-completions.ts#L86-L103: extract thestore/max_output_tokens/temperature/top_p/stop/usershaping block (currently inline here) into a shared helper, e.g.applyResponsesWireParamShaping(internalBody, route.provider).src/server/claude-messages.ts#L600-L616: replace the identical inline block with a call to the same shared helper so both handlers stay in sync going forward.♻️ Suggested shared helper sketch
// e.g. src/server/responses/param-shaping.ts export function stripUnsupportedResponsesSamplingParams( body: Rec, provider: Pick<OcxProviderConfig, "authMode">, ): void { // Forward mode is the only route that 400s on max_output_tokens; a routed Responses // gateway (Copilot, api.openai.com, ...) honors the field. if (provider.authMode === "forward") delete body.max_output_tokens; // temperature/top_p/stop/user stay stripped on every Responses route. delete body.temperature; delete body.top_p; delete body.stop; delete body.user; }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/server/chat-completions.ts` around lines 86 - 103, Extract the Responses-wire parameter shaping from the inline block near the route handling in src/server/chat-completions.ts:86-103 into a shared helper that applies the forward-only max_output_tokens removal and unconditional temperature, top_p, stop, and user removal, while preserving store handling. Replace the duplicate block in src/server/claude-messages.ts:600-616 with the same helper call so both handlers use one implementation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/server/responses/core.ts`:
- Around line 1519-1576: Update the recovery replay loop around noteAttemptSend
in the routed Responses passthrough to track the recovery kind that triggered
each replay. Set the kind to "oauth-401" for the OAuth refresh branch and
"key-429" for the key-rotation branch, then pass it as the recovery argument to
noteAttemptSend so recoveryKinds reflects the corresponding send.
---
Outside diff comments:
In `@src/server/chat-completions.ts`:
- Around line 86-103: Extract the Responses-wire parameter shaping from the
inline block near the route handling in src/server/chat-completions.ts:86-103
into a shared helper that applies the forward-only max_output_tokens removal and
unconditional temperature, top_p, stop, and user removal, while preserving store
handling. Replace the duplicate block in src/server/claude-messages.ts:600-616
with the same helper call so both handlers use one implementation.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: c589c86f-972b-48df-96dc-b348f4d1ae25
📒 Files selected for processing (14)
docs-site/src/content/docs/guides/providers.mddocs-site/src/content/docs/ja/guides/providers.mddocs-site/src/content/docs/ko/guides/providers.mddocs-site/src/content/docs/ru/guides/providers.mddocs-site/src/content/docs/zh-cn/guides/providers.mdsrc/providers/registry.tssrc/router.tssrc/server/adapter-resolve.tssrc/server/chat-completions.tssrc/server/claude-messages.tssrc/server/responses/core.tstests/github-copilot-responses-wire-recovery.test.tstests/github-copilot-wire-defaults.test.tstests/router.test.ts
CodeRabbit follow-up review — both findings applied (
|
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/server/responses/core.ts (1)
1543-1549: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winPropagate refreshed Kiro auth context before replay.
The new replay updates only
route.provider.apiKey, butpassthroughAdapter.buildRequest(parsed, ...)also consumesparsed._kiroAuthContext. The existing recovery path updates this field fromrefreshed.kiroat Lines 2237-2239; without the same update here, a Kiro OAuth refresh can replay with stale context and fail again.🔧 Proposed fix
sentOAuthSnapshot = refreshed; + if (route.providerName === "kiro") { + parsed._kiroAuthContext = { ...(refreshed.kiro ?? {}) }; + } nextProvider = resolveProviderTransport(As per path instructions,
src/**changes must flag provider/adapter contract drift and preserve shared routing/config behavior.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/server/responses/core.ts` around lines 1543 - 1549, Update the OAuth refresh replay path around sentOAuthSnapshot and nextProvider to also propagate refreshed.kiro into parsed._kiroAuthContext before passthroughAdapter.buildRequest(parsed, ...); preserve the existing route.provider apiKey update, provider transport resolution, and shared routing/config behavior.Source: Path instructions
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@src/server/responses/core.ts`:
- Around line 1543-1549: Update the OAuth refresh replay path around
sentOAuthSnapshot and nextProvider to also propagate refreshed.kiro into
parsed._kiroAuthContext before passthroughAdapter.buildRequest(parsed, ...);
preserve the existing route.provider apiKey update, provider transport
resolution, and shared routing/config behavior.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 52683971-e920-4ef5-baf6-5b8f22962172
📒 Files selected for processing (4)
src/server/chat-completions.tssrc/server/claude-messages.tssrc/server/responses-wire-params.tssrc/server/responses/core.ts
|
NEEDS-SECURITY-REVIEW — blocked on review, not on changes. This is a bigger change than the title suggests, and that is why it needs a reviewer rather than a merge. Beyond Replay-after-credential-change is the part that needs someone hostile looking at it. The questions a reviewer should be able to answer from the code:
Your tests cover wire selection, override precedence, destination and credential preservation, OAuth replay, pool rotation, and non-Copilot isolation. That is a genuinely useful starting point for the reviewer — it is not a substitute for the approval. What happens next: request explicit security review. No changes are being asked for ahead of it. CI context (shared across the current review round). |
The dev checkout is mid-optimization, so this round stages on codex/260731-pr-merge-round instead of dev. The user merges it into dev and releases once that unit lands. Four reviewers triaged every open PR against origin/dev=356924263 on disjoint slices, and the load-bearing finding is that seven PRs are already fixed on dev by different commits. Merging those heads now would REVERT the newer work. lidge-jun#736 is the clearest case: dev carries decodeSchtasksOutput() at src/service.ts:364-393 because schtasks /query /xml emits UTF-16LE, and the PR head deletes that block and restores encoding: "utf8" -- landing it reopens lidge-jun#722. A clean merge-tree is not permission to merge. An adversarial audit then returned five blockers, all folded in rather than argued away. Two changed what actually gets merged. lidge-jun#744 was routed as a routine catalog fix, but 59d95c0 and 39543a3 change OAuth reconciliation, persist provider settings, and move token resolution around the static branch -- security review per MAINTAINERS.md, so it left the batch. lidge-jun#781's topic commit also swaps three /api/logs test files onto a logsFromApiBody helper that accepts both the array and the {logs} envelope; adopting it would pre-accept the very contract batch B rejects, so only the Anthropic file and its own test come across. The audit also caught three PRs missing from a matrix that claimed to be complete (lidge-jun#750, lidge-jun#746, lidge-jun#644), and lidge-jun#644 additionally carries .codexclaw/goalplans/** and two .DS_Store files. Nothing is merged yet. Batch A is six PRs, batch B rebuilds three whose implementations no longer fit the tree, and 20+ are held for security review, provider evidence, or their own cycle -- each with what would unblock it recorded.
|
@mushikingh Pleasse resolve conflicts & CI Errors, then we will security review it. |
…ompletions Copilot fronts a mixed-wire catalog. The preset adapter `openai-chat` is correct for its Claude, Gemini, GPT-3.5/4 and gpt-5-mini entries, but the newer OpenAI models are Responses-only on this upstream and fail with model "gpt-5.6-sol" is not accessible via the /chat/completions endpoint gpt-5.4 hides the same problem behind a passing smoke test: a text-only chat request succeeds and only a real Codex request fails, because Codex supplies function tools plus a reasoning effort. Declare the seven affected models in the registry's `modelWireDefaults`, so they resolve to `openai-responses` without every user hand-writing the same `modelAdapters` block. Unlike DeepSeek's Flash entry these are bare strings rather than inbound-scoped: the upstream serves no chat route at all for them, so a Chat Completions or Claude Code client has to be translated too instead of being kept on the wire it already speaks. Also refresh the Copilot cold-start seed from the verified live catalog — the old seed listed gpt-4.1-mini and claude-sonnet-4, neither of which it contains — so a qualified selector resolves before live discovery lands. Separately, bare OpenAI-family ids are reserved for the canonical `openai` provider, so `-m gpt-5.6-sol` cannot mean Copilot. NoEnabledOpenAiProviderError now names a qualified selector that would work when another enabled provider serves that id. Registry `modelWireDefaults` and configured `modelAdapters` keys also feed knownModelIdsForProvider, so a provider naming a model only there is selectable as `provider/model`. Verified 2026-07-30 against a live 37-entry Copilot catalog with real `codex exec` runs rather than minimal text probes: presence in GET /models proves neither, and a text-only success does not prove the model survives function tools plus reasoning_effort.
…s gateways Two defects that a registry `modelWireDefaults` entry makes reachable: putting a model on the Responses wire moves it onto code paths written for the canonical ChatGPT forward provider. Sampling params. Both translate-and-replay inbound 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, and extract the shared block both handlers had been carrying in duplicate. 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. Credential recovery. The passthrough branch formats non-2xx immediately, while the OAuth 401 refresh and key-pool 429 rotation live only in the normal adapter loop — so a model routed onto this wire would surface a 401 a refresh fixes, or a 429 a healthy pool key absorbs, while its chat-wire siblings on the same provider recover. Add a bounded recovery before the error formatting: at most one forced refresh, then one replay per remaining pool key, both guards monotonic so a persistently failing upstream cannot loop. Nothing has streamed and the body is a replayable string, the same precondition the transient-5xx retry relies on. The failed response's socket is released before the refresh round-trip, not only before the replay, since a failed refresh returns early. The rotated credential is rebuilt through resolveProviderTransport under the SAME provider name, so it can only be addressed at that provider's validated destination, and the request is rebuilt from the rotated provider so the new credential replaces the old one everywhere it is carried. The canonical ChatGPT forward provider is excluded entirely, leaving its own pool-retry path untouched. Replays record oauth-401 / key-429 on the attempt so the request log explains why the send count grew.
5e4bf38 to
17e5263
Compare
Rebased onto current
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 17e5263f87
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
…t on Responses recovery Add gpt-5.4-nano to the Copilot seed and modelWireDefaults so the Responses-only model is not left on openai-chat for qualified selectors, and set _kiroAuthContext on the new passthrough OAuth 401 recovery path to match the normal loop.
[shipping-github] Security reviewPR: Findingsnone confirmed SummaryReviewed the final head Residual
Final-head delta reviewed
|
…ed on The passthrough recovery loop rebuilt its adapter with no inbound argument, so `resolveWireProtocolOverride` fell back to its `"responses"` default instead of the inbound the request actually arrived on. A registry default scoped to a chat or anthropic inbound would then resolve one wire on the first send and a different one on the replay, putting the retried request on a protocol the original never used. This was the only one of nine call sites in the file missing the argument. Not reachable from a shipped entry today: Copilot's defaults are bare strings that apply to every inbound, and DeepSeek's is scoped to the responses inbound, which is also the fallback. It is what the next inbound-scoped default would inherit silently. Also record that gpt-5.4-nano is the one wire default not backed by a real `codex exec` run, so it does not sit under a comment claiming otherwise.
Codex review
|
Heads-up: the head moved 10 minutes after the security review@Wibias — the review above is against The entire delta passthroughAdapter = resolveAdapter(
- resolveWireProtocolOverride(route.providerName, route.modelId, route.provider),
+ resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
config.cacheRetention,
);plus a comment on the Against the properties you verified
So I read it as security-neutral, and mildly positive on the third point. That is my assessment of my own patch, not a substitute for yours. Your call: re-run the review at |
|
Thanks for the careful work here and for following up on the review feedback. Before we can move this forward, please:
The routing fix itself is correct and wanted. Thank you for pushing it forward. |
Closes #748
Problem
The
github-copilotpreset configures a provider-wideopenai-chatadapter. That is correct for most of the Copilot catalog, but Copilot fronts a mixed-wire catalog and several newer OpenAI models are only served by the Responses API:gpt-5.4hides the same problem behind a passing smoke test. A minimal text-only Chat Completions request succeeds; a real Codex request fails, because Codex supplies function tools and a reasoning effort:modelAdaptersalready solves this, but every Copilot user has to discover the failure and hand-write the same map. The gap is the missing built-in mapping.Change
Built-in per-model wire defaults — new
src/providers/model-wire-defaults.tsholds a per-provider default map, consulted fromresolveWireProtocolOverride. Precedence becomes:Design notes:
modelAdaptersentry always wins, including one that names the provider-wide adapter — that is how a user opts a model back out of a default. The previousrequested !== providerConfig.adaptershort-circuit would have silently dropped such an entry into the default path, so the override is now resolved before that comparison.MODEL_ADAPTER_OVERRIDE_ALLOWED), so they never override a provider a user moved to some other adapter.types.ts: a pin means the upstream speaks exactly one wire and a user override must not apply. These are defaults.gpt-5.4-2026-07-30) resolve the same way — Copilot publishes dated ids (gpt-4o-2024-05-13).Models routed to
openai-responses: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.Cold-start seed refresh — the Copilot registry seed listed
gpt-4.1-miniandclaude-sonnet-4, neither of which is in the live catalog. Refreshed from the verified catalog (embeddings and the internaltrajectory-compactionentry excluded — they are not chat models) so a qualified selector resolves before live discovery lands.Selector error hint — bare OpenAI-family ids are reserved for the canonical
openaiprovider, so-m gpt-5.6-solcannot mean Copilot and fails withModel gpt-5.6-sol requires the canonical openai provider.NoEnabledOpenAiProviderErrornow also names a qualified selector that would work when another enabled provider serves that id, instead of only suggestingocx provider add openai. Message-only; routing is unchanged.Verification
Field verification was done on OpenCodex
2.7.43/ Codex CLI0.145.0against a live 37-entry Copilot catalog, with realcodex execruns rather than minimal text probes — presence inGET /modelsdoes not prove a model works on/chat/completions, and a text-only success does not prove it survives function tools plusreasoning_effort. All 33 generative models returned successfully under the resulting mapping; before it, the seven above returnedunsupported_api_for_modelor the function-tools/reasoning incompatibility error.In-repo:
bun run typecheckclean.tests/github-copilot-wire-defaults.test.tscovers both wire groups, dated snapshots, explicit-override precedence in both directions, an out-of-allow-list override falling through to the default, resolve idempotence, credential/destination survival across the swap, non-OpenAI-wire providers being left alone, and seed coverage.tests/adapter-resolve.test.ts(defaults do not leak to other providers) andtests/router.test.ts(the selector hint).tests/{adapter-resolve,router,reasoning-effort,effort-policy}.test.ts+ the new file: 122 pass / 0 fail. The fullbun run testsuite was not run locally — please let CI be the gate.Not changed
With
adapter: "openai-responses"and noresponsesPath, the URL builds ashttps://api.githubcopilot.com/v1/responses. Copilot's own documented path is/responses. The field verification reported HTTP 200 on real Codex runs through/v1/responses, so Copilot evidently accepts both and I left it alone rather than guess without a Copilot subscription to test against. Worth a follow-up if a maintainer can confirm.Maintenance
The map needs a recheck whenever Copilot adds or changes models; the rationale and the verification caveat are recorded in the source comment. Automatic derivation from live discovery (if Copilot exposes per-model
supported_endpoints) would be the durable fix and is deliberately out of scope here.Docs
English
guides/providers.mdgains a "GitHub Copilot model wires" section (affected models, both error shapes, the opt-out example, and the qualified selector);reference/configuration.mdnotes built-in defaults on themodelAdaptersrow. ko/ja/zh-cn/ru get matching notes so the locales do not contradict the English source.Summary by CodeRabbit
New Features
Bug Fixes
Documentation
modelAdaptersprecedence.Tests