@providerkit/core 0.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/LICENSE +21 -0
- package/README.md +245 -0
- package/dist/context.d.ts +69 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +132 -0
- package/dist/context.js.map +1 -0
- package/dist/errors.d.ts +86 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +356 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/providers/anthropic.d.ts +26 -0
- package/dist/providers/anthropic.d.ts.map +1 -0
- package/dist/providers/anthropic.js +245 -0
- package/dist/providers/anthropic.js.map +1 -0
- package/dist/providers/openai.d.ts +30 -0
- package/dist/providers/openai.d.ts.map +1 -0
- package/dist/providers/openai.js +185 -0
- package/dist/providers/openai.js.map +1 -0
- package/dist/retry.d.ts +79 -0
- package/dist/retry.d.ts.map +1 -0
- package/dist/retry.js +200 -0
- package/dist/retry.js.map +1 -0
- package/dist/schema.d.ts +2 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +48 -0
- package/dist/schema.js.map +1 -0
- package/dist/tool-args.d.ts +12 -0
- package/dist/tool-args.d.ts.map +1 -0
- package/dist/tool-args.js +113 -0
- package/dist/tool-args.js.map +1 -0
- package/dist/tools.d.ts +82 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +155 -0
- package/dist/tools.js.map +1 -0
- package/dist/transport.d.ts +31 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +157 -0
- package/dist/transport.js.map +1 -0
- package/dist/types.d.ts +168 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +75 -0
- package/dist/types.js.map +1 -0
- package/dist/usage.d.ts +50 -0
- package/dist/usage.d.ts.map +1 -0
- package/dist/usage.js +71 -0
- package/dist/usage.js.map +1 -0
- package/dist/watchdog.d.ts +34 -0
- package/dist/watchdog.d.ts.map +1 -0
- package/dist/watchdog.js +85 -0
- package/dist/watchdog.js.map +1 -0
- package/dist/zod.d.ts +32 -0
- package/dist/zod.d.ts.map +1 -0
- package/dist/zod.js +49 -0
- package/dist/zod.js.map +1 -0
- package/package.json +76 -0
- package/src/context.ts +150 -0
- package/src/errors.ts +398 -0
- package/src/index.ts +12 -0
- package/src/providers/anthropic.ts +315 -0
- package/src/providers/openai.ts +246 -0
- package/src/retry.ts +246 -0
- package/src/schema.ts +67 -0
- package/src/tool-args.ts +117 -0
- package/src/tools.ts +237 -0
- package/src/transport.ts +162 -0
- package/src/types.ts +231 -0
- package/src/usage.ts +106 -0
- package/src/watchdog.ts +119 -0
- package/src/zod.ts +74 -0
package/dist/watchdog.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// The stream-idle watchdog.
|
|
2
|
+
//
|
|
3
|
+
// A provider that stops sending bytes is indistinguishable from a long prefill
|
|
4
|
+
// — except that it never ends, and every SDK's default is to wait forever. A
|
|
5
|
+
// queued route or a wedged prefill upstream hangs the caller indefinitely, and
|
|
6
|
+
// the symptom is the worst kind: nothing. No error, no log, no timeout.
|
|
7
|
+
//
|
|
8
|
+
// So the seam gives itself a deadline. Any byte of any kind re-arms it
|
|
9
|
+
// (reasoning models emit thinking deltas continuously, so silence really is
|
|
10
|
+
// silence). When it fires, the watchdog aborts ITS OWN controller and the
|
|
11
|
+
// caller's signal is only bridged in — which is what keeps a person's Stop
|
|
12
|
+
// distinguishable from our timeout. One is their cancel and is never retried;
|
|
13
|
+
// the other is ours, is transient, and fires while nothing has streamed yet,
|
|
14
|
+
// so the retry is always safe.
|
|
15
|
+
import { ProviderError } from "./errors.js";
|
|
16
|
+
/** No byte at all for this long and the stream is considered wedged. */
|
|
17
|
+
export const STREAM_IDLE_MS = 60_000;
|
|
18
|
+
export function streamWatch(opts = {}) {
|
|
19
|
+
const provider = opts.provider ?? "provider";
|
|
20
|
+
const idleMs = opts.idleMs ?? STREAM_IDLE_MS;
|
|
21
|
+
const callerSignal = opts.signal;
|
|
22
|
+
const started = Date.now();
|
|
23
|
+
const timeout = new AbortController();
|
|
24
|
+
let firstChunk = null;
|
|
25
|
+
let idle = false;
|
|
26
|
+
let disposed = false;
|
|
27
|
+
// The bridge is structural rather than an event listener: AbortSignal.any
|
|
28
|
+
// aborts synchronously when an input is ALREADY aborted, which is the race
|
|
29
|
+
// no listener can catch (the event fired before we subscribed).
|
|
30
|
+
const signal = callerSignal ? AbortSignal.any([callerSignal, timeout.signal]) : timeout.signal;
|
|
31
|
+
const idleError = (cause) => new ProviderError(provider, "timeout", `stream went ${idleMs / 1000}s without a byte`, {
|
|
32
|
+
cause,
|
|
33
|
+
});
|
|
34
|
+
function arm() {
|
|
35
|
+
const timer = setTimeout(() => {
|
|
36
|
+
idle = true;
|
|
37
|
+
timeout.abort(idleError());
|
|
38
|
+
}, idleMs);
|
|
39
|
+
// An orphaned watch — its consumer gone, dispose never called — must not
|
|
40
|
+
// hold a Node event loop open for a full deadline.
|
|
41
|
+
timer.unref?.();
|
|
42
|
+
return timer;
|
|
43
|
+
}
|
|
44
|
+
let timer = arm();
|
|
45
|
+
return {
|
|
46
|
+
signal,
|
|
47
|
+
sawByte() {
|
|
48
|
+
firstChunk ??= Date.now() - started;
|
|
49
|
+
clearTimeout(timer);
|
|
50
|
+
if (!disposed && !signal.aborted)
|
|
51
|
+
timer = arm();
|
|
52
|
+
},
|
|
53
|
+
firstChunkMs: () => firstChunk,
|
|
54
|
+
classify(err) {
|
|
55
|
+
// Our deadline, not theirs — and not the caller's Stop.
|
|
56
|
+
if (idle && !(callerSignal?.aborted ?? false))
|
|
57
|
+
return idleError(err);
|
|
58
|
+
return err;
|
|
59
|
+
},
|
|
60
|
+
dispose() {
|
|
61
|
+
disposed = true;
|
|
62
|
+
clearTimeout(timer);
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Wrap a stream so every chunk re-arms `watch`, and a failure is re-classified
|
|
68
|
+
* through it. Disposes on any exit — completion, throw, or the consumer
|
|
69
|
+
* breaking out of the loop.
|
|
70
|
+
*/
|
|
71
|
+
export async function* watchChunks(watch, chunks) {
|
|
72
|
+
try {
|
|
73
|
+
for await (const chunk of chunks) {
|
|
74
|
+
watch.sawByte();
|
|
75
|
+
yield chunk;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
throw watch.classify(err);
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
watch.dispose();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=watchdog.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"watchdog.js","sourceRoot":"","sources":["../src/watchdog.ts"],"names":[],"mappings":"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"}
|
package/dist/zod.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { type Tool, type ToolContext } from "./tools.ts";
|
|
3
|
+
import type { JsonObjectSchema } from "./types.ts";
|
|
4
|
+
/** A zod schema as the JSON Schema every provider's tool contract wants. */
|
|
5
|
+
export declare function toJsonObjectSchema(schema: z.ZodType, label?: string): JsonObjectSchema;
|
|
6
|
+
export interface ZodToolSpec<I, O> {
|
|
7
|
+
name: string;
|
|
8
|
+
description: string;
|
|
9
|
+
input: z.ZodType<I>;
|
|
10
|
+
run: (input: I, ctx: ToolContext) => Promise<O>;
|
|
11
|
+
summarize?: (output: O) => string;
|
|
12
|
+
timeoutMs?: number;
|
|
13
|
+
isReadOnly?: boolean;
|
|
14
|
+
needsApproval?: boolean;
|
|
15
|
+
isConcurrencySafe?: boolean;
|
|
16
|
+
isTerminal?: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Clamp overflow to the bounds the schema already advertised instead of
|
|
19
|
+
* rejecting the call. Worth it for a TERMINAL tool, which gets no second
|
|
20
|
+
* chance: a forced-submit salvage turn runs exactly once, and discarding an
|
|
21
|
+
* otherwise-valid answer over a few extra characters loses the whole run.
|
|
22
|
+
* Off by default — an ordinary tool can simply be called again.
|
|
23
|
+
*/
|
|
24
|
+
clampOverflow?: boolean;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* A tool whose arguments are validated by zod, with the failure reported to
|
|
28
|
+
* the MODEL in words it can act on — `topic: expected string, received number`
|
|
29
|
+
* beats a stack trace it cannot read.
|
|
30
|
+
*/
|
|
31
|
+
export declare function zodTool<I, O>(spec: ZodToolSpec<I, O>): Tool<I, O>;
|
|
32
|
+
//# sourceMappingURL=zod.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"zod.d.ts","sourceRoot":"","sources":["../src/zod.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAc,KAAK,IAAI,EAAE,KAAK,WAAW,EAAE,MAAM,YAAY,CAAC;AAErE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEnD,4EAA4E;AAC5E,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,CAAC,CAAC,OAAO,EAAE,KAAK,SAAW,GAAG,gBAAgB,CASxF;AAED,MAAM,WAAW,WAAW,CAAC,CAAC,EAAE,CAAC;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;IACpB,GAAG,EAAE,CAAC,KAAK,EAAE,CAAC,EAAE,GAAG,EAAE,WAAW,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IAChD,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,MAAM,CAAC;IAClC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;;;OAMG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED;;;;GAIG;AACH,wBAAgB,OAAO,CAAC,CAAC,EAAE,CAAC,EAAE,IAAI,EAAE,WAAW,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,CAyBjE"}
|
package/dist/zod.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// The optional zod ergonomics — `@providerkit/core/zod`.
|
|
2
|
+
//
|
|
3
|
+
// Kept behind its own entry point so the core stays dependency-free for
|
|
4
|
+
// consumers that have no zod (a browser extension counting every byte, for
|
|
5
|
+
// one). Import this and tools become typed end to end.
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { defineTool } from "./tools.js";
|
|
8
|
+
import { clampToSchema } from "./schema.js";
|
|
9
|
+
/** A zod schema as the JSON Schema every provider's tool contract wants. */
|
|
10
|
+
export function toJsonObjectSchema(schema, label = "schema") {
|
|
11
|
+
const json = z.toJSONSchema(schema, { io: "input" });
|
|
12
|
+
if (json.type !== "object") {
|
|
13
|
+
// Every provider requires an object at the top level of a tool's
|
|
14
|
+
// parameters; a bare string or array is rejected at the wire, far from
|
|
15
|
+
// here, with a message that names none of this.
|
|
16
|
+
throw new Error(`providerkit: ${label} must be an object schema, got ${String(json.type)}`);
|
|
17
|
+
}
|
|
18
|
+
return json;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* A tool whose arguments are validated by zod, with the failure reported to
|
|
22
|
+
* the MODEL in words it can act on — `topic: expected string, received number`
|
|
23
|
+
* beats a stack trace it cannot read.
|
|
24
|
+
*/
|
|
25
|
+
export function zodTool(spec) {
|
|
26
|
+
const inputSchema = toJsonObjectSchema(spec.input, `Tool "${spec.name}" input`);
|
|
27
|
+
return defineTool({
|
|
28
|
+
name: spec.name,
|
|
29
|
+
description: spec.description,
|
|
30
|
+
inputSchema,
|
|
31
|
+
...(spec.summarize ? { summarize: spec.summarize } : {}),
|
|
32
|
+
...(spec.timeoutMs !== undefined ? { timeoutMs: spec.timeoutMs } : {}),
|
|
33
|
+
...(spec.isReadOnly !== undefined ? { isReadOnly: spec.isReadOnly } : {}),
|
|
34
|
+
...(spec.needsApproval !== undefined ? { needsApproval: spec.needsApproval } : {}),
|
|
35
|
+
...(spec.isConcurrencySafe !== undefined ? { isConcurrencySafe: spec.isConcurrencySafe } : {}),
|
|
36
|
+
...(spec.isTerminal !== undefined ? { isTerminal: spec.isTerminal } : {}),
|
|
37
|
+
validate: (raw) => {
|
|
38
|
+
const candidate = spec.clampOverflow ? clampToSchema(raw, inputSchema) : raw;
|
|
39
|
+
const parsed = spec.input.safeParse(candidate);
|
|
40
|
+
if (parsed.success)
|
|
41
|
+
return parsed.data;
|
|
42
|
+
throw new Error(parsed.error.issues
|
|
43
|
+
.map((issue) => `${issue.path.join(".") || "input"}: ${issue.message}`)
|
|
44
|
+
.join("; "));
|
|
45
|
+
},
|
|
46
|
+
run: spec.run,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=zod.js.map
|
package/dist/zod.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"zod.js","sourceRoot":"","sources":["../src/zod.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@providerkit/core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "The layer under your agent loop: one seam for every LLM provider, plus the failure handling you only learn in production.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Gustavo Salom\u00e9",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./zod": {
|
|
14
|
+
"types": "./dist/zod.d.ts",
|
|
15
|
+
"default": "./dist/zod.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"src",
|
|
22
|
+
"README.md",
|
|
23
|
+
"LICENSE"
|
|
24
|
+
],
|
|
25
|
+
"sideEffects": false,
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"llm",
|
|
31
|
+
"openai",
|
|
32
|
+
"anthropic",
|
|
33
|
+
"gemini",
|
|
34
|
+
"agent",
|
|
35
|
+
"streaming",
|
|
36
|
+
"sse",
|
|
37
|
+
"retry",
|
|
38
|
+
"tool-calling"
|
|
39
|
+
],
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsc",
|
|
42
|
+
"typecheck": "tsc --noEmit",
|
|
43
|
+
"test": "vitest run",
|
|
44
|
+
"test:watch": "vitest",
|
|
45
|
+
"lint": "eslint src test",
|
|
46
|
+
"format": "prettier --write .",
|
|
47
|
+
"sync:docs": "cp ../README.md ../LICENSE .",
|
|
48
|
+
"prepublishOnly": "bun run lint && bun run typecheck && bun run test && bun run build && bun run sync:docs",
|
|
49
|
+
"release": "bun publish --access public",
|
|
50
|
+
"release:patch": "bun pm version patch && bun publish --access public",
|
|
51
|
+
"release:minor": "bun pm version minor && bun publish --access public",
|
|
52
|
+
"release:major": "bun pm version major && bun publish --access public",
|
|
53
|
+
"release:alpha": "bun publish --access public --tag alpha"
|
|
54
|
+
},
|
|
55
|
+
"peerDependencies": {
|
|
56
|
+
"zod": "^4"
|
|
57
|
+
},
|
|
58
|
+
"peerDependenciesMeta": {
|
|
59
|
+
"zod": {
|
|
60
|
+
"optional": true
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
"devDependencies": {
|
|
64
|
+
"typescript": "^5.9.3",
|
|
65
|
+
"vitest": "^4.1.2",
|
|
66
|
+
"zod": "^4.3.6"
|
|
67
|
+
},
|
|
68
|
+
"repository": {
|
|
69
|
+
"type": "git",
|
|
70
|
+
"url": "git+https://github.com/providerkit/providerkit.git"
|
|
71
|
+
},
|
|
72
|
+
"homepage": "https://providerkit.dev",
|
|
73
|
+
"bugs": {
|
|
74
|
+
"url": "https://github.com/providerkit/providerkit/issues"
|
|
75
|
+
}
|
|
76
|
+
}
|
package/src/context.ts
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// Deciding when a conversation no longer fits, and where to cut it.
|
|
2
|
+
//
|
|
3
|
+
// Everything here is pure and testable. The model call that writes the summary
|
|
4
|
+
// and the row that stores it belong to the caller — what is decidable without
|
|
5
|
+
// I/O is decided here.
|
|
6
|
+
import type { ChatMessage } from "./types.ts";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* What the answer needs after the prompt: an output budget, plus the tools'
|
|
10
|
+
* own schemas, plus the slack no provider documents.
|
|
11
|
+
*
|
|
12
|
+
* Absolute rather than a percentage on purpose — a 1M window does not need a
|
|
13
|
+
* 100k cushion, and a 128k window needs more than 12k.
|
|
14
|
+
*/
|
|
15
|
+
export const CONTEXT_RESERVE_TOKENS = 32_000;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Four characters per token — the rule of thumb every provider's own
|
|
19
|
+
* calculator agrees with to within a fifth, which is all the precision this
|
|
20
|
+
* needs. It decides WHEN to fold: folding one turn early costs a cheap model
|
|
21
|
+
* call, folding one turn late costs the whole turn.
|
|
22
|
+
*/
|
|
23
|
+
export function estimateTokens(text: string): number {
|
|
24
|
+
return Math.ceil(text.length / 4);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function textOf(message: ChatMessage): string {
|
|
28
|
+
if (message.role === "user" && typeof message.content !== "string") {
|
|
29
|
+
// Only the words are countable; an image's cost is the provider's own
|
|
30
|
+
// arithmetic and no character count approximates it.
|
|
31
|
+
return message.content.map((part) => (part.type === "text" ? part.text : "")).join("");
|
|
32
|
+
}
|
|
33
|
+
return typeof message.content === "string" ? message.content : "";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** What one message costs to re-send: its words, its reasoning, and the
|
|
37
|
+
* arguments of any calls it made. */
|
|
38
|
+
export function messageTokens(message: ChatMessage): number {
|
|
39
|
+
const extra =
|
|
40
|
+
message.role === "assistant"
|
|
41
|
+
? (message.reasoning ?? "") +
|
|
42
|
+
(message.toolCalls?.map((call) => call.name + call.arguments).join("") ?? "")
|
|
43
|
+
: "";
|
|
44
|
+
return estimateTokens(textOf(message) + extra);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function conversationTokens(messages: readonly ChatMessage[]): number {
|
|
48
|
+
return messages.reduce((total, message) => total + messageTokens(message), 0);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The provider's own input count says the wall is close — fold before the next
|
|
53
|
+
* step rather than after the 400.
|
|
54
|
+
*
|
|
55
|
+
* Takes the REPORTED count, not an estimate, because it is the only figure
|
|
56
|
+
* that is not a guess. Estimate only when there is no reported count yet.
|
|
57
|
+
*/
|
|
58
|
+
export function needsCompaction(inputTokens: number, contextWindow: number): boolean {
|
|
59
|
+
return inputTokens >= contextWindow - CONTEXT_RESERVE_TOKENS;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* How much of the window the history BEHIND the current turn may spend: a
|
|
64
|
+
* tenth, floored so a small window still gets a usable memory. The tail stays
|
|
65
|
+
* verbatim and the summary is short, so the rest is what folding buys back.
|
|
66
|
+
*/
|
|
67
|
+
export function historyBudgetTokens(contextWindow: number): number {
|
|
68
|
+
return Math.max(6_000, Math.floor(contextWindow * 0.1));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Where to cut so the messages AFTER the cut fit `budget`, walking backwards
|
|
73
|
+
* from the newest.
|
|
74
|
+
*
|
|
75
|
+
* Two invariants the cut must respect, and both are correctness rather than
|
|
76
|
+
* taste:
|
|
77
|
+
*
|
|
78
|
+
* 1. Never split a tool call from its result. Every provider rejects a tool
|
|
79
|
+
* result whose call is missing, so a cut landing between them produces a
|
|
80
|
+
* 400 on the very next turn — the failure compaction was called to avoid.
|
|
81
|
+
* 2. Never cut into the system prompt. It is not history.
|
|
82
|
+
*
|
|
83
|
+
* Returns the index the kept tail starts at, or 0 when everything already fits.
|
|
84
|
+
*/
|
|
85
|
+
export function pickCut(messages: readonly ChatMessage[], budget: number): number {
|
|
86
|
+
const firstNonSystem = messages.findIndex((message) => message.role !== "system");
|
|
87
|
+
// Nothing but a system prompt: there is no history to fold.
|
|
88
|
+
if (firstNonSystem === -1) return messages.length;
|
|
89
|
+
|
|
90
|
+
// The newest message is always kept, budget or not. A conversation whose
|
|
91
|
+
// latest turn alone overruns the budget is not fixable by cutting history,
|
|
92
|
+
// and answering a summary instead of the question the person just asked is
|
|
93
|
+
// never the right failure.
|
|
94
|
+
const latest = messages.length - 1;
|
|
95
|
+
let cut = latest;
|
|
96
|
+
let total = messageTokens(messages[latest]!);
|
|
97
|
+
|
|
98
|
+
for (let i = latest - 1; i >= firstNonSystem; i--) {
|
|
99
|
+
total += messageTokens(messages[i]!);
|
|
100
|
+
if (total > budget) break;
|
|
101
|
+
cut = i;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Walk back off any tool result whose assistant turn would be left behind
|
|
105
|
+
// it. Bounded by the first non-system message, so it can never run off the
|
|
106
|
+
// front. This can exceed the budget by a message or two, which is the right
|
|
107
|
+
// trade: an orphaned tool result is a hard 400, not an overrun.
|
|
108
|
+
while (cut > firstNonSystem && messages[cut]!.role === "tool") cut--;
|
|
109
|
+
|
|
110
|
+
return cut;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Fold `messages` into `[…system, summary, …tail]`.
|
|
115
|
+
*
|
|
116
|
+
* The summary arrives as a user turn rather than a system one: a system
|
|
117
|
+
* message added mid-conversation reads to the model as a new instruction, and
|
|
118
|
+
* several providers require the system block to be first and singular anyway.
|
|
119
|
+
*/
|
|
120
|
+
export function applyCompaction(
|
|
121
|
+
messages: readonly ChatMessage[],
|
|
122
|
+
cut: number,
|
|
123
|
+
summary: string,
|
|
124
|
+
): ChatMessage[] {
|
|
125
|
+
const system = messages.filter((message) => message.role === "system");
|
|
126
|
+
const tail = messages.slice(cut).filter((message) => message.role !== "system");
|
|
127
|
+
return [
|
|
128
|
+
...system,
|
|
129
|
+
{ role: "user", content: `[Earlier conversation, summarized]\n\n${summary}` },
|
|
130
|
+
...tail,
|
|
131
|
+
];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* The context window for a model, when nothing volunteered one.
|
|
136
|
+
*
|
|
137
|
+
* The ladder exists because the endpoints disagree about whether to tell you:
|
|
138
|
+
* OpenRouter, LM Studio and Ollama publish `context_length`; Anthropic and
|
|
139
|
+
* OpenAI do not. A reported number always wins; this is the floor under it.
|
|
140
|
+
*
|
|
141
|
+
* A conservative default is the right failure: too small folds one turn early
|
|
142
|
+
* and costs a cheap call, too large hits a hard 400 mid-run.
|
|
143
|
+
*/
|
|
144
|
+
export function guessContextWindow(model: string): number {
|
|
145
|
+
const id = model.toLowerCase();
|
|
146
|
+
if (/gemini|gpt-4\.1|grok-4|llama-4/.test(id)) return 1_000_000;
|
|
147
|
+
if (/claude|gpt-5|o[34]|deepseek|kimi|glm|qwen/.test(id)) return 200_000;
|
|
148
|
+
if (/gpt-4o|mistral|command-r/.test(id)) return 128_000;
|
|
149
|
+
return 128_000;
|
|
150
|
+
}
|