@columenlabs/lumen-ai 0.1.225 → 0.1.227
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 +1 -0
- package/dist/agent/agent.d.ts +13 -1
- package/dist/agent/agent.js +29 -2
- package/dist/agent/stream.d.ts +3 -1
- package/dist/agent/stream.js +17 -1
- package/dist/assistant/engine.d.ts +36 -0
- package/dist/assistant/engine.js +89 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -104,6 +104,7 @@ app.all('/api/assistant/*', (c) => routes(c.req.raw));
|
|
|
104
104
|
- **Chats** run on the agent (`./agent`, `LumenAgent`, below): the app routes `POST /chat` and each chat's reads, cards and Undo to its own agent, and `assistantRoutes` answers `POST /chat` with 410 `moved`. Chats saved by the old turn loop stay readable (`GET /chats`, `GET|DELETE /chats/:id`) until `assistant.purge()` drops them 30 days after their last message.
|
|
105
105
|
- **Screen moves**: while the person's Settings ▸ Screen is Move (the default), the panel sends its page map (`AssistantMessage.screen`: routes, nav pages, Settings sections, cards on screen, rows to open) and `LumenAgent` gives the model a `screen` tool (`./agent` `screen.ts`). Each call's moves (go, settings, chart, open, point) are checked against the map, kept in the chat and streamed as a `screen` event; the panel runs them through `AssistantScreenProvider` (mounted by `LumenShell`) with an Undo chip, and their outcomes go back with the next message (`outcomes`). Nothing is saved by a move.
|
|
106
106
|
- **Background work** an app runs for a person (a score, a summary) is `assistant.complete(caller, { task, system, prompt, images?, maxTokens })`: one answer on the task's tier (`tasks`, so the person picks its model in Settings ▸ Models), no chat and no tools (`images`, base64 with their media type, are read with the prompt: a scanned page, a screenshot), billed to their payer and counted in their spend and allowance like a chat turn. `maxTokens` (default 1,000) goes up to `COMPLETE_MAX_TOKENS` (32,000, thinking included); the answer is streamed, so a long one keeps its connection, and `cut: true` says it stopped at the cap before it ended (the app can ask again, shorter, or continue). It throws `AssistantError` `not_connected`, `app_cap` or `own_cap` when nobody can pay, so the caller waits and tries later. It returns the answer's `usd`, `model` and `usage` (tokens in, cache reads, out, per model). `effort` sets how hard the model thinks on that one call (`low` to `max`; on an OpenAI or xAI model it is the request's reasoning effort), over what the person saved for the tier. `run: { id, title }` makes the calls that share an `id` one line of the person's usage history (every round of one rebuild); without it a task's calls that day share a line. The month's spend also adds up per task and model (`spend:YYYY-MM` `tasks`: answers, calls, tokens, cost and time, a chat answer as `chat`), so an app's admin page can show what each kind of work costs; `tag` counts one call under another name when a task does more than one kind of work (a first try, a repair).
|
|
107
|
+
- **Images** an app makes for a person are `assistant.image(caller, { task, prompt, images?, size?, quality?, run? })`: one image on the payer's OpenAI key (`IMAGE_MODEL`, gpt-image-2; a ChatGPT plan sign-in cannot make one), from `images` when given (a reference, a sketch), billed and capped like `complete` at the model's list price per token. It returns the PNG as base64 with its `usd`. A chat tool that returns `image: { url, alt?, cost? }` shows as an image card in the panel (`StepItem.image`); `LumenAssistant` `imageTool` names that tool (its "Making the image" box while it runs, Make another) and `modes` adds composer toggles (Make an image) whose id goes with the message as `use`.
|
|
107
108
|
- **Usage history.** Each chat (`agentTurn` with `chat`) and each run of background work is one line of the person's month (`usage:YYYY-MM`, the newest 300): its title, first and last use, tokens, cost and its parts per tier and model. `GET /usage?month=YYYY-MM` returns it beside that month's spend and the months that have any; `LumenAssistant`'s footer shows this month's total and opens it (`UsageHistoryView`).
|
|
108
109
|
|
|
109
110
|
### Commands, `@` and `#`
|
package/dist/agent/agent.d.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* An `Assistant` gives both: `assistant.agentTurn(caller, input)` returns a plan and its settle.
|
|
13
13
|
*/
|
|
14
14
|
import { type ChatResponseResult, type PrepareStepContext, type Session, type StepConfig, type StepContext, Think, type ThinkModel, type TurnConfig, type TurnContext } from '@cloudflare/think';
|
|
15
|
-
import { type AssistantChat, type AssistantEffort, type AssistantEvent, type AssistantImage, type AssistantRefNote, type AssistantScreenMap, type AssistantScreenOutcome, type ChatUsage, type UsagePart } from '@columenlabs/lumen-ui-types/assistant';
|
|
15
|
+
import { type AssistantChat, type AssistantEffort, type AssistantEvent, type AssistantImage, type AssistantQuote, type AssistantRefNote, type AssistantScreenMap, type AssistantScreenOutcome, type ChatUsage, type UsagePart } from '@columenlabs/lumen-ui-types/assistant';
|
|
16
16
|
import { type ModelMessage, type ToolSet } from 'ai';
|
|
17
17
|
import type { ModelDef } from '../assistant/models.js';
|
|
18
18
|
import { type AgentConnector, type ConnectorAt } from './connectors.js';
|
|
@@ -134,6 +134,10 @@ export interface AskInput {
|
|
|
134
134
|
note?: unknown;
|
|
135
135
|
};
|
|
136
136
|
refs?: AssistantRefNote[];
|
|
137
|
+
/** The piece of the chat this message replies to (Reply on selected text). */
|
|
138
|
+
quote?: AssistantQuote;
|
|
139
|
+
/** A composer mode the person turned on for this message, by id; the app's plan reads it. */
|
|
140
|
+
use?: string;
|
|
137
141
|
tier?: string;
|
|
138
142
|
time_zone?: string;
|
|
139
143
|
/** What the browser can show (Screen on): the `screen` tool and its rules join the turn. */
|
|
@@ -141,6 +145,14 @@ export interface AskInput {
|
|
|
141
145
|
/** How earlier screen calls ended in the browser. */
|
|
142
146
|
outcomes?: AssistantScreenOutcome[];
|
|
143
147
|
}
|
|
148
|
+
/** A quote as sent, checked: its text cut to `ASSISTANT_QUOTE_MAX`, and whose message it came from. */
|
|
149
|
+
export declare function quoteOf(q: unknown): AssistantQuote | undefined;
|
|
150
|
+
/**
|
|
151
|
+
* The words the model reads before a message that replies to a quote: which message it came from
|
|
152
|
+
* and the quote itself. It is the message's first text part; the panel shows the quote from the
|
|
153
|
+
* message's metadata instead, so `itemsOf` leaves this part out.
|
|
154
|
+
*/
|
|
155
|
+
export declare function quoteLead(q: AssistantQuote): string;
|
|
144
156
|
/**
|
|
145
157
|
* The images a message may carry, as file parts: allowed types only, at most `ASSISTANT_IMAGES_MAX`,
|
|
146
158
|
* and no more than `ASSISTANT_IMAGE_CHARS` of base64 together; the rest are left out.
|
package/dist/agent/agent.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { anthropic } from '@ai-sdk/anthropic';
|
|
15
15
|
import { Think, } from '@cloudflare/think';
|
|
16
|
-
import { ASSISTANT_IMAGES_MAX, ASSISTANT_IMAGE_CHARS, ASSISTANT_IMAGE_TYPES, } from '@columenlabs/lumen-ui-types/assistant';
|
|
16
|
+
import { ASSISTANT_IMAGES_MAX, ASSISTANT_IMAGE_CHARS, ASSISTANT_IMAGE_TYPES, ASSISTANT_QUOTE_MAX, } from '@columenlabs/lumen-ui-types/assistant';
|
|
17
17
|
import { createCompactFunction } from 'agents/sessions';
|
|
18
18
|
import { generateText } from 'ai';
|
|
19
19
|
import { CONNECTOR_GROUP, checkConnectorUrl, connectorGroup, connectorId, connectorSource, } from './connectors.js';
|
|
@@ -73,6 +73,27 @@ export function withLead(messages, lead) {
|
|
|
73
73
|
: [{ type: 'text', text: lead }, ...m.content];
|
|
74
74
|
return [...messages.slice(0, i), { ...m, content }, ...messages.slice(i + 1)];
|
|
75
75
|
}
|
|
76
|
+
/** A quote as sent, checked: its text cut to `ASSISTANT_QUOTE_MAX`, and whose message it came from. */
|
|
77
|
+
export function quoteOf(q) {
|
|
78
|
+
const o = q;
|
|
79
|
+
const text = typeof o?.text === 'string' ? o.text.trim().slice(0, ASSISTANT_QUOTE_MAX) : '';
|
|
80
|
+
if (!text)
|
|
81
|
+
return undefined;
|
|
82
|
+
return { text, from: o?.from === 'user' ? 'user' : 'assistant' };
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* The words the model reads before a message that replies to a quote: which message it came from
|
|
86
|
+
* and the quote itself. It is the message's first text part; the panel shows the quote from the
|
|
87
|
+
* message's metadata instead, so `itemsOf` leaves this part out.
|
|
88
|
+
*/
|
|
89
|
+
export function quoteLead(q) {
|
|
90
|
+
const whose = q.from === 'user' ? 'their own earlier message' : 'your earlier answer';
|
|
91
|
+
const lines = q.text
|
|
92
|
+
.split('\n')
|
|
93
|
+
.map((l) => `> ${l}`)
|
|
94
|
+
.join('\n');
|
|
95
|
+
return `The person replies to this part of ${whose}:\n${lines}\n\n`;
|
|
96
|
+
}
|
|
76
97
|
/**
|
|
77
98
|
* The images a message may carry, as file parts: allowed types only, at most `ASSISTANT_IMAGES_MAX`,
|
|
78
99
|
* and no more than `ASSISTANT_IMAGE_CHARS` of base64 together; the rest are left out.
|
|
@@ -209,10 +230,12 @@ export class LumenAgent extends Think {
|
|
|
209
230
|
this.#put('at', at);
|
|
210
231
|
await sink.event({ event: 'chat', data: { id: this.name, title } });
|
|
211
232
|
this.#outcomes(screenOutcomesOf(input.outcomes));
|
|
233
|
+
const quote = quoteOf(input.quote);
|
|
212
234
|
const meta = {
|
|
213
235
|
at,
|
|
214
236
|
...(input.context ? { context: { label: input.context.label } } : {}),
|
|
215
237
|
...(input.refs?.length ? { refs: input.refs } : {}),
|
|
238
|
+
...(quote ? { quote } : {}),
|
|
216
239
|
...(input.tier ? { tier: input.tier } : {}),
|
|
217
240
|
};
|
|
218
241
|
const toEvents = chunkEvents({
|
|
@@ -237,7 +260,11 @@ export class LumenAgent extends Think {
|
|
|
237
260
|
await this.chat({
|
|
238
261
|
id: userId,
|
|
239
262
|
role: 'user',
|
|
240
|
-
parts: [
|
|
263
|
+
parts: [
|
|
264
|
+
...(quote ? [{ type: 'text', text: quoteLead(quote) }] : []),
|
|
265
|
+
{ type: 'text', text: input.message },
|
|
266
|
+
...imageParts(input.images),
|
|
267
|
+
],
|
|
241
268
|
metadata: meta,
|
|
242
269
|
}, {
|
|
243
270
|
onStart: () => undefined,
|
package/dist/agent/stream.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* panel's `AssistantEvent`s, and a stored chat's messages and cards as its `ChatItem`s. The panel
|
|
4
4
|
* (`@columenlabs/lumen-ui`'s `LumenAssistant`) reads a Think chat the same way as before.
|
|
5
5
|
*/
|
|
6
|
-
import type { AssistantEvent, AssistantRefNote, ChatItem } from '@columenlabs/lumen-ui-types/assistant';
|
|
6
|
+
import type { AssistantEvent, AssistantQuote, AssistantRefNote, ChatItem } from '@columenlabs/lumen-ui-types/assistant';
|
|
7
7
|
import type { AgentCard } from './agent.js';
|
|
8
8
|
import type { ScreenCall } from './screen.js';
|
|
9
9
|
/** A tool's name as a line's title: `notes__find_notes` → `find notes`. */
|
|
@@ -40,6 +40,8 @@ export interface AskMeta {
|
|
|
40
40
|
label: string;
|
|
41
41
|
};
|
|
42
42
|
refs?: AssistantRefNote[];
|
|
43
|
+
/** The piece of the chat the message replies to; its words to the model are the first text part. */
|
|
44
|
+
quote?: AssistantQuote;
|
|
43
45
|
tier?: string;
|
|
44
46
|
}
|
|
45
47
|
/** A stored chat as the panel's items: the messages in order, each tool call as it ended. */
|
package/dist/agent/stream.js
CHANGED
|
@@ -6,6 +6,18 @@ export function stepTitle(name) {
|
|
|
6
6
|
return own.replace(/_/g, ' ');
|
|
7
7
|
}
|
|
8
8
|
const obj = (v) => v && typeof v === 'object' && !Array.isArray(v) ? v : null;
|
|
9
|
+
/** A tool's `image` output as the panel's card: a same-site path or an https address only. */
|
|
10
|
+
function stepImage(v) {
|
|
11
|
+
const i = obj(v);
|
|
12
|
+
const url = typeof i?.url === 'string' ? i.url : '';
|
|
13
|
+
if (!/^(\/[^/\\]|https:\/\/)/.test(url))
|
|
14
|
+
return undefined;
|
|
15
|
+
return {
|
|
16
|
+
url,
|
|
17
|
+
...(typeof i?.alt === 'string' ? { alt: i.alt.slice(0, 300) } : {}),
|
|
18
|
+
...(typeof i?.cost === 'string' ? { cost: i.cost.slice(0, 40) } : {}),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
9
21
|
/** What one finished tool call shows as: a line, a card, a hand-off section or nothing. */
|
|
10
22
|
function settledCall(id, c, out, ms, cards, screens = new Map()) {
|
|
11
23
|
const o = obj(out.output);
|
|
@@ -52,6 +64,7 @@ function settledCall(id, c, out, ms, cards, screens = new Map()) {
|
|
|
52
64
|
}
|
|
53
65
|
const error = out.error ?? (o && typeof o.error === 'string' ? o.error : undefined);
|
|
54
66
|
const rows = rowCount(o && 'result' in o ? o.result : out.output);
|
|
67
|
+
const image = stepImage(o?.image);
|
|
55
68
|
const web = webStep(c.name, c.input, out.output, !!error || obj(out.output)?.ok === false);
|
|
56
69
|
const step = {
|
|
57
70
|
kind: 'step',
|
|
@@ -66,6 +79,7 @@ function settledCall(id, c, out, ms, cards, screens = new Map()) {
|
|
|
66
79
|
? { write: { undo: true, status: card?.status === 'undone' ? 'undone' : 'done' } }
|
|
67
80
|
: {}),
|
|
68
81
|
...(web ? { web } : {}),
|
|
82
|
+
...(image && !error ? { image } : {}),
|
|
69
83
|
};
|
|
70
84
|
return [{ event: 'step', data: step }];
|
|
71
85
|
}
|
|
@@ -137,11 +151,12 @@ export function itemsOf(messages, cards, screens = new Map(), steers = {}) {
|
|
|
137
151
|
for (const m of messages) {
|
|
138
152
|
if (m.role === 'user') {
|
|
139
153
|
asked = m.id;
|
|
154
|
+
const meta = (obj(m.metadata) ?? {});
|
|
140
155
|
const text = m.parts
|
|
141
156
|
.filter((p) => p.type === 'text')
|
|
157
|
+
.slice(meta.quote ? 1 : 0)
|
|
142
158
|
.map((p) => String(p.text ?? ''))
|
|
143
159
|
.join('');
|
|
144
|
-
const meta = (obj(m.metadata) ?? {});
|
|
145
160
|
const images = m.parts
|
|
146
161
|
.filter((p) => p.type === 'file' && typeof p.url === 'string' && p.url.startsWith('data:image/'))
|
|
147
162
|
.map((p) => p.url);
|
|
@@ -152,6 +167,7 @@ export function itemsOf(messages, cards, screens = new Map(), steers = {}) {
|
|
|
152
167
|
...(images.length ? { images } : {}),
|
|
153
168
|
...(meta.context?.label ? { context: meta.context.label } : {}),
|
|
154
169
|
...(meta.refs?.length ? { refs: meta.refs } : {}),
|
|
170
|
+
...(meta.quote ? { quote: meta.quote } : {}),
|
|
155
171
|
...(meta.tier ? { tier: meta.tier } : {}),
|
|
156
172
|
});
|
|
157
173
|
continue;
|
|
@@ -19,6 +19,17 @@ export interface AssistantCommandDef {
|
|
|
19
19
|
}
|
|
20
20
|
/** Who is asking: their id (the store's scope for their things) and whether they are an admin. */
|
|
21
21
|
/** A photo `complete` reads with its prompt: base64 bytes and their media type (`image/jpeg`). */
|
|
22
|
+
/**
|
|
23
|
+
* The model `image` runs and its list price per million tokens (OpenAI, gpt-image-2: text in $5,
|
|
24
|
+
* image in $8, image out $30). `fallbackUsd` is a medium square image, for an answer with no usage.
|
|
25
|
+
*/
|
|
26
|
+
export declare const IMAGE_MODEL: {
|
|
27
|
+
id: string;
|
|
28
|
+
text: number;
|
|
29
|
+
image: number;
|
|
30
|
+
out: number;
|
|
31
|
+
fallbackUsd: number;
|
|
32
|
+
};
|
|
22
33
|
export interface CompleteImage {
|
|
23
34
|
mediaType: string;
|
|
24
35
|
data: string;
|
|
@@ -334,6 +345,31 @@ export declare class Assistant {
|
|
|
334
345
|
cut: boolean;
|
|
335
346
|
usage: UsagePart;
|
|
336
347
|
}>;
|
|
348
|
+
/**
|
|
349
|
+
* One image made as the person, on the payer's OpenAI key (a plan sign-in cannot make one),
|
|
350
|
+
* billed and capped like `complete`: the price is the model's list price per token, read from the
|
|
351
|
+
* answer's usage. With `images` the model works from them (a reference, a sketch). Throws
|
|
352
|
+
* `AssistantError` with `not_connected`, `app_cap` or `own_cap` when nobody can pay, or `upstream`
|
|
353
|
+
* when the model fails. Returns the PNG as base64.
|
|
354
|
+
*/
|
|
355
|
+
image(caller: Caller, o: {
|
|
356
|
+
task: string;
|
|
357
|
+
prompt: string;
|
|
358
|
+
images?: CompleteImage[];
|
|
359
|
+
/** Wide, tall or square (default). */
|
|
360
|
+
size?: '1024x1024' | '1536x1024' | '1024x1536';
|
|
361
|
+
quality?: 'low' | 'medium' | 'high';
|
|
362
|
+
signal?: AbortSignal;
|
|
363
|
+
run?: {
|
|
364
|
+
id: string;
|
|
365
|
+
title: string;
|
|
366
|
+
};
|
|
367
|
+
}): Promise<{
|
|
368
|
+
data: string;
|
|
369
|
+
mediaType: 'image/png';
|
|
370
|
+
usd: number;
|
|
371
|
+
model: string;
|
|
372
|
+
}>;
|
|
337
373
|
/** Starts a person's ChatGPT plan sign-in: the address to open in a new tab. */
|
|
338
374
|
startSignin(caller: Caller, provider: AssistantProvider): Promise<{
|
|
339
375
|
url: string;
|
package/dist/assistant/engine.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import Anthropic from '@anthropic-ai/sdk';
|
|
11
11
|
import { HAND_OFF_WHYS } from '@columenlabs/lumen-ui-types/assistant';
|
|
12
12
|
import { generateText, jsonSchema, stepCountIs, tool } from 'ai';
|
|
13
|
-
import OpenAI from 'openai';
|
|
13
|
+
import OpenAI, { toFile } from 'openai';
|
|
14
14
|
import { aiModel, turnSettings } from '../agent/models.js';
|
|
15
15
|
import { bearings } from './bearings.js';
|
|
16
16
|
import { CHATGPT_REDIRECT, SigninError, finishChatgpt, startChatgpt, testChatgpt, } from './chatgpt.js';
|
|
@@ -19,6 +19,13 @@ import { DEFAULT_CAP, State, keyDocName, keyState, planDocName, waitText, } from
|
|
|
19
19
|
import { Docs } from './store.js';
|
|
20
20
|
import { toolName, withDeadline } from './tools.js';
|
|
21
21
|
import { scrapeqlTools, webFetchTools } from './web.js';
|
|
22
|
+
/** Who is asking: their id (the store's scope for their things) and whether they are an admin. */
|
|
23
|
+
/** A photo `complete` reads with its prompt: base64 bytes and their media type (`image/jpeg`). */
|
|
24
|
+
/**
|
|
25
|
+
* The model `image` runs and its list price per million tokens (OpenAI, gpt-image-2: text in $5,
|
|
26
|
+
* image in $8, image out $30). `fallbackUsd` is a medium square image, for an answer with no usage.
|
|
27
|
+
*/
|
|
28
|
+
export const IMAGE_MODEL = { id: 'gpt-image-2', text: 5, image: 8, out: 30, fallbackUsd: 0.06 };
|
|
22
29
|
const DOLLARS = { one: 'dollar', many: 'dollars', usd: 1 };
|
|
23
30
|
const round2 = (n) => Math.round(n * 100) / 100;
|
|
24
31
|
/** A request may ask for at most this many times the starter or monthly cap. */
|
|
@@ -990,6 +997,87 @@ export class Assistant {
|
|
|
990
997
|
}
|
|
991
998
|
return { text, usd: s.meter.usd, model: s.model.id, cut, usage: s.meter.part() };
|
|
992
999
|
}
|
|
1000
|
+
// ---------------------------------------------------------------- images
|
|
1001
|
+
/**
|
|
1002
|
+
* One image made as the person, on the payer's OpenAI key (a plan sign-in cannot make one),
|
|
1003
|
+
* billed and capped like `complete`: the price is the model's list price per token, read from the
|
|
1004
|
+
* answer's usage. With `images` the model works from them (a reference, a sketch). Throws
|
|
1005
|
+
* `AssistantError` with `not_connected`, `app_cap` or `own_cap` when nobody can pay, or `upstream`
|
|
1006
|
+
* when the model fails. Returns the PNG as base64.
|
|
1007
|
+
*/
|
|
1008
|
+
async image(caller, o) {
|
|
1009
|
+
const began = Date.now();
|
|
1010
|
+
const { paying, wait } = await this.state.paying(caller.id, 'openai');
|
|
1011
|
+
if (!paying) {
|
|
1012
|
+
const code = wait ?? 'not_connected';
|
|
1013
|
+
throw new AssistantError(waitText(code, 'openai', !!this.o.allowance), code, 402);
|
|
1014
|
+
}
|
|
1015
|
+
const key = paying.keys.openai;
|
|
1016
|
+
if (!key || paying.plans.openai)
|
|
1017
|
+
throw new AssistantError(waitText('not_connected', 'openai'), 'not_connected', 402);
|
|
1018
|
+
const m = IMAGE_MODEL;
|
|
1019
|
+
const client = this.openai(key);
|
|
1020
|
+
const common = {
|
|
1021
|
+
model: m.id,
|
|
1022
|
+
prompt: o.prompt.slice(0, 4000),
|
|
1023
|
+
size: o.size ?? '1024x1024',
|
|
1024
|
+
quality: o.quality ?? 'medium',
|
|
1025
|
+
n: 1,
|
|
1026
|
+
};
|
|
1027
|
+
let usd = 0;
|
|
1028
|
+
let tokens = { in: 0, out: 0 };
|
|
1029
|
+
try {
|
|
1030
|
+
const refs = (o.images ?? []).slice(0, 4);
|
|
1031
|
+
const res = refs.length
|
|
1032
|
+
? await client.images.edit({
|
|
1033
|
+
...common,
|
|
1034
|
+
image: await Promise.all(refs.map((r, i) => toFile(Uint8Array.from(atob(r.data), (c) => c.charCodeAt(0)), `ref-${i + 1}.${r.mediaType.split('/')[1] ?? 'png'}`, { type: r.mediaType }))),
|
|
1035
|
+
}, o.signal ? { signal: o.signal } : undefined)
|
|
1036
|
+
: await client.images.generate(common, o.signal ? { signal: o.signal } : undefined);
|
|
1037
|
+
const u = res.usage;
|
|
1038
|
+
if (u) {
|
|
1039
|
+
const text = u.input_tokens_details?.text_tokens ?? u.input_tokens;
|
|
1040
|
+
const pics = u.input_tokens_details?.image_tokens ?? 0;
|
|
1041
|
+
tokens = { in: u.input_tokens, out: u.output_tokens };
|
|
1042
|
+
usd = (text * m.text + pics * m.image + u.output_tokens * m.out) / 1e6;
|
|
1043
|
+
}
|
|
1044
|
+
else
|
|
1045
|
+
usd = m.fallbackUsd;
|
|
1046
|
+
const data = res.data?.[0]?.b64_json;
|
|
1047
|
+
if (!data)
|
|
1048
|
+
throw new AssistantError('The image model sent no image.', 'upstream', 502);
|
|
1049
|
+
return { data, mediaType: 'image/png', usd, model: m.id };
|
|
1050
|
+
}
|
|
1051
|
+
catch (e) {
|
|
1052
|
+
if (e instanceof AssistantError)
|
|
1053
|
+
throw e;
|
|
1054
|
+
throw new AssistantError(`The image model did not answer: ${String(e?.message ?? e).slice(0, 200)}`, 'upstream', 502);
|
|
1055
|
+
}
|
|
1056
|
+
finally {
|
|
1057
|
+
const part = {
|
|
1058
|
+
label: 'Image',
|
|
1059
|
+
calls: 1,
|
|
1060
|
+
in: tokens.in,
|
|
1061
|
+
read: 0,
|
|
1062
|
+
out: tokens.out,
|
|
1063
|
+
usd,
|
|
1064
|
+
model: m.id,
|
|
1065
|
+
};
|
|
1066
|
+
await this.state.addSpend(caller.id, paying.payer, [part], {
|
|
1067
|
+
task: o.task,
|
|
1068
|
+
ms: Date.now() - began,
|
|
1069
|
+
});
|
|
1070
|
+
const task = this.tasks.find((t) => t.id === o.task);
|
|
1071
|
+
await this.state
|
|
1072
|
+
.addUsage(caller.id, {
|
|
1073
|
+
id: `task:${o.task}:${o.run?.id ?? new Date().toISOString().slice(0, 10)}`,
|
|
1074
|
+
kind: 'task',
|
|
1075
|
+
task: o.task,
|
|
1076
|
+
title: o.run?.title ?? task?.label ?? o.task,
|
|
1077
|
+
}, [part])
|
|
1078
|
+
.catch(() => undefined);
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
993
1081
|
// ---------------------------------------------------------------- keys
|
|
994
1082
|
/** Starts a person's ChatGPT plan sign-in: the address to open in a new tab. */
|
|
995
1083
|
async startSignin(caller, provider) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@columenlabs/lumen-ai",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.227",
|
|
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.
|
|
56
|
-
"@columenlabs/lumen-ui-types": "0.1.
|
|
55
|
+
"@columenlabs/lumen-ops": "0.1.227",
|
|
56
|
+
"@columenlabs/lumen-ui-types": "0.1.227",
|
|
57
57
|
"agents": "0.32.0",
|
|
58
58
|
"ai": "7.0.162",
|
|
59
59
|
"just-bash": "3.6.0",
|