@zenera/neo 1.1.9 → 1.1.10

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.
@@ -1,4 +1,5 @@
1
1
  import { embeddingResponse } from "../embedding.js";
2
+ import { RETRY_STATUS_CODES } from "../models/openrouter.js";
2
3
  import { zeroUsage } from "../types.js";
3
4
  // ---------------------------------------------------------------------------
4
5
  // OpenRouter embeddings adapter
@@ -31,7 +32,7 @@ export class OpenRouterEmbedder {
31
32
  // branch of `embedding` below is what catches a gateway that
32
33
  // decides otherwise.
33
34
  };
34
- const res = await this.#client.embeddings.generate({ requestBody }, { fetchOptions: { signal: req.signal } });
35
+ const res = await this.#client.embeddings.generate({ requestBody }, { fetchOptions: { signal: req.signal }, retryCodes: RETRY_STATUS_CODES });
35
36
  // The response is declared as the body *or* a bare string, so the
36
37
  // parsed shape has to be established before anything is read off it.
37
38
  if (typeof res === 'string') {
@@ -342,6 +342,48 @@ export class ModelRegistry {
342
342
  return declared ?? (isProviderKind(name) ? name : 'openai');
343
343
  }
344
344
  }
345
+ // ---------------------------------------------------------------------------
346
+ // Retries
347
+ // ---------------------------------------------------------------------------
348
+ /**
349
+ * One backoff policy for every vendor, because a 429 is not a failure of the
350
+ * run — it is the provider asking to be called again later, and an agent that
351
+ * gives up on the first one loses the whole trajectory to a wait of a few
352
+ * seconds. Every SDK here can retry, but none of them agree on what happens
353
+ * when nothing is configured: the OpenAI and Anthropic clients retry twice,
354
+ * `@google/genai` does not retry *at all* unless `retryOptions` is present, and
355
+ * OpenRouter's generated client retries `5XX` only — so a Vertex or OpenRouter
356
+ * 429 was fatal by default. These constants are what make the four behave the
357
+ * same; `ProviderSpec.maxRetries` (0 disables) is the per-provider override.
358
+ *
359
+ * Retries live at the transport, not around `Model.generate`, so a retried call
360
+ * is one that never started: no partial stream is replayed, and a `Retry-After`
361
+ * header is honoured by the SDK that read it.
362
+ */
363
+ const DEFAULT_MAX_RETRIES = 4;
364
+ /** wait after the first failure; doubles per attempt up to `RETRY_MAX_MS` */
365
+ const RETRY_INITIAL_MS = 1_000;
366
+ const RETRY_MAX_MS = 60_000;
367
+ const RETRY_EXPONENT = 2;
368
+ function retriesOf(opts) {
369
+ return Math.max(0, opts.maxRetries ?? DEFAULT_MAX_RETRIES);
370
+ }
371
+ /**
372
+ * The same schedule expressed as a wall-clock budget, which is the only shape
373
+ * OpenRouter's client accepts. It is the sum of the waits a count of `retries`
374
+ * would spend, so the two agree on how long a hopeless call may take — not on
375
+ * exactly how many times it is made, since this budget also has to cover the
376
+ * requests themselves.
377
+ */
378
+ function retryWindowMs(retries) {
379
+ let wait = RETRY_INITIAL_MS;
380
+ let total = 0;
381
+ for (let i = 0; i < retries; i++) {
382
+ total += Math.min(wait, RETRY_MAX_MS);
383
+ wait *= RETRY_EXPONENT;
384
+ }
385
+ return total;
386
+ }
345
387
  /**
346
388
  * Credential resolution, in one place so a `${VAR}` reference behaves the same
347
389
  * whether it came from yaml or from application code. `creds` may be a model
@@ -375,7 +417,7 @@ function buildClient(name, kind, creds, opts) {
375
417
  baseURL,
376
418
  defaultHeaders: headers,
377
419
  timeout: opts.timeoutMs,
378
- maxRetries: opts.maxRetries,
420
+ maxRetries: retriesOf(opts),
379
421
  ...(opts.token ? { fetch: bearerFetch(opts.token) } : {}),
380
422
  };
381
423
  if (defaults.protocol === 'anthropic') {
@@ -408,13 +450,25 @@ function buildOpenRouter(kind, apiKey, baseURL, headers, opts) {
408
450
  });
409
451
  }
410
452
  const { token } = opts;
453
+ const retries = retriesOf(opts);
411
454
  return new OpenRouter({
412
455
  apiKey: token ? async () => token() : apiKey,
413
456
  serverURL: baseURL,
414
457
  timeoutMs: opts.timeoutMs,
415
458
  // A count of zero is the one value that has to change shape: this SDK
416
459
  // says "do not retry" with a strategy, not with a number.
417
- retryConfig: opts.maxRetries === 0 ? { strategy: 'none' } : undefined,
460
+ retryConfig: retries === 0
461
+ ? { strategy: 'none' }
462
+ : {
463
+ strategy: 'backoff',
464
+ backoff: {
465
+ initialInterval: RETRY_INITIAL_MS,
466
+ maxInterval: RETRY_MAX_MS,
467
+ exponent: RETRY_EXPONENT,
468
+ maxElapsedTime: retryWindowMs(retries),
469
+ },
470
+ retryConnectionErrors: true,
471
+ },
418
472
  httpClient,
419
473
  });
420
474
  }
@@ -438,7 +492,15 @@ function buildOpenRouter(kind, apiKey, baseURL, headers, opts) {
438
492
  */
439
493
  function buildGenAI(kind, apiKey, baseURL, headers, opts, where) {
440
494
  const { GoogleGenAI } = sdk('@google/genai', kind);
441
- const httpOptions = { baseUrl: baseURL, headers, timeout: opts.timeoutMs };
495
+ // `attempts` counts the initial call, so a retry budget of zero is one
496
+ // attempt. Delays are fractions of a second here, unlike everywhere else.
497
+ const retryOptions = {
498
+ attempts: retriesOf(opts) + 1,
499
+ initialDelay: RETRY_INITIAL_MS / 1000,
500
+ maxDelay: RETRY_MAX_MS / 1000,
501
+ expBase: RETRY_EXPONENT,
502
+ };
503
+ const httpOptions = { baseUrl: baseURL, headers, timeout: opts.timeoutMs, retryOptions };
442
504
  if (kind !== 'vertex') {
443
505
  return new GoogleGenAI({ apiKey, httpOptions });
444
506
  }
@@ -2,6 +2,16 @@ import type { OpenRouter } from '@openrouter/sdk';
2
2
  import type { ChatRequestPlugin, ProviderPreferences } from '@openrouter/sdk/models';
3
3
  import type { StreamDelta } from '../events.ts';
4
4
  import type { Model, ModelRequest, ModelResponse } from '../model.ts';
5
+ /**
6
+ * Which failures the SDK's backoff applies to: rate limits, request timeouts,
7
+ * and the upstream's own errors. Families are written `5XX` in this matcher.
8
+ *
9
+ * It has to be passed per call. The generated client resolves `retryCodes` at
10
+ * the *operation*, defaulting it to `5XX` alone, and no client-wide option
11
+ * reaches it — so configuring a retry policy and leaving this out gives a
12
+ * client that backs off diligently on 500s and dies on the first 429.
13
+ */
14
+ export declare const RETRY_STATUS_CODES: string[];
5
15
  /**
6
16
  * Effort levels, spelled out rather than imported: the SDK's is an *open* enum
7
17
  * (its literals plus any string), which would let a typo through, and naming
@@ -1,5 +1,31 @@
1
1
  var _a;
2
2
  import { zeroUsage } from "../types.js";
3
+ // ---------------------------------------------------------------------------
4
+ // OpenRouter adapter
5
+ //
6
+ // Not the chat-completions adapter pointed at another base url. OpenRouter's
7
+ // own SDK is a different wire contract in TypeScript terms — camelCase fields
8
+ // remapped to snake_case on the way out, a response union that covers both
9
+ // streaming and not, assistant content that may arrive as an array — so it
10
+ // gets its own adapter rather than a widened `OpenAIModel`.
11
+ //
12
+ // What that buys is the part of OpenRouter the OpenAI client cannot express:
13
+ // provider routing, a model fallback chain, and per-call cost. What it costs
14
+ // is that responses are zod-parsed on the way in, so a provider returning a
15
+ // shape the schema does not know raises `ResponseValidationError` where the
16
+ // OpenAI client would have shrugged. `kind: openai-compatible` against the
17
+ // same base url remains the way back if that ever bites.
18
+ // ---------------------------------------------------------------------------
19
+ /**
20
+ * Which failures the SDK's backoff applies to: rate limits, request timeouts,
21
+ * and the upstream's own errors. Families are written `5XX` in this matcher.
22
+ *
23
+ * It has to be passed per call. The generated client resolves `retryCodes` at
24
+ * the *operation*, defaulting it to `5XX` alone, and no client-wide option
25
+ * reaches it — so configuring a retry policy and leaving this out gives a
26
+ * client that backs off diligently on 500s and dies on the first 429.
27
+ */
28
+ export const RETRY_STATUS_CODES = ['408', '429', '5XX'];
3
29
  function toStopReason(finish) {
4
30
  switch (finish) {
5
31
  case 'tool_calls':
@@ -154,7 +180,7 @@ export class OpenRouterModel {
154
180
  * SDK keeps transport concerns out of the body it validates.
155
181
  */
156
182
  #send(req, stream) {
157
- return this.#client.chat.send({ chatRequest: this.#request(req, stream) }, { fetchOptions: { signal: req.signal } });
183
+ return this.#client.chat.send({ chatRequest: this.#request(req, stream) }, { fetchOptions: { signal: req.signal }, retryCodes: RETRY_STATUS_CODES });
158
184
  }
159
185
  #request(req, stream) {
160
186
  const o = this.#options;
@@ -47,10 +47,10 @@ declare const modelSpec: z.ZodObject<{
47
47
  thinkingBudgetTokens: z.ZodOptional<z.ZodInt>;
48
48
  thinkingBudget: z.ZodOptional<z.ZodInt>;
49
49
  thinkingLevel: z.ZodOptional<z.ZodEnum<{
50
+ minimal: "minimal";
50
51
  low: "low";
51
52
  medium: "medium";
52
53
  high: "high";
53
- minimal: "minimal";
54
54
  }>>;
55
55
  includeThoughts: z.ZodOptional<z.ZodBoolean>;
56
56
  routing: z.ZodOptional<z.ZodObject<{
@@ -185,10 +185,10 @@ export declare const projectSchema: z.ZodObject<{
185
185
  thinkingBudgetTokens: z.ZodOptional<z.ZodInt>;
186
186
  thinkingBudget: z.ZodOptional<z.ZodInt>;
187
187
  thinkingLevel: z.ZodOptional<z.ZodEnum<{
188
+ minimal: "minimal";
188
189
  low: "low";
189
190
  medium: "medium";
190
191
  high: "high";
191
- minimal: "minimal";
192
192
  }>>;
193
193
  includeThoughts: z.ZodOptional<z.ZodBoolean>;
194
194
  routing: z.ZodOptional<z.ZodObject<{
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zenera/neo",
3
- "version": "1.1.9",
3
+ "version": "1.1.10",
4
4
  "description": "A multi-agent runtime for Node.js: agents, models, tools, skills, memory and an append-only trajectory.",
5
5
  "keywords": [
6
6
  "agents",