Skip to content

Support Responses-API-only models (gpt-5.x/6.x reasoning family) - #274

Open
Charles-HL wants to merge 1 commit into
ericc-ch:masterfrom
Charles-HL:feat/responses-api-fallback
Open

Charles-HL wants to merge 1 commit into
ericc-ch:masterfrom
Charles-HL:feat/responses-api-fallback

Conversation

@Charles-HL

Copy link
Copy Markdown

Problem

Copilot's model catalog (GET /models) now tags some models with a
supported_endpoints list 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) and mai-code-1.1-flash. Requesting
one of those through /chat/completions (both the OpenAI-compatible
route and, since it goes through the same createChatCompletions,
the Anthropic-compatible /v1/messages route) fails outright with
not accessible via the /chat/completions endpoint, no matter what
the payload looks like - I checked this directly against
api.githubcopilot.com with a few different models before writing
any code.

Some models (gpt-5.4, gpt-5-mini) list both endpoints and keep
working today; this only affects the endpoint-restricted ones.

Fix

createChatCompletions() now looks up the selected model's
supported_endpoints. When /chat/completions isn't listed but
/responses is, it transparently translates the request into a
Responses API payload, calls /responses, and translates the result
back into the Chat Completions shape everything downstream already
expects (lib/responses-translation.ts, new services/copilot/ create-responses.ts).

Because both existing routes - /chat/completions and the
Anthropic-compatible /v1/messages - already funnel through this one
function, neither route, nor stream-translation.ts/non-stream- translation.ts, needed any changes. The fallback is invisible to
callers: same request in, same response shape out, whether the model
answers via /chat/completions or /responses under 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.com with gpt-5.6-luna:

  • /v1/chat/completions, non-streaming - works
  • /v1/messages (Anthropic-compatible), non-streaming - works
  • /v1/messages, streaming - correct SSE event sequence, matches a
    normal Claude Code stream
  • Tool calls, non-streaming, on both routes - correct tool_calls /
    tool_use output
  • Tool calls, streaming, on /v1/chat/completions - correct
    incremental tool_calls deltas and [DONE] terminator
  • Regression: claude-haiku-4.5 (an unaffected, /chat/completions-
    only model) still works unchanged through /v1/messages

tsc and eslint pass clean on the changed/new files. The 5
pre-existing lint errors on master in unrelated files (lib/ proxy.ts, routes/messages/anthropic-types.ts, routes/messages/ non-stream-translation.ts, start.ts) are untouched by this PR.

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.
@coderabbitai

coderabbitai Bot commented Sep 15, 2026

Copy link
Copy Markdown

Review Change StackReview Change Stack

Walkthrough

The 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. createChatCompletions routes Responses-only models through the new client and translates the result back to Chat Completions formats. Other requests retain the existing route.

Priority: ➖ Normal

Merge Risk: 🟡 Moderate · up to e6e41

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)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 9 functions across 4 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: adding support for Copilot models that expose only the Responses API.
Description check ✅ Passed The description directly explains the endpoint limitation, the translation-based fix, affected routes, supported request modes, and verification performed.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
  • Fix all pre-merge checks with AI
✨ Finishing Touches
✨ Simplify code
  • Create PR with simplified code

Warning

Some tools did not complete. Review the errors below.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

src/lib/responses-translation.ts

ESLint 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.ts

ESLint skipped: the matched ESLint configuration already failed (missing-dependency).

src/services/copilot/create-responses.ts

ESLint skipped: the matched ESLint configuration already failed (missing-dependency).

  • 1 others

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 3803bb94-a07b-4bde-9b25-49b152f2d2f6

📥 Commits

Reviewing files that changed from the base of the PR and between 0ea08fe and e6e41de.

📒 Files selected for processing (4)
  • src/lib/responses-translation.ts
  • src/services/copilot/create-chat-completions.ts
  • src/services/copilot/create-responses.ts
  • src/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 {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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 {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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 300

Repository: 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 200

Repository: 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 300

Repository: 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.ts

Repository: 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.ts

Repository: 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",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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),

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant