@parcha/agentrun-kernel 0.0.0-bootstrap.0 → 0.1.0-beta.29

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.
Files changed (82) hide show
  1. package/CHANGELOG.md +135 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +6 -0
  4. package/README.md +53 -0
  5. package/dist/delegate.d.ts +87 -0
  6. package/dist/delegate.js +143 -0
  7. package/dist/errors.d.ts +36 -0
  8. package/dist/errors.js +37 -0
  9. package/dist/host/alive.d.ts +2 -0
  10. package/dist/host/alive.js +28 -0
  11. package/dist/host/box-slot.d.ts +26 -0
  12. package/dist/host/box-slot.js +112 -0
  13. package/dist/host/box.d.ts +104 -0
  14. package/dist/host/box.js +16 -0
  15. package/dist/host/egress-proxy.d.ts +45 -0
  16. package/dist/host/egress-proxy.js +416 -0
  17. package/dist/host/egress-table.d.ts +24 -0
  18. package/dist/host/egress-table.js +165 -0
  19. package/dist/host/index.d.ts +16 -0
  20. package/dist/host/index.js +12 -0
  21. package/dist/host/overhead.d.ts +53 -0
  22. package/dist/host/overhead.js +75 -0
  23. package/dist/host/parent-death.d.ts +15 -0
  24. package/dist/host/parent-death.js +42 -0
  25. package/dist/host/parent-watch.d.ts +1 -0
  26. package/dist/host/parent-watch.js +38 -0
  27. package/dist/host/pinned-binary.d.ts +6 -0
  28. package/dist/host/pinned-binary.js +48 -0
  29. package/dist/host/proxy.d.ts +179 -0
  30. package/dist/host/proxy.js +629 -0
  31. package/dist/host/serial.d.ts +3 -0
  32. package/dist/host/serial.js +10 -0
  33. package/dist/index.d.ts +11 -0
  34. package/dist/index.js +6 -0
  35. package/dist/kernel/blobs.d.ts +16 -0
  36. package/dist/kernel/blobs.js +59 -0
  37. package/dist/kernel/drive.d.ts +35 -0
  38. package/dist/kernel/drive.js +202 -0
  39. package/dist/kernel/errors.d.ts +27 -0
  40. package/dist/kernel/errors.js +42 -0
  41. package/dist/kernel/follow-up.d.ts +67 -0
  42. package/dist/kernel/follow-up.js +146 -0
  43. package/dist/kernel/halt.d.ts +11 -0
  44. package/dist/kernel/halt.js +17 -0
  45. package/dist/kernel/index.d.ts +8 -0
  46. package/dist/kernel/index.js +5 -0
  47. package/dist/kernel/types.d.ts +120 -0
  48. package/dist/kernel/types.js +1 -0
  49. package/dist/process/builtins.d.ts +142 -0
  50. package/dist/process/builtins.js +207 -0
  51. package/dist/process/inbox.d.ts +29 -0
  52. package/dist/process/inbox.js +52 -0
  53. package/dist/process/index.d.ts +9 -0
  54. package/dist/process/index.js +11 -0
  55. package/dist/process/journal.d.ts +111 -0
  56. package/dist/process/journal.js +232 -0
  57. package/dist/process/records.d.ts +143 -0
  58. package/dist/process/records.js +142 -0
  59. package/dist/process/recovery.d.ts +76 -0
  60. package/dist/process/recovery.js +139 -0
  61. package/dist/process/settle.d.ts +11 -0
  62. package/dist/process/settle.js +20 -0
  63. package/dist/process/tail.d.ts +20 -0
  64. package/dist/process/tail.js +53 -0
  65. package/dist/process/tools.d.ts +105 -0
  66. package/dist/process/tools.js +120 -0
  67. package/dist/process/zod.d.ts +32 -0
  68. package/dist/process/zod.js +109 -0
  69. package/dist/runner.d.ts +154 -0
  70. package/dist/runner.js +547 -0
  71. package/dist/schema-check-cases.d.ts +7 -0
  72. package/dist/schema-check-cases.js +73 -0
  73. package/dist/schema-check.d.ts +3 -0
  74. package/dist/schema-check.js +88 -0
  75. package/dist/skill-files.d.ts +23 -0
  76. package/dist/skill-files.js +136 -0
  77. package/dist/testing.d.ts +91 -0
  78. package/dist/testing.js +196 -0
  79. package/dist/types.d.ts +237 -0
  80. package/dist/types.js +10 -0
  81. package/package.json +76 -1
  82. package/vendor/durable-harness/UPSTREAM.json +67 -0
@@ -0,0 +1,53 @@
1
+ /** What of a request decides what its provider adds: whether it offers tools, how it asks for them, and its images. */
2
+ export interface InputShape {
3
+ /** The request offers at least one tool. */
4
+ tools: boolean;
5
+ /** Its tool_choice, as Anthropic names them; absent for the provider's default. */
6
+ toolChoice?: "auto" | "none" | "any" | "tool";
7
+ /** Its image inputs, at any depth (a tool's result included). */
8
+ images: number;
9
+ }
10
+ /**
11
+ * The tool-use system prompt the Messages API adds to a request with tools, in tokens, by tool_choice. Documented per
12
+ * model under "Pricing" in https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview, and taken at the largest
13
+ * figure listed for any current model (Claude Opus 4.7's): 675 for auto or none, 804 for any or tool. None without tools.
14
+ */
15
+ export declare const ANTHROPIC_TOOL_USE_SYSTEM_PROMPT_TOKENS: Readonly<{
16
+ autoOrNone: 675;
17
+ anyOrTool: 804;
18
+ }>;
19
+ /**
20
+ * The most tokens one image is billed as on the Messages API, whatever its bytes: Claude bills 28x28-pixel patches of the
21
+ * image as it resized it. The largest "Max visual tokens" documented for any model in "Resolution and token cost",
22
+ * https://platform.claude.com/docs/en/build-with-claude/vision: 4,784 on the high-resolution tier (Claude 4.7 and later),
23
+ * against 1,568 on the standard tier.
24
+ */
25
+ export declare const ANTHROPIC_IMAGE_TOKENS_MAX = 4784;
26
+ /**
27
+ * The most input tokens one image can be billed as on the Responses API, whatever its bytes. An image is covered by 32 px
28
+ * patches, one over 30,000 patches is rejected, and its patches are multiplied by the model's multiplier, at most 2.46
29
+ * among the models listed ("Calculating costs", https://developers.openai.com/api/docs/guides/images-vision). The guide
30
+ * states no per-image maximum, so this is that ceiling: 30,000 x 2.46. The tile-based models cost less per image. The
31
+ * Responses API documents no prompt of its own that it adds for tools.
32
+ */
33
+ export declare const OPENAI_IMAGE_TOKENS_MAX = 73800;
34
+ /**
35
+ * The tool prompt assumed for an API whose provider documents none: 2,048 tokens a request with tools. It is an
36
+ * assumption, not a documented figure, and it is above the largest documented one (Anthropic's 804), so a provider that
37
+ * adds a prompt of its own for tools, as Anthropic and OpenAI do, is not undercounted where its figure is unknown.
38
+ */
39
+ export declare const ASSUMED_TOOL_PROMPT_TOKENS = 2048;
40
+ /**
41
+ * The API kinds whose figures are documented, as pi names them. Azure's and Codex's Responses are billed as OpenAI's.
42
+ * Every other kind (pi's "mistral-conversations", "bedrock-converse-stream", "google-generative-ai", "google-vertex",
43
+ * "pi-messages", a faux "faux:<id>", or any other) gets the general figure: nothing for its text, which the byte bound
44
+ * covers, `ASSUMED_TOOL_PROMPT_TOKENS` when it has tools, and OpenAI's per-image maximum, the largest documented, for each
45
+ * image.
46
+ */
47
+ export declare const INPUT_OVERHEAD_APIS: readonly string[];
48
+ /**
49
+ * The input tokens a provider bills for a request of `shape` beyond its body's bytes, on the API `api` (pi's names:
50
+ * "anthropic-messages", "openai-responses", ...). A documented API gets its documented figures; any other gets the
51
+ * general figure (`INPUT_OVERHEAD_APIS`). Never undefined: an input that cannot be measured is the body's, not the API's.
52
+ */
53
+ export declare function providerInputOverhead(api: string, shape: InputShape): number;
@@ -0,0 +1,75 @@
1
+ // The input a provider bills for a request beyond the bytes of its body, by the provider's API: one place for the figures
2
+ // and their sources, used by the model proxy's policies and by a host that bounds a request it sends itself. Written for
3
+ // AgentRun.
4
+ //
5
+ // A request's input can never be more tokens than its body's UTF-8 bytes (`inputTokensMaxOf`), except for what the
6
+ // provider adds or bills by another measure: a prompt it adds itself when tools are present, and images, which it bills by
7
+ // their area after it resizes them, not by their bytes. Each figure is the largest the provider documents for any current
8
+ // model, so no model is undercounted.
9
+ /**
10
+ * The tool-use system prompt the Messages API adds to a request with tools, in tokens, by tool_choice. Documented per
11
+ * model under "Pricing" in https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview, and taken at the largest
12
+ * figure listed for any current model (Claude Opus 4.7's): 675 for auto or none, 804 for any or tool. None without tools.
13
+ */
14
+ export const ANTHROPIC_TOOL_USE_SYSTEM_PROMPT_TOKENS = Object.freeze({ autoOrNone: 675, anyOrTool: 804 });
15
+ /**
16
+ * The most tokens one image is billed as on the Messages API, whatever its bytes: Claude bills 28x28-pixel patches of the
17
+ * image as it resized it. The largest "Max visual tokens" documented for any model in "Resolution and token cost",
18
+ * https://platform.claude.com/docs/en/build-with-claude/vision: 4,784 on the high-resolution tier (Claude 4.7 and later),
19
+ * against 1,568 on the standard tier.
20
+ */
21
+ export const ANTHROPIC_IMAGE_TOKENS_MAX = 4_784;
22
+ /**
23
+ * The most input tokens one image can be billed as on the Responses API, whatever its bytes. An image is covered by 32 px
24
+ * patches, one over 30,000 patches is rejected, and its patches are multiplied by the model's multiplier, at most 2.46
25
+ * among the models listed ("Calculating costs", https://developers.openai.com/api/docs/guides/images-vision). The guide
26
+ * states no per-image maximum, so this is that ceiling: 30,000 x 2.46. The tile-based models cost less per image. The
27
+ * Responses API documents no prompt of its own that it adds for tools.
28
+ */
29
+ export const OPENAI_IMAGE_TOKENS_MAX = 73_800;
30
+ /**
31
+ * Chat Completions ("openai-completions") is spoken by OpenAI and by OpenAI-compatible gateways, which route to other
32
+ * providers' models (Anthropic's among them). Its figure is the largest of any provider it may reach:
33
+ * - messages: nothing. It adds formatting tokens around each message, 3 a message, 1 for a name and 3 that prime the
34
+ * reply (num_tokens_from_messages in https://developers.openai.com/cookbook/examples/how_to_count_tokens_with_tiktoken),
35
+ * and each message's own JSON in the body ({"role":...,"content":...}, some 30 bytes of keys) is more bytes than that,
36
+ * so the byte bound covers them;
37
+ * - tools: never nothing. A gateway that routes to an Anthropic model is billed Anthropic's tool-use system prompt
38
+ * whatever API it speaks, and OpenAI renders tools into a prompt of its own with a fixed header; the largest documented
39
+ * of these is Anthropic's, 804 tokens (`ANTHROPIC_TOOL_USE_SYSTEM_PROMPT_TOKENS.anyOrTool`), taken whatever the
40
+ * tool_choice, since a gateway may not pass it through as Anthropic reads it;
41
+ * - images: OpenAI's per-image maximum, which is above Anthropic's, so a gateway routing to either is covered.
42
+ */
43
+ const CHAT_COMPLETIONS = "openai-completions";
44
+ /**
45
+ * The tool prompt assumed for an API whose provider documents none: 2,048 tokens a request with tools. It is an
46
+ * assumption, not a documented figure, and it is above the largest documented one (Anthropic's 804), so a provider that
47
+ * adds a prompt of its own for tools, as Anthropic and OpenAI do, is not undercounted where its figure is unknown.
48
+ */
49
+ export const ASSUMED_TOOL_PROMPT_TOKENS = 2_048;
50
+ /**
51
+ * The API kinds whose figures are documented, as pi names them. Azure's and Codex's Responses are billed as OpenAI's.
52
+ * Every other kind (pi's "mistral-conversations", "bedrock-converse-stream", "google-generative-ai", "google-vertex",
53
+ * "pi-messages", a faux "faux:<id>", or any other) gets the general figure: nothing for its text, which the byte bound
54
+ * covers, `ASSUMED_TOOL_PROMPT_TOKENS` when it has tools, and OpenAI's per-image maximum, the largest documented, for each
55
+ * image.
56
+ */
57
+ export const INPUT_OVERHEAD_APIS = Object.freeze(["anthropic-messages", "openai-responses", "azure-openai-responses", "openai-codex-responses", CHAT_COMPLETIONS]);
58
+ /** The tool-use prompt Anthropic adds for a request of `shape`: none without tools. */
59
+ const anthropicToolPrompt = (shape) => (!shape.tools ? 0 : shape.toolChoice === "any" || shape.toolChoice === "tool" ? ANTHROPIC_TOOL_USE_SYSTEM_PROMPT_TOKENS.anyOrTool : ANTHROPIC_TOOL_USE_SYSTEM_PROMPT_TOKENS.autoOrNone);
60
+ /**
61
+ * The input tokens a provider bills for a request of `shape` beyond its body's bytes, on the API `api` (pi's names:
62
+ * "anthropic-messages", "openai-responses", ...). A documented API gets its documented figures; any other gets the
63
+ * general figure (`INPUT_OVERHEAD_APIS`). Never undefined: an input that cannot be measured is the body's, not the API's.
64
+ */
65
+ export function providerInputOverhead(api, shape) {
66
+ if (!Number.isSafeInteger(shape.images) || shape.images < 0)
67
+ throw new Error(`not an image count: ${String(shape.images).slice(0, 40)}`);
68
+ if (api === "anthropic-messages")
69
+ return anthropicToolPrompt(shape) + shape.images * ANTHROPIC_IMAGE_TOKENS_MAX;
70
+ if (api === CHAT_COMPLETIONS)
71
+ return (shape.tools ? ANTHROPIC_TOOL_USE_SYSTEM_PROMPT_TOKENS.anyOrTool : 0) + shape.images * OPENAI_IMAGE_TOKENS_MAX;
72
+ if (INPUT_OVERHEAD_APIS.includes(api))
73
+ return shape.images * OPENAI_IMAGE_TOKENS_MAX;
74
+ return (shape.tools ? ASSUMED_TOOL_PROMPT_TOKENS : 0) + shape.images * OPENAI_IMAGE_TOKENS_MAX;
75
+ }
@@ -0,0 +1,15 @@
1
+ /** A command and its arguments. */
2
+ export interface LaunchCommand {
3
+ command: string;
4
+ args: string[];
5
+ }
6
+ /** How a child is tied to its parent: the kernel's parent-death signal, or the shim that watches. */
7
+ export type ParentWatch = "pdeathsig" | "shim";
8
+ /** The util-linux setpriv that sets the parent-death signal. */
9
+ export declare const SETPRIV = "/usr/bin/setpriv";
10
+ /** The shim, run with this process's node where setpriv cannot be. */
11
+ export declare const PARENT_WATCH_SHIM: string;
12
+ /** The watch this machine supports: pdeathsig on Linux with a setpriv that has `--pdeathsig`, else the shim. Read once. */
13
+ export declare function parentWatchHere(): ParentWatch;
14
+ /** `child`, started so that it dies with this process (`how`, default what this machine supports). */
15
+ export declare function parentWatched(child: Readonly<LaunchCommand>, how?: ParentWatch): LaunchCommand;
@@ -0,0 +1,42 @@
1
+ // A harness's own process (its CLI, its app-server) dies with the process that started it. Written for AgentRun.
2
+ //
3
+ // A session machine starts that process detached, in its own process group, so its group kill (abort, close) ends it
4
+ // and everything it started. A SIGKILLed host runs no close, and a detached child outlives it: reparented, it sleeps on
5
+ // (the pinned Claude Agent CLI does not exit when its stdin closes), one more per crash. So the child is started with a
6
+ // watch on its parent:
7
+ // - Linux with util-linux setpriv: `setpriv --pdeathsig KILL --` in front of it. setpriv sets the parent-death signal
8
+ // and execs the child in place (the same pid, still the host's direct child), so the kernel sends it SIGKILL the
9
+ // moment the host's spawning thread exits: libuv spawns from the event loop's thread, the main one, so a worker
10
+ // thread's exit does not trigger it. The signal survives the exec, as the child is not set-user-ID.
11
+ // - elsewhere (macOS, or no setpriv): `node parent-watch.js <host pid> -- <child>`, a shim that runs the child and
12
+ // kills it when the shim is reparented, checked four times a second.
13
+ // The watch goes INSIDE the host's confinement (the machine applies its confine prefix to the watched command): the
14
+ // kernel clears a parent-death signal when credentials change, so it is set after any switch of user the prefix makes.
15
+ // The child's environment is the machine's: the watch adds nothing to it.
16
+ import { execFileSync } from "node:child_process";
17
+ import { existsSync } from "node:fs";
18
+ import { fileURLToPath } from "node:url";
19
+ /** The util-linux setpriv that sets the parent-death signal. */
20
+ export const SETPRIV = "/usr/bin/setpriv";
21
+ /** The shim, run with this process's node where setpriv cannot be. */
22
+ export const PARENT_WATCH_SHIM = fileURLToPath(new URL("./parent-watch.js", import.meta.url));
23
+ let here;
24
+ /** The watch this machine supports: pdeathsig on Linux with a setpriv that has `--pdeathsig`, else the shim. Read once. */
25
+ export function parentWatchHere() {
26
+ if (here)
27
+ return here;
28
+ let pdeathsig = false;
29
+ if (process.platform === "linux" && existsSync(SETPRIV)) {
30
+ try {
31
+ pdeathsig = execFileSync(SETPRIV, ["--help"], { encoding: "utf8", env: { PATH: "/usr/bin:/bin" }, timeout: 5_000 }).includes("--pdeathsig");
32
+ }
33
+ catch { }
34
+ }
35
+ return (here = pdeathsig ? "pdeathsig" : "shim");
36
+ }
37
+ /** `child`, started so that it dies with this process (`how`, default what this machine supports). */
38
+ export function parentWatched(child, how = parentWatchHere()) {
39
+ if (how === "pdeathsig")
40
+ return { command: SETPRIV, args: ["--pdeathsig", "KILL", "--", child.command, ...child.args] };
41
+ return { command: process.execPath, args: [PARENT_WATCH_SHIM, String(process.pid), "--", child.command, ...child.args] };
42
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,38 @@
1
+ // The parent watch where the kernel's parent-death signal is not available (parent-death.ts). Written for AgentRun.
2
+ // node parent-watch.js <parent pid> -- <command> [args...]
3
+ // Runs the command with this process's stdio and environment, in this process's group (so a group kill ends both),
4
+ // passes TERM, INT and HUP on to it, exits as it exits, and kills it when this process is reparented: its parent, the
5
+ // host, is gone.
6
+ import { spawn } from "node:child_process";
7
+ const [parentArg, separator, command, ...args] = process.argv.slice(2);
8
+ const parent = Number(parentArg);
9
+ if (!Number.isSafeInteger(parent) || parent < 1 || separator !== "--" || !command) {
10
+ process.stderr.write("usage: parent-watch.js <parent pid> -- <command> [args...]\n");
11
+ process.exit(2);
12
+ }
13
+ // Started after the host was already gone: nothing to run for.
14
+ if (process.ppid !== parent)
15
+ process.exit(1);
16
+ const child = spawn(command, args, { stdio: "inherit" });
17
+ const passed = ["SIGTERM", "SIGINT", "SIGHUP"];
18
+ for (const signal of passed)
19
+ process.on(signal, () => child.kill(signal));
20
+ child.on("error", (error) => {
21
+ process.stderr.write(`parent-watch: ${error.message}\n`);
22
+ process.exit(127);
23
+ });
24
+ const watch = setInterval(() => {
25
+ if (process.ppid === parent)
26
+ return;
27
+ child.kill("SIGKILL");
28
+ process.exit(1);
29
+ }, 250);
30
+ child.on("exit", (code, signal) => {
31
+ clearInterval(watch);
32
+ if (code !== null)
33
+ process.exit(code);
34
+ // Ended by a signal: end the same way, so the host sees what the child saw.
35
+ for (const s of passed)
36
+ process.removeAllListeners(s);
37
+ process.kill(process.pid, signal ?? "SIGKILL");
38
+ });
@@ -0,0 +1,6 @@
1
+ /** The sha256 of the file at `path`, as hex, read a chunk at a time. Throws as `openSync` and `readSync` do. */
2
+ export declare function sha256OfFile(path: string): string;
3
+ /** `check`'s result for the binary at `path` against `pins`, run once per identity in this process: a result `ok` calls
4
+ * passed is kept, and answered again at once while the binary stays the same file, unchanged; any other is never kept.
5
+ * A binary that changed while it was checked is not kept, and is checked again next time. */
6
+ export declare function checkedOnce<T>(path: string, pins: string, check: () => T, ok: (result: T) => boolean): T;
@@ -0,0 +1,48 @@
1
+ // A harness's binary, checked against its pins (codex's, claude-agent's): its sha256 read a chunk at a time, and each
2
+ // check that passed kept for the process by what the binary is. A binary is hundreds of MiB; read whole, each check held
3
+ // all of it in memory, and a host that checks it again for each model it resolves held it again each time. Here a process
4
+ // holds one chunk at most, and checks a binary once per identity: its path, the file it names (device and inode), its
5
+ // size and its modification and change times, with the pins it met. Replaced or touched, it is checked again; a check
6
+ // that failed is never kept, so the next one runs again.
7
+ import { createHash } from "node:crypto";
8
+ import { closeSync, openSync, readSync, statSync } from "node:fs";
9
+ /** The bytes read at a time. */
10
+ const CHUNK = 1 << 20;
11
+ /** The sha256 of the file at `path`, as hex, read a chunk at a time. Throws as `openSync` and `readSync` do. */
12
+ export function sha256OfFile(path) {
13
+ const hash = createHash("sha256");
14
+ const fd = openSync(path, "r");
15
+ try {
16
+ const chunk = Buffer.allocUnsafe(CHUNK);
17
+ for (let read; (read = readSync(fd, chunk, 0, CHUNK, null)) > 0;)
18
+ hash.update(chunk.subarray(0, read));
19
+ }
20
+ finally {
21
+ closeSync(fd);
22
+ }
23
+ return hash.digest("hex");
24
+ }
25
+ /** What the file at `path` is, for `pins` (any text that names them): undefined when it cannot be read. */
26
+ function identityOf(path, pins) {
27
+ try {
28
+ const file = statSync(path, { bigint: true });
29
+ return [path, file.dev, file.ino, file.size, file.mtimeNs, file.ctimeNs, pins].join("\0");
30
+ }
31
+ catch {
32
+ return undefined;
33
+ }
34
+ }
35
+ /** What each check that passed answered, by the binary's identity. */
36
+ const passed = new Map();
37
+ /** `check`'s result for the binary at `path` against `pins`, run once per identity in this process: a result `ok` calls
38
+ * passed is kept, and answered again at once while the binary stays the same file, unchanged; any other is never kept.
39
+ * A binary that changed while it was checked is not kept, and is checked again next time. */
40
+ export function checkedOnce(path, pins, check, ok) {
41
+ const identity = identityOf(path, pins);
42
+ if (identity !== undefined && passed.has(identity))
43
+ return passed.get(identity);
44
+ const result = check();
45
+ if (identity !== undefined && ok(result) && identityOf(path, pins) === identity)
46
+ passed.set(identity, result);
47
+ return result;
48
+ }
@@ -0,0 +1,179 @@
1
+ import { type IncomingHttpHeaders } from "node:http";
2
+ /** A route a policy admits: its method, its exact path, and the request headers forwarded with it. */
3
+ export interface ProxyRoute {
4
+ readonly method: string;
5
+ readonly path: string;
6
+ /** Exact query strings it admits besides none (`beta=true`); forwarded as the table has them. */
7
+ readonly query?: readonly string[];
8
+ readonly headers?: readonly string[];
9
+ /** Answered here with this status and an empty body, never forwarded (a client's connectivity check). */
10
+ readonly local?: number;
11
+ /** Not a paid request (a token count): forwarded without asking the runner, and no usage is reported. */
12
+ readonly free?: boolean;
13
+ }
14
+ /** Token counts a response reports. */
15
+ export interface ProxyUsage {
16
+ input: number;
17
+ output: number;
18
+ cacheRead: number;
19
+ cacheWrite?: number;
20
+ }
21
+ /** What one provider's API may carry through the proxy. */
22
+ export interface ProviderPolicy {
23
+ /** A name for messages. */
24
+ readonly id: string;
25
+ readonly routes: readonly ProxyRoute[];
26
+ /** The key the client sent, from its headers ("" when it sent none). */
27
+ keyOf(headers: IncomingHttpHeaders): string;
28
+ /** The headers that carry the provider's credential. */
29
+ authHeaders(credential: string): Record<string, string>;
30
+ /** A listed request header's value as forwarded, or undefined to drop it; a `ProxyRefusal` refuses the request before
31
+ * the runner is asked (a beta the policy does not know). Default: as sent. */
32
+ header?(name: string, value: string): string | undefined;
33
+ /** The body the provider gets for a request on `route`: throw a `ProxyRefusal` to refuse it. */
34
+ apply(body: unknown, route: ProxyRoute): Record<string, unknown>;
35
+ /** The usage a completed response reports, if it reports one: from its whole text, or, for a long one, its first and
36
+ * last 64 KiB (a provider may report input tokens as a stream starts and output tokens as it ends). */
37
+ usageOf(text: string): ProxyUsage | undefined;
38
+ /** Called once for each request the proxy forwards that the provider answers, as the answer starts. Requests overlap (a
39
+ * client's background calls run beside its turn), so what it keeps is for the sink to tell apart: each call returns its own
40
+ * observer. The observer is given each piece of the answer as text, BEFORE that piece is written to the client, so what it
41
+ * saw of an answer, the client has not yet acted on: the one thing the proxy can say causally about what a client does
42
+ * next. `end` is called when the answer is over, however it ended. It only reads; one that throws is reported, `failed` is
43
+ * called (the answer was not watched to its end) and the answer goes on. A `watch` must not throw after it has told its
44
+ * sink the answer started (its sink counts open answers, and only `end` closes one): do that last. */
45
+ watch?(): {
46
+ chunk(text: string): void;
47
+ failed?(): void;
48
+ end?(): void;
49
+ } | undefined;
50
+ /** Called when `watch` threw: this answer is not watched, so nothing the policy knew of an earlier one may stand for it. */
51
+ unwatched?(): void;
52
+ /** The model the body `apply` returned asks for, once `apply` checked it is one this session may use (its own, or a
53
+ * smaller one for a client's background calls). The runner is asked about, and the price taken for, that model.
54
+ * Absent, or undefined for a body: the session's model. */
55
+ modelOf?(body: Record<string, unknown>): string | undefined;
56
+ /** The input tokens the provider bills for the body `apply` returned that the body does not carry: a prompt it adds
57
+ * itself, such as a tool-use system prompt. Added to the body's bytes for `inputTokensMax`. Undefined: the body has
58
+ * no input bound the policy can state (a hosted tool that reads pages into the input), so `inputTokensMax` is
59
+ * undefined, never 0 or the body's bytes alone. Absent: none. */
60
+ serverInputOverhead?(body: Record<string, unknown>): number | undefined;
61
+ /** The output tokens the body `apply` returned asks for at most (its own limit, as `apply` shaped it). Absent or
62
+ * undefined: the body does not say. */
63
+ outputAsked?(body: Record<string, unknown>): number | undefined;
64
+ /** The least output the body may be limited to and still answer: 1,024 tokens for the answer, plus the least thinking
65
+ * its provider takes when it thinks. Absent: 1,024. */
66
+ outputLeast?(body: Record<string, unknown>): number;
67
+ /** The body `apply` returned with its output limited to `max` tokens, thinking included (what is left of a spend cap
68
+ * can buy): never more than it asked for, and never longer as sent, since its input bound was measured on it. A host
69
+ * that limits a request's output needs it: without it such a request is refused, never sent unlimited. */
70
+ limitOutput?(body: Record<string, unknown>, max: number): Record<string, unknown>;
71
+ /** The status a request the runner refused gets: one the client does not retry. Default 400. */
72
+ readonly refusedStatus?: number;
73
+ }
74
+ export interface ModelUsageReport extends ProxyUsage {
75
+ /** The provider's (or its gateway's) own figure when the response gave one (`reportedCostOf`); else the price table's,
76
+ * `estimated`; null when neither is known. */
77
+ usd: number | null;
78
+ estimated?: boolean;
79
+ /** Beside a reported figure, what the price table makes of the same usage: kept to audit the table. */
80
+ tableUsd?: number | null;
81
+ /** The ids the response's headers gave it (`providerIdsOf`), when they came. */
82
+ providerIds?: Record<string, string>;
83
+ /** A response its client left before its end (a stop), with the bytes of it the client was sent: what streamed. Not
84
+ * set for one the provider ended early (an error part way), nor for one the client read whole. */
85
+ stoppedAfterBytes?: number;
86
+ }
87
+ /** A paid request as the host is asked about it, before it leaves. */
88
+ export interface ProxyRequest {
89
+ model: string;
90
+ /** The most input tokens it can be billed for (see `inputTokensMaxOf`), undefined when that could not be measured. */
91
+ inputTokensMax?: number;
92
+ /** The most output tokens it asks for (the policy's `outputAsked`), when the body says. */
93
+ outputTokensAsked?: number;
94
+ /** The least output it may be limited to and still answer (the policy's `outputLeast`). */
95
+ outputTokensLeast?: number;
96
+ }
97
+ export interface ModelProxyHooks {
98
+ /** Before a paid request leaves; a throw refuses it. What it returns (a ticket) is handed back to this request's
99
+ * `afterRequest`, since requests may be in flight together. A ticket's `outputTokensMax` limits the request's output
100
+ * (thinking included) to that many tokens, written into its body by the policy's `limitOutput` before it is sent. */
101
+ beforeRequest(request: ProxyRequest): Promise<unknown>;
102
+ /** When a request's response has completed, with its usage and the ticket its `beforeRequest` returned. */
103
+ afterRequest(usage: ModelUsageReport, ticket?: unknown): Promise<void>;
104
+ /** When a paid request's response headers arrive, before its body: the ids they give it (`providerIdsOf`), so a
105
+ * request a crash cuts off after this point is known to have reached the provider, under those ids. */
106
+ responded?(ids: Record<string, string>, ticket?: unknown): Promise<void>;
107
+ }
108
+ export interface ModelProxyOptions {
109
+ policy: ProviderPolicy;
110
+ hooks: ModelProxyHooks;
111
+ /** The session's model, as the runner names it to `beforeRequest`. The policy fixes the body's model. */
112
+ model: string;
113
+ /** The provider's base: its origin and, for a gateway that serves the API under a path, that path
114
+ * (`https://api.example.com`, `https://gateway.example/api`). A route's path is appended to it. A base with userinfo, a
115
+ * query or a fragment is refused, so the key goes only where the base reads. */
116
+ origin: string;
117
+ /** The provider's credential, read for each request. */
118
+ credential: () => string | Promise<string>;
119
+ /** The price of a response, or null when it is unknown. */
120
+ price?: (model: string, usage: ProxyUsage) => number | null;
121
+ log?: (message: string, detail?: Record<string, unknown>) => void;
122
+ /** Told each forwarded request's route and the provider's status once it answers: what a route does on this
123
+ * provider (a free route it may not serve), as a status code only, never a body or a key. */
124
+ onResponse?: (response: {
125
+ method: string;
126
+ path: string;
127
+ free: boolean;
128
+ status: number;
129
+ }) => void;
130
+ /** Headers the host adds to every request it forwards (a gateway's spend tags, LiteLLM's `x-litellm-tags`, say):
131
+ * after the client's, so the client can neither set nor override them, and before the credential's. Checked when the
132
+ * proxy starts (`hostHeaderProblems`). */
133
+ headers?: Readonly<Record<string, string>>;
134
+ /** For tests: the fetch the proxy forwards with. */
135
+ fetch?: typeof fetch;
136
+ }
137
+ /** What is wrong with headers a host would add to forwarded requests: a name that is not a token or is reserved, a value
138
+ * an HTTP header cannot carry (a line break or other control character, or a character beyond Latin-1, which the fetch
139
+ * the proxy forwards with refuses) or over 1 KiB. Empty when they are fine. */
140
+ export declare function hostHeaderProblems(headers: Readonly<Record<string, unknown>> | undefined): string[];
141
+ export interface ModelProxy {
142
+ /** The proxy's origin, `http://127.0.0.1:<port>`. */
143
+ readonly url: string;
144
+ /** The key the harness sends, which the proxy admits. */
145
+ readonly key: string;
146
+ close(): Promise<void>;
147
+ }
148
+ /** The ids a response's headers give its request, by header name, so a run's books can be joined to the provider's (or
149
+ * a gateway's) request by request; undefined when there are none. Only id-named headers with id-shaped values. */
150
+ export declare function providerIdsOf(headers: Headers): Record<string, string> | undefined;
151
+ /** What a response's headers say it cost, when they say so: LiteLLM's `x-litellm-response-cost`, or its `-original` when
152
+ * the discount and margin it reports are both exactly 0. Only a positive number counts. A missing, malformed or zero
153
+ * figure is unknown, never $0: a streamed answer's headers leave before it is priced, and LiteLLM writes 0.0 there. */
154
+ export declare function reportedCostOf(headers: Headers): number | undefined;
155
+ export declare class ProxyRefusal extends Error {
156
+ readonly status: number;
157
+ constructor(status: number, message: string);
158
+ }
159
+ /** Match a request's method and path text against the routes, never parsing the path as a URL: the route, and the
160
+ * query it carried, which is one the route lists exactly, or none. */
161
+ export declare function matchRoute(routes: readonly ProxyRoute[], method: string, path: string): {
162
+ route: ProxyRoute;
163
+ query: string;
164
+ };
165
+ /** Parse one JSON value, refusing a duplicate key at any depth and nesting past 64 levels. */
166
+ export declare function parseStrict(text: string): unknown;
167
+ /** Whether `given` is `key`, in time that does not depend on where they differ. */
168
+ export declare const sameKey: (given: string, key: string) => boolean;
169
+ /** Start a session's model proxy on the loopback interface, admitting `key`. */
170
+ /**
171
+ * The most input tokens a request with body `sent` (the body `apply` returned, as it is sent) can be billed for: its UTF-8
172
+ * byte count, plus the input the policy says the provider adds that the body does not carry. A provider's tokenizer is a
173
+ * byte-level BPE, whose every token covers at least one byte of what it encodes, so the text the body carries can never
174
+ * be more tokens than bytes, however dense (CJK, emoji, base64). It is not an average of characters per token, which
175
+ * undercounts dense text. Undefined, never 0, when the policy says the body has no bound (its undefined) or its figure
176
+ * cannot be read (a throw, or no count, which is reported): the input is then unmeasured.
177
+ */
178
+ export declare function inputTokensMaxOf(policy: ProviderPolicy, body: Record<string, unknown>, sent: string, say?: (message: string, detail?: Record<string, unknown>) => void): number | undefined;
179
+ export declare function startModelProxy(options: ModelProxyOptions, key: string): Promise<ModelProxy>;