@konneal/engine 0.2.31 → 0.2.33
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-43C2WZZO.js → ask-GU554Y45.js} +2 -2
- package/dist/{chunk-S7NLWPNX.js → chunk-5LSEBH3J.js} +1 -1
- package/dist/{chunk-F66NYFUK.js → chunk-NHHQAGBW.js} +2 -2
- package/dist/{chunk-TDKNQPA2.js → chunk-WSLADX7O.js} +2 -2
- package/dist/quota.d.ts +1 -0
- package/dist/{search-NHTT5DDI.js → search-LGDQR43R.js} +2 -2
- package/dist/worker_public/src/index.js +29 -11
- package/package.json +1 -1
- package/workers/worker_public/migrations/0015_ask_retries.sql +4 -0
- package/workers/worker_public/src/ask.ts +1 -1
- package/workers/worker_public/src/index.ts +37 -6
- package/workers/worker_public/src/quota.ts +3 -3
|
@@ -24373,8 +24373,8 @@ function telemetry(env, ctx, tier, route, model, ok, answerChars, queryHash, lan
|
|
|
24373
24373
|
ctx.waitUntil(
|
|
24374
24374
|
env.DB.batch([
|
|
24375
24375
|
env.DB.prepare(
|
|
24376
|
-
"INSERT INTO queries (ts, day, tier, route, model, ok, answer_chars, query_hash, lang, cache, duration_ms, key_id) VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12)"
|
|
24377
|
-
).bind((/* @__PURE__ */ new Date()).toISOString(), day, tier, route, model, ok ? 1 : 0, answerChars, queryHash, lang ?? null, cache ?? null, meta?.durationMs ?? null, meta?.keyId ?? null),
|
|
24376
|
+
"INSERT INTO queries (ts, day, tier, route, model, ok, answer_chars, query_hash, lang, cache, duration_ms, key_id, retries) VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13)"
|
|
24377
|
+
).bind((/* @__PURE__ */ new Date()).toISOString(), day, tier, route, model, ok ? 1 : 0, answerChars, queryHash, lang ?? null, cache ?? null, meta?.durationMs ?? null, meta?.keyId ?? null, meta?.retries ?? 0),
|
|
24378
24378
|
env.DB.prepare(
|
|
24379
24379
|
"INSERT INTO spend (day, tier, model, requests) VALUES (?1,?2,?3,1) ON CONFLICT(day, tier, model) DO UPDATE SET requests = requests + 1"
|
|
24380
24380
|
).bind(day, tier, model ?? "none")
|
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
syntheticUnderstanding,
|
|
29
29
|
telemetry,
|
|
30
30
|
understandQuery
|
|
31
|
-
} from "./chunk-
|
|
31
|
+
} from "./chunk-NHHQAGBW.js";
|
|
32
32
|
import {
|
|
33
33
|
corsHeaders,
|
|
34
34
|
err,
|
|
@@ -1612,7 +1612,7 @@ async function* sseTokens(stream) {
|
|
|
1612
1612
|
}
|
|
1613
1613
|
async function handleAsk(env, ctx, req, tier, key) {
|
|
1614
1614
|
const tStart = Date.now();
|
|
1615
|
-
const telemetryMeta = () => ({ durationMs: Date.now() - tStart, keyId: key?.id ?? null });
|
|
1615
|
+
const telemetryMeta = () => ({ durationMs: Date.now() - tStart, keyId: key?.id ?? null, retries: generateRetries });
|
|
1616
1616
|
const stageTiming = {};
|
|
1617
1617
|
let generateRetries = 0;
|
|
1618
1618
|
const readAs = () => understanding ? {
|
package/dist/quota.d.ts
CHANGED
|
@@ -13,4 +13,5 @@ export declare function checkQuota(env: Env, bucket: string, id: string, limit:
|
|
|
13
13
|
export declare function telemetry(env: Env, ctx: Background, tier: string, route: string, model: string | null, ok: boolean, answerChars: number, queryHash: string, lang?: string, cache?: "exact" | "semantic", meta?: {
|
|
14
14
|
durationMs?: number;
|
|
15
15
|
keyId?: string | null;
|
|
16
|
+
retries?: number;
|
|
16
17
|
}): void;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
handleSearch
|
|
3
|
-
} from "../../chunk-
|
|
3
|
+
} from "../../chunk-5LSEBH3J.js";
|
|
4
4
|
import {
|
|
5
5
|
bindModelNode,
|
|
6
6
|
checkQuoteAnchors,
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
modelGroundingBlock,
|
|
10
10
|
scoreJudge,
|
|
11
11
|
standardForDocNumber
|
|
12
|
-
} from "../../chunk-
|
|
12
|
+
} from "../../chunk-WSLADX7O.js";
|
|
13
13
|
import {
|
|
14
14
|
buildMessages,
|
|
15
15
|
citations,
|
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
sessionFrom,
|
|
33
33
|
telemetry,
|
|
34
34
|
understandQuery
|
|
35
|
-
} from "../../chunk-
|
|
35
|
+
} from "../../chunk-NHHQAGBW.js";
|
|
36
36
|
import {
|
|
37
37
|
authenticate,
|
|
38
38
|
corsHeaders,
|
|
@@ -996,7 +996,7 @@ async function handleMcp(env, ctx, req, tier, key) {
|
|
|
996
996
|
// stream:false forces the JSON lane (anon defaults to SSE)
|
|
997
997
|
body: JSON.stringify({ ...args, stream: false })
|
|
998
998
|
});
|
|
999
|
-
const res = name === "ask" ? await (await import("../../ask-
|
|
999
|
+
const res = name === "ask" ? await (await import("../../ask-GU554Y45.js")).handleAsk(env, ctx, inner, tier, key) : await (await import("../../search-LGDQR43R.js")).handleSearch(env, ctx, inner, tier, key);
|
|
1000
1000
|
return res.json().catch(() => ({ error: { message: "tool transport failed", status: res.status } }));
|
|
1001
1001
|
});
|
|
1002
1002
|
if (out.ok && "accepted" in out) return new Response(null, { status: 202 });
|
|
@@ -1382,14 +1382,16 @@ async function adminStatsRoute(c) {
|
|
|
1382
1382
|
if (!env.ADMIN_TOKEN) return err(501, "admin_disabled", "ADMIN_TOKEN secret is not configured");
|
|
1383
1383
|
const auth = req.headers.get("authorization") ?? "";
|
|
1384
1384
|
if (auth !== `Bearer ${env.ADMIN_TOKEN}`) return err(401, "unauthorized", "Invalid admin token");
|
|
1385
|
-
const [byDay, byModel, feedback, convCount, cacheMix, durations] = await Promise.all([
|
|
1385
|
+
const [byDay, byModel, feedback, convCount, cacheMix, durations, retriesRow] = await Promise.all([
|
|
1386
1386
|
env.DB.prepare("SELECT day, tier, COUNT(*) as n, SUM(ok) as ok FROM queries WHERE day >= date('now','-7 days') GROUP BY day, tier ORDER BY day DESC").all(),
|
|
1387
1387
|
env.DB.prepare("SELECT model, SUM(requests) as requests FROM spend WHERE day >= date('now','-7 days') GROUP BY model ORDER BY requests DESC").all(),
|
|
1388
1388
|
env.DB.prepare("SELECT rating, COUNT(*) as n FROM feedback GROUP BY rating").all(),
|
|
1389
1389
|
env.DB.prepare("SELECT COUNT(*) as n FROM conversations").first(),
|
|
1390
1390
|
env.DB.prepare("SELECT COALESCE(cache, 'miss') AS cache, COUNT(*) AS n FROM queries WHERE day >= date('now','-7 days') AND route = 'ask' GROUP BY cache").all(),
|
|
1391
|
-
env.DB.prepare("SELECT duration_ms FROM queries WHERE day >= date('now','-7 days') AND route = 'ask' AND duration_ms IS NOT NULL").all()
|
|
1391
|
+
env.DB.prepare("SELECT duration_ms FROM queries WHERE day >= date('now','-7 days') AND route = 'ask' AND duration_ms IS NOT NULL").all(),
|
|
1392
|
+
env.DB.prepare("SELECT COALESCE(SUM(retries), 0) AS total, SUM(CASE WHEN retries > 0 THEN 1 ELSE 0 END) AS answers FROM queries WHERE day >= date('now','-7 days') AND route = 'ask'").all()
|
|
1392
1393
|
]);
|
|
1394
|
+
const retries = retriesRow?.results?.[0] ?? { total: 0, answers: 0 };
|
|
1393
1395
|
const ds = durations.results.map((r) => r.duration_ms).sort((a, b) => a - b);
|
|
1394
1396
|
const pct = (q) => ds.length ? ds[Math.min(ds.length - 1, Math.floor(q * ds.length))] : null;
|
|
1395
1397
|
const totalQueries = byDay.results.reduce((a, r) => a + (r.n || 0), 0) || 0;
|
|
@@ -1407,6 +1409,7 @@ async function adminStatsRoute(c) {
|
|
|
1407
1409
|
feedback: feedback.results,
|
|
1408
1410
|
cache_mix_7d: cacheMix.results,
|
|
1409
1411
|
latency_ms: ds.length ? { n: ds.length, p50: pct(0.5), p95: pct(0.95) } : null,
|
|
1412
|
+
generate_retries_7d: { total: Number(retries.total) || 0, answers_retried: Number(retries.answers) || 0 },
|
|
1410
1413
|
conversations: convCount?.n ?? 0,
|
|
1411
1414
|
error_rate_pct: errorRate,
|
|
1412
1415
|
index_version: env.INDEX_VERSION,
|
|
@@ -1470,11 +1473,20 @@ async function verifyRoute(c) {
|
|
|
1470
1473
|
}
|
|
1471
1474
|
const bound = await bindModelNode(env, { query, standardKeys: entitlementScope(standardKeysFrom(body)) });
|
|
1472
1475
|
const modelGrounding = bound && !bound.gated ? modelGroundingBlock(bound) : null;
|
|
1473
|
-
const
|
|
1474
|
-
|
|
1476
|
+
const supplied = (Array.isArray(body?.passages) ? body.passages : []).map((p) => typeof p === "string" ? p : p?.text ?? "").map((p) => p.trim()).filter(Boolean).slice(0, 8);
|
|
1477
|
+
let passages;
|
|
1478
|
+
let structured = [];
|
|
1479
|
+
if (supplied.length) {
|
|
1480
|
+
passages = supplied;
|
|
1481
|
+
structured = supplied.map((p) => ({ text: p }));
|
|
1482
|
+
} else {
|
|
1483
|
+
const retrieved = await retrieve(env, query, { understanding: u, standardKeys: entitlementScope(standardKeysFrom(body)), lexicalBoost, editionSteer });
|
|
1484
|
+
passages = retrieved.hits.map((h) => h.text);
|
|
1485
|
+
structured = retrieved.hits.map((h) => ({ text: h.text, table: h.metadata.block === "table" || void 0 }));
|
|
1486
|
+
}
|
|
1475
1487
|
const anchors = checkQuoteAnchors(answer, passages);
|
|
1476
1488
|
const refs = [...answer.matchAll(/\[\[u:([^\]]+)\]\]/g)].map((m) => m[1]);
|
|
1477
|
-
const validRefs = refs.filter((r) =>
|
|
1489
|
+
const validRefs = refs.filter((r) => supplied.length ? supplied.some((p) => p.includes(`u:${r}`)) : structured.some((h) => h.text && !h.table));
|
|
1478
1490
|
const checks = [
|
|
1479
1491
|
{ name: "quote_anchors", deterministic: true, pass: anchors.violations.length === 0, detail: `${anchors.violations.length} of ${anchors.total} quoted spans absent from the retrieved passages` },
|
|
1480
1492
|
{ name: "unit_references", deterministic: true, pass: refs.length === validRefs.length, detail: refs.length ? `${validRefs.length}/${refs.length} unit references resolve to served units` : "no unit references" },
|
|
@@ -1482,11 +1494,17 @@ async function verifyRoute(c) {
|
|
|
1482
1494
|
];
|
|
1483
1495
|
const machine = (Array.isArray(body?.blocks) ? body.blocks : []).filter((b) => b?.type === "verdict" && b?.payload).map((b) => [b.payload.check, b.payload.meaning, b.payload.definition, b.payload.violation_meaning].filter((x) => typeof x === "string" && x).join(" \u2014 ")).filter(Boolean);
|
|
1484
1496
|
if (modelGrounding) machine.unshift(modelGrounding);
|
|
1485
|
-
const faith = await scoreFaithfulness(
|
|
1497
|
+
const faith = await scoreFaithfulness(
|
|
1498
|
+
env.AI,
|
|
1499
|
+
roleModel(env, "grader"),
|
|
1500
|
+
answer,
|
|
1501
|
+
structured,
|
|
1502
|
+
machine
|
|
1503
|
+
);
|
|
1486
1504
|
return json({
|
|
1487
1505
|
checks,
|
|
1488
1506
|
judged: faith ? { name: "faithfulness", deterministic: false, score: faith.score, ungrounded_claims: faith.ungrounded_claims.slice(0, 5) } : null,
|
|
1489
|
-
passages_used:
|
|
1507
|
+
passages_used: passages.length
|
|
1490
1508
|
});
|
|
1491
1509
|
} catch (e) {
|
|
1492
1510
|
return err(502, "verify_failed", String(e).slice(0, 200));
|
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.33",
|
|
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",
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
-- the latency program's aggregate: how many generation retries each ask
|
|
2
|
+
-- needed (Server-Timing carried the count per answer; this column makes
|
|
3
|
+
-- it rollup-able). Existing rows read 0 — they predate the column.
|
|
4
|
+
ALTER TABLE queries ADD COLUMN retries INTEGER NOT NULL DEFAULT 0;
|
|
@@ -237,7 +237,7 @@ async function handleAsk(
|
|
|
237
237
|
// the latency program's clock: every telemetry write below reports the
|
|
238
238
|
// wall time from request entry to its own exit
|
|
239
239
|
const tStart = Date.now();
|
|
240
|
-
const telemetryMeta = () => ({ durationMs: Date.now() - tStart, keyId: key?.id ?? null });
|
|
240
|
+
const telemetryMeta = () => ({ durationMs: Date.now() - tStart, keyId: key?.id ?? null, retries: generateRetries });
|
|
241
241
|
// the latency anatomy, surfaced as standard Server-Timing headers on
|
|
242
242
|
// the JSON response — the reduction program's per-stage data
|
|
243
243
|
const stageTiming: Record<string, number> = {};
|
|
@@ -152,14 +152,16 @@ async function adminStatsRoute(c: RouteContext): Promise<Response> {
|
|
|
152
152
|
if (!env.ADMIN_TOKEN) return err(501, "admin_disabled", "ADMIN_TOKEN secret is not configured");
|
|
153
153
|
const auth = req.headers.get("authorization") ?? "";
|
|
154
154
|
if (auth !== `Bearer ${env.ADMIN_TOKEN}`) return err(401, "unauthorized", "Invalid admin token");
|
|
155
|
-
const [byDay, byModel, feedback, convCount, cacheMix, durations] = await Promise.all([
|
|
155
|
+
const [byDay, byModel, feedback, convCount, cacheMix, durations, retriesRow] = await Promise.all([
|
|
156
156
|
env.DB.prepare("SELECT day, tier, COUNT(*) as n, SUM(ok) as ok FROM queries WHERE day >= date('now','-7 days') GROUP BY day, tier ORDER BY day DESC").all(),
|
|
157
157
|
env.DB.prepare("SELECT model, SUM(requests) as requests FROM spend WHERE day >= date('now','-7 days') GROUP BY model ORDER BY requests DESC").all(),
|
|
158
158
|
env.DB.prepare("SELECT rating, COUNT(*) as n FROM feedback GROUP BY rating").all(),
|
|
159
159
|
env.DB.prepare("SELECT COUNT(*) as n FROM conversations").first(),
|
|
160
160
|
env.DB.prepare("SELECT COALESCE(cache, 'miss') AS cache, COUNT(*) AS n FROM queries WHERE day >= date('now','-7 days') AND route = 'ask' GROUP BY cache").all(),
|
|
161
161
|
env.DB.prepare("SELECT duration_ms FROM queries WHERE day >= date('now','-7 days') AND route = 'ask' AND duration_ms IS NOT NULL").all(),
|
|
162
|
+
env.DB.prepare("SELECT COALESCE(SUM(retries), 0) AS total, SUM(CASE WHEN retries > 0 THEN 1 ELSE 0 END) AS answers FROM queries WHERE day >= date('now','-7 days') AND route = 'ask'").all(),
|
|
162
163
|
]);
|
|
164
|
+
const retries = (retriesRow as any)?.results?.[0] ?? { total: 0, answers: 0 };
|
|
163
165
|
const ds = (durations.results as any[]).map((r) => r.duration_ms as number).sort((a, b) => a - b);
|
|
164
166
|
const pct = (q: number) => (ds.length ? ds[Math.min(ds.length - 1, Math.floor(q * ds.length))] : null);
|
|
165
167
|
const totalQueries = (byDay.results as any[]).reduce((a, r) => a + (r.n || 0), 0) || 0;
|
|
@@ -177,6 +179,7 @@ async function adminStatsRoute(c: RouteContext): Promise<Response> {
|
|
|
177
179
|
feedback: feedback.results,
|
|
178
180
|
cache_mix_7d: cacheMix.results,
|
|
179
181
|
latency_ms: ds.length ? { n: ds.length, p50: pct(0.5), p95: pct(0.95) } : null,
|
|
182
|
+
generate_retries_7d: { total: Number(retries.total) || 0, answers_retried: Number(retries.answers) || 0 },
|
|
180
183
|
conversations: (convCount as any)?.n ?? 0,
|
|
181
184
|
error_rate_pct: errorRate,
|
|
182
185
|
index_version: env.INDEX_VERSION,
|
|
@@ -250,11 +253,33 @@ async function verifyRoute(c: RouteContext): Promise<Response> {
|
|
|
250
253
|
}
|
|
251
254
|
const bound = await bindModelNode(env, { query, standardKeys: entitlementScope(standardKeysFrom(body)) });
|
|
252
255
|
const modelGrounding = bound && !bound.gated ? modelGroundingBlock(bound) : null;
|
|
253
|
-
|
|
254
|
-
|
|
256
|
+
// the caller's grounding WINS when present: the verify button (and
|
|
257
|
+
// the harness) hold the passages the answer was actually grounded on
|
|
258
|
+
// — verify's own fresh retrieval is a DIFFERENT pipeline (no grade,
|
|
259
|
+
// no listwise, no corrective widen) and returns different passages,
|
|
260
|
+
// which judges the answer against text it never saw
|
|
261
|
+
const supplied = (Array.isArray(body?.passages) ? body.passages : [])
|
|
262
|
+
.map((p: unknown) => (typeof p === "string" ? p : (p as any)?.text ?? ""))
|
|
263
|
+
.map((p: string) => p.trim())
|
|
264
|
+
.filter(Boolean)
|
|
265
|
+
.slice(0, 8);
|
|
266
|
+
let passages: string[];
|
|
267
|
+
let structured: { text: string; table?: boolean }[] = [];
|
|
268
|
+
if (supplied.length) {
|
|
269
|
+
passages = supplied;
|
|
270
|
+
structured = supplied.map((p: string) => ({ text: p }));
|
|
271
|
+
} else {
|
|
272
|
+
const retrieved = await retrieve(env, query, { understanding: u, standardKeys: entitlementScope(standardKeysFrom(body)), lexicalBoost, editionSteer });
|
|
273
|
+
passages = retrieved.hits.map((h: Hit) => h.text);
|
|
274
|
+
structured = retrieved.hits.map((h: Hit) => ({ text: h.text, table: (h.metadata as any).block === "table" || undefined }));
|
|
275
|
+
}
|
|
255
276
|
const anchors = checkQuoteAnchors(answer, passages);
|
|
256
277
|
const refs = [...answer.matchAll(/\[\[u:([^\]]+)\]\]/g)].map((m) => m[1]);
|
|
257
|
-
|
|
278
|
+
// unit references resolve against the grounding in use
|
|
279
|
+
const validRefs = refs.filter((r: string) =>
|
|
280
|
+
supplied.length
|
|
281
|
+
? supplied.some((p: string) => p.includes(`u:${r}`))
|
|
282
|
+
: structured.some((h) => h.text && !h.table));
|
|
258
283
|
const checks = [
|
|
259
284
|
{ name: "quote_anchors", deterministic: true, pass: anchors.violations.length === 0, detail: `${anchors.violations.length} of ${anchors.total} quoted spans absent from the retrieved passages` },
|
|
260
285
|
{ name: "unit_references", deterministic: true, pass: refs.length === validRefs.length, detail: refs.length ? `${validRefs.length}/${refs.length} unit references resolve to served units` : "no unit references" },
|
|
@@ -269,11 +294,17 @@ async function verifyRoute(c: RouteContext): Promise<Response> {
|
|
|
269
294
|
.filter((x: unknown) => typeof x === "string" && x).join(" — "))
|
|
270
295
|
.filter(Boolean);
|
|
271
296
|
if (modelGrounding) machine.unshift(modelGrounding);
|
|
272
|
-
const faith = await scoreFaithfulness(
|
|
297
|
+
const faith = await scoreFaithfulness(
|
|
298
|
+
env.AI,
|
|
299
|
+
roleModel(env, "grader"),
|
|
300
|
+
answer,
|
|
301
|
+
structured,
|
|
302
|
+
machine,
|
|
303
|
+
);
|
|
273
304
|
return json({
|
|
274
305
|
checks,
|
|
275
306
|
judged: faith ? { name: "faithfulness", deterministic: false, score: faith.score, ungrounded_claims: faith.ungrounded_claims.slice(0, 5) } : null,
|
|
276
|
-
passages_used:
|
|
307
|
+
passages_used: passages.length,
|
|
277
308
|
});
|
|
278
309
|
} catch (e) {
|
|
279
310
|
return err(502, "verify_failed", String(e).slice(0, 200));
|
|
@@ -40,14 +40,14 @@ export function telemetry(
|
|
|
40
40
|
queryHash: string,
|
|
41
41
|
lang?: string,
|
|
42
42
|
cache?: "exact" | "semantic",
|
|
43
|
-
meta?: { durationMs?: number; keyId?: string | null },
|
|
43
|
+
meta?: { durationMs?: number; keyId?: string | null; retries?: number },
|
|
44
44
|
) {
|
|
45
45
|
const day = today();
|
|
46
46
|
ctx.waitUntil(
|
|
47
47
|
env.DB.batch([
|
|
48
48
|
env.DB.prepare(
|
|
49
|
-
"INSERT INTO queries (ts, day, tier, route, model, ok, answer_chars, query_hash, lang, cache, duration_ms, key_id) VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12)",
|
|
50
|
-
).bind(new Date().toISOString(), day, tier, route, model, ok ? 1 : 0, answerChars, queryHash, lang ?? null, cache ?? null, meta?.durationMs ?? null, meta?.keyId ?? null),
|
|
49
|
+
"INSERT INTO queries (ts, day, tier, route, model, ok, answer_chars, query_hash, lang, cache, duration_ms, key_id, retries) VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13)",
|
|
50
|
+
).bind(new Date().toISOString(), day, tier, route, model, ok ? 1 : 0, answerChars, queryHash, lang ?? null, cache ?? null, meta?.durationMs ?? null, meta?.keyId ?? null, meta?.retries ?? 0),
|
|
51
51
|
env.DB.prepare(
|
|
52
52
|
"INSERT INTO spend (day, tier, model, requests) VALUES (?1,?2,?3,1) ON CONFLICT(day, tier, model) DO UPDATE SET requests = requests + 1",
|
|
53
53
|
).bind(day, tier, model ?? "none"),
|