@axlsdk/axl 0.17.9 → 0.18.1

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/README.md CHANGED
@@ -58,6 +58,8 @@ export const config = defineConfig({
58
58
 
59
59
  Provider API keys are also read automatically from environment variables (`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GOOGLE_API_KEY`/`GEMINI_API_KEY`), so for local development you can skip the `providers` block entirely.
60
60
 
61
+ Each provider also accepts an opt-in `rateLimit` (`{ maxConcurrent?, minIntervalMs?, acquireTimeoutMs? }`) for proactive client-side pacing on top of the automatic 429/503/529 backoff — useful when a large fan-out (e.g. an eval) shares one API key. It caps in-flight request concurrency (not token throughput) for that provider's chat calls. See [Providers → Rate limiting](../../docs/providers.md#rate-limiting-opt-in).
62
+
61
63
  State store options: `'memory'` (default), `'sqlite'` (requires `better-sqlite3`), or a `RedisStore` instance for multi-process deployments. See [State Stores](#state-stores).
62
64
 
63
65
  ### Tools, Agents, and Workflows
@@ -85,7 +87,7 @@ import { lookupOrder } from '../tools/db.js';
85
87
 
86
88
  export const supportAgent = agent({
87
89
  name: 'support',
88
- model: 'openai-responses:gpt-5.4',
90
+ model: 'openai-responses:gpt-5.5',
89
91
  system: 'You are a customer support agent. Use tools to look up order information.',
90
92
  tools: [lookupOrder],
91
93
  });
@@ -172,7 +174,7 @@ import { agent } from '@axlsdk/axl';
172
174
 
173
175
  const researcher = agent({
174
176
  name: 'researcher',
175
- model: 'openai-responses:gpt-5.4',
177
+ model: 'openai-responses:gpt-5.5',
176
178
  system: 'You are a research assistant.',
177
179
  tools: [calculator],
178
180
  effort: 'high',
@@ -189,7 +191,7 @@ Dynamic model and system prompt selection:
189
191
  const dynamicAgent = agent({
190
192
  model: (ctx) =>
191
193
  ctx.metadata?.tier === 'premium'
192
- ? 'openai-responses:gpt-5.4'
194
+ ? 'openai-responses:gpt-5.5'
193
195
  : 'openai-responses:gpt-5-nano',
194
196
  system: (ctx) => `You are a ${ctx.metadata?.role ?? 'general'} assistant.`,
195
197
  });
@@ -548,7 +550,7 @@ const containsPII = (text: string) => /\b\d{3}-\d{2}-\d{4}\b/.test(text);
548
550
  const isOffTopic = (text: string) => !text.toLowerCase().includes('support');
549
551
 
550
552
  const safe = agent({
551
- model: 'openai-responses:gpt-5.4',
553
+ model: 'openai-responses:gpt-5.5',
552
554
  system: 'You are a helpful assistant.',
553
555
  guardrails: {
554
556
  input: async (prompt, ctx) => {
@@ -739,7 +741,7 @@ import {
739
741
  Four built-in providers using the `provider:model` URI scheme:
740
742
 
741
743
  ```
742
- openai-responses:gpt-5.4 # OpenAI Responses API (preferred over Chat Completions)
744
+ openai-responses:gpt-5.5 # OpenAI Responses API (preferred over Chat Completions)
743
745
  openai:gpt-5.4 # OpenAI Chat Completions
744
746
  anthropic:claude-opus-4-7 # Anthropic (supports effort: 'xhigh')
745
747
  google:gemini-3.1-pro-preview # Google Gemini
package/dist/index.cjs CHANGED
@@ -136,6 +136,7 @@ __export(index_exports, {
136
136
  QuorumNotMet: () => QuorumNotMet,
137
137
  REDACTED: () => REDACTED,
138
138
  REDACTION_RULES: () => REDACTION_RULES,
139
+ RateLimiter: () => RateLimiter,
139
140
  RedisStore: () => RedisStore,
140
141
  SQLiteStore: () => SQLiteStore,
141
142
  Session: () => Session,
@@ -310,27 +311,180 @@ function sleep2(ms, signal) {
310
311
  );
311
312
  });
312
313
  }
313
- async function fetchWithRetry(input, init, maxRetries = MAX_RETRIES) {
314
- for (let attempt = 0; ; attempt++) {
315
- const res = await fetch(input, init);
316
- if (res.ok || !RETRYABLE_STATUS_CODES.has(res.status) || attempt >= maxRetries) {
317
- return res;
318
- }
319
- if (init?.signal?.aborted) {
320
- return res;
321
- }
322
- const retryAfter = res.headers.get("retry-after");
323
- let delay;
324
- if (retryAfter && !isNaN(Number(retryAfter))) {
325
- delay = Number(retryAfter) * 1e3;
326
- } else {
327
- delay = BASE_DELAY_MS * 2 ** attempt;
314
+ async function fetchWithRetry(input, init, opts) {
315
+ const maxRetries = opts?.maxRetries ?? MAX_RETRIES;
316
+ const governor = opts?.governor;
317
+ let acquired = false;
318
+ if (governor) {
319
+ await governor.acquire(init?.signal ?? void 0);
320
+ acquired = true;
321
+ }
322
+ try {
323
+ for (let attempt = 0; ; attempt++) {
324
+ const res = await fetch(input, init);
325
+ governor?.observe(res);
326
+ if (res.ok || !RETRYABLE_STATUS_CODES.has(res.status) || attempt >= maxRetries) {
327
+ return res;
328
+ }
329
+ if (init?.signal?.aborted) {
330
+ return res;
331
+ }
332
+ const retryAfter = res.headers.get("retry-after");
333
+ let delay;
334
+ if (retryAfter && !isNaN(Number(retryAfter))) {
335
+ delay = Number(retryAfter) * 1e3;
336
+ } else {
337
+ delay = BASE_DELAY_MS * 2 ** attempt;
338
+ }
339
+ delay *= 0.75 + Math.random() * 0.5;
340
+ await sleep2(delay, init?.signal ?? void 0);
328
341
  }
329
- delay *= 0.75 + Math.random() * 0.5;
330
- await sleep2(delay, init?.signal ?? void 0);
342
+ } finally {
343
+ if (acquired) governor.release();
331
344
  }
332
345
  }
333
346
 
347
+ // src/providers/rate-limiter.ts
348
+ function abortError() {
349
+ return Object.assign(new Error("The operation was aborted"), { name: "AbortError" });
350
+ }
351
+ var RateLimiter = class {
352
+ maxConcurrent;
353
+ minIntervalMs;
354
+ acquireTimeoutMs;
355
+ active = 0;
356
+ // `-Infinity` (not 0) so the FIRST grant is always immediate regardless of the
357
+ // wall clock: `Date.now() - (-Infinity) === Infinity`, which always satisfies
358
+ // `minIntervalMs`. Initializing to 0 would (only under a mocked/near-zero clock)
359
+ // wrongly delay the first grant by a full interval, since the limiter has never
360
+ // actually granted anything yet.
361
+ lastGrantAt = Number.NEGATIVE_INFINITY;
362
+ queue = [];
363
+ spacingTimer;
364
+ warnedQueued = false;
365
+ constructor(config = {}) {
366
+ const mc = config.maxConcurrent;
367
+ if (mc == null) {
368
+ this.maxConcurrent = Infinity;
369
+ } else if (!Number.isFinite(mc) || mc < 1) {
370
+ console.warn(
371
+ `[axl] RateLimiter: ignoring invalid maxConcurrent (${mc}); expected an integer >= 1. No concurrency cap applied.`
372
+ );
373
+ this.maxConcurrent = Infinity;
374
+ } else {
375
+ this.maxConcurrent = Math.floor(mc);
376
+ if (this.maxConcurrent < 2) {
377
+ console.warn(
378
+ `[axl] RateLimiter: maxConcurrent=${this.maxConcurrent} serializes every request to this provider. Nested same-provider calls still complete (permits aren't held across a nested ask), but throughput is a floor, not a guarantee.`
379
+ );
380
+ }
381
+ }
382
+ this.minIntervalMs = config.minIntervalMs != null && Number.isFinite(config.minIntervalMs) && config.minIntervalMs > 0 ? config.minIntervalMs : 0;
383
+ this.acquireTimeoutMs = config.acquireTimeoutMs != null && Number.isFinite(config.acquireTimeoutMs) && config.acquireTimeoutMs > 0 ? config.acquireTimeoutMs : void 0;
384
+ }
385
+ /**
386
+ * Acquire a permit. Resolves when one is granted (respecting `maxConcurrent`
387
+ * and `minIntervalMs`). The caller MUST pair every resolved `acquire()` with
388
+ * exactly one {@link release}.
389
+ *
390
+ * Abort handling:
391
+ * - Pre-aborted signal ⇒ rejects immediately, no permit taken.
392
+ * - Aborted while queued ⇒ spliced out of the queue, no counter change, rejects.
393
+ * - (Once granted, the abort listener is removed — the holder owns the permit
394
+ * and releases it via `fetchWithRetry`'s `finally`.)
395
+ *
396
+ * With `acquireTimeoutMs`, a waiter that sits in the queue too long rejects.
397
+ */
398
+ acquire(signal) {
399
+ if (signal?.aborted) return Promise.reject(abortError());
400
+ return new Promise((resolve2, reject) => {
401
+ const waiter = { resolve: resolve2, reject, signal };
402
+ if (signal) {
403
+ waiter.onAbort = () => this.settleWaiter(waiter, abortError());
404
+ signal.addEventListener("abort", waiter.onAbort, { once: true });
405
+ }
406
+ if (this.acquireTimeoutMs != null) {
407
+ waiter.timer = setTimeout(
408
+ () => this.settleWaiter(
409
+ waiter,
410
+ new Error(`RateLimiter.acquire timed out after ${this.acquireTimeoutMs}ms`)
411
+ ),
412
+ this.acquireTimeoutMs
413
+ );
414
+ }
415
+ this.queue.push(waiter);
416
+ this.pump();
417
+ if (!waiter.granted && !this.warnedQueued) {
418
+ this.warnedQueued = true;
419
+ console.warn(
420
+ `[axl] RateLimiter: request queued (maxConcurrent=${this.maxConcurrent}${this.minIntervalMs ? `, minIntervalMs=${this.minIntervalMs}` : ""}). This is expected backpressure; lower fan-out or raise the cap if throughput suffers.`
421
+ );
422
+ }
423
+ });
424
+ }
425
+ /** Release a previously acquired permit and wake the next waiter. */
426
+ release() {
427
+ if (this.active === 0) {
428
+ console.warn(
429
+ "[axl] RateLimiter.release() called with no active permits \u2014 likely a double-release bug."
430
+ );
431
+ return;
432
+ }
433
+ this.active--;
434
+ this.pump();
435
+ }
436
+ /** v1 no-op seam for a future adaptive (rate-limit-header-driven) pacing follow-up. */
437
+ observe(_res) {
438
+ }
439
+ /** Drain the queue: grant head waiters while a permit is free and spacing allows. */
440
+ pump() {
441
+ while (this.queue.length > 0 && this.active < this.maxConcurrent) {
442
+ if (this.minIntervalMs > 0) {
443
+ const waitMs = this.minIntervalMs - (Date.now() - this.lastGrantAt);
444
+ if (waitMs > 0) {
445
+ if (!this.spacingTimer) {
446
+ this.spacingTimer = setTimeout(() => {
447
+ this.spacingTimer = void 0;
448
+ this.pump();
449
+ }, waitMs);
450
+ }
451
+ return;
452
+ }
453
+ }
454
+ const waiter = this.queue.shift();
455
+ this.grant(waiter);
456
+ }
457
+ }
458
+ grant(waiter) {
459
+ waiter.settled = true;
460
+ waiter.granted = true;
461
+ this.active++;
462
+ this.lastGrantAt = Date.now();
463
+ this.clearWaiterTimers(waiter);
464
+ waiter.resolve();
465
+ }
466
+ /** Reject a still-queued waiter (abort or timeout) and remove it cleanly. */
467
+ settleWaiter(waiter, err) {
468
+ if (waiter.settled) return;
469
+ waiter.settled = true;
470
+ const idx = this.queue.indexOf(waiter);
471
+ if (idx !== -1) this.queue.splice(idx, 1);
472
+ this.clearWaiterTimers(waiter);
473
+ waiter.reject(err);
474
+ this.pump();
475
+ }
476
+ clearWaiterTimers(waiter) {
477
+ if (waiter.timer) {
478
+ clearTimeout(waiter.timer);
479
+ waiter.timer = void 0;
480
+ }
481
+ if (waiter.signal && waiter.onAbort) {
482
+ waiter.signal.removeEventListener("abort", waiter.onAbort);
483
+ waiter.onAbort = void 0;
484
+ }
485
+ }
486
+ };
487
+
334
488
  // src/providers/openai.ts
335
489
  var OPENAI_PRICING = {
336
490
  // gpt-4o era — cache reads at 50% of input rate
@@ -358,7 +512,10 @@ var OPENAI_PRICING = {
358
512
  "gpt-5.2": [175e-8, 14e-6, 0.1],
359
513
  "gpt-5.3": [175e-8, 14e-6, 0.1],
360
514
  "gpt-5.4": [25e-7, 15e-6, 0.1],
361
- "gpt-5.4-pro": [3e-5, 18e-5, 0.1]
515
+ "gpt-5.4-pro": [3e-5, 18e-5, 0.1],
516
+ // gpt-5.5 era (flagship released 2026-04) — cache reads at 10% of input rate
517
+ "gpt-5.5": [5e-6, 3e-5, 0.1],
518
+ "gpt-5.5-pro": [3e-5, 18e-5, 0.1]
362
519
  };
363
520
  var PRICING_KEYS_BY_LENGTH = Object.keys(OPENAI_PRICING).sort((a, b) => b.length - a.length);
364
521
  function estimateOpenAICost(model, promptTokens, completionTokens, cachedTokens) {
@@ -407,9 +564,11 @@ var OpenAIProvider = class {
407
564
  name = "openai";
408
565
  baseUrl;
409
566
  apiKey;
567
+ governor;
410
568
  constructor(options = {}) {
411
569
  this.apiKey = options.apiKey ?? process.env.OPENAI_API_KEY ?? "";
412
570
  this.baseUrl = (options.baseUrl ?? process.env.OPENAI_BASE_URL ?? "https://api.openai.com/v1").replace(/\/$/, "");
571
+ this.governor = options.rateLimit ? new RateLimiter(options.rateLimit) : void 0;
413
572
  if (!this.apiKey) {
414
573
  throw new Error("OpenAI API key is required. Set OPENAI_API_KEY or pass apiKey in options.");
415
574
  }
@@ -419,15 +578,19 @@ var OpenAIProvider = class {
419
578
  // ---------------------------------------------------------------------------
420
579
  async chat(messages, options) {
421
580
  const body = this.buildRequestBody(messages, options, false);
422
- const res = await fetchWithRetry(`${this.baseUrl}/chat/completions`, {
423
- method: "POST",
424
- headers: {
425
- "Content-Type": "application/json",
426
- Authorization: `Bearer ${this.apiKey}`
581
+ const res = await fetchWithRetry(
582
+ `${this.baseUrl}/chat/completions`,
583
+ {
584
+ method: "POST",
585
+ headers: {
586
+ "Content-Type": "application/json",
587
+ Authorization: `Bearer ${this.apiKey}`
588
+ },
589
+ body: JSON.stringify(body),
590
+ signal: options.signal
427
591
  },
428
- body: JSON.stringify(body),
429
- signal: options.signal
430
- });
592
+ { governor: this.governor }
593
+ );
431
594
  if (!res.ok) {
432
595
  const errorBody = await res.text();
433
596
  const message = this.extractErrorMessage(errorBody, res.status);
@@ -467,15 +630,19 @@ var OpenAIProvider = class {
467
630
  // ---------------------------------------------------------------------------
468
631
  async *stream(messages, options) {
469
632
  const body = this.buildRequestBody(messages, options, true);
470
- const res = await fetchWithRetry(`${this.baseUrl}/chat/completions`, {
471
- method: "POST",
472
- headers: {
473
- "Content-Type": "application/json",
474
- Authorization: `Bearer ${this.apiKey}`
633
+ const res = await fetchWithRetry(
634
+ `${this.baseUrl}/chat/completions`,
635
+ {
636
+ method: "POST",
637
+ headers: {
638
+ "Content-Type": "application/json",
639
+ Authorization: `Bearer ${this.apiKey}`
640
+ },
641
+ body: JSON.stringify(body),
642
+ signal: options.signal
475
643
  },
476
- body: JSON.stringify(body),
477
- signal: options.signal
478
- });
644
+ { governor: this.governor }
645
+ );
479
646
  if (!res.ok) {
480
647
  const errorBody = await res.text();
481
648
  const message = this.extractErrorMessage(errorBody, res.status);
@@ -648,9 +815,11 @@ var OpenAIResponsesProvider = class {
648
815
  name = "openai-responses";
649
816
  baseUrl;
650
817
  apiKey;
818
+ governor;
651
819
  constructor(options = {}) {
652
820
  this.apiKey = options.apiKey ?? process.env.OPENAI_API_KEY ?? "";
653
821
  this.baseUrl = (options.baseUrl ?? process.env.OPENAI_BASE_URL ?? "https://api.openai.com/v1").replace(/\/$/, "");
822
+ this.governor = options.rateLimit ? new RateLimiter(options.rateLimit) : void 0;
654
823
  if (!this.apiKey) {
655
824
  throw new Error("OpenAI API key is required. Set OPENAI_API_KEY or pass apiKey in options.");
656
825
  }
@@ -660,15 +829,19 @@ var OpenAIResponsesProvider = class {
660
829
  // ---------------------------------------------------------------------------
661
830
  async chat(messages, options) {
662
831
  const body = this.buildRequestBody(messages, options, false);
663
- const res = await fetchWithRetry(`${this.baseUrl}/responses`, {
664
- method: "POST",
665
- headers: {
666
- "Content-Type": "application/json",
667
- Authorization: `Bearer ${this.apiKey}`
832
+ const res = await fetchWithRetry(
833
+ `${this.baseUrl}/responses`,
834
+ {
835
+ method: "POST",
836
+ headers: {
837
+ "Content-Type": "application/json",
838
+ Authorization: `Bearer ${this.apiKey}`
839
+ },
840
+ body: JSON.stringify(body),
841
+ signal: options.signal
668
842
  },
669
- body: JSON.stringify(body),
670
- signal: options.signal
671
- });
843
+ { governor: this.governor }
844
+ );
672
845
  if (!res.ok) {
673
846
  const errorBody = await res.text();
674
847
  const message = this.extractErrorMessage(errorBody, res.status);
@@ -682,15 +855,19 @@ var OpenAIResponsesProvider = class {
682
855
  // ---------------------------------------------------------------------------
683
856
  async *stream(messages, options) {
684
857
  const body = this.buildRequestBody(messages, options, true);
685
- const res = await fetchWithRetry(`${this.baseUrl}/responses`, {
686
- method: "POST",
687
- headers: {
688
- "Content-Type": "application/json",
689
- Authorization: `Bearer ${this.apiKey}`
858
+ const res = await fetchWithRetry(
859
+ `${this.baseUrl}/responses`,
860
+ {
861
+ method: "POST",
862
+ headers: {
863
+ "Content-Type": "application/json",
864
+ Authorization: `Bearer ${this.apiKey}`
865
+ },
866
+ body: JSON.stringify(body),
867
+ signal: options.signal
690
868
  },
691
- body: JSON.stringify(body),
692
- signal: options.signal
693
- });
869
+ { governor: this.governor }
870
+ );
694
871
  if (!res.ok) {
695
872
  const errorBody = await res.text();
696
873
  const message = this.extractErrorMessage(errorBody, res.status);
@@ -1007,6 +1184,7 @@ var OpenAIResponsesProvider = class {
1007
1184
  // src/providers/anthropic.ts
1008
1185
  var ANTHROPIC_API_VERSION = "2023-06-01";
1009
1186
  var ANTHROPIC_PRICING = {
1187
+ "claude-opus-4-8": [5e-6, 25e-6],
1010
1188
  "claude-opus-4-7": [5e-6, 25e-6],
1011
1189
  "claude-opus-4-6": [5e-6, 25e-6],
1012
1190
  "claude-sonnet-4-6": [3e-6, 15e-6],
@@ -1052,16 +1230,16 @@ var THINKING_BUDGETS = {
1052
1230
  max: 3e4
1053
1231
  };
1054
1232
  function supportsAdaptiveThinking(model) {
1055
- return model.startsWith("claude-opus-4-7") || model.startsWith("claude-opus-4-6") || model.startsWith("claude-sonnet-4-6");
1233
+ return model.startsWith("claude-opus-4-8") || model.startsWith("claude-opus-4-7") || model.startsWith("claude-opus-4-6") || model.startsWith("claude-sonnet-4-6");
1056
1234
  }
1057
1235
  function supportsMaxEffort(model) {
1058
- return model.startsWith("claude-opus-4-7") || model.startsWith("claude-opus-4-6");
1236
+ return model.startsWith("claude-opus-4-8") || model.startsWith("claude-opus-4-7") || model.startsWith("claude-opus-4-6");
1059
1237
  }
1060
1238
  function supportsXhigh2(model) {
1061
- return model.startsWith("claude-opus-4-7");
1239
+ return model.startsWith("claude-opus-4-8") || model.startsWith("claude-opus-4-7");
1062
1240
  }
1063
1241
  function supportsEffort(model) {
1064
- return model.startsWith("claude-opus-4-7") || model.startsWith("claude-opus-4-6") || model.startsWith("claude-sonnet-4-6") || model.startsWith("claude-opus-4-5");
1242
+ return model.startsWith("claude-opus-4-8") || model.startsWith("claude-opus-4-7") || model.startsWith("claude-opus-4-6") || model.startsWith("claude-sonnet-4-6") || model.startsWith("claude-opus-4-5");
1065
1243
  }
1066
1244
  function clampAnthropicEffort(model, effort) {
1067
1245
  if (effort === "max" && !supportsMaxEffort(model)) return "high";
@@ -1073,9 +1251,11 @@ var AnthropicProvider = class {
1073
1251
  baseUrl;
1074
1252
  apiKey;
1075
1253
  currentModel;
1254
+ governor;
1076
1255
  constructor(options = {}) {
1077
1256
  this.apiKey = options.apiKey ?? process.env.ANTHROPIC_API_KEY ?? "";
1078
1257
  this.baseUrl = (options.baseUrl ?? "https://api.anthropic.com/v1").replace(/\/$/, "");
1258
+ this.governor = options.rateLimit ? new RateLimiter(options.rateLimit) : void 0;
1079
1259
  if (!this.apiKey) {
1080
1260
  throw new Error(
1081
1261
  "Anthropic API key is required. Set ANTHROPIC_API_KEY or pass apiKey in options."
@@ -1088,12 +1268,16 @@ var AnthropicProvider = class {
1088
1268
  async chat(messages, options) {
1089
1269
  this.currentModel = options.model;
1090
1270
  const body = this.buildRequestBody(messages, options, false);
1091
- const res = await fetchWithRetry(`${this.baseUrl}/messages`, {
1092
- method: "POST",
1093
- headers: this.buildHeaders(),
1094
- body: JSON.stringify(body),
1095
- signal: options.signal
1096
- });
1271
+ const res = await fetchWithRetry(
1272
+ `${this.baseUrl}/messages`,
1273
+ {
1274
+ method: "POST",
1275
+ headers: this.buildHeaders(),
1276
+ body: JSON.stringify(body),
1277
+ signal: options.signal
1278
+ },
1279
+ { governor: this.governor }
1280
+ );
1097
1281
  if (!res.ok) {
1098
1282
  const errorBody = await res.text();
1099
1283
  const message = this.extractErrorMessage(errorBody, res.status);
@@ -1107,12 +1291,16 @@ var AnthropicProvider = class {
1107
1291
  // ---------------------------------------------------------------------------
1108
1292
  async *stream(messages, options) {
1109
1293
  const body = this.buildRequestBody(messages, options, true);
1110
- const res = await fetchWithRetry(`${this.baseUrl}/messages`, {
1111
- method: "POST",
1112
- headers: this.buildHeaders(),
1113
- body: JSON.stringify(body),
1114
- signal: options.signal
1115
- });
1294
+ const res = await fetchWithRetry(
1295
+ `${this.baseUrl}/messages`,
1296
+ {
1297
+ method: "POST",
1298
+ headers: this.buildHeaders(),
1299
+ body: JSON.stringify(body),
1300
+ signal: options.signal
1301
+ },
1302
+ { governor: this.governor }
1303
+ );
1116
1304
  if (!res.ok) {
1117
1305
  const errorBody = await res.text();
1118
1306
  const message = this.extractErrorMessage(errorBody, res.status);
@@ -1599,12 +1787,14 @@ var GeminiProvider = class {
1599
1787
  baseUrl;
1600
1788
  apiKey;
1601
1789
  callCounter = 0;
1790
+ governor;
1602
1791
  constructor(options = {}) {
1603
1792
  this.apiKey = options.apiKey ?? process.env.GOOGLE_API_KEY ?? process.env.GEMINI_API_KEY ?? "";
1604
1793
  this.baseUrl = (options.baseUrl ?? "https://generativelanguage.googleapis.com/v1beta").replace(
1605
1794
  /\/$/,
1606
1795
  ""
1607
1796
  );
1797
+ this.governor = options.rateLimit ? new RateLimiter(options.rateLimit) : void 0;
1608
1798
  if (!this.apiKey) {
1609
1799
  throw new Error("Google API key is required. Set GOOGLE_API_KEY or pass apiKey in options.");
1610
1800
  }
@@ -1614,12 +1804,16 @@ var GeminiProvider = class {
1614
1804
  // ---------------------------------------------------------------------------
1615
1805
  async chat(messages, options) {
1616
1806
  const body = this.buildRequestBody(messages, options);
1617
- const res = await fetchWithRetry(`${this.baseUrl}/models/${options.model}:generateContent`, {
1618
- method: "POST",
1619
- headers: this.buildHeaders(),
1620
- body: JSON.stringify(body),
1621
- signal: options.signal
1622
- });
1807
+ const res = await fetchWithRetry(
1808
+ `${this.baseUrl}/models/${options.model}:generateContent`,
1809
+ {
1810
+ method: "POST",
1811
+ headers: this.buildHeaders(),
1812
+ body: JSON.stringify(body),
1813
+ signal: options.signal
1814
+ },
1815
+ { governor: this.governor }
1816
+ );
1623
1817
  if (!res.ok) {
1624
1818
  const errorBody = await res.text();
1625
1819
  const message = this.extractErrorMessage(errorBody, res.status);
@@ -1640,7 +1834,8 @@ var GeminiProvider = class {
1640
1834
  headers: this.buildHeaders(),
1641
1835
  body: JSON.stringify(body),
1642
1836
  signal: options.signal
1643
- }
1837
+ },
1838
+ { governor: this.governor }
1644
1839
  );
1645
1840
  if (!res.ok) {
1646
1841
  const errorBody = await res.text();
@@ -2019,19 +2214,39 @@ var GeminiProvider = class {
2019
2214
  var builtinFactories = {
2020
2215
  openai: (config) => {
2021
2216
  const opts = config.providers?.openai ?? {};
2022
- return new OpenAIProvider({ apiKey: opts.apiKey, baseUrl: opts.baseUrl });
2217
+ return new OpenAIProvider({
2218
+ apiKey: opts.apiKey,
2219
+ baseUrl: opts.baseUrl,
2220
+ rateLimit: opts.rateLimit
2221
+ });
2023
2222
  },
2223
+ // NB: each adapter builds its OWN RateLimiter from its config — caps are
2224
+ // per-adapter-instance, not pooled. Configuring `openai` AND using both
2225
+ // `openai:` and `openai-responses:` yields two independent governors against
2226
+ // the same key (effective concurrency = the sum). Documented in providers.md.
2024
2227
  "openai-responses": (config) => {
2025
2228
  const opts = config.providers?.["openai-responses"] ?? config.providers?.openai ?? {};
2026
- return new OpenAIResponsesProvider({ apiKey: opts.apiKey, baseUrl: opts.baseUrl });
2229
+ return new OpenAIResponsesProvider({
2230
+ apiKey: opts.apiKey,
2231
+ baseUrl: opts.baseUrl,
2232
+ rateLimit: opts.rateLimit
2233
+ });
2027
2234
  },
2028
2235
  anthropic: (config) => {
2029
2236
  const opts = config.providers?.anthropic ?? {};
2030
- return new AnthropicProvider({ apiKey: opts.apiKey, baseUrl: opts.baseUrl });
2237
+ return new AnthropicProvider({
2238
+ apiKey: opts.apiKey,
2239
+ baseUrl: opts.baseUrl,
2240
+ rateLimit: opts.rateLimit
2241
+ });
2031
2242
  },
2032
2243
  google: (config) => {
2033
2244
  const opts = config.providers?.google ?? {};
2034
- return new GeminiProvider({ apiKey: opts.apiKey, baseUrl: opts.baseUrl });
2245
+ return new GeminiProvider({
2246
+ apiKey: opts.apiKey,
2247
+ baseUrl: opts.baseUrl,
2248
+ rateLimit: opts.rateLimit
2249
+ });
2035
2250
  }
2036
2251
  };
2037
2252
  var ProviderRegistry = class {
@@ -8764,14 +8979,11 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
8764
8979
  captureTraces: options?.captureTraces
8765
8980
  });
8766
8981
  } else {
8767
- result = await this.eval(
8768
- entry.config,
8769
- {
8770
- onProgress: options?.onProgress,
8771
- signal: options?.signal,
8772
- captureTraces: options?.captureTraces
8773
- }
8774
- );
8982
+ result = await this.eval(entry.config, {
8983
+ onProgress: options?.onProgress,
8984
+ signal: options?.signal,
8985
+ captureTraces: options?.captureTraces
8986
+ });
8775
8987
  }
8776
8988
  const resultObj = result;
8777
8989
  if (options?.metadata) {
@@ -10711,6 +10923,7 @@ function cosineSimilarity2(a, b) {
10711
10923
  QuorumNotMet,
10712
10924
  REDACTED,
10713
10925
  REDACTION_RULES,
10926
+ RateLimiter,
10714
10927
  RedisStore,
10715
10928
  SQLiteStore,
10716
10929
  Session,