@columenlabs/lumen-ai 0.1.206 → 0.1.208

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
@@ -94,8 +94,8 @@ const routes = assistantRoutes(assistant, {
94
94
  app.all('/api/assistant/*', (c) => routes(c.req.raw));
95
95
  ```
96
96
 
97
- - **Providers.** A tier runs any model in `models` (Claude's and OpenAI's by default, `CLAUDE_MODELS`, `OPENAI_MODELS`): Claude through Anthropic's Messages API, OpenAI through the Responses API, with the same tools, drafts, Stop and caps. A hand-off may cross providers. The advisor and prompt caching marks are Claude's only.
98
- - **Keys.** Each provider has its own keys (`?provider=anthropic|openai` on the key routes). An admin pastes the shared key everyone uses (`PUT /key`); it is tested with one free call before it is kept, then sealed, and no route returns it. A person may add their own (`PUT /my-key`), with one monthly cap on their own keys (`PUT /my-key/cap`).
97
+ - **Providers.** A tier runs any model in `models` (Claude's and OpenAI's by default, `CLAUDE_MODELS`, `OPENAI_MODELS`): Claude through Anthropic's Messages API, OpenAI through the Responses API, with the same tools, drafts, Stop and caps. xAI's Grok models (`XAI_MODELS`) are opt in: an app that lists them gets an xAI row in Model providers (an API key, `xai-…`); they run on the same Responses client at `XAI_BASE_URL` (`https://api.x.ai/v1`). xAI has no public sign-in for other apps, so Grok takes a key only. A hand-off may cross providers. The advisor and prompt caching marks are Claude's only.
98
+ - **Keys.** Each provider has its own keys (`?provider=anthropic|openai|xai` on the key routes). An admin pastes the shared key everyone uses (`PUT /key`); it is tested with one free call before it is kept, then sealed, and no route returns it. A person may add their own (`PUT /my-key`), with one monthly cap on their own keys (`PUT /my-key/cap`).
99
99
  - **ChatGPT sign-in.** In place of their own OpenAI key a person may sign in with their ChatGPT plan (`POST /my-signin`, then `POST /my-signin/finish {url}`): the sign-in opens in a new tab and ends on `http://127.0.0.1:1455/auth/callback`, a page that does not load, whose address the person pastes back. The tokens are sealed as their own key and refreshed before they run out; answers on the plan count tokens and no dollars.
100
100
  - **Who pays.** The person's pick (the shared keys by default) while it has a key for the tier's provider and money left this month under its cap (an admin sets the shared keys' cap per person, `PUT /app`), else the other; with neither the panel says why.
101
101
  - **Tools.** Each group is Off, Read, Read + draft or Read + write per person. A tool is a read when its MCP `readOnlyHint` is set. On Read + draft a write only drafts a card, and runs as the person when they press Save, after access is checked again. A group with `readOnly: true` (read tools only) is offered Off or Read alone. Read + write is offered only for a group with `writes: true`: its writes run at once, and with `undo` set each one's line in the chat has Undo, which calls `undo(caller, { tool, args, result })`; the model is told what was undone. Cards drafted one after another show as one stack with Save all. Use `mcpServerTools` (an SDK `McpServer`), `mcpFetchTools` (a server served as a fetch handler, any SDK), `remoteMcpTools` or `plainTools`.
@@ -18,7 +18,7 @@ import { generateText } from 'ai';
18
18
  import { CONNECTOR_GROUP, checkConnectorUrl, connectorGroup, connectorId, connectorSource, } from './connectors.js';
19
19
  import { aiModel, cachedTurn, planFallback, turnSettings } from './models.js';
20
20
  import { screenMapOf, screenNews, screenOutcomesOf, screenRules, screenTool, } from './screen.js';
21
- import { LAZY_AT, TOOL_SEARCH, lazyTools } from './search.js';
21
+ import { LAZY_AT, TOOL_SEARCH, lazyTools, usedTools } from './search.js';
22
22
  import { chunkEvents, itemsOf } from './stream.js';
23
23
  import { openAiTools, } from './tools.js';
24
24
  import { UsageMeter } from './usage.js';
@@ -515,7 +515,7 @@ export class LumenAgent extends Think {
515
515
  ];
516
516
  if (plan.first && !core.includes(plan.first))
517
517
  core.push(plan.first);
518
- lazy = lazyTools(tools, core, (n) => groups.get(n) ?? 'other');
518
+ lazy = lazyTools(tools, core, (n) => groups.get(n) ?? 'other', usedTools(ctx.messages));
519
519
  tools[TOOL_SEARCH] = lazy.search;
520
520
  system = `${system}\n\n${lazy.catalog}`;
521
521
  this.#turn.system = system;
@@ -1,6 +1,6 @@
1
1
  import type { AssistantEffort } from '@columenlabs/lumen-ui-types/assistant';
2
2
  import type { JSONValue, LanguageModel, ModelMessage, SystemModelMessage } from 'ai';
3
- import type { ModelDef } from '../assistant/models.js';
3
+ import { type ModelDef } from '../assistant/models.js';
4
4
  /** The key a model runs on. `plan`: an OpenAI access token from a ChatGPT plan sign-in. */
5
5
  export interface ModelKey {
6
6
  apiKey: string;
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import { createAnthropic } from '@ai-sdk/anthropic';
7
7
  import { createOpenAI } from '@ai-sdk/openai';
8
+ import { XAI_BASE_URL } from '../assistant/models.js';
8
9
  /**
9
10
  * A ChatGPT plan sign-in takes function tools only inside one namespace and no output cap, and a
10
11
  * forced call is the one tool offered with any call required (developers.openai.com/siwc, Preview
@@ -84,6 +85,12 @@ function textOf(content) {
84
85
  export function aiModel(model, key, o = {}) {
85
86
  if (model.provider === 'anthropic')
86
87
  return createAnthropic({ apiKey: key.apiKey, ...(o.fetch ? { fetch: o.fetch } : {}) })(model.id);
88
+ if (model.provider === 'xai')
89
+ return createOpenAI({
90
+ apiKey: key.apiKey,
91
+ baseURL: XAI_BASE_URL,
92
+ ...(o.fetch ? { fetch: o.fetch } : {}),
93
+ }).responses(model.id);
87
94
  const f = key.plan ? planFetch(o.fetch, o.name) : o.fetch;
88
95
  return createOpenAI({ apiKey: key.apiKey, ...(f ? { fetch: f } : {}) }).responses(model.id);
89
96
  }
@@ -18,8 +18,15 @@ export interface LazyTools {
18
18
  }
19
19
  /** How well a tool's name and description match the query's words. */
20
20
  export declare function toolScore(query: string, name: string, description?: string): number;
21
+ /**
22
+ * The tools a chat already used or found in earlier turns: each turn starts with only its core
23
+ * tools on, so a reply such as "yes" to a card offered last turn would call a tool that is off
24
+ * (AI_NoSuchToolError). Read from the history's tool calls and `tool_search` results.
25
+ */
26
+ export declare function usedTools(messages: readonly unknown[]): string[];
21
27
  /**
22
28
  * A search over `tools` whose finds stay on for the rest of the turn. `core` names are always on;
23
- * `groupOf` names each tool's group for the catalog.
29
+ * `groupOf` names each tool's group for the catalog; `start` names tools already on (those the
30
+ * chat used or found in earlier turns, `usedTools`), at most `FOUND_MAX` of the latest.
24
31
  */
25
- export declare function lazyTools(tools: ToolSet, core: string[], groupOf: (name: string) => string): LazyTools;
32
+ export declare function lazyTools(tools: ToolSet, core: string[], groupOf: (name: string) => string, start?: readonly string[]): LazyTools;
@@ -32,12 +32,38 @@ export function toolScore(query, name, description = '') {
32
32
  }
33
33
  return score;
34
34
  }
35
+ /**
36
+ * The tools a chat already used or found in earlier turns: each turn starts with only its core
37
+ * tools on, so a reply such as "yes" to a card offered last turn would call a tool that is off
38
+ * (AI_NoSuchToolError). Read from the history's tool calls and `tool_search` results.
39
+ */
40
+ export function usedTools(messages) {
41
+ const out = new Set();
42
+ for (const m of messages) {
43
+ const content = m.content;
44
+ if (!Array.isArray(content))
45
+ continue;
46
+ for (const part of content) {
47
+ if (part.type === 'tool-call' && part.toolName)
48
+ out.add(part.toolName);
49
+ if (part.type === 'tool-result' && part.toolName === TOOL_SEARCH) {
50
+ const o = part.output;
51
+ const v = (o && typeof o === 'object' && 'value' in o ? o.value : o);
52
+ for (const f of v?.found ?? [])
53
+ if (typeof f?.name === 'string')
54
+ out.add(f.name);
55
+ }
56
+ }
57
+ }
58
+ return [...out];
59
+ }
35
60
  /**
36
61
  * A search over `tools` whose finds stay on for the rest of the turn. `core` names are always on;
37
- * `groupOf` names each tool's group for the catalog.
62
+ * `groupOf` names each tool's group for the catalog; `start` names tools already on (those the
63
+ * chat used or found in earlier turns, `usedTools`), at most `FOUND_MAX` of the latest.
38
64
  */
39
- export function lazyTools(tools, core, groupOf) {
40
- const found = new Set();
65
+ export function lazyTools(tools, core, groupOf, start = []) {
66
+ const found = new Set(start.filter((n) => n in tools && !core.includes(n) && n !== TOOL_SEARCH).slice(-FOUND_MAX));
41
67
  const about = (n) => {
42
68
  const d = tools[n]?.description;
43
69
  return typeof d === 'string' ? d : '';
@@ -202,6 +202,7 @@ export declare class Assistant {
202
202
  forRequest(): Assistant;
203
203
  private anthropic;
204
204
  private openai;
205
+ private xai;
205
206
  private clients;
206
207
  /** The providers the app's models run on, in the order the models list them. */
207
208
  get providers(): AssistantProvider[];
@@ -274,7 +275,7 @@ export declare class Assistant {
274
275
  private askedFor;
275
276
  /** Swaps the history for a short summary. False with nothing to compact. */
276
277
  /**
277
- * One plain answer from OpenAI, metered on `s`: no tools, no chat. Streamed and read from the raw
278
+ * One plain answer from OpenAI (or xAI, on the same API), metered on `s`: no tools, no chat. Streamed and read from the raw
278
279
  * events like the chat loop, because a ChatGPT plan sign-in answers only streamed calls, takes no
279
280
  * output cap and may send its words only as deltas.
280
281
  */
@@ -14,7 +14,7 @@ import OpenAI 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';
17
- import { ALL_MODELS, DEFAULT_OUT, Meter, PROVIDERS, effortParams, } from './models.js';
17
+ import { ALL_MODELS, DEFAULT_OUT, Meter, PROVIDERS, XAI_BASE_URL, effortParams, } from './models.js';
18
18
  import { DEFAULT_CAP, State, keyDocName, keyState, planDocName, waitText, } from './state.js';
19
19
  import { Docs } from './store.js';
20
20
  import { toolName, withDeadline } from './tools.js';
@@ -162,6 +162,10 @@ export class Assistant {
162
162
  // A plan sign-in's calls are not retried: a usage limit is final for now.
163
163
  return new OpenAI({ apiKey, maxRetries: plan ? 0 : 1, ...(f ? { fetch: f } : {}) });
164
164
  }
165
+ xai(apiKey) {
166
+ const f = this.o.fetch;
167
+ return new OpenAI({ apiKey, baseURL: XAI_BASE_URL, maxRetries: 1, ...(f ? { fetch: f } : {}) });
168
+ }
165
169
  clients(paying) {
166
170
  const { keys, plans } = paying;
167
171
  return {
@@ -169,6 +173,7 @@ export class Assistant {
169
173
  ...(keys.openai
170
174
  ? { openai: this.openai(keys.openai, !!plans.openai), openaiPlan: !!plans.openai }
171
175
  : {}),
176
+ ...(keys.xai ? { xai: this.xai(keys.xai) } : {}),
172
177
  };
173
178
  }
174
179
  /** The providers the app's models run on, in the order the models list them. */
@@ -815,13 +820,15 @@ export class Assistant {
815
820
  }
816
821
  /** Swaps the history for a short summary. False with nothing to compact. */
817
822
  /**
818
- * One plain answer from OpenAI, metered on `s`: no tools, no chat. Streamed and read from the raw
823
+ * One plain answer from OpenAI (or xAI, on the same API), metered on `s`: no tools, no chat. Streamed and read from the raw
819
824
  * events like the chat loop, because a ChatGPT plan sign-in answers only streamed calls, takes no
820
825
  * output cap and may send its words only as deltas.
821
826
  */
822
827
  async openaiOnce(clients, s, instructions, input, max, signal, images) {
823
- const client = clients.openai;
824
- const plan = !!clients.openaiPlan;
828
+ // xAI's API is OpenAI's Responses API at another address.
829
+ const xai = s.model.provider === 'xai';
830
+ const client = (xai ? clients.xai : clients.openai);
831
+ const plan = !xai && !!clients.openaiPlan;
825
832
  let text = '';
826
833
  let done;
827
834
  try {
@@ -900,7 +907,8 @@ export class Assistant {
900
907
  const max = Math.min(o.maxTokens ?? 1000, 8000);
901
908
  let text = '';
902
909
  try {
903
- if (s.model.provider === 'openai' && clients.openai) {
910
+ if ((s.model.provider === 'openai' && clients.openai) ||
911
+ (s.model.provider === 'xai' && clients.xai)) {
904
912
  text = await this.openaiOnce(clients, s, o.system, o.prompt, max, o.signal, o.images);
905
913
  }
906
914
  else if (clients.anthropic) {
@@ -1025,6 +1033,8 @@ export class Assistant {
1025
1033
  try {
1026
1034
  if (provider === 'openai')
1027
1035
  await this.openai(apiKey).models.retrieve(model.id);
1036
+ else if (provider === 'xai')
1037
+ await this.xai(apiKey).models.retrieve(model.id);
1028
1038
  else
1029
1039
  await this.anthropic(apiKey).models.retrieve(model.id);
1030
1040
  return { ok: true, detail: `${label} took the key.`, at };
@@ -20,6 +20,14 @@ export interface ModelDef {
20
20
  export declare const CLAUDE_MODELS: ModelDef[];
21
21
  /** OpenAI models, smallest first (list prices from OpenAI's pricing page, October 2026). */
22
22
  export declare const OPENAI_MODELS: ModelDef[];
23
+ /**
24
+ * xAI's Grok models, smallest first (list prices from docs.x.ai/developers/pricing, October 2026,
25
+ * under 200k tokens of context). xAI's API is OpenAI-compatible (`XAI_BASE_URL`), so they run on the
26
+ * same Responses client as OpenAI's. Not in `ALL_MODELS`: an app opts in by listing them.
27
+ */
28
+ export declare const XAI_MODELS: ModelDef[];
29
+ /** xAI's OpenAI-compatible API. */
30
+ export declare const XAI_BASE_URL = "https://api.x.ai/v1";
23
31
  /** Every model the kit knows: what a tier may run unless the app lists its own. */
24
32
  export declare const ALL_MODELS: ModelDef[];
25
33
  /** How Settings names each provider, how its keys start and where a person makes one. */
@@ -43,6 +43,26 @@ export const OPENAI_MODELS = [
43
43
  out: o,
44
44
  read: r,
45
45
  }));
46
+ /**
47
+ * xAI's Grok models, smallest first (list prices from docs.x.ai/developers/pricing, October 2026,
48
+ * under 200k tokens of context). xAI's API is OpenAI-compatible (`XAI_BASE_URL`), so they run on the
49
+ * same Responses client as OpenAI's. Not in `ALL_MODELS`: an app opts in by listing them.
50
+ */
51
+ export const XAI_MODELS = [
52
+ ['grok-4.3', 'Grok 4.3', 1.25, 2.5, 0.2],
53
+ ['grok-4.6', 'Grok 4.6', 2, 6, 0.5],
54
+ ['grok-4.7', 'Grok 4.7', 2, 6, 0.5],
55
+ ].map(([id, label, i, o, r]) => ({
56
+ id: id,
57
+ label: label,
58
+ provider: 'xai',
59
+ group: 'xAI',
60
+ in: i,
61
+ out: o,
62
+ read: r,
63
+ }));
64
+ /** xAI's OpenAI-compatible API. */
65
+ export const XAI_BASE_URL = 'https://api.x.ai/v1';
46
66
  /** Every model the kit knows: what a tier may run unless the app lists its own. */
47
67
  export const ALL_MODELS = [...CLAUDE_MODELS, ...OPENAI_MODELS];
48
68
  /** How Settings names each provider, how its keys start and where a person makes one. */
@@ -58,6 +78,11 @@ export const PROVIDERS = {
58
78
  keysUrl: 'https://platform.openai.com/api-keys',
59
79
  signin: 'chatgpt',
60
80
  },
81
+ xai: {
82
+ label: 'xAI',
83
+ prefix: 'xai-',
84
+ keysUrl: 'https://console.x.ai/team/default/api-keys',
85
+ },
61
86
  };
62
87
  /** Output tokens per model call when a tier sets none. */
63
88
  export const DEFAULT_OUT = 16_000;
@@ -193,8 +193,8 @@ export declare class State {
193
193
  addSpend(id: string, payer: AssistantPayer, parts: UsagePart[]): Promise<void>;
194
194
  /** The payer's key of each provider, those set, and which are plan sign-ins. */
195
195
  keysOf(id: string, payer: AssistantPayer): Promise<{
196
- keys: Partial<Record<"anthropic" | "openai", string>>;
197
- plans: Partial<Record<"anthropic" | "openai", boolean>>;
196
+ keys: Partial<Record<"anthropic" | "openai" | "xai", string>>;
197
+ plans: Partial<Record<"anthropic" | "openai" | "xai", boolean>>;
198
198
  }>;
199
199
  /** Who pays the person's next answer on `provider`'s model, or why nobody can. */
200
200
  paying(id: string, provider: AssistantProvider): Promise<{
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@columenlabs/lumen-ai",
3
- "version": "0.1.206",
3
+ "version": "0.1.208",
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.206",
56
- "@columenlabs/lumen-ui-types": "0.1.206",
55
+ "@columenlabs/lumen-ops": "0.1.208",
56
+ "@columenlabs/lumen-ui-types": "0.1.208",
57
57
  "agents": "0.32.0",
58
58
  "ai": "7.0.162",
59
59
  "just-bash": "3.6.0",