@zhivex-ai/gateway 1.3.0 → 1.4.0

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
@@ -1,5 +1,33 @@
1
1
  # @zhivex-ai/gateway
2
2
 
3
+ ## Vertex partner routing
4
+
5
+ Register one Vertex adapter with Google Cloud credentials. The publisher stays
6
+ inside `modelId`; the gateway provider remains `vertex` for Google-hosted models.
7
+
8
+ ```ts
9
+ import { createGateway } from "@zhivex-ai/gateway";
10
+ import { createVertex } from "@zhivex-ai/vertex";
11
+
12
+ const gateway = createGateway({
13
+ adapters: { vertex: createVertex({
14
+ projectId: process.env.GOOGLE_CLOUD_PROJECT,
15
+ location: "global"
16
+ }) },
17
+ maxRetries: 0
18
+ });
19
+
20
+ const result = await gateway.generate({
21
+ primary: { provider: "vertex", modelId: "openai/gpt-oss-120b-maas" },
22
+ fallbacks: [{ provider: "vertex", modelId: "gemini-3.7-flash" }],
23
+ messages: [{ role: "user", content: "Explain a binary search." }]
24
+ });
25
+ ```
26
+
27
+ This requires working ADC or another bearer credential resolved by Vertex.
28
+ Model permissions, regional availability and quotas apply separately to each
29
+ target. See the [Vertex provider](../vertex/README.md) for route-specific limits.
30
+
3
31
  Routing and fallback package for Zhivex AI SDK.
4
32
 
5
33
  The gateway now supports:
@@ -18,10 +46,10 @@ For agent routing, the gateway can also filter by `agentCapabilities`, such as p
18
46
  ## Install
19
47
 
20
48
  ```bash
21
- bun add @zhivex-ai/gateway @zhivex-ai/core @zhivex-ai/anthropic @zhivex-ai/openai @zhivex-ai/ollama
49
+ bun add @zhivex-ai/gateway @zhivex-ai/core @zhivex-ai/anthropic @zhivex-ai/openai @zhivex-ai/ollama @zhivex-ai/vertex
22
50
  ```
23
51
 
24
- Install the provider packages used by your own adapter map; the examples below use OpenAI, Ollama and Anthropic.
52
+ Install the provider packages used by your own adapter map; these examples use OpenAI, Ollama, Anthropic and Vertex.
25
53
 
26
54
  ## Usage
27
55
 
@@ -75,7 +103,7 @@ The gateway also supports `streamText()`, `generateObject()`, and `streamObject(
75
103
  - Requests containing image attachments only route to models that declare `capabilities.vision: true`. The gateway never removes images to make a target appear compatible; if one target cannot accept the original request, it is skipped in favor of a compatible fallback.
76
104
  - `scoreTarget(context)` can replace the built-in name-based heuristic with application metrics. It must return a finite number; higher scores route first.
77
105
  - Routing amplification is bounded even if a request is assembled from external input: requests accept at most `maxFallbacks` targets (default 8, hard maximum 32), `maxRetries` cannot exceed 5, and `maxTotalAttempts` caps provider calls across the whole routed operation including later agent steps (default 32, hard maximum 128). Model IDs are non-empty, limited to 256 characters, and cannot contain control characters. `maxCostPer1kTokens` and configured/catalog costs must be finite and non-negative.
78
- - `onAttempt` and `onAgentRoute` are best-effort observers. They receive an `abortSignal` and are allowed `observerTimeoutMs` to finish (default 1 second); rejection, timeout, or request cancellation cannot retry successful provider work or block routing indefinitely.
106
+ - `onAttempt` and `onAgentRoute` are best-effort observers. Legacy callbacks run detached; `observerMode: "await"` explicitly awaits attempt callbacks and `"background"` queues them with bounded capacity. They receive an `abortSignal` and are allowed `observerTimeoutMs` to finish (default 1 second); rejection, timeout, or request cancellation cannot retry successful provider work or block routing indefinitely.
79
107
 
80
108
  Keep primary/fallback selection and these ceilings under application control when mapping an HTTP request into `GatewayRequest`. `maxCostPer1kTokens` limits model price, not the final invoice; use `maxTotalAttempts`, provider-side spend limits, authentication, and rate limiting for a complete cost boundary.
81
109
 
@@ -240,7 +268,7 @@ Cooldown is `min(maxCooldownMs, max(cooldownMs, Retry-After))`, with defaults of
240
268
 
241
269
  `adaptiveRouting` is an explicit alternative to legacy scoring. Supply a policy version, nonnegative weights for `latency`, `cost`, `quality`, `load`, `errorRate`, positive `latencyScaleMs`/`costScale`, and explicit `coldStart`, `unknownCost`, `missingQuality` policies (`allow`/`reject`). Quality profiles carry a target, task `intent`, score in [0,1] and evaluation version. There are no model-name quality heuristics in this mode.
242
270
 
243
- The score is quality reward minus normalized p95 latency, estimated request cost, in-flight load and observed error rate penalties. Enable `metrics` and `costAccounting` for those signals. Missing signals under `allow` omit that score term and remain listed as missing; `reject` excludes the destination. Expired samples become cold start. Capabilities and circuits are filtered before ordering, and circuits are rechecked atomically before each call. Ties preserve primary/fallback input order. `routeDecision.adaptive` records policy version, signals, profile versions and exclusions. Do not configure both `scoreTarget` and `adaptiveRouting`. These rules are transparent heuristics, not a claim of globally optimal routing.
271
+ The score is quality reward minus normalized p95 latency, estimated request cost, in-flight load and observed error rate penalties. Enable `metrics` and `costAccounting` for those signals. Missing penalty signals under `allow` receive a conservative normalized penalty (default 1), missing quality/throughput receive no reward, and all remain listed as missing; `reject` excludes the destination. Expired samples become cold start. Capabilities and circuits are filtered before ordering, and circuits are rechecked atomically before each call. Ties preserve primary/fallback input order. `routeDecision.adaptive` records policy version, signals, profile versions and exclusions. Do not configure both `scoreTarget` and `adaptiveRouting`. These rules are transparent heuristics, not a claim of globally optimal routing.
244
272
 
245
273
  ## Composing configured agents
246
274
 
@@ -249,3 +277,180 @@ Pass `agent: configuredAgent` to `runAgent` or `streamAgent`. The gateway substi
249
277
  Configured gateway runs persist a `gatewayAgentRouteBinding` metadata value for agent ID, primary/fallback targets, routing policy version and harness fingerprint. Resume must retain that binding; changing it fails before provider work. An older direct run without this binding requires a separately designed migration instead of silent adoption. Core still validates harness/environment fingerprints. Reserve `gatewayAgentRouteBinding` and `gatewayPortableHistory` for runtime use.
250
278
 
251
279
  For canonical history, import a fresh run with `messages` only. Do not combine import with prompt, state, runId, handoff, approvals or idempotencyKey. Resolved historical tools are input, never effects to replay. Then resume with store/runId or state and approvals as needed, without messages. Portable-history capability checks survive store reloads and compaction. Legacy inputs remain supported. Provider-specific approval data stays in durable state; it is not part of the portable history import format.
280
+
281
+
282
+ ## Production controls
283
+
284
+ All controls below are optional and compose with text, object and agent routing.
285
+ They do not require provider-specific changes. Existing default adapter maps and
286
+ legacy scoring remain supported. Adaptive missing-data penalties are intentionally
287
+ more conservative than the previous zero-penalty behavior.
288
+
289
+ ```ts
290
+ import {
291
+ createGateway, createGatewayAdmissionController, createGatewayBudgetStore,
292
+ createGatewayMetrics, createGatewayRoutingPolicy
293
+ } from "@zhivex-ai/gateway";
294
+ import { createInMemoryGenerateCache, createModelCatalog } from "@zhivex-ai/core";
295
+ import { createOpenAI } from "@zhivex-ai/openai";
296
+
297
+ // Supply a verified catalog snapshot for your actual models before enabling cost routing.
298
+ const catalog = createModelCatalog([{
299
+ provider: "openai", modelId: "your-model",
300
+ inputCostPer1kTokens: 0.001, outputCostPer1kTokens: 0.003
301
+ }], { snapshotVersion: "example-only", pricing: {
302
+ version: "example-only", currency: "USD", unit: "per_1k_tokens"
303
+ } });
304
+ const budgets = createGatewayBudgetStore({ limit: 20, currency: "USD" });
305
+ const gateway = createGateway({
306
+ adapters: { openai: createOpenAI() },
307
+ modelCatalog: catalog,
308
+ costAccounting: { unknownCostPolicy: "reject", cacheAssumption: "none" },
309
+ timeoutMs: 20_000,
310
+ metrics: createGatewayMetrics(),
311
+ adaptiveRouting: createGatewayRoutingPolicy("interactive"),
312
+ admission: createGatewayAdmissionController({
313
+ maxConcurrent: 16, requestsPerMinute: 600, tokensPerMinute: 100_000,
314
+ maxQueue: 32, queueTimeoutMs: 500
315
+ }),
316
+ budget: { store: budgets, currency: "USD", reserveAmount: 0.10 },
317
+ cache: { store: createInMemoryGenerateCache(), scope: "credential-revision-1" },
318
+ affinity: { ttlMs: 300_000, maxEntries: 1000, maxScoreLoss: 0.1 },
319
+ observerMode: "background", observerQueueCapacity: 256
320
+ });
321
+ const result = await gateway.generate({
322
+ primary: { provider: "openai", modelId: "your-model" },
323
+ messages: [{ role: "user", content: "Explain binary search." }],
324
+ maxTokens: 200, budgetScope: "tenant-123:2026-09", cacheScope: "tenant-123",
325
+ affinityKey: "conversation-456"
326
+ });
327
+ await gateway.flushObservers();
328
+ await gateway.flushControls();
329
+ console.log(result.text, gateway.diagnostics(), budgets.snapshot("tenant-123:2026-09"));
330
+ ```
331
+
332
+ ### Deadline and admission
333
+
334
+ `timeoutMs` covers one invocation, including queueing, retries, backoff, tools,
335
+ observers and streamed output. A request may shorten the configured deadline but
336
+ cannot extend it. Expiration raises `GatewayDeadlineError` and aborts downstream
337
+ signals. Providers/tools must cooperate to stop external effects; ignoring abort
338
+ cannot keep the gateway response pending indefinitely. Durable resumes start a
339
+ new invocation deadline. Normal completion removes timers and signal listeners.
340
+
341
+ Admission is per provider/model/deployment and happens before every upstream
342
+ call, including retries and later agent steps. Concurrency slots are released on
343
+ completion, abort or stream iterator return. The global queue is bounded and
344
+ waits at most `queueTimeoutMs`; the local implementation polls at up to 10 ms
345
+ intervals and does not promise strict FIFO fairness. Capacity denial skips to an
346
+ eligible fallback without marking the destination unhealthy.
347
+
348
+ RPM and TPM use fixed 60-second windows. TPM reserves serialized input length / 4
349
+ plus `maxTokens`, and requires `maxTokens`; this is an estimate, not provider token
350
+ parity. Reported total tokens reconcile reservations in the same window; unknown
351
+ usage keeps the reservation. RPM is not refunded after admission. Capacity
352
+ pressure never evicts an unexpired quota window. Set upstream provider limits as
353
+ the final boundary for usage the provider may continue after cancellation.
354
+
355
+ ### Monetary reservations
356
+
357
+ Each dispatched model attempt reserves `budget.reserveAmount` in the trusted
358
+ `budgetScope`. Successful known usage reconciles that amount against catalog
359
+ pricing; failed, cancelled, partial or unpriceable usage retains the reservation
360
+ as uncertain spend. No reservation is made for an exact cache hit. Budget denial
361
+ stops the operation without retry or fallback. The configured currency must
362
+ match the catalog valuation to release any unused reservation.
363
+
364
+ This limits **admitted reservations**, not the exact provider invoice. Choose a
365
+ conservative per-attempt reservation alongside `maxTokens`; actual reported cost
366
+ can exceed the reservation and is recorded fully, blocking subsequent work when
367
+ the scope is exhausted. Unknown spend remains blocked until the application
368
+ reconciles its accounting externally. Local scopes never evict or automatically
369
+ reset; use explicit period scopes and bounded `maxScopes`. Do not use arbitrary
370
+ client-supplied scope values. For authoritative billing, implement the store
371
+ against the service's durable ledger.
372
+
373
+ `GatewayBudgetStore` and `GatewayAdmissionController` are asynchronous injection
374
+ contracts for shared backends. The supplied factories are process-local; they do
375
+ not implement Redis or cross-process coordination. Remote implementations must
376
+ reserve atomically, expire abandoned concurrency leases, honor abort and make
377
+ settlement/release idempotent. Settlement runs outside the provider response path. Failed or timed-out settlement
378
+ cannot retry successful provider work; diagnostics report it and further budgeted dispatches fail closed on that
379
+ gateway instance. Recover the backend/ledger before replacing the instance.
380
+
381
+ ### Adaptive policies and destination identity
382
+
383
+ `createGatewayRoutingPolicy("interactive" | "economy" | "quality")` returns an
384
+ explicit versioned starting policy. Configure quality profiles for economy and
385
+ quality presets, which reject missing quality. Calibrate costs and latency scales
386
+ on your workload; presets are not measured service-level guarantees.
387
+
388
+ `latencyMetric: "ttft"` uses first-text latency; `"total"` retains full-attempt
389
+ latency. Optional `weights.throughput` uses measured median output tokens/second
390
+ with `throughputScale` (default 100). Throughput is an approximation using reported
391
+ output tokens minus the first token and elapsed time after first text. Cache hits
392
+ and cancellations do not supply provider latency evidence. `minSamples` prevents
393
+ single samples from being treated as mature health evidence, and `minQuality`
394
+ enforces an evaluated quality floor before ranking.
395
+
396
+ Allowed missing latency/cost/load/error signals incur `missingSignalPenalty`
397
+ (default 1, minimum 1). They are never represented as measured zero cost/latency.
398
+ `explorationEvery` optionally probes one eligible cold destination every N
399
+ operations; presets use 20. Exploration rotates candidates and cannot bypass
400
+ capability, cost, quality-floor or circuit exclusions. It is local and deterministic,
401
+ not a learned optimal policy.
402
+
403
+ A target may supply `deploymentId`. Register it in
404
+ `deployments: { "region-a": { provider: "openai", adapter } }` with an adapter
405
+ configured for that endpoint, region and credential. Unknown or mismatched IDs
406
+ are skipped without silently using the default adapter. Deduplication, metrics,
407
+ circuits, quotas, quality profiles, cache partitions and attempt diagnostics
408
+ include deployment identity. Catalog pricing still uses provider/model identity;
409
+ deployment-specific negotiated tariffs require a separate pricing design.
410
+
411
+ Optional affinity requires adaptive routing and both `cacheScope` and
412
+ `affinityKey`. After a successful attempt, subsequent invocations may reuse that
413
+ destination only if it remains eligible and within `maxScoreLoss` of the best
414
+ score. Expired entries are ignored; capacity evicts the oldest stored affinity.
415
+ Exploration takes precedence. This promotes provider prompt-cache locality; it
416
+ does not prove an upstream cache hit or change provider caching parameters.
417
+
418
+ ### Exact cache and observer lifecycle
419
+
420
+ Gateway caching reuses Core's canonical cache key middleware and configured store.
421
+ It requires both a configured authentication scope and a per-request `cacheScope`;
422
+ keys additionally include budget scope and deployment. Rotate the configured scope
423
+ when credentials/endpoints change. Store TTL/retention belongs to the selected
424
+ Core cache implementation. No semantic similarity cache is enabled.
425
+
426
+ Only non-streaming text-only model steps without tools or `providerOptions` are
427
+ eligible. Tool effects, portable tool history, images and provider state bypass
428
+ this cache. Identical simultaneous misses share one upstream call (up to 1024
429
+ active keys). One caller cancelling does not cancel the other subscribers; all
430
+ subscribers leaving aborts the shared call. Cache reads time out after `cache.timeoutMs` (default 50 ms); writes are detached,
431
+ bounded to 256 pending operations and use the same timeout. Cache failures do not
432
+ retry successful provider work. `flushControls()` waits for the current cache-write
433
+ and resource-settlement batch; resource settlements have `resourceTimeoutMs`
434
+ (default 1000 ms). Pending counts and dropped cache writes are available in diagnostics. `attempt.cacheHit` identifies reuse, and detailed attempt valuation
435
+ uses zero new upstream tokens; response usage still describes the reused output.
436
+ A coalesced follower is also a reuse; correlate requests with your application
437
+ ledger when attributing the originating call's cost.
438
+
439
+ The legacy observer behavior stays detached. `observerMode: "background"` adds a
440
+ bounded FIFO attempt queue, `diagnostics().droppedObservers`, and
441
+ `flushObservers()` for the currently queued batch. Every callback remains bounded
442
+ by `observerTimeoutMs`. `"await"` explicitly awaits attempt observers. None of
443
+ these modes can preempt synchronous CPU work inside a callback. Route-selection
444
+ observers keep their existing detached behavior.
445
+
446
+
447
+ ### Verification
448
+
449
+ Run `bun run test packages/gateway/tests` for routing and resource-lifetime
450
+ regressions. After `bun run build`, run
451
+ `bun scripts/benchmarks/gateway-controls.mjs /tmp/gateway-controls.json` to compare
452
+ local default/control overhead and verify exact-cache/shared-miss provider call
453
+ counts. The benchmark asserts correct answers and actual fixture calls, includes
454
+ runtime/source/artifact fingerprints, and explicitly excludes live provider
455
+ performance or competitive claims. Its five measured trials are a smoke baseline,
456
+ not a statistically reliable production tail-latency estimate.
@@ -9,12 +9,22 @@ export interface GatewayQualityProfile {
9
9
  }
10
10
  export interface GatewayAdaptiveRoutingPolicy {
11
11
  version: string;
12
+ /** Select latency relevant to the workload; full attempt is the legacy default. */
13
+ latencyMetric?: "total" | "ttft";
14
+ minSamples?: number;
15
+ /** Probe one allowed cold destination every N operations. Disabled unless configured. */
16
+ explorationEvery?: number;
17
+ throughputScale?: number;
18
+ /** Conservative normalized penalties used for allowed missing signals. */
19
+ missingSignalPenalty?: number;
20
+ minQuality?: number;
12
21
  weights: {
13
22
  latency: number;
14
23
  cost: number;
15
24
  quality: number;
16
25
  load: number;
17
26
  errorRate: number;
27
+ throughput?: number;
18
28
  };
19
29
  latencyScaleMs: number;
20
30
  costScale: number;
@@ -37,4 +47,6 @@ export interface GatewayAdaptiveCandidate {
37
47
  }
38
48
  export declare const validateAdaptivePolicy: (policy: GatewayAdaptiveRoutingPolicy) => void;
39
49
  export declare const scoreAdaptiveTarget: (policy: GatewayAdaptiveRoutingPolicy, target: GatewayModelTarget, intent: GatewayTaskIntent, metrics?: GatewayMetricsSnapshot, cost?: ModelCostValuation) => GatewayAdaptiveCandidate;
50
+ /** Explicit starting policies. Calibrate scales and quality profiles against your own workloads. */
51
+ export declare const createGatewayRoutingPolicy: (preset: "interactive" | "economy" | "quality", overrides?: Partial<GatewayAdaptiveRoutingPolicy>) => GatewayAdaptiveRoutingPolicy;
40
52
  //# sourceMappingURL=adaptive-routing.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"adaptive-routing.d.ts","sourceRoot":"","sources":["../src/adaptive-routing.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,kBAAkB,EAAE,KAAK,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAC3F,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AAC3D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAC1D,MAAM,WAAW,qBAAqB;IAAG,MAAM,EAAE,kBAAkB,CAAC;IAAC,MAAM,EAAE,iBAAiB,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;CAAE;AACjI,MAAM,WAAW,4BAA4B;IAC3C,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;IAC7F,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,OAAO,GAAG,QAAQ,CAAC;IAC9B,WAAW,EAAE,OAAO,GAAG,QAAQ,CAAC;IAChC,cAAc,EAAE,OAAO,GAAG,QAAQ,CAAC;IACnC,eAAe,CAAC,EAAE,qBAAqB,EAAE,CAAC;CAC3C;AACD,MAAM,WAAW,wBAAwB;IACvC,MAAM,EAAE,kBAAkB,CAAC;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,OAAO,CAAC,EAAE,sBAAsB,CAAC;IACjC,IAAI,CAAC,EAAE,kBAAkB,CAAC;IAC1B,OAAO,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;CAC9C;AACD,eAAO,MAAM,sBAAsB,WAAY,4BAA4B,SAY1E,CAAC;AACF,eAAO,MAAM,mBAAmB,WAAY,4BAA4B,UAAU,kBAAkB,UAAU,iBAAiB,YAAY,sBAAsB,SAAS,kBAAkB,KAAG,wBAiB9L,CAAC"}
1
+ {"version":3,"file":"adaptive-routing.d.ts","sourceRoot":"","sources":["../src/adaptive-routing.ts"],"names":[],"mappings":"AACA,OAAO,EAAgB,KAAK,kBAAkB,EAAE,KAAK,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAC3F,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AAC3D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAC1D,MAAM,WAAW,qBAAqB;IAAG,MAAM,EAAE,kBAAkB,CAAC;IAAC,MAAM,EAAE,iBAAiB,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;CAAE;AACjI,MAAM,WAAW,4BAA4B;IAC3C,OAAO,EAAE,MAAM,CAAC;IAChB,mFAAmF;IACnF,aAAa,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;IACjC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,yFAAyF;IACzF,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,0EAA0E;IAC1E,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAClH,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,OAAO,GAAG,QAAQ,CAAC;IAC9B,WAAW,EAAE,OAAO,GAAG,QAAQ,CAAC;IAChC,cAAc,EAAE,OAAO,GAAG,QAAQ,CAAC;IACnC,eAAe,CAAC,EAAE,qBAAqB,EAAE,CAAC;CAC3C;AACD,MAAM,WAAW,wBAAwB;IACvC,MAAM,EAAE,kBAAkB,CAAC;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,OAAO,CAAC,EAAE,sBAAsB,CAAC;IACjC,IAAI,CAAC,EAAE,kBAAkB,CAAC;IAC1B,OAAO,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;CAC9C;AACD,eAAO,MAAM,sBAAsB,WAAY,4BAA4B,SAkB1E,CAAC;AACF,eAAO,MAAM,mBAAmB,WAAY,4BAA4B,UAAU,kBAAkB,UAAU,iBAAiB,YAAY,sBAAsB,SAAS,kBAAkB,KAAG,wBAwB9L,CAAC;AAEF,oGAAoG;AACpG,eAAO,MAAM,0BAA0B,WAAY,aAAa,GAAG,SAAS,GAAG,SAAS,cAAa,OAAO,CAAC,4BAA4B,CAAC,KAAQ,4BAWjJ,CAAC"}
@@ -1,17 +1,30 @@
1
+ import { targetKey, sameTarget } from "./target.js";
1
2
  import { GatewayError } from "./types.js";
2
3
  export const validateAdaptivePolicy = (policy) => {
3
4
  if (!policy.version?.trim())
4
5
  throw new GatewayError("Adaptive policy requires a version.", false);
5
6
  if (![policy.latencyScaleMs, policy.costScale].every(x => Number.isFinite(x) && x > 0))
6
7
  throw new GatewayError("Adaptive scales must be positive.", false);
7
- const values = [policy.weights?.latency, policy.weights?.cost, policy.weights?.quality, policy.weights?.load, policy.weights?.errorRate];
8
+ const values = [policy.weights?.latency, policy.weights?.cost, policy.weights?.quality, policy.weights?.load, policy.weights?.errorRate, policy.weights?.throughput ?? 0];
8
9
  if (!values.every(x => Number.isFinite(x) && x >= 0) || values.every(x => x === 0))
9
10
  throw new GatewayError("Adaptive weights must be nonnegative with at least one positive weight.", false);
10
11
  if (![policy.coldStart, policy.unknownCost, policy.missingQuality].every(x => x === "allow" || x === "reject"))
11
12
  throw new GatewayError("Adaptive missing-data policies must be explicit.", false);
13
+ if (policy.latencyMetric !== undefined && !["total", "ttft"].includes(policy.latencyMetric))
14
+ throw new GatewayError("Invalid latencyMetric.", false);
15
+ if (policy.minSamples !== undefined && (!Number.isSafeInteger(policy.minSamples) || policy.minSamples < 1))
16
+ throw new GatewayError("Invalid minSamples.", false);
17
+ if (policy.missingSignalPenalty !== undefined && (!Number.isFinite(policy.missingSignalPenalty) || policy.missingSignalPenalty < 1))
18
+ throw new GatewayError("missingSignalPenalty must be at least one.", false);
19
+ if (policy.minQuality !== undefined && (!Number.isFinite(policy.minQuality) || policy.minQuality < 0 || policy.minQuality > 1))
20
+ throw new GatewayError("Invalid minQuality.", false);
21
+ if (policy.throughputScale !== undefined && (!Number.isFinite(policy.throughputScale) || policy.throughputScale <= 0))
22
+ throw new GatewayError("Invalid throughputScale.", false);
23
+ if (policy.explorationEvery !== undefined && (!Number.isSafeInteger(policy.explorationEvery) || policy.explorationEvery < 2))
24
+ throw new GatewayError("explorationEvery must be at least two.", false);
12
25
  const keys = new Set();
13
26
  for (const profile of policy.qualityProfiles ?? []) {
14
- const key = JSON.stringify([profile.target.provider, profile.target.modelId, profile.intent]);
27
+ const key = JSON.stringify([targetKey(profile.target), profile.intent]);
15
28
  if (keys.has(key) || !profile.version?.trim() || !Number.isFinite(profile.score) || profile.score < 0 || profile.score > 1)
16
29
  throw new GatewayError("Invalid or ambiguous quality profile.", false);
17
30
  keys.add(key);
@@ -19,29 +32,53 @@ export const validateAdaptivePolicy = (policy) => {
19
32
  };
20
33
  export const scoreAdaptiveTarget = (policy, target, intent, metrics, cost) => {
21
34
  const result = { target: { ...target }, exclusions: [], missingSignals: [], ...(metrics ? { metrics } : {}), ...(cost ? { cost } : {}) };
22
- const quality = policy.qualityProfiles?.find(x => x.target.provider === target.provider && x.target.modelId === target.modelId && x.intent === intent);
35
+ const quality = policy.qualityProfiles?.find(x => sameTarget(x.target, target) && x.intent === intent);
23
36
  if (quality)
24
37
  result.quality = { score: quality.score, version: quality.version };
25
38
  const missing = (signal, action) => { result.missingSignals.push(signal); if (action === "reject")
26
39
  result.exclusions.push(`${signal}-unavailable`); };
27
- if ((policy.weights.latency || policy.weights.errorRate) && (!metrics || metrics.successes + metrics.errors === 0))
40
+ const healthy = !!metrics && metrics.successes + metrics.errors >= (policy.minSamples ?? 1);
41
+ const latency = policy.latencyMetric === "ttft" ? metrics?.p95TtftMs : metrics?.p95LatencyMs;
42
+ if ((policy.weights.latency || policy.weights.errorRate) && !healthy)
28
43
  missing("health", policy.coldStart);
44
+ if (policy.weights.latency && healthy && latency === undefined)
45
+ missing("latency", policy.coldStart);
46
+ if (policy.weights.throughput && (!healthy || metrics?.p50TokensPerSecond === undefined))
47
+ missing("throughput", policy.coldStart);
29
48
  if (policy.weights.load && !metrics)
30
49
  missing("load", policy.coldStart);
31
50
  if (policy.weights.cost && (!cost || cost.amount === null))
32
51
  missing("cost", policy.unknownCost);
33
52
  if (policy.weights.quality && !quality)
34
53
  missing("quality", policy.missingQuality);
54
+ if (policy.minQuality !== undefined && (!quality || quality.score < policy.minQuality))
55
+ result.exclusions.push("quality-floor");
35
56
  if (result.exclusions.length)
36
57
  return result;
37
- const errorRate = metrics && metrics.successes + metrics.errors > 0 ? metrics.errors / (metrics.successes + metrics.errors) : 0;
58
+ const penalty = policy.missingSignalPenalty ?? 1;
59
+ const errorRate = healthy ? metrics.errors / (metrics.successes + metrics.errors) : penalty;
38
60
  const score = (quality?.score ?? 0) * policy.weights.quality
39
- - (metrics?.p95LatencyMs ?? 0) / policy.latencyScaleMs * policy.weights.latency
40
- - (cost?.amount ?? 0) / policy.costScale * policy.weights.cost
41
- - (metrics?.inFlight ?? 0) * policy.weights.load - errorRate * policy.weights.errorRate;
61
+ + (healthy ? metrics?.p50TokensPerSecond ?? 0 : 0) / (policy.throughputScale ?? 100) * (policy.weights.throughput ?? 0)
62
+ - (healthy && latency !== undefined ? latency / policy.latencyScaleMs : penalty) * policy.weights.latency
63
+ - (cost?.amount != null ? cost.amount / policy.costScale : penalty) * policy.weights.cost
64
+ - (metrics?.inFlight ?? penalty) * policy.weights.load - errorRate * policy.weights.errorRate;
42
65
  if (!Number.isFinite(score))
43
66
  throw new GatewayError("Adaptive signals produced a nonfinite score.", false);
44
67
  result.score = score;
45
68
  return result;
46
69
  };
70
+ /** Explicit starting policies. Calibrate scales and quality profiles against your own workloads. */
71
+ export const createGatewayRoutingPolicy = (preset, overrides = {}) => {
72
+ const policy = {
73
+ version: `gateway-${preset}-v1`, latencyMetric: preset === "interactive" ? "ttft" : "total",
74
+ latencyScaleMs: 1000, costScale: 0.01, minSamples: 5, explorationEvery: 20,
75
+ coldStart: "allow", unknownCost: "reject", missingQuality: "reject",
76
+ weights: preset === "interactive" ? { latency: 2, cost: 1, quality: 0, load: 1, errorRate: 3 }
77
+ : preset === "economy" ? { latency: .25, cost: 3, quality: 1, load: 1, errorRate: 3 }
78
+ : { latency: .25, cost: .25, quality: 3, load: 1, errorRate: 3 },
79
+ ...overrides
80
+ };
81
+ validateAdaptivePolicy(policy);
82
+ return policy;
83
+ };
47
84
  //# sourceMappingURL=adaptive-routing.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"adaptive-routing.js","sourceRoot":"","sources":["../src/adaptive-routing.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"adaptive-routing.js","sourceRoot":"","sources":["../src/adaptive-routing.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ import { GatewayError, type GatewayModelTarget } from "./types.js";
2
+ export declare class GatewayAdmissionError extends GatewayError {
3
+ constructor();
4
+ }
5
+ export interface GatewayAdmissionLease {
6
+ release(actualTokens?: number): void | Promise<void>;
7
+ }
8
+ /** Remote implementations must reserve atomically, honor abort, and expire abandoned leases. */
9
+ export interface GatewayAdmissionController {
10
+ acquire(input: {
11
+ target: GatewayModelTarget;
12
+ tokens?: number;
13
+ signal?: AbortSignal;
14
+ }): Promise<GatewayAdmissionLease>;
15
+ }
16
+ export interface GatewayAdmissionOptions {
17
+ maxConcurrent: number;
18
+ requestsPerMinute?: number;
19
+ tokensPerMinute?: number;
20
+ maxQueue?: number;
21
+ queueTimeoutMs?: number;
22
+ maxTargets?: number;
23
+ now?: () => number;
24
+ }
25
+ export declare const createGatewayAdmissionController: (options: GatewayAdmissionOptions) => GatewayAdmissionController;
26
+ //# sourceMappingURL=admission.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"admission.d.ts","sourceRoot":"","sources":["../src/admission.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,KAAK,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAEnE,qBAAa,qBAAsB,SAAQ,YAAY;IACrD,cAAyH;CAC1H;AACD,MAAM,WAAW,qBAAqB;IAAG,OAAO,CAAC,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAAE;AAChG,gGAAgG;AAChG,MAAM,WAAW,0BAA0B;IACzC,OAAO,CAAC,KAAK,EAAE;QAAE,MAAM,EAAE,kBAAkB,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,WAAW,CAAA;KAAE,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAAC;CACvH;AACD,MAAM,WAAW,uBAAuB;IACtC,aAAa,EAAE,MAAM,CAAC;IACtB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AACD,eAAO,MAAM,gCAAgC,YAAa,uBAAuB,KAAG,0BAqDnF,CAAC"}
@@ -0,0 +1,88 @@
1
+ import { GatewayError } from "./types.js";
2
+ import { targetKey } from "./target.js";
3
+ export class GatewayAdmissionError extends GatewayError {
4
+ constructor() { super("Gateway destination admission capacity exhausted.", false); this.name = "GatewayAdmissionError"; }
5
+ }
6
+ export const createGatewayAdmissionController = (options) => {
7
+ const maxQueue = options.maxQueue ?? 0, queueTimeout = options.queueTimeoutMs ?? 1000, maxTargets = options.maxTargets ?? 128;
8
+ for (const n of [options.maxConcurrent, queueTimeout, maxTargets, options.requestsPerMinute ?? 1, options.tokensPerMinute ?? 1]) {
9
+ if (!Number.isSafeInteger(n) || n < 1)
10
+ throw new GatewayError("Admission limits must be positive safe integers.", false);
11
+ }
12
+ if (!Number.isSafeInteger(maxQueue) || maxQueue < 0 || maxQueue > 10000 || maxTargets > 10000 || queueTimeout > 60000)
13
+ throw new GatewayError("Invalid admission queue/capacity.", false);
14
+ const now = options.now ?? Date.now;
15
+ const entries = new Map();
16
+ let queued = 0;
17
+ return {
18
+ async acquire({ target, tokens, signal }) {
19
+ if ((tokens !== undefined && (!Number.isSafeInteger(tokens) || tokens < 0)) || (options.tokensPerMinute !== undefined && tokens === undefined))
20
+ throw new GatewayError("Invalid admission token reservation.", false);
21
+ if (options.tokensPerMinute !== undefined && tokens > options.tokensPerMinute)
22
+ throw new GatewayAdmissionError();
23
+ const reservedTokens = tokens ?? 0;
24
+ const id = targetKey(target), deadline = Date.now() + queueTimeout;
25
+ let waiting = false;
26
+ try {
27
+ for (;;) {
28
+ signal?.throwIfAborted();
29
+ const at = now();
30
+ let entry = entries.get(id);
31
+ if (!entry) {
32
+ if (entries.size >= maxTargets) {
33
+ const disposable = [...entries].find(([, value]) => value.active === 0 && at >= value.resetAt);
34
+ if (disposable)
35
+ entries.delete(disposable[0]);
36
+ else
37
+ throw new GatewayAdmissionError();
38
+ }
39
+ entry = { active: 0, requests: 0, tokens: 0, resetAt: at + 60000 };
40
+ entries.set(id, entry);
41
+ }
42
+ if (at >= entry.resetAt) {
43
+ entry.requests = 0;
44
+ entry.tokens = 0;
45
+ entry.resetAt = at + 60000;
46
+ }
47
+ if (entry.active < options.maxConcurrent && entry.requests < (options.requestsPerMinute ?? Infinity) && entry.tokens + reservedTokens <= (options.tokensPerMinute ?? Infinity)) {
48
+ entry.active++;
49
+ entry.requests++;
50
+ entry.tokens += reservedTokens;
51
+ const window = entry.resetAt;
52
+ let released = false;
53
+ return { release(actualTokens) {
54
+ if (!released) {
55
+ released = true;
56
+ entry.active--;
57
+ if (entry.resetAt === window && actualTokens !== undefined && Number.isSafeInteger(actualTokens) && actualTokens >= 0)
58
+ entry.tokens = Math.max(0, entry.tokens + actualTokens - reservedTokens);
59
+ }
60
+ } };
61
+ }
62
+ if (!waiting) {
63
+ if (queued >= maxQueue)
64
+ throw new GatewayAdmissionError();
65
+ queued++;
66
+ waiting = true;
67
+ }
68
+ const remaining = deadline - Date.now();
69
+ if (remaining <= 0)
70
+ throw new GatewayAdmissionError();
71
+ await new Promise((resolve, reject) => {
72
+ const clean = () => { clearTimeout(timer); signal?.removeEventListener("abort", abort); };
73
+ const abort = () => { clean(); reject(signal.reason); };
74
+ const timer = setTimeout(() => { clean(); resolve(); }, Math.min(10, remaining));
75
+ signal?.addEventListener("abort", abort, { once: true });
76
+ if (signal?.aborted)
77
+ abort();
78
+ });
79
+ }
80
+ }
81
+ finally {
82
+ if (waiting)
83
+ queued--;
84
+ }
85
+ }
86
+ };
87
+ };
88
+ //# sourceMappingURL=admission.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"admission.js","sourceRoot":"","sources":["../src/admission.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ import { GatewayError } from "./types.js";
2
+ export declare class GatewayBudgetError extends GatewayError {
3
+ constructor();
4
+ }
5
+ export interface GatewayBudgetReservation {
6
+ /** null retains the entire reservation as uncertain spend; settlement is idempotent. */
7
+ settle(actualAmount: number | null): void | Promise<void>;
8
+ /** Only use when no upstream request was dispatched. */
9
+ cancel(): void | Promise<void>;
10
+ }
11
+ export interface GatewayBudgetStore {
12
+ reserve(input: {
13
+ scope: string;
14
+ currency: string;
15
+ amount: number;
16
+ signal?: AbortSignal;
17
+ }): Promise<GatewayBudgetReservation>;
18
+ }
19
+ export interface GatewayBudgetSnapshot {
20
+ spent: number;
21
+ reserved: number;
22
+ uncertain: number;
23
+ remaining: number;
24
+ }
25
+ /** Process-local lifetime budgets. Use a new store/scope for a new accounting period. */
26
+ export declare const createGatewayBudgetStore: (options: {
27
+ limit: number;
28
+ currency: string;
29
+ maxScopes?: number;
30
+ }) => GatewayBudgetStore & {
31
+ snapshot(scope: string): GatewayBudgetSnapshot;
32
+ };
33
+ //# sourceMappingURL=budget.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"budget.d.ts","sourceRoot":"","sources":["../src/budget.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC1C,qBAAa,kBAAmB,SAAQ,YAAY;IAClD,cAAsH;CACvH;AACD,MAAM,WAAW,wBAAwB;IACvC,wFAAwF;IACxF,MAAM,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1D,wDAAwD;IACxD,MAAM,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAChC;AACD,MAAM,WAAW,kBAAkB;IACjC,OAAO,CAAC,KAAK,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,WAAW,CAAA;KAAE,GAAG,OAAO,CAAC,wBAAwB,CAAC,CAAC;CAC9H;AACD,MAAM,WAAW,qBAAqB;IAAG,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;CAAE;AACjH,yFAAyF;AACzF,eAAO,MAAM,wBAAwB,YAAa;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,KAAG,kBAAkB,GAAG;IAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,qBAAqB,CAAA;CAiChL,CAAC"}
package/dist/budget.js ADDED
@@ -0,0 +1,53 @@
1
+ import { GatewayError } from "./types.js";
2
+ export class GatewayBudgetError extends GatewayError {
3
+ constructor() { super("Gateway budget reservation denied or unavailable.", false); this.name = "GatewayBudgetError"; }
4
+ }
5
+ /** Process-local lifetime budgets. Use a new store/scope for a new accounting period. */
6
+ export const createGatewayBudgetStore = (options) => {
7
+ const capacity = options.maxScopes ?? 1000;
8
+ if (!Number.isFinite(options.limit) || options.limit < 0 || !options.currency.trim() || !Number.isSafeInteger(capacity) || capacity < 1 || capacity > 100000)
9
+ throw new GatewayError("Invalid gateway budget configuration.", false);
10
+ const entries = new Map();
11
+ return {
12
+ async reserve({ scope, amount, currency, signal }) {
13
+ signal?.throwIfAborted();
14
+ if (!scope.trim() || scope.length > 256 || currency !== options.currency || !Number.isFinite(amount) || amount <= 0)
15
+ throw new GatewayBudgetError();
16
+ let entry = entries.get(scope);
17
+ if (!entry) {
18
+ // Never evict spend state: doing so would reset a caller's budget.
19
+ if (entries.size >= capacity)
20
+ throw new GatewayBudgetError();
21
+ entry = { spent: 0, reserved: 0, uncertain: 0 };
22
+ entries.set(scope, entry);
23
+ }
24
+ if (entry.spent + entry.reserved + entry.uncertain + amount > options.limit)
25
+ throw new GatewayBudgetError();
26
+ entry.reserved += amount;
27
+ let settled = false;
28
+ return {
29
+ settle(actual) {
30
+ if (settled)
31
+ return;
32
+ if (actual !== null && (!Number.isFinite(actual) || actual < 0))
33
+ throw new GatewayBudgetError();
34
+ settled = true;
35
+ entry.reserved = Math.max(0, entry.reserved - amount);
36
+ if (actual === null)
37
+ entry.uncertain += amount;
38
+ else
39
+ entry.spent += actual;
40
+ },
41
+ cancel() { if (!settled) {
42
+ settled = true;
43
+ entry.reserved = Math.max(0, entry.reserved - amount);
44
+ } }
45
+ };
46
+ },
47
+ snapshot(scope) {
48
+ const entry = entries.get(scope) ?? { spent: 0, reserved: 0, uncertain: 0 };
49
+ return { ...entry, remaining: Math.max(0, options.limit - entry.spent - entry.reserved - entry.uncertain) };
50
+ }
51
+ };
52
+ };
53
+ //# sourceMappingURL=budget.js.map