@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.
@@ -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
- { id: 'claude-opus-5-5', label: 'Claude Opus 5.5', group: 'Claude', in: 4, out: 20, read: 0.2 },
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
- add(usage) {
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
- this.usd += p.usd;
99
+ if (!free)
100
+ this.usd += p.usd;
58
101
  }
59
102
  part() {
60
103
  const { label, calls, read, out, usd } = this;
@@ -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 key
10
- * PUT /key {api_key} admin: the app's Claude key, tested before it is kept
11
- * DELETE /key, POST /key/test admin
12
- * PUT /my-key {api_key, cap_usd?}, PUT /my-key/cap {cap_usd}, DELETE /my-key, POST /my-key/test
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
- if (!key)
173
- return fail('Paste the key (sk-ant-…).');
174
- const c = b.cap_usd === undefined ? undefined : cap(b.cap_usd);
175
- if (c === null)
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.update(scope, 'key', (d) => ({
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
- const d = await a.state.ownKey(caller.id);
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, 'key');
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 mine = route === 'POST /my-key/test';
213
- if (!mine) {
214
- const no = admin();
215
- if (no)
216
- return no;
217
- }
218
- const scope = mine ? caller.id : APP_SCOPE;
219
- const d = await docs.read(scope, 'key');
220
- const key = d?.api_key ?? (mine ? null : (await a.state.appApiKey())?.key);
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(scope, 'key', { ...d, test });
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 key
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
- * key (a person's scope) their own Anthropic key and its monthly cap, sealed
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 key by default) while it has money left this month, else
12
- * the other; with neither the assistant waits and says why.
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: the person's own key only. */
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
- /** Who pays the person's next answer, or why nobody can. */
104
- paying(id: string): Promise<{
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 const WAIT_TEXT: Record<AssistantWait, string>;
140
+ export declare function waitText(wait: AssistantWait, provider: AssistantProvider): string;
@@ -1,5 +1,8 @@
1
- import { keyHint } from './models.js';
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, 'key');
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, 'key');
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
- /** Who pays the person's next answer, or why nobody can. */
100
- async paying(id) {
101
- const [person, app, own, spend, appCap] = await Promise.all([
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.appApiKey(),
104
- this.ownKey(id),
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 ? { payer: 'app', apiKey: app.key, cap: appCap, left: appCap - spent('app') } : null,
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.api_key,
115
- cap: own.cap_usd ?? this.o.defaultCap,
116
- left: (own.cap_usd ?? this.o.defaultCap) - spent('own'),
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 const WAIT_TEXT = {
135
- not_connected: 'No Claude key is set: an admin adds one in Settings ▸ Model providers.',
136
- app_cap: "You reached this month's cap on the app's key. Add your own key, or wait for next month.",
137
- own_cap: 'Your own key reached its monthly cap: raise it in Settings ▸ Spend.',
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.77",
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
- "@columenlabs/lumen-ops": "0.1.77",
44
- "@columenlabs/lumen-ui-types": "0.1.77",
45
- "zod": "^4.3.6",
46
- "@anthropic-ai/sdk": "0.133.0"
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"