Skip to content

feat: add GitHub Copilot App support via OpenAI-compatible chat completions - #279

Merged
Ingwannu merged 7 commits into
lidge-jun:devfrom
HaydernCenterpoint:feat/github-copilot-app-support
Jul 23, 2026
Merged

feat: add GitHub Copilot App support via OpenAI-compatible chat completions#279
Ingwannu merged 7 commits into
lidge-jun:devfrom
HaydernCenterpoint:feat/github-copilot-app-support

Conversation

@HaydernCenterpoint

Copy link
Copy Markdown
Contributor

Summary

  • Add OpenAI-compatible POST /v1/chat/completions so GitHub Copilot App can use OpenCodex as a custom Model provider.
  • Translate Chat Completions requests into the existing Responses path and bridge stream/non-stream replies back to Chat Completions shape.
  • Document setup (docs/github-copilot-app.md) and cover discovery/chat endpoint behavior with tests.

This is a client integration for GitHub Copilot App (BYOK / Model providers). It is separate from the existing experimental upstream github-copilot provider.

Replaces #278 (opened against main by mistake).

How to use

  1. Run OpenCodex (ocx start)
  2. In GitHub Copilot App → Settings → Model providers → Add provider
  3. Base URL: http://127.0.0.1:10100/v1
  4. API key: empty on loopback
  5. Sync models / add model by id (provider/model)

Test plan

  • bun test tests/chat-completions-endpoint.test.ts
  • bun test tests/server-auth.test.ts -t "root fallback"
  • Manual: GET /v1/models returns OpenAI list shape
  • Manual: POST /v1/chat/completions stream + non-stream succeed for configured providers
  • Manual: GitHub Copilot App can chat after pointing at /v1 and selecting an authenticated model

Expose POST /v1/chat/completions so GitHub Copilot App and other OpenAI-compatible clients can use OpenCodex as a Model provider via GET /v1/models + chat completions, reusing the existing Responses path.
Copilot AI review requested due to automatic review settings July 22, 2026 10:01

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 12fb3107cd

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/chat/outbound.ts Outdated
Comment on lines +193 to +198
// Prefer item_id matching when present; otherwise use the last open tool index.
let toolIndex = nextToolIndex > 0 ? nextToolIndex - 1 : 0;
const itemId = typeof data.item_id === "string" ? data.item_id : undefined;
if (itemId) {
// Some bridges put call_id on the item, not item_id; keep last index.
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Route tool argument deltas by item id

When a Responses stream opens more than one function_call before argument deltas arrive, such as parallel tool calls from the native Responses passthrough path, this branch ignores data.item_id and always emits every argument chunk for nextToolIndex - 1. That attaches earlier tools' arguments to the last opened tool in the Chat Completions stream, so the client can execute the wrong tool call with empty or corrupted arguments. Keep a map from Responses item.id/item_id to the Chat tool index and use it for these deltas.

Useful? React with 👍 / 👎.

Comment on lines +121 to +123
const upstream = await handleResponses(internalReq, config, logCtx, {
abortSignal: req.signal,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Finalize logs for native chat passthrough streams

When the selected model routes to the native openai-responses passthrough path, handleResponses returns a native passthrough SSE response that responseWithDeferredRequestLog deliberately does not wrap; the existing /v1/responses path compensates by passing native terminal/cancel callbacks into handleResponses. This new endpoint only passes abortSignal, so Copilot/App chat requests against bare native gpt-* models stream successfully but never finalize in the request log/usage view. Pass the same native passthrough terminal and cancel logging hooks here before wrapping the response.

Useful? React with 👍 / 👎.

Comment on lines +68 to +76
if (route.provider.adapter === "openai-responses") {
_nativeRoute = true;
// ChatGPT backend rejects store:true and unsupported sampling knobs.
internalBody.store = false;
delete internalBody.max_output_tokens;
delete internalBody.temperature;
delete internalBody.top_p;
delete internalBody.stop;
delete internalBody.user;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve API-key Responses chat controls

This branch runs for every openai-responses provider, including the openai-apikey OpenAI API route, not just the ChatGPT forward backend mentioned in the comment. In that scenario a Chat Completions caller's max_tokens, temperature, top_p, stop, and user fields are removed before the platform /v1/responses request, so clients cannot cap output length or apply sampling/stop controls and may get unexpectedly long or differently sampled completions. Gate this scrubbing to the ChatGPT forward provider and leave supported API-key parameters intact.

Useful? React with 👍 / 👎.

Comment thread src/server/chat-completions.ts Outdated

const headers = new Headers({ "content-type": "application/json" });
for (const name of FORWARD_HEADERS) {
if (name === "authorization") continue;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Forward the direct-mode caller bearer

When the OpenAI provider is configured in Direct mode, handleResponses requires the caller's ChatGPT Authorization bearer and forwards that identity upstream. This new Chat Completions replay path explicitly drops the caller authorization header before constructing the internal /v1/responses request, and may substitute the main login token instead, so direct-mode clients either get a 401 when no main token exists or silently run as the wrong account. Preserve the caller bearer for direct-mode native routes while still keeping proxy admission secrets out of upstream requests.

Useful? React with 👍 / 👎.

Comment thread src/chat/inbound.ts
Comment on lines +275 to +276
const text = responseFormatToText(raw.response_format);
if (text) body.text = text;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve response_format for routed chat providers

When a Chat Completions client requests JSON mode or a JSON schema against a non-native routed chat provider, this stores the request as Responses text.format, but the parser only reduces that to a boolean _structuredOutput and the openai-chat adapter never serializes it back to response_format. The model call therefore runs unconstrained even though the client asked for JSON/schema output, which can break clients that parse the reply as structured data. Carry the format/schema through to the routed chat adapter or reject unsupported formats instead of silently dropping them.

Useful? React with 👍 / 👎.

@lidge-jun

Copy link
Copy Markdown
Owner

Thanks — this is substantial work and the endpoint itself is wanted (it's effectively the general /v1/chat/completions surface #208 asks for, so worth titling it that way: "general Chat Completions compatibility, initially for Copilot App"). CI is green, but four correctness blockers before this can merge:

  1. Parallel tool-call streaming is broken. src/chat/outbound.ts reads item_id but ignores it, appending every argument delta to the last tool index. With two overlapping function calls the first tool gets empty arguments and the last gets interleaved ones. The Responses events do provide item_id — deltas need to be routed by it.
  2. Direct-mode auth semantics. src/server/chat-completions.ts unconditionally discards the caller's Authorization and substitutes the main-account token. In direct mode the caller bearer is the user identity — without a main token this 401s, and with one it can execute as a different account. Please follow the existing Responses convention (admission token vs upstream bearer via x-opencodex-api-key).
  3. response_format is accepted but silently dropped at the adapter boundary for routed openai-chat calls — clients expecting JSON/schema output get plain text. Either thread it through or reject with an explicit 400.
  4. Native passthrough never finalizes the request log — the terminal/cancel callbacks that /v1/responses passes are missing, so successful native gpt-* requests leave usage/request-log entries incomplete.

Tests to add alongside: parallel tool deltas, direct-mode identity, structured output (or its 400), and native terminal/cancel logging. Also please document the supported-field subset (penalties, n, logprobs etc. are unverified) rather than implying full Chat Completions parity. Happy to re-review once these land.

@Ingwannu Ingwannu left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

This integration is valuable and the auth/destination/secret posture is generally consistent with the existing data-plane, but two correctness blockers must be fixed before merge.

  1. response.function_call_arguments.delta carries item_id, but the new bridge ignores it and appends every delta to the last opened tool index. Interleaved parallel calls therefore corrupt arguments: in my two-call repro, call 1 ended empty and call 2 received concatenated malformed JSON. response.output_item.done can also drop final arguments after an added frame. Track tool calls by item id/call id and emit each delta/final snapshot to the matching index.

  2. response.failed and truncated internal streams are converted into a normal chat-completion assistant delta containing [error] ... followed by [DONE]; non-streaming collection then returns HTTP 200 with model-authored-looking error text. Preserve typed failure/truncation state and return an OpenAI-compatible error/non-success response instead of success content.

Please add adversarial coverage for two interleaved function calls, done-frame final arguments, streaming failure, non-streaming failure, and truncated internal streams. The current full suite/CI passes because those cases are not covered. No malware, backdoor, credential persistence, unsafe process/filesystem behavior, or dependency concern was found.

HaydernCenterpoint and others added 2 commits July 22, 2026 18:06
Route parallel tool argument deltas by Responses item_id, preserve
direct-mode caller Authorization with Responses-style admission, reject
unsupported routed response_format, and finalize native passthrough
request logs. Document the verified Chat Completions field subset.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…ion-review

[WRONG BRANCH] fix(chat-completions): address Copilot App review blockers
@HaydernCenterpoint

Copy link
Copy Markdown
Contributor Author

Thanks for you guys review. I will update it when im home.

@lidge-jun

Copy link
Copy Markdown
Owner

Re-reviewed head 43cc1a8a — thanks for the fast turnaround. All four original blockers are genuinely fixed:

  1. ✅ Parallel tool deltas now route by item_id (src/chat/outbound.ts), with an interleaved-delta test.
  2. ✅ Direct-mode identity preserved; Responses-style admission applied (src/server/index.ts, chat-completions.ts), with identity tests.
  3. response_format: mapped for native, explicit 400 for routed openai-chat.
  4. ✅ Native terminal/cancel logging wired idempotently, with a success-terminal test.

Full CI is green on the new head. Two remaining blockers before merge:

  1. Failures still masquerade as HTTP 200. fail() in src/chat/outbound.ts:137-147 converts errors into assistant text ([error] ...) followed by [DONE]; response.failed (:260-265) and truncated SSE both take this path, and non-streaming collects it as a normal completion with 200 (chat-completions.ts:183-199). Chat Completions clients need a real error: non-streaming should return an error status + OpenAI-style error body; streaming should emit an error event / terminate abnormally rather than a clean [DONE]. Please add failure/truncation regression tests.
  2. output_item.done final arguments are dropped when the call was already registered from output_item.added (outbound.ts:213-243) — if the done-frame snapshot contains the authoritative final arguments, they're lost. Reconcile the done-frame item.arguments (last-write-wins) and add a done-frame test.

Nice-to-have (non-blocking): a client-cancel logging test, and a remote-mode test covering x-opencodex-api-key admission + upstream bearer separation. With the two blockers addressed this is mergeable.

Owner review blockers on lidge-jun#279:
- Failures no longer masquerade as HTTP 200/[DONE] success completions.
  Stream emits an OpenAI-style error frame and closes without [DONE];
  non-stream returns an error status + error body.
- output_item.done final arguments are last-write-wins when a tool call
  was already registered from output_item.added.
- Regression tests for failure, truncation, and done-frame snapshots.
@HaydernCenterpoint

Copy link
Copy Markdown
Contributor Author

@lidge-jun Updated on head 152e37ea — the two remaining blockers from your re-review should now be addressed.

1. Failures no longer masquerade as HTTP 200 / clean [DONE]

  • fail() in src/chat/outbound.ts now emits an OpenAI-style error SSE frame and closes the stream without [DONE] (no more assistant text like [error] ... + success termination).
  • Non-streaming path: collectChatCompletion() surfaces ChatCompletionsStreamError, and handleChatCompletions returns a real error status + OpenAI-style error body instead of a 200 completion.

2. output_item.done final arguments reconciled (last-write-wins)

  • When a tool call was already registered from output_item.added, the done-frame item.arguments snapshot is now emitted as the authoritative final arguments (last-write-wins), instead of being dropped.

Tests added

  • done-frame final arguments reconcile
  • stream response.failed → error frame, no clean [DONE]
  • truncated stream → error frame, no clean [DONE]
  • non-stream e2e failure → error status + body
  • stream e2e failure → error frame, no clean [DONE]

Local: bun test tests/chat-completions-endpoint.test.ts16 pass / 0 fail.

Ready for re-review. Thanks!

…tGPT clients

GitHub Copilot App / replace-style clients overwrite the whole function object
on each tool_calls delta. Emitting arguments-only deltas after output_item.added
dropped function.name, so the next turn 400'd with:
  tool_calls entries require function.name

- Always re-emit function.name on args deltas and done-frame snapshots
- Recover missing names from earlier same-call_id function_calls on inbound
- Regression tests for named deltas + call_id name recovery
@HaydernCenterpoint

Copy link
Copy Markdown
Contributor Author

Follow-up fix on head for ChatGPT / Copilot App tool-call history:

Bug: CAPIError: 400 tool_calls entries require function.name

Cause: after output_item.added, subsequent args/done deltas sometimes emitted function.arguments without function.name. Clients that replace (not merge) the function object then stored nameless tool_calls and the next turn 400'd in chatCompletionsToResponsesBody.

Fix:

  • Always re-emit function.name on args deltas and done-frame snapshots (src/chat/outbound.ts)
  • Recover missing names from earlier same-call_id function_calls on inbound (src/chat/inbound.ts)
  • Tests: named deltas + call_id name recovery

bun test tests/chat-completions-endpoint.test.ts18 pass / 0 fail

Narrow toolIndex after Map.get so done-frame name maps accept number,
and avoid ReadableStreamReadResult assignability mismatch in collectChatCompletion.
@HaydernCenterpoint

Copy link
Copy Markdown
Contributor Author

CI typecheck fix pushed on head:

src/chat/outbound.ts

  • Narrow toolIndex after Map.get (was number | undefined into Map.set/get)
  • Avoid ReadableStreamReadResult assignability mismatch in collectChatCompletion

Local: bun x tsc --noEmit → clean.

@HaydernCenterpoint
HaydernCenterpoint deleted the feat/github-copilot-app-support branch July 22, 2026 15:22
@HaydernCenterpoint
HaydernCenterpoint restored the feat/github-copilot-app-support branch July 23, 2026 00:59
@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d50ad944-fe99-4fff-85a5-1bc9ac5c9585

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@Ingwannu Ingwannu left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Re-reviewed the final head after all requested fixes. Parallel tool deltas now route by item id, done-frame arguments are reconciled, direct-mode identity/admission semantics match Responses, routed structured output fails explicitly, native request logs finalize, and failed/truncated internal streams no longer become successful assistant text. I also removed the trailing diff-check whitespace with the contributor branch’s maintainer-edit permission. The five-PR current-dev integration stack passed 3,667 tests / 0 failures, and this final head passed Linux, macOS, Windows, npm-global, typecheck, GUI, and CodeRabbit checks. The lone label failure is a fork-token 403 while applying a label, not a code failure. Approved.

@Ingwannu
Ingwannu merged commit 8aa9b02 into lidge-jun:dev Jul 23, 2026
8 of 9 checks passed
eachann1024 pushed a commit to eachann1024/opencodex that referenced this pull request Jul 23, 2026
eachann1024 pushed a commit to eachann1024/opencodex that referenced this pull request Jul 23, 2026
…chat-completions SSE via shared decoder (Sol audit blockers 1+3)
eachann1024 pushed a commit to eachann1024/opencodex that referenced this pull request Jul 23, 2026
…omptly releases idle upstream (Sol re-verify blocker)
eachann1024 pushed a commit to eachann1024/opencodex that referenced this pull request Jul 23, 2026
eachann1024 pushed a commit to eachann1024/opencodex that referenced this pull request Jul 23, 2026
Wibias added a commit that referenced this pull request Jul 25, 2026
Extend the API Access page and /api/keys metadata for issue #357 follow-up work.
PR #279 already provides /v1/chat/completions on dev; this slice focuses on
dashboard guidance, endpoint metadata, model catalog visibility, and copy/test UX.

- add buildApiAccessEndpoints() and richer /api/keys response
- expand API Access UI with base URL, protocol endpoints, auth notes, and examples
- show externally callable models with copy/test actions
- add regression test and i18n coverage
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.

4 participants