@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.
- package/dist/{ask-XAI5SVJ7.js → ask-5D2TYRXH.js} +3 -3
- package/dist/{chunk-2JUWER2A.js → chunk-7AXXR7GD.js} +15 -1
- package/dist/{chunk-ZO6MMKGD.js → chunk-AIL7MJS3.js} +50 -18
- package/dist/{chunk-P6IRYZLL.js → chunk-MWOY24B6.js} +2 -2
- package/dist/{chunk-DJ65EF5G.js → chunk-W7J5SFXJ.js} +1 -1
- package/dist/quota.d.ts +14 -0
- package/dist/{search-OVUHVV35.js → search-6LP755Q7.js} +3 -3
- package/dist/worker_mcp/src/index.js +1 -1
- package/dist/worker_public/src/index.js +10 -9
- package/package.json +1 -1
- package/workers/worker_public/migrations/0019_api_keys_owner.sql +6 -0
- package/workers/worker_public/schema.sql +1 -0
- package/workers/worker_public/src/admin.ts +7 -4
- package/workers/worker_public/src/ask.ts +67 -17
- package/workers/worker_public/src/lib/http.ts +6 -1
- package/workers/worker_public/src/quota.ts +26 -0
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
handleAsk
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-AIL7MJS3.js";
|
|
4
4
|
import "./chunk-J6LBR6GI.js";
|
|
5
|
-
import "./chunk-
|
|
5
|
+
import "./chunk-7AXXR7GD.js";
|
|
6
6
|
import "./chunk-TH5XGPD5.js";
|
|
7
|
-
import "./chunk-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
|
1862
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
2087
|
-
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
4
|
-
import "./chunk-
|
|
3
|
+
} from "./chunk-MWOY24B6.js";
|
|
4
|
+
import "./chunk-7AXXR7GD.js";
|
|
5
5
|
import "./chunk-TH5XGPD5.js";
|
|
6
|
-
import "./chunk-
|
|
6
|
+
import "./chunk-W7J5SFXJ.js";
|
|
7
7
|
import "./chunk-A4A26BCV.js";
|
|
8
8
|
import "./chunk-4NXH5ZI6.js";
|
|
9
9
|
import "./chunk-VT7DR6NQ.js";
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
handleMemories,
|
|
5
5
|
scoreFaithfulness,
|
|
6
6
|
scoreJudge
|
|
7
|
-
} from "../../chunk-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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.
|
|
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;
|
|
@@ -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
|
-
|
|
414
|
-
|
|
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
|
-
|
|
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
|
-
|
|
775
|
-
|
|
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
|
+
}
|