fix(copilot_auth): route Responses-only GPT models through /responses - #58
Open
snair20-1984 wants to merge 1 commit into
Open
fix(copilot_auth): route Responses-only GPT models through /responses#58snair20-1984 wants to merge 1 commit into
snair20-1984 wants to merge 1 commit into
Conversation
Copilot's newer GPT models (gpt-5.5, gpt-5.6-*, gpt-5.4-mini, codex, grok,
mai-code) only accept /responses and 400 with unsupported_api_for_model on
/chat/completions, which is all the plugin ever sent.
- Keep supported_endpoints and max_context_window_tokens from the /models
catalogue and persist a copilot_api ("chat" | "responses") field per
registered model; chat stays the default for dual-endpoint models.
- Build OpenAIResponsesModel when copilot_api == "responses", with a
gpt-5* / codex name heuristic for entries registered before the field.
- Add responses_stream.py: Copilot re-encrypts every item id on every SSE
event and pydantic-ai keys streamed parts by that id, so tool-call
arguments arrived empty and text split per delta. The shim rewrites
id / item_id per output_index so each item keeps one stable id.
Fixes mpfaffenberger#57
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Every Copilot model was built as an
OpenAIChatModel, so every request went to/chat/completions. Copilot's newer GPT models (gpt-5.5, gpt-5.6-luna/sol/terra, gpt-5.4-mini, gpt-5.3-codex, plus grok and mai-code) are Responses-only and reject that with:The Copilot
/modelscatalogue already says which endpoints each model accepts (supported_endpoints), butfetch_copilot_modelsthrew that away andadd_models_to_configpersisted nothing the model factory could key off.Routing those models through
OpenAIResponsesModelclears the 400 but exposes a second problem: Copilot's/responsesproxy re-encrypts every identifier on every SSE event. Theidinoutput_item.added, theitem_idon eachfunction_call_arguments.delta/output_text.delta, and theitem.idinoutput_item.doneare all different opaque strings for the same item. pydantic-ai keys streamed parts by that id, so tool calls came through with empty arguments (read_file ... file_path Field required, retried to death) and text was split into one part per delta.What
utils.py—fetch_copilot_modelsnow returns{"id", "api", "context_length"}entries derived fromsupported_endpointsandcapabilities.limits.max_context_window_tokens.add_models_to_configpersists a newcopilot_api: "chat" | "responses"field and uses the catalogue context length (terra was registered at 128k; the API says 400k). Chat stays the default for dual-endpoint models (gpt-5.4, gpt-5-mini) and for Claude/Gemini, so thereasoning_opaquepath is untouched. Bare-string ids and theDEFAULT_COPILOT_MODELSfallback still work.register_callbacks.py—_create_copilot_modelpicksOpenAIResponsesModelwhencopilot_api == "responses". Entries written before the field existed fall back to a name heuristic (gpt-5*/codex), so existingcopilot_models.jsonfiles are fixed without a re-login. Provider, headers,_CopilotAuthand base_url handling are unchanged; the chat branch still returns the same class as before, so this rebases cleanly onto fix(copilot_auth): tolerate missing object/index in chat completions #23'sCopilotChatModel.responses_stream.py(new) — httpx byte-stream shim, same shape as thereasoning_client.pyinterceptor, that rewritesid/item_idperoutput_indexso every event for an item shares the first id seen. Installed only on the Responses path;/chat/completions, non-streaming JSON and error responses pass throughsenduntouched.Tests
tests/test_copilot_auth_utils.py(new):preferred_api_for_endpointstruth table, catalogue parsing with the real response shape, registration writescopilot_apiand context length, normalised entries round-trip.tests/test_copilot_responses_stream.py(new): id normalisation across the exact event sequence captured from gpt-5.6-terra, chunk-boundary buffering, non-JSON/[DONE]passthrough,MockTransportcoverage that only streaming/responsesbodies are wrapped.tests/test_copilot_auth_model.py: Responses vs chat class selection, heuristic fallback, shim installed only on the Responses path, Claude keeps its reasoning profile.Full suite: 2181 passed, 2 skipped.
ruff check/ruff format --checkclean.Verified live against an enterprise Copilot host:
copilot-gpt-5.6-terraanswers and completes a multi-tool round trip (list_files→read_file→ answer),copilot-claude-sonnet-5(chat +reasoning_opaque) andcopilot-gpt-5.4(dual-endpoint, chat) still work, and a legacy entry withoutcopilot_apiselects the Responses model via the heuristic.Follow-ups (not in this PR)
model_factorystill stripsreasoning_effortfor all Copilot GPT models because/chat/completionsrejected it; the/responsescatalogue entries advertisereasoning_effortsupport, so that can now be enabled behindcopilot_api == "responses".httpx.AsyncClient, which triggers pydantic-ai'shttpx2deprecation warning; separate change.Fixes #57