@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.
- package/README.md +92 -3
- package/UPGRADING.md +43 -1
- package/dist/adapters/engines/anthropic.js +98 -10
- package/dist/adapters/engines/google.js +8 -1
- package/dist/adapters/engines/ollama.js +6 -0
- package/dist/adapters/engines/openaiChat.d.ts +6 -0
- package/dist/adapters/engines/openaiChat.js +51 -1
- package/dist/agent/loop.d.ts +3 -1
- package/dist/agent/loop.js +4 -1
- package/dist/cjs/adapters/engines/anthropic.js +96 -8
- package/dist/cjs/adapters/engines/google.js +7 -0
- package/dist/cjs/adapters/engines/ollama.js +6 -0
- package/dist/cjs/adapters/engines/openaiChat.js +52 -1
- package/dist/cjs/agent/loop.js +4 -1
- package/dist/cjs/handler.js +28 -3
- package/dist/cjs/index.js +1 -0
- package/dist/cjs/reasoning.js +159 -0
- package/dist/cjs/util.js +14 -0
- package/dist/handler.d.ts +6 -0
- package/dist/handler.js +28 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/reasoning.d.ts +45 -0
- package/dist/reasoning.js +154 -0
- package/dist/types.d.ts +25 -0
- package/dist/util.d.ts +7 -0
- package/dist/util.js +13 -0
- package/nugget/VERSION.txt +2 -2
- package/nugget/dist/adapters/engines/anthropic.d.ts.map +1 -1
- package/nugget/dist/adapters/engines/anthropic.js +98 -10
- package/nugget/dist/adapters/engines/anthropic.js.map +1 -1
- package/nugget/dist/adapters/engines/google.js +8 -1
- package/nugget/dist/adapters/engines/google.js.map +1 -1
- package/nugget/dist/adapters/engines/ollama.js +6 -0
- package/nugget/dist/adapters/engines/ollama.js.map +1 -1
- package/nugget/dist/adapters/engines/openaiChat.d.ts +6 -0
- package/nugget/dist/adapters/engines/openaiChat.d.ts.map +1 -1
- package/nugget/dist/adapters/engines/openaiChat.js +51 -1
- package/nugget/dist/adapters/engines/openaiChat.js.map +1 -1
- package/nugget/dist/agent/loop.d.ts +3 -1
- package/nugget/dist/agent/loop.d.ts.map +1 -1
- package/nugget/dist/agent/loop.js +4 -1
- package/nugget/dist/agent/loop.js.map +1 -1
- package/nugget/dist/cjs/adapters/engines/anthropic.js +96 -8
- package/nugget/dist/cjs/adapters/engines/google.js +7 -0
- package/nugget/dist/cjs/adapters/engines/ollama.js +6 -0
- package/nugget/dist/cjs/adapters/engines/openaiChat.js +52 -1
- package/nugget/dist/cjs/agent/loop.js +4 -1
- package/nugget/dist/cjs/handler.js +28 -3
- package/nugget/dist/cjs/index.js +1 -0
- package/nugget/dist/cjs/reasoning.js +159 -0
- package/nugget/dist/cjs/util.js +14 -0
- package/nugget/dist/handler.d.ts +6 -0
- package/nugget/dist/handler.d.ts.map +1 -1
- package/nugget/dist/handler.js +28 -3
- package/nugget/dist/handler.js.map +1 -1
- package/nugget/dist/index.d.ts +1 -0
- package/nugget/dist/index.d.ts.map +1 -1
- package/nugget/dist/index.js +1 -0
- package/nugget/dist/index.js.map +1 -1
- package/nugget/dist/reasoning.d.ts +45 -0
- package/nugget/dist/reasoning.d.ts.map +1 -0
- package/nugget/dist/reasoning.js +154 -0
- package/nugget/dist/reasoning.js.map +1 -0
- package/nugget/dist/types.d.ts +25 -0
- package/nugget/dist/types.d.ts.map +1 -1
- package/nugget/dist/types.js.map +1 -1
- package/nugget/dist/util.d.ts +7 -0
- package/nugget/dist/util.d.ts.map +1 -1
- package/nugget/dist/util.js +13 -0
- package/nugget/dist/util.js.map +1 -1
- package/nugget/src/adapters/engines/anthropic.ts +87 -10
- package/nugget/src/adapters/engines/google.ts +8 -1
- package/nugget/src/adapters/engines/ollama.ts +5 -0
- package/nugget/src/adapters/engines/openaiChat.ts +46 -1
- package/nugget/src/agent/loop.ts +7 -2
- package/nugget/src/handler.ts +30 -3
- package/nugget/src/index.ts +1 -0
- package/nugget/src/reasoning.ts +174 -0
- package/nugget/src/types.ts +27 -0
- package/nugget/src/util.ts +14 -0
- 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
|
-
|
|
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
package/dist/index.js
CHANGED
|
@@ -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
|
package/nugget/VERSION.txt
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
0.
|
|
2
|
-
|
|
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;
|
|
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
|
|
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 === '
|
|
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
|
-
|
|
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
|