@columenlabs/lumen-ai 0.1.269 → 0.1.271
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/agent/agent.js +1 -0
- package/dist/agent/backoff.d.ts +4 -0
- package/dist/agent/backoff.js +63 -0
- package/dist/agent/models.js +6 -8
- package/dist/assistant/engine.js +13 -11
- package/package.json +3 -3
package/dist/agent/agent.js
CHANGED
|
@@ -633,6 +633,7 @@ export class LumenAgent extends Think {
|
|
|
633
633
|
let meter = new UsageMeter(plan.model, `${plan.label} · compact`, !!plan.key.plan);
|
|
634
634
|
this.#owed.push(meter);
|
|
635
635
|
const r = await generateText({
|
|
636
|
+
maxRetries: 0,
|
|
636
637
|
model: this.#model(plan, () => {
|
|
637
638
|
const fb = plan.fallback;
|
|
638
639
|
if (!fb)
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** The wait a refused answer asks for, in ms, or undefined when it names none. */
|
|
2
|
+
export declare function retryAfterMs(h: Headers): number | undefined;
|
|
3
|
+
/** `base` with the retry policy above. */
|
|
4
|
+
export declare function backoffFetch(base?: typeof fetch): typeof fetch;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One retry policy for every model provider call. A call refused for load or a limit (408, 409,
|
|
3
|
+
* 429, 5xx, 529) is sent again after the wait the provider asks for (`retry-after-ms`,
|
|
4
|
+
* `retry-after` in seconds or as a date), else an exponential wait with jitter (1, 2, 4, 8 s).
|
|
5
|
+
* It stops at 4 retries or 30 s of waiting in all, at once on the caller's abort, and never retries a
|
|
6
|
+
* body it cannot send twice (a stream). Its last answer goes back as it came, so the caller still
|
|
7
|
+
* sees the provider's status and message. The SDKs' own retries are off where this runs, so a call
|
|
8
|
+
* is never retried twice over.
|
|
9
|
+
*/
|
|
10
|
+
const RETRY = new Set([408, 409, 429, 500, 502, 503, 504, 529]);
|
|
11
|
+
const TRIES = 4;
|
|
12
|
+
const BUDGET_MS = 30_000;
|
|
13
|
+
const BASE_MS = 1_000;
|
|
14
|
+
/** The wait a refused answer asks for, in ms, or undefined when it names none. */
|
|
15
|
+
export function retryAfterMs(h) {
|
|
16
|
+
const ms = Number(h.get('retry-after-ms'));
|
|
17
|
+
if (Number.isFinite(ms) && ms > 0)
|
|
18
|
+
return ms;
|
|
19
|
+
const v = h.get('retry-after');
|
|
20
|
+
if (!v)
|
|
21
|
+
return undefined;
|
|
22
|
+
const s = Number(v);
|
|
23
|
+
if (Number.isFinite(s))
|
|
24
|
+
return s >= 0 ? s * 1000 : undefined;
|
|
25
|
+
const at = Date.parse(v);
|
|
26
|
+
return Number.isFinite(at) ? Math.max(0, at - Date.now()) : undefined;
|
|
27
|
+
}
|
|
28
|
+
const sleep = (ms, signal) => new Promise((resolve, reject) => {
|
|
29
|
+
if (signal?.aborted)
|
|
30
|
+
return reject(signal.reason);
|
|
31
|
+
const t = setTimeout(resolve, ms);
|
|
32
|
+
signal?.addEventListener('abort', () => {
|
|
33
|
+
clearTimeout(t);
|
|
34
|
+
reject(signal.reason);
|
|
35
|
+
}, { once: true });
|
|
36
|
+
});
|
|
37
|
+
/** `base` with the retry policy above. */
|
|
38
|
+
export function backoffFetch(base = fetch) {
|
|
39
|
+
return async (input, init) => {
|
|
40
|
+
const body = init?.body;
|
|
41
|
+
const resendable = body == null ||
|
|
42
|
+
typeof body === 'string' ||
|
|
43
|
+
body instanceof ArrayBuffer ||
|
|
44
|
+
ArrayBuffer.isView(body) ||
|
|
45
|
+
body instanceof URLSearchParams ||
|
|
46
|
+
body instanceof Blob;
|
|
47
|
+
if (!resendable || input instanceof Request)
|
|
48
|
+
return base(input, init);
|
|
49
|
+
let waited = 0;
|
|
50
|
+
for (let n = 0;; n += 1) {
|
|
51
|
+
const res = await base(input, init);
|
|
52
|
+
if (!RETRY.has(res.status) || n >= TRIES)
|
|
53
|
+
return res;
|
|
54
|
+
const asked = retryAfterMs(res.headers);
|
|
55
|
+
const wait = asked ?? BASE_MS * 2 ** n * (0.75 + Math.random() * 0.5);
|
|
56
|
+
if (waited + wait > BUDGET_MS)
|
|
57
|
+
return res;
|
|
58
|
+
await res.body?.cancel().catch(() => undefined);
|
|
59
|
+
await sleep(wait, init?.signal);
|
|
60
|
+
waited += wait;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}
|
package/dist/agent/models.js
CHANGED
|
@@ -7,6 +7,7 @@ import { createAnthropic } from '@ai-sdk/anthropic';
|
|
|
7
7
|
import { createOpenAI } from '@ai-sdk/openai';
|
|
8
8
|
import { Stream } from 'openai/core/streaming';
|
|
9
9
|
import { XAI_BASE_URL } from '../assistant/models.js';
|
|
10
|
+
import { backoffFetch } from './backoff.js';
|
|
10
11
|
/**
|
|
11
12
|
* A ChatGPT plan sign-in takes function tools only inside one namespace and no output cap, and a
|
|
12
13
|
* forced call is the one tool offered with any call required (developers.openai.com/siwc, Preview
|
|
@@ -133,16 +134,13 @@ function textOf(content) {
|
|
|
133
134
|
}
|
|
134
135
|
/** The AI SDK model a tier runs: its provider's client on the payer's key. */
|
|
135
136
|
export function aiModel(model, key, o = {}) {
|
|
137
|
+
// Every provider call backs off the same way on a limit or overload (backoff.ts).
|
|
138
|
+
const f = backoffFetch(o.fetch ?? fetch);
|
|
136
139
|
if (model.provider === 'anthropic')
|
|
137
|
-
return createAnthropic({ apiKey: key.apiKey,
|
|
140
|
+
return createAnthropic({ apiKey: key.apiKey, fetch: f })(model.id);
|
|
138
141
|
if (model.provider === 'xai')
|
|
139
|
-
return createOpenAI({
|
|
140
|
-
|
|
141
|
-
baseURL: XAI_BASE_URL,
|
|
142
|
-
...(o.fetch ? { fetch: o.fetch } : {}),
|
|
143
|
-
}).responses(model.id);
|
|
144
|
-
const f = key.plan ? planFetch(o.fetch, o.name) : o.fetch;
|
|
145
|
-
return createOpenAI({ apiKey: key.apiKey, ...(f ? { fetch: f } : {}) }).responses(model.id);
|
|
142
|
+
return createOpenAI({ apiKey: key.apiKey, baseURL: XAI_BASE_URL, fetch: f }).responses(model.id);
|
|
143
|
+
return createOpenAI({ apiKey: key.apiKey, fetch: key.plan ? planFetch(f, o.name) : f }).responses(model.id);
|
|
146
144
|
}
|
|
147
145
|
/**
|
|
148
146
|
* Claude's prompt cache. The request's own breakpoint follows the newest block, so a turn's later
|
package/dist/assistant/engine.js
CHANGED
|
@@ -11,6 +11,7 @@ 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
13
|
import OpenAI, { toFile } from 'openai';
|
|
14
|
+
import { backoffFetch } from '../agent/backoff.js';
|
|
14
15
|
import { aiModel, turnSettings } from '../agent/models.js';
|
|
15
16
|
import { webLookupTools } from '../subagents/web.js';
|
|
16
17
|
import { bearings } from './bearings.js';
|
|
@@ -240,26 +241,26 @@ export class Assistant {
|
|
|
240
241
|
Object.defineProperty(view, 'state', { value: this.state.withDocs(this.state.docs.cached()) });
|
|
241
242
|
return view;
|
|
242
243
|
}
|
|
244
|
+
// The SDKs' own retries are off: every call backs off once, the same way (agent/backoff.ts).
|
|
243
245
|
anthropic(apiKey) {
|
|
244
|
-
|
|
245
|
-
return new Anthropic({ apiKey, maxRetries: 1, ...(f ? { fetch: f } : {}) });
|
|
246
|
+
return new Anthropic({ apiKey, maxRetries: 0, fetch: backoffFetch(this.o.fetch ?? fetch) });
|
|
246
247
|
}
|
|
247
|
-
openai(apiKey
|
|
248
|
-
|
|
249
|
-
// A plan sign-in's calls are not retried: a usage limit is final for now.
|
|
250
|
-
return new OpenAI({ apiKey, maxRetries: plan ? 0 : 1, ...(f ? { fetch: f } : {}) });
|
|
248
|
+
openai(apiKey) {
|
|
249
|
+
return new OpenAI({ apiKey, maxRetries: 0, fetch: backoffFetch(this.o.fetch ?? fetch) });
|
|
251
250
|
}
|
|
252
251
|
xai(apiKey) {
|
|
253
|
-
|
|
254
|
-
|
|
252
|
+
return new OpenAI({
|
|
253
|
+
apiKey,
|
|
254
|
+
baseURL: XAI_BASE_URL,
|
|
255
|
+
maxRetries: 0,
|
|
256
|
+
fetch: backoffFetch(this.o.fetch ?? fetch),
|
|
257
|
+
});
|
|
255
258
|
}
|
|
256
259
|
clients(paying) {
|
|
257
260
|
const { keys, plans } = paying;
|
|
258
261
|
return {
|
|
259
262
|
...(keys.anthropic ? { anthropic: this.anthropic(keys.anthropic) } : {}),
|
|
260
|
-
...(keys.openai
|
|
261
|
-
? { openai: this.openai(keys.openai, !!plans.openai), openaiPlan: !!plans.openai }
|
|
262
|
-
: {}),
|
|
263
|
+
...(keys.openai ? { openai: this.openai(keys.openai), openaiPlan: !!plans.openai } : {}),
|
|
263
264
|
...(keys.xai ? { xai: this.xai(keys.xai) } : {}),
|
|
264
265
|
};
|
|
265
266
|
}
|
|
@@ -596,6 +597,7 @@ export class Assistant {
|
|
|
596
597
|
const t0 = Date.now();
|
|
597
598
|
try {
|
|
598
599
|
const r = await generateText({
|
|
600
|
+
maxRetries: 0,
|
|
599
601
|
model: aiModel(sub.model, key, {
|
|
600
602
|
...(this.o.fetch ? { fetch: this.o.fetch } : {}),
|
|
601
603
|
name: this.name,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@columenlabs/lumen-ai",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.271",
|
|
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",
|
|
@@ -56,8 +56,8 @@
|
|
|
56
56
|
"@ai-sdk/openai": "4.0.108",
|
|
57
57
|
"@anthropic-ai/sdk": "0.133.0",
|
|
58
58
|
"@cloudflare/think": "0.23.0",
|
|
59
|
-
"@columenlabs/lumen-ops": "0.1.
|
|
60
|
-
"@columenlabs/lumen-ui-types": "0.1.
|
|
59
|
+
"@columenlabs/lumen-ops": "0.1.271",
|
|
60
|
+
"@columenlabs/lumen-ui-types": "0.1.271",
|
|
61
61
|
"agents": "0.32.0",
|
|
62
62
|
"ai": "7.0.162",
|
|
63
63
|
"just-bash": "3.6.0",
|