@konneal/engine 0.2.124 → 0.2.130

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,10 +1,10 @@
1
1
  import {
2
2
  handleAsk
3
- } from "./chunk-ZO6MMKGD.js";
3
+ } from "./chunk-AIL7MJS3.js";
4
4
  import "./chunk-J6LBR6GI.js";
5
- import "./chunk-2JUWER2A.js";
5
+ import "./chunk-7AXXR7GD.js";
6
6
  import "./chunk-TH5XGPD5.js";
7
- import "./chunk-DJ65EF5G.js";
7
+ import "./chunk-W7J5SFXJ.js";
8
8
  import "./chunk-A4A26BCV.js";
9
9
  import "./chunk-4NXH5ZI6.js";
10
10
  import "./chunk-VT7DR6NQ.js";
@@ -9,7 +9,7 @@ import {
9
9
  import {
10
10
  bubbleConfirmPage,
11
11
  isAllowedBubbleOrigin
12
- } from "./chunk-DJ65EF5G.js";
12
+ } from "./chunk-W7J5SFXJ.js";
13
13
  import {
14
14
  DATASETS,
15
15
  LIMITS,
@@ -24725,6 +24725,18 @@ function usageTotal(usage) {
24725
24725
  const total = p + c;
24726
24726
  return Number.isFinite(total) && total > 0 ? total : null;
24727
24727
  }
24728
+ var CREDIT_TOKEN_UNIT = 1e3;
24729
+ function creditLimit(env, bucketId) {
24730
+ return Math.floor(tokenLimit(env, bucketId) / CREDIT_TOKEN_UNIT);
24731
+ }
24732
+ async function creditBudget(env, bucketId) {
24733
+ const b = await tokenBudget(env, bucketId);
24734
+ return { credits_used: Math.ceil(b.used / CREDIT_TOKEN_UNIT), credits_limit: creditLimit(env, bucketId) };
24735
+ }
24736
+ function creditEstimate(opts) {
24737
+ const effortCredits = opts.effort === "low" ? 1 : opts.effort === "medium" ? 2 : 3;
24738
+ return effortCredits + Math.max(0, opts.toolTurns) + (opts.image ? 1 : 0);
24739
+ }
24728
24740
 
24729
24741
  // workers/worker_public/src/graph.ts
24730
24742
  function docNumberOf(nodeId) {
@@ -24816,6 +24828,8 @@ export {
24816
24828
  tokenBudget,
24817
24829
  chargeTokens,
24818
24830
  usageTotal,
24831
+ creditBudget,
24832
+ creditEstimate,
24819
24833
  graphExpand,
24820
24834
  editionNote
24821
24835
  };
@@ -34,6 +34,8 @@ import {
34
34
  citations,
35
35
  clientIp,
36
36
  contextNote,
37
+ creditBudget,
38
+ creditEstimate,
37
39
  editionNote,
38
40
  estimateTokens,
39
41
  experimentalSourceLabels,
@@ -63,7 +65,7 @@ import {
63
65
  tokenBudget,
64
66
  understandQuery,
65
67
  usageTotal
66
- } from "./chunk-2JUWER2A.js";
68
+ } from "./chunk-7AXXR7GD.js";
67
69
  import {
68
70
  embed,
69
71
  generateOnce,
@@ -76,7 +78,7 @@ import {
76
78
  json,
77
79
  readJson,
78
80
  validateQuery
79
- } from "./chunk-DJ65EF5G.js";
81
+ } from "./chunk-W7J5SFXJ.js";
80
82
  import {
81
83
  canonicalRefusal,
82
84
  exciseMisplacedPin,
@@ -1788,8 +1790,8 @@ function messageChars(messages) {
1788
1790
  }
1789
1791
  return n;
1790
1792
  }
1791
- async function chargeAnswerTokens(env, bucketId, stats, promptChars, answerChars) {
1792
- const total = usageTotal(stats?.usage) ?? estimateTokens(promptChars + answerChars);
1793
+ async function chargeAnswerTokens(env, bucketId, stats, promptChars, answerChars, extraTokens = 0) {
1794
+ const total = (usageTotal(stats?.usage) ?? estimateTokens(promptChars + answerChars)) + extraTokens;
1793
1795
  await chargeTokens(env, bucketId, total);
1794
1796
  const budget = await tokenBudget(env, bucketId);
1795
1797
  return { tokens_used: budget.used, token_limit: budget.limit };
@@ -1858,15 +1860,18 @@ async function handleAsk(env, ctx, req, tier, key) {
1858
1860
  const effort = requestEffort(env, member, body?.effort);
1859
1861
  const declaredEffort = body?.effort;
1860
1862
  const routeEffort = (route) => route === "fast" && !declaredEffort ? "low" : effort;
1861
- const limit = tier === "key" ? key.day_limit : tier === "member" || member ? num(env, "MEMBER_DAY_ASK", 300) : num(env, "ANON_DAY_ASK", 20);
1862
- const bucketId = tier === "key" ? `key:${key.id}` : member ? `sub:${member.sub}` : clientIp(req);
1863
+ const memberKey = tier === "key" ? key?.owner_sub ?? null : null;
1864
+ const limit = memberKey ? num(env, "MEMBER_DAY_ASK", 300) : tier === "key" ? key.day_limit : tier === "member" || member ? num(env, "MEMBER_DAY_ASK", 300) : num(env, "ANON_DAY_ASK", 20);
1865
+ const bucketId = memberKey ? `sub:${memberKey}` : tier === "key" ? `key:${key.id}` : member ? `sub:${member.sub}` : clientIp(req);
1863
1866
  const quota = await checkQuota(env, "ask", bucketId, limit, effort === "low" ? 1 : 2);
1867
+ const creditPreview = creditEstimate({ effort, toolTurns: body?.multi_turn !== false ? 3 : 0, image: false });
1864
1868
  if (!quota.ok) {
1865
1869
  return err(429, "quota_exceeded", `Daily question limit reached (${quota.limit}). Try again tomorrow.`);
1866
1870
  }
1867
1871
  const tokBudget = await tokenBudget(env, bucketId);
1868
1872
  if (tokBudget.used >= tokBudget.limit) {
1869
- return err(429, "quota_exceeded", `Daily token budget reached (${tokBudget.limit} tokens). Try again tomorrow.`);
1873
+ const cb = await creditBudget(env, bucketId);
1874
+ return err(429, "quota_exceeded", `Daily credits reached (${cb.credits_limit} credits, ${cb.credits_used} used). Try again tomorrow.`);
1870
1875
  }
1871
1876
  quota.tokens_used = tokBudget.used;
1872
1877
  quota.token_limit = tokBudget.limit;
@@ -1928,6 +1933,7 @@ async function handleAsk(env, ctx, req, tier, key) {
1928
1933
  const summary = overflow.length >= 2 ? await summarizeHistory(env, MODELS.understand, overflow) ?? void 0 : void 0;
1929
1934
  let retrieved;
1930
1935
  const fresh = freshRequested(body);
1936
+ const multiTurn = body?.multi_turn !== false;
1931
1937
  const gen = await corpusGen(env.CACHE);
1932
1938
  const withImage = effectiveImage || storedImage;
1933
1939
  const cached = fresh || contextual || declaredCtx || draftAct || apiCallIntent || withImage || ablate ? null : await cacheGet(env, gen, ns, q.query, q.lang, salt);
@@ -2082,9 +2088,33 @@ async function handleAsk(env, ctx, req, tier, key) {
2082
2088
  if (fam) {
2083
2089
  const agg = await registerAggregate(env.DB, fam);
2084
2090
  if (agg) {
2091
+ const latestLine = agg.latest ? `The latest certificate in the snapshot is ${agg.latest.num} \u2014 holder ${agg.latest.holder}, model "${agg.latest.model}"${agg.latest.year ? `, issued ${agg.latest.year}` : ""}, status ${agg.latest.status}.` : "No certificate rows are present in the snapshot.";
2092
+ const linkLine = agg.latest?.pdf_key ? `
2093
+
2094
+ [The latest certificate's document](${`/cert-pdf/${agg.latest.pdf_key}`}) is on file.` : "";
2095
+ const answer2 = aggIntent === "count" ? `The ${P().publisher.name} ${agg.family} family holds **${agg.count} certificate${agg.count === 1 ? "" : "s"}** in the ${P().publisher.name}-CS register snapshot (${agg.valid} valid${agg.suspended ? `, ${agg.suspended} suspended` : ""}).
2096
+
2097
+ Certificates are issued per recommendation family \u2014 the register carries no edition dimension, so the 2021 you named does not narrow the count. The register is a snapshot rather than live certification status.` : `${latestLine}${linkLine}
2098
+
2099
+ Certificates are issued per recommendation family \u2014 the register carries no edition dimension. The register is a snapshot rather than live certification status.`;
2100
+ const block = aggregateBlock(agg);
2101
+ registerBlock = block;
2085
2102
  aggregateNoteText = aggregateNote(agg);
2086
- registerBlock = aggregateBlock(agg);
2087
- console.log("register aggregate:", agg.family, "count", agg.count, "latest", agg.latest?.num ?? "none");
2103
+ const queryHash2 = await sha256Hex(q.query);
2104
+ telemetry(env, ctx, tier, "ask", MODELS.member, true, answer2.length, queryHash2, q.lang, void 0, telemetryMeta());
2105
+ const tq2 = await chargeAnswerTokens(env, bucketId, void 0, q.query.length, answer2.length);
2106
+ return json({
2107
+ answer: answer2,
2108
+ citations: [{ docidentifier: `${P().publisher.name}-CS certificate register (family ${agg.family})`, corpus: "register" }],
2109
+ model: MODELS.member,
2110
+ query_hash: queryHash2,
2111
+ follow_ups: [],
2112
+ blocks: [block],
2113
+ context_applied: NO_CONTEXT,
2114
+ read: readAs(),
2115
+ ...ablate ? { ablate } : {},
2116
+ quota: { ...quota, ...tq2, credits: await creditBudget(env, bucketId), credit_estimate: creditPreview }
2117
+ });
2088
2118
  }
2089
2119
  }
2090
2120
  }
@@ -2119,8 +2149,8 @@ ${summary}` }] : [],
2119
2149
  }
2120
2150
  } catch {
2121
2151
  }
2122
- const tq2 = await chargeAnswerTokens(env, bucketId, stats, messageChars(messages2), full.length);
2123
- send({ type: "quota", quota: { ...quota, ...tq2 } });
2152
+ const tq2 = await chargeAnswerTokens(env, bucketId, stats, messageChars(messages2), full.length, probeTokensUsed);
2153
+ send({ type: "quota", quota: { ...quota, ...tq2, credits: await creditBudget(env, bucketId), credit_estimate: creditPreview } });
2124
2154
  send({ type: "done", model, query_hash: queryHash2, context_applied: NO_CONTEXT, read: readAs() });
2125
2155
  telemetry(env, ctx, tier, "ask", model, true, full.length, queryHash2, q.lang, void 0, telemetryMeta());
2126
2156
  controller.close();
@@ -2177,7 +2207,7 @@ ${summary}` }] : [],
2177
2207
  );
2178
2208
  }
2179
2209
  const tq2 = await chargeAnswerTokens(env, bucketId, void 0, q.query.length, verdict.answer.length);
2180
- return json({ answer: verdict.answer, citations: citations2, model, query_hash: queryHash2, follow_ups: [], context_applied: draftCtxApplied, read: readAs(), ...draftPayload ? { draft: draftPayload } : {}, ...ablate ? { ablate } : {}, quota: { ...quota, ...tq2 } });
2210
+ return json({ answer: verdict.answer, citations: citations2, model, query_hash: queryHash2, follow_ups: [], context_applied: draftCtxApplied, read: readAs(), ...draftPayload ? { draft: draftPayload } : {}, ...ablate ? { ablate } : {}, quota: { ...quota, ...tq2, credits: await creditBudget(env, bucketId), credit_estimate: creditPreview } });
2181
2211
  }
2182
2212
  if (apiCallIntent) {
2183
2213
  const callCtxApplied = declaredCtx ? appliedContext(declaredCtx, null) : NO_CONTEXT;
@@ -2205,7 +2235,7 @@ ${summary}` }] : [],
2205
2235
  );
2206
2236
  }
2207
2237
  const tq2 = await chargeAnswerTokens(env, bucketId, void 0, q.query.length, verdict.answer.length);
2208
- return json({ answer: verdict.answer, citations: [], model, query_hash: queryHash2, follow_ups: [], context_applied: callCtxApplied, read: readAs(), ...draftPayload ? { draft: draftPayload } : {}, ...ablate ? { ablate } : {}, quota: { ...quota, ...tq2 } });
2238
+ return json({ answer: verdict.answer, citations: [], model, query_hash: queryHash2, follow_ups: [], context_applied: callCtxApplied, read: readAs(), ...draftPayload ? { draft: draftPayload } : {}, ...ablate ? { ablate } : {}, quota: { ...quota, ...tq2, credits: await creditBudget(env, bucketId), credit_estimate: creditPreview } });
2209
2239
  }
2210
2240
  let liveRecords;
2211
2241
  let accountNote;
@@ -2489,7 +2519,8 @@ Answer account questions from these records ONLY: name the record when you use i
2489
2519
  );
2490
2520
  await attachFigureImages(env, messages, usedHits, q.query);
2491
2521
  const pendingTools = [];
2492
- if (/certificate|certified|certification/i.test(q.query) && env.DB) {
2522
+ let probeTokensUsed = 0;
2523
+ if (multiTurn && /certificate|certified|certification/i.test(q.query) && env.DB) {
2493
2524
  try {
2494
2525
  const imageTurn = typeof withImage === "string";
2495
2526
  if (imageTurn) mark("Reading the photograph to identify the instrument");
@@ -2505,6 +2536,7 @@ ${q.query}`.replace("\n\n\n\n", "\n\n") }];
2505
2536
  reasoning_effort: imageTurn ? "high" : "low"
2506
2537
  });
2507
2538
  const probeText = typeof probe?.response === "string" ? probe.response : probe?.choices?.[0]?.message?.content ?? "";
2539
+ probeTokensUsed += usageTotal(probe?.usage) ?? 0;
2508
2540
  const call = parseToolCall(probeText);
2509
2541
  if (call) {
2510
2542
  mark(`Searching the certificate register for \u201C${String(call.args?.query ?? "")}\u201D`);
@@ -2566,8 +2598,8 @@ ${q.query}`.replace("\n\n\n\n", "\n\n") }];
2566
2598
  }
2567
2599
  } catch {
2568
2600
  }
2569
- const tq2 = await chargeAnswerTokens(env, bucketId, stats, messageChars(messages), full.length);
2570
- send({ type: "quota", quota: { ...quota, ...tq2 } });
2601
+ const tq2 = await chargeAnswerTokens(env, bucketId, stats, messageChars(messages), full.length, probeTokensUsed);
2602
+ send({ type: "quota", quota: { ...quota, ...tq2, credits: await creditBudget(env, bucketId), credit_estimate: creditPreview } });
2571
2603
  if (withImage && register?.rows?.length && full.includes(refusalAnswer())) {
2572
2604
  console.log("blind-answer(stream): regenerating once");
2573
2605
  mark("Re-answering from the register results");
@@ -2786,8 +2818,8 @@ ${q.query}`.replace("\n\n\n\n", "\n\n") }];
2786
2818
  text: h.text.slice(0, 1200),
2787
2819
  ...h.metadata.table_selection ? { sel: h.metadata.table_selection } : {}
2788
2820
  }));
2789
- const tq = await chargeAnswerTokens(env, bucketId, void 0, messageChars(messages), answer.length);
2790
- return json({ ...out, context: contextOut, read: readAs(), quota: { ...quota, ...tq } }, 200, { ...corsHeaders(req), "server-timing": serverTiming() });
2821
+ const tq = await chargeAnswerTokens(env, bucketId, void 0, messageChars(messages), answer.length, probeTokensUsed);
2822
+ return json({ ...out, context: contextOut, read: readAs(), quota: { ...quota, ...tq, credits: await creditBudget(env, bucketId), credit_estimate: creditPreview } }, 200, { ...corsHeaders(req), "server-timing": serverTiming() });
2791
2823
  }
2792
2824
  function sseResponse(events, cors) {
2793
2825
  const encoder = new TextEncoder();
@@ -6,7 +6,7 @@ import {
6
6
  sessionFrom,
7
7
  telemetry,
8
8
  understandQuery
9
- } from "./chunk-2JUWER2A.js";
9
+ } from "./chunk-7AXXR7GD.js";
10
10
  import {
11
11
  portModelRunner
12
12
  } from "./chunk-TH5XGPD5.js";
@@ -16,7 +16,7 @@ import {
16
16
  json,
17
17
  readJson,
18
18
  validateQuery
19
- } from "./chunk-DJ65EF5G.js";
19
+ } from "./chunk-W7J5SFXJ.js";
20
20
  import {
21
21
  entitlementScope,
22
22
  standardKeysFrom
@@ -123,7 +123,7 @@ async function authenticate(env, req) {
123
123
  if (!m) return null;
124
124
  const keyHash = await sha256Hex(m[1].trim());
125
125
  const row = await env.DB.prepare(
126
- "SELECT id, name, day_limit FROM api_keys WHERE key_hash = ?1 AND revoked = 0"
126
+ "SELECT id, name, day_limit, owner_sub FROM api_keys WHERE key_hash = ?1 AND revoked = 0"
127
127
  ).bind(keyHash).first();
128
128
  return row ?? null;
129
129
  }
package/dist/quota.d.ts CHANGED
@@ -23,3 +23,17 @@ export declare function tokenBudget(env: Env, bucketId: string): Promise<{
23
23
  }>;
24
24
  export declare function chargeTokens(env: Env, bucketId: string, tokens: number): Promise<void>;
25
25
  export declare function usageTotal(usage: unknown): number | null;
26
+ export declare const CREDIT_TOKEN_UNIT = 1000;
27
+ export declare function creditLimit(env: Env, bucketId: string): number;
28
+ export declare function creditBudget(env: Env, bucketId: string): Promise<{
29
+ credits_used: number;
30
+ credits_limit: number;
31
+ }>;
32
+ /** The credit-price preview for an ask: effort, tool turns armed, and
33
+ * multimodality each add real consumption — the estimate shown BEFORE
34
+ * generation so users see what a mode costs. Honest, not exact. */
35
+ export declare function creditEstimate(opts: {
36
+ effort: string;
37
+ toolTurns: number;
38
+ image: boolean;
39
+ }): number;
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  handleSearch
3
- } from "./chunk-P6IRYZLL.js";
4
- import "./chunk-2JUWER2A.js";
3
+ } from "./chunk-MWOY24B6.js";
4
+ import "./chunk-7AXXR7GD.js";
5
5
  import "./chunk-TH5XGPD5.js";
6
- import "./chunk-DJ65EF5G.js";
6
+ import "./chunk-W7J5SFXJ.js";
7
7
  import "./chunk-A4A26BCV.js";
8
8
  import "./chunk-4NXH5ZI6.js";
9
9
  import "./chunk-VT7DR6NQ.js";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  authenticate
3
- } from "../../chunk-DJ65EF5G.js";
3
+ } from "../../chunk-W7J5SFXJ.js";
4
4
  import "../../chunk-VT7DR6NQ.js";
5
5
  import {
6
6
  P,
@@ -4,7 +4,7 @@ import {
4
4
  handleMemories,
5
5
  scoreFaithfulness,
6
6
  scoreJudge
7
- } from "../../chunk-ZO6MMKGD.js";
7
+ } from "../../chunk-AIL7MJS3.js";
8
8
  import {
9
9
  TOOLS_REGISTRY,
10
10
  bindModelNode,
@@ -12,7 +12,7 @@ import {
12
12
  } from "../../chunk-J6LBR6GI.js";
13
13
  import {
14
14
  handleSearch
15
- } from "../../chunk-P6IRYZLL.js";
15
+ } from "../../chunk-MWOY24B6.js";
16
16
  import {
17
17
  buildMessages,
18
18
  citations,
@@ -34,7 +34,7 @@ import {
34
34
  sessionFrom,
35
35
  telemetry,
36
36
  understandQuery
37
- } from "../../chunk-2JUWER2A.js";
37
+ } from "../../chunk-7AXXR7GD.js";
38
38
  import {
39
39
  cfBlobs,
40
40
  embed,
@@ -49,7 +49,7 @@ import {
49
49
  readJson,
50
50
  validateQuery,
51
51
  withCors
52
- } from "../../chunk-DJ65EF5G.js";
52
+ } from "../../chunk-W7J5SFXJ.js";
53
53
  import {
54
54
  canonicalRefusal
55
55
  } from "../../chunk-A4A26BCV.js";
@@ -710,20 +710,21 @@ async function handleCreateKey(env, req) {
710
710
  const body = await readJson(req);
711
711
  if (!body?.name || typeof body.name !== "string") return err(400, "invalid_input", "name is required");
712
712
  const dayLimit = Number.isFinite(Number(body.day_limit)) && Number(body.day_limit) > 0 ? Number(body.day_limit) : num(env, "KEY_DAY_ASK_DEFAULT", 2e3);
713
+ const memberSub = typeof body?.member_sub === "string" && body.member_sub.trim() ? body.member_sub.trim().slice(0, 64) : null;
713
714
  const raw = `${P().publisher.id}_${[...crypto.getRandomValues(new Uint8Array(24))].map((b) => b.toString(16).padStart(2, "0")).join("")}`;
714
715
  const id = crypto.randomUUID();
715
716
  const keyHash = await sha256Hex(raw);
716
717
  await env.DB.prepare(
717
- "INSERT INTO api_keys (id, name, key_hash, day_limit, created_at, revoked) VALUES (?1,?2,?3,?4,?5,0)"
718
- ).bind(id, body.name, keyHash, dayLimit, (/* @__PURE__ */ new Date()).toISOString()).run();
719
- return json({ id, name: body.name, day_limit: dayLimit, key: raw, note: "Store this key now \u2014 it is not retrievable again." });
718
+ "INSERT INTO api_keys (id, name, key_hash, day_limit, owner_sub, created_at, revoked) VALUES (?1,?2,?3,?4,?5,?6,0)"
719
+ ).bind(id, body.name, keyHash, dayLimit, memberSub, (/* @__PURE__ */ new Date()).toISOString()).run();
720
+ return json({ id, name: body.name, day_limit: dayLimit, ...memberSub ? { member_sub: memberSub } : {}, key: raw, note: "Store this key now \u2014 it is not retrievable again." + (memberSub ? " This key draws from its member's shared daily credit cap." : "") });
720
721
  }
721
722
  async function handleListKeys(env, req) {
722
723
  if (!env.ADMIN_TOKEN) return err(501, "admin_disabled", "ADMIN_TOKEN secret is not configured");
723
724
  const auth = req.headers.get("authorization") ?? "";
724
725
  if (auth !== `Bearer ${env.ADMIN_TOKEN}`) return err(401, "unauthorized", "Invalid admin token");
725
726
  const rows = await env.DB.prepare(
726
- "SELECT id, name, day_limit, created_at, revoked FROM api_keys ORDER BY created_at DESC"
727
+ "SELECT id, name, day_limit, owner_sub, created_at, revoked FROM api_keys ORDER BY created_at DESC"
727
728
  ).all();
728
729
  return json({ keys: rows.results, ...corsHeaders(req) });
729
730
  }
@@ -1049,7 +1050,7 @@ async function callToolOnce(env, ctx, _req, tier, key, name, args, t0) {
1049
1050
  const r = await runTool({ ...env, DB: env.DB }, { name, args }, "mcp");
1050
1051
  payload = r ? { tool: r.name, query: r.query, result: r.output } : { error: { message: `tool ${name} returned nothing for the given arguments` } };
1051
1052
  } else {
1052
- const res = name === "ask" ? await (await import("../../ask-XAI5SVJ7.js")).handleAsk(env, ctx, inner, tier, key) : await (await import("../../search-OVUHVV35.js")).handleSearch(env, ctx, inner, tier, key);
1053
+ const res = name === "ask" ? await (await import("../../ask-5D2TYRXH.js")).handleAsk(env, ctx, inner, tier, key) : await (await import("../../search-6LP755Q7.js")).handleSearch(env, ctx, inner, tier, key);
1053
1054
  payload = await res.json().catch(() => ({ error: { message: "tool transport failed", status: res.status } }));
1054
1055
  }
1055
1056
  telemetry(env, ctx, tier, `mcp:${name}`, null, !payload?.error, JSON.stringify(payload).length, await sha256Hex(`${name}:${JSON.stringify(args ?? {})}`), void 0, void 0, { durationMs: Date.now() - t0 });
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.124",
30
+ "version": "0.2.130",
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,6 @@
1
+ -- API keys draw from their OWNER MEMBER's credit cap (the owner's
2
+ -- 2026-10-12 direction): a member-issued key is not a separate budget
3
+ -- tier — key traffic and the member's web traffic share ONE daily
4
+ -- allowance (sub:<owner> bucket). Keys without an owner (service
5
+ -- keys) keep the per-key cap.
6
+ ALTER TABLE api_keys ADD COLUMN owner_sub TEXT;
@@ -3,6 +3,7 @@ CREATE TABLE IF NOT EXISTS api_keys (
3
3
  name TEXT NOT NULL,
4
4
  key_hash TEXT NOT NULL UNIQUE,
5
5
  day_limit INTEGER NOT NULL DEFAULT 2000,
6
+ owner_sub TEXT,
6
7
  created_at TEXT NOT NULL,
7
8
  revoked INTEGER NOT NULL DEFAULT 0
8
9
  );
@@ -440,15 +440,18 @@ export async function handleCreateKey(env: Env, req: Request): Promise<Response>
440
440
  const body = await readJson(req);
441
441
  if (!body?.name || typeof body.name !== "string") return err(400, "invalid_input", "name is required");
442
442
  const dayLimit = Number.isFinite(Number(body.day_limit)) && Number(body.day_limit) > 0 ? Number(body.day_limit) : num(env as any, "KEY_DAY_ASK_DEFAULT", 2000);
443
+ // the owner member (2026-10-12): a key minted for a member draws from
444
+ // HER credit cap — one shared daily allowance across key and web
445
+ const memberSub = typeof body?.member_sub === "string" && body.member_sub.trim() ? body.member_sub.trim().slice(0, 64) : null;
443
446
  const raw = `${P().publisher.id}_${[...crypto.getRandomValues(new Uint8Array(24))].map((b) => b.toString(16).padStart(2, "0")).join("")}`;
444
447
  const id = crypto.randomUUID();
445
448
  const keyHash = await sha256Hex(raw);
446
449
  await env.DB.prepare(
447
- "INSERT INTO api_keys (id, name, key_hash, day_limit, created_at, revoked) VALUES (?1,?2,?3,?4,?5,0)",
450
+ "INSERT INTO api_keys (id, name, key_hash, day_limit, owner_sub, created_at, revoked) VALUES (?1,?2,?3,?4,?5,?6,0)",
448
451
  )
449
- .bind(id, body.name, keyHash, dayLimit, new Date().toISOString())
452
+ .bind(id, body.name, keyHash, dayLimit, memberSub, new Date().toISOString())
450
453
  .run();
451
- return json({ id, name: body.name, day_limit: dayLimit, key: raw, note: "Store this key now — it is not retrievable again." });
454
+ return json({ id, name: body.name, day_limit: dayLimit, ...(memberSub ? { member_sub: memberSub } : {}), key: raw, note: "Store this key now — it is not retrievable again." + (memberSub ? " This key draws from its member's shared daily credit cap." : "") });
452
455
  }
453
456
 
454
457
  export async function handleListKeys(env: Env, req: Request): Promise<Response> {
@@ -456,7 +459,7 @@ export async function handleListKeys(env: Env, req: Request): Promise<Response>
456
459
  const auth = req.headers.get("authorization") ?? "";
457
460
  if (auth !== `Bearer ${env.ADMIN_TOKEN}`) return err(401, "unauthorized", "Invalid admin token");
458
461
  const rows = await env.DB.prepare(
459
- "SELECT id, name, day_limit, created_at, revoked FROM api_keys ORDER BY created_at DESC",
462
+ "SELECT id, name, day_limit, owner_sub, created_at, revoked FROM api_keys ORDER BY created_at DESC",
460
463
  ).all();
461
464
  return json({ keys: rows.results, ...corsHeaders(req) });
462
465
  }
@@ -42,7 +42,7 @@ import speculativeVerifyPrompt from "../prompts/speculative-verify.md";
42
42
  import type { Env } from "./env";
43
43
  export type { Env };
44
44
  import { json, err, corsHeaders, readJson, validateQuery, type ApiKey } from "./lib/http";
45
- import { chargeTokens, checkQuota, clientIp, estimateTokens, telemetry, tokenBudget, usageTotal } from "./quota";
45
+ import { chargeTokens, checkQuota, clientIp, creditBudget, creditEstimate, estimateTokens, telemetry, tokenBudget, usageTotal } from "./quota";
46
46
  import { graphExpand, editionNote } from "./graph";
47
47
  import { registerNote, searchRegister, certificateLinks, registerAggregateIntent, registerAggregate, aggregateNote, aggregateBlock, queryFamily } from "./certificates";
48
48
  import { TOOL_DECLARATION, parseToolCall, runTool, toolNote } from "./tools";
@@ -276,8 +276,8 @@ function messageChars(messages: any[]): number {
276
276
  /** Post-generation true-up: the model's own usage when the stream
277
277
  * reported it, the character estimate otherwise — charged, then the
278
278
  * budget's fresh state returned for the quota payload. */
279
- async function chargeAnswerTokens(env: Env, bucketId: string, stats: { usage?: unknown } | undefined, promptChars: number, answerChars: number): Promise<{ tokens_used: number; token_limit: number }> {
280
- const total = usageTotal(stats?.usage) ?? estimateTokens(promptChars + answerChars);
279
+ async function chargeAnswerTokens(env: Env, bucketId: string, stats: { usage?: unknown } | undefined, promptChars: number, answerChars: number, extraTokens = 0): Promise<{ tokens_used: number; token_limit: number }> {
280
+ const total = (usageTotal(stats?.usage) ?? estimateTokens(promptChars + answerChars)) + extraTokens;
281
281
  await chargeTokens(env, bucketId, total);
282
282
  const budget = await tokenBudget(env, bucketId);
283
283
  return { tokens_used: budget.used, token_limit: budget.limit };
@@ -409,18 +409,31 @@ async function handleAsk(
409
409
  const routeEffort = (route: "fast" | "deep") =>
410
410
  route === "fast" && !declaredEffort ? ("low" as const) : effort;
411
411
 
412
+ // the owner-member law (2026-10-12): a member-owned API key draws
413
+ // from HER credit cap — key traffic and web traffic share ONE daily
414
+ // bucket (sub:<owner>), so a key can never multiply an allowance.
415
+ // Ownerless service keys keep the per-key bucket.
416
+ const memberKey = tier === "key" ? key?.owner_sub ?? null : null;
412
417
  const limit =
413
- tier === "key" ? key!.day_limit : tier === "member" || member ? num(env as any, "MEMBER_DAY_ASK", 300) : num(env as any, "ANON_DAY_ASK", 20);
414
- const bucketId = tier === "key" ? `key:${key!.id}` : member ? `sub:${member.sub}` : clientIp(req);
418
+ memberKey ? num(env as any, "MEMBER_DAY_ASK", 300)
419
+ : tier === "key" ? key!.day_limit
420
+ : tier === "member" || member ? num(env as any, "MEMBER_DAY_ASK", 300)
421
+ : num(env as any, "ANON_DAY_ASK", 20);
422
+ const bucketId = memberKey ? `sub:${memberKey}` : tier === "key" ? `key:${key!.id}` : member ? `sub:${member.sub}` : clientIp(req);
415
423
  const quota = await checkQuota(env, "ask", bucketId, limit, effort === "low" ? 1 : 2);
424
+ const creditPreview = creditEstimate({ effort, toolTurns: body?.multi_turn !== false ? 3 : 0, image: false });
416
425
  if (!quota.ok) {
417
426
  return err(429, "quota_exceeded", `Daily question limit reached (${quota.limit}). Try again tomorrow.`);
418
427
  }
419
428
  // the real allowance is the day's TOKENS (the 2026-09-29 shift): the
420
429
  // request cap above is the anti-abuse backstop, this is the budget
421
430
  const tokBudget = await tokenBudget(env, bucketId);
431
+ // the credit preview (2026-10-11): what this ask will cost, shown in
432
+ // the refusal when the day's credits are spent — effort + armed tool
433
+ // turns + multimodality, one honest number
422
434
  if (tokBudget.used >= tokBudget.limit) {
423
- return err(429, "quota_exceeded", `Daily token budget reached (${tokBudget.limit} tokens). Try again tomorrow.`);
435
+ const cb = await creditBudget(env, bucketId);
436
+ return err(429, "quota_exceeded", `Daily credits reached (${cb.credits_limit} credits, ${cb.credits_used} used). Try again tomorrow.`);
424
437
  }
425
438
  (quota as any).tokens_used = tokBudget.used;
426
439
  (quota as any).token_limit = tokBudget.limit;
@@ -533,6 +546,10 @@ async function handleAsk(
533
546
  // / image, not the query text alone. The fresh parse is answercache's
534
547
  // single freshRequested, honored at every answer-cache read below.
535
548
  const fresh = freshRequested(body);
549
+ // multi-turn agent (2026-10-11): the tool loop is ON by default — the
550
+ // model may chain tools; a caller opts OUT for the cheap single-pass
551
+ // (and the credit estimate prices the difference)
552
+ const multiTurn = body?.multi_turn !== false;
536
553
  // the corpus-generation stamp (KV sys:corpus_gen) namespaces both
537
554
  // answer caches; corpus surgery bumps it (scripts/invalidate_answer_
538
555
  // cache.py) and old-generation entries miss (oimlsmart/rag#72)
@@ -770,9 +787,38 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
770
787
  if (fam) {
771
788
  const agg = await registerAggregate(env.DB, fam);
772
789
  if (agg) {
790
+ // DETERMINISTIC ANSWER (the verdict engine's law applied to the
791
+ // register): a count/latest question is computed data — the
792
+ // model never enters the loop, so its scope training can never
793
+ // refuse a computed fact. The prose mirrors the note; the block
794
+ // carries the same numbers for the UI.
795
+ const latestLine = agg.latest
796
+ ? `The latest certificate in the snapshot is ${agg.latest.num} — holder ${agg.latest.holder}, model "${agg.latest.model}"${agg.latest.year ? `, issued ${agg.latest.year}` : ""}, status ${agg.latest.status}.`
797
+ : "No certificate rows are present in the snapshot.";
798
+ const linkLine = agg.latest?.pdf_key
799
+ ? `\n\n[The latest certificate's document](${`/cert-pdf/${agg.latest.pdf_key}`}) is on file.`
800
+ : "";
801
+ const answer = aggIntent === "count"
802
+ ? `The ${P().publisher.name} ${agg.family} family holds **${agg.count} certificate${agg.count === 1 ? "" : "s"}** in the ${P().publisher.name}-CS register snapshot (${agg.valid} valid${agg.suspended ? `, ${agg.suspended} suspended` : ""}).\n\nCertificates are issued per recommendation family — the register carries no edition dimension, so the 2021 you named does not narrow the count. The register is a snapshot rather than live certification status.`
803
+ : `${latestLine}${linkLine}\n\nCertificates are issued per recommendation family — the register carries no edition dimension. The register is a snapshot rather than live certification status.`;
804
+ const block = aggregateBlock(agg);
805
+ registerBlock = block;
773
806
  aggregateNoteText = aggregateNote(agg);
774
- registerBlock = aggregateBlock(agg);
775
- console.log("register aggregate:", agg.family, "count", agg.count, "latest", agg.latest?.num ?? "none");
807
+ const queryHash = await sha256Hex(q.query);
808
+ telemetry(env, ctx, tier, "ask", MODELS.member, true, answer.length, queryHash, q.lang, undefined, telemetryMeta());
809
+ const tq = await chargeAnswerTokens(env, bucketId, undefined, q.query.length, answer.length);
810
+ return json({
811
+ answer,
812
+ citations: [{ docidentifier: `${P().publisher.name}-CS certificate register (family ${agg.family})`, corpus: "register" }],
813
+ model: MODELS.member,
814
+ query_hash: queryHash,
815
+ follow_ups: [],
816
+ blocks: [block],
817
+ context_applied: NO_CONTEXT,
818
+ read: readAs(),
819
+ ...(ablate ? { ablate } : {}),
820
+ quota: { ...quota, ...tq, credits: await creditBudget(env, bucketId), credit_estimate: creditPreview },
821
+ });
776
822
  }
777
823
  }
778
824
  }
@@ -814,8 +860,8 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
814
860
  } catch {
815
861
  // stream ended prematurely — deliver what we have
816
862
  }
817
- const tq = await chargeAnswerTokens(env, bucketId, stats, messageChars(messages), full.length);
818
- send({ type: "quota", quota: { ...quota, ...tq } });
863
+ const tq = await chargeAnswerTokens(env, bucketId, stats, messageChars(messages), full.length, probeTokensUsed);
864
+ send({ type: "quota", quota: { ...quota, ...tq, credits: await creditBudget(env, bucketId), credit_estimate: creditPreview } });
819
865
  send({ type: "done", model, query_hash: queryHash, context_applied: NO_CONTEXT, read: readAs() });
820
866
  telemetry(env, ctx, tier, "ask", model, true, full.length, queryHash, q.lang, undefined, telemetryMeta());
821
867
  controller.close();
@@ -888,7 +934,7 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
888
934
  );
889
935
  }
890
936
  const tq = await chargeAnswerTokens(env, bucketId, undefined, q.query.length, verdict.answer.length);
891
- return json({ answer: verdict.answer, citations, model, query_hash: queryHash, follow_ups: [], context_applied: draftCtxApplied, read: readAs(), ...(draftPayload ? { draft: draftPayload } : {}), ...(ablate ? { ablate } : {}), quota: { ...quota, ...tq } });
937
+ return json({ answer: verdict.answer, citations, model, query_hash: queryHash, follow_ups: [], context_applied: draftCtxApplied, read: readAs(), ...(draftPayload ? { draft: draftPayload } : {}), ...(ablate ? { ablate } : {}), quota: { ...quota, ...tq, credits: await creditBudget(env, bucketId), credit_estimate: creditPreview } });
892
938
  }
893
939
 
894
940
  // ── The api_call draft (TODO.ai-platform/09) — the operations
@@ -926,7 +972,7 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
926
972
  );
927
973
  }
928
974
  const tq = await chargeAnswerTokens(env, bucketId, undefined, q.query.length, verdict.answer.length);
929
- return json({ answer: verdict.answer, citations: [], model, query_hash: queryHash, follow_ups: [], context_applied: callCtxApplied, read: readAs(), ...(draftPayload ? { draft: draftPayload } : {}), ...(ablate ? { ablate } : {}), quota: { ...quota, ...tq } });
975
+ return json({ answer: verdict.answer, citations: [], model, query_hash: queryHash, follow_ups: [], context_applied: callCtxApplied, read: readAs(), ...(draftPayload ? { draft: draftPayload } : {}), ...(ablate ? { ablate } : {}), quota: { ...quota, ...tq, credits: await creditBudget(env, bucketId), credit_estimate: creditPreview } });
930
976
  }
931
977
 
932
978
  // (declared before the retrieval try: the account block, the refusal
@@ -1363,12 +1409,15 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
1363
1409
  // invocations record into pendingTools; the stream emits them as its
1364
1410
  // first events (the UI shows what the agent consulted).
1365
1411
  const pendingTools: { name: string; query: string; output: string }[] = [];
1412
+ // the agent's own consumption (2026-10-11): every tool probe is a real
1413
+ // model call — its usage joins the charge so credits price the true cost
1414
+ let probeTokensUsed = 0;
1366
1415
  // On an IMAGE turn the probe CARRIES THE IMAGE (2026-10-03, the owner's
1367
1416
  // architecture): the model that reads the plate is the model that issues
1368
1417
  // the tool call — one reader, one tool, no parallel extraction racing a
1369
1418
  // second vision pass that drifts hour to hour. The old probe dropped the
1370
1419
  // image-bearing message, which is why the private bridge had to exist.
1371
- if (/certificate|certified|certification/i.test(q.query) && env.DB) {
1420
+ if (multiTurn && /certificate|certified|certification/i.test(q.query) && env.DB) {
1372
1421
  try {
1373
1422
  const imageTurn = typeof withImage === "string";
1374
1423
  if (imageTurn) mark("Reading the photograph to identify the instrument");
@@ -1380,6 +1429,7 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
1380
1429
  reasoning_effort: imageTurn ? "high" : "low",
1381
1430
  });
1382
1431
  const probeText = typeof probe?.response === "string" ? probe.response : probe?.choices?.[0]?.message?.content ?? "";
1432
+ probeTokensUsed += usageTotal((probe as any)?.usage) ?? 0;
1383
1433
  const call = parseToolCall(probeText);
1384
1434
  if (call) {
1385
1435
  mark(`Searching the certificate register for “${String(call.args?.query ?? "")}”`);
@@ -1457,8 +1507,8 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
1457
1507
  } catch {
1458
1508
  // stream ended prematurely — deliver what we have
1459
1509
  }
1460
- const tq = await chargeAnswerTokens(env, bucketId, stats, messageChars(messages), full.length);
1461
- send({ type: "quota", quota: { ...quota, ...tq } });
1510
+ const tq = await chargeAnswerTokens(env, bucketId, stats, messageChars(messages), full.length, probeTokensUsed);
1511
+ send({ type: "quota", quota: { ...quota, ...tq, credits: await creditBudget(env, bucketId), credit_estimate: creditPreview } });
1462
1512
  // the blind-answer guard ON THE STREAM PATH (the JSON path has
1463
1513
  // had it since 0.2.80): an image rode the turn, the register
1464
1514
  // note carries rows, and the streamed text is the no-information
@@ -1772,8 +1822,8 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
1772
1822
  text: h.text.slice(0, 1200),
1773
1823
  ...((h.metadata as any).table_selection ? { sel: (h.metadata as any).table_selection } : {}),
1774
1824
  }));
1775
- const tq = await chargeAnswerTokens(env, bucketId, undefined, messageChars(messages), answer.length);
1776
- return json({ ...out, context: contextOut, read: readAs(), quota: { ...quota, ...tq } }, 200, { ...corsHeaders(req), "server-timing": serverTiming() });
1825
+ const tq = await chargeAnswerTokens(env, bucketId, undefined, messageChars(messages), answer.length, probeTokensUsed);
1826
+ return json({ ...out, context: contextOut, read: readAs(), quota: { ...quota, ...tq, credits: await creditBudget(env, bucketId), credit_estimate: creditPreview } }, 200, { ...corsHeaders(req), "server-timing": serverTiming() });
1777
1827
  }
1778
1828
 
1779
1829
  function sseResponse(events: unknown[], cors: Record<string, string>): Response {
@@ -46,6 +46,11 @@ export interface ApiKey {
46
46
  id: string;
47
47
  name: string;
48
48
  day_limit: number;
49
+ /** the member whose credit cap this key draws from (the owner's
50
+ * 2026-10-12 law): a member-owned key shares the member's daily
51
+ * allowance — key and web traffic are ONE budget. Null = a service
52
+ * key with its own per-key cap. */
53
+ owner_sub?: string | null;
49
54
  }
50
55
 
51
56
  export async function authenticate(env: Pick<Env, "DB">, req: Request): Promise<ApiKey | null> {
@@ -54,7 +59,7 @@ export async function authenticate(env: Pick<Env, "DB">, req: Request): Promise<
54
59
  if (!m) return null;
55
60
  const keyHash = await sha256Hex(m[1].trim());
56
61
  const row = await env.DB.prepare(
57
- "SELECT id, name, day_limit FROM api_keys WHERE key_hash = ?1 AND revoked = 0",
62
+ "SELECT id, name, day_limit, owner_sub FROM api_keys WHERE key_hash = ?1 AND revoked = 0",
58
63
  )
59
64
  .bind(keyHash)
60
65
  .first<ApiKey>();
@@ -95,3 +95,29 @@ export function usageTotal(usage: unknown): number | null {
95
95
  const total = p + c;
96
96
  return Number.isFinite(total) && total > 0 ? total : null;
97
97
  }
98
+
99
+ // ── credits (2026-10-11, the owner's direction): the user-facing quota
100
+ // unit. 1 credit = CREDIT_TOKEN_UNIT tokens of actual consumption
101
+ // (default 1000 — a simple ask is ~1 credit, an elevated-effort answer
102
+ // a few, a multi-turn agent ask with tool probes what it truly cost).
103
+ // The KV token counters stay the source of truth; credits are their
104
+ // honest denomination. Every quota payload carries them so the UI shows
105
+ // one number users can reason about.
106
+ export const CREDIT_TOKEN_UNIT = 1000;
107
+
108
+ export function creditLimit(env: Env, bucketId: string): number {
109
+ return Math.floor(tokenLimit(env, bucketId) / CREDIT_TOKEN_UNIT);
110
+ }
111
+
112
+ export async function creditBudget(env: Env, bucketId: string): Promise<{ credits_used: number; credits_limit: number }> {
113
+ const b = await tokenBudget(env, bucketId);
114
+ return { credits_used: Math.ceil(b.used / CREDIT_TOKEN_UNIT), credits_limit: creditLimit(env, bucketId) };
115
+ }
116
+
117
+ /** The credit-price preview for an ask: effort, tool turns armed, and
118
+ * multimodality each add real consumption — the estimate shown BEFORE
119
+ * generation so users see what a mode costs. Honest, not exact. */
120
+ export function creditEstimate(opts: { effort: string; toolTurns: number; image: boolean }): number {
121
+ const effortCredits = opts.effort === "low" ? 1 : opts.effort === "medium" ? 2 : 3;
122
+ return effortCredits + Math.max(0, opts.toolTurns) + (opts.image ? 1 : 0);
123
+ }