@columenlabs/lumen-ai 0.1.92 → 0.1.94
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 +12 -4
- package/dist/assistant/engine.js +46 -15
- package/dist/assistant/routes.js +29 -5
- package/dist/assistant/state.d.ts +18 -2
- package/dist/assistant/state.js +53 -16
- package/dist/assistant/store.d.ts +11 -0
- package/dist/assistant/store.js +40 -1
- package/package.json +3 -3
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AllowanceRequest, AllowanceState, AllowanceUnit, AssistantAccess, AssistantChat, AssistantChatRow, AssistantMessage, AssistantProvider, AssistantStatus, DraftItem, StepItem } from '@columenlabs/lumen-ui-types/assistant';
|
|
1
|
+
import type { AllowanceRequest, AllowanceState, AllowanceUnit, AssistantAccess, AssistantChat, AssistantChatRow, AssistantMessage, AssistantOwnWay, 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';
|
|
@@ -234,10 +234,18 @@ export declare class Assistant {
|
|
|
234
234
|
url: string;
|
|
235
235
|
redirect: string;
|
|
236
236
|
}>;
|
|
237
|
-
/**
|
|
237
|
+
/**
|
|
238
|
+
* Finishes it from the address it ended on. The plan is kept beside the person's key; a person who
|
|
239
|
+
* already has a key keeps running on it until they pick the plan.
|
|
240
|
+
*/
|
|
238
241
|
finishSignin(caller: Caller, provider: AssistantProvider, pasted: string): Promise<void>;
|
|
239
|
-
/**
|
|
240
|
-
|
|
242
|
+
/** Signs the person out of their plan; their key, if any, runs from then on. */
|
|
243
|
+
signOut(caller: Caller, provider: AssistantProvider): Promise<void>;
|
|
244
|
+
/**
|
|
245
|
+
* Tests one of the person's own (`way`, else the one in use), refreshing a plan first, and keeps
|
|
246
|
+
* the result on it.
|
|
247
|
+
*/
|
|
248
|
+
testOwn(caller: Caller, provider: AssistantProvider, way?: AssistantOwnWay): Promise<import("./state.js").KeyTest | null>;
|
|
241
249
|
/** One free call (reading a model) that says whether the provider takes the key. */
|
|
242
250
|
testKey(provider: AssistantProvider, apiKey: string): Promise<{
|
|
243
251
|
ok: boolean;
|
package/dist/assistant/engine.js
CHANGED
|
@@ -21,7 +21,7 @@ import { HAND_OFF_WHYS } from '@columenlabs/lumen-ui-types/assistant';
|
|
|
21
21
|
import OpenAI from 'openai';
|
|
22
22
|
import { CHATGPT_REDIRECT, SigninError, finishChatgpt, startChatgpt, testChatgpt, } from './chatgpt.js';
|
|
23
23
|
import { ALL_MODELS, DEFAULT_OUT, Meter, PROVIDERS, effortParams, } from './models.js';
|
|
24
|
-
import { DEFAULT_CAP, State, keyDocName, keyState, waitText, } from './state.js';
|
|
24
|
+
import { DEFAULT_CAP, State, keyDocName, keyState, planDocName, 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';
|
|
@@ -248,10 +248,12 @@ export class Assistant {
|
|
|
248
248
|
const [p, providers, scrapeql, spend, appCap, ownCap, { paying, wait }] = await Promise.all([
|
|
249
249
|
this.state.person(caller.id),
|
|
250
250
|
Promise.all(this.providers.map(async (id) => {
|
|
251
|
-
const [doc, key, own] = await Promise.all([
|
|
251
|
+
const [doc, key, own, plan, use] = await Promise.all([
|
|
252
252
|
this.state.appKey(id),
|
|
253
253
|
this.state.appApiKey(id),
|
|
254
254
|
this.state.ownKey(caller.id, id),
|
|
255
|
+
PROVIDERS[id].signin ? this.state.ownPlan(caller.id, id) : Promise.resolve(null),
|
|
256
|
+
this.state.ownUse(caller.id, id),
|
|
255
257
|
]);
|
|
256
258
|
const app = caller.admin
|
|
257
259
|
? keyState(doc, (d) => d.api_key)
|
|
@@ -264,6 +266,10 @@ export class Assistant {
|
|
|
264
266
|
...(PROVIDERS[id].signin ? { signin: PROVIDERS[id].signin } : {}),
|
|
265
267
|
app: { ...app, ...(key?.fromEnv ? { from_env: true } : {}) },
|
|
266
268
|
own: keyState(own, (d) => d.api_key),
|
|
269
|
+
...(PROVIDERS[id].signin
|
|
270
|
+
? { plan: keyState(plan?.doc ?? null, (d) => d.api_key) }
|
|
271
|
+
: {}),
|
|
272
|
+
...(use ? { use } : {}),
|
|
267
273
|
};
|
|
268
274
|
})),
|
|
269
275
|
this.o.web?.scrapeql ? this.state.scrapeql() : Promise.resolve(null),
|
|
@@ -1421,19 +1427,27 @@ export class Assistant {
|
|
|
1421
1427
|
await this.state.docs.write(caller.id, name, reg);
|
|
1422
1428
|
return { url, redirect: CHATGPT_REDIRECT };
|
|
1423
1429
|
}
|
|
1424
|
-
/**
|
|
1430
|
+
/**
|
|
1431
|
+
* Finishes it from the address it ended on. The plan is kept beside the person's key; a person who
|
|
1432
|
+
* already has a key keeps running on it until they pick the plan.
|
|
1433
|
+
*/
|
|
1425
1434
|
async finishSignin(caller, provider, pasted) {
|
|
1426
1435
|
const name = `signin:${provider}`;
|
|
1427
1436
|
try {
|
|
1428
1437
|
const { tokens, reg } = await finishChatgpt(await this.state.docs.read(caller.id, name), pasted, this.o.fetch ?? fetch);
|
|
1429
1438
|
await this.state.docs.write(caller.id, name, reg);
|
|
1430
1439
|
const test = await testChatgpt(tokens, this.o.fetch ?? fetch);
|
|
1431
|
-
await this.state.
|
|
1440
|
+
const legacy = await this.state.ownPlan(caller.id, provider);
|
|
1441
|
+
await this.state.docs.write(caller.id, planDocName(provider), {
|
|
1432
1442
|
api_key: tokens.access_token,
|
|
1433
1443
|
saved_at: Date.now(),
|
|
1434
1444
|
test,
|
|
1435
1445
|
chatgpt: tokens,
|
|
1436
1446
|
});
|
|
1447
|
+
if (legacy && legacy.name !== planDocName(provider))
|
|
1448
|
+
await this.state.docs.remove(caller.id, legacy.name);
|
|
1449
|
+
if (await this.state.ownKey(caller.id, provider))
|
|
1450
|
+
await this.state.setPerson(caller.id, (d) => d.own_use?.[provider] ? d : { ...d, own_use: { ...d.own_use, [provider]: 'key' } });
|
|
1437
1451
|
}
|
|
1438
1452
|
catch (err) {
|
|
1439
1453
|
if (err instanceof SigninError)
|
|
@@ -1441,18 +1455,35 @@ export class Assistant {
|
|
|
1441
1455
|
throw err;
|
|
1442
1456
|
}
|
|
1443
1457
|
}
|
|
1444
|
-
/**
|
|
1445
|
-
async
|
|
1446
|
-
const
|
|
1447
|
-
if (
|
|
1458
|
+
/** Signs the person out of their plan; their key, if any, runs from then on. */
|
|
1459
|
+
async signOut(caller, provider) {
|
|
1460
|
+
const found = await this.state.ownPlan(caller.id, provider);
|
|
1461
|
+
if (found)
|
|
1462
|
+
await this.state.docs.remove(caller.id, found.name);
|
|
1463
|
+
}
|
|
1464
|
+
/**
|
|
1465
|
+
* Tests one of the person's own (`way`, else the one in use), refreshing a plan first, and keeps
|
|
1466
|
+
* the result on it.
|
|
1467
|
+
*/
|
|
1468
|
+
async testOwn(caller, provider, way) {
|
|
1469
|
+
const w = way ?? (await this.state.ownUse(caller.id, provider));
|
|
1470
|
+
if (w === 'key') {
|
|
1471
|
+
const d = await this.state.ownKey(caller.id, provider);
|
|
1472
|
+
if (!d)
|
|
1473
|
+
return null;
|
|
1474
|
+
const test = await this.testKey(provider, d.api_key);
|
|
1475
|
+
await this.state.docs.write(caller.id, keyDocName(provider), { ...d, test });
|
|
1476
|
+
return test;
|
|
1477
|
+
}
|
|
1478
|
+
if (w !== 'plan' || !(await this.state.ownPlan(caller.id, provider)))
|
|
1448
1479
|
return null;
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
return
|
|
1480
|
+
const token = await this.state.planToken(caller.id, provider);
|
|
1481
|
+
const now = await this.state.ownPlan(caller.id, provider);
|
|
1482
|
+
if (!token || !now)
|
|
1483
|
+
return now?.doc.test ?? { ok: false, detail: 'Sign in again.', at: Date.now() };
|
|
1484
|
+
const test = await testChatgpt(now.doc.chatgpt, this.o.fetch ?? fetch);
|
|
1485
|
+
await this.state.docs.write(caller.id, now.name, { ...now.doc, test });
|
|
1486
|
+
return test;
|
|
1456
1487
|
}
|
|
1457
1488
|
/** One free call (reading a model) that says whether the provider takes the key. */
|
|
1458
1489
|
async testKey(provider, apiKey) {
|
package/dist/assistant/routes.js
CHANGED
|
@@ -14,6 +14,9 @@
|
|
|
14
14
|
* PUT /my-key/cap {cap_usd} what the person may spend a month on their own keys
|
|
15
15
|
* POST /my-signin starts a ChatGPT plan sign-in (`?provider=openai`): {url, redirect}
|
|
16
16
|
* POST /my-signin/finish {url} finishes it from the 127.0.0.1 address the sign-in ended on
|
|
17
|
+
* DELETE /my-signin signs out of the plan (the person's key, if set, runs on)
|
|
18
|
+
* (`POST /my-key/test?way=key|plan` tests one; the plan and the
|
|
19
|
+
* key are kept side by side, `own_use` in /settings picks)
|
|
17
20
|
* PUT /web/scrapeql {client_id, client_secret}, DELETE /web/scrapeql, POST /web/test admin
|
|
18
21
|
* POST /chat one answer as server-sent events; closing the request stops it
|
|
19
22
|
* GET /chats, GET|DELETE /chats/:id
|
|
@@ -27,10 +30,10 @@
|
|
|
27
30
|
* form or fetch is refused before it reaches a rule. Keys and tokens are write-only: no route
|
|
28
31
|
* returns one, only a hint (`sk-ant-…abcd`).
|
|
29
32
|
*/
|
|
30
|
-
import { ASSISTANT_ACCESS, ASSISTANT_EFFORTS, ASSISTANT_PAYERS, HAND_OFF_POLICIES, } from '@columenlabs/lumen-ui-types/assistant';
|
|
33
|
+
import { ASSISTANT_ACCESS, ASSISTANT_EFFORTS, ASSISTANT_OWN_WAYS, ASSISTANT_PAYERS, HAND_OFF_POLICIES, } from '@columenlabs/lumen-ui-types/assistant';
|
|
31
34
|
import { AssistantError } from './engine.js';
|
|
32
35
|
import { PROVIDERS } from './models.js';
|
|
33
|
-
import { keyDocName } from './state.js';
|
|
36
|
+
import { keyDocName, planDocName } from './state.js';
|
|
34
37
|
import { APP_SCOPE } from './store.js';
|
|
35
38
|
const MUTATING = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);
|
|
36
39
|
const MAX_MESSAGE = 20_000;
|
|
@@ -101,6 +104,16 @@ function settingsChange(a, groups, b) {
|
|
|
101
104
|
return 'web is {fetch: true | false}.';
|
|
102
105
|
if (p.pay !== undefined && !oneOf(ASSISTANT_PAYERS, p.pay))
|
|
103
106
|
return 'pay is app or own.';
|
|
107
|
+
if (p.own_use !== undefined) {
|
|
108
|
+
if (!p.own_use || typeof p.own_use !== 'object')
|
|
109
|
+
return 'own_use is {provider: key | plan}.';
|
|
110
|
+
for (const [prov, w] of Object.entries(p.own_use)) {
|
|
111
|
+
if (!a.providers.includes(prov))
|
|
112
|
+
return `No provider ${prov}.`;
|
|
113
|
+
if (!oneOf(ASSISTANT_OWN_WAYS, w))
|
|
114
|
+
return `No way ${w}.`;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
104
117
|
return (d) => {
|
|
105
118
|
const tiers = { ...(d.tiers ?? {}) };
|
|
106
119
|
for (const [id, t] of Object.entries(p.tiers ?? {})) {
|
|
@@ -118,6 +131,7 @@ function settingsChange(a, groups, b) {
|
|
|
118
131
|
...(p.access !== undefined ? { access: { ...(d.access ?? {}), ...p.access } } : {}),
|
|
119
132
|
...(p.web !== undefined ? { web: p.web.fetch } : {}),
|
|
120
133
|
...(p.pay !== undefined ? { pay: p.pay } : {}),
|
|
134
|
+
...(p.own_use !== undefined ? { own_use: { ...(d.own_use ?? {}), ...p.own_use } } : {}),
|
|
121
135
|
};
|
|
122
136
|
};
|
|
123
137
|
}
|
|
@@ -195,6 +209,10 @@ export function assistantRoutes(app, o) {
|
|
|
195
209
|
if (!test.ok)
|
|
196
210
|
return fail(test.detail, 400, 'key_refused');
|
|
197
211
|
const scope = mine ? caller.id : APP_SCOPE;
|
|
212
|
+
// A plan sign-in from before 2026-10-27 sits in the key's doc: move it beside the key.
|
|
213
|
+
const legacy = mine ? await a.state.ownPlan(caller.id, prov) : null;
|
|
214
|
+
if (legacy?.name === docName)
|
|
215
|
+
await docs.write(scope, planDocName(prov), legacy.doc);
|
|
198
216
|
await docs.write(scope, docName, { api_key: key, saved_at: Date.now(), test });
|
|
199
217
|
return status();
|
|
200
218
|
}
|
|
@@ -218,6 +236,9 @@ export function assistantRoutes(app, o) {
|
|
|
218
236
|
}
|
|
219
237
|
case 'POST /my-signin':
|
|
220
238
|
return json(await a.startSignin(caller, prov));
|
|
239
|
+
case 'DELETE /my-signin':
|
|
240
|
+
await a.signOut(caller, prov);
|
|
241
|
+
return status();
|
|
221
242
|
case 'POST /my-signin/finish': {
|
|
222
243
|
const pasted = str((await body(req)).url, 4000);
|
|
223
244
|
if (!pasted)
|
|
@@ -226,10 +247,13 @@ export function assistantRoutes(app, o) {
|
|
|
226
247
|
return status();
|
|
227
248
|
}
|
|
228
249
|
case 'POST /my-key/test': {
|
|
229
|
-
const
|
|
250
|
+
const askedWay = url.searchParams.get('way');
|
|
251
|
+
const way = oneOf(ASSISTANT_OWN_WAYS, askedWay);
|
|
252
|
+
if (askedWay !== null && !way)
|
|
253
|
+
return fail('way is key or plan.');
|
|
254
|
+
const test = await a.testOwn(caller, prov, way);
|
|
230
255
|
if (!test)
|
|
231
|
-
return fail('No key is set.', 404);
|
|
232
|
-
await docs.update(caller.id, docName, (d) => (d ? { ...d, test } : null));
|
|
256
|
+
return fail(way === 'plan' ? 'Not signed in.' : 'No key is set.', 404);
|
|
233
257
|
return json(test);
|
|
234
258
|
}
|
|
235
259
|
case 'POST /key/test': {
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* tier runs on and money left this month, else the other; with neither the assistant waits and says
|
|
20
20
|
* why. A cap covers every provider of its payer.
|
|
21
21
|
*/
|
|
22
|
-
import type { AllowanceRequest, AssistantAccess, AssistantEffort, AssistantPayer, AssistantProvider, AssistantWait, HandOffPolicy, UsagePart } from '@columenlabs/lumen-ui-types/assistant';
|
|
22
|
+
import type { AllowanceRequest, AssistantAccess, AssistantEffort, AssistantOwnWay, AssistantPayer, AssistantProvider, AssistantWait, HandOffPolicy, UsagePart } from '@columenlabs/lumen-ui-types/assistant';
|
|
23
23
|
import { type ChatgptTokens } from './chatgpt.js';
|
|
24
24
|
import { type Docs } from './store.js';
|
|
25
25
|
export interface KeyTest {
|
|
@@ -38,6 +38,8 @@ export interface KeyDoc {
|
|
|
38
38
|
}
|
|
39
39
|
/** Where a provider's key is kept: `key` for Anthropic, `key:<provider>` for the rest. */
|
|
40
40
|
export declare const keyDocName: (provider: AssistantProvider) => string;
|
|
41
|
+
/** Where a person's plan sign-in is kept, beside their key (before 2026-10-27 it replaced the key). */
|
|
42
|
+
export declare const planDocName: (provider: AssistantProvider) => string;
|
|
41
43
|
export interface TokenDoc {
|
|
42
44
|
client_id: string;
|
|
43
45
|
client_secret: string;
|
|
@@ -62,6 +64,8 @@ export interface PersonDoc {
|
|
|
62
64
|
pay?: AssistantPayer;
|
|
63
65
|
/** USD a month on their own keys. */
|
|
64
66
|
own_cap?: number;
|
|
67
|
+
/** Which of their own runs on a provider when both a key and a plan are set. */
|
|
68
|
+
own_use?: Partial<Record<AssistantProvider, AssistantOwnWay>>;
|
|
65
69
|
}
|
|
66
70
|
/** How the app counts allowance on its shared keys (`createAssistant` `allowance`). */
|
|
67
71
|
export interface AllowanceConfig {
|
|
@@ -156,8 +160,20 @@ export declare class State {
|
|
|
156
160
|
key: string;
|
|
157
161
|
fromEnv: boolean;
|
|
158
162
|
} | null>;
|
|
163
|
+
/** The person's own API key (a key doc that holds a plan sign-in is their plan). */
|
|
159
164
|
ownKey(id: string, provider: AssistantProvider): Promise<KeyDoc | null>;
|
|
160
|
-
/** The person's
|
|
165
|
+
/** The person's plan sign-in and the doc it is kept in (the key's, for a sign-in from before). */
|
|
166
|
+
ownPlan(id: string, provider: AssistantProvider): Promise<{
|
|
167
|
+
doc: KeyDoc & {
|
|
168
|
+
chatgpt: ChatgptTokens;
|
|
169
|
+
};
|
|
170
|
+
name: string;
|
|
171
|
+
} | null>;
|
|
172
|
+
/** Which of their own runs: the one they picked while it is set, else the plan, else the key. */
|
|
173
|
+
ownUse(id: string, provider: AssistantProvider): Promise<AssistantOwnWay | null>;
|
|
174
|
+
/** The plan's access token, refreshed first when it is about to run out; null once it fails. */
|
|
175
|
+
planToken(id: string, provider: AssistantProvider): Promise<string | null>;
|
|
176
|
+
/** The person's own key or plan, the one in use first, the other when that one fails. */
|
|
161
177
|
ownApiKey(id: string, provider: AssistantProvider): Promise<{
|
|
162
178
|
key: string;
|
|
163
179
|
plan: boolean;
|
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
|
+
/** Where a person's plan sign-in is kept, beside their key (before 2026-10-27 it replaced the key). */
|
|
7
|
+
export const planDocName = (provider) => `plan:${provider}`;
|
|
6
8
|
/** Requests kept for approvers: the newest this many. */
|
|
7
9
|
const REQUESTS_KEPT = 300;
|
|
8
10
|
/** USD a month when nobody set a cap. */
|
|
@@ -107,35 +109,70 @@ export class State {
|
|
|
107
109
|
const env = await this.o.appKey?.(provider);
|
|
108
110
|
return env ? { key: env, fromEnv: true } : null;
|
|
109
111
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
+
/** The person's own API key (a key doc that holds a plan sign-in is their plan). */
|
|
113
|
+
async ownKey(id, provider) {
|
|
114
|
+
const d = await this.docs.read(id, keyDocName(provider));
|
|
115
|
+
return d && !d.chatgpt ? d : null;
|
|
112
116
|
}
|
|
113
|
-
/** The person's
|
|
114
|
-
async
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
+
/** The person's plan sign-in and the doc it is kept in (the key's, for a sign-in from before). */
|
|
118
|
+
async ownPlan(id, provider) {
|
|
119
|
+
for (const name of [planDocName(provider), keyDocName(provider)]) {
|
|
120
|
+
const doc = await this.docs.read(id, name);
|
|
121
|
+
if (doc?.chatgpt)
|
|
122
|
+
return { doc: doc, name };
|
|
123
|
+
}
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
/** Which of their own runs: the one they picked while it is set, else the plan, else the key. */
|
|
127
|
+
async ownUse(id, provider) {
|
|
128
|
+
const [p, key, plan] = await Promise.all([
|
|
129
|
+
this.person(id),
|
|
130
|
+
this.ownKey(id, provider),
|
|
131
|
+
this.ownPlan(id, provider),
|
|
132
|
+
]);
|
|
133
|
+
const use = p.own_use?.[provider];
|
|
134
|
+
if (use === 'key' && key)
|
|
135
|
+
return 'key';
|
|
136
|
+
if (use === 'plan' && plan)
|
|
137
|
+
return 'plan';
|
|
138
|
+
return plan ? 'plan' : key ? 'key' : null;
|
|
139
|
+
}
|
|
140
|
+
/** The plan's access token, refreshed first when it is about to run out; null once it fails. */
|
|
141
|
+
async planToken(id, provider) {
|
|
142
|
+
const found = await this.ownPlan(id, provider);
|
|
143
|
+
if (!found)
|
|
117
144
|
return null;
|
|
118
|
-
|
|
119
|
-
return { key: d.api_key, plan: false };
|
|
145
|
+
const { doc: d, name } = found;
|
|
120
146
|
if (!staleChatgpt(d.chatgpt))
|
|
121
|
-
return
|
|
147
|
+
return d.chatgpt.access_token;
|
|
122
148
|
try {
|
|
123
149
|
const t = await refreshChatgpt(d.chatgpt, this.o.fetch ?? fetch);
|
|
124
|
-
await this.docs.write(id,
|
|
125
|
-
|
|
126
|
-
api_key: t.access_token,
|
|
127
|
-
chatgpt: t,
|
|
128
|
-
});
|
|
129
|
-
return { key: t.access_token, plan: true };
|
|
150
|
+
await this.docs.write(id, name, { ...d, api_key: t.access_token, chatgpt: t });
|
|
151
|
+
return t.access_token;
|
|
130
152
|
}
|
|
131
153
|
catch (err) {
|
|
132
|
-
await this.docs.write(id,
|
|
154
|
+
await this.docs.write(id, name, {
|
|
133
155
|
...d,
|
|
134
156
|
test: { at: Date.now(), ok: false, detail: err.message },
|
|
135
157
|
});
|
|
136
158
|
return null;
|
|
137
159
|
}
|
|
138
160
|
}
|
|
161
|
+
/** The person's own key or plan, the one in use first, the other when that one fails. */
|
|
162
|
+
async ownApiKey(id, provider) {
|
|
163
|
+
const use = await this.ownUse(id, provider);
|
|
164
|
+
if (!use)
|
|
165
|
+
return null;
|
|
166
|
+
const key = async () => {
|
|
167
|
+
const d = await this.ownKey(id, provider);
|
|
168
|
+
return d ? { key: d.api_key, plan: false } : null;
|
|
169
|
+
};
|
|
170
|
+
const plan = async () => {
|
|
171
|
+
const t = await this.planToken(id, provider);
|
|
172
|
+
return t ? { key: t, plan: true } : null;
|
|
173
|
+
};
|
|
174
|
+
return use === 'key' ? ((await key()) ?? (await plan())) : ((await plan()) ?? (await key()));
|
|
175
|
+
}
|
|
139
176
|
/** USD a month on the person's own keys. */
|
|
140
177
|
async ownCap(id) {
|
|
141
178
|
const p = await this.person(id);
|
|
@@ -25,6 +25,13 @@ export interface AssistantStore {
|
|
|
25
25
|
/** Creates the table. Safe to call often. */
|
|
26
26
|
init(): Promise<void>;
|
|
27
27
|
get(scope: string, key: string): Promise<StoredDoc | null>;
|
|
28
|
+
/** Several documents in one call, in the order asked (null where missing). Optional: a store
|
|
29
|
+
* across a network hop (a Durable Object stub) gives it so a request's reads in one tick are one
|
|
30
|
+
* call; `Docs.cached` uses it when present and `get` otherwise. */
|
|
31
|
+
getMany?(refs: {
|
|
32
|
+
scope: string;
|
|
33
|
+
key: string;
|
|
34
|
+
}[]): Promise<(StoredDoc | null)[]>;
|
|
28
35
|
put(scope: string, key: string, value: string, meta?: string | null): Promise<void>;
|
|
29
36
|
remove(scope: string, key: string): Promise<void>;
|
|
30
37
|
/** Keys starting with `prefix`, newest first, without their values. */
|
|
@@ -77,6 +84,10 @@ export declare class Docs {
|
|
|
77
84
|
private open;
|
|
78
85
|
private id;
|
|
79
86
|
private fresh;
|
|
87
|
+
/** Reads asked in the same tick, waiting to go as one `getMany` call. */
|
|
88
|
+
private batch;
|
|
89
|
+
/** One stored doc, joined with the other reads of this tick when the store can batch them. */
|
|
90
|
+
private batched;
|
|
80
91
|
read<T>(scope: string, key: string): Promise<T | null>;
|
|
81
92
|
write<T>(scope: string, key: string, value: T, meta?: string): Promise<void>;
|
|
82
93
|
/** Read (from the store, never the request's copy), change and write back; `change` returning
|
package/dist/assistant/store.js
CHANGED
|
@@ -55,6 +55,24 @@ export function sqlAssistantStore(sql) {
|
|
|
55
55
|
}
|
|
56
56
|
: null;
|
|
57
57
|
},
|
|
58
|
+
async getMany(refs) {
|
|
59
|
+
await init();
|
|
60
|
+
if (!refs.length)
|
|
61
|
+
return [];
|
|
62
|
+
const rows = await sql.all(`SELECT scope, key, meta, value, at FROM ${TABLE} WHERE ${refs.map(() => '(scope = ? AND key = ?)').join(' OR ')}`, ...refs.flatMap((r) => [r.scope, r.key]));
|
|
63
|
+
const by = new Map(rows.map((r) => [`${r.scope}\u0000${r.key}`, r]));
|
|
64
|
+
return refs.map(({ scope, key }) => {
|
|
65
|
+
const r = by.get(`${scope}\u0000${key}`);
|
|
66
|
+
return r
|
|
67
|
+
? {
|
|
68
|
+
key: String(r.key),
|
|
69
|
+
meta: r.meta == null ? null : String(r.meta),
|
|
70
|
+
value: String(r.value),
|
|
71
|
+
at: Number(r.at),
|
|
72
|
+
}
|
|
73
|
+
: null;
|
|
74
|
+
});
|
|
75
|
+
},
|
|
58
76
|
async put(scope, key, value, meta = null) {
|
|
59
77
|
await init();
|
|
60
78
|
await sql.run(`INSERT INTO ${TABLE} (scope, key, meta, value, at) VALUES (?, ?, ?, ?, ?) ON CONFLICT (scope, key) DO UPDATE SET meta = excluded.meta, value = excluded.value, at = excluded.at`, scope, key, meta, value, Date.now());
|
|
@@ -108,12 +126,33 @@ export class Docs {
|
|
|
108
126
|
const d = await this.store.get(scope, key);
|
|
109
127
|
return d ? this.open(d.value) : null;
|
|
110
128
|
}
|
|
129
|
+
/** Reads asked in the same tick, waiting to go as one `getMany` call. */
|
|
130
|
+
batch = null;
|
|
131
|
+
/** One stored doc, joined with the other reads of this tick when the store can batch them. */
|
|
132
|
+
batched(scope, key) {
|
|
133
|
+
const many = this.store.getMany?.bind(this.store);
|
|
134
|
+
if (!many)
|
|
135
|
+
return this.fresh(scope, key);
|
|
136
|
+
const doc = new Promise((resolve, reject) => {
|
|
137
|
+
if (!this.batch) {
|
|
138
|
+
const queue = [];
|
|
139
|
+
this.batch = queue;
|
|
140
|
+
queueMicrotask(() => {
|
|
141
|
+
this.batch = null;
|
|
142
|
+
const all = Promise.resolve().then(() => many(queue.map(({ scope, key }) => ({ scope, key }))));
|
|
143
|
+
queue.forEach((q, i) => q.done(all.then((docs) => docs[i] ?? null)));
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
this.batch.push({ scope, key, done: (d) => d.then(resolve, reject) });
|
|
147
|
+
});
|
|
148
|
+
return doc.then((d) => (d ? this.open(d.value) : null));
|
|
149
|
+
}
|
|
111
150
|
async read(scope, key) {
|
|
112
151
|
let text;
|
|
113
152
|
if (this.memo) {
|
|
114
153
|
const id = this.id(scope, key);
|
|
115
154
|
const hit = this.memo.get(id);
|
|
116
|
-
text = hit ?? this.
|
|
155
|
+
text = hit ?? this.batched(scope, key);
|
|
117
156
|
if (!hit) {
|
|
118
157
|
this.memo.set(id, text);
|
|
119
158
|
text.catch(() => this.memo?.delete(id));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@columenlabs/lumen-ai",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.94",
|
|
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.94",
|
|
45
|
+
"@columenlabs/lumen-ui-types": "0.1.94",
|
|
46
46
|
"openai": "7.32.0",
|
|
47
47
|
"zod": "^4.3.6"
|
|
48
48
|
},
|