Skip to content

Conversation

@ananas-viber
Copy link

@ananas-viber ananas-viber commented Jan 15, 2026

Summary

Fixes #8030

Builds on #8393 — Adds synthetic message detection to prevent excessive premium requests.

The official plugin (#8393) uses last?.role !== "user" which fails for synthetic user messages created by OpenCode (compaction, tool attachments, subtasks). This PR detects those synthetic messages and correctly marks them as agent-initiated.

How VSCode Copilot Chat Handles This

From microsoft/vscode-copilot-chat @ main:

// toolCallingLoop.ts:469
userInitiatedRequest: iterationNumber === 0 && !isContinuation && !this.options.request.subAgentInvocationId

VSCode uses explicit flags to mark tool loops, continuations, and subagents as agent. OpenCode doesn't have these flags, so we infer the same intent by checking message history.

Detection Logic

Scenario X-Initiator Charges Premium?
First user message user ✅ Yes
After assistant/tool message exists agent ❌ No
Synthetic user message (compaction, tool result) agent ❌ No

Rules:

  1. If ANY assistant/tool message exists → agent (matches VSCode's tool loop behavior)
  2. If LAST user message is synthetic → agent
  3. Otherwise → user

Synthetic patterns detected:

  • "What did we do so far?" (compaction)
  • "Tool X returned an attachment:" (tool results)
  • "The following tool was executed by the user" (subtasks)

Why This Approach

OpenCode creates synthetic user messages for internal operations that VSCode handles differently. Since we can't use VSCode's explicit flags (iterationNumber, isContinuation, subAgentInvocationId), we infer agent status from:

  1. Presence of assistant/tool messages in history
  2. Pattern matching on synthetic user message content

This aligns with third-party Copilot clients (litellm, copilot-api, crush) that use the same "Sticky" inference approach.

Changes

copilot.ts:

  • Added isSynthetic() and hasSyntheticContent() for pattern matching
  • Added detectAgent() with history-based inference
  • Unified detectVision() for both Completions and Responses API

copilot.test.ts (new):

  • 16 tests covering synthetic detection and agent detection

Verification

Check Result
Prettier ✅ Pass
Type check ✅ Pass
Copilot tests (local) ✅ 16 pass
Copilot tests (Docker) ✅ 16 pass
Rebased on latest dev ✅ (includes #8393)

Related

@ananas-viber ananas-viber marked this pull request as draft January 15, 2026 19:16
@github-actions
Copy link
Contributor

The following comment was made by an LLM, it may be inaccurate:

No duplicate PRs found

@ananas-viber ananas-viber force-pushed the fix/copilot-premium-requests branch 2 times, most recently from 2107784 to 1cd138b Compare January 15, 2026 20:02
@ananas-viber ananas-viber marked this pull request as ready for review January 15, 2026 20:02
@ananas-viber
Copy link
Author

@thdxr Your opencode work is very inspiring. Would appreciate if the team can review my PR.

@ananas-viber ananas-viber force-pushed the fix/copilot-premium-requests branch from 1cd138b to 16042a3 Compare January 15, 2026 20:31
@ananas-viber
Copy link
Author

Also please let me know if you need the isolated docker setup I use for my testing and I will open another PR for that so it's handy for others.

@ananas-viber ananas-viber changed the title fix(opencode): prevent excessive Copilot premium request consumption fix: Prevent excessive Copilot premium request consumption Jan 15, 2026
Fixes the X-Initiator header detection to correctly identify agent-initiated
vs user-initiated requests. The previous logic only checked if the last
message role was not "user", which failed for synthetic user messages
created by message-v2.ts for tool attachments, compactions, and subtasks.

New detection strategy:
1. If ANY assistant/tool message exists -> agent (continuation)
2. If multiple user messages exist -> agent (multi-turn)
3. If user message matches synthetic patterns -> agent

This ensures only the first real user message consumes a premium request.

Fixes anomalyco#8030
Fixes anomalyco#8067
@ananas-viber ananas-viber force-pushed the fix/copilot-premium-requests branch from 16042a3 to 5414c79 Compare January 15, 2026 22:09
Addresses @bowmanjd feedback - removed the flawed 'multiple user messages'
rule. Real user follow-ups now correctly charge premium, matching
Copilot CLI behavior.

Detection logic:
1. If ANY assistant/tool message exists → agent
2. If LAST user message is synthetic → agent
3. Otherwise → user (charges premium)
@ananas-viber ananas-viber force-pushed the fix/copilot-premium-requests branch from 49f6a4e to 7bc7e18 Compare January 15, 2026 22:27
@github-actions
Copy link
Contributor

Thanks for your contribution!

This PR doesn't have a linked issue. All PRs must reference an existing issue.

Please:

  1. Open an issue describing the bug/feature (if one doesn't exist)
  2. Add Fixes #<number> or Closes #<number> to this PR description

See CONTRIBUTING.md for details.

@berenar
Copy link
Contributor

berenar commented Jan 15, 2026

Thanks for this! I've experienced this issue too

@FrescoFlacko
Copy link

This is good.. latest fix made Opencode unbearable with Copilot license. Actually, fixing this faster may allow people to raise less issues since more people are moving over to Opencode with the Copilot integration announcement.

Just wondering: would the synthetic messages have to be maintained? Assuming any change in Copilot's output messages would affect this.

@ananas-viber
Copy link
Author

ananas-viber commented Jan 15, 2026

@FrescoFlacko Great question!

The synthetic patterns are generated by OpenCode itself, not by Copilot:

Pattern Source
"What did we do so far?" message-v2.ts:460 (compaction)
"Tool X returned an attachment:" message-v2.ts:507 (tool results)
"The following tool was executed..." message-v2.ts:466 (subtasks)

So they won't break if Copilot changes - they're internal strings in this repo.

Maintenance risk is low, but not zero. If someone changes these strings in message-v2.ts without updating copilot.ts, detection would fail silently.


Long-term alternatives (not in this PR)

Option A: Metadata flag

  • Add synthetic: true metadata where these messages are created
  • Check the flag instead of pattern matching

Option B: Tool outputs for guidance (per @rekram1-node's suggestion)

  • Instead of creating synthetic user messages for guidance (compaction, tool attachments, subtasks)
  • Embed that guidance in tool outputs directly
  • Since role: "tool" triggers X-Initiator: agent, no pattern matching needed
  • Architecturally cleaner but requires changes to message-v2.ts

This PR is a minimal, focused fix for the immediate issue. Happy to adjust if maintainers prefer a different direction.

@aefmind
Copy link

aefmind commented Jan 16, 2026

Maintenance risk is low, but not zero. If someone changes these strings in message-v2.ts without updating copilot.ts, detection would fail silently.

This solution has weak points:

  • The need of remember to update copilot plugin if the message changes.
  • The user can include this patterns in his own messages to avoid quota consumption in legit user prompts.

So this is fragile and "hackable".

Option A: Metadata flag

Add synthetic: true metadata where these messages are created
Check the flag instead of pattern matching

This is the better way. something like synthetic: true or isAgent: true and when the copilot plugin detect this meta, set role = "assistant"

@ananas-viber
Copy link
Author

@aefmind You raise valid points. Let me address them:

Maintenance risk: The strings are internal to this repo (not from Copilot), so changes would be in the same codebase. But you're right there's no compile-time enforcement.

Hackability: Fair point, though users would need to know the exact patterns, and they'd only be gaming their own billing (not a security issue).

On the metadata approach: You're right that it's architecturally cleaner. I investigated and found:

  • synthetic: boolean already exists on TextPart (message-v2.ts:64)
  • It's already being set in prompt.ts:1374
  • BUT it's not propagated through toModelMessage() to the final messages

Implementing metadata-based detection would require changes to message-v2.ts (core infrastructure), which is a larger scope than this focused fix.

Path forward: This PR ships a minimal fix for the urgent #8030 issue. I'll open a follow-up issue for "migrate to metadata-based synthetic detection" as a cleaner long-term solution.

@nsoufian
Copy link

nsoufian commented Jan 17, 2026

@ananas-viber

GitHub Copilot treats every explicit user input as a new premium request, even when tools or assistant messages were used earlier in the session.

The previous detectAgent rule classified a request as agent-initiated if the conversation history contained any assistant or tool message. Because message history is cumulative, this causes all subsequent user inputs to be permanently marked as "agent" after the first tool invocation.

This behavior is incorrect:

  • A user premium request remains user-initiated regardless of prior tool usage
  • Tool execution does not convert the session into an agent-driven flow
  • Initiator classification must reflect the current user action, not past turns

As a result, X-Initiator is incorrectly set to "agent" for normal user follow-up requests, misrepresenting the request semantics to GitHub Copilot.

Affected code :

  // Rule 1: If any assistant/tool message exists, this is a continuation
  const hasNonUser = messages.some((msg: any) => ["assistant", "tool"].includes(msg.role))
  if (hasNonUser) return true

@rekram1-node
Copy link
Collaborator

I'm discussing w/ copilot team if subagent invocations are okay to not count against quota, it seems like the line is blurry with compliance

@aefmind
Copy link

aefmind commented Jan 17, 2026

@rekram1-node we will wait for the response. In the case of subagents in Copilot VS Code, they aren't counted against quota, but they use the same model as the main agent. The edge case here could be when subagents rely on different models with different cost or even from different providers.

@nsoufian
Copy link

@rekram1-node
I think the main challenge for allowing sub-agent invocations to not count against quota is the Copilot multiplier model, not the agent concept itself.

In VS Code, sub-agents always run on the same model as the primary agent, so the multiplier is fixed and predictable.

In OpenCode, sub-agents can be configured with different models. This creates a scenario where a user could run the primary agent on a low-multiplier model and delegate work to a sub-agent using a higher-multiplier model, effectively bypassing quota constraints.

That model mismatch seems like the real compliance blocker. If OpenCode enforced that sub-agents cannot use a model with a higher multiplier than the primary agent (or forced a fallback to the same model), free sub-agent quota would be easier to justify — though it adds complexity.

This is the only concrete blocker I see to aligning OpenCode sub-agents with Copilot’s quota semantics.

@nsoufian
Copy link

additional context: Copilot Chat does support running sub-agents with a different model, gated behind the chat.customAgentInSubagent.enabled setting. This is discussed in microsoft/vscode#275855

@rekram1-node
Copy link
Collaborator

@rekram1-node I think the main challenge for allowing sub-agent invocations to not count against quota is the Copilot multiplier model, not the agent concept itself.

In VS Code, sub-agents always run on the same model as the primary agent, so the multiplier is fixed and predictable.

In OpenCode, sub-agents can be configured with different models. This creates a scenario where a user could run the primary agent on a low-multiplier model and delegate work to a sub-agent using a higher-multiplier model, effectively bypassing quota constraints.

That model mismatch seems like the real compliance blocker. If OpenCode enforced that sub-agents cannot use a model with a higher multiplier than the primary agent (or forced a fallback to the same model), free sub-agent quota would be easier to justify — though it adds complexity.

This is the only concrete blocker I see to aligning OpenCode sub-agents with Copilot’s quota semantics.

This would never happne unless the user has explicitly configured opencode to do this or is using some external plugin, at worst the same model will be used for subagents, at best a cheaper one

@yanndegat
Copy link

hi !

is there any workaround to use opencode with copilot api in the meantime? currently, the tool is burning the premium requests quota much too fast.
do you know for instance to which version / microversion of opencode we could switch back ? i've tried up to 1.0.x but didnt manage to find one which doesnt have the issue.

thanks a lot and best regards.

@chenmi319
Copy link

hi !

is there any workaround to use opencode with copilot api in the meantime? currently, the tool is burning the premium requests quota much too fast. do you know for instance to which version / microversion of opencode we could switch back ? i've tried up to 1.0.x but didnt manage to find one which doesnt have the issue.

thanks a lot and best regards.

Take a try 1.1.13, use haiku for cheapest test.

opencode upgrade 1.1.13

@caozhiyuan
Copy link
Contributor

@ananas-viber With this modification, entering user prompts multiple times will only consume quota once. However, in the Copilot extension, each time you enter a user prompt, it consumes quota.

@ananas-viber
Copy link
Author

With this modification, entering user prompts multiple times will only consume quota once. However, in the Copilot extension, each time you enter a user prompt, it consumes quota.

@caozhiyuan You're correct - this is the same issue @nsoufian identified earlier. Rule 1's "sticky" approach marks all subsequent user messages as agent once any assistant/tool message exists in history.

The fix would be to only check the last message role (like pi-mono and codecompanion do) rather than scanning the entire history.

However, since @rekram1-node is implementing source-layer fixes directly, this PR may be superseded. I'll leave it open for now in case the detection approach is still wanted as a fallback.

@JuozasIzi1
Copy link

@ananas-viber @rekram1-node what is status of this?

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.

Copilot auth now sets far too many requests as "user" consuming premium requests rapidly

10 participants