@zhivex-ai/gateway 1.2.2 → 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 +249 -4
- package/dist/adaptive-routing.d.ts +52 -0
- package/dist/adaptive-routing.d.ts.map +1 -0
- package/dist/adaptive-routing.js +84 -0
- package/dist/adaptive-routing.js.map +1 -0
- package/dist/admission.d.ts +26 -0
- package/dist/admission.d.ts.map +1 -0
- package/dist/admission.js +88 -0
- package/dist/admission.js.map +1 -0
- package/dist/budget.d.ts +33 -0
- package/dist/budget.d.ts.map +1 -0
- package/dist/budget.js +53 -0
- package/dist/budget.js.map +1 -0
- package/dist/circuit-breaker.d.ts +36 -0
- package/dist/circuit-breaker.d.ts.map +1 -0
- package/dist/circuit-breaker.js +86 -0
- package/dist/circuit-breaker.js.map +1 -0
- package/dist/execution.d.ts +17 -0
- package/dist/execution.d.ts.map +1 -0
- package/dist/execution.js +223 -0
- package/dist/execution.js.map +1 -0
- package/dist/index.d.ts +29 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +417 -33
- package/dist/index.js.map +1 -1
- package/dist/metrics.d.ts +31 -0
- package/dist/metrics.d.ts.map +1 -0
- package/dist/metrics.js +69 -0
- package/dist/metrics.js.map +1 -0
- package/dist/operation.d.ts +11 -0
- package/dist/operation.d.ts.map +1 -0
- package/dist/operation.js +29 -0
- package/dist/operation.js.map +1 -0
- package/dist/target.d.ts +4 -0
- package/dist/target.d.ts.map +1 -0
- package/dist/target.js +3 -0
- package/dist/target.js.map +1 -0
- package/dist/types.d.ts +71 -4
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,4 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { targetKey, sameTarget } from "./target.js";
|
|
2
|
+
import { operationControl, GatewayDeadlineError } from "./operation.js";
|
|
3
|
+
import { createGatewayExecutor, cachedResults } from "./execution.js";
|
|
4
|
+
import { GatewayBudgetError } from "./budget.js";
|
|
5
|
+
import { GatewayAdmissionError } from "./admission.js";
|
|
6
|
+
export { GatewayDeadlineError } from "./operation.js";
|
|
7
|
+
export { createGatewayAdmissionController, GatewayAdmissionError } from "./admission.js";
|
|
8
|
+
export { createGatewayBudgetStore, GatewayBudgetError } from "./budget.js";
|
|
9
|
+
export { createGatewayRoutingPolicy } from "./adaptive-routing.js";
|
|
10
|
+
import { ConflictError, GuardrailTriggeredError, ProviderHTTPError, ValidationError, createAgent, calculateModelCost, createStructuredOutputPrompt, createTextMessage, generateObject, generateText, runAgent, streamAgent, streamObject, streamText } from "@zhivex-ai/core";
|
|
11
|
+
import { GatewayCircuitOpenError } from "./circuit-breaker.js";
|
|
12
|
+
import { scoreAdaptiveTarget, validateAdaptivePolicy } from "./adaptive-routing.js";
|
|
13
|
+
export { createGatewayCircuitBreaker, GatewayCircuitOpenError } from "./circuit-breaker.js";
|
|
14
|
+
export { createGatewayMetrics } from "./metrics.js";
|
|
2
15
|
import { createRouteDecision, gatewayMessagesToModelMessages } from "./compat.js";
|
|
3
16
|
import { hasToolHistory, validateGatewayMessages } from "./history.js";
|
|
4
17
|
import { GatewayError } from "./types.js";
|
|
@@ -59,7 +72,7 @@ const scoreTarget = (mode, intent, target, primary, config) => {
|
|
|
59
72
|
mode,
|
|
60
73
|
intent,
|
|
61
74
|
target,
|
|
62
|
-
isPrimary: target
|
|
75
|
+
isPrimary: sameTarget(target, primary),
|
|
63
76
|
configuredCostPer1kTokens: config.providerCostsPer1kTokens?.[target.provider],
|
|
64
77
|
catalogCostPer1kTokens: config.modelCatalog?.find(target.provider, target.modelId)?.costPer1kTokens,
|
|
65
78
|
latencyBiasMs: config.latencyBiasMs?.[target.provider]
|
|
@@ -107,11 +120,21 @@ const validateTarget = (target, label) => {
|
|
|
107
120
|
const validateRouteRequest = (config, request) => {
|
|
108
121
|
if (request.messages !== undefined)
|
|
109
122
|
validateGatewayMessages(request.messages);
|
|
123
|
+
if (config.budget && !request.budgetScope)
|
|
124
|
+
throw new GatewayBudgetError();
|
|
110
125
|
const fallbacks = request.fallbacks ?? [];
|
|
126
|
+
for (const scope of [request.budgetScope, request.cacheScope, request.affinityKey]) {
|
|
127
|
+
if (scope !== undefined && (typeof scope !== "string" || !scope.trim() || scope.length > 256))
|
|
128
|
+
throw new GatewayError("Invalid gateway scope.", false);
|
|
129
|
+
}
|
|
111
130
|
const maxFallbacks = getMaxFallbacks(config);
|
|
112
131
|
if (fallbacks.length > maxFallbacks) {
|
|
113
132
|
throw new GatewayError(`Gateway request contains ${fallbacks.length} fallback targets; the configured maximum is ${maxFallbacks}.`, false);
|
|
114
133
|
}
|
|
134
|
+
for (const target of [request.primary, ...fallbacks]) {
|
|
135
|
+
if (target.deploymentId !== undefined && (typeof target.deploymentId !== "string" || !/^[a-zA-Z0-9_.-]{1,128}$/.test(target.deploymentId)))
|
|
136
|
+
throw new GatewayError("Invalid deploymentId.", false);
|
|
137
|
+
}
|
|
115
138
|
validateTarget(request.primary, "Gateway primary target");
|
|
116
139
|
fallbacks.forEach((target, index) => validateTarget(target, `Gateway fallback target at index ${index}`));
|
|
117
140
|
if (request.maxCostPer1kTokens != null &&
|
|
@@ -121,7 +144,7 @@ const validateRouteRequest = (config, request) => {
|
|
|
121
144
|
}
|
|
122
145
|
};
|
|
123
146
|
const orderTargets = (mode, intent, primary, fallbacks, config) => [primary, ...fallbacks]
|
|
124
|
-
.filter((target, index, list) => list.findIndex((candidate) => candidate
|
|
147
|
+
.filter((target, index, list) => list.findIndex((candidate) => sameTarget(candidate, target)) === index)
|
|
125
148
|
.map((target, index) => ({
|
|
126
149
|
target,
|
|
127
150
|
index,
|
|
@@ -227,6 +250,7 @@ const createAttemptControl = (parentSignal, timeoutMs) => {
|
|
|
227
250
|
reject(controller.signal.reason instanceof Error ? controller.signal.reason : timeoutError);
|
|
228
251
|
};
|
|
229
252
|
if (controller.signal.aborted) {
|
|
253
|
+
void promise.catch(() => undefined);
|
|
230
254
|
onAbort();
|
|
231
255
|
return;
|
|
232
256
|
}
|
|
@@ -276,6 +300,8 @@ const redactSensitiveErrorMessage = (message) => message
|
|
|
276
300
|
.replace(/([?&](?:api[-_]?key|key|token|access[-_]?token|secret)=)[^&#\s]*/gi, "$1[REDACTED]")
|
|
277
301
|
.replace(/\b(Bearer)\s+[a-z\d._~+/=-]+/gi, "$1 [REDACTED]");
|
|
278
302
|
const normalizeError = (error) => {
|
|
303
|
+
if (error instanceof GatewayBudgetError || error instanceof GatewayDeadlineError)
|
|
304
|
+
return { error, retrySameTarget: false, fallbackNextTarget: false };
|
|
279
305
|
if (error instanceof ValidationError || error instanceof ConflictError || error instanceof GuardrailTriggeredError) {
|
|
280
306
|
return {
|
|
281
307
|
error: new GatewayError(redactSensitiveErrorMessage(error.message), false),
|
|
@@ -367,6 +393,7 @@ const retryBackoffMs = (config, retry, retryAfterMs) => {
|
|
|
367
393
|
const createAttempt = (target, ok, latencyMs, targetRank, options = {}) => ({
|
|
368
394
|
provider: target.provider,
|
|
369
395
|
modelId: target.modelId,
|
|
396
|
+
...(target.deploymentId ? { deploymentId: target.deploymentId } : {}),
|
|
370
397
|
ok,
|
|
371
398
|
latencyMs,
|
|
372
399
|
targetRank,
|
|
@@ -406,24 +433,27 @@ const runBoundedObserver = (config, parentSignal, observer) => {
|
|
|
406
433
|
return;
|
|
407
434
|
}
|
|
408
435
|
const completion = Promise.resolve(observerResult).catch(() => undefined);
|
|
409
|
-
|
|
436
|
+
return Promise.race([completion, boundary]).finally(cleanup);
|
|
410
437
|
};
|
|
411
438
|
const notifyAttempt = async (config, attempt, parentSignal) => {
|
|
412
439
|
if (!config.onAttempt) {
|
|
413
440
|
return;
|
|
414
441
|
}
|
|
415
|
-
runBoundedObserver(config, parentSignal, (abortSignal) => config.onAttempt?.({
|
|
442
|
+
const completion = runBoundedObserver(config, parentSignal, (abortSignal) => config.onAttempt?.({
|
|
416
443
|
...attempt,
|
|
417
444
|
retry: attempt.retry ?? 0,
|
|
418
445
|
targetRank: attempt.targetRank ?? 0,
|
|
419
446
|
abortSignal
|
|
420
447
|
}));
|
|
448
|
+
if (config.observerMode === "await" || config.observerMode === "background")
|
|
449
|
+
await completion;
|
|
421
450
|
};
|
|
422
451
|
const normalizeUsage = (usage, inputText, outputText) => {
|
|
423
452
|
const inputTokens = usage?.inputTokens ?? estimateTokens(inputText);
|
|
424
453
|
const outputTokens = usage?.outputTokens ?? estimateTokens(outputText);
|
|
425
454
|
const totalTokens = usage?.totalTokens ?? inputTokens + outputTokens;
|
|
426
455
|
return {
|
|
456
|
+
...usage,
|
|
427
457
|
inputTokens,
|
|
428
458
|
outputTokens,
|
|
429
459
|
totalTokens,
|
|
@@ -494,11 +524,41 @@ const historyAbortSignal = (signal) => {
|
|
|
494
524
|
historySignals.set(signal, controller.signal);
|
|
495
525
|
return controller.signal;
|
|
496
526
|
};
|
|
497
|
-
const
|
|
498
|
-
if (request.
|
|
499
|
-
throw new GatewayError("
|
|
527
|
+
const prepareAgentHistoryRequest = (request) => {
|
|
528
|
+
if (request.prompt !== undefined && request.messages !== undefined)
|
|
529
|
+
throw new GatewayError("Pass prompt or messages, not both.", false);
|
|
530
|
+
if (!request.messages?.some(message => "parts" in message))
|
|
531
|
+
return request;
|
|
532
|
+
validateGatewayMessages(request.messages);
|
|
533
|
+
if (request.state || request.runId || request.handoff || request.approvals || request.idempotencyKey) {
|
|
534
|
+
throw new GatewayError("Import canonical agent history as a fresh run; resume durable state using state or runId without messages or an import idempotency key.", false);
|
|
500
535
|
}
|
|
536
|
+
return { ...request, metadata: { ...request.metadata, gatewayPortableHistory: true }, abortSignal: historyAbortSignal(request.abortSignal) };
|
|
501
537
|
};
|
|
538
|
+
const agentHistoryStore = (store, context, binding) => store ? new Proxy(store, {
|
|
539
|
+
get(target, key) {
|
|
540
|
+
const value = Reflect.get(target, key);
|
|
541
|
+
if (key === "load")
|
|
542
|
+
return async (...args) => {
|
|
543
|
+
const state = await target.load(...args);
|
|
544
|
+
if (state && binding !== undefined && state.metadata?.gatewayAgentRouteBinding !== binding)
|
|
545
|
+
throw new ConflictError("Agent state belongs to a different gateway route binding.");
|
|
546
|
+
if (state?.metadata?.gatewayPortableHistory === true)
|
|
547
|
+
context.toolHistory = true;
|
|
548
|
+
return state;
|
|
549
|
+
};
|
|
550
|
+
if (key === "claimIdempotencyKey" && target.claimIdempotencyKey)
|
|
551
|
+
return async (...args) => {
|
|
552
|
+
const claim = await target.claimIdempotencyKey(...args);
|
|
553
|
+
if (binding !== undefined && claim.state.metadata?.gatewayAgentRouteBinding !== binding)
|
|
554
|
+
throw new ConflictError("Agent idempotency key belongs to a different gateway route binding.");
|
|
555
|
+
if (claim.state.metadata?.gatewayPortableHistory === true)
|
|
556
|
+
context.toolHistory = true;
|
|
557
|
+
return claim;
|
|
558
|
+
};
|
|
559
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
560
|
+
}
|
|
561
|
+
}) : undefined;
|
|
502
562
|
const buildRequiredCapabilities = (request, extra = {}) => ({
|
|
503
563
|
...(request.requiredCapabilities ?? {}),
|
|
504
564
|
...(request.tools || requestHasToolHistory(request) ? { tools: true } : {}),
|
|
@@ -595,6 +655,9 @@ const createAgentRunInput = (request) => {
|
|
|
595
655
|
providerOptions: request.providerOptions,
|
|
596
656
|
policy: request.policy,
|
|
597
657
|
metadata: request.metadata,
|
|
658
|
+
context: request.context,
|
|
659
|
+
compaction: request.compaction,
|
|
660
|
+
executionEnvironment: request.executionEnvironment,
|
|
598
661
|
abortSignal: request.abortSignal
|
|
599
662
|
};
|
|
600
663
|
};
|
|
@@ -611,12 +674,115 @@ const enrichAgentResult = (target, attempts, routeDecision, startedAt, result) =
|
|
|
611
674
|
}
|
|
612
675
|
});
|
|
613
676
|
export const createGateway = (config) => {
|
|
677
|
+
for (const value of [config.resourceTimeoutMs, config.cache?.timeoutMs]) {
|
|
678
|
+
if (value !== undefined && (!Number.isSafeInteger(value) || value < 1 || value > 60000))
|
|
679
|
+
throw new GatewayError("Invalid resource/cache timeout.", false);
|
|
680
|
+
}
|
|
681
|
+
const executor = createGatewayExecutor(config);
|
|
682
|
+
if (config.budget && (!config.modelCatalog || !config.budget.currency.trim() || !Number.isFinite(config.budget.reserveAmount) || config.budget.reserveAmount <= 0))
|
|
683
|
+
throw new GatewayError("Budget requires a catalog, currency and positive reservation amount.", false);
|
|
684
|
+
if (config.cache && !config.cache.scope.trim())
|
|
685
|
+
throw new GatewayError("Cache requires an authentication scope.", false);
|
|
686
|
+
if (config.observerMode !== undefined && !["legacy", "await", "background"].includes(config.observerMode))
|
|
687
|
+
throw new GatewayError("Invalid observer mode.", false);
|
|
688
|
+
const observerCapacity = config.observerQueueCapacity ?? 256;
|
|
689
|
+
if (!Number.isSafeInteger(observerCapacity) || observerCapacity < 1 || observerCapacity > 10000)
|
|
690
|
+
throw new GatewayError("Invalid observer queue capacity.", false);
|
|
691
|
+
const affinityTtl = config.affinity?.ttlMs ?? 300000, affinityCapacity = config.affinity?.maxEntries ?? 1000, affinityLoss = config.affinity?.maxScoreLoss ?? 0;
|
|
692
|
+
if (config.affinity && (!config.adaptiveRouting || !Number.isSafeInteger(affinityTtl) || affinityTtl < 1 || affinityTtl > 86400000 || !Number.isSafeInteger(affinityCapacity) || affinityCapacity < 1 || affinityCapacity > 100000 || !Number.isFinite(affinityLoss) || affinityLoss < 0))
|
|
693
|
+
throw new GatewayError("Affinity requires adaptive routing and valid bounded limits.", false);
|
|
694
|
+
const affinities = new Map();
|
|
695
|
+
let routingOperations = 0, explorationCursor = 0;
|
|
696
|
+
const pendingObservers = new Set();
|
|
697
|
+
let droppedObservers = 0, observerTail = Promise.resolve();
|
|
698
|
+
const backgroundObserver = (action) => {
|
|
699
|
+
if (pendingObservers.size >= observerCapacity) {
|
|
700
|
+
droppedObservers++;
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
const task = observerTail.then(action).catch(() => undefined);
|
|
704
|
+
observerTail = task;
|
|
705
|
+
pendingObservers.add(task);
|
|
706
|
+
void task.finally(() => pendingObservers.delete(task));
|
|
707
|
+
};
|
|
708
|
+
if (config.adaptiveRouting) {
|
|
709
|
+
validateAdaptivePolicy(config.adaptiveRouting);
|
|
710
|
+
if (config.scoreTarget)
|
|
711
|
+
throw new GatewayError("Choose adaptiveRouting or scoreTarget, not both.", false);
|
|
712
|
+
}
|
|
713
|
+
const configuredAgentRequest = (request) => {
|
|
714
|
+
const definition = request.agent;
|
|
715
|
+
if (!definition)
|
|
716
|
+
return request;
|
|
717
|
+
const defaults = {
|
|
718
|
+
agentId: definition.id, instructions: definition.instructions, tools: Array.isArray(definition.tools) ? Object.fromEntries(definition.tools.map(tool => [tool.name, tool])) : definition.tools,
|
|
719
|
+
maxSteps: definition.maxSteps, temperature: definition.temperature, maxTokens: definition.maxTokens,
|
|
720
|
+
reasoning: definition.reasoning, toolExecution: definition.toolExecution, toolApprovalPolicy: definition.toolApprovalPolicy,
|
|
721
|
+
providerOptions: definition.providerOptions, store: definition.store, memory: definition.memory,
|
|
722
|
+
onTelemetryEvent: definition.onTelemetryEvent, hookFailurePolicy: definition.hookFailurePolicy,
|
|
723
|
+
compaction: definition.compaction, executionEnvironment: definition.executionEnvironment
|
|
724
|
+
};
|
|
725
|
+
const merged = { ...defaults, ...Object.fromEntries(Object.entries(request).filter(([, value]) => value !== undefined)),
|
|
726
|
+
policy: { ...definition.policy, ...request.policy }, metadata: { ...definition.metadata, ...request.metadata }
|
|
727
|
+
};
|
|
728
|
+
const binding = JSON.stringify([1, merged.agentId ?? null, request.primary, request.fallbacks ?? [], config.adaptiveRouting?.version ?? "legacy", definition.harness?.fingerprint ?? null]);
|
|
729
|
+
merged.metadata = { ...merged.metadata, gatewayAgentRouteBinding: binding };
|
|
730
|
+
if (request.state && request.state.metadata?.gatewayAgentRouteBinding !== binding)
|
|
731
|
+
throw new ConflictError("Agent state belongs to a different gateway route binding.");
|
|
732
|
+
return merged;
|
|
733
|
+
};
|
|
734
|
+
const beginMetrics = (target, signal) => {
|
|
735
|
+
let handle;
|
|
736
|
+
try {
|
|
737
|
+
handle = config.metrics?.begin(target);
|
|
738
|
+
}
|
|
739
|
+
catch { /* Metrics cannot affect execution. */ }
|
|
740
|
+
let ended = false;
|
|
741
|
+
const end = (outcome, outputTokens) => {
|
|
742
|
+
if (ended)
|
|
743
|
+
return;
|
|
744
|
+
ended = true;
|
|
745
|
+
signal?.removeEventListener("abort", onAbort);
|
|
746
|
+
try {
|
|
747
|
+
handle?.end(outcome, outputTokens);
|
|
748
|
+
}
|
|
749
|
+
catch { /* Best effort. */ }
|
|
750
|
+
};
|
|
751
|
+
const onAbort = () => end("cancelled");
|
|
752
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
753
|
+
if (signal?.aborted)
|
|
754
|
+
onAbort();
|
|
755
|
+
return { end, firstText: () => { try {
|
|
756
|
+
handle?.firstText();
|
|
757
|
+
}
|
|
758
|
+
catch { /* Best effort. */ } } };
|
|
759
|
+
};
|
|
760
|
+
if (config.costAccounting && !config.modelCatalog)
|
|
761
|
+
throw new GatewayError("costAccounting requires a modelCatalog snapshot.", false);
|
|
762
|
+
if (config.costAccounting?.unknownCostPolicy !== undefined && !["allow", "reject"].includes(config.costAccounting.unknownCostPolicy))
|
|
763
|
+
throw new GatewayError("Invalid detailed unknownCostPolicy.", false);
|
|
764
|
+
if (config.costAccounting?.cacheAssumption !== undefined && !["reported", "none"].includes(config.costAccounting.cacheAssumption))
|
|
765
|
+
throw new GatewayError("Invalid cacheAssumption.", false);
|
|
614
766
|
const createRouteContext = (request, options = {}) => {
|
|
615
767
|
validateRouteRequest(config, request);
|
|
616
768
|
const mode = request.routingMode ?? "balanced";
|
|
617
769
|
const intent = request.taskIntent ?? options.defaultIntent ?? "chat";
|
|
618
|
-
const orderedTargets =
|
|
770
|
+
const orderedTargets = config.adaptiveRouting
|
|
771
|
+
? [request.primary, ...(request.fallbacks ?? [])].filter((target, index, all) => all.findIndex(x => sameTarget(x, target)) === index)
|
|
772
|
+
: orderTargets(mode, intent, request.primary, request.fallbacks ?? [], config);
|
|
619
773
|
const routeDecision = createRouteDecision(mode, intent, orderedTargets);
|
|
774
|
+
const affinityId = config.affinity && request.affinityKey && request.cacheScope
|
|
775
|
+
? JSON.stringify([request.budgetScope ?? null, request.cacheScope, request.affinityKey]) : undefined;
|
|
776
|
+
if (config.costAccounting) {
|
|
777
|
+
routeDecision.estimatedCosts = orderedTargets.map(target => calculateModelCost({
|
|
778
|
+
catalog: config.modelCatalog, ...target,
|
|
779
|
+
usage: {
|
|
780
|
+
inputTokens: estimateTokens([request.systemPrompt, request.system, request.instructions, request.prompt, JSON.stringify(request.messages ?? [])].filter(Boolean).join("\n")),
|
|
781
|
+
outputTokens: config.costAccounting.expectedOutputTokens ?? request.maxTokens
|
|
782
|
+
},
|
|
783
|
+
cacheAssumption: "none", estimated: true
|
|
784
|
+
}));
|
|
785
|
+
}
|
|
620
786
|
const attempts = [];
|
|
621
787
|
const candidates = [];
|
|
622
788
|
let notificationChain = Promise.resolve();
|
|
@@ -624,11 +790,16 @@ export const createGateway = (config) => {
|
|
|
624
790
|
attempt = { ...attempt, ...(attempt.errorMessage !== undefined
|
|
625
791
|
? { errorMessage: redactSensitiveErrorMessage(attempt.errorMessage) } : {}) };
|
|
626
792
|
attempts.push(attempt);
|
|
627
|
-
|
|
793
|
+
if (config.observerMode === "background")
|
|
794
|
+
backgroundObserver(() => notifyAttempt(config, attempt));
|
|
795
|
+
else
|
|
796
|
+
notificationChain = notificationChain.then(() => notifyAttempt(config, attempt, request.abortSignal));
|
|
628
797
|
};
|
|
629
798
|
const requiredCapabilities = buildRequiredCapabilities(request, options.extraRequiredCapabilities ?? {});
|
|
630
799
|
for (const [targetRank, target] of orderedTargets.entries()) {
|
|
631
|
-
const
|
|
800
|
+
const deployment = target.deploymentId === undefined ? undefined : config.deployments?.[target.deploymentId];
|
|
801
|
+
const adapter = target.deploymentId === undefined ? config.adapters[target.provider]
|
|
802
|
+
: deployment?.provider === target.provider ? deployment.adapter : undefined;
|
|
632
803
|
if (!adapter) {
|
|
633
804
|
queueAttempt(createAttempt(target, false, 0, targetRank, {
|
|
634
805
|
reasonCode: "operation-skip",
|
|
@@ -670,6 +841,10 @@ export const createGateway = (config) => {
|
|
|
670
841
|
}));
|
|
671
842
|
continue;
|
|
672
843
|
}
|
|
844
|
+
if (config.costAccounting?.unknownCostPolicy === "reject" && routeDecision.estimatedCosts?.[targetRank]?.status === "unknown") {
|
|
845
|
+
queueAttempt(createAttempt(target, false, 0, targetRank, { reasonCode: "cost-budget", errorMessage: "Skipped because detailed request cost is unknown." }));
|
|
846
|
+
continue;
|
|
847
|
+
}
|
|
673
848
|
const skip = options.getSkipReason?.(model, target);
|
|
674
849
|
if (skip) {
|
|
675
850
|
queueAttempt(createAttempt(target, false, 0, targetRank, {
|
|
@@ -680,10 +855,62 @@ export const createGateway = (config) => {
|
|
|
680
855
|
}
|
|
681
856
|
candidates.push({ target, targetRank, model });
|
|
682
857
|
}
|
|
858
|
+
if (config.adaptiveRouting) {
|
|
859
|
+
const policy = config.adaptiveRouting;
|
|
860
|
+
const evaluated = orderedTargets.map((target, index) => {
|
|
861
|
+
const candidate = candidates.find(x => x.target === target);
|
|
862
|
+
let snapshot;
|
|
863
|
+
try {
|
|
864
|
+
snapshot = config.metrics?.snapshot(target);
|
|
865
|
+
}
|
|
866
|
+
catch { /* Treat unavailable metrics as cold start. */ }
|
|
867
|
+
const evaluation = scoreAdaptiveTarget(policy, target, intent, snapshot, routeDecision.estimatedCosts?.[index]);
|
|
868
|
+
if (!candidate)
|
|
869
|
+
evaluation.exclusions.push(...attempts.filter(x => sameTarget(x, target)).map(x => x.reasonCode ?? "operation-skip"));
|
|
870
|
+
if (config.circuitBreaker && !config.circuitBreaker.canAttempt(target))
|
|
871
|
+
evaluation.exclusions.push("circuit-open");
|
|
872
|
+
if (evaluation.exclusions.length && candidate)
|
|
873
|
+
queueAttempt(createAttempt(target, false, 0, index, { reasonCode: evaluation.exclusions.includes("circuit-open") ? "circuit-open" : "operation-skip", errorMessage: `Adaptive exclusion: ${evaluation.exclusions.join(", ")}.` }));
|
|
874
|
+
return evaluation;
|
|
875
|
+
});
|
|
876
|
+
routeDecision.reasonCode = "routing-adaptive";
|
|
877
|
+
routeDecision.adaptive = { policyVersion: policy.version, candidates: evaluated };
|
|
878
|
+
const byTarget = new Map(evaluated.map(value => [targetKey(value.target), value]));
|
|
879
|
+
const eligible = candidates.filter(candidate => !byTarget.get(targetKey(candidate.target)).exclusions.length);
|
|
880
|
+
eligible.sort((left, right) => byTarget.get(targetKey(right.target)).score - byTarget.get(targetKey(left.target)).score || left.targetRank - right.targetRank);
|
|
881
|
+
const every = policy.explorationEvery;
|
|
882
|
+
if (every && ++routingOperations % every === 0) {
|
|
883
|
+
const cold = eligible.filter(candidate => byTarget.get(targetKey(candidate.target)).missingSignals.includes("health"));
|
|
884
|
+
if (cold.length) {
|
|
885
|
+
const probe = cold[explorationCursor++ % cold.length];
|
|
886
|
+
eligible.splice(eligible.indexOf(probe), 1);
|
|
887
|
+
eligible.unshift(probe);
|
|
888
|
+
routeDecision.adaptive.exploration = true;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
const sticky = affinityId ? affinities.get(affinityId) : undefined;
|
|
892
|
+
if (sticky && sticky.expires <= Date.now())
|
|
893
|
+
affinities.delete(affinityId);
|
|
894
|
+
if (sticky && sticky.expires > Date.now() && !routeDecision.adaptive.exploration && eligible.length) {
|
|
895
|
+
const index = eligible.findIndex(candidate => targetKey(candidate.target) === sticky.target);
|
|
896
|
+
if (index > 0 && byTarget.get(targetKey(eligible[0].target)).score - byTarget.get(sticky.target).score <= affinityLoss) {
|
|
897
|
+
eligible.unshift(eligible.splice(index, 1)[0]);
|
|
898
|
+
routeDecision.adaptive.affinity = true;
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
candidates.splice(0, candidates.length, ...eligible);
|
|
902
|
+
candidates.forEach((candidate, index) => { candidate.targetRank = index; });
|
|
903
|
+
routeDecision.orderedTargets = candidates.map(x => x.target);
|
|
904
|
+
routeDecision.reason = `Ordered by adaptive policy ${policy.version}; ties preserve request order.`;
|
|
905
|
+
}
|
|
683
906
|
if (!candidates.length) {
|
|
907
|
+
if (attempts.at(-1)?.reasonCode === "circuit-open")
|
|
908
|
+
throw new GatewayCircuitOpenError();
|
|
684
909
|
throw new GatewayError(attempts.at(-1)?.errorMessage ?? "No gateway target satisfied the request.", false);
|
|
685
910
|
}
|
|
686
911
|
const context = {
|
|
912
|
+
budgetScope: request.budgetScope,
|
|
913
|
+
cacheScope: request.cacheScope,
|
|
687
914
|
toolHistory: requestHasToolHistory(request),
|
|
688
915
|
attempts,
|
|
689
916
|
candidates,
|
|
@@ -691,12 +918,29 @@ export const createGateway = (config) => {
|
|
|
691
918
|
startedAt: Date.now(),
|
|
692
919
|
flushAttempts: () => notificationChain,
|
|
693
920
|
recordAttempt: async (attempt) => {
|
|
921
|
+
if (affinityId && attempt.ok && attempt.reasonCode === "provider-success") {
|
|
922
|
+
affinities.delete(affinityId);
|
|
923
|
+
if (affinities.size >= affinityCapacity)
|
|
924
|
+
affinities.delete(affinities.keys().next().value);
|
|
925
|
+
affinities.set(affinityId, { target: targetKey(attempt), expires: Date.now() + affinityTtl });
|
|
926
|
+
}
|
|
927
|
+
if (config.costAccounting && ["provider-success", "provider-error", "request-aborted"].includes(attempt.reasonCode ?? "")) {
|
|
928
|
+
const costInput = { catalog: config.modelCatalog, provider: attempt.provider, modelId: attempt.modelId };
|
|
929
|
+
try {
|
|
930
|
+
attempt = { ...attempt, cost: calculateModelCost({ ...costInput, usage: attempt.cacheHit ? { inputTokens: 0, outputTokens: 0, totalTokens: 0, cachedInputTokens: 0, cacheWriteTokens: 0 } : attempt.usage, cacheAssumption: config.costAccounting.cacheAssumption, reasoningAccounting: config.costAccounting.reasoningAccounting?.[attempt.provider] }) };
|
|
931
|
+
}
|
|
932
|
+
catch {
|
|
933
|
+
// Accounting cannot turn a successful model invocation into a retry.
|
|
934
|
+
const cost = calculateModelCost(costInput);
|
|
935
|
+
cost.unknownReasons.push("invalid-reported-usage");
|
|
936
|
+
attempt = { ...attempt, cost };
|
|
937
|
+
}
|
|
938
|
+
}
|
|
694
939
|
queueAttempt(attempt);
|
|
695
940
|
await notificationChain;
|
|
696
941
|
},
|
|
697
942
|
lock: async (candidate) => {
|
|
698
|
-
if (context.winner
|
|
699
|
-
context.winner.target.modelId === candidate.target.modelId) {
|
|
943
|
+
if (context.winner && sameTarget(context.winner.target, candidate.target)) {
|
|
700
944
|
return;
|
|
701
945
|
}
|
|
702
946
|
context.winner = candidate;
|
|
@@ -743,6 +987,10 @@ export const createGateway = (config) => {
|
|
|
743
987
|
}));
|
|
744
988
|
};
|
|
745
989
|
const throwFinalError = () => {
|
|
990
|
+
if (context.attempts.at(-1)?.reasonCode === "circuit-open")
|
|
991
|
+
throw new GatewayCircuitOpenError();
|
|
992
|
+
if (context.attempts.at(-1)?.reasonCode === "admission-denied")
|
|
993
|
+
throw new GatewayAdmissionError();
|
|
746
994
|
throw new GatewayError(context.attempts.at(-1)?.errorMessage ?? "All gateway attempts failed.", false);
|
|
747
995
|
};
|
|
748
996
|
const generate = async (input) => {
|
|
@@ -754,24 +1002,41 @@ export const createGateway = (config) => {
|
|
|
754
1002
|
for (const candidate of candidates) {
|
|
755
1003
|
const candidateInput = prepareInput(candidate.model, input);
|
|
756
1004
|
const inputSkipReason = modelInputSkipReason(candidate.model, candidateInput) ??
|
|
757
|
-
(context.toolHistory ? historyInputSkipReason(candidate.model, input) : undefined);
|
|
1005
|
+
(context.toolHistory ? historySkipReason(candidate.model, hasToolHistory(input.messages)) ?? historyInputSkipReason(candidate.model, input) : undefined);
|
|
758
1006
|
if (inputSkipReason) {
|
|
759
1007
|
await recordInputSkip(candidate, inputSkipReason);
|
|
760
1008
|
continue;
|
|
761
1009
|
}
|
|
762
1010
|
for (let retry = 0; retry <= maxRetries; retry += 1) {
|
|
763
|
-
|
|
1011
|
+
const timeoutMs = getAttemptTimeoutMs(config, candidate.target.provider);
|
|
1012
|
+
const permit = config.circuitBreaker?.acquire(candidate.target);
|
|
1013
|
+
if (config.circuitBreaker && !permit) {
|
|
1014
|
+
await context.recordAttempt(createAttempt(candidate.target, false, 0, candidate.targetRank, { reasonCode: "circuit-open", errorMessage: "Destination circuit is open or probe capacity is exhausted." }));
|
|
1015
|
+
break;
|
|
1016
|
+
}
|
|
1017
|
+
try {
|
|
1018
|
+
reserveProviderAttempt();
|
|
1019
|
+
}
|
|
1020
|
+
catch (error) {
|
|
1021
|
+
permit?.end("neutral");
|
|
1022
|
+
throw error;
|
|
1023
|
+
}
|
|
764
1024
|
const attemptStartedAt = Date.now();
|
|
765
|
-
const control = createAttemptControl(input.abortSignal,
|
|
1025
|
+
const control = createAttemptControl(input.abortSignal, timeoutMs);
|
|
1026
|
+
const metrics = beginMetrics(candidate.target, input.abortSignal);
|
|
766
1027
|
try {
|
|
767
|
-
const result = await control.waitFor(candidate.model.
|
|
1028
|
+
const result = await control.waitFor(executor.generate(candidate.model, candidate.target, {
|
|
768
1029
|
...prepareHistoryInput(candidate.model, candidateInput, context.toolHistory),
|
|
769
1030
|
abortSignal: control.signal
|
|
770
|
-
}));
|
|
1031
|
+
}, context.budgetScope, context.cacheScope));
|
|
771
1032
|
control.stopTimeout();
|
|
1033
|
+
metrics.end(cachedResults.has(result) ? "cache" : "success", result.usage?.outputTokens);
|
|
1034
|
+
permit?.end(cachedResults.has(result) ? "neutral" : "success");
|
|
772
1035
|
await context.recordAttempt(createAttempt(candidate.target, true, Date.now() - attemptStartedAt, candidate.targetRank, {
|
|
773
1036
|
retry,
|
|
774
|
-
reasonCode: "provider-success"
|
|
1037
|
+
reasonCode: "provider-success",
|
|
1038
|
+
...(cachedResults.has(result) ? { cacheHit: true } : {}),
|
|
1039
|
+
...(config.costAccounting ? { usage: result.usage } : {})
|
|
775
1040
|
}));
|
|
776
1041
|
await context.lock(candidate);
|
|
777
1042
|
control.dispose();
|
|
@@ -779,10 +1044,12 @@ export const createGateway = (config) => {
|
|
|
779
1044
|
}
|
|
780
1045
|
catch (rawError) {
|
|
781
1046
|
const callerAborted = input.abortSignal?.aborted === true;
|
|
1047
|
+
metrics.end(callerAborted || rawError instanceof GatewayAdmissionError || rawError instanceof GatewayBudgetError ? "cancelled" : "error");
|
|
782
1048
|
const error = control.timedOut() ? control.timeoutError : rawError;
|
|
783
1049
|
control.abort(error);
|
|
784
1050
|
control.dispose();
|
|
785
1051
|
if (callerAborted) {
|
|
1052
|
+
permit?.end("neutral");
|
|
786
1053
|
await context.recordAttempt(createAttempt(candidate.target, false, Date.now() - attemptStartedAt, candidate.targetRank, {
|
|
787
1054
|
retry,
|
|
788
1055
|
reasonCode: "request-aborted",
|
|
@@ -791,11 +1058,14 @@ export const createGateway = (config) => {
|
|
|
791
1058
|
throw abortReason(input.abortSignal);
|
|
792
1059
|
}
|
|
793
1060
|
const disposition = dispositionFor(error);
|
|
1061
|
+
permit?.end(disposition.retrySameTarget ? "retryable-error" : "neutral", disposition.retryAfterMs);
|
|
794
1062
|
await context.recordAttempt(createAttempt(candidate.target, false, Date.now() - attemptStartedAt, candidate.targetRank, {
|
|
795
1063
|
retry,
|
|
796
|
-
reasonCode: "provider-error",
|
|
1064
|
+
reasonCode: error instanceof GatewayAdmissionError ? "admission-denied" : error instanceof GatewayBudgetError ? "budget-denied" : "provider-error",
|
|
797
1065
|
errorMessage: disposition.error.message
|
|
798
1066
|
}));
|
|
1067
|
+
if (config.circuitBreaker?.snapshot(candidate.target)?.state === "open")
|
|
1068
|
+
break;
|
|
799
1069
|
if (retry < maxRetries && disposition.retrySameTarget) {
|
|
800
1070
|
await abortableSleep(retryBackoffMs(config, retry, disposition.retryAfterMs), input.abortSignal);
|
|
801
1071
|
continue;
|
|
@@ -818,7 +1088,7 @@ export const createGateway = (config) => {
|
|
|
818
1088
|
for (const candidate of candidates) {
|
|
819
1089
|
const candidateInput = prepareInput(candidate.model, input);
|
|
820
1090
|
const inputSkipReason = modelInputSkipReason(candidate.model, candidateInput) ??
|
|
821
|
-
(context.toolHistory ? historyInputSkipReason(candidate.model, input) : undefined);
|
|
1091
|
+
(context.toolHistory ? historySkipReason(candidate.model, hasToolHistory(input.messages)) ?? historyInputSkipReason(candidate.model, input) : undefined);
|
|
822
1092
|
if (inputSkipReason) {
|
|
823
1093
|
await recordInputSkip(candidate, inputSkipReason);
|
|
824
1094
|
continue;
|
|
@@ -828,15 +1098,28 @@ export const createGateway = (config) => {
|
|
|
828
1098
|
continue;
|
|
829
1099
|
}
|
|
830
1100
|
for (let retry = 0; retry <= maxRetries; retry += 1) {
|
|
831
|
-
|
|
1101
|
+
const timeoutMs = getAttemptTimeoutMs(config, candidate.target.provider);
|
|
1102
|
+
const permit = config.circuitBreaker?.acquire(candidate.target);
|
|
1103
|
+
if (config.circuitBreaker && !permit) {
|
|
1104
|
+
await context.recordAttempt(createAttempt(candidate.target, false, 0, candidate.targetRank, { reasonCode: "circuit-open", errorMessage: "Destination circuit is open or probe capacity is exhausted." }));
|
|
1105
|
+
break;
|
|
1106
|
+
}
|
|
1107
|
+
try {
|
|
1108
|
+
reserveProviderAttempt();
|
|
1109
|
+
}
|
|
1110
|
+
catch (error) {
|
|
1111
|
+
permit?.end("neutral");
|
|
1112
|
+
throw error;
|
|
1113
|
+
}
|
|
832
1114
|
const attemptStartedAt = Date.now();
|
|
833
|
-
const control = createAttemptControl(input.abortSignal,
|
|
1115
|
+
const control = createAttemptControl(input.abortSignal, timeoutMs);
|
|
834
1116
|
let iterator;
|
|
1117
|
+
const metrics = beginMetrics(candidate.target, input.abortSignal);
|
|
835
1118
|
try {
|
|
836
|
-
const providerStream = await control.waitFor(candidate.model.
|
|
1119
|
+
const providerStream = await control.waitFor(executor.stream(candidate.model, candidate.target, {
|
|
837
1120
|
...prepareHistoryInput(candidate.model, candidateInput, context.toolHistory),
|
|
838
1121
|
abortSignal: control.signal
|
|
839
|
-
}));
|
|
1122
|
+
}, context.budgetScope));
|
|
840
1123
|
iterator = providerStream[Symbol.asyncIterator]();
|
|
841
1124
|
const firstEvent = await control.waitFor(iterator.next());
|
|
842
1125
|
if (firstEvent.done) {
|
|
@@ -845,6 +1128,8 @@ export const createGateway = (config) => {
|
|
|
845
1128
|
if (firstEvent.value.type === "error") {
|
|
846
1129
|
throw firstEvent.value.error;
|
|
847
1130
|
}
|
|
1131
|
+
if (firstEvent.value.type === "text-delta")
|
|
1132
|
+
metrics.firstText();
|
|
848
1133
|
control.stopTimeout();
|
|
849
1134
|
await context.lock(candidate);
|
|
850
1135
|
const streamIdleTimeoutMs = getStreamIdleTimeoutMs(config, candidate.target.provider);
|
|
@@ -863,28 +1148,39 @@ export const createGateway = (config) => {
|
|
|
863
1148
|
};
|
|
864
1149
|
return (async function* () {
|
|
865
1150
|
let completed = false;
|
|
1151
|
+
let usage = firstEvent.value.type === "finish" ? firstEvent.value.usage : undefined;
|
|
866
1152
|
try {
|
|
867
1153
|
yield firstEvent.value;
|
|
868
1154
|
for (;;) {
|
|
869
1155
|
const next = await nextEvent();
|
|
870
1156
|
if (next.done) {
|
|
871
1157
|
completed = true;
|
|
1158
|
+
metrics.end("success", usage?.outputTokens);
|
|
1159
|
+
permit?.end("success");
|
|
872
1160
|
await context.recordAttempt(createAttempt(candidate.target, true, Date.now() - attemptStartedAt, candidate.targetRank, {
|
|
873
|
-
retry, reasonCode: "provider-success"
|
|
1161
|
+
retry, reasonCode: "provider-success", ...(config.costAccounting ? { usage } : {})
|
|
874
1162
|
}));
|
|
875
1163
|
return;
|
|
876
1164
|
}
|
|
877
1165
|
if (next.value.type === "error")
|
|
878
1166
|
throw next.value.error;
|
|
1167
|
+
if (next.value.type === "text-delta")
|
|
1168
|
+
metrics.firstText();
|
|
1169
|
+
if (next.value.type === "finish" && next.value.usage)
|
|
1170
|
+
usage = { ...usage, ...next.value.usage };
|
|
879
1171
|
yield next.value;
|
|
880
1172
|
}
|
|
881
1173
|
}
|
|
882
1174
|
catch (error) {
|
|
883
1175
|
const aborted = input.abortSignal?.aborted === true;
|
|
884
|
-
|
|
1176
|
+
metrics.end(aborted ? "cancelled" : "error");
|
|
1177
|
+
const disposition = dispositionFor(error);
|
|
1178
|
+
permit?.end(aborted ? "neutral" : disposition.retrySameTarget ? "retryable-error" : "neutral", disposition.retryAfterMs);
|
|
1179
|
+
const diagnostic = disposition.error;
|
|
885
1180
|
const failure = aborted ? abortReason(input.abortSignal) : context.toolHistory ? diagnostic : error;
|
|
886
1181
|
await context.recordAttempt(createAttempt(candidate.target, false, Date.now() - attemptStartedAt, candidate.targetRank, {
|
|
887
1182
|
retry,
|
|
1183
|
+
...(config.costAccounting ? { usage } : {}),
|
|
888
1184
|
reasonCode: aborted ? "request-aborted" : "provider-error",
|
|
889
1185
|
errorMessage: aborted ? abortReason(input.abortSignal).message : diagnostic.message
|
|
890
1186
|
}));
|
|
@@ -892,6 +1188,8 @@ export const createGateway = (config) => {
|
|
|
892
1188
|
}
|
|
893
1189
|
finally {
|
|
894
1190
|
if (!completed) {
|
|
1191
|
+
metrics.end("cancelled");
|
|
1192
|
+
permit?.end("neutral");
|
|
895
1193
|
control.abort(new DOMException("Gateway stream consumer closed.", "AbortError"));
|
|
896
1194
|
}
|
|
897
1195
|
control.dispose();
|
|
@@ -908,6 +1206,7 @@ export const createGateway = (config) => {
|
|
|
908
1206
|
}
|
|
909
1207
|
catch (rawError) {
|
|
910
1208
|
const callerAborted = input.abortSignal?.aborted === true;
|
|
1209
|
+
metrics.end(callerAborted || rawError instanceof GatewayAdmissionError || rawError instanceof GatewayBudgetError ? "cancelled" : "error");
|
|
911
1210
|
const error = control.timedOut() ? control.timeoutError : rawError;
|
|
912
1211
|
control.abort(error);
|
|
913
1212
|
control.dispose();
|
|
@@ -918,6 +1217,7 @@ export const createGateway = (config) => {
|
|
|
918
1217
|
catch { /* Cleanup must not prevent fallback. */ }
|
|
919
1218
|
}
|
|
920
1219
|
if (callerAborted) {
|
|
1220
|
+
permit?.end("neutral");
|
|
921
1221
|
await context.recordAttempt(createAttempt(candidate.target, false, Date.now() - attemptStartedAt, candidate.targetRank, {
|
|
922
1222
|
retry,
|
|
923
1223
|
reasonCode: "request-aborted",
|
|
@@ -926,11 +1226,14 @@ export const createGateway = (config) => {
|
|
|
926
1226
|
throw abortReason(input.abortSignal);
|
|
927
1227
|
}
|
|
928
1228
|
const disposition = dispositionFor(error);
|
|
1229
|
+
permit?.end(disposition.retrySameTarget ? "retryable-error" : "neutral", disposition.retryAfterMs);
|
|
929
1230
|
await context.recordAttempt(createAttempt(candidate.target, false, Date.now() - attemptStartedAt, candidate.targetRank, {
|
|
930
1231
|
retry,
|
|
931
|
-
reasonCode: "provider-error",
|
|
1232
|
+
reasonCode: error instanceof GatewayAdmissionError ? "admission-denied" : error instanceof GatewayBudgetError ? "budget-denied" : "provider-error",
|
|
932
1233
|
errorMessage: disposition.error.message
|
|
933
1234
|
}));
|
|
1235
|
+
if (config.circuitBreaker?.snapshot(candidate.target)?.state === "open")
|
|
1236
|
+
break;
|
|
934
1237
|
if (retry < maxRetries && disposition.retrySameTarget) {
|
|
935
1238
|
await abortableSleep(retryBackoffMs(config, retry, disposition.retryAfterMs), input.abortSignal);
|
|
936
1239
|
continue;
|
|
@@ -968,7 +1271,7 @@ export const createGateway = (config) => {
|
|
|
968
1271
|
model: createRoutedLanguageModel(context)
|
|
969
1272
|
};
|
|
970
1273
|
};
|
|
971
|
-
|
|
1274
|
+
const gateway = {
|
|
972
1275
|
async generate(request) {
|
|
973
1276
|
const route = createStandardRoute(request);
|
|
974
1277
|
const result = await generateText(createTextOptions(route.model, request));
|
|
@@ -1036,7 +1339,7 @@ export const createGateway = (config) => {
|
|
|
1036
1339
|
};
|
|
1037
1340
|
},
|
|
1038
1341
|
async runAgent(request) {
|
|
1039
|
-
|
|
1342
|
+
request = prepareAgentHistoryRequest(configuredAgentRequest(request));
|
|
1040
1343
|
const route = createStandardRoute(request, {
|
|
1041
1344
|
defaultIntent: "tool-heavy",
|
|
1042
1345
|
getSkipReason: (model) => supportsRequiredAgentCapabilities(model, request.requiredAgentCapabilities)
|
|
@@ -1056,7 +1359,10 @@ export const createGateway = (config) => {
|
|
|
1056
1359
|
});
|
|
1057
1360
|
}
|
|
1058
1361
|
});
|
|
1362
|
+
if (request.state?.metadata?.gatewayPortableHistory === true)
|
|
1363
|
+
route.context.toolHistory = true;
|
|
1059
1364
|
const agent = createAgent({
|
|
1365
|
+
...request.agent,
|
|
1060
1366
|
id: request.agentId,
|
|
1061
1367
|
model: route.model,
|
|
1062
1368
|
instructions: request.instructions,
|
|
@@ -1070,7 +1376,7 @@ export const createGateway = (config) => {
|
|
|
1070
1376
|
providerOptions: request.providerOptions,
|
|
1071
1377
|
policy: request.policy,
|
|
1072
1378
|
metadata: request.metadata,
|
|
1073
|
-
store: request.store,
|
|
1379
|
+
store: agentHistoryStore(request.store, route.context, request.agent ? request.metadata?.gatewayAgentRouteBinding : undefined),
|
|
1074
1380
|
memory: request.memory,
|
|
1075
1381
|
onTelemetryEvent: request.onTelemetryEvent,
|
|
1076
1382
|
hookFailurePolicy: request.hookFailurePolicy
|
|
@@ -1079,7 +1385,7 @@ export const createGateway = (config) => {
|
|
|
1079
1385
|
return enrichAgentResult(targetForResult(route.context), route.context.attempts, route.context.routeDecision, route.context.startedAt, result);
|
|
1080
1386
|
},
|
|
1081
1387
|
streamAgent(request) {
|
|
1082
|
-
|
|
1388
|
+
request = prepareAgentHistoryRequest(configuredAgentRequest(request));
|
|
1083
1389
|
const route = createStandardRoute(request, {
|
|
1084
1390
|
defaultIntent: "tool-heavy",
|
|
1085
1391
|
extraRequiredCapabilities: { streaming: true },
|
|
@@ -1100,7 +1406,10 @@ export const createGateway = (config) => {
|
|
|
1100
1406
|
});
|
|
1101
1407
|
}
|
|
1102
1408
|
});
|
|
1409
|
+
if (request.state?.metadata?.gatewayPortableHistory === true)
|
|
1410
|
+
route.context.toolHistory = true;
|
|
1103
1411
|
const agent = createAgent({
|
|
1412
|
+
...request.agent,
|
|
1104
1413
|
id: request.agentId,
|
|
1105
1414
|
model: route.model,
|
|
1106
1415
|
instructions: request.instructions,
|
|
@@ -1114,7 +1423,7 @@ export const createGateway = (config) => {
|
|
|
1114
1423
|
providerOptions: request.providerOptions,
|
|
1115
1424
|
policy: request.policy,
|
|
1116
1425
|
metadata: request.metadata,
|
|
1117
|
-
store: request.store,
|
|
1426
|
+
store: agentHistoryStore(request.store, route.context, request.agent ? request.metadata?.gatewayAgentRouteBinding : undefined),
|
|
1118
1427
|
memory: request.memory,
|
|
1119
1428
|
onTelemetryEvent: request.onTelemetryEvent,
|
|
1120
1429
|
hookFailurePolicy: request.hookFailurePolicy
|
|
@@ -1130,5 +1439,80 @@ export const createGateway = (config) => {
|
|
|
1130
1439
|
};
|
|
1131
1440
|
}
|
|
1132
1441
|
};
|
|
1442
|
+
const operationSignal = (request) => {
|
|
1443
|
+
const candidate = request;
|
|
1444
|
+
return requestHasToolHistory(candidate) || candidate.state?.metadata?.gatewayPortableHistory === true
|
|
1445
|
+
? historyAbortSignal(request.abortSignal) : request.abortSignal;
|
|
1446
|
+
};
|
|
1447
|
+
const timeoutFor = (request) => {
|
|
1448
|
+
for (const value of [request.timeoutMs, config.timeoutMs]) {
|
|
1449
|
+
if (value !== undefined && (!Number.isSafeInteger(value) || value < 1 || value > 2147483647))
|
|
1450
|
+
throw new GatewayError("Invalid operation timeoutMs.", false);
|
|
1451
|
+
}
|
|
1452
|
+
return request.timeoutMs === undefined ? config.timeoutMs : config.timeoutMs === undefined ? request.timeoutMs : Math.min(request.timeoutMs, config.timeoutMs);
|
|
1453
|
+
};
|
|
1454
|
+
const managedCall = async (request, start) => {
|
|
1455
|
+
const control = operationControl(operationSignal(request), timeoutFor(request));
|
|
1456
|
+
try {
|
|
1457
|
+
control.signal.throwIfAborted();
|
|
1458
|
+
return await control.wait(start({ ...request, abortSignal: control.signal }));
|
|
1459
|
+
}
|
|
1460
|
+
finally {
|
|
1461
|
+
control.dispose();
|
|
1462
|
+
}
|
|
1463
|
+
};
|
|
1464
|
+
const managedStream = (request, start) => {
|
|
1465
|
+
const timeoutMs = timeoutFor(request);
|
|
1466
|
+
if (!config.metrics && !config.circuitBreaker && !config.admission && !config.budget && timeoutMs === undefined)
|
|
1467
|
+
return start(request);
|
|
1468
|
+
const control = operationControl(operationSignal(request), timeoutMs);
|
|
1469
|
+
try {
|
|
1470
|
+
control.signal.throwIfAborted();
|
|
1471
|
+
const result = start({ ...request, abortSignal: control.signal });
|
|
1472
|
+
const completion = control.wait(result.collect()).finally(control.dispose);
|
|
1473
|
+
void completion.catch(() => undefined);
|
|
1474
|
+
const wrapped = { ...result, collect: () => completion };
|
|
1475
|
+
for (const key of ["eventStream", "textStream", "partialObjectStream"]) {
|
|
1476
|
+
const source = result[key];
|
|
1477
|
+
if (!source)
|
|
1478
|
+
continue;
|
|
1479
|
+
wrapped[key] = {
|
|
1480
|
+
[Symbol.asyncIterator]() {
|
|
1481
|
+
const iterator = source[Symbol.asyncIterator]();
|
|
1482
|
+
return {
|
|
1483
|
+
next: () => control.wait(Promise.resolve(iterator.next())),
|
|
1484
|
+
return: async () => {
|
|
1485
|
+
control.cancel();
|
|
1486
|
+
control.dispose();
|
|
1487
|
+
try {
|
|
1488
|
+
void Promise.resolve(iterator.return?.()).catch(() => undefined);
|
|
1489
|
+
}
|
|
1490
|
+
catch { /* Best effort. */ }
|
|
1491
|
+
return { done: true, value: undefined };
|
|
1492
|
+
}
|
|
1493
|
+
};
|
|
1494
|
+
}
|
|
1495
|
+
};
|
|
1496
|
+
}
|
|
1497
|
+
return wrapped;
|
|
1498
|
+
}
|
|
1499
|
+
catch (error) {
|
|
1500
|
+
control.cancel();
|
|
1501
|
+
control.dispose();
|
|
1502
|
+
throw error;
|
|
1503
|
+
}
|
|
1504
|
+
};
|
|
1505
|
+
return {
|
|
1506
|
+
...gateway,
|
|
1507
|
+
flushControls: executor.flush,
|
|
1508
|
+
flushObservers: async () => { await Promise.all([...pendingObservers]); },
|
|
1509
|
+
diagnostics: () => ({ pendingObservers: pendingObservers.size, droppedObservers, affinityEntries: affinities.size, ...executor.diagnostics() }),
|
|
1510
|
+
generate: (request) => managedCall(request, gateway.generate),
|
|
1511
|
+
generateObject: (request) => managedCall(request, gateway.generateObject),
|
|
1512
|
+
runAgent: (request) => managedCall(request, gateway.runAgent),
|
|
1513
|
+
streamText: (request) => managedStream(request, gateway.streamText),
|
|
1514
|
+
streamObject: (request) => managedStream(request, gateway.streamObject),
|
|
1515
|
+
streamAgent: (request) => managedStream(request, gateway.streamAgent)
|
|
1516
|
+
};
|
|
1133
1517
|
};
|
|
1134
1518
|
//# sourceMappingURL=index.js.map
|