@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.
- package/GATEWAY.md +491 -0
- package/LICENSE +21 -0
- package/PRIVACY.md +36 -0
- package/README.md +80 -2
- package/agent-cost-check.yml +49 -0
- package/check-agent.mjs +3298 -0
- package/cli.mjs +22 -0
- package/gateway.mjs +3365 -0
- package/package.json +45 -4
- package/sdk.mjs +207719 -0
- package/server.mjs +3392 -0
- package/token-lens.mcpb +0 -0
- package/types/src/core/advice.d.ts +27 -0
- package/types/src/core/budget.d.ts +28 -0
- package/types/src/core/compress.d.ts +29 -0
- package/types/src/core/cost.d.ts +32 -0
- package/types/src/core/guard.d.ts +20 -0
- package/types/src/core/hotspots.d.ts +124 -0
- package/types/src/core/index.d.ts +74 -0
- package/types/src/core/media.d.ts +29 -0
- package/types/src/core/models.d.ts +81 -0
- package/types/src/core/plans.d.ts +88 -0
- package/types/src/core/redos.d.ts +32 -0
- package/types/src/core/review.d.ts +46 -0
- package/types/src/core/rules.d.ts +47 -0
- package/types/src/core/tasks.d.ts +44 -0
- package/types/src/core/tokens.d.ts +19 -0
- package/types/src/core/verify.d.ts +19 -0
- package/types/src/gateway/config.d.ts +217 -0
- package/types/src/gateway/guard.d.ts +56 -0
- package/types/src/gateway/images.d.ts +40 -0
- package/types/src/gateway/metrics.d.ts +287 -0
- package/types/src/gateway/optimize.d.ts +102 -0
- package/types/src/gateway/protocol.d.ts +68 -0
- package/types/src/gateway/runs.d.ts +182 -0
- package/types/src/gateway/shared.d.ts +57 -0
- package/types/src/gateway/signals.d.ts +38 -0
- package/types/src/gateway/storage.d.ts +88 -0
- package/types/src/gateway/verify.d.ts +51 -0
- package/types/src/sdk/index.d.ts +228 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Work out what the user is doing from the prompt itself, so optimization can
|
|
3
|
+
* adapt to the task: debugging wants its logs trimmed, editing must leave the
|
|
4
|
+
* pasted text alone, a quick question benefits from a length cap, and so on.
|
|
5
|
+
*/
|
|
6
|
+
export type Task = "coding" | "debugging" | "writing" | "editing" | "summarizing" | "translation" | "research" | "analysis" | "brainstorming" | "chat";
|
|
7
|
+
export declare const TASK_LABELS: Record<Task, string>;
|
|
8
|
+
export interface TaskSignals {
|
|
9
|
+
/** Contains fenced code or lines that look like source code. */
|
|
10
|
+
hasCode: boolean;
|
|
11
|
+
/** Contains log lines or a stack trace. */
|
|
12
|
+
hasLogs: boolean;
|
|
13
|
+
/** Contains tabular / JSON data. */
|
|
14
|
+
hasData: boolean;
|
|
15
|
+
/** Estimated tokens of material pasted below the instruction. */
|
|
16
|
+
pastedTokens: number;
|
|
17
|
+
/** The prompt already says how long or in what shape the answer should be. */
|
|
18
|
+
hasLengthLimit: boolean;
|
|
19
|
+
/** The prompt asks for a whole file / full rewrite back. */
|
|
20
|
+
asksForFullOutput: boolean;
|
|
21
|
+
}
|
|
22
|
+
export interface TaskInfo {
|
|
23
|
+
task: Task;
|
|
24
|
+
/** 0..1, how sure the classifier is. */
|
|
25
|
+
confidence: number;
|
|
26
|
+
signals: TaskSignals;
|
|
27
|
+
}
|
|
28
|
+
export declare const CODE_LINE_RE: RegExp;
|
|
29
|
+
export declare const LOG_LINE_RE: RegExp;
|
|
30
|
+
/**
|
|
31
|
+
* Split a prompt into the instruction (first paragraph) and pasted material
|
|
32
|
+
* (the rest). Prompts usually lead with the ask and paste below it.
|
|
33
|
+
*/
|
|
34
|
+
export declare function splitInstruction(text: string): {
|
|
35
|
+
instruction: string;
|
|
36
|
+
material: string;
|
|
37
|
+
};
|
|
38
|
+
export declare function detectTask(text: string): TaskInfo;
|
|
39
|
+
/**
|
|
40
|
+
* Tasks where text pasted below the instruction is the user's own notes and
|
|
41
|
+
* can be tightened. For every other task it is source material (the text to
|
|
42
|
+
* edit, summarize or translate, code, logs, data) and is left as written.
|
|
43
|
+
*/
|
|
44
|
+
export declare const COMPRESSIBLE_MATERIAL: ReadonlySet<Task>;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token counting, adapted to the model's tokenizer.
|
|
3
|
+
*
|
|
4
|
+
* OpenAI models get an exact count from the o200k tokenizer (bundled, works
|
|
5
|
+
* offline). Families without a public offline tokenizer get an estimate from
|
|
6
|
+
* characters and word pieces, tuned per family, and are marked `exact: false`.
|
|
7
|
+
*/
|
|
8
|
+
import type { ModelProfile } from "./models";
|
|
9
|
+
export interface TokenCount {
|
|
10
|
+
tokens: number;
|
|
11
|
+
exact: boolean;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* BPE tokenizers average ~4 characters per token on English prose but split
|
|
15
|
+
* punctuation, digits and rare words more aggressively, so take the larger of
|
|
16
|
+
* a character-based estimate and a count of word pieces.
|
|
17
|
+
*/
|
|
18
|
+
export declare function estimateTokens(text: string, charsPerToken?: number): number;
|
|
19
|
+
export declare function countTokens(text: string, profile: ModelProfile): TokenCount;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Evidence that an optimized agent behaves the same: every check compares the
|
|
3
|
+
* original and optimized spec and says what was compared. The success
|
|
4
|
+
* criterion is fewer tokens with the agent's work unchanged, so a change that
|
|
5
|
+
* fails any check is not applied.
|
|
6
|
+
*/
|
|
7
|
+
import type { AgentSpec } from "./hotspots";
|
|
8
|
+
export interface Check {
|
|
9
|
+
name: string;
|
|
10
|
+
passed: boolean;
|
|
11
|
+
detail: string;
|
|
12
|
+
}
|
|
13
|
+
export interface Verification {
|
|
14
|
+
passed: boolean;
|
|
15
|
+
checks: Check[];
|
|
16
|
+
}
|
|
17
|
+
/** Is this rewrite of one text safe? Facts/constraints kept and no instruction words lost. */
|
|
18
|
+
export declare function textEquivalent(original: string, rewritten: string): boolean;
|
|
19
|
+
export declare function verifyEquivalent(before: AgentSpec, after: AgentSpec): Verification;
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gateway configuration: a JSON file (TOKEN_LENS_CONFIG) plus environment
|
|
3
|
+
* shortcuts, so the simplest deployment needs only an API key.
|
|
4
|
+
*
|
|
5
|
+
* Secrets are never written in the file itself: any string value of the form
|
|
6
|
+
* "env:NAME" is read from the environment variable NAME.
|
|
7
|
+
*/
|
|
8
|
+
import type { CustomModel, Level } from "../core";
|
|
9
|
+
import type { StorageConfig } from "./shared";
|
|
10
|
+
export type UpstreamType = "openai" | "anthropic";
|
|
11
|
+
export interface UpstreamConfig {
|
|
12
|
+
/**
|
|
13
|
+
* "openai": any OpenAI-compatible API (OpenAI, Azure OpenAI v1, vLLM, TGI,
|
|
14
|
+
* Ollama, LiteLLM, Together, Groq, an in-house model server…).
|
|
15
|
+
* "anthropic": the Anthropic Messages API (or a compatible endpoint).
|
|
16
|
+
*/
|
|
17
|
+
type: UpstreamType;
|
|
18
|
+
/** As in the official SDKs: "https://api.openai.com/v1", "https://api.anthropic.com". */
|
|
19
|
+
baseUrl: string;
|
|
20
|
+
/** Upstream key. When unset, the caller's own key is passed through. */
|
|
21
|
+
apiKey?: string;
|
|
22
|
+
/** How the key is sent: Bearer token (OpenAI), x-api-key (Anthropic) or api-key (Azure). */
|
|
23
|
+
authHeader?: "authorization" | "x-api-key" | "api-key";
|
|
24
|
+
/** Extra headers sent upstream on every request. */
|
|
25
|
+
headers?: Record<string, string>;
|
|
26
|
+
/** Offers OpenAI's Batch API (/files + /batches). Default: on for api.openai.com and Anthropic. */
|
|
27
|
+
batch?: boolean;
|
|
28
|
+
/** Offers OpenAI's flex service tier. Default: on for api.openai.com only. */
|
|
29
|
+
flex?: boolean;
|
|
30
|
+
/** Send OpenAI's prompt_cache_key so calls in one run share a cache. Default: on for api.openai.com only. */
|
|
31
|
+
promptCacheKey?: boolean;
|
|
32
|
+
}
|
|
33
|
+
export interface Route {
|
|
34
|
+
/** Model id glob, e.g. "acme-*" or "gpt-4o*". */
|
|
35
|
+
model: string;
|
|
36
|
+
upstream: string;
|
|
37
|
+
}
|
|
38
|
+
export interface ClientKey {
|
|
39
|
+
name: string;
|
|
40
|
+
key: string;
|
|
41
|
+
}
|
|
42
|
+
export type Mode = "optimize" | "observe" | "off";
|
|
43
|
+
/** Optimizations that never change what the request means. On by default. */
|
|
44
|
+
export interface SafeOptimizations {
|
|
45
|
+
/** Rewrite the newest user message with the core optimizer (code, quotes and pasted material stay verbatim). */
|
|
46
|
+
prompt: boolean;
|
|
47
|
+
/** Re-serialize pretty-printed JSON in tool outputs without whitespace. */
|
|
48
|
+
minifyJson: boolean;
|
|
49
|
+
/** Collapse runs of identical lines in tool outputs ("[previous line repeated 40 more times]"). */
|
|
50
|
+
collapseRepeats: boolean;
|
|
51
|
+
/** A tool output identical to an earlier one in the same conversation is sent once; later copies point back to it. */
|
|
52
|
+
dedupeToolOutputs: boolean;
|
|
53
|
+
/** Send a passage repeated within a request (retrieved chunks, quoted documents) once. */
|
|
54
|
+
dedupePassages: boolean;
|
|
55
|
+
/** Send an image repeated later in the conversation once. */
|
|
56
|
+
dedupeImages: boolean;
|
|
57
|
+
/** Add prompt-caching breakpoints / cache keys once a run repeats a large prefix. */
|
|
58
|
+
promptCaching: boolean;
|
|
59
|
+
/** Replay the response to an identical temperature-0, non-streamed request seen in the last `retryWindowSeconds`. */
|
|
60
|
+
dedupeRetries: boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Embeddings (/v1/embeddings): a text this team already embedded with the same
|
|
63
|
+
* model and settings is answered from memory; only new texts go upstream.
|
|
64
|
+
* Texts aren't stored, only a hash of each and its vector.
|
|
65
|
+
*/
|
|
66
|
+
cacheEmbeddings: boolean;
|
|
67
|
+
}
|
|
68
|
+
/** Optimizations that can change the answer. Off unless enabled here or per request. */
|
|
69
|
+
export interface OptInOptimizations {
|
|
70
|
+
/** Replace tool outputs older than the newest `keepLast` with a short stub. */
|
|
71
|
+
elideOldToolOutputs: false | {
|
|
72
|
+
keepLast: number;
|
|
73
|
+
minTokens: number;
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* Send tools the run hasn't used yet as a name and one-line summary, and load
|
|
77
|
+
* a tool's full definition only when the model asks for it. Applies to
|
|
78
|
+
* non-streamed requests with at least `minTools` function tools.
|
|
79
|
+
*/
|
|
80
|
+
lazyTools: false | {
|
|
81
|
+
minTools: number;
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* In older tool outputs (JSON), leave out fields the agent never referred to
|
|
85
|
+
* later in the conversation. Keeps the newest `keepLast` outputs whole.
|
|
86
|
+
*/
|
|
87
|
+
trimToolFields: false | {
|
|
88
|
+
keepLast: number;
|
|
89
|
+
};
|
|
90
|
+
/** Write lists of records in JSON tool outputs as column names plus rows. */
|
|
91
|
+
compactJson: boolean;
|
|
92
|
+
/**
|
|
93
|
+
* Keep older tool outputs over `minTokens` outside the conversation (their first
|
|
94
|
+
* lines and an id stay), with a reader tool the model calls to search or slice
|
|
95
|
+
* them. Non-streamed OpenAI Chat and Anthropic requests.
|
|
96
|
+
*/
|
|
97
|
+
parkToolOutputs: false | {
|
|
98
|
+
minTokens: number;
|
|
99
|
+
keepLast: number;
|
|
100
|
+
};
|
|
101
|
+
/**
|
|
102
|
+
* The provider's cheaper, slower processing on the same model (OpenAI's
|
|
103
|
+
* service_tier "flex"). Falls back to the normal tier when flex is unavailable.
|
|
104
|
+
*/
|
|
105
|
+
flex: boolean;
|
|
106
|
+
/**
|
|
107
|
+
* When several calls with the same large prefix arrive at once, send the first
|
|
108
|
+
* alone and hold the rest (up to `maxWaitMs`) so they read its cache.
|
|
109
|
+
*/
|
|
110
|
+
warmCache: false | {
|
|
111
|
+
maxWaitMs: number;
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Output checks: for a sample of optimized calls, also send the original
|
|
116
|
+
* request and compare the answers, as evidence the work is unchanged.
|
|
117
|
+
*/
|
|
118
|
+
export interface VerifyConfig {
|
|
119
|
+
/** Share of optimized calls to check, 0..1; 0 turns checks off. Calls using an opt-in fix are checked at least 25% of the time. */
|
|
120
|
+
sampleRate: number;
|
|
121
|
+
/** Text similarity (0..1) at or above which two answers count as the same; tool calls must match exactly. */
|
|
122
|
+
minSimilarity: number;
|
|
123
|
+
/**
|
|
124
|
+
* Checks cost tokens too: after the first `freeChecks` per team and agent, a call
|
|
125
|
+
* is only checked while check tokens stay under this share of tokens saved.
|
|
126
|
+
*/
|
|
127
|
+
maxShareOfSavings: number;
|
|
128
|
+
freeChecks: number;
|
|
129
|
+
/**
|
|
130
|
+
* Opt-in fixes are on probation for each team and agent until this many answers
|
|
131
|
+
* in a row matched the original's: until then the original is sent alongside
|
|
132
|
+
* and the app gets the optimized answer only when it matches.
|
|
133
|
+
*/
|
|
134
|
+
probation: number;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* A spending limit per run, off by default. Once a run reaches it, further calls
|
|
138
|
+
* get an error instead of being sent, which stops a looping agent from running
|
|
139
|
+
* up cost. Can also be set per request with x-token-lens-max-calls and
|
|
140
|
+
* x-token-lens-max-cost-usd.
|
|
141
|
+
*/
|
|
142
|
+
export interface RunBudget {
|
|
143
|
+
maxCalls?: number;
|
|
144
|
+
maxCostUsd?: number;
|
|
145
|
+
}
|
|
146
|
+
export interface GatewayConfig {
|
|
147
|
+
port: number;
|
|
148
|
+
host: string;
|
|
149
|
+
mode: Mode;
|
|
150
|
+
level: Level | "auto";
|
|
151
|
+
safe: SafeOptimizations;
|
|
152
|
+
optIn: OptInOptimizations;
|
|
153
|
+
verify: VerifyConfig;
|
|
154
|
+
runBudget: RunBudget;
|
|
155
|
+
/** Work that can wait (x-token-lens-batch): how often queued calls go out and results are checked, and the price factor. */
|
|
156
|
+
batch: {
|
|
157
|
+
flushSeconds: number;
|
|
158
|
+
pollSeconds: number;
|
|
159
|
+
maxRequests: number;
|
|
160
|
+
discount: number;
|
|
161
|
+
};
|
|
162
|
+
/** Price factor of the flex tier against the normal one. */
|
|
163
|
+
flexDiscount: number;
|
|
164
|
+
/** Memory for cached embeddings, per gateway. */
|
|
165
|
+
embeddingsCacheMB: number;
|
|
166
|
+
/**
|
|
167
|
+
* Models that charge a higher rate once a request passes a size (for some
|
|
168
|
+
* models, every token of a request above about 200K input tokens). Matched by
|
|
169
|
+
* model id glob. Built-in models already carry their list price for this
|
|
170
|
+
* (Gemini Pro above 200K input tokens); add or override models here.
|
|
171
|
+
*/
|
|
172
|
+
priceTiers: PriceTier[];
|
|
173
|
+
/** Shared report storage (Redis, or a file); off by default, everything stays in memory. */
|
|
174
|
+
storage: StorageConfig;
|
|
175
|
+
retryWindowSeconds: number;
|
|
176
|
+
/** Reject requests that cannot fit the model's context window before paying for them. */
|
|
177
|
+
blockOverContext: boolean;
|
|
178
|
+
upstreams: Record<string, UpstreamConfig>;
|
|
179
|
+
routes: Route[];
|
|
180
|
+
/** Upstream for OpenAI-style requests no route matches. */
|
|
181
|
+
defaultUpstream?: string;
|
|
182
|
+
/** Custom models: a company's own LLM, or exact numbers for a built-in one. */
|
|
183
|
+
models: CustomModel[];
|
|
184
|
+
/** Keys callers must present. Empty: no gateway auth (only for private networks). */
|
|
185
|
+
clientKeys: ClientKey[];
|
|
186
|
+
/** Key for the admin endpoints (registering models at runtime). */
|
|
187
|
+
adminKey?: string;
|
|
188
|
+
maxBodyBytes: number;
|
|
189
|
+
/** Runs kept in memory for /token-lens/runs. */
|
|
190
|
+
maxRuns: number;
|
|
191
|
+
logRequests: boolean;
|
|
192
|
+
/**
|
|
193
|
+
* Show tool-call inputs (which may hold customer data) in run reports. Off by
|
|
194
|
+
* default: repeats are still detected, and reports name the input by a short hash.
|
|
195
|
+
*/
|
|
196
|
+
reportToolArgs: boolean;
|
|
197
|
+
}
|
|
198
|
+
export interface PriceTier {
|
|
199
|
+
model: string;
|
|
200
|
+
aboveInputTokens: number;
|
|
201
|
+
/** Rates for the whole request once it is above the limit, USD per million tokens. */
|
|
202
|
+
inputPerMTok: number;
|
|
203
|
+
outputPerMTok: number;
|
|
204
|
+
}
|
|
205
|
+
export declare const DEFAULTS: GatewayConfig;
|
|
206
|
+
type Env = Record<string, string | undefined>;
|
|
207
|
+
/** The config file: every field optional, nested sections merged with the defaults. */
|
|
208
|
+
export type ConfigFile = Partial<Omit<GatewayConfig, "safe" | "optIn" | "verify" | "batch" | "storage">> & {
|
|
209
|
+
safe?: Partial<SafeOptimizations>;
|
|
210
|
+
optIn?: Partial<OptInOptimizations>;
|
|
211
|
+
verify?: Partial<VerifyConfig>;
|
|
212
|
+
batch?: Partial<GatewayConfig["batch"]>;
|
|
213
|
+
storage?: Partial<StorageConfig>;
|
|
214
|
+
};
|
|
215
|
+
export declare function buildConfig(partial?: ConfigFile, env?: Env): GatewayConfig;
|
|
216
|
+
export declare function loadConfig(env?: Env): GatewayConfig;
|
|
217
|
+
export {};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keeps answers unchanged: the rule every gateway fix follows.
|
|
3
|
+
*
|
|
4
|
+
* - A fix that was in play when an output check found a different answer is
|
|
5
|
+
* switched off for that team and agent, unless the model's own answers to the
|
|
6
|
+
* original request differ just as much (then the fix isn't the cause). It
|
|
7
|
+
* stays off until an admin turns it back on (POST /token-lens/guard/reset).
|
|
8
|
+
* - Fixes that can change an answer by design (opt-in: elide-tool-outputs,
|
|
9
|
+
* lazy-tools, trim-tool-fields) start on probation: the gateway sends the original request
|
|
10
|
+
* alongside and answers with the optimized result only when the two match;
|
|
11
|
+
* otherwise the app gets the original's answer. After `probation` matches in a
|
|
12
|
+
* row the fix runs on its own, with output checks sampled as usual.
|
|
13
|
+
* - While on probation, opt-in fixes are left out of streamed and batched calls,
|
|
14
|
+
* whose answer can't be compared before the app gets it.
|
|
15
|
+
*/
|
|
16
|
+
import type { GatewayConfig } from "./config";
|
|
17
|
+
import type { Check } from "./verify";
|
|
18
|
+
export declare const OPT_IN_FIXES: Set<string>;
|
|
19
|
+
export declare const guardable: (id: string) => boolean;
|
|
20
|
+
export declare class OutputGuard {
|
|
21
|
+
private probation;
|
|
22
|
+
private states;
|
|
23
|
+
constructor(probation: number);
|
|
24
|
+
private state;
|
|
25
|
+
/** The settings for one request: switched-off fixes removed, and opt-in fixes on probation removed where the answer can't be compared first. */
|
|
26
|
+
configFor(cfg: GatewayConfig, app: string, agent: string, allow: Set<string>, uncheckable: boolean): {
|
|
27
|
+
cfg: GatewayConfig;
|
|
28
|
+
allow: Set<string>;
|
|
29
|
+
};
|
|
30
|
+
/** The opt-in fixes among `ids` that still need their answer compared before it is sent. */
|
|
31
|
+
onProbation(app: string, agent: string, ids: string[]): string[];
|
|
32
|
+
confirm(app: string, agent: string, ids: string[]): void;
|
|
33
|
+
/** Switches off every fix that was in play when an answer changed. */
|
|
34
|
+
suspend(app: string, agent: string, ids: string[], check: Pick<Check, "at" | "similarity" | "sameToolCalls">): string[];
|
|
35
|
+
isSuspended(app: string, agent: string, id: string): boolean;
|
|
36
|
+
/** Turns fixes back on (all, one app, or one app and agent). Opt-in fixes start a new probation. */
|
|
37
|
+
reset(app?: string, agent?: string): number;
|
|
38
|
+
/** Makes the switched-off fixes exactly `list` (from shared storage, so every gateway copy agrees). */
|
|
39
|
+
replaceSuspended(list: Array<{
|
|
40
|
+
app: string;
|
|
41
|
+
agent: string;
|
|
42
|
+
fix: string;
|
|
43
|
+
since: string;
|
|
44
|
+
similarity: number;
|
|
45
|
+
sameToolCalls: boolean;
|
|
46
|
+
}>): void;
|
|
47
|
+
/** Switched-off fixes, for stats and the report. */
|
|
48
|
+
list(): Array<{
|
|
49
|
+
app: string;
|
|
50
|
+
agent: string;
|
|
51
|
+
fix: string;
|
|
52
|
+
since: string;
|
|
53
|
+
similarity: number;
|
|
54
|
+
sameToolCalls: boolean;
|
|
55
|
+
}>;
|
|
56
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Images in requests: what they cost, and sending a repeated image once.
|
|
3
|
+
*
|
|
4
|
+
* Image tokens are estimated from each image's size with the provider's own
|
|
5
|
+
* published rules. Hosted providers shrink large images before counting them,
|
|
6
|
+
* so sending an image smaller than their limit saves upload time but not
|
|
7
|
+
* tokens. The token savings come from not re-sending the same image, cropping,
|
|
8
|
+
* or a lower detail setting where it is enough (which can change answers).
|
|
9
|
+
*/
|
|
10
|
+
import type { Protocol } from "./protocol";
|
|
11
|
+
export interface ImageRef {
|
|
12
|
+
/** Position of the message in the conversation. */
|
|
13
|
+
turn: number;
|
|
14
|
+
/** Same key, same image (hash of the data or the URL). */
|
|
15
|
+
key: string;
|
|
16
|
+
width?: number;
|
|
17
|
+
height?: number;
|
|
18
|
+
detail?: string;
|
|
19
|
+
/** Replaces the image with a text note. */
|
|
20
|
+
replace(text: string): void;
|
|
21
|
+
}
|
|
22
|
+
/** Width and height from the first bytes of a PNG, JPEG, GIF or WebP file. */
|
|
23
|
+
export declare function imageSize(buf: Buffer): {
|
|
24
|
+
width: number;
|
|
25
|
+
height: number;
|
|
26
|
+
} | undefined;
|
|
27
|
+
/** Every image in the request, in order, with a way to replace it by text. */
|
|
28
|
+
export declare function findImages(protocol: Protocol, body: any): ImageRef[];
|
|
29
|
+
/**
|
|
30
|
+
* Estimated input tokens for one image, after the provider's own resizing.
|
|
31
|
+
* Anthropic: about width × height / 750 once the long edge fits 1,568 px (at most
|
|
32
|
+
* about 1,600 tokens). OpenAI tile models: 85 + 170 per 512 px tile after fitting
|
|
33
|
+
* 2,048 px and a 768 px short side; "low" detail is 85. Unknown size: undefined.
|
|
34
|
+
*/
|
|
35
|
+
export declare function imageTokens(provider: "openai" | "anthropic", img: Pick<ImageRef, "width" | "height" | "detail">): number | undefined;
|
|
36
|
+
/** Replaces later copies of an image with a note pointing to the first. Returns the tokens saved and how many. */
|
|
37
|
+
export declare function dedupeImages(images: ImageRef[], provider: "openai" | "anthropic"): {
|
|
38
|
+
saved: number;
|
|
39
|
+
count: number;
|
|
40
|
+
};
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Savings accounting: tokens and money before and after Token Lens, per team
|
|
3
|
+
* (client key), agent, model and run, next to the output checks that show the
|
|
4
|
+
* work was unchanged.
|
|
5
|
+
*
|
|
6
|
+
* "Before" is what the same calls would have cost without the gateway: the
|
|
7
|
+
* tokens it removed added back, cached input priced as normal input, replayed
|
|
8
|
+
* retries paid in full. "After" is what was actually billed. The output checks'
|
|
9
|
+
* own extra calls are reported separately, so the net saving is honest.
|
|
10
|
+
*/
|
|
11
|
+
import type { ModelProfile } from "../core";
|
|
12
|
+
import type { Usage } from "./protocol";
|
|
13
|
+
export interface Totals {
|
|
14
|
+
requests: number;
|
|
15
|
+
errors: number;
|
|
16
|
+
replayed: number;
|
|
17
|
+
/** Input tokens actually sent (including cached). */
|
|
18
|
+
inputTokens: number;
|
|
19
|
+
/** Input tokens the same calls would have sent without Token Lens. */
|
|
20
|
+
inputTokensBefore: number;
|
|
21
|
+
outputTokens: number;
|
|
22
|
+
/** Output tokens not generated because a retry was replayed. */
|
|
23
|
+
outputTokensAvoided: number;
|
|
24
|
+
cachedInputTokens: number;
|
|
25
|
+
reasoningTokens: number;
|
|
26
|
+
savedInputTokens: number;
|
|
27
|
+
costUsd: number;
|
|
28
|
+
costBeforeUsd: number;
|
|
29
|
+
/** Calls that have a price (a model without pricing counts tokens only). */
|
|
30
|
+
pricedRequests: number;
|
|
31
|
+
checks: number;
|
|
32
|
+
checksMatched: number;
|
|
33
|
+
checkCostUsd: number;
|
|
34
|
+
}
|
|
35
|
+
export declare const emptyTotals: () => Totals;
|
|
36
|
+
/** A model's higher rate for the whole request once its input passes a size. */
|
|
37
|
+
export interface PriceTierRates {
|
|
38
|
+
aboveInputTokens: number;
|
|
39
|
+
inputPerMTok: number;
|
|
40
|
+
outputPerMTok: number;
|
|
41
|
+
}
|
|
42
|
+
/** A profile, with the higher-price tier of the model actually called when it has one. */
|
|
43
|
+
export type Priced = ModelProfile & {
|
|
44
|
+
priceTier?: PriceTierRates;
|
|
45
|
+
};
|
|
46
|
+
/** Billed cost of a call's usage: cached reads and cache writes at their own rates when known. */
|
|
47
|
+
export declare function cost(profile: Priced, usage: Usage): number;
|
|
48
|
+
/** What a call would have cost without Token Lens: removed tokens back, no cache discount. */
|
|
49
|
+
export declare function costBefore(profile: Priced, usage: Usage, savedInput: number): number;
|
|
50
|
+
export interface CallMetrics {
|
|
51
|
+
app: string;
|
|
52
|
+
agent: string;
|
|
53
|
+
run: string;
|
|
54
|
+
profile: Priced;
|
|
55
|
+
status: number;
|
|
56
|
+
usage: Usage;
|
|
57
|
+
/** Price multiplier the provider applied (0.5 for its batch option). */
|
|
58
|
+
priceFactor?: number;
|
|
59
|
+
/** Gateway's own input count, used when the upstream doesn't report usage. */
|
|
60
|
+
estimatedInput: number;
|
|
61
|
+
savedInput: number;
|
|
62
|
+
replayed: boolean;
|
|
63
|
+
}
|
|
64
|
+
/** Add one call to a totals row. */
|
|
65
|
+
export declare function addCall(t: Totals, m: Omit<CallMetrics, "app" | "agent" | "run">): void;
|
|
66
|
+
export declare class Metrics {
|
|
67
|
+
/** Keyed by app, agent and model. */
|
|
68
|
+
private rows;
|
|
69
|
+
readonly started: string;
|
|
70
|
+
private row;
|
|
71
|
+
record(m: CallMetrics): void;
|
|
72
|
+
/** One output check; `counted` false records only the cost of an extra call made for a check. */
|
|
73
|
+
recordCheck(app: string, agent: string, profile: ModelProfile, matched: boolean, checkCost: number, counted?: boolean): void;
|
|
74
|
+
/** The raw rows (key: team, agent and model), for shared storage. */
|
|
75
|
+
snapshot(): Map<string, Totals>;
|
|
76
|
+
private grouped;
|
|
77
|
+
/** One row per team, agent and model, for exports. */
|
|
78
|
+
detail(rows?: Map<string, Totals>): {
|
|
79
|
+
costUsd: number;
|
|
80
|
+
costBeforeUsd: number;
|
|
81
|
+
checkCostUsd: number;
|
|
82
|
+
savedTokens: number;
|
|
83
|
+
savedTokensPct: number;
|
|
84
|
+
savedUsd: number;
|
|
85
|
+
savedUsdPct: number;
|
|
86
|
+
netSavedUsd: number;
|
|
87
|
+
outputUnchangedPct: number;
|
|
88
|
+
unpricedRequests: number;
|
|
89
|
+
requests: number;
|
|
90
|
+
errors: number;
|
|
91
|
+
replayed: number;
|
|
92
|
+
/** Input tokens actually sent (including cached). */
|
|
93
|
+
inputTokens: number;
|
|
94
|
+
/** Input tokens the same calls would have sent without Token Lens. */
|
|
95
|
+
inputTokensBefore: number;
|
|
96
|
+
outputTokens: number;
|
|
97
|
+
/** Output tokens not generated because a retry was replayed. */
|
|
98
|
+
outputTokensAvoided: number;
|
|
99
|
+
cachedInputTokens: number;
|
|
100
|
+
reasoningTokens: number;
|
|
101
|
+
savedInputTokens: number;
|
|
102
|
+
/** Calls that have a price (a model without pricing counts tokens only). */
|
|
103
|
+
pricedRequests: number;
|
|
104
|
+
checks: number;
|
|
105
|
+
checksMatched: number;
|
|
106
|
+
}[];
|
|
107
|
+
/** Totals by team, agent and model; `rows` from shared storage when given, else this gateway's own. */
|
|
108
|
+
json(rows?: Map<string, Totals>, since?: string): {
|
|
109
|
+
since: string;
|
|
110
|
+
total: {
|
|
111
|
+
costUsd: number;
|
|
112
|
+
costBeforeUsd: number;
|
|
113
|
+
checkCostUsd: number;
|
|
114
|
+
savedTokens: number;
|
|
115
|
+
savedTokensPct: number;
|
|
116
|
+
savedUsd: number;
|
|
117
|
+
savedUsdPct: number;
|
|
118
|
+
netSavedUsd: number;
|
|
119
|
+
outputUnchangedPct: number;
|
|
120
|
+
unpricedRequests: number;
|
|
121
|
+
requests: number;
|
|
122
|
+
errors: number;
|
|
123
|
+
replayed: number;
|
|
124
|
+
/** Input tokens actually sent (including cached). */
|
|
125
|
+
inputTokens: number;
|
|
126
|
+
/** Input tokens the same calls would have sent without Token Lens. */
|
|
127
|
+
inputTokensBefore: number;
|
|
128
|
+
outputTokens: number;
|
|
129
|
+
/** Output tokens not generated because a retry was replayed. */
|
|
130
|
+
outputTokensAvoided: number;
|
|
131
|
+
cachedInputTokens: number;
|
|
132
|
+
reasoningTokens: number;
|
|
133
|
+
savedInputTokens: number;
|
|
134
|
+
/** Calls that have a price (a model without pricing counts tokens only). */
|
|
135
|
+
pricedRequests: number;
|
|
136
|
+
checks: number;
|
|
137
|
+
checksMatched: number;
|
|
138
|
+
};
|
|
139
|
+
byTeam: {
|
|
140
|
+
costUsd: number;
|
|
141
|
+
costBeforeUsd: number;
|
|
142
|
+
checkCostUsd: number;
|
|
143
|
+
savedTokens: number;
|
|
144
|
+
savedTokensPct: number;
|
|
145
|
+
savedUsd: number;
|
|
146
|
+
savedUsdPct: number;
|
|
147
|
+
netSavedUsd: number;
|
|
148
|
+
outputUnchangedPct: number;
|
|
149
|
+
unpricedRequests: number;
|
|
150
|
+
requests: number;
|
|
151
|
+
errors: number;
|
|
152
|
+
replayed: number;
|
|
153
|
+
/** Input tokens actually sent (including cached). */
|
|
154
|
+
inputTokens: number;
|
|
155
|
+
/** Input tokens the same calls would have sent without Token Lens. */
|
|
156
|
+
inputTokensBefore: number;
|
|
157
|
+
outputTokens: number;
|
|
158
|
+
/** Output tokens not generated because a retry was replayed. */
|
|
159
|
+
outputTokensAvoided: number;
|
|
160
|
+
cachedInputTokens: number;
|
|
161
|
+
reasoningTokens: number;
|
|
162
|
+
savedInputTokens: number;
|
|
163
|
+
/** Calls that have a price (a model without pricing counts tokens only). */
|
|
164
|
+
pricedRequests: number;
|
|
165
|
+
checks: number;
|
|
166
|
+
checksMatched: number;
|
|
167
|
+
}[];
|
|
168
|
+
byAgent: {
|
|
169
|
+
costUsd: number;
|
|
170
|
+
costBeforeUsd: number;
|
|
171
|
+
checkCostUsd: number;
|
|
172
|
+
savedTokens: number;
|
|
173
|
+
savedTokensPct: number;
|
|
174
|
+
savedUsd: number;
|
|
175
|
+
savedUsdPct: number;
|
|
176
|
+
netSavedUsd: number;
|
|
177
|
+
outputUnchangedPct: number;
|
|
178
|
+
unpricedRequests: number;
|
|
179
|
+
requests: number;
|
|
180
|
+
errors: number;
|
|
181
|
+
replayed: number;
|
|
182
|
+
/** Input tokens actually sent (including cached). */
|
|
183
|
+
inputTokens: number;
|
|
184
|
+
/** Input tokens the same calls would have sent without Token Lens. */
|
|
185
|
+
inputTokensBefore: number;
|
|
186
|
+
outputTokens: number;
|
|
187
|
+
/** Output tokens not generated because a retry was replayed. */
|
|
188
|
+
outputTokensAvoided: number;
|
|
189
|
+
cachedInputTokens: number;
|
|
190
|
+
reasoningTokens: number;
|
|
191
|
+
savedInputTokens: number;
|
|
192
|
+
/** Calls that have a price (a model without pricing counts tokens only). */
|
|
193
|
+
pricedRequests: number;
|
|
194
|
+
checks: number;
|
|
195
|
+
checksMatched: number;
|
|
196
|
+
}[];
|
|
197
|
+
byModel: {
|
|
198
|
+
costUsd: number;
|
|
199
|
+
costBeforeUsd: number;
|
|
200
|
+
checkCostUsd: number;
|
|
201
|
+
savedTokens: number;
|
|
202
|
+
savedTokensPct: number;
|
|
203
|
+
savedUsd: number;
|
|
204
|
+
savedUsdPct: number;
|
|
205
|
+
netSavedUsd: number;
|
|
206
|
+
outputUnchangedPct: number;
|
|
207
|
+
unpricedRequests: number;
|
|
208
|
+
requests: number;
|
|
209
|
+
errors: number;
|
|
210
|
+
replayed: number;
|
|
211
|
+
/** Input tokens actually sent (including cached). */
|
|
212
|
+
inputTokens: number;
|
|
213
|
+
/** Input tokens the same calls would have sent without Token Lens. */
|
|
214
|
+
inputTokensBefore: number;
|
|
215
|
+
outputTokens: number;
|
|
216
|
+
/** Output tokens not generated because a retry was replayed. */
|
|
217
|
+
outputTokensAvoided: number;
|
|
218
|
+
cachedInputTokens: number;
|
|
219
|
+
reasoningTokens: number;
|
|
220
|
+
savedInputTokens: number;
|
|
221
|
+
/** Calls that have a price (a model without pricing counts tokens only). */
|
|
222
|
+
pricedRequests: number;
|
|
223
|
+
checks: number;
|
|
224
|
+
checksMatched: number;
|
|
225
|
+
}[];
|
|
226
|
+
byAppAndModel: {
|
|
227
|
+
costUsd: number;
|
|
228
|
+
costBeforeUsd: number;
|
|
229
|
+
checkCostUsd: number;
|
|
230
|
+
savedTokens: number;
|
|
231
|
+
savedTokensPct: number;
|
|
232
|
+
savedUsd: number;
|
|
233
|
+
savedUsdPct: number;
|
|
234
|
+
netSavedUsd: number;
|
|
235
|
+
outputUnchangedPct: number;
|
|
236
|
+
unpricedRequests: number;
|
|
237
|
+
requests: number;
|
|
238
|
+
errors: number;
|
|
239
|
+
replayed: number;
|
|
240
|
+
/** Input tokens actually sent (including cached). */
|
|
241
|
+
inputTokens: number;
|
|
242
|
+
/** Input tokens the same calls would have sent without Token Lens. */
|
|
243
|
+
inputTokensBefore: number;
|
|
244
|
+
outputTokens: number;
|
|
245
|
+
/** Output tokens not generated because a retry was replayed. */
|
|
246
|
+
outputTokensAvoided: number;
|
|
247
|
+
cachedInputTokens: number;
|
|
248
|
+
reasoningTokens: number;
|
|
249
|
+
savedInputTokens: number;
|
|
250
|
+
/** Calls that have a price (a model without pricing counts tokens only). */
|
|
251
|
+
pricedRequests: number;
|
|
252
|
+
checks: number;
|
|
253
|
+
checksMatched: number;
|
|
254
|
+
}[];
|
|
255
|
+
};
|
|
256
|
+
prometheus(): string;
|
|
257
|
+
}
|
|
258
|
+
/** A totals row with the derived savings figures. */
|
|
259
|
+
export declare function view(t: Totals): {
|
|
260
|
+
costUsd: number;
|
|
261
|
+
costBeforeUsd: number;
|
|
262
|
+
checkCostUsd: number;
|
|
263
|
+
savedTokens: number;
|
|
264
|
+
savedTokensPct: number;
|
|
265
|
+
savedUsd: number;
|
|
266
|
+
savedUsdPct: number;
|
|
267
|
+
netSavedUsd: number;
|
|
268
|
+
outputUnchangedPct: number;
|
|
269
|
+
unpricedRequests: number;
|
|
270
|
+
requests: number;
|
|
271
|
+
errors: number;
|
|
272
|
+
replayed: number;
|
|
273
|
+
/** Input tokens actually sent (including cached). */
|
|
274
|
+
inputTokens: number;
|
|
275
|
+
/** Input tokens the same calls would have sent without Token Lens. */
|
|
276
|
+
inputTokensBefore: number;
|
|
277
|
+
outputTokens: number;
|
|
278
|
+
/** Output tokens not generated because a retry was replayed. */
|
|
279
|
+
outputTokensAvoided: number;
|
|
280
|
+
cachedInputTokens: number;
|
|
281
|
+
reasoningTokens: number;
|
|
282
|
+
savedInputTokens: number;
|
|
283
|
+
/** Calls that have a price (a model without pricing counts tokens only). */
|
|
284
|
+
pricedRequests: number;
|
|
285
|
+
checks: number;
|
|
286
|
+
checksMatched: number;
|
|
287
|
+
};
|