summaryrefslogtreecommitdiffhomepage
path: root/AGENTS.md
blob: 6c191b9bd834d90b677220da6d6bb24a191ed06d (plain)
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.