@kunal_dutt/token-lens 0.0.0-stage → 0.2.17

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 (40) hide show
  1. package/GATEWAY.md +491 -0
  2. package/LICENSE +21 -0
  3. package/PRIVACY.md +36 -0
  4. package/README.md +80 -2
  5. package/agent-cost-check.yml +49 -0
  6. package/check-agent.mjs +3298 -0
  7. package/cli.mjs +22 -0
  8. package/gateway.mjs +3365 -0
  9. package/package.json +45 -4
  10. package/sdk.mjs +207719 -0
  11. package/server.mjs +3392 -0
  12. package/token-lens.mcpb +0 -0
  13. package/types/src/core/advice.d.ts +27 -0
  14. package/types/src/core/budget.d.ts +28 -0
  15. package/types/src/core/compress.d.ts +29 -0
  16. package/types/src/core/cost.d.ts +32 -0
  17. package/types/src/core/guard.d.ts +20 -0
  18. package/types/src/core/hotspots.d.ts +124 -0
  19. package/types/src/core/index.d.ts +74 -0
  20. package/types/src/core/media.d.ts +29 -0
  21. package/types/src/core/models.d.ts +81 -0
  22. package/types/src/core/plans.d.ts +88 -0
  23. package/types/src/core/redos.d.ts +32 -0
  24. package/types/src/core/review.d.ts +46 -0
  25. package/types/src/core/rules.d.ts +47 -0
  26. package/types/src/core/tasks.d.ts +44 -0
  27. package/types/src/core/tokens.d.ts +19 -0
  28. package/types/src/core/verify.d.ts +19 -0
  29. package/types/src/gateway/config.d.ts +217 -0
  30. package/types/src/gateway/guard.d.ts +56 -0
  31. package/types/src/gateway/images.d.ts +40 -0
  32. package/types/src/gateway/metrics.d.ts +287 -0
  33. package/types/src/gateway/optimize.d.ts +102 -0
  34. package/types/src/gateway/protocol.d.ts +68 -0
  35. package/types/src/gateway/runs.d.ts +182 -0
  36. package/types/src/gateway/shared.d.ts +57 -0
  37. package/types/src/gateway/signals.d.ts +38 -0
  38. package/types/src/gateway/storage.d.ts +88 -0
  39. package/types/src/gateway/verify.d.ts +51 -0
  40. package/types/src/sdk/index.d.ts +228 -0
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Request optimizations. Each one targets a part of the input that grows in
3
+ * real workloads, and each is either safe (the request means exactly the same
4
+ * thing) or opt-in (it could change the answer).
5
+ *
6
+ * Safe, on by default:
7
+ * prompt core optimizer on the newest user message
8
+ * minify-json pretty-printed JSON tool output -> compact JSON
9
+ * collapse-repeats runs of identical lines in tool output -> one line + count
10
+ * prompt-caching cache breakpoints (Anthropic) / cache key (OpenAI) once a run repeats its prefix
11
+ *
12
+ * Opt-in (config `optIn`, or the `x-token-lens-allow` header):
13
+ * elide-tool-outputs stub out tool outputs older than the newest few
14
+ */
15
+ import { type Level, type ModelProfile, type Tip } from "../core";
16
+ import type { GatewayConfig, UpstreamConfig } from "./config";
17
+ import { type ParsedRequest, type Part, type TextRef } from "./protocol";
18
+ /**
19
+ * Token counts in the model's own tokens: exact where the core has the
20
+ * tokenizer, otherwise the core's estimate scaled by what the upstream has
21
+ * actually been reporting for that model.
22
+ */
23
+ export declare class Counter {
24
+ private factors;
25
+ factor(profile: ModelProfile): number;
26
+ /** Uncalibrated count, the basis for calibration. */
27
+ raw(text: string, profile: ModelProfile): number;
28
+ count(text: string, profile: ModelProfile): number;
29
+ /** Learn from the upstream's real input count for a request we estimated at `raw` tokens. */
30
+ observe(profile: ModelProfile, raw: number, actual: number): void;
31
+ snapshot(): Record<string, {
32
+ factor: number;
33
+ samples: number;
34
+ }>;
35
+ }
36
+ export interface PartCounts {
37
+ parts: Record<Part, number>;
38
+ total: number;
39
+ /** Uncalibrated total, fed back into calibration once the upstream reports usage. */
40
+ raw: number;
41
+ /** Sizes of each tool output, oldest first. */
42
+ toolOutputs: number[];
43
+ }
44
+ export declare function countParts(refs: TextRef[], profile: ModelProfile, counter: Counter): PartCounts;
45
+ export interface Action {
46
+ id: string;
47
+ /** Input tokens removed from this request (0 for caching, whose savings show up in usage). */
48
+ saved: number;
49
+ count?: number;
50
+ }
51
+ export declare function minifyJson(text: string): string | null;
52
+ export declare function collapseRepeats(text: string, minRun?: number): string | null;
53
+ export interface OptimizeInput {
54
+ cfg: GatewayConfig;
55
+ body: Record<string, any>;
56
+ parsed: ParsedRequest;
57
+ profile: ModelProfile;
58
+ upstream: UpstreamConfig;
59
+ counter: Counter;
60
+ /** Earlier calls in this run that sent the same system prompt and tools. */
61
+ prefixRepeats: number;
62
+ runId: string;
63
+ level: Level | "auto";
64
+ /** Opt-in optimizations allowed for this request. */
65
+ allow: Set<string>;
66
+ /** Tokens before the newest message, for context-size tips. */
67
+ conversationTokens: number;
68
+ }
69
+ export interface OptimizeResult {
70
+ actions: Action[];
71
+ tips: Tip[];
72
+ level: Level;
73
+ task?: string;
74
+ }
75
+ /**
76
+ * Lists of records in a JSON tool output, written once as column names plus rows
77
+ * instead of repeating every key in every record:
78
+ * {"_columns": ["id", "name"], "_rows": [[1, "a"], [2, "b"]]}. Only lists of 5+
79
+ * records that all have the same keys; nothing is dropped. Returns null when it
80
+ * saves under a tenth.
81
+ */
82
+ export declare function compactJson(text: string): string | null;
83
+ /**
84
+ * Leaves out fields of a JSON tool output that the conversation never referred
85
+ * to afterwards: no value from the field (a string of 6+ characters or a number
86
+ * of 4+ digits) and not its name appears in any later message or tool call.
87
+ * Small fields stay. Returns null when nothing is left out.
88
+ */
89
+ export declare function trimToolFields(text: string, later: string): string | null;
90
+ /**
91
+ * Replaces paragraphs (200+ characters) already seen earlier in the request with
92
+ * a pointer to where they first appear. `seenAt` carries first sightings across
93
+ * messages, in request order.
94
+ */
95
+ export declare function dedupePassages(text: string, turn: number, seenAt: Map<string, number>): string | null;
96
+ export declare function optimizeRequest(input: OptimizeInput): OptimizeResult;
97
+ /**
98
+ * Prompt caching, only once a run has shown it re-sends a prefix big enough to
99
+ * cache: a cache write costs more than a plain read, so one-off calls
100
+ * shouldn't pay for it. Runs after counting, since it restructures the body.
101
+ */
102
+ export declare function applyPromptCaching(input: OptimizeInput, counts: PartCounts): Action | null;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * The three request shapes the gateway understands, reduced to one view: the
3
+ * text in a request, labelled by the part of the input it belongs to, with a
4
+ * setter so optimizations can rewrite it in place.
5
+ *
6
+ * system system prompt / instructions
7
+ * tools tool (function) definitions
8
+ * history earlier user and assistant turns, including tool calls
9
+ * tool_output results of tool calls
10
+ * context material pasted or retrieved into the newest user message
11
+ * prompt the newest user instruction itself
12
+ */
13
+ export type Protocol = "openai-chat" | "openai-responses" | "anthropic";
14
+ export declare const PARTS: readonly ["system", "tools", "history", "tool_output", "context", "prompt"];
15
+ export type Part = (typeof PARTS)[number];
16
+ export interface TextRef {
17
+ /** "prompt" covers the whole newest user text; counting splits it into prompt + context. */
18
+ part: Exclude<Part, "context">;
19
+ /** Position of the message in the conversation (system and tools are -1). */
20
+ turn: number;
21
+ get(): string;
22
+ /** Written by the user (not the assistant, a tool or the system prompt). */
23
+ user?: boolean;
24
+ /** Absent when the text can't be rewritten (tool definitions, tool-call arguments). */
25
+ set?(text: string): void;
26
+ }
27
+ export interface ParsedRequest {
28
+ protocol: Protocol;
29
+ model: string;
30
+ stream: boolean;
31
+ temperature?: number;
32
+ maxOutputTokens?: number;
33
+ refs: TextRef[];
34
+ /** Turns in the request; the newest turn starts after the last assistant message. */
35
+ turns: number;
36
+ /** Tool calls the agent has made so far in this conversation, oldest first. */
37
+ toolCalls: ToolCall[];
38
+ }
39
+ export interface ToolCall {
40
+ name: string;
41
+ /** Arguments in a canonical form (sorted keys), so equal inputs compare equal. */
42
+ args: string;
43
+ turn: number;
44
+ }
45
+ /** JSON with sorted keys; non-JSON strings are kept as they are. */
46
+ export declare function canonicalArgs(args: unknown): string;
47
+ type Json = Record<string, any>;
48
+ export declare function parseRequest(protocol: Protocol, body: Json): ParsedRequest;
49
+ export declare function protocolForPath(path: string): Protocol | null;
50
+ /** Text of each part, with the newest user text split into instruction and pasted material. */
51
+ export declare function partTexts(refs: TextRef[]): Record<Part, string[]>;
52
+ export interface Usage {
53
+ /** All input tokens, including cached ones. */
54
+ inputTokens?: number;
55
+ outputTokens?: number;
56
+ cachedInputTokens?: number;
57
+ cacheWriteTokens?: number;
58
+ reasoningTokens?: number;
59
+ }
60
+ /** Normalize a usage object from any of the three APIs. */
61
+ export declare function readUsage(u: Json | undefined): Usage;
62
+ /** Combine usage seen across stream events: later events report running totals. */
63
+ export declare function mergeUsage(a: Usage, b: Usage): Usage;
64
+ /** Find a usage object in a response body or stream event. */
65
+ export declare function usageIn(obj: Json): Usage;
66
+ /** An error in the shape the caller's SDK expects. */
67
+ export declare function errorBody(protocol: Protocol | null, status: number, message: string): Json;
68
+ export {};
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Runs: the calls an app, workflow or agent makes in one session. The gateway
3
+ * groups calls into runs, records what each call's input was made of, and
4
+ * works out which part is driving token use up and what fixes it.
5
+ *
6
+ * A caller can name its run with the `x-token-lens-run` header. Otherwise a
7
+ * run is identified by what agent and chat loops re-send on every call: the
8
+ * app, the model, the system prompt and the first user message.
9
+ */
10
+ import type { Part, Protocol, Usage } from "./protocol";
11
+ import type { BreakKind, PrefixPrint } from "./signals";
12
+ import type { Action } from "./optimize";
13
+ import { type Check } from "./verify";
14
+ import { type PriceTierRates } from "./metrics";
15
+ export interface CallRecord {
16
+ at: string;
17
+ model: string;
18
+ profile: string;
19
+ upstream: string;
20
+ status: number;
21
+ /** Input tokens by part, after optimization. */
22
+ parts: Record<Part, number>;
23
+ input: number;
24
+ toolOutputs: number[];
25
+ /** What the upstream reported, when it did. */
26
+ usage: Usage;
27
+ actions: Action[];
28
+ /** Served from the gateway's copy of an identical earlier call. */
29
+ replayed?: boolean;
30
+ /** Identical to an earlier call in this run (a retry). */
31
+ duplicate?: boolean;
32
+ window: number;
33
+ ms: number;
34
+ /** Billed cost of this step, and what it would have cost without Token Lens (0 without pricing). */
35
+ costUsd: number;
36
+ costBeforeUsd: number;
37
+ /** Tool calls the agent had made by this step, and the ones it repeated with the same input. */
38
+ toolCalls: number;
39
+ repeatedToolCalls: Array<{
40
+ name: string;
41
+ args: string;
42
+ count: number;
43
+ }>;
44
+ /** The agent's most recent tool calls were the same call, this many times in a row. */
45
+ sameCallStreak?: {
46
+ name: string;
47
+ args: string;
48
+ times: number;
49
+ };
50
+ /** Refused because the run reached its budget. */
51
+ overBudget?: boolean;
52
+ /** Price multiplier the provider applied (0.5 for its batch option). */
53
+ priceFactor?: number;
54
+ /** Images in the request, with estimated tokens (all, and those before the newest two messages). */
55
+ images?: {
56
+ count: number;
57
+ tokens: number;
58
+ oldTokens: number;
59
+ provider: "openai" | "anthropic";
60
+ };
61
+ /** The API the call used. */
62
+ api?: Protocol;
63
+ /** The prefix started like the previous call's, then changed, so the provider's cache missed from there (tokens after the change). */
64
+ cacheBreak?: {
65
+ kind: BreakKind;
66
+ where: "system prompt" | "tools";
67
+ tokens: number;
68
+ };
69
+ /** Times the conversation shows the model's JSON failing to parse and being asked again. */
70
+ jsonRetries?: number;
71
+ structuredOutput?: boolean;
72
+ /** The model's higher rate above a request size, when it has one. */
73
+ priceTier?: PriceTierRates;
74
+ }
75
+ export interface Run {
76
+ id: string;
77
+ app: string;
78
+ /** The agent or workflow, from the x-token-lens-agent header ("-" when not given). */
79
+ agent: string;
80
+ named: boolean;
81
+ firstSeen: string;
82
+ lastSeen: string;
83
+ calls: CallRecord[];
84
+ /** Hash of system prompt + tools for each call, to spot a re-sent prefix. */
85
+ prefixes: Map<string, number>;
86
+ requestHashes: Set<string>;
87
+ /** Tools whose full definitions this run has loaded or used (lazy-tools). */
88
+ loadedTools?: Set<string>;
89
+ /** Tool outputs kept outside the conversation (park-tool-outputs), by id. */
90
+ parked?: Map<string, string>;
91
+ /** Fingerprint of the previous call's prefix, to spot what breaks caching. */
92
+ prefixPrint?: PrefixPrint;
93
+ /** Files or documents this run read that other runs of the same team read too. */
94
+ sharedReads?: {
95
+ files: number;
96
+ of: number;
97
+ runs: number;
98
+ agents: string[];
99
+ };
100
+ /** Output checks: optimized vs original answers. */
101
+ checks: Array<Check & {
102
+ actions: string[];
103
+ costUsd: number;
104
+ }>;
105
+ }
106
+ export declare const hash: (...parts: string[]) => string;
107
+ export declare class RunStore {
108
+ private maxRuns;
109
+ private runs;
110
+ constructor(maxRuns?: number);
111
+ get(id: string): Run | undefined;
112
+ open(id: string, app: string, named: boolean, agent?: string): Run;
113
+ record(run: Run, call: CallRecord): void;
114
+ list(): Run[];
115
+ }
116
+ export interface Finding {
117
+ id: string;
118
+ part: Part | "output" | "calls";
119
+ title: string;
120
+ evidence: string;
121
+ /** What the gateway already does about it on every call. */
122
+ automatic: string[];
123
+ /** A fix that could change answers, so it waits for an opt-in. */
124
+ optIn?: {
125
+ id: string;
126
+ how: string;
127
+ estSavedTokensPerCall?: number;
128
+ };
129
+ /** Other changes in the app or workflow that would cut it. */
130
+ advice?: string;
131
+ /** Rough input tokens a call could save with every listed fix applied. */
132
+ estSavedTokensPerCall: number;
133
+ /** The same priced at the model's input rate (0 when the model has no pricing). */
134
+ estSavedUsdPerCall: number;
135
+ /** What a run like this one would save with the fixes in place from its first call. */
136
+ estSavedTokensPerRun: number;
137
+ estSavedUsdPerRun: number;
138
+ /** Ready-to-apply changes, gateway-side first, each with where it goes and what to paste. */
139
+ fixes: Fix[];
140
+ }
141
+ export interface Fix {
142
+ where: "gateway config" | "request header" | "agent code" | "system prompt" | "model server";
143
+ what: string;
144
+ /** Paste-ready: a config fragment, a header, a code change or a prompt line. */
145
+ apply: string;
146
+ }
147
+ export declare const OPT_IN_HOW: (id: string) => string;
148
+ /**
149
+ * Where a run's tokens are going and what is being done about it, biggest
150
+ * opportunity first. Needs at least two calls to talk about growth.
151
+ */
152
+ export declare function findings(run: Run): Finding[];
153
+ /** The part that grew most across the run, for the per-call header. */
154
+ export declare function fastestGrowing(run: Run): Part | undefined;
155
+ export declare function runSummary(run: Run): {
156
+ id: string;
157
+ app: string;
158
+ agent: string;
159
+ named: boolean;
160
+ firstSeen: string;
161
+ lastSeen: string;
162
+ calls: number;
163
+ model: string;
164
+ tokensBefore: number;
165
+ tokensAfter: number;
166
+ savedTokens: number;
167
+ savedTokensPct: number;
168
+ costBeforeUsd: number;
169
+ costUsd: number;
170
+ savedUsd: number;
171
+ inputTokens: number;
172
+ outputTokens: number;
173
+ cachedInputTokens: number;
174
+ savedInputTokens: number;
175
+ growing: "system" | "tools" | "history" | "prompt" | "tool_output" | "context";
176
+ checkCostUsd: number;
177
+ netSavedUsd: number;
178
+ steps: number;
179
+ retriesReplayed: number;
180
+ stoppedByBudget: boolean;
181
+ outputCheck: import("./verify").CheckSummary;
182
+ };
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Keeps reports in shared storage (see storage.ts): savings totals added up
3
+ * across gateway copies, run reports, and the fixes switched off to keep
4
+ * answers unchanged.
5
+ */
6
+ import type { OutputGuard } from "./guard";
7
+ import { type Metrics, type Totals } from "./metrics";
8
+ import type { Run } from "./runs";
9
+ import { type KV } from "./storage";
10
+ export interface StorageConfig {
11
+ /** redis:// or rediss:// URL of the company's own Redis or Valkey. */
12
+ redisUrl?: string;
13
+ /** A JSON file, for a single gateway that keeps reports across restarts. */
14
+ file?: string;
15
+ keyPrefix: string;
16
+ /** How often this copy writes its numbers and reads the shared ones. */
17
+ flushSeconds: number;
18
+ /** How long run reports are kept. */
19
+ runDays: number;
20
+ }
21
+ type SwitchedOff = ReturnType<OutputGuard["list"]>[number];
22
+ export declare class SharedReports {
23
+ private kv;
24
+ private cfg;
25
+ private deps;
26
+ private flushed;
27
+ private dirtyRuns;
28
+ private timer?;
29
+ private busy?;
30
+ /** Where this copy's numbers are written; shown in /token-lens/stats. */
31
+ readonly kind: "redis" | "file";
32
+ constructor(kv: KV, cfg: StorageConfig, deps: {
33
+ metrics: Metrics;
34
+ guard: OutputGuard;
35
+ runView(run: Run): object;
36
+ log(line: string): void;
37
+ });
38
+ static open(cfg: StorageConfig, deps: SharedReports["deps"]): SharedReports | undefined;
39
+ private k;
40
+ start(): void;
41
+ stop(): void;
42
+ markRun(run: Run): void;
43
+ private error;
44
+ /** Writes what changed since the last flush and reads the shared switched-off fixes. */
45
+ flush(): Promise<void>;
46
+ private doFlush;
47
+ /** Totals of every copy, by team, agent and model. */
48
+ totals(): Promise<{
49
+ rows: Map<string, Totals>;
50
+ since: string;
51
+ }>;
52
+ recentRuns(limit: number): Promise<any[]>;
53
+ run(id: string): Promise<any | undefined>;
54
+ suspend(list: SwitchedOff[]): Promise<void>;
55
+ resetGuard(app?: string, agent?: string): Promise<number>;
56
+ }
57
+ export {};
@@ -0,0 +1,38 @@
1
+ /**
2
+ * What a run's calls reveal beyond their size: something that keeps the
3
+ * provider from reusing its cache, retries after broken JSON, files several
4
+ * agents read, and calls nearing a model's higher-price size. Each feeds a run
5
+ * finding with a ready-to-apply fix; none of them changes a request.
6
+ */
7
+ import type { PriceTier } from "./config";
8
+ import type { Protocol, ToolCall } from "./protocol";
9
+ /** A compact fingerprint of the cacheable prefix (system prompt, then tools): a hash per block of text. */
10
+ export interface PrefixPrint {
11
+ system: string[];
12
+ tools: string[];
13
+ toolOrder: string;
14
+ toolSet: string;
15
+ }
16
+ export declare function prefixPrint(system: string, toolTexts: string[], toolNames: string[]): PrefixPrint;
17
+ export type BreakKind = "timestamp" | "date" | "id" | "number" | "reordered tools" | "changed tools" | "changed text";
18
+ export interface CacheBreak {
19
+ kind: BreakKind;
20
+ where: "system prompt" | "tools";
21
+ /** Share of the prefix after the first change, which the provider can't reuse. */
22
+ lostShare: number;
23
+ }
24
+ /** What changed in the text around a position, named without quoting it (it can hold customer data). */
25
+ export declare function classify(text: string): BreakKind;
26
+ /**
27
+ * Whether this call's prefix starts like the previous call's and then changes,
28
+ * which makes the provider's cache miss from that point on. `system` and
29
+ * `toolsText` are this call's texts, used to name what changed.
30
+ */
31
+ export declare function cacheBreak(prev: PrefixPrint | undefined, cur: PrefixPrint, system: string, toolsText: string): CacheBreak | undefined;
32
+ /** Times the conversation shows the model answering with JSON that didn't parse, then being asked again. */
33
+ export declare function brokenJsonRetries(protocol: Protocol, body: any): number;
34
+ /** Whether the request already uses the provider's strict structured output. */
35
+ export declare function usesStructuredOutput(protocol: Protocol, body: any): boolean;
36
+ /** Hashed keys of the files or documents read by tool calls in this conversation. */
37
+ export declare function fileReads(calls: ToolCall[]): string[];
38
+ export declare function priceTier(tiers: PriceTier[], model: string): PriceTier | undefined;
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Shared storage for reports, so several gateway copies show one set of
3
+ * numbers and nothing is lost on a restart. Optional: without it everything
4
+ * stays in memory, as before.
5
+ *
6
+ * - Redis (`storage.redisUrl`, or TOKEN_LENS_REDIS_URL): the company's own Redis
7
+ * or Valkey, redis:// or rediss://. Every copy adds its savings totals to one
8
+ * shared set of counters, writes run reports (kept `runDays` days), and shares
9
+ * which fixes are switched off.
10
+ * - A file (`storage.file`): one gateway that keeps its reports across restarts.
11
+ *
12
+ * Only what the reports already show is stored: totals, run summaries and
13
+ * findings (tool inputs hashed as in reports), never prompts or answers.
14
+ */
15
+ /** The few commands the gateway needs; Redis and the file store both provide them. */
16
+ export interface KV {
17
+ hincrbyfloat(key: string, field: string, by: number): Promise<void>;
18
+ hgetall(key: string): Promise<Record<string, string>>;
19
+ hset(key: string, field: string, value: string): Promise<void>;
20
+ hdel(key: string, field: string): Promise<void>;
21
+ setex(key: string, seconds: number, value: string): Promise<void>;
22
+ get(key: string): Promise<string | null>;
23
+ mget(keys: string[]): Promise<Array<string | null>>;
24
+ zadd(key: string, score: number, member: string): Promise<void>;
25
+ zrevrange(key: string, start: number, stop: number): Promise<string[]>;
26
+ zremrangebyscore(key: string, max: number): Promise<void>;
27
+ setnx(key: string, value: string): Promise<void>;
28
+ close(): void;
29
+ }
30
+ type Reply = string | number | null | Reply[] | Error;
31
+ /** A minimal Redis client (RESP2): one connection, commands answered in order. */
32
+ export declare class RedisKV implements KV {
33
+ private url;
34
+ private socket?;
35
+ private ready?;
36
+ private buf;
37
+ private waiting;
38
+ constructor(url: string);
39
+ private connect;
40
+ private fail;
41
+ private raw;
42
+ cmd(...args: Array<string | number>): Promise<Reply>;
43
+ private data;
44
+ hincrbyfloat(key: string, field: string, by: number): Promise<void>;
45
+ hgetall(key: string): Promise<Record<string, string>>;
46
+ hset(key: string, field: string, value: string): Promise<void>;
47
+ hdel(key: string, field: string): Promise<void>;
48
+ setex(key: string, seconds: number, value: string): Promise<void>;
49
+ get(key: string): Promise<string>;
50
+ mget(keys: string[]): Promise<string[]>;
51
+ zadd(key: string, score: number, member: string): Promise<void>;
52
+ zrevrange(key: string, start: number, stop: number): Promise<string[]>;
53
+ zremrangebyscore(key: string, max: number): Promise<void>;
54
+ setnx(key: string, value: string): Promise<void>;
55
+ close(): void;
56
+ }
57
+ /** Parses one RESP2 reply starting at `at`; undefined when more bytes are needed. */
58
+ export declare function parseReply(buf: Buffer, at: number): {
59
+ value: Reply;
60
+ end: number;
61
+ } | undefined;
62
+ /** The same commands kept in memory and saved to one JSON file (a single gateway). */
63
+ export declare class FileKV implements KV {
64
+ private path;
65
+ private hashes;
66
+ private strings;
67
+ private zsets;
68
+ private dirty;
69
+ constructor(path: string);
70
+ private hash;
71
+ hincrbyfloat(key: string, field: string, by: number): Promise<void>;
72
+ hgetall(key: string): Promise<{
73
+ [k: string]: string;
74
+ }>;
75
+ hset(key: string, field: string, value: string): Promise<void>;
76
+ hdel(key: string, field: string): Promise<void>;
77
+ setex(key: string, seconds: number, value: string): Promise<void>;
78
+ get(key: string): Promise<string>;
79
+ mget(keys: string[]): Promise<string[]>;
80
+ zadd(key: string, score: number, member: string): Promise<void>;
81
+ zrevrange(key: string, start: number, stop: number): Promise<string[]>;
82
+ zremrangebyscore(key: string, max: number): Promise<void>;
83
+ setnx(key: string, value: string): Promise<void>;
84
+ /** Writes the file (atomically) when something changed. */
85
+ save(): void;
86
+ close(): void;
87
+ }
88
+ export {};
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Output checks: evidence that an optimized request gets the same work done.
3
+ *
4
+ * For a sample of optimized calls, the gateway also sends the original,
5
+ * unoptimized request to the same model in the background and compares the
6
+ * two answers: the tool calls the agent makes (name and arguments) must be the
7
+ * same, and the text must say the same thing. Streamed calls are checked too:
8
+ * the streamed answer is put back together and compared with a non-streamed
9
+ * original. A check costs one extra call per sampled request.
10
+ */
11
+ import type { Protocol } from "./protocol";
12
+ export interface Output {
13
+ text: string;
14
+ toolCalls: Array<{
15
+ name: string;
16
+ args: unknown;
17
+ }>;
18
+ }
19
+ /** The answer and tool calls in a non-streamed response of any of the three APIs. */
20
+ export declare function readOutput(protocol: Protocol, body: any): Output;
21
+ /**
22
+ * How much two answers say the same thing, 0..1: overlap of their words and
23
+ * word pairs (Dice coefficient), so reworded or reordered sentences still score
24
+ * high while a different answer scores low. Numbers must match exactly.
25
+ */
26
+ export declare function similarity(a: string, b: string): number;
27
+ export interface Check {
28
+ at: string;
29
+ /** Tool calls identical (same tools, same arguments, same order). */
30
+ sameToolCalls: boolean;
31
+ /** Text similarity of the two answers, 0..1. */
32
+ similarity: number;
33
+ /** The output counts as unchanged. */
34
+ match: boolean;
35
+ /** Input tokens the optimized call saved. */
36
+ saved: number;
37
+ /** The model's own answers to the original request differed as much, so the difference isn't the fixes'. */
38
+ modelVaries?: boolean;
39
+ }
40
+ export declare function compareOutputs(optimized: Output, original: Output, saved: number, minSimilarity: number): Check;
41
+ export interface CheckSummary {
42
+ checked: number;
43
+ matched: number;
44
+ sameToolCalls: number;
45
+ averageSimilarity: number;
46
+ /** Input tokens saved on the checked calls. */
47
+ savedOnChecked: number;
48
+ }
49
+ export declare function summarizeChecks(checks: Check[]): CheckSummary;
50
+ /** The answer and tool calls in a streamed (server-sent events) response of any of the three APIs. */
51
+ export declare function readStreamOutput(protocol: Protocol, sse: string): Output;