@jxburros/ai-nugget 0.7.0 → 0.7.3

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/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # AI Nugget
2
2
 
3
- **Version 0.7.0** · [What's changed since 0.6.0](#whats-changed-since-060) · [Changelog](./CHANGELOG.md) · [Upgrading](./UPGRADING.md)
3
+ <img src="assets/ai-nugget-logo.png" alt="AI Nugget official logo" width="180" />
4
+
5
+ **Version 0.7.3** · [What's changed in 0.7.x](#whats-changed-in-07x) · [Changelog](./CHANGELOG.md) · [Upgrading](./UPGRADING.md)
4
6
 
5
7
  A small, zero-dependency, isomorphic TypeScript nugget for talking to AI model
6
8
  providers through one pipeline:
@@ -24,6 +26,13 @@ Requires Node `^20.19.0` or `>=22.12.0` ([why](./docs/distribution.md#requiremen
24
26
  npm install @jxburros/ai-nugget
25
27
  ```
26
28
 
29
+ > **0.7.1–0.7.3 are not on npm yet.** The registry's `latest` is still 0.7.0
30
+ > until the 0.7.3 Release run publishes, so the command above may install 0.7.0
31
+ > (without the 0.7.1 Anthropic fixes or the 0.7.2 `/agent` types fix). Until
32
+ > then, pin a commit from this repository
33
+ > (`npm install github:jxburros/AI-Nugget#<commit>`) or vendor `nugget/` — see
34
+ > [distribution paths](./docs/distribution.md#distribution-paths).
35
+
27
36
  ```ts
28
37
  import { AIHandler, envKeySource } from '@jxburros/ai-nugget';
29
38
 
@@ -50,7 +59,9 @@ No key? Point at a local model instead — same code, no `keyRef`:
50
59
  const conn = { id: 'local', provider: 'ollama', baseUrl: 'http://127.0.0.1:11434' };
51
60
  ```
52
61
 
53
- CommonJS works too: `const { AIHandler } = require('@jxburros/ai-nugget')`.
62
+ CommonJS works too: `const { AIHandler } = require('@jxburros/ai-nugget')`. TypeScript
63
+ finds the root and `/agent` types under `node10` (classic `module: commonjs`)
64
+ and `bundler` resolution, and under `node16`/`nodenext` from ESM files.
54
65
 
55
66
  For a running server, copy a starter from
56
67
  [`examples/integrations/`](./examples/integrations/) — Express, Next.js,
@@ -116,7 +127,48 @@ new AIHandler({
116
127
  honest `stopReason`s, an `ApprovalGate` for side-effecting tools, and
117
128
  `native`/`promptJson`/`auto` tool modes.
118
129
 
119
- ## What's changed since 0.6.0
130
+ ## Anthropic compatibility
131
+
132
+ Claude Opus/Sonnet/Haiku 5.5 requests omit unsupported sampling parameters.
133
+ Modern Claude JSON schemas use `output_config.format`; requests without a schema
134
+ use a JSON instruction and emit `json_mode_downgraded`. Older models retain the
135
+ forced-tool fallback. Explicit forced-tool choices on models that reject them
136
+ fail with `invalid_request` rather than silently changing the choice.
137
+
138
+ Anthropic results include opaque `providerState`. Custom tool loops must copy it
139
+ onto the corresponding assistant `ChatMessage`; `runAgent` does this automatically.
140
+ Keep it unchanged and private: it contains signed thinking blocks needed for replay,
141
+ not display or telemetry data. Replay is limited to the same provider and model.
142
+ See [provider details](./docs/providers.md#anthropic-compatibility).
143
+
144
+ ## What's changed in 0.7.x
145
+
146
+ ### 0.7.3
147
+
148
+ **Release-pipeline fix.** Package contents are identical to 0.7.2; 0.7.3 exists
149
+ because the 0.7.1 and 0.7.2 Release runs failed before publishing (see the
150
+ [changelog](./CHANGELOG.md)).
151
+
152
+ ### 0.7.2
153
+
154
+ **`/agent` types under classic `node10` resolution.** A `typesVersions` entry
155
+ lets TypeScript projects on `module: commonjs` (default `node10` resolution)
156
+ resolve `@jxburros/ai-nugget/agent`. Otherwise documentation only — notably the
157
+ [JX Runtime guidance](./docs/providers.md#pointing-at-a-jx-runtime-server) for
158
+ JX 1.5, including the `stream_options` opt-in for real streamed token counts.
159
+ No runtime behavior changed.
160
+
161
+ ### 0.7.1
162
+
163
+ **Anthropic correctness for modern Claude models.** Unsupported sampling
164
+ parameters are omitted, JSON schemas use native structured output, and signed
165
+ thinking is preserved for tool-loop replay through the new opaque
166
+ `providerState` on results (`runAgent` copies it for you; custom tool loops must
167
+ copy it onto the assistant message). SSE API errors become typed failures.
168
+ Details in [Anthropic compatibility](#anthropic-compatibility),
169
+ [UPGRADING.md](./UPGRADING.md#070--071), and the [changelog](./CHANGELOG.md).
170
+
171
+ ### 0.7.0
120
172
 
121
173
  **0.7.0 is backward-compatible for ordinary consumers** — the first two changes are additive; the third changes what `delta` carries for reasoning models (see UPGRADING). Full detail in the
122
174
  [changelog](./CHANGELOG.md) and [UPGRADING.md](./UPGRADING.md#06x--070).
@@ -206,3 +258,11 @@ enforce rules, not a place the library imposes its own.
206
258
  ## Project Stewardship
207
259
 
208
260
  This project is developed by **Jeffrey** through **JX Holdings, LLC**. GitHub: [`jxburros`](https://github.com/jxburros).
261
+
262
+ ## Unified release procedure
263
+
264
+ `release.json` is the authoritative release manifest. Keep configured embedded versions synchronized and advance affected apps (patch by default) before merging changes into main. The first adoption establishes a baseline; later PRs must pass **Release version validation**.
265
+
266
+ Use **Actions → Release → Run workflow** from **main**, choose stable or preview, and leave **dry_run** enabled first. Only apps ahead of their published version in their own namespace are selected. Build/test/sign all required packages before publishing. Tag pushes and merges no longer publish migrated apps automatically. After inspecting the dry run, dispatch with dry_run disabled and satisfy configured approvals. Published versions are never overwritten; use a new version for a correction.
267
+
268
+ See [.release/README.md](.release/README.md) for this repository's inventory, credentials, compatibility, and validation limits. Shared implementation: [JX Holdings release infrastructure](https://github.com/JX-Holdings-LLC/release-infrastructure).
package/UPGRADING.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Upgrading
2
2
 
3
+ ## 0.7.2 → 0.7.3
4
+
5
+ No changes: 0.7.3 republishes the 0.7.2 package contents after a release-pipeline fix.
6
+
7
+ ## 0.7.1 → 0.7.2
8
+
9
+ No code changes required. `package.json` gains a `typesVersions` entry so
10
+ TypeScript on classic `node10` resolution can find the `/agent` types; consumers
11
+ on `node16`/`nodenext`/`bundler` resolve exactly as before.
12
+
13
+ ## 0.7.0 → 0.7.1
14
+
15
+ Custom Anthropic tool loops should copy `result.providerState` onto their assistant
16
+ messages to preserve signed thinking; `runAgent` already does this. Sampling fields
17
+ are omitted on known modern Claude models. Native JSON schemas replace forced JSON
18
+ tools there; schema-less requests disclose an instruction-only downgrade. Explicit
19
+ unsupported forced-tool and thinking-off choices fail with `invalid_request`.
20
+
3
21
  ## 0.6.x → 0.7.0
4
22
 
5
23
  **Backward-compatible for ordinary consumers.** Two additive changes and one
@@ -86,7 +104,7 @@ signature changed and no default behavior changed, so an app on 0.4.x can bump
86
104
  without code edits and adopt the new capabilities as needed.
87
105
 
88
106
  The numbers in brackets are the suggestion numbers from the AI Server Studio
89
- production friction report (`docs/reviews/2026-08-09-ai-server-studio-friction-report.md`),
107
+ production friction report ([`docs/archive/2026-08-09-ai-server-studio-friction-report.md`](./docs/archive/2026-08-09-ai-server-studio-friction-report.md)),
90
108
  so you can trace each change back to the friction it resolves.
91
109
 
92
110
  ### Packaging
@@ -1,4 +1,4 @@
1
- import { AIError } from '../../errors.js';
1
+ import { AIError, classify } from '../../errors.js';
2
2
  import { estimatedUsage } from '../../tokens.js';
3
3
  import { fetchJson, postResponse, sseLines } from '../../transport.js';
4
4
  import { applyProviderOptions, REASONING_BUDGET_TOKENS, asNumber, asRecord, asString, joinUrl, textFromMessages } from '../../util.js';
@@ -23,7 +23,8 @@ export class AnthropicAdapter {
23
23
  }
24
24
  async *stream(conn, req) {
25
25
  const started = Date.now();
26
- const jsonMode = req.responseFormat?.type === 'json' && !req.tools?.length;
26
+ const nativeJson = modernModel(req.model);
27
+ const jsonMode = req.responseFormat?.type === 'json' && !nativeJson && !req.tools?.length;
27
28
  let firstTokenMs = null;
28
29
  let text = '';
29
30
  let inputTokens;
@@ -31,6 +32,7 @@ export class AnthropicAdapter {
31
32
  let stopReason;
32
33
  let sawTerminal = false;
33
34
  const emittedTools = [];
35
+ const responseBlocks = new Map();
34
36
  // Partial tool_use blocks keyed by content-block index (input_json_delta accumulation).
35
37
  const blocks = new Map();
36
38
  const timeout = streamTimeout(conn, req.signal);
@@ -38,19 +40,29 @@ export class AnthropicAdapter {
38
40
  // Anthropic JSON mode is implemented as a forced `json_output` tool, which
39
41
  // cannot coexist with the caller's own tools — same silent-downgrade shape
40
42
  // as Gemini, so it gets the same signal rather than quietly dropping.
41
- if (req.responseFormat?.type === 'json' && req.tools?.length) {
43
+ if (req.responseFormat?.type === 'json' && !nativeJson && req.tools?.length) {
42
44
  yield {
43
45
  type: 'context',
44
46
  kind: 'json_mode_downgraded',
45
47
  data: { reason: 'Anthropic JSON mode uses a forced tool and cannot be combined with caller tools; the request was sent without JSON mode', provider: conn.provider },
46
48
  };
47
49
  }
50
+ if (req.responseFormat?.type === 'json' && nativeJson && !req.responseFormat.schema) {
51
+ yield { type: 'context', kind: 'json_mode_downgraded', data: { provider: conn.provider, reason: 'Claude native JSON outputs require a schema; using a JSON instruction without constrained decoding.' } };
52
+ }
48
53
  try {
49
54
  const res = await postResponse(`${conn.baseUrl}/v1/messages`, body(req, jsonMode), conn.headers, timeout.signal, conn.provider);
50
55
  const contentType = res.headers.get('content-type') ?? '';
51
56
  if (!contentType.includes('text/event-stream')) {
52
57
  const data = await res.json();
53
58
  const parsed = parseResponse(data);
59
+ const content = asRecord(data)?.content;
60
+ if (Array.isArray(content))
61
+ content.forEach((block, i) => {
62
+ const record = asRecord(block);
63
+ if (record)
64
+ responseBlocks.set(i, record);
65
+ });
54
66
  inputTokens = parsed.inputTokens;
55
67
  outputTokens = parsed.outputTokens;
56
68
  stopReason = parsed.stopReason;
@@ -77,7 +89,12 @@ export class AnthropicAdapter {
77
89
  if (!record)
78
90
  continue;
79
91
  const type = asString(record.type);
80
- if (type === 'message_start') {
92
+ if (type === 'error') {
93
+ const error = asRecord(record.error);
94
+ const statuses = { overloaded_error: 529, rate_limit_error: 429, authentication_error: 401, permission_error: 403, invalid_request_error: 400, not_found_error: 404, api_error: 500, billing_error: 402, timeout_error: 504, request_too_large: 413 };
95
+ throw classify(statuses[asString(error?.type) ?? ''] ?? 500, JSON.stringify(record), conn.provider);
96
+ }
97
+ else if (type === 'message_start') {
81
98
  const usage = asRecord(asRecord(record.message)?.usage);
82
99
  inputTokens = asNumber(usage?.input_tokens) ?? inputTokens;
83
100
  outputTokens = asNumber(usage?.output_tokens) ?? outputTokens;
@@ -85,6 +102,8 @@ export class AnthropicAdapter {
85
102
  else if (type === 'content_block_start') {
86
103
  const index = asNumber(record.index) ?? 0;
87
104
  const block = asRecord(record.content_block);
105
+ if (block)
106
+ responseBlocks.set(index, { ...block });
88
107
  if (block?.type === 'tool_use') {
89
108
  blocks.set(index, { id: asString(block.id) ?? randomId(), name: asString(block.name) ?? 'unknown', raw: '' });
90
109
  }
@@ -92,6 +111,14 @@ export class AnthropicAdapter {
92
111
  else if (type === 'content_block_delta') {
93
112
  const index = asNumber(record.index) ?? 0;
94
113
  const delta = asRecord(record.delta);
114
+ const block = responseBlocks.get(index);
115
+ if (block && delta) {
116
+ const field = delta.type === 'text_delta' ? 'text' : delta.type === 'thinking_delta' ? 'thinking' : delta.type === 'signature_delta' ? 'signature' : undefined;
117
+ if (field)
118
+ block[field] = String(block[field] ?? '') + String(delta[field] ?? '');
119
+ if (delta.type === 'citations_delta')
120
+ block.citations = [...(Array.isArray(block.citations) ? block.citations : []), delta.citation];
121
+ }
95
122
  if (delta?.type === 'text_delta') {
96
123
  const piece = asString(delta.text) ?? '';
97
124
  if (piece) {
@@ -118,6 +145,9 @@ export class AnthropicAdapter {
118
145
  const partial = blocks.get(index);
119
146
  if (partial) {
120
147
  const call = { id: partial.id, name: partial.name, raw: partial.raw, arguments: parseArgs(partial.raw) };
148
+ const block = responseBlocks.get(index);
149
+ if (block)
150
+ block.input = call.arguments;
121
151
  blocks.delete(index);
122
152
  if (jsonMode && call.name === JSON_MODE_TOOL) {
123
153
  text = jsonTextFrom([call], text);
@@ -149,6 +179,7 @@ export class AnthropicAdapter {
149
179
  const hasTools = emittedTools.length > 0;
150
180
  yield { type: 'done', result: {
151
181
  text,
182
+ providerState: { provider: conn.provider, model: req.model, content: [...responseBlocks.entries()].sort(([a], [b]) => a - b).map(([, block]) => block) },
152
183
  toolCalls: hasTools ? emittedTools : undefined,
153
184
  finishReason: mapStop(stopReason, hasTools),
154
185
  usage: inputTokens !== undefined || outputTokens !== undefined ? { inputTokens, outputTokens, estimated: false } : estimatedUsage(textFromMessages(req.messages), text),
@@ -192,9 +223,15 @@ function body(req, jsonMode) {
192
223
  top_p: req.topP,
193
224
  stop_sequences: req.stopSequences,
194
225
  system,
195
- messages: toAnthropicMessages(req.messages.filter((m) => m.role !== 'system')),
226
+ messages: toAnthropicMessages(req.messages.filter((m) => m.role !== 'system'), req.model),
196
227
  stream: true,
197
228
  };
229
+ if (req.responseFormat?.type === 'json' && modernModel(req.model) && req.responseFormat.schema) {
230
+ base.output_config = { format: { type: 'json_schema', schema: schemaFor(req) } };
231
+ }
232
+ if (req.responseFormat?.type === 'json' && modernModel(req.model) && !req.responseFormat.schema) {
233
+ base.system = [system, 'Return only valid JSON, without markdown fences or a preamble.'].filter(Boolean).join('\n\n');
234
+ }
198
235
  if (jsonMode) {
199
236
  // Forced-tool JSON mode: Anthropic has no response_format, so we make the
200
237
  // model answer through a single required tool whose input is the JSON we want.
@@ -215,7 +252,14 @@ function body(req, jsonMode) {
215
252
  // `budget_tokens < max_tokens` and rejects temperature/top_p alongside
216
253
  // thinking, so the budget is fitted under max_tokens (raising max_tokens
217
254
  // when the caller's value leaves no room) and the samplers are dropped.
218
- if (req.reasoningEffort === 'none') {
255
+ if (modernModel(req.model)) {
256
+ const none = req.reasoningEffort === 'none';
257
+ if (none && alwaysThinking(req.model))
258
+ throw new AIError('This Claude model cannot disable thinking; choose a reasoning effort instead.', { kind: 'invalid_request', provider: 'anthropic' });
259
+ base.thinking = { type: none ? (/^claude-sonnet-5-5(?:-|$)/.test(req.model) ? 'between_tools' : 'disabled') : 'adaptive' };
260
+ base.output_config = { ...asRecord(base.output_config), effort: none ? 'low' : req.reasoningEffort === 'minimal' ? 'low' : req.reasoningEffort };
261
+ }
262
+ else if (req.reasoningEffort === 'none') {
219
263
  base.thinking = { type: 'disabled' };
220
264
  }
221
265
  else {
@@ -230,14 +274,24 @@ function body(req, jsonMode) {
230
274
  }
231
275
  // providerOptions carries Anthropic-native fields (`thinking`, `metadata`,
232
276
  // top-level `cache_control` extras, `service_tier`, …) without a release.
233
- return applyProviderOptions(base, req.providerOptions);
277
+ const result = applyProviderOptions(base, req.providerOptions, ['output_config']);
278
+ if (modernModel(req.model)) {
279
+ delete result.temperature;
280
+ delete result.top_p;
281
+ delete result.top_k;
282
+ }
283
+ const choice = asRecord(result.tool_choice);
284
+ if (noForcedTools(req.model) && (choice?.type === 'tool' || choice?.type === 'any')) {
285
+ throw new AIError('This Claude model does not support forced tool choice; use auto or structured outputs.', { kind: 'invalid_request', provider: 'anthropic' });
286
+ }
287
+ return result;
234
288
  }
235
- function toAnthropicMessages(messages) {
289
+ function toAnthropicMessages(messages, model) {
236
290
  const out = [];
237
291
  for (let index = 0; index < messages.length; index += 1) {
238
292
  const message = messages[index];
239
293
  if (message.role !== 'tool') {
240
- out.push(toAnthropicMessage(message));
294
+ out.push(toAnthropicMessage(message, model));
241
295
  continue;
242
296
  }
243
297
  const content = [];
@@ -251,7 +305,10 @@ function toAnthropicMessages(messages) {
251
305
  }
252
306
  return out;
253
307
  }
254
- function toAnthropicMessage(m) {
308
+ function toAnthropicMessage(m, model) {
309
+ if (m.role === 'assistant' && m.providerState?.provider === 'anthropic' && m.providerState.model === model) {
310
+ return { role: 'assistant', content: m.providerState.content };
311
+ }
255
312
  // Tool-role messages are handled by toAnthropicMessages' batching loop and
256
313
  // never reach here, so no tool branch is needed.
257
314
  // assistant turns that carried tool calls replay them as tool_use blocks,
@@ -316,8 +373,21 @@ function jsonTextFrom(toolCalls, fallback) {
316
373
  function mapStop(stop, hasTools) {
317
374
  if (stop === 'tool_use' && hasTools)
318
375
  return 'tool';
376
+ if (stop === 'refusal')
377
+ return 'content_filter';
319
378
  if (stop === 'max_tokens')
320
379
  return 'length';
321
380
  return 'stop';
322
381
  }
382
+ /** Models released after Opus 4.6 use adaptive effort and reject sampling knobs.
383
+ * Unknown/custom IDs retain legacy behavior rather than guessing capabilities. */
384
+ function modernModel(model) {
385
+ return /^claude-(?:opus-(?:4-[78]|5(?:-5)?)|sonnet-5(?:-5)?|haiku-5-5|(?:fable|mythos)-5(?:-1)?|mythos-preview)(?:-\d{8}|-latest)?$/.test(model);
386
+ }
387
+ function alwaysThinking(model) {
388
+ return /^claude-(?:fable-|mythos-|opus-5-5(?:-|$))/.test(model);
389
+ }
390
+ function noForcedTools(model) {
391
+ return /^claude-(?:(?:opus|sonnet)-5-5(?:-|$)|(?:fable|mythos)-5-1(?:-|$))/.test(model);
392
+ }
323
393
  //# sourceMappingURL=anthropic.js.map
@@ -32,6 +32,7 @@ async function* run(opts, resolveResult) {
32
32
  yield emit(opts, { type: 'step_start', step });
33
33
  const calls = [];
34
34
  let stepText = '';
35
+ let providerState;
35
36
  let streamFailure;
36
37
  // In promptJson mode a tool directive must not leak into the visible
37
38
  // stream, so directive-looking text is withheld from `delta` events (still
@@ -65,6 +66,7 @@ async function* run(opts, resolveResult) {
65
66
  if (event.type === 'error')
66
67
  streamFailure = event.error;
67
68
  if (event.type === 'done') {
69
+ providerState = event.result.providerState;
68
70
  usage = usage ? mergeUsage(usage, event.result.usage) : event.result.usage;
69
71
  if (toolMode === 'promptJson' && calls.length === 0)
70
72
  calls.push(...callsFromPromptJson(stepText));
@@ -85,7 +87,7 @@ async function* run(opts, resolveResult) {
85
87
  finalText = stepText;
86
88
  messages.push(toolMode === 'promptJson'
87
89
  ? { role: 'assistant', content: stepText }
88
- : { role: 'assistant', content: stepText, toolCalls: calls.length ? calls : undefined });
90
+ : { role: 'assistant', content: stepText, providerState, toolCalls: calls.length ? calls : undefined });
89
91
  if (calls.length === 0)
90
92
  return yield* yieldDone('finished');
91
93
  for (const call of calls) {
@@ -26,7 +26,8 @@ class AnthropicAdapter {
26
26
  }
27
27
  async *stream(conn, req) {
28
28
  const started = Date.now();
29
- const jsonMode = req.responseFormat?.type === 'json' && !req.tools?.length;
29
+ const nativeJson = modernModel(req.model);
30
+ const jsonMode = req.responseFormat?.type === 'json' && !nativeJson && !req.tools?.length;
30
31
  let firstTokenMs = null;
31
32
  let text = '';
32
33
  let inputTokens;
@@ -34,6 +35,7 @@ class AnthropicAdapter {
34
35
  let stopReason;
35
36
  let sawTerminal = false;
36
37
  const emittedTools = [];
38
+ const responseBlocks = new Map();
37
39
  // Partial tool_use blocks keyed by content-block index (input_json_delta accumulation).
38
40
  const blocks = new Map();
39
41
  const timeout = (0, base_js_1.streamTimeout)(conn, req.signal);
@@ -41,19 +43,29 @@ class AnthropicAdapter {
41
43
  // Anthropic JSON mode is implemented as a forced `json_output` tool, which
42
44
  // cannot coexist with the caller's own tools — same silent-downgrade shape
43
45
  // as Gemini, so it gets the same signal rather than quietly dropping.
44
- if (req.responseFormat?.type === 'json' && req.tools?.length) {
46
+ if (req.responseFormat?.type === 'json' && !nativeJson && req.tools?.length) {
45
47
  yield {
46
48
  type: 'context',
47
49
  kind: 'json_mode_downgraded',
48
50
  data: { reason: 'Anthropic JSON mode uses a forced tool and cannot be combined with caller tools; the request was sent without JSON mode', provider: conn.provider },
49
51
  };
50
52
  }
53
+ if (req.responseFormat?.type === 'json' && nativeJson && !req.responseFormat.schema) {
54
+ yield { type: 'context', kind: 'json_mode_downgraded', data: { provider: conn.provider, reason: 'Claude native JSON outputs require a schema; using a JSON instruction without constrained decoding.' } };
55
+ }
51
56
  try {
52
57
  const res = await (0, transport_js_1.postResponse)(`${conn.baseUrl}/v1/messages`, body(req, jsonMode), conn.headers, timeout.signal, conn.provider);
53
58
  const contentType = res.headers.get('content-type') ?? '';
54
59
  if (!contentType.includes('text/event-stream')) {
55
60
  const data = await res.json();
56
61
  const parsed = parseResponse(data);
62
+ const content = (0, util_js_1.asRecord)(data)?.content;
63
+ if (Array.isArray(content))
64
+ content.forEach((block, i) => {
65
+ const record = (0, util_js_1.asRecord)(block);
66
+ if (record)
67
+ responseBlocks.set(i, record);
68
+ });
57
69
  inputTokens = parsed.inputTokens;
58
70
  outputTokens = parsed.outputTokens;
59
71
  stopReason = parsed.stopReason;
@@ -80,7 +92,12 @@ class AnthropicAdapter {
80
92
  if (!record)
81
93
  continue;
82
94
  const type = (0, util_js_1.asString)(record.type);
83
- if (type === 'message_start') {
95
+ if (type === 'error') {
96
+ const error = (0, util_js_1.asRecord)(record.error);
97
+ const statuses = { overloaded_error: 529, rate_limit_error: 429, authentication_error: 401, permission_error: 403, invalid_request_error: 400, not_found_error: 404, api_error: 500, billing_error: 402, timeout_error: 504, request_too_large: 413 };
98
+ throw (0, errors_js_1.classify)(statuses[(0, util_js_1.asString)(error?.type) ?? ''] ?? 500, JSON.stringify(record), conn.provider);
99
+ }
100
+ else if (type === 'message_start') {
84
101
  const usage = (0, util_js_1.asRecord)((0, util_js_1.asRecord)(record.message)?.usage);
85
102
  inputTokens = (0, util_js_1.asNumber)(usage?.input_tokens) ?? inputTokens;
86
103
  outputTokens = (0, util_js_1.asNumber)(usage?.output_tokens) ?? outputTokens;
@@ -88,6 +105,8 @@ class AnthropicAdapter {
88
105
  else if (type === 'content_block_start') {
89
106
  const index = (0, util_js_1.asNumber)(record.index) ?? 0;
90
107
  const block = (0, util_js_1.asRecord)(record.content_block);
108
+ if (block)
109
+ responseBlocks.set(index, { ...block });
91
110
  if (block?.type === 'tool_use') {
92
111
  blocks.set(index, { id: (0, util_js_1.asString)(block.id) ?? (0, base_js_1.randomId)(), name: (0, util_js_1.asString)(block.name) ?? 'unknown', raw: '' });
93
112
  }
@@ -95,6 +114,14 @@ class AnthropicAdapter {
95
114
  else if (type === 'content_block_delta') {
96
115
  const index = (0, util_js_1.asNumber)(record.index) ?? 0;
97
116
  const delta = (0, util_js_1.asRecord)(record.delta);
117
+ const block = responseBlocks.get(index);
118
+ if (block && delta) {
119
+ const field = delta.type === 'text_delta' ? 'text' : delta.type === 'thinking_delta' ? 'thinking' : delta.type === 'signature_delta' ? 'signature' : undefined;
120
+ if (field)
121
+ block[field] = String(block[field] ?? '') + String(delta[field] ?? '');
122
+ if (delta.type === 'citations_delta')
123
+ block.citations = [...(Array.isArray(block.citations) ? block.citations : []), delta.citation];
124
+ }
98
125
  if (delta?.type === 'text_delta') {
99
126
  const piece = (0, util_js_1.asString)(delta.text) ?? '';
100
127
  if (piece) {
@@ -121,6 +148,9 @@ class AnthropicAdapter {
121
148
  const partial = blocks.get(index);
122
149
  if (partial) {
123
150
  const call = { id: partial.id, name: partial.name, raw: partial.raw, arguments: (0, base_js_1.parseArgs)(partial.raw) };
151
+ const block = responseBlocks.get(index);
152
+ if (block)
153
+ block.input = call.arguments;
124
154
  blocks.delete(index);
125
155
  if (jsonMode && call.name === JSON_MODE_TOOL) {
126
156
  text = jsonTextFrom([call], text);
@@ -152,6 +182,7 @@ class AnthropicAdapter {
152
182
  const hasTools = emittedTools.length > 0;
153
183
  yield { type: 'done', result: {
154
184
  text,
185
+ providerState: { provider: conn.provider, model: req.model, content: [...responseBlocks.entries()].sort(([a], [b]) => a - b).map(([, block]) => block) },
155
186
  toolCalls: hasTools ? emittedTools : undefined,
156
187
  finishReason: mapStop(stopReason, hasTools),
157
188
  usage: inputTokens !== undefined || outputTokens !== undefined ? { inputTokens, outputTokens, estimated: false } : (0, tokens_js_1.estimatedUsage)((0, util_js_1.textFromMessages)(req.messages), text),
@@ -196,9 +227,15 @@ function body(req, jsonMode) {
196
227
  top_p: req.topP,
197
228
  stop_sequences: req.stopSequences,
198
229
  system,
199
- messages: toAnthropicMessages(req.messages.filter((m) => m.role !== 'system')),
230
+ messages: toAnthropicMessages(req.messages.filter((m) => m.role !== 'system'), req.model),
200
231
  stream: true,
201
232
  };
233
+ if (req.responseFormat?.type === 'json' && modernModel(req.model) && req.responseFormat.schema) {
234
+ base.output_config = { format: { type: 'json_schema', schema: schemaFor(req) } };
235
+ }
236
+ if (req.responseFormat?.type === 'json' && modernModel(req.model) && !req.responseFormat.schema) {
237
+ base.system = [system, 'Return only valid JSON, without markdown fences or a preamble.'].filter(Boolean).join('\n\n');
238
+ }
202
239
  if (jsonMode) {
203
240
  // Forced-tool JSON mode: Anthropic has no response_format, so we make the
204
241
  // model answer through a single required tool whose input is the JSON we want.
@@ -219,7 +256,14 @@ function body(req, jsonMode) {
219
256
  // `budget_tokens < max_tokens` and rejects temperature/top_p alongside
220
257
  // thinking, so the budget is fitted under max_tokens (raising max_tokens
221
258
  // when the caller's value leaves no room) and the samplers are dropped.
222
- if (req.reasoningEffort === 'none') {
259
+ if (modernModel(req.model)) {
260
+ const none = req.reasoningEffort === 'none';
261
+ if (none && alwaysThinking(req.model))
262
+ throw new errors_js_1.AIError('This Claude model cannot disable thinking; choose a reasoning effort instead.', { kind: 'invalid_request', provider: 'anthropic' });
263
+ base.thinking = { type: none ? (/^claude-sonnet-5-5(?:-|$)/.test(req.model) ? 'between_tools' : 'disabled') : 'adaptive' };
264
+ base.output_config = { ...(0, util_js_1.asRecord)(base.output_config), effort: none ? 'low' : req.reasoningEffort === 'minimal' ? 'low' : req.reasoningEffort };
265
+ }
266
+ else if (req.reasoningEffort === 'none') {
223
267
  base.thinking = { type: 'disabled' };
224
268
  }
225
269
  else {
@@ -234,14 +278,24 @@ function body(req, jsonMode) {
234
278
  }
235
279
  // providerOptions carries Anthropic-native fields (`thinking`, `metadata`,
236
280
  // top-level `cache_control` extras, `service_tier`, …) without a release.
237
- return (0, util_js_1.applyProviderOptions)(base, req.providerOptions);
281
+ const result = (0, util_js_1.applyProviderOptions)(base, req.providerOptions, ['output_config']);
282
+ if (modernModel(req.model)) {
283
+ delete result.temperature;
284
+ delete result.top_p;
285
+ delete result.top_k;
286
+ }
287
+ const choice = (0, util_js_1.asRecord)(result.tool_choice);
288
+ if (noForcedTools(req.model) && (choice?.type === 'tool' || choice?.type === 'any')) {
289
+ throw new errors_js_1.AIError('This Claude model does not support forced tool choice; use auto or structured outputs.', { kind: 'invalid_request', provider: 'anthropic' });
290
+ }
291
+ return result;
238
292
  }
239
- function toAnthropicMessages(messages) {
293
+ function toAnthropicMessages(messages, model) {
240
294
  const out = [];
241
295
  for (let index = 0; index < messages.length; index += 1) {
242
296
  const message = messages[index];
243
297
  if (message.role !== 'tool') {
244
- out.push(toAnthropicMessage(message));
298
+ out.push(toAnthropicMessage(message, model));
245
299
  continue;
246
300
  }
247
301
  const content = [];
@@ -255,7 +309,10 @@ function toAnthropicMessages(messages) {
255
309
  }
256
310
  return out;
257
311
  }
258
- function toAnthropicMessage(m) {
312
+ function toAnthropicMessage(m, model) {
313
+ if (m.role === 'assistant' && m.providerState?.provider === 'anthropic' && m.providerState.model === model) {
314
+ return { role: 'assistant', content: m.providerState.content };
315
+ }
259
316
  // Tool-role messages are handled by toAnthropicMessages' batching loop and
260
317
  // never reach here, so no tool branch is needed.
261
318
  // assistant turns that carried tool calls replay them as tool_use blocks,
@@ -320,7 +377,20 @@ function jsonTextFrom(toolCalls, fallback) {
320
377
  function mapStop(stop, hasTools) {
321
378
  if (stop === 'tool_use' && hasTools)
322
379
  return 'tool';
380
+ if (stop === 'refusal')
381
+ return 'content_filter';
323
382
  if (stop === 'max_tokens')
324
383
  return 'length';
325
384
  return 'stop';
326
385
  }
386
+ /** Models released after Opus 4.6 use adaptive effort and reject sampling knobs.
387
+ * Unknown/custom IDs retain legacy behavior rather than guessing capabilities. */
388
+ function modernModel(model) {
389
+ return /^claude-(?:opus-(?:4-[78]|5(?:-5)?)|sonnet-5(?:-5)?|haiku-5-5|(?:fable|mythos)-5(?:-1)?|mythos-preview)(?:-\d{8}|-latest)?$/.test(model);
390
+ }
391
+ function alwaysThinking(model) {
392
+ return /^claude-(?:fable-|mythos-|opus-5-5(?:-|$))/.test(model);
393
+ }
394
+ function noForcedTools(model) {
395
+ return /^claude-(?:(?:opus|sonnet)-5-5(?:-|$)|(?:fable|mythos)-5-1(?:-|$))/.test(model);
396
+ }
@@ -35,6 +35,7 @@ async function* run(opts, resolveResult) {
35
35
  yield emit(opts, { type: 'step_start', step });
36
36
  const calls = [];
37
37
  let stepText = '';
38
+ let providerState;
38
39
  let streamFailure;
39
40
  // In promptJson mode a tool directive must not leak into the visible
40
41
  // stream, so directive-looking text is withheld from `delta` events (still
@@ -68,6 +69,7 @@ async function* run(opts, resolveResult) {
68
69
  if (event.type === 'error')
69
70
  streamFailure = event.error;
70
71
  if (event.type === 'done') {
72
+ providerState = event.result.providerState;
71
73
  usage = usage ? (0, tokens_js_1.mergeUsage)(usage, event.result.usage) : event.result.usage;
72
74
  if (toolMode === 'promptJson' && calls.length === 0)
73
75
  calls.push(...callsFromPromptJson(stepText));
@@ -88,7 +90,7 @@ async function* run(opts, resolveResult) {
88
90
  finalText = stepText;
89
91
  messages.push(toolMode === 'promptJson'
90
92
  ? { role: 'assistant', content: stepText }
91
- : { role: 'assistant', content: stepText, toolCalls: calls.length ? calls : undefined });
93
+ : { role: 'assistant', content: stepText, providerState, toolCalls: calls.length ? calls : undefined });
92
94
  if (calls.length === 0)
93
95
  return yield* yieldDone('finished');
94
96
  for (const call of calls) {
@@ -86,7 +86,7 @@ class AIHandler {
86
86
  }
87
87
  continue;
88
88
  }
89
- if (event.type === 'delta' || event.type === 'tool_call')
89
+ if (event.type === 'delta' || event.type === 'tool_call' || event.type === 'reasoning')
90
90
  emittedOutput = true;
91
91
  if (event.type === 'delta' && stripper) {
92
92
  // Inline reasoning never reaches `delta`; it rides the reasoning channel like a
package/dist/handler.js CHANGED
@@ -82,7 +82,7 @@ export class AIHandler {
82
82
  }
83
83
  continue;
84
84
  }
85
- if (event.type === 'delta' || event.type === 'tool_call')
85
+ if (event.type === 'delta' || event.type === 'tool_call' || event.type === 'reasoning')
86
86
  emittedOutput = true;
87
87
  if (event.type === 'delta' && stripper) {
88
88
  // Inline reasoning never reaches `delta`; it rides the reasoning channel like a
package/dist/types.d.ts CHANGED
@@ -7,9 +7,17 @@ export interface ContentPart {
7
7
  imageBase64?: string;
8
8
  mimeType?: string;
9
9
  }
10
+ /** Opaque provider response content, for replay within the same model/tool workflow.
11
+ * Keep it unmodified; never include it in telemetry or display it as answer text. */
12
+ export interface ProviderState {
13
+ provider: string;
14
+ model: string;
15
+ content: Record<string, unknown>[];
16
+ }
10
17
  export interface ChatMessage {
11
18
  role: Role;
12
19
  content: string | ContentPart[];
20
+ providerState?: ProviderState;
13
21
  toolCalls?: ToolCall[];
14
22
  toolCallId?: string;
15
23
  name?: string;
@@ -130,6 +138,7 @@ export interface Usage {
130
138
  }
131
139
  export interface ChatResult {
132
140
  text: string;
141
+ providerState?: ProviderState;
133
142
  toolCalls?: ToolCall[];
134
143
  finishReason: 'stop' | 'length' | 'tool' | 'content_filter' | 'error' | 'canceled';
135
144
  usage: Usage;
@@ -1,2 +1,2 @@
1
- 0.7.0
2
- e0e4c28d06eb5e7474c628652e1c56fa5f29ce41c24428270895d7af3b8cd75e
1
+ 0.7.3
2
+ f6bc628594f887826274e9019dff75be686d5043e9823ccdbb0a4d9765552752
@@ -1 +1 @@
1
- {"version":3,"file":"anthropic.d.ts","sourceRoot":"","sources":["../../../src/adapters/engines/anthropic.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAe,WAAW,EAAE,UAAU,EAAE,SAAS,EAAE,eAAe,EAAE,kBAAkB,EAAE,WAAW,EAAY,MAAM,gBAAgB,CAAC;AAMlJ,qBAAa,gBAAiB,YAAW,eAAe;IACtD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;gBACd,QAAQ,EAAE,MAAM;IAItB,IAAI,CAAC,IAAI,EAAE,kBAAkB,EAAE,GAAG,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC;IAUpE,MAAM,CAAC,IAAI,EAAE,kBAAkB,EAAE,GAAG,EAAE,WAAW,GAAG,aAAa,CAAC,WAAW,CAAC;IA2HrF;;;;OAIG;IACG,UAAU,CAAC,IAAI,EAAE,kBAAkB,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;CAajE"}
1
+ {"version":3,"file":"anthropic.d.ts","sourceRoot":"","sources":["../../../src/adapters/engines/anthropic.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAe,WAAW,EAAE,UAAU,EAAE,SAAS,EAAE,eAAe,EAAE,kBAAkB,EAAE,WAAW,EAAY,MAAM,gBAAgB,CAAC;AAMlJ,qBAAa,gBAAiB,YAAW,eAAe;IACtD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;gBACd,QAAQ,EAAE,MAAM;IAItB,IAAI,CAAC,IAAI,EAAE,kBAAkB,EAAE,GAAG,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC;IAUpE,MAAM,CAAC,IAAI,EAAE,kBAAkB,EAAE,GAAG,EAAE,WAAW,GAAG,aAAa,CAAC,WAAW,CAAC;IAmJrF;;;;OAIG;IACG,UAAU,CAAC,IAAI,EAAE,kBAAkB,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;CAajE"}