@columenlabs/lumen-ai 0.1.238 → 0.1.240

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 CHANGED
@@ -92,6 +92,7 @@ const assistant = createAssistant({
92
92
  // turn when a ChatGPT plan runs out ('shared': the same model on the app's key, shared spend).
93
93
  // ownWays: async (caller) => ({ openai: ['key', 'plan'] }),
94
94
  // ownFallback: async (caller) => true,
95
+ // credits: (tier) => tier !== 'reviewer', // false: that tier runs on the app's key, never counted against allowance
95
96
  // planFallback: 'shared',
96
97
  });
97
98
  // A check the app always runs on its own key, whoever pays the person's answers:
@@ -205,6 +205,12 @@ export interface AssistantOptions {
205
205
  * its cap or their plan is used up. True by default.
206
206
  */
207
207
  ownFallback?: (caller: Caller) => boolean | Promise<boolean>;
208
+ /**
209
+ * Whether a tier's work counts against the person's allowance (Admin ▸ Models' Credits switch).
210
+ * False: it runs on the app's key whatever the person pays with or has left, and its spend is
211
+ * kept as free (shown, never counted). Left out, every tier counts.
212
+ */
213
+ credits?: (tier: string) => boolean | Promise<boolean>;
208
214
  /**
209
215
  * Who answers when a ChatGPT plan reaches its usage limit mid-turn. 'claude' (default): the
210
216
  * app's smallest Claude on the same payer's Anthropic key. 'shared': the same model on the app's
@@ -273,6 +279,10 @@ export declare class Assistant {
273
279
  mayOwn(caller: Caller, provider: AssistantProvider, way: AssistantOwnWay): Promise<boolean>;
274
280
  /** The app's rules for who pays this person's answers (own ways, fallback). */
275
281
  private payingRules;
282
+ /** True when this tier's work is the app's own (`credits` answers false): the app's key, not counted. */
283
+ private freeTier;
284
+ /** The paying rules for a tier: its own rules, or the app's key uncounted for a free tier. */
285
+ private rulesFor;
276
286
  /**
277
287
  * One turn as a Think agent's plan (`LumenAgent` in `@columenlabs/lumen-ai/agent`): the person's
278
288
  * tier and its model on whoever pays, the tool groups at their access, the kit's rules, the
@@ -246,6 +246,18 @@ export class Assistant {
246
246
  ]);
247
247
  return { ...(ways ? { ways } : {}), fallback, ...(only ? { only } : {}) };
248
248
  }
249
+ /** True when this tier's work is the app's own (`credits` answers false): the app's key, not counted. */
250
+ async freeTier(tier) {
251
+ if (!this.o.credits)
252
+ return false;
253
+ return (await Promise.resolve(this.o.credits(tier)).catch(() => true)) === false;
254
+ }
255
+ /** The paying rules for a tier: its own rules, or the app's key uncounted for a free tier. */
256
+ async rulesFor(caller, tier, only) {
257
+ const free = await this.freeTier(tier);
258
+ const rules = await this.payingRules(caller, free ? 'app' : only);
259
+ return { rules: free ? { ...rules, free: true } : rules, free };
260
+ }
249
261
  // ---------------------------------------------------------------- a turn on Think
250
262
  /**
251
263
  * One turn as a Think agent's plan (`LumenAgent` in `@columenlabs/lumen-ai/agent`): the person's
@@ -263,7 +275,7 @@ export class Assistant {
263
275
  ? this.tierById(input.tier)
264
276
  : undefined;
265
277
  const main = await this.session(caller, lower ?? picked);
266
- const rules = await this.payingRules(caller);
278
+ const { rules, free } = await this.rulesFor(caller, main.tier.id);
267
279
  const { paying, wait } = await this.state.paying(caller.id, main.model.provider, rules);
268
280
  if (!paying) {
269
281
  const code = wait ?? 'not_connected';
@@ -376,7 +388,7 @@ export class Assistant {
376
388
  const onShared = sharedLabel ? end.parts.filter((x) => x.label === sharedLabel) : [];
377
389
  const onPayer = onShared.length ? end.parts.filter((x) => !onShared.includes(x)) : end.parts;
378
390
  await this.state
379
- .addSpend(caller.id, paying.payer, onPayer, { task: 'chat', ms: end.ms })
391
+ .addSpend(caller.id, paying.payer, onPayer, { task: 'chat', ms: end.ms }, free)
380
392
  .catch(() => undefined);
381
393
  if (onShared.length)
382
394
  await this.state
@@ -571,7 +583,7 @@ export class Assistant {
571
583
  this.state.spend(caller.id),
572
584
  this.state.appCap(),
573
585
  this.state.ownCap(caller.id),
574
- this.payingRules(caller).then((r) => this.state.paying(caller.id, mainModel.provider, r)),
586
+ this.rulesFor(caller, main.id).then(({ rules }) => this.state.paying(caller.id, mainModel.provider, rules)),
575
587
  ]);
576
588
  const link = await linkTest;
577
589
  const tiers = await Promise.all(this.tiers.map(async (t) => {
@@ -970,7 +982,8 @@ export class Assistant {
970
982
  async complete(caller, o) {
971
983
  const began = Date.now();
972
984
  const s = await this.session(caller, await this.mainTier(caller, o.task));
973
- const { paying, wait } = await this.state.paying(caller.id, s.model.provider, await this.payingRules(caller, o.payer));
985
+ const { rules, free } = await this.rulesFor(caller, s.tier.id, o.payer);
986
+ const { paying, wait } = await this.state.paying(caller.id, s.model.provider, rules);
974
987
  if (!paying) {
975
988
  const code = wait ?? 'not_connected';
976
989
  throw new AssistantError(waitText(code, s.model.provider, !!this.o.allowance), code, 402);
@@ -1031,10 +1044,7 @@ export class Assistant {
1031
1044
  }
1032
1045
  finally {
1033
1046
  const part = s.meter.part();
1034
- await this.state.addSpend(caller.id, paying.payer, [part], {
1035
- task: o.tag ?? o.task,
1036
- ms: Date.now() - began,
1037
- });
1047
+ await this.state.addSpend(caller.id, paying.payer, [part], { task: o.tag ?? o.task, ms: Date.now() - began }, free);
1038
1048
  const task = this.tasks.find((t) => t.id === o.task);
1039
1049
  await this.state
1040
1050
  .addUsage(caller.id, {
@@ -94,8 +94,11 @@ export interface AllowanceDoc {
94
94
  export interface RequestsDoc {
95
95
  rows: AllowanceRequest[];
96
96
  }
97
+ /** `free`: run on the app's key for a job the app does not count against the person's allowance
98
+ * (`AssistantOptions.credits`). Kept apart so the admin still sees what it cost. */
97
99
  export type SpendRow = UsagePart & {
98
100
  payer: AssistantPayer;
101
+ free?: true;
99
102
  };
100
103
  /** The ways of their own a person may pay with, per provider; a provider left out has none. */
101
104
  export type OwnWays = Partial<Record<AssistantProvider, AssistantOwnWay[]>>;
@@ -107,6 +110,8 @@ export interface PayingRules {
107
110
  fallback?: boolean;
108
111
  /** Only this payer: an app's own check always runs on the shared key (`complete({ payer })`). */
109
112
  only?: AssistantPayer;
113
+ /** The app's key, not counted against the person's allowance and never stopped by it. */
114
+ free?: boolean;
110
115
  }
111
116
  /**
112
117
  * One task's use of one model this month: what a kind of work cost (a chat answer, a background
@@ -250,7 +255,7 @@ export declare class State {
250
255
  addSpend(id: string, payer: AssistantPayer, parts: UsagePart[], at?: {
251
256
  task: string;
252
257
  ms?: number;
253
- }): Promise<void>;
258
+ }, free?: boolean): Promise<void>;
254
259
  /**
255
260
  * Adds one answer's use to a chat or run in this month's history (`usage:YYYY-MM`): one entry a
256
261
  * chat or run, its parts merged, the newest `USAGE_KEPT` kept. `title` names a new entry only;
@@ -92,7 +92,9 @@ export class State {
92
92
  async appBudget(id, spentThisMonth) {
93
93
  const al = this.o.allowance;
94
94
  const used = spentThisMonth ??
95
- (await this.spend(id)).rows.filter((r) => r.payer === 'app').reduce((a, r) => a + r.usd, 0);
95
+ (await this.spend(id)).rows
96
+ .filter((r) => r.payer === 'app' && !r.free)
97
+ .reduce((a, r) => a + r.usd, 0);
96
98
  if (!al)
97
99
  return { total: await this.appCap(), used };
98
100
  const d = await this.allowance(id);
@@ -239,7 +241,7 @@ export class State {
239
241
  * Adds an answer's use to this month's spend, by label and payer. `at` names the task the answer
240
242
  * was for and how long it took, so the month also adds up per task and model (`tasks`).
241
243
  */
242
- async addSpend(id, payer, parts, at) {
244
+ async addSpend(id, payer, parts, at, free = false) {
243
245
  if (!parts.some((p) => p.calls))
244
246
  return;
245
247
  await this.docs.update(id, `spend:${this.month()}`, (d) => {
@@ -275,7 +277,7 @@ export class State {
275
277
  for (const p of parts) {
276
278
  if (!p.calls)
277
279
  continue;
278
- const i = rows.findIndex((r) => r.label === p.label && r.payer === payer);
280
+ const i = rows.findIndex((r) => r.label === p.label && r.payer === payer && !!r.free === free);
279
281
  const prev = i >= 0 ? rows[i] : undefined;
280
282
  const row = prev
281
283
  ? {
@@ -286,7 +288,7 @@ export class State {
286
288
  out: prev.out + p.out,
287
289
  usd: prev.usd + p.usd,
288
290
  }
289
- : { ...p, payer };
291
+ : { ...p, payer, ...(free ? { free: true } : {}) };
290
292
  if (i >= 0)
291
293
  rows[i] = row;
292
294
  else
@@ -294,7 +296,7 @@ export class State {
294
296
  }
295
297
  return tasks.length ? { rows, tasks } : { rows };
296
298
  });
297
- if (payer === 'app' && this.o.allowance?.period === 'once') {
299
+ if (payer === 'app' && !free && this.o.allowance?.period === 'once') {
298
300
  const usd = parts.reduce((a, p) => a + p.usd, 0);
299
301
  if (usd > 0)
300
302
  await this.setAllowance(id, (d) => ({ ...d, spent_usd: (d.spent_usd ?? 0) + usd }));
@@ -376,7 +378,7 @@ export class State {
376
378
  this.spend(id),
377
379
  this.ownCap(id),
378
380
  ]);
379
- const spent = (p) => spend.rows.filter((r) => r.payer === p).reduce((a, r) => a + r.usd, 0);
381
+ const spent = (p) => spend.rows.filter((r) => r.payer === p && !r.free).reduce((a, r) => a + r.usd, 0);
380
382
  const budget = await this.appBudget(id, spent('app'));
381
383
  const app = appKeys.keys[provider];
382
384
  const own = ownKeys.keys[provider];
@@ -387,7 +389,8 @@ export class State {
387
389
  apiKey: app,
388
390
  ...appKeys,
389
391
  cap: budget.total,
390
- left: budget.total - budget.used,
392
+ // A free job runs on the app's key whatever the person's allowance holds.
393
+ left: rules.free ? Number.POSITIVE_INFINITY : budget.total - budget.used,
391
394
  }
392
395
  : null,
393
396
  own: own
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@columenlabs/lumen-ai",
3
- "version": "0.1.238",
3
+ "version": "0.1.240",
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",
@@ -52,8 +52,8 @@
52
52
  "@ai-sdk/openai": "4.0.108",
53
53
  "@anthropic-ai/sdk": "0.133.0",
54
54
  "@cloudflare/think": "0.23.0",
55
- "@columenlabs/lumen-ops": "0.1.238",
56
- "@columenlabs/lumen-ui-types": "0.1.238",
55
+ "@columenlabs/lumen-ops": "0.1.240",
56
+ "@columenlabs/lumen-ui-types": "0.1.240",
57
57
  "agents": "0.32.0",
58
58
  "ai": "7.0.162",
59
59
  "just-bash": "3.6.0",