Skip to content

agentHost/claude: add CAPI-backed local Anthropic proxy service#313677

Merged
TylerLeonhardt merged 2 commits into
mainfrom
tyler/colourful-rooster
May 1, 2026
Merged

agentHost/claude: add CAPI-backed local Anthropic proxy service#313677
TylerLeonhardt merged 2 commits into
mainfrom
tyler/colourful-rooster

Conversation

@TylerLeonhardt
Copy link
Copy Markdown
Member

Summary

Lands ClaudeProxyService and supporting helpers under src/vs/platform/agentHost/node/claude/ — a refcounted local HTTP proxy that speaks the Anthropic Messages API on the inbound side and ICopilotApiService on the outbound side. A future ClaudeAgent (Phase 4) will run a Claude Agent SDK subprocess pointing at this proxy via ANTHROPIC_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:

Route Behavior
GET / unauthenticated health check ('ok')
GET /v1/models filtered to vendor Anthropic + supported_endpoints: '/v1/messages', IDs translated CAPI→SDK, reshaped into Anthropic.ModelInfo page envelope
POST /v1/messages non-streaming + SSE streaming pass-through; model field translated SDK→CAPI on the way in and CAPI→SDK on the way out
POST /v1/messages/count_tokens 501 api_error (CAPI has no equivalent)
anything else (authed) 404 not_found_error

Modules:

  • claudeProxyService.ts — interface, impl, IClaudeProxyHandle, refcounted lifecycle, http.createServer(), dispatch, route handlers
  • claudeModelId.ts — bidirectional model ID parser (claude-opus-4-6claude-opus-4-6-20250929)
  • anthropicBetas.tsanthropic-beta allowlist filter (interleaved-thinking, context-management, advanced-tool-use, date-suffix discipline)
  • anthropicErrors.ts — proxy-authored Anthropic error envelopes; uses Anthropic.ErrorType from the SDK
  • claudeProxyAuth.tsBearer <nonce>.<sessionId> parser

Phase 1.5 contract changes in CopilotApiService:

  • New CopilotApiError class carrying Anthropic.ErrorResponse + status — replaces plain Error so the proxy can passthrough the upstream envelope verbatim
  • COPILOT_API_ERROR_STATUS_STREAMING = 520 sentinel for mid-stream errors that have no upstream HTTP status. The proxy coerces this to 502 if it surfaces before SSE headers are sent.

DI registration added in agentHostMain.ts (no callers yet).

Lifecycle

ClaudeProxyService is a Disposable registered on the agent host main store. start() returns refcounted handles; the listener binds lazily on first start() and tears down when refcount hits 0 (or dispose() is called explicitly).

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.

Subprocess ownership invariant (documented on IClaudeProxyHandle): 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.

Tests

187 / 187 passing across the four test files:

  • claudeModelId.test.ts — round-trip parser fixtures
  • anthropicBetas.test.ts — allowlist filter
  • claudeProxyAuth.test.ts — bearer parsing matrix
  • claudeProxyService.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.ts cases for the Phase 1.5 contract additions.

Validation

  • compile-check-ts-native clean
  • eslint clean
  • valid-layers-check clean
  • ✅ All 187 tests green
  • ✅ Real-CAPI smoke verified against all three surfaces (GET /v1/models, streaming + non-streaming POST /v1/messages) with full SSE event sequence and SDK-format model rewrite
  • ✅ Council-reviewed; consensus blockers F1 (concurrent-bind orphan) + F2 (dispose-during-bind) fixed; F3 test coverage added

Drafted 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.

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.
Copilot AI review requested due to automatic review settings May 1, 2026 06:31
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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) under src/vs/platform/agentHost/node/claude/.
  • Extends ICopilotApiService error semantics with CopilotApiError and 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

Comment thread src/vs/platform/agentHost/node/claude/claudeProxyService.ts
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.
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 1, 2026

Screenshot Changes

Base: aa73b3e0 Current: a8a3b6a6

Changed (1)

agentSessionsViewer/BackgroundProvider/Light
Before After
before after

@TylerLeonhardt TylerLeonhardt marked this pull request as ready for review May 1, 2026 06:59
@TylerLeonhardt TylerLeonhardt merged commit 0d54c25 into main May 1, 2026
26 checks passed
@TylerLeonhardt TylerLeonhardt deleted the tyler/colourful-rooster branch May 1, 2026 07:10
@vs-code-engineering vs-code-engineering Bot added this to the 1.119.0 milestone May 1, 2026
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.

3 participants