export type Message = import("../types").Message; export type ToolDef = import("../types").ToolDef; export type GenerateResult = import("../types").GenerateResult; export type AnthropicOptions = { /** * - Anthropic API key (required). */ apiKey?: string | undefined; /** * - Model ID. */ model?: string | undefined; /** * - API base (override for proxies/gateways; the request posts to `${baseUrl}/messages`). */ baseUrl?: string | undefined; /** * - Opt-in prompt caching: send the system prompt with a `cache_control` breakpoint so Anthropic caches it. Anthropic does NOT auto-cache, so without this its cache tiers are always 0. Overridable per call via `generate(..., { cacheSystem })`. NOTE: on its own this rarely helps a tool loop — Anthropic's minimum cacheable prefix is 1024–4096 tokens (model-dependent) and a typical system persona is a few hundred, so it silently never caches. The transcript is where a tool loop's tokens actually live — see `cacheMessages`. */ cacheSystem?: boolean | undefined; /** * - Opt-in TRANSCRIPT caching (BA-1): roll a `cache_control` breakpoint onto the last content block of the last message, so Anthropic caches the whole conversation prefix and the loop stops re-buying it at full price every round. In a tool loop the transcript IS the tool results (file bodies from `shell_read`) and it always ENDS on one, which `_toAnthropicMessage` rebuilds from scratch — so no caller-side seam (`assemble` included) can reach it, and this has to live in the provider. Measured on `claude-sonnet-5` with a ~15k-token tool-result transcript (`poc/ba1-message-caching.mjs`): steady state **$0.0753 → $0.0110 per round, 6.8x cheaper**; round 1 pays a 1.25x cache WRITE once. Off by default — it changes the wire format, so adopters opt in. Overridable per call via `generate(..., { cacheMessages })`. **Interaction:** a destructive `trim`/stash fold that rewrites the transcript PREFIX invalidates the cache (the prefix is the cache key), so a fold must keep the head stable or you re-pay the write premium every round for nothing. */ cacheMessages?: boolean | undefined; /** * - Opt-in extended thinking (BA-7), forwarded to `body.thinking` VERBATIM and unvalidated — e.g. `{ type: 'adaptive' }`, or `{ type: 'adaptive', display: 'summarized' }` to surface the reasoning (the default `display` is `'omitted'`). Deliberately opaque: this parameter has already broken once (`budget_tokens` was removed and now 400s on `claude-sonnet-5` / Opus 4.7+), and a library that reshapes it would need a release every time the API moves. Overridable per call via `generate(..., { thinking })`; pass `null` there to suppress an instance default. * WARNING: per-call options (incl. `thinking`) are forwarded to EVERY provider in a `FallbackProvider` chain — for a mixed-vendor * chain set `thinking` on each provider's CONSTRUCTOR instead of per-call. * * **MEASURED CAVEAT — this option does not "turn thinking on".** On `claude-sonnet-5` adaptive thinking is ALREADY the default: sending this changed the observed thinking rate not at all (2/10 rounds with it vs 3/10 without — `poc/ba7-adaptive-default.mjs`). Its real use is pinning the mode and reaching `display`/`effort`. The change that mattered is that thinking blocks are now PRESERVED and replayed (see `Message.providerBlocks`), which happens whether or not you ever set this. */ thinking?: any; /** * - Normalized thinking switch: `'on'` or `'off'`, and bare-agent sends the right Anthropic `thinking` request field for the provider/model, tries the next known shape when a shape request fails with a 400/422 (not the temperature path), and the call ALWAYS RUNS: if every shape fails that way, ONE plain request without a thinking field decides — plain OK ⇒ `thinkingStatus:'unsupported'`, plain fails ⇒ that plain request's error is thrown as-is; other failures propagate after one request; a model measured as not switchable is sent plain directly. Only the winning shape is cached per instance+model ('unsupported' is never cached); warns once per status. Every reply is checked: `GenerateResult.thinkingStatus` is `'applied'`/`'unsupported'`/`'ignored'`/`'unknown'` (`'ignored'` = 'off' was accepted but the reply shows thinking anyway — warns once, result kept) and the legacy `thinkingApplied` is derived (`true`/`false`/`null`). Unset ⇒ request body byte-identical to before. Overridable per call via `generate(..., { thinkingMode })`; `null` suppresses an instance default. Any value but 'on'/'off' ⇒ `ValidationError`. A NEW field: it does not change the raw `thinking` option; setting both (after per-call override) ⇒ `ValidationError`. See `src/thinking-mode.js` / `thinkingSupport()`. * Per-call `thinkingMode` reaches EVERY provider in a `FallbackProvider` chain — correct for a normalized field (each provider maps it to its own shape; one that cannot — CLIPipe, a non-switchable model — just runs without it and reports `thinkingStatus:'unsupported'`, so the first member answers and the chain does not fall through). */ thinkingMode?: "on" | "off" | null | undefined; /** * - Attach the full upstream response to `err.body` on HTTP errors (off by default to avoid leaking unexpected fields through error logs; `err.message` still carries the API error). */ exposeErrorBody?: boolean | undefined; /** * - BA-18: request/idle timeout in ms. A silent or never-answering socket (dropped by the server, or a response that never starts) was otherwise bounded only by the OS TCP timeout (~2h) — a hang, not an error, so every retry policy above it was inert. Bounds on socket INACTIVITY (the timer resets on activity, so a slow-but-streaming response is not killed); on trip, `generate()` rejects with a retryable `TimeoutError` (`code: 'ETIMEDOUT'`, `context.bound: 'idle'`) that a wired `Retry` (`Loop({ retry })`) retries. Default 10 min sits above any single non-streaming completion; `0` or `Infinity` disables it (pre-BA-18 behaviour). Overridable per call via `generate(..., { timeoutMs })`. */ timeoutMs?: number | undefined; /** * - BA-19: TOTAL call-duration deadline in ms, beside `timeoutMs`. The idle bound resets on any socket activity, so a response that trickles a byte forever (a "zombie stream") never trips it and hangs the caller for hours. This is an absolute, non-resetting wall-clock ceiling; on trip, `generate()` rejects with a TERMINAL `TimeoutError` (`code: 'EDEADLINE'`, `context.bound: 'deadline'`, `retryable: false` — a hard ceiling meant to STOP, not re-spend). DISABLED by default (a deliberately long single call is legitimate); `0`/`Infinity` disable. When both are set with `timeoutMs < deadlineMs`, a silent socket trips the idle bound first. Overridable per call via `generate(..., { deadlineMs })`. */ deadlineMs?: number | undefined; }; /** @typedef {import('../types').Message} Message */ /** @typedef {import('../types').ToolDef} ToolDef */ /** @typedef {import('../types').GenerateResult} GenerateResult */ /** * @typedef {object} AnthropicOptions * @property {string} [apiKey] - Anthropic API key (required). * @property {string} [model='claude-haiku-4-5-20251001'] - Model ID. * @property {string} [baseUrl='https://api.anthropic.com/v1'] - API base (override for proxies/gateways; the request posts to `${baseUrl}/messages`). * @property {boolean} [cacheSystem=false] - Opt-in prompt caching: send the system prompt with a `cache_control` breakpoint so Anthropic caches it. Anthropic does NOT auto-cache, so without this its cache tiers are always 0. Overridable per call via `generate(..., { cacheSystem })`. NOTE: on its own this rarely helps a tool loop — Anthropic's minimum cacheable prefix is 1024–4096 tokens (model-dependent) and a typical system persona is a few hundred, so it silently never caches. The transcript is where a tool loop's tokens actually live — see `cacheMessages`. * @property {boolean} [cacheMessages=false] - Opt-in TRANSCRIPT caching (BA-1): roll a `cache_control` breakpoint onto the last content block of the last message, so Anthropic caches the whole conversation prefix and the loop stops re-buying it at full price every round. In a tool loop the transcript IS the tool results (file bodies from `shell_read`) and it always ENDS on one, which `_toAnthropicMessage` rebuilds from scratch — so no caller-side seam (`assemble` included) can reach it, and this has to live in the provider. Measured on `claude-sonnet-5` with a ~15k-token tool-result transcript (`poc/ba1-message-caching.mjs`): steady state **$0.0753 → $0.0110 per round, 6.8x cheaper**; round 1 pays a 1.25x cache WRITE once. Off by default — it changes the wire format, so adopters opt in. Overridable per call via `generate(..., { cacheMessages })`. **Interaction:** a destructive `trim`/stash fold that rewrites the transcript PREFIX invalidates the cache (the prefix is the cache key), so a fold must keep the head stable or you re-pay the write premium every round for nothing. * @property {any} [thinking] - Opt-in extended thinking (BA-7), forwarded to `body.thinking` VERBATIM and unvalidated — e.g. `{ type: 'adaptive' }`, or `{ type: 'adaptive', display: 'summarized' }` to surface the reasoning (the default `display` is `'omitted'`). Deliberately opaque: this parameter has already broken once (`budget_tokens` was removed and now 400s on `claude-sonnet-5` / Opus 4.7+), and a library that reshapes it would need a release every time the API moves. Overridable per call via `generate(..., { thinking })`; pass `null` there to suppress an instance default. * WARNING: per-call options (incl. `thinking`) are forwarded to EVERY provider in a `FallbackProvider` chain — for a mixed-vendor * chain set `thinking` on each provider's CONSTRUCTOR instead of per-call. * * **MEASURED CAVEAT — this option does not "turn thinking on".** On `claude-sonnet-5` adaptive thinking is ALREADY the default: sending this changed the observed thinking rate not at all (2/10 rounds with it vs 3/10 without — `poc/ba7-adaptive-default.mjs`). Its real use is pinning the mode and reaching `display`/`effort`. The change that mattered is that thinking blocks are now PRESERVED and replayed (see `Message.providerBlocks`), which happens whether or not you ever set this. * @property {'on'|'off'|null} [thinkingMode] - Normalized thinking switch: `'on'` or `'off'`, and bare-agent sends the right Anthropic `thinking` request field for the provider/model, tries the next known shape when a shape request fails with a 400/422 (not the temperature path), and the call ALWAYS RUNS: if every shape fails that way, ONE plain request without a thinking field decides — plain OK ⇒ `thinkingStatus:'unsupported'`, plain fails ⇒ that plain request's error is thrown as-is; other failures propagate after one request; a model measured as not switchable is sent plain directly. Only the winning shape is cached per instance+model ('unsupported' is never cached); warns once per status. Every reply is checked: `GenerateResult.thinkingStatus` is `'applied'`/`'unsupported'`/`'ignored'`/`'unknown'` (`'ignored'` = 'off' was accepted but the reply shows thinking anyway — warns once, result kept) and the legacy `thinkingApplied` is derived (`true`/`false`/`null`). Unset ⇒ request body byte-identical to before. Overridable per call via `generate(..., { thinkingMode })`; `null` suppresses an instance default. Any value but 'on'/'off' ⇒ `ValidationError`. A NEW field: it does not change the raw `thinking` option; setting both (after per-call override) ⇒ `ValidationError`. See `src/thinking-mode.js` / `thinkingSupport()`. * Per-call `thinkingMode` reaches EVERY provider in a `FallbackProvider` chain — correct for a normalized field (each provider maps it to its own shape; one that cannot — CLIPipe, a non-switchable model — just runs without it and reports `thinkingStatus:'unsupported'`, so the first member answers and the chain does not fall through). * @property {boolean} [exposeErrorBody=false] - Attach the full upstream response to `err.body` on HTTP errors (off by default to avoid leaking unexpected fields through error logs; `err.message` still carries the API error). * @property {number} [timeoutMs=600000] - BA-18: request/idle timeout in ms. A silent or never-answering socket (dropped by the server, or a response that never starts) was otherwise bounded only by the OS TCP timeout (~2h) — a hang, not an error, so every retry policy above it was inert. Bounds on socket INACTIVITY (the timer resets on activity, so a slow-but-streaming response is not killed); on trip, `generate()` rejects with a retryable `TimeoutError` (`code: 'ETIMEDOUT'`, `context.bound: 'idle'`) that a wired `Retry` (`Loop({ retry })`) retries. Default 10 min sits above any single non-streaming completion; `0` or `Infinity` disables it (pre-BA-18 behaviour). Overridable per call via `generate(..., { timeoutMs })`. * @property {number} [deadlineMs=0] - BA-19: TOTAL call-duration deadline in ms, beside `timeoutMs`. The idle bound resets on any socket activity, so a response that trickles a byte forever (a "zombie stream") never trips it and hangs the caller for hours. This is an absolute, non-resetting wall-clock ceiling; on trip, `generate()` rejects with a TERMINAL `TimeoutError` (`code: 'EDEADLINE'`, `context.bound: 'deadline'`, `retryable: false` — a hard ceiling meant to STOP, not re-spend). DISABLED by default (a deliberately long single call is legitimate); `0`/`Infinity` disable. When both are set with `timeoutMs < deadlineMs`, a silent socket trips the idle bound first. Overridable per call via `generate(..., { deadlineMs })`. */ export class AnthropicProvider { /** * @param {AnthropicOptions} [options] * @throws {Error} `[AnthropicProvider] requires apiKey` — when apiKey is missing. * @when you want Claude models as the Loop's provider — native Messages API with opt-in prompt caching and thinking-block passthrough * @fails throws on a missing apiKey; normalizes stopReason and usage (usage:null when the API omits it); a socket idle/deadline/transport cut rejects with a retryable Timeout/ProviderError. * @example * const provider = new AnthropicProvider({ apiKey, model: 'claude-sonnet-5' }); */ constructor(options?: AnthropicOptions); apiKey: string; model: string; baseUrl: string; cacheSystem: boolean; cacheMessages: boolean; thinking: any; thinkingMode: "on" | "off" | null; /** @type {Map} shape that worked per mode+model (no re-probing) */ _thinkingShapes: Map; exposeErrorBody: boolean; timeoutMs: number | undefined; deadlineMs: number | undefined; /** * Generate a response from the Anthropic API. * @param {Message[]} messages - Conversation messages (OpenAI format, auto-converted). * @param {ToolDef[]} [tools=[]] - Tool definitions. * @param {Record} [options={}] - Options (thinkingMode — per-call override of the constructor's, `null` suppresses; temperature, maxTokens, system, timeoutMs — a per-call override of the constructor's `timeoutMs`, see BA-18; deadlineMs — a per-call override of the constructor's `deadlineMs`, see BA-19). * @returns {Promise} * @throws {Error} `[AnthropicProvider] ...` — on HTTP errors (4xx/5xx) or invalid JSON response. */ generate(messages: Message[], tools?: ToolDef[], options?: Record): Promise; /** One-time warning (per reason) that this model rejected `temperature` and the request was retried without it (BA-10). */ _warnTemperatureDropped(reason?: string): void; /** * BA-7: the provider-native blocks to replay at the FRONT of an assistant turn's content, or `[]`. * * Only ever returns blocks this model itself produced. The tag carries `this.model` (the CONFIGURED * id, not the response's resolved one — those can differ by date suffix, and a mismatch there would * silently disable preservation, which is the very bug class BA-7 exists to close). * * Front, because Anthropic requires `thinking` to lead the content array — the verbatim order we * measured a successful round-trip on (`poc/ba7-thinking-contract.mjs`, R3). * * @param {Message} msg * @returns {any[]} */ _nativeBlocks(msg: Message): any[]; /** * @param {Message} msg * @returns {any} */ _toAnthropicMessage(msg: Message): any; /** * @param {Record} body * @param {number} [timeoutMs=0] - Idle-socket timeout (ms); 0 disables. See BA-18 / provider-http. * @param {number} [deadlineMs=0] - Total call-duration deadline (ms); 0 disables. See BA-19 / provider-http. * @returns {Promise} */ _request(body: Record, timeoutMs?: number, deadlineMs?: number): Promise; _warnedInsecure: boolean | undefined; }