@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.
@@ -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
+ }
@@ -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, ...(o.fetch ? { fetch: o.fetch } : {}) })(model.id);
140
+ return createAnthropic({ apiKey: key.apiKey, fetch: f })(model.id);
138
141
  if (model.provider === 'xai')
139
- return createOpenAI({
140
- apiKey: key.apiKey,
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
@@ -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
- const f = this.o.fetch;
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, plan = false) {
248
- const f = this.o.fetch;
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
- const f = this.o.fetch;
254
- return new OpenAI({ apiKey, baseURL: XAI_BASE_URL, maxRetries: 1, ...(f ? { fetch: f } : {}) });
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.269",
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.269",
60
- "@columenlabs/lumen-ui-types": "0.1.269",
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",