Skip to content

[WRONG BRANCH] fix(providers): un-obfuscate GitHub Copilot streamed tool-call arguments - #990

Closed
Simon-Opopeee wants to merge 4 commits into
lidge-jun:mainfrom
Simon-Opopeee:fix/copilot-obfuscated-tool-call-stream
Closed

[WRONG BRANCH] fix(providers): un-obfuscate GitHub Copilot streamed tool-call arguments#990
Simon-Opopeee wants to merge 4 commits into
lidge-jun:mainfrom
Simon-Opopeee:fix/copilot-obfuscated-tool-call-stream

Conversation

@Simon-Opopeee

@Simon-Opopeee Simon-Opopeee commented Aug 4, 2026

Copy link
Copy Markdown

What

GitHub Copilot's /responses wire is not compatible with Responses clients for the vscode-chat integration. Agentic turns on Responses-only Copilot models (gpt-5.6-luna, gpt-5.6-sol, gpt-5.6-terra, gpt-5.4, gpt-5.4-mini, gpt-5.5, gpt-5.3-codex) stall in Codex clients: the app stays stuck on "thinking" and tool calls never complete. Fixes #989.

Root cause (three layers)

  1. Function-call argument deltas are obfuscated (ciphertext delta + obfuscation field); the .done event carries the plaintext arguments.
  2. Reasoning items ship as GitHub-encrypted encrypted_content with no plaintext summary; clients cannot decrypt them.
  3. The response id and every item id are re-encrypted per event, so the terminal completed payload disagrees with the streamed events (call_id is the only stable field).

Fix

New createGithubCopilotObfuscationRewrite() in src/server/sse-payload-rewrite.ts, wired into the passthrough payloadRewrites composition when route.providerName === "github-copilot" (applies to all Copilot models on both relay paths):

  • Obfuscated deltas become empty deltas; the plaintext arguments are re-emitted as one delta immediately before .done, keeping the stream protocol-conformant.
  • Reasoning items with encrypted_content are replaced with a canonical empty plaintext summary (in output_item events and the terminal completed payload).
  • Response id and item ids are pinned to their first-seen values across created, output_item, and completed events.
  • The obfuscation metadata is stripped from every event; clean events pass through byte-identical.

Tests

Added to tests/sse-payload-rewrite.test.ts: ciphertext deltas → empty deltas + plaintext delta before .done; reasoning neutralization; response/item id pinning; clean events pass through unchanged. bun test tests/sse-payload-rewrite.test.ts and bun x tsc --noEmit pass.

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown

⚠️ Wrong target branch

This pull request currently targets main, but pull requests must target one of dev.

@Simon-Opopeee Please retarget this PR to dev. All contributions go to dev; main receives only release promotions. See our Contributing guide for details. Thanks! 🙏

Its title has been prefixed with [WRONG BRANCH].

This pull request is being kept as a draft automatically. Once every issue above is resolved, it will be marked ready for review again.

@github-actions github-actions Bot changed the title fix(providers): un-obfuscate GitHub Copilot streamed tool-call arguments [WRONG BRANCH] fix(providers): un-obfuscate GitHub Copilot streamed tool-call arguments Aug 4, 2026
@github-actions
github-actions Bot marked this pull request as draft August 4, 2026 09:05
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The change adds a GitHub Copilot SSE rewrite that removes obfuscation metadata, suppresses ciphertext argument deltas, and emits plaintext arguments before matching completion events. The rewrite applies only to GitHub Copilot streaming responses.

Changes

GitHub Copilot obfuscation handling

Layer / File(s) Summary
Obfuscation event reconstruction
src/server/sse-payload-rewrite.ts, tests/sse-payload-rewrite.test.ts
The new rewrite removes obfuscation, replaces ciphertext argument deltas with empty deltas, tracks item_id values, and emits plaintext arguments before matching done events. Tests cover function-call deltas, text deltas, clean events, and unsupported payloads.
Provider-specific rewrite wiring
src/server/responses/core.ts
GitHub Copilot streaming responses compose the new rewrite with the existing SSE rewrites. Other providers do not use it.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant GitHubCopilot
  participant SSEPayloadRewrite
  participant ResponsesClient
  GitHubCopilot->>SSEPayloadRewrite: Send obfuscated function-call delta
  SSEPayloadRewrite->>ResponsesClient: Send empty delta without obfuscation
  GitHubCopilot->>SSEPayloadRewrite: Send done event with plaintext arguments
  SSEPayloadRewrite->>ResponsesClient: Send plaintext arguments delta
  SSEPayloadRewrite->>ResponsesClient: Send matching done event
Loading

Possibly related issues

Possibly related PRs

  • lidge-jun/opencodex#940 — Refactors the same SsePayloadRewrite infrastructure used by this obfuscation rewrite.
  • lidge-jun/opencodex#363 — Also changes streamed function-call argument handling to preserve correct plaintext deltas.
  • lidge-jun/opencodex#947 — Changes the same response streaming pipeline where the new GitHub Copilot rewrite is composed.

Suggested reviewers: ingwannu, lidge-jun, wibias

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The rewrite in src/server/sse-payload-rewrite.ts and its integration in core.ts address all coding requirements in issue #989.
Out of Scope Changes check ✅ Passed All changes are limited to the GitHub Copilot SSE rewrite, its pipeline integration, and focused tests.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: removing obfuscation from GitHub Copilot streamed tool-call arguments.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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: 1

🤖 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 1772-1774: Extract the github-copilot provider check and
createGithubCopilotObfuscationRewrite call into a separate rewrite flag
variable, then include that flag in the needsClientRewrite conditional
expression on Line 1765 so the rewrite is considered when determining whether
clientBody should use rewrittenBody instead of nativeBody. Use the same rewrite
flag variable when building payloadRewrites to ensure win32EagerRewrite and
selectEagerPath apply the rewrite consistently. Add a focused regression test
covering the Windows native-passthrough relay path and eager relay selection
path to verify the Copilot rewrite is applied end-to-end.
🪄 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: ab306201-5f12-413f-acdb-b0b75289d10d

📥 Commits

Reviewing files that changed from the base of the PR and between f9b9440 and 0602e6f.

📒 Files selected for processing (3)
  • src/server/responses/core.ts
  • src/server/sse-payload-rewrite.ts
  • tests/sse-payload-rewrite.test.ts

Comment on lines +1772 to +1774
route.providerName === "github-copilot"
? createGithubCopilotObfuscationRewrite()
: undefined,

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

Include the Copilot rewrite in needsClientRewrite.

On Line 1765, needsClientRewrite excludes the condition added on Lines 1772-1774. On Windows, clientBody then returns nativeBody and bypasses rewrittenBody. If selectEagerPath selects the eager relay, it also omits rewritePayload because win32EagerRewrite uses the same false gate. The client receives the original ciphertext deltas.

Define one Copilot rewrite flag. Include it in needsClientRewrite. Use the same flag when building payloadRewrites. Add a focused regression test for the Windows and eager relay paths.

Proposed fix
       const repairConfig = route.provider.responsesItemIdRepair;
-      const needsClientRewrite = imageGenCallAliases.size > 0 || hasResponsesItemIdRepair(repairConfig);
+      const needsGithubCopilotObfuscationRewrite = route.providerName === "github-copilot";
+      const needsClientRewrite = imageGenCallAliases.size > 0
+        || hasResponsesItemIdRepair(repairConfig)
+        || needsGithubCopilotObfuscationRewrite;
       // Compose opt-in payload rewrites into one parse/stringify pass (image-gen restore first).
       const payloadRewrites = [
         createImageGenCallRestoreRewrite(imageGenCallAliases),
         hasResponsesItemIdRepair(repairConfig)
           ? createResponsesItemIdPayloadRewrite(repairConfig!, translatorBudget)
           : undefined,
-        route.providerName === "github-copilot"
+        needsGithubCopilotObfuscationRewrite
           ? createGithubCopilotObfuscationRewrite()
           : undefined,
       ].filter((rewrite): rewrite is NonNullable<typeof rewrite> => rewrite !== undefined);

Based on the PR objective, the rewrite must apply to both native-passthrough relay paths. As per path instructions, “A behavior change in src/ should come with a focused regression test near the existing tests for that subsystem.”

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
route.providerName === "github-copilot"
? createGithubCopilotObfuscationRewrite()
: undefined,
needsGithubCopilotObfuscationRewrite
? createGithubCopilotObfuscationRewrite()
: undefined,
🤖 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 1772 - 1774, Extract the
github-copilot provider check and createGithubCopilotObfuscationRewrite call
into a separate rewrite flag variable, then include that flag in the
needsClientRewrite conditional expression on Line 1765 so the rewrite is
considered when determining whether clientBody should use rewrittenBody instead
of nativeBody. Use the same rewrite flag variable when building payloadRewrites
to ensure win32EagerRewrite and selectEagerPath apply the rewrite consistently.
Add a focused regression test covering the Windows native-passthrough relay path
and eager relay selection path to verify the Copilot rewrite is applied
end-to-end.

Source: Path instructions

@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: 0602e6fc66

ℹ️ 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 on lines +1772 to +1774
route.providerName === "github-copilot"
? createGithubCopilotObfuscationRewrite()
: undefined,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Mark Copilot streams as needing a client rewrite

For a GitHub Copilot Responses stream with neither image-generation aliases nor item-ID repair—the normal tool-call case—the new rewrite is added to payloadRewrites while needsClientRewrite remains false. On Windows this prevents win32EagerRewrite and later selects the unchanged nativeBody; configured eager relay also receives no rewritePayload, as does Darwin with streamMode: "eager-relay". The ciphertext therefore still reaches Codex and the tool turn stalls on those paths. Include the Copilot condition in needsClientRewrite and cover the platform gates with a regression test.

AGENTS.md reference: src/AGENTS.md:L19-L19

Useful? React with 👍 / 👎.

};
// The relay writes `data: <payload>` then the block delimiter, so this
// payload becomes two consecutive valid SSE events (delta, then done).
return `${JSON.stringify(deltaEvent)}\n\ndata: ${JSON.stringify(parsed)}`;

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 SSE event names when inserting the delta

When the upstream done frame includes event: response.function_call_arguments.done, as the repository's own src/bridge.ts:18-20 framing does, embedding a blank line inside the replacement payload leaves that event field attached to the synthetic delta and emits the original done data as an unnamed message event. Clients that dispatch by SSE event name therefore receive neither event under its correct name and can still fail to commit the tool call. Emit two complete blocks with their respective event: fields, and test a named upstream frame.

AGENTS.md reference: src/AGENTS.md:L19-L19

Useful? React with 👍 / 👎.

type: "response.function_call_arguments.delta",
item_id: parsed.item_id,
output_index: parsed.output_index,
sequence_number: parsed.sequence_number,

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 Assign a unique sequence number to the synthetic delta

The inserted delta copies the done event's sequence_number, so the emitted stream contains two consecutive recognized events with the same number—for example, the new test produces both the plaintext delta and done event at sequence 5. Other generated Responses streams increment the sequence for every event in src/bridge.ts:278, while src/server/ws-bridge.ts:247 forwards rewritten payloads verbatim; clients that order or deduplicate by this field can therefore discard either the plaintext arguments or the completion event. Track an insertion offset and renumber the done and subsequent events so the sequence remains unique and monotonic.

AGENTS.md reference: src/AGENTS.md:L19-L19

Useful? React with 👍 / 👎.

Comment thread src/server/sse-payload-rewrite.ts Outdated
if (hadObfuscation) delete parsed.obfuscation;
const type = parsed.type;
if (type === "response.function_call_arguments.delta" && hadObfuscation) {
if (typeof parsed.item_id === "string") obfuscatedCallIds.add(parsed.item_id);

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 Bound tracked obfuscated call IDs

If a malformed, truncated, or long-running Copilot stream sends obfuscated deltas for distinct item_id values without matching done events, every ID remains in obfuscatedCallIds until the entire stream is collected. The relay releases its transient translator-budget charge after each event, so this cumulative set is not covered by the existing byte limit and an upstream can grow the proxy heap indefinitely even while client backpressure is working. Charge and cap the tracked IDs, removing them on all item terminal paths, and surface overflow through the existing typed stream-failure representation.

AGENTS.md reference: src/AGENTS.md:L17-L17

Useful? React with 👍 / 👎.

GitHub re-encrypts item_id per SSE event, so delta and done never share an
id; tracking by output_index keeps the plaintext delta injection working.
GitHub encrypts reasoning summaries with its own scheme (re-encrypted per
event); Responses clients cannot decrypt them and stay stuck on thinking.
Replace the ciphertext with a canonical empty plaintext summary in
output_item events and the terminal completed payload.
…n values

GitHub re-encrypts the response id and every item id per SSE event, so the
terminal completed payload disagrees with the streamed events; clients
reconcile by id and fail to finalize the turn. Pin each entity to its
first-seen id across created, output_item, and completed events.
@Simon-Opopeee

Copy link
Copy Markdown
Author

Closing: wrong base (main). Will reopen a single clean PR against dev.

@Simon-Opopeee
Simon-Opopeee deleted the fix/copilot-obfuscated-tool-call-stream branch August 4, 2026 10:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

GitHub Copilot streamed tool-call arguments are obfuscated, stalling agentic turns in Codex clients

1 participant