# 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_<name>` (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.
