@warlock.js/ai-anthropic 5.28.0 → 5.30.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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,37 @@ All notable changes to `@warlock.js/ai-anthropic` are documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). `@warlock.js/*` packages are released in lockstep — every package shares the same version number, so a version below may list only the changes that affected this package.
6
6
 
7
+ ## 5.30.0 - 2026-10-16
8
+
9
+ ### Upgrading
10
+
11
+ - `model_context_window_exceeded` now maps to `finishReason: "context_exceeded"` (was `length`), `pause_turn` to `pause_turn` and `refusal` to `refusal` (both were `error`).
12
+ - `providerOptions.anthropic.body` may not set `model`, `messages`, `system`, `tools`, `mcp_servers` or `stream`: the call fails with `InvalidRequestError` instead of silently replacing the conversation, the model or the registered tools.
13
+
14
+ ### Added
15
+
16
+ - Responses with thinking, redacted-thinking, compaction, server-tool or cited-text blocks return the whole ordered turn as `ModelResponse.blocks` (and on the streaming `done` chunk); an assistant message carrying Anthropic `blocks` is replayed verbatim (WR-022).
17
+ - `providerOptions.anthropic` (`AnthropicCallOptions`): `betas` routes the call through `client.beta.messages`, `body` merges extra request fields last (`context_management`, `fallbacks`, `inference_geo`, …), `thinkingDisplay` sets `thinking.display` (WR-025).
18
+ - `AnthropicModel.countTokens(messages, options?)` counts input tokens through `messages.countTokens`; `AnthropicModel` is exported and `AnthropicSDK.model()` returns it (WR-025).
19
+ - Per-call `cacheControl.ttl` overrides the model's `promptCacheTtl`.
20
+
21
+ ### Changed
22
+
23
+ - Consecutive tool results are sent as one `user` message with several `tool_result` blocks, as Anthropic expects for parallel tool use (WR-024).
24
+ - Claude 5-family thinking rules: Opus 5.5, Sonnet 5.5 and Fable 5 / 5.1 never receive `budget_tokens` or `disabled` (WR-026).
25
+ - `effort: "none"` on Claude Sonnet 5.5 sends `thinking: { type: "between_tools" }` (its only thinking-off form, sent alone with no `output_config.effort` or `display`); it used to omit `thinking`, so the model still thought at its default effort. Opus 5.5 and Fable 5 / 5.1 have no thinking-off form and keep omitting `thinking` (WR-026).
26
+
27
+ ### Fixed
28
+
29
+ - `providerOptions.anthropic.thinkingDisplay` is applied only to adaptive thinking, so it is never sent beside `between_tools` (a 400).
30
+ - **Security:** a stored block is replayed only when its `data.type` equals its declared `type` and the type is one Claude returns on an assistant turn (text, thinking, redacted thinking, tool use, server-tool use and results, MCP, container upload, compaction). A transcript from outside the process can no longer smuggle a `tool_use` or user-side block past the tool-id check; such turns are rebuilt from `content` + `toolCalls`.
31
+
32
+ ## 5.29.0 - 2026-10-01
33
+
34
+ ### Changed
35
+
36
+ - Lockstep release; package APIs are unchanged. Skills refer to other topics by name instead of file paths, so cross-references keep working after `agent-kit` copies them into an app.
37
+
7
38
  ## 5.28.0 - 2026-10-01
8
39
 
9
40
  ### Changed
package/cjs/index.cjs CHANGED
@@ -31,7 +31,7 @@ _anthropic_ai_sdk = __toESM(_anthropic_ai_sdk, 1);
31
31
  let _warlock_js_ai = require("@warlock.js/ai");
32
32
  let _warlock_js_logger = require("@warlock.js/logger");
33
33
 
34
- //#region ../ai-anthropic/src/known-vision-models.ts
34
+ //#region ../../packages/ai-anthropic/src/known-vision-models.ts
35
35
  /**
36
36
  * Model-name prefixes for Claude families that accept image input
37
37
  * (vision) on the Messages API.
@@ -86,7 +86,7 @@ function inferVisionCapability(modelName) {
86
86
  }
87
87
 
88
88
  //#endregion
89
- //#region ../ai-anthropic/src/thinking-mode.ts
89
+ //#region ../../packages/ai-anthropic/src/thinking-mode.ts
90
90
  /**
91
91
  * Family prefixes that are budget-mode in their entirety — plain
92
92
  * `startsWith` match (dated and `-latest` variants included).
@@ -163,39 +163,150 @@ function inferThinkingMode(modelName) {
163
163
  function acceptsTemperature(mode) {
164
164
  return mode === "budget";
165
165
  }
166
+ /**
167
+ * Model release roots that accept `thinking: { type: "between_tools" }`,
168
+ * the only way to turn thinking off on them. Source: claude-api ref,
169
+ * "Thinking & Effort" per-model table (cached 2026-10-06): Claude Sonnet
170
+ * 5.5 returns a 400 on `{ type: "disabled" }` and takes `between_tools`
171
+ * instead (no other field alongside it, effort `high` or below). No other
172
+ * current model is documented to accept it. Opus 5.5 and Fable 5/5.1 have
173
+ * no thinking-off form at all: `disabled` and `budget_tokens` are a 400 at
174
+ * every effort, so the lowest they go is `effort: "low"`.
175
+ */
176
+ const BETWEEN_TOOLS_RELEASE_ROOTS = ["claude-sonnet-5-5"];
177
+ /**
178
+ * Whether `effort: "none"` can turn thinking off on this model with
179
+ * `thinking: { type: "between_tools" }`. Matches the release id with or
180
+ * without a platform prefix (`anthropic.claude-sonnet-5-5` on Bedrock)
181
+ * and with dated or tagged suffixes, but not a later minor release.
182
+ *
183
+ * @example
184
+ * acceptsBetweenTools("claude-sonnet-5-5"); // → true
185
+ * acceptsBetweenTools("claude-opus-5-5"); // → false
186
+ */
187
+ function acceptsBetweenTools(modelName) {
188
+ const normalized = modelName.toLowerCase();
189
+ const id = normalized.slice(normalized.lastIndexOf(".") + 1);
190
+ return BETWEEN_TOOLS_RELEASE_ROOTS.some((root) => matchesReleaseRoot(id, root));
191
+ }
166
192
 
167
193
  //#endregion
168
- //#region ../ai-anthropic/src/utils/map-stop-reason.ts
194
+ //#region ../../packages/ai-anthropic/src/utils/map-stop-reason.ts
169
195
  const stopReasonMap = {
170
196
  end_turn: "stop",
171
197
  stop_sequence: "stop",
198
+ compaction: "stop",
172
199
  max_tokens: "length",
173
- model_context_window_exceeded: "length",
200
+ model_context_window_exceeded: "context_exceeded",
201
+ pause_turn: "pause_turn",
202
+ refusal: "refusal",
174
203
  tool_use: "tool_calls"
175
204
  };
176
205
  /**
177
206
  * Map Anthropic's `stop_reason` to the normalized `FinishReason` union.
178
207
  *
179
- * `end_turn` / `stop_sequence` are both natural stops. `max_tokens` and
180
- * `model_context_window_exceeded` map to `length`. `tool_use` maps to
181
- * `tool_calls`. Everything else —
182
- * `refusal` (policy intervention), `pause_turn` (incomplete
183
- * long-running turn), `null`, or any value a future API version adds —
184
- * falls through to `"error"` so the agent treats the trip as a
185
- * non-clean terminal rather than silently accepting a partial result.
208
+ * `end_turn` / `stop_sequence` (and beta `compaction`) are natural stops,
209
+ * `max_tokens` maps to `length`, `tool_use` to `tool_calls`.
210
+ * `model_context_window_exceeded`, `pause_turn` and `refusal` keep their
211
+ * own reasons (`context_exceeded`, `pause_turn`, `refusal`) because each
212
+ * needs a different reaction: compact the history, send the turn back to
213
+ * resume, or fall back to another model. `null` and any value a future
214
+ * API version adds fall through to `"error"`.
186
215
  *
187
216
  * @example
188
- * mapStopReason("end_turn"); // "stop"
189
- * mapStopReason("tool_use"); // "tool_calls"
190
- * mapStopReason("refusal"); // "error"
191
- * mapStopReason(null); // "error"
217
+ * mapStopReason("end_turn"); // "stop"
218
+ * mapStopReason("tool_use"); // "tool_calls"
219
+ * mapStopReason("refusal"); // "refusal"
220
+ * mapStopReason("pause_turn"); // "pause_turn"
221
+ * mapStopReason(null); // "error"
192
222
  */
193
223
  function mapStopReason(raw) {
194
224
  return stopReasonMap[raw ?? ""] ?? "error";
195
225
  }
196
226
 
197
227
  //#endregion
198
- //#region ../ai-anthropic/src/utils/to-anthropic-messages.ts
228
+ //#region ../../packages/ai-anthropic/src/utils/provider-blocks.ts
229
+ /** The `ProviderContentBlock.provider` value this adapter writes and replays. */
230
+ const ANTHROPIC_BLOCK_FORMAT = "anthropic";
231
+ /** Block types the neutral `content` + `toolCalls` fields already carry losslessly. */
232
+ const NEUTRAL_BLOCK_TYPES = new Set(["text", "tool_use"]);
233
+ /** Block types that carry a model-requested call whose id must match `toolCalls`. */
234
+ const TOOL_USE_BLOCK_TYPES = new Set(["tool_use"]);
235
+ /**
236
+ * Assistant-turn block types that may be sent back verbatim. Server-tool
237
+ * results all end in `_tool_result` (`web_search_tool_result`,
238
+ * `code_execution_tool_result`, …) and are matched by suffix; anything
239
+ * else (an unknown or user-side type such as `image`, `document` or a
240
+ * client `tool_result`) is not replayed.
241
+ */
242
+ const REPLAYABLE_BLOCK_TYPES = new Set([
243
+ "text",
244
+ "thinking",
245
+ "redacted_thinking",
246
+ "tool_use",
247
+ "server_tool_use",
248
+ "mcp_tool_use",
249
+ "mcp_tool_result",
250
+ "container_upload",
251
+ "compaction"
252
+ ]);
253
+ const SERVER_TOOL_RESULT_TYPE = /^[a-z][a-z0-9_]*_tool_result$/;
254
+ /**
255
+ * Wrap an assistant turn's content blocks as `ProviderContentBlock`s, or
256
+ * return `undefined` when the neutral fields already carry the whole turn
257
+ * (only plain text and `tool_use` blocks). Any other block — thinking,
258
+ * redacted thinking, compaction, server-tool use/results, cited text —
259
+ * makes the full ordered list worth keeping so the next request can
260
+ * replay the turn verbatim.
261
+ *
262
+ * @example
263
+ * toProviderBlocks([{ type: "text", text: "hi", citations: null }]); // undefined
264
+ * toProviderBlocks([{ type: "thinking", thinking: "", signature: "s" }, { type: "text", text: "hi" }]);
265
+ * // [{ provider: "anthropic", type: "thinking", data: {...} }, { provider: "anthropic", type: "text", data: {...} }]
266
+ */
267
+ function toProviderBlocks(content) {
268
+ if (!content.some(needsVerbatimReplay)) return;
269
+ return content.map((block) => ({
270
+ provider: ANTHROPIC_BLOCK_FORMAT,
271
+ type: block.type,
272
+ data: { ...block }
273
+ }));
274
+ }
275
+ /**
276
+ * Rebuild an assistant turn's Anthropic content from stored blocks.
277
+ * Returns `undefined` — so the caller falls back to `content` +
278
+ * `toolCalls` — when there are no blocks, when any block belongs to
279
+ * another provider's format, when a block is not a well-formed
280
+ * assistant-turn block (its `data.type` must equal its `type`, and the
281
+ * type must be one Anthropic returns on an assistant turn), or when the
282
+ * blocks' `tool_use` ids differ from the message's `toolCalls` (the turn
283
+ * was edited after the response, e.g. by a middleware or a recovered
284
+ * stream call, so a verbatim replay would no longer match the tool
285
+ * results that follow). The checks matter when a transcript comes from
286
+ * outside the process (a client, a store): the block data is sent as is.
287
+ */
288
+ function fromProviderBlocks(blocks, toolCalls) {
289
+ if (!blocks || blocks.length === 0) return;
290
+ if (!blocks.every((block) => block.provider === "anthropic" && isReplayable(block))) return;
291
+ const blockToolIds = blocks.filter((block) => TOOL_USE_BLOCK_TYPES.has(block.type)).map((block) => block.data.id);
292
+ const requestedIds = (toolCalls ?? []).map((toolCall) => toolCall.id);
293
+ if (blockToolIds.length !== requestedIds.length || blockToolIds.some((id, index) => id !== requestedIds[index])) return;
294
+ return blocks.map((block) => ({ ...block.data }));
295
+ }
296
+ function needsVerbatimReplay(block) {
297
+ if (!NEUTRAL_BLOCK_TYPES.has(block.type)) return true;
298
+ const citations = block.citations;
299
+ return Array.isArray(citations) && citations.length > 0;
300
+ }
301
+ function isReplayable(block) {
302
+ const data = block.data;
303
+ if (data === null || typeof data !== "object" || Array.isArray(data)) return false;
304
+ if (data.type !== block.type) return false;
305
+ return REPLAYABLE_BLOCK_TYPES.has(block.type) || SERVER_TOOL_RESULT_TYPE.test(block.type);
306
+ }
307
+
308
+ //#endregion
309
+ //#region ../../packages/ai-anthropic/src/utils/to-anthropic-messages.ts
199
310
  /**
200
311
  * Convert vendor-neutral `Message[]` into Anthropic's request shape.
201
312
  *
@@ -206,15 +317,24 @@ function mapStopReason(raw) {
206
317
  * separated) and returned separately as the top-level `system`
207
318
  * parameter.
208
319
  * 2. **Tool results are `user` turns.** A neutral `tool` message becomes
209
- * a `user` message whose content is a single `tool_result` block
210
- * keyed by `tool_use_id`.
320
+ * a `tool_result` block keyed by `tool_use_id`. Consecutive tool
321
+ * messages (the results of one parallel tool-use turn) are grouped
322
+ * into a single `user` message, as Anthropic expects — splitting them
323
+ * across messages teaches the model to stop calling tools in parallel.
211
324
  * 3. **Tool calls are `tool_use` content blocks.** An assistant message
212
325
  * carrying `toolCalls` becomes an `assistant` message whose content
213
326
  * is an optional leading `text` block followed by one `tool_use`
214
327
  * block per call.
215
328
  *
216
- * Consecutive same-role turns are left as-is — the Messages API merges
217
- * them server-side.
329
+ * An assistant message that carries Anthropic `blocks` (thinking,
330
+ * redacted thinking, compaction, server-tool blocks — see
331
+ * `ProviderContentBlock`) is replayed verbatim from those blocks, which
332
+ * keeps signed thinking and compaction state valid. Blocks from another
333
+ * provider, or blocks whose `tool_use` ids no longer match `toolCalls`,
334
+ * are ignored and the turn is rebuilt from `content` + `toolCalls`.
335
+ *
336
+ * Other consecutive same-role turns are left as-is — the Messages API
337
+ * merges them server-side.
218
338
  *
219
339
  * @example
220
340
  * const { system, messages } = toAnthropicMessages([
@@ -233,16 +353,29 @@ function toAnthropicMessages(messages) {
233
353
  continue;
234
354
  }
235
355
  if (message.role === "tool") {
236
- mapped.push({
356
+ const toolResult = {
357
+ type: "tool_result",
358
+ tool_use_id: message.toolCallId ?? "",
359
+ content: stringifyContent(message.content)
360
+ };
361
+ const previous = mapped[mapped.length - 1];
362
+ if (previous && previous.role === "user" && isToolResultTurn(previous)) previous.content.push(toolResult);
363
+ else mapped.push({
237
364
  role: "user",
238
- content: [{
239
- type: "tool_result",
240
- tool_use_id: message.toolCallId ?? "",
241
- content: stringifyContent(message.content)
242
- }]
365
+ content: [toolResult]
243
366
  });
244
367
  continue;
245
368
  }
369
+ if (message.role === "assistant") {
370
+ const replayed = fromProviderBlocks(message.blocks, message.toolCalls);
371
+ if (replayed) {
372
+ mapped.push({
373
+ role: "assistant",
374
+ content: replayed
375
+ });
376
+ continue;
377
+ }
378
+ }
246
379
  if (message.role === "assistant" && message.toolCalls && message.toolCalls.length > 0) {
247
380
  const blocks = extractThinkingBlocks(message.toolCalls);
248
381
  const text = stringifyContent(message.content);
@@ -305,6 +438,14 @@ function toAnthropicThinkingBlock(block) {
305
438
  }];
306
439
  return [];
307
440
  }
441
+ /**
442
+ * A `user` turn this function built from tool results — its content is a
443
+ * non-empty array made only of `tool_result` blocks. Later tool results
444
+ * of the same tool-use turn are appended to it.
445
+ */
446
+ function isToolResultTurn(message) {
447
+ return Array.isArray(message.content) && message.content.length > 0 && message.content.every((block) => block.type === "tool_result");
448
+ }
308
449
  function isRecord(value) {
309
450
  return typeof value === "object" && value !== null;
310
451
  }
@@ -359,7 +500,7 @@ function toAnthropicContentBlock(part) {
359
500
  }
360
501
 
361
502
  //#endregion
362
- //#region ../ai-anthropic/src/utils/to-anthropic-tools.ts
503
+ //#region ../../packages/ai-anthropic/src/utils/to-anthropic-tools.ts
363
504
  /**
364
505
  * Convert vendor-neutral `ToolConfig[]` into Anthropic's `tools` array.
365
506
  * Uses the shared `extractJsonSchema` helper; Anthropic requires the
@@ -392,7 +533,7 @@ function toInputSchema(input) {
392
533
  }
393
534
 
394
535
  //#endregion
395
- //#region ../ai-anthropic/src/utils/wrap-anthropic-error.ts
536
+ //#region ../../packages/ai-anthropic/src/utils/wrap-anthropic-error.ts
396
537
  /**
397
538
  * Wrap any thrown value caught inside the Anthropic adapter into the
398
539
  * appropriate `@warlock.js/ai` `AIError` subclass.
@@ -545,7 +686,7 @@ function parseRetryAfter(headers) {
545
686
  }
546
687
 
547
688
  //#endregion
548
- //#region ../ai-anthropic/src/model.ts
689
+ //#region ../../packages/ai-anthropic/src/model.ts
549
690
  const LOG_MODULE = "ai.anthropic";
550
691
  /**
551
692
  * Anthropic requires `max_tokens` on every request (unlike OpenAI,
@@ -602,6 +743,12 @@ const EFFORT_TO_OUTPUT_EFFORT = {
602
743
  xhigh: "xhigh",
603
744
  max: "max"
604
745
  };
746
+ /** Content-block types whose input arrives as `input_json_delta` while streaming. */
747
+ const STREAMED_INPUT_BLOCK_TYPES = new Set([
748
+ "tool_use",
749
+ "server_tool_use",
750
+ "mcp_tool_use"
751
+ ]);
605
752
  /**
606
753
  * Anthropic-backed implementation of `ModelContract`.
607
754
  *
@@ -672,16 +819,17 @@ var AnthropicModel = class {
672
819
  });
673
820
  let response;
674
821
  try {
675
- response = await this.client.messages.create({
676
- ...this.buildParams(messages, options),
822
+ response = await this.send({
823
+ ...this.buildBody(messages, options),
677
824
  stream: false
678
- }, options?.signal ? { signal: options.signal } : void 0);
825
+ }, options, "create");
679
826
  } catch (thrown) {
680
827
  throw this.logAndWrap(thrown);
681
828
  }
682
829
  const finishReason = mapStopReason(response.stop_reason);
683
830
  const usage = this.extractUsage(response.usage);
684
831
  const toolCalls = this.extractToolCalls(response.content);
832
+ const blocks = toProviderBlocks(response.content);
685
833
  this.logger.debug(LOG_MODULE, "response", "call to messages.create succeeded", {
686
834
  finishReason,
687
835
  usage
@@ -690,10 +838,32 @@ var AnthropicModel = class {
690
838
  content: this.extractText(response.content),
691
839
  finishReason,
692
840
  usage,
693
- toolCalls
841
+ toolCalls,
842
+ ...blocks ? { blocks } : {}
694
843
  };
695
844
  }
696
845
  /**
846
+ * Count the input tokens a request would use, through Anthropic's
847
+ * `messages.countTokens` endpoint (a network call; no tokens are
848
+ * generated or billed as output). Takes the same messages and options
849
+ * as `complete()` — system prompt, tools, thinking and
850
+ * `providerOptions.anthropic.betas` are counted exactly as they would be
851
+ * sent. For an offline estimate use `AnthropicSDK.count()`.
852
+ *
853
+ * @example
854
+ * const tokens = await model.countTokens(messages, { tools });
855
+ */
856
+ async countTokens(messages, options) {
857
+ const body = this.buildBody(messages, options);
858
+ const countBody = {};
859
+ for (const key of COUNT_TOKENS_FIELDS) if (body[key] !== void 0) countBody[key] = body[key];
860
+ try {
861
+ return (await this.send(countBody, options, "countTokens")).input_tokens;
862
+ } catch (thrown) {
863
+ throw this.logAndWrap(thrown);
864
+ }
865
+ }
866
+ /**
697
867
  * Incremental streaming completion. Yields neutral `ModelStreamChunk`s
698
868
  * — `delta` for text tokens, `tool-call` once a `tool_use` block's
699
869
  * arguments have fully accumulated, and a terminal `done` carrying the
@@ -709,10 +879,10 @@ var AnthropicModel = class {
709
879
  });
710
880
  let stream;
711
881
  try {
712
- stream = await this.client.messages.create({
713
- ...this.buildParams(messages, options),
882
+ stream = await this.send({
883
+ ...this.buildBody(messages, options),
714
884
  stream: true
715
- }, options?.signal ? { signal: options.signal } : void 0);
885
+ }, options, "create");
716
886
  } catch (thrown) {
717
887
  throw this.logAndWrap(thrown);
718
888
  }
@@ -724,8 +894,10 @@ var AnthropicModel = class {
724
894
  };
725
895
  const toolBlocks = /* @__PURE__ */ new Map();
726
896
  const thinkingBlocks = /* @__PURE__ */ new Map();
897
+ const streamedBlocks = /* @__PURE__ */ new Map();
727
898
  try {
728
899
  for await (const event of stream) {
900
+ this.trackStreamedBlock(streamedBlocks, event);
729
901
  if (event.type === "message_start") {
730
902
  usage.input = event.message.usage.input_tokens ?? 0;
731
903
  const thinkingTokens = event.message.usage.output_tokens_details?.thinking_tokens;
@@ -794,6 +966,7 @@ var AnthropicModel = class {
794
966
  }
795
967
  usage.total = usage.input + usage.output;
796
968
  const finishReason = mapStopReason(rawStopReason);
969
+ const blocks = toProviderBlocks(this.finishStreamedBlocks(streamedBlocks));
797
970
  this.logger.debug(LOG_MODULE, "response", "Streaming call to messages.create succeeded", {
798
971
  finishReason,
799
972
  usage
@@ -801,7 +974,79 @@ var AnthropicModel = class {
801
974
  yield {
802
975
  type: "done",
803
976
  finishReason,
804
- usage
977
+ usage,
978
+ ...blocks ? { blocks } : {}
979
+ };
980
+ }
981
+ /**
982
+ * Fold one stream event into the per-index block accumulator. Unknown
983
+ * block and delta types (beta `compaction`, future additions) are kept
984
+ * as their start payload plus whatever their delta carries, so they
985
+ * round-trip without the adapter knowing their shape.
986
+ */
987
+ trackStreamedBlock(blocks, event) {
988
+ if (event.type === "content_block_start") {
989
+ const block = { ...event.content_block };
990
+ blocks.set(event.index, {
991
+ block,
992
+ ...STREAMED_INPUT_BLOCK_TYPES.has(String(block.type)) ? { json: "" } : {}
993
+ });
994
+ return;
995
+ }
996
+ if (event.type !== "content_block_delta") return;
997
+ const entry = blocks.get(event.index);
998
+ if (!entry) return;
999
+ const delta = event.delta;
1000
+ const block = entry.block;
1001
+ if (delta.type === "text_delta") block.text = String(block.text ?? "") + String(delta.text ?? "");
1002
+ else if (delta.type === "thinking_delta") block.thinking = String(block.thinking ?? "") + String(delta.thinking ?? "");
1003
+ else if (delta.type === "signature_delta") block.signature = String(block.signature ?? "") + String(delta.signature ?? "");
1004
+ else if (delta.type === "input_json_delta") entry.json = (entry.json ?? "") + String(delta.partial_json ?? "");
1005
+ else if (delta.type === "citations_delta") block.citations = [...Array.isArray(block.citations) ? block.citations : [], delta.citation];
1006
+ else for (const [key, value] of Object.entries(delta)) if (key !== "type") block[key] = value;
1007
+ }
1008
+ /** Close the accumulated stream blocks into an ordered content array. */
1009
+ finishStreamedBlocks(blocks) {
1010
+ return Array.from(blocks.entries()).sort(([left], [right]) => left - right).map(([, { block, json }]) => {
1011
+ if (json !== void 0) block.input = (0, _warlock_js_ai.safeJsonParse)(json, {});
1012
+ return block;
1013
+ });
1014
+ }
1015
+ /**
1016
+ * Send a request through the SDK. With `providerOptions.anthropic.betas`
1017
+ * the call goes to `client.beta.messages` (which takes the `betas` list
1018
+ * and the beta-only body fields); otherwise to `client.messages`. The
1019
+ * cancellation signal rides in the request options, as before.
1020
+ */
1021
+ send(body, options, method) {
1022
+ const betas = resolveAnthropicOptions(options).betas;
1023
+ const requestOptions = options?.signal ? { signal: options.signal } : void 0;
1024
+ const api = betas && betas.length > 0 ? this.client.beta.messages : this.client.messages;
1025
+ return api[method].call(api, betas && betas.length > 0 ? {
1026
+ ...body,
1027
+ betas
1028
+ } : body, requestOptions);
1029
+ }
1030
+ /**
1031
+ * The full request body: the adapter-built params, then
1032
+ * `thinking.display` when thinking is sent on an adaptive model, then
1033
+ * `providerOptions.anthropic.body` merged last (caller fields win). The
1034
+ * body may add fields the adapter does not build (`context_management`,
1035
+ * `fallbacks`, …) but may not replace the conversation or the model —
1036
+ * see {@link RESERVED_BODY_FIELDS}.
1037
+ */
1038
+ buildBody(messages, options) {
1039
+ const anthropic = resolveAnthropicOptions(options);
1040
+ const body = { ...this.buildParams(messages, options) };
1041
+ if (anthropic.thinkingDisplay && this.thinkingMode === "adaptive" && body.thinking?.type === "adaptive") body.thinking = {
1042
+ ...body.thinking,
1043
+ display: anthropic.thinkingDisplay
1044
+ };
1045
+ const reserved = Object.keys(anthropic.body ?? {}).filter((key) => RESERVED_BODY_FIELDS.has(key));
1046
+ if (reserved.length > 0) throw new _warlock_js_ai.InvalidRequestError(`providerOptions.anthropic.body cannot set ${reserved.map((key) => `"${key}"`).join(", ")}: the adapter builds these from the call's messages, tools and model. Use the agent/model options instead.`, { context: { fields: reserved } });
1047
+ return {
1048
+ ...body,
1049
+ ...anthropic.body
805
1050
  };
806
1051
  }
807
1052
  /**
@@ -832,7 +1077,7 @@ var AnthropicModel = class {
832
1077
  ...this.buildAutomaticCacheControl(options?.cacheControl),
833
1078
  ...this.buildSystem(system, options?.cacheControl),
834
1079
  ...sendTemperature ? { temperature } : {},
835
- ...this.buildTools(options?.tools),
1080
+ ...this.buildTools(options?.tools, options?.cacheControl),
836
1081
  ...this.buildOutputConfig(options?.responseSchema, thinking.effort),
837
1082
  ...thinking.thinking ? { thinking: thinking.thinking } : {}
838
1083
  };
@@ -855,7 +1100,7 @@ var AnthropicModel = class {
855
1100
  if (this.capabilities.promptCaching && breakpoints > 0) return { system: [{
856
1101
  type: "text",
857
1102
  text: system,
858
- cache_control: this.cacheControl()
1103
+ cache_control: this.cacheControl(cacheControl?.ttl)
859
1104
  }] };
860
1105
  return { system };
861
1106
  }
@@ -866,13 +1111,18 @@ var AnthropicModel = class {
866
1111
  */
867
1112
  buildAutomaticCacheControl(cacheControl) {
868
1113
  const breakpoints = cacheControl?.breakpoints ?? 0;
869
- return this.config.promptCaching && breakpoints <= 0 ? { cache_control: this.cacheControl() } : {};
1114
+ return this.config.promptCaching && breakpoints <= 0 ? { cache_control: this.cacheControl(cacheControl?.ttl) } : {};
870
1115
  }
871
- /** Build a typed cache-control marker, omitting TTL for Anthropic's 5-minute default. */
872
- cacheControl() {
1116
+ /**
1117
+ * Build a typed cache-control marker. The per-call
1118
+ * `cacheControl.ttl` wins over the model's `promptCacheTtl`; with
1119
+ * neither, TTL is omitted for Anthropic's 5-minute default.
1120
+ */
1121
+ cacheControl(callTtl) {
1122
+ const ttl = callTtl ?? this.config.promptCacheTtl;
873
1123
  return {
874
1124
  type: "ephemeral",
875
- ...this.config.promptCacheTtl ? { ttl: this.config.promptCacheTtl } : {}
1125
+ ...ttl ? { ttl } : {}
876
1126
  };
877
1127
  }
878
1128
  /**
@@ -894,12 +1144,22 @@ var AnthropicModel = class {
894
1144
  * budget. `budget_tokens` must be ≥ 1024 (`ThinkingConfigEnabled`,
895
1145
  * `messages.d.ts:1084`), so the budget is floored at that minimum.
896
1146
  *
897
- * The neutral `effort: "none"` ("run without reasoning") maps to no
898
- * `thinking` field at all in both modes.
1147
+ * The neutral `effort: "none"` ("run without reasoning") is mapped per
1148
+ * model:
1149
+ * - Claude Sonnet 5.5 (`acceptsBetweenTools`): `thinking: { type:
1150
+ * "between_tools" }`, its only thinking-off form. It is sent alone —
1151
+ * no `output_config.effort` (the model default, `high`, is within the
1152
+ * `high`-or-below range it requires) and no `display`, both of which
1153
+ * the API rejects alongside it.
1154
+ * - Opus 5.5 and Fable 5/5.1: no `thinking` field. These models have no
1155
+ * thinking-off form (`disabled` and `budget_tokens` are a 400 at every
1156
+ * effort), so thinking still runs at the model's default effort. Pass
1157
+ * `effort: "low"` to spend less.
1158
+ * - Every other model: no `thinking` field, as before.
899
1159
  */
900
1160
  buildThinking(reasoning) {
901
1161
  if (!this.capabilities.reasoning || !reasoning) return {};
902
- if (reasoning.effort === "none") return {};
1162
+ if (reasoning.effort === "none") return acceptsBetweenTools(this.name) ? { thinking: { type: "between_tools" } } : {};
903
1163
  if (reasoning.maxTokens === void 0 && reasoning.effort === void 0) return {};
904
1164
  const effort = reasoning.effort ?? DEFAULT_THINKING_EFFORT;
905
1165
  const budget = Math.max(MIN_THINKING_BUDGET, reasoning.maxTokens ?? EFFORT_THINKING_BUDGET[effort]);
@@ -929,7 +1189,7 @@ var AnthropicModel = class {
929
1189
  * breakpoint there would pay the ~1.25x write surcharge every turn
930
1190
  * with no reads.
931
1191
  */
932
- buildTools(tools) {
1192
+ buildTools(tools, cacheControl) {
933
1193
  const mapped = toAnthropicTools(tools);
934
1194
  if (!mapped) return {};
935
1195
  if (this.config.promptCaching && mapped.length > 0) {
@@ -937,7 +1197,7 @@ var AnthropicModel = class {
937
1197
  const lastTool = mapped[last];
938
1198
  if (lastTool) mapped[last] = {
939
1199
  ...lastTool,
940
- cache_control: this.cacheControl()
1200
+ cache_control: this.cacheControl(cacheControl?.ttl)
941
1201
  };
942
1202
  }
943
1203
  return { tools: mapped };
@@ -1050,9 +1310,42 @@ var AnthropicModel = class {
1050
1310
  return wrapped;
1051
1311
  }
1052
1312
  };
1313
+ /**
1314
+ * Request fields `providerOptions.anthropic.body` may not set: they carry
1315
+ * the conversation, the model and the tool surface the agent registered,
1316
+ * so an override would silently replace the agent's input (or let the
1317
+ * model call tools the agent never registered). `stream` is chosen per
1318
+ * call by `complete()` / `stream()`.
1319
+ */
1320
+ const RESERVED_BODY_FIELDS = new Set([
1321
+ "model",
1322
+ "messages",
1323
+ "system",
1324
+ "tools",
1325
+ "mcp_servers",
1326
+ "stream"
1327
+ ]);
1328
+ /** Body fields `messages.countTokens` accepts, copied from the full request body. */
1329
+ const COUNT_TOKENS_FIELDS = [
1330
+ "model",
1331
+ "messages",
1332
+ "system",
1333
+ "tools",
1334
+ "tool_choice",
1335
+ "thinking",
1336
+ "output_config",
1337
+ "cache_control",
1338
+ "context_management",
1339
+ "mcp_servers"
1340
+ ];
1341
+ /** Read `providerOptions.anthropic`, tolerating its absence or a wrong shape. */
1342
+ function resolveAnthropicOptions(options) {
1343
+ const raw = options?.providerOptions?.anthropic;
1344
+ return typeof raw === "object" && raw !== null ? raw : {};
1345
+ }
1053
1346
 
1054
1347
  //#endregion
1055
- //#region ../ai-anthropic/src/sdk.ts
1348
+ //#region ../../packages/ai-anthropic/src/sdk.ts
1056
1349
  /**
1057
1350
  * Anthropic-backed implementation of `SDKAdapterContract`.
1058
1351
  *
@@ -1118,9 +1411,9 @@ var AnthropicSDK = class {
1118
1411
  * Rough token-count estimate for a given text. Uses the
1119
1412
  * character-heuristic (`approximateTokenCount`) from the core package
1120
1413
  * — good enough for budgeting and quota guards, not for billing.
1121
- * Anthropic does expose a `messages.countTokens` endpoint, but that
1122
- * is a network round-trip; `count()` is intentionally offline and
1123
- * synchronous-cost. The optional model id is reserved for future
1414
+ * For an exact count use `AnthropicModel.countTokens()`, which calls
1415
+ * Anthropic's `messages.countTokens` endpoint (a network round-trip);
1416
+ * `count()` is intentionally offline and synchronous-cost. The optional model id is reserved for future
1124
1417
  * per-model tokenizer dispatch; currently ignored.
1125
1418
  */
1126
1419
  async count(text, _model) {
@@ -1129,5 +1422,6 @@ var AnthropicSDK = class {
1129
1422
  };
1130
1423
 
1131
1424
  //#endregion
1425
+ exports.AnthropicModel = AnthropicModel;
1132
1426
  exports.AnthropicSDK = AnthropicSDK;
1133
1427
  //# sourceMappingURL=index.cjs.map