@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/dist/handler.js CHANGED
@@ -2,6 +2,7 @@ import { adapterFor } from './adapters/index.js';
2
2
  import { applyAuth, profileFor } from './adapters/profiles.js';
3
3
  import { AIError, fromUnknown } from './errors.js';
4
4
  import { extractJson } from './json.js';
5
+ import { containsReasoningBlock, createReasoningStripper, stripReasoningBlocks } from './reasoning.js';
5
6
  import { allowAllPolicy } from './policy.js';
6
7
  import { SessionRedactor } from './redact.js';
7
8
  import { promptChars, sleep } from './util.js';
@@ -67,8 +68,11 @@ export class AIHandler {
67
68
  const maxAttempts = this.opts.retry?.maxAttempts ?? 3;
68
69
  let emittedStart = false;
69
70
  let emittedOutput = false;
71
+ const stripInline = this.opts.stripInlineReasoning !== false;
70
72
  for (;;) {
71
73
  attempt += 1;
74
+ // One stripper per attempt: a retried stream starts its tag state over.
75
+ const stripper = stripInline ? createReasoningStripper() : null;
72
76
  try {
73
77
  for await (const event of adapter.stream(resolved, req)) {
74
78
  if (event.type === 'start') {
@@ -78,12 +82,33 @@ export class AIHandler {
78
82
  }
79
83
  continue;
80
84
  }
81
- if (event.type === 'delta' || event.type === 'tool_call')
85
+ if (event.type === 'delta' || event.type === 'tool_call' || event.type === 'reasoning')
82
86
  emittedOutput = true;
87
+ if (event.type === 'delta' && stripper) {
88
+ // Inline reasoning never reaches `delta`; it rides the reasoning channel like a
89
+ // provider-native thinking field would.
90
+ const piece = stripper.push(event.text);
91
+ if (piece.reasoning)
92
+ yield { type: 'reasoning', text: piece.reasoning };
93
+ if (piece.visible)
94
+ yield { type: 'delta', text: piece.visible };
95
+ continue;
96
+ }
83
97
  if (event.type === 'done') {
84
- await this.recordSuccess(callId, conn, req, startedAt, attempt, event.result);
98
+ let result = event.result;
99
+ if (stripper) {
100
+ const tail = stripper.end();
101
+ if (tail.reasoning)
102
+ yield { type: 'reasoning', text: tail.reasoning };
103
+ if (tail.visible)
104
+ yield { type: 'delta', text: tail.visible };
105
+ if (containsReasoningBlock(result.text)) {
106
+ result = { ...result, text: stripReasoningBlocks(result.text) };
107
+ }
108
+ }
109
+ await this.recordSuccess(callId, conn, req, startedAt, attempt, result);
85
110
  recorded = true;
86
- yield event;
111
+ yield { ...event, result };
87
112
  return;
88
113
  }
89
114
  yield event;
package/dist/index.d.ts CHANGED
@@ -2,6 +2,7 @@ export * from './types.js';
2
2
  export * from './errors.js';
3
3
  export * from './transport.js';
4
4
  export * from './json.js';
5
+ export * from './reasoning.js';
5
6
  export * from './tokens.js';
6
7
  export * from './keys.js';
7
8
  export * from './connect.js';
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@ export * from './types.js';
2
2
  export * from './errors.js';
3
3
  export * from './transport.js';
4
4
  export * from './json.js';
5
+ export * from './reasoning.js';
5
6
  export * from './tokens.js';
6
7
  export * from './keys.js';
7
8
  export * from './connect.js';
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Inline reasoning stripper — keeps a reasoning model's chain of thought out of `delta` events
3
+ * and `ChatResult.text`, routing it to the `reasoning` stream event instead.
4
+ *
5
+ * Reasoning models (deepseek-r1, phi4-reasoning, qwq, …) emit their thinking either in the
6
+ * provider's dedicated field — which every engine already maps to `{ type: 'reasoning' }` — or
7
+ * INLINE in the answer as `<think>…</think>`-style blocks. Inline reasoning routinely quotes the
8
+ * system prompt and tool instructions back verbatim, so an app that streams `delta` straight to
9
+ * a screen, a database or a JSON parser leaks it. Every consumer would otherwise write this
10
+ * state machine, and most would get the split-tag and template-opened cases wrong; the handler
11
+ * runs it once, on every engine, unless `HandlerOptions.stripInlineReasoning` is `false`.
12
+ *
13
+ * Ported from AI Server Studio's `services/reasoningFilter.ts` (2026-09-05), which had to exist
14
+ * because the nugget did not do this.
15
+ *
16
+ * Two subtleties the tests pin:
17
+ * - Tags may be split across chunks, so a trailing partial `<thi` is held back until the next
18
+ * chunk decides whether it opens a tag. An unterminated block is reasoning to the end.
19
+ * - The **orphan close**: a closing tag with no opening one at the head of the stream, because
20
+ * the runtime's chat template emitted `<think>` itself. Handled once per stream, only before
21
+ * any visible text has gone out, and only when the closing tag starts its own line — a
22
+ * `</think>` quoted mid-sentence in a real answer is left alone.
23
+ */
24
+ export interface ReasoningStripResult {
25
+ /** Text safe to show from this chunk (may be empty). */
26
+ visible: string;
27
+ /** Reasoning text removed from this chunk (may be empty). */
28
+ reasoning: string;
29
+ }
30
+ export interface ReasoningStripper {
31
+ /** Feed the next raw delta; returns what may be shown and what was reasoning. */
32
+ push(chunk: string): ReasoningStripResult;
33
+ /** End of stream: flushes any held-back prefix that never became a tag. */
34
+ end(): ReasoningStripResult;
35
+ /** True while inside an unterminated reasoning block. */
36
+ readonly inReasoning: boolean;
37
+ }
38
+ export declare function createReasoningStripper(): ReasoningStripper;
39
+ /** Whole-string form: `{ visible, reasoning }` for already-buffered text. */
40
+ export declare function stripReasoningBlocksDetailed(text: string): ReasoningStripResult;
41
+ /** Whole-string form returning only the visible text. */
42
+ export declare function stripReasoningBlocks(text: string): string;
43
+ /** Cheap pre-check: does `text` carry any inline reasoning tag (open or close)? */
44
+ export declare function containsReasoningBlock(text: string): boolean;
45
+ //# sourceMappingURL=reasoning.d.ts.map
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Inline reasoning stripper — keeps a reasoning model's chain of thought out of `delta` events
3
+ * and `ChatResult.text`, routing it to the `reasoning` stream event instead.
4
+ *
5
+ * Reasoning models (deepseek-r1, phi4-reasoning, qwq, …) emit their thinking either in the
6
+ * provider's dedicated field — which every engine already maps to `{ type: 'reasoning' }` — or
7
+ * INLINE in the answer as `<think>…</think>`-style blocks. Inline reasoning routinely quotes the
8
+ * system prompt and tool instructions back verbatim, so an app that streams `delta` straight to
9
+ * a screen, a database or a JSON parser leaks it. Every consumer would otherwise write this
10
+ * state machine, and most would get the split-tag and template-opened cases wrong; the handler
11
+ * runs it once, on every engine, unless `HandlerOptions.stripInlineReasoning` is `false`.
12
+ *
13
+ * Ported from AI Server Studio's `services/reasoningFilter.ts` (2026-09-05), which had to exist
14
+ * because the nugget did not do this.
15
+ *
16
+ * Two subtleties the tests pin:
17
+ * - Tags may be split across chunks, so a trailing partial `<thi` is held back until the next
18
+ * chunk decides whether it opens a tag. An unterminated block is reasoning to the end.
19
+ * - The **orphan close**: a closing tag with no opening one at the head of the stream, because
20
+ * the runtime's chat template emitted `<think>` itself. Handled once per stream, only before
21
+ * any visible text has gone out, and only when the closing tag starts its own line — a
22
+ * `</think>` quoted mid-sentence in a real answer is left alone.
23
+ */
24
+ const TAG_PAIRS = [
25
+ { open: '<think>', close: '</think>' },
26
+ { open: '<thinking>', close: '</thinking>' },
27
+ { open: '<reasoning>', close: '</reasoning>' },
28
+ { open: '<|begin_of_thought|>', close: '<|end_of_thought|>' },
29
+ ];
30
+ const LONGEST_TAG = Math.max(...TAG_PAIRS.flatMap((p) => [p.open.length, p.close.length]));
31
+ function partialTagSuffixLength(text) {
32
+ const lower = text.toLowerCase();
33
+ const maxLen = Math.min(LONGEST_TAG - 1, lower.length);
34
+ for (let len = maxLen; len >= 1; len -= 1) {
35
+ const tail = lower.slice(lower.length - len);
36
+ if (tail[0] !== '<')
37
+ continue;
38
+ for (const pair of TAG_PAIRS) {
39
+ if (pair.open.startsWith(tail) || pair.close.startsWith(tail))
40
+ return len;
41
+ }
42
+ }
43
+ return 0;
44
+ }
45
+ function closesOwnLine(text, idx) {
46
+ let i = idx - 1;
47
+ while (i >= 0 && (text[i] === ' ' || text[i] === '\t' || text[i] === '\r'))
48
+ i -= 1;
49
+ return i < 0 || text[i] === '\n';
50
+ }
51
+ export function createReasoningStripper() {
52
+ let pending = '';
53
+ let inReasoning = false;
54
+ let closeTag = '';
55
+ let emittedVisible = false;
56
+ let orphanConsumed = false;
57
+ function process(input, flushAll) {
58
+ let text = pending + input;
59
+ pending = '';
60
+ let visible = '';
61
+ let reasoning = '';
62
+ while (text.length > 0) {
63
+ const lower = text.toLowerCase();
64
+ if (inReasoning) {
65
+ const idx = lower.indexOf(closeTag);
66
+ if (idx === -1) {
67
+ const hold = flushAll ? 0 : partialTagSuffixLength(text);
68
+ const consumed = text.length - hold;
69
+ reasoning += text.slice(0, consumed);
70
+ pending = text.slice(consumed);
71
+ text = '';
72
+ if (flushAll) {
73
+ reasoning += pending;
74
+ pending = '';
75
+ }
76
+ break;
77
+ }
78
+ reasoning += text.slice(0, idx);
79
+ text = text.slice(idx + closeTag.length);
80
+ inReasoning = false;
81
+ closeTag = '';
82
+ if (text.startsWith('\n'))
83
+ text = text.slice(1);
84
+ continue;
85
+ }
86
+ let openIdx = -1;
87
+ let pair = null;
88
+ for (const p of TAG_PAIRS) {
89
+ const i = lower.indexOf(p.open);
90
+ if (i !== -1 && (openIdx === -1 || i < openIdx)) {
91
+ openIdx = i;
92
+ pair = p;
93
+ }
94
+ }
95
+ let orphanCloseIdx = -1;
96
+ let orphanCloseLen = 0;
97
+ if (!emittedVisible && !orphanConsumed) {
98
+ for (const p of TAG_PAIRS) {
99
+ const i = lower.indexOf(p.close);
100
+ if (i !== -1 && (orphanCloseIdx === -1 || i < orphanCloseIdx) && closesOwnLine(text, i)) {
101
+ orphanCloseIdx = i;
102
+ orphanCloseLen = p.close.length;
103
+ }
104
+ }
105
+ }
106
+ if (orphanCloseIdx !== -1 && (openIdx === -1 || orphanCloseIdx < openIdx)) {
107
+ orphanConsumed = true;
108
+ reasoning += text.slice(0, orphanCloseIdx);
109
+ text = text.slice(orphanCloseIdx + orphanCloseLen).replace(/^\n+/, '');
110
+ continue;
111
+ }
112
+ if (openIdx === -1 || !pair) {
113
+ const hold = flushAll ? 0 : partialTagSuffixLength(text);
114
+ visible += text.slice(0, text.length - hold);
115
+ pending = text.slice(text.length - hold);
116
+ text = '';
117
+ break;
118
+ }
119
+ visible += text.slice(0, openIdx);
120
+ text = text.slice(openIdx + pair.open.length);
121
+ inReasoning = true;
122
+ closeTag = pair.close;
123
+ }
124
+ if (visible.trim().length > 0)
125
+ emittedVisible = true;
126
+ return { visible, reasoning };
127
+ }
128
+ return {
129
+ push: (chunk) => process(chunk, false),
130
+ end: () => process('', true),
131
+ get inReasoning() {
132
+ return inReasoning;
133
+ },
134
+ };
135
+ }
136
+ /** Whole-string form: `{ visible, reasoning }` for already-buffered text. */
137
+ export function stripReasoningBlocksDetailed(text) {
138
+ if (!text)
139
+ return { visible: '', reasoning: '' };
140
+ const stripper = createReasoningStripper();
141
+ const a = stripper.push(text);
142
+ const b = stripper.end();
143
+ return { visible: (a.visible + b.visible).trim(), reasoning: a.reasoning + b.reasoning };
144
+ }
145
+ /** Whole-string form returning only the visible text. */
146
+ export function stripReasoningBlocks(text) {
147
+ return stripReasoningBlocksDetailed(text).visible;
148
+ }
149
+ /** Cheap pre-check: does `text` carry any inline reasoning tag (open or close)? */
150
+ export function containsReasoningBlock(text) {
151
+ const lower = text.toLowerCase();
152
+ return TAG_PAIRS.some((p) => lower.includes(p.open) || lower.includes(p.close));
153
+ }
154
+ //# sourceMappingURL=reasoning.js.map
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;
@@ -25,12 +33,28 @@ export interface ToolSchema {
25
33
  description: string;
26
34
  parameters: object;
27
35
  }
36
+ /**
37
+ * How hard a reasoning-capable model should think before answering. The nugget
38
+ * maps one vocabulary onto each provider's own knob (see `docs/providers.md`,
39
+ * "Reasoning effort"): OpenAI `reasoning_effort`, Anthropic `thinking`
40
+ * (`budget_tokens` tiers), Google `generationConfig.thinkingConfig.thinkingBudget`,
41
+ * Ollama `think`. `'none'` disables reasoning where the provider allows it.
42
+ * Unset means the provider's own default — the nugget never sends the knob
43
+ * unasked, because non-reasoning models reject it.
44
+ */
45
+ export type ReasoningEffort = 'none' | 'minimal' | 'low' | 'medium' | 'high';
28
46
  export interface ChatRequest {
29
47
  model: string;
30
48
  messages: ChatMessage[];
31
49
  temperature?: number;
32
50
  maxTokens?: number;
33
51
  topP?: number;
52
+ /**
53
+ * Reasoning effort for thinking-capable models — see {@link ReasoningEffort}.
54
+ * A provider-native value in `providerOptions` (e.g. `reasoning_effort`,
55
+ * `thinking`, `thinkingConfig`, `think`) wins over this on collision.
56
+ */
57
+ reasoningEffort?: ReasoningEffort;
34
58
  responseFormat?: {
35
59
  type: 'text';
36
60
  } | {
@@ -114,6 +138,7 @@ export interface Usage {
114
138
  }
115
139
  export interface ChatResult {
116
140
  text: string;
141
+ providerState?: ProviderState;
117
142
  toolCalls?: ToolCall[];
118
143
  finishReason: 'stop' | 'length' | 'tool' | 'content_filter' | 'error' | 'canceled';
119
144
  usage: Usage;
package/dist/util.d.ts CHANGED
@@ -21,4 +21,11 @@ export declare function joinUrl(base: string, path: string): string;
21
21
  */
22
22
  export declare function mapWithConcurrency<T, R>(items: readonly T[], limit: number, fn: (item: T, index: number) => Promise<R>): Promise<R[]>;
23
23
  export declare function sleep(ms: number, signal?: AbortSignal): Promise<void>;
24
+ /**
25
+ * Token budgets behind the {@link ReasoningEffort} tiers for providers that
26
+ * express effort as a thinking-token budget (Anthropic `budget_tokens`, Google
27
+ * `thinkingBudget`). `'none'` is 0. Deliberately conservative: they are floors
28
+ * a model may stop under, not ceilings it fills.
29
+ */
30
+ export declare const REASONING_BUDGET_TOKENS: Record<'none' | 'minimal' | 'low' | 'medium' | 'high', number>;
24
31
  //# sourceMappingURL=util.d.ts.map
package/dist/util.js CHANGED
@@ -98,4 +98,17 @@ export function sleep(ms, signal) {
98
98
  }
99
99
  });
100
100
  }
101
+ /**
102
+ * Token budgets behind the {@link ReasoningEffort} tiers for providers that
103
+ * express effort as a thinking-token budget (Anthropic `budget_tokens`, Google
104
+ * `thinkingBudget`). `'none'` is 0. Deliberately conservative: they are floors
105
+ * a model may stop under, not ceilings it fills.
106
+ */
107
+ export const REASONING_BUDGET_TOKENS = {
108
+ none: 0,
109
+ minimal: 1024,
110
+ low: 2048,
111
+ medium: 8192,
112
+ high: 16384,
113
+ };
101
114
  //# sourceMappingURL=util.js.map
@@ -1,2 +1,2 @@
1
- 0.6.0
2
- 762595605eef73dde3751687e21190d8281be3ade9d699d02620920f6c021de2
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"}
@@ -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