@cubicecho/agent-core 2.15.0 → 2.16.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 +41 -20
- package/dist/agent-loop.d.ts +2 -1
- package/dist/agent-loop.js +5 -6
- package/dist/capabilities.d.ts +40 -0
- package/dist/capabilities.js +131 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/side-task.js +7 -3
- package/dist/snapshot.d.ts +8 -0
- package/dist/snapshot.js +20 -1
- package/llms.txt +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -132,28 +132,49 @@ again, with the setting still reading `high` and nothing anywhere saying it had
|
|
|
132
132
|
So they hang off the endpoint under the name the endpoint knows the model by. Pass `model` and
|
|
133
133
|
the same loop answers both levels; leave it out and nothing changes.
|
|
134
134
|
|
|
135
|
-
A refusal of the *value* is not one of these, however alike the two read:
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
135
|
+
A refusal of the *value* is not one of these, however alike the two read: a `max_tokens` larger
|
|
136
|
+
than the model's ceiling, a temperature out of range. Dropping the field answers those too — at
|
|
137
|
+
the model's own default, latched for the rest of the process, with the settings row still reading
|
|
138
|
+
what was typed and nothing saying it had stopped meaning it. They are passed to the caller
|
|
139
|
+
instead, where whoever typed the number can see it.
|
|
140
|
+
|
|
141
|
+
The effort is the exception, because there the model has said what it *would* take:
|
|
142
|
+
`Unsupported value: 'reasoning_effort' does not support 'none' with this model. Supported values
|
|
143
|
+
are: 'minimal', 'low', 'medium', and 'high'.` `negotiate` latches the rung that was refused and
|
|
144
|
+
re-sends at the cheapest one above it, which is a request rather than a failure. Where the refusal
|
|
145
|
+
lists nothing it walks `EFFORT_LADDER` — `none`, `minimal`, `low`, `medium`, `high` — a rung per
|
|
146
|
+
refusal. Both latch per `(endpoint, model)` and ride in the snapshot, so a restart does not walk
|
|
147
|
+
the ladder again.
|
|
148
|
+
|
|
149
|
+
Only ever upward, and only from a value on the ladder. Answering a refused `xhigh` with `high`
|
|
150
|
+
would quietly reason less than whoever typed it asked for; stepping up from `none` only costs
|
|
151
|
+
tokens, and says so in a notice. When the ladder runs out, or the value is not on it, the refusal
|
|
152
|
+
goes to the caller as it did before.
|
|
153
|
+
|
|
154
|
+
`effortFor(model, asked)` is what a body builder calls to get the value to send — `buildBody` and
|
|
155
|
+
the side tasks both do — and it answers the empty string for a model that takes no effort at all,
|
|
156
|
+
for `"off"` and for an absent setting.
|
|
140
157
|
|
|
141
158
|
```ts
|
|
142
|
-
const turn = await negotiate(
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
159
|
+
const turn = await negotiate(
|
|
160
|
+
supports,
|
|
161
|
+
(supports, produced, model) => {
|
|
162
|
+
// Each rebuilt per attempt from what this model has already refused.
|
|
163
|
+
const asked = effortFor(model, effort);
|
|
164
|
+
return streamTurn(
|
|
165
|
+
getClient(config),
|
|
166
|
+
{
|
|
167
|
+
model: name, messages, stream: true,
|
|
168
|
+
...(asked ? { reasoning_effort: asked } : {}),
|
|
169
|
+
...(model?.legacyTokenLimit === false
|
|
170
|
+
? { max_completion_tokens: limit }
|
|
171
|
+
: { max_tokens: limit }),
|
|
172
|
+
...(model?.chosenTemperature ? { temperature } : {}),
|
|
173
|
+
tools: supports.strictSchemas ? declared : relaxTools(declared),
|
|
174
|
+
},
|
|
175
|
+
{ produced, signal },
|
|
176
|
+
);
|
|
177
|
+
},
|
|
157
178
|
{ model: name },
|
|
158
179
|
);
|
|
159
180
|
```
|
package/dist/agent-loop.d.ts
CHANGED
|
@@ -18,7 +18,8 @@ import { type ToolOrder } from "./tool-loading.ts";
|
|
|
18
18
|
* one has to read the same — which is the test one of the three copies had inverted.
|
|
19
19
|
*
|
|
20
20
|
* @param config What to ask for. `maxTokens` of zero or less sends no ceiling; `reasoningEffort`
|
|
21
|
-
* absent or `"off"` sends no effort
|
|
21
|
+
* absent or `"off"` sends no effort, and one the model has refused by value is stepped up to the
|
|
22
|
+
* cheapest it takes by `effortFor`.
|
|
22
23
|
* @param supports What the endpoint has refused, as `negotiate` hands it to `send`.
|
|
23
24
|
* @param refused What the model has refused, as `negotiate` hands it over. Absent is a model
|
|
24
25
|
* that has refused nothing.
|
package/dist/agent-loop.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { charsPerTokenFor } from "./calibration.js";
|
|
2
|
-
import { capabilitiesFor } from "./capabilities.js";
|
|
2
|
+
import { capabilitiesFor, effortFor, } from "./capabilities.js";
|
|
3
3
|
import { firstTokenMs, getClient, NO_KEY, timeoutMs } from "./client.js";
|
|
4
4
|
import { continueTurn } from "./continuation.js";
|
|
5
5
|
import { errorMessage } from "./errors.js";
|
|
@@ -35,7 +35,8 @@ const RESERVED = new Set(["model", "messages", "stream", "tools"]);
|
|
|
35
35
|
* one has to read the same — which is the test one of the three copies had inverted.
|
|
36
36
|
*
|
|
37
37
|
* @param config What to ask for. `maxTokens` of zero or less sends no ceiling; `reasoningEffort`
|
|
38
|
-
* absent or `"off"` sends no effort
|
|
38
|
+
* absent or `"off"` sends no effort, and one the model has refused by value is stepped up to the
|
|
39
|
+
* cheapest it takes by `effortFor`.
|
|
39
40
|
* @param supports What the endpoint has refused, as `negotiate` hands it to `send`.
|
|
40
41
|
* @param refused What the model has refused, as `negotiate` hands it over. Absent is a model
|
|
41
42
|
* that has refused nothing.
|
|
@@ -51,7 +52,7 @@ export function buildBody(config, supports, refused, messages, tools = [], order
|
|
|
51
52
|
const declared = supports.strictSchemas
|
|
52
53
|
? sanitizeTools(sorted)
|
|
53
54
|
: relaxTools(sanitizeTools(sorted));
|
|
54
|
-
const effort = config.reasoningEffort;
|
|
55
|
+
const effort = effortFor(refused, config.reasoningEffort);
|
|
55
56
|
const extra = Object.entries(config.extraBody ?? {}).filter(([field]) => !RESERVED.has(field) && !refused?.refusedFields.has(field));
|
|
56
57
|
return {
|
|
57
58
|
...(config.maxTokens > 0
|
|
@@ -60,9 +61,7 @@ export function buildBody(config, supports, refused, messages, tools = [], order
|
|
|
60
61
|
: { max_tokens: config.maxTokens }
|
|
61
62
|
: {}),
|
|
62
63
|
...(refused?.chosenTemperature === false ? {} : { temperature: config.temperature }),
|
|
63
|
-
...(effort
|
|
64
|
-
? { reasoning_effort: effort }
|
|
65
|
-
: {}),
|
|
64
|
+
...(effort ? { reasoning_effort: effort } : {}),
|
|
66
65
|
...(supports.usageInStream ? { stream_options: { include_usage: true } } : {}),
|
|
67
66
|
...Object.fromEntries(extra),
|
|
68
67
|
model: config.model,
|
package/dist/capabilities.d.ts
CHANGED
|
@@ -93,6 +93,22 @@ export interface ModelCapabilities {
|
|
|
93
93
|
* endpoint's because one key reaches models that differ here, the way they differ on effort.
|
|
94
94
|
*/
|
|
95
95
|
structuredOutput: boolean;
|
|
96
|
+
/**
|
|
97
|
+
* Efforts this model refused by value rather than by field — a request that named `none` on a
|
|
98
|
+
* model whose list starts at `minimal`. Only ever grows, which is this set's way of latching.
|
|
99
|
+
*
|
|
100
|
+
* Separate from `reasoningEffort` because the two refusals mean opposite things: that one says
|
|
101
|
+
* the model cannot reason and the field must go, this one says it reasons and was handed an
|
|
102
|
+
* effort off a list this package does not know. Dropping the field there would run at the
|
|
103
|
+
* model's own default, which is neither what the caller asked for nor something it can see.
|
|
104
|
+
*/
|
|
105
|
+
refusedEfforts: Set<string>;
|
|
106
|
+
/**
|
|
107
|
+
* The efforts a refusal published as this model's, in ladder order, filtered to the ones
|
|
108
|
+
* `EFFORT_LADDER` can place. Absent until a refusal lists them, and a model that lists nothing
|
|
109
|
+
* is walked up the ladder a rung per refusal instead.
|
|
110
|
+
*/
|
|
111
|
+
supportedEfforts?: string[];
|
|
96
112
|
/**
|
|
97
113
|
* Continues a trailing assistant message rather than answering afresh, which `continueTurn`
|
|
98
114
|
* relies on. llama.cpp renders one as a prefill and picks up mid-word; hosted OpenAI takes the
|
|
@@ -170,6 +186,30 @@ export declare function resetCapabilities(endpoint?: {
|
|
|
170
186
|
* @returns How many endpoints were forgotten.
|
|
171
187
|
*/
|
|
172
188
|
export declare function expireCapabilities(maxAgeMs: number, now?: number): number;
|
|
189
|
+
/**
|
|
190
|
+
* The efforts this package can place, cheapest first. A substitution only ever walks *up* it.
|
|
191
|
+
*
|
|
192
|
+
* Not a list of what any model takes — `medium` is refused by a model whose list is
|
|
193
|
+
* `minimal, low, high`, and `xhigh` is real and deliberately absent. It is the order the values
|
|
194
|
+
* OpenAI has shipped stand in, which is all a step needs to know. A value not on it cannot be
|
|
195
|
+
* placed, and an unplaceable value is left alone rather than guessed at: stepping down from a
|
|
196
|
+
* refused `xhigh` to `high` would quietly answer a question with less deliberation than whoever
|
|
197
|
+
* typed it asked for, where stepping up from `none` to `minimal` only costs tokens and says so.
|
|
198
|
+
*/
|
|
199
|
+
export declare const EFFORT_LADDER: readonly ["none", "minimal", "low", "medium", "high"];
|
|
200
|
+
/**
|
|
201
|
+
* What to actually put in `reasoning_effort` for a model that has refused the value asked for.
|
|
202
|
+
*
|
|
203
|
+
* The caller's own value, until this model has said that value is not one of its own; then the
|
|
204
|
+
* cheapest it will take that is at least as much deliberation. A model that takes no effort at
|
|
205
|
+
* all, and an absent or `"off"` setting, both answer the empty string, which is the body
|
|
206
|
+
* builders' signal to send no field.
|
|
207
|
+
*
|
|
208
|
+
* @param refused What the model has refused, as `negotiate` hands it over. Absent is a model that
|
|
209
|
+
* has refused nothing.
|
|
210
|
+
* @param asked What the config asks for. `"off"` and absent mean no effort.
|
|
211
|
+
*/
|
|
212
|
+
export declare function effortFor(refused: ModelCapabilities | undefined, asked: string | undefined): string;
|
|
173
213
|
/** What `negotiate` takes besides the request. All optional. */
|
|
174
214
|
export interface NegotiateOptions {
|
|
175
215
|
/**
|
package/dist/capabilities.js
CHANGED
|
@@ -56,6 +56,7 @@ export function modelCapabilitiesFor(supports, model) {
|
|
|
56
56
|
chosenTemperature: true,
|
|
57
57
|
refusedFields: new Set(),
|
|
58
58
|
structuredOutput: true,
|
|
59
|
+
refusedEfforts: new Set(),
|
|
59
60
|
assistantPrefill: true,
|
|
60
61
|
};
|
|
61
62
|
supports.models.set(model, known);
|
|
@@ -154,7 +155,8 @@ const rejectsResponseFormat = (detail) => /response_format|json_schema/i.test(de
|
|
|
154
155
|
* perfectly well; it was handed an effort off a list this package does not know. Dropping the
|
|
155
156
|
* field succeeds, at the model's own default effort, which is neither what the caller asked for
|
|
156
157
|
* nor something it can see — and the drop latches, so every later turn on that model reasons at
|
|
157
|
-
* the default with the setting still reading what the operator typed.
|
|
158
|
+
* the default with the setting still reading what the operator typed. `stepEffort` answers this
|
|
159
|
+
* one instead, by naming an effort the model does take.
|
|
158
160
|
*/
|
|
159
161
|
const REFUSED_VALUE = /unsupported value|invalid value|supported values/i;
|
|
160
162
|
/**
|
|
@@ -165,6 +167,127 @@ const REFUSED_VALUE = /unsupported value|invalid value|supported values/i;
|
|
|
165
167
|
* latching.
|
|
166
168
|
*/
|
|
167
169
|
const rejectsEffort = (detail) => /reasoning_effort/i.test(detail) && !REFUSED_VALUE.test(detail);
|
|
170
|
+
/**
|
|
171
|
+
* The efforts this package can place, cheapest first. A substitution only ever walks *up* it.
|
|
172
|
+
*
|
|
173
|
+
* Not a list of what any model takes — `medium` is refused by a model whose list is
|
|
174
|
+
* `minimal, low, high`, and `xhigh` is real and deliberately absent. It is the order the values
|
|
175
|
+
* OpenAI has shipped stand in, which is all a step needs to know. A value not on it cannot be
|
|
176
|
+
* placed, and an unplaceable value is left alone rather than guessed at: stepping down from a
|
|
177
|
+
* refused `xhigh` to `high` would quietly answer a question with less deliberation than whoever
|
|
178
|
+
* typed it asked for, where stepping up from `none` to `minimal` only costs tokens and says so.
|
|
179
|
+
*/
|
|
180
|
+
export const EFFORT_LADDER = ["none", "minimal", "low", "medium", "high"];
|
|
181
|
+
const rankOf = (effort) => EFFORT_LADDER.indexOf(effort.toLowerCase());
|
|
182
|
+
/**
|
|
183
|
+
* The efforts a refusal lists as this model's: `Supported values are: 'minimal', 'low', 'medium',
|
|
184
|
+
* and 'high'.` Quoted or bare, separated by commas and a trailing `and` or `or`.
|
|
185
|
+
*/
|
|
186
|
+
function listedEfforts(detail) {
|
|
187
|
+
const listed = detail.match(/supported values(?:\s+\w+)?\s*(?:are|is|include)?\s*:?\s*([^\n.]+)/i)?.[1];
|
|
188
|
+
if (!listed)
|
|
189
|
+
return undefined;
|
|
190
|
+
const values = listed
|
|
191
|
+
.split(/,|\band\b|\bor\b/)
|
|
192
|
+
.map((value) => value
|
|
193
|
+
.trim()
|
|
194
|
+
.replace(/^['"`]+|['"`]+$/g, "")
|
|
195
|
+
.toLowerCase())
|
|
196
|
+
.filter((value) => rankOf(value) >= 0);
|
|
197
|
+
return values.length ? values : undefined;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Which effort a refusal says was refused, in the two shapes the wording takes: the field quoted
|
|
201
|
+
* then `does not support 'none'`, and `reasoning_effort: none`.
|
|
202
|
+
*
|
|
203
|
+
* Read rather than remembered, because `negotiate` builds no request and so does not know what
|
|
204
|
+
* went out — and a proxy that rewrites the value before passing it on is refusing the one it sent
|
|
205
|
+
* rather than the one it was given. A refusal that names no value steps nothing: guessing which
|
|
206
|
+
* rung was refused is how a ladder walks past the value that would have worked.
|
|
207
|
+
*
|
|
208
|
+
* @param detail The refusal.
|
|
209
|
+
* @param supported What the same refusal listed, which the value cannot be one of.
|
|
210
|
+
*/
|
|
211
|
+
function refusedEffortValue(detail, supported = []) {
|
|
212
|
+
const found = detail.match(/reasoning_effort['"`]?\s*(?:does not support|is not supported with|:)\s*['"`]?([\w-]+)/i)?.[1];
|
|
213
|
+
const value = found?.toLowerCase();
|
|
214
|
+
return value && !supported.includes(value) ? value : undefined;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* The cheapest effort above this one that the model has not refused, or `undefined` when the
|
|
218
|
+
* ladder is out of rungs.
|
|
219
|
+
*
|
|
220
|
+
* Above, never below: see `EFFORT_LADDER`. Where a refusal published a list that is the whole of
|
|
221
|
+
* what is tried, so the step lands in one request; where it published none the ladder is walked a
|
|
222
|
+
* rung at a time, each refusal latching the rung it named.
|
|
223
|
+
*/
|
|
224
|
+
function nextEffort(refused, asked) {
|
|
225
|
+
const rank = rankOf(asked);
|
|
226
|
+
if (rank < 0)
|
|
227
|
+
return undefined;
|
|
228
|
+
let best;
|
|
229
|
+
for (const value of refused.supportedEfforts ?? EFFORT_LADDER) {
|
|
230
|
+
const at = rankOf(value);
|
|
231
|
+
if (at <= rank || refused.refusedEfforts.has(value))
|
|
232
|
+
continue;
|
|
233
|
+
if (best === undefined || at < rankOf(best))
|
|
234
|
+
best = value;
|
|
235
|
+
}
|
|
236
|
+
return best;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* What to actually put in `reasoning_effort` for a model that has refused the value asked for.
|
|
240
|
+
*
|
|
241
|
+
* The caller's own value, until this model has said that value is not one of its own; then the
|
|
242
|
+
* cheapest it will take that is at least as much deliberation. A model that takes no effort at
|
|
243
|
+
* all, and an absent or `"off"` setting, both answer the empty string, which is the body
|
|
244
|
+
* builders' signal to send no field.
|
|
245
|
+
*
|
|
246
|
+
* @param refused What the model has refused, as `negotiate` hands it over. Absent is a model that
|
|
247
|
+
* has refused nothing.
|
|
248
|
+
* @param asked What the config asks for. `"off"` and absent mean no effort.
|
|
249
|
+
*/
|
|
250
|
+
export function effortFor(refused, asked) {
|
|
251
|
+
if (!asked || asked === "off")
|
|
252
|
+
return "";
|
|
253
|
+
if (!refused)
|
|
254
|
+
return asked;
|
|
255
|
+
if (!refused.reasoningEffort)
|
|
256
|
+
return "";
|
|
257
|
+
const known = refused.supportedEfforts;
|
|
258
|
+
const listed = known ? known.includes(asked.toLowerCase()) : true;
|
|
259
|
+
if (listed && !refused.refusedEfforts.has(asked.toLowerCase()))
|
|
260
|
+
return asked;
|
|
261
|
+
return nextEffort(refused, asked) ?? asked;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* How to answer a refused effort *value*, or `undefined` when there is nothing new to learn or
|
|
265
|
+
* nowhere left to go — in which case the refusal is the caller's, as it was before this existed.
|
|
266
|
+
*
|
|
267
|
+
* Pure, so `negotiate` can work out whether this refusal is one of its own before deciding to
|
|
268
|
+
* answer it, and latch only in the branch it takes.
|
|
269
|
+
*/
|
|
270
|
+
function planEffortStep(detail, refused) {
|
|
271
|
+
if (!refused.reasoningEffort)
|
|
272
|
+
return undefined;
|
|
273
|
+
if (!/reasoning_effort/i.test(detail) || !REFUSED_VALUE.test(detail))
|
|
274
|
+
return undefined;
|
|
275
|
+
const supported = listedEfforts(detail);
|
|
276
|
+
const value = refusedEffortValue(detail, supported);
|
|
277
|
+
if (value === undefined)
|
|
278
|
+
return undefined;
|
|
279
|
+
// Something has to be new, or a server that answers every request by naming an effort nobody
|
|
280
|
+
// sent would have `negotiate` re-send for as long as it kept saying it.
|
|
281
|
+
const fresh = supported?.join() !== refused.supportedEfforts?.join();
|
|
282
|
+
if (refused.refusedEfforts.has(value) && !fresh)
|
|
283
|
+
return undefined;
|
|
284
|
+
const next = nextEffort({
|
|
285
|
+
...refused,
|
|
286
|
+
refusedEfforts: new Set(refused.refusedEfforts).add(value),
|
|
287
|
+
supportedEfforts: supported ?? refused.supportedEfforts,
|
|
288
|
+
}, value);
|
|
289
|
+
return next === undefined ? undefined : { value, next, ...(supported ? { supported } : {}) };
|
|
290
|
+
}
|
|
168
291
|
/**
|
|
169
292
|
* Read only alongside the name it is asking for: `'max_tokens' is not supported with this model.
|
|
170
293
|
* Use 'max_completion_tokens' instead.`
|
|
@@ -280,6 +403,7 @@ export async function negotiate(supports, send, { produced = { any: false }, onN
|
|
|
280
403
|
if (produced.any)
|
|
281
404
|
throw error;
|
|
282
405
|
const detail = errorMessage(error);
|
|
406
|
+
const stepped = named && planEffortStep(detail, named.refused);
|
|
283
407
|
if (supports.strictSchemas && isGrammarError(detail)) {
|
|
284
408
|
supports.strictSchemas = false;
|
|
285
409
|
onNotice?.("server could not build a grammar; retrying without pattern/format");
|
|
@@ -292,6 +416,12 @@ export async function negotiate(supports, send, { produced = { any: false }, onN
|
|
|
292
416
|
named.refused.reasoningEffort = false;
|
|
293
417
|
onNotice?.(`${named.name} does not take a reasoning effort; retrying without one`);
|
|
294
418
|
}
|
|
419
|
+
else if (named && stepped) {
|
|
420
|
+
named.refused.refusedEfforts.add(stepped.value);
|
|
421
|
+
if (stepped.supported)
|
|
422
|
+
named.refused.supportedEfforts = stepped.supported;
|
|
423
|
+
onNotice?.(`${named.name} does not reason at ${stepped.value}; retrying at ${stepped.next}`);
|
|
424
|
+
}
|
|
295
425
|
else if (named?.refused.legacyTokenLimit && wantsCompletionLimit(detail)) {
|
|
296
426
|
named.refused.legacyTokenLimit = false;
|
|
297
427
|
onNotice?.(`${named.name} wants max_completion_tokens; retrying with the limit spelled that way`);
|
package/dist/index.d.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
export { type AgentLoopHooks, type AgentLoopOptions, type AgentLoopResult, buildBody, preselect, preview, resolveApiKey, runAgentLoop, type ToolCallOutcome, type ToolCallRequest, } from "./agent-loop.ts";
|
|
13
13
|
export { calibrate, charsPerTokenFor, resetCalibration } from "./calibration.ts";
|
|
14
|
-
export { type Capabilities, capabilitiesFor, expireCapabilities, type ModelCapabilities, modelCapabilitiesFor, type NegotiateOptions, negotiate, resetCapabilities, } from "./capabilities.ts";
|
|
14
|
+
export { type Capabilities, capabilitiesFor, EFFORT_LADDER, effortFor, expireCapabilities, type ModelCapabilities, modelCapabilitiesFor, type NegotiateOptions, negotiate, resetCapabilities, } from "./capabilities.ts";
|
|
15
15
|
export type { CatalogServer } from "./catalog.ts";
|
|
16
16
|
export { type ClientPoolOptions, configureClients, contextLimitFor, endpointId, endpointKey, FIRST_TOKEN_FACTOR, firstTokenMs, getClient, listModels, type ModelInfo, NO_KEY, resetClients, servedWindow, timeoutMs, } from "./client.ts";
|
|
17
17
|
export { applyCompaction, COMPACT_AT, type CompactionOptions, type CompactionPlan, type CompactionRecord, type CompactionRunOptions, compactTranscript, KEEP_RATIO, type PruneOptions, planCompaction, pruneToolResults, requestIndex, runCompaction, SUMMARY_LEAD, SUMMARY_PROMPT, summariser, summaryInput, } from "./compaction.ts";
|
package/dist/index.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
export { buildBody, preselect, preview, resolveApiKey, runAgentLoop, } from "./agent-loop.js";
|
|
13
13
|
export { calibrate, charsPerTokenFor, resetCalibration } from "./calibration.js";
|
|
14
|
-
export { capabilitiesFor, expireCapabilities, modelCapabilitiesFor, negotiate, resetCapabilities, } from "./capabilities.js";
|
|
14
|
+
export { capabilitiesFor, EFFORT_LADDER, effortFor, expireCapabilities, modelCapabilitiesFor, negotiate, resetCapabilities, } from "./capabilities.js";
|
|
15
15
|
export { configureClients, contextLimitFor, endpointId, endpointKey, FIRST_TOKEN_FACTOR, firstTokenMs, getClient, listModels, NO_KEY, resetClients, servedWindow, timeoutMs, } from "./client.js";
|
|
16
16
|
export { applyCompaction, COMPACT_AT, compactTranscript, KEEP_RATIO, planCompaction, pruneToolResults, requestIndex, runCompaction, SUMMARY_LEAD, SUMMARY_PROMPT, summariser, summaryInput, } from "./compaction.js";
|
|
17
17
|
export { continueTurn, isContinuable, } from "./continuation.js";
|
package/dist/side-task.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import OpenAI from "openai";
|
|
2
|
-
import { capabilitiesFor, modelCapabilitiesFor, negotiate, } from "./capabilities.js";
|
|
2
|
+
import { capabilitiesFor, effortFor, modelCapabilitiesFor, negotiate, } from "./capabilities.js";
|
|
3
3
|
import { endpointId, getClient } from "./client.js";
|
|
4
4
|
import { errorMessage } from "./errors.js";
|
|
5
5
|
import { isTransient } from "./retry.js";
|
|
@@ -85,7 +85,11 @@ async function complete(config, model, system, user, { maxTokens = 512, temperat
|
|
|
85
85
|
// Whether the last request carried an effort, which `negotiate` decides and not this function.
|
|
86
86
|
let sentEffort = false;
|
|
87
87
|
const send = (hints, effort, supports, refused) => {
|
|
88
|
-
|
|
88
|
+
// `none` is what a side task wants and not always what the model offers: OpenAI's reasoning
|
|
89
|
+
// models refuse it by value and list `minimal` as their floor. `effortFor` answers with the
|
|
90
|
+
// cheapest rung this one takes, which `negotiate` has been stepping up as it was refused.
|
|
91
|
+
const asked = effort ? effortFor(refused, "none") : "";
|
|
92
|
+
sentEffort = asked !== "";
|
|
89
93
|
return getClient(config).chat.completions.create({
|
|
90
94
|
model,
|
|
91
95
|
// The reasoning models want the ceiling spelled the other way, and they are exactly the
|
|
@@ -103,7 +107,7 @@ async function complete(config, model, system, user, { maxTokens = 512, temperat
|
|
|
103
107
|
...(hints ? NO_THINKING : {}),
|
|
104
108
|
// Not gated on `hints`: a model that refuses `chat_template_kwargs` may still read the
|
|
105
109
|
// effort, and the two latches would otherwise contradict each other.
|
|
106
|
-
...(sentEffort ? { reasoning_effort:
|
|
110
|
+
...(sentEffort ? { reasoning_effort: asked } : {}),
|
|
107
111
|
...(format && refused ? format(supports, refused) : {}),
|
|
108
112
|
}, { signal });
|
|
109
113
|
};
|
package/dist/snapshot.d.ts
CHANGED
|
@@ -22,6 +22,14 @@ export interface ModelSnapshot {
|
|
|
22
22
|
assistantPrefill: boolean;
|
|
23
23
|
/** Takes the no-thinking hints `ask` sends. */
|
|
24
24
|
thinkingHints: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Efforts refused by value rather than by field, so a restart does not spend a request per rung
|
|
27
|
+
* walking the ladder again. Absent in a snapshot taken before they were latched, which reads as
|
|
28
|
+
* none refused.
|
|
29
|
+
*/
|
|
30
|
+
refusedEfforts?: string[];
|
|
31
|
+
/** The efforts a refusal published as this model's, in ladder order. Absent is none published. */
|
|
32
|
+
supportedEfforts?: string[];
|
|
25
33
|
}
|
|
26
34
|
/** What one endpoint refused, and under it what each of its models did. */
|
|
27
35
|
export interface EndpointSnapshot {
|
package/dist/snapshot.js
CHANGED
|
@@ -18,6 +18,7 @@ const optimisticModel = () => ({
|
|
|
18
18
|
structuredOutput: true,
|
|
19
19
|
assistantPrefill: true,
|
|
20
20
|
thinkingHints: true,
|
|
21
|
+
refusedEfforts: [],
|
|
21
22
|
});
|
|
22
23
|
const refusedAnything = (model) => !model.reasoningEffort ||
|
|
23
24
|
!model.legacyTokenLimit ||
|
|
@@ -25,7 +26,8 @@ const refusedAnything = (model) => !model.reasoningEffort ||
|
|
|
25
26
|
!model.thinkingHints ||
|
|
26
27
|
!model.structuredOutput ||
|
|
27
28
|
!model.assistantPrefill ||
|
|
28
|
-
model.refusedFields.length > 0
|
|
29
|
+
model.refusedFields.length > 0 ||
|
|
30
|
+
(model.refusedEfforts?.length ?? 0) > 0;
|
|
29
31
|
/**
|
|
30
32
|
* Every refusal this process has latched, as a JSON-safe blob to store and hand back on boot.
|
|
31
33
|
*
|
|
@@ -59,6 +61,10 @@ export function exportCapabilities() {
|
|
|
59
61
|
refusedFields: [...refused.refusedFields].sort(),
|
|
60
62
|
structuredOutput: refused.structuredOutput,
|
|
61
63
|
assistantPrefill: refused.assistantPrefill,
|
|
64
|
+
refusedEfforts: [...refused.refusedEfforts].sort(),
|
|
65
|
+
// Only alongside a refusal, since on its own a published list latches nothing: the model
|
|
66
|
+
// named it while refusing a rung, and that rung is in `refusedEfforts`.
|
|
67
|
+
...(refused.supportedEfforts ? { supportedEfforts: [...refused.supportedEfforts] } : {}),
|
|
62
68
|
};
|
|
63
69
|
if (refusedAnything(model))
|
|
64
70
|
models[name] = model;
|
|
@@ -128,6 +134,19 @@ export function importCapabilities(snapshot) {
|
|
|
128
134
|
refused.structuredOutput = false;
|
|
129
135
|
if (model.assistantPrefill === false)
|
|
130
136
|
refused.assistantPrefill = false;
|
|
137
|
+
if (Array.isArray(model.refusedEfforts)) {
|
|
138
|
+
for (const effort of model.refusedEfforts) {
|
|
139
|
+
if (typeof effort === "string")
|
|
140
|
+
refused.refusedEfforts.add(effort);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// Replaced rather than merged: two lists of what one model takes are two readings of the
|
|
144
|
+
// same fact, and the stored one is at least as recent as an empty absent.
|
|
145
|
+
if (Array.isArray(model.supportedEfforts)) {
|
|
146
|
+
const listed = model.supportedEfforts.filter((value) => typeof value === "string");
|
|
147
|
+
if (listed.length)
|
|
148
|
+
refused.supportedEfforts = listed;
|
|
149
|
+
}
|
|
131
150
|
if (Array.isArray(model.refusedFields)) {
|
|
132
151
|
for (const field of model.refusedFields) {
|
|
133
152
|
if (typeof field === "string")
|
package/llms.txt
CHANGED
|
@@ -38,6 +38,8 @@ What an endpoint turned out not to support, and answering it when it says so.
|
|
|
38
38
|
|
|
39
39
|
- `Capabilities` (type) — What one endpoint turned out not to support.
|
|
40
40
|
- `capabilitiesFor` — What this endpoint is known not to support.
|
|
41
|
+
- `EFFORT_LADDER` — The efforts this package can place, cheapest first.
|
|
42
|
+
- `effortFor` — What to actually put in `reasoning_effort` for a model that has refused the value asked for.
|
|
41
43
|
- `expireCapabilities` — Forgets every endpoint whose entry is older than this, so the next request finds out again.
|
|
42
44
|
- `ModelCapabilities` (type) — What one model on that endpoint turned out not to support.
|
|
43
45
|
- `modelCapabilitiesFor` — What this model on this endpoint is known not to support.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cubicecho/agent-core",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.16.0",
|
|
4
4
|
"description": "The endpoint-agnostic half of an OpenAI-compatible agent loop: tool-schema compatibility, on-demand tool loading, one-shot side tasks, run events, and a pooled client.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"openai",
|