@konneal/engine 0.2.126 → 0.2.131
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-RAQHKSPN.js → ask-5D2TYRXH.js} +3 -3
- package/dist/{chunk-2JUWER2A.js → chunk-7AXXR7GD.js} +15 -1
- package/dist/{chunk-ZHHORWM6.js → chunk-AIL7MJS3.js} +25 -17
- package/dist/{chunk-P6IRYZLL.js → chunk-MWOY24B6.js} +2 -2
- package/dist/{chunk-DJ65EF5G.js → chunk-W7J5SFXJ.js} +1 -1
- package/dist/openapi-surface.gen.d.ts +1 -1
- package/dist/openapi-types.d.ts +76 -0
- 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 +33 -9
- package/package.json +1 -1
- package/workers/worker_public/migrations/0019_api_keys_owner.sql +6 -0
- package/workers/worker_public/openapi.yaml +23 -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 +37 -16
- package/workers/worker_public/src/index.ts +16 -0
- package/workers/worker_public/src/lib/http.ts +6 -1
- package/workers/worker_public/src/openapi-surface.gen.ts +12 -0
- 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);
|
|
@@ -2107,7 +2113,7 @@ Certificates are issued per recommendation family \u2014 the register carries no
|
|
|
2107
2113
|
context_applied: NO_CONTEXT,
|
|
2108
2114
|
read: readAs(),
|
|
2109
2115
|
...ablate ? { ablate } : {},
|
|
2110
|
-
quota: { ...quota, ...tq2 }
|
|
2116
|
+
quota: { ...quota, ...tq2, credits: await creditBudget(env, bucketId), credit_estimate: creditPreview }
|
|
2111
2117
|
});
|
|
2112
2118
|
}
|
|
2113
2119
|
}
|
|
@@ -2143,8 +2149,8 @@ ${summary}` }] : [],
|
|
|
2143
2149
|
}
|
|
2144
2150
|
} catch {
|
|
2145
2151
|
}
|
|
2146
|
-
const tq2 = await chargeAnswerTokens(env, bucketId, stats, messageChars(messages2), full.length);
|
|
2147
|
-
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 } });
|
|
2148
2154
|
send({ type: "done", model, query_hash: queryHash2, context_applied: NO_CONTEXT, read: readAs() });
|
|
2149
2155
|
telemetry(env, ctx, tier, "ask", model, true, full.length, queryHash2, q.lang, void 0, telemetryMeta());
|
|
2150
2156
|
controller.close();
|
|
@@ -2201,7 +2207,7 @@ ${summary}` }] : [],
|
|
|
2201
2207
|
);
|
|
2202
2208
|
}
|
|
2203
2209
|
const tq2 = await chargeAnswerTokens(env, bucketId, void 0, q.query.length, verdict.answer.length);
|
|
2204
|
-
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 } });
|
|
2205
2211
|
}
|
|
2206
2212
|
if (apiCallIntent) {
|
|
2207
2213
|
const callCtxApplied = declaredCtx ? appliedContext(declaredCtx, null) : NO_CONTEXT;
|
|
@@ -2229,7 +2235,7 @@ ${summary}` }] : [],
|
|
|
2229
2235
|
);
|
|
2230
2236
|
}
|
|
2231
2237
|
const tq2 = await chargeAnswerTokens(env, bucketId, void 0, q.query.length, verdict.answer.length);
|
|
2232
|
-
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 } });
|
|
2233
2239
|
}
|
|
2234
2240
|
let liveRecords;
|
|
2235
2241
|
let accountNote;
|
|
@@ -2513,7 +2519,8 @@ Answer account questions from these records ONLY: name the record when you use i
|
|
|
2513
2519
|
);
|
|
2514
2520
|
await attachFigureImages(env, messages, usedHits, q.query);
|
|
2515
2521
|
const pendingTools = [];
|
|
2516
|
-
|
|
2522
|
+
let probeTokensUsed = 0;
|
|
2523
|
+
if (multiTurn && /certificate|certified|certification/i.test(q.query) && env.DB) {
|
|
2517
2524
|
try {
|
|
2518
2525
|
const imageTurn = typeof withImage === "string";
|
|
2519
2526
|
if (imageTurn) mark("Reading the photograph to identify the instrument");
|
|
@@ -2529,6 +2536,7 @@ ${q.query}`.replace("\n\n\n\n", "\n\n") }];
|
|
|
2529
2536
|
reasoning_effort: imageTurn ? "high" : "low"
|
|
2530
2537
|
});
|
|
2531
2538
|
const probeText = typeof probe?.response === "string" ? probe.response : probe?.choices?.[0]?.message?.content ?? "";
|
|
2539
|
+
probeTokensUsed += usageTotal(probe?.usage) ?? 0;
|
|
2532
2540
|
const call = parseToolCall(probeText);
|
|
2533
2541
|
if (call) {
|
|
2534
2542
|
mark(`Searching the certificate register for \u201C${String(call.args?.query ?? "")}\u201D`);
|
|
@@ -2590,8 +2598,8 @@ ${q.query}`.replace("\n\n\n\n", "\n\n") }];
|
|
|
2590
2598
|
}
|
|
2591
2599
|
} catch {
|
|
2592
2600
|
}
|
|
2593
|
-
const tq2 = await chargeAnswerTokens(env, bucketId, stats, messageChars(messages), full.length);
|
|
2594
|
-
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 } });
|
|
2595
2603
|
if (withImage && register?.rows?.length && full.includes(refusalAnswer())) {
|
|
2596
2604
|
console.log("blind-answer(stream): regenerating once");
|
|
2597
2605
|
mark("Re-answering from the register results");
|
|
@@ -2810,8 +2818,8 @@ ${q.query}`.replace("\n\n\n\n", "\n\n") }];
|
|
|
2810
2818
|
text: h.text.slice(0, 1200),
|
|
2811
2819
|
...h.metadata.table_selection ? { sel: h.metadata.table_selection } : {}
|
|
2812
2820
|
}));
|
|
2813
|
-
const tq = await chargeAnswerTokens(env, bucketId, void 0, messageChars(messages), answer.length);
|
|
2814
|
-
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() });
|
|
2815
2823
|
}
|
|
2816
2824
|
function sseResponse(events, cors) {
|
|
2817
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
|
}
|
|
@@ -3,7 +3,7 @@ export interface OpenApiRoute {
|
|
|
3
3
|
pattern: string;
|
|
4
4
|
operationId: string;
|
|
5
5
|
}
|
|
6
|
-
export type OpenApiOperationId = "askAnonymous" | "askKeyed" | "search" | "searchKeyed" | "verify" | "verifyKeyed" | "research" | "researchKeyed" | "mcp" | "datasets" | "keyUsage" | "health" | "listConversations" | "createConversation" | "getConversation" | "renameConversation" | "deleteConversation" | "appendMessage" | "shareConversation" | "getShared" | "uploadAttachment" | "listAttachments" | "getAttachment" | "deleteAttachment" | "listMemories" | "createMemory" | "deleteMemory" | "listProjects" | "createProject" | "deleteProject" | "listProjectFiles" | "attachProjectFile" | "detachProjectFile" | "laneQuery" | "laneKeyed" | "feedback" | "adminEnrich" | "adminEnrichAlias" | "adminSection" | "adminFamilySummary" | "adminSectionAlias" | "adminFamilySummaryAlias" | "adminVectors" | "adminCaption" | "adminJudge" | "adminJudgeAlias" | "adminRevokeKey" | "authMe" | "authLogin" | "authCallback" | "authLogout" | "authLogoutLink" | "adminStats" | "adminListKeys" | "adminCreateKey";
|
|
6
|
+
export type OpenApiOperationId = "credits" | "creditsAlias" | "askAnonymous" | "askKeyed" | "search" | "searchKeyed" | "verify" | "verifyKeyed" | "research" | "researchKeyed" | "mcp" | "datasets" | "keyUsage" | "health" | "listConversations" | "createConversation" | "getConversation" | "renameConversation" | "deleteConversation" | "appendMessage" | "shareConversation" | "getShared" | "uploadAttachment" | "listAttachments" | "getAttachment" | "deleteAttachment" | "listMemories" | "createMemory" | "deleteMemory" | "listProjects" | "createProject" | "deleteProject" | "listProjectFiles" | "attachProjectFile" | "detachProjectFile" | "laneQuery" | "laneKeyed" | "feedback" | "adminEnrich" | "adminEnrichAlias" | "adminSection" | "adminFamilySummary" | "adminSectionAlias" | "adminFamilySummaryAlias" | "adminVectors" | "adminCaption" | "adminJudge" | "adminJudgeAlias" | "adminRevokeKey" | "authMe" | "authLogin" | "authCallback" | "authLogout" | "authLogoutLink" | "adminStats" | "adminListKeys" | "adminCreateKey";
|
|
7
7
|
export declare const OPENAPI_SURFACE: readonly (Omit<OpenApiRoute, "operationId"> & {
|
|
8
8
|
operationId: OpenApiOperationId;
|
|
9
9
|
})[];
|
package/dist/openapi-types.d.ts
CHANGED
|
@@ -6,6 +6,46 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
export interface paths {
|
|
9
|
+
"/api/credits": {
|
|
10
|
+
parameters: {
|
|
11
|
+
query?: never;
|
|
12
|
+
header?: never;
|
|
13
|
+
path?: never;
|
|
14
|
+
cookie?: never;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* The caller's daily credit balance
|
|
18
|
+
* @description Returns the credits the caller has left today — resolved by the same tiering as ask (anonymous by IP, member by session, key by its owner member's shared cap). 1 credit ≈ 1000 tokens of consumption; the UI's meter reads this.
|
|
19
|
+
*/
|
|
20
|
+
get: operations["credits"];
|
|
21
|
+
put?: never;
|
|
22
|
+
post?: never;
|
|
23
|
+
delete?: never;
|
|
24
|
+
options?: never;
|
|
25
|
+
head?: never;
|
|
26
|
+
patch?: never;
|
|
27
|
+
trace?: never;
|
|
28
|
+
};
|
|
29
|
+
"/v1/credits": {
|
|
30
|
+
parameters: {
|
|
31
|
+
query?: never;
|
|
32
|
+
header?: never;
|
|
33
|
+
path?: never;
|
|
34
|
+
cookie?: never;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* The caller's daily credit balance (the authenticated path)
|
|
38
|
+
* @description The same balance, authenticated by session or key.
|
|
39
|
+
*/
|
|
40
|
+
get: operations["creditsAlias"];
|
|
41
|
+
put?: never;
|
|
42
|
+
post?: never;
|
|
43
|
+
delete?: never;
|
|
44
|
+
options?: never;
|
|
45
|
+
head?: never;
|
|
46
|
+
patch?: never;
|
|
47
|
+
trace?: never;
|
|
48
|
+
};
|
|
9
49
|
"/api/ask": {
|
|
10
50
|
parameters: {
|
|
11
51
|
query?: never;
|
|
@@ -998,6 +1038,42 @@ export interface components {
|
|
|
998
1038
|
}
|
|
999
1039
|
export type $defs = Record<string, never>;
|
|
1000
1040
|
export interface operations {
|
|
1041
|
+
credits: {
|
|
1042
|
+
parameters: {
|
|
1043
|
+
query?: never;
|
|
1044
|
+
header?: never;
|
|
1045
|
+
path?: never;
|
|
1046
|
+
cookie?: never;
|
|
1047
|
+
};
|
|
1048
|
+
requestBody?: never;
|
|
1049
|
+
responses: {
|
|
1050
|
+
/** @description The balance. */
|
|
1051
|
+
200: {
|
|
1052
|
+
headers: {
|
|
1053
|
+
[name: string]: unknown;
|
|
1054
|
+
};
|
|
1055
|
+
content?: never;
|
|
1056
|
+
};
|
|
1057
|
+
};
|
|
1058
|
+
};
|
|
1059
|
+
creditsAlias: {
|
|
1060
|
+
parameters: {
|
|
1061
|
+
query?: never;
|
|
1062
|
+
header?: never;
|
|
1063
|
+
path?: never;
|
|
1064
|
+
cookie?: never;
|
|
1065
|
+
};
|
|
1066
|
+
requestBody?: never;
|
|
1067
|
+
responses: {
|
|
1068
|
+
/** @description The balance. */
|
|
1069
|
+
200: {
|
|
1070
|
+
headers: {
|
|
1071
|
+
[name: string]: unknown;
|
|
1072
|
+
};
|
|
1073
|
+
content?: never;
|
|
1074
|
+
};
|
|
1075
|
+
};
|
|
1076
|
+
};
|
|
1001
1077
|
askAnonymous: {
|
|
1002
1078
|
parameters: {
|
|
1003
1079
|
query?: never;
|
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,10 +12,12 @@ 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,
|
|
19
|
+
clientIp,
|
|
20
|
+
creditBudget,
|
|
19
21
|
editionNote,
|
|
20
22
|
fill,
|
|
21
23
|
ftsMatchQuery,
|
|
@@ -34,7 +36,7 @@ import {
|
|
|
34
36
|
sessionFrom,
|
|
35
37
|
telemetry,
|
|
36
38
|
understandQuery
|
|
37
|
-
} from "../../chunk-
|
|
39
|
+
} from "../../chunk-7AXXR7GD.js";
|
|
38
40
|
import {
|
|
39
41
|
cfBlobs,
|
|
40
42
|
embed,
|
|
@@ -49,7 +51,7 @@ import {
|
|
|
49
51
|
readJson,
|
|
50
52
|
validateQuery,
|
|
51
53
|
withCors
|
|
52
|
-
} from "../../chunk-
|
|
54
|
+
} from "../../chunk-W7J5SFXJ.js";
|
|
53
55
|
import {
|
|
54
56
|
canonicalRefusal
|
|
55
57
|
} from "../../chunk-A4A26BCV.js";
|
|
@@ -710,20 +712,21 @@ async function handleCreateKey(env, req) {
|
|
|
710
712
|
const body = await readJson(req);
|
|
711
713
|
if (!body?.name || typeof body.name !== "string") return err(400, "invalid_input", "name is required");
|
|
712
714
|
const dayLimit = Number.isFinite(Number(body.day_limit)) && Number(body.day_limit) > 0 ? Number(body.day_limit) : num(env, "KEY_DAY_ASK_DEFAULT", 2e3);
|
|
715
|
+
const memberSub = typeof body?.member_sub === "string" && body.member_sub.trim() ? body.member_sub.trim().slice(0, 64) : null;
|
|
713
716
|
const raw = `${P().publisher.id}_${[...crypto.getRandomValues(new Uint8Array(24))].map((b) => b.toString(16).padStart(2, "0")).join("")}`;
|
|
714
717
|
const id = crypto.randomUUID();
|
|
715
718
|
const keyHash = await sha256Hex(raw);
|
|
716
719
|
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." });
|
|
720
|
+
"INSERT INTO api_keys (id, name, key_hash, day_limit, owner_sub, created_at, revoked) VALUES (?1,?2,?3,?4,?5,?6,0)"
|
|
721
|
+
).bind(id, body.name, keyHash, dayLimit, memberSub, (/* @__PURE__ */ new Date()).toISOString()).run();
|
|
722
|
+
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
723
|
}
|
|
721
724
|
async function handleListKeys(env, req) {
|
|
722
725
|
if (!env.ADMIN_TOKEN) return err(501, "admin_disabled", "ADMIN_TOKEN secret is not configured");
|
|
723
726
|
const auth = req.headers.get("authorization") ?? "";
|
|
724
727
|
if (auth !== `Bearer ${env.ADMIN_TOKEN}`) return err(401, "unauthorized", "Invalid admin token");
|
|
725
728
|
const rows = await env.DB.prepare(
|
|
726
|
-
"SELECT id, name, day_limit, created_at, revoked FROM api_keys ORDER BY created_at DESC"
|
|
729
|
+
"SELECT id, name, day_limit, owner_sub, created_at, revoked FROM api_keys ORDER BY created_at DESC"
|
|
727
730
|
).all();
|
|
728
731
|
return json({ keys: rows.results, ...corsHeaders(req) });
|
|
729
732
|
}
|
|
@@ -1049,7 +1052,7 @@ async function callToolOnce(env, ctx, _req, tier, key, name, args, t0) {
|
|
|
1049
1052
|
const r = await runTool({ ...env, DB: env.DB }, { name, args }, "mcp");
|
|
1050
1053
|
payload = r ? { tool: r.name, query: r.query, result: r.output } : { error: { message: `tool ${name} returned nothing for the given arguments` } };
|
|
1051
1054
|
} else {
|
|
1052
|
-
const res = name === "ask" ? await (await import("../../ask-
|
|
1055
|
+
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
1056
|
payload = await res.json().catch(() => ({ error: { message: "tool transport failed", status: res.status } }));
|
|
1054
1057
|
}
|
|
1055
1058
|
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 });
|
|
@@ -1086,6 +1089,16 @@ function matchRoute(routes, method, path) {
|
|
|
1086
1089
|
|
|
1087
1090
|
// workers/worker_public/src/openapi-surface.gen.ts
|
|
1088
1091
|
var OPENAPI_SURFACE = [
|
|
1092
|
+
{
|
|
1093
|
+
"method": "GET",
|
|
1094
|
+
"pattern": "/api/credits",
|
|
1095
|
+
"operationId": "credits"
|
|
1096
|
+
},
|
|
1097
|
+
{
|
|
1098
|
+
"method": "GET",
|
|
1099
|
+
"pattern": "/v1/credits",
|
|
1100
|
+
"operationId": "creditsAlias"
|
|
1101
|
+
},
|
|
1089
1102
|
{
|
|
1090
1103
|
"method": "POST",
|
|
1091
1104
|
"pattern": "/api/ask",
|
|
@@ -1495,6 +1508,15 @@ async function mcpRoute(c) {
|
|
|
1495
1508
|
if (t instanceof Response) return t;
|
|
1496
1509
|
return withCors(await handleMcp(c.env, c.ctx, c.req, t.tier, t.key), corsHeaders(c.req));
|
|
1497
1510
|
}
|
|
1511
|
+
async function creditsRoute(c) {
|
|
1512
|
+
const t = await tierFor(c);
|
|
1513
|
+
if (t instanceof Response) return t;
|
|
1514
|
+
const key = t.key;
|
|
1515
|
+
const memberKey = t.tier === "key" ? key?.owner_sub ?? null : null;
|
|
1516
|
+
const bucketId = memberKey ? `sub:${memberKey}` : t.tier === "key" ? `key:${key.id}` : clientIp(c.req);
|
|
1517
|
+
const cb = await creditBudget(c.env, bucketId);
|
|
1518
|
+
return withCors(json({ credits: cb }), corsHeaders(c.req));
|
|
1519
|
+
}
|
|
1498
1520
|
async function askRoute(c) {
|
|
1499
1521
|
const t = await tierFor(c);
|
|
1500
1522
|
if (t instanceof Response) return t;
|
|
@@ -1784,6 +1806,8 @@ var INFRA_ROUTES = [
|
|
|
1784
1806
|
];
|
|
1785
1807
|
var OPENAPI_HANDLERS = {
|
|
1786
1808
|
askAnonymous: askRoute,
|
|
1809
|
+
credits: creditsRoute,
|
|
1810
|
+
creditsAlias: creditsRoute,
|
|
1787
1811
|
askKeyed: askRoute,
|
|
1788
1812
|
search: searchRoute,
|
|
1789
1813
|
searchKeyed: searchRoute,
|
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.131",
|
|
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;
|
|
@@ -58,6 +58,29 @@ tags:
|
|
|
58
58
|
the index-maintenance battery (enrichment, section units, vectors
|
|
59
59
|
and figure captions).
|
|
60
60
|
paths:
|
|
61
|
+
/api/credits:
|
|
62
|
+
get:
|
|
63
|
+
tags: [Service]
|
|
64
|
+
summary: The caller's daily credit balance
|
|
65
|
+
description: >-
|
|
66
|
+
Returns the credits the caller has left today — resolved by the
|
|
67
|
+
same tiering as ask (anonymous by IP, member by session, key by
|
|
68
|
+
its owner member's shared cap). 1 credit ≈ 1000 tokens of
|
|
69
|
+
consumption; the UI's meter reads this.
|
|
70
|
+
operationId: credits
|
|
71
|
+
responses:
|
|
72
|
+
'200':
|
|
73
|
+
description: The balance.
|
|
74
|
+
/v1/credits:
|
|
75
|
+
get:
|
|
76
|
+
tags: [Service]
|
|
77
|
+
summary: The caller's daily credit balance (the authenticated path)
|
|
78
|
+
description: The same balance, authenticated by session or key.
|
|
79
|
+
operationId: creditsAlias
|
|
80
|
+
security: [{ bearerAuth: [] }]
|
|
81
|
+
responses:
|
|
82
|
+
'200':
|
|
83
|
+
description: The balance.
|
|
61
84
|
/api/ask:
|
|
62
85
|
post:
|
|
63
86
|
tags: [Ask]
|
|
@@ -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)
|
|
@@ -800,7 +817,7 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
800
817
|
context_applied: NO_CONTEXT,
|
|
801
818
|
read: readAs(),
|
|
802
819
|
...(ablate ? { ablate } : {}),
|
|
803
|
-
quota: { ...quota, ...tq },
|
|
820
|
+
quota: { ...quota, ...tq, credits: await creditBudget(env, bucketId), credit_estimate: creditPreview },
|
|
804
821
|
});
|
|
805
822
|
}
|
|
806
823
|
}
|
|
@@ -843,8 +860,8 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
843
860
|
} catch {
|
|
844
861
|
// stream ended prematurely — deliver what we have
|
|
845
862
|
}
|
|
846
|
-
const tq = await chargeAnswerTokens(env, bucketId, stats, messageChars(messages), full.length);
|
|
847
|
-
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 } });
|
|
848
865
|
send({ type: "done", model, query_hash: queryHash, context_applied: NO_CONTEXT, read: readAs() });
|
|
849
866
|
telemetry(env, ctx, tier, "ask", model, true, full.length, queryHash, q.lang, undefined, telemetryMeta());
|
|
850
867
|
controller.close();
|
|
@@ -917,7 +934,7 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
917
934
|
);
|
|
918
935
|
}
|
|
919
936
|
const tq = await chargeAnswerTokens(env, bucketId, undefined, q.query.length, verdict.answer.length);
|
|
920
|
-
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 } });
|
|
921
938
|
}
|
|
922
939
|
|
|
923
940
|
// ── The api_call draft (TODO.ai-platform/09) — the operations
|
|
@@ -955,7 +972,7 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
955
972
|
);
|
|
956
973
|
}
|
|
957
974
|
const tq = await chargeAnswerTokens(env, bucketId, undefined, q.query.length, verdict.answer.length);
|
|
958
|
-
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 } });
|
|
959
976
|
}
|
|
960
977
|
|
|
961
978
|
// (declared before the retrieval try: the account block, the refusal
|
|
@@ -1392,12 +1409,15 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
1392
1409
|
// invocations record into pendingTools; the stream emits them as its
|
|
1393
1410
|
// first events (the UI shows what the agent consulted).
|
|
1394
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;
|
|
1395
1415
|
// On an IMAGE turn the probe CARRIES THE IMAGE (2026-10-03, the owner's
|
|
1396
1416
|
// architecture): the model that reads the plate is the model that issues
|
|
1397
1417
|
// the tool call — one reader, one tool, no parallel extraction racing a
|
|
1398
1418
|
// second vision pass that drifts hour to hour. The old probe dropped the
|
|
1399
1419
|
// image-bearing message, which is why the private bridge had to exist.
|
|
1400
|
-
if (/certificate|certified|certification/i.test(q.query) && env.DB) {
|
|
1420
|
+
if (multiTurn && /certificate|certified|certification/i.test(q.query) && env.DB) {
|
|
1401
1421
|
try {
|
|
1402
1422
|
const imageTurn = typeof withImage === "string";
|
|
1403
1423
|
if (imageTurn) mark("Reading the photograph to identify the instrument");
|
|
@@ -1409,6 +1429,7 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
1409
1429
|
reasoning_effort: imageTurn ? "high" : "low",
|
|
1410
1430
|
});
|
|
1411
1431
|
const probeText = typeof probe?.response === "string" ? probe.response : probe?.choices?.[0]?.message?.content ?? "";
|
|
1432
|
+
probeTokensUsed += usageTotal((probe as any)?.usage) ?? 0;
|
|
1412
1433
|
const call = parseToolCall(probeText);
|
|
1413
1434
|
if (call) {
|
|
1414
1435
|
mark(`Searching the certificate register for “${String(call.args?.query ?? "")}”`);
|
|
@@ -1486,8 +1507,8 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
1486
1507
|
} catch {
|
|
1487
1508
|
// stream ended prematurely — deliver what we have
|
|
1488
1509
|
}
|
|
1489
|
-
const tq = await chargeAnswerTokens(env, bucketId, stats, messageChars(messages), full.length);
|
|
1490
|
-
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 } });
|
|
1491
1512
|
// the blind-answer guard ON THE STREAM PATH (the JSON path has
|
|
1492
1513
|
// had it since 0.2.80): an image rode the turn, the register
|
|
1493
1514
|
// note carries rows, and the streamed text is the no-information
|
|
@@ -1801,8 +1822,8 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
1801
1822
|
text: h.text.slice(0, 1200),
|
|
1802
1823
|
...((h.metadata as any).table_selection ? { sel: (h.metadata as any).table_selection } : {}),
|
|
1803
1824
|
}));
|
|
1804
|
-
const tq = await chargeAnswerTokens(env, bucketId, undefined, messageChars(messages), answer.length);
|
|
1805
|
-
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() });
|
|
1806
1827
|
}
|
|
1807
1828
|
|
|
1808
1829
|
function sseResponse(events: unknown[], cors: Record<string, string>): Response {
|
|
@@ -20,6 +20,7 @@ import type { Env } from "./env";
|
|
|
20
20
|
import { portModelRunner } from "./env.ts";
|
|
21
21
|
export type { Env };
|
|
22
22
|
import { json, err, corsHeaders, withCors, readJson, authenticate, type ApiKey } from "./lib/http";
|
|
23
|
+
import { creditBudget, clientIp } from "./quota";
|
|
23
24
|
|
|
24
25
|
import { handleSearch } from "./search";
|
|
25
26
|
import { handleEnrich, handleSectionUnit, handleCaption, handleVectors, handleJudge, handleCreateKey, handleListKeys, handleRevokeKey, handleKeyUsage, handleFamilySummary } from "./admin";
|
|
@@ -215,6 +216,19 @@ async function mcpRoute(c: RouteContext): Promise<Response> {
|
|
|
215
216
|
return withCors(await handleMcp(c.env, c.ctx, c.req, t.tier, t.key), corsHeaders(c.req));
|
|
216
217
|
}
|
|
217
218
|
|
|
219
|
+
/** The caller's daily credit balance (2026-10-11): the meter the UI
|
|
220
|
+
* shows — resolved by the same tiering as ask (anon by IP, member by
|
|
221
|
+
* session, key by its owner member's shared cap). */
|
|
222
|
+
async function creditsRoute(c: RouteContext): Promise<Response> {
|
|
223
|
+
const t = await tierFor(c);
|
|
224
|
+
if (t instanceof Response) return t;
|
|
225
|
+
const key = t.key;
|
|
226
|
+
const memberKey = t.tier === "key" ? key?.owner_sub ?? null : null;
|
|
227
|
+
const bucketId = memberKey ? `sub:${memberKey}` : t.tier === "key" ? `key:${key!.id}` : clientIp(c.req);
|
|
228
|
+
const cb = await creditBudget(c.env, bucketId);
|
|
229
|
+
return withCors(json({ credits: cb }), corsHeaders(c.req));
|
|
230
|
+
}
|
|
231
|
+
|
|
218
232
|
async function askRoute(c: RouteContext): Promise<Response> {
|
|
219
233
|
const t = await tierFor(c);
|
|
220
234
|
if (t instanceof Response) return t;
|
|
@@ -571,6 +585,8 @@ const INFRA_ROUTES: Route[] = [
|
|
|
571
585
|
|
|
572
586
|
const OPENAPI_HANDLERS: Record<OpenApiOperationId, RouteHandler> = {
|
|
573
587
|
askAnonymous: askRoute,
|
|
588
|
+
credits: creditsRoute,
|
|
589
|
+
creditsAlias: creditsRoute,
|
|
574
590
|
askKeyed: askRoute,
|
|
575
591
|
search: searchRoute,
|
|
576
592
|
searchKeyed: searchRoute,
|
|
@@ -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>();
|
|
@@ -7,6 +7,8 @@ export interface OpenApiRoute {
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
export type OpenApiOperationId =
|
|
10
|
+
| "credits"
|
|
11
|
+
| "creditsAlias"
|
|
10
12
|
| "askAnonymous"
|
|
11
13
|
| "askKeyed"
|
|
12
14
|
| "search"
|
|
@@ -64,6 +66,16 @@ export type OpenApiOperationId =
|
|
|
64
66
|
| "adminCreateKey"
|
|
65
67
|
|
|
66
68
|
export const OPENAPI_SURFACE: readonly (Omit<OpenApiRoute, "operationId"> & { operationId: OpenApiOperationId })[] = [
|
|
69
|
+
{
|
|
70
|
+
"method": "GET",
|
|
71
|
+
"pattern": "/api/credits",
|
|
72
|
+
"operationId": "credits"
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"method": "GET",
|
|
76
|
+
"pattern": "/v1/credits",
|
|
77
|
+
"operationId": "creditsAlias"
|
|
78
|
+
},
|
|
67
79
|
{
|
|
68
80
|
"method": "POST",
|
|
69
81
|
"pattern": "/api/ask",
|
|
@@ -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
|
+
}
|