@jxburros/ai-nugget 0.6.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.
Files changed (82) hide show
  1. package/README.md +92 -3
  2. package/UPGRADING.md +43 -1
  3. package/dist/adapters/engines/anthropic.js +98 -10
  4. package/dist/adapters/engines/google.js +8 -1
  5. package/dist/adapters/engines/ollama.js +6 -0
  6. package/dist/adapters/engines/openaiChat.d.ts +6 -0
  7. package/dist/adapters/engines/openaiChat.js +51 -1
  8. package/dist/agent/loop.d.ts +3 -1
  9. package/dist/agent/loop.js +4 -1
  10. package/dist/cjs/adapters/engines/anthropic.js +96 -8
  11. package/dist/cjs/adapters/engines/google.js +7 -0
  12. package/dist/cjs/adapters/engines/ollama.js +6 -0
  13. package/dist/cjs/adapters/engines/openaiChat.js +52 -1
  14. package/dist/cjs/agent/loop.js +4 -1
  15. package/dist/cjs/handler.js +28 -3
  16. package/dist/cjs/index.js +1 -0
  17. package/dist/cjs/reasoning.js +159 -0
  18. package/dist/cjs/util.js +14 -0
  19. package/dist/handler.d.ts +6 -0
  20. package/dist/handler.js +28 -3
  21. package/dist/index.d.ts +1 -0
  22. package/dist/index.js +1 -0
  23. package/dist/reasoning.d.ts +45 -0
  24. package/dist/reasoning.js +154 -0
  25. package/dist/types.d.ts +25 -0
  26. package/dist/util.d.ts +7 -0
  27. package/dist/util.js +13 -0
  28. package/nugget/VERSION.txt +2 -2
  29. package/nugget/dist/adapters/engines/anthropic.d.ts.map +1 -1
  30. package/nugget/dist/adapters/engines/anthropic.js +98 -10
  31. package/nugget/dist/adapters/engines/anthropic.js.map +1 -1
  32. package/nugget/dist/adapters/engines/google.js +8 -1
  33. package/nugget/dist/adapters/engines/google.js.map +1 -1
  34. package/nugget/dist/adapters/engines/ollama.js +6 -0
  35. package/nugget/dist/adapters/engines/ollama.js.map +1 -1
  36. package/nugget/dist/adapters/engines/openaiChat.d.ts +6 -0
  37. package/nugget/dist/adapters/engines/openaiChat.d.ts.map +1 -1
  38. package/nugget/dist/adapters/engines/openaiChat.js +51 -1
  39. package/nugget/dist/adapters/engines/openaiChat.js.map +1 -1
  40. package/nugget/dist/agent/loop.d.ts +3 -1
  41. package/nugget/dist/agent/loop.d.ts.map +1 -1
  42. package/nugget/dist/agent/loop.js +4 -1
  43. package/nugget/dist/agent/loop.js.map +1 -1
  44. package/nugget/dist/cjs/adapters/engines/anthropic.js +96 -8
  45. package/nugget/dist/cjs/adapters/engines/google.js +7 -0
  46. package/nugget/dist/cjs/adapters/engines/ollama.js +6 -0
  47. package/nugget/dist/cjs/adapters/engines/openaiChat.js +52 -1
  48. package/nugget/dist/cjs/agent/loop.js +4 -1
  49. package/nugget/dist/cjs/handler.js +28 -3
  50. package/nugget/dist/cjs/index.js +1 -0
  51. package/nugget/dist/cjs/reasoning.js +159 -0
  52. package/nugget/dist/cjs/util.js +14 -0
  53. package/nugget/dist/handler.d.ts +6 -0
  54. package/nugget/dist/handler.d.ts.map +1 -1
  55. package/nugget/dist/handler.js +28 -3
  56. package/nugget/dist/handler.js.map +1 -1
  57. package/nugget/dist/index.d.ts +1 -0
  58. package/nugget/dist/index.d.ts.map +1 -1
  59. package/nugget/dist/index.js +1 -0
  60. package/nugget/dist/index.js.map +1 -1
  61. package/nugget/dist/reasoning.d.ts +45 -0
  62. package/nugget/dist/reasoning.d.ts.map +1 -0
  63. package/nugget/dist/reasoning.js +154 -0
  64. package/nugget/dist/reasoning.js.map +1 -0
  65. package/nugget/dist/types.d.ts +25 -0
  66. package/nugget/dist/types.d.ts.map +1 -1
  67. package/nugget/dist/types.js.map +1 -1
  68. package/nugget/dist/util.d.ts +7 -0
  69. package/nugget/dist/util.d.ts.map +1 -1
  70. package/nugget/dist/util.js +13 -0
  71. package/nugget/dist/util.js.map +1 -1
  72. package/nugget/src/adapters/engines/anthropic.ts +87 -10
  73. package/nugget/src/adapters/engines/google.ts +8 -1
  74. package/nugget/src/adapters/engines/ollama.ts +5 -0
  75. package/nugget/src/adapters/engines/openaiChat.ts +46 -1
  76. package/nugget/src/agent/loop.ts +7 -2
  77. package/nugget/src/handler.ts +30 -3
  78. package/nugget/src/index.ts +1 -0
  79. package/nugget/src/reasoning.ts +174 -0
  80. package/nugget/src/types.ts +27 -0
  81. package/nugget/src/util.ts +14 -0
  82. package/package.json +12 -5
package/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # AI Nugget
2
2
 
3
- **Version 0.6.0** · [What's changed since 0.5.0](#whats-changed-since-050) · [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,
@@ -108,12 +119,76 @@ new AIHandler({
108
119
  - **Seams** — `KeySource` (env/literal/memory/chain + ref parsing), `Redactor`,
109
120
  neutral `GovernancePolicy` (`blocklistPolicy`/`allowlistPolicy`/`composePolicies`),
110
121
  `TelemetrySink`, and an optional `pricing` hook. `providerOptions` passes
111
- provider-native fields through without waiting for a library release.
122
+ provider-native fields through without waiting for a library release;
123
+ `reasoningEffort` is one word for "how hard should the model think", mapped
124
+ per provider ([docs](./docs/providers.md#reasoning-effort)).
112
125
  - **Agent layer** (`@jxburros/ai-nugget/agent`) — `defineTool`, `runAgent()`
113
126
  model↔tool loop over the full pipeline, streamed `AgentEvent`s, budgets with
114
127
  honest `stopReason`s, an `ApprovalGate` for side-effecting tools, and
115
128
  `native`/`promptJson`/`auto` tool modes.
116
129
 
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
172
+
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
174
+ [changelog](./CHANGELOG.md) and [UPGRADING.md](./UPGRADING.md#06x--070).
175
+
176
+ - **`ChatRequest.reasoningEffort`** (`none` / `minimal` / `low` / `medium` /
177
+ `high`) — one vocabulary for reasoning effort, mapped onto OpenAI
178
+ `reasoning_effort`, Anthropic `thinking`, Google `thinkingConfig`, and Ollama
179
+ `think`; forwarded by the agent loop via `AgentOptions.reasoningEffort`.
180
+ Never sent unless you set it. See [Reasoning effort](./docs/providers.md#reasoning-effort).
181
+ - **OpenAI reasoning models can call tools again.** `/chat/completions` refuses
182
+ function tools unless `reasoning_effort` is `'none'`; the `openaiChat` engine
183
+ now retries that one request with it set and emits a
184
+ `reasoning_effort_disabled_for_tools` context event so the change is visible.
185
+ Learned from AI Server Studio's Build 3 shakedown, where every tool-using
186
+ turn on `gpt-5.x`/`gpt-6` 400'd.
187
+ - **Inline `<think>` reasoning never reaches `delta` or `result.text`.** The
188
+ handler strips it on every engine and emits it as `reasoning` events
189
+ (`stripInlineReasoning: false` opts out). Ported from the stripper AI Server
190
+ Studio had to run at every call site. See [Inline reasoning](./docs/providers.md#inline-reasoning-is-stripped-at-the-seam).
191
+
117
192
  ## What's changed since 0.5.0
118
193
 
119
194
  **0.6.0 is backward-compatible** — every change below is additive. The one thing
@@ -177,3 +252,17 @@ seam), and not a UI. The governance seam ships neutral: it is where an app *can*
177
252
  enforce rules, not a place the library imposes its own.
178
253
 
179
254
  <!-- GitHub Pages deployment is configured in .github/workflows/pages.yml. -->
255
+
256
+ ---
257
+
258
+ ## Project Stewardship
259
+
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,47 @@
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
+
21
+ ## 0.6.x → 0.7.0
22
+
23
+ **Backward-compatible for ordinary consumers.** Two additive changes and one
24
+ default-behaviour change worth knowing about:
25
+
26
+ - **Inline reasoning is stripped by default.** A model that writes
27
+ `<think>…</think>` (or `<thinking>`, `<reasoning>`, `<|begin_of_thought|>`)
28
+ into its answer no longer has that text in `delta` events or
29
+ `ChatResult.text`; it arrives as `{ type: 'reasoning', text }` events, the
30
+ same channel a provider-native thinking field already used. If your app
31
+ parsed or displayed those tags itself, either delete that code or pass
32
+ `new AIHandler({ stripInlineReasoning: false })` to keep the raw text.
33
+
34
+ - **`ChatRequest.reasoningEffort`** (and `AgentOptions.reasoningEffort`) — a
35
+ new optional field. Nothing is sent to any provider unless you set it. If you
36
+ already pass `reasoning_effort` / `thinking` / `thinkingConfig` / `think`
37
+ through `providerOptions`, that keeps winning on collision.
38
+ - **A new `context` stream event, `reasoning_effort_disabled_for_tools`** — the
39
+ `openaiChat` engine emits it when it retried a tool-using request with
40
+ `reasoning_effort: 'none'` after OpenAI refused the first attempt. Ignoring
41
+ it keeps the old behaviour minus the 400. If you *want* the hard failure
42
+ instead (to route to `/v1/responses` yourself), set `reasoningEffort: 'none'`
43
+ explicitly on tool-using turns — then no retry happens and any 400 is yours.
44
+
3
45
  ## 0.5.x → 0.6.0
4
46
 
5
47
  Behavior-compatible for ordinary consumers, with **two things worth checking**.
@@ -62,7 +104,7 @@ signature changed and no default behavior changed, so an app on 0.4.x can bump
62
104
  without code edits and adopt the new capabilities as needed.
63
105
 
64
106
  The numbers in brackets are the suggestion numbers from the AI Server Studio
65
- 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)),
66
108
  so you can trace each change back to the friction it resolves.
67
109
 
68
110
  ### Packaging
@@ -1,7 +1,7 @@
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
- import { applyProviderOptions, asNumber, asRecord, asString, joinUrl, textFromMessages } from '../../util.js';
4
+ import { applyProviderOptions, REASONING_BUDGET_TOKENS, asNumber, asRecord, asString, joinUrl, textFromMessages } from '../../util.js';
5
5
  import { DEFAULT_TIMEOUT_MS, parseArgs, randomId, safeParse, streamAnomaly, streamError, streamTimeout } from './base.js';
6
6
  const JSON_MODE_TOOL = 'json_output';
7
7
  export class AnthropicAdapter {
@@ -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.
@@ -210,16 +247,51 @@ function body(req, jsonMode) {
210
247
  else if (req.toolChoice === 'none')
211
248
  base.tool_choice = { type: 'none' };
212
249
  }
250
+ if (req.reasoningEffort !== undefined) {
251
+ // First-class reasoning effort → extended thinking. Anthropic requires
252
+ // `budget_tokens < max_tokens` and rejects temperature/top_p alongside
253
+ // thinking, so the budget is fitted under max_tokens (raising max_tokens
254
+ // when the caller's value leaves no room) and the samplers are dropped.
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') {
263
+ base.thinking = { type: 'disabled' };
264
+ }
265
+ else {
266
+ const budget = REASONING_BUDGET_TOKENS[req.reasoningEffort];
267
+ const maxTokens = typeof base.max_tokens === 'number' ? base.max_tokens : 4096;
268
+ if (maxTokens <= budget)
269
+ base.max_tokens = budget + 1024;
270
+ base.thinking = { type: 'enabled', budget_tokens: budget };
271
+ delete base.temperature;
272
+ delete base.top_p;
273
+ }
274
+ }
213
275
  // providerOptions carries Anthropic-native fields (`thinking`, `metadata`,
214
276
  // top-level `cache_control` extras, `service_tier`, …) without a release.
215
- 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;
216
288
  }
217
- function toAnthropicMessages(messages) {
289
+ function toAnthropicMessages(messages, model) {
218
290
  const out = [];
219
291
  for (let index = 0; index < messages.length; index += 1) {
220
292
  const message = messages[index];
221
293
  if (message.role !== 'tool') {
222
- out.push(toAnthropicMessage(message));
294
+ out.push(toAnthropicMessage(message, model));
223
295
  continue;
224
296
  }
225
297
  const content = [];
@@ -233,7 +305,10 @@ function toAnthropicMessages(messages) {
233
305
  }
234
306
  return out;
235
307
  }
236
- 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
+ }
237
312
  // Tool-role messages are handled by toAnthropicMessages' batching loop and
238
313
  // never reach here, so no tool branch is needed.
239
314
  // assistant turns that carried tool calls replay them as tool_use blocks,
@@ -298,8 +373,21 @@ function jsonTextFrom(toolCalls, fallback) {
298
373
  function mapStop(stop, hasTools) {
299
374
  if (stop === 'tool_use' && hasTools)
300
375
  return 'tool';
376
+ if (stop === 'refusal')
377
+ return 'content_filter';
301
378
  if (stop === 'max_tokens')
302
379
  return 'length';
303
380
  return 'stop';
304
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
+ }
305
393
  //# sourceMappingURL=anthropic.js.map
@@ -1,7 +1,7 @@
1
1
  import { AIError } from '../../errors.js';
2
2
  import { estimatedUsage } from '../../tokens.js';
3
3
  import { fetchJson, postResponse, sseLines } from '../../transport.js';
4
- import { applyProviderOptions, asNumber, asRecord, asString, joinUrl, textFromMessages } from '../../util.js';
4
+ import { applyProviderOptions, REASONING_BUDGET_TOKENS, asNumber, asRecord, asString, joinUrl, textFromMessages } from '../../util.js';
5
5
  import { DEFAULT_TIMEOUT_MS, randomId, safeParse, streamAnomaly, streamError, streamTimeout } from './base.js';
6
6
  export class GoogleAdapter {
7
7
  provider;
@@ -184,6 +184,13 @@ function body(req) {
184
184
  config.allowedFunctionNames = [req.toolChoice.name];
185
185
  payload.toolConfig = { functionCallingConfig: config };
186
186
  }
187
+ if (req.reasoningEffort !== undefined) {
188
+ // First-class reasoning effort → Gemini's thinking budget. 0 disables
189
+ // thinking on models that allow it; others treat it as a minimum.
190
+ payload.generationConfig.thinkingConfig = {
191
+ thinkingBudget: REASONING_BUDGET_TOKENS[req.reasoningEffort],
192
+ };
193
+ }
187
194
  // providerOptions reaches Google-native fields: top-level `safetySettings`,
188
195
  // `cachedContent`, and `generationConfig` extras (`thinkingConfig`,
189
196
  // `responseModalities`, …) merged one level deep into generationConfig.
@@ -172,6 +172,12 @@ function body(req) {
172
172
  format: req.responseFormat?.type === 'json' ? (req.responseFormat.schema ?? 'json') : undefined,
173
173
  tools: req.tools?.map((tool) => ({ type: 'function', function: { name: tool.name, description: tool.description, parameters: tool.parameters } })),
174
174
  };
175
+ // First-class reasoning effort → Ollama's `think` flag (reasoning arrives in
176
+ // `message.thinking`, which this engine already routes to `reasoning` events).
177
+ // Ollama has no graded effort for most models, so anything but `'none'` is
178
+ // `true`. Only sent when asked for: a model without thinking support rejects it.
179
+ if (req.reasoningEffort !== undefined)
180
+ base.think = req.reasoningEffort !== 'none';
175
181
  // providerOptions reaches Ollama's real request fields — `options.num_ctx`,
176
182
  // `options.num_keep`, top-level `keep_alive`, `think`, etc. `options` is
177
183
  // merged one level deep so num_ctx joins the samplers above rather than
@@ -13,4 +13,10 @@ export declare class OpenAIChatAdapter implements ProviderAdapter {
13
13
  }>;
14
14
  embed(conn: ResolvedConnection, req: EmbedRequest): Promise<EmbedResult>;
15
15
  }
16
+ /**
17
+ * True when a `/chat/completions` 400 is the "function tools with reasoning_effort
18
+ * are not supported" refusal AND the request actually carried tools AND the caller
19
+ * did not already pin `reasoning_effort` (in which case the choice is theirs).
20
+ */
21
+ export declare function shouldRetryWithoutReasoningEffort(error: unknown, req: ChatRequest): boolean;
16
22
  //# sourceMappingURL=openaiChat.d.ts.map
@@ -34,7 +34,34 @@ export class OpenAIChatAdapter {
34
34
  const timeout = streamTimeout(conn, req.signal);
35
35
  yield { type: 'start', callId: '', provider: conn.provider, model: req.model };
36
36
  try {
37
- const res = await postResponse(urlFor(conn, this.profile, req), openAiBody(req, this.profile), conn.headers, timeout.signal, conn.provider);
37
+ let res;
38
+ try {
39
+ res = await postResponse(urlFor(conn, this.profile, req), openAiBody(req, this.profile), conn.headers, timeout.signal, conn.provider);
40
+ }
41
+ catch (error) {
42
+ // OpenAI's `/chat/completions` refuses function tools on a reasoning model
43
+ // unless `reasoning_effort` is `'none'` ("Function tools with reasoning_effort
44
+ // are not supported … use /v1/responses or set reasoning_effort to 'none'").
45
+ // The nugget never sets `reasoning_effort` itself, so the provider default
46
+ // applies and every tool-using turn 400s. Rather than make each app learn
47
+ // this the hard way, retry the one request once with the effort disabled
48
+ // and say so on the stream. Reactive on purpose: sending
49
+ // `reasoning_effort` up front would 400 on non-reasoning models instead.
50
+ // A caller-requested effort is overridden too — the alternative is a hard
51
+ // 400 with no tool call at all — and the context event says so.
52
+ if (!shouldRetryWithoutReasoningEffort(error, req))
53
+ throw error;
54
+ yield {
55
+ type: 'context',
56
+ kind: 'reasoning_effort_disabled_for_tools',
57
+ data: {
58
+ reason: 'provider rejected function tools with reasoning_effort; retried with reasoning_effort: none',
59
+ requested: req.providerOptions?.reasoning_effort ?? req.reasoningEffort ?? null,
60
+ },
61
+ };
62
+ const retryReq = { ...req, providerOptions: { ...req.providerOptions, reasoning_effort: 'none' } };
63
+ res = await postResponse(urlFor(conn, this.profile, retryReq), openAiBody(retryReq, this.profile), conn.headers, timeout.signal, conn.provider);
64
+ }
38
65
  const contentType = res.headers.get('content-type') ?? '';
39
66
  if (!contentType.includes('text/event-stream')) {
40
67
  // Server ignored stream:true (or is a buffered gateway) — recover the whole body.
@@ -169,6 +196,10 @@ function openAiBody(req, profile) {
169
196
  };
170
197
  if (req.maxTokens !== undefined)
171
198
  body[profile.quirks?.maxTokensParam ?? 'max_tokens'] = req.maxTokens;
199
+ // First-class reasoning effort → OpenAI's `reasoning_effort`. Only sent when
200
+ // asked for: non-reasoning models reject the parameter outright.
201
+ if (req.reasoningEffort !== undefined)
202
+ body.reasoning_effort = req.reasoningEffort;
172
203
  // providerOptions carries OpenAI-native fields the nugget doesn't model
173
204
  // (`reasoning_effort`, `parallel_tool_calls`, `seed`, `logprobs`, …). `apiVersion`
174
205
  // is consumed by urlFor for Azure and stripped here so it never hits the body.
@@ -255,4 +286,23 @@ function mapFinish(finish, hasToolCalls) {
255
286
  return 'content_filter';
256
287
  return 'stop';
257
288
  }
289
+ /**
290
+ * True when a `/chat/completions` 400 is the "function tools with reasoning_effort
291
+ * are not supported" refusal AND the request actually carried tools AND the caller
292
+ * did not already pin `reasoning_effort` (in which case the choice is theirs).
293
+ */
294
+ export function shouldRetryWithoutReasoningEffort(error, req) {
295
+ if (!(error instanceof AIError) || error.status !== 400)
296
+ return false;
297
+ if (!req.tools || req.tools.length === 0)
298
+ return false;
299
+ // Already `'none'` (from either knob) — the refusal is about something else.
300
+ const effective = req.providerOptions && 'reasoning_effort' in req.providerOptions
301
+ ? req.providerOptions.reasoning_effort
302
+ : req.reasoningEffort;
303
+ if (effective === 'none')
304
+ return false;
305
+ const text = `${error.raw ?? ''} ${error.message}`.toLowerCase();
306
+ return text.includes('reasoning_effort') && text.includes('tool');
307
+ }
258
308
  //# sourceMappingURL=openaiChat.js.map
@@ -1,5 +1,5 @@
1
1
  import type { AIHandler } from '../handler.js';
2
- import type { AIErrorKind, ChatMessage, Connection, StreamEvent, ToolCall, Usage } from '../types.js';
2
+ import type { AIErrorKind, ChatMessage, Connection, ReasoningEffort, StreamEvent, ToolCall, Usage } from '../types.js';
3
3
  import type { ToolSpec } from './tools.js';
4
4
  export interface AgentOptions {
5
5
  handler: AIHandler;
@@ -30,6 +30,8 @@ export interface AgentOptions {
30
30
  maxTokens?: number;
31
31
  topP?: number;
32
32
  stopSequences?: string[];
33
+ /** Reasoning effort forwarded to every turn — see {@link ChatRequest.reasoningEffort}. */
34
+ reasoningEffort?: ReasoningEffort;
33
35
  /** Provider-native passthrough forwarded to every turn — see {@link ChatRequest.providerOptions}. */
34
36
  providerOptions?: Record<string, unknown>;
35
37
  budget?: {
@@ -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
@@ -46,6 +47,7 @@ async function* run(opts, resolveResult) {
46
47
  maxTokens: opts.maxTokens,
47
48
  topP: opts.topP,
48
49
  stopSequences: opts.stopSequences,
50
+ reasoningEffort: opts.reasoningEffort,
49
51
  providerOptions: opts.providerOptions,
50
52
  signal: agentSignal.signal,
51
53
  metadata: { ...opts.metadata, agentStep: step },
@@ -64,6 +66,7 @@ async function* run(opts, resolveResult) {
64
66
  if (event.type === 'error')
65
67
  streamFailure = event.error;
66
68
  if (event.type === 'done') {
69
+ providerState = event.result.providerState;
67
70
  usage = usage ? mergeUsage(usage, event.result.usage) : event.result.usage;
68
71
  if (toolMode === 'promptJson' && calls.length === 0)
69
72
  calls.push(...callsFromPromptJson(stepText));
@@ -84,7 +87,7 @@ async function* run(opts, resolveResult) {
84
87
  finalText = stepText;
85
88
  messages.push(toolMode === 'promptJson'
86
89
  ? { role: 'assistant', content: stepText }
87
- : { role: 'assistant', content: stepText, toolCalls: calls.length ? calls : undefined });
90
+ : { role: 'assistant', content: stepText, providerState, toolCalls: calls.length ? calls : undefined });
88
91
  if (calls.length === 0)
89
92
  return yield* yieldDone('finished');
90
93
  for (const call of calls) {