@columenlabs/lumen-ai 0.1.77 → 0.1.78
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/README.md +7 -4
- package/dist/assistant/chatgpt.d.ts +61 -0
- package/dist/assistant/chatgpt.js +165 -0
- package/dist/assistant/engine.d.ts +25 -8
- package/dist/assistant/engine.js +337 -70
- package/dist/assistant/index.d.ts +3 -3
- package/dist/assistant/index.js +3 -3
- package/dist/assistant/models.d.ts +16 -2
- package/dist/assistant/models.js +46 -3
- package/dist/assistant/routes.js +47 -33
- package/dist/assistant/state.d.ts +45 -14
- package/dist/assistant/state.js +93 -23
- package/package.json +6 -5
package/dist/assistant/models.js
CHANGED
|
@@ -3,6 +3,7 @@ export const CLAUDE_MODELS = [
|
|
|
3
3
|
{
|
|
4
4
|
id: 'claude-haiku-5-5',
|
|
5
5
|
label: 'Claude Haiku 5.5',
|
|
6
|
+
provider: 'anthropic',
|
|
6
7
|
group: 'Claude',
|
|
7
8
|
in: 0.1,
|
|
8
9
|
out: 0.5,
|
|
@@ -11,13 +12,53 @@ export const CLAUDE_MODELS = [
|
|
|
11
12
|
{
|
|
12
13
|
id: 'claude-sonnet-5-5',
|
|
13
14
|
label: 'Claude Sonnet 5.5',
|
|
15
|
+
provider: 'anthropic',
|
|
14
16
|
group: 'Claude',
|
|
15
17
|
in: 2,
|
|
16
18
|
out: 10,
|
|
17
19
|
read: 0.2,
|
|
18
20
|
},
|
|
19
|
-
{
|
|
21
|
+
{
|
|
22
|
+
id: 'claude-opus-5-5',
|
|
23
|
+
label: 'Claude Opus 5.5',
|
|
24
|
+
provider: 'anthropic',
|
|
25
|
+
group: 'Claude',
|
|
26
|
+
in: 4,
|
|
27
|
+
out: 20,
|
|
28
|
+
read: 0.2,
|
|
29
|
+
},
|
|
20
30
|
];
|
|
31
|
+
/** OpenAI models, smallest first (list prices from OpenAI's pricing page, October 2026). */
|
|
32
|
+
export const OPENAI_MODELS = [
|
|
33
|
+
['gpt-6-luna', 'GPT-6 Luna', 0.1, 0.5, 0.01],
|
|
34
|
+
['gpt-6.1-terra', 'GPT-6.1 Terra', 1, 5, 0.05],
|
|
35
|
+
['gpt-6.1-sol', 'GPT-6.1 Sol', 2, 10, 0.1],
|
|
36
|
+
['gpt-6-astra', 'GPT-6 Astra', 10, 50, 1],
|
|
37
|
+
].map(([id, label, i, o, r]) => ({
|
|
38
|
+
id: id,
|
|
39
|
+
label: label,
|
|
40
|
+
provider: 'openai',
|
|
41
|
+
group: 'OpenAI',
|
|
42
|
+
in: i,
|
|
43
|
+
out: o,
|
|
44
|
+
read: r,
|
|
45
|
+
}));
|
|
46
|
+
/** Every model the kit knows: what a tier may run unless the app lists its own. */
|
|
47
|
+
export const ALL_MODELS = [...CLAUDE_MODELS, ...OPENAI_MODELS];
|
|
48
|
+
/** How Settings names each provider, how its keys start and where a person makes one. */
|
|
49
|
+
export const PROVIDERS = {
|
|
50
|
+
anthropic: {
|
|
51
|
+
label: 'Anthropic',
|
|
52
|
+
prefix: 'sk-ant-',
|
|
53
|
+
keysUrl: 'https://console.anthropic.com/settings/keys',
|
|
54
|
+
},
|
|
55
|
+
openai: {
|
|
56
|
+
label: 'OpenAI',
|
|
57
|
+
prefix: 'sk-',
|
|
58
|
+
keysUrl: 'https://platform.openai.com/api-keys',
|
|
59
|
+
signin: 'chatgpt',
|
|
60
|
+
},
|
|
61
|
+
};
|
|
21
62
|
/** Output tokens per model call when a tier sets none. */
|
|
22
63
|
export const DEFAULT_OUT = 16_000;
|
|
23
64
|
/** Tokens in (cache reads and writes included) and out of one reply, and its cost at list price. */
|
|
@@ -48,13 +89,15 @@ export class Meter {
|
|
|
48
89
|
this.model = model;
|
|
49
90
|
this.label = label;
|
|
50
91
|
}
|
|
51
|
-
|
|
92
|
+
/** `free`: the call ran on a plan sign-in, so it counts tokens and no dollars. */
|
|
93
|
+
add(usage, free = false) {
|
|
52
94
|
const p = priced(this.model, usage);
|
|
53
95
|
this.calls += 1;
|
|
54
96
|
this.in += p.in;
|
|
55
97
|
this.read += p.read;
|
|
56
98
|
this.out += p.out;
|
|
57
|
-
|
|
99
|
+
if (!free)
|
|
100
|
+
this.usd += p.usd;
|
|
58
101
|
}
|
|
59
102
|
part() {
|
|
60
103
|
const { label, calls, read, out, usd } = this;
|
package/dist/assistant/routes.js
CHANGED
|
@@ -6,10 +6,14 @@
|
|
|
6
6
|
*
|
|
7
7
|
* GET /status what the panel and Settings show
|
|
8
8
|
* PUT /settings the person's tiers, tasks, hand off, advice, tool access, web, payer
|
|
9
|
-
* PUT /app {cap_usd} admin: what each person may spend a month on the app's
|
|
10
|
-
* PUT /key {api_key} admin: the app's
|
|
11
|
-
*
|
|
12
|
-
*
|
|
9
|
+
* PUT /app {cap_usd} admin: what each person may spend a month on the app's keys
|
|
10
|
+
* PUT /key {api_key} admin: the app's key of `?provider=` (anthropic by default,
|
|
11
|
+
* or openai), tested before it is kept
|
|
12
|
+
* DELETE /key, POST /key/test admin, `?provider=` the same
|
|
13
|
+
* PUT /my-key {api_key}, DELETE /my-key, POST /my-key/test the person's own, `?provider=`
|
|
14
|
+
* PUT /my-key/cap {cap_usd} what the person may spend a month on their own keys
|
|
15
|
+
* POST /my-signin starts a ChatGPT plan sign-in (`?provider=openai`): {url, redirect}
|
|
16
|
+
* POST /my-signin/finish {url} finishes it from the 127.0.0.1 address the sign-in ended on
|
|
13
17
|
* PUT /web/scrapeql {client_id, client_secret}, DELETE /web/scrapeql, POST /web/test admin
|
|
14
18
|
* POST /chat one answer as server-sent events; closing the request stops it
|
|
15
19
|
* GET /chats, GET|DELETE /chats/:id
|
|
@@ -21,6 +25,8 @@
|
|
|
21
25
|
*/
|
|
22
26
|
import { ASSISTANT_ACCESS, ASSISTANT_EFFORTS, ASSISTANT_PAYERS, HAND_OFF_POLICIES, } from '@columenlabs/lumen-ui-types/assistant';
|
|
23
27
|
import { AssistantError } from './engine.js';
|
|
28
|
+
import { PROVIDERS } from './models.js';
|
|
29
|
+
import { keyDocName } from './state.js';
|
|
24
30
|
import { APP_SCOPE } from './store.js';
|
|
25
31
|
const MUTATING = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);
|
|
26
32
|
const MAX_MESSAGE = 20_000;
|
|
@@ -137,6 +143,13 @@ export function assistantRoutes(a, o) {
|
|
|
137
143
|
return fail('Sign in first.', 403, 'forbidden');
|
|
138
144
|
const admin = () => (caller.admin ? null : fail('Admins only.', 403, 'forbidden'));
|
|
139
145
|
const status = async () => json(await a.status(caller));
|
|
146
|
+
const asked = url.searchParams.get('provider') ?? 'anthropic';
|
|
147
|
+
const provider = a.providers.find((p) => p === asked);
|
|
148
|
+
const keyRoute = /^(PUT|DELETE|POST) \/(my-)?(key|signin)(\/test|\/finish)?$/.test(route);
|
|
149
|
+
if (keyRoute && !provider)
|
|
150
|
+
return fail(`This app runs no ${asked} models.`, 404);
|
|
151
|
+
const prov = provider ?? 'anthropic';
|
|
152
|
+
const docName = keyDocName(prov);
|
|
140
153
|
try {
|
|
141
154
|
switch (route) {
|
|
142
155
|
case 'GET /status':
|
|
@@ -169,31 +182,21 @@ export function assistantRoutes(a, o) {
|
|
|
169
182
|
}
|
|
170
183
|
const b = await body(req);
|
|
171
184
|
const key = str(b.api_key, 400);
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
return fail(`cap_usd is a number of dollars, 0 to ${MAX_CAP}.`);
|
|
177
|
-
const test = await a.testKey(key);
|
|
185
|
+
const { prefix, label } = PROVIDERS[prov];
|
|
186
|
+
if (!key?.startsWith(prefix))
|
|
187
|
+
return fail(`Paste the ${label} key (${prefix}…).`);
|
|
188
|
+
const test = await a.testKey(prov, key);
|
|
178
189
|
if (!test.ok)
|
|
179
190
|
return fail(test.detail, 400, 'key_refused');
|
|
180
191
|
const scope = mine ? caller.id : APP_SCOPE;
|
|
181
|
-
await docs.
|
|
182
|
-
api_key: key,
|
|
183
|
-
saved_at: Date.now(),
|
|
184
|
-
test,
|
|
185
|
-
...(mine ? { cap_usd: c ?? d?.cap_usd ?? a.state.defaultCap } : {}),
|
|
186
|
-
}));
|
|
192
|
+
await docs.write(scope, docName, { api_key: key, saved_at: Date.now(), test });
|
|
187
193
|
return status();
|
|
188
194
|
}
|
|
189
195
|
case 'PUT /my-key/cap': {
|
|
190
196
|
const c = cap((await body(req)).cap_usd);
|
|
191
197
|
if (c === null)
|
|
192
198
|
return fail(`cap_usd is a number of dollars, 0 to ${MAX_CAP}.`);
|
|
193
|
-
|
|
194
|
-
if (!d)
|
|
195
|
-
return fail('Add your key first.', 404);
|
|
196
|
-
await docs.write(caller.id, 'key', { ...d, cap_usd: c });
|
|
199
|
+
await a.state.setPerson(caller.id, (d) => ({ ...d, own_cap: c }));
|
|
197
200
|
return status();
|
|
198
201
|
}
|
|
199
202
|
case 'DELETE /key':
|
|
@@ -204,25 +207,36 @@ export function assistantRoutes(a, o) {
|
|
|
204
207
|
if (no)
|
|
205
208
|
return no;
|
|
206
209
|
}
|
|
207
|
-
await docs.remove(mine ? caller.id : APP_SCOPE,
|
|
210
|
+
await docs.remove(mine ? caller.id : APP_SCOPE, docName);
|
|
211
|
+
return status();
|
|
212
|
+
}
|
|
213
|
+
case 'POST /my-signin':
|
|
214
|
+
return json(await a.startSignin(caller, prov));
|
|
215
|
+
case 'POST /my-signin/finish': {
|
|
216
|
+
const pasted = str((await body(req)).url, 4000);
|
|
217
|
+
if (!pasted)
|
|
218
|
+
return fail('Paste the address the sign-in ended on.');
|
|
219
|
+
await a.finishSignin(caller, prov, pasted);
|
|
208
220
|
return status();
|
|
209
221
|
}
|
|
210
|
-
case 'POST /key/test':
|
|
211
222
|
case 'POST /my-key/test': {
|
|
212
|
-
const
|
|
213
|
-
if (!
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
const
|
|
220
|
-
|
|
223
|
+
const test = await a.testOwn(caller, prov);
|
|
224
|
+
if (!test)
|
|
225
|
+
return fail('No key is set.', 404);
|
|
226
|
+
await docs.update(caller.id, docName, (d) => (d ? { ...d, test } : null));
|
|
227
|
+
return json(test);
|
|
228
|
+
}
|
|
229
|
+
case 'POST /key/test': {
|
|
230
|
+
const no = admin();
|
|
231
|
+
if (no)
|
|
232
|
+
return no;
|
|
233
|
+
const d = await docs.read(APP_SCOPE, docName);
|
|
234
|
+
const key = d?.api_key ?? (await a.state.appApiKey(prov))?.key;
|
|
221
235
|
if (!key)
|
|
222
236
|
return fail('No key is set.', 404);
|
|
223
|
-
const test = await a.testKey(key);
|
|
237
|
+
const test = await a.testKey(prov, key);
|
|
224
238
|
if (d)
|
|
225
|
-
await docs.write(
|
|
239
|
+
await docs.write(APP_SCOPE, docName, { ...d, test });
|
|
226
240
|
return json(test);
|
|
227
241
|
}
|
|
228
242
|
case 'PUT /web/scrapeql': {
|
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The assistant's settings, keys and spend, as stored documents.
|
|
3
3
|
*
|
|
4
|
-
* app (the app's scope) the cap per person on the app's
|
|
4
|
+
* app (the app's scope) the cap per person on the app's keys
|
|
5
5
|
* key (the app's scope) the app's Anthropic key, tested before it is kept, sealed
|
|
6
|
+
* key:<p> (the app's scope) the app's key of another provider (`key:openai`), the same
|
|
6
7
|
* scrapeql (the app's scope) ScrapeQL's Access service token, tested before it is kept, sealed
|
|
7
|
-
* settings (a person's scope) their tiers, start tiers, hand off, advice, tool access, web, payer
|
|
8
|
-
*
|
|
8
|
+
* settings (a person's scope) their tiers, start tiers, hand off, advice, tool access, web, payer,
|
|
9
|
+
* and the monthly cap on their own keys
|
|
10
|
+
* key, key:<p> (a person's) their own key of each provider, sealed; OpenAI's may be a ChatGPT
|
|
11
|
+
* plan sign-in, refreshed before it runs out
|
|
12
|
+
* signin:<p> (a person's) the app's sign-in registration for them, kept across sign-outs
|
|
9
13
|
* spend:YYYY-MM what each tier cost them this month, by payer
|
|
10
14
|
*
|
|
11
|
-
* Who pays: the person's pick (the app's
|
|
12
|
-
* the other; with neither the assistant waits and says
|
|
15
|
+
* Who pays: the person's pick (the app's keys by default) while it has a key for the provider the
|
|
16
|
+
* tier runs on and money left this month, else the other; with neither the assistant waits and says
|
|
17
|
+
* why. A cap covers every provider of its payer.
|
|
13
18
|
*/
|
|
14
|
-
import type { AssistantAccess, AssistantEffort, AssistantPayer, AssistantWait, HandOffPolicy, UsagePart } from '@columenlabs/lumen-ui-types/assistant';
|
|
19
|
+
import type { AssistantAccess, AssistantEffort, AssistantPayer, AssistantProvider, AssistantWait, HandOffPolicy, UsagePart } from '@columenlabs/lumen-ui-types/assistant';
|
|
20
|
+
import { type ChatgptTokens } from './chatgpt.js';
|
|
15
21
|
import { type Docs } from './store.js';
|
|
16
22
|
export interface KeyTest {
|
|
17
23
|
at: number;
|
|
@@ -22,9 +28,13 @@ export interface KeyDoc {
|
|
|
22
28
|
api_key: string;
|
|
23
29
|
saved_at: number;
|
|
24
30
|
test: KeyTest | null;
|
|
25
|
-
/** USD a month:
|
|
31
|
+
/** USD a month: a person's own Anthropic key before the cap moved to their settings. */
|
|
26
32
|
cap_usd?: number;
|
|
33
|
+
/** A ChatGPT plan sign-in in place of an API key (`api_key` is then its access token). */
|
|
34
|
+
chatgpt?: ChatgptTokens;
|
|
27
35
|
}
|
|
36
|
+
/** Where a provider's key is kept: `key` for Anthropic, `key:<provider>` for the rest. */
|
|
37
|
+
export declare const keyDocName: (provider: AssistantProvider) => string;
|
|
28
38
|
export interface TokenDoc {
|
|
29
39
|
client_id: string;
|
|
30
40
|
client_secret: string;
|
|
@@ -47,6 +57,8 @@ export interface PersonDoc {
|
|
|
47
57
|
access?: Record<string, AssistantAccess>;
|
|
48
58
|
web?: boolean;
|
|
49
59
|
pay?: AssistantPayer;
|
|
60
|
+
/** USD a month on their own keys. */
|
|
61
|
+
own_cap?: number;
|
|
50
62
|
}
|
|
51
63
|
export type SpendRow = UsagePart & {
|
|
52
64
|
payer: AssistantPayer;
|
|
@@ -63,12 +75,18 @@ export declare function keyState(d: KeyDoc | TokenDoc | null, secret: (d: KeyDoc
|
|
|
63
75
|
} | {
|
|
64
76
|
set: true;
|
|
65
77
|
hint: string;
|
|
78
|
+
via?: "chatgpt" | undefined;
|
|
66
79
|
saved_at: number;
|
|
67
80
|
test?: KeyTest | undefined;
|
|
68
81
|
};
|
|
69
82
|
export interface Paying {
|
|
70
83
|
payer: AssistantPayer;
|
|
84
|
+
/** The key of the provider asked for. */
|
|
71
85
|
apiKey: string;
|
|
86
|
+
/** The payer's key of every provider it has one for (a hand-off may run on another). */
|
|
87
|
+
keys: Partial<Record<AssistantProvider, string>>;
|
|
88
|
+
/** Providers whose key is a plan sign-in: their use costs the payer nothing. */
|
|
89
|
+
plans: Partial<Record<AssistantProvider, boolean>>;
|
|
72
90
|
/** USD left this month under the payer's cap. */
|
|
73
91
|
left: number;
|
|
74
92
|
cap: number;
|
|
@@ -79,7 +97,8 @@ export declare class State {
|
|
|
79
97
|
constructor(docs: Docs, o: {
|
|
80
98
|
timeZone: string;
|
|
81
99
|
defaultCap: number;
|
|
82
|
-
appKey?: () => Promise<string | null | undefined>;
|
|
100
|
+
appKey?: (provider: AssistantProvider) => Promise<string | null | undefined>;
|
|
101
|
+
fetch?: typeof fetch;
|
|
83
102
|
});
|
|
84
103
|
/** USD a month when nobody set a cap. */
|
|
85
104
|
get defaultCap(): number;
|
|
@@ -88,22 +107,34 @@ export declare class State {
|
|
|
88
107
|
setPerson(id: string, change: (d: PersonDoc) => PersonDoc): Promise<PersonDoc | null>;
|
|
89
108
|
app(): Promise<AppDoc>;
|
|
90
109
|
setApp(change: (d: AppDoc) => AppDoc): Promise<AppDoc | null>;
|
|
91
|
-
appKey(): Promise<KeyDoc | null>;
|
|
110
|
+
appKey(provider: AssistantProvider): Promise<KeyDoc | null>;
|
|
92
111
|
/** The app's key: the stored one, else the app's fallback (a Worker secret). */
|
|
93
|
-
appApiKey(): Promise<{
|
|
112
|
+
appApiKey(provider: AssistantProvider): Promise<{
|
|
94
113
|
key: string;
|
|
95
114
|
fromEnv: boolean;
|
|
96
115
|
} | null>;
|
|
97
|
-
ownKey(id: string): Promise<KeyDoc | null>;
|
|
116
|
+
ownKey(id: string, provider: AssistantProvider): Promise<KeyDoc | null>;
|
|
117
|
+
/** The person's own key, a plan sign-in refreshed first when it is about to run out. */
|
|
118
|
+
ownApiKey(id: string, provider: AssistantProvider): Promise<{
|
|
119
|
+
key: string;
|
|
120
|
+
plan: boolean;
|
|
121
|
+
} | null>;
|
|
122
|
+
/** USD a month on the person's own keys. */
|
|
123
|
+
ownCap(id: string): Promise<number>;
|
|
98
124
|
scrapeql(): Promise<TokenDoc | null>;
|
|
99
125
|
appCap(): Promise<number>;
|
|
100
126
|
spend(id: string, month?: string): Promise<SpendDoc>;
|
|
101
127
|
/** Adds an answer's use to this month's spend, by label and payer. */
|
|
102
128
|
addSpend(id: string, payer: AssistantPayer, parts: UsagePart[]): Promise<void>;
|
|
103
|
-
/**
|
|
104
|
-
|
|
129
|
+
/** The payer's key of each provider, those set, and which are plan sign-ins. */
|
|
130
|
+
keysOf(id: string, payer: AssistantPayer): Promise<{
|
|
131
|
+
keys: Partial<Record<"anthropic" | "openai", string>>;
|
|
132
|
+
plans: Partial<Record<"anthropic" | "openai", boolean>>;
|
|
133
|
+
}>;
|
|
134
|
+
/** Who pays the person's next answer on `provider`'s model, or why nobody can. */
|
|
135
|
+
paying(id: string, provider: AssistantProvider): Promise<{
|
|
105
136
|
paying: Paying | null;
|
|
106
137
|
wait: AssistantWait | null;
|
|
107
138
|
}>;
|
|
108
139
|
}
|
|
109
|
-
export declare
|
|
140
|
+
export declare function waitText(wait: AssistantWait, provider: AssistantProvider): string;
|
package/dist/assistant/state.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { refreshChatgpt, staleChatgpt } from './chatgpt.js';
|
|
2
|
+
import { PROVIDERS, keyHint } from './models.js';
|
|
2
3
|
import { APP_SCOPE } from './store.js';
|
|
4
|
+
/** Where a provider's key is kept: `key` for Anthropic, `key:<provider>` for the rest. */
|
|
5
|
+
export const keyDocName = (provider) => provider === 'anthropic' ? 'key' : `key:${provider}`;
|
|
3
6
|
/** USD a month when nobody set a cap. */
|
|
4
7
|
export const DEFAULT_CAP = 10;
|
|
5
8
|
/** `2026-10` in the app's time zone. */
|
|
@@ -11,9 +14,11 @@ export function monthIn(timeZone, at = new Date()) {
|
|
|
11
14
|
export function keyState(d, secret) {
|
|
12
15
|
if (!d)
|
|
13
16
|
return { set: false };
|
|
17
|
+
const plan = 'chatgpt' in d ? d.chatgpt : undefined;
|
|
14
18
|
return {
|
|
15
19
|
set: true,
|
|
16
|
-
hint: keyHint(secret(d)),
|
|
20
|
+
hint: plan ? (plan.email ?? 'ChatGPT plan') : keyHint(secret(d)),
|
|
21
|
+
...(plan ? { via: 'chatgpt' } : {}),
|
|
17
22
|
saved_at: d.saved_at,
|
|
18
23
|
...(d.test ? { test: d.test } : {}),
|
|
19
24
|
};
|
|
@@ -44,19 +49,52 @@ export class State {
|
|
|
44
49
|
setApp(change) {
|
|
45
50
|
return this.docs.update(APP_SCOPE, 'app', (d) => change(d ?? {}));
|
|
46
51
|
}
|
|
47
|
-
appKey() {
|
|
48
|
-
return this.docs.read(APP_SCOPE,
|
|
52
|
+
appKey(provider) {
|
|
53
|
+
return this.docs.read(APP_SCOPE, keyDocName(provider));
|
|
49
54
|
}
|
|
50
55
|
/** The app's key: the stored one, else the app's fallback (a Worker secret). */
|
|
51
|
-
async appApiKey() {
|
|
52
|
-
const d = await this.appKey();
|
|
56
|
+
async appApiKey(provider) {
|
|
57
|
+
const d = await this.appKey(provider);
|
|
53
58
|
if (d)
|
|
54
59
|
return { key: d.api_key, fromEnv: false };
|
|
55
|
-
const env = await this.o.appKey?.();
|
|
60
|
+
const env = await this.o.appKey?.(provider);
|
|
56
61
|
return env ? { key: env, fromEnv: true } : null;
|
|
57
62
|
}
|
|
58
|
-
ownKey(id) {
|
|
59
|
-
return this.docs.read(id,
|
|
63
|
+
ownKey(id, provider) {
|
|
64
|
+
return this.docs.read(id, keyDocName(provider));
|
|
65
|
+
}
|
|
66
|
+
/** The person's own key, a plan sign-in refreshed first when it is about to run out. */
|
|
67
|
+
async ownApiKey(id, provider) {
|
|
68
|
+
const d = await this.ownKey(id, provider);
|
|
69
|
+
if (!d)
|
|
70
|
+
return null;
|
|
71
|
+
if (!d.chatgpt)
|
|
72
|
+
return { key: d.api_key, plan: false };
|
|
73
|
+
if (!staleChatgpt(d.chatgpt))
|
|
74
|
+
return { key: d.chatgpt.access_token, plan: true };
|
|
75
|
+
try {
|
|
76
|
+
const t = await refreshChatgpt(d.chatgpt, this.o.fetch ?? fetch);
|
|
77
|
+
await this.docs.write(id, keyDocName(provider), {
|
|
78
|
+
...d,
|
|
79
|
+
api_key: t.access_token,
|
|
80
|
+
chatgpt: t,
|
|
81
|
+
});
|
|
82
|
+
return { key: t.access_token, plan: true };
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
await this.docs.write(id, keyDocName(provider), {
|
|
86
|
+
...d,
|
|
87
|
+
test: { at: Date.now(), ok: false, detail: err.message },
|
|
88
|
+
});
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/** USD a month on the person's own keys. */
|
|
93
|
+
async ownCap(id) {
|
|
94
|
+
const p = await this.person(id);
|
|
95
|
+
if (p.own_cap !== undefined)
|
|
96
|
+
return p.own_cap;
|
|
97
|
+
return (await this.ownKey(id, 'anthropic'))?.cap_usd ?? this.o.defaultCap;
|
|
60
98
|
}
|
|
61
99
|
scrapeql() {
|
|
62
100
|
return this.docs.read(APP_SCOPE, 'scrapeql');
|
|
@@ -96,24 +134,53 @@ export class State {
|
|
|
96
134
|
return { rows };
|
|
97
135
|
});
|
|
98
136
|
}
|
|
99
|
-
/**
|
|
100
|
-
async
|
|
101
|
-
const
|
|
137
|
+
/** The payer's key of each provider, those set, and which are plan sign-ins. */
|
|
138
|
+
async keysOf(id, payer) {
|
|
139
|
+
const ids = Object.keys(PROVIDERS);
|
|
140
|
+
const found = await Promise.all(ids.map((p) => payer === 'app'
|
|
141
|
+
? this.appApiKey(p).then((k) => (k ? { key: k.key, plan: false } : null))
|
|
142
|
+
: this.ownApiKey(id, p)));
|
|
143
|
+
const keys = {};
|
|
144
|
+
const plans = {};
|
|
145
|
+
ids.forEach((p, i) => {
|
|
146
|
+
const k = found[i];
|
|
147
|
+
if (k)
|
|
148
|
+
keys[p] = k.key;
|
|
149
|
+
if (k?.plan)
|
|
150
|
+
plans[p] = true;
|
|
151
|
+
});
|
|
152
|
+
return { keys, plans };
|
|
153
|
+
}
|
|
154
|
+
/** Who pays the person's next answer on `provider`'s model, or why nobody can. */
|
|
155
|
+
async paying(id, provider) {
|
|
156
|
+
const [person, appKeys, ownKeys, spend, appCap, ownCap] = await Promise.all([
|
|
102
157
|
this.person(id),
|
|
103
|
-
this.
|
|
104
|
-
this.
|
|
158
|
+
this.keysOf(id, 'app'),
|
|
159
|
+
this.keysOf(id, 'own'),
|
|
105
160
|
this.spend(id),
|
|
106
161
|
this.appCap(),
|
|
162
|
+
this.ownCap(id),
|
|
107
163
|
]);
|
|
108
164
|
const spent = (p) => spend.rows.filter((r) => r.payer === p).reduce((a, r) => a + r.usd, 0);
|
|
165
|
+
const app = appKeys.keys[provider];
|
|
166
|
+
const own = ownKeys.keys[provider];
|
|
109
167
|
const sources = {
|
|
110
|
-
app: app
|
|
168
|
+
app: app
|
|
169
|
+
? {
|
|
170
|
+
payer: 'app',
|
|
171
|
+
apiKey: app,
|
|
172
|
+
...appKeys,
|
|
173
|
+
cap: appCap,
|
|
174
|
+
left: appCap - spent('app'),
|
|
175
|
+
}
|
|
176
|
+
: null,
|
|
111
177
|
own: own
|
|
112
178
|
? {
|
|
113
179
|
payer: 'own',
|
|
114
|
-
apiKey: own
|
|
115
|
-
|
|
116
|
-
|
|
180
|
+
apiKey: own,
|
|
181
|
+
...ownKeys,
|
|
182
|
+
cap: ownCap,
|
|
183
|
+
left: ownCap - spent('own'),
|
|
117
184
|
}
|
|
118
185
|
: null,
|
|
119
186
|
};
|
|
@@ -131,8 +198,11 @@ export class State {
|
|
|
131
198
|
return { paying: null, wait: capped === 'own' ? 'own_cap' : 'app_cap' };
|
|
132
199
|
}
|
|
133
200
|
}
|
|
134
|
-
export
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
201
|
+
export function waitText(wait, provider) {
|
|
202
|
+
const label = PROVIDERS[provider].label;
|
|
203
|
+
if (wait === 'not_connected')
|
|
204
|
+
return `No ${label} key is set: add one in Settings ▸ Model providers, or pick another model in Settings ▸ Models.`;
|
|
205
|
+
if (wait === 'app_cap')
|
|
206
|
+
return "You reached this month's cap on the shared keys. Add your own key, or wait for next month.";
|
|
207
|
+
return 'Your own keys reached their monthly cap: raise it in Settings ▸ Model providers.';
|
|
208
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@columenlabs/lumen-ai",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.78",
|
|
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",
|
|
@@ -40,10 +40,11 @@
|
|
|
40
40
|
"registry": "https://registry.npmjs.org/"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@
|
|
44
|
-
"@columenlabs/lumen-
|
|
45
|
-
"
|
|
46
|
-
"
|
|
43
|
+
"@anthropic-ai/sdk": "0.133.0",
|
|
44
|
+
"@columenlabs/lumen-ops": "0.1.78",
|
|
45
|
+
"@columenlabs/lumen-ui-types": "0.1.78",
|
|
46
|
+
"openai": "7.32.0",
|
|
47
|
+
"zod": "^4.3.6"
|
|
47
48
|
},
|
|
48
49
|
"peerDependencies": {
|
|
49
50
|
"@modelcontextprotocol/sdk": "^1.29.0"
|