@zhivex-ai/gateway 0.9.2 → 1.0.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 +25 -3
- package/dist/compat.d.ts +1 -3
- package/dist/compat.d.ts.map +1 -1
- package/dist/compat.js +0 -16
- package/dist/compat.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +604 -361
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +21 -3
- 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,8 +1,8 @@
|
|
|
1
|
-
import { createAgent, generateObject,
|
|
2
|
-
import { createRouteDecision, gatewayMessagesToModelMessages
|
|
1
|
+
import { ConflictError, GuardrailTriggeredError, ProviderHTTPError, ValidationError, createAgent, generateObject, generateText, runAgent, streamAgent, streamObject, streamText } from "@zhivex-ai/core";
|
|
2
|
+
import { createRouteDecision, gatewayMessagesToModelMessages } from "./compat.js";
|
|
3
3
|
import { GatewayError } from "./types.js";
|
|
4
4
|
export { GatewayError } from "./types.js";
|
|
5
|
-
const
|
|
5
|
+
const defaultScoreTarget = (mode, intent, target, config) => {
|
|
6
6
|
const model = target.modelId.toLowerCase();
|
|
7
7
|
const localBoost = target.provider === "ollama" ? -2 : 0;
|
|
8
8
|
const qualityBoost = model.includes("pro") || model.includes("claude") ? 2 : 0;
|
|
@@ -19,22 +19,45 @@ const scoreTarget = (mode, intent, target, config) => {
|
|
|
19
19
|
}
|
|
20
20
|
return speedBoost + qualityBoost + localBoost + (intent === "reasoning" ? 1 : 0) - costPenalty - latencyPenalty;
|
|
21
21
|
};
|
|
22
|
+
const scoreTarget = (mode, intent, target, primary, config) => {
|
|
23
|
+
if (!config.scoreTarget) {
|
|
24
|
+
return defaultScoreTarget(mode, intent, target, config);
|
|
25
|
+
}
|
|
26
|
+
const score = config.scoreTarget({
|
|
27
|
+
mode,
|
|
28
|
+
intent,
|
|
29
|
+
target,
|
|
30
|
+
isPrimary: target.provider === primary.provider && target.modelId === primary.modelId,
|
|
31
|
+
configuredCostPer1kTokens: config.providerCostsPer1kTokens?.[target.provider],
|
|
32
|
+
catalogCostPer1kTokens: config.modelCatalog?.find(target.provider, target.modelId)?.costPer1kTokens,
|
|
33
|
+
latencyBiasMs: config.latencyBiasMs?.[target.provider]
|
|
34
|
+
});
|
|
35
|
+
if (!Number.isFinite(score)) {
|
|
36
|
+
throw new GatewayError("Gateway scoreTarget() must return a finite number.", false);
|
|
37
|
+
}
|
|
38
|
+
return score;
|
|
39
|
+
};
|
|
22
40
|
const orderTargets = (mode, intent, primary, fallbacks, config) => [primary, ...fallbacks]
|
|
23
41
|
.filter((target, index, list) => list.findIndex((candidate) => candidate.provider === target.provider && candidate.modelId === target.modelId) === index)
|
|
24
|
-
.
|
|
25
|
-
|
|
42
|
+
.map((target, index) => ({
|
|
43
|
+
target,
|
|
44
|
+
index,
|
|
45
|
+
score: scoreTarget(mode, intent, target, primary, config)
|
|
46
|
+
}))
|
|
47
|
+
.sort((left, right) => right.score - left.score || left.index - right.index)
|
|
48
|
+
.map(({ target }) => target);
|
|
49
|
+
const supportsRequiredCapabilities = (model, requiredCapabilities) => {
|
|
26
50
|
if (!requiredCapabilities) {
|
|
27
51
|
return true;
|
|
28
52
|
}
|
|
29
|
-
|
|
30
|
-
return Object.entries(requiredCapabilities).every(([key, required]) => required !== true || capabilities[key] === true);
|
|
53
|
+
return Object.entries(requiredCapabilities).every(([key, required]) => required !== true || model.capabilities[key] === true);
|
|
31
54
|
};
|
|
32
55
|
const agentTierRank = (tier) => tier === "tier-a" ? 3 : tier === "tier-b" ? 2 : tier === "tier-c" ? 1 : 0;
|
|
33
|
-
const supportsRequiredAgentCapabilities = (
|
|
56
|
+
const supportsRequiredAgentCapabilities = (model, requiredAgentCapabilities) => {
|
|
34
57
|
if (!requiredAgentCapabilities) {
|
|
35
58
|
return true;
|
|
36
59
|
}
|
|
37
|
-
const capabilities =
|
|
60
|
+
const capabilities = model.capabilities.agentCapabilities;
|
|
38
61
|
if (!capabilities) {
|
|
39
62
|
return false;
|
|
40
63
|
}
|
|
@@ -48,50 +71,196 @@ const supportsRequiredAgentCapabilities = (adapter, target, requiredAgentCapabil
|
|
|
48
71
|
return value !== true || capabilities[key] === true;
|
|
49
72
|
});
|
|
50
73
|
};
|
|
51
|
-
const
|
|
74
|
+
const costBudgetSkipReason = (config, request, target) => {
|
|
52
75
|
if (request.maxCostPer1kTokens == null) {
|
|
53
|
-
return
|
|
76
|
+
return undefined;
|
|
54
77
|
}
|
|
55
78
|
const configuredCost = config.providerCostsPer1kTokens?.[target.provider];
|
|
56
79
|
const catalogCost = config.modelCatalog?.find(target.provider, target.modelId)?.costPer1kTokens;
|
|
57
80
|
const effectiveCost = configuredCost ?? catalogCost;
|
|
58
81
|
if (effectiveCost == null) {
|
|
59
|
-
return
|
|
82
|
+
return config.unknownCostPolicy === "allow"
|
|
83
|
+
? undefined
|
|
84
|
+
: "Skipped because model cost is unknown under the configured budget.";
|
|
60
85
|
}
|
|
61
|
-
return effectiveCost <= request.maxCostPer1kTokens
|
|
86
|
+
return effectiveCost <= request.maxCostPer1kTokens
|
|
87
|
+
? undefined
|
|
88
|
+
: "Skipped because provider cost exceeds the configured budget.";
|
|
62
89
|
};
|
|
63
90
|
const estimateTokens = (text) => Math.max(1, Math.ceil(text.trim().length / 4));
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
91
|
+
const abortReason = (signal) => {
|
|
92
|
+
if (signal.reason instanceof Error) {
|
|
93
|
+
return signal.reason;
|
|
94
|
+
}
|
|
95
|
+
return new DOMException("The gateway request was aborted.", "AbortError");
|
|
96
|
+
};
|
|
97
|
+
const abortableSleep = (ms, signal) => new Promise((resolve, reject) => {
|
|
98
|
+
if (signal?.aborted) {
|
|
99
|
+
reject(abortReason(signal));
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const timer = setTimeout(() => {
|
|
103
|
+
signal?.removeEventListener("abort", onAbort);
|
|
104
|
+
resolve();
|
|
105
|
+
}, Math.max(0, ms));
|
|
106
|
+
const onAbort = () => {
|
|
73
107
|
clearTimeout(timer);
|
|
74
|
-
reject(
|
|
75
|
-
}
|
|
108
|
+
reject(signal ? abortReason(signal) : new DOMException("The gateway request was aborted.", "AbortError"));
|
|
109
|
+
};
|
|
110
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
76
111
|
});
|
|
112
|
+
const createAttemptControl = (parentSignal, timeoutMs) => {
|
|
113
|
+
const controller = new AbortController();
|
|
114
|
+
let timedOut = false;
|
|
115
|
+
const timeoutError = new GatewayError(`Provider timed out after ${timeoutMs}ms.`, true);
|
|
116
|
+
const onParentAbort = () => controller.abort(parentSignal ? abortReason(parentSignal) : undefined);
|
|
117
|
+
if (parentSignal?.aborted) {
|
|
118
|
+
onParentAbort();
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
parentSignal?.addEventListener("abort", onParentAbort, { once: true });
|
|
122
|
+
}
|
|
123
|
+
let timer = setTimeout(() => {
|
|
124
|
+
timedOut = true;
|
|
125
|
+
controller.abort(timeoutError);
|
|
126
|
+
}, timeoutMs);
|
|
127
|
+
const stopTimeout = () => {
|
|
128
|
+
if (timer) {
|
|
129
|
+
clearTimeout(timer);
|
|
130
|
+
timer = undefined;
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
const waitFor = (promise) => new Promise((resolve, reject) => {
|
|
134
|
+
let settled = false;
|
|
135
|
+
const onAbort = () => {
|
|
136
|
+
if (settled) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
settled = true;
|
|
140
|
+
controller.signal.removeEventListener("abort", onAbort);
|
|
141
|
+
reject(controller.signal.reason instanceof Error ? controller.signal.reason : timeoutError);
|
|
142
|
+
};
|
|
143
|
+
if (controller.signal.aborted) {
|
|
144
|
+
onAbort();
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
controller.signal.addEventListener("abort", onAbort, { once: true });
|
|
148
|
+
promise.then((value) => {
|
|
149
|
+
if (settled) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
settled = true;
|
|
153
|
+
controller.signal.removeEventListener("abort", onAbort);
|
|
154
|
+
resolve(value);
|
|
155
|
+
}, (error) => {
|
|
156
|
+
if (settled) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
settled = true;
|
|
160
|
+
controller.signal.removeEventListener("abort", onAbort);
|
|
161
|
+
reject(error);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
return {
|
|
165
|
+
signal: controller.signal,
|
|
166
|
+
waitFor,
|
|
167
|
+
timedOut: () => timedOut,
|
|
168
|
+
timeoutError,
|
|
169
|
+
stopTimeout,
|
|
170
|
+
abort: (reason) => controller.abort(reason),
|
|
171
|
+
dispose: () => {
|
|
172
|
+
stopTimeout();
|
|
173
|
+
parentSignal?.removeEventListener("abort", onParentAbort);
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
};
|
|
177
|
+
const providerHTTPStatus = (error) => {
|
|
178
|
+
if (error instanceof ProviderHTTPError) {
|
|
179
|
+
return error.status;
|
|
180
|
+
}
|
|
181
|
+
if (error instanceof Error &&
|
|
182
|
+
error.name === "ProviderHTTPError" &&
|
|
183
|
+
"status" in error &&
|
|
184
|
+
typeof error.status === "number") {
|
|
185
|
+
return error.status;
|
|
186
|
+
}
|
|
187
|
+
return undefined;
|
|
188
|
+
};
|
|
77
189
|
const normalizeError = (error) => {
|
|
190
|
+
if (error instanceof ValidationError || error instanceof ConflictError || error instanceof GuardrailTriggeredError) {
|
|
191
|
+
return {
|
|
192
|
+
error: new GatewayError(error.message, false),
|
|
193
|
+
retrySameTarget: false,
|
|
194
|
+
fallbackNextTarget: false
|
|
195
|
+
};
|
|
196
|
+
}
|
|
78
197
|
if (error instanceof GatewayError) {
|
|
79
|
-
return
|
|
198
|
+
return {
|
|
199
|
+
error,
|
|
200
|
+
retrySameTarget: error.retryable,
|
|
201
|
+
fallbackNextTarget: true
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
const status = providerHTTPStatus(error);
|
|
205
|
+
if (status != null) {
|
|
206
|
+
const retryable = status === 408 || status === 429 || status >= 500;
|
|
207
|
+
return {
|
|
208
|
+
error: new GatewayError(error instanceof Error ? error.message : `Provider HTTP ${status}.`, retryable),
|
|
209
|
+
retrySameTarget: retryable,
|
|
210
|
+
fallbackNextTarget: true
|
|
211
|
+
};
|
|
80
212
|
}
|
|
81
213
|
if (error instanceof Error) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
214
|
+
if (error.name === "AbortError") {
|
|
215
|
+
return {
|
|
216
|
+
error: new GatewayError(error.message, false),
|
|
217
|
+
retrySameTarget: false,
|
|
218
|
+
fallbackNextTarget: false
|
|
219
|
+
};
|
|
88
220
|
}
|
|
89
|
-
|
|
221
|
+
const message = error.message.toLowerCase();
|
|
222
|
+
const retryable = message.includes("timed out") ||
|
|
223
|
+
message.includes("timeout") ||
|
|
224
|
+
message.includes("429") ||
|
|
225
|
+
message.includes("rate limit") ||
|
|
226
|
+
message.includes("connect") ||
|
|
227
|
+
message.includes("econnrefused") ||
|
|
228
|
+
message.includes("enotfound") ||
|
|
229
|
+
message.includes("network") ||
|
|
230
|
+
/\b50[0234]\b/.test(message);
|
|
231
|
+
return {
|
|
232
|
+
error: new GatewayError(error.message, retryable),
|
|
233
|
+
retrySameTarget: retryable,
|
|
234
|
+
fallbackNextTarget: true
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
return {
|
|
238
|
+
error: new GatewayError("Unknown gateway error.", false),
|
|
239
|
+
retrySameTarget: false,
|
|
240
|
+
fallbackNextTarget: true
|
|
241
|
+
};
|
|
242
|
+
};
|
|
243
|
+
const getAttemptTimeoutMs = (config, provider) => {
|
|
244
|
+
const timeoutMs = config.attemptTimeoutsMs?.[provider] ?? config.attemptTimeoutMs ?? 20_000;
|
|
245
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
246
|
+
throw new GatewayError("Gateway attempt timeouts must be finite positive numbers.", false);
|
|
90
247
|
}
|
|
91
|
-
return
|
|
248
|
+
return timeoutMs;
|
|
249
|
+
};
|
|
250
|
+
const getMaxRetries = (config) => {
|
|
251
|
+
const maxRetries = config.maxRetries ?? 2;
|
|
252
|
+
if (!Number.isFinite(maxRetries) || maxRetries < 0) {
|
|
253
|
+
throw new GatewayError("Gateway maxRetries must be a finite non-negative number.", false);
|
|
254
|
+
}
|
|
255
|
+
return Math.floor(maxRetries);
|
|
256
|
+
};
|
|
257
|
+
const retryBackoffMs = (config, retry) => {
|
|
258
|
+
const base = config.retryBackoffMs ?? 200;
|
|
259
|
+
if (!Number.isFinite(base) || base < 0) {
|
|
260
|
+
throw new GatewayError("Gateway retryBackoffMs must be a finite non-negative number.", false);
|
|
261
|
+
}
|
|
262
|
+
return base * (retry + 1);
|
|
92
263
|
};
|
|
93
|
-
const getAttemptTimeoutMs = (config, provider) => config.attemptTimeoutsMs?.[provider] ?? config.attemptTimeoutMs ?? 20_000;
|
|
94
|
-
const retryBackoffMs = (config, retry) => (config.retryBackoffMs ?? 200) * (retry + 1);
|
|
95
264
|
const createAttempt = (target, ok, latencyMs, targetRank, options = {}) => ({
|
|
96
265
|
provider: target.provider,
|
|
97
266
|
modelId: target.modelId,
|
|
@@ -100,13 +269,17 @@ const createAttempt = (target, ok, latencyMs, targetRank, options = {}) => ({
|
|
|
100
269
|
targetRank,
|
|
101
270
|
...options
|
|
102
271
|
});
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
272
|
+
const notifyAttempt = async (config, attempt) => {
|
|
273
|
+
try {
|
|
274
|
+
await config.onAttempt?.({
|
|
275
|
+
...attempt,
|
|
276
|
+
retry: attempt.retry ?? 0,
|
|
277
|
+
targetRank: attempt.targetRank ?? 0
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
// Observability is best-effort and must never retry or fail provider work.
|
|
282
|
+
}
|
|
110
283
|
};
|
|
111
284
|
const normalizeUsage = (usage, inputText, outputText) => {
|
|
112
285
|
const inputTokens = usage?.inputTokens ?? estimateTokens(inputText);
|
|
@@ -119,31 +292,48 @@ const normalizeUsage = (usage, inputText, outputText) => {
|
|
|
119
292
|
estimated: usage?.inputTokens == null || usage?.outputTokens == null || usage?.totalTokens == null
|
|
120
293
|
};
|
|
121
294
|
};
|
|
122
|
-
const requestMessages = (request) => gatewayMessagesToModelMessages(stripImagesForUnsupportedModel(request.messages, request.primary.provider, request.primary.modelId), request.systemPrompt);
|
|
123
295
|
const getInputText = (request) => `${request.systemPrompt ?? ""}\n${request.messages.map((message) => message.content).join("\n")}`.trim();
|
|
296
|
+
const requestHasImages = (request) => request.messages?.some((message) => (message.images?.length ?? 0) > 0) ?? false;
|
|
124
297
|
const buildRequiredCapabilities = (request, extra = {}) => ({
|
|
125
298
|
...(request.requiredCapabilities ?? {}),
|
|
126
299
|
...(request.tools ? { tools: true } : {}),
|
|
300
|
+
...(request.toolChoice ? { toolChoice: true } : {}),
|
|
127
301
|
...(request.reasoning ? { reasoning: true } : {}),
|
|
302
|
+
...(requestHasImages(request) ? { vision: true } : {}),
|
|
128
303
|
...extra
|
|
129
304
|
});
|
|
130
|
-
const objectCapabilitySkipReason = (
|
|
131
|
-
const capabilities = adapter.languageModel(target.modelId).capabilities;
|
|
305
|
+
const objectCapabilitySkipReason = (model, request) => {
|
|
132
306
|
const mode = request.mode ?? "auto";
|
|
133
|
-
if (mode === "native" && !capabilities.structuredOutput) {
|
|
307
|
+
if (mode === "native" && !model.capabilities.structuredOutput) {
|
|
134
308
|
return "Skipped because model capabilities do not satisfy native structured output.";
|
|
135
309
|
}
|
|
136
|
-
if (mode === "prompted" && !capabilities.jsonMode) {
|
|
310
|
+
if (mode === "prompted" && !model.capabilities.jsonMode) {
|
|
137
311
|
return "Skipped because model capabilities do not satisfy prompted JSON output.";
|
|
138
312
|
}
|
|
139
|
-
if (mode === "auto" && !capabilities.structuredOutput && !capabilities.jsonMode) {
|
|
313
|
+
if (mode === "auto" && !model.capabilities.structuredOutput && !model.capabilities.jsonMode) {
|
|
140
314
|
return "Skipped because model capabilities do not satisfy object output.";
|
|
141
315
|
}
|
|
142
316
|
return undefined;
|
|
143
317
|
};
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
318
|
+
const modelInputSkipReason = (model, input) => {
|
|
319
|
+
if (input.tools && !model.capabilities.tools) {
|
|
320
|
+
return "Skipped because model does not support tools.";
|
|
321
|
+
}
|
|
322
|
+
if (input.reasoning && !model.capabilities.reasoning) {
|
|
323
|
+
return "Skipped because model does not support reasoning.";
|
|
324
|
+
}
|
|
325
|
+
if (input.messages.some((message) => message.parts.some((part) => part.type === "image")) &&
|
|
326
|
+
!model.capabilities.vision) {
|
|
327
|
+
return "Skipped because model does not support image input.";
|
|
328
|
+
}
|
|
329
|
+
if (input.structuredOutput?.mode === "native" && !model.capabilities.structuredOutput) {
|
|
330
|
+
return "Skipped because model does not support native structured output.";
|
|
331
|
+
}
|
|
332
|
+
return undefined;
|
|
333
|
+
};
|
|
334
|
+
const createTextOptions = (model, request) => ({
|
|
335
|
+
model,
|
|
336
|
+
messages: gatewayMessagesToModelMessages(request.messages, request.systemPrompt),
|
|
147
337
|
tools: request.tools,
|
|
148
338
|
toolChoice: request.toolChoice,
|
|
149
339
|
toolExecution: request.toolExecution,
|
|
@@ -159,7 +349,7 @@ const enrichTextResult = (request, target, attempts, routeDecision, startedAt, r
|
|
|
159
349
|
providerUsed: target.provider,
|
|
160
350
|
modelUsed: target.modelId,
|
|
161
351
|
latencyMs: Date.now() - startedAt,
|
|
162
|
-
attempts,
|
|
352
|
+
attempts: [...attempts],
|
|
163
353
|
usage: normalizeUsage(result.usage, getInputText(request), result.text),
|
|
164
354
|
routeDecision
|
|
165
355
|
});
|
|
@@ -168,21 +358,23 @@ const enrichObjectResult = (request, target, attempts, routeDecision, startedAt,
|
|
|
168
358
|
providerUsed: target.provider,
|
|
169
359
|
modelUsed: target.modelId,
|
|
170
360
|
latencyMs: Date.now() - startedAt,
|
|
171
|
-
attempts,
|
|
361
|
+
attempts: [...attempts],
|
|
172
362
|
usage: normalizeUsage(result.usage, getInputText(request), result.text),
|
|
173
363
|
routeDecision
|
|
174
364
|
});
|
|
175
|
-
const createAgentMessages = (request
|
|
176
|
-
|
|
177
|
-
: undefined;
|
|
178
|
-
const createAgentRunInput = (request, target) => {
|
|
365
|
+
const createAgentMessages = (request) => request.messages ? gatewayMessagesToModelMessages(request.messages, undefined) : undefined;
|
|
366
|
+
const createAgentRunInput = (request) => {
|
|
179
367
|
const source = request.prompt !== undefined
|
|
180
368
|
? { prompt: request.prompt }
|
|
181
369
|
: request.messages
|
|
182
|
-
? { messages: createAgentMessages(request
|
|
370
|
+
? { messages: createAgentMessages(request) }
|
|
183
371
|
: {};
|
|
184
372
|
return {
|
|
185
373
|
...source,
|
|
374
|
+
runId: request.runId,
|
|
375
|
+
scope: request.scope,
|
|
376
|
+
idempotencyKey: request.idempotencyKey,
|
|
377
|
+
parentRunId: request.parentRunId,
|
|
186
378
|
system: request.system,
|
|
187
379
|
state: request.state,
|
|
188
380
|
approvals: request.approvals,
|
|
@@ -190,11 +382,14 @@ const createAgentRunInput = (request, target) => {
|
|
|
190
382
|
tools: request.tools,
|
|
191
383
|
toolChoice: request.toolChoice,
|
|
192
384
|
toolExecution: request.toolExecution,
|
|
385
|
+
toolApprovalPolicy: request.toolApprovalPolicy,
|
|
193
386
|
maxSteps: request.maxSteps,
|
|
194
387
|
temperature: request.temperature,
|
|
195
388
|
maxTokens: request.maxTokens,
|
|
196
389
|
reasoning: request.reasoning,
|
|
197
390
|
providerOptions: request.providerOptions,
|
|
391
|
+
policy: request.policy,
|
|
392
|
+
metadata: request.metadata,
|
|
198
393
|
abortSignal: request.abortSignal
|
|
199
394
|
};
|
|
200
395
|
};
|
|
@@ -203,7 +398,7 @@ const enrichAgentResult = (target, attempts, routeDecision, startedAt, result) =
|
|
|
203
398
|
providerUsed: target.provider,
|
|
204
399
|
modelUsed: target.modelId,
|
|
205
400
|
latencyMs: Date.now() - startedAt,
|
|
206
|
-
attempts,
|
|
401
|
+
attempts: [...attempts],
|
|
207
402
|
routeDecision,
|
|
208
403
|
state: {
|
|
209
404
|
...result.state,
|
|
@@ -211,307 +406,368 @@ const enrichAgentResult = (target, attempts, routeDecision, startedAt, result) =
|
|
|
211
406
|
}
|
|
212
407
|
});
|
|
213
408
|
export const createGateway = (config) => {
|
|
214
|
-
const
|
|
215
|
-
const adapter = config.adapters[provider];
|
|
216
|
-
if (!adapter) {
|
|
217
|
-
throw new GatewayError(`No adapter registered for provider "${provider}".`, false);
|
|
218
|
-
}
|
|
219
|
-
return adapter;
|
|
220
|
-
};
|
|
221
|
-
const routeDecisionFor = (request) => {
|
|
222
|
-
const mode = request.routingMode ?? "balanced";
|
|
223
|
-
const intent = request.taskIntent ?? "chat";
|
|
224
|
-
const orderedTargets = orderTargets(mode, intent, request.primary, request.fallbacks ?? [], config);
|
|
225
|
-
return {
|
|
226
|
-
mode,
|
|
227
|
-
intent,
|
|
228
|
-
orderedTargets,
|
|
229
|
-
routeDecision: createRouteDecision(mode, intent, orderedTargets)
|
|
230
|
-
};
|
|
231
|
-
};
|
|
232
|
-
const selectAgentTarget = async (request) => {
|
|
409
|
+
const createRouteContext = (request, options = {}) => {
|
|
233
410
|
const mode = request.routingMode ?? "balanced";
|
|
234
|
-
const intent = request.taskIntent ?? "
|
|
411
|
+
const intent = request.taskIntent ?? options.defaultIntent ?? "chat";
|
|
235
412
|
const orderedTargets = orderTargets(mode, intent, request.primary, request.fallbacks ?? [], config);
|
|
236
413
|
const routeDecision = createRouteDecision(mode, intent, orderedTargets);
|
|
237
414
|
const attempts = [];
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
415
|
+
const candidates = [];
|
|
416
|
+
let notificationChain = Promise.resolve();
|
|
417
|
+
const queueAttempt = (attempt) => {
|
|
418
|
+
attempts.push(attempt);
|
|
419
|
+
notificationChain = notificationChain.then(() => notifyAttempt(config, attempt));
|
|
420
|
+
};
|
|
421
|
+
const requiredCapabilities = buildRequiredCapabilities(request, options.extraRequiredCapabilities ?? {});
|
|
422
|
+
for (const [targetRank, target] of orderedTargets.entries()) {
|
|
423
|
+
const adapter = config.adapters[target.provider];
|
|
424
|
+
if (!adapter) {
|
|
425
|
+
queueAttempt(createAttempt(target, false, 0, targetRank, {
|
|
426
|
+
reasonCode: "operation-skip",
|
|
427
|
+
errorMessage: `Skipped because no adapter is registered for provider "${target.provider}".`
|
|
244
428
|
}));
|
|
245
429
|
continue;
|
|
246
430
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
errorMessage: "Skipped because agent capabilities do not satisfy the request."
|
|
251
|
-
}));
|
|
252
|
-
continue;
|
|
431
|
+
let model;
|
|
432
|
+
try {
|
|
433
|
+
model = adapter.languageModel(target.modelId);
|
|
253
434
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
reasonCode: "
|
|
257
|
-
errorMessage:
|
|
435
|
+
catch (error) {
|
|
436
|
+
queueAttempt(createAttempt(target, false, 0, targetRank, {
|
|
437
|
+
reasonCode: "operation-skip",
|
|
438
|
+
errorMessage: error instanceof Error ? error.message : "Provider model construction failed."
|
|
258
439
|
}));
|
|
259
440
|
continue;
|
|
260
441
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
modelId: target.modelId,
|
|
264
|
-
routeDecision,
|
|
265
|
-
attempts,
|
|
266
|
-
targetRank: targetIndex
|
|
267
|
-
});
|
|
268
|
-
return {
|
|
269
|
-
target,
|
|
270
|
-
attempts,
|
|
271
|
-
routeDecision,
|
|
272
|
-
startedAt: Date.now(),
|
|
273
|
-
targetRank: targetIndex
|
|
274
|
-
};
|
|
275
|
-
}
|
|
276
|
-
throw new GatewayError(attempts.at(-1)?.errorMessage ?? "No gateway agent target satisfied the request.", false);
|
|
277
|
-
};
|
|
278
|
-
const runGenerate = async (request, operation, extraRequiredCapabilities = {}, getSkipReason) => {
|
|
279
|
-
const attempts = [];
|
|
280
|
-
const startedAt = Date.now();
|
|
281
|
-
const { orderedTargets, routeDecision } = routeDecisionFor(request);
|
|
282
|
-
const maxRetries = Math.max(0, config.maxRetries ?? 2);
|
|
283
|
-
const requiredCapabilities = buildRequiredCapabilities(request, extraRequiredCapabilities);
|
|
284
|
-
for (const [targetIndex, target] of orderedTargets.entries()) {
|
|
285
|
-
const adapter = getAdapter(target.provider);
|
|
286
|
-
if (!supportsRequiredCapabilities(adapter, target, requiredCapabilities)) {
|
|
287
|
-
await recordAttempt(config, attempts, createAttempt(target, false, 0, targetIndex, {
|
|
442
|
+
if (!supportsRequiredCapabilities(model, requiredCapabilities)) {
|
|
443
|
+
queueAttempt(createAttempt(target, false, 0, targetRank, {
|
|
288
444
|
reasonCode: "model-capabilities",
|
|
289
445
|
errorMessage: "Skipped because model capabilities do not satisfy the request."
|
|
290
446
|
}));
|
|
291
447
|
continue;
|
|
292
448
|
}
|
|
293
|
-
|
|
294
|
-
|
|
449
|
+
const budgetReason = costBudgetSkipReason(config, request, target);
|
|
450
|
+
if (budgetReason) {
|
|
451
|
+
queueAttempt(createAttempt(target, false, 0, targetRank, {
|
|
295
452
|
reasonCode: "cost-budget",
|
|
296
|
-
errorMessage:
|
|
453
|
+
errorMessage: budgetReason
|
|
297
454
|
}));
|
|
298
455
|
continue;
|
|
299
456
|
}
|
|
300
|
-
const
|
|
301
|
-
if (
|
|
302
|
-
|
|
303
|
-
reasonCode:
|
|
304
|
-
errorMessage:
|
|
457
|
+
const skip = options.getSkipReason?.(model, target);
|
|
458
|
+
if (skip) {
|
|
459
|
+
queueAttempt(createAttempt(target, false, 0, targetRank, {
|
|
460
|
+
reasonCode: skip.reasonCode,
|
|
461
|
+
errorMessage: skip.message
|
|
305
462
|
}));
|
|
306
463
|
continue;
|
|
307
464
|
}
|
|
308
|
-
|
|
309
|
-
|
|
465
|
+
candidates.push({ target, targetRank, model });
|
|
466
|
+
}
|
|
467
|
+
if (!candidates.length) {
|
|
468
|
+
throw new GatewayError(attempts.at(-1)?.errorMessage ?? "No gateway target satisfied the request.", false);
|
|
469
|
+
}
|
|
470
|
+
const context = {
|
|
471
|
+
attempts,
|
|
472
|
+
candidates,
|
|
473
|
+
routeDecision,
|
|
474
|
+
startedAt: Date.now(),
|
|
475
|
+
flushAttempts: () => notificationChain,
|
|
476
|
+
recordAttempt: async (attempt) => {
|
|
477
|
+
queueAttempt(attempt);
|
|
478
|
+
await notificationChain;
|
|
479
|
+
},
|
|
480
|
+
lock: async (candidate) => {
|
|
481
|
+
if (context.winner?.target.provider === candidate.target.provider &&
|
|
482
|
+
context.winner.target.modelId === candidate.target.modelId) {
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
context.winner = candidate;
|
|
310
486
|
try {
|
|
311
|
-
|
|
312
|
-
await recordAttempt(config, attempts, createAttempt(target, true, Date.now() - attemptStartedAt, targetIndex, {
|
|
313
|
-
retry,
|
|
314
|
-
reasonCode: "provider-success"
|
|
315
|
-
}));
|
|
316
|
-
return {
|
|
317
|
-
attempts,
|
|
318
|
-
target,
|
|
319
|
-
startedAt,
|
|
320
|
-
routeDecision,
|
|
321
|
-
result
|
|
322
|
-
};
|
|
487
|
+
await options.onWinner?.(candidate, [...attempts]);
|
|
323
488
|
}
|
|
324
|
-
catch
|
|
325
|
-
|
|
326
|
-
await recordAttempt(config, attempts, createAttempt(target, false, Date.now() - attemptStartedAt, targetIndex, {
|
|
327
|
-
reasonCode: "provider-error",
|
|
328
|
-
errorMessage: normalized.message,
|
|
329
|
-
retry
|
|
330
|
-
}));
|
|
331
|
-
if (retry < maxRetries && normalized.retryable) {
|
|
332
|
-
await sleep(retryBackoffMs(config, retry));
|
|
333
|
-
continue;
|
|
334
|
-
}
|
|
335
|
-
break;
|
|
489
|
+
catch {
|
|
490
|
+
// Routing observers are best-effort and cannot fail provider work.
|
|
336
491
|
}
|
|
337
492
|
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
throw new GatewayError(finalError, false);
|
|
493
|
+
};
|
|
494
|
+
return context;
|
|
341
495
|
};
|
|
342
|
-
const
|
|
343
|
-
const
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
496
|
+
const createRoutedLanguageModel = (context) => {
|
|
497
|
+
const first = context.candidates[0];
|
|
498
|
+
const recordInputSkip = async (candidate, message) => {
|
|
499
|
+
await context.recordAttempt(createAttempt(candidate.target, false, 0, candidate.targetRank, {
|
|
500
|
+
reasonCode: "operation-skip",
|
|
501
|
+
errorMessage: message
|
|
502
|
+
}));
|
|
503
|
+
};
|
|
504
|
+
const throwFinalError = () => {
|
|
505
|
+
throw new GatewayError(context.attempts.at(-1)?.errorMessage ?? "All gateway attempts failed.", false);
|
|
506
|
+
};
|
|
507
|
+
const generate = async (input) => {
|
|
508
|
+
await context.flushAttempts();
|
|
509
|
+
const candidates = context.winner
|
|
510
|
+
? [context.winner, ...context.candidates.filter((candidate) => candidate !== context.winner)]
|
|
511
|
+
: context.candidates;
|
|
512
|
+
const maxRetries = getMaxRetries(config);
|
|
513
|
+
for (const candidate of candidates) {
|
|
514
|
+
const inputSkipReason = modelInputSkipReason(candidate.model, input);
|
|
515
|
+
if (inputSkipReason) {
|
|
516
|
+
await recordInputSkip(candidate, inputSkipReason);
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
519
|
+
for (let retry = 0; retry <= maxRetries; retry += 1) {
|
|
520
|
+
const attemptStartedAt = Date.now();
|
|
521
|
+
const control = createAttemptControl(input.abortSignal, getAttemptTimeoutMs(config, candidate.target.provider));
|
|
522
|
+
try {
|
|
523
|
+
const result = await control.waitFor(candidate.model.generate({
|
|
524
|
+
...input,
|
|
525
|
+
abortSignal: control.signal
|
|
526
|
+
}));
|
|
527
|
+
control.stopTimeout();
|
|
528
|
+
await context.recordAttempt(createAttempt(candidate.target, true, Date.now() - attemptStartedAt, candidate.targetRank, {
|
|
529
|
+
retry,
|
|
530
|
+
reasonCode: "provider-success"
|
|
531
|
+
}));
|
|
532
|
+
await context.lock(candidate);
|
|
533
|
+
control.dispose();
|
|
534
|
+
return result;
|
|
380
535
|
}
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
536
|
+
catch (rawError) {
|
|
537
|
+
const callerAborted = input.abortSignal?.aborted === true;
|
|
538
|
+
const error = control.timedOut() ? control.timeoutError : rawError;
|
|
539
|
+
control.abort(error);
|
|
540
|
+
control.dispose();
|
|
541
|
+
if (callerAborted) {
|
|
542
|
+
await context.recordAttempt(createAttempt(candidate.target, false, Date.now() - attemptStartedAt, candidate.targetRank, {
|
|
543
|
+
retry,
|
|
544
|
+
reasonCode: "request-aborted",
|
|
545
|
+
errorMessage: abortReason(input.abortSignal).message
|
|
546
|
+
}));
|
|
547
|
+
throw abortReason(input.abortSignal);
|
|
384
548
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
549
|
+
const disposition = normalizeError(error);
|
|
550
|
+
await context.recordAttempt(createAttempt(candidate.target, false, Date.now() - attemptStartedAt, candidate.targetRank, {
|
|
551
|
+
retry,
|
|
552
|
+
reasonCode: "provider-error",
|
|
553
|
+
errorMessage: disposition.error.message
|
|
554
|
+
}));
|
|
555
|
+
if (retry < maxRetries && disposition.retrySameTarget) {
|
|
556
|
+
await abortableSleep(retryBackoffMs(config, retry), input.abortSignal);
|
|
557
|
+
continue;
|
|
391
558
|
}
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
retry,
|
|
395
|
-
reasonCode: "provider-success"
|
|
396
|
-
}));
|
|
397
|
-
return {
|
|
398
|
-
attempts,
|
|
399
|
-
target,
|
|
400
|
-
startedAt,
|
|
401
|
-
routeDecision,
|
|
402
|
-
streamResult: {
|
|
403
|
-
...streamResult,
|
|
404
|
-
eventStream
|
|
559
|
+
if (!disposition.fallbackNextTarget) {
|
|
560
|
+
throw disposition.error;
|
|
405
561
|
}
|
|
406
|
-
|
|
562
|
+
break;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
return throwFinalError();
|
|
567
|
+
};
|
|
568
|
+
const stream = async (input) => {
|
|
569
|
+
await context.flushAttempts();
|
|
570
|
+
const candidates = context.winner
|
|
571
|
+
? [context.winner, ...context.candidates.filter((candidate) => candidate !== context.winner)]
|
|
572
|
+
: context.candidates;
|
|
573
|
+
const maxRetries = getMaxRetries(config);
|
|
574
|
+
for (const candidate of candidates) {
|
|
575
|
+
const inputSkipReason = modelInputSkipReason(candidate.model, input);
|
|
576
|
+
if (inputSkipReason) {
|
|
577
|
+
await recordInputSkip(candidate, inputSkipReason);
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
580
|
+
if (!candidate.model.stream) {
|
|
581
|
+
await recordInputSkip(candidate, "Skipped because model does not support streaming.");
|
|
582
|
+
continue;
|
|
407
583
|
}
|
|
408
|
-
|
|
409
|
-
const
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
584
|
+
for (let retry = 0; retry <= maxRetries; retry += 1) {
|
|
585
|
+
const attemptStartedAt = Date.now();
|
|
586
|
+
const control = createAttemptControl(input.abortSignal, getAttemptTimeoutMs(config, candidate.target.provider));
|
|
587
|
+
let iterator;
|
|
588
|
+
try {
|
|
589
|
+
const providerStream = await control.waitFor(candidate.model.stream({
|
|
590
|
+
...input,
|
|
591
|
+
abortSignal: control.signal
|
|
592
|
+
}));
|
|
593
|
+
iterator = providerStream[Symbol.asyncIterator]();
|
|
594
|
+
const firstEvent = await control.waitFor(iterator.next());
|
|
595
|
+
if (firstEvent.done) {
|
|
596
|
+
throw new GatewayError("Provider stream ended before emitting an event.", false);
|
|
597
|
+
}
|
|
598
|
+
if (firstEvent.value.type === "error") {
|
|
599
|
+
throw firstEvent.value.error;
|
|
600
|
+
}
|
|
601
|
+
control.stopTimeout();
|
|
602
|
+
await context.recordAttempt(createAttempt(candidate.target, true, Date.now() - attemptStartedAt, candidate.targetRank, {
|
|
603
|
+
retry,
|
|
604
|
+
reasonCode: "provider-success"
|
|
605
|
+
}));
|
|
606
|
+
await context.lock(candidate);
|
|
607
|
+
return (async function* () {
|
|
608
|
+
let completed = false;
|
|
609
|
+
try {
|
|
610
|
+
yield firstEvent.value;
|
|
611
|
+
for (;;) {
|
|
612
|
+
const next = await control.waitFor(iterator.next());
|
|
613
|
+
if (next.done) {
|
|
614
|
+
completed = true;
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
yield next.value;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
finally {
|
|
621
|
+
if (!completed) {
|
|
622
|
+
control.abort(new DOMException("Gateway stream consumer closed.", "AbortError"));
|
|
623
|
+
}
|
|
624
|
+
control.dispose();
|
|
625
|
+
if (iterator?.return) {
|
|
626
|
+
await iterator.return();
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
})();
|
|
630
|
+
}
|
|
631
|
+
catch (rawError) {
|
|
632
|
+
const callerAborted = input.abortSignal?.aborted === true;
|
|
633
|
+
const error = control.timedOut() ? control.timeoutError : rawError;
|
|
634
|
+
control.abort(error);
|
|
635
|
+
control.dispose();
|
|
636
|
+
if (iterator?.return) {
|
|
637
|
+
void iterator.return().catch(() => undefined);
|
|
638
|
+
}
|
|
639
|
+
if (callerAborted) {
|
|
640
|
+
await context.recordAttempt(createAttempt(candidate.target, false, Date.now() - attemptStartedAt, candidate.targetRank, {
|
|
641
|
+
retry,
|
|
642
|
+
reasonCode: "request-aborted",
|
|
643
|
+
errorMessage: abortReason(input.abortSignal).message
|
|
644
|
+
}));
|
|
645
|
+
throw abortReason(input.abortSignal);
|
|
646
|
+
}
|
|
647
|
+
const disposition = normalizeError(error);
|
|
648
|
+
await context.recordAttempt(createAttempt(candidate.target, false, Date.now() - attemptStartedAt, candidate.targetRank, {
|
|
649
|
+
retry,
|
|
650
|
+
reasonCode: "provider-error",
|
|
651
|
+
errorMessage: disposition.error.message
|
|
652
|
+
}));
|
|
653
|
+
if (retry < maxRetries && disposition.retrySameTarget) {
|
|
654
|
+
await abortableSleep(retryBackoffMs(config, retry), input.abortSignal);
|
|
655
|
+
continue;
|
|
656
|
+
}
|
|
657
|
+
if (!disposition.fallbackNextTarget) {
|
|
658
|
+
throw disposition.error;
|
|
659
|
+
}
|
|
660
|
+
break;
|
|
418
661
|
}
|
|
419
|
-
break;
|
|
420
662
|
}
|
|
421
663
|
}
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
664
|
+
return throwFinalError();
|
|
665
|
+
};
|
|
666
|
+
return {
|
|
667
|
+
get provider() {
|
|
668
|
+
return context.winner?.model.provider ?? first.model.provider;
|
|
669
|
+
},
|
|
670
|
+
get modelId() {
|
|
671
|
+
return context.winner?.model.modelId ?? first.model.modelId;
|
|
672
|
+
},
|
|
673
|
+
get capabilities() {
|
|
674
|
+
return context.winner?.model.capabilities ?? first.model.capabilities;
|
|
675
|
+
},
|
|
676
|
+
generate,
|
|
677
|
+
stream
|
|
678
|
+
};
|
|
679
|
+
};
|
|
680
|
+
const targetForResult = (context) => (context.winner ?? context.candidates[0]).target;
|
|
681
|
+
const createStandardRoute = (request, options = {}) => {
|
|
682
|
+
const context = createRouteContext(request, options);
|
|
683
|
+
return {
|
|
684
|
+
context,
|
|
685
|
+
model: createRoutedLanguageModel(context)
|
|
686
|
+
};
|
|
425
687
|
};
|
|
426
688
|
return {
|
|
427
689
|
async generate(request) {
|
|
428
|
-
const
|
|
429
|
-
|
|
690
|
+
const route = createStandardRoute(request);
|
|
691
|
+
const result = await generateText(createTextOptions(route.model, request));
|
|
692
|
+
return enrichTextResult(request, targetForResult(route.context), route.context.attempts, route.context.routeDecision, route.context.startedAt, result);
|
|
430
693
|
},
|
|
431
694
|
streamText(request) {
|
|
432
|
-
const
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
relayPromise = selected.then((routed) => ({
|
|
437
|
-
collect: async () => enrichTextResult(request, routed.target, routed.attempts, routed.routeDecision, routed.startedAt, await routed.streamResult.collect())
|
|
438
|
-
}));
|
|
439
|
-
}
|
|
440
|
-
return relayPromise;
|
|
441
|
-
};
|
|
695
|
+
const route = createStandardRoute(request, {
|
|
696
|
+
extraRequiredCapabilities: { streaming: true }
|
|
697
|
+
});
|
|
698
|
+
const streamResult = streamText(createTextOptions(route.model, request));
|
|
442
699
|
return {
|
|
443
|
-
eventStream:
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
}
|
|
449
|
-
textStream: (async function* () {
|
|
450
|
-
const { streamResult } = await selected;
|
|
451
|
-
for await (const chunk of streamResult.textStream) {
|
|
452
|
-
yield chunk;
|
|
453
|
-
}
|
|
454
|
-
})(),
|
|
455
|
-
collect: async () => (await ensureRelay()).collect()
|
|
700
|
+
eventStream: streamResult.eventStream,
|
|
701
|
+
textStream: streamResult.textStream,
|
|
702
|
+
collect: async () => {
|
|
703
|
+
const result = await streamResult.collect();
|
|
704
|
+
return enrichTextResult(request, targetForResult(route.context), route.context.attempts, route.context.routeDecision, route.context.startedAt, result);
|
|
705
|
+
}
|
|
456
706
|
};
|
|
457
707
|
},
|
|
458
708
|
async generateObject(request) {
|
|
459
|
-
const
|
|
460
|
-
|
|
709
|
+
const route = createStandardRoute(request, {
|
|
710
|
+
getSkipReason: (model) => {
|
|
711
|
+
const message = objectCapabilitySkipReason(model, request);
|
|
712
|
+
return message ? { reasonCode: "operation-skip", message } : undefined;
|
|
713
|
+
}
|
|
714
|
+
});
|
|
715
|
+
const result = await generateObject({
|
|
716
|
+
...createTextOptions(route.model, request),
|
|
461
717
|
schema: request.schema,
|
|
462
718
|
mode: request.mode,
|
|
463
719
|
schemaName: request.schemaName,
|
|
464
720
|
schemaDescription: request.schemaDescription
|
|
465
|
-
})
|
|
466
|
-
return enrichObjectResult(request,
|
|
721
|
+
});
|
|
722
|
+
return enrichObjectResult(request, targetForResult(route.context), route.context.attempts, route.context.routeDecision, route.context.startedAt, result);
|
|
467
723
|
},
|
|
468
724
|
streamObject(request) {
|
|
469
|
-
const
|
|
470
|
-
|
|
725
|
+
const route = createStandardRoute(request, {
|
|
726
|
+
extraRequiredCapabilities: { streaming: true },
|
|
727
|
+
getSkipReason: (model) => {
|
|
728
|
+
const message = objectCapabilitySkipReason(model, request);
|
|
729
|
+
return message ? { reasonCode: "operation-skip", message } : undefined;
|
|
730
|
+
}
|
|
731
|
+
});
|
|
732
|
+
const streamResult = streamObject({
|
|
733
|
+
...createTextOptions(route.model, request),
|
|
471
734
|
schema: request.schema,
|
|
472
735
|
mode: request.mode,
|
|
473
736
|
schemaName: request.schemaName,
|
|
474
737
|
schemaDescription: request.schemaDescription
|
|
475
|
-
})
|
|
476
|
-
let relayPromise;
|
|
477
|
-
const ensureRelay = async () => {
|
|
478
|
-
if (!relayPromise) {
|
|
479
|
-
relayPromise = selected.then((routed) => ({
|
|
480
|
-
streamResult: routed.streamResult,
|
|
481
|
-
collect: async () => enrichObjectResult(request, routed.target, routed.attempts, routed.routeDecision, routed.startedAt, await routed.streamResult.collect())
|
|
482
|
-
}));
|
|
483
|
-
}
|
|
484
|
-
return relayPromise;
|
|
485
|
-
};
|
|
738
|
+
});
|
|
486
739
|
return {
|
|
487
|
-
eventStream:
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
const relay = await ensureRelay();
|
|
495
|
-
for await (const partial of relay.streamResult.partialObjectStream) {
|
|
496
|
-
yield partial;
|
|
497
|
-
}
|
|
498
|
-
})(),
|
|
499
|
-
textStream: (async function* () {
|
|
500
|
-
const relay = await ensureRelay();
|
|
501
|
-
for await (const chunk of relay.streamResult.textStream) {
|
|
502
|
-
yield chunk;
|
|
503
|
-
}
|
|
504
|
-
})(),
|
|
505
|
-
collect: async () => (await ensureRelay()).collect()
|
|
740
|
+
eventStream: streamResult.eventStream,
|
|
741
|
+
partialObjectStream: streamResult.partialObjectStream,
|
|
742
|
+
textStream: streamResult.textStream,
|
|
743
|
+
collect: async () => {
|
|
744
|
+
const result = await streamResult.collect();
|
|
745
|
+
return enrichObjectResult(request, targetForResult(route.context), route.context.attempts, route.context.routeDecision, route.context.startedAt, result);
|
|
746
|
+
}
|
|
506
747
|
};
|
|
507
748
|
},
|
|
508
749
|
async runAgent(request) {
|
|
509
|
-
const
|
|
510
|
-
|
|
511
|
-
|
|
750
|
+
const route = createStandardRoute(request, {
|
|
751
|
+
defaultIntent: "tool-heavy",
|
|
752
|
+
getSkipReason: (model) => supportsRequiredAgentCapabilities(model, request.requiredAgentCapabilities)
|
|
753
|
+
? undefined
|
|
754
|
+
: {
|
|
755
|
+
reasonCode: "agent-capabilities",
|
|
756
|
+
message: "Skipped because agent capabilities do not satisfy the request."
|
|
757
|
+
},
|
|
758
|
+
onWinner: async (candidate, attempts) => {
|
|
759
|
+
await config.onAgentRoute?.({
|
|
760
|
+
provider: candidate.target.provider,
|
|
761
|
+
modelId: candidate.target.modelId,
|
|
762
|
+
routeDecision: route.context.routeDecision,
|
|
763
|
+
attempts,
|
|
764
|
+
targetRank: candidate.targetRank
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
});
|
|
512
768
|
const agent = createAgent({
|
|
513
769
|
id: request.agentId,
|
|
514
|
-
model,
|
|
770
|
+
model: route.model,
|
|
515
771
|
instructions: request.instructions,
|
|
516
772
|
tools: request.tools,
|
|
517
773
|
maxSteps: request.maxSteps,
|
|
@@ -519,78 +775,65 @@ export const createGateway = (config) => {
|
|
|
519
775
|
maxTokens: request.maxTokens,
|
|
520
776
|
reasoning: request.reasoning,
|
|
521
777
|
toolExecution: request.toolExecution,
|
|
778
|
+
toolApprovalPolicy: request.toolApprovalPolicy,
|
|
522
779
|
providerOptions: request.providerOptions,
|
|
780
|
+
policy: request.policy,
|
|
523
781
|
metadata: request.metadata,
|
|
524
782
|
store: request.store,
|
|
525
783
|
memory: request.memory,
|
|
526
|
-
onTelemetryEvent: request.onTelemetryEvent
|
|
784
|
+
onTelemetryEvent: request.onTelemetryEvent,
|
|
785
|
+
hookFailurePolicy: request.hookFailurePolicy
|
|
527
786
|
});
|
|
528
|
-
const result = await runAgent(agent,
|
|
529
|
-
|
|
530
|
-
});
|
|
531
|
-
return enrichAgentResult(selection.target, [
|
|
532
|
-
...selection.attempts,
|
|
533
|
-
createAttempt(selection.target, true, Date.now() - selection.startedAt, selection.targetRank, {
|
|
534
|
-
retry: 0,
|
|
535
|
-
reasonCode: "provider-success"
|
|
536
|
-
})
|
|
537
|
-
], selection.routeDecision, selection.startedAt, result);
|
|
787
|
+
const result = await runAgent(agent, createAgentRunInput(request));
|
|
788
|
+
return enrichAgentResult(targetForResult(route.context), route.context.attempts, route.context.routeDecision, route.context.startedAt, result);
|
|
538
789
|
},
|
|
539
790
|
streamAgent(request) {
|
|
540
|
-
const
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
toolExecution: request.toolExecution,
|
|
557
|
-
providerOptions: request.providerOptions,
|
|
558
|
-
metadata: request.metadata,
|
|
559
|
-
store: request.store,
|
|
560
|
-
memory: request.memory,
|
|
561
|
-
onTelemetryEvent: request.onTelemetryEvent
|
|
562
|
-
});
|
|
563
|
-
const streamResult = streamAgent(agent, {
|
|
564
|
-
...createAgentRunInput(request, selected.target)
|
|
565
|
-
});
|
|
566
|
-
return {
|
|
567
|
-
streamResult,
|
|
568
|
-
collect: async () => enrichAgentResult(selected.target, [
|
|
569
|
-
...selected.attempts,
|
|
570
|
-
createAttempt(selected.target, true, Date.now() - selected.startedAt, selected.targetRank, {
|
|
571
|
-
retry: 0,
|
|
572
|
-
reasonCode: "provider-success"
|
|
573
|
-
})
|
|
574
|
-
], selected.routeDecision, selected.startedAt, await streamResult.collect())
|
|
575
|
-
};
|
|
791
|
+
const route = createStandardRoute(request, {
|
|
792
|
+
defaultIntent: "tool-heavy",
|
|
793
|
+
extraRequiredCapabilities: { streaming: true },
|
|
794
|
+
getSkipReason: (model) => supportsRequiredAgentCapabilities(model, request.requiredAgentCapabilities)
|
|
795
|
+
? undefined
|
|
796
|
+
: {
|
|
797
|
+
reasonCode: "agent-capabilities",
|
|
798
|
+
message: "Skipped because agent capabilities do not satisfy the request."
|
|
799
|
+
},
|
|
800
|
+
onWinner: async (candidate, attempts) => {
|
|
801
|
+
await config.onAgentRoute?.({
|
|
802
|
+
provider: candidate.target.provider,
|
|
803
|
+
modelId: candidate.target.modelId,
|
|
804
|
+
routeDecision: route.context.routeDecision,
|
|
805
|
+
attempts,
|
|
806
|
+
targetRank: candidate.targetRank
|
|
576
807
|
});
|
|
577
808
|
}
|
|
578
|
-
|
|
579
|
-
|
|
809
|
+
});
|
|
810
|
+
const agent = createAgent({
|
|
811
|
+
id: request.agentId,
|
|
812
|
+
model: route.model,
|
|
813
|
+
instructions: request.instructions,
|
|
814
|
+
tools: request.tools,
|
|
815
|
+
maxSteps: request.maxSteps,
|
|
816
|
+
temperature: request.temperature,
|
|
817
|
+
maxTokens: request.maxTokens,
|
|
818
|
+
reasoning: request.reasoning,
|
|
819
|
+
toolExecution: request.toolExecution,
|
|
820
|
+
toolApprovalPolicy: request.toolApprovalPolicy,
|
|
821
|
+
providerOptions: request.providerOptions,
|
|
822
|
+
policy: request.policy,
|
|
823
|
+
metadata: request.metadata,
|
|
824
|
+
store: request.store,
|
|
825
|
+
memory: request.memory,
|
|
826
|
+
onTelemetryEvent: request.onTelemetryEvent,
|
|
827
|
+
hookFailurePolicy: request.hookFailurePolicy
|
|
828
|
+
});
|
|
829
|
+
const streamResult = streamAgent(agent, createAgentRunInput(request));
|
|
580
830
|
return {
|
|
581
|
-
eventStream:
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
}
|
|
587
|
-
textStream: (async function* () {
|
|
588
|
-
const relay = await ensureRelay();
|
|
589
|
-
for await (const chunk of relay.streamResult.textStream) {
|
|
590
|
-
yield chunk;
|
|
591
|
-
}
|
|
592
|
-
})(),
|
|
593
|
-
collect: async () => (await ensureRelay()).collect()
|
|
831
|
+
eventStream: streamResult.eventStream,
|
|
832
|
+
textStream: streamResult.textStream,
|
|
833
|
+
collect: async () => {
|
|
834
|
+
const result = await streamResult.collect();
|
|
835
|
+
return enrichAgentResult(targetForResult(route.context), route.context.attempts, route.context.routeDecision, route.context.startedAt, result);
|
|
836
|
+
}
|
|
594
837
|
};
|
|
595
838
|
}
|
|
596
839
|
};
|