You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
feat: forward Claude messages to Copilot native /v1/messages - #280
This PR sends /v1/messages requests for claude* models to the Copilot endpoint /v1/messages. The proxy does not translate these requests to the OpenAI format. The model name, thinking and output_config.effort get to Copilot without changes.
Problem
The proxy translates Anthropic requests to OpenAI /chat/completions. The translation removes thinking and output_config.effort.
Copilot /chat/completions ignores reasoning_effort. A test with low, high and no value gave 81, 82 and 81 completion tokens.
Thus the effort setting of Claude Code has no effect through this proxy.
Changes
src/routes/messages/native-handler.ts: The route sends a request to the native path when the model name starts with claude. The route returns the upstream response body without changes. This includes streaming responses. The route asks for manual approval one time, before it selects the path.
src/services/copilot/create-native-messages.ts: The service sends the request body to {copilotBaseUrl}/v1/messages without changes. It forwards the anthropic-version and anthropic-beta headers of the client. It sets X-Initiator with the same rule as the translation path. It sets copilot-vision-request when the request contains an image, also inside a tool_result block.
src/services/copilot/thinking-type.ts: Copilot can reject a thinking.type with HTTP 400. The error message names a type that the model accepts. The service changes only thinking.type in a copy of the request and sends it one more time. The effort value does not change. After a successful retry, the service keeps the accepted type in memory for each upstream model. Later requests get the change before the first send. If Copilot rejects a type from memory, the service removes it and uses the type that the new error names.
Errors: The service throws HTTPError. The route returns the upstream status and body to the client. The route forwards the retry-after and x-should-retry headers for client retries, and cache-control for streaming responses.
src/routes/messages/anthropic-types.ts: The thinking type accepts all types that clients send. The payload type also declares output_config.
Option: --no-native-messages sends Claude models through the translation path. The README lists the option.
These requests use the translation path as before:
Requests for models that are not Claude models.
/v1/messages/count_tokens.
Requests that end with an assistant message (prefill). Copilot /v1/messages rejects a prefill for Opus 5.5 and Sonnet 5.5 with HTTP 400.
Tests: tests/thinking-type.test.ts checks how the service reads the accepted type from error messages. tests/native-messages.test.ts checks the retry, the cache, the error path, the headers and the route selection.
Why the retry reads the error message
Claude Code selects the thinking type from the model ID that it knows. Some routers change the model ID before the request gets to this proxy. For example, cc-switch maps claude-haiku-4-5 to claude-opus-5-5. Then the client can send a type that the upstream model rejects.
The accepted types are different for each model. These results are from 2026-10-27:
Upstream model
Accepted thinking.type
Rejected thinking.type
claude-opus-5-5
not set, adaptive
enabled, disabled, between_tools
claude-sonnet-5-5
not set, adaptive, between_tools
enabled, disabled
claude-haiku-5-5
not set, adaptive, disabled
enabled, between_tools
A fixed table needs a change for each new model. The error message already names the accepted type. Thus the retry uses the error message.
Effort gets to the model. Direct requests to Copilot /v1/messages used 12, 79 and 241 thinking tokens for low, high and max. Through this proxy, Claude Code used 0, 0, 52, 55 and 136 thinking tokens for low, medium, high, xhigh and max.
Claude Code 2.1.308 sent requests through cc-switch with four model IDs: claude-fable-5, claude-opus-5, claude-sonnet-5 and claude-haiku-4-5. All requests completed without an error.
A Claude Code session with one tool call completed two turns.
A request with an assistant prefill used the translation path, as before this change.
With --no-native-messages, a Claude request used the translation path and completed.
Model names: Copilot /v1/messages accepted dotted, dashed and dated IDs of the models that it offers, for example claude-opus-4-8, claude-opus-4.8 and claude-haiku-4-5-20251001. Thus the native path does not change model names.
Beta header: Copilot /v1/messages ignored an unknown anthropic-beta value and returned HTTP 200.
Billing: Both endpoints return a copilot_usage object with the same price table for claude-opus-5.5. For the same prompt of approximately 94k tokens, /chat/completions reported 46.94 AI credits. Copilot billed that prompt as a cache write. /v1/messages reported 37.53 AI credits for the same prompt as input tokens.
bun test: all tests pass.
Limits
For each model ID and thinking type, the first request gets one HTTP 400 before the accepted type is in memory. A restart clears the memory.
Claude Code sends no effort value for claude-haiku-4-5. Thus the effort setting has no effect for this model ID.
Requests with an assistant prefill keep the translation path. Thus thinking and effort of these requests do not get to Copilot.
Related PR
#246 also sends Claude requests to /v1/messages. It uses a fixed sanitizer. The sanitizer sets the effort of Opus models to medium. This PR forwards the effort value without changes. This PR changes only the thinking types that Copilot rejects.
Requests for claude* models skip the OpenAI translation, so the model
name, thinking, output_config.effort and anthropic-beta reach Copilot.
- When Copilot rejects the thinking type with HTTP 400, retry once with
the type that the error names and remember it per upstream model.
- Return upstream errors with their status and body, and forward
retry-after, x-should-retry and cache-control.
- Keep the translation path for other models, count_tokens and requests
that end with an assistant prefill.
- Add the --no-native-messages option.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The start command enables native Messages routing by default and provides an option to disable it. Eligible Claude requests go to Copilot’s native /v1/messages endpoint; other requests continue through the translated completion path. Native requests forward selected Anthropic headers and can retry recognized thinking-type rejections, with successful replacements cached per model.
Priority: Normal
Merge Risk: Minimal · up to 6d124
Native model-ID forwarding remains worth checking against Copilot, but no blocking failure is established by the available evidence.
Pre-merge checks: 4 · 1
Failed checks: 1 warning
Check name
Status
Explanation
Resolution
Docstring Coverage
Docstring coverage is 13.64% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Documentation-only edits and deletions within surv…
Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed checks: 4 passed
Check name
Status
Explanation
Title check
The title clearly summarizes the main change: forwarding Claude messages to Copilot's native /v1/messages endpoint.
Description check
The description directly explains the native request path, retry behavior, headers, fallback cases, tests, and known limits described by the changeset.
Linked Issues check
Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check
Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage
Explanation
Docstring coverage is 13.64% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Documentation-only edits and deletions within surviving functions are included; unchanged diff context and fully deleted functions are excluded. Analyzed 22 functions across 10 files. (1 skipped: 1 unsupported.)
Resolution
Write docstrings for the functions missing them to satisfy the coverage threshold.
Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
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/state.ts
ESLint skipped: missing config or dependency (missing-dependency). The ESLint configuration references a package that is not available in the sandbox.
src/routes/messages/anthropic-types.ts
ESLint skipped: the matched ESLint configuration already failed (missing-dependency).
src/routes/messages/handler.ts
ESLint skipped: the matched ESLint configuration already failed (missing-dependency).
7 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.
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
Review info
Run configuration
Source mode: Auto
Repository: Unavailable
Branch: Unavailable
File: No configuration file from this repository applied
Commit: Unavailable
Layers: Organization UI
Review profile: ASSERTIVE
Plan: Advanced
Run ID: d7562d33-2c96-42ca-ab7f-37d6e5db8678
Commits
Reviewing files that changed from the base of the PR and between 0ea08fe and 6d124a7.
Files selected for processing: 11
README.md
src/lib/state.ts
src/routes/messages/anthropic-types.ts
src/routes/messages/handler.ts
src/routes/messages/native-handler.ts
src/services/copilot/create-native-messages.ts
src/services/copilot/thinking-type.ts
src/start.ts
tests/fixtures/thinking-errors.ts
tests/native-messages.test.ts
tests/thinking-type.test.ts
Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 7 remain after this review.
Review details
Context from checks skipped due to timeout: 1
GitHub Check: CodeRabbit
Additional context used
Code guidelines: 1
AGENTS.md — auto-discovered
Path-based instructions: 1
Source excerpt: **Testing:** Use Bun's built-in test runner.
CodeRabbit inference engine (AGENTS.md)
Files:
tests/thinking-type.test.ts
tests/native-messages.test.ts
OpenGrep (1.30.1)
src/services/copilot/thinking-type.ts
[ERROR] 30-30: A nonliteral argument is passed to exec/execSync. Identify the receiver: Node child_process methods execute shell commands, while regular-expression and database methods have different contracts. For shell execution, prefer execFile or spawn with an argument array.
(coderabbit.command-injection.exec-js)
[ERROR] 32-32: A nonliteral argument is passed to exec/execSync. Identify the receiver: Node child_process methods execute shell commands, while regular-expression and database methods have different contracts. For shell execution, prefer execFile or spawn with an argument array.
(coderabbit.command-injection.exec-js)
[ERROR] 35-35: A nonliteral argument is passed to exec/execSync. Identify the receiver: Node child_process methods execute shell commands, while regular-expression and database methods have different contracts. For shell execution, prefer execFile or spawn with an argument array.
The translation path maps dated Claude IDs to Copilot-supported IDs. The native path passes payload.model unchanged, so dated model IDs can be rejected upstream.
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the review. I tested this suggestion against Copilot Business on 2026-10-27. The native path keeps the model ID without changes for these reasons:
Copilot /v1/messages accepts dashed and dated IDs of the models that it offers. These requests returned HTTP 200: claude-opus-4-8, claude-opus-4-8-20260115, claude-sonnet-5-20260601 and claude-haiku-4-5-20251001.
translateModelName changes claude-opus-4-8 to claude-opus-4. Copilot returns HTTP 400 for claude-opus-4 with the message "The requested model is not supported.". Thus the suggested change makes requests fail that work now.
Copilot does not offer some models, for example claude-sonnet-4. Requests for these models fail on both paths. This change does not make them worse.
The OpenGrep findings in src/services/copilot/thinking-type.ts are calls to RegExp.prototype.exec with literal patterns. These calls do not run shell commands.
This branch has not been deployed
No deployments
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR sends
/v1/messagesrequests forclaude*models to the Copilot endpoint/v1/messages. The proxy does not translate these requests to the OpenAI format. The model name,thinkingandoutput_config.effortget to Copilot without changes.Problem
/chat/completions. The translation removesthinkingandoutput_config.effort./chat/completionsignoresreasoning_effort. A test withlow,highand no value gave 81, 82 and 81 completion tokens.Changes
src/routes/messages/native-handler.ts: The route sends a request to the native path when the model name starts withclaude. The route returns the upstream response body without changes. This includes streaming responses. The route asks for manual approval one time, before it selects the path.src/services/copilot/create-native-messages.ts: The service sends the request body to{copilotBaseUrl}/v1/messageswithout changes. It forwards theanthropic-versionandanthropic-betaheaders of the client. It setsX-Initiatorwith the same rule as the translation path. It setscopilot-vision-requestwhen the request contains an image, also inside atool_resultblock.src/services/copilot/thinking-type.ts: Copilot can reject athinking.typewith HTTP 400. The error message names a type that the model accepts. The service changes onlythinking.typein a copy of the request and sends it one more time. The effort value does not change. After a successful retry, the service keeps the accepted type in memory for each upstream model. Later requests get the change before the first send. If Copilot rejects a type from memory, the service removes it and uses the type that the new error names.HTTPError. The route returns the upstream status and body to the client. The route forwards theretry-afterandx-should-retryheaders for client retries, andcache-controlfor streaming responses.src/routes/messages/anthropic-types.ts: Thethinkingtype accepts all types that clients send. The payload type also declaresoutput_config.--no-native-messagessends Claude models through the translation path. The README lists the option./v1/messages/count_tokens./v1/messagesrejects a prefill for Opus 5.5 and Sonnet 5.5 with HTTP 400.tests/thinking-type.test.tschecks how the service reads the accepted type from error messages.tests/native-messages.test.tschecks the retry, the cache, the error path, the headers and the route selection.Why the retry reads the error message
Claude Code selects the thinking type from the model ID that it knows. Some routers change the model ID before the request gets to this proxy. For example, cc-switch maps
claude-haiku-4-5toclaude-opus-5-5. Then the client can send a type that the upstream model rejects.The accepted types are different for each model. These results are from 2026-10-27:
thinking.typethinking.typeclaude-opus-5-5adaptiveenabled,disabled,between_toolsclaude-sonnet-5-5adaptive,between_toolsenabled,disabledclaude-haiku-5-5adaptive,disabledenabled,between_toolsA fixed table needs a change for each new model. The error message already names the accepted type. Thus the retry uses the error message.
Test results
Account type: Copilot Business. Model:
claude-opus-5.5. Date: 2026-10-27./v1/messagesused 12, 79 and 241 thinking tokens forlow,highandmax. Through this proxy, Claude Code used 0, 0, 52, 55 and 136 thinking tokens forlow,medium,high,xhighandmax.claude-fable-5,claude-opus-5,claude-sonnet-5andclaude-haiku-4-5. All requests completed without an error.--no-native-messages, a Claude request used the translation path and completed./v1/messagesaccepted dotted, dashed and dated IDs of the models that it offers, for exampleclaude-opus-4-8,claude-opus-4.8andclaude-haiku-4-5-20251001. Thus the native path does not change model names./v1/messagesignored an unknownanthropic-betavalue and returned HTTP 200.copilot_usageobject with the same price table forclaude-opus-5.5. For the same prompt of approximately 94k tokens,/chat/completionsreported 46.94 AI credits. Copilot billed that prompt as a cache write./v1/messagesreported 37.53 AI credits for the same prompt as input tokens.bun test: all tests pass.Limits
claude-haiku-4-5. Thus the effort setting has no effect for this model ID.thinkingand effort of these requests do not get to Copilot.Related PR
#246 also sends Claude requests to
/v1/messages. It uses a fixed sanitizer. The sanitizer sets the effort of Opus models tomedium. This PR forwards the effort value without changes. This PR changes only the thinking types that Copilot rejects.🤖 Generated with Claude Code