# Developer guide — dispatch-adapter-claude ## Purpose Implements `Dispatch::Adapter::Base` for Anthropic Claude using a personal Pro / Max subscription via the OAuth PKCE flow that the Claude Code CLI uses. The gem lives in `dispatch-adapter-claude/` and depends on `dispatch-adapter-interface ~> 0.2`. ## Research baseline All reverse-engineering notes, OAuth flow details, cloaking rules, and the full interface gap analysis are in: - **`.rules/research/research.md`** — primary research document ## Plan overview The implementation was built in tasks tracked under `.rules/plan/`: | Range | Area | |---|---| | `13-*` – `14-*` | Scaffold and module wiring | | `15-*` – `16-*` | Errors and token store | | `17-*` – `20-*` | PKCE, OAuth login & refresh | | `21-*` – `25-*` | Headers and cloaking (billing header, tool prefix, user_id) | | `26-*` – `32-*` | Pricing table, model catalog, request builder | | `33-*` – `39-*` | HTTP client, non-streaming chat, SSE parser, streaming | | `40-*` – `48-*` | Token counting, list_models, usage report, cost, auth lifecycle | | `49-*` – `56-*` | Test suite | | `57-*` | Adapter-tester playbook (`dispatch-adapter-tester`) | | `58-*` | This documentation | | `59-*` | Release | ## File map ``` lib/dispatch/adapter/claude.rb # public class + chat orchestration lib/dispatch/adapter/claude/ version.rb # VERSION constant errors.rb # ClaudeErrors, OverloadedError token_store.rb # ~/.config/dispatch/claude_oauth.json pkce.rb # PKCE verifier / challenge oauth.rb # PKCE login + refresh oauth/callback_server.rb # loopback HTTP server (port 54545) headers.rb # Claude Code header set + betas cloaking.rb # billing block, proxy_ prefix, user_id pricing_table.rb # bundled per-model price table model_catalog.rb # ModelInfo builder request_builder.rb # request hash assembly (entry point) request_builder/messages.rb # interface → Anthropic wire messages request_builder/tools.rb # tool definitions → wire tools request_builder/cache_control.rb # breakpoint placement + TTL ordering request_builder/thinking.rb # thinking / output_config injection response_builder.rb # non-streaming response → interface types sse_parser.rb # raw SSE bytes → (event_type, data) pairs stream_collector.rb # accumulates SSE events → final Response http_client.rb # Net::HTTP wrapper (streaming + non-streaming) usage_client.rb # GET /api/oauth/usage → UsageReport ``` ## Key design decisions ### OAuth cloaking Every OAuth request injects two synthetic system blocks before the caller's system prompt: 1. A billing header (`x-anthropic-billing-header: …`) that attributes the call to the Claude Code entitlement. 2. `"You are a Claude agent, built on Anthropic's Claude Agent SDK."` (skipped for `claude-3-5-haiku` family). The billing payload is a snapshot of the assembled request body _before_ the system block is added (mirrors `oh-my-pi`'s `buildParams` → `billingPayload`). If the caller's system prompt already contains `x-anthropic-billing-header:`, both injections are skipped. ### Tool prefixing OAuth callers must send tool names as `proxy_` (except the four Anthropic builtins: `web_search`, `code_execution`, `text_editor`, `computer`). `Cloaking.apply_prefix` / `strip_prefix` handle this transparently. Forced `tool_choice` names get the same treatment. ### Strict-tool fallback If Anthropic returns a 400 "compiled grammar too large" / "schema too complex" error, the request is automatically retried once with `strict: true` removed from every tool. The adapter instance then sets `@strict_disabled = true` so all subsequent calls skip strict schemas. ### Rate limiter `RateLimiter` (from `dispatch-adapter-interface`) enforces a minimum per-request interval (default 1.0 s) and an optional rolling window quota. State is persisted at `~/.config/dispatch/claude_rate_limit` alongside the OAuth token. ### Streaming retry Transient failures (first-event timeout, missing `message_start`, network errors) are retried up to 3 times with exponential back-off — but _only_ when no consumer-facing content (text or tool deltas) has been emitted yet, to avoid double-sending partial output. ## Constants to track Two constants in `headers.rb` drift when Anthropic ships a new Claude Code CLI: | Constant | Current value | |---|---| | `CLAUDE_CODE_VERSION` | `"2.1.63"` | | `STAINLESS_PACKAGE_VERSION` | `"0.74.0"` | The `DEFAULT_BETAS` array in the same file tracks the `Anthropic-Beta` header set and also rotates occasionally. ## Running tests ```bash cd dispatch-adapter-claude bundle exec rspec ``` Or via rubocop + rspec together: ```bash bundle exec rubocop --autocorrect-all && bundle exec rspec ``` The test suite uses `WebMock` to stub all outbound HTTP. No live Anthropic credentials are needed to run it. ## Smoke-testing against the real API A deterministic playbook for recorded testing is available in `dispatch-adapter-tester`: ```ruby require "dispatch/adapter/tester" require "dispatch/adapter/tester/playbooks/claude" steps = Dispatch::Adapter::Tester::Playbooks::Claude.smoke_text adapter = Dispatch::Adapter::Tester::Playbook.new(steps_json: steps) resp = adapter.chat([Dispatch::Adapter::Message.new( role: "user", content: [Dispatch::Adapter::TextBlock.new(text: "Say hi")] )]) raise unless resp.stop_reason == :end_turn ``` For live runs substitute `Dispatch::Adapter::Tester::Playbook` with `Dispatch::Adapter::Claude.new(...)` after calling `authenticate!`. See the playbook docstring in `dispatch-adapter-tester/lib/dispatch/adapter/tester/playbooks/claude.rb` for the full live-run examples for each scenario.