@kunal_dutt/token-lens 0.2.17 → 0.2.19

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/sdk.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/sdk/index.ts
2
- import { createHash as createHash3, randomUUID } from "node:crypto";
2
+ import { createHash as createHash4, randomUUID } from "node:crypto";
3
3
 
4
4
  // src/core/advice.ts
5
5
  var fmt = (n) => n.toLocaleString("en-US");
@@ -203,7 +203,7 @@ var BytePairEncodingCore = class {
203
203
  });
204
204
  this.bytePairNonUtfSortedEncoder = binaryLookup.sort((a, b) => compareUint8Arrays(a[0], b[0]));
205
205
  this.specialTokensEncoder = specialTokensEncoder ?? /* @__PURE__ */ new Map();
206
- this.specialTokensDecoder = specialTokensEncoder ? new Map([...specialTokensEncoder].map(([key, value]) => [value, key])) : /* @__PURE__ */ new Map();
206
+ this.specialTokensDecoder = specialTokensEncoder ? new Map([...specialTokensEncoder].map(([key2, value]) => [value, key2])) : /* @__PURE__ */ new Map();
207
207
  this.tokenSplitRegex = tokenSplitRegex;
208
208
  const allSpecialTokensRegex = [...this.specialTokensEncoder.keys()].map(escapeRegExp).join("|");
209
209
  try {
@@ -336,38 +336,38 @@ var BytePairEncodingCore = class {
336
336
  if (tokenBytesOrString) yield tokenBytesOrString;
337
337
  }
338
338
  }
339
- getBpeRankFromString(key) {
340
- return this.bytePairStringRankEncoder.get(key);
339
+ getBpeRankFromString(key2) {
340
+ return this.bytePairStringRankEncoder.get(key2);
341
341
  }
342
- getBpeRankFromStringOrThrow(key) {
343
- const value = this.getBpeRankFromString(key);
344
- if (value === void 0) throw new Error(`The byte-pair encoding does not contain a value for: ${key}`);
342
+ getBpeRankFromStringOrThrow(key2) {
343
+ const value = this.getBpeRankFromString(key2);
344
+ if (value === void 0) throw new Error(`The byte-pair encoding does not contain a value for: ${key2}`);
345
345
  return value;
346
346
  }
347
- getBpeRankFromBytes(key) {
348
- const keyAsString = tryConvertToString(key);
347
+ getBpeRankFromBytes(key2) {
348
+ const keyAsString = tryConvertToString(key2);
349
349
  if (keyAsString !== void 0) return this.getBpeRankFromString(keyAsString);
350
- const index = this.binarySearch(key);
350
+ const index = this.binarySearch(key2);
351
351
  if (index !== -1) return this.bytePairNonUtfSortedEncoder[index][1];
352
352
  }
353
- getBpeRankFromBytesOrThrow(key) {
354
- const value = this.getBpeRankFromBytes(key);
355
- if (value === void 0) throw new Error(`The byte-pair encoding does not contain a value for: ${key.toString()}`);
353
+ getBpeRankFromBytesOrThrow(key2) {
354
+ const value = this.getBpeRankFromBytes(key2);
355
+ if (value === void 0) throw new Error(`The byte-pair encoding does not contain a value for: ${key2.toString()}`);
356
356
  return value;
357
357
  }
358
- binarySearch(key) {
358
+ binarySearch(key2) {
359
359
  let low = 0;
360
360
  let high = this.bytePairNonUtfSortedEncoder.length - 1;
361
361
  while (low <= high) {
362
362
  const mid = low + high >>> 1;
363
363
  const midKey = this.bytePairNonUtfSortedEncoder[mid][0];
364
364
  let cmp = 0;
365
- const maxLength = Math.min(midKey.length, key.length);
365
+ const maxLength = Math.min(midKey.length, key2.length);
366
366
  for (let i = 0; i < maxLength; i++) {
367
- cmp = midKey[i] - key[i];
367
+ cmp = midKey[i] - key2[i];
368
368
  if (cmp !== 0) break;
369
369
  }
370
- if (cmp === 0) cmp = midKey.length - key.length;
370
+ if (cmp === 0) cmp = midKey.length - key2.length;
371
371
  if (cmp === 0) return mid;
372
372
  if (cmp < 0) low = mid + 1;
373
373
  else high = mid - 1;
@@ -394,13 +394,13 @@ var BytePairEncodingCore = class {
394
394
  }
395
395
  return this.specialTokensDecoder.get(tokenRank);
396
396
  }
397
- addToMergeCache(key, value) {
397
+ addToMergeCache(key2, value) {
398
398
  if (!this.mergeCache) return;
399
399
  if (this.mergeCache.size >= this.mergeCacheSize) {
400
400
  const firstKey = this.mergeCache.keys().next().value;
401
401
  this.mergeCache.delete(firstKey);
402
402
  }
403
- this.mergeCache.set(key, value);
403
+ this.mergeCache.set(key2, value);
404
404
  }
405
405
  bytePairEncode(input) {
406
406
  if (input.length === 1 && isAscii(input.codePointAt(0))) return [this.getBpeRankFromStringOrThrow(input)];
@@ -420,8 +420,8 @@ var BytePairEncodingCore = class {
420
420
  const ranks = [];
421
421
  const getRank = (startIndex, pairStart = starts[startIndex], pairEnd = starts[startIndex + 2]) => {
422
422
  if (pairEnd === void 0) return Number.POSITIVE_INFINITY;
423
- const key = piece.subarray(pairStart, pairEnd);
424
- return this.getBpeRankFromBytes(key) ?? Number.POSITIVE_INFINITY;
423
+ const key2 = piece.subarray(pairStart, pairEnd);
424
+ return this.getBpeRankFromBytes(key2) ?? Number.POSITIVE_INFINITY;
425
425
  };
426
426
  for (let i = 0; i <= piece.length; i++) {
427
427
  starts.push(i);
@@ -206098,8 +206098,8 @@ function counts(text) {
206098
206098
  const m = /* @__PURE__ */ new Map();
206099
206099
  for (const [kind, re] of ANCHORS) {
206100
206100
  for (const hit of text.match(re) ?? []) {
206101
- const key = `${kind}:${hit.toLowerCase()}`;
206102
- m.set(key, (m.get(key) ?? 0) + 1);
206101
+ const key2 = `${kind}:${hit.toLowerCase()}`;
206102
+ m.set(key2, (m.get(key2) ?? 0) + 1);
206103
206103
  }
206104
206104
  }
206105
206105
  return m;
@@ -206108,7 +206108,7 @@ function checkMeaning(original, rewritten, mode = "count") {
206108
206108
  const before = counts(original);
206109
206109
  const after = counts(rewritten);
206110
206110
  const lost = [];
206111
- for (const [key, n] of before) if ((after.get(key) ?? 0) < (mode === "count" ? n : 1)) lost.push(key);
206111
+ for (const [key2, n] of before) if ((after.get(key2) ?? 0) < (mode === "count" ? n : 1)) lost.push(key2);
206112
206112
  return { ok: lost.length === 0, lost };
206113
206113
  }
206114
206114
  var DROPPABLE = new Set(
@@ -206207,13 +206207,13 @@ function dedupeSentences(text, applied) {
206207
206207
  const seen = /* @__PURE__ */ new Set();
206208
206208
  return text.split("\n").map(
206209
206209
  (line) => line.split(/(?<=[.!?])\s+/).filter((sentence) => {
206210
- const key = sentence.replace(/\W+/g, " ").trim().toLowerCase();
206211
- if (key.split(" ").length < 4) return true;
206212
- if (seen.has(key)) {
206210
+ const key2 = sentence.replace(/\W+/g, " ").trim().toLowerCase();
206211
+ if (key2.split(" ").length < 4) return true;
206212
+ if (seen.has(key2)) {
206213
206213
  applied.push("repeated sentence");
206214
206214
  return false;
206215
206215
  }
206216
- seen.add(key);
206216
+ seen.add(key2);
206217
206217
  return true;
206218
206218
  }).join(" ")
206219
206219
  ).join("\n");
@@ -206261,11 +206261,11 @@ function trimLog(text, level, applied) {
206261
206261
  i++;
206262
206262
  }
206263
206263
  }
206264
- const key = level === "aggressive" ? normalizeLogLine : (l) => l;
206264
+ const key2 = level === "aggressive" ? normalizeLogLine : (l) => l;
206265
206265
  const result = [];
206266
206266
  for (let i = 0; i < out.length; ) {
206267
206267
  let j = i + 1;
206268
- while (j < out.length && out[i].trim() && key(out[j]) === key(out[i])) j++;
206268
+ while (j < out.length && out[i].trim() && key2(out[j]) === key2(out[i])) j++;
206269
206269
  result.push(out[i]);
206270
206270
  if (j - i >= 3) {
206271
206271
  result.push(`... same line repeated ${j - i - 1} more times`);
@@ -206480,6 +206480,90 @@ var PROFILES = {
206480
206480
  reasoning: false,
206481
206481
  pricing: { inputPerMTok: 2, outputPerMTok: 6 }
206482
206482
  }),
206483
+ // Open-weight families, as served by API hosts (Groq, Together, Fireworks, OpenRouter, their makers' own
206484
+ // APIs) or run locally (Ollama, LM Studio, vLLM). Hosted prices differ by host: these are typical list
206485
+ // prices, and running locally costs no tokens, so set your own price in settings or registerModel.
206486
+ llama: P({
206487
+ id: "llama",
206488
+ label: "Llama",
206489
+ family: "other",
206490
+ contextWindow: 128e3,
206491
+ tokenizer: "heuristic",
206492
+ charsPerToken: 3.9,
206493
+ reasoning: false,
206494
+ pricing: { inputPerMTok: 0.27, outputPerMTok: 0.85 }
206495
+ }),
206496
+ qwen: P({
206497
+ id: "qwen",
206498
+ label: "Qwen",
206499
+ family: "other",
206500
+ contextWindow: 128e3,
206501
+ tokenizer: "heuristic",
206502
+ charsPerToken: 3.8,
206503
+ reasoning: false,
206504
+ pricing: { inputPerMTok: 0.4, outputPerMTok: 1.2 }
206505
+ }),
206506
+ kimi: P({
206507
+ id: "kimi",
206508
+ label: "Kimi",
206509
+ family: "other",
206510
+ contextWindow: 256e3,
206511
+ tokenizer: "heuristic",
206512
+ charsPerToken: 3.8,
206513
+ reasoning: false,
206514
+ pricing: { inputPerMTok: 0.6, outputPerMTok: 2.5, cachedInputPerMTok: 0.15 }
206515
+ }),
206516
+ glm: P({
206517
+ id: "glm",
206518
+ label: "GLM",
206519
+ family: "other",
206520
+ contextWindow: 2e5,
206521
+ tokenizer: "heuristic",
206522
+ charsPerToken: 3.8,
206523
+ reasoning: false,
206524
+ pricing: { inputPerMTok: 0.6, outputPerMTok: 2.2, cachedInputPerMTok: 0.11 }
206525
+ }),
206526
+ command: P({
206527
+ id: "command",
206528
+ label: "Cohere Command",
206529
+ family: "other",
206530
+ contextWindow: 256e3,
206531
+ tokenizer: "heuristic",
206532
+ charsPerToken: 4,
206533
+ reasoning: false,
206534
+ pricing: { inputPerMTok: 2.5, outputPerMTok: 10 }
206535
+ }),
206536
+ sonar: P({
206537
+ id: "sonar",
206538
+ label: "Perplexity Sonar",
206539
+ family: "other",
206540
+ contextWindow: 128e3,
206541
+ tokenizer: "heuristic",
206542
+ charsPerToken: 4,
206543
+ reasoning: false,
206544
+ pricing: { inputPerMTok: 1, outputPerMTok: 1 }
206545
+ }),
206546
+ // Small open models, mostly run locally (where tokens cost nothing): priced at a typical host's rate.
206547
+ gemma: P({
206548
+ id: "gemma",
206549
+ label: "Gemma",
206550
+ family: "other",
206551
+ contextWindow: 128e3,
206552
+ tokenizer: "heuristic",
206553
+ charsPerToken: 3.9,
206554
+ reasoning: false,
206555
+ pricing: { inputPerMTok: 0.1, outputPerMTok: 0.2 }
206556
+ }),
206557
+ phi: P({
206558
+ id: "phi",
206559
+ label: "Phi",
206560
+ family: "other",
206561
+ contextWindow: 16e3,
206562
+ tokenizer: "heuristic",
206563
+ charsPerToken: 3.9,
206564
+ reasoning: false,
206565
+ pricing: { inputPerMTok: 0.07, outputPerMTok: 0.14 }
206566
+ }),
206483
206567
  generic: P({
206484
206568
  id: "generic",
206485
206569
  label: "Unknown model",
@@ -206497,12 +206581,22 @@ var RULES2 = [
206497
206581
  [/\bclaude\b.*\bopus\b|\bopus\b/, "claude-opus"],
206498
206582
  [/\bsonnet\b/, "claude-sonnet"],
206499
206583
  [/\bhaiku\b/, "claude-haiku"],
206584
+ // Open models whose names can carry "flash" or "pro" come before the Gemini rules.
206585
+ [/\b(qwen|qwq)/, "qwen"],
206586
+ [/\b(glm|chatglm|zhipu)\b|\bglm-?\d/, "glm"],
206587
+ [/\b(kimi|moonshot)/, "kimi"],
206588
+ [/\bgemma/, "gemma"],
206500
206589
  [/\bgemini\b.*\bflash\b|\bflash\b/, "gemini-flash"],
206501
206590
  [/\bgemini\b.*\b(pro|ultra|deep think)\b|^\s*\d(\.\d)?\s*pro\b|\bdeep think\b/, "gemini-pro"],
206502
206591
  [/\bdeepseek\b.*\b(r1|reason|think|deepthink)|\bdeepthink\b/, "deepseek-reasoning"],
206503
206592
  [/\bdeepseek\b/, "deepseek"],
206504
206593
  [/\b(mistral|le chat|magistral|codestral)\b/, "mistral"],
206505
206594
  [/\bgrok\b/, "grok"],
206595
+ // After DeepSeek, so "deepseek-r1-distill-llama" stays DeepSeek.
206596
+ [/\bllama|codellama/, "llama"],
206597
+ [/\bcommand[- ]?(r|a)\b|\bcohere\b/, "command"],
206598
+ [/\bsonar\b|\bperplexity\b/, "sonar"],
206599
+ [/\bphi-?\d/, "phi"],
206506
206600
  [/\bo[1-9](-mini|-pro)?\b|\bgpt[- ]?\S*\s*(thinking|pro)\b|\bthinking\b/, "gpt-reasoning"],
206507
206601
  [/\b(gpt|chatgpt)\b|\bgpt-?\d/, "gpt"],
206508
206602
  [/\bgemini\b/, "gemini-pro"],
@@ -206561,33 +206655,8 @@ function detectProfile(hint, siteId = "generic") {
206561
206655
  return PROFILES[SITE_DEFAULTS[siteId] ?? "generic"];
206562
206656
  }
206563
206657
 
206564
- // src/gateway/config.ts
206565
- var DEFAULTS = {
206566
- port: 8080,
206567
- host: "0.0.0.0",
206568
- mode: "optimize",
206569
- level: "auto",
206570
- safe: { prompt: true, minifyJson: true, collapseRepeats: true, dedupeToolOutputs: true, dedupePassages: true, dedupeImages: true, promptCaching: true, dedupeRetries: true, cacheEmbeddings: true },
206571
- optIn: { elideOldToolOutputs: false, lazyTools: false, trimToolFields: false, compactJson: false, parkToolOutputs: false, flex: false, warmCache: false },
206572
- verify: { sampleRate: 0.05, minSimilarity: 0.8, maxShareOfSavings: 0.2, freeChecks: 5, probation: 5 },
206573
- runBudget: {},
206574
- batch: { flushSeconds: 60, pollSeconds: 60, maxRequests: 1e3, discount: 0.5 },
206575
- flexDiscount: 0.5,
206576
- embeddingsCacheMB: 256,
206577
- // Models carry their own long-context price (src/core/models.ts); entries here override or add to them.
206578
- priceTiers: [],
206579
- storage: { keyPrefix: "token-lens:", flushSeconds: 10, runDays: 7 },
206580
- retryWindowSeconds: 120,
206581
- blockOverContext: false,
206582
- upstreams: {},
206583
- routes: [],
206584
- models: [],
206585
- clientKeys: [],
206586
- maxBodyBytes: 32 * 1024 * 1024,
206587
- maxRuns: 2e3,
206588
- logRequests: true,
206589
- reportToolArgs: false
206590
- };
206658
+ // src/gateway/customers.ts
206659
+ import { createHash } from "node:crypto";
206591
206660
 
206592
206661
  // src/gateway/metrics.ts
206593
206662
  var emptyTotals = () => ({
@@ -206671,12 +206740,260 @@ function view(t) {
206671
206740
  };
206672
206741
  }
206673
206742
 
206743
+ // src/gateway/customers.ts
206744
+ var CUSTOMER_DEFAULTS = {
206745
+ fromBody: false,
206746
+ hashIds: false,
206747
+ maxCustomers: 1e5,
206748
+ budget: {},
206749
+ plans: {},
206750
+ teamBudgets: {}
206751
+ };
206752
+ function periodKey(period, now) {
206753
+ const iso = now.toISOString();
206754
+ return period === "day" ? `d${iso.slice(0, 10)}` : `m${iso.slice(0, 7)}`;
206755
+ }
206756
+ var hashId = (id) => `h:${createHash("sha256").update(id).digest("hex").slice(0, 16)}`;
206757
+ function clean(id) {
206758
+ if (typeof id !== "string") return void 0;
206759
+ const s = id.replace(/[\u0000-\u001f\u007f]/g, "").trim().slice(0, 128);
206760
+ return s || void 0;
206761
+ }
206762
+ function customerOf(headerValue, body, cfg) {
206763
+ const raw = clean(headerValue) ?? (cfg.fromBody ? clean(body?.safety_identifier) ?? clean(body?.user) ?? clean(body?.metadata?.user_id) : void 0);
206764
+ if (!raw) return void 0;
206765
+ return cfg.hashIds ? hashId(raw) : raw;
206766
+ }
206767
+ var storedId = (id, cfg) => cfg.hashIds ? hashId(id) : id;
206768
+ var LIMITS = [
206769
+ ["dailyUsd", "day", "usd"],
206770
+ ["monthlyUsd", "month", "usd"],
206771
+ ["dailyTokens", "day", "tokens"],
206772
+ ["monthlyTokens", "month", "tokens"]
206773
+ ];
206774
+ var HEADER_LIMITS = {
206775
+ "daily-usd": "dailyUsd",
206776
+ "monthly-usd": "monthlyUsd",
206777
+ "daily-tokens": "dailyTokens",
206778
+ "monthly-tokens": "monthlyTokens"
206779
+ };
206780
+ var isLimit = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0;
206781
+ function cleanBudget(b) {
206782
+ const out = {};
206783
+ for (const [k] of LIMITS) if (isLimit(b?.[k])) out[k] = b[k];
206784
+ return out;
206785
+ }
206786
+ var UnknownPlanError = class extends Error {
206787
+ };
206788
+ function customerBudget(cfg, plan, header) {
206789
+ if (plan && !cfg.plans[plan]) throw new UnknownPlanError(`Unknown plan "${plan}". Plans in this gateway's config: ${Object.keys(cfg.plans).join(", ") || "none"}.`);
206790
+ const fromHeaders = {};
206791
+ for (const [suffix, key2] of Object.entries(HEADER_LIMITS)) {
206792
+ const v = header(`x-token-lens-customer-${suffix}`);
206793
+ if (v !== void 0 && v !== "") fromHeaders[key2] = Number(v);
206794
+ }
206795
+ return { ...cleanBudget(cfg.budget), ...cleanBudget(plan ? cfg.plans[plan] : void 0), ...cleanBudget(fromHeaders) };
206796
+ }
206797
+ var hasLimits = (b) => LIMITS.some(([k]) => b[k] !== void 0);
206798
+ var TEAM = "";
206799
+ var key = (...parts) => parts.join("\0");
206800
+ var CustomerLedger = class {
206801
+ constructor(maxCustomers) {
206802
+ this.maxCustomers = maxCustomers;
206803
+ }
206804
+ maxCustomers;
206805
+ /** key(app, customer) -> totals. Map order is least recently seen first. */
206806
+ rows = /* @__PURE__ */ new Map();
206807
+ /** key(period, app, customer | TEAM) -> spend recorded by this copy. */
206808
+ local = /* @__PURE__ */ new Map();
206809
+ /** What of `local` was already written to shared storage. */
206810
+ flushed = /* @__PURE__ */ new Map();
206811
+ /** Spend of every copy, as last read from shared storage (includes what this copy flushed). */
206812
+ shared = /* @__PURE__ */ new Map();
206813
+ periods = "";
206814
+ prune(now) {
206815
+ const day = periodKey("day", now);
206816
+ const month = periodKey("month", now);
206817
+ const current = day + month;
206818
+ if (current !== this.periods) {
206819
+ this.periods = current;
206820
+ for (const m of [this.local, this.flushed, this.shared])
206821
+ for (const k of m.keys()) {
206822
+ const p = k.slice(0, k.indexOf("\0"));
206823
+ if (p !== day && p !== month) m.delete(k);
206824
+ }
206825
+ }
206826
+ return [day, month];
206827
+ }
206828
+ /** One call: the same figures as the savings report, for the customer and its team. */
206829
+ record(app, customer, m, now = /* @__PURE__ */ new Date()) {
206830
+ const t = emptyTotals();
206831
+ addCall(t, m);
206832
+ if (customer) {
206833
+ const k = key(app, customer);
206834
+ let row = this.rows.get(k);
206835
+ if (row) this.rows.delete(k);
206836
+ else row = { totals: emptyTotals(), firstSeen: now.toISOString(), lastSeen: "" };
206837
+ row.lastSeen = now.toISOString();
206838
+ for (const f of Object.keys(t)) row.totals[f] += t[f];
206839
+ this.rows.set(k, row);
206840
+ while (this.rows.size > this.maxCustomers) this.rows.delete(this.rows.keys().next().value);
206841
+ }
206842
+ if (m.status >= 400 || m.replayed) return;
206843
+ const spend = { usd: t.costUsd, tokens: t.inputTokens + t.outputTokens };
206844
+ if (!spend.usd && !spend.tokens) return;
206845
+ for (const p of this.prune(now))
206846
+ for (const who of customer ? [customer, TEAM] : [TEAM]) {
206847
+ const k = key(p, app, who);
206848
+ const s = this.local.get(k) ?? { usd: 0, tokens: 0 };
206849
+ s.usd += spend.usd;
206850
+ s.tokens += spend.tokens;
206851
+ this.local.set(k, s);
206852
+ }
206853
+ }
206854
+ /** Spend in the current day or month, across every copy when shared storage is on. */
206855
+ spent(app, customer, period, now = /* @__PURE__ */ new Date()) {
206856
+ const [day, month] = this.prune(now);
206857
+ const k = key(period === "day" ? day : month, app, customer ?? TEAM);
206858
+ const l = this.local.get(k);
206859
+ const f = this.flushed.get(k);
206860
+ const s = this.shared.get(k);
206861
+ return {
206862
+ usd: (s?.usd ?? 0) + (l?.usd ?? 0) - (f?.usd ?? 0),
206863
+ tokens: (s?.tokens ?? 0) + (l?.tokens ?? 0) - (f?.tokens ?? 0)
206864
+ };
206865
+ }
206866
+ /** The first limit already reached, the customer's checked before the team's. */
206867
+ over(app, customer, budgets, now = /* @__PURE__ */ new Date()) {
206868
+ const checks = [["team", budgets.team, void 0]];
206869
+ if (customer) checks.unshift(["customer", budgets.customer, customer]);
206870
+ for (const [who, budget, id] of checks)
206871
+ for (const [limit, period, field] of LIMITS) {
206872
+ const cap = budget[limit];
206873
+ if (cap === void 0) continue;
206874
+ const spent = this.spent(app, id, period, now)[field];
206875
+ if (spent >= cap) return { who, limit, cap, spent };
206876
+ }
206877
+ return void 0;
206878
+ }
206879
+ /** What is left under the tightest limits that apply, for the response headers. */
206880
+ remaining(app, customer, budgets, now = /* @__PURE__ */ new Date()) {
206881
+ const out = {};
206882
+ const pairs = [[budgets.team, void 0]];
206883
+ if (customer) pairs.push([budgets.customer, customer]);
206884
+ for (const [budget, id] of pairs)
206885
+ for (const [limit, period, field] of LIMITS) {
206886
+ const cap = budget[limit];
206887
+ if (cap === void 0) continue;
206888
+ const left = Math.max(0, cap - this.spent(app, id, period, now)[field]);
206889
+ out[field] = Math.min(out[field] ?? Infinity, left);
206890
+ }
206891
+ return out;
206892
+ }
206893
+ // ---------------------------------------------------------- reports
206894
+ /** Raw totals rows (key: team and customer), for shared storage. */
206895
+ snapshot() {
206896
+ return new Map([...this.rows].map(([k, r]) => [k, { ...r.totals }]));
206897
+ }
206898
+ lastSeen(app, customer) {
206899
+ return this.rows.get(key(app, customer))?.lastSeen;
206900
+ }
206901
+ /**
206902
+ * One row per customer with savings, spend today and this month; `rows` from
206903
+ * shared storage when given, else this copy's own. `app` limits it to a team.
206904
+ */
206905
+ list(app, rows = this.snapshot(), now = /* @__PURE__ */ new Date()) {
206906
+ const out = [];
206907
+ for (const [k, t] of rows) {
206908
+ const [team, customer] = k.split("\0");
206909
+ if (app !== void 0 && team !== app) continue;
206910
+ out.push(this.view(team, customer, t, now));
206911
+ }
206912
+ return out.sort((a, b) => b.costBeforeUsd - a.costBeforeUsd);
206913
+ }
206914
+ view(team, customer, t = emptyTotals(), now = /* @__PURE__ */ new Date()) {
206915
+ const today = this.spent(team, customer, "day", now);
206916
+ const month = this.spent(team, customer, "month", now);
206917
+ const round2 = (n) => Math.round(n * 1e6) / 1e6;
206918
+ return {
206919
+ team,
206920
+ customer,
206921
+ ...view(t),
206922
+ lastSeen: this.lastSeen(team, customer),
206923
+ spentToday: { usd: round2(today.usd), tokens: today.tokens },
206924
+ spentThisMonth: { usd: round2(month.usd), tokens: month.tokens }
206925
+ };
206926
+ }
206927
+ // ---------------------------------------------------------- shared storage
206928
+ /** Spend recorded since the last flush, by period key and field; marks it flushed. */
206929
+ takeSpendDeltas(now = /* @__PURE__ */ new Date()) {
206930
+ this.prune(now);
206931
+ const out = [];
206932
+ for (const [k, s] of this.local) {
206933
+ const f = this.flushed.get(k) ?? { usd: 0, tokens: 0 };
206934
+ const [period, ...rest] = k.split("\0");
206935
+ const field = rest.join("\0");
206936
+ if (s.usd !== f.usd) out.push({ period, field: `${field}\0usd`, by: s.usd - f.usd });
206937
+ if (s.tokens !== f.tokens) out.push({ period, field: `${field}\0tokens`, by: s.tokens - f.tokens });
206938
+ this.flushed.set(k, { ...s });
206939
+ }
206940
+ return out;
206941
+ }
206942
+ /** Every copy's spend for one period, as read from shared storage (fields: team, customer, usd|tokens). */
206943
+ setSharedSpend(period, fields) {
206944
+ for (const k of [...this.shared.keys()]) if (k.startsWith(`${period}\0`)) this.shared.delete(k);
206945
+ for (const [field, value] of Object.entries(fields)) {
206946
+ const parts = field.split("\0");
206947
+ const name = parts.pop();
206948
+ if (name !== "usd" && name !== "tokens") continue;
206949
+ const k = key(period, ...parts);
206950
+ const s = this.shared.get(k) ?? { usd: 0, tokens: 0 };
206951
+ s[name] = Number(value);
206952
+ this.shared.set(k, s);
206953
+ }
206954
+ }
206955
+ /** Periods to read back from shared storage. */
206956
+ currentPeriods(now = /* @__PURE__ */ new Date()) {
206957
+ return this.prune(now);
206958
+ }
206959
+ };
206960
+
206961
+ // src/gateway/config.ts
206962
+ var DEFAULTS = {
206963
+ port: 8080,
206964
+ host: "0.0.0.0",
206965
+ mode: "optimize",
206966
+ level: "auto",
206967
+ safe: { prompt: true, minifyJson: true, collapseRepeats: true, dedupeToolOutputs: true, dedupePassages: true, dedupeImages: true, promptCaching: true, dedupeRetries: true, cacheEmbeddings: true },
206968
+ optIn: { elideOldToolOutputs: false, lazyTools: false, trimToolFields: false, compactJson: false, parkToolOutputs: false, flex: false, warmCache: false },
206969
+ verify: { sampleRate: 0.05, minSimilarity: 0.8, maxShareOfSavings: 0.2, freeChecks: 5, probation: 5 },
206970
+ runBudget: {},
206971
+ batch: { flushSeconds: 60, pollSeconds: 60, maxRequests: 1e3, discount: 0.5 },
206972
+ flexDiscount: 0.5,
206973
+ embeddingsCacheMB: 256,
206974
+ // Models carry their own long-context price (src/core/models.ts); entries here override or add to them.
206975
+ priceTiers: [],
206976
+ storage: { keyPrefix: "token-lens:", flushSeconds: 10, runDays: 7 },
206977
+ retryWindowSeconds: 120,
206978
+ blockOverContext: false,
206979
+ upstreams: {},
206980
+ routes: [],
206981
+ models: [],
206982
+ clientKeys: [],
206983
+ customers: CUSTOMER_DEFAULTS,
206984
+ isolateTeams: false,
206985
+ maxBodyBytes: 32 * 1024 * 1024,
206986
+ maxRuns: 2e3,
206987
+ logRequests: true,
206988
+ reportToolArgs: false
206989
+ };
206990
+
206674
206991
  // src/gateway/optimize.ts
206675
- import { createHash as createHash2 } from "node:crypto";
206992
+ import { createHash as createHash3 } from "node:crypto";
206676
206993
 
206677
206994
  // src/gateway/images.ts
206678
- import { createHash } from "node:crypto";
206679
- var keyOf = (s) => createHash("sha256").update(s).digest("hex").slice(0, 24);
206995
+ import { createHash as createHash2 } from "node:crypto";
206996
+ var keyOf = (s) => createHash2("sha256").update(s).digest("hex").slice(0, 24);
206680
206997
  function imageSize(buf) {
206681
206998
  if (buf.length < 24) return void 0;
206682
206999
  if (buf.readUInt32BE(0) === 2303741511) return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20) };
@@ -206713,33 +207030,33 @@ function findImages(protocol, body) {
206713
207030
  parts.forEach((p, idx) => {
206714
207031
  if (!p || typeof p !== "object") return;
206715
207032
  let data;
206716
- let key;
207033
+ let key2;
206717
207034
  let detail;
206718
207035
  let textPart = (text) => ({ type: "text", text });
206719
207036
  if (protocol === "openai-chat" && p.type === "image_url") {
206720
207037
  const url = typeof p.image_url === "string" ? p.image_url : p.image_url?.url;
206721
207038
  if (typeof url !== "string") return;
206722
207039
  data = fromDataUrl(url);
206723
- key = keyOf(url);
207040
+ key2 = keyOf(url);
206724
207041
  detail = p.image_url?.detail;
206725
207042
  } else if (protocol === "openai-responses" && p.type === "input_image") {
206726
207043
  if (typeof p.image_url !== "string") return;
206727
207044
  data = fromDataUrl(p.image_url);
206728
- key = keyOf(p.image_url);
207045
+ key2 = keyOf(p.image_url);
206729
207046
  detail = p.detail;
206730
207047
  textPart = (text) => ({ type: "input_text", text });
206731
207048
  } else if (protocol === "anthropic" && p.type === "image") {
206732
207049
  const src = p.source ?? {};
206733
207050
  if (src.type === "base64" && typeof src.data === "string") {
206734
207051
  data = Buffer.from(src.data, "base64");
206735
- key = keyOf(src.data);
206736
- } else if (typeof src.url === "string") key = keyOf(src.url);
207052
+ key2 = keyOf(src.data);
207053
+ } else if (typeof src.url === "string") key2 = keyOf(src.url);
206737
207054
  else return;
206738
207055
  } else if (protocol === "anthropic" && p.type === "tool_result" && Array.isArray(p.content)) {
206739
207056
  return visit(p.content, turn);
206740
207057
  } else return;
206741
207058
  const size = data ? imageSize(data) : void 0;
206742
- out.push({ turn, key, ...size, detail, replace: (text) => parts[idx] = textPart(text) });
207059
+ out.push({ turn, key: key2, ...size, detail, replace: (text) => parts[idx] = textPart(text) });
206743
207060
  });
206744
207061
  };
206745
207062
  const list = protocol === "openai-responses" ? Array.isArray(body?.input) ? body.input : [] : Array.isArray(body?.messages) ? body.messages : [];
@@ -206797,18 +207114,18 @@ function canonicalArgs(args) {
206797
207114
  const walk = (x) => Array.isArray(x) ? x.map(walk) : x && typeof x === "object" ? Object.fromEntries(Object.keys(x).sort().map((k) => [k, walk(x[k])])) : x;
206798
207115
  return JSON.stringify(walk(v ?? {}));
206799
207116
  }
206800
- var textRef = (part, turn, obj, key) => ({
207117
+ var textRef = (part, turn, obj, key2) => ({
206801
207118
  part,
206802
207119
  turn,
206803
- get: () => obj[key],
207120
+ get: () => obj[key2],
206804
207121
  set: (t) => {
206805
- obj[key] = t;
207122
+ obj[key2] = t;
206806
207123
  }
206807
207124
  });
206808
207125
  var readOnly = (part, turn, text) => ({ part, turn, get: () => text });
206809
- function contentRefs(part, turn, holder, key, types) {
206810
- const content = holder[key];
206811
- if (typeof content === "string") return [textRef(part, turn, holder, key)];
207126
+ function contentRefs(part, turn, holder, key2, types) {
207127
+ const content = holder[key2];
207128
+ if (typeof content === "string") return [textRef(part, turn, holder, key2)];
206812
207129
  if (!Array.isArray(content)) return [];
206813
207130
  return content.filter((b) => b && typeof b === "object" && types.includes(b.type) && typeof b.text === "string").map((b) => textRef(part, turn, b, "text"));
206814
207131
  }
@@ -206965,6 +207282,11 @@ function mergeUsage(a, b) {
206965
207282
  function usageIn(obj) {
206966
207283
  return readUsage(obj?.usage ?? obj?.message?.usage ?? obj?.response?.usage);
206967
207284
  }
207285
+ function errorBody(protocol, status, message) {
207286
+ if (protocol === "anthropic")
207287
+ return { type: "error", error: { type: status === 401 ? "authentication_error" : "invalid_request_error", message } };
207288
+ return { error: { message, type: status === 401 ? "authentication_error" : "invalid_request_error", code: null } };
207289
+ }
206968
207290
 
206969
207291
  // src/gateway/optimize.ts
206970
207292
  var Counter = class {
@@ -207103,18 +207425,18 @@ function dedupePassages(text, turn, seenAt) {
207103
207425
  let changed = false;
207104
207426
  const inThisMessage = /* @__PURE__ */ new Set();
207105
207427
  for (let i = 0; i < pieces.length; i += 2) {
207106
- const key = pieces[i].trim().replace(/\s+/g, " ");
207107
- if (key.length < 200 || key.startsWith("[Token Lens:")) continue;
207108
- const at = seenAt.get(key);
207109
- if (inThisMessage.has(key)) {
207428
+ const key2 = pieces[i].trim().replace(/\s+/g, " ");
207429
+ if (key2.length < 200 || key2.startsWith("[Token Lens:")) continue;
207430
+ const at = seenAt.get(key2);
207431
+ if (inThisMessage.has(key2)) {
207110
207432
  pieces[i] = "[Token Lens: same passage as earlier in this message]";
207111
207433
  changed = true;
207112
207434
  } else if (at !== void 0 && at !== turn) {
207113
207435
  pieces[i] = `[Token Lens: same passage as in message ${at} above]`;
207114
207436
  changed = true;
207115
207437
  } else {
207116
- if (at === void 0) seenAt.set(key, turn);
207117
- inThisMessage.add(key);
207438
+ if (at === void 0) seenAt.set(key2, turn);
207439
+ inThisMessage.add(key2);
207118
207440
  }
207119
207441
  }
207120
207442
  return changed ? pieces.join("") : null;
@@ -207174,11 +207496,11 @@ function optimizeRequest(input) {
207174
207496
  if (cfg.safe.dedupeToolOutputs) {
207175
207497
  const first = /* @__PURE__ */ new Map();
207176
207498
  const firstSeen = (t, turn) => {
207177
- const key = t.trim();
207178
- if (key.length < 200) return null;
207179
- const at = first.get(key);
207499
+ const key2 = t.trim();
207500
+ if (key2.length < 200) return null;
207501
+ const at = first.get(key2);
207180
207502
  if (at === void 0) {
207181
- first.set(key, turn);
207503
+ first.set(key2, turn);
207182
207504
  return null;
207183
207505
  }
207184
207506
  return at === turn ? null : `[Token Lens: same output as the earlier tool result in message ${at}, which is unchanged above]`;
@@ -207251,7 +207573,7 @@ function applyPromptCaching(input, counts2) {
207251
207573
  if (parsed.protocol === "anthropic") return anthropicCaching(body) ? { id: "prompt-caching", saved: 0 } : null;
207252
207574
  const wantKey = input.upstream.type === "anthropic" || (input.upstream.promptCacheKey ?? /(^|\.)api\.openai\.com$/.test(new URL(input.upstream.baseUrl).hostname));
207253
207575
  if (!wantKey || body.prompt_cache_key !== void 0) return null;
207254
- body.prompt_cache_key = `tl-${createHash2("sha256").update(input.runId).digest("hex").slice(0, 24)}`;
207576
+ body.prompt_cache_key = `tl-${createHash3("sha256").update(input.runId).digest("hex").slice(0, 24)}`;
207255
207577
  return { id: "prompt-caching", saved: 0 };
207256
207578
  }
207257
207579
 
@@ -207343,7 +207665,7 @@ function pricedFor(profile, inputTokens) {
207343
207665
  if (!lc || !profile.pricing || inputTokens <= lc.aboveTokens) return profile;
207344
207666
  return { ...profile, pricing: { ...profile.pricing, inputPerMTok: lc.inputPerMTok, outputPerMTok: lc.outputPerMTok } };
207345
207667
  }
207346
- var hash = (s) => createHash3("sha256").update(s).digest("hex");
207668
+ var hash = (s) => createHash4("sha256").update(s).digest("hex");
207347
207669
  var TokenLens = class {
207348
207670
  options;
207349
207671
  counter = new Counter();
@@ -207361,25 +207683,57 @@ var TokenLens = class {
207361
207683
  rewriteMatched = 0;
207362
207684
  /** Safe fixes switched off because a check found a different answer while they were in play. */
207363
207685
  disabled = /* @__PURE__ */ new Set();
207686
+ customers;
207687
+ ledger;
207364
207688
  constructor(options = {}) {
207365
207689
  this.options = options;
207366
207690
  this.runId = options.run ?? randomUUID();
207691
+ this.customers = { ...CUSTOMER_DEFAULTS, ...options.customers, teamBudgets: {} };
207692
+ this.ledger = new CustomerLedger(this.customers.maxCustomers);
207367
207693
  }
207368
207694
  // ------------------------------------------------------------ hooks
207369
207695
  /** A fetch that applies Token Lens to OpenAI and Anthropic API calls and passes everything else through. */
207370
207696
  fetch(base) {
207371
207697
  const real = base ?? ((input, init) => globalThis.fetch(input, init));
207372
- return async (input, init) => {
207698
+ return async (input, original) => {
207373
207699
  const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
207374
207700
  const path = new URL(url).pathname;
207701
+ const ours = /* @__PURE__ */ new Map();
207702
+ let init = original;
207703
+ if (original?.headers) {
207704
+ const headers = new Headers(original.headers);
207705
+ for (const [k, v] of headers) if (k.startsWith("x-token-lens-")) ours.set(k, v);
207706
+ if (ours.size) {
207707
+ for (const k of ours.keys()) headers.delete(k);
207708
+ init = { ...original, headers };
207709
+ }
207710
+ }
207375
207711
  const protocol = PATHS.find(([re]) => re.test(path))?.[1];
207376
207712
  if (!protocol || (init?.method ?? "GET").toUpperCase() !== "POST" || typeof init?.body !== "string") return real(input, init);
207377
207713
  let prep;
207378
207714
  try {
207379
- prep = this.prepare(protocol, init.body, new URL(url).origin);
207715
+ prep = this.prepare(protocol, init.body, new URL(url).origin, ours.get("x-token-lens-customer"));
207380
207716
  } catch {
207381
207717
  return real(input, init);
207382
207718
  }
207719
+ if (prep.customer) {
207720
+ let budget;
207721
+ try {
207722
+ budget = customerBudget(this.customers, ours.get("x-token-lens-plan"), (n) => ours.get(n));
207723
+ } catch (e) {
207724
+ if (e instanceof UnknownPlanError) return Response.json(errorBody(protocol, 400, e.message), { status: 400 });
207725
+ throw e;
207726
+ }
207727
+ if (hasLimits(budget)) {
207728
+ const over = this.ledger.over("app", prep.customer, { customer: budget, team: {} });
207729
+ if (over) {
207730
+ const period = over.limit.startsWith("daily") ? "daily" : "monthly";
207731
+ const message = `Token Lens stopped this call: customer "${prep.customer}" reached its ${period} limit (${over.limit.endsWith("Usd") ? `$${over.spent.toFixed(4)} of $${over.cap}` : `${over.spent} of ${over.cap} tokens`}).`;
207732
+ this.record({ ...this.base(prep), status: 429, usage: {}, replayed: false });
207733
+ return Response.json(errorBody(protocol, 429, message), { status: 429, headers: { "x-token-lens-budget": `customer-${period}-${over.limit.endsWith("Usd") ? "usd" : "tokens"}` } });
207734
+ }
207735
+ }
207736
+ }
207383
207737
  return this.send(real, input, init, prep);
207384
207738
  };
207385
207739
  }
@@ -207427,9 +207781,18 @@ var TokenLens = class {
207427
207781
  byFix: [...this.byFix].map(([fix, f]) => ({ fix, calls: f.calls, tokens: f.tokens, usd: Math.round(f.usd * 1e6) / 1e6 })),
207428
207782
  checks: { ...summarizeChecks(this.checks), recent: this.checks.slice(-20) },
207429
207783
  rewritePrompts: !this.options.rewritePrompts ? "off" : this.rewriteStopped ? "switched off: an output check found a different answer" : this.onProbation() ? `on probation: ${this.rewriteMatched} of ${this.probation()} answers matched so far` : "on",
207430
- switchedOff: [...this.disabled]
207784
+ switchedOff: [...this.disabled],
207785
+ byCustomer: this.ledger.list("app").map(({ team: _t, ...r }) => r)
207431
207786
  };
207432
207787
  }
207788
+ /** One customer's savings, and spend today and this month (limits count these), or undefined before its first call. */
207789
+ customer(id) {
207790
+ const stored = storedId(id, this.customers);
207791
+ const row = this.ledger.snapshot().get(`app\0${stored}`);
207792
+ if (!row) return void 0;
207793
+ const { team: _t, ...r } = this.ledger.view("app", stored, row);
207794
+ return r;
207795
+ }
207433
207796
  /** The report in a few plain lines. */
207434
207797
  summary() {
207435
207798
  const r = this.report();
@@ -207467,8 +207830,9 @@ var TokenLens = class {
207467
207830
  optIn: Object.fromEntries(Object.keys(DEFAULTS.optIn).map((k) => [k, false]))
207468
207831
  };
207469
207832
  }
207470
- prepare(protocol, raw, origin) {
207833
+ prepare(protocol, raw, origin, customerHeader) {
207471
207834
  const body = JSON.parse(raw);
207835
+ const customer = customerOf(customerHeader, body, this.customers);
207472
207836
  const parsed = parseRequest(protocol, body);
207473
207837
  const profile = this.profile(parsed.model);
207474
207838
  const before = countParts(parsed.refs, profile, this.counter);
@@ -207507,15 +207871,16 @@ var TokenLens = class {
207507
207871
  after,
207508
207872
  // As in the gateway: the sum of what each fix removed (estimates count the line breaks it drops).
207509
207873
  savedInput: actions.reduce((n, a) => n + a.saved, 0),
207510
- body: actions.length ? JSON.stringify(body) : raw
207874
+ body: actions.length ? JSON.stringify(body) : raw,
207875
+ customer
207511
207876
  };
207512
207877
  }
207513
207878
  async send(real, input, init, prep) {
207514
207879
  const { parsed } = prep;
207515
207880
  const replayable = (this.options.fixes?.dedupeRetries ?? true) && this.options.mode !== "observe" && !parsed.stream && parsed.temperature === 0;
207516
- const key = replayable ? hash(`${prep.protocol}\0${String(input instanceof Request ? input.url : input)}\0${prep.body}`) : "";
207881
+ const key2 = replayable ? hash(`${prep.customer ?? ""}\0${prep.protocol}\0${String(input instanceof Request ? input.url : input)}\0${prep.body}`) : "";
207517
207882
  const windowMs = (this.options.retryWindowSeconds ?? 120) * 1e3;
207518
- const cached = replayable ? this.replays.get(key) : void 0;
207883
+ const cached = replayable ? this.replays.get(key2) : void 0;
207519
207884
  if (cached && Date.now() - cached.at < windowMs) {
207520
207885
  this.record({ ...this.base(prep), status: cached.status, usage: cached.usage, replayed: true });
207521
207886
  return new Response(cached.text, { status: cached.status, headers: [...cached.headers, ["x-token-lens-replayed", "true"]] });
@@ -207569,7 +207934,7 @@ var TokenLens = class {
207569
207934
  const usage = usageIn(json);
207570
207935
  this.record({ ...this.base(prep), status: res.status, usage, replayed: false });
207571
207936
  if (replayable && res.ok && text) {
207572
- this.replays.set(key, { at: Date.now(), status: res.status, headers: [...res.headers], text, usage });
207937
+ this.replays.set(key2, { at: Date.now(), status: res.status, headers: [...res.headers], text, usage });
207573
207938
  if (this.replays.size > 500) this.replays.delete(this.replays.keys().next().value);
207574
207939
  }
207575
207940
  if (res.ok && json && prep.savedInput > 0) this.maybeCheck(real, input, init, prep, json);
@@ -207647,7 +208012,8 @@ var TokenLens = class {
207647
208012
  stream: prep.parsed.stream,
207648
208013
  savedInput: prep.savedInput,
207649
208014
  actions: prep.actions,
207650
- estimatedInput: prep.after.total
208015
+ estimatedInput: prep.after.total,
208016
+ customer: prep.customer
207651
208017
  };
207652
208018
  }
207653
208019
  record(c) {
@@ -207659,6 +208025,7 @@ var TokenLens = class {
207659
208025
  if (!row) this.rows.set(profile.id, row = emptyTotals());
207660
208026
  const savedInput = c.status < 400 ? c.savedInput : 0;
207661
208027
  addCall(row, { profile, status: c.status, usage, estimatedInput: c.estimatedInput, savedInput, replayed: c.replayed });
208028
+ this.ledger.record("app", c.customer, { profile, status: c.status, usage, estimatedInput: c.estimatedInput, savedInput, replayed: c.replayed });
207662
208029
  const unoptimized = pricedFor(c.profile, input + savedInput);
207663
208030
  if (!c.replayed && unoptimized !== profile) {
207664
208031
  const u = { ...usage, inputTokens: input };
@@ -207685,6 +208052,7 @@ var TokenLens = class {
207685
208052
  }
207686
208053
  this.options.onCall?.({
207687
208054
  protocol: c.protocol,
208055
+ ...c.customer ? { customer: c.customer } : {},
207688
208056
  model: c.model,
207689
208057
  profile: profile.id,
207690
208058
  status: c.status,