Support Responses-API-only models (gpt-5.x/6.x reasoning family) - #274
Charles-HL wants to merge 1 commit into
Conversation
Copilot's model catalog now marks some models with supported_endpoints that don't include /chat/completions - only /responses. Calling /chat/completions for those models fails with "not accessible via the /chat/completions endpoint", regardless of payload shape. createChatCompletions() now checks the selected model's supported_endpoints and, when /chat/completions isn't listed but /responses is, transparently translates the request/response through the Responses API instead (lib/responses-translation.ts). Both existing callers - the /chat/completions route and the Anthropic-compatible /v1/messages route - already go through this one function, so they pick up the fix with no changes of their own, for both streaming and non-streaming, text and tool calls. Verified against the live Copilot API with gpt-5.6-luna: chat completions and messages, streaming and non-streaming, plain text and tool calls, plus a regression check that an unaffected model (claude-haiku-4.5) still works unchanged.
WalkthroughThe pull request adds a Copilot Responses API client and its TypeScript contracts. It translates Chat Completions requests, responses, and stream events into the corresponding Responses API formats. Model metadata now identifies supported upstream endpoints. Priority: ➖ Normal Merge Risk: 🟡 Moderate · up to Responses-only models can return incorrect results, conceal upstream failures, or reject image requests. These compatibility defects should be fixed before merge. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches✨ Simplify code
Warning Some tools did not complete. Review the errors below. 🔧 ESLint
src/lib/responses-translation.tsESLint skipped: missing config or dependency (missing-dependency). The ESLint configuration references a package that is not available in the sandbox. src/services/copilot/create-chat-completions.tsESLint skipped: the matched ESLint configuration already failed (missing-dependency). src/services/copilot/create-responses.tsESLint skipped: the matched ESLint configuration already failed (missing-dependency).
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Advanced
Run ID: 3803bb94-a07b-4bde-9b25-49b152f2d2f6
📒 Files selected for processing (4)
src/lib/responses-translation.tssrc/services/copilot/create-chat-completions.tssrc/services/copilot/create-responses.tssrc/services/copilot/get-models.ts
Included review availability: Your plan provides up to 8 included reviews per hour; 7 remain after this review.
📜 Review details
🔇 Additional comments (2)
src/services/copilot/get-models.ts (1)
55-61: LGTM!src/services/copilot/create-chat-completions.ts (1)
26-31: LGTM!Also applies to: 42-55
| input.push(toMessageItem(message)) | ||
| } | ||
|
|
||
| return { |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Preserve or reject n.
ChatCompletionsPayload permits n, but this translated payload does not carry it. responsesResultToChatCompletion() always creates one choice. A rerouted request with n: 2 silently returns one choice.
Reject n values other than 1 before routing, or implement compatible fan-out behavior.
| if (item.type === "message") { | ||
| const text = item.content.map((part) => part.text).join("") | ||
| content = (content ?? "") + text | ||
| } else { |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- outline ---'
ast-grep outline src/lib/responses-translation.ts
printf '%s\n' '--- target file ---'
cat -n src/lib/responses-translation.ts
printf '%s\n' '--- related response item declarations and usages ---'
rg -n -C 4 'function_call|output_text|Response.*Output|output_item|item\.type|responses-translation' src package.json README.md 2>/dev/null | head -n 300Repository: ericc-ch/copilot-api
Length of output: 25206
🏁 Script executed:
pwd; sed -n '110,170p' src/lib/responses-translation.ts; rg -n -C 3 'function_call|output_text|item.type' src/lib src 2>/dev/null | head -n 200Repository: ericc-ch/copilot-api
Length of output: 12971
🏁 Script executed:
set -eu
cat -n src/lib/responses-translation.ts
printf '\n--- related symbols ---\n'
rg -n -C 4 'function_call|output_text|Response.*Output|output_item|item\.type|responses-translation' src package.json README.md 2>/dev/null | head -n 300Repository: ericc-ch/copilot-api
Length of output: 24412
🏁 Script executed:
set -eu
printf '%s\n' '--- create-responses.ts ---'
cat -n src/services/copilot/create-responses.ts
printf '%s\n' '--- create-chat-completions.ts relevant sections ---'
sed -n '1,180p' src/services/copilot/create-chat-completions.tsRepository: ericc-ch/copilot-api
Length of output: 11350
🏁 Script executed:
set -eu
cat -n src/services/copilot/create-responses.ts
sed -n '1,180p' src/services/copilot/create-chat-completions.tsRepository: ericc-ch/copilot-api
Length of output: 11269
Handle only function_call output items. The non-streaming /responses body is cast without runtime validation, so responsesResultToChatCompletion can receive output types outside ResponseOutputItem. Its else branch converts every non-message item into a tool call. A reasoning item can therefore produce a malformed tool call with missing call_id, name, and arguments, and incorrectly set finish_reason to tool_calls. Check item.type === "function_call" before adding a tool call, then ignore or translate other output types.
|
|
||
| const finishReason = finalFinishReason( | ||
| toolCalls.length > 0, | ||
| result.status === "incomplete", |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Do not translate failed Responses as stop.
Both call sites only distinguish incomplete. For the modeled status: "failed" and response.failed cases, finalFinishReason() returns "stop". Non-streaming callers receive a successful completion, and streaming callers receive a normal terminal chunk followed by [DONE].
Propagate failed Responses through the existing error path instead of emitting a successful completion.
Also applies to: 278-278
|
|
||
| const isAgentCall = payload.input.some( | ||
| (item) => | ||
| item.type === "function_call" || item.type === "function_call_output", |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Pass translated image input to copilotHeaders.
When a request is rerouted through /responses, chatPayloadToResponsesPayload converts image_url parts to input_image. createResponses calls copilotHeaders(state) with vision disabled, so Copilot can reject the request with a missing vision-header error.
Suggested fix
+ const enableVision = payload.input.some(
+ (item) =>
+ item.type === "message"
+ && item.content.some((part) => part.type === "input_image"),
+ )
+
const headers: Record<string, string> = {
- ...copilotHeaders(state),
+ ...copilotHeaders(state, enableVision),
Problem
Copilot's model catalog (
GET /models) now tags some models with asupported_endpointslist that doesn't include/chat/completions-only
/responses. This shows up on the GPT reasoning family(
gpt-5.3-codex,gpt-5.5,gpt-5.6-luna,gpt-5.6-sol,gpt-5.6-terra,gpt-6-astra) andmai-code-1.1-flash. Requestingone of those through
/chat/completions(both the OpenAI-compatibleroute and, since it goes through the same
createChatCompletions,the Anthropic-compatible
/v1/messagesroute) fails outright withnot accessible via the /chat/completions endpoint, no matter whatthe payload looks like - I checked this directly against
api.githubcopilot.comwith a few different models before writingany code.
Some models (
gpt-5.4,gpt-5-mini) list both endpoints and keepworking today; this only affects the endpoint-restricted ones.
Fix
createChatCompletions()now looks up the selected model'ssupported_endpoints. When/chat/completionsisn't listed but/responsesis, it transparently translates the request into aResponses API payload, calls
/responses, and translates the resultback into the Chat Completions shape everything downstream already
expects (
lib/responses-translation.ts, newservices/copilot/ create-responses.ts).Because both existing routes -
/chat/completionsand theAnthropic-compatible
/v1/messages- already funnel through this onefunction, neither route, nor
stream-translation.ts/non-stream- translation.ts, needed any changes. The fallback is invisible tocallers: same request in, same response shape out, whether the model
answers via
/chat/completionsor/responsesunder the hood.Covers text and tool calls, both streaming and non-streaming.
Testing
I don't have automated tests for this (the repo has none for the
copilot service calls, and this needs a live, authenticated Copilot
session to exercise meaningfully), so I verified by hand against the
real
api.githubcopilot.comwithgpt-5.6-luna:/v1/chat/completions, non-streaming - works/v1/messages(Anthropic-compatible), non-streaming - works/v1/messages, streaming - correct SSE event sequence, matches anormal Claude Code stream
tool_calls/tool_useoutput/v1/chat/completions- correctincremental
tool_callsdeltas and[DONE]terminatorclaude-haiku-4.5(an unaffected,/chat/completions-only model) still works unchanged through
/v1/messagestscandeslintpass clean on the changed/new files. The 5pre-existing lint errors on
masterin unrelated files (lib/ proxy.ts,routes/messages/anthropic-types.ts,routes/messages/ non-stream-translation.ts,start.ts) are untouched by this PR.