@cubicecho/agent-core 2.24.0 → 2.25.0
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 +10 -7
- package/dist/agent-loop.d.ts +84 -205
- package/dist/agent-loop.js +288 -597
- package/dist/calibration.d.ts +14 -10
- package/dist/calibration.js +30 -23
- package/dist/capabilities.d.ts +111 -42
- package/dist/capabilities.js +281 -138
- package/dist/client.d.ts +104 -35
- package/dist/client.js +205 -74
- package/dist/compaction.d.ts +103 -60
- package/dist/compaction.js +160 -86
- package/dist/config.d.ts +48 -12
- package/dist/config.js +20 -1
- package/dist/continuation.d.ts +24 -16
- package/dist/continuation.js +70 -44
- package/dist/digest.d.ts +7 -0
- package/dist/digest.js +18 -0
- package/dist/errors.d.ts +24 -15
- package/dist/errors.js +23 -15
- package/dist/events.d.ts +100 -63
- package/dist/events.js +250 -120
- package/dist/guards.d.ts +35 -4
- package/dist/guards.js +38 -5
- package/dist/hook-events.d.ts +21 -5
- package/dist/hook-events.js +23 -9
- package/dist/hooks.d.ts +96 -73
- package/dist/hooks.js +202 -116
- package/dist/index.d.ts +30 -26
- package/dist/index.js +29 -24
- package/dist/ledger.d.ts +62 -30
- package/dist/ledger.js +163 -56
- package/dist/platform.d.ts +13 -0
- package/dist/platform.js +13 -0
- package/dist/preselect.d.ts +222 -0
- package/dist/preselect.js +290 -0
- package/dist/request-body.d.ts +32 -0
- package/dist/request-body.js +47 -0
- package/dist/reset.d.ts +1 -0
- package/dist/reset.js +7 -6
- package/dist/retry.d.ts +29 -138
- package/dist/retry.js +69 -282
- package/dist/run-calls.d.ts +123 -0
- package/dist/run-calls.js +351 -0
- package/dist/run-turn.d.ts +36 -15
- package/dist/run-turn.js +74 -37
- package/dist/runtime.d.ts +23 -17
- package/dist/runtime.js +34 -24
- package/dist/schema-compat.d.ts +35 -18
- package/dist/schema-compat.js +340 -175
- package/dist/scope.d.ts +28 -5
- package/dist/scope.js +37 -10
- package/dist/side-task.d.ts +123 -40
- package/dist/side-task.js +181 -63
- package/dist/snapshot.d.ts +10 -5
- package/dist/snapshot.js +73 -30
- package/dist/spec.d.ts +56 -25
- package/dist/spec.js +494 -215
- package/dist/stream.d.ts +66 -16
- package/dist/stream.js +220 -97
- package/dist/thinking.d.ts +23 -8
- package/dist/thinking.js +92 -44
- package/dist/tokens.d.ts +164 -6
- package/dist/tokens.js +319 -6
- package/dist/tool-calls.d.ts +82 -17
- package/dist/tool-calls.js +198 -83
- package/dist/tool-loading.d.ts +101 -189
- package/dist/tool-loading.js +277 -371
- package/dist/wire.d.ts +81 -0
- package/dist/wire.js +73 -0
- package/llms.txt +76 -39
- package/package.json +4 -4
package/dist/thinking.d.ts
CHANGED
|
@@ -17,6 +17,7 @@ export declare const THINK_FENCE: Fence;
|
|
|
17
17
|
/**
|
|
18
18
|
* The fences nobody writes by accident, and so the ones `streamTurn` reads by default.
|
|
19
19
|
*
|
|
20
|
+
* @remarks
|
|
20
21
|
* `<think>` opening a reply is never meant as output, and the other two are made of tokens that
|
|
21
22
|
* only a model's template produces: gpt-oss's harmony analysis channel, served raw, and Kimi's.
|
|
22
23
|
*/
|
|
@@ -27,9 +28,13 @@ export declare const DEFAULT_FENCES: readonly Fence[];
|
|
|
27
28
|
* and on for a side task, whose answer is too short to be quoting anything.
|
|
28
29
|
*/
|
|
29
30
|
export declare const ALL_FENCES: readonly Fence[];
|
|
31
|
+
/** What a `Split` is called when it was inside a fence. */
|
|
32
|
+
export declare const SPLIT_REASONING: 'reasoning';
|
|
33
|
+
/** What a `Split` is called when it was not. */
|
|
34
|
+
export declare const SPLIT_OUTPUT: 'output';
|
|
30
35
|
/** A piece of `content`, said to be one or the other. */
|
|
31
36
|
export interface Split {
|
|
32
|
-
kind:
|
|
37
|
+
kind: typeof SPLIT_REASONING | typeof SPLIT_OUTPUT;
|
|
33
38
|
text: string;
|
|
34
39
|
}
|
|
35
40
|
/** What `FenceSplitter` takes besides its fences. */
|
|
@@ -37,6 +42,7 @@ export interface FenceSplitterOptions {
|
|
|
37
42
|
/**
|
|
38
43
|
* The template already opened the first fence in the prompt, so the reply starts inside it.
|
|
39
44
|
*
|
|
45
|
+
* @remarks
|
|
40
46
|
* Several chat templates end the prompt with `<think>` rather than leaving the model to write
|
|
41
47
|
* it. Without this the splitter still catches it once `</think>` arrives, and moves what came
|
|
42
48
|
* before into `reasoning`, but a watcher will have been shown it as output by then.
|
|
@@ -46,6 +52,7 @@ export interface FenceSplitterOptions {
|
|
|
46
52
|
/**
|
|
47
53
|
* A state machine over a stream of `content` that routes fenced text to reasoning.
|
|
48
54
|
*
|
|
55
|
+
* @remarks
|
|
49
56
|
* The reference shape is Vercel's `extractReasoningMiddleware`. A tag can be split across chunks,
|
|
50
57
|
* so the tail of each push that could be the start of one is held until the next push settles
|
|
51
58
|
* it; `finish` releases it. A reply cut off mid-scratchpad ends with the fence still open and its
|
|
@@ -62,24 +69,32 @@ export declare class FenceSplitter {
|
|
|
62
69
|
/** Everything that was inside a fence so far. */
|
|
63
70
|
reasoning: string;
|
|
64
71
|
/**
|
|
65
|
-
* @param fences The fences to read
|
|
66
|
-
*
|
|
67
|
-
* @param options Whether the reply starts inside the first fence.
|
|
72
|
+
* @param [fences] - The fences to read; an empty list passes everything through as output.
|
|
73
|
+
* @param [options] - Whether the reply starts inside the first fence.
|
|
68
74
|
*/
|
|
69
75
|
constructor(fences?: readonly Fence[], { startInside }?: FenceSplitterOptions);
|
|
70
76
|
/**
|
|
71
77
|
* Reads one more piece of content, returning what it settled, in order.
|
|
72
78
|
*
|
|
73
|
-
* @param text The next delta.
|
|
79
|
+
* @param text - The next delta.
|
|
80
|
+
* @returns The pieces now settled, neighbours of one kind merged, and added to `output` and
|
|
81
|
+
* `reasoning` already. Empty where the delta was all markers or all held back as a possible one.
|
|
74
82
|
*/
|
|
75
83
|
push(text: string): Split[];
|
|
76
|
-
/**
|
|
84
|
+
/**
|
|
85
|
+
* Releases whatever was held back as a possible tag, now that no more is coming.
|
|
86
|
+
*
|
|
87
|
+
* @returns The held text as one piece — reasoning where the reply ended inside a fence, output
|
|
88
|
+
* otherwise — or nothing where none was held.
|
|
89
|
+
*/
|
|
77
90
|
finish(): Split[];
|
|
78
91
|
}
|
|
79
92
|
/**
|
|
80
93
|
* What is left of a complete reply once every scratchpad is taken out of it.
|
|
81
94
|
*
|
|
82
|
-
* @param text The whole reply.
|
|
83
|
-
* @param fences The fences to read
|
|
95
|
+
* @param text - The whole reply.
|
|
96
|
+
* @param [fences] - The fences to read.
|
|
97
|
+
* @returns The answer alone, untrimmed. A fence left open takes everything after it, and a close
|
|
98
|
+
* with no open before any other fence takes everything before it.
|
|
84
99
|
*/
|
|
85
100
|
export declare function stripThinking(text: string, fences?: readonly Fence[]): string;
|
package/dist/thinking.js
CHANGED
|
@@ -8,17 +8,18 @@
|
|
|
8
8
|
* keep doing it. This is the one table of fences both the stream and the side tasks read.
|
|
9
9
|
*/
|
|
10
10
|
/** DeepSeek, Qwen3, QwQ and most distills. */
|
|
11
|
-
export const THINK_FENCE = { open:
|
|
11
|
+
export const THINK_FENCE = { open: '<think>', close: '</think>' };
|
|
12
12
|
/**
|
|
13
13
|
* The fences nobody writes by accident, and so the ones `streamTurn` reads by default.
|
|
14
14
|
*
|
|
15
|
+
* @remarks
|
|
15
16
|
* `<think>` opening a reply is never meant as output, and the other two are made of tokens that
|
|
16
17
|
* only a model's template produces: gpt-oss's harmony analysis channel, served raw, and Kimi's.
|
|
17
18
|
*/
|
|
18
19
|
export const DEFAULT_FENCES = [
|
|
19
20
|
THINK_FENCE,
|
|
20
|
-
{ open:
|
|
21
|
-
{ open:
|
|
21
|
+
{ open: '<|channel|>analysis<|message|>', close: '<|end|>' },
|
|
22
|
+
{ open: '◁think▷', close: '◁/think▷' },
|
|
22
23
|
];
|
|
23
24
|
/**
|
|
24
25
|
* Every fence known, including the two plain-word ones some fine-tunes and prompt-instructed
|
|
@@ -27,25 +28,27 @@ export const DEFAULT_FENCES = [
|
|
|
27
28
|
*/
|
|
28
29
|
export const ALL_FENCES = [
|
|
29
30
|
...DEFAULT_FENCES,
|
|
30
|
-
{ open:
|
|
31
|
-
{ open:
|
|
31
|
+
{ open: '<thinking>', close: '</thinking>' },
|
|
32
|
+
{ open: '<reasoning>', close: '</reasoning>' },
|
|
32
33
|
];
|
|
33
34
|
/**
|
|
34
35
|
* Framing that is neither scratchpad nor answer, dropped wherever it turns up outside a fence.
|
|
35
36
|
*
|
|
37
|
+
* @remarks
|
|
36
38
|
* Only harmony has any: after the analysis channel closes, raw gpt-oss output announces the final
|
|
37
39
|
* channel before the answer and ends with a return token.
|
|
38
40
|
*/
|
|
39
41
|
const FRAMING = {
|
|
40
|
-
|
|
41
|
-
"<|start|>assistant",
|
|
42
|
-
"<|channel|>final<|message|>",
|
|
43
|
-
"<|return|>",
|
|
44
|
-
],
|
|
42
|
+
'<|channel|>analysis<|message|>': ['<|start|>assistant', '<|channel|>final<|message|>', '<|return|>'],
|
|
45
43
|
};
|
|
44
|
+
/** What a `Split` is called when it was inside a fence. */
|
|
45
|
+
export const SPLIT_REASONING = 'reasoning';
|
|
46
|
+
/** What a `Split` is called when it was not. */
|
|
47
|
+
export const SPLIT_OUTPUT = 'output';
|
|
46
48
|
/**
|
|
47
49
|
* A state machine over a stream of `content` that routes fenced text to reasoning.
|
|
48
50
|
*
|
|
51
|
+
* @remarks
|
|
49
52
|
* The reference shape is Vercel's `extractReasoningMiddleware`. A tag can be split across chunks,
|
|
50
53
|
* so the tail of each push that could be the start of one is held until the next push settles
|
|
51
54
|
* it; `finish` releases it. A reply cut off mid-scratchpad ends with the fence still open and its
|
|
@@ -57,86 +60,117 @@ const FRAMING = {
|
|
|
57
60
|
*/
|
|
58
61
|
export class FenceSplitter {
|
|
59
62
|
/** The answer so far, with every fence taken out. */
|
|
60
|
-
output =
|
|
63
|
+
output = '';
|
|
61
64
|
/** Everything that was inside a fence so far. */
|
|
62
|
-
reasoning =
|
|
65
|
+
reasoning = '';
|
|
63
66
|
#fences;
|
|
64
67
|
#markers;
|
|
65
68
|
#inside;
|
|
66
69
|
#seenFence;
|
|
67
|
-
#held =
|
|
70
|
+
#held = '';
|
|
68
71
|
/**
|
|
69
|
-
* @param fences The fences to read
|
|
70
|
-
*
|
|
71
|
-
* @param options Whether the reply starts inside the first fence.
|
|
72
|
+
* @param [fences] - The fences to read; an empty list passes everything through as output.
|
|
73
|
+
* @param [options] - Whether the reply starts inside the first fence.
|
|
72
74
|
*/
|
|
73
75
|
constructor(fences = DEFAULT_FENCES, { startInside } = {}) {
|
|
74
76
|
this.#fences = fences;
|
|
75
|
-
this.#markers = fences.flatMap((fence) => [
|
|
76
|
-
fence.open,
|
|
77
|
-
fence.close,
|
|
78
|
-
...(FRAMING[fence.open] ?? []),
|
|
79
|
-
]);
|
|
77
|
+
this.#markers = fences.flatMap((fence) => [fence.open, fence.close, ...(FRAMING[fence.open] ?? [])]);
|
|
80
78
|
this.#inside = startInside ? fences[0] : undefined;
|
|
81
79
|
this.#seenFence = this.#inside !== undefined;
|
|
82
80
|
}
|
|
83
81
|
/**
|
|
84
82
|
* Reads one more piece of content, returning what it settled, in order.
|
|
85
83
|
*
|
|
86
|
-
* @param text The next delta.
|
|
84
|
+
* @param text - The next delta.
|
|
85
|
+
* @returns The pieces now settled, neighbours of one kind merged, and added to `output` and
|
|
86
|
+
* `reasoning` already. Empty where the delta was all markers or all held back as a possible one.
|
|
87
87
|
*/
|
|
88
88
|
push(text) {
|
|
89
89
|
const parts = [];
|
|
90
90
|
let rest = this.#held + text;
|
|
91
|
-
this.#held =
|
|
91
|
+
this.#held = '';
|
|
92
92
|
while (rest) {
|
|
93
93
|
const found = this.#next(rest);
|
|
94
94
|
if (!found) {
|
|
95
95
|
const keep = this.#partialTail(rest);
|
|
96
|
-
this.#
|
|
96
|
+
this.#emitInto(parts, rest.slice(0, rest.length - keep));
|
|
97
97
|
this.#held = rest.slice(rest.length - keep);
|
|
98
98
|
break;
|
|
99
99
|
}
|
|
100
|
-
this.#
|
|
100
|
+
this.#emitInto(parts, rest.slice(0, found.at));
|
|
101
101
|
rest = rest.slice(found.at + found.marker.length);
|
|
102
102
|
this.#take(found.marker);
|
|
103
103
|
}
|
|
104
104
|
return parts;
|
|
105
105
|
}
|
|
106
|
-
/**
|
|
106
|
+
/**
|
|
107
|
+
* Releases whatever was held back as a possible tag, now that no more is coming.
|
|
108
|
+
*
|
|
109
|
+
* @returns The held text as one piece — reasoning where the reply ended inside a fence, output
|
|
110
|
+
* otherwise — or nothing where none was held.
|
|
111
|
+
*/
|
|
107
112
|
finish() {
|
|
108
113
|
const parts = [];
|
|
109
|
-
this.#
|
|
110
|
-
this.#held =
|
|
114
|
+
this.#emitInto(parts, this.#held);
|
|
115
|
+
this.#held = '';
|
|
111
116
|
return parts;
|
|
112
117
|
}
|
|
113
|
-
/**
|
|
118
|
+
/**
|
|
119
|
+
* The markers worth looking for: only the close inside a fence, every one outside it.
|
|
120
|
+
*
|
|
121
|
+
* @returns Outside a fence, the splitter's own list of opens, closes and framing — not a copy.
|
|
122
|
+
*/
|
|
114
123
|
#candidates() {
|
|
115
124
|
return this.#inside ? [this.#inside.close] : this.#markers;
|
|
116
125
|
}
|
|
117
|
-
/**
|
|
126
|
+
/**
|
|
127
|
+
* The earliest marker that means something in the current state.
|
|
128
|
+
*
|
|
129
|
+
* @param text - What is left of this push to read.
|
|
130
|
+
* @returns The marker and where in `text` it starts — the longer of two that start at one place —
|
|
131
|
+
* or `undefined` where none is whole in it.
|
|
132
|
+
*/
|
|
118
133
|
#next(text) {
|
|
119
134
|
let best;
|
|
120
135
|
for (const marker of this.#candidates()) {
|
|
121
136
|
const at = text.indexOf(marker);
|
|
122
|
-
if (at === -1)
|
|
137
|
+
if (at === -1) {
|
|
123
138
|
continue;
|
|
124
|
-
|
|
139
|
+
}
|
|
140
|
+
const earlier = !best || at < best.at;
|
|
141
|
+
const longerAtTheSamePlace = best && at === best.at && marker.length > best.marker.length;
|
|
142
|
+
if (earlier || longerAtTheSamePlace) {
|
|
125
143
|
best = { at, marker };
|
|
144
|
+
}
|
|
126
145
|
}
|
|
127
146
|
return best;
|
|
128
147
|
}
|
|
129
|
-
/**
|
|
148
|
+
/**
|
|
149
|
+
* How much of the end of `text` could be the beginning of a marker.
|
|
150
|
+
*
|
|
151
|
+
* @param text - Text with no whole marker in it.
|
|
152
|
+
* @returns The length of the longest such tail over every marker worth looking for, zero where
|
|
153
|
+
* there is none. Always short of a whole marker.
|
|
154
|
+
*/
|
|
130
155
|
#partialTail(text) {
|
|
131
156
|
let keep = 0;
|
|
132
|
-
for (const marker of this.#candidates())
|
|
133
|
-
for (let length = Math.min(marker.length - 1, text.length); length > keep; length--)
|
|
157
|
+
for (const marker of this.#candidates()) {
|
|
158
|
+
for (let length = Math.min(marker.length - 1, text.length); length > keep; length--) {
|
|
134
159
|
if (text.endsWith(marker.slice(0, length))) {
|
|
135
160
|
keep = length;
|
|
136
161
|
break;
|
|
137
162
|
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
138
165
|
return keep;
|
|
139
166
|
}
|
|
167
|
+
/**
|
|
168
|
+
* Moves the state past a marker just read: out of a fence, into one, or nowhere.
|
|
169
|
+
*
|
|
170
|
+
* @param marker - The marker found. Inside a fence any marker leaves it, since only its close is
|
|
171
|
+
* looked for there. Outside, an open enters its fence, framing changes nothing, and a close met
|
|
172
|
+
* before any fence was seen moves all of `output` so far into `reasoning`.
|
|
173
|
+
*/
|
|
140
174
|
#take(marker) {
|
|
141
175
|
if (this.#inside) {
|
|
142
176
|
this.#inside = undefined;
|
|
@@ -153,28 +187,42 @@ export class FenceSplitter {
|
|
|
153
187
|
const closes = this.#fences.some((fence) => fence.close === marker);
|
|
154
188
|
if (closes && !this.#seenFence) {
|
|
155
189
|
this.reasoning += this.output;
|
|
156
|
-
this.output =
|
|
190
|
+
this.output = '';
|
|
157
191
|
}
|
|
158
|
-
if (closes)
|
|
192
|
+
if (closes) {
|
|
159
193
|
this.#seenFence = true;
|
|
194
|
+
}
|
|
160
195
|
}
|
|
161
|
-
|
|
162
|
-
|
|
196
|
+
/**
|
|
197
|
+
* Files text under the side the state is on: in `output` or `reasoning`, and in the pieces a call
|
|
198
|
+
* is returning.
|
|
199
|
+
*
|
|
200
|
+
* @param parts - The pieces settled so far. Written to — the text joins the last piece where
|
|
201
|
+
* that is of the same kind, and is pushed as a new one where it is not.
|
|
202
|
+
* @param text - What to file. Empty files nothing.
|
|
203
|
+
*/
|
|
204
|
+
#emitInto(parts, text) {
|
|
205
|
+
if (!text) {
|
|
163
206
|
return;
|
|
164
|
-
|
|
207
|
+
}
|
|
208
|
+
const kind = this.#inside ? SPLIT_REASONING : SPLIT_OUTPUT;
|
|
165
209
|
this[kind] += text;
|
|
166
210
|
const last = parts.at(-1);
|
|
167
|
-
if (last?.kind === kind)
|
|
211
|
+
if (last?.kind === kind) {
|
|
168
212
|
last.text += text;
|
|
169
|
-
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
170
215
|
parts.push({ kind, text });
|
|
216
|
+
}
|
|
171
217
|
}
|
|
172
218
|
}
|
|
173
219
|
/**
|
|
174
220
|
* What is left of a complete reply once every scratchpad is taken out of it.
|
|
175
221
|
*
|
|
176
|
-
* @param text The whole reply.
|
|
177
|
-
* @param fences The fences to read
|
|
222
|
+
* @param text - The whole reply.
|
|
223
|
+
* @param [fences] - The fences to read.
|
|
224
|
+
* @returns The answer alone, untrimmed. A fence left open takes everything after it, and a close
|
|
225
|
+
* with no open before any other fence takes everything before it.
|
|
178
226
|
*/
|
|
179
227
|
export function stripThinking(text, fences = ALL_FENCES) {
|
|
180
228
|
const splitter = new FenceSplitter(fences);
|
package/dist/tokens.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import type OpenAI from 'openai';
|
|
1
2
|
/**
|
|
2
3
|
* The characters one token is taken to be, where nothing has measured it.
|
|
3
4
|
*
|
|
5
|
+
* @remarks
|
|
4
6
|
* The fallback, not the rule: once a turn has come back with a reported prompt count, `runTurn`
|
|
5
7
|
* divides by what that endpoint's model was measured at instead. See `charsPerTokenFor`.
|
|
6
8
|
*/
|
|
@@ -10,16 +12,172 @@ export declare const CHARS_PER_TOKEN = 4;
|
|
|
10
12
|
* going to be one: a server that will not say how big its window is will not lend us its
|
|
11
13
|
* vocabulary either.
|
|
12
14
|
*
|
|
15
|
+
* @param text - Prose or serialised JSON — both counted the same way, which is why JSON reads low.
|
|
16
|
+
* @returns The length over `CHARS_PER_TOKEN`, rounded up: zero for an empty string, and one for
|
|
17
|
+
* anything shorter than a token.
|
|
18
|
+
*
|
|
19
|
+
* @remarks
|
|
13
20
|
* The estimate runs low on tool schemas — JSON packs more tokens into a character than prose
|
|
14
21
|
* does — and that is the side to be wrong on wherever it guards a window, since the cost of
|
|
15
22
|
* guessing high is a run refused that would have worked, and the cost of guessing low is the
|
|
16
23
|
* endpoint's own refusal, which is where we were before the guard existed.
|
|
17
24
|
*
|
|
18
|
-
* Its own module because it is the one number several of these agree on
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* @param text Prose or serialised JSON — both counted the same way, which is why JSON reads low.
|
|
25
|
+
* Its own module because it is the one number several of these agree on. It was extracted from
|
|
26
|
+
* `side-task` to break a cycle with `retry`; neither reads it now, but `hooks` does to cap a
|
|
27
|
+
* context block, as does any consumer sizing its own prompt. Everything else that sizes a request
|
|
28
|
+
* is below it — a message's characters, a tool block's, a request's, and the breakdown of a
|
|
29
|
+
* window — so the module imports nothing but the SDK's types and anything may read it.
|
|
24
30
|
*/
|
|
25
31
|
export declare const estimateTokens: (text: string) => number;
|
|
32
|
+
/**
|
|
33
|
+
* A token count as a message shows it: 1234 → "1.2k".
|
|
34
|
+
*
|
|
35
|
+
* @param tokens - The count to render.
|
|
36
|
+
* @returns The count in thousands to one decimal place with a `k`, or the number as it is below a
|
|
37
|
+
* thousand.
|
|
38
|
+
*
|
|
39
|
+
* @remarks
|
|
40
|
+
* The numbers in an overflow message are large and nobody reads the units digit.
|
|
41
|
+
*/
|
|
42
|
+
export declare const compact: (tokens: number) => string;
|
|
43
|
+
/** What the two token estimates below take besides what they measure. */
|
|
44
|
+
export interface TokenEstimateOptions {
|
|
45
|
+
/**
|
|
46
|
+
* The divisor — `charsPerTokenFor` for a model whose reported usage has calibrated it. A value
|
|
47
|
+
* that is not a number above zero is ignored.
|
|
48
|
+
*
|
|
49
|
+
* @defaultValue `CHARS_PER_TOKEN`
|
|
50
|
+
*/
|
|
51
|
+
charsPerToken?: number;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* How many characters one message is worth: its keys, and its content in whichever shape.
|
|
55
|
+
*
|
|
56
|
+
* @param message - The message as it will be sent.
|
|
57
|
+
* @returns The characters of its role, its content — a string, or the text and refusal parts of a
|
|
58
|
+
* list — and its name, call id, reasoning and tool calls, plus a constant for each key and part
|
|
59
|
+
* that carries them. An image or audio part adds nothing.
|
|
60
|
+
*/
|
|
61
|
+
export declare function messageChars(message: OpenAI.ChatCompletionMessageParam): number;
|
|
62
|
+
/**
|
|
63
|
+
* How many characters a tool array is worth, measured once per array.
|
|
64
|
+
*
|
|
65
|
+
* @param tools - The tool definitions as they will be sent. An empty array is worth nothing.
|
|
66
|
+
* @returns The length of the array serialised as JSON, as it was the first time this array was
|
|
67
|
+
* asked about — a change made to it since is not seen.
|
|
68
|
+
*/
|
|
69
|
+
export declare function toolsChars(tools: OpenAI.ChatCompletionTool[]): number;
|
|
70
|
+
/**
|
|
71
|
+
* How many characters a request is worth: the walk `requestTokens` divides, without the division.
|
|
72
|
+
*
|
|
73
|
+
* @param body - The request as it was sent, tools included.
|
|
74
|
+
* @returns The sum of `messageChars` over the messages. The tools are not in it, whether or not
|
|
75
|
+
* the body carries them; `toolsChars` counts those.
|
|
76
|
+
*
|
|
77
|
+
* @remarks
|
|
78
|
+
* What calibration reads a reported prompt count against, since a ratio is only as good as the
|
|
79
|
+
* character count it was taken over agreeing with the one it is later applied to.
|
|
80
|
+
*/
|
|
81
|
+
export declare function requestChars(body: OpenAI.ChatCompletionCreateParamsStreaming): number;
|
|
82
|
+
/**
|
|
83
|
+
* What this request will cost the window, in tokens, near enough.
|
|
84
|
+
*
|
|
85
|
+
* @param body - The request as it will be sent, tools included.
|
|
86
|
+
* @param [options] - The divisor, `CHARS_PER_TOKEN` when none is given.
|
|
87
|
+
* @returns Whole tokens: the messages' and the tools', each rounded up on its own and added.
|
|
88
|
+
*
|
|
89
|
+
* @remarks
|
|
90
|
+
* See `estimateTokens` for why it is characters over four and which way it is wrong on purpose,
|
|
91
|
+
* and `charsPerTokenFor` for the divisor a model's own reported usage has measured instead.
|
|
92
|
+
*
|
|
93
|
+
* Summed by walking the body rather than by serialising it. `JSON.stringify` on the messages
|
|
94
|
+
* built the entire transcript into a string on every call and threw it away having read nothing
|
|
95
|
+
* but its `.length` — against a transcript that grows by a turn each turn, and one the SDK is
|
|
96
|
+
* about to serialise again to send. What the walk misses is JSON's own punctuation and the keys,
|
|
97
|
+
* which the envelope constants put back — one per key and one per content part, rather than one
|
|
98
|
+
* per message, since the keys only some shapes carry and the parts a client appends block by
|
|
99
|
+
* block are most of what a tool-using transcript is made of. What is left is a message's
|
|
100
|
+
* escaping, which is not a constant and is small against an estimate that is already characters
|
|
101
|
+
* over four.
|
|
102
|
+
*/
|
|
103
|
+
export declare const requestTokens: (body: OpenAI.ChatCompletionCreateParamsStreaming, { charsPerToken }?: TokenEstimateOptions) => number;
|
|
104
|
+
/**
|
|
105
|
+
* What a request is made of, by the part of it a consumer can actually do something about.
|
|
106
|
+
*
|
|
107
|
+
* @remarks
|
|
108
|
+
* The question an operator asks is not how big the request is — the total already answers that —
|
|
109
|
+
* but what is filling the window, and the only useful answer names a lever: a system prompt to
|
|
110
|
+
* shorten, a tool list to load on demand instead of declaring whole, a transcript to compact,
|
|
111
|
+
* results to prune. So the cut follows the levers rather than the roles: `toolResults` is exactly
|
|
112
|
+
* what `pruneToolResults` can shrink, and `history` is everything `planCompaction` folds, the
|
|
113
|
+
* arguments of the calls in it included.
|
|
114
|
+
*/
|
|
115
|
+
export interface ContextBreakdown {
|
|
116
|
+
/** Every system and developer message, wherever it sits in the transcript. */
|
|
117
|
+
system: number;
|
|
118
|
+
/** The declared tool schemas, which a chat template renders ahead of the system prompt. */
|
|
119
|
+
tools: number;
|
|
120
|
+
/** What was said: the user and assistant messages, and the calls the assistant asked for. */
|
|
121
|
+
history: number;
|
|
122
|
+
/** What the tools handed back — the `tool` messages, and nothing else. */
|
|
123
|
+
toolResults: number;
|
|
124
|
+
/** The four above, summed. */
|
|
125
|
+
total: number;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* What each part of a request is worth in characters, by the same walk `requestTokens` divides.
|
|
129
|
+
*
|
|
130
|
+
* @param body - The request as it will be sent, tools included.
|
|
131
|
+
* @returns A new breakdown in characters. A `developer` message counts as `system`, and anything
|
|
132
|
+
* that is neither that nor a `tool` message as `history`.
|
|
133
|
+
*
|
|
134
|
+
* @remarks
|
|
135
|
+
* Exact and additive: the parts sum to `total`, which is `requestChars` plus `toolsChars`. The
|
|
136
|
+
* conversion to tokens is `contextTokens`' business, because that is where an estimate and a
|
|
137
|
+
* reported count have to be told apart.
|
|
138
|
+
*/
|
|
139
|
+
export declare function contextChars(body: OpenAI.ChatCompletionCreateParamsStreaming): ContextBreakdown;
|
|
140
|
+
/** What `contextTokens` takes besides the request. */
|
|
141
|
+
export interface ContextBreakdownOptions extends TokenEstimateOptions {
|
|
142
|
+
/**
|
|
143
|
+
* The prompt count the endpoint reported for this request, if it has answered. Given one, the
|
|
144
|
+
* parts are shares of it and the breakdown sums to what was actually charged rather than to an
|
|
145
|
+
* estimate; left out, they are shares of `requestTokens`.
|
|
146
|
+
*/
|
|
147
|
+
promptTokens?: number;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* What each part of a request costs the window, in tokens, adding up to the whole.
|
|
151
|
+
*
|
|
152
|
+
* @param body - The request as it will be sent, tools included.
|
|
153
|
+
* @param [options] - The divisor, and the reported prompt count when there is one.
|
|
154
|
+
* @returns A new breakdown in tokens. Its `total` is the reported count when one above zero was
|
|
155
|
+
* given, and `requestTokens` of the same body otherwise.
|
|
156
|
+
*
|
|
157
|
+
* @remarks
|
|
158
|
+
* Shares rather than four independent estimates, because a readout whose parts do not add up to
|
|
159
|
+
* the total beside them is a readout nobody trusts. Nothing in the round trip reports anything
|
|
160
|
+
* finer than a prompt count — a completion says how many tokens it read and not a word about
|
|
161
|
+
* where they came from — so the proportions are an estimate whatever the total is.
|
|
162
|
+
*
|
|
163
|
+
* Without a reported count the total is `requestTokens`, and the tools are counted the way it and
|
|
164
|
+
* `TurnMetrics.toolSchemaTokens` count them rather than shared out, so the two agree by
|
|
165
|
+
* construction and an operator does not read one number for the tool block in the metrics and a
|
|
166
|
+
* different one here. With a reported count every part is a share of it, the tools included:
|
|
167
|
+
* that number is the server's, and the point of using it is that the parts sum to what was
|
|
168
|
+
* charged.
|
|
169
|
+
*/
|
|
170
|
+
export declare function contextTokens(body: OpenAI.ChatCompletionCreateParamsStreaming, { charsPerToken, promptTokens }?: ContextBreakdownOptions): ContextBreakdown;
|
|
171
|
+
/**
|
|
172
|
+
* One message's estimated tokens, by the same count `requestTokens` sums for a whole request.
|
|
173
|
+
*
|
|
174
|
+
* @param message - The message as it will be sent.
|
|
175
|
+
* @param [options] - The divisor, `CHARS_PER_TOKEN` when none is given.
|
|
176
|
+
* @returns `messageChars` over the divisor, rounded up per message, where `requestTokens` rounds
|
|
177
|
+
* once over them all.
|
|
178
|
+
*
|
|
179
|
+
* @remarks
|
|
180
|
+
* For the arithmetic that weighs part of a transcript against a window — `planCompaction`'s kept
|
|
181
|
+
* tail — where `estimateTokens` on the text alone would leave out the calls and the envelope.
|
|
182
|
+
*/
|
|
183
|
+
export declare const messageTokens: (message: OpenAI.ChatCompletionMessageParam, { charsPerToken }?: TokenEstimateOptions) => number;
|