@columenlabs/lumen-ai 0.1.100 → 0.1.102
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/dist/assistant/engine.js +69 -13
- package/package.json +3 -3
package/dist/assistant/engine.js
CHANGED
|
@@ -155,6 +155,14 @@ 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 ?? ''}`);
|
|
163
|
+
/** Whether a response's output has words or a call to show. */
|
|
164
|
+
const shows = (output) => output.some((b) => b.type === 'function_call' ||
|
|
165
|
+
(b.type === 'message' && b.content.some((c) => c.type === 'output_text' && c.text)));
|
|
158
166
|
const isApiError = (err) => err instanceof Anthropic.APIError || err instanceof OpenAI.APIError;
|
|
159
167
|
export class Assistant {
|
|
160
168
|
o;
|
|
@@ -1093,7 +1101,9 @@ export class Assistant {
|
|
|
1093
1101
|
strict: false,
|
|
1094
1102
|
}));
|
|
1095
1103
|
// The chat so far is plain text turns; this answer's calls are appended as OpenAI items.
|
|
1096
|
-
|
|
1104
|
+
// An earlier answer that said nothing (only a cut mark) is left out: an empty turn is refused.
|
|
1105
|
+
const input = o.messages
|
|
1106
|
+
.map((m) => ({
|
|
1097
1107
|
role: m.role,
|
|
1098
1108
|
content: typeof m.content === 'string'
|
|
1099
1109
|
? m.content
|
|
@@ -1101,15 +1111,19 @@ export class Assistant {
|
|
|
1101
1111
|
.map((b) => (b.type === 'text' ? b.text : ''))
|
|
1102
1112
|
.filter(Boolean)
|
|
1103
1113
|
.join('\n'),
|
|
1104
|
-
}))
|
|
1114
|
+
}))
|
|
1115
|
+
.filter((m) => m.role !== 'assistant' || m.content.replace('…(cut)', '').trim() !== '');
|
|
1105
1116
|
let answer = '';
|
|
1106
1117
|
let force = o.first?.type === 'tool' ? { type: 'function', name: o.first.name } : undefined;
|
|
1107
|
-
// A plan sign-in takes function tools only inside a namespace
|
|
1108
|
-
|
|
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) => {
|
|
1109
1121
|
const fns = only ? fnTools.filter((t) => t.name === only) : fnTools;
|
|
1122
|
+
const out = cap ? { max_output_tokens: session.tier.out ?? DEFAULT_OUT } : {};
|
|
1110
1123
|
if (!plan)
|
|
1111
|
-
return { tools: fns,
|
|
1124
|
+
return { tools: fns, ...out };
|
|
1112
1125
|
return {
|
|
1126
|
+
...out,
|
|
1113
1127
|
tools: [
|
|
1114
1128
|
{
|
|
1115
1129
|
type: 'namespace',
|
|
@@ -1132,8 +1146,8 @@ export class Assistant {
|
|
|
1132
1146
|
let partial = '';
|
|
1133
1147
|
// Event types seen and the items finished one by one: a plan sign-in's final event may carry
|
|
1134
1148
|
// an empty `output`, its items having come only as `response.output_item.done`.
|
|
1135
|
-
|
|
1136
|
-
|
|
1149
|
+
let seen = {};
|
|
1150
|
+
let items = [];
|
|
1137
1151
|
// An error OpenAI sends inside the stream: logged in full, shown as a plain sentence.
|
|
1138
1152
|
const streamFailure = (er) => {
|
|
1139
1153
|
if (partial)
|
|
@@ -1150,16 +1164,24 @@ export class Assistant {
|
|
|
1150
1164
|
}));
|
|
1151
1165
|
return new AssistantError(openaiStreamError(er.code || er.type || '', plan), 'upstream');
|
|
1152
1166
|
};
|
|
1153
|
-
|
|
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
|
+
partial = '';
|
|
1171
|
+
seen = {};
|
|
1172
|
+
items = [];
|
|
1173
|
+
return askOnce(cap, eff);
|
|
1174
|
+
};
|
|
1175
|
+
const askOnce = (cap, eff) => (async () => {
|
|
1154
1176
|
const stream = await client.responses.create({
|
|
1155
1177
|
model: session.model.id,
|
|
1156
1178
|
instructions,
|
|
1157
1179
|
input,
|
|
1158
|
-
...sent(only),
|
|
1180
|
+
...sent(only, cap),
|
|
1159
1181
|
store: false,
|
|
1160
1182
|
stream: true,
|
|
1161
1183
|
include: ['reasoning.encrypted_content'],
|
|
1162
|
-
...(
|
|
1184
|
+
...(eff ? { reasoning: { effort: eff } } : {}),
|
|
1163
1185
|
...(choice ? { tool_choice: choice } : {}),
|
|
1164
1186
|
}, signal ? { signal } : {});
|
|
1165
1187
|
let done;
|
|
@@ -1198,6 +1220,38 @@ export class Assistant {
|
|
|
1198
1220
|
}
|
|
1199
1221
|
throw err;
|
|
1200
1222
|
});
|
|
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
|
+
}
|
|
1237
|
+
// A reasoning model can spend the whole output on thinking and say nothing: ask once more
|
|
1238
|
+
// with less thinking before giving up.
|
|
1239
|
+
if (res.status === 'incomplete' &&
|
|
1240
|
+
res.incomplete_details?.reason === 'max_output_tokens' &&
|
|
1241
|
+
!partial &&
|
|
1242
|
+
!shows(res.output?.length ? res.output : items) &&
|
|
1243
|
+
effort !== 'low') {
|
|
1244
|
+
console.error(JSON.stringify({
|
|
1245
|
+
event: 'assistant_openai_retry',
|
|
1246
|
+
model: session.model.id,
|
|
1247
|
+
plan,
|
|
1248
|
+
cap,
|
|
1249
|
+
why: 'no_words',
|
|
1250
|
+
usage: res.usage ?? null,
|
|
1251
|
+
events: seen,
|
|
1252
|
+
}));
|
|
1253
|
+
res = await ask(cap, 'low');
|
|
1254
|
+
}
|
|
1201
1255
|
if (res.status === 'failed') {
|
|
1202
1256
|
if (partial)
|
|
1203
1257
|
sink.push({ kind: 'text', text: partial });
|
|
@@ -1247,9 +1301,11 @@ export class Assistant {
|
|
|
1247
1301
|
parts: output.flatMap((b) => b.type === 'message' ? b.content.map((c) => c.type) : []),
|
|
1248
1302
|
}));
|
|
1249
1303
|
if (!answer)
|
|
1250
|
-
throw new AssistantError(res.incomplete_details?.reason
|
|
1251
|
-
?
|
|
1252
|
-
:
|
|
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');
|
|
1253
1309
|
}
|
|
1254
1310
|
const cut = res.incomplete_details?.reason === 'max_output_tokens' && (words || hasCall)
|
|
1255
1311
|
? ' …(cut)'
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@columenlabs/lumen-ai",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.102",
|
|
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.
|
|
45
|
-
"@columenlabs/lumen-ui-types": "0.1.
|
|
44
|
+
"@columenlabs/lumen-ops": "0.1.102",
|
|
45
|
+
"@columenlabs/lumen-ui-types": "0.1.102",
|
|
46
46
|
"openai": "7.32.0",
|
|
47
47
|
"zod": "^4.3.6"
|
|
48
48
|
},
|