1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
|
# 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.
|