@zhivex-ai/gateway 0.3.1-next.0 → 0.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 +3 -36
- package/dist/compat.d.ts +2 -1
- package/dist/compat.d.ts.map +1 -1
- package/dist/compat.js +6 -0
- package/dist/compat.js.map +1 -1
- package/dist/index.d.ts +3 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +443 -155
- package/dist/index.js.map +1 -1
- package/dist/runtime-state.d.ts +1 -1
- package/dist/types.d.ts +49 -194
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +0 -10
- package/dist/types.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,13 +1,96 @@
|
|
|
1
|
-
import { generateObject, generateText, streamObject, streamText } from "@zhivex-ai/core";
|
|
2
|
-
import {
|
|
3
|
-
import { gatewayMessagesToModelMessages, stripImagesForUnsupportedModel } from "./compat.js";
|
|
4
|
-
import { createDefaultRetryPolicy, createDefaultRouterPolicy } from "./policies.js";
|
|
5
|
-
import { createInMemoryGatewayRuntimeStore, gatewayTargetKey } from "./runtime-state.js";
|
|
1
|
+
import { createAgent, generateObject, runAgent, generateText, streamAgent, streamObject, streamText } from "@zhivex-ai/core";
|
|
2
|
+
import { createRouteDecision, gatewayMessagesToModelMessages, stripImagesForUnsupportedModel } from "./compat.js";
|
|
6
3
|
import { GatewayError } from "./types.js";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
4
|
+
const scoreTarget = (mode, intent, target, config) => {
|
|
5
|
+
const model = target.modelId.toLowerCase();
|
|
6
|
+
const localBoost = target.provider === "ollama" ? -2 : 0;
|
|
7
|
+
const qualityBoost = model.includes("pro") || model.includes("claude") ? 2 : 0;
|
|
8
|
+
const speedBoost = model.includes("flash") || model.includes("lite") ? 2 : 0;
|
|
9
|
+
const reasoningBoost = model.includes("pro") || model.includes("claude") ? 2 : 0;
|
|
10
|
+
const catalogCost = config.modelCatalog?.find(target.provider, target.modelId)?.costPer1kTokens;
|
|
11
|
+
const costPenalty = config.providerCostsPer1kTokens?.[target.provider] ?? catalogCost ?? 0;
|
|
12
|
+
const latencyPenalty = (config.latencyBiasMs?.[target.provider] ?? 0) / 100;
|
|
13
|
+
if (mode === "speed") {
|
|
14
|
+
return speedBoost + localBoost - latencyPenalty;
|
|
15
|
+
}
|
|
16
|
+
if (mode === "quality") {
|
|
17
|
+
return qualityBoost + (intent === "reasoning" ? reasoningBoost : 0) - costPenalty;
|
|
18
|
+
}
|
|
19
|
+
return speedBoost + qualityBoost + localBoost + (intent === "reasoning" ? 1 : 0) - costPenalty - latencyPenalty;
|
|
20
|
+
};
|
|
21
|
+
const orderTargets = (mode, intent, primary, fallbacks, config) => [primary, ...fallbacks]
|
|
22
|
+
.filter((target, index, list) => list.findIndex((candidate) => candidate.provider === target.provider && candidate.modelId === target.modelId) === index)
|
|
23
|
+
.sort((left, right) => scoreTarget(mode, intent, right, config) - scoreTarget(mode, intent, left, config));
|
|
24
|
+
const supportsRequiredCapabilities = (adapter, target, requiredCapabilities) => {
|
|
25
|
+
if (!requiredCapabilities) {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
const capabilities = adapter.languageModel(target.modelId).capabilities;
|
|
29
|
+
return Object.entries(requiredCapabilities).every(([key, required]) => required !== true || capabilities[key] === true);
|
|
30
|
+
};
|
|
31
|
+
const agentTierRank = (tier) => tier === "tier-a" ? 3 : tier === "tier-b" ? 2 : tier === "tier-c" ? 1 : 0;
|
|
32
|
+
const supportsRequiredAgentCapabilities = (adapter, target, requiredAgentCapabilities) => {
|
|
33
|
+
if (!requiredAgentCapabilities) {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
const capabilities = adapter.languageModel(target.modelId).capabilities.agentCapabilities;
|
|
37
|
+
if (!capabilities) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
return Object.entries(requiredAgentCapabilities).every(([key, value]) => {
|
|
41
|
+
if (value == null) {
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
if (key === "supportTier") {
|
|
45
|
+
return agentTierRank(capabilities.supportTier) >= agentTierRank(value);
|
|
46
|
+
}
|
|
47
|
+
return value !== true || capabilities[key] === true;
|
|
48
|
+
});
|
|
49
|
+
};
|
|
50
|
+
const withinCostBudget = (config, request, target) => {
|
|
51
|
+
if (request.maxCostPer1kTokens == null) {
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
const configuredCost = config.providerCostsPer1kTokens?.[target.provider];
|
|
55
|
+
const catalogCost = config.modelCatalog?.find(target.provider, target.modelId)?.costPer1kTokens;
|
|
56
|
+
const effectiveCost = configuredCost ?? catalogCost;
|
|
57
|
+
if (effectiveCost == null) {
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
return effectiveCost <= request.maxCostPer1kTokens;
|
|
61
|
+
};
|
|
10
62
|
const estimateTokens = (text) => Math.max(1, Math.ceil(text.trim().length / 4));
|
|
63
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
64
|
+
const withTimeout = async (promise, timeoutMs) => new Promise((resolve, reject) => {
|
|
65
|
+
const timer = setTimeout(() => reject(new GatewayError(`Provider timed out after ${timeoutMs}ms.`, true)), timeoutMs);
|
|
66
|
+
promise
|
|
67
|
+
.then((value) => {
|
|
68
|
+
clearTimeout(timer);
|
|
69
|
+
resolve(value);
|
|
70
|
+
})
|
|
71
|
+
.catch((error) => {
|
|
72
|
+
clearTimeout(timer);
|
|
73
|
+
reject(error);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
const normalizeError = (error) => {
|
|
77
|
+
if (error instanceof GatewayError) {
|
|
78
|
+
return error;
|
|
79
|
+
}
|
|
80
|
+
if (error instanceof Error) {
|
|
81
|
+
const message = error.message.toLowerCase();
|
|
82
|
+
if (message.includes("timed out") || message.includes("429") || message.includes("rate")) {
|
|
83
|
+
return new GatewayError(error.message, true);
|
|
84
|
+
}
|
|
85
|
+
if (message.includes("connect") || message.includes("econnrefused") || message.includes("503")) {
|
|
86
|
+
return new GatewayError(error.message, true);
|
|
87
|
+
}
|
|
88
|
+
return new GatewayError(error.message, false);
|
|
89
|
+
}
|
|
90
|
+
return new GatewayError("Unknown gateway error.", false);
|
|
91
|
+
};
|
|
92
|
+
const getAttemptTimeoutMs = (config, provider) => config.attemptTimeoutsMs?.[provider] ?? config.attemptTimeoutMs ?? 20_000;
|
|
93
|
+
const retryBackoffMs = (config, retry) => (config.retryBackoffMs ?? 200) * (retry + 1);
|
|
11
94
|
const normalizeUsage = (usage, inputText, outputText) => {
|
|
12
95
|
const inputTokens = usage?.inputTokens ?? estimateTokens(inputText);
|
|
13
96
|
const outputTokens = usage?.outputTokens ?? estimateTokens(outputText);
|
|
@@ -19,17 +102,14 @@ const normalizeUsage = (usage, inputText, outputText) => {
|
|
|
19
102
|
estimated: usage?.inputTokens == null || usage?.outputTokens == null || usage?.totalTokens == null
|
|
20
103
|
};
|
|
21
104
|
};
|
|
105
|
+
const requestMessages = (request) => gatewayMessagesToModelMessages(stripImagesForUnsupportedModel(request.messages, request.primary.provider, request.primary.modelId), request.systemPrompt);
|
|
22
106
|
const getInputText = (request) => `${request.systemPrompt ?? ""}\n${request.messages.map((message) => message.content).join("\n")}`.trim();
|
|
23
|
-
const dedupeTargets = (primary, fallbacks = []) => [primary, ...fallbacks].filter((target, index, list) => list.findIndex((candidate) => candidate.provider === target.provider && candidate.modelId === target.modelId) === index);
|
|
24
107
|
const buildRequiredCapabilities = (request, extra = {}) => ({
|
|
25
108
|
...(request.requiredCapabilities ?? {}),
|
|
26
109
|
...(request.tools ? { tools: true } : {}),
|
|
27
110
|
...(request.reasoning ? { reasoning: true } : {}),
|
|
28
|
-
...(request.stepType === "grounded" ? { webSearch: true } : {}),
|
|
29
111
|
...extra
|
|
30
112
|
});
|
|
31
|
-
const getTaskIntent = (request) => request.taskIntent ??
|
|
32
|
-
(request.stepType === "plan" ? "reasoning" : request.stepType === "act" ? "tool-heavy" : "chat");
|
|
33
113
|
const createTextOptions = (adapter, target, request) => ({
|
|
34
114
|
model: adapter.languageModel(target.modelId),
|
|
35
115
|
messages: gatewayMessagesToModelMessages(stripImagesForUnsupportedModel(request.messages, target.provider, target.modelId), request.systemPrompt),
|
|
@@ -43,176 +123,308 @@ const createTextOptions = (adapter, target, request) => ({
|
|
|
43
123
|
providerOptions: request.providerOptions,
|
|
44
124
|
abortSignal: request.abortSignal
|
|
45
125
|
});
|
|
46
|
-
const enrichTextResult = (request, target, attempts, routeDecision, startedAt, result
|
|
126
|
+
const enrichTextResult = (request, target, attempts, routeDecision, startedAt, result) => ({
|
|
47
127
|
...result,
|
|
48
128
|
providerUsed: target.provider,
|
|
49
129
|
modelUsed: target.modelId,
|
|
50
|
-
latencyMs:
|
|
130
|
+
latencyMs: Date.now() - startedAt,
|
|
51
131
|
attempts,
|
|
52
132
|
usage: normalizeUsage(result.usage, getInputText(request), result.text),
|
|
53
133
|
routeDecision
|
|
54
134
|
});
|
|
55
|
-
const enrichObjectResult = (request, target, attempts, routeDecision, startedAt, result
|
|
135
|
+
const enrichObjectResult = (request, target, attempts, routeDecision, startedAt, result) => ({
|
|
56
136
|
...result,
|
|
57
137
|
providerUsed: target.provider,
|
|
58
138
|
modelUsed: target.modelId,
|
|
59
|
-
latencyMs:
|
|
139
|
+
latencyMs: Date.now() - startedAt,
|
|
60
140
|
attempts,
|
|
61
141
|
usage: normalizeUsage(result.usage, getInputText(request), result.text),
|
|
62
142
|
routeDecision
|
|
63
143
|
});
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
yield next.value;
|
|
76
|
-
}
|
|
77
|
-
})();
|
|
144
|
+
const createAgentMessages = (request, target) => request.messages
|
|
145
|
+
? gatewayMessagesToModelMessages(stripImagesForUnsupportedModel(request.messages, target.provider, target.modelId), undefined)
|
|
146
|
+
: undefined;
|
|
147
|
+
const createAgentRunInput = (request, target) => {
|
|
148
|
+
const source = request.prompt !== undefined
|
|
149
|
+
? { prompt: request.prompt }
|
|
150
|
+
: request.messages
|
|
151
|
+
? { messages: createAgentMessages(request, target) }
|
|
152
|
+
: {};
|
|
78
153
|
return {
|
|
79
|
-
...
|
|
80
|
-
|
|
154
|
+
...source,
|
|
155
|
+
system: request.system,
|
|
156
|
+
state: request.state,
|
|
157
|
+
approvals: request.approvals,
|
|
158
|
+
handoff: request.handoff,
|
|
159
|
+
tools: request.tools,
|
|
160
|
+
toolChoice: request.toolChoice,
|
|
161
|
+
toolExecution: request.toolExecution,
|
|
162
|
+
maxSteps: request.maxSteps,
|
|
163
|
+
temperature: request.temperature,
|
|
164
|
+
maxTokens: request.maxTokens,
|
|
165
|
+
reasoning: request.reasoning,
|
|
166
|
+
providerOptions: request.providerOptions,
|
|
167
|
+
abortSignal: request.abortSignal
|
|
81
168
|
};
|
|
82
169
|
};
|
|
170
|
+
const enrichAgentResult = (target, attempts, routeDecision, startedAt, result) => ({
|
|
171
|
+
...result,
|
|
172
|
+
providerUsed: target.provider,
|
|
173
|
+
modelUsed: target.modelId,
|
|
174
|
+
latencyMs: Date.now() - startedAt,
|
|
175
|
+
attempts,
|
|
176
|
+
routeDecision,
|
|
177
|
+
state: {
|
|
178
|
+
...result.state,
|
|
179
|
+
routeDecision
|
|
180
|
+
}
|
|
181
|
+
});
|
|
83
182
|
export const createGateway = (config) => {
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
183
|
+
const getAdapter = (provider) => {
|
|
184
|
+
const adapter = config.adapters[provider];
|
|
185
|
+
if (!adapter) {
|
|
186
|
+
throw new GatewayError(`No adapter registered for provider "${provider}".`, false);
|
|
187
|
+
}
|
|
188
|
+
return adapter;
|
|
90
189
|
};
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
190
|
+
const routeDecisionFor = (request) => {
|
|
191
|
+
const mode = request.routingMode ?? "balanced";
|
|
192
|
+
const intent = request.taskIntent ?? "chat";
|
|
193
|
+
const orderedTargets = orderTargets(mode, intent, request.primary, request.fallbacks ?? [], config);
|
|
194
|
+
return {
|
|
195
|
+
mode,
|
|
196
|
+
intent,
|
|
197
|
+
orderedTargets,
|
|
198
|
+
routeDecision: createRouteDecision(mode, intent, orderedTargets)
|
|
96
199
|
};
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
200
|
+
};
|
|
201
|
+
const selectAgentTarget = async (request) => {
|
|
202
|
+
const mode = request.routingMode ?? "balanced";
|
|
203
|
+
const intent = request.taskIntent ?? "tool-heavy";
|
|
204
|
+
const orderedTargets = orderTargets(mode, intent, request.primary, request.fallbacks ?? [], config);
|
|
205
|
+
const routeDecision = createRouteDecision(mode, intent, orderedTargets);
|
|
206
|
+
const attempts = [];
|
|
207
|
+
for (const target of orderedTargets) {
|
|
208
|
+
const adapter = getAdapter(target.provider);
|
|
209
|
+
if (!supportsRequiredCapabilities(adapter, target, buildRequiredCapabilities(request))) {
|
|
210
|
+
attempts.push({
|
|
211
|
+
provider: target.provider,
|
|
212
|
+
modelId: target.modelId,
|
|
213
|
+
ok: false,
|
|
214
|
+
latencyMs: 0,
|
|
215
|
+
errorMessage: "Skipped because model capabilities do not satisfy the request."
|
|
216
|
+
});
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if (!supportsRequiredAgentCapabilities(adapter, target, request.requiredAgentCapabilities)) {
|
|
220
|
+
attempts.push({
|
|
221
|
+
provider: target.provider,
|
|
222
|
+
modelId: target.modelId,
|
|
223
|
+
ok: false,
|
|
224
|
+
latencyMs: 0,
|
|
225
|
+
errorMessage: "Skipped because agent capabilities do not satisfy the request."
|
|
226
|
+
});
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
if (!withinCostBudget(config, request, target)) {
|
|
230
|
+
attempts.push({
|
|
231
|
+
provider: target.provider,
|
|
232
|
+
modelId: target.modelId,
|
|
233
|
+
ok: false,
|
|
234
|
+
latencyMs: 0,
|
|
235
|
+
errorMessage: "Skipped because provider cost exceeds the configured budget."
|
|
236
|
+
});
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
await config.onAgentRoute?.({
|
|
240
|
+
provider: target.provider,
|
|
241
|
+
modelId: target.modelId,
|
|
242
|
+
routeDecision
|
|
243
|
+
});
|
|
244
|
+
return {
|
|
245
|
+
target,
|
|
246
|
+
attempts,
|
|
247
|
+
routeDecision,
|
|
248
|
+
startedAt: Date.now()
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
throw new GatewayError(attempts.at(-1)?.errorMessage ?? "No gateway agent target satisfied the request.", false);
|
|
117
252
|
};
|
|
118
253
|
const runGenerate = async (request, operation, extraRequiredCapabilities = {}) => {
|
|
119
|
-
const
|
|
120
|
-
const
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
254
|
+
const attempts = [];
|
|
255
|
+
const startedAt = Date.now();
|
|
256
|
+
const { orderedTargets, routeDecision } = routeDecisionFor(request);
|
|
257
|
+
const maxRetries = Math.max(0, config.maxRetries ?? 2);
|
|
258
|
+
const requiredCapabilities = buildRequiredCapabilities(request, extraRequiredCapabilities);
|
|
259
|
+
for (const [targetIndex, target] of orderedTargets.entries()) {
|
|
260
|
+
const adapter = getAdapter(target.provider);
|
|
261
|
+
if (!supportsRequiredCapabilities(adapter, target, requiredCapabilities)) {
|
|
262
|
+
attempts.push({
|
|
263
|
+
provider: target.provider,
|
|
264
|
+
modelId: target.modelId,
|
|
265
|
+
ok: false,
|
|
266
|
+
latencyMs: 0,
|
|
267
|
+
errorMessage: "Skipped because model capabilities do not satisfy the request."
|
|
268
|
+
});
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
if (!withinCostBudget(config, request, target)) {
|
|
272
|
+
attempts.push({
|
|
273
|
+
provider: target.provider,
|
|
274
|
+
modelId: target.modelId,
|
|
275
|
+
ok: false,
|
|
276
|
+
latencyMs: 0,
|
|
277
|
+
errorMessage: "Skipped because provider cost exceeds the configured budget."
|
|
278
|
+
});
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
for (let retry = 0; retry <= maxRetries; retry += 1) {
|
|
282
|
+
const attemptStartedAt = Date.now();
|
|
283
|
+
try {
|
|
284
|
+
await config.onAttempt?.({
|
|
285
|
+
provider: target.provider,
|
|
286
|
+
modelId: target.modelId,
|
|
287
|
+
ok: true,
|
|
288
|
+
latencyMs: 0,
|
|
289
|
+
retry,
|
|
290
|
+
targetRank: targetIndex
|
|
291
|
+
});
|
|
292
|
+
const result = await withTimeout(operation(adapter, target), getAttemptTimeoutMs(config, target.provider));
|
|
293
|
+
attempts.push({
|
|
294
|
+
provider: target.provider,
|
|
295
|
+
modelId: target.modelId,
|
|
296
|
+
ok: true,
|
|
297
|
+
latencyMs: Date.now() - attemptStartedAt
|
|
298
|
+
});
|
|
299
|
+
return {
|
|
300
|
+
attempts,
|
|
301
|
+
target,
|
|
302
|
+
startedAt,
|
|
303
|
+
routeDecision,
|
|
304
|
+
result
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
catch (error) {
|
|
308
|
+
const normalized = normalizeError(error);
|
|
309
|
+
attempts.push({
|
|
310
|
+
provider: target.provider,
|
|
311
|
+
modelId: target.modelId,
|
|
312
|
+
ok: false,
|
|
313
|
+
latencyMs: Date.now() - attemptStartedAt,
|
|
314
|
+
errorMessage: normalized.message
|
|
315
|
+
});
|
|
316
|
+
await config.onAttempt?.({
|
|
317
|
+
provider: target.provider,
|
|
318
|
+
modelId: target.modelId,
|
|
319
|
+
ok: false,
|
|
320
|
+
latencyMs: Date.now() - attemptStartedAt,
|
|
321
|
+
errorMessage: normalized.message,
|
|
322
|
+
retry,
|
|
323
|
+
targetRank: targetIndex
|
|
324
|
+
});
|
|
325
|
+
if (retry < maxRetries && normalized.retryable) {
|
|
326
|
+
await sleep(retryBackoffMs(config, retry));
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
break;
|
|
134
330
|
}
|
|
135
|
-
return operation(adapter, target);
|
|
136
331
|
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
startedAt,
|
|
141
|
-
routeDecision
|
|
142
|
-
};
|
|
332
|
+
}
|
|
333
|
+
const finalError = attempts.at(-1)?.errorMessage ?? "All gateway attempts failed.";
|
|
334
|
+
throw new GatewayError(finalError, false);
|
|
143
335
|
};
|
|
144
336
|
const runStream = async (request, operation, extraRequiredCapabilities = {}) => {
|
|
145
|
-
const
|
|
146
|
-
const
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
337
|
+
const attempts = [];
|
|
338
|
+
const startedAt = Date.now();
|
|
339
|
+
const { orderedTargets, routeDecision } = routeDecisionFor(request);
|
|
340
|
+
const maxRetries = Math.max(0, config.maxRetries ?? 2);
|
|
341
|
+
const requiredCapabilities = buildRequiredCapabilities(request, { streaming: true, ...extraRequiredCapabilities });
|
|
342
|
+
for (const [targetIndex, target] of orderedTargets.entries()) {
|
|
343
|
+
const adapter = getAdapter(target.provider);
|
|
344
|
+
if (!supportsRequiredCapabilities(adapter, target, requiredCapabilities)) {
|
|
345
|
+
attempts.push({
|
|
346
|
+
provider: target.provider,
|
|
347
|
+
modelId: target.modelId,
|
|
348
|
+
ok: false,
|
|
349
|
+
latencyMs: 0,
|
|
350
|
+
errorMessage: "Skipped because model capabilities do not satisfy the request."
|
|
351
|
+
});
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
if (!withinCostBudget(config, request, target)) {
|
|
355
|
+
attempts.push({
|
|
356
|
+
provider: target.provider,
|
|
357
|
+
modelId: target.modelId,
|
|
358
|
+
ok: false,
|
|
359
|
+
latencyMs: 0,
|
|
360
|
+
errorMessage: "Skipped because provider cost exceeds the configured budget."
|
|
361
|
+
});
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
for (let retry = 0; retry <= maxRetries; retry += 1) {
|
|
365
|
+
const attemptStartedAt = Date.now();
|
|
162
366
|
try {
|
|
367
|
+
await config.onAttempt?.({
|
|
368
|
+
provider: target.provider,
|
|
369
|
+
modelId: target.modelId,
|
|
370
|
+
ok: true,
|
|
371
|
+
latencyMs: 0,
|
|
372
|
+
retry,
|
|
373
|
+
targetRank: targetIndex
|
|
374
|
+
});
|
|
375
|
+
const streamResult = operation(adapter, target);
|
|
163
376
|
const iterator = streamResult.eventStream[Symbol.asyncIterator]();
|
|
164
|
-
const first = await iterator.next();
|
|
377
|
+
const first = await withTimeout(iterator.next(), getAttemptTimeoutMs(config, target.provider));
|
|
165
378
|
if (!first.done && first.value.type === "error") {
|
|
166
|
-
await streamResult.collect().catch(() => undefined);
|
|
167
379
|
throw first.value.error;
|
|
168
380
|
}
|
|
169
|
-
|
|
381
|
+
attempts.push({
|
|
382
|
+
provider: target.provider,
|
|
383
|
+
modelId: target.modelId,
|
|
384
|
+
ok: true,
|
|
385
|
+
latencyMs: Date.now() - attemptStartedAt
|
|
386
|
+
});
|
|
387
|
+
return {
|
|
388
|
+
attempts,
|
|
389
|
+
target,
|
|
390
|
+
startedAt,
|
|
391
|
+
routeDecision,
|
|
392
|
+
streamResult
|
|
393
|
+
};
|
|
170
394
|
}
|
|
171
395
|
catch (error) {
|
|
172
|
-
|
|
173
|
-
|
|
396
|
+
const normalized = normalizeError(error);
|
|
397
|
+
attempts.push({
|
|
398
|
+
provider: target.provider,
|
|
399
|
+
modelId: target.modelId,
|
|
400
|
+
ok: false,
|
|
401
|
+
latencyMs: Date.now() - attemptStartedAt,
|
|
402
|
+
errorMessage: normalized.message
|
|
403
|
+
});
|
|
404
|
+
await config.onAttempt?.({
|
|
405
|
+
provider: target.provider,
|
|
406
|
+
modelId: target.modelId,
|
|
407
|
+
ok: false,
|
|
408
|
+
latencyMs: Date.now() - attemptStartedAt,
|
|
409
|
+
errorMessage: normalized.message,
|
|
410
|
+
retry,
|
|
411
|
+
targetRank: targetIndex
|
|
412
|
+
});
|
|
413
|
+
if (retry < maxRetries && normalized.retryable) {
|
|
414
|
+
await sleep(retryBackoffMs(config, retry));
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
break;
|
|
174
418
|
}
|
|
175
419
|
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
startedAt,
|
|
180
|
-
routeDecision
|
|
181
|
-
};
|
|
420
|
+
}
|
|
421
|
+
const finalError = attempts.at(-1)?.errorMessage ?? "All gateway attempts failed.";
|
|
422
|
+
throw new GatewayError(finalError, false);
|
|
182
423
|
};
|
|
183
424
|
return {
|
|
184
|
-
async resolveModel(request) {
|
|
185
|
-
const routeDecision = await planRoute(request);
|
|
186
|
-
const target = routeDecision.orderedTargets[0];
|
|
187
|
-
if (!target) {
|
|
188
|
-
throw new GatewayError("No available gateway targets matched the request.", false);
|
|
189
|
-
}
|
|
190
|
-
const adapter = getAdapter(target.provider);
|
|
191
|
-
if (!adapter) {
|
|
192
|
-
throw new GatewayError(`No adapter registered for provider "${target.provider}".`, false);
|
|
193
|
-
}
|
|
194
|
-
if (request.stepType === "grounded") {
|
|
195
|
-
const groundedModel = adapter.groundedLanguageModel?.(target.modelId);
|
|
196
|
-
if (!groundedModel) {
|
|
197
|
-
throw new GatewayError(`Provider "${target.provider}" does not expose grounded models.`, false);
|
|
198
|
-
}
|
|
199
|
-
return {
|
|
200
|
-
target,
|
|
201
|
-
routeDecision,
|
|
202
|
-
model: groundedModel,
|
|
203
|
-
kind: "grounded"
|
|
204
|
-
};
|
|
205
|
-
}
|
|
206
|
-
return {
|
|
207
|
-
target,
|
|
208
|
-
routeDecision,
|
|
209
|
-
model: adapter.languageModel(target.modelId),
|
|
210
|
-
kind: "language"
|
|
211
|
-
};
|
|
212
|
-
},
|
|
213
425
|
async generate(request) {
|
|
214
426
|
const routed = await runGenerate(request, (adapter, target) => generateText(createTextOptions(adapter, target, request)));
|
|
215
|
-
return enrichTextResult(request, routed.target, routed.attempts, routed.routeDecision, routed.startedAt, routed.result
|
|
427
|
+
return enrichTextResult(request, routed.target, routed.attempts, routed.routeDecision, routed.startedAt, routed.result);
|
|
216
428
|
},
|
|
217
429
|
streamText(request) {
|
|
218
430
|
const selected = runStream(request, (adapter, target) => streamText(createTextOptions(adapter, target, request)));
|
|
@@ -220,21 +432,21 @@ export const createGateway = (config) => {
|
|
|
220
432
|
const ensureRelay = async () => {
|
|
221
433
|
if (!relayPromise) {
|
|
222
434
|
relayPromise = selected.then((routed) => ({
|
|
223
|
-
collect: async () => enrichTextResult(request, routed.target, routed.attempts, routed.routeDecision, routed.startedAt, await routed.
|
|
435
|
+
collect: async () => enrichTextResult(request, routed.target, routed.attempts, routed.routeDecision, routed.startedAt, await routed.streamResult.collect())
|
|
224
436
|
}));
|
|
225
437
|
}
|
|
226
438
|
return relayPromise;
|
|
227
439
|
};
|
|
228
440
|
return {
|
|
229
441
|
eventStream: (async function* () {
|
|
230
|
-
const {
|
|
231
|
-
for await (const event of
|
|
442
|
+
const { streamResult } = await selected;
|
|
443
|
+
for await (const event of streamResult.eventStream) {
|
|
232
444
|
yield event;
|
|
233
445
|
}
|
|
234
446
|
})(),
|
|
235
447
|
textStream: (async function* () {
|
|
236
|
-
const {
|
|
237
|
-
for await (const chunk of
|
|
448
|
+
const { streamResult } = await selected;
|
|
449
|
+
for await (const chunk of streamResult.textStream) {
|
|
238
450
|
yield chunk;
|
|
239
451
|
}
|
|
240
452
|
})(),
|
|
@@ -248,8 +460,8 @@ export const createGateway = (config) => {
|
|
|
248
460
|
mode: request.mode,
|
|
249
461
|
schemaName: request.schemaName,
|
|
250
462
|
schemaDescription: request.schemaDescription
|
|
251
|
-
})
|
|
252
|
-
return enrichObjectResult(request, routed.target, routed.attempts, routed.routeDecision, routed.startedAt, routed.result
|
|
463
|
+
}));
|
|
464
|
+
return enrichObjectResult(request, routed.target, routed.attempts, routed.routeDecision, routed.startedAt, routed.result);
|
|
253
465
|
},
|
|
254
466
|
streamObject(request) {
|
|
255
467
|
const selected = runStream(request, (adapter, target) => streamObject({
|
|
@@ -258,21 +470,21 @@ export const createGateway = (config) => {
|
|
|
258
470
|
mode: request.mode,
|
|
259
471
|
schemaName: request.schemaName,
|
|
260
472
|
schemaDescription: request.schemaDescription
|
|
261
|
-
})
|
|
473
|
+
}));
|
|
262
474
|
let relayPromise;
|
|
263
475
|
const ensureRelay = async () => {
|
|
264
476
|
if (!relayPromise) {
|
|
265
477
|
relayPromise = selected.then((routed) => ({
|
|
266
|
-
streamResult: routed.
|
|
267
|
-
collect: async () => enrichObjectResult(request, routed.target, routed.attempts, routed.routeDecision, routed.startedAt, await routed.
|
|
478
|
+
streamResult: routed.streamResult,
|
|
479
|
+
collect: async () => enrichObjectResult(request, routed.target, routed.attempts, routed.routeDecision, routed.startedAt, await routed.streamResult.collect())
|
|
268
480
|
}));
|
|
269
481
|
}
|
|
270
482
|
return relayPromise;
|
|
271
483
|
};
|
|
272
484
|
return {
|
|
273
485
|
eventStream: (async function* () {
|
|
274
|
-
const {
|
|
275
|
-
for await (const event of
|
|
486
|
+
const { streamResult } = await selected;
|
|
487
|
+
for await (const event of streamResult.eventStream) {
|
|
276
488
|
yield event;
|
|
277
489
|
}
|
|
278
490
|
})(),
|
|
@@ -290,6 +502,82 @@ export const createGateway = (config) => {
|
|
|
290
502
|
})(),
|
|
291
503
|
collect: async () => (await ensureRelay()).collect()
|
|
292
504
|
};
|
|
505
|
+
},
|
|
506
|
+
async runAgent(request) {
|
|
507
|
+
const selection = await selectAgentTarget(request);
|
|
508
|
+
const adapter = getAdapter(selection.target.provider);
|
|
509
|
+
const model = adapter.languageModel(selection.target.modelId);
|
|
510
|
+
const agent = createAgent({
|
|
511
|
+
id: request.agentId,
|
|
512
|
+
model,
|
|
513
|
+
instructions: request.instructions,
|
|
514
|
+
tools: request.tools,
|
|
515
|
+
maxSteps: request.maxSteps,
|
|
516
|
+
temperature: request.temperature,
|
|
517
|
+
maxTokens: request.maxTokens,
|
|
518
|
+
reasoning: request.reasoning,
|
|
519
|
+
toolExecution: request.toolExecution,
|
|
520
|
+
providerOptions: request.providerOptions,
|
|
521
|
+
metadata: request.metadata,
|
|
522
|
+
store: request.store,
|
|
523
|
+
memory: request.memory,
|
|
524
|
+
onTelemetryEvent: request.onTelemetryEvent
|
|
525
|
+
});
|
|
526
|
+
const result = await runAgent(agent, {
|
|
527
|
+
...createAgentRunInput(request, selection.target)
|
|
528
|
+
});
|
|
529
|
+
return enrichAgentResult(selection.target, selection.attempts, selection.routeDecision, selection.startedAt, result);
|
|
530
|
+
},
|
|
531
|
+
streamAgent(request) {
|
|
532
|
+
const selection = selectAgentTarget(request);
|
|
533
|
+
let relayPromise;
|
|
534
|
+
const ensureRelay = async () => {
|
|
535
|
+
if (!relayPromise) {
|
|
536
|
+
relayPromise = selection.then(async (selected) => {
|
|
537
|
+
const adapter = getAdapter(selected.target.provider);
|
|
538
|
+
const model = adapter.languageModel(selected.target.modelId);
|
|
539
|
+
const agent = createAgent({
|
|
540
|
+
id: request.agentId,
|
|
541
|
+
model,
|
|
542
|
+
instructions: request.instructions,
|
|
543
|
+
tools: request.tools,
|
|
544
|
+
maxSteps: request.maxSteps,
|
|
545
|
+
temperature: request.temperature,
|
|
546
|
+
maxTokens: request.maxTokens,
|
|
547
|
+
reasoning: request.reasoning,
|
|
548
|
+
toolExecution: request.toolExecution,
|
|
549
|
+
providerOptions: request.providerOptions,
|
|
550
|
+
metadata: request.metadata,
|
|
551
|
+
store: request.store,
|
|
552
|
+
memory: request.memory,
|
|
553
|
+
onTelemetryEvent: request.onTelemetryEvent
|
|
554
|
+
});
|
|
555
|
+
const streamResult = streamAgent(agent, {
|
|
556
|
+
...createAgentRunInput(request, selected.target)
|
|
557
|
+
});
|
|
558
|
+
return {
|
|
559
|
+
streamResult,
|
|
560
|
+
collect: async () => enrichAgentResult(selected.target, selected.attempts, selected.routeDecision, selected.startedAt, await streamResult.collect())
|
|
561
|
+
};
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
return relayPromise;
|
|
565
|
+
};
|
|
566
|
+
return {
|
|
567
|
+
eventStream: (async function* () {
|
|
568
|
+
const relay = await ensureRelay();
|
|
569
|
+
for await (const event of relay.streamResult.eventStream) {
|
|
570
|
+
yield event;
|
|
571
|
+
}
|
|
572
|
+
})(),
|
|
573
|
+
textStream: (async function* () {
|
|
574
|
+
const relay = await ensureRelay();
|
|
575
|
+
for await (const chunk of relay.streamResult.textStream) {
|
|
576
|
+
yield chunk;
|
|
577
|
+
}
|
|
578
|
+
})(),
|
|
579
|
+
collect: async () => (await ensureRelay()).collect()
|
|
580
|
+
};
|
|
293
581
|
}
|
|
294
582
|
};
|
|
295
583
|
};
|