@zenera/neo 1.1.9 → 1.1.11
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/embedding.d.ts +20 -1
- package/dist/embedding.js +3 -2
- package/dist/embeddings/fanout.d.ts +51 -0
- package/dist/embeddings/fanout.js +76 -0
- package/dist/embeddings/gemini.d.ts +3 -10
- package/dist/embeddings/gemini.js +46 -30
- package/dist/embeddings/index.d.ts +3 -0
- package/dist/embeddings/index.js +3 -0
- package/dist/embeddings/limiter.d.ts +36 -0
- package/dist/embeddings/limiter.js +182 -0
- package/dist/embeddings/openai.d.ts +3 -2
- package/dist/embeddings/openai.js +54 -13
- package/dist/embeddings/openrouter.d.ts +2 -1
- package/dist/embeddings/openrouter.js +52 -22
- package/dist/embeddings/rate-limit.d.ts +16 -0
- package/dist/embeddings/rate-limit.js +131 -0
- package/dist/failure.d.ts +82 -0
- package/dist/failure.js +323 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/models/anthropic.d.ts +2 -1
- package/dist/models/anthropic.js +11 -4
- package/dist/models/factory.d.ts +15 -0
- package/dist/models/factory.js +114 -13
- package/dist/models/gemini.d.ts +2 -1
- package/dist/models/gemini.js +8 -3
- package/dist/models/openai-chat.d.ts +2 -1
- package/dist/models/openai-chat.js +10 -5
- package/dist/models/openai-responses.d.ts +2 -1
- package/dist/models/openai-responses.js +22 -7
- package/dist/models/openrouter.d.ts +12 -1
- package/dist/models/openrouter.js +41 -7
- package/dist/project/config.d.ts +4 -0
- package/dist/project/config.js +13 -1
- package/package.json +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type Anthropic from '@anthropic-ai/sdk';
|
|
2
2
|
import type { StreamDelta } from '../events.ts';
|
|
3
|
+
import { type ProviderNamed } from '../failure.ts';
|
|
3
4
|
import type { Model, ModelRequest, ModelResponse } from '../model.ts';
|
|
4
5
|
/**
|
|
5
6
|
* Provider-specific knobs.
|
|
@@ -9,7 +10,7 @@ import type { Model, ModelRequest, ModelResponse } from '../model.ts';
|
|
|
9
10
|
* exist. 8192 is the largest value every current Claude model accepts, which
|
|
10
11
|
* makes it the only safe default — raise it per model when you know better.
|
|
11
12
|
*/
|
|
12
|
-
export interface AnthropicModelOptions {
|
|
13
|
+
export interface AnthropicModelOptions extends ProviderNamed {
|
|
13
14
|
/** hard cap on output tokens; required by the API, so it always has a value */
|
|
14
15
|
maxTokens?: number;
|
|
15
16
|
/**
|
package/dist/models/anthropic.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { callSites, called, reading } from "../failure.js";
|
|
1
2
|
import { zeroUsage, } from "../types.js";
|
|
2
3
|
/** Every current Claude model accepts at least this many output tokens. */
|
|
3
4
|
const DEFAULT_MAX_TOKENS = 8192;
|
|
@@ -16,9 +17,11 @@ export class AnthropicModel {
|
|
|
16
17
|
#client;
|
|
17
18
|
#maxTokens;
|
|
18
19
|
#thinkingBudget;
|
|
20
|
+
#site;
|
|
19
21
|
constructor(id, client, options = {}) {
|
|
20
22
|
this.id = id;
|
|
21
23
|
this.#client = client;
|
|
24
|
+
this.#site = callSites(id, options.provider);
|
|
22
25
|
this.#thinkingBudget =
|
|
23
26
|
options.thinkingBudgetTokens && options.thinkingBudgetTokens >= MIN_THINKING_BUDGET
|
|
24
27
|
? options.thinkingBudgetTokens
|
|
@@ -28,7 +31,7 @@ export class AnthropicModel {
|
|
|
28
31
|
this.#maxTokens = Math.max(options.maxTokens ?? DEFAULT_MAX_TOKENS, (this.#thinkingBudget ?? 0) + MIN_THINKING_BUDGET);
|
|
29
32
|
}
|
|
30
33
|
async generate(req) {
|
|
31
|
-
const res = await this.#client.messages.create(this.#params(req), { signal: req.signal });
|
|
34
|
+
const res = await called(this.#site('llm generation', 'messages.create'), () => this.#client.messages.create(this.#params(req), { signal: req.signal }));
|
|
32
35
|
return {
|
|
33
36
|
...readBlocks(res.content),
|
|
34
37
|
usage: toUsage(res.usage),
|
|
@@ -36,11 +39,15 @@ export class AnthropicModel {
|
|
|
36
39
|
};
|
|
37
40
|
}
|
|
38
41
|
async stream(req, onDelta) {
|
|
39
|
-
const
|
|
42
|
+
const site = this.#site('llm streaming', 'messages.stream');
|
|
43
|
+
// Reassigned when a broken stream is reopened, so that `finalMessage()`
|
|
44
|
+
// below is asked of whichever stream actually delivered the answer.
|
|
45
|
+
let stream = this.#client.messages.stream(this.#params(req), { signal: req.signal });
|
|
46
|
+
const open = async () => (stream = this.#client.messages.stream(this.#params(req), { signal: req.signal }));
|
|
40
47
|
// Blocks are addressed by index, and a tool call's id and name arrive in
|
|
41
48
|
// its `content_block_start` while the arguments trickle in afterwards.
|
|
42
49
|
const calls = new Map();
|
|
43
|
-
for await (const event of stream) {
|
|
50
|
+
for await (const event of reading(site, stream, open)) {
|
|
44
51
|
if (event.type === 'content_block_start') {
|
|
45
52
|
const block = event.content_block;
|
|
46
53
|
if (block.type === 'tool_use') {
|
|
@@ -79,7 +86,7 @@ export class AnthropicModel {
|
|
|
79
86
|
// The accumulated message is authoritative: it has the parsed tool
|
|
80
87
|
// inputs and the final usage, so the deltas above are for the UI only
|
|
81
88
|
// and nothing here depends on having reassembled them correctly.
|
|
82
|
-
const final = await stream.finalMessage();
|
|
89
|
+
const final = await called(site, () => stream.finalMessage());
|
|
83
90
|
return {
|
|
84
91
|
...readBlocks(final.content),
|
|
85
92
|
usage: toUsage(final.usage),
|
package/dist/models/factory.d.ts
CHANGED
|
@@ -64,6 +64,16 @@ export interface ProviderSpec extends Credentials {
|
|
|
64
64
|
headers?: Record<string, string>;
|
|
65
65
|
timeoutMs?: number;
|
|
66
66
|
maxRetries?: number;
|
|
67
|
+
/**
|
|
68
|
+
* Ceiling on embedding requests in flight against this connection. The
|
|
69
|
+
* floor and the actual number are discovered from the provider's own
|
|
70
|
+
* refusals, so this only says how high the discovery may climb — set it
|
|
71
|
+
* when an account is shared with something else that also needs quota.
|
|
72
|
+
*
|
|
73
|
+
* On the provider rather than on a model because a rate limit belongs to
|
|
74
|
+
* the key: two embedders on one account are one budget, not two.
|
|
75
|
+
*/
|
|
76
|
+
concurrency?: number;
|
|
67
77
|
/**
|
|
68
78
|
* Pre-built client — bypasses credential resolution entirely. The seam for
|
|
69
79
|
* auth no configuration can express: Vertex service accounts, Bedrock
|
|
@@ -182,6 +192,11 @@ export declare class ModelRegistry {
|
|
|
182
192
|
requirement(ref: ModelRef | EmbeddingRef): ModelRequirement;
|
|
183
193
|
/** `[provider[/api]:]model` → spec. Exposed so a caller can validate a ref. */
|
|
184
194
|
parse(ref: string): ModelSpec;
|
|
195
|
+
/**
|
|
196
|
+
* Splits `[provider:]model` into the spec it stands for, so a caller holding
|
|
197
|
+
* a shorthand can still set the knobs a shorthand has no room for.
|
|
198
|
+
*/
|
|
199
|
+
parseEmbedding(ref: string): EmbeddingSpec;
|
|
185
200
|
/** A declared kind wins; otherwise a built-in name is its own kind. */
|
|
186
201
|
kindOf(name: string): ProviderKind;
|
|
187
202
|
}
|
package/dist/models/factory.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
3
|
import { GeminiEmbedder } from "../embeddings/gemini.js";
|
|
4
|
+
import { DEFAULT_START, RateLimiter } from "../embeddings/limiter.js";
|
|
4
5
|
import { OpenAIEmbedder } from "../embeddings/openai.js";
|
|
5
6
|
import { OpenRouterEmbedder } from "../embeddings/openrouter.js";
|
|
6
7
|
import { AnthropicModel } from "./anthropic.js";
|
|
@@ -159,6 +160,7 @@ function expand(value, where) {
|
|
|
159
160
|
export class ModelRegistry {
|
|
160
161
|
#providers = new Map();
|
|
161
162
|
#clients = new Map();
|
|
163
|
+
#limiters = new Map();
|
|
162
164
|
#default = 'openai';
|
|
163
165
|
/** Declares (or replaces) a provider. Nothing is contacted until it is used. */
|
|
164
166
|
provider(name, spec = {}) {
|
|
@@ -170,6 +172,7 @@ export class ModelRegistry {
|
|
|
170
172
|
// Re-declaring after a client was handed out would otherwise leave the
|
|
171
173
|
// old credentials in service for the rest of the process.
|
|
172
174
|
this.#clients.delete(name);
|
|
175
|
+
this.#limiters.delete(name);
|
|
173
176
|
return this;
|
|
174
177
|
}
|
|
175
178
|
/** Names the provider a bare model id belongs to. */
|
|
@@ -201,6 +204,27 @@ export class ModelRegistry {
|
|
|
201
204
|
this.#clients.set(name, built);
|
|
202
205
|
return built;
|
|
203
206
|
}
|
|
207
|
+
/**
|
|
208
|
+
* The pacing shared by every embedder on a provider, so that what one of
|
|
209
|
+
* them learns from a 429 the others do not have to learn again — and so
|
|
210
|
+
* that two of them cannot each ramp up to the full limit against one key.
|
|
211
|
+
*/
|
|
212
|
+
#limiter(name) {
|
|
213
|
+
const existing = this.#limiters.get(name);
|
|
214
|
+
if (existing) {
|
|
215
|
+
return existing;
|
|
216
|
+
}
|
|
217
|
+
const spec = this.#providers.get(name);
|
|
218
|
+
const built = new RateLimiter({
|
|
219
|
+
max: spec?.concurrency,
|
|
220
|
+
start: spec?.concurrency === undefined
|
|
221
|
+
? undefined
|
|
222
|
+
: Math.min(spec.concurrency, DEFAULT_START),
|
|
223
|
+
maxRetries: spec?.maxRetries,
|
|
224
|
+
});
|
|
225
|
+
this.#limiters.set(name, built);
|
|
226
|
+
return built;
|
|
227
|
+
}
|
|
204
228
|
/** Turns a shorthand or a spec into a `Model`. */
|
|
205
229
|
model(ref) {
|
|
206
230
|
const spec = typeof ref === 'string' ? this.parse(ref) : ref;
|
|
@@ -213,6 +237,10 @@ export class ModelRegistry {
|
|
|
213
237
|
// silently ignore them.
|
|
214
238
|
const client = spec.client ??
|
|
215
239
|
(hasCredentials(spec) ? buildClient(name, kind, spec, provider) : this.client(name));
|
|
240
|
+
// The *resolved* name, not `spec.provider`: a bare ref carries none,
|
|
241
|
+
// and a failure that cannot say which connection refused it is the
|
|
242
|
+
// thing this is here to prevent.
|
|
243
|
+
const options = { ...spec, provider: name };
|
|
216
244
|
// Every protocol but OpenAI's has exactly one API, so naming one is a
|
|
217
245
|
// mistake worth reporting rather than a field to ignore.
|
|
218
246
|
if (defaults.protocol !== 'openai') {
|
|
@@ -221,11 +249,11 @@ export class ModelRegistry {
|
|
|
221
249
|
}
|
|
222
250
|
switch (defaults.protocol) {
|
|
223
251
|
case 'anthropic':
|
|
224
|
-
return new AnthropicModel(spec.model, client,
|
|
252
|
+
return new AnthropicModel(spec.model, client, options);
|
|
225
253
|
case 'gemini':
|
|
226
|
-
return new GeminiModel(spec.model, client,
|
|
254
|
+
return new GeminiModel(spec.model, client, options);
|
|
227
255
|
case 'openrouter':
|
|
228
|
-
return new OpenRouterModel(spec.model, client,
|
|
256
|
+
return new OpenRouterModel(spec.model, client, options);
|
|
229
257
|
}
|
|
230
258
|
}
|
|
231
259
|
const api = spec.api ?? defaults.apis[0];
|
|
@@ -234,8 +262,8 @@ export class ModelRegistry {
|
|
|
234
262
|
`(supported: ${defaults.apis.join(', ')})`);
|
|
235
263
|
}
|
|
236
264
|
return api === 'responses'
|
|
237
|
-
? new OpenAIResponsesModel(spec.model, client,
|
|
238
|
-
: new OpenAIModel(spec.model, client,
|
|
265
|
+
? new OpenAIResponsesModel(spec.model, client, options)
|
|
266
|
+
: new OpenAIModel(spec.model, client, options);
|
|
239
267
|
}
|
|
240
268
|
/**
|
|
241
269
|
* Turns a shorthand or a spec into an `Embedder`, over the same providers
|
|
@@ -243,19 +271,26 @@ export class ModelRegistry {
|
|
|
243
271
|
* generates and embeds without being mentioned twice.
|
|
244
272
|
*/
|
|
245
273
|
embedder(ref) {
|
|
246
|
-
const spec = typeof ref === 'string' ? this
|
|
274
|
+
const spec = typeof ref === 'string' ? this.parseEmbedding(ref) : ref;
|
|
247
275
|
const name = spec.provider ?? this.#default;
|
|
248
276
|
const provider = this.#spec(name);
|
|
249
277
|
const kind = this.kindOf(name);
|
|
250
278
|
const client = spec.client ??
|
|
251
279
|
(hasCredentials(spec) ? buildClient(name, kind, spec, provider) : this.client(name));
|
|
280
|
+
// Inline credentials are a different connection, and so a different
|
|
281
|
+
// budget: they get pacing of their own rather than the provider's.
|
|
282
|
+
const options = {
|
|
283
|
+
...spec,
|
|
284
|
+
provider: name,
|
|
285
|
+
limiter: spec.limiter ?? (hasCredentials(spec) ? undefined : this.#limiter(name)),
|
|
286
|
+
};
|
|
252
287
|
switch (KINDS[kind].protocol) {
|
|
253
288
|
case 'openai':
|
|
254
|
-
return new OpenAIEmbedder(spec.model, client,
|
|
289
|
+
return new OpenAIEmbedder(spec.model, client, options);
|
|
255
290
|
case 'gemini':
|
|
256
|
-
return new GeminiEmbedder(spec.model, client,
|
|
291
|
+
return new GeminiEmbedder(spec.model, client, options);
|
|
257
292
|
case 'openrouter':
|
|
258
|
-
return new OpenRouterEmbedder(spec.model, client,
|
|
293
|
+
return new OpenRouterEmbedder(spec.model, client, options);
|
|
259
294
|
case 'anthropic':
|
|
260
295
|
// Not an omission to be filled in later: Anthropic publishes no
|
|
261
296
|
// embeddings API at all, and points at third parties instead.
|
|
@@ -318,7 +353,11 @@ export class ModelRegistry {
|
|
|
318
353
|
}
|
|
319
354
|
return { provider, api, model };
|
|
320
355
|
}
|
|
321
|
-
|
|
356
|
+
/**
|
|
357
|
+
* Splits `[provider:]model` into the spec it stands for, so a caller holding
|
|
358
|
+
* a shorthand can still set the knobs a shorthand has no room for.
|
|
359
|
+
*/
|
|
360
|
+
parseEmbedding(ref) {
|
|
322
361
|
const { provider, api, model } = this.parse(ref);
|
|
323
362
|
if (api) {
|
|
324
363
|
throw new TypeError(`"${ref}": embeddings have one api, so "${api}" means nothing here`);
|
|
@@ -342,6 +381,48 @@ export class ModelRegistry {
|
|
|
342
381
|
return declared ?? (isProviderKind(name) ? name : 'openai');
|
|
343
382
|
}
|
|
344
383
|
}
|
|
384
|
+
// ---------------------------------------------------------------------------
|
|
385
|
+
// Retries
|
|
386
|
+
// ---------------------------------------------------------------------------
|
|
387
|
+
/**
|
|
388
|
+
* One backoff policy for every vendor, because a 429 is not a failure of the
|
|
389
|
+
* run — it is the provider asking to be called again later, and an agent that
|
|
390
|
+
* gives up on the first one loses the whole trajectory to a wait of a few
|
|
391
|
+
* seconds. Every SDK here can retry, but none of them agree on what happens
|
|
392
|
+
* when nothing is configured: the OpenAI and Anthropic clients retry twice,
|
|
393
|
+
* `@google/genai` does not retry *at all* unless `retryOptions` is present, and
|
|
394
|
+
* OpenRouter's generated client retries `5XX` only — so a Vertex or OpenRouter
|
|
395
|
+
* 429 was fatal by default. These constants are what make the four behave the
|
|
396
|
+
* same; `ProviderSpec.maxRetries` (0 disables) is the per-provider override.
|
|
397
|
+
*
|
|
398
|
+
* Retries live at the transport, not around `Model.generate`, so a retried call
|
|
399
|
+
* is one that never started: no partial stream is replayed, and a `Retry-After`
|
|
400
|
+
* header is honoured by the SDK that read it.
|
|
401
|
+
*/
|
|
402
|
+
const DEFAULT_MAX_RETRIES = 4;
|
|
403
|
+
/** wait after the first failure; doubles per attempt up to `RETRY_MAX_MS` */
|
|
404
|
+
const RETRY_INITIAL_MS = 1_000;
|
|
405
|
+
const RETRY_MAX_MS = 60_000;
|
|
406
|
+
const RETRY_EXPONENT = 2;
|
|
407
|
+
function retriesOf(opts) {
|
|
408
|
+
return Math.max(0, opts.maxRetries ?? DEFAULT_MAX_RETRIES);
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* The same schedule expressed as a wall-clock budget, which is the only shape
|
|
412
|
+
* OpenRouter's client accepts. It is the sum of the waits a count of `retries`
|
|
413
|
+
* would spend, so the two agree on how long a hopeless call may take — not on
|
|
414
|
+
* exactly how many times it is made, since this budget also has to cover the
|
|
415
|
+
* requests themselves.
|
|
416
|
+
*/
|
|
417
|
+
function retryWindowMs(retries) {
|
|
418
|
+
let wait = RETRY_INITIAL_MS;
|
|
419
|
+
let total = 0;
|
|
420
|
+
for (let i = 0; i < retries; i++) {
|
|
421
|
+
total += Math.min(wait, RETRY_MAX_MS);
|
|
422
|
+
wait *= RETRY_EXPONENT;
|
|
423
|
+
}
|
|
424
|
+
return total;
|
|
425
|
+
}
|
|
345
426
|
/**
|
|
346
427
|
* Credential resolution, in one place so a `${VAR}` reference behaves the same
|
|
347
428
|
* whether it came from yaml or from application code. `creds` may be a model
|
|
@@ -375,7 +456,7 @@ function buildClient(name, kind, creds, opts) {
|
|
|
375
456
|
baseURL,
|
|
376
457
|
defaultHeaders: headers,
|
|
377
458
|
timeout: opts.timeoutMs,
|
|
378
|
-
maxRetries: opts
|
|
459
|
+
maxRetries: retriesOf(opts),
|
|
379
460
|
...(opts.token ? { fetch: bearerFetch(opts.token) } : {}),
|
|
380
461
|
};
|
|
381
462
|
if (defaults.protocol === 'anthropic') {
|
|
@@ -408,13 +489,25 @@ function buildOpenRouter(kind, apiKey, baseURL, headers, opts) {
|
|
|
408
489
|
});
|
|
409
490
|
}
|
|
410
491
|
const { token } = opts;
|
|
492
|
+
const retries = retriesOf(opts);
|
|
411
493
|
return new OpenRouter({
|
|
412
494
|
apiKey: token ? async () => token() : apiKey,
|
|
413
495
|
serverURL: baseURL,
|
|
414
496
|
timeoutMs: opts.timeoutMs,
|
|
415
497
|
// A count of zero is the one value that has to change shape: this SDK
|
|
416
498
|
// says "do not retry" with a strategy, not with a number.
|
|
417
|
-
retryConfig:
|
|
499
|
+
retryConfig: retries === 0
|
|
500
|
+
? { strategy: 'none' }
|
|
501
|
+
: {
|
|
502
|
+
strategy: 'backoff',
|
|
503
|
+
backoff: {
|
|
504
|
+
initialInterval: RETRY_INITIAL_MS,
|
|
505
|
+
maxInterval: RETRY_MAX_MS,
|
|
506
|
+
exponent: RETRY_EXPONENT,
|
|
507
|
+
maxElapsedTime: retryWindowMs(retries),
|
|
508
|
+
},
|
|
509
|
+
retryConnectionErrors: true,
|
|
510
|
+
},
|
|
418
511
|
httpClient,
|
|
419
512
|
});
|
|
420
513
|
}
|
|
@@ -438,7 +531,15 @@ function buildOpenRouter(kind, apiKey, baseURL, headers, opts) {
|
|
|
438
531
|
*/
|
|
439
532
|
function buildGenAI(kind, apiKey, baseURL, headers, opts, where) {
|
|
440
533
|
const { GoogleGenAI } = sdk('@google/genai', kind);
|
|
441
|
-
|
|
534
|
+
// `attempts` counts the initial call, so a retry budget of zero is one
|
|
535
|
+
// attempt. Delays are fractions of a second here, unlike everywhere else.
|
|
536
|
+
const retryOptions = {
|
|
537
|
+
attempts: retriesOf(opts) + 1,
|
|
538
|
+
initialDelay: RETRY_INITIAL_MS / 1000,
|
|
539
|
+
maxDelay: RETRY_MAX_MS / 1000,
|
|
540
|
+
expBase: RETRY_EXPONENT,
|
|
541
|
+
};
|
|
542
|
+
const httpOptions = { baseUrl: baseURL, headers, timeout: opts.timeoutMs, retryOptions };
|
|
442
543
|
if (kind !== 'vertex') {
|
|
443
544
|
return new GoogleGenAI({ apiKey, httpOptions });
|
|
444
545
|
}
|
package/dist/models/gemini.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { GoogleGenAI } from '@google/genai';
|
|
2
2
|
import type { StreamDelta } from '../events.ts';
|
|
3
|
+
import { type ProviderNamed } from '../failure.ts';
|
|
3
4
|
import type { Model, ModelRequest, ModelResponse } from '../model.ts';
|
|
4
5
|
/** The coarse dial Gemini 3 takes in place of a token budget. */
|
|
5
6
|
export type GeminiThinkingLevel = 'minimal' | 'low' | 'medium' | 'high';
|
|
@@ -11,7 +12,7 @@ export type GeminiThinkingLevel = 'minimal' | 'low' | 'medium' | 'high';
|
|
|
11
12
|
* 3 takes a coarse `thinkingLevel`, and passing the wrong one is an API error
|
|
12
13
|
* rather than something this adapter can paper over.
|
|
13
14
|
*/
|
|
14
|
-
export interface GeminiModelOptions {
|
|
15
|
+
export interface GeminiModelOptions extends ProviderNamed {
|
|
15
16
|
/** hard cap on output tokens; the model's own default applies when unset */
|
|
16
17
|
maxTokens?: number;
|
|
17
18
|
/** 2.5-era budget, in tokens: `0` disables thinking, `-1` lets the model decide */
|
package/dist/models/gemini.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { callSites, called, reading } from "../failure.js";
|
|
1
2
|
import { zeroUsage, } from "../types.js";
|
|
2
3
|
/**
|
|
3
4
|
* Google's `ThinkingLevel` is a *string* enum, so its members are just their
|
|
@@ -40,14 +41,16 @@ export class GeminiModel {
|
|
|
40
41
|
id;
|
|
41
42
|
#client;
|
|
42
43
|
#options;
|
|
44
|
+
#site;
|
|
43
45
|
#nextId = 0;
|
|
44
46
|
constructor(id, client, options = {}) {
|
|
45
47
|
this.id = id;
|
|
46
48
|
this.#client = client;
|
|
47
49
|
this.#options = options;
|
|
50
|
+
this.#site = callSites(id, options.provider);
|
|
48
51
|
}
|
|
49
52
|
async generate(req) {
|
|
50
|
-
const res = await this.#client.models.generateContent(this.#params(req));
|
|
53
|
+
const res = await called(this.#site('llm generation', 'models.generateContent'), () => this.#client.models.generateContent(this.#params(req)));
|
|
51
54
|
const read = emptyRead();
|
|
52
55
|
for (const part of candidateParts(res)) {
|
|
53
56
|
this.#readPart(part, read);
|
|
@@ -55,10 +58,12 @@ export class GeminiModel {
|
|
|
55
58
|
return this.#finish(read, res);
|
|
56
59
|
}
|
|
57
60
|
async stream(req, onDelta) {
|
|
58
|
-
const
|
|
61
|
+
const site = this.#site('llm streaming', 'models.generateContentStream');
|
|
62
|
+
const open = () => this.#client.models.generateContentStream(this.#params(req));
|
|
63
|
+
const stream = await called(site, open);
|
|
59
64
|
const read = emptyRead();
|
|
60
65
|
let last;
|
|
61
|
-
for await (const chunk of stream) {
|
|
66
|
+
for await (const chunk of reading(site, stream, open)) {
|
|
62
67
|
last = chunk;
|
|
63
68
|
for (const part of candidateParts(chunk)) {
|
|
64
69
|
this.#readPart(part, read, onDelta);
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import type OpenAI from 'openai';
|
|
2
2
|
import type { StreamDelta } from '../events.ts';
|
|
3
|
+
import { type ProviderNamed } from '../failure.ts';
|
|
3
4
|
import type { Model, ModelRequest, ModelResponse } from '../model.ts';
|
|
4
5
|
/**
|
|
5
6
|
* Provider-specific knobs. Reasoning effort is deliberately *not* part of
|
|
6
7
|
* `ModelRequest`: every provider spells it differently (and most models ignore
|
|
7
8
|
* it), so it belongs to the concrete adapter rather than the `Model` contract.
|
|
8
9
|
*/
|
|
9
|
-
export interface OpenAIModelOptions {
|
|
10
|
+
export interface OpenAIModelOptions extends ProviderNamed {
|
|
10
11
|
reasoningEffort?: OpenAI.ReasoningEffort;
|
|
11
12
|
}
|
|
12
13
|
export declare class OpenAIModel implements Model {
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
var _a;
|
|
2
|
+
import { callSites, called, reading } from "../failure.js";
|
|
2
3
|
import { zeroUsage } from "../types.js";
|
|
4
|
+
/** The SDK call these methods make, named as it is named in a failure. */
|
|
5
|
+
const API = 'chat.completions.create';
|
|
3
6
|
// ---------------------------------------------------------------------------
|
|
4
7
|
// OpenAI chat-completions adapter
|
|
5
8
|
// ---------------------------------------------------------------------------
|
|
@@ -20,15 +23,15 @@ export class OpenAIModel {
|
|
|
20
23
|
id;
|
|
21
24
|
#client;
|
|
22
25
|
#reasoningEffort;
|
|
26
|
+
#site;
|
|
23
27
|
constructor(id, client, options = {}) {
|
|
24
28
|
this.id = id;
|
|
25
29
|
this.#client = client;
|
|
26
30
|
this.#reasoningEffort = options.reasoningEffort;
|
|
31
|
+
this.#site = callSites(id, options.provider);
|
|
27
32
|
}
|
|
28
33
|
async generate(req) {
|
|
29
|
-
const res = await this.#client.chat.completions.create(this.#params(req), {
|
|
30
|
-
signal: req.signal,
|
|
31
|
-
});
|
|
34
|
+
const res = await called(this.#site('llm generation', API), () => this.#client.chat.completions.create(this.#params(req), { signal: req.signal }));
|
|
32
35
|
const choice = res.choices[0];
|
|
33
36
|
const message = choice?.message;
|
|
34
37
|
return {
|
|
@@ -42,7 +45,9 @@ export class OpenAIModel {
|
|
|
42
45
|
};
|
|
43
46
|
}
|
|
44
47
|
async stream(req, onDelta) {
|
|
45
|
-
const
|
|
48
|
+
const site = this.#site('llm streaming', API);
|
|
49
|
+
const open = () => this.#client.chat.completions.create({ ...this.#params(req), stream: true, stream_options: { include_usage: true } }, { signal: req.signal });
|
|
50
|
+
const stream = await called(site, open);
|
|
46
51
|
let text = '';
|
|
47
52
|
let thinking = '';
|
|
48
53
|
let usage;
|
|
@@ -50,7 +55,7 @@ export class OpenAIModel {
|
|
|
50
55
|
// Tool calls arrive as deltas keyed by index, with the id and name in the
|
|
51
56
|
// first fragment and arguments trickling in afterwards.
|
|
52
57
|
const calls = new Map();
|
|
53
|
-
for await (const chunk of stream) {
|
|
58
|
+
for await (const chunk of reading(site, stream, open)) {
|
|
54
59
|
if (chunk.usage) {
|
|
55
60
|
usage = toUsage(chunk.usage);
|
|
56
61
|
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import type OpenAI from 'openai';
|
|
2
2
|
import type { StreamDelta } from '../events.ts';
|
|
3
|
+
import { type ProviderNamed } from '../failure.ts';
|
|
3
4
|
import type { Model, ModelRequest, ModelResponse } from '../model.ts';
|
|
4
5
|
/**
|
|
5
6
|
* Provider-specific knobs, mirroring `OpenAIModelOptions` but expressed the way
|
|
6
7
|
* the responses API spells them.
|
|
7
8
|
*/
|
|
8
|
-
export interface OpenAIResponsesModelOptions {
|
|
9
|
+
export interface OpenAIResponsesModelOptions extends ProviderNamed {
|
|
9
10
|
reasoningEffort?: OpenAI.ReasoningEffort;
|
|
10
11
|
/** ask the model for a reasoning summary — the only reasoning text this API exposes */
|
|
11
12
|
reasoningSummary?: 'auto' | 'concise' | 'detailed';
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
var _a;
|
|
2
|
+
import { callSites, called, failed, reading, } from "../failure.js";
|
|
2
3
|
import { zeroUsage } from "../types.js";
|
|
4
|
+
/** The SDK call these methods make, named as it is named in a failure. */
|
|
5
|
+
const API = 'responses.create';
|
|
3
6
|
/**
|
|
4
7
|
* Same `Model` contract as `OpenAIModel`, over `/v1/responses` instead of
|
|
5
8
|
* `/v1/chat/completions`. Reasoning models are the reason to prefer it: effort
|
|
@@ -11,17 +14,17 @@ export class OpenAIResponsesModel {
|
|
|
11
14
|
#reasoningEffort;
|
|
12
15
|
#reasoningSummary;
|
|
13
16
|
#store;
|
|
17
|
+
#site;
|
|
14
18
|
constructor(id, client, options = {}) {
|
|
15
19
|
this.id = id;
|
|
16
20
|
this.#client = client;
|
|
17
21
|
this.#reasoningEffort = options.reasoningEffort;
|
|
18
22
|
this.#reasoningSummary = options.reasoningSummary;
|
|
19
23
|
this.#store = options.store ?? false;
|
|
24
|
+
this.#site = callSites(id, options.provider);
|
|
20
25
|
}
|
|
21
26
|
async generate(req) {
|
|
22
|
-
const res = await this.#client.responses.create(this.#params(req), {
|
|
23
|
-
signal: req.signal,
|
|
24
|
-
});
|
|
27
|
+
const res = await called(this.#site('llm generation', API), () => this.#client.responses.create(this.#params(req), { signal: req.signal }));
|
|
25
28
|
const toolCalls = readToolCalls(res.output);
|
|
26
29
|
return {
|
|
27
30
|
text: readText(res.output),
|
|
@@ -32,7 +35,11 @@ export class OpenAIResponsesModel {
|
|
|
32
35
|
};
|
|
33
36
|
}
|
|
34
37
|
async stream(req, onDelta) {
|
|
35
|
-
const
|
|
38
|
+
const site = this.#site('llm streaming', API);
|
|
39
|
+
const open = () => this.#client.responses.create({ ...this.#params(req), stream: true }, {
|
|
40
|
+
signal: req.signal,
|
|
41
|
+
});
|
|
42
|
+
const stream = await called(site, open);
|
|
36
43
|
let text = '';
|
|
37
44
|
let thinking = '';
|
|
38
45
|
let final;
|
|
@@ -40,7 +47,7 @@ export class OpenAIResponsesModel {
|
|
|
40
47
|
// every delta event carries — that is the only stable key while the
|
|
41
48
|
// items are still being assembled.
|
|
42
49
|
const calls = new Map();
|
|
43
|
-
for await (const event of stream) {
|
|
50
|
+
for await (const event of reading(site, stream, open)) {
|
|
44
51
|
// Some models/gateways emit `response.reasoning.delta`, which the
|
|
45
52
|
// SDK does not carry in its event union, so it is matched before
|
|
46
53
|
// the typed switch.
|
|
@@ -103,9 +110,17 @@ export class OpenAIResponsesModel {
|
|
|
103
110
|
}
|
|
104
111
|
break;
|
|
105
112
|
}
|
|
106
|
-
case 'response.completed':
|
|
107
|
-
case 'response.incomplete':
|
|
108
113
|
case 'response.failed': {
|
|
114
|
+
// The request was accepted, so the refusal arrives as an
|
|
115
|
+
// event in a 200 rather than as a status. Returning it as a
|
|
116
|
+
// response would make it an empty answer.
|
|
117
|
+
const said = event.response.error;
|
|
118
|
+
throw failed(site, Object.assign(new Error(said?.message ?? 'the response failed'), {
|
|
119
|
+
code: said?.code,
|
|
120
|
+
}));
|
|
121
|
+
}
|
|
122
|
+
case 'response.completed':
|
|
123
|
+
case 'response.incomplete': {
|
|
109
124
|
final = event.response;
|
|
110
125
|
break;
|
|
111
126
|
}
|
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
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
|
+
import { type ProviderNamed } from '../failure.ts';
|
|
4
5
|
import type { Model, ModelRequest, ModelResponse } from '../model.ts';
|
|
6
|
+
/**
|
|
7
|
+
* Which failures the SDK's backoff applies to: rate limits, request timeouts,
|
|
8
|
+
* and the upstream's own errors. Families are written `5XX` in this matcher.
|
|
9
|
+
*
|
|
10
|
+
* It has to be passed per call. The generated client resolves `retryCodes` at
|
|
11
|
+
* the *operation*, defaulting it to `5XX` alone, and no client-wide option
|
|
12
|
+
* reaches it — so configuring a retry policy and leaving this out gives a
|
|
13
|
+
* client that backs off diligently on 500s and dies on the first 429.
|
|
14
|
+
*/
|
|
15
|
+
export declare const RETRY_STATUS_CODES: string[];
|
|
5
16
|
/**
|
|
6
17
|
* Effort levels, spelled out rather than imported: the SDK's is an *open* enum
|
|
7
18
|
* (its literals plus any string), which would let a typo through, and naming
|
|
@@ -11,7 +22,7 @@ import type { Model, ModelRequest, ModelResponse } from '../model.ts';
|
|
|
11
22
|
*/
|
|
12
23
|
export type OpenRouterEffort = 'none' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'max' | null;
|
|
13
24
|
/** Provider-specific knobs. */
|
|
14
|
-
export interface OpenRouterModelOptions {
|
|
25
|
+
export interface OpenRouterModelOptions extends ProviderNamed {
|
|
15
26
|
reasoningEffort?: OpenRouterEffort;
|
|
16
27
|
/** ask for a reasoning summary from models that expose one */
|
|
17
28
|
reasoningSummary?: 'auto' | 'concise' | 'detailed';
|
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
var _a;
|
|
2
|
+
import { callSites, called, reading } from "../failure.js";
|
|
2
3
|
import { zeroUsage } from "../types.js";
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// OpenRouter adapter
|
|
6
|
+
//
|
|
7
|
+
// Not the chat-completions adapter pointed at another base url. OpenRouter's
|
|
8
|
+
// own SDK is a different wire contract in TypeScript terms — camelCase fields
|
|
9
|
+
// remapped to snake_case on the way out, a response union that covers both
|
|
10
|
+
// streaming and not, assistant content that may arrive as an array — so it
|
|
11
|
+
// gets its own adapter rather than a widened `OpenAIModel`.
|
|
12
|
+
//
|
|
13
|
+
// What that buys is the part of OpenRouter the OpenAI client cannot express:
|
|
14
|
+
// provider routing, a model fallback chain, and per-call cost. What it costs
|
|
15
|
+
// is that responses are zod-parsed on the way in, so a provider returning a
|
|
16
|
+
// shape the schema does not know raises `ResponseValidationError` where the
|
|
17
|
+
// OpenAI client would have shrugged. `kind: openai-compatible` against the
|
|
18
|
+
// same base url remains the way back if that ever bites.
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
/**
|
|
21
|
+
* Which failures the SDK's backoff applies to: rate limits, request timeouts,
|
|
22
|
+
* and the upstream's own errors. Families are written `5XX` in this matcher.
|
|
23
|
+
*
|
|
24
|
+
* It has to be passed per call. The generated client resolves `retryCodes` at
|
|
25
|
+
* the *operation*, defaulting it to `5XX` alone, and no client-wide option
|
|
26
|
+
* reaches it — so configuring a retry policy and leaving this out gives a
|
|
27
|
+
* client that backs off diligently on 500s and dies on the first 429.
|
|
28
|
+
*/
|
|
29
|
+
export const RETRY_STATUS_CODES = ['408', '429', '5XX'];
|
|
3
30
|
function toStopReason(finish) {
|
|
4
31
|
switch (finish) {
|
|
5
32
|
case 'tool_calls':
|
|
@@ -46,13 +73,15 @@ export class OpenRouterModel {
|
|
|
46
73
|
id;
|
|
47
74
|
#client;
|
|
48
75
|
#options;
|
|
76
|
+
#site;
|
|
49
77
|
constructor(id, client, options = {}) {
|
|
50
78
|
this.id = id;
|
|
51
79
|
this.#client = client;
|
|
52
80
|
this.#options = options;
|
|
81
|
+
this.#site = callSites(id, options.provider);
|
|
53
82
|
}
|
|
54
83
|
async generate(req) {
|
|
55
|
-
const res = await this.#send(req, false);
|
|
84
|
+
const res = await called(this.#site('llm generation', 'chat.send'), () => this.#send(req, false));
|
|
56
85
|
// Both overloads are declared to return the same union, so the narrowing
|
|
57
86
|
// the `stream` flag implies has to be done here.
|
|
58
87
|
if (Symbol.asyncIterator in res) {
|
|
@@ -73,10 +102,15 @@ export class OpenRouterModel {
|
|
|
73
102
|
};
|
|
74
103
|
}
|
|
75
104
|
async stream(req, onDelta) {
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
105
|
+
const site = this.#site('llm streaming', 'chat.send');
|
|
106
|
+
const open = async () => {
|
|
107
|
+
const res = await this.#send(req, true);
|
|
108
|
+
if (!(Symbol.asyncIterator in res)) {
|
|
109
|
+
throw new Error(`model "${this.id}": asked for a stream, got one completion`);
|
|
110
|
+
}
|
|
111
|
+
return res;
|
|
112
|
+
};
|
|
113
|
+
const res = await called(site, open);
|
|
80
114
|
let text = '';
|
|
81
115
|
let thinking = '';
|
|
82
116
|
let usage;
|
|
@@ -84,7 +118,7 @@ export class OpenRouterModel {
|
|
|
84
118
|
// Tool calls arrive as deltas keyed by index, with the id and name in the
|
|
85
119
|
// first fragment and arguments trickling in afterwards.
|
|
86
120
|
const calls = new Map();
|
|
87
|
-
for await (const chunk of res) {
|
|
121
|
+
for await (const chunk of reading(site, res, open)) {
|
|
88
122
|
// An upstream failure mid-stream arrives as a chunk on a 200, so it
|
|
89
123
|
// has to be raised here or the turn ends early and looks complete.
|
|
90
124
|
if (chunk.error) {
|
|
@@ -154,7 +188,7 @@ export class OpenRouterModel {
|
|
|
154
188
|
* SDK keeps transport concerns out of the body it validates.
|
|
155
189
|
*/
|
|
156
190
|
#send(req, stream) {
|
|
157
|
-
return this.#client.chat.send({ chatRequest: this.#request(req, stream) }, { fetchOptions: { signal: req.signal } });
|
|
191
|
+
return this.#client.chat.send({ chatRequest: this.#request(req, stream) }, { fetchOptions: { signal: req.signal }, retryCodes: RETRY_STATUS_CODES });
|
|
158
192
|
}
|
|
159
193
|
#request(req, stream) {
|
|
160
194
|
const o = this.#options;
|