@zenera/neo 1.1.10 → 1.1.12

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.
Files changed (90) hide show
  1. package/README.md +3 -3
  2. package/dist/agent.d.ts +8 -7
  3. package/dist/agent.js +15 -8
  4. package/dist/architecture.d.ts +7 -5
  5. package/dist/architecture.js +20 -12
  6. package/dist/embedding.d.ts +20 -1
  7. package/dist/embedding.js +3 -2
  8. package/dist/embeddings/fanout.d.ts +51 -0
  9. package/dist/embeddings/fanout.js +76 -0
  10. package/dist/embeddings/gemini.d.ts +3 -10
  11. package/dist/embeddings/gemini.js +46 -30
  12. package/dist/embeddings/index.d.ts +3 -0
  13. package/dist/embeddings/index.js +3 -0
  14. package/dist/embeddings/limiter.d.ts +36 -0
  15. package/dist/embeddings/limiter.js +182 -0
  16. package/dist/embeddings/openai.d.ts +3 -2
  17. package/dist/embeddings/openai.js +54 -13
  18. package/dist/embeddings/openrouter.d.ts +2 -1
  19. package/dist/embeddings/openrouter.js +52 -23
  20. package/dist/embeddings/rate-limit.d.ts +16 -0
  21. package/dist/embeddings/rate-limit.js +131 -0
  22. package/dist/failure.d.ts +82 -0
  23. package/dist/failure.js +323 -0
  24. package/dist/index.d.ts +13 -2
  25. package/dist/index.js +13 -2
  26. package/dist/inspect/client.d.ts +1 -1
  27. package/dist/inspect/client.js +42 -26
  28. package/dist/kernel.d.ts +1 -1
  29. package/dist/kernel.js +40 -23
  30. package/dist/memory/files.d.ts +24 -0
  31. package/dist/memory/files.js +85 -0
  32. package/dist/memory/graph.d.ts +56 -0
  33. package/dist/memory/graph.js +159 -0
  34. package/dist/memory/index.d.ts +93 -0
  35. package/dist/memory/index.js +357 -0
  36. package/dist/memory/instructions.d.ts +15 -0
  37. package/dist/memory/instructions.js +103 -0
  38. package/dist/memory/page.d.ts +8 -0
  39. package/dist/memory/page.js +797 -0
  40. package/dist/memory/recall.d.ts +35 -0
  41. package/dist/memory/recall.js +161 -0
  42. package/dist/memory/render.d.ts +10 -0
  43. package/dist/memory/render.js +228 -0
  44. package/dist/memory/report.d.ts +57 -0
  45. package/dist/memory/report.js +113 -0
  46. package/dist/memory/store.d.ts +49 -0
  47. package/dist/memory/store.js +226 -0
  48. package/dist/memory/tools.d.ts +11 -0
  49. package/dist/memory/tools.js +310 -0
  50. package/dist/memory/types.d.ts +192 -0
  51. package/dist/memory/types.js +105 -0
  52. package/dist/memory/vectors.d.ts +29 -0
  53. package/dist/memory/vectors.js +152 -0
  54. package/dist/models/anthropic.d.ts +2 -1
  55. package/dist/models/anthropic.js +11 -4
  56. package/dist/models/factory.d.ts +15 -0
  57. package/dist/models/factory.js +49 -10
  58. package/dist/models/gemini.d.ts +2 -1
  59. package/dist/models/gemini.js +8 -3
  60. package/dist/models/openai-chat.d.ts +2 -1
  61. package/dist/models/openai-chat.js +10 -5
  62. package/dist/models/openai-responses.d.ts +2 -1
  63. package/dist/models/openai-responses.js +22 -7
  64. package/dist/models/openrouter.d.ts +2 -1
  65. package/dist/models/openrouter.js +14 -6
  66. package/dist/project/config.d.ts +55 -3
  67. package/dist/project/config.js +76 -1
  68. package/dist/project/index.d.ts +2 -2
  69. package/dist/project/index.js +1 -1
  70. package/dist/project/load.d.ts +16 -2
  71. package/dist/project/load.js +67 -1
  72. package/dist/runner.d.ts +4 -2
  73. package/dist/runner.js +29 -30
  74. package/dist/services.d.ts +23 -6
  75. package/dist/services.js +25 -15
  76. package/dist/tools/workspace.js +195 -17
  77. package/dist/trajectory.d.ts +14 -12
  78. package/dist/types.d.ts +5 -5
  79. package/dist/types.js +3 -3
  80. package/package.json +2 -5
  81. package/dist/memory-stores/factory.d.ts +0 -15
  82. package/dist/memory-stores/factory.js +0 -29
  83. package/dist/memory-stores/file.d.ts +0 -37
  84. package/dist/memory-stores/file.js +0 -174
  85. package/dist/memory-stores/in-memory.d.ts +0 -17
  86. package/dist/memory-stores/in-memory.js +0 -80
  87. package/dist/memory-stores/index.d.ts +0 -4
  88. package/dist/memory-stores/index.js +0 -5
  89. package/dist/memory.d.ts +0 -115
  90. package/dist/memory.js +0 -213
@@ -0,0 +1,182 @@
1
+ import { classify, retryAfterMs } from "./rate-limit.js";
2
+ export const DEFAULT_MIN = 1;
3
+ export const DEFAULT_START = 4;
4
+ export const DEFAULT_MAX = 16;
5
+ export const DEFAULT_STRIDE = 8;
6
+ export const DEFAULT_INITIAL_BACKOFF_MS = 1_000;
7
+ export const DEFAULT_MAX_BACKOFF_MS = 60_000;
8
+ export const DEFAULT_MAX_RETRIES = 4;
9
+ export class RateLimiter {
10
+ #min;
11
+ #max;
12
+ #stride;
13
+ #initial;
14
+ #maxBackoff;
15
+ #retries;
16
+ #limit;
17
+ #active = 0;
18
+ #successes = 0;
19
+ /** bumped on every decrease; a refusal from an older one is already answered */
20
+ #epoch = 0;
21
+ #penalty;
22
+ #pausedUntil = 0;
23
+ #timer;
24
+ #waiting = [];
25
+ constructor(options = {}) {
26
+ this.#min = Math.max(1, options.min ?? DEFAULT_MIN);
27
+ this.#max = Math.max(this.#min, options.max ?? DEFAULT_MAX);
28
+ this.#stride = Math.max(1, options.stride ?? DEFAULT_STRIDE);
29
+ this.#initial = Math.max(0, options.initialBackoffMs ?? DEFAULT_INITIAL_BACKOFF_MS);
30
+ this.#maxBackoff = Math.max(this.#initial, options.maxBackoffMs ?? DEFAULT_MAX_BACKOFF_MS);
31
+ this.#retries = Math.max(0, options.maxRetries ?? DEFAULT_MAX_RETRIES);
32
+ this.#limit = clamp(options.start ?? DEFAULT_START, this.#min, this.#max);
33
+ this.#penalty = this.#initial;
34
+ }
35
+ /** How many may be in flight as things stand. Moves; read it for reporting only. */
36
+ get limit() {
37
+ return this.#limit;
38
+ }
39
+ get inFlight() {
40
+ return this.#active;
41
+ }
42
+ /**
43
+ * Runs the task when there is room, retries it when the failure allows, and
44
+ * gives up when it does not. The slot is released before any wait, so a
45
+ * task serving its backoff is not also holding the concurrency down.
46
+ */
47
+ async run(task, signal) {
48
+ let backoff = this.#initial;
49
+ for (let attempt = 0;; attempt++) {
50
+ const epoch = await this.#acquire(signal);
51
+ let failure = 'fatal';
52
+ let err;
53
+ try {
54
+ const value = await task();
55
+ this.#succeeded();
56
+ return value;
57
+ }
58
+ catch (thrown) {
59
+ err = thrown;
60
+ failure = classify(thrown);
61
+ // Before the slot is let go, so the pause is already in force
62
+ // when the next waiter is granted one.
63
+ if (failure === 'rate-limit') {
64
+ this.#slowDown(epoch, retryAfterMs(thrown));
65
+ }
66
+ }
67
+ finally {
68
+ this.#release();
69
+ }
70
+ if (failure === 'fatal' || attempt >= this.#retries) {
71
+ throw err;
72
+ }
73
+ // A refusal is already being served by the shared pause, which the
74
+ // next acquire blocks on; waiting again here would double it.
75
+ if (failure === 'transient') {
76
+ await sleep(jitter(Math.min(backoff, this.#maxBackoff)), signal);
77
+ backoff *= 2;
78
+ }
79
+ }
80
+ }
81
+ #acquire(signal) {
82
+ if (signal?.aborted) {
83
+ return Promise.reject(signal.reason);
84
+ }
85
+ if (this.#free()) {
86
+ this.#active++;
87
+ return Promise.resolve(this.#epoch);
88
+ }
89
+ return new Promise((resolve, reject) => {
90
+ const waiter = {
91
+ grant: (epoch) => {
92
+ signal?.removeEventListener('abort', abort);
93
+ resolve(epoch);
94
+ },
95
+ fail: reject,
96
+ };
97
+ const abort = () => {
98
+ const at = this.#waiting.indexOf(waiter);
99
+ if (at >= 0) {
100
+ this.#waiting.splice(at, 1);
101
+ }
102
+ reject(signal?.reason);
103
+ };
104
+ signal?.addEventListener('abort', abort, { once: true });
105
+ this.#waiting.push(waiter);
106
+ });
107
+ }
108
+ #free() {
109
+ return this.#active < this.#limit && Date.now() >= this.#pausedUntil;
110
+ }
111
+ #release() {
112
+ this.#active--;
113
+ this.#pump();
114
+ }
115
+ /**
116
+ * The slot is taken here rather than by the waiter, so several woken at
117
+ * once cannot each see the same free one before any of them claims it.
118
+ */
119
+ #pump() {
120
+ while (this.#waiting.length > 0 && this.#free()) {
121
+ this.#active++;
122
+ this.#waiting.shift().grant(this.#epoch);
123
+ }
124
+ }
125
+ #succeeded() {
126
+ this.#penalty = this.#initial;
127
+ // Only a limit that is actually the constraint has earned being raised;
128
+ // with nothing queued, more slots would go unused anyway.
129
+ if (this.#waiting.length === 0) {
130
+ return;
131
+ }
132
+ if (++this.#successes >= this.#stride) {
133
+ this.#successes = 0;
134
+ this.#limit = Math.min(this.#max, this.#limit + 1);
135
+ this.#pump();
136
+ }
137
+ }
138
+ #slowDown(epoch, retryAfter) {
139
+ if (epoch === this.#epoch) {
140
+ this.#epoch++;
141
+ this.#limit = Math.max(this.#min, Math.floor(this.#limit / 2));
142
+ this.#successes = 0;
143
+ this.#penalty = Math.min(this.#maxBackoff, this.#penalty * 2);
144
+ }
145
+ // Even a refusal that arrived too late to lower the limit still knows
146
+ // how long this provider wants to be left alone.
147
+ this.#pause(retryAfter ?? this.#penalty);
148
+ }
149
+ #pause(ms) {
150
+ const until = Date.now() + Math.max(0, ms);
151
+ if (until <= this.#pausedUntil) {
152
+ return;
153
+ }
154
+ this.#pausedUntil = until;
155
+ clearTimeout(this.#timer);
156
+ this.#timer = setTimeout(() => {
157
+ this.#timer = undefined;
158
+ this.#pump();
159
+ }, until - Date.now());
160
+ this.#timer.unref?.();
161
+ }
162
+ }
163
+ const clamp = (n, low, high) => Math.min(high, Math.max(low, n));
164
+ /** Full jitter: without it, everything that backed off together returns together. */
165
+ const jitter = (ms) => ms / 2 + Math.random() * (ms / 2);
166
+ function sleep(ms, signal) {
167
+ if (signal?.aborted) {
168
+ return Promise.reject(signal.reason);
169
+ }
170
+ return new Promise((resolve, reject) => {
171
+ const timer = setTimeout(() => {
172
+ signal?.removeEventListener('abort', abort);
173
+ resolve();
174
+ }, ms);
175
+ const abort = () => {
176
+ clearTimeout(timer);
177
+ reject(signal?.reason);
178
+ };
179
+ signal?.addEventListener('abort', abort, { once: true });
180
+ });
181
+ }
182
+ //# sourceMappingURL=limiter.js.map
@@ -1,11 +1,12 @@
1
1
  import type OpenAI from 'openai';
2
- import { type Embedder, type EmbeddingRequest, type EmbeddingResponse } from '../embedding.ts';
2
+ import type { Embedder, EmbeddingRequest, EmbeddingResponse } from '../embedding.ts';
3
+ import { type BatchOptions } from './fanout.ts';
3
4
  /**
4
5
  * Provider-specific knobs. `taskType` has no counterpart here: OpenAI's models
5
6
  * are symmetric, so a query and a document are encoded identically and the
6
7
  * request's hint is simply not sent.
7
8
  */
8
- export interface OpenAIEmbedderOptions {
9
+ export interface OpenAIEmbedderOptions extends BatchOptions {
9
10
  /** default width; a request may override it */
10
11
  dimensions?: number;
11
12
  }
@@ -1,26 +1,67 @@
1
- import { embeddingResponse, } from "../embedding.js";
2
- import { zeroUsage } from "../types.js";
1
+ import { callSites, called } from "../failure.js";
2
+ import { fanout } from "./fanout.js";
3
+ import { RateLimiter } from "./limiter.js";
4
+ /**
5
+ * What OpenAI documents for `text-embedding-*`: 2048 inputs, and 300k tokens
6
+ * across them. The token figure is the one that fires in practice, and it is
7
+ * held under the real cap because the estimate feeding it counts characters.
8
+ */
9
+ const MAX_BATCH = 2048;
10
+ const MAX_BATCH_TOKENS = 250_000;
11
+ /**
12
+ * A gateway is not OpenAI. `openai-compatible` reaches vLLM, Ollama and
13
+ * whatever else speaks the shape, none of which promise those numbers, so an
14
+ * unrecognised id gets something any implementation can be expected to hold.
15
+ */
16
+ const GATEWAY_BATCH = 96;
17
+ const GATEWAY_BATCH_TOKENS = 100_000;
18
+ const OPENAI_MODEL = /^text-embedding-/;
3
19
  export class OpenAIEmbedder {
4
20
  id;
5
21
  #client;
6
22
  #dimensions;
23
+ #limiter;
24
+ #maxBatch;
25
+ #maxBatchTokens;
26
+ #site;
7
27
  constructor(id, client, options = {}) {
8
28
  this.id = id;
9
29
  this.#client = client;
10
30
  this.#dimensions = options.dimensions;
31
+ this.#limiter = options.limiter ?? new RateLimiter();
32
+ this.#site = callSites(id, options.provider);
33
+ const known = OPENAI_MODEL.test(id);
34
+ this.#maxBatch = options.maxBatch ?? (known ? MAX_BATCH : GATEWAY_BATCH);
35
+ this.#maxBatchTokens =
36
+ options.maxBatchTokens ?? (known ? MAX_BATCH_TOKENS : GATEWAY_BATCH_TOKENS);
11
37
  }
12
38
  async embed(req) {
13
- const res = await this.#client.embeddings.create({
14
- model: this.id,
15
- input: req.input,
16
- dimensions: req.dimensions ?? this.#dimensions,
17
- }, { signal: req.signal });
18
- // The API documents the order but does not promise it, and `index` is
19
- // the only thing that ties a vector back to its text.
20
- const vectors = [...res.data].sort((a, b) => a.index - b.index).map((d) => d.embedding);
21
- return embeddingResponse(vectors, req, {
22
- ...zeroUsage(),
23
- inputTokens: res.usage.prompt_tokens,
39
+ return await fanout(req, {
40
+ id: this.id,
41
+ limiter: this.#limiter,
42
+ maxBatch: this.#maxBatch,
43
+ maxBatchTokens: this.#maxBatchTokens,
44
+ send: async (input) => {
45
+ const res = await called(this.#site('embedding', 'embeddings.create'), () => this.#client.embeddings.create({
46
+ model: this.id,
47
+ input,
48
+ dimensions: req.dimensions ?? this.#dimensions,
49
+ },
50
+ // The client's own retrying is off because a retry it
51
+ // serves is one the limiter never sees: the call
52
+ // succeeds, nothing learns the provider is refusing, and
53
+ // the run quietly takes as long as the backoff it did
54
+ // not report.
55
+ { signal: req.signal, maxRetries: 0 }));
56
+ // The API documents the order but does not promise it, and
57
+ // `index` is the only thing tying a vector back to its text.
58
+ return {
59
+ vectors: [...res.data]
60
+ .sort((a, b) => a.index - b.index)
61
+ .map((d) => d.embedding),
62
+ inputTokens: res.usage.prompt_tokens,
63
+ };
64
+ },
24
65
  });
25
66
  }
26
67
  }
@@ -1,8 +1,9 @@
1
1
  import type { OpenRouter } from '@openrouter/sdk';
2
2
  import type { ProviderPreferences } from '@openrouter/sdk/models';
3
3
  import type { Embedder, EmbeddingRequest, EmbeddingResponse } from '../embedding.ts';
4
+ import { type BatchOptions } from './fanout.ts';
4
5
  /** Provider-specific knobs. */
5
- export interface OpenRouterEmbedderOptions {
6
+ export interface OpenRouterEmbedderOptions extends BatchOptions {
6
7
  /** default width; a request may override it */
7
8
  dimensions?: number;
8
9
  /**
@@ -1,6 +1,6 @@
1
- import { embeddingResponse } from "../embedding.js";
2
- import { RETRY_STATUS_CODES } from "../models/openrouter.js";
3
- import { zeroUsage } from "../types.js";
1
+ import { callSites, called } from "../failure.js";
2
+ import { fanout } from "./fanout.js";
3
+ import { RateLimiter } from "./limiter.js";
4
4
  // ---------------------------------------------------------------------------
5
5
  // OpenRouter embeddings adapter
6
6
  //
@@ -10,36 +10,65 @@ import { zeroUsage } from "../types.js";
10
10
  // ---------------------------------------------------------------------------
11
11
  /** OpenRouter's word for the retrieval side, which it takes as a free string. */
12
12
  const INPUT_TYPES = { query: 'search_query', document: 'search_document' };
13
+ /**
14
+ * Conservative on purpose. What a batch may hold is whatever the upstream
15
+ * provider routing picked accepts, which is not knowable from here and can
16
+ * differ between two requests for the same model.
17
+ */
18
+ const MAX_BATCH = 96;
19
+ const MAX_BATCH_TOKENS = 100_000;
13
20
  export class OpenRouterEmbedder {
14
21
  id;
15
22
  #client;
16
23
  #options;
24
+ #limiter;
25
+ #site;
17
26
  constructor(id, client, options = {}) {
18
27
  this.id = id;
19
28
  this.#client = client;
20
29
  this.#options = options;
30
+ this.#limiter = options.limiter ?? new RateLimiter();
31
+ this.#site = callSites(id, options.provider);
21
32
  }
22
33
  async embed(req) {
23
- const requestBody = {
24
- model: this.id,
25
- input: req.input,
26
- dimensions: req.dimensions ?? this.#options.dimensions,
27
- inputType: req.taskType ? INPUT_TYPES[req.taskType] : undefined,
28
- provider: this.#options.routing,
29
- // `encodingFormat` is deliberately not sent. Its type is an open
30
- // enum, whose values are branded and so unreachable without a value
31
- // import of the SDK — and float is the default anyway. The string
32
- // branch of `embedding` below is what catches a gateway that
33
- // decides otherwise.
34
- };
35
- const res = await this.#client.embeddings.generate({ requestBody }, { fetchOptions: { signal: req.signal }, retryCodes: RETRY_STATUS_CODES });
36
- // The response is declared as the body *or* a bare string, so the
37
- // parsed shape has to be established before anything is read off it.
38
- if (typeof res === 'string') {
39
- throw new Error(`embedder "${this.id}": expected an embeddings response, got text`);
40
- }
41
- const vectors = [...res.data].sort(byIndex).map((d) => vectorOf(d, this.id));
42
- return embeddingResponse(vectors, req, res.usage ? { ...zeroUsage(), inputTokens: res.usage.promptTokens } : undefined);
34
+ return await fanout(req, {
35
+ id: this.id,
36
+ limiter: this.#limiter,
37
+ maxBatch: this.#options.maxBatch ?? MAX_BATCH,
38
+ maxBatchTokens: this.#options.maxBatchTokens ?? MAX_BATCH_TOKENS,
39
+ send: async (input) => {
40
+ const requestBody = {
41
+ model: this.id,
42
+ input,
43
+ dimensions: req.dimensions ?? this.#options.dimensions,
44
+ inputType: req.taskType ? INPUT_TYPES[req.taskType] : undefined,
45
+ provider: this.#options.routing,
46
+ // `encodingFormat` is deliberately not sent. Its type is an
47
+ // open enum, whose values are branded and so unreachable
48
+ // without a value import of the SDK — and float is the
49
+ // default anyway. The string branch of `embedding` below is
50
+ // what catches a gateway that decides otherwise.
51
+ };
52
+ const res = await called(this.#site('embedding', 'embeddings.generate'), () => this.#client.embeddings.generate({ requestBody }, {
53
+ fetchOptions: { signal: req.signal },
54
+ // No retrying here: a 429 the SDK swallows is a 429
55
+ // the limiter never hears about, and it is the only
56
+ // thing that can slow the whole run down rather than
57
+ // this one request.
58
+ retries: { strategy: 'none' },
59
+ }));
60
+ // The response is declared as the body *or* a bare string, so
61
+ // the parsed shape has to be established before anything is
62
+ // read off it.
63
+ if (typeof res === 'string') {
64
+ throw new Error(`embedder "${this.id}": expected an embeddings response, got text`);
65
+ }
66
+ return {
67
+ vectors: [...res.data].sort(byIndex).map((d) => vectorOf(d, this.id)),
68
+ inputTokens: res.usage?.promptTokens,
69
+ };
70
+ },
71
+ });
43
72
  }
44
73
  }
45
74
  /** `index` is optional here, so its absence has to mean "keep the order given". */
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Three answers, because a limiter can only do three things: send it again
3
+ * later and slow down, send it again later, or give up.
4
+ */
5
+ export type Failure = 'rate-limit' | 'transient' | 'fatal';
6
+ /** The HTTP status, wherever this vendor's SDK decided to keep it. */
7
+ export declare function statusOf(err: unknown): number | undefined;
8
+ export declare function classify(err: unknown): Failure;
9
+ export declare function isAbort(err: unknown): boolean;
10
+ /**
11
+ * How long the provider asked to be left alone. Absent far more often than
12
+ * not — only some vendors send it, and only on some of their limits — so the
13
+ * limiter has to have a schedule of its own regardless.
14
+ */
15
+ export declare function retryAfterMs(err: unknown): number | undefined;
16
+ //# sourceMappingURL=rate-limit.d.ts.map
@@ -0,0 +1,131 @@
1
+ // ---------------------------------------------------------------------------
2
+ // What a failed request means
3
+ //
4
+ // Four SDKs, four error shapes, one question: may this be sent again, and does
5
+ // the provider want to be called less often. Everything below is structural —
6
+ // no SDK type is imported, because three of the four are optional peers and an
7
+ // error that arrives from a gateway is not an instance of anything anyway.
8
+ //
9
+ // The one difference worth naming out loud is the status field. OpenAI,
10
+ // Anthropic and @google/genai put it on `status`; the OpenRouter client, being
11
+ // generated, calls it `statusCode`. Reading only one of them reports a revoked
12
+ // key or an exhausted quota as an unknown failure, which is the same trap the
13
+ // CLI's liveness probe already documents.
14
+ // ---------------------------------------------------------------------------
15
+ /** The provider is asking to be called less often, not saying the call was wrong. */
16
+ const RATE_LIMITED = new Set([429, 503]);
17
+ /** Worth another attempt, but says nothing about the rate. */
18
+ const TRANSIENT = new Set([408, 409, 500, 502, 504]);
19
+ /** Undici's words for a connection that never produced a response. */
20
+ const NETWORK = new Set([
21
+ 'ECONNRESET',
22
+ 'ECONNREFUSED',
23
+ 'ETIMEDOUT',
24
+ 'EPIPE',
25
+ 'EAI_AGAIN',
26
+ 'ENOTFOUND',
27
+ 'UND_ERR_SOCKET',
28
+ 'UND_ERR_CONNECT_TIMEOUT',
29
+ 'UND_ERR_HEADERS_TIMEOUT',
30
+ 'UND_ERR_BODY_TIMEOUT',
31
+ 'ERR_STREAM_PREMATURE_CLOSE',
32
+ ]);
33
+ /** The error and everything it blames, guarded against a self-referential cause. */
34
+ function* chain(err) {
35
+ const seen = new Set();
36
+ for (let at = err; at && typeof at === 'object' && !seen.has(at); at = at.cause) {
37
+ seen.add(at);
38
+ yield at;
39
+ }
40
+ }
41
+ /** The HTTP status, wherever this vendor's SDK decided to keep it. */
42
+ export function statusOf(err) {
43
+ for (const link of chain(err)) {
44
+ const raw = link.status ?? link.statusCode;
45
+ const status = typeof raw === 'string' ? Number(raw) : raw;
46
+ if (typeof status === 'number' && Number.isFinite(status)) {
47
+ return status;
48
+ }
49
+ }
50
+ return undefined;
51
+ }
52
+ export function classify(err) {
53
+ // An abort is the caller changing its mind, so it is never retried.
54
+ if (isAbort(err)) {
55
+ return 'fatal';
56
+ }
57
+ const status = statusOf(err);
58
+ if (status !== undefined) {
59
+ if (RATE_LIMITED.has(status)) {
60
+ return 'rate-limit';
61
+ }
62
+ return TRANSIENT.has(status) || status >= 500 ? 'transient' : 'fatal';
63
+ }
64
+ for (const link of chain(err)) {
65
+ if (typeof link.code === 'string' && NETWORK.has(link.code)) {
66
+ return 'transient';
67
+ }
68
+ }
69
+ return 'fatal';
70
+ }
71
+ export function isAbort(err) {
72
+ for (const link of chain(err)) {
73
+ if (link.name === 'AbortError' || link.code === 'ABORT_ERR') {
74
+ return true;
75
+ }
76
+ }
77
+ return false;
78
+ }
79
+ /**
80
+ * How long the provider asked to be left alone. Absent far more often than
81
+ * not — only some vendors send it, and only on some of their limits — so the
82
+ * limiter has to have a schedule of its own regardless.
83
+ */
84
+ export function retryAfterMs(err) {
85
+ for (const link of chain(err)) {
86
+ const headers = link.headers;
87
+ if (!headers) {
88
+ continue;
89
+ }
90
+ // OpenAI's own, and the more precise of the two when both are sent.
91
+ const ms = header(headers, 'retry-after-ms');
92
+ if (ms !== undefined) {
93
+ const value = Number(ms);
94
+ if (Number.isFinite(value) && value >= 0) {
95
+ return value;
96
+ }
97
+ }
98
+ const after = header(headers, 'retry-after');
99
+ if (after === undefined) {
100
+ continue;
101
+ }
102
+ // Seconds, or an HTTP date. The date form is rare but legal.
103
+ const seconds = Number(after);
104
+ if (Number.isFinite(seconds) && seconds >= 0) {
105
+ return seconds * 1000;
106
+ }
107
+ const at = Date.parse(after);
108
+ if (Number.isFinite(at)) {
109
+ return Math.max(0, at - Date.now());
110
+ }
111
+ }
112
+ return undefined;
113
+ }
114
+ /** Headers arrive as a `Headers` instance from one SDK and a plain object from another. */
115
+ function header(headers, name) {
116
+ if (typeof headers !== 'object' || headers === null) {
117
+ return undefined;
118
+ }
119
+ const get = headers.get;
120
+ if (typeof get === 'function') {
121
+ const value = get.call(headers, name);
122
+ return typeof value === 'string' ? value : undefined;
123
+ }
124
+ for (const [key, value] of Object.entries(headers)) {
125
+ if (key.toLowerCase() === name && typeof value === 'string') {
126
+ return value;
127
+ }
128
+ }
129
+ return undefined;
130
+ }
131
+ //# sourceMappingURL=rate-limit.js.map
@@ -0,0 +1,82 @@
1
+ /** What was being asked of a provider, in the runtime's own words. */
2
+ export type Doing = 'llm generation' | 'llm streaming' | 'embedding';
3
+ /** Which call, on whose connection, for which model. */
4
+ export interface CallSite {
5
+ doing: Doing;
6
+ /** the SDK call actually made, spelled the way the vendor spells it */
7
+ api: string;
8
+ /** the model or embedding id it was spent on */
9
+ model: string;
10
+ /** the declared provider name, when a registry built the adapter */
11
+ provider?: string;
12
+ }
13
+ /**
14
+ * The half of a call site an adapter knows before it is called, bound once so
15
+ * that each call names only itself.
16
+ */
17
+ export type CallSites = (doing: Doing, api: string) => CallSite;
18
+ export declare const callSites: (model: string, provider?: string) => CallSites;
19
+ /**
20
+ * Carried by every adapter's options so a failure can name the connection.
21
+ * Reporting only: the client is already built by the time an adapter sees it,
22
+ * so the name is here to be printed, not to be resolved.
23
+ */
24
+ export interface ProviderNamed {
25
+ /** the registered provider name this adapter speaks through */
26
+ provider?: string;
27
+ }
28
+ /** How a call went before it failed, which is most of what makes a failure readable. */
29
+ export interface Attempted {
30
+ /**
31
+ * How many times THIS runtime opened the call. Usually one: every SDK here
32
+ * is configured to retry the request itself, and all of that happens inside
33
+ * a single attempt — which is why the elapsed time matters more than the
34
+ * count. Above one only where a broken stream was reopened.
35
+ */
36
+ attempts: number;
37
+ elapsedMs: number;
38
+ /**
39
+ * Chunks delivered before the stream broke. Any at all means the answer had
40
+ * already started, and a stream that has emitted cannot be replayed: the
41
+ * deltas are on screen.
42
+ */
43
+ chunks?: number;
44
+ }
45
+ /** A provider call that failed, with the call it was. */
46
+ export declare class ProviderError extends Error {
47
+ readonly site: CallSite;
48
+ /** the vendor's own sentence, unwrapped out of whatever it arrived in */
49
+ readonly detail: string;
50
+ /** the HTTP status, wherever the SDK or the body left it */
51
+ readonly status?: number;
52
+ readonly attempts: number;
53
+ readonly elapsedMs: number;
54
+ readonly chunks?: number;
55
+ constructor(site: CallSite, detail: string, status: number | undefined, cause: unknown, tried?: Attempted);
56
+ }
57
+ /** Runs one provider call, and makes sure a refusal says what it refused. */
58
+ export declare function called<T>(site: CallSite, run: () => Promise<T>): Promise<T>;
59
+ /**
60
+ * The same for a response body: an HTTP error, and every mid-stream failure,
61
+ * arrives while the stream is being read — long after the call that opened it
62
+ * returned successfully. None of the SDKs retry that: their backoff wraps the
63
+ * request, and by the time a body is being consumed the request has succeeded.
64
+ *
65
+ * So this does, but only while nothing has come out of it. A connection reset
66
+ * before the first chunk is indistinguishable from one that never opened, and
67
+ * reopening is invisible to the caller. One chunk later it is not: the deltas
68
+ * have been delivered, a second stream would repeat them, and the failure has
69
+ * to be reported instead.
70
+ */
71
+ export declare function reading<T>(site: CallSite, source: AsyncIterable<T>, reopen?: () => Promise<AsyncIterable<T>>): AsyncIterable<T>;
72
+ /** The error to throw in place of `err`, which may be `err` itself. */
73
+ export declare function failed(site: CallSite, err: unknown, tried?: Attempted): unknown;
74
+ /**
75
+ * The sentence a vendor buried, and the status it came with. Exported because
76
+ * it is useful anywhere a raw provider error has to be shown to someone.
77
+ */
78
+ export declare function explain(err: unknown): {
79
+ detail: string;
80
+ status?: number;
81
+ };
82
+ //# sourceMappingURL=failure.d.ts.map