@cubicecho/agent-core 3.0.0 → 4.1.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 +34 -3
- package/dist/{compaction.d.ts → context/compaction.d.ts} +6 -6
- package/dist/{compaction.js → context/compaction.js} +9 -9
- package/dist/{ledger.d.ts → context/ledger.d.ts} +2 -2
- package/dist/{ledger.js → context/ledger.js} +3 -3
- package/dist/{guards.d.ts → core/guards.d.ts} +2 -3
- package/dist/{guards.js → core/guards.js} +1 -2
- package/dist/{scope.d.ts → core/scope.d.ts} +3 -3
- package/dist/{calibration.js → endpoint/calibration.js} +4 -4
- package/dist/{capabilities.d.ts → endpoint/capabilities.d.ts} +2 -2
- package/dist/{capabilities.js → endpoint/capabilities.js} +3 -3
- package/dist/{client.d.ts → endpoint/client.d.ts} +27 -11
- package/dist/{client.js → endpoint/client.js} +85 -26
- package/dist/{schema-compat.js → endpoint/schema-compat.js} +2 -2
- package/dist/{hooks.js → hooks/hooks.js} +6 -6
- package/dist/index.d.ts +30 -30
- package/dist/index.js +29 -29
- package/dist/{agent-loop.d.ts → run/agent-loop.d.ts} +25 -16
- package/dist/{agent-loop.js → run/agent-loop.js} +34 -25
- package/dist/{events.d.ts → run/events.d.ts} +3 -3
- package/dist/{events.js → run/events.js} +6 -6
- package/dist/{request-body.d.ts → run/request-body.d.ts} +3 -3
- package/dist/{request-body.js → run/request-body.js} +4 -4
- package/dist/{run-calls.d.ts → run/run-calls.d.ts} +3 -3
- package/dist/{run-calls.js → run/run-calls.js} +4 -4
- package/dist/{reset.js → runtime/reset.js} +6 -6
- package/dist/{runtime.d.ts → runtime/runtime.d.ts} +11 -11
- package/dist/{runtime.js → runtime/runtime.js} +12 -12
- package/dist/{snapshot.js → runtime/snapshot.js} +4 -4
- package/dist/{spec.d.ts → spec/spec.d.ts} +2 -2
- package/dist/{spec.js → spec/spec.js} +3 -3
- package/dist/{preselect.d.ts → tools/preselect.d.ts} +5 -4
- package/dist/{preselect.js → tools/preselect.js} +6 -5
- package/dist/{tool-loading.js → tools/tool-loading.js} +3 -3
- package/dist/{continuation.d.ts → turn/continuation.d.ts} +4 -4
- package/dist/{continuation.js → turn/continuation.js} +7 -7
- package/dist/{run-turn.d.ts → turn/run-turn.d.ts} +2 -2
- package/dist/{run-turn.js → turn/run-turn.js} +7 -7
- package/dist/{side-task.d.ts → turn/side-task.d.ts} +2 -2
- package/dist/{side-task.js → turn/side-task.js} +9 -9
- package/dist/{errors.d.ts → wire/errors.d.ts} +4 -0
- package/dist/{errors.js → wire/errors.js} +4 -0
- package/dist/{retry.d.ts → wire/retry.d.ts} +1 -1
- package/dist/{retry.js → wire/retry.js} +2 -2
- package/dist/{stream.js → wire/stream.js} +1 -1
- package/dist/{tokens.d.ts → wire/tokens.d.ts} +2 -2
- package/dist/{tokens.js → wire/tokens.js} +2 -2
- package/dist/{tool-calls.js → wire/tool-calls.js} +1 -1
- package/llms.txt +177 -179
- package/package.json +3 -3
- /package/dist/{config.d.ts → core/config.d.ts} +0 -0
- /package/dist/{config.js → core/config.js} +0 -0
- /package/dist/{digest.d.ts → core/digest.d.ts} +0 -0
- /package/dist/{digest.js → core/digest.js} +0 -0
- /package/dist/{platform.d.ts → core/platform.d.ts} +0 -0
- /package/dist/{platform.js → core/platform.js} +0 -0
- /package/dist/{scope.js → core/scope.js} +0 -0
- /package/dist/{calibration.d.ts → endpoint/calibration.d.ts} +0 -0
- /package/dist/{schema-compat.d.ts → endpoint/schema-compat.d.ts} +0 -0
- /package/dist/{hook-events.d.ts → hooks/hook-events.d.ts} +0 -0
- /package/dist/{hook-events.js → hooks/hook-events.js} +0 -0
- /package/dist/{hooks.d.ts → hooks/hooks.d.ts} +0 -0
- /package/dist/{reset.d.ts → runtime/reset.d.ts} +0 -0
- /package/dist/{snapshot.d.ts → runtime/snapshot.d.ts} +0 -0
- /package/dist/{catalog.d.ts → tools/catalog.d.ts} +0 -0
- /package/dist/{catalog.js → tools/catalog.js} +0 -0
- /package/dist/{tool-loading.d.ts → tools/tool-loading.d.ts} +0 -0
- /package/dist/{stream.d.ts → wire/stream.d.ts} +0 -0
- /package/dist/{thinking.d.ts → wire/thinking.d.ts} +0 -0
- /package/dist/{thinking.js → wire/thinking.js} +0 -0
- /package/dist/{tool-calls.d.ts → wire/tool-calls.d.ts} +0 -0
- /package/dist/{wire.d.ts → wire/wire.d.ts} +0 -0
- /package/dist/{wire.js → wire/wire.js} +0 -0
package/README.md
CHANGED
|
@@ -299,6 +299,23 @@ neither native route gives a number. A server with neither route is latched and
|
|
|
299
299
|
`/api/v0/models` is also dropped on its own: for the life of the process once it answers 404, 405
|
|
300
300
|
or 501, and for `listingMissMs` after any other error.
|
|
301
301
|
|
|
302
|
+
A host that sizes the window before each run passes the run's signal as the third argument, so a
|
|
303
|
+
stopped run does not wait on the lookup:
|
|
304
|
+
|
|
305
|
+
```ts
|
|
306
|
+
const contextLimit = await contextLimitFor({ ...settings, model }, settings.contextLength, { signal });
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
It rejects with the signal's reason, closes the request it had open, and remembers nothing of the
|
|
310
|
+
lookup. Nothing else makes it reject. `listModels` and `servedWindow` take the same option.
|
|
311
|
+
|
|
312
|
+
The lookup is also bounded on its own, at ten seconds across the listing and the native routes
|
|
313
|
+
together, or at the endpoint's request timeout where that is shorter. `requestTimeoutSeconds` is
|
|
314
|
+
how long to wait for a model to speak, and an endpoint that accepts the connection and then says
|
|
315
|
+
nothing would otherwise hold the lookup for that long, or for ever where there is none. Past the
|
|
316
|
+
bound `contextLimitFor` answers zero, as it does for a server it could not reach, and
|
|
317
|
+
`listModels` rejects with a `TimeoutError`.
|
|
318
|
+
|
|
302
319
|
A top-level listing key is trusted without a second opinion, so on a server that puts a trained
|
|
303
320
|
window there, pass `contextLength`. Ollama reports no window on any route this reads, and truncates
|
|
304
321
|
an over-long prompt rather than refusing it, so on Ollama pass `contextLength` or there is no guard
|
|
@@ -703,7 +720,7 @@ try {
|
|
|
703
720
|
store.add(sessionId, turn?.reasoning ? { ...message, reasoning_content: turn.reasoning } : message),
|
|
704
721
|
});
|
|
705
722
|
} catch (error) {
|
|
706
|
-
const run = failedRun(error); // messages, usage, toolCalls, loaded, used — or undefined
|
|
723
|
+
const run = failedRun(error); // messages, usage, toolCalls, loaded, used, preselected — or undefined
|
|
707
724
|
if (run) await store.keep(sessionId, { usage: run.usage, loaded: run.loaded });
|
|
708
725
|
if (signal.aborted) return; // a stop, told by the signal as it always was
|
|
709
726
|
if (error instanceof ContextOverflow) return compactAndRetry(run?.messages);
|
|
@@ -1195,6 +1212,18 @@ with `tasks.compaction` writing the summary and no falling back to the main mode
|
|
|
1195
1212
|
are off by default: a host that already calls `preselect` or compacts in `beforeStep` is not sent
|
|
1196
1213
|
a second request.
|
|
1197
1214
|
|
|
1215
|
+
What the loop picked it says twice. A `notice` event names the tools before the first `turn` event
|
|
1216
|
+
— `PRESELECTED_NOTICE` and then the names, comma-separated — and the result's `preselected` is the
|
|
1217
|
+
shortlist the run opened with, the host's own where it gave one and empty where nothing was picked.
|
|
1218
|
+
A run that fails carries it too, beside `loaded` and `used`.
|
|
1219
|
+
|
|
1220
|
+
An empty `toolSelectModel` means no preselection, and says nothing about it. A host whose settings
|
|
1221
|
+
read `""` as "use the run's model" has to say so in the config it builds:
|
|
1222
|
+
|
|
1223
|
+
```ts
|
|
1224
|
+
const config = { ...settings, toolSelectModel: settings.toolSelectModel || settings.model };
|
|
1225
|
+
```
|
|
1226
|
+
|
|
1198
1227
|
```ts
|
|
1199
1228
|
const result = await runAgentLoop({ config, preselect: { keywords: true }, compact: true, ... });
|
|
1200
1229
|
```
|
|
@@ -1225,7 +1254,8 @@ OpenRouter `:free` suffix, a typo) would otherwise fetch the listing on every ca
|
|
|
1225
1254
|
same zero each time. Half a minute later it asks again, so a model pulled onto a box that has been
|
|
1226
1255
|
up a week is still picked up without a restart.
|
|
1227
1256
|
|
|
1228
|
-
Both numbers are defaults
|
|
1257
|
+
Both numbers are defaults, as is the ten seconds a window lookup is given
|
|
1258
|
+
(`lookupTimeoutMs`). `configureClients` moves them for the process, the way `configureEvents`
|
|
1229
1259
|
and `configureHooks` do — a multi-tenant host raising the pool so tenants stop evicting each other,
|
|
1230
1260
|
a dev box shortening the miss window so a model it has just pulled shows up sooner:
|
|
1231
1261
|
|
|
@@ -1245,7 +1275,8 @@ cannot drift. `endpointKey` holds the key in the clear; `endpointId`, its SHA-25
|
|
|
1245
1275
|
that is safe to write down.
|
|
1246
1276
|
|
|
1247
1277
|
`resetAll` drops all five, and `reset.ts` names each seam separately for a test that wants one —
|
|
1248
|
-
`resetCalibration` for the measured ratios
|
|
1278
|
+
`resetCalibration` for the measured ratios, `resetHints` for the models that refused the
|
|
1279
|
+
no-thinking hints.
|
|
1249
1280
|
|
|
1250
1281
|
The latches can outlive the process as well, because otherwise every restart spends one refused
|
|
1251
1282
|
request per endpoint and model learning the same facts again. `exportCapabilities` returns every
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type OpenAI from 'openai';
|
|
2
|
-
import type { Endpoint } from '
|
|
3
|
-
import { type HookContext, type HookRunner, type OnNote } from '
|
|
4
|
-
import { type SideTaskOptions } from '
|
|
2
|
+
import type { Endpoint } from '../core/config.ts';
|
|
3
|
+
import { type HookContext, type HookRunner, type OnNote } from '../hooks/hooks.ts';
|
|
4
|
+
import { type SideTaskOptions } from '../turn/side-task.ts';
|
|
5
5
|
/**
|
|
6
6
|
* Keeping a long run inside its window: stale tool results cleared, and the oldest stretch folded
|
|
7
7
|
* into a summary the model writes itself.
|
|
@@ -186,7 +186,7 @@ export declare function summaryInput(plan: CompactionPlan): string;
|
|
|
186
186
|
* @param model - The model to write it, which may be a smaller one than the run's.
|
|
187
187
|
* @param [options] - Cancellation and notices, and the instruction and ceiling the summary is
|
|
188
188
|
* written under.
|
|
189
|
-
* @returns A function from
|
|
189
|
+
* @returns A function from the text of what is being folded to the summary. It rejects as `ask` does, and
|
|
190
190
|
* resolves with an empty string where the model wrote nothing.
|
|
191
191
|
*/
|
|
192
192
|
export declare const summariser: (config: Endpoint, model: string, { system, maxTokens, ...options }?: SideTaskOptions & {
|
|
@@ -233,7 +233,7 @@ export interface CompactionRunOptions {
|
|
|
233
233
|
*
|
|
234
234
|
* @param messages - The transcript the plan was made for. Read only, and only for the hooks.
|
|
235
235
|
* @param plan - What `planCompaction` returned for it.
|
|
236
|
-
* @param summarise - Writes the summary from
|
|
236
|
+
* @param summarise - Writes the summary from the text it is handed. See `summariser`. Not called
|
|
237
237
|
* when a hook vetoes.
|
|
238
238
|
* @param [options] - Hooks to tell and whether the window is already past. See `CompactionRunOptions`.
|
|
239
239
|
* @returns `undefined` when nothing was folded — a hook vetoed, or the summary came back empty —
|
|
@@ -292,7 +292,7 @@ export declare const requestIndex: (index: number, record?: Pick<CompactionRecor
|
|
|
292
292
|
*
|
|
293
293
|
* @param messages - The transcript the plan was made for. Not written to.
|
|
294
294
|
* @param plan - What `planCompaction` returned for it.
|
|
295
|
-
* @param summarise - Writes the summary from
|
|
295
|
+
* @param summarise - Writes the summary from the text it is handed. See `summariser`. Not called
|
|
296
296
|
* when a hook vetoes.
|
|
297
297
|
* @param [options] - Hooks to tell and whether the window is already past. See
|
|
298
298
|
* `CompactionRunOptions`.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { HookEvent } from '
|
|
2
|
-
import { consult, notify, textOf, turnMessages } from '
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { FUNCTION_TOOL, Role } from '
|
|
1
|
+
import { HookEvent } from '../hooks/hook-events.js';
|
|
2
|
+
import { consult, notify, textOf, turnMessages, } from '../hooks/hooks.js';
|
|
3
|
+
import { holdsDefinitions } from '../tools/tool-loading.js';
|
|
4
|
+
import { ask } from '../turn/side-task.js';
|
|
5
|
+
import { messageTokens } from '../wire/tokens.js';
|
|
6
|
+
import { FUNCTION_TOOL, Role } from '../wire/wire.js';
|
|
7
7
|
/** The fraction of the window in use before a summary is worth its own round trip. */
|
|
8
8
|
export const COMPACT_AT = 0.75;
|
|
9
9
|
/** The fraction of the window the kept tail may fill, leaving room for the run to grow again. */
|
|
@@ -252,7 +252,7 @@ export function summaryInput(plan) {
|
|
|
252
252
|
* @param model - The model to write it, which may be a smaller one than the run's.
|
|
253
253
|
* @param [options] - Cancellation and notices, and the instruction and ceiling the summary is
|
|
254
254
|
* written under.
|
|
255
|
-
* @returns A function from
|
|
255
|
+
* @returns A function from the text of what is being folded to the summary. It rejects as `ask` does, and
|
|
256
256
|
* resolves with an empty string where the model wrote nothing.
|
|
257
257
|
*/
|
|
258
258
|
export const summariser = (config, model, { system = SUMMARY_PROMPT, maxTokens = SUMMARY_TOKENS, ...options } = {}) => (text) => ask(config, model, system, text, { maxTokens, ...options });
|
|
@@ -261,7 +261,7 @@ export const summariser = (config, model, { system = SUMMARY_PROMPT, maxTokens =
|
|
|
261
261
|
*
|
|
262
262
|
* @param messages - The transcript the plan was made for. Read only, and only for the hooks.
|
|
263
263
|
* @param plan - What `planCompaction` returned for it.
|
|
264
|
-
* @param summarise - Writes the summary from
|
|
264
|
+
* @param summarise - Writes the summary from the text it is handed. See `summariser`. Not called
|
|
265
265
|
* when a hook vetoes.
|
|
266
266
|
* @param [options] - Hooks to tell and whether the window is already past. See `CompactionRunOptions`.
|
|
267
267
|
* @returns `undefined` when nothing was folded — a hook vetoed, or the summary came back empty —
|
|
@@ -357,7 +357,7 @@ export const requestIndex = (index, record, head = 0) => {
|
|
|
357
357
|
*
|
|
358
358
|
* @param messages - The transcript the plan was made for. Not written to.
|
|
359
359
|
* @param plan - What `planCompaction` returned for it.
|
|
360
|
-
* @param summarise - Writes the summary from
|
|
360
|
+
* @param summarise - Writes the summary from the text it is handed. See `summariser`. Not called
|
|
361
361
|
* when a hook vetoes.
|
|
362
362
|
* @param [options] - Hooks to tell and whether the window is already past. See
|
|
363
363
|
* `CompactionRunOptions`.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type OpenAI from 'openai';
|
|
2
|
-
import type { TurnUsage } from '
|
|
3
|
-
import { type TokenEstimateOptions } from '
|
|
2
|
+
import type { TurnUsage } from '../wire/stream.ts';
|
|
3
|
+
import { type TokenEstimateOptions } from '../wire/tokens.ts';
|
|
4
4
|
/**
|
|
5
5
|
* What stretches of a transcript cost, read off the prompt counts the server already reported.
|
|
6
6
|
*
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { CacheBreakReason } from '
|
|
2
|
-
import { messageChars, messageTokens } from '
|
|
3
|
-
import { Role } from '
|
|
1
|
+
import { CacheBreakReason } from '../wire/stream.js';
|
|
2
|
+
import { messageChars, messageTokens } from '../wire/tokens.js';
|
|
3
|
+
import { Role } from '../wire/wire.js';
|
|
4
4
|
/**
|
|
5
5
|
* Whether two messages say the same thing, by identity first since most of a transcript is.
|
|
6
6
|
*
|
|
@@ -37,12 +37,11 @@ interface Lookup<K, V> {
|
|
|
37
37
|
*
|
|
38
38
|
* @param map - Where to look. Gains an entry on a miss, which is the point of calling this.
|
|
39
39
|
* @param key - What to look under.
|
|
40
|
-
* @param create - Builds the value on a miss, and is not called on a hit.
|
|
41
|
-
* reads as a miss, so one that returns it is called every time.
|
|
40
|
+
* @param create - Builds the value on a miss, and is not called on a hit.
|
|
42
41
|
* @returns What the map holds under the key afterwards: the held value itself on a hit, what
|
|
43
42
|
* `create` returned on a miss.
|
|
44
43
|
*/
|
|
45
|
-
export declare function getOrCreate<K, V>(map: Lookup<K, V>, key: K, create: () => V): V;
|
|
44
|
+
export declare function getOrCreate<K, V extends {}>(map: Lookup<K, V>, key: K, create: () => V): V;
|
|
46
45
|
/**
|
|
47
46
|
* A count and its noun, plural unless the count is one: `1 tool`, `3 tools`.
|
|
48
47
|
*
|
|
@@ -32,8 +32,7 @@ export const isPositive = (value) => typeof value === 'number' && value > 0;
|
|
|
32
32
|
*
|
|
33
33
|
* @param map - Where to look. Gains an entry on a miss, which is the point of calling this.
|
|
34
34
|
* @param key - What to look under.
|
|
35
|
-
* @param create - Builds the value on a miss, and is not called on a hit.
|
|
36
|
-
* reads as a miss, so one that returns it is called every time.
|
|
35
|
+
* @param create - Builds the value on a miss, and is not called on a hit.
|
|
37
36
|
* @returns What the map holds under the key afterwards: the held value itself on a hit, what
|
|
38
37
|
* `create` returned on a miss.
|
|
39
38
|
*/
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* A map rather than a record of named caches, so a module that grows a cache declares it where
|
|
6
6
|
* it is used and `runtime.ts` does not have to hear about it.
|
|
7
7
|
*/
|
|
8
|
-
export type Scope = Map<symbol, unknown
|
|
8
|
+
export type Scope = Map<symbol, NonNullable<unknown>>;
|
|
9
9
|
/** The default runtime's scope: what the top-level functions use when no runtime is running. */
|
|
10
10
|
export declare const rootScope: Scope;
|
|
11
11
|
/**
|
|
@@ -23,7 +23,7 @@ export declare const currentScope: () => Scope;
|
|
|
23
23
|
*/
|
|
24
24
|
export declare const inScope: <T>(scope: Scope, fn: () => T) => T;
|
|
25
25
|
/** One module's state, read from whichever scope is current. See `scoped`. */
|
|
26
|
-
export interface Scoped<T> {
|
|
26
|
+
export interface Scoped<T extends {}> {
|
|
27
27
|
/** The current scope's copy, made on first use. */
|
|
28
28
|
(): T;
|
|
29
29
|
/** Drops the current scope's copy, so the next read makes a fresh one. */
|
|
@@ -41,7 +41,7 @@ export interface Scoped<T> {
|
|
|
41
41
|
* and each use reads `cache()`. The copy is made lazily, so a runtime pays only for the modules
|
|
42
42
|
* it touches.
|
|
43
43
|
*/
|
|
44
|
-
export declare function scoped<T>(create: () => T): Scoped<T>;
|
|
44
|
+
export declare function scoped<T extends {}>(create: () => T): Scoped<T>;
|
|
45
45
|
/**
|
|
46
46
|
* Writes what a caller gave onto the settings in force, field by field, skipping what is unusable.
|
|
47
47
|
*
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { getOrCreate } from '
|
|
2
|
-
import { scoped } from '
|
|
3
|
-
import { CHARS_PER_TOKEN, requestChars, toolsChars } from '
|
|
4
|
-
import { PartType } from '
|
|
1
|
+
import { getOrCreate } from '../core/guards.js';
|
|
2
|
+
import { scoped } from '../core/scope.js';
|
|
3
|
+
import { CHARS_PER_TOKEN, requestChars, toolsChars } from '../wire/tokens.js';
|
|
4
|
+
import { PartType } from '../wire/wire.js';
|
|
5
5
|
/**
|
|
6
6
|
* How many characters a token is worth on one model, learned from what its endpoint reports.
|
|
7
7
|
*
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { EndpointIdentity } from '
|
|
2
|
-
import type { Produced } from '
|
|
1
|
+
import type { EndpointIdentity } from '../core/config.ts';
|
|
2
|
+
import type { Produced } from '../wire/stream.ts';
|
|
3
3
|
/**
|
|
4
4
|
* What an endpoint turned out not to support, and answering it when it says so.
|
|
5
5
|
*
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
import { getOrCreate } from '../core/guards.js';
|
|
2
|
+
import { scoped } from '../core/scope.js';
|
|
3
|
+
import { errorMessage } from '../wire/errors.js';
|
|
1
4
|
import { endpointId } from './client.js';
|
|
2
|
-
import { errorMessage } from './errors.js';
|
|
3
|
-
import { getOrCreate } from './guards.js';
|
|
4
5
|
import { isGrammarError } from './schema-compat.js';
|
|
5
|
-
import { scoped } from './scope.js';
|
|
6
6
|
/**
|
|
7
7
|
* A model's on/off flags as it starts out, every one optimistic.
|
|
8
8
|
*
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import OpenAI from 'openai';
|
|
2
|
-
import type { Endpoint, EndpointIdentity, RetryPolicy } from '
|
|
2
|
+
import type { Endpoint, EndpointIdentity, RetryPolicy } from '../core/config.ts';
|
|
3
3
|
/**
|
|
4
4
|
* The SDK insists on a non-empty key even where the server will not look at it. This is what it
|
|
5
5
|
* gets. It also reads as "this endpoint has no key" at a call site, which an empty string does
|
|
@@ -36,6 +36,11 @@ export interface ClientPoolOptions {
|
|
|
36
36
|
* missing route — is taken at its word before the endpoint is asked again.
|
|
37
37
|
*/
|
|
38
38
|
listingMissMs?: number;
|
|
39
|
+
/**
|
|
40
|
+
* How long, in milliseconds, one `contextLimitFor`, `servedWindow` or `listModels` may take over
|
|
41
|
+
* all of its requests. An endpoint's own request timeout still applies where it is shorter.
|
|
42
|
+
*/
|
|
43
|
+
lookupTimeoutMs?: number;
|
|
39
44
|
}
|
|
40
45
|
/**
|
|
41
46
|
* Changes what the client pool is held to, for a process whose endpoints are not shaped like the
|
|
@@ -43,8 +48,8 @@ export interface ClientPoolOptions {
|
|
|
43
48
|
*
|
|
44
49
|
* @param [options] - The bounds to change. A field left out — or given anything that is not a number
|
|
45
50
|
* above zero — keeps what it has, so a half-built config narrows nothing. `Infinity` is a number
|
|
46
|
-
* above zero: as `maxClients` it lifts the bound,
|
|
47
|
-
*
|
|
51
|
+
* above zero: as `maxClients` it lifts the bound, as `listingMissMs` a miss is never asked about
|
|
52
|
+
* again until `resetClients`, and as `lookupTimeoutMs` a lookup waits out the request timeout.
|
|
48
53
|
* @returns Everything in force afterwards, including what this call did not change.
|
|
49
54
|
*
|
|
50
55
|
* @remarks
|
|
@@ -145,13 +150,19 @@ export declare const modelKey: (endpoint: string, model: string) => string;
|
|
|
145
150
|
* is how a host finds its own endpoint's entry in a `CapabilitySnapshot`.
|
|
146
151
|
*/
|
|
147
152
|
export declare const endpointId: (config: EndpointIdentity) => string;
|
|
153
|
+
/** What a lookup takes besides the endpoint: `listModels`, `servedWindow` and `contextLimitFor`. */
|
|
154
|
+
export interface LookupOptions {
|
|
155
|
+
/** Abandons the lookup, which then rejects with the signal's reason and remembers nothing. */
|
|
156
|
+
signal?: AbortSignal;
|
|
157
|
+
}
|
|
148
158
|
/**
|
|
149
159
|
* The window a local server's own API says it is serving a model in, for the servers whose
|
|
150
160
|
* listing does not say.
|
|
151
161
|
*
|
|
152
162
|
* @param config - The endpoint, plus the model whose window is wanted.
|
|
153
|
-
* @
|
|
154
|
-
*
|
|
163
|
+
* @param [options] - Cancellation. See `LookupOptions`.
|
|
164
|
+
* @returns The window in tokens, or zero where none was found. Rejects only with the reason of
|
|
165
|
+
* `options.signal`: a server that could not be reached answers zero too.
|
|
155
166
|
*
|
|
156
167
|
* @remarks
|
|
157
168
|
* llama.cpp reports it on `/props` as `default_generation_settings.n_ctx`, per slot, and LM
|
|
@@ -169,21 +180,26 @@ export declare const endpointId: (config: EndpointIdentity) => string;
|
|
|
169
180
|
*/
|
|
170
181
|
export declare function servedWindow(config: Endpoint & {
|
|
171
182
|
model: string;
|
|
172
|
-
}): Promise<number>;
|
|
183
|
+
}, { signal }?: LookupOptions): Promise<number>;
|
|
173
184
|
/**
|
|
174
185
|
* Asks an endpoint what it serves, and remembers the answer.
|
|
175
186
|
*
|
|
176
187
|
* @param config - The endpoint to ask. Remembered per base URL and key, not per model.
|
|
177
|
-
* @
|
|
188
|
+
* @param [options] - Cancellation. See `LookupOptions`.
|
|
189
|
+
* @returns The models in order of id, whatever order the endpoint listed them in. Rejects with the
|
|
190
|
+
* reason of `options.signal` where that stopped it, a `TimeoutError` at `lookupTimeoutMs`, and the
|
|
191
|
+
* request's own error otherwise.
|
|
178
192
|
*/
|
|
179
|
-
export declare function listModels(config: Endpoint): Promise<ModelInfo[]>;
|
|
193
|
+
export declare function listModels(config: Endpoint, { signal }?: LookupOptions): Promise<ModelInfo[]>;
|
|
180
194
|
/**
|
|
181
195
|
* How much a model will read, in tokens. Zero means nobody knows.
|
|
182
196
|
*
|
|
183
197
|
* @param config - The endpoint, plus the model whose window is wanted.
|
|
184
198
|
* @param [declared] - The operator's own number. Above zero it wins and the endpoint is not asked.
|
|
185
|
-
* @
|
|
186
|
-
*
|
|
199
|
+
* @param [options] - Cancellation. See `LookupOptions`.
|
|
200
|
+
* @returns The window in tokens, or zero. Rejects only with the reason of `options.signal`: a
|
|
201
|
+
* listing or a native route that failed, or the lookup running out of `lookupTimeoutMs`, is an
|
|
202
|
+
* unknown window.
|
|
187
203
|
*
|
|
188
204
|
* @remarks
|
|
189
205
|
* `declared` is the operator's own number, and it wins outright: an endpoint can report the
|
|
@@ -206,7 +222,7 @@ export declare function listModels(config: Endpoint): Promise<ModelInfo[]>;
|
|
|
206
222
|
*/
|
|
207
223
|
export declare function contextLimitFor(config: Endpoint & {
|
|
208
224
|
model: string;
|
|
209
|
-
}, declared?: number): Promise<number>;
|
|
225
|
+
}, declared?: number, { signal }?: LookupOptions): Promise<number>;
|
|
210
226
|
/**
|
|
211
227
|
* Forgets every cached client and listing, and puts `configureClients` back to the defaults. For
|
|
212
228
|
* tests, and for a settings change under test.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import OpenAI from 'openai';
|
|
2
|
-
import { digestOf } from '
|
|
3
|
-
import { byCodeUnit, isPositive } from '
|
|
4
|
-
import { MS_PER_SECOND } from '
|
|
5
|
-
import { assignSettings, scoped } from '
|
|
6
|
-
import { HttpStatus } from '
|
|
2
|
+
import { digestOf } from '../core/digest.js';
|
|
3
|
+
import { byCodeUnit, isPositive } from '../core/guards.js';
|
|
4
|
+
import { MS_PER_SECOND } from '../core/platform.js';
|
|
5
|
+
import { assignSettings, scoped } from '../core/scope.js';
|
|
6
|
+
import { HttpStatus } from '../wire/wire.js';
|
|
7
7
|
/**
|
|
8
8
|
* The SDK insists on a non-empty key even where the server will not look at it. This is what it
|
|
9
9
|
* gets. It also reads as "this endpoint has no key" at a call site, which an empty string does
|
|
@@ -73,10 +73,19 @@ const clients = scoped(() => new Map());
|
|
|
73
73
|
const MAX_CLIENTS = 32;
|
|
74
74
|
/** How long a model an endpoint did not list stays unlisted before it is asked about again. */
|
|
75
75
|
const LISTING_MISS_MS = 30_000;
|
|
76
|
+
/**
|
|
77
|
+
* How long a window lookup may take in all, until `configureClients` moves it.
|
|
78
|
+
*
|
|
79
|
+
* @remarks
|
|
80
|
+
* Its own number because `requestTimeoutSeconds` is how long to wait for a model to speak, and a
|
|
81
|
+
* listing that has not answered in ten seconds is not going to.
|
|
82
|
+
*/
|
|
83
|
+
const LOOKUP_TIMEOUT_MS = 10_000;
|
|
76
84
|
/** The numbers this module was written with. */
|
|
77
85
|
const CLIENT_DEFAULTS = {
|
|
78
86
|
maxClients: MAX_CLIENTS,
|
|
79
87
|
listingMissMs: LISTING_MISS_MS,
|
|
88
|
+
lookupTimeoutMs: LOOKUP_TIMEOUT_MS,
|
|
80
89
|
};
|
|
81
90
|
/** What is in force now. Read where it is used, so a change applies from the next call. */
|
|
82
91
|
const poolLimits = scoped(() => ({ ...CLIENT_DEFAULTS }));
|
|
@@ -100,8 +109,8 @@ const evict = () => {
|
|
|
100
109
|
*
|
|
101
110
|
* @param [options] - The bounds to change. A field left out — or given anything that is not a number
|
|
102
111
|
* above zero — keeps what it has, so a half-built config narrows nothing. `Infinity` is a number
|
|
103
|
-
* above zero: as `maxClients` it lifts the bound,
|
|
104
|
-
*
|
|
112
|
+
* above zero: as `maxClients` it lifts the bound, as `listingMissMs` a miss is never asked about
|
|
113
|
+
* again until `resetClients`, and as `lookupTimeoutMs` a lookup waits out the request timeout.
|
|
105
114
|
* @returns Everything in force afterwards, including what this call did not change.
|
|
106
115
|
*
|
|
107
116
|
* @remarks
|
|
@@ -339,8 +348,20 @@ const refused = (endpoint) => {
|
|
|
339
348
|
const refusal = refusals().get(endpoint);
|
|
340
349
|
return refusal !== undefined && (refusal.gone || Date.now() - refusal.at < poolLimits().listingMissMs);
|
|
341
350
|
};
|
|
342
|
-
/**
|
|
343
|
-
|
|
351
|
+
/**
|
|
352
|
+
* The signal every request of one lookup is made under, so the lookup is bounded as a whole.
|
|
353
|
+
*
|
|
354
|
+
* @param config - Read for `requestTimeoutSeconds`, which bounds the lookup where it is shorter
|
|
355
|
+
* than `lookupTimeoutMs`.
|
|
356
|
+
* @param [signal] - The caller's own, which aborts the lookup with its reason.
|
|
357
|
+
* @returns A signal that fires with a `TimeoutError` at the bound or with the caller's reason,
|
|
358
|
+
* whichever is first. Never fires where neither is set.
|
|
359
|
+
*/
|
|
360
|
+
const lookupSignal = (config, signal) => {
|
|
361
|
+
const boundMs = Math.min(timeoutMs(config) ?? Number.POSITIVE_INFINITY, poolLimits().lookupTimeoutMs);
|
|
362
|
+
const bound = Number.isFinite(boundMs) ? [AbortSignal.timeout(boundMs)] : [];
|
|
363
|
+
return AbortSignal.any([...bound, ...(signal ? [signal] : [])]);
|
|
364
|
+
};
|
|
344
365
|
/**
|
|
345
366
|
* The server root behind an OpenAI-compatible base URL, which is where the native APIs live.
|
|
346
367
|
*
|
|
@@ -358,9 +379,9 @@ const NOT_THERE = new Set([
|
|
|
358
379
|
/**
|
|
359
380
|
* Asks one native endpoint.
|
|
360
381
|
*
|
|
361
|
-
* @param config - The endpoint. Its key goes as a bearer token where it has one
|
|
362
|
-
* timeout bounds the wait — `PROBE_TIMEOUT_MS` where it has none.
|
|
382
|
+
* @param config - The endpoint. Its key goes as a bearer token where it has one.
|
|
363
383
|
* @param path - The route and its query, from the server root rather than from the base URL.
|
|
384
|
+
* @param within - The lookup's signal. Fired, the request throws as an unreachable server does.
|
|
364
385
|
* @returns How the server answered, with `body` only for a 2xx that parsed as JSON.
|
|
365
386
|
*
|
|
366
387
|
* @remarks
|
|
@@ -368,11 +389,11 @@ const NOT_THERE = new Set([
|
|
|
368
389
|
* without the route; `body` is absent for both, and for a 2xx that was not JSON. A server that
|
|
369
390
|
* could not be reached throws.
|
|
370
391
|
*/
|
|
371
|
-
async function probe(config, path) {
|
|
392
|
+
async function probe(config, path, within) {
|
|
372
393
|
const apiKey = config.apiKey || undefined;
|
|
373
394
|
const response = await fetch(`${rootOf(config.baseUrl)}${path}`, {
|
|
374
395
|
headers: apiKey ? { authorization: `Bearer ${apiKey}` } : {},
|
|
375
|
-
signal:
|
|
396
|
+
signal: within,
|
|
376
397
|
});
|
|
377
398
|
if (!response.ok) {
|
|
378
399
|
return { failed: true, missing: NOT_THERE.has(response.status) };
|
|
@@ -389,8 +410,9 @@ async function probe(config, path) {
|
|
|
389
410
|
* listing does not say.
|
|
390
411
|
*
|
|
391
412
|
* @param config - The endpoint, plus the model whose window is wanted.
|
|
392
|
-
* @
|
|
393
|
-
*
|
|
413
|
+
* @param [options] - Cancellation. See `LookupOptions`.
|
|
414
|
+
* @returns The window in tokens, or zero where none was found. Rejects only with the reason of
|
|
415
|
+
* `options.signal`: a server that could not be reached answers zero too.
|
|
394
416
|
*
|
|
395
417
|
* @remarks
|
|
396
418
|
* llama.cpp reports it on `/props` as `default_generation_settings.n_ctx`, per slot, and LM
|
|
@@ -406,7 +428,19 @@ async function probe(config, path) {
|
|
|
406
428
|
*
|
|
407
429
|
* The listing is not read here; `contextLimitFor` reads it first and comes here after.
|
|
408
430
|
*/
|
|
409
|
-
export async function servedWindow(config) {
|
|
431
|
+
export async function servedWindow(config, { signal } = {}) {
|
|
432
|
+
const window = await readServed(config, lookupSignal(config, signal));
|
|
433
|
+
signal?.throwIfAborted();
|
|
434
|
+
return window;
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* `servedWindow` under a lookup's signal, which `contextLimitFor` shares with its listing.
|
|
438
|
+
*
|
|
439
|
+
* @param config - The endpoint, plus the model whose window is wanted.
|
|
440
|
+
* @param within - The lookup's signal. Fired, the answer is zero and nothing is remembered.
|
|
441
|
+
* @returns The window in tokens, or zero where none was found. Never rejects.
|
|
442
|
+
*/
|
|
443
|
+
async function readServed(config, within) {
|
|
410
444
|
const endpoint = endpointKey(config);
|
|
411
445
|
if (unserved().has(endpoint)) {
|
|
412
446
|
return 0;
|
|
@@ -419,12 +453,12 @@ export async function servedWindow(config) {
|
|
|
419
453
|
let window = 0;
|
|
420
454
|
try {
|
|
421
455
|
// Named, for a llama.cpp router serving several models; a single-model server ignores it.
|
|
422
|
-
const props = await probe(config, `/props?model=${encodeURIComponent(config.model)}
|
|
456
|
+
const props = await probe(config, `/props?model=${encodeURIComponent(config.model)}`, within);
|
|
423
457
|
const settings = props.body?.default_generation_settings;
|
|
424
458
|
window = positive(settings?.n_ctx);
|
|
425
459
|
if (!window) {
|
|
426
460
|
if (!refused(endpoint)) {
|
|
427
|
-
const lmstudio = await probe(config, '/api/v0/models');
|
|
461
|
+
const lmstudio = await probe(config, '/api/v0/models', within);
|
|
428
462
|
if (lmstudio.failed) {
|
|
429
463
|
refusals().set(endpoint, { at: Date.now(), gone: lmstudio.missing });
|
|
430
464
|
}
|
|
@@ -453,10 +487,29 @@ export async function servedWindow(config) {
|
|
|
453
487
|
* Asks an endpoint what it serves, and remembers the answer.
|
|
454
488
|
*
|
|
455
489
|
* @param config - The endpoint to ask. Remembered per base URL and key, not per model.
|
|
456
|
-
* @
|
|
490
|
+
* @param [options] - Cancellation. See `LookupOptions`.
|
|
491
|
+
* @returns The models in order of id, whatever order the endpoint listed them in. Rejects with the
|
|
492
|
+
* reason of `options.signal` where that stopped it, a `TimeoutError` at `lookupTimeoutMs`, and the
|
|
493
|
+
* request's own error otherwise.
|
|
457
494
|
*/
|
|
458
|
-
export
|
|
459
|
-
|
|
495
|
+
export function listModels(config, { signal } = {}) {
|
|
496
|
+
return readListing(config, lookupSignal(config, signal));
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* `listModels` under a lookup's signal, which `contextLimitFor` shares with the native routes.
|
|
500
|
+
*
|
|
501
|
+
* @param config - The endpoint to ask.
|
|
502
|
+
* @param within - The lookup's signal. Fired, the listing rejects with its reason.
|
|
503
|
+
* @returns The models in order of id. A listing that fails leaves the last one remembered.
|
|
504
|
+
*/
|
|
505
|
+
async function readListing(config, within) {
|
|
506
|
+
const { data } = await getClient(config)
|
|
507
|
+
.models.list({ signal: within })
|
|
508
|
+
.catch((error) => {
|
|
509
|
+
// The SDK throws an abort error of its own, whatever the signal's reason was.
|
|
510
|
+
within.throwIfAborted();
|
|
511
|
+
throw error;
|
|
512
|
+
});
|
|
460
513
|
const trained = new Set();
|
|
461
514
|
const models = [];
|
|
462
515
|
for (const model of data) {
|
|
@@ -477,8 +530,10 @@ export async function listModels(config) {
|
|
|
477
530
|
*
|
|
478
531
|
* @param config - The endpoint, plus the model whose window is wanted.
|
|
479
532
|
* @param [declared] - The operator's own number. Above zero it wins and the endpoint is not asked.
|
|
480
|
-
* @
|
|
481
|
-
*
|
|
533
|
+
* @param [options] - Cancellation. See `LookupOptions`.
|
|
534
|
+
* @returns The window in tokens, or zero. Rejects only with the reason of `options.signal`: a
|
|
535
|
+
* listing or a native route that failed, or the lookup running out of `lookupTimeoutMs`, is an
|
|
536
|
+
* unknown window.
|
|
482
537
|
*
|
|
483
538
|
* @remarks
|
|
484
539
|
* `declared` is the operator's own number, and it wins outright: an endpoint can report the
|
|
@@ -499,11 +554,13 @@ export async function listModels(config) {
|
|
|
499
554
|
* first, they were also asked again every `listingMissMs` for the life of the process on a server
|
|
500
555
|
* whose `/props` answers without a window, by any host that sizes the window per turn.
|
|
501
556
|
*/
|
|
502
|
-
export async function contextLimitFor(config, declared = 0) {
|
|
557
|
+
export async function contextLimitFor(config, declared = 0, { signal } = {}) {
|
|
503
558
|
if (declared > 0) {
|
|
504
559
|
return declared;
|
|
505
560
|
}
|
|
506
561
|
const key = endpointKey(config);
|
|
562
|
+
// One signal for the listing and the native routes, so three silent requests wait once.
|
|
563
|
+
const within = lookupSignal(config, signal);
|
|
507
564
|
const listed = () => listings()
|
|
508
565
|
.get(key)
|
|
509
566
|
?.find((model) => model.id === config.model);
|
|
@@ -523,7 +580,8 @@ export async function contextLimitFor(config, declared = 0) {
|
|
|
523
580
|
if (asked === undefined || Date.now() - asked >= poolLimits().listingMissMs) {
|
|
524
581
|
// A failure is not remembered: an endpoint that was down when the last run started is not
|
|
525
582
|
// an endpoint with no models, and a window nobody could ask about is not a failed run.
|
|
526
|
-
const answered = await
|
|
583
|
+
const answered = await readListing(config, within).then(() => true, () => false);
|
|
584
|
+
signal?.throwIfAborted();
|
|
527
585
|
// Recorded where the endpoint was actually asked, and only there. Stamping it on the calls
|
|
528
586
|
// that skipped the request would push the deadline out ahead of any caller polling faster
|
|
529
587
|
// than the interval, which is a memory that never expires rather than one that expires in
|
|
@@ -545,7 +603,8 @@ export async function contextLimitFor(config, declared = 0) {
|
|
|
545
603
|
if (fromListing > 0 && !trainedOnly().get(key)?.has(config.model)) {
|
|
546
604
|
return fromListing;
|
|
547
605
|
}
|
|
548
|
-
const window = await
|
|
606
|
+
const window = await readServed(config, within);
|
|
607
|
+
signal?.throwIfAborted();
|
|
549
608
|
return window > 0 ? window : fromListing;
|
|
550
609
|
}
|
|
551
610
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { getOrCreate, isRecord } from '
|
|
2
|
-
import { FUNCTION_TOOL, SchemaType } from '
|
|
1
|
+
import { getOrCreate, isRecord } from '../core/guards.js';
|
|
2
|
+
import { FUNCTION_TOOL, SchemaType } from '../wire/wire.js';
|
|
3
3
|
/** Lookahead and lookbehind: `(?=`, `(?!`, `(?<=`, `(?<!`. */
|
|
4
4
|
const LOOKAROUND = /\(\?<?[=!]/;
|
|
5
5
|
const PRIMITIVES = new Set(Object.values(SchemaType));
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { digestOf } from '
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { digestOf } from '../core/digest.js';
|
|
2
|
+
import { isPositive } from '../core/guards.js';
|
|
3
|
+
import { assignSettings, scoped } from '../core/scope.js';
|
|
4
|
+
import { errorMessage } from '../wire/errors.js';
|
|
5
|
+
import { CHARS_PER_TOKEN, estimateTokens } from '../wire/tokens.js';
|
|
6
|
+
import { PartType, Role } from '../wire/wire.js';
|
|
4
7
|
import { INJECT_EVENTS } from './hook-events.js';
|
|
5
|
-
import { assignSettings, scoped } from './scope.js';
|
|
6
|
-
import { CHARS_PER_TOKEN, estimateTokens } from './tokens.js';
|
|
7
|
-
import { PartType, Role } from './wire.js';
|
|
8
8
|
/**
|
|
9
9
|
* The most context all of a request's hooks add between them by default, in estimated tokens.
|
|
10
10
|
*
|