@konneal/engine 0.2.72 → 0.2.74
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/dist/{ask-UOTYZ5DC.js → ask-FHA56VIN.js} +2 -2
- package/dist/{chunk-TPV5GPY2.js → chunk-7HWJOFTQ.js} +83 -49
- package/dist/{chunk-UW5M2AXR.js → chunk-CNSTPYX3.js} +57 -10
- package/dist/{chunk-FLNX4O7C.js → chunk-KO6DRI7C.js} +1 -1
- package/dist/quota.d.ts +8 -0
- package/dist/{search-PBOODCMQ.js → search-LVUQLA3C.js} +2 -2
- package/dist/worker_public/src/index.js +4 -4
- package/package.json +1 -1
- package/workers/worker_public/src/ai.ts +13 -7
- package/workers/worker_public/src/ask.ts +52 -8
- package/workers/worker_public/src/operations.ts +16 -3
- package/workers/worker_public/src/quota.ts +42 -1
|
@@ -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
|
|
@@ -44,13 +45,16 @@ async function rerank(ai, model, query, texts) {
|
|
|
44
45
|
async function generateOnce(env, model, messages, effort) {
|
|
45
46
|
for (let attempt2 = 0; attempt2 < 2; attempt2++) {
|
|
46
47
|
try {
|
|
47
|
-
const res = await
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
48
|
+
const res = await Promise.race([
|
|
49
|
+
env.AI.run(model, {
|
|
50
|
+
messages,
|
|
51
|
+
max_tokens: effortBudget(effort ?? answerEffort(env)),
|
|
52
|
+
reasoning_effort: effort ?? answerEffort(env),
|
|
53
|
+
temperature: 0.6,
|
|
54
|
+
top_p: 0.95
|
|
55
|
+
}),
|
|
56
|
+
new Promise((r) => setTimeout(() => r(null), 12e4))
|
|
57
|
+
]);
|
|
54
58
|
if (typeof res?.response === "string" && res.response.trim()) return res.response;
|
|
55
59
|
if (typeof res?.choices?.[0]?.message?.content === "string" && res.choices[0].message.content.trim()) return res.choices[0].message.content;
|
|
56
60
|
if (attempt2 === 0) console.error("generate returned empty:", model);
|
|
@@ -817,8 +821,8 @@ var BaseUrnGenerator = class {
|
|
|
817
821
|
const ed = this.maybe("edition");
|
|
818
822
|
if (ed === void 0 || ed === null)
|
|
819
823
|
return void 0;
|
|
820
|
-
const
|
|
821
|
-
return
|
|
824
|
+
const num2 = typeof ed === "object" && "number" in ed ? ed.number : ed;
|
|
825
|
+
return num2 === void 0 || num2 === null || num2 === "" ? void 0 : `ed.${String(num2)}`;
|
|
822
826
|
}
|
|
823
827
|
urnLanguage() {
|
|
824
828
|
const langs = this.maybe("languages");
|
|
@@ -7139,9 +7143,9 @@ var IsoIdentifier = class _IsoIdentifier extends BaseIdentifier {
|
|
|
7139
7143
|
}
|
|
7140
7144
|
}
|
|
7141
7145
|
}
|
|
7142
|
-
const
|
|
7143
|
-
if (
|
|
7144
|
-
result += result === "" ?
|
|
7146
|
+
const num2 = this.numberPortion(true);
|
|
7147
|
+
if (num2 !== "")
|
|
7148
|
+
result += result === "" ? num2 : ` ${num2}`;
|
|
7145
7149
|
result += this.languagePortion(this.languageSingle());
|
|
7146
7150
|
if (this.all_parts === true)
|
|
7147
7151
|
result += " (all parts)";
|
|
@@ -7306,9 +7310,9 @@ var IsoSingleIdentifier = class extends IsoIdentifier {
|
|
|
7306
7310
|
result += `${sep}${stage}`;
|
|
7307
7311
|
}
|
|
7308
7312
|
}
|
|
7309
|
-
const
|
|
7310
|
-
if (
|
|
7311
|
-
result += result === "" ?
|
|
7313
|
+
const num2 = this.numberPortion(true);
|
|
7314
|
+
if (num2 !== "")
|
|
7315
|
+
result += result === "" ? num2 : ` ${num2}`;
|
|
7312
7316
|
result += this.languagePortion(this.languageSingle());
|
|
7313
7317
|
if (this.all_parts === true)
|
|
7314
7318
|
result += " (all parts)";
|
|
@@ -7611,11 +7615,11 @@ var IsoSupplementIdentifier = class extends IsoSingleIdentifier {
|
|
|
7611
7615
|
const ts = this.typedStageState;
|
|
7612
7616
|
const stage = ts === void 0 ? "" : stageAbbr(ts.entry, long, ts.originalAbbr);
|
|
7613
7617
|
let result = `${baseStr}/${stage}`;
|
|
7614
|
-
const
|
|
7615
|
-
if (
|
|
7616
|
-
if (!
|
|
7618
|
+
const num2 = this.numberPortion(true);
|
|
7619
|
+
if (num2 !== "") {
|
|
7620
|
+
if (!num2.startsWith(":") && !stage.endsWith("."))
|
|
7617
7621
|
result += " ";
|
|
7618
|
-
result +=
|
|
7622
|
+
result += num2;
|
|
7619
7623
|
}
|
|
7620
7624
|
result += this.languagePortion(this.languageSingle());
|
|
7621
7625
|
return result;
|
|
@@ -7670,20 +7674,20 @@ var IsoDirectives = class extends IsoSingleIdentifier {
|
|
|
7670
7674
|
const abbr = this.typedStageState === void 0 ? "" : stageAbbr(this.typedStageState.entry, false);
|
|
7671
7675
|
if (abbr !== "")
|
|
7672
7676
|
head += ` ${abbr}`;
|
|
7673
|
-
let
|
|
7677
|
+
let num2 = this.number ?? "";
|
|
7674
7678
|
if (this.part !== void 0)
|
|
7675
|
-
|
|
7679
|
+
num2 += ` ${this.part}`;
|
|
7676
7680
|
if (this.subpart !== void 0)
|
|
7677
|
-
|
|
7681
|
+
num2 += `-${this.subpart}`;
|
|
7678
7682
|
if (this.stage_iteration !== void 0)
|
|
7679
|
-
|
|
7683
|
+
num2 += `.${this.stage_iteration.render() ?? ""}`;
|
|
7680
7684
|
if (this.date !== void 0 && this.date.present())
|
|
7681
|
-
|
|
7682
|
-
if (
|
|
7685
|
+
num2 += `:${this.date.render()}`;
|
|
7686
|
+
if (num2 === "")
|
|
7683
7687
|
return head;
|
|
7684
|
-
if (
|
|
7685
|
-
return head +
|
|
7686
|
-
return `${head} ${
|
|
7688
|
+
if (num2.startsWith(":"))
|
|
7689
|
+
return head + num2;
|
|
7690
|
+
return `${head} ${num2}`;
|
|
7687
7691
|
}
|
|
7688
7692
|
};
|
|
7689
7693
|
registerType(IsoDirectives);
|
|
@@ -8606,9 +8610,9 @@ function preprocessNist(input) {
|
|
|
8606
8610
|
cleaned = cleaned.replace(/(?<!e)(\d)(rev\d{4})/g, "$1 $2");
|
|
8607
8611
|
cleaned = cleaned.replace(/(\d+e\d+)\.([A-Za-z]{3,9}\d{4})/g, "$1rev$2");
|
|
8608
8612
|
if (/\bIR\b/.test(cleaned) && !cleaned.includes("CIRC")) {
|
|
8609
|
-
cleaned = cleaned.replace(/(\d)r(\d{1,2})\/(\d{2,4})/g, (_m,
|
|
8613
|
+
cleaned = cleaned.replace(/(\d)r(\d{1,2})\/(\d{2,4})/g, (_m, num2, mon, yr) => {
|
|
8610
8614
|
const yyyy = yr.length === 2 ? `19${yr}` : yr;
|
|
8611
|
-
return `${
|
|
8615
|
+
return `${num2}/Upd1-${yyyy}${mon.padStart(2, "0")}`;
|
|
8612
8616
|
});
|
|
8613
8617
|
}
|
|
8614
8618
|
if (!cleaned.includes("LCIRC") && !cleaned.includes("CIRC")) {
|
|
@@ -11321,17 +11325,17 @@ function normalizeJointStageSpellings(input) {
|
|
|
11321
11325
|
let cleaned = input;
|
|
11322
11326
|
const pubs = "((?:ISO/IEC/IEEE|IEEE/ISO/IEC|IEEE/IEC/ISO|ISO/IEEE|IEC/IEEE|IEEE/IEC|ISO/IEC)(?:/ ?| ))";
|
|
11323
11327
|
const stage = "(FDIS|FCD|CDV|DIS\\d?|CD\\d?|WD|PWI|NP)";
|
|
11324
|
-
const
|
|
11328
|
+
const num2 = "(P?\\d+(?:[.-]\\d+)?)";
|
|
11325
11329
|
const tail = "(,? (?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Sept|Oct|Nov|Dec)[a-z]* \\d{4}|$|-\\d{4}(?:-\\d\\d)?)";
|
|
11326
11330
|
cleaned = cleaned.replace(/(\d)_D(\d)/g, "$1/D$2");
|
|
11327
11331
|
cleaned = cleaned.replace(/,(?=[A-Za-z])/, ", ");
|
|
11328
|
-
cleaned = cleaned.replace(new RegExp(`^${pubs}${
|
|
11329
|
-
cleaned = cleaned.replace(new RegExp(`^${pubs}${
|
|
11330
|
-
cleaned = cleaned.replace(new RegExp(`^${pubs}${
|
|
11331
|
-
cleaned = cleaned.replace(new RegExp(`^${pubs}${
|
|
11332
|
-
cleaned = cleaned.replace(new RegExp(`^${pubs}${
|
|
11333
|
-
cleaned = cleaned.replace(new RegExp(`^${pubs}${
|
|
11334
|
-
cleaned = cleaned.replace(new RegExp(`^${pubs}${stage} ${
|
|
11332
|
+
cleaned = cleaned.replace(new RegExp(`^${pubs}${num2}/(D[\\d.]+)/ ?${stage}\\b`), "$1$4 $2/$3");
|
|
11333
|
+
cleaned = cleaned.replace(new RegExp(`^${pubs}${num2}/ ${stage}${tail}`), "$1$3 $2$4");
|
|
11334
|
+
cleaned = cleaned.replace(new RegExp(`^${pubs}${num2}/${stage} (\\d{4})\\b`), "$1$3 $2-$4");
|
|
11335
|
+
cleaned = cleaned.replace(new RegExp(`^${pubs}${num2} ${stage}${tail}`), "$1$3 $2$4");
|
|
11336
|
+
cleaned = cleaned.replace(new RegExp(`^${pubs}${num2} ${stage} (D\\d+)\\b`), "$1$3 $2/$4");
|
|
11337
|
+
cleaned = cleaned.replace(new RegExp(`^${pubs}${num2}/ ?${stage}(-(?:19|20)\\d\\d(?:-\\d\\d)?)`), "$1$3 $2$4");
|
|
11338
|
+
cleaned = cleaned.replace(new RegExp(`^${pubs}${stage} ${num2}/? ?(D\\d+)\\b`), "$1$2 $3/$4");
|
|
11335
11339
|
return cleaned;
|
|
11336
11340
|
}
|
|
11337
11341
|
function preprocessIeee(input) {
|
|
@@ -14556,8 +14560,8 @@ var CenBuilder = class {
|
|
|
14556
14560
|
const numStr = (str3(d["number"]) ?? "").trim();
|
|
14557
14561
|
if (!/^\d{1,6}$/.test(numStr))
|
|
14558
14562
|
return void 0;
|
|
14559
|
-
const
|
|
14560
|
-
if (!(
|
|
14563
|
+
const num2 = Number.parseInt(numStr, 10);
|
|
14564
|
+
if (!(num2 >= 6e4 && num2 <= 79999))
|
|
14561
14565
|
return void 0;
|
|
14562
14566
|
const impl = grammarImplementation("iec");
|
|
14563
14567
|
if (impl === void 0)
|
|
@@ -19006,9 +19010,9 @@ var CsaAdoptedClass = (() => {
|
|
|
19006
19010
|
baseStr = baseStr.replace(`:${year}`, `:${year.slice(2)}`);
|
|
19007
19011
|
}
|
|
19008
19012
|
}
|
|
19009
|
-
baseStr = baseStr.replace(/\/Amd\s+(\d+)([:/-])(\d{2,4})\b/g, (_m,
|
|
19013
|
+
baseStr = baseStr.replace(/\/Amd\s+(\d+)([:/-])(\d{2,4})\b/g, (_m, num2, sep, year) => {
|
|
19010
19014
|
const short = year.length === 4 && (year.startsWith("20") || year.startsWith("19")) ? year.slice(2) : year;
|
|
19011
|
-
return `/A${
|
|
19015
|
+
return `/A${num2}${sep}${short}`;
|
|
19012
19016
|
});
|
|
19013
19017
|
let result;
|
|
19014
19018
|
if (this.publisher_prefix !== void 0 && this.publisher_prefix.endsWith("-")) {
|
|
@@ -19635,9 +19639,9 @@ function parseCsa(input) {
|
|
|
19635
19639
|
const full = Number(two) < 50 ? `20${two}` : `19${two}`;
|
|
19636
19640
|
wrapped = wrapped.replace(new RegExp(`:${two}\\b`), `:${full}`);
|
|
19637
19641
|
}
|
|
19638
|
-
wrapped = wrapped.replace(/\/A(\d+)([:/-])(\d{2,4})\b/g, (_m,
|
|
19642
|
+
wrapped = wrapped.replace(/\/A(\d+)([:/-])(\d{2,4})\b/g, (_m, num2, sep, year) => {
|
|
19639
19643
|
const full = year.length === 2 ? Number(year) < 50 ? `20${year}` : `19${year}` : year;
|
|
19640
|
-
return `/Amd ${
|
|
19644
|
+
return `/Amd ${num2}${sep}${full}`;
|
|
19641
19645
|
});
|
|
19642
19646
|
const base = parseExternalStandard(wrapped);
|
|
19643
19647
|
if (base === void 0) {
|
|
@@ -21908,13 +21912,13 @@ function parseOimlSpine(display) {
|
|
|
21908
21912
|
const kind = String(h._type ?? "").split(":").pop() ?? "";
|
|
21909
21913
|
const letter = TYPE_LETTER[kind] ?? "";
|
|
21910
21914
|
if (!letter) return null;
|
|
21911
|
-
const
|
|
21915
|
+
const num2 = String(Number(h.number));
|
|
21912
21916
|
const part = h.part !== void 0 ? String(h.part) : void 0;
|
|
21913
21917
|
const ed = h.year !== void 0 ? String(h.year) : h.edition !== void 0 ? String(h.edition) : void 0;
|
|
21914
21918
|
return {
|
|
21915
|
-
doc_number:
|
|
21919
|
+
doc_number: num2,
|
|
21916
21920
|
...ed ? { edition: ed } : {},
|
|
21917
|
-
label: `OIML ${letter} ${
|
|
21921
|
+
label: `OIML ${letter} ${num2}${part ? `-${part}` : ""}${ed ? `:${ed}` : ""}`
|
|
21918
21922
|
};
|
|
21919
21923
|
} catch {
|
|
21920
21924
|
return null;
|
|
@@ -21949,9 +21953,9 @@ var oimlPubid = {
|
|
|
21949
21953
|
for (const m of query.matchAll(re)) {
|
|
21950
21954
|
const [, oimlPrefix, letter, gap, digits, edition] = m;
|
|
21951
21955
|
if (digits.length === 1 && !oimlPrefix && !gap) continue;
|
|
21952
|
-
const
|
|
21956
|
+
const num2 = String(Number(digits));
|
|
21953
21957
|
const type = letter.toUpperCase();
|
|
21954
|
-
return { doc_number:
|
|
21958
|
+
return { doc_number: num2, ...edition ? { edition } : {}, label: `OIML ${type} ${num2}${edition ? `:${edition}` : ""}` };
|
|
21955
21959
|
}
|
|
21956
21960
|
return null;
|
|
21957
21961
|
},
|
|
@@ -24578,6 +24582,32 @@ function telemetry(env, ctx, tier, route, model, ok, answerChars, queryHash, lan
|
|
|
24578
24582
|
])
|
|
24579
24583
|
);
|
|
24580
24584
|
}
|
|
24585
|
+
function estimateTokens(chars) {
|
|
24586
|
+
return Math.ceil(chars / 4);
|
|
24587
|
+
}
|
|
24588
|
+
function tokenTier(bucketId) {
|
|
24589
|
+
return bucketId.startsWith("key:") ? "key" : bucketId.startsWith("sub:") ? "member" : "anon";
|
|
24590
|
+
}
|
|
24591
|
+
function tokenLimit(env, bucketId) {
|
|
24592
|
+
const tier = tokenTier(bucketId);
|
|
24593
|
+
return tier === "key" ? num(env, "KEY_DAY_TOKENS", 4e6) : tier === "member" ? num(env, "MEMBER_DAY_TOKENS", 15e5) : num(env, "ANON_DAY_TOKENS", 3e5);
|
|
24594
|
+
}
|
|
24595
|
+
async function tokenBudget(env, bucketId) {
|
|
24596
|
+
const used = Number(await env.CACHE.get(`t:${today()}:ask:${await sha256Hex(bucketId)}`) ?? "0");
|
|
24597
|
+
return { used, limit: tokenLimit(env, bucketId) };
|
|
24598
|
+
}
|
|
24599
|
+
async function chargeTokens(env, bucketId, tokens) {
|
|
24600
|
+
if (!(tokens > 0)) return;
|
|
24601
|
+
await kvIncr(env.CACHE, `t:${today()}:ask:${await sha256Hex(bucketId)}`, tokens);
|
|
24602
|
+
}
|
|
24603
|
+
function usageTotal(usage) {
|
|
24604
|
+
if (!usage || typeof usage !== "object") return null;
|
|
24605
|
+
const u = usage;
|
|
24606
|
+
const p = Number(u.prompt_tokens ?? 0);
|
|
24607
|
+
const c = Number(u.completion_tokens ?? 0);
|
|
24608
|
+
const total = p + c;
|
|
24609
|
+
return Number.isFinite(total) && total > 0 ? total : null;
|
|
24610
|
+
}
|
|
24581
24611
|
|
|
24582
24612
|
// workers/worker_public/src/graph.ts
|
|
24583
24613
|
function docNumberOf(nodeId) {
|
|
@@ -24662,6 +24692,10 @@ export {
|
|
|
24662
24692
|
clientIp,
|
|
24663
24693
|
checkQuota,
|
|
24664
24694
|
telemetry,
|
|
24695
|
+
estimateTokens,
|
|
24696
|
+
tokenBudget,
|
|
24697
|
+
chargeTokens,
|
|
24698
|
+
usageTotal,
|
|
24665
24699
|
graphExpand,
|
|
24666
24700
|
editionNote
|
|
24667
24701
|
};
|
|
@@ -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
|
-
|
|
38
|
-
|
|
39
|
+
tokenBudget,
|
|
40
|
+
understandQuery,
|
|
41
|
+
usageTotal
|
|
42
|
+
} from "./chunk-7HWJOFTQ.js";
|
|
39
43
|
import {
|
|
40
44
|
corsHeaders,
|
|
41
45
|
err,
|
|
@@ -1990,6 +1994,13 @@ ${anchors.map((a) => `- ${a}`).join("\n")}`;
|
|
|
1990
1994
|
}
|
|
1991
1995
|
async function operationsCatalogNote(db, query) {
|
|
1992
1996
|
if (!isOperationIntent(query)) return void 0;
|
|
1997
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
1998
|
+
const note = await operationsCatalogNoteOnce(db, query, attempt > 0);
|
|
1999
|
+
if (note !== void 0) return note;
|
|
2000
|
+
}
|
|
2001
|
+
return void 0;
|
|
2002
|
+
}
|
|
2003
|
+
async function operationsCatalogNoteOnce(db, query, retried) {
|
|
1993
2004
|
try {
|
|
1994
2005
|
const rows = (await db.prepare("SELECT clause_anchor AS anchor, substr(text, 1, 300) AS text FROM chunks WHERE corpus = 'smart-ops' AND clause_anchor IS NOT NULL ORDER BY clause_anchor").all()).results ?? [];
|
|
1995
2006
|
const opRows = rows.map((r) => ({ anchor: String(r.anchor ?? ""), text: String(r.text ?? "") })).filter((r) => r.anchor);
|
|
@@ -2001,7 +2012,8 @@ ${matched.map((m) => `- ${m.text}`).join("\n")}`);
|
|
|
2001
2012
|
}
|
|
2002
2013
|
parts.push(catalogNote(opRows.map((r) => r.anchor)));
|
|
2003
2014
|
return parts.filter(Boolean).join("\n\n") || void 0;
|
|
2004
|
-
} catch {
|
|
2015
|
+
} catch (e) {
|
|
2016
|
+
console.error(`operations-catalog: D1 read failed${retried ? " twice" : ""} \u2014 the note is dropped`, String(e).slice(0, 120));
|
|
2005
2017
|
return void 0;
|
|
2006
2018
|
}
|
|
2007
2019
|
}
|
|
@@ -2166,12 +2178,31 @@ async function summarizeHistory(env, model, turns) {
|
|
|
2166
2178
|
return null;
|
|
2167
2179
|
}
|
|
2168
2180
|
}
|
|
2169
|
-
|
|
2181
|
+
function messageChars(messages) {
|
|
2182
|
+
let n = 0;
|
|
2183
|
+
for (const m of messages) {
|
|
2184
|
+
if (typeof m.content === "string") n += m.content.length;
|
|
2185
|
+
else if (Array.isArray(m.content)) {
|
|
2186
|
+
for (const part of m.content) if (part?.type === "text" && typeof part.text === "string") n += part.text.length;
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
return n;
|
|
2190
|
+
}
|
|
2191
|
+
async function chargeAnswerTokens(env, bucketId, stats, promptChars, answerChars) {
|
|
2192
|
+
const total = usageTotal(stats?.usage) ?? estimateTokens(promptChars + answerChars);
|
|
2193
|
+
await chargeTokens(env, bucketId, total);
|
|
2194
|
+
const budget = await tokenBudget(env, bucketId);
|
|
2195
|
+
return { tokens_used: budget.used, token_limit: budget.limit };
|
|
2196
|
+
}
|
|
2197
|
+
async function* sseTokens(stream, stats) {
|
|
2170
2198
|
const reader = stream.getReader();
|
|
2171
2199
|
const decoder = new TextDecoder();
|
|
2172
2200
|
let buf = "";
|
|
2173
2201
|
while (true) {
|
|
2174
|
-
const { done, value } = await
|
|
2202
|
+
const { done, value } = await Promise.race([
|
|
2203
|
+
reader.read(),
|
|
2204
|
+
new Promise((r) => setTimeout(() => r({ done: true }), 12e4))
|
|
2205
|
+
]);
|
|
2175
2206
|
if (done) break;
|
|
2176
2207
|
buf += decoder.decode(value, { stream: true });
|
|
2177
2208
|
const lines = buf.split("\n");
|
|
@@ -2184,6 +2215,7 @@ async function* sseTokens(stream) {
|
|
|
2184
2215
|
try {
|
|
2185
2216
|
const evt = JSON.parse(payload);
|
|
2186
2217
|
const tok = typeof evt?.response === "string" ? evt.response : evt?.choices?.[0]?.delta?.content;
|
|
2218
|
+
if (evt.usage && stats && stats.usage === void 0) stats.usage = evt.usage;
|
|
2187
2219
|
if (tok) yield tok;
|
|
2188
2220
|
} catch {
|
|
2189
2221
|
}
|
|
@@ -2227,6 +2259,12 @@ async function handleAsk(env, ctx, req, tier, key) {
|
|
|
2227
2259
|
if (!quota.ok) {
|
|
2228
2260
|
return err(429, "quota_exceeded", `Daily question limit reached (${quota.limit}). Try again tomorrow.`);
|
|
2229
2261
|
}
|
|
2262
|
+
const tokBudget = await tokenBudget(env, bucketId);
|
|
2263
|
+
if (tokBudget.used >= tokBudget.limit) {
|
|
2264
|
+
return err(429, "quota_exceeded", `Daily token budget reached (${tokBudget.limit} tokens). Try again tomorrow.`);
|
|
2265
|
+
}
|
|
2266
|
+
quota.tokens_used = tokBudget.used;
|
|
2267
|
+
quota.token_limit = tokBudget.limit;
|
|
2230
2268
|
const hardCap = num(env, "ANON_DAY_HARD_CAP", 5e3);
|
|
2231
2269
|
if (tier === "anon" && quota.used > hardCap) {
|
|
2232
2270
|
return err(503, "generation_disabled", "Generation is temporarily paused; search remains available.");
|
|
@@ -2444,14 +2482,17 @@ ${summary}` }] : [],
|
|
|
2444
2482
|
`));
|
|
2445
2483
|
if (readAs()) send({ type: "read", read: readAs() });
|
|
2446
2484
|
send({ type: "citations", citations: [], context_applied: NO_CONTEXT, quota });
|
|
2485
|
+
const stats = {};
|
|
2447
2486
|
let full = "";
|
|
2448
2487
|
try {
|
|
2449
|
-
for await (const tok of sseTokens(stream)) {
|
|
2488
|
+
for await (const tok of sseTokens(stream, stats)) {
|
|
2450
2489
|
full += tok;
|
|
2451
2490
|
send({ type: "token", v: tok });
|
|
2452
2491
|
}
|
|
2453
2492
|
} catch {
|
|
2454
2493
|
}
|
|
2494
|
+
const tq2 = await chargeAnswerTokens(env, bucketId, stats, messageChars(messages2), full.length);
|
|
2495
|
+
send({ type: "quota", quota: { ...quota, ...tq2 } });
|
|
2455
2496
|
send({ type: "done", model, query_hash: queryHash2, context_applied: NO_CONTEXT, read: readAs() });
|
|
2456
2497
|
telemetry(env, ctx, tier, "ask", model, true, full.length, queryHash2, q.lang, void 0, telemetryMeta());
|
|
2457
2498
|
controller.close();
|
|
@@ -2507,7 +2548,8 @@ ${summary}` }] : [],
|
|
|
2507
2548
|
corsHeaders(req)
|
|
2508
2549
|
);
|
|
2509
2550
|
}
|
|
2510
|
-
|
|
2551
|
+
const tq2 = await chargeAnswerTokens(env, bucketId, void 0, q.query.length, verdict.answer.length);
|
|
2552
|
+
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
2553
|
}
|
|
2512
2554
|
if (apiCallIntent) {
|
|
2513
2555
|
const callCtxApplied = declaredCtx ? appliedContext(declaredCtx, null) : NO_CONTEXT;
|
|
@@ -2534,7 +2576,8 @@ ${summary}` }] : [],
|
|
|
2534
2576
|
corsHeaders(req)
|
|
2535
2577
|
);
|
|
2536
2578
|
}
|
|
2537
|
-
|
|
2579
|
+
const tq2 = await chargeAnswerTokens(env, bucketId, void 0, q.query.length, verdict.answer.length);
|
|
2580
|
+
return json({ answer: verdict.answer, citations: [], model, query_hash: queryHash2, follow_ups: [], context_applied: callCtxApplied, read: readAs(), ...draftPayload ? { draft: draftPayload } : {}, quota: { ...quota, ...tq2 } });
|
|
2538
2581
|
}
|
|
2539
2582
|
let liveRecords;
|
|
2540
2583
|
let accountNote;
|
|
@@ -2834,14 +2877,17 @@ ${q.query}` }],
|
|
|
2834
2877
|
send({ type: "read", read: readAs() });
|
|
2835
2878
|
const sourceQuality = answerQuality(cites.map((c) => c.quality));
|
|
2836
2879
|
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 });
|
|
2880
|
+
const stats = {};
|
|
2837
2881
|
let full = "";
|
|
2838
2882
|
try {
|
|
2839
|
-
for await (const tok of sseTokens(stream)) {
|
|
2883
|
+
for await (const tok of sseTokens(stream, stats)) {
|
|
2840
2884
|
full += tok;
|
|
2841
2885
|
send({ type: "token", v: tok });
|
|
2842
2886
|
}
|
|
2843
2887
|
} catch {
|
|
2844
2888
|
}
|
|
2889
|
+
const tq2 = await chargeAnswerTokens(env, bucketId, stats, messageChars(messages), full.length);
|
|
2890
|
+
send({ type: "quota", quota: { ...quota, ...tq2 } });
|
|
2845
2891
|
const canonical0 = canonicalRefusal(full);
|
|
2846
2892
|
const c2 = canonical0.includes(refusalAnswer()) ? { text: canonical0, blocks: [], dropped: [] } : await contractV2(env.DB, canonical0, usedHits);
|
|
2847
2893
|
send({
|
|
@@ -3007,7 +3053,8 @@ ${q.query}` }],
|
|
|
3007
3053
|
text: h.text.slice(0, 1200),
|
|
3008
3054
|
...h.metadata.table_selection ? { sel: h.metadata.table_selection } : {}
|
|
3009
3055
|
}));
|
|
3010
|
-
|
|
3056
|
+
const tq = await chargeAnswerTokens(env, bucketId, void 0, messageChars(messages), answer.length);
|
|
3057
|
+
return json({ ...out, context: contextOut, read: readAs(), quota: { ...quota, ...tq } }, 200, { ...corsHeaders(req), "server-timing": serverTiming() });
|
|
3011
3058
|
}
|
|
3012
3059
|
function sseResponse(events, cors) {
|
|
3013
3060
|
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;
|
|
@@ -6,10 +6,10 @@ import {
|
|
|
6
6
|
modelGroundingBlock,
|
|
7
7
|
scoreFaithfulness,
|
|
8
8
|
scoreJudge
|
|
9
|
-
} from "../../chunk-
|
|
9
|
+
} from "../../chunk-CNSTPYX3.js";
|
|
10
10
|
import {
|
|
11
11
|
handleSearch
|
|
12
|
-
} from "../../chunk-
|
|
12
|
+
} from "../../chunk-KO6DRI7C.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-
|
|
38
|
+
} from "../../chunk-7HWJOFTQ.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-
|
|
962
|
+
const res = name === "ask" ? await (await import("../../ask-FHA56VIN.js")).handleAsk(env, ctx, inner, tier, key) : await (await import("../../search-LVUQLA3C.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.
|
|
30
|
+
"version": "0.2.74",
|
|
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",
|
|
@@ -54,13 +54,19 @@ export async function generateOnce(env: any, model: string, messages: any[], eff
|
|
|
54
54
|
// silently blindfolds figure answers)
|
|
55
55
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
56
56
|
try {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
57
|
+
// bounded: a STALLED call (no error, no response — observed live
|
|
58
|
+
// 2026-09-29, asks hanging at zero bytes for minutes) must fall
|
|
59
|
+
// through to the retry/fallback, never hang the ask forever
|
|
60
|
+
const res: any = await Promise.race([
|
|
61
|
+
env.AI.run(model, {
|
|
62
|
+
messages,
|
|
63
|
+
max_tokens: effortBudget(effort ?? answerEffort(env)),
|
|
64
|
+
reasoning_effort: effort ?? answerEffort(env),
|
|
65
|
+
temperature: 0.6,
|
|
66
|
+
top_p: 0.95,
|
|
67
|
+
}),
|
|
68
|
+
new Promise<null>((r) => setTimeout(() => r(null), 120_000)),
|
|
69
|
+
]);
|
|
64
70
|
// an EMPTY string is a failed generation, not an answer: the
|
|
65
71
|
// platform intermittently returns empty bodies with a 200 (observed
|
|
66
72
|
// live 2026-09-20 — answers went blank platform-wide while retrieval
|
|
@@ -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 {
|
|
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,12 +206,38 @@ async function summarizeHistory(
|
|
|
206
206
|
}
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
-
|
|
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 = "";
|
|
213
234
|
while (true) {
|
|
214
|
-
|
|
235
|
+
// bounded read: a stalled stream (no error, no chunk) delivers what
|
|
236
|
+
// it has instead of hanging the ask at zero tokens
|
|
237
|
+
const { done, value } = await Promise.race([
|
|
238
|
+
reader.read(),
|
|
239
|
+
new Promise<{ done: true; value?: undefined }>((r) => setTimeout(() => r({ done: true }), 120_000)),
|
|
240
|
+
]);
|
|
215
241
|
if (done) break;
|
|
216
242
|
buf += decoder.decode(value, { stream: true });
|
|
217
243
|
const lines = buf.split("\n");
|
|
@@ -227,6 +253,7 @@ async function* sseTokens(stream: ReadableStream<Uint8Array>): AsyncGenerator<st
|
|
|
227
253
|
typeof evt?.response === "string"
|
|
228
254
|
? evt.response
|
|
229
255
|
: evt?.choices?.[0]?.delta?.content;
|
|
256
|
+
if (evt.usage && stats && stats.usage === undefined) stats.usage = evt.usage;
|
|
230
257
|
if (tok) yield tok;
|
|
231
258
|
} catch {
|
|
232
259
|
// partial JSON in line splitting — ignore
|
|
@@ -320,6 +347,14 @@ async function handleAsk(
|
|
|
320
347
|
if (!quota.ok) {
|
|
321
348
|
return err(429, "quota_exceeded", `Daily question limit reached (${quota.limit}). Try again tomorrow.`);
|
|
322
349
|
}
|
|
350
|
+
// the real allowance is the day's TOKENS (the 2026-09-29 shift): the
|
|
351
|
+
// request cap above is the anti-abuse backstop, this is the budget
|
|
352
|
+
const tokBudget = await tokenBudget(env, bucketId);
|
|
353
|
+
if (tokBudget.used >= tokBudget.limit) {
|
|
354
|
+
return err(429, "quota_exceeded", `Daily token budget reached (${tokBudget.limit} tokens). Try again tomorrow.`);
|
|
355
|
+
}
|
|
356
|
+
(quota as any).tokens_used = tokBudget.used;
|
|
357
|
+
(quota as any).token_limit = tokBudget.limit;
|
|
323
358
|
|
|
324
359
|
const hardCap = num(env as any, "ANON_DAY_HARD_CAP", 5000);
|
|
325
360
|
if (tier === "anon" && quota.used > hardCap) {
|
|
@@ -668,15 +703,18 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
668
703
|
// the reading arrives first — a conversational turn is still a reading
|
|
669
704
|
if (readAs()) send({ type: "read", read: readAs() });
|
|
670
705
|
send({ type: "citations", citations: [], context_applied: NO_CONTEXT, quota, });
|
|
706
|
+
const stats: { usage?: unknown } = {};
|
|
671
707
|
let full = "";
|
|
672
708
|
try {
|
|
673
|
-
for await (const tok of sseTokens(stream)) {
|
|
709
|
+
for await (const tok of sseTokens(stream, stats)) {
|
|
674
710
|
full += tok;
|
|
675
711
|
send({ type: "token", v: tok });
|
|
676
712
|
}
|
|
677
713
|
} catch {
|
|
678
714
|
// stream ended prematurely — deliver what we have
|
|
679
715
|
}
|
|
716
|
+
const tq = await chargeAnswerTokens(env, bucketId, stats, messageChars(messages), full.length);
|
|
717
|
+
send({ type: "quota", quota: { ...quota, ...tq } });
|
|
680
718
|
send({ type: "done", model, query_hash: queryHash, context_applied: NO_CONTEXT, read: readAs() });
|
|
681
719
|
telemetry(env, ctx, tier, "ask", model, true, full.length, queryHash, q.lang, undefined, telemetryMeta());
|
|
682
720
|
controller.close();
|
|
@@ -748,7 +786,8 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
748
786
|
corsHeaders(req),
|
|
749
787
|
);
|
|
750
788
|
}
|
|
751
|
-
|
|
789
|
+
const tq = await chargeAnswerTokens(env, bucketId, undefined, q.query.length, verdict.answer.length);
|
|
790
|
+
return json({ answer: verdict.answer, citations, model, query_hash: queryHash, follow_ups: [], context_applied: draftCtxApplied, read: readAs(), ...(draftPayload ? { draft: draftPayload } : {}), quota: { ...quota, ...tq } });
|
|
752
791
|
}
|
|
753
792
|
|
|
754
793
|
// ── The api_call draft (TODO.ai-platform/09) — the operations
|
|
@@ -785,7 +824,8 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
785
824
|
corsHeaders(req),
|
|
786
825
|
);
|
|
787
826
|
}
|
|
788
|
-
|
|
827
|
+
const tq = await chargeAnswerTokens(env, bucketId, undefined, q.query.length, verdict.answer.length);
|
|
828
|
+
return json({ answer: verdict.answer, citations: [], model, query_hash: queryHash, follow_ups: [], context_applied: callCtxApplied, read: readAs(), ...(draftPayload ? { draft: draftPayload } : {}), quota: { ...quota, ...tq } });
|
|
789
829
|
}
|
|
790
830
|
|
|
791
831
|
// (declared before the retrieval try: the account block, the refusal
|
|
@@ -1227,15 +1267,18 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
1227
1267
|
send({ type: "read", read: readAs() });
|
|
1228
1268
|
const sourceQuality = answerQuality(cites.map((c: any) => c.quality));
|
|
1229
1269
|
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, });
|
|
1270
|
+
const stats: { usage?: unknown } = {};
|
|
1230
1271
|
let full = "";
|
|
1231
1272
|
try {
|
|
1232
|
-
for await (const tok of sseTokens(stream)) {
|
|
1273
|
+
for await (const tok of sseTokens(stream, stats)) {
|
|
1233
1274
|
full += tok;
|
|
1234
1275
|
send({ type: "token", v: tok });
|
|
1235
1276
|
}
|
|
1236
1277
|
} catch {
|
|
1237
1278
|
// stream ended prematurely — deliver what we have
|
|
1238
1279
|
}
|
|
1280
|
+
const tq = await chargeAnswerTokens(env, bucketId, stats, messageChars(messages), full.length);
|
|
1281
|
+
send({ type: "quota", quota: { ...quota, ...tq } });
|
|
1239
1282
|
const canonical0 = canonicalRefusal(full);
|
|
1240
1283
|
// answer contract v2: validate [[u:]] refs, resolve typed blocks
|
|
1241
1284
|
const c2 = canonical0.includes(refusalAnswer())
|
|
@@ -1475,7 +1518,8 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
1475
1518
|
text: h.text.slice(0, 1200),
|
|
1476
1519
|
...((h.metadata as any).table_selection ? { sel: (h.metadata as any).table_selection } : {}),
|
|
1477
1520
|
}));
|
|
1478
|
-
|
|
1521
|
+
const tq = await chargeAnswerTokens(env, bucketId, undefined, messageChars(messages), answer.length);
|
|
1522
|
+
return json({ ...out, context: contextOut, read: readAs(), quota: { ...quota, ...tq } }, 200, { ...corsHeaders(req), "server-timing": serverTiming() });
|
|
1479
1523
|
}
|
|
1480
1524
|
|
|
1481
1525
|
function sseResponse(events: unknown[], cors: Record<string, string>): Response {
|
|
@@ -70,7 +70,18 @@ export function catalogNote(anchors: string[]): string {
|
|
|
70
70
|
|
|
71
71
|
export async function operationsCatalogNote(db: any, query: string): Promise<string | undefined> {
|
|
72
72
|
if (!isOperationIntent(query)) return undefined;
|
|
73
|
-
|
|
73
|
+
// one retry, and the failure is LOUD: a silently dropped note answers
|
|
74
|
+
// an operation question with the OIML refusal (observed live 2026-09-29
|
|
75
|
+
// — a transient D1 read failed into the catch and the lane vanished)
|
|
76
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
77
|
+
const note = await operationsCatalogNoteOnce(db, query, attempt > 0);
|
|
78
|
+
if (note !== undefined) return note;
|
|
79
|
+
}
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function operationsCatalogNoteOnce(db: any, query: string, retried: boolean): Promise<string | undefined> {
|
|
84
|
+
try {
|
|
74
85
|
const rows = (await db.prepare("SELECT clause_anchor AS anchor, substr(text, 1, 300) AS text FROM chunks WHERE corpus = 'smart-ops' AND clause_anchor IS NOT NULL ORDER BY clause_anchor").all()).results ?? [];
|
|
75
86
|
const opRows = (rows as any[]).map((r) => ({ anchor: String(r.anchor ?? ""), text: String(r.text ?? "") })).filter((r) => r.anchor);
|
|
76
87
|
const matched = matchOperations(query, opRows);
|
|
@@ -80,7 +91,9 @@ export async function operationsCatalogNote(db: any, query: string): Promise<str
|
|
|
80
91
|
}
|
|
81
92
|
parts.push(catalogNote(opRows.map((r) => r.anchor)));
|
|
82
93
|
return parts.filter(Boolean).join("\n\n") || undefined;
|
|
83
|
-
} catch {
|
|
84
|
-
|
|
94
|
+
} catch (e) {
|
|
95
|
+
// the lane is optional — but never silent (the blind-failure lesson)
|
|
96
|
+
console.error(`operations-catalog: D1 read failed${retried ? " twice" : ""} — the note is dropped`, String(e).slice(0, 120));
|
|
97
|
+
return undefined;
|
|
85
98
|
}
|
|
86
99
|
}
|
|
@@ -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
|
+
}
|