@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
@@ -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.
@@ -214,16 +251,51 @@ function body(req, jsonMode) {
214
251
  else if (req.toolChoice === 'none')
215
252
  base.tool_choice = { type: 'none' };
216
253
  }
254
+ if (req.reasoningEffort !== undefined) {
255
+ // First-class reasoning effort → extended thinking. Anthropic requires
256
+ // `budget_tokens < max_tokens` and rejects temperature/top_p alongside
257
+ // thinking, so the budget is fitted under max_tokens (raising max_tokens
258
+ // when the caller's value leaves no room) and the samplers are dropped.
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') {
267
+ base.thinking = { type: 'disabled' };
268
+ }
269
+ else {
270
+ const budget = util_js_1.REASONING_BUDGET_TOKENS[req.reasoningEffort];
271
+ const maxTokens = typeof base.max_tokens === 'number' ? base.max_tokens : 4096;
272
+ if (maxTokens <= budget)
273
+ base.max_tokens = budget + 1024;
274
+ base.thinking = { type: 'enabled', budget_tokens: budget };
275
+ delete base.temperature;
276
+ delete base.top_p;
277
+ }
278
+ }
217
279
  // providerOptions carries Anthropic-native fields (`thinking`, `metadata`,
218
280
  // top-level `cache_control` extras, `service_tier`, …) without a release.
219
- 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;
220
292
  }
221
- function toAnthropicMessages(messages) {
293
+ function toAnthropicMessages(messages, model) {
222
294
  const out = [];
223
295
  for (let index = 0; index < messages.length; index += 1) {
224
296
  const message = messages[index];
225
297
  if (message.role !== 'tool') {
226
- out.push(toAnthropicMessage(message));
298
+ out.push(toAnthropicMessage(message, model));
227
299
  continue;
228
300
  }
229
301
  const content = [];
@@ -237,7 +309,10 @@ function toAnthropicMessages(messages) {
237
309
  }
238
310
  return out;
239
311
  }
240
- 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
+ }
241
316
  // Tool-role messages are handled by toAnthropicMessages' batching loop and
242
317
  // never reach here, so no tool branch is needed.
243
318
  // assistant turns that carried tool calls replay them as tool_use blocks,
@@ -302,7 +377,20 @@ function jsonTextFrom(toolCalls, fallback) {
302
377
  function mapStop(stop, hasTools) {
303
378
  if (stop === 'tool_use' && hasTools)
304
379
  return 'tool';
380
+ if (stop === 'refusal')
381
+ return 'content_filter';
305
382
  if (stop === 'max_tokens')
306
383
  return 'length';
307
384
  return 'stop';
308
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
+ }
@@ -188,6 +188,13 @@ function body(req) {
188
188
  config.allowedFunctionNames = [req.toolChoice.name];
189
189
  payload.toolConfig = { functionCallingConfig: config };
190
190
  }
191
+ if (req.reasoningEffort !== undefined) {
192
+ // First-class reasoning effort → Gemini's thinking budget. 0 disables
193
+ // thinking on models that allow it; others treat it as a minimum.
194
+ payload.generationConfig.thinkingConfig = {
195
+ thinkingBudget: util_js_1.REASONING_BUDGET_TOKENS[req.reasoningEffort],
196
+ };
197
+ }
191
198
  // providerOptions reaches Google-native fields: top-level `safetySettings`,
192
199
  // `cachedContent`, and `generationConfig` extras (`thinkingConfig`,
193
200
  // `responseModalities`, …) merged one level deep into generationConfig.
@@ -176,6 +176,12 @@ function body(req) {
176
176
  format: req.responseFormat?.type === 'json' ? (req.responseFormat.schema ?? 'json') : undefined,
177
177
  tools: req.tools?.map((tool) => ({ type: 'function', function: { name: tool.name, description: tool.description, parameters: tool.parameters } })),
178
178
  };
179
+ // First-class reasoning effort → Ollama's `think` flag (reasoning arrives in
180
+ // `message.thinking`, which this engine already routes to `reasoning` events).
181
+ // Ollama has no graded effort for most models, so anything but `'none'` is
182
+ // `true`. Only sent when asked for: a model without thinking support rejects it.
183
+ if (req.reasoningEffort !== undefined)
184
+ base.think = req.reasoningEffort !== 'none';
179
185
  // providerOptions reaches Ollama's real request fields — `options.num_ctx`,
180
186
  // `options.num_keep`, top-level `keep_alive`, `think`, etc. `options` is
181
187
  // merged one level deep so num_ctx joins the samplers above rather than
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.OpenAIChatAdapter = void 0;
4
+ exports.shouldRetryWithoutReasoningEffort = shouldRetryWithoutReasoningEffort;
4
5
  const errors_js_1 = require("../../errors.js");
5
6
  const tokens_js_1 = require("../../tokens.js");
6
7
  const transport_js_1 = require("../../transport.js");
@@ -37,7 +38,34 @@ class OpenAIChatAdapter {
37
38
  const timeout = (0, base_js_1.streamTimeout)(conn, req.signal);
38
39
  yield { type: 'start', callId: '', provider: conn.provider, model: req.model };
39
40
  try {
40
- const res = await (0, transport_js_1.postResponse)(urlFor(conn, this.profile, req), openAiBody(req, this.profile), conn.headers, timeout.signal, conn.provider);
41
+ let res;
42
+ try {
43
+ res = await (0, transport_js_1.postResponse)(urlFor(conn, this.profile, req), openAiBody(req, this.profile), conn.headers, timeout.signal, conn.provider);
44
+ }
45
+ catch (error) {
46
+ // OpenAI's `/chat/completions` refuses function tools on a reasoning model
47
+ // unless `reasoning_effort` is `'none'` ("Function tools with reasoning_effort
48
+ // are not supported … use /v1/responses or set reasoning_effort to 'none'").
49
+ // The nugget never sets `reasoning_effort` itself, so the provider default
50
+ // applies and every tool-using turn 400s. Rather than make each app learn
51
+ // this the hard way, retry the one request once with the effort disabled
52
+ // and say so on the stream. Reactive on purpose: sending
53
+ // `reasoning_effort` up front would 400 on non-reasoning models instead.
54
+ // A caller-requested effort is overridden too — the alternative is a hard
55
+ // 400 with no tool call at all — and the context event says so.
56
+ if (!shouldRetryWithoutReasoningEffort(error, req))
57
+ throw error;
58
+ yield {
59
+ type: 'context',
60
+ kind: 'reasoning_effort_disabled_for_tools',
61
+ data: {
62
+ reason: 'provider rejected function tools with reasoning_effort; retried with reasoning_effort: none',
63
+ requested: req.providerOptions?.reasoning_effort ?? req.reasoningEffort ?? null,
64
+ },
65
+ };
66
+ const retryReq = { ...req, providerOptions: { ...req.providerOptions, reasoning_effort: 'none' } };
67
+ res = await (0, transport_js_1.postResponse)(urlFor(conn, this.profile, retryReq), openAiBody(retryReq, this.profile), conn.headers, timeout.signal, conn.provider);
68
+ }
41
69
  const contentType = res.headers.get('content-type') ?? '';
42
70
  if (!contentType.includes('text/event-stream')) {
43
71
  // Server ignored stream:true (or is a buffered gateway) — recover the whole body.
@@ -173,6 +201,10 @@ function openAiBody(req, profile) {
173
201
  };
174
202
  if (req.maxTokens !== undefined)
175
203
  body[profile.quirks?.maxTokensParam ?? 'max_tokens'] = req.maxTokens;
204
+ // First-class reasoning effort → OpenAI's `reasoning_effort`. Only sent when
205
+ // asked for: non-reasoning models reject the parameter outright.
206
+ if (req.reasoningEffort !== undefined)
207
+ body.reasoning_effort = req.reasoningEffort;
176
208
  // providerOptions carries OpenAI-native fields the nugget doesn't model
177
209
  // (`reasoning_effort`, `parallel_tool_calls`, `seed`, `logprobs`, …). `apiVersion`
178
210
  // is consumed by urlFor for Azure and stripped here so it never hits the body.
@@ -259,3 +291,22 @@ function mapFinish(finish, hasToolCalls) {
259
291
  return 'content_filter';
260
292
  return 'stop';
261
293
  }
294
+ /**
295
+ * True when a `/chat/completions` 400 is the "function tools with reasoning_effort
296
+ * are not supported" refusal AND the request actually carried tools AND the caller
297
+ * did not already pin `reasoning_effort` (in which case the choice is theirs).
298
+ */
299
+ function shouldRetryWithoutReasoningEffort(error, req) {
300
+ if (!(error instanceof errors_js_1.AIError) || error.status !== 400)
301
+ return false;
302
+ if (!req.tools || req.tools.length === 0)
303
+ return false;
304
+ // Already `'none'` (from either knob) — the refusal is about something else.
305
+ const effective = req.providerOptions && 'reasoning_effort' in req.providerOptions
306
+ ? req.providerOptions.reasoning_effort
307
+ : req.reasoningEffort;
308
+ if (effective === 'none')
309
+ return false;
310
+ const text = `${error.raw ?? ''} ${error.message}`.toLowerCase();
311
+ return text.includes('reasoning_effort') && text.includes('tool');
312
+ }
@@ -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
@@ -49,6 +50,7 @@ async function* run(opts, resolveResult) {
49
50
  maxTokens: opts.maxTokens,
50
51
  topP: opts.topP,
51
52
  stopSequences: opts.stopSequences,
53
+ reasoningEffort: opts.reasoningEffort,
52
54
  providerOptions: opts.providerOptions,
53
55
  signal: agentSignal.signal,
54
56
  metadata: { ...opts.metadata, agentStep: step },
@@ -67,6 +69,7 @@ async function* run(opts, resolveResult) {
67
69
  if (event.type === 'error')
68
70
  streamFailure = event.error;
69
71
  if (event.type === 'done') {
72
+ providerState = event.result.providerState;
70
73
  usage = usage ? (0, tokens_js_1.mergeUsage)(usage, event.result.usage) : event.result.usage;
71
74
  if (toolMode === 'promptJson' && calls.length === 0)
72
75
  calls.push(...callsFromPromptJson(stepText));
@@ -87,7 +90,7 @@ async function* run(opts, resolveResult) {
87
90
  finalText = stepText;
88
91
  messages.push(toolMode === 'promptJson'
89
92
  ? { role: 'assistant', content: stepText }
90
- : { role: 'assistant', content: stepText, toolCalls: calls.length ? calls : undefined });
93
+ : { role: 'assistant', content: stepText, providerState, toolCalls: calls.length ? calls : undefined });
91
94
  if (calls.length === 0)
92
95
  return yield* yieldDone('finished');
93
96
  for (const call of calls) {
@@ -6,6 +6,7 @@ const index_js_1 = require("./adapters/index.js");
6
6
  const profiles_js_1 = require("./adapters/profiles.js");
7
7
  const errors_js_1 = require("./errors.js");
8
8
  const json_js_1 = require("./json.js");
9
+ const reasoning_js_1 = require("./reasoning.js");
9
10
  const policy_js_1 = require("./policy.js");
10
11
  const redact_js_1 = require("./redact.js");
11
12
  const util_js_1 = require("./util.js");
@@ -71,8 +72,11 @@ class AIHandler {
71
72
  const maxAttempts = this.opts.retry?.maxAttempts ?? 3;
72
73
  let emittedStart = false;
73
74
  let emittedOutput = false;
75
+ const stripInline = this.opts.stripInlineReasoning !== false;
74
76
  for (;;) {
75
77
  attempt += 1;
78
+ // One stripper per attempt: a retried stream starts its tag state over.
79
+ const stripper = stripInline ? (0, reasoning_js_1.createReasoningStripper)() : null;
76
80
  try {
77
81
  for await (const event of adapter.stream(resolved, req)) {
78
82
  if (event.type === 'start') {
@@ -82,12 +86,33 @@ class AIHandler {
82
86
  }
83
87
  continue;
84
88
  }
85
- if (event.type === 'delta' || event.type === 'tool_call')
89
+ if (event.type === 'delta' || event.type === 'tool_call' || event.type === 'reasoning')
86
90
  emittedOutput = true;
91
+ if (event.type === 'delta' && stripper) {
92
+ // Inline reasoning never reaches `delta`; it rides the reasoning channel like a
93
+ // provider-native thinking field would.
94
+ const piece = stripper.push(event.text);
95
+ if (piece.reasoning)
96
+ yield { type: 'reasoning', text: piece.reasoning };
97
+ if (piece.visible)
98
+ yield { type: 'delta', text: piece.visible };
99
+ continue;
100
+ }
87
101
  if (event.type === 'done') {
88
- await this.recordSuccess(callId, conn, req, startedAt, attempt, event.result);
102
+ let result = event.result;
103
+ if (stripper) {
104
+ const tail = stripper.end();
105
+ if (tail.reasoning)
106
+ yield { type: 'reasoning', text: tail.reasoning };
107
+ if (tail.visible)
108
+ yield { type: 'delta', text: tail.visible };
109
+ if ((0, reasoning_js_1.containsReasoningBlock)(result.text)) {
110
+ result = { ...result, text: (0, reasoning_js_1.stripReasoningBlocks)(result.text) };
111
+ }
112
+ }
113
+ await this.recordSuccess(callId, conn, req, startedAt, attempt, result);
89
114
  recorded = true;
90
- yield event;
115
+ yield { ...event, result };
91
116
  return;
92
117
  }
93
118
  yield event;
@@ -18,6 +18,7 @@ __exportStar(require("./types.js"), exports);
18
18
  __exportStar(require("./errors.js"), exports);
19
19
  __exportStar(require("./transport.js"), exports);
20
20
  __exportStar(require("./json.js"), exports);
21
+ __exportStar(require("./reasoning.js"), exports);
21
22
  __exportStar(require("./tokens.js"), exports);
22
23
  __exportStar(require("./keys.js"), exports);
23
24
  __exportStar(require("./connect.js"), exports);
@@ -0,0 +1,159 @@
1
+ "use strict";
2
+ /**
3
+ * Inline reasoning stripper — keeps a reasoning model's chain of thought out of `delta` events
4
+ * and `ChatResult.text`, routing it to the `reasoning` stream event instead.
5
+ *
6
+ * Reasoning models (deepseek-r1, phi4-reasoning, qwq, …) emit their thinking either in the
7
+ * provider's dedicated field — which every engine already maps to `{ type: 'reasoning' }` — or
8
+ * INLINE in the answer as `<think>…</think>`-style blocks. Inline reasoning routinely quotes the
9
+ * system prompt and tool instructions back verbatim, so an app that streams `delta` straight to
10
+ * a screen, a database or a JSON parser leaks it. Every consumer would otherwise write this
11
+ * state machine, and most would get the split-tag and template-opened cases wrong; the handler
12
+ * runs it once, on every engine, unless `HandlerOptions.stripInlineReasoning` is `false`.
13
+ *
14
+ * Ported from AI Server Studio's `services/reasoningFilter.ts` (2026-09-05), which had to exist
15
+ * because the nugget did not do this.
16
+ *
17
+ * Two subtleties the tests pin:
18
+ * - Tags may be split across chunks, so a trailing partial `<thi` is held back until the next
19
+ * chunk decides whether it opens a tag. An unterminated block is reasoning to the end.
20
+ * - The **orphan close**: a closing tag with no opening one at the head of the stream, because
21
+ * the runtime's chat template emitted `<think>` itself. Handled once per stream, only before
22
+ * any visible text has gone out, and only when the closing tag starts its own line — a
23
+ * `</think>` quoted mid-sentence in a real answer is left alone.
24
+ */
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.createReasoningStripper = createReasoningStripper;
27
+ exports.stripReasoningBlocksDetailed = stripReasoningBlocksDetailed;
28
+ exports.stripReasoningBlocks = stripReasoningBlocks;
29
+ exports.containsReasoningBlock = containsReasoningBlock;
30
+ const TAG_PAIRS = [
31
+ { open: '<think>', close: '</think>' },
32
+ { open: '<thinking>', close: '</thinking>' },
33
+ { open: '<reasoning>', close: '</reasoning>' },
34
+ { open: '<|begin_of_thought|>', close: '<|end_of_thought|>' },
35
+ ];
36
+ const LONGEST_TAG = Math.max(...TAG_PAIRS.flatMap((p) => [p.open.length, p.close.length]));
37
+ function partialTagSuffixLength(text) {
38
+ const lower = text.toLowerCase();
39
+ const maxLen = Math.min(LONGEST_TAG - 1, lower.length);
40
+ for (let len = maxLen; len >= 1; len -= 1) {
41
+ const tail = lower.slice(lower.length - len);
42
+ if (tail[0] !== '<')
43
+ continue;
44
+ for (const pair of TAG_PAIRS) {
45
+ if (pair.open.startsWith(tail) || pair.close.startsWith(tail))
46
+ return len;
47
+ }
48
+ }
49
+ return 0;
50
+ }
51
+ function closesOwnLine(text, idx) {
52
+ let i = idx - 1;
53
+ while (i >= 0 && (text[i] === ' ' || text[i] === '\t' || text[i] === '\r'))
54
+ i -= 1;
55
+ return i < 0 || text[i] === '\n';
56
+ }
57
+ function createReasoningStripper() {
58
+ let pending = '';
59
+ let inReasoning = false;
60
+ let closeTag = '';
61
+ let emittedVisible = false;
62
+ let orphanConsumed = false;
63
+ function process(input, flushAll) {
64
+ let text = pending + input;
65
+ pending = '';
66
+ let visible = '';
67
+ let reasoning = '';
68
+ while (text.length > 0) {
69
+ const lower = text.toLowerCase();
70
+ if (inReasoning) {
71
+ const idx = lower.indexOf(closeTag);
72
+ if (idx === -1) {
73
+ const hold = flushAll ? 0 : partialTagSuffixLength(text);
74
+ const consumed = text.length - hold;
75
+ reasoning += text.slice(0, consumed);
76
+ pending = text.slice(consumed);
77
+ text = '';
78
+ if (flushAll) {
79
+ reasoning += pending;
80
+ pending = '';
81
+ }
82
+ break;
83
+ }
84
+ reasoning += text.slice(0, idx);
85
+ text = text.slice(idx + closeTag.length);
86
+ inReasoning = false;
87
+ closeTag = '';
88
+ if (text.startsWith('\n'))
89
+ text = text.slice(1);
90
+ continue;
91
+ }
92
+ let openIdx = -1;
93
+ let pair = null;
94
+ for (const p of TAG_PAIRS) {
95
+ const i = lower.indexOf(p.open);
96
+ if (i !== -1 && (openIdx === -1 || i < openIdx)) {
97
+ openIdx = i;
98
+ pair = p;
99
+ }
100
+ }
101
+ let orphanCloseIdx = -1;
102
+ let orphanCloseLen = 0;
103
+ if (!emittedVisible && !orphanConsumed) {
104
+ for (const p of TAG_PAIRS) {
105
+ const i = lower.indexOf(p.close);
106
+ if (i !== -1 && (orphanCloseIdx === -1 || i < orphanCloseIdx) && closesOwnLine(text, i)) {
107
+ orphanCloseIdx = i;
108
+ orphanCloseLen = p.close.length;
109
+ }
110
+ }
111
+ }
112
+ if (orphanCloseIdx !== -1 && (openIdx === -1 || orphanCloseIdx < openIdx)) {
113
+ orphanConsumed = true;
114
+ reasoning += text.slice(0, orphanCloseIdx);
115
+ text = text.slice(orphanCloseIdx + orphanCloseLen).replace(/^\n+/, '');
116
+ continue;
117
+ }
118
+ if (openIdx === -1 || !pair) {
119
+ const hold = flushAll ? 0 : partialTagSuffixLength(text);
120
+ visible += text.slice(0, text.length - hold);
121
+ pending = text.slice(text.length - hold);
122
+ text = '';
123
+ break;
124
+ }
125
+ visible += text.slice(0, openIdx);
126
+ text = text.slice(openIdx + pair.open.length);
127
+ inReasoning = true;
128
+ closeTag = pair.close;
129
+ }
130
+ if (visible.trim().length > 0)
131
+ emittedVisible = true;
132
+ return { visible, reasoning };
133
+ }
134
+ return {
135
+ push: (chunk) => process(chunk, false),
136
+ end: () => process('', true),
137
+ get inReasoning() {
138
+ return inReasoning;
139
+ },
140
+ };
141
+ }
142
+ /** Whole-string form: `{ visible, reasoning }` for already-buffered text. */
143
+ function stripReasoningBlocksDetailed(text) {
144
+ if (!text)
145
+ return { visible: '', reasoning: '' };
146
+ const stripper = createReasoningStripper();
147
+ const a = stripper.push(text);
148
+ const b = stripper.end();
149
+ return { visible: (a.visible + b.visible).trim(), reasoning: a.reasoning + b.reasoning };
150
+ }
151
+ /** Whole-string form returning only the visible text. */
152
+ function stripReasoningBlocks(text) {
153
+ return stripReasoningBlocksDetailed(text).visible;
154
+ }
155
+ /** Cheap pre-check: does `text` carry any inline reasoning tag (open or close)? */
156
+ function containsReasoningBlock(text) {
157
+ const lower = text.toLowerCase();
158
+ return TAG_PAIRS.some((p) => lower.includes(p.open) || lower.includes(p.close));
159
+ }
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.REASONING_BUDGET_TOKENS = void 0;
3
4
  exports.asRecord = asRecord;
4
5
  exports.asString = asString;
5
6
  exports.asNumber = asNumber;
@@ -110,3 +111,16 @@ function sleep(ms, signal) {
110
111
  }
111
112
  });
112
113
  }
114
+ /**
115
+ * Token budgets behind the {@link ReasoningEffort} tiers for providers that
116
+ * express effort as a thinking-token budget (Anthropic `budget_tokens`, Google
117
+ * `thinkingBudget`). `'none'` is 0. Deliberately conservative: they are floors
118
+ * a model may stop under, not ceilings it fills.
119
+ */
120
+ exports.REASONING_BUDGET_TOKENS = {
121
+ none: 0,
122
+ minimal: 1024,
123
+ low: 2048,
124
+ medium: 8192,
125
+ high: 16384,
126
+ };
@@ -30,6 +30,12 @@ export interface HandlerOptions {
30
30
  * explicit `policy: allowAllPolicy()` says the same thing and is clearer.
31
31
  */
32
32
  silencePolicyWarning?: boolean;
33
+ /**
34
+ * Strip inline `<think>…</think>`-style reasoning out of `delta` events and `ChatResult.text`,
35
+ * emitting it as `{ type: 'reasoning' }` events instead (default `true`; see `src/reasoning.ts`).
36
+ * Set `false` to receive the raw answer text exactly as the provider sent it.
37
+ */
38
+ stripInlineReasoning?: boolean;
33
39
  /**
34
40
  * Optional cost estimator. Called once per successful call/embedding with the
35
41
  * final usage; its return value (USD) is stored on the telemetry record's
@@ -1 +1 @@
1
- {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../src/handler.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../src/handler.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EACV,QAAQ,EACR,UAAU,EACV,WAAW,EACX,UAAU,EACV,UAAU,EACV,YAAY,EACZ,WAAW,EACX,gBAAgB,EAChB,SAAS,EACT,SAAS,EACT,YAAY,EACZ,QAAQ,EAER,gBAAgB,EAChB,WAAW,EACX,aAAa,EACb,KAAK,EACN,MAAM,YAAY,CAAC;AAUpB,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,SAAS,CAAC;IACrB,MAAM,CAAC,EAAE,gBAAgB,CAAC;IAC1B,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,SAAS,CAAC,EAAE,aAAa,CAAC;IAC1B,KAAK,CAAC,EAAE;QACN,UAAU,CAAC,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,GAAG,MAAM,CAAC,CAAC;QACpD,SAAS,CAAC,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;KAC/C,CAAC;IACF,KAAK,CAAC,EAAE;QAAE,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC5E;;;;;;OAMG;IACH,MAAM,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,MAAM,CAAC;QAAC,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC5D;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B;;;;;OAKG;IACH,OAAO,CAAC,CAAC,IAAI,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,KAAK,CAAA;KAAE,GAAG,MAAM,GAAG,SAAS,CAAC;CACvF;AASD,4DAA4D;AAC5D,wBAAgB,0BAA0B,IAAI,IAAI,CAEjD;AAED,qBAAa,SAAS;IAOR,OAAO,CAAC,IAAI;IANxB,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,KAAK,CAAqB;IAClC,OAAO,CAAC,WAAW,CAAK;IACxB,OAAO,CAAC,eAAe,CAAyB;IAChD,OAAO,CAAC,MAAM,CAAmB;gBAEb,IAAI,EAAE,cAAc;IAelC,IAAI,CAAC,IAAI,EAAE,UAAU,EAAE,GAAG,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC;IAU5D,MAAM,CAAC,IAAI,EAAE,UAAU,EAAE,GAAG,EAAE,WAAW,GAAG,aAAa,CAAC,WAAW,CAAC;IAkG7E;;;;;;OAMG;YACW,SAAS;IAwCvB,8EAA8E;YAC/D,UAAU;IAKnB,UAAU,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;IAOlD,cAAc,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC;QAAE,EAAE,EAAE,OAAO,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAiBjF;;;;;OAKG;IACG,OAAO,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IAQ9C;;;;;;;OAOG;IACG,UAAU,CAAC,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,GAAG,EAAE,WAAW,EAAE,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IAoB9G;;;;;OAKG;IACG,KAAK,CAAC,IAAI,EAAE,UAAU,EAAE,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC;YA8BxD,WAAW;YAoBX,QAAQ;YA8BR,iBAAiB;YAkBjB,OAAO;IAoCrB,OAAO,CAAC,OAAO;IAWf,OAAO,CAAC,UAAU;YAQJ,aAAa;YAmBb,aAAa;YAkBb,MAAM;IAWpB,OAAO,CAAC,MAAM;IAId,OAAO,CAAC,aAAa;IAYrB,OAAO,CAAC,aAAa;CAStB"}