@columenlabs/lumen-ai 0.1.82 → 0.1.84
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/assistant/engine.d.ts +44 -2
- package/dist/assistant/engine.js +122 -2
- package/dist/assistant/index.d.ts +4 -0
- package/dist/assistant/index.js +4 -0
- package/dist/assistant/routes.js +20 -0
- package/dist/assistant/state.d.ts +43 -2
- package/dist/assistant/state.js +56 -6
- package/package.json +3 -3
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AssistantAccess, AssistantChat, AssistantChatRow, AssistantMessage, AssistantProvider, AssistantStatus, DraftItem, StepItem } from '@columenlabs/lumen-ui-types/assistant';
|
|
1
|
+
import type { AllowanceRequest, AllowanceState, AllowanceUnit, AssistantAccess, AssistantChat, AssistantChatRow, AssistantMessage, AssistantProvider, AssistantStatus, DraftItem, StepItem } from '@columenlabs/lumen-ui-types/assistant';
|
|
2
2
|
import { type ModelDef, type TaskDef, type TierDef } from './models.js';
|
|
3
3
|
import { State } from './state.js';
|
|
4
4
|
import { type AssistantStore, type Sealer } from './store.js';
|
|
@@ -19,6 +19,35 @@ export interface AssistantCommandDef {
|
|
|
19
19
|
export interface Caller {
|
|
20
20
|
id: string;
|
|
21
21
|
admin: boolean;
|
|
22
|
+
/** What approvers see a request from them as: their email or name. */
|
|
23
|
+
label?: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* How an app counts allowance on its shared keys, and how a person asks for more. The kit holds
|
|
27
|
+
* the ledger and the forms; the app picks the unit, the size, who approves and how they hear.
|
|
28
|
+
*/
|
|
29
|
+
export interface AllowanceOptions {
|
|
30
|
+
/** What it is counted in; dollars by default. */
|
|
31
|
+
unit?: AllowanceUnit;
|
|
32
|
+
/** `month` (default): the admin's monthly cap plus grants made that month; `once`: one balance. */
|
|
33
|
+
period?: 'month' | 'once';
|
|
34
|
+
/**
|
|
35
|
+
* Units: each person's balance to start (once), or the monthly cap before an admin sets one
|
|
36
|
+
* (month).
|
|
37
|
+
*/
|
|
38
|
+
starter?: number;
|
|
39
|
+
/** Amounts the Request more form offers. */
|
|
40
|
+
amounts?: number[];
|
|
41
|
+
/** Who may grant or decline requests; admins by default. */
|
|
42
|
+
approver?: (caller: Caller) => boolean;
|
|
43
|
+
/** Hears each request and each decision: tell the approvers, then the person. */
|
|
44
|
+
notify?: (n: AllowanceNotice) => void | Promise<void>;
|
|
45
|
+
}
|
|
46
|
+
/** A request made, or decided. */
|
|
47
|
+
export interface AllowanceNotice {
|
|
48
|
+
kind: 'requested' | 'granted' | 'declined';
|
|
49
|
+
request: AllowanceRequest;
|
|
50
|
+
unit: AllowanceUnit;
|
|
22
51
|
}
|
|
23
52
|
/** A change a write tool made at once, as `undo` gets it. */
|
|
24
53
|
export interface AssistantWrite {
|
|
@@ -103,8 +132,10 @@ export interface AssistantOptions {
|
|
|
103
132
|
sealer?: Sealer;
|
|
104
133
|
/** The app's key of a provider when none is stored in Settings (a Worker secret). */
|
|
105
134
|
appKey?: (provider: AssistantProvider) => Promise<string | null | undefined>;
|
|
106
|
-
/** USD a person may spend a month when nobody set a cap; 10. */
|
|
135
|
+
/** USD a person may spend a month when nobody set a cap; 10 (or `allowance.starter`). */
|
|
107
136
|
defaultCap?: number;
|
|
137
|
+
/** An allowance on the shared keys a person can ask to raise (Request more). */
|
|
138
|
+
allowance?: AllowanceOptions;
|
|
108
139
|
/** Months and dates in this zone; UTC by default. */
|
|
109
140
|
timeZone?: string;
|
|
110
141
|
/** Chats are deleted this many days after their last message (`purge`); 30. */
|
|
@@ -145,6 +176,17 @@ export declare class Assistant {
|
|
|
145
176
|
private access;
|
|
146
177
|
private groupById;
|
|
147
178
|
status(caller: Caller): Promise<AssistantStatus>;
|
|
179
|
+
private get unit();
|
|
180
|
+
private approver;
|
|
181
|
+
/** The person's allowance in the app's unit, and their newest request. */
|
|
182
|
+
allowanceState(caller: Caller): Promise<AllowanceState>;
|
|
183
|
+
private notify;
|
|
184
|
+
/** Asks the approvers for more; one open request at a time. */
|
|
185
|
+
requestMore(caller: Caller, amount: number, why: string): Promise<AllowanceRequest>;
|
|
186
|
+
/** Approvers: every request, newest first. */
|
|
187
|
+
allowanceRequests(caller: Caller): Promise<AllowanceRequest[]>;
|
|
188
|
+
/** Approvers: grants (any amount, the asked one by default) or declines an open request. */
|
|
189
|
+
decideRequest(caller: Caller, id: string, action: 'grant' | 'decline', amount?: number): Promise<AllowanceRequest>;
|
|
148
190
|
listChats(caller: Caller): Promise<AssistantChatRow[]>;
|
|
149
191
|
readChat(caller: Caller, id: string): Promise<AssistantChat | null>;
|
|
150
192
|
removeChat(caller: Caller, id: string): Promise<void>;
|
package/dist/assistant/engine.js
CHANGED
|
@@ -25,6 +25,10 @@ import { DEFAULT_CAP, State, keyDocName, keyState, waitText, } from './state.js'
|
|
|
25
25
|
import { Docs } from './store.js';
|
|
26
26
|
import { ToolError, rowCount, toolName, withDeadline, } from './tools.js';
|
|
27
27
|
import { scrapeqlTools, webFetchTools } from './web.js';
|
|
28
|
+
const DOLLARS = { one: 'dollar', many: 'dollars', usd: 1 };
|
|
29
|
+
const round2 = (n) => Math.round(n * 100) / 100;
|
|
30
|
+
/** A request may ask for at most this many times the starter or monthly cap. */
|
|
31
|
+
const REQUEST_MAX_TIMES = 100;
|
|
28
32
|
/** A turn's failure the person can act on. */
|
|
29
33
|
export class AssistantError extends Error {
|
|
30
34
|
code;
|
|
@@ -149,11 +153,23 @@ export class Assistant {
|
|
|
149
153
|
{ id: 'chat', label: 'Chat', about: 'The side panel', tier: o.tiers[0].id },
|
|
150
154
|
];
|
|
151
155
|
this.groups = o.groups ?? [];
|
|
156
|
+
const al = o.allowance;
|
|
157
|
+
const unit = al?.unit ?? DOLLARS;
|
|
152
158
|
this.state = new State(new Docs(o.store, o.sealer), {
|
|
153
159
|
timeZone: o.timeZone ?? 'UTC',
|
|
154
|
-
defaultCap: o.defaultCap ??
|
|
160
|
+
defaultCap: o.defaultCap ??
|
|
161
|
+
(al?.starter !== undefined && al.period !== 'once' ? al.starter * unit.usd : DEFAULT_CAP),
|
|
155
162
|
...(o.appKey ? { appKey: o.appKey } : {}),
|
|
156
163
|
...(o.fetch ? { fetch: o.fetch } : {}),
|
|
164
|
+
...(al
|
|
165
|
+
? {
|
|
166
|
+
allowance: {
|
|
167
|
+
usd: unit.usd,
|
|
168
|
+
period: al.period ?? 'month',
|
|
169
|
+
starter: al.starter ?? DEFAULT_CAP / unit.usd,
|
|
170
|
+
},
|
|
171
|
+
}
|
|
172
|
+
: {}),
|
|
157
173
|
});
|
|
158
174
|
}
|
|
159
175
|
anthropic(apiKey) {
|
|
@@ -310,7 +326,111 @@ export class Assistant {
|
|
|
310
326
|
payer: paying?.payer ?? null,
|
|
311
327
|
rows: spend.rows,
|
|
312
328
|
},
|
|
329
|
+
...(this.o.allowance ? { allowance: await this.allowanceState(caller) } : {}),
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
// ---------------------------------------------------------------- allowance
|
|
333
|
+
get unit() {
|
|
334
|
+
return this.o.allowance?.unit ?? DOLLARS;
|
|
335
|
+
}
|
|
336
|
+
approver(caller) {
|
|
337
|
+
return this.o.allowance?.approver ? this.o.allowance.approver(caller) : caller.admin;
|
|
338
|
+
}
|
|
339
|
+
/** The person's allowance in the app's unit, and their newest request. */
|
|
340
|
+
async allowanceState(caller) {
|
|
341
|
+
const al = this.o.allowance ?? {};
|
|
342
|
+
const u = this.unit;
|
|
343
|
+
const [budget, doc, approver] = await Promise.all([
|
|
344
|
+
this.state.appBudget(caller.id),
|
|
345
|
+
this.state.allowance(caller.id),
|
|
346
|
+
this.approver(caller),
|
|
347
|
+
]);
|
|
348
|
+
const total = round2(budget.total / u.usd);
|
|
349
|
+
const used = round2(budget.used / u.usd);
|
|
350
|
+
const base = this.state.allowanceConfig?.period === 'once'
|
|
351
|
+
? (this.state.allowanceConfig?.starter ?? total)
|
|
352
|
+
: (await this.state.appCap()) / u.usd;
|
|
353
|
+
const waiting = approver
|
|
354
|
+
? (await this.state.requests()).rows.filter((r) => r.status === 'open').length
|
|
355
|
+
: undefined;
|
|
356
|
+
return {
|
|
357
|
+
unit: u,
|
|
358
|
+
period: al.period ?? 'month',
|
|
359
|
+
total,
|
|
360
|
+
used,
|
|
361
|
+
left: round2(Math.max(0, total - used)),
|
|
362
|
+
request: doc.request ?? null,
|
|
363
|
+
amounts: al.amounts ?? [...new Set([base, base * 3].map((n) => Math.max(1, Math.round(n))))],
|
|
364
|
+
approver,
|
|
365
|
+
...(waiting !== undefined ? { waiting } : {}),
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
notify(kind, request) {
|
|
369
|
+
const n = this.o.allowance?.notify;
|
|
370
|
+
if (!n)
|
|
371
|
+
return;
|
|
372
|
+
// A notice that fails is logged; the request or decision stands.
|
|
373
|
+
return Promise.resolve(n({ kind, request, unit: this.unit })).catch((err) => console.error(JSON.stringify({ event: 'assistant_allowance_notify_failed', kind, error: String(err) })));
|
|
374
|
+
}
|
|
375
|
+
/** Asks the approvers for more; one open request at a time. */
|
|
376
|
+
async requestMore(caller, amount, why) {
|
|
377
|
+
if (!this.o.allowance)
|
|
378
|
+
throw new AssistantError('This app has no allowance to raise.', 'not_found', 404);
|
|
379
|
+
const s = await this.allowanceState(caller);
|
|
380
|
+
if (s.request?.status === 'open')
|
|
381
|
+
throw new AssistantError('Your last request is still waiting.', 'open_request', 409);
|
|
382
|
+
const max = Math.max(...s.amounts, 1) * REQUEST_MAX_TIMES;
|
|
383
|
+
if (!(amount > 0) || amount > max)
|
|
384
|
+
throw new AssistantError(`Ask for 1 to ${max} ${this.unit.many}.`, 'bad_amount');
|
|
385
|
+
const request = {
|
|
386
|
+
id: `ar_${crypto.randomUUID().replace(/-/g, '').slice(0, 16)}`,
|
|
387
|
+
person: caller.id,
|
|
388
|
+
who: caller.label ?? caller.id,
|
|
389
|
+
amount: round2(amount),
|
|
390
|
+
why: why.slice(0, 500),
|
|
391
|
+
at: Date.now(),
|
|
392
|
+
status: 'open',
|
|
313
393
|
};
|
|
394
|
+
await this.state.setAllowance(caller.id, (d) => ({ ...d, request }));
|
|
395
|
+
await this.state.putRequest(request);
|
|
396
|
+
await this.notify('requested', request);
|
|
397
|
+
return request;
|
|
398
|
+
}
|
|
399
|
+
/** Approvers: every request, newest first. */
|
|
400
|
+
async allowanceRequests(caller) {
|
|
401
|
+
if (!(await this.approver(caller)))
|
|
402
|
+
throw new AssistantError('Only an approver can see requests.', 'forbidden', 403);
|
|
403
|
+
return (await this.state.requests()).rows;
|
|
404
|
+
}
|
|
405
|
+
/** Approvers: grants (any amount, the asked one by default) or declines an open request. */
|
|
406
|
+
async decideRequest(caller, id, action, amount) {
|
|
407
|
+
if (!(await this.approver(caller)))
|
|
408
|
+
throw new AssistantError('Only an approver can decide requests.', 'forbidden', 403);
|
|
409
|
+
const r = (await this.state.requests()).rows.find((x) => x.id === id);
|
|
410
|
+
if (!r)
|
|
411
|
+
throw new AssistantError('That request is gone.', 'not_found', 404);
|
|
412
|
+
if (r.status !== 'open')
|
|
413
|
+
throw new AssistantError('That request was already decided.', 'decided', 409);
|
|
414
|
+
const granted = action === 'grant' ? round2(amount ?? r.amount) : undefined;
|
|
415
|
+
if (granted !== undefined && !(granted > 0))
|
|
416
|
+
throw new AssistantError(`Grant more than 0 ${this.unit.many}.`, 'bad_amount');
|
|
417
|
+
const done = {
|
|
418
|
+
...r,
|
|
419
|
+
status: action === 'grant' ? 'granted' : 'declined',
|
|
420
|
+
...(granted !== undefined ? { granted } : {}),
|
|
421
|
+
decided_at: Date.now(),
|
|
422
|
+
decided_by: caller.label ?? caller.id,
|
|
423
|
+
};
|
|
424
|
+
await this.state.setAllowance(r.person, (d) => ({
|
|
425
|
+
...d,
|
|
426
|
+
grants: granted !== undefined
|
|
427
|
+
? [...d.grants, { at: Date.now(), amount: granted, by: caller.id, request: r.id }]
|
|
428
|
+
: d.grants,
|
|
429
|
+
...(d.request?.id === r.id ? { request: done } : {}),
|
|
430
|
+
}));
|
|
431
|
+
await this.state.putRequest(done);
|
|
432
|
+
await this.notify(action === 'grant' ? 'granted' : 'declined', done);
|
|
433
|
+
return done;
|
|
314
434
|
}
|
|
315
435
|
// ---------------------------------------------------------------- chats
|
|
316
436
|
async listChats(caller) {
|
|
@@ -475,7 +595,7 @@ export class Assistant {
|
|
|
475
595
|
const { paying, wait } = await this.state.paying(caller.id, main.model.provider);
|
|
476
596
|
if (!paying) {
|
|
477
597
|
const code = wait ?? 'not_connected';
|
|
478
|
-
const text = waitText(code, main.model.provider);
|
|
598
|
+
const text = waitText(code, main.model.provider, !!this.o.allowance);
|
|
479
599
|
doc.items.push({ kind: 'error', text, code });
|
|
480
600
|
await this.writeChat(caller, chatId, doc);
|
|
481
601
|
emit('error', { message: text, code });
|
|
@@ -8,7 +8,11 @@
|
|
|
8
8
|
*
|
|
9
9
|
* The panel and the Settings sections are `@columenlabs/lumen-ui`'s; the types they share are
|
|
10
10
|
* `@columenlabs/lumen-ui-types/assistant`.
|
|
11
|
+
*
|
|
12
|
+
* The ChatGPT plan sign-in's parts are exported on their own too, for an app that keeps the tokens and
|
|
13
|
+
* calls OpenAI itself (`LumenChatgptSignin` is the panel's part for it).
|
|
11
14
|
*/
|
|
15
|
+
export * from './chatgpt.js';
|
|
12
16
|
export * from './engine.js';
|
|
13
17
|
export * from './models.js';
|
|
14
18
|
export * from './routes.js';
|
package/dist/assistant/index.js
CHANGED
|
@@ -8,7 +8,11 @@
|
|
|
8
8
|
*
|
|
9
9
|
* The panel and the Settings sections are `@columenlabs/lumen-ui`'s; the types they share are
|
|
10
10
|
* `@columenlabs/lumen-ui-types/assistant`.
|
|
11
|
+
*
|
|
12
|
+
* The ChatGPT plan sign-in's parts are exported on their own too, for an app that keeps the tokens and
|
|
13
|
+
* calls OpenAI itself (`LumenChatgptSignin` is the panel's part for it).
|
|
11
14
|
*/
|
|
15
|
+
export * from './chatgpt.js';
|
|
12
16
|
export * from './engine.js';
|
|
13
17
|
export * from './models.js';
|
|
14
18
|
export * from './routes.js';
|
package/dist/assistant/routes.js
CHANGED
|
@@ -19,6 +19,9 @@
|
|
|
19
19
|
* GET /chats, GET|DELETE /chats/:id
|
|
20
20
|
* POST /chats/:id/drafts/:draft {action: save | dismiss}
|
|
21
21
|
* POST /chats/:id/writes/:step/undo
|
|
22
|
+
* POST /allowance/request {amount, why} asks for more allowance (one open at a time)
|
|
23
|
+
* GET /allowance/requests approvers: every request
|
|
24
|
+
* POST /allowance/requests/:id {action: grant | decline, amount?} approvers
|
|
22
25
|
*
|
|
23
26
|
* Writes must be same-origin JSON: a signed-in browser session is a cookie, so a foreign page's
|
|
24
27
|
* form or fetch is refused before it reaches a rule. Keys and tokens are write-only: no route
|
|
@@ -300,6 +303,23 @@ export function assistantRoutes(a, o) {
|
|
|
300
303
|
}
|
|
301
304
|
case 'GET /chats':
|
|
302
305
|
return json(await a.listChats(caller));
|
|
306
|
+
case 'POST /allowance/request': {
|
|
307
|
+
const b = await body(req);
|
|
308
|
+
const amount = typeof b.amount === 'number' && Number.isFinite(b.amount) ? b.amount : 0;
|
|
309
|
+
const why = typeof b.why === 'string' ? b.why.trim() : '';
|
|
310
|
+
await a.requestMore(caller, amount, why);
|
|
311
|
+
return status();
|
|
312
|
+
}
|
|
313
|
+
case 'GET /allowance/requests':
|
|
314
|
+
return json(await a.allowanceRequests(caller));
|
|
315
|
+
}
|
|
316
|
+
if (parts[0] === 'allowance' && parts[1] === 'requests' && parts[2] && m === 'POST') {
|
|
317
|
+
const b = await body(req);
|
|
318
|
+
const action = b.action;
|
|
319
|
+
if (action !== 'grant' && action !== 'decline')
|
|
320
|
+
return fail('action is grant or decline.');
|
|
321
|
+
const amount = typeof b.amount === 'number' && Number.isFinite(b.amount) ? b.amount : undefined;
|
|
322
|
+
return json(await a.decideRequest(caller, parts[2], action, amount));
|
|
303
323
|
}
|
|
304
324
|
if (parts[0] === 'chats' && parts[1]) {
|
|
305
325
|
const id = parts[1];
|
|
@@ -11,12 +11,15 @@
|
|
|
11
11
|
* plan sign-in, refreshed before it runs out
|
|
12
12
|
* signin:<p> (a person's) the app's sign-in registration for them, kept across sign-outs
|
|
13
13
|
* spend:YYYY-MM what each tier cost them this month, by payer
|
|
14
|
+
* allowance (a person's) grants of more allowance, their newest request, and (when it is
|
|
15
|
+
* once for good) what they spent on the app's keys in all
|
|
16
|
+
* allowance_requests (the app's) every request, newest first, for approvers
|
|
14
17
|
*
|
|
15
18
|
* Who pays: the person's pick (the app's keys by default) while it has a key for the provider the
|
|
16
19
|
* tier runs on and money left this month, else the other; with neither the assistant waits and says
|
|
17
20
|
* why. A cap covers every provider of its payer.
|
|
18
21
|
*/
|
|
19
|
-
import type { AssistantAccess, AssistantEffort, AssistantPayer, AssistantProvider, AssistantWait, HandOffPolicy, UsagePart } from '@columenlabs/lumen-ui-types/assistant';
|
|
22
|
+
import type { AllowanceRequest, AssistantAccess, AssistantEffort, AssistantPayer, AssistantProvider, AssistantWait, HandOffPolicy, UsagePart } from '@columenlabs/lumen-ui-types/assistant';
|
|
20
23
|
import { type ChatgptTokens } from './chatgpt.js';
|
|
21
24
|
import { type Docs } from './store.js';
|
|
22
25
|
export interface KeyTest {
|
|
@@ -60,6 +63,29 @@ export interface PersonDoc {
|
|
|
60
63
|
/** USD a month on their own keys. */
|
|
61
64
|
own_cap?: number;
|
|
62
65
|
}
|
|
66
|
+
/** How the app counts allowance on its shared keys (`createAssistant` `allowance`). */
|
|
67
|
+
export interface AllowanceConfig {
|
|
68
|
+
/** USD one unit buys. */
|
|
69
|
+
usd: number;
|
|
70
|
+
/** `month`: the cap resets each month, grants count in their month; `once`: one balance for good. */
|
|
71
|
+
period: 'month' | 'once';
|
|
72
|
+
/** Units each person starts with (once); a month's cap is the admin's cap in Settings. */
|
|
73
|
+
starter: number;
|
|
74
|
+
}
|
|
75
|
+
export interface AllowanceDoc {
|
|
76
|
+
grants: {
|
|
77
|
+
at: number;
|
|
78
|
+
amount: number;
|
|
79
|
+
by: string;
|
|
80
|
+
request?: string;
|
|
81
|
+
}[];
|
|
82
|
+
/** USD spent on the app's keys in all (counted only when the allowance is once for good). */
|
|
83
|
+
spent_usd?: number;
|
|
84
|
+
request?: AllowanceRequest;
|
|
85
|
+
}
|
|
86
|
+
export interface RequestsDoc {
|
|
87
|
+
rows: AllowanceRequest[];
|
|
88
|
+
}
|
|
63
89
|
export type SpendRow = UsagePart & {
|
|
64
90
|
payer: AssistantPayer;
|
|
65
91
|
};
|
|
@@ -99,7 +125,22 @@ export declare class State {
|
|
|
99
125
|
defaultCap: number;
|
|
100
126
|
appKey?: (provider: AssistantProvider) => Promise<string | null | undefined>;
|
|
101
127
|
fetch?: typeof fetch;
|
|
128
|
+
allowance?: AllowanceConfig;
|
|
102
129
|
});
|
|
130
|
+
get allowanceConfig(): AllowanceConfig | undefined;
|
|
131
|
+
allowance(id: string): Promise<AllowanceDoc>;
|
|
132
|
+
setAllowance(id: string, change: (d: AllowanceDoc) => AllowanceDoc): Promise<AllowanceDoc | null>;
|
|
133
|
+
requests(): Promise<RequestsDoc>;
|
|
134
|
+
/** Adds or replaces one request in the approvers' list. */
|
|
135
|
+
putRequest(r: AllowanceRequest): Promise<RequestsDoc | null>;
|
|
136
|
+
/**
|
|
137
|
+
* USD the person has on the app's keys for the period and USD used: the admin's monthly cap plus
|
|
138
|
+
* this month's grants, or (once for good) the starter and every grant against all spend.
|
|
139
|
+
*/
|
|
140
|
+
appBudget(id: string, spentThisMonth?: number): Promise<{
|
|
141
|
+
total: number;
|
|
142
|
+
used: number;
|
|
143
|
+
}>;
|
|
103
144
|
/** USD a month when nobody set a cap. */
|
|
104
145
|
get defaultCap(): number;
|
|
105
146
|
month(): string;
|
|
@@ -137,4 +178,4 @@ export declare class State {
|
|
|
137
178
|
wait: AssistantWait | null;
|
|
138
179
|
}>;
|
|
139
180
|
}
|
|
140
|
-
export declare function waitText(wait: AssistantWait, provider: AssistantProvider): string;
|
|
181
|
+
export declare function waitText(wait: AssistantWait, provider: AssistantProvider, canRequest?: boolean): string;
|
package/dist/assistant/state.js
CHANGED
|
@@ -3,6 +3,8 @@ import { PROVIDERS, keyHint } from './models.js';
|
|
|
3
3
|
import { APP_SCOPE } from './store.js';
|
|
4
4
|
/** Where a provider's key is kept: `key` for Anthropic, `key:<provider>` for the rest. */
|
|
5
5
|
export const keyDocName = (provider) => provider === 'anthropic' ? 'key' : `key:${provider}`;
|
|
6
|
+
/** Requests kept for approvers: the newest this many. */
|
|
7
|
+
const REQUESTS_KEPT = 300;
|
|
6
8
|
/** USD a month when nobody set a cap. */
|
|
7
9
|
export const DEFAULT_CAP = 10;
|
|
8
10
|
/** `2026-10` in the app's time zone. */
|
|
@@ -30,6 +32,47 @@ export class State {
|
|
|
30
32
|
this.docs = docs;
|
|
31
33
|
this.o = o;
|
|
32
34
|
}
|
|
35
|
+
get allowanceConfig() {
|
|
36
|
+
return this.o.allowance;
|
|
37
|
+
}
|
|
38
|
+
allowance(id) {
|
|
39
|
+
return this.docs.read(id, 'allowance').then((d) => d ?? { grants: [] });
|
|
40
|
+
}
|
|
41
|
+
setAllowance(id, change) {
|
|
42
|
+
return this.docs.update(id, 'allowance', (d) => change(d ?? { grants: [] }));
|
|
43
|
+
}
|
|
44
|
+
requests() {
|
|
45
|
+
return this.docs
|
|
46
|
+
.read(APP_SCOPE, 'allowance_requests')
|
|
47
|
+
.then((d) => d ?? { rows: [] });
|
|
48
|
+
}
|
|
49
|
+
/** Adds or replaces one request in the approvers' list. */
|
|
50
|
+
putRequest(r) {
|
|
51
|
+
return this.docs.update(APP_SCOPE, 'allowance_requests', (d) => ({
|
|
52
|
+
rows: [r, ...(d?.rows ?? []).filter((x) => x.id !== r.id)].slice(0, REQUESTS_KEPT),
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* USD the person has on the app's keys for the period and USD used: the admin's monthly cap plus
|
|
57
|
+
* this month's grants, or (once for good) the starter and every grant against all spend.
|
|
58
|
+
*/
|
|
59
|
+
async appBudget(id, spentThisMonth) {
|
|
60
|
+
const al = this.o.allowance;
|
|
61
|
+
const used = spentThisMonth ??
|
|
62
|
+
(await this.spend(id)).rows.filter((r) => r.payer === 'app').reduce((a, r) => a + r.usd, 0);
|
|
63
|
+
if (!al)
|
|
64
|
+
return { total: await this.appCap(), used };
|
|
65
|
+
const d = await this.allowance(id);
|
|
66
|
+
if (al.period === 'once') {
|
|
67
|
+
const units = al.starter + d.grants.reduce((a, g) => a + g.amount, 0);
|
|
68
|
+
return { total: units * al.usd, used: d.spent_usd ?? 0 };
|
|
69
|
+
}
|
|
70
|
+
const month = this.month();
|
|
71
|
+
const granted = d.grants
|
|
72
|
+
.filter((g) => monthIn(this.o.timeZone, new Date(g.at)) === month)
|
|
73
|
+
.reduce((a, g) => a + g.amount, 0);
|
|
74
|
+
return { total: (await this.appCap()) + granted * al.usd, used };
|
|
75
|
+
}
|
|
33
76
|
/** USD a month when nobody set a cap. */
|
|
34
77
|
get defaultCap() {
|
|
35
78
|
return this.o.defaultCap;
|
|
@@ -133,6 +176,11 @@ export class State {
|
|
|
133
176
|
}
|
|
134
177
|
return { rows };
|
|
135
178
|
});
|
|
179
|
+
if (payer === 'app' && this.o.allowance?.period === 'once') {
|
|
180
|
+
const usd = parts.reduce((a, p) => a + p.usd, 0);
|
|
181
|
+
if (usd > 0)
|
|
182
|
+
await this.setAllowance(id, (d) => ({ ...d, spent_usd: (d.spent_usd ?? 0) + usd }));
|
|
183
|
+
}
|
|
136
184
|
}
|
|
137
185
|
/** The payer's key of each provider, those set, and which are plan sign-ins. */
|
|
138
186
|
async keysOf(id, payer) {
|
|
@@ -153,15 +201,15 @@ export class State {
|
|
|
153
201
|
}
|
|
154
202
|
/** Who pays the person's next answer on `provider`'s model, or why nobody can. */
|
|
155
203
|
async paying(id, provider) {
|
|
156
|
-
const [person, appKeys, ownKeys, spend,
|
|
204
|
+
const [person, appKeys, ownKeys, spend, ownCap] = await Promise.all([
|
|
157
205
|
this.person(id),
|
|
158
206
|
this.keysOf(id, 'app'),
|
|
159
207
|
this.keysOf(id, 'own'),
|
|
160
208
|
this.spend(id),
|
|
161
|
-
this.appCap(),
|
|
162
209
|
this.ownCap(id),
|
|
163
210
|
]);
|
|
164
211
|
const spent = (p) => spend.rows.filter((r) => r.payer === p).reduce((a, r) => a + r.usd, 0);
|
|
212
|
+
const budget = await this.appBudget(id, spent('app'));
|
|
165
213
|
const app = appKeys.keys[provider];
|
|
166
214
|
const own = ownKeys.keys[provider];
|
|
167
215
|
const sources = {
|
|
@@ -170,8 +218,8 @@ export class State {
|
|
|
170
218
|
payer: 'app',
|
|
171
219
|
apiKey: app,
|
|
172
220
|
...appKeys,
|
|
173
|
-
cap:
|
|
174
|
-
left:
|
|
221
|
+
cap: budget.total,
|
|
222
|
+
left: budget.total - budget.used,
|
|
175
223
|
}
|
|
176
224
|
: null,
|
|
177
225
|
own: own
|
|
@@ -198,11 +246,13 @@ export class State {
|
|
|
198
246
|
return { paying: null, wait: capped === 'own' ? 'own_cap' : 'app_cap' };
|
|
199
247
|
}
|
|
200
248
|
}
|
|
201
|
-
export function waitText(wait, provider) {
|
|
249
|
+
export function waitText(wait, provider, canRequest = false) {
|
|
202
250
|
const label = PROVIDERS[provider].label;
|
|
203
251
|
if (wait === 'not_connected')
|
|
204
252
|
return `No ${label} key is set: add one in Settings ▸ Model providers, or pick another model in Settings ▸ Models.`;
|
|
205
253
|
if (wait === 'app_cap')
|
|
206
|
-
return
|
|
254
|
+
return canRequest
|
|
255
|
+
? 'You used your allowance on the shared keys. Request more in Settings, or add your own key.'
|
|
256
|
+
: "You reached this month's cap on the shared keys. Add your own key, or wait for next month.";
|
|
207
257
|
return 'Your own keys reached their monthly cap: raise it in Settings ▸ Model providers.';
|
|
208
258
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@columenlabs/lumen-ai",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.84",
|
|
4
4
|
"description": "Lumen's assistant and agent parts: a chat assistant's server (Claude tiers, tools over MCP, the web, hand off, spend caps), serving an MCP server from a Worker, read-only tool sets and metered tool calls.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -41,8 +41,8 @@
|
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@anthropic-ai/sdk": "0.133.0",
|
|
44
|
-
"@columenlabs/lumen-ops": "0.1.
|
|
45
|
-
"@columenlabs/lumen-ui-types": "0.1.
|
|
44
|
+
"@columenlabs/lumen-ops": "0.1.84",
|
|
45
|
+
"@columenlabs/lumen-ui-types": "0.1.84",
|
|
46
46
|
"openai": "7.32.0",
|
|
47
47
|
"zod": "^4.3.6"
|
|
48
48
|
},
|