@zenera/neo 1.1.10 → 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.
@@ -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
  /**
@@ -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 stream = this.#client.messages.stream(this.#params(req), { signal: req.signal });
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),
@@ -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
  }
@@ -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, spec);
252
+ return new AnthropicModel(spec.model, client, options);
225
253
  case 'gemini':
226
- return new GeminiModel(spec.model, client, spec);
254
+ return new GeminiModel(spec.model, client, options);
227
255
  case 'openrouter':
228
- return new OpenRouterModel(spec.model, client, spec);
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, spec)
238
- : new OpenAIModel(spec.model, client, spec);
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.#parseEmbedding(ref) : ref;
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, spec);
289
+ return new OpenAIEmbedder(spec.model, client, options);
255
290
  case 'gemini':
256
- return new GeminiEmbedder(spec.model, client, spec);
291
+ return new GeminiEmbedder(spec.model, client, options);
257
292
  case 'openrouter':
258
- return new OpenRouterEmbedder(spec.model, client, spec);
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
- #parseEmbedding(ref) {
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`);
@@ -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 */
@@ -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 stream = await this.#client.models.generateContentStream(this.#params(req));
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 stream = await this.#client.chat.completions.create({ ...this.#params(req), stream: true, stream_options: { include_usage: true } }, { signal: req.signal });
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 stream = await this.#client.responses.create({ ...this.#params(req), stream: true }, { signal: req.signal });
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,6 +1,7 @@
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';
5
6
  /**
6
7
  * Which failures the SDK's backoff applies to: rate limits, request timeouts,
@@ -21,7 +22,7 @@ export declare const RETRY_STATUS_CODES: string[];
21
22
  */
22
23
  export type OpenRouterEffort = 'none' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'max' | null;
23
24
  /** Provider-specific knobs. */
24
- export interface OpenRouterModelOptions {
25
+ export interface OpenRouterModelOptions extends ProviderNamed {
25
26
  reasoningEffort?: OpenRouterEffort;
26
27
  /** ask for a reasoning summary from models that expose one */
27
28
  reasoningSummary?: 'auto' | 'concise' | 'detailed';
@@ -1,4 +1,5 @@
1
1
  var _a;
2
+ import { callSites, called, reading } from "../failure.js";
2
3
  import { zeroUsage } from "../types.js";
3
4
  // ---------------------------------------------------------------------------
4
5
  // OpenRouter adapter
@@ -72,13 +73,15 @@ export class OpenRouterModel {
72
73
  id;
73
74
  #client;
74
75
  #options;
76
+ #site;
75
77
  constructor(id, client, options = {}) {
76
78
  this.id = id;
77
79
  this.#client = client;
78
80
  this.#options = options;
81
+ this.#site = callSites(id, options.provider);
79
82
  }
80
83
  async generate(req) {
81
- const res = await this.#send(req, false);
84
+ const res = await called(this.#site('llm generation', 'chat.send'), () => this.#send(req, false));
82
85
  // Both overloads are declared to return the same union, so the narrowing
83
86
  // the `stream` flag implies has to be done here.
84
87
  if (Symbol.asyncIterator in res) {
@@ -99,10 +102,15 @@ export class OpenRouterModel {
99
102
  };
100
103
  }
101
104
  async stream(req, onDelta) {
102
- const res = await this.#send(req, true);
103
- if (!(Symbol.asyncIterator in res)) {
104
- throw new Error(`model "${this.id}": asked for a stream, got one completion`);
105
- }
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);
106
114
  let text = '';
107
115
  let thinking = '';
108
116
  let usage;
@@ -110,7 +118,7 @@ export class OpenRouterModel {
110
118
  // Tool calls arrive as deltas keyed by index, with the id and name in the
111
119
  // first fragment and arguments trickling in afterwards.
112
120
  const calls = new Map();
113
- for await (const chunk of res) {
121
+ for await (const chunk of reading(site, res, open)) {
114
122
  // An upstream failure mid-stream arrives as a chunk on a 200, so it
115
123
  // has to be raised here or the turn ends early and looks complete.
116
124
  if (chunk.error) {
@@ -18,6 +18,7 @@ declare const provider: z.ZodObject<{
18
18
  headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
19
19
  timeoutMs: z.ZodOptional<z.ZodNumber>;
20
20
  maxRetries: z.ZodOptional<z.ZodInt>;
21
+ concurrency: z.ZodOptional<z.ZodInt>;
21
22
  apiKey: z.ZodOptional<z.ZodString>;
22
23
  apiKeyEnv: z.ZodOptional<z.ZodString>;
23
24
  baseURL: z.ZodOptional<z.ZodString>;
@@ -47,10 +48,10 @@ declare const modelSpec: z.ZodObject<{
47
48
  thinkingBudgetTokens: z.ZodOptional<z.ZodInt>;
48
49
  thinkingBudget: z.ZodOptional<z.ZodInt>;
49
50
  thinkingLevel: z.ZodOptional<z.ZodEnum<{
50
- minimal: "minimal";
51
51
  low: "low";
52
52
  medium: "medium";
53
53
  high: "high";
54
+ minimal: "minimal";
54
55
  }>>;
55
56
  includeThoughts: z.ZodOptional<z.ZodBoolean>;
56
57
  routing: z.ZodOptional<z.ZodObject<{
@@ -93,6 +94,7 @@ declare const embeddingSpec: z.ZodObject<{
93
94
  dimensions: z.ZodOptional<z.ZodInt>;
94
95
  title: z.ZodOptional<z.ZodString>;
95
96
  maxBatch: z.ZodOptional<z.ZodInt>;
97
+ maxBatchTokens: z.ZodOptional<z.ZodInt>;
96
98
  routing: z.ZodOptional<z.ZodObject<{
97
99
  order: z.ZodOptional<z.ZodArray<z.ZodString>>;
98
100
  only: z.ZodOptional<z.ZodArray<z.ZodString>>;
@@ -161,6 +163,7 @@ export declare const projectSchema: z.ZodObject<{
161
163
  headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
162
164
  timeoutMs: z.ZodOptional<z.ZodNumber>;
163
165
  maxRetries: z.ZodOptional<z.ZodInt>;
166
+ concurrency: z.ZodOptional<z.ZodInt>;
164
167
  apiKey: z.ZodOptional<z.ZodString>;
165
168
  apiKeyEnv: z.ZodOptional<z.ZodString>;
166
169
  baseURL: z.ZodOptional<z.ZodString>;
@@ -185,10 +188,10 @@ export declare const projectSchema: z.ZodObject<{
185
188
  thinkingBudgetTokens: z.ZodOptional<z.ZodInt>;
186
189
  thinkingBudget: z.ZodOptional<z.ZodInt>;
187
190
  thinkingLevel: z.ZodOptional<z.ZodEnum<{
188
- minimal: "minimal";
189
191
  low: "low";
190
192
  medium: "medium";
191
193
  high: "high";
194
+ minimal: "minimal";
192
195
  }>>;
193
196
  includeThoughts: z.ZodOptional<z.ZodBoolean>;
194
197
  routing: z.ZodOptional<z.ZodObject<{
@@ -226,6 +229,7 @@ export declare const projectSchema: z.ZodObject<{
226
229
  dimensions: z.ZodOptional<z.ZodInt>;
227
230
  title: z.ZodOptional<z.ZodString>;
228
231
  maxBatch: z.ZodOptional<z.ZodInt>;
232
+ maxBatchTokens: z.ZodOptional<z.ZodInt>;
229
233
  routing: z.ZodOptional<z.ZodObject<{
230
234
  order: z.ZodOptional<z.ZodArray<z.ZodString>>;
231
235
  only: z.ZodOptional<z.ZodArray<z.ZodString>>;
@@ -46,6 +46,13 @@ const provider = z
46
46
  headers: z.record(z.string().min(1), z.string()).optional(),
47
47
  timeoutMs: z.number().positive().optional(),
48
48
  maxRetries: z.int().nonnegative().optional(),
49
+ /**
50
+ * Ceiling on embedding requests in flight against this connection. The
51
+ * working number is discovered from the provider's own refusals; this
52
+ * only caps how high that may climb, for a key shared with something
53
+ * else that also needs quota.
54
+ */
55
+ concurrency: z.int().positive().optional(),
49
56
  })
50
57
  .strict();
51
58
  /**
@@ -120,8 +127,13 @@ const embeddingSpec = z
120
127
  dimensions: z.int().positive().optional(),
121
128
  /** gemini only: a document title the retrieval task type takes into account */
122
129
  title: z.string().min(1).optional(),
123
- /** gemini only: texts per request; every `gemini-embedding-*` model takes one */
130
+ /**
131
+ * Ceilings on how a batch is split into requests. Both default to what
132
+ * the model is known to accept, so setting them is for a gateway whose
133
+ * caps are not the vendor's.
134
+ */
124
135
  maxBatch: z.int().positive().optional(),
136
+ maxBatchTokens: z.int().positive().optional(),
125
137
  /** openrouter only: who serves the request */
126
138
  routing: routing.optional(),
127
139
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zenera/neo",
3
- "version": "1.1.10",
3
+ "version": "1.1.11",
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",