@columenlabs/lumen-ai 0.1.110 → 0.1.112

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.
@@ -155,11 +155,11 @@ function openaiStreamError(code, plan) {
155
155
  return 'OpenAI declined to answer this message.';
156
156
  return `OpenAI could not answer (${code || 'no reason given'}). It is logged; try again or pick another model.`;
157
157
  }
158
- /** Set once a plan backend refuses `max_output_tokens`, for the rest of the isolate. */
159
- let planCapRefused = false;
160
- const refusesCap = (err) => err instanceof OpenAI.APIError &&
161
- err.status === 400 &&
162
- /max_output_tokens/.test(`${err.message} ${err.error?.param ?? ''}`);
158
+ /** A tool's schema without JSON Schema's `$schema` line, which zod writes and no provider needs. */
159
+ const withoutMeta = (schema) => {
160
+ const { $schema: _, ...rest } = schema ?? {};
161
+ return rest;
162
+ };
163
163
  /** Whether a response's output has words or a call to show. */
164
164
  const shows = (output) => output.some((b) => b.type === 'function_call' ||
165
165
  (b.type === 'message' && b.content.some((c) => c.type === 'output_text' && c.text)));
@@ -564,7 +564,10 @@ export class Assistant {
564
564
  return [...fns.entries()].map(([name, f]) => ({
565
565
  name,
566
566
  description: `${f.draft ? "DRAFT ONLY: this call changes nothing. It puts a card on the person's screen that they Save or Dismiss. " : f.write ? 'WRITES AT ONCE: this call makes the change now; the person can undo it. ' : ''}${f.def.title ? `${f.def.title}. ` : ''}${f.def.description ?? ''}`.slice(0, 4000),
567
- input_schema: { type: 'object', ...f.def.inputSchema },
567
+ input_schema: {
568
+ type: 'object',
569
+ ...withoutMeta(f.def.inputSchema),
570
+ },
568
571
  }));
569
572
  }
570
573
  // ---------------------------------------------------------------- the turn
@@ -1115,11 +1118,11 @@ export class Assistant {
1115
1118
  .filter((m) => m.role !== 'assistant' || m.content.replace('…(cut)', '').trim() !== '');
1116
1119
  let answer = '';
1117
1120
  let force = o.first?.type === 'tool' ? { type: 'function', name: o.first.name } : undefined;
1118
- // A plan sign-in takes function tools only inside a namespace. Its output cap is sent too,
1119
- // unless the backend refused one before (then it keeps its own).
1120
- const sent = (only, cap) => {
1121
+ // A plan sign-in takes function tools only inside a namespace, and no output cap
1122
+ // (`max_output_tokens` is unsupported there: developers.openai.com/siwc, Preview limitations).
1123
+ const sent = (only) => {
1121
1124
  const fns = only ? fnTools.filter((t) => t.name === only) : fnTools;
1122
- const out = cap ? { max_output_tokens: session.tier.out ?? DEFAULT_OUT } : {};
1125
+ const out = plan ? {} : { max_output_tokens: session.tier.out ?? DEFAULT_OUT };
1123
1126
  if (!plan)
1124
1127
  return { tools: fns, ...out };
1125
1128
  return {
@@ -1164,20 +1167,20 @@ export class Assistant {
1164
1167
  }));
1165
1168
  return new AssistantError(openaiStreamError(er.code || er.type || '', plan), 'upstream');
1166
1169
  };
1167
- // One call of this round; asked again (fresh stream state) when the plan refuses the cap or
1168
- // spends its whole output on reasoning.
1169
- const ask = (cap, eff) => {
1170
+ // One call of this round; asked again (fresh stream state) when it spends its whole output
1171
+ // on reasoning.
1172
+ const ask = (eff) => {
1170
1173
  partial = '';
1171
1174
  seen = {};
1172
1175
  items = [];
1173
- return askOnce(cap, eff);
1176
+ return askOnce(eff);
1174
1177
  };
1175
- const askOnce = (cap, eff) => (async () => {
1178
+ const askOnce = (eff) => (async () => {
1176
1179
  const stream = await client.responses.create({
1177
1180
  model: session.model.id,
1178
1181
  instructions,
1179
1182
  input,
1180
- ...sent(only, cap),
1183
+ ...sent(only),
1181
1184
  store: false,
1182
1185
  stream: true,
1183
1186
  include: ['reasoning.encrypted_content'],
@@ -1220,24 +1223,13 @@ export class Assistant {
1220
1223
  }
1221
1224
  throw err;
1222
1225
  });
1223
- let cap = !plan || !planCapRefused;
1224
- let res;
1225
- try {
1226
- res = await ask(cap, effort);
1227
- }
1228
- catch (err) {
1229
- // A plan backend that refuses an output cap keeps its own: ask once more without it.
1230
- if (!(plan && cap && refusesCap(err)))
1231
- throw err;
1232
- planCapRefused = true;
1233
- cap = false;
1234
- console.error(JSON.stringify({ event: 'assistant_openai_cap_refused', model: session.model.id }));
1235
- res = await ask(cap, effort);
1236
- }
1226
+ let res = await ask(effort);
1237
1227
  // A reasoning model can spend the whole output on thinking and say nothing: ask once more
1238
- // with less thinking before giving up.
1228
+ // with less thinking before giving up. One that stopped before writing anything is not asked
1229
+ // again: less thinking cannot help it.
1239
1230
  if (res.status === 'incomplete' &&
1240
1231
  res.incomplete_details?.reason === 'max_output_tokens' &&
1232
+ (res.usage?.output_tokens ?? 0) > 0 &&
1241
1233
  !partial &&
1242
1234
  !shows(res.output?.length ? res.output : items) &&
1243
1235
  effort !== 'low') {
@@ -1245,12 +1237,11 @@ export class Assistant {
1245
1237
  event: 'assistant_openai_retry',
1246
1238
  model: session.model.id,
1247
1239
  plan,
1248
- cap,
1249
1240
  why: 'no_words',
1250
1241
  usage: res.usage ?? null,
1251
1242
  events: seen,
1252
1243
  }));
1253
- res = await ask(cap, 'low');
1244
+ res = await ask('low');
1254
1245
  }
1255
1246
  if (res.status === 'failed') {
1256
1247
  if (partial)
@@ -1301,11 +1292,13 @@ export class Assistant {
1301
1292
  parts: output.flatMap((b) => b.type === 'message' ? b.content.map((c) => c.type) : []),
1302
1293
  }));
1303
1294
  if (!answer)
1304
- throw new AssistantError(res.incomplete_details?.reason === 'max_output_tokens'
1305
- ? 'OpenAI spent the whole answer thinking and wrote nothing. Try again, or pick a smaller tier.'
1306
- : res.incomplete_details?.reason
1307
- ? `OpenAI stopped before answering (${res.incomplete_details.reason}).`
1308
- : 'OpenAI sent no answer.', 'upstream');
1295
+ throw new AssistantError(res.incomplete_details?.reason === 'max_output_tokens' && !res.usage?.output_tokens
1296
+ ? 'OpenAI stopped before reading the message. Try again, or pick another model for this tier.'
1297
+ : res.incomplete_details?.reason === 'max_output_tokens'
1298
+ ? 'OpenAI spent the whole answer thinking and wrote nothing. Try again, or pick a smaller tier.'
1299
+ : res.incomplete_details?.reason
1300
+ ? `OpenAI stopped before answering (${res.incomplete_details.reason}).`
1301
+ : 'OpenAI sent no answer.', 'upstream');
1309
1302
  }
1310
1303
  const cut = res.incomplete_details?.reason === 'max_output_tokens' && (words || hasCall)
1311
1304
  ? ' …(cut)'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@columenlabs/lumen-ai",
3
- "version": "0.1.110",
3
+ "version": "0.1.112",
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",
@@ -41,8 +41,8 @@
41
41
  },
42
42
  "dependencies": {
43
43
  "@anthropic-ai/sdk": "0.133.0",
44
- "@columenlabs/lumen-ops": "0.1.110",
45
- "@columenlabs/lumen-ui-types": "0.1.110",
44
+ "@columenlabs/lumen-ops": "0.1.112",
45
+ "@columenlabs/lumen-ui-types": "0.1.112",
46
46
  "openai": "7.32.0",
47
47
  "zod": "^4.3.6"
48
48
  },