@warlock.js/ai-anthropic 5.30.0 → 5.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/CHANGELOG.md +44 -6
  2. package/cjs/index.cjs +815 -132
  3. package/cjs/index.cjs.map +1 -1
  4. package/esm/config.type.d.mts +107 -4
  5. package/esm/config.type.d.mts.map +1 -1
  6. package/esm/index.d.mts +8 -3
  7. package/esm/index.mjs +7 -1
  8. package/esm/known-output-limits.mjs +46 -0
  9. package/esm/known-output-limits.mjs.map +1 -0
  10. package/esm/known-pricing.d.mts +58 -0
  11. package/esm/known-pricing.d.mts.map +1 -0
  12. package/esm/known-pricing.mjs +165 -0
  13. package/esm/known-pricing.mjs.map +1 -0
  14. package/esm/model.d.mts +89 -17
  15. package/esm/model.d.mts.map +1 -1
  16. package/esm/model.mjs +383 -86
  17. package/esm/model.mjs.map +1 -1
  18. package/esm/request.type.d.mts +25 -0
  19. package/esm/request.type.d.mts.map +1 -0
  20. package/esm/thinking-mode.d.mts +14 -1
  21. package/esm/thinking-mode.d.mts.map +1 -1
  22. package/esm/thinking-mode.mjs +102 -2
  23. package/esm/thinking-mode.mjs.map +1 -1
  24. package/esm/utils/cache-usage.mjs +62 -0
  25. package/esm/utils/cache-usage.mjs.map +1 -0
  26. package/esm/utils/index.d.mts +3 -0
  27. package/esm/utils/map-stop-reason.d.mts +1 -0
  28. package/esm/utils/provider-blocks.d.mts +39 -0
  29. package/esm/utils/provider-blocks.d.mts.map +1 -0
  30. package/esm/utils/to-anthropic-messages.d.mts +71 -0
  31. package/esm/utils/to-anthropic-messages.d.mts.map +1 -0
  32. package/esm/utils/to-anthropic-messages.mjs +12 -4
  33. package/esm/utils/to-anthropic-messages.mjs.map +1 -1
  34. package/esm/utils/to-anthropic-tools.d.mts +19 -0
  35. package/esm/utils/to-anthropic-tools.d.mts.map +1 -0
  36. package/esm/utils/wrap-anthropic-error.d.mts +1 -0
  37. package/esm/utils/wrap-anthropic-error.mjs +6 -1
  38. package/esm/utils/wrap-anthropic-error.mjs.map +1 -1
  39. package/llms-full.txt +214 -32
  40. package/llms.txt +1 -1
  41. package/package.json +3 -3
  42. package/skills/README.md +1 -1
  43. package/skills/setup-anthropic/SKILL.md +214 -32
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: setup-anthropic
3
- description: 'Wire @warlock.js/ai-anthropic — new AnthropicSDK({apiKey, baseURL?, provider?}) for Claude, .model({name, vision?, structuredOutput?, reasoning?, thinkingMode?, promptCaching?, maxTokens?}). System-prompt hoisting, max_tokens required (default 4096), extended thinking via options.reasoning → adaptive thinking + output_config.effort (current/unknown models) or budget_tokens (Haiku 4.5, 4.6 and older; override with thinkingMode), prompt caching via promptCaching + options.cacheControl (per-call ttl), provider blocks (thinking/compaction) replayed verbatim via ModelResponse.blocks / Message.blocks, providerOptions.anthropic (betas, body fields such as context_management / fallbacks, thinkingDisplay), model.countTokens(), distinct finish reasons (pause_turn, refusal, context_exceeded), cost-truth usage (cachedTokens/cacheWriteTokens), no first-party embeddings. Triggers: `AnthropicSDK`, `anthropic.model`, `anthropic.count`, `maxTokens`, `reasoning`, `thinking`, `budget_tokens`, `thinkingMode`, `adaptive`, `cacheControl`, `providerOptions`, `blocks`, `countTokens`, `pause_turn`, `refusal`, `context_management`, `compaction`, `promptCaching`, `cachedTokens`, `cacheWriteTokens`, `claude-sonnet-4-6`, `claude-haiku-4-5`, `claude-opus-4-7`; "wire claude into warlock agent", "configure anthropic provider", "use claude sonnet", "anthropic gateway baseURL", "claude extended thinking", "anthropic prompt caching cost"; typical import `import { AnthropicSDK } from "@warlock.js/ai-anthropic"`. Skip: embeddings — the `setup-openai` topic of the `warlock-js-ai-openai` skill; sibling adapters `@warlock.js/ai-openai`, `@warlock.js/ai-bedrock`, `@warlock.js/ai-google`, `@warlock.js/ai-ollama`; raw `@anthropic-ai/sdk`; Vercel `@ai-sdk/anthropic`.'
3
+ description: 'Wire @warlock.js/ai-anthropic — new AnthropicSDK({apiKey, baseURL?, provider?}) for Claude, .model({name, vision?, structuredOutput?, reasoning?, thinkingMode?, promptCaching?, maxTokens?, maxOutputTokens?}). System-prompt hoisting, max_tokens required (default: model max output when streaming, 16384 non-streaming; maxOutputTokens for gateway ids), extended thinking via options.reasoning → adaptive thinking + output_config.effort (current/unknown models) or budget_tokens (Haiku 4.5, 4.6 and older; override with thinkingMode), prompt caching via promptCaching + options.cacheControl (per-call ttl), provider blocks (thinking/compaction) replayed verbatim via ModelResponse.blocks / Message.blocks, providerOptions.anthropic (betas, body fields such as context_management / fallbacks, thinkingDisplay), model.countTokens(), distinct finish reasons (pause_turn, refusal, context_exceeded), built-in Claude list prices (ANTHROPIC_MODEL_PRICING, anthropicPricingFor, withAnthropicCacheWriteRates), cost-truth usage (inputIncludesCache: false, cachedTokens/cacheWriteTokens/cacheWrite1hTokens, usage.cost on complete()), ProviderOverloadedError for 529, no first-party embeddings. Triggers: `AnthropicSDK`, `anthropic.model`, `anthropic.count`, `maxTokens`, `maxOutputTokens`, `max_tokens`, `reasoning`, `thinking`, `budget_tokens`, `thinkingMode`, `adaptive`, `cacheControl`, `providerOptions`, `blocks`, `countTokens`, `pause_turn`, `refusal`, `context_management`, `contextManagement`, `serverCompaction`, `clear_tool_uses`, `clear_thinking`, `thinkingBlockBinding`, `block_binding`, `preservedThinking`, `ai.providerTool`, `web_search_20250305`, `bash_20250124`, `text_editor_20250728`, `streamChunks`, `thinking_delta`, `compaction`, `promptCaching`, `cachedTokens`, `cacheWriteTokens`, `cacheWrite1hTokens`, `inputIncludesCache`, `ANTHROPIC_MODEL_PRICING`, `ProviderOverloadedError`, `claude-sonnet-4-6`, `claude-haiku-4-5`, `claude-opus-4-7`; "wire claude into warlock agent", "configure anthropic provider", "use claude sonnet", "anthropic gateway baseURL", "claude extended thinking", "anthropic prompt caching cost"; typical import `import { AnthropicSDK } from "@warlock.js/ai-anthropic"`. Skip: embeddings — the `setup-openai` topic of the `warlock-js-ai-openai` skill; sibling adapters `@warlock.js/ai-openai`, `@warlock.js/ai-bedrock`, `@warlock.js/ai-google`, `@warlock.js/ai-ollama`; raw `@anthropic-ai/sdk`; Vercel `@ai-sdk/anthropic`.'
4
4
  ---
5
5
 
6
6
  # `@warlock.js/ai-anthropic`
@@ -47,41 +47,95 @@ Explicit config always wins (`structuredOutput`, `vision`, `reasoning`, `thinkin
47
47
 
48
48
  ## Pricing & cost
49
49
 
50
- Pricing is opt-in. Supply USD-per-million-token rates and every report this SDK's models produce carries `Usage.cost`; omit them and `cost` stays `undefined` (honest absence, never a false zero). Two declaration sites:
50
+ Every model is priced out of the box from the built-in Claude list prices; supply your own rates to override them. Pricing reaches `Usage.cost` on every agent report **and** on the `usage` of a direct `model.complete()` / the streaming `done` chunk, so side calls outside an agent are costed too. Unknown ids stay unpriced (`cost` is `undefined` — honest absence, never a false zero).
51
+
52
+ Resolution at `model()` time: per-model `pricing` > SDK registry entry for that name > built-in `ANTHROPIC_MODEL_PRICING` (alias or dated snapshot id) > `undefined`.
51
53
 
52
54
  ```ts
53
- // SDK-level registry — one source of truth, keyed by model name:
55
+ import {
56
+ ANTHROPIC_MODEL_PRICING,
57
+ AnthropicSDK,
58
+ anthropicPricingFor,
59
+ withAnthropicCacheWriteRates,
60
+ } from "@warlock.js/ai-anthropic";
61
+
54
62
  const anthropic = new AnthropicSDK({
55
- apiKey,
56
- pricing: {
57
- "claude-haiku-4-5": { input: 1, output: 5, cachedInput: 0.1 },
58
- "claude-sonnet-4-6": { input: 3, output: 15, cachedInput: 0.3 },
59
- },
63
+ apiKey: process.env.ANTHROPIC_API_KEY ?? "",
64
+ // SDK-level registry — wins over the built-in table, keyed by model name.
65
+ pricing: { "claude-sonnet-4-6": { input: 3, output: 15, cachedInput: 0.3 } },
60
66
  });
61
67
 
62
- // Per-model override (multi-tenant / contract-specific rates):
63
- anthropic.model({ name: "claude-sonnet-4-6", pricing: { input: 3, output: 15 } });
68
+ anthropic.model({ name: "claude-opus-5-5" }).pricing; // built-in: { input: 4, output: 20, cachedInput: 0.2, cacheWrite: 5, cacheWrite1h: 8 }
69
+ anthropic.model({ name: "claude-sonnet-4-6" }).pricing; // registry entry + write rates filled: cacheWrite 3.75, cacheWrite1h 6
70
+ anthropic.model({ name: "claude-haiku-4-5", pricing: { input: 1, output: 5 } }); // per-model override
71
+
72
+ anthropicPricingFor("claude-haiku-4-5-20251001"); // dated snapshot → the alias entry
73
+ withAnthropicCacheWriteRates({ input: 2, output: 10 }); // { input: 2, output: 10, cacheWrite: 2.5, cacheWrite1h: 4 }
74
+ Object.keys(ANTHROPIC_MODEL_PRICING); // the priced ids
64
75
  ```
65
76
 
66
- Resolution at `model()` time: per-model `pricing` > SDK registry entry for that name > `undefined`. `ModelPricing` is `{ input, output, cachedInput?, cachedOutput? }`.
77
+ - **Source.** `ANTHROPIC_MODEL_PRICING` holds the "Model pricing" table of https://platform.claude.com/docs/en/about-claude/pricing (fetched 2026-10-17): input, output, cache read (`cachedInput`), 5-minute write (`cacheWrite`) and 1-hour write (`cacheWrite1h`), USD per 1M tokens, standard global routing.
78
+ - **Write rates are filled for you.** Anthropic charges 1.25x input for a 5-minute cache write and 2x for a 1-hour write on every Claude model, so any entry without `cacheWrite` / `cacheWrite1h` (your own `{ input, output }` included) gets them from those multipliers (`withAnthropicCacheWriteRates`, exported with `ANTHROPIC_CACHE_WRITE_MULTIPLIER` / `ANTHROPIC_CACHE_WRITE_1H_MULTIPLIER`). The read rate differs per model (0.1x, 0.05x on Opus 5.5 / Sonnet 5.5, 0.025x on Fable 5.1), so it is never derived — set `cachedInput` yourself when you supply pricing, or cache reads bill at the full input rate.
79
+ - **Deliberately unpriced** (pass `pricing` yourself): Claude Haiku 5.5 (priced by prompt length — higher rates above 100,000 prompt tokens, which a flat rate cannot express), US-only inference (`inference_geo: "us"`, 1.1x), fast mode, the Batch API, limited-availability and retired models, and Claude on Bedrock / Vertex (partner-priced — see `@warlock.js/ai-bedrock`).
67
80
 
68
81
  ## Tokens & usage accounting
69
82
 
70
- Every `ModelResponse.usage` (and the streaming terminal `done`) is normalized to the neutral `Usage` shape. Anthropic reports `input_tokens` / `output_tokens` separately with no pre-summed total, so `total` is computed (`input + output`). The two cache channels are surfaced only when non-zero:
83
+ Every `ModelResponse.usage` (and the streaming terminal `done`) is normalized to the neutral `Usage` shape. Anthropic's `input_tokens` is the **uncached remainder only** — cache reads and writes come on top of it — so the adapter marks the usage `inputIncludesCache: false` and `computeCost` bills each channel exactly once. `total` is computed (`input + output`); the full prompt size is `input + cachedTokens + cacheWriteTokens`.
84
+
85
+ | Neutral field | Anthropic source | Meaning / billing |
86
+ | -------------------------- | ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
87
+ | `usage.input` | `input_tokens` | Uncached input. Bills at `input`. |
88
+ | `usage.inputIncludesCache` | — (always `false`) | Tells `computeCost` not to subtract cache tokens from `input`. |
89
+ | `usage.cachedTokens` | `cache_read_input_tokens` | Tokens read from the prompt cache. Bills at `cachedInput` (falls back to `input` when unset). |
90
+ | `usage.cacheWriteTokens` | `cache_creation_input_tokens` | Tokens **written** to the cache. Bills at `cacheWrite` (5-minute, 1.25x input). |
91
+ | `usage.cacheWrite1hTokens` | `cache_creation.ephemeral_1h_input_tokens` | The 1-hour share of the writes. Bills at `cacheWrite1h` (2x input). Without the breakdown, all writes count as 1-hour when every marker asked for `"1h"`. |
92
+ | `usage.reasoningTokens` | `output_tokens_details.thinking_tokens` | The thinking subset of `output_tokens`, informational; billed inside `output`. |
93
+ | `usage.cost` | computed | Per-channel USD (`input`, `output`, `cachedInput`, `cacheWrite`) when the model is priced. Sum with `totalCost(usage.cost)` from `@warlock.js/ai`. |
94
+
95
+ ```ts
96
+ import { totalCost } from "@warlock.js/ai";
97
+ import { AnthropicSDK } from "@warlock.js/ai-anthropic";
98
+
99
+ const model = new AnthropicSDK({ apiKey: process.env.ANTHROPIC_API_KEY ?? "" }).model({
100
+ name: "claude-sonnet-4-6",
101
+ });
102
+
103
+ const { usage } = await model.complete([{ role: "user", content: "Summarise this thread." }]);
104
+
105
+ // 100 uncached + 50,000 read + 20,000 written + 500 output is billed as
106
+ // 100×3 + 50,000×0.3 + 20,000×3.75 + 500×15 (USD per 1M tokens).
107
+ console.log(
108
+ usage.inputIncludesCache,
109
+ usage.cachedTokens,
110
+ usage.cacheWriteTokens,
111
+ totalCost(usage.cost),
112
+ );
113
+ ```
71
114
 
72
- | Neutral field | Anthropic source | Meaning / billing |
73
- | ------------------------ | ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
74
- | `usage.cachedTokens` | `cache_read_input_tokens` | Subset of input served from the prompt cache. Bills at `cachedInput` (falls back to `input` when unset). |
75
- | `usage.cacheWriteTokens` | `cache_creation_input_tokens` | Input tokens **written** to the cache on this call (the ~1.25x write surcharge). Bills at `cachedOutput` when set. |
76
- | `usage.reasoningTokens` | — _(not reported separately)_ | Anthropic bills extended-thinking tokens **inside** `output_tokens`, so this stays unset — populating it would double-count against `output`. |
115
+ With server-side compaction (`serverCompaction`), the `compaction` entries of `usage.iterations` are added to `input` / `output` / the cache counts, since the top-level counts leave the compaction step out.
77
116
 
78
- In streaming, `cachedTokens` / `cacheWriteTokens` are seeded from `message_start` and then **overwritten by the cumulative counts on `message_delta`** when those are present and non-zero, so the terminal `done` carries the final tally.
117
+ In streaming, the cache counters are seeded from `message_start` (including the 1-hour breakdown) and then **overwritten by the cumulative counts on `message_delta`** when present and non-zero, so the terminal `done` carries the final tally and its `cost`.
79
118
 
80
119
  ## `max_tokens` is required
81
120
 
82
- Unlike OpenAI, Anthropic **requires** `max_tokens` on every request. Resolution: per-call `options.maxTokens` > `config.maxTokens` > **default `4096`**. A caller who never sets a cap still gets a complete answer instead of a 400.
121
+ Unlike OpenAI, Anthropic **requires** `max_tokens` on every request. Resolution: per-call `options.maxTokens` (or the agent's per-run / configured `modelOptions.maxTokens`) > `config.maxTokens` > a default sized for agent work:
122
+
123
+ | Request | Default `max_tokens` |
124
+ | ---------------------------- | -------------------------------------------------------------------------------------------------------------------- |
125
+ | Streaming (`stream()`) | the model's documented max output: 128K (Claude 5 family, Opus 4.6–4.8, Sonnet 4.6), 64K (Opus / Sonnet / Haiku 4.5) |
126
+ | Non-streaming (`complete()`) | `16384`, capped by the model's max output |
127
+ | Unknown id | `16384`; set `maxOutputTokens` on the model for gateway / proxy ids |
128
+
129
+ The non-streaming default stays at 16384 because `@anthropic-ai/sdk` refuses a non-streaming request whose `max_tokens` implies more than ten minutes (above ~21K tokens) unless the client sets a `timeout`. Limits come from Anthropic's model pages (checked 2026-10-17).
130
+
131
+ ```ts
132
+ anthropic.model({ name: "claude-haiku-4-5" }); // stream 64000, complete 16384
133
+ anthropic.model({ name: "my-gateway/opus", maxOutputTokens: 128_000 });
134
+ ```
83
135
 
84
- With extended thinking enabled and no explicit cap, the adapter reserves answer room in both modes: `max_tokens = thinking budget + 4096`. In budget mode an explicit `maxTokens` must be greater than `budget_tokens` or the call throws before it reaches Anthropic.
136
+ With extended thinking enabled and no explicit cap, the default keeps room for the thinking budget plus 4096 answer tokens (never above the model's max output). In budget mode an explicit `maxTokens` must be greater than `budget_tokens` or the call throws before it reaches Anthropic.
137
+
138
+ A response that still hits the cap (`finishReason: "length"`) ends an agent run as incomplete (`AgentIncompleteError`) unless the agent sets `continueOnOutputLimit`.
85
139
 
86
140
  **Tiny caps + adaptive thinking:** thinking tokens count inside `max_tokens`, so a very small cap can be spent entirely on thinking. Live check: Sonnet 5.5 with `maxTokens: 60` returned `finishReason: "length"` and empty content. For tiny outputs give thinking room (raise `maxTokens`) or turn reasoning off (`reasoning: false` on the model, or no `options.reasoning`).
87
141
 
@@ -113,7 +167,7 @@ Adaptive models reject `budget_tokens` with a 400, so the shape matters.
113
167
 
114
168
  - **Adaptive:** `reasoning.effort` (`"low" | "medium" | "high"`) → `output_config.effort` of the same name. With only `reasoning.maxTokens` set, effort is `"medium"`; `maxTokens` then only sizes the reserved `max_tokens` room, it is not sent as a budget.
115
169
  - **Budget:** `reasoning.maxTokens` → `budget_tokens` verbatim; `reasoning.effort` → a tiered budget (`1024` / `4096` / `12000`) when no `maxTokens` is given. Any resolved budget is floored at Anthropic's `1024` minimum.
116
- - `reasoning` with neither `effort` nor `maxTokens`, or `effort: "none"`, emits nothing — except `effort: "none"` on Claude Sonnet 5.5, which sends `thinking: { type: "between_tools" }` (see below).
170
+ - `reasoning` with neither `effort` nor `maxTokens` emits nothing. `effort: "none"` emits nothing either, except on the Claude 5 models listed below (Sonnet 5.5, Sonnet 5, Haiku 5.5, Opus 5), which each get their own form.
117
171
 
118
172
  ### `thinkingMode` override
119
173
 
@@ -137,21 +191,40 @@ anthropic.model({ name: "claude-sonnet-4-6", thinkingMode: "adaptive" }); // opt
137
191
  `temperature` is **not sent** to models after Opus 4.6 (adaptive mode) — they reject any non-default value with a 400. It is also dropped on any request that enables thinking, since Anthropic 400s on thinking plus an explicit temperature. So it is forwarded only in budget mode on requests without thinking; `thinkingMode` decides this too. Set `reasoning: false` on the model config to suppress the thinking param entirely for proxied/legacy targets.
138
192
 
139
193
  ```ts
140
- await agent.execute("Prove it.", { reasoning: { effort: "high" } });
194
+ await agent.execute("Prove it.", { modelOptions: { reasoning: { effort: "high" } } });
141
195
  // adaptive model: thinking { type: "adaptive" }, output_config.effort "high"
142
196
  // budget model: thinking { type: "enabled", budget_tokens: 12000 }
143
197
  await model.complete(messages, { reasoning: { maxTokens: 8000 } });
144
198
  // adaptive model: effort "medium"; budget model: budget_tokens 8000
145
199
  ```
146
200
 
147
- **Claude 5 family (Opus 5.5, Sonnet 5.5, Fable 5 / 5.1):** thinking is always on. These models reject `thinking: { type: "disabled" }` and `budget_tokens` with a 400, so the adapter never sends either: they are adaptive and `reasoning.maxTokens` only sizes `max_tokens`. `effort: "none"` is mapped per model:
201
+ **Claude 5 family:** adaptive thinking runs by default and `budget_tokens` is a 400, so `reasoning.maxTokens` only sizes `max_tokens`. `effort: "none"` is mapped per model:
148
202
 
149
203
  | Model | `effort: "none"` sends | Result |
150
204
  | ----------------------- | -------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
151
205
  | Sonnet 5.5 | `thinking: { type: "between_tools" }`, alone (no `output_config.effort`, no `display`) | Thinking off. `between_tools` takes no other field and needs effort `high` or below; the model default, `high`, applies. |
206
+ | Sonnet 5, Haiku 5.5 | `thinking: { type: "disabled" }`, alone (no `output_config.effort`, no `display`) | Thinking off. Haiku 5.5 accepts `disabled` only at effort `high` or below; its default, `medium`, applies. |
207
+ | Opus 5 | `thinking: { type: "adaptive" }` + `output_config.effort: "low"` | Thinking stays on at the lowest effort: with `disabled`, Opus 5 can emit a tool call as plain text (never run) and leak thinking tags. |
152
208
  | Opus 5.5, Fable 5 / 5.1 | no `thinking` field | No thinking-off form exists, so the model still thinks at its default effort (Opus 5.5: `medium`). Pass `effort: "low"` to think less. |
153
209
 
154
- `thinkingDisplay` is applied only to adaptive thinking, never beside `between_tools`.
210
+ `thinkingDisplay` is applied only to adaptive thinking, never beside `between_tools` or `disabled`.
211
+
212
+ ```ts
213
+ import { AnthropicSDK } from "@warlock.js/ai-anthropic";
214
+
215
+ const anthropic = new AnthropicSDK({ apiKey: process.env.ANTHROPIC_API_KEY! });
216
+ const messages = [{ role: "user" as const, content: "Classify: great product!" }];
217
+
218
+ // Sonnet 5 / Haiku 5.5 → thinking: { type: "disabled" }
219
+ await anthropic.model({ name: "claude-haiku-5-5" }).complete(messages, {
220
+ reasoning: { effort: "none" },
221
+ });
222
+
223
+ // Opus 5 → thinking: { type: "adaptive" }, output_config: { effort: "low" }
224
+ await anthropic.model({ name: "claude-opus-5" }).complete(messages, {
225
+ reasoning: { effort: "none" },
226
+ });
227
+ ```
155
228
 
156
229
  `thinking.display` is set with `providerOptions.anthropic.thinkingDisplay` (`"summarized"` for readable summaries; the 5-family default is `"omitted"`, empty thinking text). It is applied whenever the adapter sends `thinking` to an adaptive model, so pair it with a `reasoning` option.
157
230
 
@@ -176,30 +249,52 @@ history = [
176
249
  ];
177
250
  ```
178
251
 
252
+ Or let `ai.session({ id, store, agent })` store the turns for you (the `keep-agent-conversations` topic of the `warlock-js-ai` skill).
253
+
179
254
  ## Provider options (`providerOptions.anthropic`)
180
255
 
181
256
  Anthropic-only request options, per call or in the agent's `modelOptions`. Other adapters ignore the key. Type: `AnthropicCallOptions`.
182
257
 
183
- | Field | Effect |
184
- | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
185
- | `betas` | Beta flags. When non-empty the request goes through `client.beta.messages` (needed for beta body fields and beta blocks such as `compaction`). |
186
- | `body` | Extra body fields merged **last** (caller wins): `context_management`, `fallbacks`, `inference_geo`, `tool_choice`, … Not validated, except that `model`, `messages`, `system`, `tools`, `mcp_servers` and `stream` are refused (`InvalidRequestError`). |
187
- | `thinkingDisplay` | `thinking.display` on adaptive models: `"summarized"`, `"omitted"`, `"updates"` (the last needs its beta). |
258
+ | Field | Effect |
259
+ | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
260
+ | `betas` | Beta flags. When non-empty the request goes through `client.beta.messages` (needed for beta body fields and beta blocks such as `compaction`). |
261
+ | `body` | Extra body fields merged **last** (caller wins): `context_management`, `fallbacks`, `inference_geo`, `tool_choice`, … Not validated, except that `model`, `messages`, `system`, `tools`, `mcp_servers` and `stream` are refused (`InvalidRequestError`). |
262
+ | `thinkingDisplay` | `thinking.display` on adaptive models: `"summarized"`, `"omitted"`, `"updates"` (the last needs its beta). |
263
+ | `contextManagement` | Typed context editing: `clearToolUses` (`trigger: { inputTokens } \| { toolUses }`, `keep`, `clearAtLeast`, `excludeTools`, `clearToolInputs`) → `clear_tool_uses_20250919`; `clearThinking` (`keep: n \| "all"`) → `clear_thinking_20251015`, listed first. Adds beta `context-management-2025-06-27`. |
264
+ | `serverCompaction` | `true` or `{ triggerTokens (≥ 50,000; API default 150,000), instructions }` → `compact_20260112`, beta `compact-2026-01-12`. Can't be combined with `body.context_management` (`InvalidRequestError`). |
265
+ | `thinkingBlockBinding` | `"error"` \| `"drop_block"` → `thinking.block_binding.prefix_mismatch_behavior` + beta `thinking-binding-controls-2026-08-01`; adaptive or budget thinking only (skipped with a debug log on `between_tools`). |
188
266
 
189
267
  ```ts
190
268
  const agent = ai.agent({
191
269
  model: anthropic.model({ name: "claude-opus-5-5" }),
270
+ tools,
192
271
  modelOptions: {
193
272
  providerOptions: {
194
273
  anthropic: {
195
- betas: ["compact-2026-01-12"],
196
- body: { context_management: { edits: [{ type: "compact_20260112" }] } },
274
+ contextManagement: { clearToolUses: { trigger: { inputTokens: 100_000 }, keep: 5 } },
275
+ serverCompaction: { triggerTokens: 200_000 },
197
276
  },
198
277
  },
199
278
  },
200
279
  });
201
280
  ```
202
281
 
282
+ The response then starts with a `compaction` block, kept on `blocks` and replayed (the API ignores everything before it). Compaction tokens are billed outside the top-level `input_tokens` / `output_tokens`; the adapter adds the `compaction` entries of `usage.iterations` into `usage`. Leave `pause_after_compaction` off inside an agent: a paused response reads as a final stop.
283
+
284
+ ### Preserved thinking after client-side compaction
285
+
286
+ Claude Fable 5.1, Opus 5.5, Sonnet 5.5 and Haiku 5.5 accept a replayed thinking block only while everything before it is unchanged. The agent's own compaction marks turns it kept after a summary `compaction: "retained"`; on those models (inferred, or `anthropic.model({ name, preservedThinking: true })`) the adapter replays them **without** `thinking` / `redacted_thinking` blocks. Other models (Opus 5, Sonnet 5, Fable 5, Mythos 5.1, 4.x, budget mode) keep them. Setting `thinkingBlockBinding` disables the stripping and lets the API drop (`"drop_block"`) or reject (`"error"`) instead — also the way to recover a stored conversation someone edited:
287
+
288
+ ```ts
289
+ const recovering = ai.agent({
290
+ model: anthropic.model({ name: "claude-opus-5-5" }),
291
+ modelOptions: { providerOptions: { anthropic: { thinkingBlockBinding: "drop_block" } } },
292
+ });
293
+ await recovering.execute(question, { history });
294
+
295
+ inferPreservedThinking("claude-opus-5-5"); // true (exported helper)
296
+ ```
297
+
203
298
  Cache TTL per call: `options.cacheControl.ttl` (`"5m"` | `"1h"`) overrides the model's `promptCacheTtl` for that call.
204
299
 
205
300
  ## Prompt caching (`cache_control`)
@@ -240,6 +335,30 @@ await agent.execute("…", { cacheControl: { breakpoints: 1 } }); // cache the s
240
335
  - `tool_use` blocks accumulate `input_json_delta` fragments and emit one consolidated `{ type: "tool-call", ... }` at `content_block_stop` (no partial-arg `{}` artifact — input is parsed once, whole)
241
336
  - every content block is also assembled by index (text, thinking + signature, server-tool input, beta `compaction_delta`) and returned as `done.blocks` when the turn has a non-neutral block
242
337
  - terminal `{ type: "done", finishReason, usage, blocks? }` — `usage.input` from `message_start`, `usage.output` from `message_delta`, `total` computed; `cachedTokens` / `cacheWriteTokens` from `message_start` then the cumulative `message_delta` counts (see [Tokens & usage accounting](#tokens--usage-accounting))
338
+ - opt-in (`options.streamChunks`, set by `agent.stream()`): `{ type: "reasoning", delta }` per non-empty `thinking_delta`, `{ type: "tool-args", callId, name, delta }` per `input_json_delta` of a client `tool_use` block (never for `server_tool_use`), `{ type: "usage", usage }` after `message_start` and each `message_delta` (running totals). Claude 5 models stream thinking text only with `providerOptions.anthropic.thinkingDisplay: "summarized"`.
339
+
340
+ ```ts
341
+ for await (const chunk of model.stream(messages, { streamChunks: true, reasoning: { effort: "high" } })) {
342
+ if (chunk.type === "reasoning") process.stderr.write(chunk.delta);
343
+ }
344
+ ```
345
+
346
+ ## Provider-defined tools
347
+
348
+ `ai.providerTool({ provider: "anthropic", definition })` entries are sent verbatim, before the function tools (the prompt-cache breakpoint stays on the last function tool). Server tools (`web_search_20250305`, `web_fetch_20250910`, `code_execution_20250825`) run inside the response and their `server_tool_use` / `*_tool_result` blocks round-trip via `blocks`. Client tools (`bash_20250124`, `text_editor_20250728` named `str_replace_based_edit_tool`, `memory_20250818`) come back as `tool_use` and dispatch to the provider tool's `execute`. Other providers' tools are skipped with a `providerTool.skipped` warning. `providerOptions.anthropic.body` still cannot set `tools`.
349
+
350
+ ```ts
351
+ const webSearch = ai.providerTool({
352
+ provider: "anthropic",
353
+ definition: { type: "web_search_20250305", name: "web_search", max_uses: 5 },
354
+ });
355
+ const editor = ai.providerTool({
356
+ provider: "anthropic",
357
+ definition: { type: "text_editor_20250728", name: "str_replace_based_edit_tool" },
358
+ execute: async (input: { command: string; path: string }) => files.edit(input),
359
+ });
360
+ ai.agent({ model: anthropic.model({ name: "claude-sonnet-4-6" }), tools: [webSearch, editor] });
361
+ ```
243
362
 
244
363
  ## Finish-reason mapping
245
364
 
@@ -255,9 +374,72 @@ Raw SDK errors are wrapped into the typed `@warlock.js/ai` `AIError` hierarchy.
255
374
  - `rate_limit_error` / 429 → `ProviderRateLimitError` (+`retryAfter` from `retry-after`)
256
375
  - `billing_error` → `QuotaExceededError`
257
376
  - `invalid_request_error` with "prompt is too long" → `ContextLengthExceededError`, else `InvalidRequestError`
258
- - 5xx / overloaded → `ProviderError`
377
+ - `overloaded_error` / 529 → `ProviderOverloadedError` (`code: "PROVIDER_OVERLOADED"`, `retryable: true`, `retryAfter` from `retry-after`). Also raised for an `overloaded_error` delivered mid-stream as an SSE `error` event, which has no `status`.
378
+ - other 5xx → `ProviderError` (`retryable: true`)
259
379
  - Connection timeouts → `ProviderTimeoutError`
260
380
 
381
+ Every wrapped error carries `status` (the HTTP status, when there was one), `retryable`, and `billed`: `false` for 401 / 402 / 403 (the request never reached a billable account), `"unknown"` otherwise — Anthropic publishes no rule for failed requests, so treat them as possibly billed when you settle a budget. `fallbackModel` fails over on `PROVIDER_OVERLOADED` by default.
382
+
383
+ ```ts
384
+ import { ai, ProviderOverloadedError } from "@warlock.js/ai";
385
+ import { AnthropicSDK } from "@warlock.js/ai-anthropic";
386
+
387
+ const anthropic = new AnthropicSDK({ apiKey: process.env.ANTHROPIC_API_KEY ?? "" });
388
+ const agent = ai.agent({ model: anthropic.model({ name: "claude-sonnet-4-6" }) });
389
+
390
+ const result = await agent.execute("hi");
391
+
392
+ if (result.error instanceof ProviderOverloadedError) {
393
+ console.log(
394
+ result.error.status,
395
+ result.error.retryable,
396
+ result.error.billed,
397
+ result.error.retryAfter,
398
+ );
399
+ }
400
+ ```
401
+
402
+ ## Build and send an audited request
403
+
404
+ `model.buildRequest(messages, options?)` is synchronous and makes no network call.
405
+ It returns a detached `{ endpoint, body, betas }` snapshot, including thinking,
406
+ cache markers, provider body options and merged tool results. `body.stream` is
407
+ `false` by default; pass `{ ...options, stream: true }` to build a streaming request.
408
+ `betas` identifies the beta header flags; the SDK receives these separately from
409
+ its serialized Messages body.
410
+
411
+ ```ts
412
+ const request = model.buildRequest(messages, options);
413
+ const bodyJSON = JSON.stringify(request.body);
414
+ // Hash or reserve spend against bodyJSON before sending.
415
+ const response = await model.send(request, { signal });
416
+
417
+ const streamingRequest = model.buildRequest(messages, { ...options, stream: true });
418
+ for await (const chunk of model.send(streamingRequest, { signal })) {
419
+ // Consume the same neutral chunks as model.stream().
420
+ }
421
+ ```
422
+
423
+ `send()` uses the built endpoint, body and beta flags without rebuilding the
424
+ conversation or applying new defaults. It passes `betas` only when `endpoint` is
425
+ `"beta.messages"`: a request whose `endpoint` is `"messages"` is sent without
426
+ them, even if `betas` was edited by hand. Transport cancellation stays separate
427
+ from the audited body. `complete()`, `stream()` and `countTokens()` use this same
428
+ build path and always select their respective request modes. `buildRequest()`
429
+ throws build failures raw (an `InvalidRequestError` for a reserved `body` key, a
430
+ `DataCloneError` for a `body` value `structuredClone` cannot copy, such as a
431
+ function); `complete()`, `stream()` and `countTokens()` log them like send
432
+ failures and reject with an `AIError`, wrapping anything else in an
433
+ `InvalidRequestError`.
434
+ Treat the snapshot as read-only after hashing it; SDK JSON serialization and beta
435
+ header handling still apply.
436
+
437
+ Hosts with their own transport can import `toAnthropicMessages`,
438
+ `toAnthropicTools`, `toProviderBlocks`, `fromProviderBlocks`,
439
+ `ANTHROPIC_BLOCK_FORMAT` and the `AnthropicMessages` type from the package root.
440
+ `fromProviderBlocks(blocks, toolCalls)` retains the adapter's assistant-block
441
+ allowlist and tool-id validation.
442
+
261
443
  ## Token counting
262
444
 
263
445
  ```ts