agentHost/claude: add CAPI-backed local Anthropic proxy service#313677
Merged
Conversation
Nothing wires this up yet — this lands the proxy + supporting helpers
in preparation for the Claude Agent integration. No callers; no behavior
changes for existing agents.
Introduces ClaudeProxyService — a refcounted local HTTP proxy that
speaks the Anthropic Messages API on the inbound side and CopilotApiService
on the outbound side. Lets a Claude Agent SDK subprocess (future work)
connect via ANTHROPIC_BASE_URL + ANTHROPIC_AUTH_TOKEN and see this as
a real Anthropic endpoint while we route through CAPI.
Surfaces:
- GET / health check (unauthenticated)
- GET /v1/models filtered to Anthropic-vendor + /v1/messages
- POST /v1/messages non-streaming + SSE streaming pass-through
- POST /v1/messages/count_tokens 501 (CAPI does not support it)
Other modules:
- claudeModelId.ts parse/format SDK <-> CAPI model IDs (e.g.
claude-opus-4-6 <-> claude-opus-4-6-20250929)
- anthropicBetas.ts filter inbound anthropic-beta headers to a CAPI-
supported allowlist
- anthropicErrors.ts proxy-authored Anthropic error envelopes (uses
Anthropic.ErrorType from @anthropic-ai/sdk)
- claudeProxyAuth.ts parse Bearer <nonce>.<sessionId> auth header
Phase 1.5 contract changes in CopilotApiService:
- introduce CopilotApiError carrying Anthropic.ErrorResponse envelope
- COPILOT_API_ERROR_STATUS_STREAMING (520) sentinel for mid-stream errors
that have no upstream HTTP status; the proxy coerces this to 502 when
surfacing the error before SSE headers are sent.
Lifecycle:
- ClaudeProxyService is a Disposable registered on the agent host main
disposable store. Start() returns refcounted handles; the listener
binds lazily on first start and tears down when refcount reaches 0
(or dispose() is called).
- Concurrent start() calls share an in-flight bind via a _starting
promise to avoid orphaned servers; if dispose() runs while binding,
the just-bound server is torn down and the awaiting caller's promise
rejects.
Tests cover model ID round-trips, beta-header filtering, auth parsing,
the full proxy request lifecycle (non-streaming, streaming, error
mapping, refcounting, concurrent start/dispose, late-binding token
update), and the Phase 1.5 CopilotApiService contract additions.
Subprocess ownership invariant: callers that hand baseUrl + nonce to
a Claude SDK subprocess MUST kill the subprocess before disposing the
handle. After dispose() the proxy may rebind on a different port and
the subprocess would silently lose its endpoint.
Contributor
There was a problem hiding this comment.
Pull request overview
Adds Claude Phase 2 infrastructure to the agent host: a refcounted local HTTP proxy that accepts Anthropic Messages API requests and forwards them to CAPI via ICopilotApiService, with supporting helpers, docs, DI wiring, and thorough test coverage.
Changes:
- Introduces
ClaudeProxyService(+ auth, beta filtering, error envelope helpers, model ID translation) undersrc/vs/platform/agentHost/node/claude/. - Extends
ICopilotApiServiceerror semantics withCopilotApiErrorand a streaming-status sentinel to enable verbatim Anthropic error passthrough. - Adds unit/service tests and in-tree Phase 2 planning/roadmap docs for the Claude proxy work.
Show a summary per file
| File | Description |
|---|---|
| src/vs/platform/agentHost/node/shared/copilotApiService.ts | Adds CopilotApiError + COPILOT_API_ERROR_STATUS_STREAMING and uses typed errors for HTTP + SSE error cases. |
| src/vs/platform/agentHost/node/claude/claudeProxyService.ts | Implements the local Anthropic-compatible proxy server (routes, streaming, refcounted lifecycle, passthrough error mapping). |
| src/vs/platform/agentHost/node/claude/claudeProxyAuth.ts | Adds Authorization: Bearer <nonce>.<sessionId> parsing/validation helper. |
| src/vs/platform/agentHost/node/claude/claudeModelId.ts | Adds bidirectional Claude model ID parsing/normalization (SDK ↔ endpoint) with caching. |
| src/vs/platform/agentHost/node/claude/anthropicBetas.ts | Adds allowlist-based filtering for anthropic-beta header values. |
| src/vs/platform/agentHost/node/claude/anthropicErrors.ts | Adds helpers for proxy-authored Anthropic error envelopes and SSE error framing. |
| src/vs/platform/agentHost/node/claude/CONTEXT.md | Adds Claude agent/proxy terminology and design log context. |
| src/vs/platform/agentHost/node/claude/phase-plan.2.md | Adds Phase 2 plan/design record for the proxy service and contract changes. |
| src/vs/platform/agentHost/node/claude/roadmap.md | Marks Phase 2 as done in the roadmap. |
| src/vs/platform/agentHost/node/agentHostMain.ts | Registers ClaudeProxyService in DI and disposables (no callers yet). |
| src/vs/platform/agentHost/test/node/shared/copilotApiService.test.ts | Updates existing tests to assert CopilotApiError and adds contract coverage. |
| src/vs/platform/agentHost/test/node/claudeProxyService.test.ts | Adds end-to-end proxy lifecycle/route/streaming/error/refcount/abort tests. |
| src/vs/platform/agentHost/test/node/claudeProxyAuth.test.ts | Adds auth parser test matrix. |
| src/vs/platform/agentHost/test/node/claudeModelId.test.ts | Adds model ID parser fixture and round-trip tests. |
| src/vs/platform/agentHost/test/node/anthropicBetas.test.ts | Adds beta allowlist filter tests. |
Copilot's findings
- Files reviewed: 15/15 changed files
- Comments generated: 1
Address PR review: the http.createServer handler closed over `runtime`
before it was assigned. There's a narrow microtask window between
`server.listen()` resolving and runtime construction completing, in
which an incoming request would hit a temporal-dead-zone ReferenceError
on `runtime`.
Fix: pass no handler to `createServer()`, build runtime fully (it can
now be `const`), then `server.on('request', ...)` afterwards. Node's
single-threaded event loop guarantees no `request` event is parsed
and dispatched between `listen` resolving and the synchronous
`server.on('request', ...)` registration, so the handler safely
closes over `runtime` with no TDZ window and no `let` indirection.
Contributor
dmitrivMS
approved these changes
May 1, 2026
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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
Lands
ClaudeProxyServiceand supporting helpers undersrc/vs/platform/agentHost/node/claude/— a refcounted local HTTP proxy that speaks the Anthropic Messages API on the inbound side andICopilotApiServiceon the outbound side. A futureClaudeAgent(Phase 4) will run a Claude Agent SDK subprocess pointing at this proxy viaANTHROPIC_BASE_URL+ANTHROPIC_AUTH_TOKEN, getting Claude models routed through CAPI without ever talking to api.anthropic.com.Note
Nothing wires this up yet — this is preparation for the Claude Agent integration. No callers; no behavior changes for existing agents. The service is registered in DI so Phase 4 can pick it up directly.
What's in this PR
Phase 2 surfaces:
GET /'ok')GET /v1/modelsAnthropic+supported_endpoints: '/v1/messages', IDs translated CAPI→SDK, reshaped intoAnthropic.ModelInfopage envelopePOST /v1/messagesmodelfield translated SDK→CAPI on the way in and CAPI→SDK on the way outPOST /v1/messages/count_tokens501 api_error(CAPI has no equivalent)404 not_found_errorModules:
claudeProxyService.ts— interface, impl,IClaudeProxyHandle, refcounted lifecycle,http.createServer(), dispatch, route handlersclaudeModelId.ts— bidirectional model ID parser (claude-opus-4-6↔claude-opus-4-6-20250929)anthropicBetas.ts—anthropic-betaallowlist filter (interleaved-thinking,context-management,advanced-tool-use, date-suffix discipline)anthropicErrors.ts— proxy-authored Anthropic error envelopes; usesAnthropic.ErrorTypefrom the SDKclaudeProxyAuth.ts—Bearer <nonce>.<sessionId>parserPhase 1.5 contract changes in
CopilotApiService:CopilotApiErrorclass carryingAnthropic.ErrorResponse+ status — replaces plainErrorso the proxy can passthrough the upstream envelope verbatimCOPILOT_API_ERROR_STATUS_STREAMING = 520sentinel for mid-stream errors that have no upstream HTTP status. The proxy coerces this to502if it surfaces before SSE headers are sent.DI registration added in
agentHostMain.ts(no callers yet).Lifecycle
ClaudeProxyServiceis aDisposableregistered on the agent host main store.start()returns refcounted handles; the listener binds lazily on firststart()and tears down when refcount hits 0 (ordispose()is called explicitly).Concurrent
start()calls share an in-flight bind via a_startingpromise to avoid orphaned servers. Ifdispose()runs while binding, the just-bound server is torn down and the awaiting caller's promise rejects.Subprocess ownership invariant (documented on
IClaudeProxyHandle): callers that handbaseUrl+nonceto a Claude SDK subprocess MUST kill the subprocess before disposing the handle. Afterdispose()the proxy may rebind on a different port and the subprocess would silently lose its endpoint.Tests
187 / 187 passing across the four test files:
claudeModelId.test.ts— round-trip parser fixturesanthropicBetas.test.ts— allowlist filterclaudeProxyAuth.test.ts— bearer parsing matrixclaudeProxyService.test.ts— full proxy request lifecycle (non-streaming, streaming, error mapping, refcounting, concurrent start/dispose, dispose-while-binding race, late-binding token update, pre-stream sentinel coercion)Plus new
copilotApiService.test.tscases for the Phase 1.5 contract additions.Validation
compile-check-ts-nativecleaneslintcleanvalid-layers-checkcleanGET /v1/models, streaming + non-streamingPOST /v1/messages) with full SSE event sequence and SDK-format model rewriteDrafted because
Plan + roadmap docs (
CONTEXT.md,phase-plan.2.md,roadmap.md) are committed alongside the code for in-tree design history. If reviewers prefer those land separately, happy to split.