agentfences 0.0.0-stage → 0.3.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 CHANGED
@@ -1,3 +1,64 @@
1
- # Temporary Holding Version
1
+ # agentfences (TypeScript / JavaScript)
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The TypeScript SDK for [Fences](https://github.com/bhaviksardar/fences), on-call for AI agents: limits your agents can't raise, cost tracking from real usage, a decision trail, and a stop button. Works offline too: local limits, no account. Node 18+, no dependencies.
4
+
5
+ ```bash
6
+ npm install agentfences
7
+ ```
8
+
9
+ ## Quickstart (no account needed)
10
+
11
+ ```ts
12
+ import { init, governed, checkpoint, logDecision } from "agentfences";
13
+
14
+ init({ localOnly: true });
15
+
16
+ const research = governed(async (question: string) => {
17
+ for (;;) {
18
+ logDecision("asking the model for the next step", "llm_call");
19
+ const response = await openai.chat.completions.create({ model: "gpt-4o", messages });
20
+ const result = await checkpoint(response); // Fences reads the usage and prices the call
21
+ if (result.breached) return result.message; // "I've reached my budget limit (spent $0.5100 of $0.5000)..."
22
+ // ...
23
+ }
24
+ }, { budgetUsd: 0.5, maxIterations: 20 });
25
+ ```
26
+
27
+ `checkpoint()` never throws for a breach: it returns a result with `ok`, `breached`, `breachType`, `message` (ready for the user), `systemPrompt` (to give the model so it wraps up) and `spentUsd`.
28
+
29
+ ## Cost tracking
30
+
31
+ Pass the model's response to `checkpoint()` and Fences works out what the call cost from its model and token usage, including cache reads and writes. It understands results from the OpenAI and Anthropic SDKs, the Vercel AI SDK (`generateText`), Gemini and LangChain.js, or any object with the same usage fields. Prices come from a built-in table of about 290 models (the same one as the Python SDK).
32
+
33
+ - Unknown model: its tokens are counted and Fences warns once. Add prices in USD per 1M tokens: `init({ localOnly: true, prices: { "my-finetune": { input: 2, output: 6 } } })`.
34
+ - Extra costs, or no response at all: `checkpoint(response, { costDeltaUsd: 0.01 })` or `checkpoint(null, { costDeltaUsd: 0.02, tokensUsed: 450 })`.
35
+
36
+ ## Limits
37
+
38
+ `budgetUsd`, `maxIterations` (default 100), `maxDurationMs` (default 5 minutes) and `maxTokens` (default none). All are optional. A limit trips once it's exceeded, not when reached. With no budget anywhere, the run's spend isn't capped and Fences warns once per agent. On Fences Cloud, limits left out come from the agent's page in the dashboard.
39
+
40
+ ## Tools, context, errors and redaction
41
+
42
+ ```ts
43
+ import { tool, context } from "agentfences";
44
+
45
+ const webSearch = tool(async ({ query }: { query: string }) => search(query)); // each call recorded as a span
46
+
47
+ await context({ user_id: "u_123", session_id: "s_9" }, () => research(question)); // tags every run inside
48
+ ```
49
+
50
+ - Tool and model calls are recorded as OpenTelemetry-shaped spans (`execute_tool webSearch`, `gen_ai.*` attributes), on `getActiveRun().events`.
51
+ - When a governed run throws, Fences records the error's type, message and stack with the run; the error still reaches your code.
52
+ - `init({ redact: (event) => ... })` sees everything before it leaves the process and can change or drop it.
53
+
54
+ ## Fences Cloud
55
+
56
+ ```ts
57
+ init({ apiKey: process.env.FENCES_API_KEY, endpoint: "https://your-fences-host", environment: "prod", release: "v1.4.2" });
58
+ ```
59
+
60
+ The server is authoritative for limits and sees spend across processes. If it can't be reached, limits are enforced locally; pass `failClosed: true` to stop instead. A rejected key throws `FencesAuthError`; a quarantined agent throws `AgentQuarantined` before any of its code runs. In serverless handlers, `await flush()` before returning so queued decisions and spans are sent.
61
+
62
+ ## Not yet in the TypeScript SDK
63
+
64
+ The Python SDK also has live control from the dashboard (pause, approvals), automatic recording of OpenAI/Anthropic calls (`instrument`), and LangGraph / OpenAI Agents integrations. They're next. Everything here follows the same server protocol ([PROTOCOL.md](../PROTOCOL.md)).
@@ -0,0 +1,17 @@
1
+ export type Reply = Record<string, any>;
2
+ export declare class FencesClient {
3
+ readonly endpoint: string;
4
+ private readonly timeoutMs;
5
+ private readonly headers;
6
+ constructor(apiKey: string, endpoint: string, timeoutMs?: number);
7
+ startRun(body: Record<string, unknown>): Promise<Reply>;
8
+ checkpoint(runId: string, body: Record<string, unknown>): Promise<Reply>;
9
+ logDecision(runId: string, body: Record<string, unknown>): Promise<Reply>;
10
+ endRun(runId: string, body: Record<string, unknown>): Promise<Reply>;
11
+ logSpans(runId: string, spans: unknown[]): Promise<Reply>;
12
+ /**
13
+ * optional: an endpoint older servers may not have. A missing route returns
14
+ * { unsupported: true } and nothing throws, not even a rejected key.
15
+ */
16
+ post(path: string, payload: unknown, optional?: boolean): Promise<Reply>;
17
+ }
package/dist/client.js ADDED
@@ -0,0 +1,59 @@
1
+ /** HTTP calls to a Fences server, as specified in PROTOCOL.md. Uses the fetch built into Node 18+. */
2
+ import { FencesAuthError } from "./errors.js";
3
+ import { VERSION } from "./version.js";
4
+ export class FencesClient {
5
+ endpoint;
6
+ timeoutMs;
7
+ headers;
8
+ constructor(apiKey, endpoint, timeoutMs = 3000) {
9
+ this.endpoint = endpoint;
10
+ this.timeoutMs = timeoutMs;
11
+ this.endpoint = endpoint.replace(/\/+$/, "");
12
+ // The server uses the SDK version to warn about old SDKs and to skip commands they don't understand
13
+ this.headers = { "Content-Type": "application/json", "X-API-Key": apiKey, "X-Fences-SDK": `js/${VERSION}` };
14
+ }
15
+ startRun(body) { return this.post("/api/runs/start", body); }
16
+ checkpoint(runId, body) { return this.post(`/api/runs/${runId}/checkpoint`, body); }
17
+ logDecision(runId, body) { return this.post(`/api/runs/${runId}/decisions`, body); }
18
+ endRun(runId, body) { return this.post(`/api/runs/${runId}/end`, body); }
19
+ logSpans(runId, spans) { return this.post(`/api/runs/${runId}/spans`, { spans }, true); }
20
+ /**
21
+ * optional: an endpoint older servers may not have. A missing route returns
22
+ * { unsupported: true } and nothing throws, not even a rejected key.
23
+ */
24
+ async post(path, payload, optional = false) {
25
+ let resp;
26
+ try {
27
+ resp = await fetch(this.endpoint + path, {
28
+ method: "POST", headers: this.headers, body: JSON.stringify(payload), signal: AbortSignal.timeout(this.timeoutMs),
29
+ });
30
+ }
31
+ catch (e) {
32
+ return { network_error: String(e) };
33
+ }
34
+ const text = await resp.text().catch(() => "");
35
+ let body = {}, isJson = false;
36
+ try {
37
+ const parsed = JSON.parse(text);
38
+ if (parsed && typeof parsed === "object")
39
+ [body, isJson] = [parsed, true];
40
+ }
41
+ catch { /* not JSON: the status code still decides */ }
42
+ const detail = typeof body.detail === "string" ? body.detail : "";
43
+ if (optional) {
44
+ if ((resp.status === 404 || resp.status === 405) && (detail === "Not Found" || detail === "Method Not Allowed")) {
45
+ return { unsupported: true };
46
+ }
47
+ return resp.ok ? body : { ok: false, status: resp.status };
48
+ }
49
+ if (resp.status === 401 || resp.status === 403)
50
+ throw new FencesAuthError(`Fences API key rejected: ${text}`);
51
+ if (resp.status === 423)
52
+ return { ok: false, quarantined: true, detail }; // the agent is quarantined: the run must not start
53
+ if (resp.status === 409)
54
+ return { ok: false, conflict: detail }; // e.g. checkpoint on a run already fenced
55
+ if (!resp.ok || !isJson)
56
+ return { network_error: resp.ok ? "response was not JSON" : `HTTP ${resp.status}` };
57
+ return body;
58
+ }
59
+ }
package/dist/core.d.ts ADDED
@@ -0,0 +1,123 @@
1
+ import { FencesClient } from "./client.js";
2
+ import * as events from "./events.js";
3
+ import { type CustomPrice } from "./pricing.js";
4
+ export interface CheckpointResult {
5
+ ok: boolean;
6
+ breached: boolean;
7
+ /** "budget_exceeded" | "iteration_limit" | "time_limit" | "token_limit" | "stopped_by_user" | ... (see PROTOCOL.md) */
8
+ breachType: string | null;
9
+ /** First-person text, ready to return to the user. */
10
+ message: string;
11
+ /** An instruction to give the model so it wraps up. */
12
+ systemPrompt: string;
13
+ /** The run's total spend so far. */
14
+ spentUsd: number;
15
+ }
16
+ /** Every limit is optional; see governed(). */
17
+ export interface Limits {
18
+ /** Maximum spend for the run in USD. None anywhere: unlimited spend, with a warning. */
19
+ budgetUsd?: number | null;
20
+ /** Maximum checkpoints (steps). Default 100. */
21
+ maxIterations?: number | null;
22
+ /** Maximum wall-clock time in milliseconds. Default 300000 (5 minutes). */
23
+ maxDurationMs?: number | null;
24
+ /** Maximum tokens (input + output). 0 or unset: no limit. */
25
+ maxTokens?: number | null;
26
+ }
27
+ export declare const MAX_EVENTS_PER_RUN = 1000;
28
+ export interface Span {
29
+ name: string;
30
+ start_ts: number;
31
+ end_ts: number;
32
+ status: "ok" | "error";
33
+ attributes: Record<string, unknown>;
34
+ }
35
+ export declare class Run {
36
+ readonly agentName: string;
37
+ budgetUsd: number | null;
38
+ maxIterations: number;
39
+ maxDurationMs: number;
40
+ maxTokens: number;
41
+ readonly context: Record<string, string>;
42
+ readonly runId: `${string}-${string}-${string}-${string}-${string}`;
43
+ costUsd: number;
44
+ iterations: number;
45
+ tokensUsed: number;
46
+ readonly startedAt: number;
47
+ readonly decisions: {
48
+ timestamp: number;
49
+ iteration: number;
50
+ reasoning: string;
51
+ action: string | null;
52
+ }[];
53
+ /** Tool and model calls as OpenTelemetry-shaped spans, newest 1,000 kept. */
54
+ readonly events: Span[];
55
+ lastBreach: string | null;
56
+ exception?: events.ExceptionInfo;
57
+ warned: boolean;
58
+ constructor(agentName: string, budgetUsd: number | null, maxIterations: number, maxDurationMs: number, maxTokens: number, context: Record<string, string>);
59
+ get durationMs(): number;
60
+ }
61
+ /** The run in progress in this async context, if any. */
62
+ export declare function getActiveRun(): Run | null;
63
+ export interface InitOptions {
64
+ /** Your Fences API key (fc_...). Required unless localOnly. */
65
+ apiKey?: string;
66
+ /** Your Fences server. Defaults to http://localhost:8000. */
67
+ endpoint?: string;
68
+ /** Enforce limits in-process only: no account, nothing leaves the process. */
69
+ localOnly?: boolean;
70
+ /** Treat an unreachable server as a breach (fences_unreachable) instead of enforcing locally. */
71
+ failClosed?: boolean;
72
+ /** Add or override model prices, USD per 1M tokens. */
73
+ prices?: Record<string, CustomPrice>;
74
+ /** Tag every run, e.g. "prod". */
75
+ environment?: string;
76
+ /** Tag every run, e.g. "v1.4.2". */
77
+ release?: string;
78
+ /** Sees every outgoing event and returns it (changed or not), or null to drop it. */
79
+ redact?: events.RedactHook;
80
+ }
81
+ /** Call once at startup, before any governed function runs. */
82
+ export declare function init(options?: InitOptions): void;
83
+ /** @internal For tests: point the SDK at a fake server client. */
84
+ export declare function _setClient(c: FencesClient | null): void;
85
+ /**
86
+ * Make each call of fn a governed run with these limits:
87
+ *
88
+ * const research = governed(async (q: string) => { ... }, { budgetUsd: 0.5, maxIterations: 20 });
89
+ *
90
+ * Every limit is optional. On Fences Cloud, a limit left out comes from the agent's page in
91
+ * the dashboard. The run is named after the function (or `name`). checkpoint() never throws:
92
+ * a crossed limit comes back as a breached result, so the agent can wrap up gracefully.
93
+ */
94
+ export declare function governed<A extends unknown[], R>(fn: (...args: A) => R | Promise<R>, options?: Limits & {
95
+ name?: string;
96
+ }): (...args: A) => Promise<Awaited<R>>;
97
+ export interface Extra {
98
+ /** Extra spend for this step in USD, e.g. a paid tool call. Without a response, the step's whole cost. */
99
+ costDeltaUsd?: number;
100
+ /** Extra tokens for this step. */
101
+ tokensUsed?: number;
102
+ }
103
+ /**
104
+ * Record one step, then check every limit. Pass the model's response and Fences works out
105
+ * the cost from its model and token usage:
106
+ *
107
+ * const response = await openai.chat.completions.create({ ... });
108
+ * const result = await checkpoint(response);
109
+ * if (result.breached) return result.message;
110
+ *
111
+ * Without a response: checkpoint(null, { costDeltaUsd: 0.02, tokensUsed: 450 }).
112
+ * Never throws for a breach. Outside a governed run it does nothing.
113
+ */
114
+ export declare function checkpoint(response?: unknown, extra?: Extra): Promise<CheckpointResult>;
115
+ /** Wait up to timeoutMs for queued decisions and spans to reach the server. Call before a serverless handler returns. */
116
+ export declare function flush(timeoutMs?: number): Promise<void>;
117
+ /** Record the agent's reasoning at this step. Does nothing outside a governed run. */
118
+ export declare function logDecision(reasoning: string, action?: string | null): void;
119
+ /**
120
+ * Add a tool or model call to the active run as an OpenTelemetry-shaped span (GenAI
121
+ * semantic conventions): kept on the run, and sent through the redact hook in cloud mode.
122
+ */
123
+ export declare function recordSpan(name: string, elapsedMs: number, attributes: Record<string, unknown>, error?: unknown): void;
package/dist/core.js ADDED
@@ -0,0 +1,342 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ import { randomUUID } from "node:crypto";
3
+ import { FencesClient } from "./client.js";
4
+ import { AgentQuarantined, FencesStop } from "./errors.js";
5
+ import * as events from "./events.js";
6
+ import { costOf, setCustomPrices } from "./pricing.js";
7
+ const SUMMARY = "I'll summarize what I found so far.";
8
+ const WRAP_UP = "Stop your current task immediately and summarize what you have found or completed so far.";
9
+ const money = (x) => `$${x.toFixed(4)}`;
10
+ const count = (n) => n.toLocaleString("en-US");
11
+ function breachResult(type, run) {
12
+ const spent = run.costUsd, limit = run.budgetUsd ?? 0;
13
+ const text = {
14
+ budget_exceeded: [`I've reached my budget limit (spent ${money(spent)} of ${money(limit)}). ${SUMMARY}`,
15
+ `You have reached your budget limit (${money(spent)} of ${money(limit)} spent). Stop your current task immediately ` +
16
+ "and provide a clear summary of what you have found or completed so far. Tell the user you stopped due to budget and what you accomplished."],
17
+ iteration_limit: [`I've reached my iteration limit (${run.iterations} steps). ${SUMMARY}`,
18
+ `You have reached your iteration limit (${run.iterations} steps). ${WRAP_UP} Tell the user you stopped due to the iteration limit.`],
19
+ time_limit: [`I've reached my time limit (${run.durationMs}ms). ${SUMMARY}`,
20
+ `You have reached your time limit. ${WRAP_UP} Tell the user you stopped due to the time limit.`],
21
+ token_limit: [`I've reached my token limit (${count(run.tokensUsed)} tokens). ${SUMMARY}`,
22
+ `You have reached your token limit (${count(run.tokensUsed)} tokens used). ${WRAP_UP} Tell the user you stopped due to the token limit.`],
23
+ stopped_by_user: [`I was stopped from the Fences dashboard. ${SUMMARY}`,
24
+ `A person stopped this run from the Fences dashboard. ${WRAP_UP} Tell the user you were stopped by an operator.`],
25
+ key_daily_budget: [`My daily spending cap has been reached. ${SUMMARY}`,
26
+ `Your daily spending cap, across all of your runs, has been reached. ${WRAP_UP} Tell the user you stopped because the daily cap was reached.`],
27
+ key_monthly_budget: [`My monthly spending cap has been reached. ${SUMMARY}`,
28
+ `Your monthly spending cap, across all of your runs, has been reached. ${WRAP_UP} Tell the user you stopped because the monthly cap was reached.`],
29
+ fences_unreachable: [`I can't reach Fences to confirm I'm within my limits, so I'm stopping to be safe. ${SUMMARY}`,
30
+ `Your governance service can't be reached and you are configured to stop when that happens. ${WRAP_UP}`],
31
+ paused: [`I was paused from the Fences dashboard and not resumed in time. ${SUMMARY}`,
32
+ `A person paused this run from the Fences dashboard and it was not resumed in time. Stop your current task and summarize ` +
33
+ "what you have found or completed so far. Tell the user the run was paused by an operator."],
34
+ };
35
+ const [message, systemPrompt] = text[type] ??
36
+ ["Governance limit reached.", "A governance limit has been reached. Summarize what you have done so far."];
37
+ return { ok: false, breached: true, breachType: type, message, systemPrompt, spentUsd: run.costUsd };
38
+ }
39
+ const okResult = (spentUsd = 0) => ({ ok: true, breached: false, breachType: null, message: "", systemPrompt: "", spentUsd });
40
+ const DEFAULTS = { maxIterations: 100, maxDurationMs: 300_000, maxTokens: 0 };
41
+ export const MAX_EVENTS_PER_RUN = 1000;
42
+ export class Run {
43
+ agentName;
44
+ budgetUsd;
45
+ maxIterations;
46
+ maxDurationMs;
47
+ maxTokens;
48
+ context;
49
+ runId = randomUUID();
50
+ costUsd = 0;
51
+ iterations = 0;
52
+ tokensUsed = 0;
53
+ startedAt = Date.now();
54
+ decisions = [];
55
+ /** Tool and model calls as OpenTelemetry-shaped spans, newest 1,000 kept. */
56
+ events = [];
57
+ lastBreach = null;
58
+ exception;
59
+ warned = false;
60
+ constructor(agentName, budgetUsd, maxIterations, maxDurationMs, maxTokens, context) {
61
+ this.agentName = agentName;
62
+ this.budgetUsd = budgetUsd;
63
+ this.maxIterations = maxIterations;
64
+ this.maxDurationMs = maxDurationMs;
65
+ this.maxTokens = maxTokens;
66
+ this.context = context;
67
+ }
68
+ get durationMs() { return Date.now() - this.startedAt; }
69
+ }
70
+ const store = new AsyncLocalStorage();
71
+ /** The run in progress in this async context, if any. */
72
+ export function getActiveRun() {
73
+ return store.getStore() ?? null;
74
+ }
75
+ let client = null;
76
+ let localOnly = false;
77
+ let failClosed = false;
78
+ /** Call once at startup, before any governed function runs. */
79
+ export function init(options = {}) {
80
+ setCustomPrices(options.prices);
81
+ events.configure(options.environment, options.release, options.redact);
82
+ localOnly = !!options.localOnly;
83
+ failClosed = !!options.failClosed;
84
+ if (localOnly) {
85
+ client = null;
86
+ return;
87
+ }
88
+ if (!options.apiKey) {
89
+ throw new Error("apiKey is required unless localOnly is set. Use init({ localOnly: true }) for local usage.");
90
+ }
91
+ client = new FencesClient(options.apiKey, options.endpoint ?? "http://localhost:8000");
92
+ }
93
+ /** @internal For tests: point the SDK at a fake server client. */
94
+ export function _setClient(c) { client = c; }
95
+ function requireInit() {
96
+ if (!localOnly && !client) {
97
+ throw new Error("Call agentfences.init() before using Fences. For local usage: init({ localOnly: true })");
98
+ }
99
+ }
100
+ function warnUnreachable(run, err) {
101
+ if (run.warned)
102
+ return;
103
+ run.warned = true;
104
+ console.warn(`agentfences: Fences backend unreachable for run ${run.runId} (${err}); enforcing limits locally`);
105
+ }
106
+ // ── governed ──────────────────────────────────────────────────────────────────
107
+ /**
108
+ * Make each call of fn a governed run with these limits:
109
+ *
110
+ * const research = governed(async (q: string) => { ... }, { budgetUsd: 0.5, maxIterations: 20 });
111
+ *
112
+ * Every limit is optional. On Fences Cloud, a limit left out comes from the agent's page in
113
+ * the dashboard. The run is named after the function (or `name`). checkpoint() never throws:
114
+ * a crossed limit comes back as a breached result, so the agent can wrap up gracefully.
115
+ */
116
+ export function governed(fn, options = {}) {
117
+ const name = options.name ?? (fn.name || "agent");
118
+ return async (...args) => {
119
+ requireInit();
120
+ const run = await startRun(name, options);
121
+ let error, failed = false;
122
+ try {
123
+ return await store.run(run, () => fn(...args));
124
+ }
125
+ catch (e) {
126
+ [error, failed] = [e, true];
127
+ throw e;
128
+ }
129
+ finally {
130
+ await endRun(run, ...outcome(run, failed, error));
131
+ }
132
+ };
133
+ }
134
+ function outcome(run, failed, error) {
135
+ if (!failed)
136
+ return [run.lastBreach ? "breached" : "success", null];
137
+ if (error instanceof FencesStop)
138
+ return ["breached", null]; // a framework integration halted the agent
139
+ run.exception = events.exceptionInfo(error);
140
+ const e = run.exception;
141
+ return ["error", e.message ? `${e.type}: ${e.message}` : e.type];
142
+ }
143
+ const LIMIT_KEYS = { budget_usd: "budgetUsd", max_iterations: "maxIterations", max_duration_ms: "maxDurationMs", max_tokens: "maxTokens" };
144
+ async function startRun(agentName, limits) {
145
+ const run = new Run(agentName, limits.budgetUsd ?? null, limits.maxIterations ?? DEFAULTS.maxIterations, limits.maxDurationMs ?? DEFAULTS.maxDurationMs, limits.maxTokens ?? DEFAULTS.maxTokens, events.currentContext());
146
+ if (!client) {
147
+ if (run.budgetUsd === null)
148
+ noBudget(agentName, "Set one with governed(fn, { budgetUsd: ... }).");
149
+ return run;
150
+ }
151
+ // The run must start even if redaction drops its context: limits depend on it
152
+ const sent = Object.keys(run.context).length
153
+ ? events.redact({ type: "run_start", agent_name: agentName, context: { ...run.context } }) : null;
154
+ // Limits left out in code go as null: the server fills them from the agent's dashboard settings
155
+ const body = { run_id: run.runId, agent_name: agentName, budget_usd: limits.budgetUsd ?? null,
156
+ max_iterations: limits.maxIterations ?? null, max_duration_ms: limits.maxDurationMs ?? null, max_tokens: limits.maxTokens ?? null };
157
+ if (sent?.context)
158
+ body.context = sent.context;
159
+ const resp = await client.startRun(body);
160
+ if (resp.quarantined)
161
+ throw new AgentQuarantined(agentName, resp.detail);
162
+ // The server caps limits at the agent's ceilings: enforce the same numbers if it becomes unreachable later
163
+ for (const [wire, field] of Object.entries(LIMIT_KEYS)) {
164
+ const v = resp.limits?.[wire];
165
+ if (typeof v === "number" || (wire === "budget_usd" && v === null))
166
+ run[field] = v; // eslint-disable-line @typescript-eslint/no-explicit-any
167
+ }
168
+ for (const n of resp.notices ?? []) {
169
+ events.warnOnce(`${agentName}:${n.code}`, `${n.message ?? ""}${n.url ? ` ${n.url}` : ""}`);
170
+ }
171
+ if (!("limits" in resp) && run.budgetUsd === null && !resp.network_error) {
172
+ noBudget(agentName, "Set one on its page in the Fences dashboard.");
173
+ }
174
+ if (resp.network_error)
175
+ warnUnreachable(run, resp.network_error);
176
+ return run;
177
+ }
178
+ function noBudget(agentName, how) {
179
+ events.warnOnce(`${agentName}:no_budget`, `${agentName} has no budget, so its spend isn't capped. ${how}`);
180
+ }
181
+ async function endRun(run, status, error) {
182
+ if (!client)
183
+ return;
184
+ // The run must end even if redaction drops its error details
185
+ const sent = error ? events.redact({ type: "run_end", status, error, exception: run.exception }) : null;
186
+ const body = { status, error: sent?.error ?? null };
187
+ if (sent?.exception)
188
+ body.exception = sent.exception;
189
+ await client.endRun(run.runId, body);
190
+ }
191
+ /**
192
+ * Record one step, then check every limit. Pass the model's response and Fences works out
193
+ * the cost from its model and token usage:
194
+ *
195
+ * const response = await openai.chat.completions.create({ ... });
196
+ * const result = await checkpoint(response);
197
+ * if (result.breached) return result.message;
198
+ *
199
+ * Without a response: checkpoint(null, { costDeltaUsd: 0.02, tokensUsed: 450 }).
200
+ * Never throws for a breach. Outside a governed run it does nothing.
201
+ */
202
+ export async function checkpoint(response, extra = {}) {
203
+ const run = getActiveRun();
204
+ if (!run)
205
+ return okResult();
206
+ if (typeof response === "number") {
207
+ throw new TypeError("checkpoint() takes the model's response first; pass a cost as checkpoint(null, { costDeltaUsd })");
208
+ }
209
+ const [cost, tokens] = response === undefined || response === null ? [0, 0] : costOf(response);
210
+ const stepCost = cost + (extra.costDeltaUsd ?? 0), stepTokens = tokens + (extra.tokensUsed ?? 0);
211
+ run.costUsd += stepCost;
212
+ run.tokensUsed += stepTokens;
213
+ run.iterations += 1;
214
+ const resp = client ? await client.checkpoint(run.runId, {
215
+ cost_delta_usd: stepCost, tokens_used: stepTokens, iterations: run.iterations, duration_ms: run.durationMs
216
+ }) : null;
217
+ return decide(run, resp);
218
+ }
219
+ function localBreach(run) {
220
+ // Limits trip only once exceeded, same rule as the server; round away float drift (0.02*5 != 0.10)
221
+ if (run.budgetUsd !== null && Math.round(run.costUsd * 1e9) / 1e9 > run.budgetUsd)
222
+ return "budget_exceeded";
223
+ if (run.iterations > run.maxIterations)
224
+ return "iteration_limit";
225
+ if (run.durationMs > run.maxDurationMs)
226
+ return "time_limit";
227
+ if (run.maxTokens > 0 && run.tokensUsed > run.maxTokens)
228
+ return "token_limit";
229
+ return null;
230
+ }
231
+ function decide(run, resp) {
232
+ if (!resp || resp.network_error) {
233
+ if (resp) {
234
+ warnUnreachable(run, resp.network_error);
235
+ if (failClosed) {
236
+ run.lastBreach = "fences_unreachable";
237
+ return breachResult(run.lastBreach, run);
238
+ }
239
+ }
240
+ run.lastBreach = localBreach(run);
241
+ }
242
+ else {
243
+ // The server is authoritative: adopt its totals and (on a breach) its current limits
244
+ run.costUsd = resp.spent_usd ?? run.costUsd;
245
+ run.iterations = resp.iterations ?? run.iterations;
246
+ run.tokensUsed = resp.tokens_used ?? run.tokensUsed;
247
+ for (const [wire, field] of Object.entries(LIMIT_KEYS)) {
248
+ if (wire in resp)
249
+ run[field] = resp[wire]; // eslint-disable-line @typescript-eslint/no-explicit-any
250
+ }
251
+ // A fresh breach, or a 409 because the run is still fenced from an earlier one
252
+ run.lastBreach = resp.ok ? null : (resp.breach ?? run.lastBreach ?? "limit_reached");
253
+ }
254
+ return run.lastBreach ? breachResult(run.lastBreach, run) : okResult(run.costUsd);
255
+ }
256
+ // ── Decision trail and spans ──────────────────────────────────────────────────
257
+ const SPAN_BATCH = 50;
258
+ const outbox = [];
259
+ let draining = null;
260
+ let spansSupported = true;
261
+ function enqueue(item) {
262
+ outbox.push(item);
263
+ draining ??= drain().finally(() => { draining = null; });
264
+ }
265
+ /** Decisions and spans go to the server in the background, in order; consecutive spans of a run go together. */
266
+ async function drain() {
267
+ while (outbox.length) {
268
+ const item = outbox.shift();
269
+ const c = client;
270
+ if (!c)
271
+ continue;
272
+ try {
273
+ if (item.kind === "decision") {
274
+ await c.logDecision(item.runId, item.body);
275
+ continue;
276
+ }
277
+ const batch = [item.span];
278
+ while (outbox[0]?.kind === "span" && outbox[0].runId === item.runId && batch.length < SPAN_BATCH) {
279
+ batch.push(outbox.shift().span);
280
+ }
281
+ if (spansSupported && (await c.logSpans(item.runId, batch)).unsupported) {
282
+ spansSupported = false;
283
+ console.warn("agentfences: this Fences server doesn't accept tool and model call spans yet; " +
284
+ "they're kept on the run locally but not sent");
285
+ }
286
+ }
287
+ catch { /* never let delivery break the agent */ }
288
+ }
289
+ }
290
+ /** Wait up to timeoutMs for queued decisions and spans to reach the server. Call before a serverless handler returns. */
291
+ export async function flush(timeoutMs = 5000) {
292
+ if (!draining)
293
+ return;
294
+ await Promise.race([draining, new Promise((r) => setTimeout(r, timeoutMs).unref())]);
295
+ }
296
+ /** Record the agent's reasoning at this step. Does nothing outside a governed run. */
297
+ export function logDecision(reasoning, action = null) {
298
+ const run = getActiveRun();
299
+ if (!run)
300
+ return;
301
+ run.decisions.push({ timestamp: Date.now() / 1000, iteration: run.iterations, reasoning, action });
302
+ if (!client)
303
+ return;
304
+ const sent = events.redact({ type: "decision", reasoning, action });
305
+ if (sent && typeof sent.reasoning === "string" && sent.reasoning) {
306
+ enqueue({ kind: "decision", runId: run.runId,
307
+ body: { iteration: run.iterations, reasoning: sent.reasoning, action: sent.action ?? null } });
308
+ }
309
+ }
310
+ /**
311
+ * Add a tool or model call to the active run as an OpenTelemetry-shaped span (GenAI
312
+ * semantic conventions): kept on the run, and sent through the redact hook in cloud mode.
313
+ */
314
+ export function recordSpan(name, elapsedMs, attributes, error) {
315
+ const run = getActiveRun();
316
+ if (!run)
317
+ return;
318
+ const end = Date.now() / 1000;
319
+ const attrs = {};
320
+ for (const [k, v] of Object.entries(attributes))
321
+ if (v !== null && v !== undefined)
322
+ attrs[k] = v;
323
+ attrs["fences.iteration"] = run.iterations;
324
+ if (error !== undefined) {
325
+ attrs["error.type"] = events.errorType(error);
326
+ attrs["fences.error.message"] = events.errorText(error);
327
+ const status = error?.status; // the OpenAI and Anthropic JS SDKs put the HTTP status here
328
+ if (typeof status === "number")
329
+ attrs["http.response.status_code"] = status;
330
+ }
331
+ const span = { name, start_ts: end - elapsedMs / 1000, end_ts: end, status: error === undefined ? "ok" : "error", attributes: attrs };
332
+ run.events.push(span);
333
+ if (run.events.length > MAX_EVENTS_PER_RUN)
334
+ run.events.shift(); // a runaway agent can't eat memory
335
+ if (!client || !spansSupported)
336
+ return;
337
+ const sent = events.redact({ type: "span", ...span });
338
+ if (sent) {
339
+ const { type: _, ...redacted } = sent; // the type key is for the redact hook only
340
+ enqueue({ kind: "span", runId: run.runId, span: redacted });
341
+ }
342
+ }
@@ -0,0 +1,22 @@
1
+ import type { CheckpointResult } from "./core.js";
2
+ export declare class FencesError extends Error {
3
+ constructor(message: string);
4
+ }
5
+ /** The Fences server rejected the API key. Thrown so a bad key is noticed, not silently ignored. */
6
+ export declare class FencesAuthError extends FencesError {
7
+ }
8
+ /** A governed function was called for an agent quarantined in the dashboard. No agent code ran. */
9
+ export declare class AgentQuarantined extends FencesError {
10
+ readonly agentName: string;
11
+ constructor(agentName: string, detail?: string);
12
+ }
13
+ /**
14
+ * Thrown by framework integrations when a limit is crossed, because a framework callback
15
+ * can't hand a result back to the agent. Carries the same fields as a breached CheckpointResult.
16
+ */
17
+ export declare class FencesStop extends FencesError {
18
+ readonly result: CheckpointResult;
19
+ readonly breachType: string | null;
20
+ readonly systemPrompt: string;
21
+ constructor(result: CheckpointResult);
22
+ }
package/dist/errors.js ADDED
@@ -0,0 +1,33 @@
1
+ export class FencesError extends Error {
2
+ constructor(message) {
3
+ super(message);
4
+ this.name = new.target.name;
5
+ }
6
+ }
7
+ /** The Fences server rejected the API key. Thrown so a bad key is noticed, not silently ignored. */
8
+ export class FencesAuthError extends FencesError {
9
+ }
10
+ /** A governed function was called for an agent quarantined in the dashboard. No agent code ran. */
11
+ export class AgentQuarantined extends FencesError {
12
+ agentName;
13
+ constructor(agentName, detail) {
14
+ super(`Agent '${agentName}' is quarantined in Fences, so this run was not started` +
15
+ (detail && detail !== "quarantined" ? `: ${detail}` : ""));
16
+ this.agentName = agentName;
17
+ }
18
+ }
19
+ /**
20
+ * Thrown by framework integrations when a limit is crossed, because a framework callback
21
+ * can't hand a result back to the agent. Carries the same fields as a breached CheckpointResult.
22
+ */
23
+ export class FencesStop extends FencesError {
24
+ result;
25
+ breachType;
26
+ systemPrompt;
27
+ constructor(result) {
28
+ super(result.message);
29
+ this.result = result;
30
+ this.breachType = result.breachType;
31
+ this.systemPrompt = result.systemPrompt;
32
+ }
33
+ }
@@ -0,0 +1,38 @@
1
+ export declare const MAX_CONTEXT_KEYS = 32;
2
+ export declare const MAX_CONTEXT_VALUE = 256;
3
+ export declare const MAX_MESSAGE = 1000;
4
+ export declare const MAX_FRAMES = 15;
5
+ export type Event = {
6
+ type: string;
7
+ [key: string]: unknown;
8
+ };
9
+ export type RedactHook = (event: Event) => Event | null | undefined;
10
+ /** Log a warning once per key per process. */
11
+ export declare function warnOnce(key: string, message: string): void;
12
+ export declare function configure(environment?: string, release?: string, redact?: RedactHook): void;
13
+ /**
14
+ * Tag every run started inside fn, e.g. with the end user or session it serves:
15
+ *
16
+ * await context({ user_id: "u_123", session_id: "s_9" }, () => supportAgent(question))
17
+ *
18
+ * Calls nest; inner values win. Values are stored as strings (up to 256 characters,
19
+ * 32 keys); null and undefined leave a key out.
20
+ */
21
+ export declare function context<T>(values: Record<string, unknown>, fn: () => T): T;
22
+ export declare function currentContext(): Record<string, string>;
23
+ /** `Type: message`, or just the type when there's no message. */
24
+ export declare function errorText(e: unknown): string;
25
+ export declare function errorType(e: unknown): string;
26
+ export interface ExceptionInfo {
27
+ type: string;
28
+ message: string;
29
+ stack: string[];
30
+ }
31
+ /** Type, message and the last frames of the stack (innermost last), for the run's error report. */
32
+ export declare function exceptionInfo(e: unknown): ExceptionInfo;
33
+ /**
34
+ * Pass an outgoing event through the user's redact hook. Returns the event to send, or
35
+ * null to drop it. If the hook throws or returns something else, the event is dropped:
36
+ * never send unredacted data, and never crash the agent.
37
+ */
38
+ export declare function redact(event: Event): Event | null;
package/dist/events.js ADDED
@@ -0,0 +1,102 @@
1
+ /**
2
+ * What a run carries besides numbers: its context (environment, release, user, session),
3
+ * the error that ended it, and the redact hook every outgoing event passes through.
4
+ */
5
+ import { AsyncLocalStorage } from "node:async_hooks";
6
+ export const MAX_CONTEXT_KEYS = 32;
7
+ export const MAX_CONTEXT_VALUE = 256;
8
+ export const MAX_MESSAGE = 1000;
9
+ export const MAX_FRAMES = 15;
10
+ const processContext = {}; // environment / release, from init()
11
+ const contextStore = new AsyncLocalStorage();
12
+ let redactHook;
13
+ let redactFailed = false;
14
+ const warned = new Set();
15
+ /** Log a warning once per key per process. */
16
+ export function warnOnce(key, message) {
17
+ if (warned.has(key))
18
+ return;
19
+ warned.add(key);
20
+ console.warn(`agentfences: ${message}`);
21
+ }
22
+ export function configure(environment, release, redact) {
23
+ for (const k of Object.keys(processContext))
24
+ delete processContext[k];
25
+ if (environment)
26
+ processContext.environment = environment.slice(0, MAX_CONTEXT_VALUE);
27
+ if (release)
28
+ processContext.release = release.slice(0, MAX_CONTEXT_VALUE);
29
+ redactHook = redact;
30
+ redactFailed = false;
31
+ }
32
+ /**
33
+ * Tag every run started inside fn, e.g. with the end user or session it serves:
34
+ *
35
+ * await context({ user_id: "u_123", session_id: "s_9" }, () => supportAgent(question))
36
+ *
37
+ * Calls nest; inner values win. Values are stored as strings (up to 256 characters,
38
+ * 32 keys); null and undefined leave a key out.
39
+ */
40
+ export function context(values, fn) {
41
+ const merged = { ...(contextStore.getStore() ?? {}) };
42
+ for (const [k, v] of Object.entries(values)) {
43
+ if (v !== null && v !== undefined)
44
+ merged[k] = String(v).slice(0, MAX_CONTEXT_VALUE);
45
+ }
46
+ if (Object.keys(merged).length > MAX_CONTEXT_KEYS) {
47
+ throw new RangeError(`agentfences.context() takes at most ${MAX_CONTEXT_KEYS} keys`);
48
+ }
49
+ return contextStore.run(merged, fn);
50
+ }
51
+ export function currentContext() {
52
+ return { ...processContext, ...(contextStore.getStore() ?? {}) };
53
+ }
54
+ /** `Type: message`, or just the type when there's no message. */
55
+ export function errorText(e) {
56
+ const type = errorType(e);
57
+ const message = e instanceof Error ? e.message : e === undefined ? "" : String(e);
58
+ return (message ? `${type}: ${message}` : type).slice(0, MAX_MESSAGE);
59
+ }
60
+ export function errorType(e) {
61
+ if (e instanceof Error)
62
+ return e.name && e.name !== "Error" ? e.name : e.constructor.name;
63
+ return typeof e;
64
+ }
65
+ /** Type, message and the last frames of the stack (innermost last), for the run's error report. */
66
+ export function exceptionInfo(e) {
67
+ const frames = e instanceof Error && e.stack
68
+ ? e.stack.split("\n").slice(1).map((l) => l.trim()).filter((l) => l.startsWith("at ")).map((l) => {
69
+ const m = l.match(/^at (?:(.+?) \()?(.+?):(\d+):\d+\)?$/); // "at fn (file:line:col)" or "at file:line:col"
70
+ return m ? `${m[2]}:${m[3]} in ${m[1] ?? "<anonymous>"}` : l.slice(3);
71
+ })
72
+ : [];
73
+ return {
74
+ type: errorType(e),
75
+ message: (e instanceof Error ? e.message : String(e)).slice(0, MAX_MESSAGE),
76
+ stack: frames.reverse().slice(-MAX_FRAMES),
77
+ };
78
+ }
79
+ /**
80
+ * Pass an outgoing event through the user's redact hook. Returns the event to send, or
81
+ * null to drop it. If the hook throws or returns something else, the event is dropped:
82
+ * never send unredacted data, and never crash the agent.
83
+ */
84
+ export function redact(event) {
85
+ if (!redactHook)
86
+ return event;
87
+ try {
88
+ const out = redactHook(structuredClone(event));
89
+ if (out === null || out === undefined)
90
+ return null;
91
+ if (typeof out !== "object" || Array.isArray(out))
92
+ throw new TypeError(`returned ${typeof out}, not an object or null`);
93
+ return out;
94
+ }
95
+ catch (e) {
96
+ if (!redactFailed) {
97
+ redactFailed = true;
98
+ console.warn(`agentfences: redact hook failed (${errorText(e)}); dropping events it fails on`);
99
+ }
100
+ return null;
101
+ }
102
+ }
@@ -0,0 +1,9 @@
1
+ export { init, governed, checkpoint, logDecision, flush, getActiveRun, Run } from "./core.js";
2
+ export type { CheckpointResult, InitOptions, Limits, Extra, Span } from "./core.js";
3
+ export { context } from "./events.js";
4
+ export type { RedactHook, Event } from "./events.js";
5
+ export { tool } from "./tools.js";
6
+ export { priceOf } from "./pricing.js";
7
+ export type { CustomPrice } from "./pricing.js";
8
+ export { FencesError, FencesAuthError, AgentQuarantined, FencesStop } from "./errors.js";
9
+ export { VERSION } from "./version.js";
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ export { init, governed, checkpoint, logDecision, flush, getActiveRun, Run } from "./core.js";
2
+ export { context } from "./events.js";
3
+ export { tool } from "./tools.js";
4
+ export { priceOf } from "./pricing.js";
5
+ export { FencesError, FencesAuthError, AgentQuarantined, FencesStop } from "./errors.js";
6
+ export { VERSION } from "./version.js";
@@ -0,0 +1,11 @@
1
+ export declare const PRICES: Record<string, PriceEntry>;
2
+ export interface Rates {
3
+ in: number;
4
+ out: number;
5
+ cache_read?: number;
6
+ cache_write?: number;
7
+ }
8
+ export interface PriceEntry extends Rates {
9
+ above?: number;
10
+ tier?: Rates;
11
+ }
@@ -0,0 +1,3 @@
1
+ // Generated by scripts/prices.mjs from sdk/agentfences/prices.json. Do not edit.
2
+ // LiteLLM model_prices_and_context_window.json, MIT License, Copyright (c) 2023 Berri AI, fetched 2026-10-09
3
+ export const PRICES = { "chat-latest": { "in": 0.000005, "out": 0.00003, "cache_read": 5e-7 }, "claude-fable-5": { "in": 0.00001, "out": 0.00005, "cache_read": 0.000001, "cache_write": 0.0000125 }, "claude-fable-5-1": { "in": 0.00001, "out": 0.00005, "cache_read": 2.5e-7, "cache_write": 0.0000125 }, "claude-haiku-4-5": { "in": 0.000001, "out": 0.000005, "cache_read": 1e-7, "cache_write": 0.00000125 }, "claude-haiku-4-5-20251001": { "in": 0.000001, "out": 0.000005, "cache_read": 1e-7, "cache_write": 0.00000125 }, "claude-haiku-5-5": { "in": 1e-7, "out": 5e-7, "cache_read": 1e-8, "cache_write": 1.25e-7 }, "claude-mythos-5": { "in": 0.00001, "out": 0.00005, "cache_read": 0.000001, "cache_write": 0.0000125 }, "claude-mythos-5-1": { "in": 0.00001, "out": 0.00005, "cache_read": 2.5e-7, "cache_write": 0.0000125 }, "claude-mythos-preview": { "in": 0.00001, "out": 0.00005, "cache_read": 0.000001, "cache_write": 0.0000125 }, "claude-opus-4-5": { "in": 0.000005, "out": 0.000025, "cache_read": 5e-7, "cache_write": 0.00000625 }, "claude-opus-4-5-20251101": { "in": 0.000005, "out": 0.000025, "cache_read": 5e-7, "cache_write": 0.00000625 }, "claude-opus-4-6": { "in": 0.000005, "out": 0.000025, "cache_read": 5e-7, "cache_write": 0.00000625 }, "claude-opus-4-6-20260205": { "in": 0.000005, "out": 0.000025, "cache_read": 5e-7, "cache_write": 0.00000625 }, "claude-opus-4-7": { "in": 0.000005, "out": 0.000025, "cache_read": 5e-7, "cache_write": 0.00000625 }, "claude-opus-4-7-20260416": { "in": 0.000005, "out": 0.000025, "cache_read": 5e-7, "cache_write": 0.00000625 }, "claude-opus-4-8": { "in": 0.000005, "out": 0.000025, "cache_read": 5e-7, "cache_write": 0.00000625 }, "claude-opus-5": { "in": 0.000005, "out": 0.000025, "cache_read": 5e-7, "cache_write": 0.00000625 }, "claude-opus-5-5": { "in": 0.000004, "out": 0.00002, "cache_read": 2e-7, "cache_write": 0.000005 }, "claude-sonnet-4-5": { "in": 0.000003, "out": 0.000015, "cache_read": 3e-7, "cache_write": 0.00000375 }, "claude-sonnet-4-5-20250929": { "in": 0.000003, "out": 0.000015, "cache_read": 3e-7, "cache_write": 0.00000375 }, "claude-sonnet-4-6": { "in": 0.000003, "out": 0.000015, "cache_read": 3e-7, "cache_write": 0.00000375 }, "claude-sonnet-5": { "in": 0.000002, "out": 0.00001, "cache_read": 2e-7, "cache_write": 0.0000025 }, "claude-sonnet-5-5": { "in": 0.000002, "out": 0.00001, "cache_read": 1e-7, "cache_write": 0.0000025 }, "codestral-2508": { "in": 3e-7, "out": 9e-7, "cache_read": 3e-8 }, "codestral-latest": { "in": 3e-7, "out": 9e-7, "cache_read": 3e-8 }, "codestral-mamba-latest": { "in": 2.5e-7, "out": 2.5e-7, "cache_read": 2.5e-8 }, "computer-use-preview": { "in": 0.000003, "out": 0.000012 }, "daybreak-blue-latest": { "in": 0.000004, "out": 0.00002, "cache_read": 4e-7, "cache_write": 0.000005, "above": 272000, "tier": { "in": 0.000008, "out": 0.00003, "cache_read": 8e-7, "cache_write": 0.00001 } }, "daybreak-red-latest": { "in": 0.0000125, "out": 0.000075, "cache_read": 0.00000125, "cache_write": 0.000015625, "above": 272000, "tier": { "in": 0.000025, "out": 0.0001125, "cache_read": 0.0000025, "cache_write": 0.00003125 } }, "deep-research-max-preview-04-2026": { "in": 0.000002, "out": 0.000012 }, "deep-research-preview-04-2026": { "in": 0.000002, "out": 0.000012 }, "deep-research-pro-preview-12-2025": { "in": 0.000002, "out": 0.000012 }, "deepseek-chat": { "in": 2.8e-7, "out": 4.2e-7, "cache_read": 2.8e-8 }, "deepseek-coder": { "in": 1.4e-7, "out": 2.8e-7, "cache_read": 1.4e-8 }, "deepseek-flash": { "in": 3e-7, "out": 0.0000012, "cache_read": 6e-9, "cache_write": 0 }, "deepseek-r1": { "in": 5.5e-7, "out": 0.00000219, "cache_read": 1.4e-7 }, "deepseek-reasoner": { "in": 2.8e-7, "out": 4.2e-7, "cache_read": 2.8e-8 }, "deepseek-v3": { "in": 2.7e-7, "out": 0.0000011, "cache_read": 7e-8, "cache_write": 0 }, "deepseek-v3.2": { "in": 2.8e-7, "out": 4e-7, "cache_read": 2.8e-8 }, "deepseek-v4-flash": { "in": 3e-7, "out": 0.0000012, "cache_read": 6e-9, "cache_write": 0 }, "deepseek-v4-flash-vision-exp": { "in": 3e-7, "out": 0.0000012, "cache_read": 6e-9, "cache_write": 0 }, "deepseek-v4-pro": { "in": 0.00000132, "out": 0.00000396, "cache_read": 4.4e-8, "cache_write": 0 }, "devstral-latest": { "in": 4e-7, "out": 0.000002, "cache_read": 4e-8 }, "devstral-medium-latest": { "in": 4e-7, "out": 0.000002, "cache_read": 4e-8 }, "devstral-small-latest": { "in": 1e-7, "out": 3e-7, "cache_read": 1e-8 }, "ft:gpt-3.5-turbo": { "in": 0.000003, "out": 0.000006 }, "ft:gpt-3.5-turbo-0125": { "in": 0.000003, "out": 0.000006 }, "ft:gpt-3.5-turbo-0613": { "in": 0.000003, "out": 0.000006 }, "ft:gpt-3.5-turbo-1106": { "in": 0.000003, "out": 0.000006 }, "ft:gpt-4-0613": { "in": 0.00003, "out": 0.00006 }, "ft:gpt-4.1-2025-04-14": { "in": 0.000003, "out": 0.000012, "cache_read": 7.5e-7 }, "ft:gpt-4.1-mini-2025-04-14": { "in": 8e-7, "out": 0.0000032, "cache_read": 2e-7 }, "ft:gpt-4.1-nano-2025-04-14": { "in": 2e-7, "out": 8e-7, "cache_read": 5e-8 }, "ft:gpt-4o-2024-08-06": { "in": 0.00000375, "out": 0.000015, "cache_read": 0.000001875 }, "ft:gpt-4o-2024-11-20": { "in": 0.00000375, "out": 0.000015, "cache_write": 0.000001875 }, "ft:gpt-4o-mini-2024-07-18": { "in": 3e-7, "out": 0.0000012, "cache_read": 1.5e-7 }, "ft:o4-mini-2025-04-16": { "in": 0.000004, "out": 0.000016, "cache_read": 0.000001 }, "gemini-2.5-computer-use-preview-10-2025": { "in": 0.00000125, "out": 0.00001, "above": 200000, "tier": { "in": 0.0000025, "out": 0.000015 } }, "gemini-2.5-flash": { "in": 3e-7, "out": 0.0000025, "cache_read": 3e-8 }, "gemini-2.5-flash-lite": { "in": 1e-7, "out": 4e-7, "cache_read": 1e-8 }, "gemini-2.5-flash-lite-preview-09-2025": { "in": 1e-7, "out": 4e-7, "cache_read": 1e-8 }, "gemini-2.5-flash-preview-09-2025": { "in": 3e-7, "out": 0.0000025, "cache_read": 3e-8 }, "gemini-2.5-pro": { "in": 0.00000125, "out": 0.00001, "cache_read": 1.25e-7, "above": 200000, "tier": { "in": 0.0000025, "out": 0.000015, "cache_read": 2.5e-7 } }, "gemini-2.5-pro-preview-tts": { "in": 0.000001, "out": 0.00002, "cache_read": 1.25e-7 }, "gemini-3-flash-preview": { "in": 5e-7, "out": 0.000003, "cache_read": 5e-8 }, "gemini-3.1-flash-lite": { "in": 2.5e-7, "out": 0.0000015, "cache_read": 2.5e-8 }, "gemini-3.1-flash-lite-preview": { "in": 2.5e-7, "out": 0.0000015, "cache_read": 2.5e-8 }, "gemini-3.1-pro-preview": { "in": 0.000002, "out": 0.000012, "cache_read": 2e-7, "above": 200000, "tier": { "in": 0.000004, "out": 0.000018, "cache_read": 4e-7 } }, "gemini-3.1-pro-preview-customtools": { "in": 0.000002, "out": 0.000012, "cache_read": 2e-7, "above": 200000, "tier": { "in": 0.000004, "out": 0.000018, "cache_read": 4e-7 } }, "gemini-3.5-flash": { "in": 0.0000015, "out": 0.000009, "cache_read": 1.5e-7 }, "gemini-3.5-flash-lite": { "in": 3e-7, "out": 0.0000025, "cache_read": 3e-8 }, "gemini-3.6-flash": { "in": 7.5e-7, "out": 0.00000375, "cache_read": 7.5e-8 }, "gemini-3.7-flash": { "in": 7.5e-7, "out": 0.00000375, "cache_read": 7.5e-8 }, "gemini-3.8-flash": { "in": 7.5e-7, "out": 0.00000375, "cache_read": 7.5e-8 }, "gemini-3.8-flash-cyber": { "in": 0.0000015, "out": 0.0000075, "cache_read": 1.5e-7 }, "gemini-exp-1114": { "in": 0, "out": 0 }, "gemini-exp-1206": { "in": 0, "out": 0 }, "gemini-flash-latest": { "in": 7.5e-7, "out": 0.00000375, "cache_read": 7.5e-8 }, "gemini-flash-lite-latest": { "in": 3e-7, "out": 0.0000025, "cache_read": 3e-8 }, "gemini-gemma-2-27b-it": { "in": 3.5e-7, "out": 0.00000105 }, "gemini-gemma-2-9b-it": { "in": 3.5e-7, "out": 0.00000105 }, "gemini-omni-1.1-flash": { "in": 0.0000015, "out": 0.000009 }, "gemini-omni-flash-preview": { "in": 0.0000015, "out": 0.000009 }, "gemini-pro-latest": { "in": 0.000002, "out": 0.000012, "cache_read": 2e-7, "above": 200000, "tier": { "in": 0.000004, "out": 0.000018, "cache_read": 4e-7 } }, "gemini-robotics-er-1.5-preview": { "in": 3e-7, "out": 0.0000025, "cache_read": 0 }, "gemini-robotics-er-2-preview": { "in": 0.000001, "out": 0.000005, "cache_read": 1e-7 }, "gemini-robotics-er-2-streaming-preview": { "in": 0.000001, "out": 0.000005 }, "gemma-3-27b-it": { "in": 0, "out": 0 }, "gemma-4-26b-a4b-it": { "in": 0, "out": 0 }, "gemma-4-31b-it": { "in": 0, "out": 0 }, "glm-5-2": { "in": 0.0000014, "out": 0.0000044, "cache_read": 1.4e-7 }, "gpt-3.5-turbo": { "in": 5e-7, "out": 0.0000015 }, "gpt-3.5-turbo-0125": { "in": 5e-7, "out": 0.0000015 }, "gpt-3.5-turbo-1106": { "in": 0.000001, "out": 0.000002 }, "gpt-3.5-turbo-16k": { "in": 0.000003, "out": 0.000004 }, "gpt-4": { "in": 0.00003, "out": 0.00006 }, "gpt-4-0613": { "in": 0.00003, "out": 0.00006 }, "gpt-4-1106-preview": { "in": 0.00001, "out": 0.00003 }, "gpt-4-turbo": { "in": 0.00001, "out": 0.00003 }, "gpt-4-turbo-2024-04-09": { "in": 0.00001, "out": 0.00003 }, "gpt-4.1": { "in": 0.000002, "out": 0.000008, "cache_read": 5e-7 }, "gpt-4.1-2025-04-14": { "in": 0.000002, "out": 0.000008, "cache_read": 5e-7 }, "gpt-4.1-mini": { "in": 4e-7, "out": 0.0000016, "cache_read": 1e-7 }, "gpt-4.1-mini-2025-04-14": { "in": 4e-7, "out": 0.0000016, "cache_read": 1e-7 }, "gpt-4.1-nano": { "in": 1e-7, "out": 4e-7, "cache_read": 2.5e-8 }, "gpt-4.1-nano-2025-04-14": { "in": 1e-7, "out": 4e-7, "cache_read": 2.5e-8 }, "gpt-4o": { "in": 0.0000025, "out": 0.00001, "cache_read": 0.00000125 }, "gpt-4o-2024-05-13": { "in": 0.000005, "out": 0.000015 }, "gpt-4o-2024-08-06": { "in": 0.0000025, "out": 0.00001, "cache_read": 0.00000125 }, "gpt-4o-2024-11-20": { "in": 0.0000025, "out": 0.00001, "cache_read": 0.00000125 }, "gpt-4o-audio-preview-2024-12-17": { "in": 0.0000025, "out": 0.00001 }, "gpt-4o-audio-preview-2025-06-03": { "in": 0.0000025, "out": 0.00001 }, "gpt-4o-mini": { "in": 1.5e-7, "out": 6e-7, "cache_read": 7.5e-8 }, "gpt-4o-mini-2024-07-18": { "in": 1.5e-7, "out": 6e-7, "cache_read": 7.5e-8 }, "gpt-4o-mini-audio-preview-2024-12-17": { "in": 1.5e-7, "out": 6e-7 }, "gpt-4o-mini-search-preview": { "in": 1.5e-7, "out": 6e-7, "cache_read": 7.5e-8 }, "gpt-4o-search-preview": { "in": 0.0000025, "out": 0.00001, "cache_read": 0.00000125 }, "gpt-5": { "in": 0.00000125, "out": 0.00001, "cache_read": 1.25e-7 }, "gpt-5-2025-08-07": { "in": 0.00000125, "out": 0.00001, "cache_read": 1.25e-7 }, "gpt-5-chat": { "in": 0.00000125, "out": 0.00001, "cache_read": 1.25e-7 }, "gpt-5-chat-latest": { "in": 0.00000125, "out": 0.00001, "cache_read": 1.25e-7 }, "gpt-5-codex": { "in": 0.00000125, "out": 0.00001, "cache_read": 1.25e-7 }, "gpt-5-mini": { "in": 2.5e-7, "out": 0.000002, "cache_read": 2.5e-8 }, "gpt-5-mini-2025-08-07": { "in": 2.5e-7, "out": 0.000002, "cache_read": 2.5e-8 }, "gpt-5-nano": { "in": 5e-8, "out": 4e-7, "cache_read": 5e-9 }, "gpt-5-nano-2025-08-07": { "in": 5e-8, "out": 4e-7, "cache_read": 5e-9 }, "gpt-5-pro": { "in": 0.000015, "out": 0.00012 }, "gpt-5-pro-2025-10-06": { "in": 0.000015, "out": 0.00012 }, "gpt-5-search-api": { "in": 0.00000125, "out": 0.00001, "cache_read": 1.25e-7 }, "gpt-5-search-api-2025-10-14": { "in": 0.00000125, "out": 0.00001, "cache_read": 1.25e-7 }, "gpt-5.1": { "in": 0.00000125, "out": 0.00001, "cache_read": 1.25e-7 }, "gpt-5.1-2025-11-13": { "in": 0.00000125, "out": 0.00001, "cache_read": 1.25e-7 }, "gpt-5.1-chat-latest": { "in": 0.00000125, "out": 0.00001, "cache_read": 1.25e-7 }, "gpt-5.1-codex": { "in": 0.00000125, "out": 0.00001, "cache_read": 1.25e-7 }, "gpt-5.1-codex-max": { "in": 0.00000125, "out": 0.00001, "cache_read": 1.25e-7 }, "gpt-5.1-codex-mini": { "in": 2.5e-7, "out": 0.000002, "cache_read": 2.5e-8 }, "gpt-5.2": { "in": 0.00000175, "out": 0.000014, "cache_read": 1.75e-7 }, "gpt-5.2-2025-12-11": { "in": 0.00000175, "out": 0.000014, "cache_read": 1.75e-7 }, "gpt-5.2-chat-latest": { "in": 0.00000175, "out": 0.000014, "cache_read": 1.75e-7 }, "gpt-5.2-codex": { "in": 0.00000175, "out": 0.000014, "cache_read": 1.75e-7 }, "gpt-5.2-pro": { "in": 0.000021, "out": 0.000168 }, "gpt-5.2-pro-2025-12-11": { "in": 0.000021, "out": 0.000168 }, "gpt-5.3-chat-latest": { "in": 0.00000175, "out": 0.000014, "cache_read": 1.75e-7 }, "gpt-5.3-codex": { "in": 0.00000175, "out": 0.000014, "cache_read": 1.75e-7 }, "gpt-5.4": { "in": 0.0000025, "out": 0.000015, "cache_read": 2.5e-7, "above": 272000, "tier": { "in": 0.000005, "out": 0.0000225, "cache_read": 5e-7 } }, "gpt-5.4-2026-03-05": { "in": 0.0000025, "out": 0.000015, "cache_read": 2.5e-7, "above": 272000, "tier": { "in": 0.000005, "out": 0.0000225, "cache_read": 5e-7 } }, "gpt-5.4-mini": { "in": 7.5e-7, "out": 0.0000045, "cache_read": 7.5e-8 }, "gpt-5.4-mini-2026-03-17": { "in": 7.5e-7, "out": 0.0000045, "cache_read": 7.5e-8 }, "gpt-5.4-nano": { "in": 2e-7, "out": 0.00000125, "cache_read": 2e-8 }, "gpt-5.4-nano-2026-03-17": { "in": 2e-7, "out": 0.00000125, "cache_read": 2e-8 }, "gpt-5.4-pro": { "in": 0.00003, "out": 0.00018, "above": 272000, "tier": { "in": 0.00006, "out": 0.00027 } }, "gpt-5.4-pro-2026-03-05": { "in": 0.00003, "out": 0.00018, "above": 272000, "tier": { "in": 0.00006, "out": 0.00027 } }, "gpt-5.5": { "in": 0.000005, "out": 0.00003, "cache_read": 5e-7, "above": 272000, "tier": { "in": 0.00001, "out": 0.000045, "cache_read": 0.000001 } }, "gpt-5.5-2026-04-23": { "in": 0.000005, "out": 0.00003, "cache_read": 5e-7, "above": 272000, "tier": { "in": 0.00001, "out": 0.000045, "cache_read": 0.000001 } }, "gpt-5.5-cyber": { "in": 0.0000125, "out": 0.000075, "cache_read": 0.00000125 }, "gpt-5.5-pro": { "in": 0.00003, "out": 0.00018, "above": 272000, "tier": { "in": 0.00006, "out": 0.00027 } }, "gpt-5.5-pro-2026-04-23": { "in": 0.00003, "out": 0.00018, "above": 272000, "tier": { "in": 0.00006, "out": 0.00027 } }, "gpt-5.6": { "in": 0.000004, "out": 0.00002, "cache_read": 4e-7, "cache_write": 0.000005, "above": 272000, "tier": { "in": 0.000008, "out": 0.00003, "cache_read": 8e-7, "cache_write": 0.00001 } }, "gpt-5.6-cyber": { "in": 0.0000125, "out": 0.000075, "cache_read": 0.00000125, "cache_write": 0.000015625, "above": 272000, "tier": { "in": 0.000025, "out": 0.0001125, "cache_read": 0.0000025, "cache_write": 0.00003125 } }, "gpt-5.6-luna": { "in": 2e-7, "out": 0.0000012, "cache_read": 2e-8, "cache_write": 2.5e-7, "above": 272000, "tier": { "in": 4e-7, "out": 0.0000018, "cache_read": 4e-8, "cache_write": 5e-7 } }, "gpt-5.6-sol": { "in": 0.000004, "out": 0.00002, "cache_read": 4e-7, "cache_write": 0.000005, "above": 272000, "tier": { "in": 0.000008, "out": 0.00003, "cache_read": 8e-7, "cache_write": 0.00001 } }, "gpt-5.6-terra": { "in": 0.000002, "out": 0.000012, "cache_read": 2e-7, "cache_write": 0.0000025, "above": 272000, "tier": { "in": 0.000004, "out": 0.000018, "cache_read": 4e-7, "cache_write": 0.000005 } }, "gpt-6-astra": { "in": 0.00001, "out": 0.00005, "cache_read": 0.000001, "cache_write": 0.0000125, "above": 272000, "tier": { "in": 0.00002, "out": 0.000075, "cache_read": 0.000002, "cache_write": 0.000025 } }, "gpt-6-luna": { "in": 1e-7, "out": 5e-7, "cache_read": 1e-8, "cache_write": 1.25e-7, "above": 272000, "tier": { "in": 2e-7, "out": 7.5e-7, "cache_read": 2e-8, "cache_write": 2.5e-7 } }, "gpt-6-sol": { "in": 0.000002, "out": 0.00001, "cache_read": 2e-7, "cache_write": 0.0000025, "above": 272000, "tier": { "in": 0.000004, "out": 0.000015, "cache_read": 4e-7, "cache_write": 0.000005 } }, "gpt-6.1-sol": { "in": 0.000002, "out": 0.00001, "cache_read": 1e-7, "cache_write": 0.0000025, "above": 272000, "tier": { "in": 0.000004, "out": 0.000015, "cache_read": 2e-7, "cache_write": 0.000005 } }, "gpt-audio": { "in": 0.0000025, "out": 0.00001 }, "gpt-audio-1.5": { "in": 0.0000025, "out": 0.00001 }, "gpt-audio-2025-08-28": { "in": 0.0000025, "out": 0.00001 }, "gpt-audio-mini": { "in": 6e-7, "out": 0.0000024 }, "gpt-audio-mini-2025-12-15": { "in": 6e-7, "out": 0.0000024 }, "gpt-daybreak-blue-latest": { "in": 0.000004, "out": 0.00002, "cache_read": 4e-7, "cache_write": 0.000005, "above": 272000, "tier": { "in": 0.000008, "out": 0.00003, "cache_read": 8e-7, "cache_write": 0.00001 } }, "gpt-daybreak-red-latest": { "in": 0.0000125, "out": 0.000075, "cache_read": 0.00000125, "cache_write": 0.000015625, "above": 272000, "tier": { "in": 0.000025, "out": 0.0001125, "cache_read": 0.0000025, "cache_write": 0.00003125 } }, "gpt-oss-120b": { "in": 1.5e-7, "out": 6e-7, "cache_read": 7.5e-8 }, "gpt-oss-20b": { "in": 7.5e-8, "out": 3e-7, "cache_read": 3.75e-8 }, "gpt-oss-safeguard-20b": { "in": 7.5e-8, "out": 3e-7, "cache_read": 3.7e-8 }, "gpt-rosalind-discovery": { "in": 0.000005, "out": 0.000025, "cache_read": 5e-7 }, "gpt-rosalind-research": { "in": 0.000005, "out": 0.000025, "cache_read": 5e-7 }, "grok-4.20": { "in": 0.00000125, "out": 0.0000025, "cache_read": 2e-7, "above": 200000, "tier": { "in": 0.0000025, "out": 0.000005, "cache_read": 4e-7 } }, "grok-4.20-0309": { "in": 0.00000125, "out": 0.0000025, "cache_read": 2e-7, "above": 200000, "tier": { "in": 0.0000025, "out": 0.000005, "cache_read": 4e-7 } }, "grok-4.20-0309-non-reasoning": { "in": 0.00000125, "out": 0.0000025, "cache_read": 2e-7, "above": 200000, "tier": { "in": 0.0000025, "out": 0.000005, "cache_read": 4e-7 } }, "grok-4.20-0309-reasoning": { "in": 0.00000125, "out": 0.0000025, "cache_read": 2e-7, "above": 200000, "tier": { "in": 0.0000025, "out": 0.000005, "cache_read": 4e-7 } }, "grok-4.20-beta": { "in": 0.00000125, "out": 0.0000025, "cache_read": 2e-7, "above": 200000, "tier": { "in": 0.0000025, "out": 0.000005, "cache_read": 4e-7 } }, "grok-4.20-beta-0309": { "in": 0.00000125, "out": 0.0000025, "cache_read": 2e-7, "above": 200000, "tier": { "in": 0.0000025, "out": 0.000005, "cache_read": 4e-7 } }, "grok-4.20-beta-0309-non-reasoning": { "in": 0.00000125, "out": 0.0000025, "cache_read": 2e-7, "above": 200000, "tier": { "in": 0.0000025, "out": 0.000005, "cache_read": 4e-7 } }, "grok-4.20-beta-0309-reasoning": { "in": 0.00000125, "out": 0.0000025, "cache_read": 2e-7, "above": 200000, "tier": { "in": 0.0000025, "out": 0.000005, "cache_read": 4e-7 } }, "grok-4.20-beta-latest": { "in": 0.00000125, "out": 0.0000025, "cache_read": 2e-7, "above": 200000, "tier": { "in": 0.0000025, "out": 0.000005, "cache_read": 4e-7 } }, "grok-4.20-beta-latest-non-reasoning": { "in": 0.00000125, "out": 0.0000025, "cache_read": 2e-7, "above": 200000, "tier": { "in": 0.0000025, "out": 0.000005, "cache_read": 4e-7 } }, "grok-4.20-beta-latest-reasoning": { "in": 0.00000125, "out": 0.0000025, "cache_read": 2e-7, "above": 200000, "tier": { "in": 0.0000025, "out": 0.000005, "cache_read": 4e-7 } }, "grok-4.20-beta-non-reasoning": { "in": 0.00000125, "out": 0.0000025, "cache_read": 2e-7, "above": 200000, "tier": { "in": 0.0000025, "out": 0.000005, "cache_read": 4e-7 } }, "grok-4.20-beta-reasoning": { "in": 0.00000125, "out": 0.0000025, "cache_read": 2e-7, "above": 200000, "tier": { "in": 0.0000025, "out": 0.000005, "cache_read": 4e-7 } }, "grok-4.20-experimental-beta-0304": { "in": 0.00000125, "out": 0.0000025, "cache_read": 2e-7, "above": 200000, "tier": { "in": 0.0000025, "out": 0.000005, "cache_read": 4e-7 } }, "grok-4.20-experimental-beta-0304-non-reasoning": { "in": 0.00000125, "out": 0.0000025, "cache_read": 2e-7, "above": 200000, "tier": { "in": 0.0000025, "out": 0.000005, "cache_read": 4e-7 } }, "grok-4.20-experimental-beta-0304-reasoning": { "in": 0.00000125, "out": 0.0000025, "cache_read": 2e-7, "above": 200000, "tier": { "in": 0.0000025, "out": 0.000005, "cache_read": 4e-7 } }, "grok-4.20-experimental-beta-latest": { "in": 0.00000125, "out": 0.0000025, "cache_read": 2e-7, "above": 200000, "tier": { "in": 0.0000025, "out": 0.000005, "cache_read": 4e-7 } }, "grok-4.20-experimental-beta-non-reasoning-latest": { "in": 0.00000125, "out": 0.0000025, "cache_read": 2e-7, "above": 200000, "tier": { "in": 0.0000025, "out": 0.000005, "cache_read": 4e-7 } }, "grok-4.20-experimental-beta-reasoning-latest": { "in": 0.00000125, "out": 0.0000025, "cache_read": 2e-7, "above": 200000, "tier": { "in": 0.0000025, "out": 0.000005, "cache_read": 4e-7 } }, "grok-4.20-multi-agent": { "in": 0.00000125, "out": 0.0000025, "cache_read": 2e-7, "above": 200000, "tier": { "in": 0.0000025, "out": 0.000005, "cache_read": 4e-7 } }, "grok-4.20-multi-agent-0309": { "in": 0.00000125, "out": 0.0000025, "cache_read": 2e-7, "above": 200000, "tier": { "in": 0.0000025, "out": 0.000005, "cache_read": 4e-7 } }, "grok-4.20-multi-agent-beta-0309": { "in": 0.00000125, "out": 0.0000025, "cache_read": 2e-7, "above": 200000, "tier": { "in": 0.0000025, "out": 0.000005, "cache_read": 4e-7 } }, "grok-4.20-multi-agent-beta-latest": { "in": 0.00000125, "out": 0.0000025, "cache_read": 2e-7, "above": 200000, "tier": { "in": 0.0000025, "out": 0.000005, "cache_read": 4e-7 } }, "grok-4.20-multi-agent-experimental-beta-0304": { "in": 0.00000125, "out": 0.0000025, "cache_read": 2e-7, "above": 200000, "tier": { "in": 0.0000025, "out": 0.000005, "cache_read": 4e-7 } }, "grok-4.20-multi-agent-experimental-beta-latest": { "in": 0.00000125, "out": 0.0000025, "cache_read": 2e-7, "above": 200000, "tier": { "in": 0.0000025, "out": 0.000005, "cache_read": 4e-7 } }, "grok-4.20-multi-agent-latest": { "in": 0.00000125, "out": 0.0000025, "cache_read": 2e-7, "above": 200000, "tier": { "in": 0.0000025, "out": 0.000005, "cache_read": 4e-7 } }, "grok-4.20-non-reasoning": { "in": 0.00000125, "out": 0.0000025, "cache_read": 2e-7, "above": 200000, "tier": { "in": 0.0000025, "out": 0.000005, "cache_read": 4e-7 } }, "grok-4.20-non-reasoning-gv2": { "in": 0.00000125, "out": 0.0000025, "cache_read": 2e-7, "above": 200000, "tier": { "in": 0.0000025, "out": 0.000005, "cache_read": 4e-7 } }, "grok-4.20-non-reasoning-latest": { "in": 0.00000125, "out": 0.0000025, "cache_read": 2e-7, "above": 200000, "tier": { "in": 0.0000025, "out": 0.000005, "cache_read": 4e-7 } }, "grok-4.20-reasoning": { "in": 0.00000125, "out": 0.0000025, "cache_read": 2e-7, "above": 200000, "tier": { "in": 0.0000025, "out": 0.000005, "cache_read": 4e-7 } }, "grok-4.20-reasoning-gv2": { "in": 0.00000125, "out": 0.0000025, "cache_read": 2e-7, "above": 200000, "tier": { "in": 0.0000025, "out": 0.000005, "cache_read": 4e-7 } }, "grok-4.20-reasoning-latest": { "in": 0.00000125, "out": 0.0000025, "cache_read": 2e-7, "above": 200000, "tier": { "in": 0.0000025, "out": 0.000005, "cache_read": 4e-7 } }, "grok-4.3": { "in": 0.00000125, "out": 0.0000025, "cache_read": 2e-7, "above": 200000, "tier": { "in": 0.0000025, "out": 0.000005, "cache_read": 4e-7 } }, "grok-4.3-latest": { "in": 0.00000125, "out": 0.0000025, "cache_read": 2e-7, "above": 200000, "tier": { "in": 0.0000025, "out": 0.000005, "cache_read": 4e-7 } }, "grok-4.5": { "in": 0.000002, "out": 0.000006, "cache_read": 3e-7, "above": 200000, "tier": { "in": 0.000004, "out": 0.000012, "cache_read": 6e-7 } }, "grok-4.5-latest": { "in": 0.000002, "out": 0.000006, "cache_read": 3e-7, "above": 200000, "tier": { "in": 0.000004, "out": 0.000012, "cache_read": 6e-7 } }, "grok-4.6": { "in": 0.000002, "out": 0.000006, "cache_read": 5e-7, "above": 200000, "tier": { "in": 0.000004, "out": 0.000012, "cache_read": 0.000001 } }, "grok-4.7": { "in": 0.000002, "out": 0.000006, "cache_read": 5e-7, "above": 200000, "tier": { "in": 0.000004, "out": 0.000012, "cache_read": 0.000001 } }, "grok-build-0.1": { "in": 0.000001, "out": 0.000002, "cache_read": 2e-7, "above": 200000, "tier": { "in": 0.000002, "out": 0.000004, "cache_read": 4e-7 } }, "grok-build-latest": { "in": 0.000002, "out": 0.000006, "cache_read": 3e-7, "above": 200000, "tier": { "in": 0.000004, "out": 0.000012, "cache_read": 6e-7 } }, "grok-code-fast": { "in": 0.000001, "out": 0.000002, "cache_read": 2e-7, "above": 200000, "tier": { "in": 0.000002, "out": 0.000004, "cache_read": 4e-7 } }, "grok-code-fast-1": { "in": 0.000001, "out": 0.000002, "cache_read": 2e-7, "above": 200000, "tier": { "in": 0.000002, "out": 0.000004, "cache_read": 4e-7 } }, "grok-code-fast-1-0825": { "in": 0.000001, "out": 0.000002, "cache_read": 2e-7, "above": 200000, "tier": { "in": 0.000002, "out": 0.000004, "cache_read": 4e-7 } }, "labs-leanstral-1-5": { "in": 0, "out": 0 }, "labs-leanstral-1-5-1": { "in": 0, "out": 0 }, "learnlm-1.5-pro-experimental": { "in": 0, "out": 0 }, "llama-guard-3-8b": { "in": 2e-7, "out": 2e-7 }, "llama-prompt-guard-2-22m": { "in": 3e-8, "out": 3e-8 }, "llama-prompt-guard-2-86m": { "in": 4e-8, "out": 4e-8 }, "lyria-3-clip-preview": { "in": 0, "out": 0 }, "lyria-3-pro-preview": { "in": 0, "out": 0 }, "lyria-3.5": { "in": 0, "out": 0 }, "lyria-3.5-clip-preview": { "in": 0, "out": 0 }, "lyria-3.5-pro-preview": { "in": 0, "out": 0 }, "lyria-realtime-exp": { "in": 0, "out": 0 }, "magistral-medium-latest": { "in": 0.0000015, "out": 0.0000075, "cache_read": 1.5e-7 }, "magistral-small-latest": { "in": 1.5e-7, "out": 6e-7, "cache_read": 1.5e-8 }, "ministral-14b-2512": { "in": 2e-7, "out": 2e-7, "cache_read": 2e-8 }, "ministral-14b-latest": { "in": 2e-7, "out": 2e-7, "cache_read": 2e-8 }, "ministral-3-14b-2512": { "in": 2e-7, "out": 2e-7, "cache_read": 2e-8 }, "ministral-3-3b-2512": { "in": 1e-7, "out": 1e-7, "cache_read": 1e-8 }, "ministral-3-8b-2512": { "in": 1.5e-7, "out": 1.5e-7, "cache_read": 1.5e-8 }, "ministral-3b-2512": { "in": 1e-7, "out": 1e-7, "cache_read": 1e-8 }, "ministral-3b-latest": { "in": 1e-7, "out": 1e-7, "cache_read": 1e-8 }, "ministral-8b-2512": { "in": 1.5e-7, "out": 1.5e-7, "cache_read": 1.5e-8 }, "ministral-8b-latest": { "in": 1.5e-7, "out": 1.5e-7, "cache_read": 1.5e-8 }, "mistral-code-agent-latest": { "in": 4e-7, "out": 0.000002, "cache_read": 4e-8 }, "mistral-code-fim-latest": { "in": 3e-7, "out": 9e-7, "cache_read": 3e-8 }, "mistral-code-latest": { "in": 3e-7, "out": 9e-7, "cache_read": 3e-8 }, "mistral-large-2512": { "in": 5e-7, "out": 0.0000015, "cache_read": 5e-8 }, "mistral-large-3": { "in": 5e-7, "out": 0.0000015, "cache_read": 5e-8 }, "mistral-large-4": { "in": 6.8e-7, "out": 0.00000209, "cache_read": 6.8e-8 }, "mistral-large-4-0": { "in": 6.8e-7, "out": 0.00000209, "cache_read": 6.8e-8 }, "mistral-large-latest": { "in": 5e-7, "out": 0.0000015, "cache_read": 5e-8 }, "mistral-medium": { "in": 0.0000015, "out": 0.0000075, "cache_read": 1.5e-7 }, "mistral-medium-2604": { "in": 0.0000015, "out": 0.0000075, "cache_read": 1.5e-7 }, "mistral-medium-3": { "in": 0.0000015, "out": 0.0000075, "cache_read": 1.5e-7 }, "mistral-medium-3-5": { "in": 0.0000015, "out": 0.0000075, "cache_read": 1.5e-7 }, "mistral-medium-3.5": { "in": 0.0000015, "out": 0.0000075, "cache_read": 1.5e-7 }, "mistral-medium-latest": { "in": 0.0000015, "out": 0.0000075, "cache_read": 1.5e-7 }, "mistral-small": { "in": 1e-7, "out": 3e-7, "cache_read": 1e-8 }, "mistral-small-2603": { "in": 1.5e-7, "out": 6e-7, "cache_read": 1.5e-8 }, "mistral-small-latest": { "in": 1.5e-7, "out": 6e-7, "cache_read": 1.5e-8 }, "mistral-tiny": { "in": 2.5e-7, "out": 2.5e-7, "cache_read": 2.5e-8 }, "mistral-vibe-cli-fast": { "in": 1.5e-7, "out": 6e-7, "cache_read": 1.5e-8 }, "mistral-vibe-cli-latest": { "in": 0.0000015, "out": 0.0000075, "cache_read": 1.5e-7 }, "mistral-vibe-cli-with-tools": { "in": 0.0000015, "out": 0.0000075, "cache_read": 1.5e-7 }, "o1": { "in": 0.000015, "out": 0.00006, "cache_read": 0.0000075 }, "o1-2024-12-17": { "in": 0.000015, "out": 0.00006, "cache_read": 0.0000075 }, "o1-pro": { "in": 0.00015, "out": 0.0006 }, "o1-pro-2025-03-19": { "in": 0.00015, "out": 0.0006 }, "o3": { "in": 0.000002, "out": 0.000008, "cache_read": 5e-7 }, "o3-2025-04-16": { "in": 0.000002, "out": 0.000008, "cache_read": 5e-7 }, "o3-deep-research": { "in": 0.00001, "out": 0.00004, "cache_read": 0.0000025 }, "o3-mini": { "in": 0.0000011, "out": 0.0000044, "cache_read": 5.5e-7 }, "o3-mini-2025-01-31": { "in": 0.0000011, "out": 0.0000044, "cache_read": 5.5e-7 }, "o3-pro": { "in": 0.00002, "out": 0.00008 }, "o3-pro-2025-06-10": { "in": 0.00002, "out": 0.00008 }, "o4-mini": { "in": 0.0000011, "out": 0.0000044, "cache_read": 2.75e-7 }, "o4-mini-2025-04-16": { "in": 0.0000011, "out": 0.0000044, "cache_read": 2.75e-7 }, "o4-mini-deep-research": { "in": 0.000002, "out": 0.000008, "cache_read": 5e-7 }, "open-mistral-nemo": { "in": 3e-7, "out": 3e-7, "cache_read": 3e-8 }, "pixtral-large-latest": { "in": 0.000002, "out": 0.000006, "cache_read": 2e-7 }, "qwen3.8-27b": { "in": 8e-7, "out": 0.000004 }, "voxtral-small-2507": { "in": 1e-7, "out": 4e-7, "cache_read": 1e-8 }, "voxtral-small-latest": { "in": 1e-7, "out": 4e-7, "cache_read": 1e-8 }, "zai-glm-5": { "in": 0.0000014, "out": 0.0000044, "cache_read": 1.4e-7 }, "zai-glm-5-2": { "in": 0.0000014, "out": 0.0000044, "cache_read": 1.4e-7 }, "zai-glm-5-3": { "in": 0.0000014, "out": 0.0000044, "cache_read": 1.4e-7 }, "zai-glm-latest": { "in": 0.0000014, "out": 0.0000044, "cache_read": 1.4e-7 } };
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Turns a model response into [cost in USD, tokens]. Prices come from the same table as the
3
+ * Python SDK (sdk/agentfences/prices.json, a snapshot of LiteLLM's MIT-licensed price list).
4
+ */
5
+ import { type PriceEntry } from "./prices.generated.js";
6
+ /** Per-1M-token prices for models the table doesn't know, from init({ prices }). */
7
+ export interface CustomPrice {
8
+ input: number;
9
+ output: number;
10
+ cacheRead?: number;
11
+ cacheWrite?: number;
12
+ }
13
+ export declare function setCustomPrices(prices?: Record<string, CustomPrice>): void;
14
+ export declare function priceOf(model: string): PriceEntry | undefined;
15
+ export interface Usage {
16
+ model: string;
17
+ input: number;
18
+ cacheRead: number;
19
+ cacheWrite: number;
20
+ output: number;
21
+ reportedCost: number | null;
22
+ }
23
+ /**
24
+ * Normalise usage from the common response shapes: OpenAI (Chat Completions, Responses),
25
+ * Anthropic, Gemini, the Vercel AI SDK and LangChain.js, or any object with the same fields.
26
+ * Returns null if the response carries no usage.
27
+ */
28
+ export declare function readUsage(response: unknown): Usage | null;
29
+ /** [cost in USD, tokens] for one model response. Warns once for a model it can't price. */
30
+ export declare function costOf(response: unknown): [number, number];
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Turns a model response into [cost in USD, tokens]. Prices come from the same table as the
3
+ * Python SDK (sdk/agentfences/prices.json, a snapshot of LiteLLM's MIT-licensed price list).
4
+ */
5
+ import { PRICES } from "./prices.generated.js";
6
+ import { warnOnce } from "./events.js";
7
+ const custom = {};
8
+ export function setCustomPrices(prices = {}) {
9
+ for (const k of Object.keys(custom))
10
+ delete custom[k];
11
+ for (const [model, p] of Object.entries(prices)) {
12
+ const e = { in: p.input / 1e6, out: p.output / 1e6 };
13
+ if (p.cacheRead !== undefined)
14
+ e.cache_read = p.cacheRead / 1e6;
15
+ if (p.cacheWrite !== undefined)
16
+ e.cache_write = p.cacheWrite / 1e6;
17
+ custom[model.toLowerCase()] = e;
18
+ }
19
+ }
20
+ export function priceOf(model) {
21
+ const name = model.toLowerCase().split("/").pop() ?? ""; // "models/gemini-2.5-pro", "openai/gpt-4o"
22
+ const undated = name.replace(/-(\d{4}-\d{2}-\d{2}|\d{8})$/, ""); // "claude-sonnet-4-5-20250929"
23
+ for (const candidate of [name, undated]) {
24
+ const hit = custom[candidate] ?? PRICES[candidate];
25
+ if (hit)
26
+ return hit;
27
+ }
28
+ return undefined;
29
+ }
30
+ const num = (...values) => {
31
+ for (const v of values)
32
+ if (typeof v === "number" && Number.isFinite(v))
33
+ return v;
34
+ return 0;
35
+ };
36
+ /**
37
+ * Normalise usage from the common response shapes: OpenAI (Chat Completions, Responses),
38
+ * Anthropic, Gemini, the Vercel AI SDK and LangChain.js, or any object with the same fields.
39
+ * Returns null if the response carries no usage.
40
+ */
41
+ export function readUsage(response) {
42
+ const r = response;
43
+ if (!r || typeof r !== "object")
44
+ return null;
45
+ const model = r.model ?? r.modelVersion ?? r.model_version ?? r.response?.modelId
46
+ ?? r.response_metadata?.model_name ?? r.response_metadata?.model ?? "";
47
+ const u = r.usage;
48
+ if (u && typeof u === "object") {
49
+ const reported = typeof u.cost === "number" ? u.cost : null; // gateways like OpenRouter report the billed cost
50
+ if (u.inputTokens !== undefined || u.outputTokens !== undefined) { // Vercel AI SDK: input includes cached
51
+ const read = num(u.cachedInputTokens, u.inputTokenDetails?.cacheReadTokens);
52
+ const write = num(r.providerMetadata?.anthropic?.cacheCreationInputTokens, u.inputTokenDetails?.cacheWriteTokens);
53
+ return { model, input: num(u.inputTokens) - read - write, cacheRead: read, cacheWrite: write,
54
+ output: num(u.outputTokens), reportedCost: reported };
55
+ }
56
+ const openai = u.prompt_tokens !== undefined ? [u.prompt_tokens_details, u.prompt_tokens, u.completion_tokens] // Chat Completions
57
+ : u.input_tokens_details !== undefined ? [u.input_tokens_details, u.input_tokens, u.output_tokens] : null; // Responses API
58
+ if (openai) { // OpenAI: the input total includes cache reads and writes
59
+ const [details, total, out] = openai;
60
+ const read = num(details?.cached_tokens), write = num(details?.cache_write_tokens);
61
+ return { model, input: num(total) - read - write, cacheRead: read, cacheWrite: write, output: num(out), reportedCost: reported };
62
+ }
63
+ return { model, input: num(u.input_tokens), cacheRead: num(u.cache_read_input_tokens), // Anthropic: input excludes cache
64
+ cacheWrite: num(u.cache_creation_input_tokens), output: num(u.output_tokens), reportedCost: reported };
65
+ }
66
+ const g = r.usageMetadata ?? r.usage_metadata;
67
+ if (!g || typeof g !== "object")
68
+ return null;
69
+ if (g.promptTokenCount !== undefined || g.prompt_token_count !== undefined) { // Gemini: thinking billed as output
70
+ const read = num(g.cachedContentTokenCount, g.cached_content_token_count);
71
+ return { model, input: num(g.promptTokenCount, g.prompt_token_count) - read, cacheRead: read, cacheWrite: 0,
72
+ output: num(g.candidatesTokenCount, g.candidates_token_count) + num(g.thoughtsTokenCount, g.thoughts_token_count),
73
+ reportedCost: null };
74
+ }
75
+ const read = num(g.input_token_details?.cache_read), write = num(g.input_token_details?.cache_creation); // LangChain.js
76
+ return { model, input: num(g.input_tokens) - read - write, cacheRead: read, cacheWrite: write, output: num(g.output_tokens),
77
+ reportedCost: null };
78
+ }
79
+ /** [cost in USD, tokens] for one model response. Warns once for a model it can't price. */
80
+ export function costOf(response) {
81
+ const u = readUsage(response);
82
+ if (!u) {
83
+ const kind = response?.constructor?.name ?? typeof response;
84
+ warnOnce(`no-usage:${kind}`, `${kind} has no token usage, so this step costs $0. For streams, pass the final ` +
85
+ "message or a chunk that includes usage.");
86
+ return [0, 0];
87
+ }
88
+ const tokens = u.input + u.cacheRead + u.cacheWrite + u.output;
89
+ if (u.reportedCost !== null)
90
+ return [u.reportedCost, tokens];
91
+ const p = u.model ? priceOf(u.model) : undefined;
92
+ if (!p) {
93
+ warnOnce(`no-price:${u.model}`, `no price for model '${u.model}'. Its tokens are counted but its cost isn't, so ` +
94
+ `budgetUsd can't stop it. Add it with init({ prices: { "${u.model || "model-name"}": { input: ..., output: ... } } }) ` +
95
+ "(USD per 1M tokens).");
96
+ return [0, tokens];
97
+ }
98
+ const prompt = u.input + u.cacheRead + u.cacheWrite;
99
+ const rates = p.above !== undefined && p.tier && prompt > p.above ? { ...p, ...p.tier } : p; // long-context pricing
100
+ const cost = u.input * rates.in + u.cacheRead * (rates.cache_read ?? rates.in)
101
+ + u.cacheWrite * (rates.cache_write ?? rates.in) + u.output * rates.out;
102
+ return [cost, tokens];
103
+ }
@@ -0,0 +1,11 @@
1
+ export declare const MAX_ARGS = 300;
2
+ /** A `chat {model}` span. started is a performance.now() reading; response carries the usage. */
3
+ export declare function recordLlmCall(provider: string, requestModel: string | undefined, started: number, response?: unknown, error?: unknown, opts?: {
4
+ stream?: boolean;
5
+ integration?: string;
6
+ }): void;
7
+ /** An `execute_tool {name}` span; args is a short summary of the call's arguments. */
8
+ export declare function recordToolCall(name: string, args: string, started: number, error?: unknown, opts?: {
9
+ callId?: string;
10
+ integration?: string;
11
+ }): void;
package/dist/spans.js ADDED
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Attributes for model and tool call spans, named as in the OpenTelemetry GenAI semantic
3
+ * conventions (gen_ai.*), plus Fences' own (fences.*). Same format as the Python SDK.
4
+ */
5
+ import { recordSpan } from "./core.js";
6
+ import { costOf, readUsage } from "./pricing.js";
7
+ export const MAX_ARGS = 300; // characters of a tool call's argument summary
8
+ /** A `chat {model}` span. started is a performance.now() reading; response carries the usage. */
9
+ export function recordLlmCall(provider, requestModel, started, response, error, opts = {}) {
10
+ const attrs = { "gen_ai.operation.name": "chat", "gen_ai.provider.name": provider,
11
+ "gen_ai.request.model": requestModel, "fences.integration": opts.integration };
12
+ if (opts.stream)
13
+ attrs["gen_ai.request.stream"] = true; // usage arrives with the stream, so none is recorded here
14
+ const usage = response !== undefined && error === undefined ? readUsage(response) : null;
15
+ if (usage) {
16
+ Object.assign(attrs, {
17
+ "gen_ai.response.model": usage.model || undefined,
18
+ "gen_ai.usage.input_tokens": usage.input + usage.cacheRead + usage.cacheWrite, // OTel counts cached input in the total
19
+ "gen_ai.usage.cache_read.input_tokens": usage.cacheRead,
20
+ "gen_ai.usage.cache_write.input_tokens": usage.cacheWrite,
21
+ "gen_ai.usage.output_tokens": usage.output,
22
+ "fences.cost_usd": costOf(response)[0],
23
+ });
24
+ }
25
+ const model = attrs["gen_ai.response.model"] || requestModel || "unknown";
26
+ recordSpan(`chat ${model}`, performance.now() - started, attrs, error);
27
+ }
28
+ /** An `execute_tool {name}` span; args is a short summary of the call's arguments. */
29
+ export function recordToolCall(name, args, started, error, opts = {}) {
30
+ recordSpan(`execute_tool ${name}`, performance.now() - started, {
31
+ "gen_ai.operation.name": "execute_tool", "gen_ai.tool.name": name, "gen_ai.tool.call.id": opts.callId,
32
+ "gen_ai.tool.call.arguments": args.length <= MAX_ARGS ? args : args.slice(0, MAX_ARGS - 1) + "…",
33
+ "fences.integration": opts.integration,
34
+ }, error);
35
+ }
@@ -0,0 +1,15 @@
1
+ /** `{"query":"cats","limit":5}`: the arguments as compact JSON (one object argument on its own), capped. */
2
+ export declare function argsSummary(args: unknown[]): string;
3
+ /**
4
+ * Record every call of an agent tool on the active run:
5
+ *
6
+ * const webSearch = tool(async (query: string) => { ... });
7
+ * const fetchPage = tool(fetchPageImpl, { name: "fetch" });
8
+ *
9
+ * Records the tool's name, a short summary of its arguments, whether it succeeded (and the
10
+ * error if not) and how long it took. Errors still reach the caller; sync tools stay sync.
11
+ * Outside a governed run it just calls the function.
12
+ */
13
+ export declare function tool<A extends unknown[], R>(fn: (...args: A) => R, options?: {
14
+ name?: string;
15
+ }): (...args: A) => R;
package/dist/tools.js ADDED
@@ -0,0 +1,44 @@
1
+ import { getActiveRun } from "./core.js";
2
+ import { MAX_ARGS, recordToolCall } from "./spans.js";
3
+ /** `{"query":"cats","limit":5}`: the arguments as compact JSON (one object argument on its own), capped. */
4
+ export function argsSummary(args) {
5
+ let text;
6
+ try {
7
+ text = JSON.stringify(args.length === 1 ? args[0] : args, (_k, v) => typeof v === "bigint" ? String(v) : v) ?? "";
8
+ }
9
+ catch {
10
+ text = args.map(String).join(", "); // circular or odd values
11
+ }
12
+ return text.length <= MAX_ARGS ? text : text.slice(0, MAX_ARGS - 1) + "…";
13
+ }
14
+ /**
15
+ * Record every call of an agent tool on the active run:
16
+ *
17
+ * const webSearch = tool(async (query: string) => { ... });
18
+ * const fetchPage = tool(fetchPageImpl, { name: "fetch" });
19
+ *
20
+ * Records the tool's name, a short summary of its arguments, whether it succeeded (and the
21
+ * error if not) and how long it took. Errors still reach the caller; sync tools stay sync.
22
+ * Outside a governed run it just calls the function.
23
+ */
24
+ export function tool(fn, options = {}) {
25
+ const name = options.name ?? (fn.name || "tool");
26
+ return (...args) => {
27
+ if (!getActiveRun())
28
+ return fn(...args);
29
+ const summary = argsSummary(args), started = performance.now();
30
+ let out;
31
+ try {
32
+ out = fn(...args);
33
+ }
34
+ catch (e) {
35
+ recordToolCall(name, summary, started, e);
36
+ throw e;
37
+ }
38
+ if (out instanceof Promise) {
39
+ return out.then((v) => { recordToolCall(name, summary, started); return v; }, (e) => { recordToolCall(name, summary, started, e); throw e; });
40
+ }
41
+ recordToolCall(name, summary, started);
42
+ return out;
43
+ };
44
+ }
@@ -0,0 +1,2 @@
1
+ /** The one place the version lives; sent to Fences on every request. Keep in step with package.json. */
2
+ export declare const VERSION = "0.3.0";
@@ -0,0 +1,2 @@
1
+ /** The one place the version lives; sent to Fences on every request. Keep in step with package.json. */
2
+ export const VERSION = "0.3.0";
package/package.json CHANGED
@@ -1,6 +1,24 @@
1
1
  {
2
2
  "name": "agentfences",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.3.0",
4
+ "description": "On-call for AI agents: limits your agents can't raise, cost tracking, a decision trail, and a stop button",
5
+ "license": "MIT",
6
+ "author": "Bhavik Sardar",
7
+ "homepage": "https://github.com/bhaviksardar/fences",
8
+ "repository": { "type": "git", "url": "git+https://github.com/bhaviksardar/fences.git", "directory": "sdk-js" },
9
+ "keywords": ["ai", "agents", "governance", "llm", "budget", "on-call"],
10
+ "type": "module",
11
+ "main": "./dist/index.js",
12
+ "types": "./dist/index.d.ts",
13
+ "exports": { ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" } },
14
+ "files": ["dist"],
15
+ "engines": { "node": ">=18" },
16
+ "scripts": {
17
+ "build": "node scripts/prices.mjs && tsc",
18
+ "test": "npm run build && node --test test/*.test.mjs"
19
+ },
20
+ "devDependencies": {
21
+ "@types/node": "^22.0.0",
22
+ "typescript": "^5.6.0"
23
+ }
24
+ }