@konneal/engine 0.2.72 → 0.2.73

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.
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  handleAsk
3
- } from "./chunk-UW5M2AXR.js";
4
- import "./chunk-TPV5GPY2.js";
3
+ } from "./chunk-V7PEVNNO.js";
4
+ import "./chunk-LL3LWBZH.js";
5
5
  import "./chunk-SPKPH54J.js";
6
6
  import "./chunk-A3QHHUN5.js";
7
7
  import "./chunk-LVZTWUVJ.js";
@@ -9,6 +9,7 @@ import {
9
9
  THRESHOLDS,
10
10
  answerEffort,
11
11
  effortBudget,
12
+ num,
12
13
  processExpansion,
13
14
  sha256Hex,
14
15
  today
@@ -817,8 +818,8 @@ var BaseUrnGenerator = class {
817
818
  const ed = this.maybe("edition");
818
819
  if (ed === void 0 || ed === null)
819
820
  return void 0;
820
- const num = typeof ed === "object" && "number" in ed ? ed.number : ed;
821
- return num === void 0 || num === null || num === "" ? void 0 : `ed.${String(num)}`;
821
+ const num2 = typeof ed === "object" && "number" in ed ? ed.number : ed;
822
+ return num2 === void 0 || num2 === null || num2 === "" ? void 0 : `ed.${String(num2)}`;
822
823
  }
823
824
  urnLanguage() {
824
825
  const langs = this.maybe("languages");
@@ -7139,9 +7140,9 @@ var IsoIdentifier = class _IsoIdentifier extends BaseIdentifier {
7139
7140
  }
7140
7141
  }
7141
7142
  }
7142
- const num = this.numberPortion(true);
7143
- if (num !== "")
7144
- result += result === "" ? num : ` ${num}`;
7143
+ const num2 = this.numberPortion(true);
7144
+ if (num2 !== "")
7145
+ result += result === "" ? num2 : ` ${num2}`;
7145
7146
  result += this.languagePortion(this.languageSingle());
7146
7147
  if (this.all_parts === true)
7147
7148
  result += " (all parts)";
@@ -7306,9 +7307,9 @@ var IsoSingleIdentifier = class extends IsoIdentifier {
7306
7307
  result += `${sep}${stage}`;
7307
7308
  }
7308
7309
  }
7309
- const num = this.numberPortion(true);
7310
- if (num !== "")
7311
- result += result === "" ? num : ` ${num}`;
7310
+ const num2 = this.numberPortion(true);
7311
+ if (num2 !== "")
7312
+ result += result === "" ? num2 : ` ${num2}`;
7312
7313
  result += this.languagePortion(this.languageSingle());
7313
7314
  if (this.all_parts === true)
7314
7315
  result += " (all parts)";
@@ -7611,11 +7612,11 @@ var IsoSupplementIdentifier = class extends IsoSingleIdentifier {
7611
7612
  const ts = this.typedStageState;
7612
7613
  const stage = ts === void 0 ? "" : stageAbbr(ts.entry, long, ts.originalAbbr);
7613
7614
  let result = `${baseStr}/${stage}`;
7614
- const num = this.numberPortion(true);
7615
- if (num !== "") {
7616
- if (!num.startsWith(":") && !stage.endsWith("."))
7615
+ const num2 = this.numberPortion(true);
7616
+ if (num2 !== "") {
7617
+ if (!num2.startsWith(":") && !stage.endsWith("."))
7617
7618
  result += " ";
7618
- result += num;
7619
+ result += num2;
7619
7620
  }
7620
7621
  result += this.languagePortion(this.languageSingle());
7621
7622
  return result;
@@ -7670,20 +7671,20 @@ var IsoDirectives = class extends IsoSingleIdentifier {
7670
7671
  const abbr = this.typedStageState === void 0 ? "" : stageAbbr(this.typedStageState.entry, false);
7671
7672
  if (abbr !== "")
7672
7673
  head += ` ${abbr}`;
7673
- let num = this.number ?? "";
7674
+ let num2 = this.number ?? "";
7674
7675
  if (this.part !== void 0)
7675
- num += ` ${this.part}`;
7676
+ num2 += ` ${this.part}`;
7676
7677
  if (this.subpart !== void 0)
7677
- num += `-${this.subpart}`;
7678
+ num2 += `-${this.subpart}`;
7678
7679
  if (this.stage_iteration !== void 0)
7679
- num += `.${this.stage_iteration.render() ?? ""}`;
7680
+ num2 += `.${this.stage_iteration.render() ?? ""}`;
7680
7681
  if (this.date !== void 0 && this.date.present())
7681
- num += `:${this.date.render()}`;
7682
- if (num === "")
7682
+ num2 += `:${this.date.render()}`;
7683
+ if (num2 === "")
7683
7684
  return head;
7684
- if (num.startsWith(":"))
7685
- return head + num;
7686
- return `${head} ${num}`;
7685
+ if (num2.startsWith(":"))
7686
+ return head + num2;
7687
+ return `${head} ${num2}`;
7687
7688
  }
7688
7689
  };
7689
7690
  registerType(IsoDirectives);
@@ -8606,9 +8607,9 @@ function preprocessNist(input) {
8606
8607
  cleaned = cleaned.replace(/(?<!e)(\d)(rev\d{4})/g, "$1 $2");
8607
8608
  cleaned = cleaned.replace(/(\d+e\d+)\.([A-Za-z]{3,9}\d{4})/g, "$1rev$2");
8608
8609
  if (/\bIR\b/.test(cleaned) && !cleaned.includes("CIRC")) {
8609
- cleaned = cleaned.replace(/(\d)r(\d{1,2})\/(\d{2,4})/g, (_m, num, mon, yr) => {
8610
+ cleaned = cleaned.replace(/(\d)r(\d{1,2})\/(\d{2,4})/g, (_m, num2, mon, yr) => {
8610
8611
  const yyyy = yr.length === 2 ? `19${yr}` : yr;
8611
- return `${num}/Upd1-${yyyy}${mon.padStart(2, "0")}`;
8612
+ return `${num2}/Upd1-${yyyy}${mon.padStart(2, "0")}`;
8612
8613
  });
8613
8614
  }
8614
8615
  if (!cleaned.includes("LCIRC") && !cleaned.includes("CIRC")) {
@@ -11321,17 +11322,17 @@ function normalizeJointStageSpellings(input) {
11321
11322
  let cleaned = input;
11322
11323
  const pubs = "((?:ISO/IEC/IEEE|IEEE/ISO/IEC|IEEE/IEC/ISO|ISO/IEEE|IEC/IEEE|IEEE/IEC|ISO/IEC)(?:/ ?| ))";
11323
11324
  const stage = "(FDIS|FCD|CDV|DIS\\d?|CD\\d?|WD|PWI|NP)";
11324
- const num = "(P?\\d+(?:[.-]\\d+)?)";
11325
+ const num2 = "(P?\\d+(?:[.-]\\d+)?)";
11325
11326
  const tail = "(,? (?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Sept|Oct|Nov|Dec)[a-z]* \\d{4}|$|-\\d{4}(?:-\\d\\d)?)";
11326
11327
  cleaned = cleaned.replace(/(\d)_D(\d)/g, "$1/D$2");
11327
11328
  cleaned = cleaned.replace(/,(?=[A-Za-z])/, ", ");
11328
- cleaned = cleaned.replace(new RegExp(`^${pubs}${num}/(D[\\d.]+)/ ?${stage}\\b`), "$1$4 $2/$3");
11329
- cleaned = cleaned.replace(new RegExp(`^${pubs}${num}/ ${stage}${tail}`), "$1$3 $2$4");
11330
- cleaned = cleaned.replace(new RegExp(`^${pubs}${num}/${stage} (\\d{4})\\b`), "$1$3 $2-$4");
11331
- cleaned = cleaned.replace(new RegExp(`^${pubs}${num} ${stage}${tail}`), "$1$3 $2$4");
11332
- cleaned = cleaned.replace(new RegExp(`^${pubs}${num} ${stage} (D\\d+)\\b`), "$1$3 $2/$4");
11333
- cleaned = cleaned.replace(new RegExp(`^${pubs}${num}/ ?${stage}(-(?:19|20)\\d\\d(?:-\\d\\d)?)`), "$1$3 $2$4");
11334
- cleaned = cleaned.replace(new RegExp(`^${pubs}${stage} ${num}/? ?(D\\d+)\\b`), "$1$2 $3/$4");
11329
+ cleaned = cleaned.replace(new RegExp(`^${pubs}${num2}/(D[\\d.]+)/ ?${stage}\\b`), "$1$4 $2/$3");
11330
+ cleaned = cleaned.replace(new RegExp(`^${pubs}${num2}/ ${stage}${tail}`), "$1$3 $2$4");
11331
+ cleaned = cleaned.replace(new RegExp(`^${pubs}${num2}/${stage} (\\d{4})\\b`), "$1$3 $2-$4");
11332
+ cleaned = cleaned.replace(new RegExp(`^${pubs}${num2} ${stage}${tail}`), "$1$3 $2$4");
11333
+ cleaned = cleaned.replace(new RegExp(`^${pubs}${num2} ${stage} (D\\d+)\\b`), "$1$3 $2/$4");
11334
+ cleaned = cleaned.replace(new RegExp(`^${pubs}${num2}/ ?${stage}(-(?:19|20)\\d\\d(?:-\\d\\d)?)`), "$1$3 $2$4");
11335
+ cleaned = cleaned.replace(new RegExp(`^${pubs}${stage} ${num2}/? ?(D\\d+)\\b`), "$1$2 $3/$4");
11335
11336
  return cleaned;
11336
11337
  }
11337
11338
  function preprocessIeee(input) {
@@ -14556,8 +14557,8 @@ var CenBuilder = class {
14556
14557
  const numStr = (str3(d["number"]) ?? "").trim();
14557
14558
  if (!/^\d{1,6}$/.test(numStr))
14558
14559
  return void 0;
14559
- const num = Number.parseInt(numStr, 10);
14560
- if (!(num >= 6e4 && num <= 79999))
14560
+ const num2 = Number.parseInt(numStr, 10);
14561
+ if (!(num2 >= 6e4 && num2 <= 79999))
14561
14562
  return void 0;
14562
14563
  const impl = grammarImplementation("iec");
14563
14564
  if (impl === void 0)
@@ -19006,9 +19007,9 @@ var CsaAdoptedClass = (() => {
19006
19007
  baseStr = baseStr.replace(`:${year}`, `:${year.slice(2)}`);
19007
19008
  }
19008
19009
  }
19009
- baseStr = baseStr.replace(/\/Amd\s+(\d+)([:/-])(\d{2,4})\b/g, (_m, num, sep, year) => {
19010
+ baseStr = baseStr.replace(/\/Amd\s+(\d+)([:/-])(\d{2,4})\b/g, (_m, num2, sep, year) => {
19010
19011
  const short = year.length === 4 && (year.startsWith("20") || year.startsWith("19")) ? year.slice(2) : year;
19011
- return `/A${num}${sep}${short}`;
19012
+ return `/A${num2}${sep}${short}`;
19012
19013
  });
19013
19014
  let result;
19014
19015
  if (this.publisher_prefix !== void 0 && this.publisher_prefix.endsWith("-")) {
@@ -19635,9 +19636,9 @@ function parseCsa(input) {
19635
19636
  const full = Number(two) < 50 ? `20${two}` : `19${two}`;
19636
19637
  wrapped = wrapped.replace(new RegExp(`:${two}\\b`), `:${full}`);
19637
19638
  }
19638
- wrapped = wrapped.replace(/\/A(\d+)([:/-])(\d{2,4})\b/g, (_m, num, sep, year) => {
19639
+ wrapped = wrapped.replace(/\/A(\d+)([:/-])(\d{2,4})\b/g, (_m, num2, sep, year) => {
19639
19640
  const full = year.length === 2 ? Number(year) < 50 ? `20${year}` : `19${year}` : year;
19640
- return `/Amd ${num}${sep}${full}`;
19641
+ return `/Amd ${num2}${sep}${full}`;
19641
19642
  });
19642
19643
  const base = parseExternalStandard(wrapped);
19643
19644
  if (base === void 0) {
@@ -21908,13 +21909,13 @@ function parseOimlSpine(display) {
21908
21909
  const kind = String(h._type ?? "").split(":").pop() ?? "";
21909
21910
  const letter = TYPE_LETTER[kind] ?? "";
21910
21911
  if (!letter) return null;
21911
- const num = String(Number(h.number));
21912
+ const num2 = String(Number(h.number));
21912
21913
  const part = h.part !== void 0 ? String(h.part) : void 0;
21913
21914
  const ed = h.year !== void 0 ? String(h.year) : h.edition !== void 0 ? String(h.edition) : void 0;
21914
21915
  return {
21915
- doc_number: num,
21916
+ doc_number: num2,
21916
21917
  ...ed ? { edition: ed } : {},
21917
- label: `OIML ${letter} ${num}${part ? `-${part}` : ""}${ed ? `:${ed}` : ""}`
21918
+ label: `OIML ${letter} ${num2}${part ? `-${part}` : ""}${ed ? `:${ed}` : ""}`
21918
21919
  };
21919
21920
  } catch {
21920
21921
  return null;
@@ -21949,9 +21950,9 @@ var oimlPubid = {
21949
21950
  for (const m of query.matchAll(re)) {
21950
21951
  const [, oimlPrefix, letter, gap, digits, edition] = m;
21951
21952
  if (digits.length === 1 && !oimlPrefix && !gap) continue;
21952
- const num = String(Number(digits));
21953
+ const num2 = String(Number(digits));
21953
21954
  const type = letter.toUpperCase();
21954
- return { doc_number: num, ...edition ? { edition } : {}, label: `OIML ${type} ${num}${edition ? `:${edition}` : ""}` };
21955
+ return { doc_number: num2, ...edition ? { edition } : {}, label: `OIML ${type} ${num2}${edition ? `:${edition}` : ""}` };
21955
21956
  }
21956
21957
  return null;
21957
21958
  },
@@ -24578,6 +24579,32 @@ function telemetry(env, ctx, tier, route, model, ok, answerChars, queryHash, lan
24578
24579
  ])
24579
24580
  );
24580
24581
  }
24582
+ function estimateTokens(chars) {
24583
+ return Math.ceil(chars / 4);
24584
+ }
24585
+ function tokenTier(bucketId) {
24586
+ return bucketId.startsWith("key:") ? "key" : bucketId.startsWith("sub:") ? "member" : "anon";
24587
+ }
24588
+ function tokenLimit(env, bucketId) {
24589
+ const tier = tokenTier(bucketId);
24590
+ return tier === "key" ? num(env, "KEY_DAY_TOKENS", 4e6) : tier === "member" ? num(env, "MEMBER_DAY_TOKENS", 15e5) : num(env, "ANON_DAY_TOKENS", 3e5);
24591
+ }
24592
+ async function tokenBudget(env, bucketId) {
24593
+ const used = Number(await env.CACHE.get(`t:${today()}:ask:${await sha256Hex(bucketId)}`) ?? "0");
24594
+ return { used, limit: tokenLimit(env, bucketId) };
24595
+ }
24596
+ async function chargeTokens(env, bucketId, tokens) {
24597
+ if (!(tokens > 0)) return;
24598
+ await kvIncr(env.CACHE, `t:${today()}:ask:${await sha256Hex(bucketId)}`, tokens);
24599
+ }
24600
+ function usageTotal(usage) {
24601
+ if (!usage || typeof usage !== "object") return null;
24602
+ const u = usage;
24603
+ const p = Number(u.prompt_tokens ?? 0);
24604
+ const c = Number(u.completion_tokens ?? 0);
24605
+ const total = p + c;
24606
+ return Number.isFinite(total) && total > 0 ? total : null;
24607
+ }
24581
24608
 
24582
24609
  // workers/worker_public/src/graph.ts
24583
24610
  function docNumberOf(nodeId) {
@@ -24662,6 +24689,10 @@ export {
24662
24689
  clientIp,
24663
24690
  checkQuota,
24664
24691
  telemetry,
24692
+ estimateTokens,
24693
+ tokenBudget,
24694
+ chargeTokens,
24695
+ usageTotal,
24665
24696
  graphExpand,
24666
24697
  editionNote
24667
24698
  };
@@ -7,7 +7,7 @@ import {
7
7
  sessionFrom,
8
8
  telemetry,
9
9
  understandQuery
10
- } from "./chunk-TPV5GPY2.js";
10
+ } from "./chunk-LL3LWBZH.js";
11
11
  import {
12
12
  corsHeaders,
13
13
  err,
@@ -3,12 +3,14 @@ import {
3
3
  answerQuality,
4
4
  appliedContext,
5
5
  buildMessages,
6
+ chargeTokens,
6
7
  checkQuota,
7
8
  citations,
8
9
  clientIp,
9
10
  contextNote,
10
11
  editionNote,
11
12
  embed,
13
+ estimateTokens,
12
14
  experimentalSourceLabels,
13
15
  generateOnce,
14
16
  graphExpand,
@@ -34,8 +36,10 @@ import {
34
36
  splitHistory,
35
37
  syntheticUnderstanding,
36
38
  telemetry,
37
- understandQuery
38
- } from "./chunk-TPV5GPY2.js";
39
+ tokenBudget,
40
+ understandQuery,
41
+ usageTotal
42
+ } from "./chunk-LL3LWBZH.js";
39
43
  import {
40
44
  corsHeaders,
41
45
  err,
@@ -2166,7 +2170,23 @@ async function summarizeHistory(env, model, turns) {
2166
2170
  return null;
2167
2171
  }
2168
2172
  }
2169
- async function* sseTokens(stream) {
2173
+ function messageChars(messages) {
2174
+ let n = 0;
2175
+ for (const m of messages) {
2176
+ if (typeof m.content === "string") n += m.content.length;
2177
+ else if (Array.isArray(m.content)) {
2178
+ for (const part of m.content) if (part?.type === "text" && typeof part.text === "string") n += part.text.length;
2179
+ }
2180
+ }
2181
+ return n;
2182
+ }
2183
+ async function chargeAnswerTokens(env, bucketId, stats, promptChars, answerChars) {
2184
+ const total = usageTotal(stats?.usage) ?? estimateTokens(promptChars + answerChars);
2185
+ await chargeTokens(env, bucketId, total);
2186
+ const budget = await tokenBudget(env, bucketId);
2187
+ return { tokens_used: budget.used, token_limit: budget.limit };
2188
+ }
2189
+ async function* sseTokens(stream, stats) {
2170
2190
  const reader = stream.getReader();
2171
2191
  const decoder = new TextDecoder();
2172
2192
  let buf = "";
@@ -2184,6 +2204,7 @@ async function* sseTokens(stream) {
2184
2204
  try {
2185
2205
  const evt = JSON.parse(payload);
2186
2206
  const tok = typeof evt?.response === "string" ? evt.response : evt?.choices?.[0]?.delta?.content;
2207
+ if (evt.usage && stats && stats.usage === void 0) stats.usage = evt.usage;
2187
2208
  if (tok) yield tok;
2188
2209
  } catch {
2189
2210
  }
@@ -2227,6 +2248,12 @@ async function handleAsk(env, ctx, req, tier, key) {
2227
2248
  if (!quota.ok) {
2228
2249
  return err(429, "quota_exceeded", `Daily question limit reached (${quota.limit}). Try again tomorrow.`);
2229
2250
  }
2251
+ const tokBudget = await tokenBudget(env, bucketId);
2252
+ if (tokBudget.used >= tokBudget.limit) {
2253
+ return err(429, "quota_exceeded", `Daily token budget reached (${tokBudget.limit} tokens). Try again tomorrow.`);
2254
+ }
2255
+ quota.tokens_used = tokBudget.used;
2256
+ quota.token_limit = tokBudget.limit;
2230
2257
  const hardCap = num(env, "ANON_DAY_HARD_CAP", 5e3);
2231
2258
  if (tier === "anon" && quota.used > hardCap) {
2232
2259
  return err(503, "generation_disabled", "Generation is temporarily paused; search remains available.");
@@ -2444,14 +2471,17 @@ ${summary}` }] : [],
2444
2471
  `));
2445
2472
  if (readAs()) send({ type: "read", read: readAs() });
2446
2473
  send({ type: "citations", citations: [], context_applied: NO_CONTEXT, quota });
2474
+ const stats = {};
2447
2475
  let full = "";
2448
2476
  try {
2449
- for await (const tok of sseTokens(stream)) {
2477
+ for await (const tok of sseTokens(stream, stats)) {
2450
2478
  full += tok;
2451
2479
  send({ type: "token", v: tok });
2452
2480
  }
2453
2481
  } catch {
2454
2482
  }
2483
+ const tq2 = await chargeAnswerTokens(env, bucketId, stats, messageChars(messages2), full.length);
2484
+ send({ type: "quota", quota: { ...quota, ...tq2 } });
2455
2485
  send({ type: "done", model, query_hash: queryHash2, context_applied: NO_CONTEXT, read: readAs() });
2456
2486
  telemetry(env, ctx, tier, "ask", model, true, full.length, queryHash2, q.lang, void 0, telemetryMeta());
2457
2487
  controller.close();
@@ -2507,7 +2537,8 @@ ${summary}` }] : [],
2507
2537
  corsHeaders(req)
2508
2538
  );
2509
2539
  }
2510
- return json({ answer: verdict.answer, citations: citations2, model, query_hash: queryHash2, follow_ups: [], context_applied: draftCtxApplied, read: readAs(), ...draftPayload ? { draft: draftPayload } : {}, quota });
2540
+ const tq2 = await chargeAnswerTokens(env, bucketId, void 0, q.query.length, verdict.answer.length);
2541
+ return json({ answer: verdict.answer, citations: citations2, model, query_hash: queryHash2, follow_ups: [], context_applied: draftCtxApplied, read: readAs(), ...draftPayload ? { draft: draftPayload } : {}, quota: { ...quota, ...tq2 } });
2511
2542
  }
2512
2543
  if (apiCallIntent) {
2513
2544
  const callCtxApplied = declaredCtx ? appliedContext(declaredCtx, null) : NO_CONTEXT;
@@ -2534,7 +2565,8 @@ ${summary}` }] : [],
2534
2565
  corsHeaders(req)
2535
2566
  );
2536
2567
  }
2537
- return json({ answer: verdict.answer, citations: [], model, query_hash: queryHash2, follow_ups: [], context_applied: callCtxApplied, read: readAs(), ...draftPayload ? { draft: draftPayload } : {}, quota });
2568
+ const tq2 = await chargeAnswerTokens(env, bucketId, void 0, q.query.length, verdict.answer.length);
2569
+ return json({ answer: verdict.answer, citations: [], model, query_hash: queryHash2, follow_ups: [], context_applied: callCtxApplied, read: readAs(), ...draftPayload ? { draft: draftPayload } : {}, quota: { ...quota, ...tq2 } });
2538
2570
  }
2539
2571
  let liveRecords;
2540
2572
  let accountNote;
@@ -2834,14 +2866,17 @@ ${q.query}` }],
2834
2866
  send({ type: "read", read: readAs() });
2835
2867
  const sourceQuality = answerQuality(cites.map((c) => c.quality));
2836
2868
  send({ type: "citations", citations: cites, context_applied: ctxApplied, ...sourceQuality ? { source_quality: sourceQuality, confidence_note: qualityNote(sourceQuality), ...sourceQuality === "ocr" ? { experimental_sources: experimentalSourceLabels(cites) } : {} } : {}, ...liveRecords ? { records: liveRecords } : {}, quota });
2869
+ const stats = {};
2837
2870
  let full = "";
2838
2871
  try {
2839
- for await (const tok of sseTokens(stream)) {
2872
+ for await (const tok of sseTokens(stream, stats)) {
2840
2873
  full += tok;
2841
2874
  send({ type: "token", v: tok });
2842
2875
  }
2843
2876
  } catch {
2844
2877
  }
2878
+ const tq2 = await chargeAnswerTokens(env, bucketId, stats, messageChars(messages), full.length);
2879
+ send({ type: "quota", quota: { ...quota, ...tq2 } });
2845
2880
  const canonical0 = canonicalRefusal(full);
2846
2881
  const c2 = canonical0.includes(refusalAnswer()) ? { text: canonical0, blocks: [], dropped: [] } : await contractV2(env.DB, canonical0, usedHits);
2847
2882
  send({
@@ -3007,7 +3042,8 @@ ${q.query}` }],
3007
3042
  text: h.text.slice(0, 1200),
3008
3043
  ...h.metadata.table_selection ? { sel: h.metadata.table_selection } : {}
3009
3044
  }));
3010
- return json({ ...out, context: contextOut, read: readAs(), quota }, 200, { ...corsHeaders(req), "server-timing": serverTiming() });
3045
+ const tq = await chargeAnswerTokens(env, bucketId, void 0, messageChars(messages), answer.length);
3046
+ return json({ ...out, context: contextOut, read: readAs(), quota: { ...quota, ...tq } }, 200, { ...corsHeaders(req), "server-timing": serverTiming() });
3011
3047
  }
3012
3048
  function sseResponse(events, cors) {
3013
3049
  const encoder = new TextEncoder();
package/dist/quota.d.ts CHANGED
@@ -15,3 +15,11 @@ export declare function telemetry(env: Env, ctx: Background, tier: string, route
15
15
  keyId?: string | null;
16
16
  retries?: number;
17
17
  }): void;
18
+ export declare function estimateTokens(chars: number): number;
19
+ export declare function tokenLimit(env: Env, bucketId: string): number;
20
+ export declare function tokenBudget(env: Env, bucketId: string): Promise<{
21
+ used: number;
22
+ limit: number;
23
+ }>;
24
+ export declare function chargeTokens(env: Env, bucketId: string, tokens: number): Promise<void>;
25
+ export declare function usageTotal(usage: unknown): number | null;
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  handleSearch
3
- } from "./chunk-FLNX4O7C.js";
4
- import "./chunk-TPV5GPY2.js";
3
+ } from "./chunk-OXTRNMTZ.js";
4
+ import "./chunk-LL3LWBZH.js";
5
5
  import "./chunk-SPKPH54J.js";
6
6
  import "./chunk-A3QHHUN5.js";
7
7
  import "./chunk-LVZTWUVJ.js";
@@ -6,10 +6,10 @@ import {
6
6
  modelGroundingBlock,
7
7
  scoreFaithfulness,
8
8
  scoreJudge
9
- } from "../../chunk-UW5M2AXR.js";
9
+ } from "../../chunk-V7PEVNNO.js";
10
10
  import {
11
11
  handleSearch
12
- } from "../../chunk-FLNX4O7C.js";
12
+ } from "../../chunk-OXTRNMTZ.js";
13
13
  import {
14
14
  buildMessages,
15
15
  cfBlobs,
@@ -35,7 +35,7 @@ import {
35
35
  sessionFrom,
36
36
  telemetry,
37
37
  understandQuery
38
- } from "../../chunk-TPV5GPY2.js";
38
+ } from "../../chunk-LL3LWBZH.js";
39
39
  import {
40
40
  authenticate,
41
41
  corsHeaders,
@@ -959,7 +959,7 @@ async function handleMcp(env, ctx, req, tier, key) {
959
959
  // stream:false forces the JSON lane (anon defaults to SSE)
960
960
  body: JSON.stringify({ ...args, stream: false })
961
961
  });
962
- const res = name === "ask" ? await (await import("../../ask-UOTYZ5DC.js")).handleAsk(env, ctx, inner, tier, key) : await (await import("../../search-PBOODCMQ.js")).handleSearch(env, ctx, inner, tier, key);
962
+ const res = name === "ask" ? await (await import("../../ask-MFFOHVQ2.js")).handleAsk(env, ctx, inner, tier, key) : await (await import("../../search-LN5Q7OSZ.js")).handleSearch(env, ctx, inner, tier, key);
963
963
  return res.json().catch(() => ({ error: { message: "tool transport failed", status: res.status } }));
964
964
  });
965
965
  if (out.ok && "accepted" in out) return new Response(null, { status: 202 });
package/package.json CHANGED
@@ -27,7 +27,7 @@
27
27
  "playwright": "^1.62.1",
28
28
  "yaml": "^2.9.1"
29
29
  },
30
- "version": "0.2.72",
30
+ "version": "0.2.73",
31
31
  "description": "The Konneal engine: the publisher-agnostic build pipeline and API plane for standards intelligence (retrieval, answer contract, verdicts, evaluation).",
32
32
  "license": "BSD-3-Clause",
33
33
  "type": "module",
@@ -37,7 +37,7 @@ import { cacheKeyMaterial, corpusGen, exactCacheKey, freshRequested } from "./an
37
37
  import type { Env } from "./env";
38
38
  export type { Env };
39
39
  import { json, err, corsHeaders, readJson, validateQuery, type ApiKey } from "./lib/http";
40
- import { clientIp, checkQuota, telemetry } from "./quota";
40
+ import { chargeTokens, checkQuota, clientIp, estimateTokens, telemetry, tokenBudget, usageTotal } from "./quota";
41
41
  import { graphExpand, editionNote } from "./graph";
42
42
  import { registerNote, searchRegister, certificateLinks } from "./certificates";
43
43
  import { TOOL_DECLARATION, parseToolCall, runTool, toolNote } from "./tools";
@@ -206,7 +206,28 @@ async function summarizeHistory(
206
206
  }
207
207
  }
208
208
 
209
- async function* sseTokens(stream: ReadableStream<Uint8Array>): AsyncGenerator<string> {
209
+ /** The prompt's character weight — text parts only, image data URLs
210
+ * excluded (a photo must not read as half a million characters). */
211
+ function messageChars(messages: any[]): number {
212
+ let n = 0;
213
+ for (const m of messages) {
214
+ if (typeof m.content === "string") n += m.content.length;
215
+ else if (Array.isArray(m.content)) for (const part of m.content) if (part?.type === "text" && typeof part.text === "string") n += part.text.length;
216
+ }
217
+ return n;
218
+ }
219
+
220
+ /** Post-generation true-up: the model's own usage when the stream
221
+ * reported it, the character estimate otherwise — charged, then the
222
+ * budget's fresh state returned for the quota payload. */
223
+ async function chargeAnswerTokens(env: Env, bucketId: string, stats: { usage?: unknown } | undefined, promptChars: number, answerChars: number): Promise<{ tokens_used: number; token_limit: number }> {
224
+ const total = usageTotal(stats?.usage) ?? estimateTokens(promptChars + answerChars);
225
+ await chargeTokens(env, bucketId, total);
226
+ const budget = await tokenBudget(env, bucketId);
227
+ return { tokens_used: budget.used, token_limit: budget.limit };
228
+ }
229
+
230
+ async function* sseTokens(stream: ReadableStream<Uint8Array>, stats?: { usage?: unknown }): AsyncGenerator<string> {
210
231
  const reader = stream.getReader();
211
232
  const decoder = new TextDecoder();
212
233
  let buf = "";
@@ -227,6 +248,7 @@ async function* sseTokens(stream: ReadableStream<Uint8Array>): AsyncGenerator<st
227
248
  typeof evt?.response === "string"
228
249
  ? evt.response
229
250
  : evt?.choices?.[0]?.delta?.content;
251
+ if (evt.usage && stats && stats.usage === undefined) stats.usage = evt.usage;
230
252
  if (tok) yield tok;
231
253
  } catch {
232
254
  // partial JSON in line splitting — ignore
@@ -320,6 +342,14 @@ async function handleAsk(
320
342
  if (!quota.ok) {
321
343
  return err(429, "quota_exceeded", `Daily question limit reached (${quota.limit}). Try again tomorrow.`);
322
344
  }
345
+ // the real allowance is the day's TOKENS (the 2026-09-29 shift): the
346
+ // request cap above is the anti-abuse backstop, this is the budget
347
+ const tokBudget = await tokenBudget(env, bucketId);
348
+ if (tokBudget.used >= tokBudget.limit) {
349
+ return err(429, "quota_exceeded", `Daily token budget reached (${tokBudget.limit} tokens). Try again tomorrow.`);
350
+ }
351
+ (quota as any).tokens_used = tokBudget.used;
352
+ (quota as any).token_limit = tokBudget.limit;
323
353
 
324
354
  const hardCap = num(env as any, "ANON_DAY_HARD_CAP", 5000);
325
355
  if (tier === "anon" && quota.used > hardCap) {
@@ -668,15 +698,18 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
668
698
  // the reading arrives first — a conversational turn is still a reading
669
699
  if (readAs()) send({ type: "read", read: readAs() });
670
700
  send({ type: "citations", citations: [], context_applied: NO_CONTEXT, quota, });
701
+ const stats: { usage?: unknown } = {};
671
702
  let full = "";
672
703
  try {
673
- for await (const tok of sseTokens(stream)) {
704
+ for await (const tok of sseTokens(stream, stats)) {
674
705
  full += tok;
675
706
  send({ type: "token", v: tok });
676
707
  }
677
708
  } catch {
678
709
  // stream ended prematurely — deliver what we have
679
710
  }
711
+ const tq = await chargeAnswerTokens(env, bucketId, stats, messageChars(messages), full.length);
712
+ send({ type: "quota", quota: { ...quota, ...tq } });
680
713
  send({ type: "done", model, query_hash: queryHash, context_applied: NO_CONTEXT, read: readAs() });
681
714
  telemetry(env, ctx, tier, "ask", model, true, full.length, queryHash, q.lang, undefined, telemetryMeta());
682
715
  controller.close();
@@ -748,7 +781,8 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
748
781
  corsHeaders(req),
749
782
  );
750
783
  }
751
- return json({ answer: verdict.answer, citations, model, query_hash: queryHash, follow_ups: [], context_applied: draftCtxApplied, read: readAs(), ...(draftPayload ? { draft: draftPayload } : {}), quota, });
784
+ const tq = await chargeAnswerTokens(env, bucketId, undefined, q.query.length, verdict.answer.length);
785
+ return json({ answer: verdict.answer, citations, model, query_hash: queryHash, follow_ups: [], context_applied: draftCtxApplied, read: readAs(), ...(draftPayload ? { draft: draftPayload } : {}), quota: { ...quota, ...tq } });
752
786
  }
753
787
 
754
788
  // ── The api_call draft (TODO.ai-platform/09) — the operations
@@ -785,7 +819,8 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
785
819
  corsHeaders(req),
786
820
  );
787
821
  }
788
- return json({ answer: verdict.answer, citations: [], model, query_hash: queryHash, follow_ups: [], context_applied: callCtxApplied, read: readAs(), ...(draftPayload ? { draft: draftPayload } : {}), quota, });
822
+ const tq = await chargeAnswerTokens(env, bucketId, undefined, q.query.length, verdict.answer.length);
823
+ return json({ answer: verdict.answer, citations: [], model, query_hash: queryHash, follow_ups: [], context_applied: callCtxApplied, read: readAs(), ...(draftPayload ? { draft: draftPayload } : {}), quota: { ...quota, ...tq } });
789
824
  }
790
825
 
791
826
  // (declared before the retrieval try: the account block, the refusal
@@ -1227,15 +1262,18 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
1227
1262
  send({ type: "read", read: readAs() });
1228
1263
  const sourceQuality = answerQuality(cites.map((c: any) => c.quality));
1229
1264
  send({ type: "citations", citations: cites, context_applied: ctxApplied, ...(sourceQuality ? { source_quality: sourceQuality, confidence_note: qualityNote(sourceQuality), ...(sourceQuality === "ocr" ? { experimental_sources: experimentalSourceLabels(cites) } : {}) } : {}), ...(liveRecords ? { records: liveRecords } : {}), quota, });
1265
+ const stats: { usage?: unknown } = {};
1230
1266
  let full = "";
1231
1267
  try {
1232
- for await (const tok of sseTokens(stream)) {
1268
+ for await (const tok of sseTokens(stream, stats)) {
1233
1269
  full += tok;
1234
1270
  send({ type: "token", v: tok });
1235
1271
  }
1236
1272
  } catch {
1237
1273
  // stream ended prematurely — deliver what we have
1238
1274
  }
1275
+ const tq = await chargeAnswerTokens(env, bucketId, stats, messageChars(messages), full.length);
1276
+ send({ type: "quota", quota: { ...quota, ...tq } });
1239
1277
  const canonical0 = canonicalRefusal(full);
1240
1278
  // answer contract v2: validate [[u:]] refs, resolve typed blocks
1241
1279
  const c2 = canonical0.includes(refusalAnswer())
@@ -1475,7 +1513,8 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
1475
1513
  text: h.text.slice(0, 1200),
1476
1514
  ...((h.metadata as any).table_selection ? { sel: (h.metadata as any).table_selection } : {}),
1477
1515
  }));
1478
- return json({ ...out, context: contextOut, read: readAs(), quota }, 200, { ...corsHeaders(req), "server-timing": serverTiming() });
1516
+ const tq = await chargeAnswerTokens(env, bucketId, undefined, messageChars(messages), answer.length);
1517
+ return json({ ...out, context: contextOut, read: readAs(), quota: { ...quota, ...tq } }, 200, { ...corsHeaders(req), "server-timing": serverTiming() });
1479
1518
  }
1480
1519
 
1481
1520
  function sseResponse(events: unknown[], cors: Record<string, string>): Response {
@@ -1,6 +1,6 @@
1
1
  // Quota + telemetry: per-bucket daily counters (KV) and the D1
2
2
  // queries/spend ledger writes (TODO.impl/23).
3
- import { sha256Hex, today } from "./config";
3
+ import { num, sha256Hex, today } from "./config.ts";
4
4
  import type { Env } from "./env";
5
5
  import type { Background } from "./ports/runtime.ts";
6
6
 
@@ -54,3 +54,44 @@ export function telemetry(
54
54
  ]),
55
55
  );
56
56
  }
57
+
58
+ // ── the token budget (2026-09-29): the daily allowance is TOKENS, not
59
+ // questions — an elevated-effort answer over a wide retrieval context
60
+ // costs orders of magnitude more than a cached refusal, and one number
61
+ // should say so. Charged after generation from the model's own usage
62
+ // when the stream reports it, estimated by characters otherwise.
63
+ export function estimateTokens(chars: number): number {
64
+ return Math.ceil(chars / 4);
65
+ }
66
+
67
+ function tokenTier(bucketId: string): "anon" | "member" | "key" {
68
+ return bucketId.startsWith("key:") ? "key" : bucketId.startsWith("sub:") ? "member" : "anon";
69
+ }
70
+
71
+ export function tokenLimit(env: Env, bucketId: string): number {
72
+ const tier = tokenTier(bucketId);
73
+ return tier === "key"
74
+ ? num(env as any, "KEY_DAY_TOKENS", 4_000_000)
75
+ : tier === "member"
76
+ ? num(env as any, "MEMBER_DAY_TOKENS", 1_500_000)
77
+ : num(env as any, "ANON_DAY_TOKENS", 300_000);
78
+ }
79
+
80
+ export async function tokenBudget(env: Env, bucketId: string): Promise<{ used: number; limit: number }> {
81
+ const used = Number((await env.CACHE.get(`t:${today()}:ask:${await sha256Hex(bucketId)}`)) ?? "0");
82
+ return { used, limit: tokenLimit(env, bucketId) };
83
+ }
84
+
85
+ export async function chargeTokens(env: Env, bucketId: string, tokens: number): Promise<void> {
86
+ if (!(tokens > 0)) return;
87
+ await kvIncr(env.CACHE, `t:${today()}:ask:${await sha256Hex(bucketId)}`, tokens);
88
+ }
89
+
90
+ export function usageTotal(usage: unknown): number | null {
91
+ if (!usage || typeof usage !== "object") return null;
92
+ const u = usage as any;
93
+ const p = Number(u.prompt_tokens ?? 0);
94
+ const c = Number(u.completion_tokens ?? 0);
95
+ const total = p + c;
96
+ return Number.isFinite(total) && total > 0 ? total : null;
97
+ }