@khanglvm/llm-router 1.3.1 → 2.0.0-beta.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/CHANGELOG.md +39 -0
- package/README.md +337 -41
- package/package.json +19 -3
- package/src/cli/router-module.js +7331 -3805
- package/src/cli/wrangler-toml.js +1 -1
- package/src/cli-entry.js +162 -24
- package/src/node/amp-client-config.js +426 -0
- package/src/node/coding-tool-config.js +763 -0
- package/src/node/config-store.js +49 -18
- package/src/node/instance-state.js +213 -12
- package/src/node/listen-port.js +5 -37
- package/src/node/local-server-settings.js +122 -0
- package/src/node/local-server.js +3 -2
- package/src/node/provider-probe.js +13 -0
- package/src/node/start-command.js +282 -40
- package/src/node/startup-manager.js +64 -29
- package/src/node/web-command.js +106 -0
- package/src/node/web-console-assets.js +26 -0
- package/src/node/web-console-client.js +56 -0
- package/src/node/web-console-dev-assets.js +258 -0
- package/src/node/web-console-server.js +3146 -0
- package/src/node/web-console-styles.generated.js +1 -0
- package/src/node/web-console-ui/config-editor-utils.js +616 -0
- package/src/node/web-console-ui/lib/utils.js +6 -0
- package/src/node/web-console-ui/rate-limit-utils.js +144 -0
- package/src/node/web-console-ui/select-search-utils.js +36 -0
- package/src/runtime/codex-request-transformer.js +46 -5
- package/src/runtime/codex-response-transformer.js +268 -35
- package/src/runtime/config.js +1394 -35
- package/src/runtime/handler/amp-gemini.js +913 -0
- package/src/runtime/handler/amp-response.js +308 -0
- package/src/runtime/handler/amp.js +290 -0
- package/src/runtime/handler/auth.js +17 -2
- package/src/runtime/handler/provider-call.js +168 -50
- package/src/runtime/handler/provider-translation.js +937 -26
- package/src/runtime/handler/request.js +149 -6
- package/src/runtime/handler/route-debug.js +22 -1
- package/src/runtime/handler.js +449 -9
- package/src/runtime/subscription-auth.js +1 -6
- package/src/shared/local-router-defaults.js +62 -0
- package/src/translator/index.js +3 -1
- package/src/translator/request/openai-to-claude.js +217 -6
- package/src/translator/response/openai-to-claude.js +206 -58
package/src/runtime/config.js
CHANGED
|
@@ -8,10 +8,12 @@ import {
|
|
|
8
8
|
CODEX_SUBSCRIPTION_MODELS,
|
|
9
9
|
CLAUDE_CODE_SUBSCRIPTION_MODELS
|
|
10
10
|
} from "./subscription-constants.js";
|
|
11
|
+
import { sanitizeRuntimeMetadata } from "../shared/local-router-defaults.js";
|
|
11
12
|
|
|
12
13
|
export const CONFIG_VERSION = 2;
|
|
13
14
|
export const MIN_SUPPORTED_CONFIG_VERSION = 1;
|
|
14
15
|
export const PROVIDER_ID_PATTERN = /^[a-z][a-z0-9-]*$/;
|
|
16
|
+
export const DEFAULT_MODEL_ALIAS_ID = "default";
|
|
15
17
|
const DEFAULT_PROVIDER_USER_AGENT_NAME = "AICodeClient";
|
|
16
18
|
const DEFAULT_PROVIDER_USER_AGENT_VERSION = "1.0.0";
|
|
17
19
|
export const DEFAULT_PROVIDER_USER_AGENT = buildDefaultProviderUserAgent();
|
|
@@ -65,6 +67,10 @@ function toArray(value) {
|
|
|
65
67
|
return [value];
|
|
66
68
|
}
|
|
67
69
|
|
|
70
|
+
function hasOwn(object, key) {
|
|
71
|
+
return Boolean(object) && Object.prototype.hasOwnProperty.call(object, key);
|
|
72
|
+
}
|
|
73
|
+
|
|
68
74
|
function dedupeStrings(values) {
|
|
69
75
|
const seen = new Set();
|
|
70
76
|
const result = [];
|
|
@@ -149,6 +155,586 @@ function sanitizeEndpointUrl(value) {
|
|
|
149
155
|
return parsed.toString();
|
|
150
156
|
}
|
|
151
157
|
|
|
158
|
+
function normalizeBooleanValue(value, fallback = false) {
|
|
159
|
+
if (value === undefined || value === null || value === "") return fallback;
|
|
160
|
+
if (typeof value === "boolean") return value;
|
|
161
|
+
const normalized = String(value).trim().toLowerCase();
|
|
162
|
+
if (["1", "true", "yes", "y", "on"].includes(normalized)) return true;
|
|
163
|
+
if (["0", "false", "no", "n", "off"].includes(normalized)) return false;
|
|
164
|
+
return fallback;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function normalizeAmpModelMappingEntry(entry) {
|
|
168
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) return null;
|
|
169
|
+
|
|
170
|
+
const from = String(
|
|
171
|
+
entry.from ??
|
|
172
|
+
entry.match ??
|
|
173
|
+
entry.pattern ??
|
|
174
|
+
entry.model ??
|
|
175
|
+
""
|
|
176
|
+
).trim();
|
|
177
|
+
const to = String(
|
|
178
|
+
entry.to ??
|
|
179
|
+
entry.target ??
|
|
180
|
+
entry.route ??
|
|
181
|
+
entry.ref ??
|
|
182
|
+
""
|
|
183
|
+
).trim();
|
|
184
|
+
|
|
185
|
+
if (!from || !to) return null;
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
from,
|
|
189
|
+
to
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function normalizeAmpIdentifierText(value) {
|
|
194
|
+
return String(value || "")
|
|
195
|
+
.trim()
|
|
196
|
+
.toLowerCase()
|
|
197
|
+
.replace(/[–—]+/g, "-")
|
|
198
|
+
.replace(/[\s_]+/g, "-")
|
|
199
|
+
.replace(/-+/g, "-")
|
|
200
|
+
.replace(/^-+|-+$/g, "");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function normalizeAmpVersionToken(value) {
|
|
204
|
+
const text = String(value || "")
|
|
205
|
+
.trim()
|
|
206
|
+
.toLowerCase()
|
|
207
|
+
.replace(/_/g, ".")
|
|
208
|
+
.replace(/\s+/g, "");
|
|
209
|
+
if (!text) return "";
|
|
210
|
+
if (/^\d+-\d+$/.test(text)) return text.replace(/-/g, ".");
|
|
211
|
+
return text;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function normalizeAmpSignatureKey(value) {
|
|
215
|
+
const text = normalizeAmpIdentifierText(String(value || "").replace(/^@+/, ""));
|
|
216
|
+
if (!text) return "";
|
|
217
|
+
return `@${text}`;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function normalizeAmpRouteKey(value) {
|
|
221
|
+
const text = String(value || "").trim();
|
|
222
|
+
if (!text) return "";
|
|
223
|
+
if (text.trim().startsWith("@")) return normalizeAmpSignatureKey(text);
|
|
224
|
+
return normalizeAmpSubagentKey(text);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function normalizeAmpRouteMappings(rawMappings) {
|
|
228
|
+
if (rawMappings === undefined || rawMappings === null || rawMappings === "") return undefined;
|
|
229
|
+
const out = {};
|
|
230
|
+
const source = Array.isArray(rawMappings)
|
|
231
|
+
? rawMappings
|
|
232
|
+
: (typeof rawMappings === "object" && rawMappings !== null
|
|
233
|
+
? Object.entries(rawMappings).map(([name, to]) => ({ name, to }))
|
|
234
|
+
: []);
|
|
235
|
+
|
|
236
|
+
for (const entry of source) {
|
|
237
|
+
if (!entry || typeof entry !== "object") continue;
|
|
238
|
+
const key = normalizeAmpRouteKey(
|
|
239
|
+
entry.id ?? entry.key ?? entry.name ?? entry.agent ?? entry.subagent ?? entry.signature
|
|
240
|
+
);
|
|
241
|
+
const target = String(entry.to ?? entry.target ?? entry.route ?? entry.ref ?? "").trim();
|
|
242
|
+
if (!key || !target) continue;
|
|
243
|
+
out[key] = target;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return out;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function normalizeAmpMatchEntry(entry) {
|
|
250
|
+
if (typeof entry === "string") {
|
|
251
|
+
const text = String(entry || "").trim();
|
|
252
|
+
return text ? text : null;
|
|
253
|
+
}
|
|
254
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) return null;
|
|
255
|
+
|
|
256
|
+
const vendor = normalizeAmpIdentifierText(entry.vendor);
|
|
257
|
+
const family = normalizeAmpIdentifierText(entry.family);
|
|
258
|
+
const version = normalizeAmpVersionToken(entry.version ?? entry.modelVersion);
|
|
259
|
+
const versionPrefix = normalizeAmpVersionToken(entry.versionPrefix ?? entry.versionStartsWith);
|
|
260
|
+
const variant = normalizeAmpIdentifierText(entry.variant ?? entry.modelVariant);
|
|
261
|
+
const variantPrefix = normalizeAmpIdentifierText(entry.variantPrefix ?? entry.variantStartsWith);
|
|
262
|
+
const modifiers = dedupeStrings(
|
|
263
|
+
toArray(entry.modifiers ?? entry.flags ?? entry.tags)
|
|
264
|
+
.map((value) => normalizeAmpIdentifierText(value))
|
|
265
|
+
.filter(Boolean)
|
|
266
|
+
);
|
|
267
|
+
const normalized = {
|
|
268
|
+
...(vendor ? { vendor } : {}),
|
|
269
|
+
...(family ? { family } : {}),
|
|
270
|
+
...(version ? { version } : {}),
|
|
271
|
+
...(versionPrefix ? { versionPrefix } : {}),
|
|
272
|
+
...(variant ? { variant } : {}),
|
|
273
|
+
...(variantPrefix ? { variantPrefix } : {}),
|
|
274
|
+
...(modifiers.length > 0 ? { modifiers } : {}),
|
|
275
|
+
...(normalizeBooleanValue(entry.variantAbsent, false) ? { variantAbsent: true } : {})
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
return Object.keys(normalized).length > 0 ? normalized : null;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function normalizeAmpMatchList(value) {
|
|
282
|
+
return toArray(value)
|
|
283
|
+
.flatMap((entry) => (Array.isArray(entry) ? entry : [entry]))
|
|
284
|
+
.map((entry) => normalizeAmpMatchEntry(entry))
|
|
285
|
+
.filter(Boolean);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function normalizeAmpEntityDefinitionEntry(entry) {
|
|
289
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) return null;
|
|
290
|
+
|
|
291
|
+
const id = normalizeAmpSubagentKey(entry.id ?? entry.agent ?? entry.subagent ?? entry.name ?? entry.key);
|
|
292
|
+
const type = normalizeAmpIdentifierText(entry.type ?? entry.kind ?? entry.category);
|
|
293
|
+
const aliases = dedupeStrings(
|
|
294
|
+
toArray(entry.aliases ?? entry.alias)
|
|
295
|
+
.map((value) => normalizeAmpSubagentKey(value))
|
|
296
|
+
.filter(Boolean)
|
|
297
|
+
);
|
|
298
|
+
const match = normalizeAmpMatchList(
|
|
299
|
+
entry.match ?? entry.matches ?? entry.patterns ?? entry.models ?? entry.model ?? entry.pattern
|
|
300
|
+
);
|
|
301
|
+
const signatures = dedupeStrings(
|
|
302
|
+
toArray(entry.signatures ?? entry.signatureIds ?? entry.signature)
|
|
303
|
+
.map((value) => normalizeAmpSignatureKey(value))
|
|
304
|
+
.filter(Boolean)
|
|
305
|
+
);
|
|
306
|
+
const route = String(entry.route ?? entry.to ?? entry.target ?? entry.ref ?? "").trim();
|
|
307
|
+
if (!id) return null;
|
|
308
|
+
|
|
309
|
+
return {
|
|
310
|
+
id,
|
|
311
|
+
...(type ? { type } : {}),
|
|
312
|
+
...(typeof entry.description === "string" && entry.description.trim()
|
|
313
|
+
? { description: entry.description.trim() }
|
|
314
|
+
: {}),
|
|
315
|
+
...(aliases.length > 0 ? { aliases } : {}),
|
|
316
|
+
...(match.length > 0 ? { match } : {}),
|
|
317
|
+
...(signatures.length > 0 ? { signatures } : {}),
|
|
318
|
+
...(route ? { route } : {}),
|
|
319
|
+
...(entry.enabled === false ? { enabled: false } : {})
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function normalizeAmpSignatureDefinitionEntry(entry) {
|
|
324
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) return null;
|
|
325
|
+
|
|
326
|
+
const id = normalizeAmpSignatureKey(entry.id ?? entry.signature ?? entry.name ?? entry.key);
|
|
327
|
+
const aliases = dedupeStrings(
|
|
328
|
+
toArray(entry.aliases ?? entry.alias)
|
|
329
|
+
.map((value) => normalizeAmpSignatureKey(value))
|
|
330
|
+
.filter(Boolean)
|
|
331
|
+
);
|
|
332
|
+
const match = normalizeAmpMatchList(
|
|
333
|
+
entry.match ?? entry.matches ?? entry.patterns ?? entry.models ?? entry.model ?? entry.pattern
|
|
334
|
+
);
|
|
335
|
+
const route = String(entry.route ?? entry.to ?? entry.target ?? entry.ref ?? "").trim();
|
|
336
|
+
if (!id) return null;
|
|
337
|
+
|
|
338
|
+
return {
|
|
339
|
+
id,
|
|
340
|
+
...(typeof entry.description === "string" && entry.description.trim()
|
|
341
|
+
? { description: entry.description.trim() }
|
|
342
|
+
: {}),
|
|
343
|
+
...(aliases.length > 0 ? { aliases } : {}),
|
|
344
|
+
...(match.length > 0 ? { match } : {}),
|
|
345
|
+
...(route ? { route } : {}),
|
|
346
|
+
...(entry.enabled === false ? { enabled: false } : {})
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function normalizeAmpDefinitionList(value, entryNormalizer) {
|
|
351
|
+
if (value === undefined || value === null || value === "") return undefined;
|
|
352
|
+
const entries = Array.isArray(value)
|
|
353
|
+
? value
|
|
354
|
+
: (typeof value === "object" && value !== null
|
|
355
|
+
? Object.entries(value).map(([id, entry]) => ({
|
|
356
|
+
...(entry && typeof entry === "object" && !Array.isArray(entry) ? entry : {}),
|
|
357
|
+
id: entry?.id ?? id
|
|
358
|
+
}))
|
|
359
|
+
: []);
|
|
360
|
+
|
|
361
|
+
const seen = new Set();
|
|
362
|
+
const out = [];
|
|
363
|
+
for (const entry of entries.map((candidate) => entryNormalizer(candidate)).filter(Boolean)) {
|
|
364
|
+
if (seen.has(entry.id)) continue;
|
|
365
|
+
seen.add(entry.id);
|
|
366
|
+
out.push(entry);
|
|
367
|
+
}
|
|
368
|
+
return out;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function normalizeAmpOverrides(rawOverrides) {
|
|
372
|
+
if (rawOverrides === undefined || rawOverrides === null || rawOverrides === "") return undefined;
|
|
373
|
+
const source = rawOverrides && typeof rawOverrides === "object" && !Array.isArray(rawOverrides)
|
|
374
|
+
? rawOverrides
|
|
375
|
+
: {};
|
|
376
|
+
const entities = normalizeAmpDefinitionList(source.entities, normalizeAmpEntityDefinitionEntry);
|
|
377
|
+
const signatures = normalizeAmpDefinitionList(source.signatures, normalizeAmpSignatureDefinitionEntry);
|
|
378
|
+
if (entities === undefined && signatures === undefined) return undefined;
|
|
379
|
+
return {
|
|
380
|
+
...(entities !== undefined ? { entities } : {}),
|
|
381
|
+
...(signatures !== undefined ? { signatures } : {})
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function normalizeAmpFallbackAction(value) {
|
|
386
|
+
const text = normalizeAmpIdentifierText(value);
|
|
387
|
+
if (!text) return undefined;
|
|
388
|
+
if (["default", "default-route", "defaultroute"].includes(text)) return "default-route";
|
|
389
|
+
if (["default-model", "defaultmodel"].includes(text)) return "default-model";
|
|
390
|
+
if (["upstream", "proxy", "proxy-upstream"].includes(text)) return "upstream";
|
|
391
|
+
if (["none", "disabled", "off"].includes(text)) return "none";
|
|
392
|
+
return undefined;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function normalizeAmpFallback(rawFallback) {
|
|
396
|
+
if (rawFallback === undefined || rawFallback === null || rawFallback === "") return undefined;
|
|
397
|
+
const source = rawFallback && typeof rawFallback === "object" && !Array.isArray(rawFallback)
|
|
398
|
+
? rawFallback
|
|
399
|
+
: {};
|
|
400
|
+
const onUnknown = normalizeAmpFallbackAction(source.onUnknown ?? source["on-unknown"]);
|
|
401
|
+
const onAmbiguous = normalizeAmpFallbackAction(source.onAmbiguous ?? source["on-ambiguous"]);
|
|
402
|
+
const hasProxyFlag = hasOwn(source, "proxyUpstream") || hasOwn(source, "proxy-upstream");
|
|
403
|
+
const proxyUpstream = hasProxyFlag
|
|
404
|
+
? normalizeBooleanValue(source.proxyUpstream ?? source["proxy-upstream"], true)
|
|
405
|
+
: undefined;
|
|
406
|
+
if (onUnknown === undefined && onAmbiguous === undefined && proxyUpstream === undefined) return undefined;
|
|
407
|
+
return {
|
|
408
|
+
...(onUnknown ? { onUnknown } : {}),
|
|
409
|
+
...(onAmbiguous ? { onAmbiguous } : {}),
|
|
410
|
+
...(proxyUpstream !== undefined ? { proxyUpstream } : {})
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function normalizeAmpPreset(value) {
|
|
415
|
+
if (value === undefined || value === null) return undefined;
|
|
416
|
+
const text = normalizeAmpIdentifierText(value);
|
|
417
|
+
if (!text) return "builtin";
|
|
418
|
+
if (["default", "builtin", "builtins"].includes(text)) return "builtin";
|
|
419
|
+
if (["none", "disabled", "off"].includes(text)) return "none";
|
|
420
|
+
return text;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function normalizeAmpSubagentKey(value) {
|
|
424
|
+
const text = String(value || "").trim().toLowerCase();
|
|
425
|
+
if (!text) return "";
|
|
426
|
+
if (["lookat", "look-at", "look_at", "look at"].includes(text)) return "look-at";
|
|
427
|
+
if (["title", "titling"].includes(text)) return "title";
|
|
428
|
+
return text;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function normalizeAmpSubagentMappings(rawMappings) {
|
|
432
|
+
if (rawMappings === undefined || rawMappings === null || rawMappings === "") return {};
|
|
433
|
+
const out = {};
|
|
434
|
+
const source = Array.isArray(rawMappings)
|
|
435
|
+
? rawMappings
|
|
436
|
+
: (typeof rawMappings === "object" && rawMappings !== null
|
|
437
|
+
? Object.entries(rawMappings).map(([agent, to]) => ({ agent, to }))
|
|
438
|
+
: []);
|
|
439
|
+
for (const entry of source) {
|
|
440
|
+
if (!entry || typeof entry !== "object") continue;
|
|
441
|
+
const key = normalizeAmpSubagentKey(entry.agent ?? entry.subagent ?? entry.name ?? entry.id);
|
|
442
|
+
const target = String(entry.to ?? entry.target ?? entry.route ?? entry.ref ?? "").trim();
|
|
443
|
+
if (!key || !target) continue;
|
|
444
|
+
out[key] = target;
|
|
445
|
+
}
|
|
446
|
+
return out;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function normalizeAmpSubagentDefinitionEntry(entry) {
|
|
450
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) return null;
|
|
451
|
+
|
|
452
|
+
const id = normalizeAmpSubagentKey(entry.id ?? entry.agent ?? entry.subagent ?? entry.name ?? entry.key);
|
|
453
|
+
const patterns = dedupeStrings(toArray(entry.patterns ?? entry.matches ?? entry.models ?? entry.model ?? entry.pattern));
|
|
454
|
+
if (!id || patterns.length === 0) return null;
|
|
455
|
+
|
|
456
|
+
return {
|
|
457
|
+
id,
|
|
458
|
+
patterns
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function normalizeAmpSubagentDefinitions(rawDefinitions) {
|
|
463
|
+
if (rawDefinitions === undefined || rawDefinitions === null || rawDefinitions === "") return undefined;
|
|
464
|
+
if (!Array.isArray(rawDefinitions)) return undefined;
|
|
465
|
+
|
|
466
|
+
const seen = new Set();
|
|
467
|
+
const out = [];
|
|
468
|
+
for (const entry of rawDefinitions.map(normalizeAmpSubagentDefinitionEntry).filter(Boolean)) {
|
|
469
|
+
if (seen.has(entry.id)) continue;
|
|
470
|
+
seen.add(entry.id);
|
|
471
|
+
out.push(entry);
|
|
472
|
+
}
|
|
473
|
+
return out;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function normalizeAmpConfig(rawAmp) {
|
|
477
|
+
const source = rawAmp && typeof rawAmp === "object" && !Array.isArray(rawAmp)
|
|
478
|
+
? rawAmp
|
|
479
|
+
: {};
|
|
480
|
+
const hasProxyWebSearchToUpstream = hasOwn(source, "proxyWebSearchToUpstream") || hasOwn(source, "proxy-web-search-to-upstream");
|
|
481
|
+
const hasPreset = hasOwn(source, "preset");
|
|
482
|
+
const hasDefaultRoute = hasOwn(source, "defaultRoute") || hasOwn(source, "default-route");
|
|
483
|
+
const hasRoutes = hasOwn(source, "routes");
|
|
484
|
+
const hasRawModelRoutes = hasOwn(source, "rawModelRoutes") || hasOwn(source, "raw-model-routes");
|
|
485
|
+
const hasOverrides = hasOwn(source, "overrides");
|
|
486
|
+
const hasFallback = hasOwn(source, "fallback");
|
|
487
|
+
const normalizedSubagentDefinitions = normalizeAmpSubagentDefinitions(
|
|
488
|
+
source.subagentDefinitions ?? source["subagent-definitions"]
|
|
489
|
+
);
|
|
490
|
+
const normalizedPreset = normalizeAmpPreset(source.preset);
|
|
491
|
+
const normalizedDefaultRoute = String(source.defaultRoute ?? source["default-route"] ?? "").trim();
|
|
492
|
+
const normalizedRoutes = normalizeAmpRouteMappings(source.routes);
|
|
493
|
+
const normalizedRawModelRoutes = toArray(
|
|
494
|
+
source.rawModelRoutes ?? source["raw-model-routes"]
|
|
495
|
+
)
|
|
496
|
+
.map(normalizeAmpModelMappingEntry)
|
|
497
|
+
.filter(Boolean);
|
|
498
|
+
const normalizedOverrides = normalizeAmpOverrides(source.overrides);
|
|
499
|
+
const normalizedFallback = normalizeAmpFallback(source.fallback);
|
|
500
|
+
|
|
501
|
+
return {
|
|
502
|
+
upstreamUrl: sanitizeEndpointUrl(
|
|
503
|
+
source.upstreamUrl ??
|
|
504
|
+
source["upstream-url"] ??
|
|
505
|
+
source.baseUrl ??
|
|
506
|
+
source["base-url"] ??
|
|
507
|
+
""
|
|
508
|
+
),
|
|
509
|
+
upstreamApiKey: String(
|
|
510
|
+
source.upstreamApiKey ??
|
|
511
|
+
source["upstream-api-key"] ??
|
|
512
|
+
""
|
|
513
|
+
).trim() || undefined,
|
|
514
|
+
restrictManagementToLocalhost: normalizeBooleanValue(
|
|
515
|
+
source.restrictManagementToLocalhost ?? source["restrict-management-to-localhost"],
|
|
516
|
+
false
|
|
517
|
+
),
|
|
518
|
+
forceModelMappings: normalizeBooleanValue(
|
|
519
|
+
source.forceModelMappings ?? source["force-model-mappings"],
|
|
520
|
+
false
|
|
521
|
+
),
|
|
522
|
+
...(hasProxyWebSearchToUpstream
|
|
523
|
+
? {
|
|
524
|
+
proxyWebSearchToUpstream: normalizeBooleanValue(
|
|
525
|
+
source.proxyWebSearchToUpstream ?? source["proxy-web-search-to-upstream"],
|
|
526
|
+
false
|
|
527
|
+
)
|
|
528
|
+
}
|
|
529
|
+
: {}),
|
|
530
|
+
modelMappings: toArray(
|
|
531
|
+
source.modelMappings ?? source["model-mappings"]
|
|
532
|
+
)
|
|
533
|
+
.map(normalizeAmpModelMappingEntry)
|
|
534
|
+
.filter(Boolean),
|
|
535
|
+
subagentMappings: normalizeAmpSubagentMappings(
|
|
536
|
+
source.subagentMappings ?? source["subagent-mappings"]
|
|
537
|
+
),
|
|
538
|
+
...(hasPreset
|
|
539
|
+
? {
|
|
540
|
+
preset: normalizedPreset
|
|
541
|
+
}
|
|
542
|
+
: {}),
|
|
543
|
+
...(hasDefaultRoute
|
|
544
|
+
? {
|
|
545
|
+
defaultRoute: normalizedDefaultRoute
|
|
546
|
+
}
|
|
547
|
+
: {}),
|
|
548
|
+
...(hasRoutes
|
|
549
|
+
? {
|
|
550
|
+
routes: normalizedRoutes || {}
|
|
551
|
+
}
|
|
552
|
+
: {}),
|
|
553
|
+
...(hasRawModelRoutes
|
|
554
|
+
? {
|
|
555
|
+
rawModelRoutes: normalizedRawModelRoutes
|
|
556
|
+
}
|
|
557
|
+
: {}),
|
|
558
|
+
...(hasOverrides && normalizedOverrides !== undefined
|
|
559
|
+
? {
|
|
560
|
+
overrides: normalizedOverrides
|
|
561
|
+
}
|
|
562
|
+
: {}),
|
|
563
|
+
...(hasFallback && normalizedFallback !== undefined
|
|
564
|
+
? {
|
|
565
|
+
fallback: normalizedFallback
|
|
566
|
+
}
|
|
567
|
+
: {}),
|
|
568
|
+
...(normalizedSubagentDefinitions !== undefined
|
|
569
|
+
? {
|
|
570
|
+
subagentDefinitions: normalizedSubagentDefinitions
|
|
571
|
+
}
|
|
572
|
+
: {})
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function escapeRegex(text) {
|
|
577
|
+
return String(text || "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function parseAmpModelPattern(pattern) {
|
|
581
|
+
const text = String(pattern || "").trim();
|
|
582
|
+
if (!text) return null;
|
|
583
|
+
|
|
584
|
+
if (text.startsWith("/") && text.lastIndexOf("/") > 0) {
|
|
585
|
+
const endIndex = text.lastIndexOf("/");
|
|
586
|
+
const source = text.slice(1, endIndex);
|
|
587
|
+
const flags = text.slice(endIndex + 1);
|
|
588
|
+
try {
|
|
589
|
+
return new RegExp(source, flags);
|
|
590
|
+
} catch {
|
|
591
|
+
return null;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
if (text.includes("*")) {
|
|
596
|
+
return new RegExp(`^${escapeRegex(text).replace(/\\\*/g, ".*")}$`);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
return null;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function canonicalizeAmpModelText(value) {
|
|
603
|
+
const text = String(value || "").trim();
|
|
604
|
+
if (!text) return "";
|
|
605
|
+
if (text.startsWith("/") && text.lastIndexOf("/") > 0) return text;
|
|
606
|
+
|
|
607
|
+
return text
|
|
608
|
+
.toLowerCase()
|
|
609
|
+
.replace(/[–—]+/g, "-")
|
|
610
|
+
.replace(/[\s_]+/g, "-")
|
|
611
|
+
.replace(/-+/g, "-")
|
|
612
|
+
.replace(/\bclaude-(opus|sonnet|haiku)-(\d+)-(\d+)\b/g, "claude-$1-$2.$3")
|
|
613
|
+
.replace(/\b(opus|sonnet|haiku)-(\d+)-(\d+)\b/g, "$1-$2.$3")
|
|
614
|
+
.replace(/\bgpt-(\d+)-(\d+)(?=$|[-])\b/g, "gpt-$1.$2")
|
|
615
|
+
.replace(/\bgemini-(\d+)-(\d+)(?=-)\b/g, "gemini-$1.$2")
|
|
616
|
+
.replace(/^-+|-+$/g, "");
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function isAmpVersionToken(value) {
|
|
620
|
+
const text = normalizeAmpVersionToken(value);
|
|
621
|
+
return /^\d+(?:\.\d+)?$/.test(text);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function parseAmpModelDescriptor(value) {
|
|
625
|
+
const raw = String(value || "").trim();
|
|
626
|
+
const canonical = canonicalizeAmpModelText(raw);
|
|
627
|
+
const tokens = canonical
|
|
628
|
+
.split(/[\/-]+/)
|
|
629
|
+
.map((token) => token.trim())
|
|
630
|
+
.filter(Boolean);
|
|
631
|
+
|
|
632
|
+
const descriptor = {
|
|
633
|
+
raw,
|
|
634
|
+
canonical,
|
|
635
|
+
vendor: "",
|
|
636
|
+
family: "",
|
|
637
|
+
version: "",
|
|
638
|
+
variant: "",
|
|
639
|
+
variantChain: "",
|
|
640
|
+
modifiers: []
|
|
641
|
+
};
|
|
642
|
+
|
|
643
|
+
if (tokens.length === 0) return descriptor;
|
|
644
|
+
|
|
645
|
+
if (tokens[0] === "claude" || ["opus", "sonnet", "haiku"].includes(tokens[0])) {
|
|
646
|
+
descriptor.vendor = "anthropic";
|
|
647
|
+
let cursor = tokens[0] === "claude" ? 1 : 0;
|
|
648
|
+
descriptor.family = tokens[cursor] || "";
|
|
649
|
+
cursor += 1;
|
|
650
|
+
if (isAmpVersionToken(tokens[cursor])) {
|
|
651
|
+
descriptor.version = normalizeAmpVersionToken(tokens[cursor]);
|
|
652
|
+
cursor += 1;
|
|
653
|
+
} else if (/^\d+$/.test(tokens[cursor] || "") && /^\d+$/.test(tokens[cursor + 1] || "")) {
|
|
654
|
+
descriptor.version = `${tokens[cursor]}.${tokens[cursor + 1]}`;
|
|
655
|
+
cursor += 2;
|
|
656
|
+
}
|
|
657
|
+
const rest = tokens.slice(cursor);
|
|
658
|
+
descriptor.variant = rest[0] || "";
|
|
659
|
+
descriptor.variantChain = rest.join("-");
|
|
660
|
+
descriptor.modifiers = rest.slice(1);
|
|
661
|
+
return descriptor;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
if (tokens[0] === "gpt") {
|
|
665
|
+
descriptor.vendor = "openai";
|
|
666
|
+
descriptor.family = "gpt";
|
|
667
|
+
let cursor = 1;
|
|
668
|
+
if (isAmpVersionToken(tokens[cursor])) {
|
|
669
|
+
descriptor.version = normalizeAmpVersionToken(tokens[cursor]);
|
|
670
|
+
cursor += 1;
|
|
671
|
+
}
|
|
672
|
+
const rest = tokens.slice(cursor);
|
|
673
|
+
descriptor.variant = rest[0] || "";
|
|
674
|
+
descriptor.variantChain = rest.join("-");
|
|
675
|
+
descriptor.modifiers = rest.slice(1);
|
|
676
|
+
return descriptor;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
if (tokens[0] === "gemini") {
|
|
680
|
+
descriptor.vendor = "google";
|
|
681
|
+
descriptor.family = "gemini";
|
|
682
|
+
let cursor = 1;
|
|
683
|
+
if (isAmpVersionToken(tokens[cursor])) {
|
|
684
|
+
descriptor.version = normalizeAmpVersionToken(tokens[cursor]);
|
|
685
|
+
cursor += 1;
|
|
686
|
+
}
|
|
687
|
+
const rest = tokens.slice(cursor);
|
|
688
|
+
descriptor.variant = rest[0] || "";
|
|
689
|
+
descriptor.variantChain = rest.join("-");
|
|
690
|
+
descriptor.modifiers = rest.slice(1);
|
|
691
|
+
return descriptor;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
return descriptor;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function matchAmpModelSelector(selector, value, descriptor = parseAmpModelDescriptor(value)) {
|
|
698
|
+
if (typeof selector === "string") return matchAmpModelPattern(selector, value);
|
|
699
|
+
if (!selector || typeof selector !== "object" || Array.isArray(selector)) return false;
|
|
700
|
+
|
|
701
|
+
if (selector.vendor && selector.vendor !== descriptor.vendor) return false;
|
|
702
|
+
if (selector.family && selector.family !== descriptor.family) return false;
|
|
703
|
+
if (selector.version && selector.version !== descriptor.version) return false;
|
|
704
|
+
if (selector.versionPrefix && !String(descriptor.version || "").startsWith(selector.versionPrefix)) return false;
|
|
705
|
+
if (selector.variant && selector.variant !== descriptor.variant) return false;
|
|
706
|
+
if (selector.variantPrefix && !String(descriptor.variantChain || "").startsWith(selector.variantPrefix)) return false;
|
|
707
|
+
if (selector.variantAbsent === true && descriptor.variantChain) return false;
|
|
708
|
+
if (Array.isArray(selector.modifiers) && selector.modifiers.some((modifier) => !descriptor.modifiers.includes(modifier))) return false;
|
|
709
|
+
|
|
710
|
+
return true;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function matchAmpModelPattern(pattern, value) {
|
|
714
|
+
const normalizedValue = String(value || "").trim();
|
|
715
|
+
const normalizedPattern = String(pattern || "").trim();
|
|
716
|
+
if (!normalizedPattern || !normalizedValue) return false;
|
|
717
|
+
|
|
718
|
+
const valueCandidates = dedupeStrings([
|
|
719
|
+
normalizedValue,
|
|
720
|
+
canonicalizeAmpModelText(normalizedValue)
|
|
721
|
+
]);
|
|
722
|
+
const patternCandidates = normalizedPattern.startsWith("/") && normalizedPattern.lastIndexOf("/") > 0
|
|
723
|
+
? [normalizedPattern]
|
|
724
|
+
: dedupeStrings([
|
|
725
|
+
normalizedPattern,
|
|
726
|
+
canonicalizeAmpModelText(normalizedPattern)
|
|
727
|
+
]);
|
|
728
|
+
|
|
729
|
+
for (const candidatePattern of patternCandidates) {
|
|
730
|
+
if (valueCandidates.includes(candidatePattern)) return true;
|
|
731
|
+
const regex = parseAmpModelPattern(candidatePattern);
|
|
732
|
+
if (regex && valueCandidates.some((candidateValue) => regex.test(candidateValue))) return true;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
return false;
|
|
736
|
+
}
|
|
737
|
+
|
|
152
738
|
function parseRouteReference(value) {
|
|
153
739
|
const text = String(value || "").trim();
|
|
154
740
|
if (!text) return { type: "invalid", ref: "", reason: "empty" };
|
|
@@ -243,12 +829,20 @@ function normalizeRateLimitBucketEntry(entry, index = 0, { reservedIds } = {}) {
|
|
|
243
829
|
|
|
244
830
|
const unitRaw =
|
|
245
831
|
entry.window?.unit ??
|
|
832
|
+
entry["window.unit"] ??
|
|
246
833
|
entry.windowUnit ??
|
|
247
834
|
entry["window-unit"];
|
|
248
835
|
const sizeRaw =
|
|
249
836
|
entry.window?.size ??
|
|
837
|
+
entry.window?.value ??
|
|
838
|
+
entry["window.size"] ??
|
|
839
|
+
entry["window.value"] ??
|
|
250
840
|
entry.windowSize ??
|
|
251
841
|
entry["window-size"];
|
|
842
|
+
const requestsRaw =
|
|
843
|
+
entry.requests ??
|
|
844
|
+
entry.limit ??
|
|
845
|
+
entry["requests-per-window"];
|
|
252
846
|
const models = dedupeStrings(toArray(entry.models ?? entry.model));
|
|
253
847
|
const explicitId = String(entry.id || "").trim();
|
|
254
848
|
const name = sanitizeRateLimitBucketName(entry.name);
|
|
@@ -262,7 +856,7 @@ function normalizeRateLimitBucketEntry(entry, index = 0, { reservedIds } = {}) {
|
|
|
262
856
|
id,
|
|
263
857
|
...(name ? { name } : {}),
|
|
264
858
|
models,
|
|
265
|
-
requests: parsePositiveInteger(
|
|
859
|
+
requests: parsePositiveInteger(requestsRaw),
|
|
266
860
|
window: {
|
|
267
861
|
unit: typeof unitRaw === "string" ? unitRaw.trim().toLowerCase() : "",
|
|
268
862
|
size: parsePositiveInteger(sizeRaw)
|
|
@@ -504,6 +1098,56 @@ function normalizeModelAliases(rawModelAliases) {
|
|
|
504
1098
|
};
|
|
505
1099
|
}
|
|
506
1100
|
|
|
1101
|
+
function buildDefaultModelAlias(defaultModel, aliases = {}) {
|
|
1102
|
+
const existingDefault = aliases?.[DEFAULT_MODEL_ALIAS_ID];
|
|
1103
|
+
if (existingDefault && typeof existingDefault === "object" && !Array.isArray(existingDefault)) {
|
|
1104
|
+
return {
|
|
1105
|
+
...existingDefault,
|
|
1106
|
+
id: DEFAULT_MODEL_ALIAS_ID,
|
|
1107
|
+
strategy: String(existingDefault.strategy || "ordered").trim().toLowerCase() || "ordered",
|
|
1108
|
+
targets: dedupeAliasTargets(existingDefault.targets || []),
|
|
1109
|
+
fallbackTargets: dedupeAliasTargets(existingDefault.fallbackTargets || [])
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
const configuredDefault = String(defaultModel || "").trim();
|
|
1114
|
+
if (!configuredDefault || configuredDefault === "smart") {
|
|
1115
|
+
return {
|
|
1116
|
+
id: DEFAULT_MODEL_ALIAS_ID,
|
|
1117
|
+
strategy: "ordered",
|
|
1118
|
+
targets: [],
|
|
1119
|
+
fallbackTargets: []
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
const parsed = parseRouteReference(configuredDefault);
|
|
1124
|
+
if (parsed.type === "alias" && aliases?.[parsed.aliasId]) {
|
|
1125
|
+
const aliasedDefault = aliases[parsed.aliasId];
|
|
1126
|
+
return {
|
|
1127
|
+
...aliasedDefault,
|
|
1128
|
+
id: DEFAULT_MODEL_ALIAS_ID,
|
|
1129
|
+
strategy: String(aliasedDefault.strategy || "ordered").trim().toLowerCase() || "ordered",
|
|
1130
|
+
targets: dedupeAliasTargets(aliasedDefault.targets || []),
|
|
1131
|
+
fallbackTargets: dedupeAliasTargets(aliasedDefault.fallbackTargets || [])
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
return {
|
|
1136
|
+
id: DEFAULT_MODEL_ALIAS_ID,
|
|
1137
|
+
strategy: "ordered",
|
|
1138
|
+
targets: dedupeAliasTargets([{ ref: configuredDefault }]),
|
|
1139
|
+
fallbackTargets: []
|
|
1140
|
+
};
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
function ensureDefaultModelAlias(defaultModel, aliases = {}) {
|
|
1144
|
+
const nextAliases = {
|
|
1145
|
+
...(aliases && typeof aliases === "object" && !Array.isArray(aliases) ? aliases : {})
|
|
1146
|
+
};
|
|
1147
|
+
nextAliases[DEFAULT_MODEL_ALIAS_ID] = buildDefaultModelAlias(defaultModel, nextAliases);
|
|
1148
|
+
return nextAliases;
|
|
1149
|
+
}
|
|
1150
|
+
|
|
507
1151
|
function hasV2ConfigFields(raw, providers, modelAliases) {
|
|
508
1152
|
if (Object.keys(modelAliases || {}).length > 0) return true;
|
|
509
1153
|
if ((providers || []).some((provider) => Array.isArray(provider.rateLimits) && provider.rateLimits.length > 0)) {
|
|
@@ -698,15 +1342,17 @@ export function normalizeRuntimeConfig(rawConfig, options = {}) {
|
|
|
698
1342
|
.filter((provider) => provider.enabled !== false)
|
|
699
1343
|
);
|
|
700
1344
|
const modelAliasResult = normalizeModelAliases(raw.modelAliases || raw["model-aliases"]);
|
|
701
|
-
const
|
|
1345
|
+
const rawDefaultModel = typeof raw.defaultModel === "string"
|
|
1346
|
+
? raw.defaultModel
|
|
1347
|
+
: (typeof raw["default-model"] === "string" ? raw["default-model"] : undefined);
|
|
1348
|
+
const modelAliases = ensureDefaultModelAlias(rawDefaultModel, modelAliasResult.aliases);
|
|
702
1349
|
|
|
703
1350
|
const masterKey = typeof raw.masterKey === "string"
|
|
704
1351
|
? raw.masterKey
|
|
705
1352
|
: (typeof raw["master-key"] === "string" ? raw["master-key"] : undefined);
|
|
706
1353
|
|
|
707
|
-
const defaultModel =
|
|
708
|
-
|
|
709
|
-
: (typeof raw["default-model"] === "string" ? raw["default-model"] : undefined);
|
|
1354
|
+
const defaultModel = rawDefaultModel;
|
|
1355
|
+
const amp = normalizeAmpConfig(raw.amp ?? raw.ampcode ?? raw["amp-code"]);
|
|
710
1356
|
|
|
711
1357
|
const normalized = {
|
|
712
1358
|
version: inferNormalizedConfigVersion(raw, providers, modelAliases),
|
|
@@ -714,7 +1360,8 @@ export function normalizeRuntimeConfig(rawConfig, options = {}) {
|
|
|
714
1360
|
defaultModel,
|
|
715
1361
|
providers,
|
|
716
1362
|
modelAliases,
|
|
717
|
-
|
|
1363
|
+
amp,
|
|
1364
|
+
metadata: sanitizeRuntimeMetadata(raw.metadata)
|
|
718
1365
|
};
|
|
719
1366
|
Object.defineProperty(normalized, NORMALIZATION_ISSUES_SYMBOL, {
|
|
720
1367
|
value: {
|
|
@@ -727,6 +1374,20 @@ export function normalizeRuntimeConfig(rawConfig, options = {}) {
|
|
|
727
1374
|
return attachRoutingIndex(normalized, buildRoutingIndex(normalized));
|
|
728
1375
|
}
|
|
729
1376
|
|
|
1377
|
+
function getDefaultRouteReference(config, routingIndex = getRoutingIndex(config)) {
|
|
1378
|
+
if (routingIndex?.aliasById?.has(DEFAULT_MODEL_ALIAS_ID)) {
|
|
1379
|
+
return DEFAULT_MODEL_ALIAS_ID;
|
|
1380
|
+
}
|
|
1381
|
+
return String(config?.defaultModel || "").trim();
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
function getSmartRouteReference(config, routingIndex = getRoutingIndex(config)) {
|
|
1385
|
+
if (routingIndex?.aliasById?.has("smart")) {
|
|
1386
|
+
return "smart";
|
|
1387
|
+
}
|
|
1388
|
+
return getDefaultRouteReference(config, routingIndex);
|
|
1389
|
+
}
|
|
1390
|
+
|
|
730
1391
|
export function parseRuntimeConfigJson(json, options = undefined) {
|
|
731
1392
|
return normalizeRuntimeConfig(JSON.parse(json), options);
|
|
732
1393
|
}
|
|
@@ -869,9 +1530,6 @@ function validateModelAliases(config, routingIndex, errors) {
|
|
|
869
1530
|
if (!ALLOWED_ALIAS_STRATEGIES.has(alias?.strategy || "ordered")) {
|
|
870
1531
|
errors.push(`Alias '${aliasId}' has unsupported strategy '${alias?.strategy}'.`);
|
|
871
1532
|
}
|
|
872
|
-
if (!Array.isArray(alias?.targets) || alias.targets.length === 0) {
|
|
873
|
-
errors.push(`Alias '${aliasId}' must define at least one target.`);
|
|
874
|
-
}
|
|
875
1533
|
|
|
876
1534
|
for (const entry of collectAliasReferenceEntries(alias, aliasId)) {
|
|
877
1535
|
const ref = String(entry.target?.ref || "").trim();
|
|
@@ -900,6 +1558,51 @@ function validateModelAliases(config, routingIndex, errors) {
|
|
|
900
1558
|
detectAliasCycles(config, errors);
|
|
901
1559
|
}
|
|
902
1560
|
|
|
1561
|
+
function validateAmpRouteReference(ref, routingIndex, errors, context) {
|
|
1562
|
+
const parsed = parseRouteReference(ref);
|
|
1563
|
+
if (parsed.type === "invalid") {
|
|
1564
|
+
errors.push(`${context} has invalid ref '${ref}'.`);
|
|
1565
|
+
return;
|
|
1566
|
+
}
|
|
1567
|
+
if (parsed.type === "direct") {
|
|
1568
|
+
const resolved = routingIndex.modelByRef.get(parsed.ref) || routingIndex.modelByAliasRef.get(parsed.ref);
|
|
1569
|
+
if (!resolved) errors.push(`${context} references unknown model '${parsed.ref}'.`);
|
|
1570
|
+
return;
|
|
1571
|
+
}
|
|
1572
|
+
if (!routingIndex.aliasById.has(parsed.aliasId)) {
|
|
1573
|
+
errors.push(`${context} references unknown alias '${parsed.aliasId}'.`);
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
function validateAmpConfig(config, routingIndex, errors) {
|
|
1578
|
+
const amp = config?.amp;
|
|
1579
|
+
if (!amp || typeof amp !== "object") return;
|
|
1580
|
+
|
|
1581
|
+
if (amp.defaultRoute) {
|
|
1582
|
+
validateAmpRouteReference(String(amp.defaultRoute), routingIndex, errors, "AMP defaultRoute");
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
for (const [key, ref] of Object.entries(amp.routes && typeof amp.routes === "object" ? amp.routes : {})) {
|
|
1586
|
+
if (!ref) continue;
|
|
1587
|
+
validateAmpRouteReference(String(ref), routingIndex, errors, `AMP route '${key}'`);
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
for (const mapping of (Array.isArray(amp.rawModelRoutes) ? amp.rawModelRoutes : [])) {
|
|
1591
|
+
if (!mapping?.to) continue;
|
|
1592
|
+
validateAmpRouteReference(String(mapping.to), routingIndex, errors, `AMP rawModelRoute '${mapping.from || "(match)"}'`);
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
for (const entry of (amp?.overrides?.entities || [])) {
|
|
1596
|
+
if (!entry?.route) continue;
|
|
1597
|
+
validateAmpRouteReference(String(entry.route), routingIndex, errors, `AMP override entity '${entry.id || "(unknown)"}'`);
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
for (const entry of (amp?.overrides?.signatures || [])) {
|
|
1601
|
+
if (!entry?.route) continue;
|
|
1602
|
+
validateAmpRouteReference(String(entry.route), routingIndex, errors, `AMP override signature '${entry.id || "(unknown)"}'`);
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
|
|
903
1606
|
export function validateRuntimeConfig(config, { requireMasterKey = false, requireProvider = false } = {}) {
|
|
904
1607
|
const errors = [];
|
|
905
1608
|
|
|
@@ -947,6 +1650,7 @@ export function validateRuntimeConfig(config, { requireMasterKey = false, requir
|
|
|
947
1650
|
const routingIndex = getRoutingIndex(config);
|
|
948
1651
|
validateProviderRateLimits(config, routingIndex, errors);
|
|
949
1652
|
validateModelAliases(config, routingIndex, errors);
|
|
1653
|
+
validateAmpConfig(config, routingIndex, errors);
|
|
950
1654
|
|
|
951
1655
|
if (requireMasterKey && !config.masterKey) {
|
|
952
1656
|
errors.push("masterKey is required for worker deployment/export.");
|
|
@@ -972,12 +1676,22 @@ export function resolveProviderFormat(provider, sourceFormat = undefined) {
|
|
|
972
1676
|
return FORMATS.OPENAI;
|
|
973
1677
|
}
|
|
974
1678
|
|
|
975
|
-
export function resolveProviderUrl(provider, targetFormat) {
|
|
1679
|
+
export function resolveProviderUrl(provider, targetFormat, requestKind = undefined) {
|
|
976
1680
|
const baseUrl = sanitizeEndpointUrl(provider?.baseUrlByFormat?.[targetFormat] || provider?.baseUrl || "").replace(/\/+$/, "");
|
|
977
1681
|
if (!baseUrl) return "";
|
|
978
1682
|
const isVersionedApiRoot = /\/v\d+(?:\.\d+)?$/i.test(baseUrl);
|
|
979
1683
|
|
|
980
1684
|
if (targetFormat === FORMATS.OPENAI) {
|
|
1685
|
+
if (requestKind === "responses") {
|
|
1686
|
+
if (baseUrl.endsWith("/responses")) return baseUrl;
|
|
1687
|
+
if (baseUrl.endsWith("/v1") || isVersionedApiRoot) return `${baseUrl}/responses`;
|
|
1688
|
+
return `${baseUrl}/v1/responses`;
|
|
1689
|
+
}
|
|
1690
|
+
if (requestKind === "completions") {
|
|
1691
|
+
if (baseUrl.endsWith("/completions")) return baseUrl;
|
|
1692
|
+
if (baseUrl.endsWith("/v1") || isVersionedApiRoot) return `${baseUrl}/completions`;
|
|
1693
|
+
return `${baseUrl}/v1/completions`;
|
|
1694
|
+
}
|
|
981
1695
|
if (baseUrl.endsWith("/chat/completions")) return baseUrl;
|
|
982
1696
|
if (baseUrl.endsWith("/v1") || isVersionedApiRoot) return `${baseUrl}/chat/completions`;
|
|
983
1697
|
return `${baseUrl}/v1/chat/completions`;
|
|
@@ -1099,6 +1813,12 @@ export function sanitizeConfigForDisplay(config) {
|
|
|
1099
1813
|
return {
|
|
1100
1814
|
...config,
|
|
1101
1815
|
masterKey: config.masterKey ? maskSecret(config.masterKey) : undefined,
|
|
1816
|
+
amp: config.amp
|
|
1817
|
+
? {
|
|
1818
|
+
...config.amp,
|
|
1819
|
+
upstreamApiKey: config.amp.upstreamApiKey ? maskSecret(config.amp.upstreamApiKey) : undefined
|
|
1820
|
+
}
|
|
1821
|
+
: undefined,
|
|
1102
1822
|
providers: (config.providers || []).map((provider) => ({
|
|
1103
1823
|
...provider,
|
|
1104
1824
|
apiKey: provider.apiKey ? maskSecret(provider.apiKey) : undefined
|
|
@@ -1106,11 +1826,34 @@ export function sanitizeConfigForDisplay(config) {
|
|
|
1106
1826
|
};
|
|
1107
1827
|
}
|
|
1108
1828
|
|
|
1829
|
+
function getConfiguredModelFormats(model) {
|
|
1830
|
+
return dedupeStrings([...(model?.formats || []), model?.format])
|
|
1831
|
+
.filter((value) => value === FORMATS.OPENAI || value === FORMATS.CLAUDE);
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
function getProbedModelFormats(provider, model) {
|
|
1835
|
+
const modelId = typeof model?.id === "string" ? model.id.trim() : "";
|
|
1836
|
+
if (!modelId) return [];
|
|
1837
|
+
|
|
1838
|
+
const preferredFormat = provider?.lastProbe?.modelPreferredFormat?.[modelId];
|
|
1839
|
+
if (preferredFormat === FORMATS.OPENAI || preferredFormat === FORMATS.CLAUDE) {
|
|
1840
|
+
return [preferredFormat];
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
return dedupeStrings(provider?.lastProbe?.modelSupport?.[modelId] || [])
|
|
1844
|
+
.filter((value) => value === FORMATS.OPENAI || value === FORMATS.CLAUDE);
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
function getRuntimeModelFormats(provider, model) {
|
|
1848
|
+
const probedFormats = getProbedModelFormats(provider, model);
|
|
1849
|
+
if (probedFormats.length > 0) return probedFormats;
|
|
1850
|
+
return getConfiguredModelFormats(model);
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1109
1853
|
function buildTargetCandidate(provider, model, sourceFormat, target = undefined) {
|
|
1110
1854
|
const providerFormats = dedupeStrings([...(provider?.formats || []), provider?.format])
|
|
1111
1855
|
.filter((value) => value === FORMATS.OPENAI || value === FORMATS.CLAUDE);
|
|
1112
|
-
const modelFormats =
|
|
1113
|
-
.filter((value) => value === FORMATS.OPENAI || value === FORMATS.CLAUDE);
|
|
1856
|
+
const modelFormats = getRuntimeModelFormats(provider, model);
|
|
1114
1857
|
const supportedFormats = modelFormats.length > 0
|
|
1115
1858
|
? providerFormats.filter((fmt) => modelFormats.includes(fmt))
|
|
1116
1859
|
: providerFormats;
|
|
@@ -1179,8 +1922,7 @@ function applyAliasTargetOptions(candidate, target, routeTier) {
|
|
|
1179
1922
|
function modelSupportsProviderFormat(provider, model) {
|
|
1180
1923
|
const providerFormats = dedupeStrings([...(provider.formats || []), provider.format])
|
|
1181
1924
|
.filter((value) => value === FORMATS.OPENAI || value === FORMATS.CLAUDE);
|
|
1182
|
-
const modelFormats =
|
|
1183
|
-
.filter((value) => value === FORMATS.OPENAI || value === FORMATS.CLAUDE);
|
|
1925
|
+
const modelFormats = getRuntimeModelFormats(provider, model);
|
|
1184
1926
|
if (modelFormats.length === 0) return true;
|
|
1185
1927
|
return providerFormats.some((fmt) => modelFormats.includes(fmt));
|
|
1186
1928
|
}
|
|
@@ -1195,6 +1937,110 @@ function resolveQualifiedModel(config, qualifiedModel, routingIndex = getRouting
|
|
|
1195
1937
|
return routingIndex.modelByRef.get(parsed.ref) || routingIndex.modelByAliasRef.get(parsed.ref) || null;
|
|
1196
1938
|
}
|
|
1197
1939
|
|
|
1940
|
+
function sortAmpCandidates(candidates, providerHint) {
|
|
1941
|
+
if (!providerHint) return candidates;
|
|
1942
|
+
const hint = String(providerHint || "").trim().toLowerCase();
|
|
1943
|
+
return [...candidates].sort((left, right) => {
|
|
1944
|
+
const leftScore = left?.providerId === hint ? 0 : 1;
|
|
1945
|
+
const rightScore = right?.providerId === hint ? 0 : 1;
|
|
1946
|
+
return leftScore - rightScore;
|
|
1947
|
+
});
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
function attachAmpCandidateMetadata(candidate, details) {
|
|
1951
|
+
if (!candidate) return candidate;
|
|
1952
|
+
return {
|
|
1953
|
+
...candidate,
|
|
1954
|
+
amp: {
|
|
1955
|
+
requestedModel: details.requestedModel,
|
|
1956
|
+
resolvedInputModel: details.resolvedInputModel,
|
|
1957
|
+
providerHint: details.providerHint || undefined,
|
|
1958
|
+
mappedFrom: details.mappedFrom || undefined,
|
|
1959
|
+
subagents: Array.isArray(details.ampSubagents) && details.ampSubagents.length > 0 ? details.ampSubagents : undefined,
|
|
1960
|
+
entities: Array.isArray(details.ampEntities) && details.ampEntities.length > 0 ? details.ampEntities : undefined,
|
|
1961
|
+
signatures: Array.isArray(details.ampSignatures) && details.ampSignatures.length > 0 ? details.ampSignatures : undefined
|
|
1962
|
+
}
|
|
1963
|
+
};
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
function decorateAmpResolvedRoute(route, details) {
|
|
1967
|
+
if (!route || !route.primary) return route;
|
|
1968
|
+
return {
|
|
1969
|
+
...route,
|
|
1970
|
+
routeType: `amp-${details.routeType || route.routeType || "direct"}`,
|
|
1971
|
+
routeMetadata: {
|
|
1972
|
+
...(route.routeMetadata && typeof route.routeMetadata === "object" ? route.routeMetadata : {}),
|
|
1973
|
+
amp: {
|
|
1974
|
+
requestedModel: details.requestedModel,
|
|
1975
|
+
resolvedInputModel: details.resolvedInputModel,
|
|
1976
|
+
providerHint: details.providerHint || undefined,
|
|
1977
|
+
mappedFrom: details.mappedFrom || undefined,
|
|
1978
|
+
subagents: Array.isArray(details.ampSubagents) && details.ampSubagents.length > 0 ? details.ampSubagents : undefined,
|
|
1979
|
+
entities: Array.isArray(details.ampEntities) && details.ampEntities.length > 0 ? details.ampEntities : undefined,
|
|
1980
|
+
signatures: Array.isArray(details.ampSignatures) && details.ampSignatures.length > 0 ? details.ampSignatures : undefined
|
|
1981
|
+
}
|
|
1982
|
+
},
|
|
1983
|
+
primary: attachAmpCandidateMetadata(route.primary, details),
|
|
1984
|
+
fallbacks: (route.fallbacks || []).map((candidate) => attachAmpCandidateMetadata(candidate, details))
|
|
1985
|
+
};
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
function resolveBareModelRoutePlan(config, bareModelId, normalizedRequested, sourceFormat, routingIndex, options = {}) {
|
|
1989
|
+
const exactCandidates = [];
|
|
1990
|
+
const aliasCandidates = [];
|
|
1991
|
+
const seen = new Set();
|
|
1992
|
+
|
|
1993
|
+
for (const provider of (config?.providers || [])) {
|
|
1994
|
+
if (!provider || provider.enabled === false) continue;
|
|
1995
|
+
|
|
1996
|
+
for (const model of (provider.models || [])) {
|
|
1997
|
+
if (!model || model.enabled === false) continue;
|
|
1998
|
+
const isExactMatch = model.id === bareModelId;
|
|
1999
|
+
const isAliasMatch = !isExactMatch && (model.aliases || []).includes(bareModelId);
|
|
2000
|
+
if (!isExactMatch && !isAliasMatch) continue;
|
|
2001
|
+
if (!modelSupportsProviderFormat(provider, model)) continue;
|
|
2002
|
+
|
|
2003
|
+
const candidate = buildTargetCandidate(provider, model, sourceFormat);
|
|
2004
|
+
if (seen.has(candidate.requestModelId)) continue;
|
|
2005
|
+
seen.add(candidate.requestModelId);
|
|
2006
|
+
|
|
2007
|
+
if (isExactMatch) {
|
|
2008
|
+
exactCandidates.push(candidate);
|
|
2009
|
+
} else {
|
|
2010
|
+
aliasCandidates.push(candidate);
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
const candidates = [
|
|
2016
|
+
...sortAmpCandidates(exactCandidates, options.providerHint),
|
|
2017
|
+
...sortAmpCandidates(aliasCandidates, options.providerHint)
|
|
2018
|
+
];
|
|
2019
|
+
const primary = candidates[0] || null;
|
|
2020
|
+
|
|
2021
|
+
if (!primary) {
|
|
2022
|
+
return {
|
|
2023
|
+
requestedModel: normalizedRequested,
|
|
2024
|
+
resolvedModel: null,
|
|
2025
|
+
routeType: "bare-model",
|
|
2026
|
+
routeRef: bareModelId,
|
|
2027
|
+
primary: null,
|
|
2028
|
+
fallbacks: [],
|
|
2029
|
+
error: `Model '${bareModelId}' is not configured under any enabled provider.`
|
|
2030
|
+
};
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
return {
|
|
2034
|
+
requestedModel: normalizedRequested,
|
|
2035
|
+
resolvedModel: bareModelId,
|
|
2036
|
+
routeType: "bare-model",
|
|
2037
|
+
routeRef: bareModelId,
|
|
2038
|
+
routeStrategy: "ordered",
|
|
2039
|
+
primary,
|
|
2040
|
+
fallbacks: candidates.slice(1)
|
|
2041
|
+
};
|
|
2042
|
+
}
|
|
2043
|
+
|
|
1198
2044
|
function resolveDirectRoutePlan(config, effectiveRequested, normalizedRequested, sourceFormat, routingIndex) {
|
|
1199
2045
|
const parsed = parseRouteReference(effectiveRequested);
|
|
1200
2046
|
if (parsed.type !== "direct") {
|
|
@@ -1427,6 +2273,7 @@ function resolveAliasRoutePlan(config, aliasId, normalizedRequested, sourceForma
|
|
|
1427
2273
|
|
|
1428
2274
|
const primary = primaryCandidates[0] || null;
|
|
1429
2275
|
if (!primary) {
|
|
2276
|
+
const hasConfiguredTargets = (alias.targets || []).length > 0 || (alias.fallbackTargets || []).length > 0;
|
|
1430
2277
|
return {
|
|
1431
2278
|
requestedModel: normalizedRequested,
|
|
1432
2279
|
resolvedModel: null,
|
|
@@ -1434,7 +2281,9 @@ function resolveAliasRoutePlan(config, aliasId, normalizedRequested, sourceForma
|
|
|
1434
2281
|
routeRef: aliasId,
|
|
1435
2282
|
primary: null,
|
|
1436
2283
|
fallbacks: [],
|
|
1437
|
-
error:
|
|
2284
|
+
error: hasConfiguredTargets
|
|
2285
|
+
? `Alias '${aliasId}' has no resolvable target candidates.`
|
|
2286
|
+
: `Alias '${aliasId}' has no target candidates configured.`
|
|
1438
2287
|
};
|
|
1439
2288
|
}
|
|
1440
2289
|
|
|
@@ -1450,16 +2299,512 @@ function resolveAliasRoutePlan(config, aliasId, normalizedRequested, sourceForma
|
|
|
1450
2299
|
};
|
|
1451
2300
|
}
|
|
1452
2301
|
|
|
1453
|
-
|
|
2302
|
+
function resolveRequestedRouteCore(config, effectiveRequested, normalizedRequested, sourceFormat, routingIndex) {
|
|
2303
|
+
const parsed = parseRouteReference(effectiveRequested);
|
|
2304
|
+
if (parsed.type === "alias") {
|
|
2305
|
+
return resolveAliasRoutePlan(config, parsed.aliasId, normalizedRequested, sourceFormat, routingIndex);
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
return resolveDirectRoutePlan(
|
|
2309
|
+
config,
|
|
2310
|
+
effectiveRequested,
|
|
2311
|
+
normalizedRequested,
|
|
2312
|
+
sourceFormat,
|
|
2313
|
+
routingIndex
|
|
2314
|
+
);
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2317
|
+
function resolveAmpMappedModel(config, requestedModel) {
|
|
2318
|
+
const mappings = Array.isArray(config?.amp?.modelMappings)
|
|
2319
|
+
? config.amp.modelMappings
|
|
2320
|
+
: [];
|
|
2321
|
+
|
|
2322
|
+
for (const mapping of mappings) {
|
|
2323
|
+
if (!mapping || typeof mapping !== "object") continue;
|
|
2324
|
+
if (!matchAmpModelPattern(mapping.from, requestedModel)) continue;
|
|
2325
|
+
const target = String(mapping.to || "").trim();
|
|
2326
|
+
if (target) return target;
|
|
2327
|
+
}
|
|
2328
|
+
|
|
2329
|
+
return "";
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2332
|
+
export const DEFAULT_AMP_SIGNATURE_DEFINITIONS = Object.freeze([
|
|
2333
|
+
Object.freeze({
|
|
2334
|
+
id: "@anthropic-opus",
|
|
2335
|
+
description: "Anthropic Opus family used by AMP smart-like flows.",
|
|
2336
|
+
match: [{ vendor: "anthropic", family: "opus", variantAbsent: true }],
|
|
2337
|
+
defaultMatch: "claude-opus-{number}"
|
|
2338
|
+
}),
|
|
2339
|
+
Object.freeze({
|
|
2340
|
+
id: "@anthropic-sonnet",
|
|
2341
|
+
description: "Anthropic Sonnet family used by AMP librarian-like flows.",
|
|
2342
|
+
match: [{ vendor: "anthropic", family: "sonnet", variantAbsent: true }],
|
|
2343
|
+
defaultMatch: "claude-sonnet-{number}"
|
|
2344
|
+
}),
|
|
2345
|
+
Object.freeze({
|
|
2346
|
+
id: "@anthropic-haiku-shared",
|
|
2347
|
+
description: "Anthropic Haiku family shared by AMP rush/title-like flows.",
|
|
2348
|
+
match: [{ vendor: "anthropic", family: "haiku", variantAbsent: true }],
|
|
2349
|
+
defaultMatch: "claude-haiku-{number}"
|
|
2350
|
+
}),
|
|
2351
|
+
Object.freeze({
|
|
2352
|
+
id: "@openai-gpt-base",
|
|
2353
|
+
description: "Base GPT family currently observed for AMP Oracle-like flows.",
|
|
2354
|
+
match: [{ vendor: "openai", family: "gpt", variantAbsent: true }],
|
|
2355
|
+
defaultMatch: "gpt-{number}"
|
|
2356
|
+
}),
|
|
2357
|
+
Object.freeze({
|
|
2358
|
+
id: "@openai-gpt-codex",
|
|
2359
|
+
description: "OpenAI GPT Codex family used by AMP deep-like flows.",
|
|
2360
|
+
match: [{ vendor: "openai", family: "gpt", variantPrefix: "codex" }, "gpt-*-codex*"],
|
|
2361
|
+
defaultMatch: "gpt-*-codex*"
|
|
2362
|
+
}),
|
|
2363
|
+
Object.freeze({
|
|
2364
|
+
id: "@google-gemini-pro",
|
|
2365
|
+
description: "Google Gemini Pro family used by AMP review-like flows.",
|
|
2366
|
+
match: [{ vendor: "google", family: "gemini", variant: "pro" }, "gemini-*-pro*"],
|
|
2367
|
+
defaultMatch: "gemini-*-pro*"
|
|
2368
|
+
}),
|
|
2369
|
+
Object.freeze({
|
|
2370
|
+
id: "@google-gemini-pro-image",
|
|
2371
|
+
description: "Google Gemini image family used by AMP painter-like flows.",
|
|
2372
|
+
match: ["gemini-3-pro-image", "gemini-3-pro-image*", "gemini-*-image*"],
|
|
2373
|
+
defaultMatch: "gemini-*-image*"
|
|
2374
|
+
}),
|
|
2375
|
+
Object.freeze({
|
|
2376
|
+
id: "@google-gemini-flash-shared",
|
|
2377
|
+
description: "Google Gemini Flash family shared by AMP search/look-at/handoff-like flows.",
|
|
2378
|
+
match: [{ vendor: "google", family: "gemini", variantPrefix: "flash" }, "gemini-*-flash*"],
|
|
2379
|
+
defaultMatch: "gemini-*-flash*"
|
|
2380
|
+
})
|
|
2381
|
+
]);
|
|
2382
|
+
|
|
2383
|
+
export const DEFAULT_AMP_ENTITY_DEFINITIONS = Object.freeze([
|
|
2384
|
+
Object.freeze({ id: "smart", type: "mode", description: "Unconstrained state-of-the-art model use", signatures: ["@anthropic-opus"] }),
|
|
2385
|
+
Object.freeze({ id: "rush", type: "mode", description: "Faster and cheaper, for small well-defined tasks", signatures: ["@anthropic-haiku-shared"] }),
|
|
2386
|
+
Object.freeze({ id: "deep", type: "mode", description: "Deep reasoning with extended thinking", signatures: ["@openai-gpt-codex"] }),
|
|
2387
|
+
Object.freeze({ id: "review", type: "feature", description: "Bug identification and code review assistance", signatures: ["@google-gemini-pro"] }),
|
|
2388
|
+
Object.freeze({ id: "search", type: "agent", description: "Fast, accurate codebase retrieval", signatures: ["@google-gemini-flash-shared"] }),
|
|
2389
|
+
Object.freeze({ id: "oracle", type: "agent", description: "Complex reasoning and planning on code", signatures: ["@openai-gpt-base"] }),
|
|
2390
|
+
Object.freeze({ id: "librarian", type: "agent", description: "Large-scale retrieval and research on external code", signatures: ["@anthropic-sonnet"] }),
|
|
2391
|
+
Object.freeze({ id: "look-at", type: "system", description: "Image, PDF, and media file analysis", aliases: ["look at", "lookat"], signatures: ["@google-gemini-flash-shared"] }),
|
|
2392
|
+
Object.freeze({ id: "painter", type: "system", description: "Image generation and editing", signatures: ["@google-gemini-pro-image"] }),
|
|
2393
|
+
Object.freeze({ id: "handoff", type: "system", description: "Fallback context analysis for task continuation", signatures: ["@google-gemini-flash-shared"] }),
|
|
2394
|
+
Object.freeze({ id: "title", type: "system", description: "Fast title generation for threads", aliases: ["titling"], signatures: ["@anthropic-haiku-shared"] })
|
|
2395
|
+
]);
|
|
2396
|
+
|
|
2397
|
+
export const DEFAULT_AMP_SUBAGENT_DEFINITIONS = Object.freeze([
|
|
2398
|
+
Object.freeze({ id: "oracle", patterns: ["/^gpt-\\d+(?:\\.\\d+)?$/"] }),
|
|
2399
|
+
Object.freeze({ id: "librarian", patterns: ["/^(?:claude-)?sonnet-\\d+(?:\\.\\d+)?$/"] }),
|
|
2400
|
+
Object.freeze({ id: "title", patterns: ["/^(?:claude-)?haiku-\\d+(?:\\.\\d+)?$/"] }),
|
|
2401
|
+
Object.freeze({ id: "painter", patterns: ["gemini-3-pro-image", "gemini-3-pro-image*"] }),
|
|
2402
|
+
Object.freeze({ id: "search", patterns: ["gemini-2.5-flash", "gemini-2.5-flash*", "gemini-3-flash", "gemini-3-flash*"] }),
|
|
2403
|
+
Object.freeze({ id: "look-at", patterns: ["gemini-2.5-flash", "gemini-2.5-flash*", "gemini-3-flash", "gemini-3-flash*"] }),
|
|
2404
|
+
Object.freeze({ id: "handoff", patterns: ["gemini-3-flash", "gemini-3-flash*"] })
|
|
2405
|
+
]);
|
|
2406
|
+
|
|
2407
|
+
function getAmpSubagentDefinitions(config) {
|
|
2408
|
+
const configuredDefinitions = Array.isArray(config?.amp?.subagentDefinitions)
|
|
2409
|
+
? config.amp.subagentDefinitions
|
|
2410
|
+
: undefined;
|
|
2411
|
+
if (configuredDefinitions !== undefined) return configuredDefinitions;
|
|
2412
|
+
return DEFAULT_AMP_SUBAGENT_DEFINITIONS;
|
|
2413
|
+
}
|
|
2414
|
+
|
|
2415
|
+
function ampHasNewRoutingSchema(config) {
|
|
2416
|
+
const amp = config?.amp;
|
|
2417
|
+
return Boolean(
|
|
2418
|
+
amp
|
|
2419
|
+
&& (
|
|
2420
|
+
hasOwn(amp, "preset")
|
|
2421
|
+
|| hasOwn(amp, "defaultRoute")
|
|
2422
|
+
|| hasOwn(amp, "routes")
|
|
2423
|
+
|| hasOwn(amp, "rawModelRoutes")
|
|
2424
|
+
|| hasOwn(amp, "overrides")
|
|
2425
|
+
|| hasOwn(amp, "fallback")
|
|
2426
|
+
)
|
|
2427
|
+
);
|
|
2428
|
+
}
|
|
2429
|
+
|
|
2430
|
+
function mergeAmpDefinitionEntries(baseEntries, overrideEntries = []) {
|
|
2431
|
+
const merged = new Map();
|
|
2432
|
+
for (const entry of baseEntries || []) {
|
|
2433
|
+
if (!entry?.id) continue;
|
|
2434
|
+
merged.set(entry.id, { ...entry });
|
|
2435
|
+
}
|
|
2436
|
+
|
|
2437
|
+
for (const entry of overrideEntries || []) {
|
|
2438
|
+
if (!entry?.id) continue;
|
|
2439
|
+
if (entry.enabled === false) {
|
|
2440
|
+
merged.delete(entry.id);
|
|
2441
|
+
continue;
|
|
2442
|
+
}
|
|
2443
|
+
const current = merged.get(entry.id) || {};
|
|
2444
|
+
merged.set(entry.id, {
|
|
2445
|
+
...current,
|
|
2446
|
+
...entry,
|
|
2447
|
+
...(current.aliases || entry.aliases
|
|
2448
|
+
? { aliases: dedupeStrings([...(current.aliases || []), ...(entry.aliases || [])]) }
|
|
2449
|
+
: {}),
|
|
2450
|
+
...(current.signatures || entry.signatures
|
|
2451
|
+
? { signatures: dedupeStrings([...(current.signatures || []), ...(entry.signatures || [])]) }
|
|
2452
|
+
: {}),
|
|
2453
|
+
...(entry.match !== undefined
|
|
2454
|
+
? { match: entry.match }
|
|
2455
|
+
: (current.match !== undefined ? { match: current.match } : {}))
|
|
2456
|
+
});
|
|
2457
|
+
}
|
|
2458
|
+
|
|
2459
|
+
return [...merged.values()];
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2462
|
+
function getAmpPresetEntityDefinitions(config) {
|
|
2463
|
+
const preset = normalizeAmpPreset(config?.amp?.preset) || "builtin";
|
|
2464
|
+
if (preset === "none") return [];
|
|
2465
|
+
return DEFAULT_AMP_ENTITY_DEFINITIONS;
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2468
|
+
function getAmpPresetSignatureDefinitions(config) {
|
|
2469
|
+
const preset = normalizeAmpPreset(config?.amp?.preset) || "builtin";
|
|
2470
|
+
if (preset === "none") return [];
|
|
2471
|
+
return DEFAULT_AMP_SIGNATURE_DEFINITIONS;
|
|
2472
|
+
}
|
|
2473
|
+
|
|
2474
|
+
function getAmpEntityDefinitions(config) {
|
|
2475
|
+
return mergeAmpDefinitionEntries(
|
|
2476
|
+
getAmpPresetEntityDefinitions(config),
|
|
2477
|
+
config?.amp?.overrides?.entities
|
|
2478
|
+
);
|
|
2479
|
+
}
|
|
2480
|
+
|
|
2481
|
+
function getAmpSignatureDefinitions(config) {
|
|
2482
|
+
return mergeAmpDefinitionEntries(
|
|
2483
|
+
getAmpPresetSignatureDefinitions(config),
|
|
2484
|
+
config?.amp?.overrides?.signatures
|
|
2485
|
+
);
|
|
2486
|
+
}
|
|
2487
|
+
|
|
2488
|
+
function findAmpMatchingSignatures(config, requestedModel) {
|
|
2489
|
+
const descriptor = parseAmpModelDescriptor(requestedModel);
|
|
2490
|
+
return getAmpSignatureDefinitions(config)
|
|
2491
|
+
.filter((entry) => Array.isArray(entry?.match) && entry.match.some((selector) => matchAmpModelSelector(selector, requestedModel, descriptor)));
|
|
2492
|
+
}
|
|
2493
|
+
|
|
2494
|
+
function findAmpMatchingEntities(config, requestedModel, matchingSignatures) {
|
|
2495
|
+
const descriptor = parseAmpModelDescriptor(requestedModel);
|
|
2496
|
+
const matchedSignatureIds = new Set((matchingSignatures || []).map((entry) => entry.id));
|
|
2497
|
+
return getAmpEntityDefinitions(config)
|
|
2498
|
+
.filter((entry) => {
|
|
2499
|
+
const directMatch = Array.isArray(entry?.match) && entry.match.some((selector) => matchAmpModelSelector(selector, requestedModel, descriptor));
|
|
2500
|
+
const signatureMatch = Array.isArray(entry?.signatures) && entry.signatures.some((id) => matchedSignatureIds.has(id));
|
|
2501
|
+
return directMatch || signatureMatch;
|
|
2502
|
+
});
|
|
2503
|
+
}
|
|
2504
|
+
|
|
2505
|
+
function resolveAmpConfiguredDefinitionTarget(definitions, routes, propertyName = "route") {
|
|
2506
|
+
const activeDefinitions = Array.isArray(definitions) ? definitions : [];
|
|
2507
|
+
if (activeDefinitions.length === 0) return { target: "", ambiguous: false };
|
|
2508
|
+
|
|
2509
|
+
const configuredMatches = activeDefinitions
|
|
2510
|
+
.map((entry) => ({ id: entry.id, target: String(entry?.[propertyName] || routes?.[entry.id] || "").trim() }))
|
|
2511
|
+
.filter((entry) => entry.target);
|
|
2512
|
+
|
|
2513
|
+
if (configuredMatches.length === 0) {
|
|
2514
|
+
return { target: "", ambiguous: false };
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2517
|
+
const uniqueTargets = [...new Set(configuredMatches.map((entry) => entry.target))];
|
|
2518
|
+
if (activeDefinitions.length > 1 && configuredMatches.length !== activeDefinitions.length) {
|
|
2519
|
+
return { target: "", ambiguous: true };
|
|
2520
|
+
}
|
|
2521
|
+
if (uniqueTargets.length !== 1) {
|
|
2522
|
+
return { target: "", ambiguous: true };
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2525
|
+
return {
|
|
2526
|
+
target: uniqueTargets[0],
|
|
2527
|
+
ambiguous: false
|
|
2528
|
+
};
|
|
2529
|
+
}
|
|
2530
|
+
|
|
2531
|
+
function resolveAmpRawModelMappedTarget(config, requestedModel) {
|
|
2532
|
+
const mappings = Array.isArray(config?.amp?.rawModelRoutes)
|
|
2533
|
+
? config.amp.rawModelRoutes
|
|
2534
|
+
: [];
|
|
2535
|
+
|
|
2536
|
+
for (const mapping of mappings) {
|
|
2537
|
+
if (!mapping || typeof mapping !== "object") continue;
|
|
2538
|
+
if (!matchAmpModelPattern(mapping.from, requestedModel)) continue;
|
|
2539
|
+
const target = String(mapping.to || "").trim();
|
|
2540
|
+
if (target) return target;
|
|
2541
|
+
}
|
|
2542
|
+
|
|
2543
|
+
return "";
|
|
2544
|
+
}
|
|
2545
|
+
|
|
2546
|
+
function getAmpFallbackAction(config, { ambiguous = false } = {}) {
|
|
2547
|
+
const fallback = config?.amp?.fallback && typeof config.amp.fallback === "object"
|
|
2548
|
+
? config.amp.fallback
|
|
2549
|
+
: {};
|
|
2550
|
+
return ambiguous
|
|
2551
|
+
? (fallback.onAmbiguous || undefined)
|
|
2552
|
+
: (fallback.onUnknown || undefined);
|
|
2553
|
+
}
|
|
2554
|
+
|
|
2555
|
+
function shouldAllowAmpUpstreamProxy(config, fallbackAction) {
|
|
2556
|
+
const proxyUpstream = config?.amp?.fallback?.proxyUpstream;
|
|
2557
|
+
if (fallbackAction === "none") return false;
|
|
2558
|
+
if (proxyUpstream === false) return false;
|
|
2559
|
+
return true;
|
|
2560
|
+
}
|
|
2561
|
+
|
|
2562
|
+
function buildAmpUnresolvedRoute(normalizedRequested, error, { allowAmpProxy = true } = {}) {
|
|
2563
|
+
return {
|
|
2564
|
+
requestedModel: normalizedRequested,
|
|
2565
|
+
resolvedModel: null,
|
|
2566
|
+
routeType: "unknown",
|
|
2567
|
+
routeRef: null,
|
|
2568
|
+
primary: null,
|
|
2569
|
+
fallbacks: [],
|
|
2570
|
+
error,
|
|
2571
|
+
allowAmpProxy
|
|
2572
|
+
};
|
|
2573
|
+
}
|
|
2574
|
+
|
|
2575
|
+
function resolveAmpSubagentMappedModel(config, requestedModel) {
|
|
2576
|
+
const mappings = config?.amp?.subagentMappings && typeof config.amp.subagentMappings === "object"
|
|
2577
|
+
? config.amp.subagentMappings
|
|
2578
|
+
: {};
|
|
2579
|
+
const matchingProfiles = getAmpSubagentDefinitions(config)
|
|
2580
|
+
.filter((profile) => Array.isArray(profile?.patterns) && profile.patterns.some((pattern) => matchAmpModelPattern(pattern, requestedModel)));
|
|
2581
|
+
if (matchingProfiles.length === 0) return { target: "", subagents: [], ambiguous: false };
|
|
2582
|
+
|
|
2583
|
+
const configuredMatches = matchingProfiles
|
|
2584
|
+
.map((profile) => ({ subagent: profile.id, target: String(mappings[profile.id] || "").trim() }))
|
|
2585
|
+
.filter((entry) => entry.target);
|
|
2586
|
+
|
|
2587
|
+
if (configuredMatches.length === 0) {
|
|
2588
|
+
return { target: "", subagents: matchingProfiles.map((profile) => profile.id), ambiguous: false };
|
|
2589
|
+
}
|
|
2590
|
+
|
|
2591
|
+
const uniqueTargets = [...new Set(configuredMatches.map((entry) => entry.target))];
|
|
2592
|
+
if (matchingProfiles.length > 1 && configuredMatches.length !== matchingProfiles.length) {
|
|
2593
|
+
return { target: "", subagents: matchingProfiles.map((profile) => profile.id), ambiguous: true };
|
|
2594
|
+
}
|
|
2595
|
+
if (uniqueTargets.length !== 1) {
|
|
2596
|
+
return { target: "", subagents: matchingProfiles.map((profile) => profile.id), ambiguous: true };
|
|
2597
|
+
}
|
|
2598
|
+
|
|
2599
|
+
return {
|
|
2600
|
+
target: uniqueTargets[0],
|
|
2601
|
+
subagents: matchingProfiles.map((profile) => profile.id),
|
|
2602
|
+
ambiguous: false
|
|
2603
|
+
};
|
|
2604
|
+
}
|
|
2605
|
+
|
|
2606
|
+
function resolveAmpRequestedRoute(config, effectiveRequested, normalizedRequested, sourceFormat, routingIndex, options = {}) {
|
|
2607
|
+
const providerHint = String(options.providerHint || "").trim().toLowerCase();
|
|
2608
|
+
const forceModelMappings = config?.amp?.forceModelMappings === true;
|
|
2609
|
+
|
|
2610
|
+
const resolveLocalRoute = (targetModel, details = {}) => {
|
|
2611
|
+
const mappedFrom = String(details.mappedFrom || "").trim();
|
|
2612
|
+
const routeTypeOverride = String(details.routeTypeOverride || "").trim();
|
|
2613
|
+
const ampSubagents = Array.isArray(details.ampSubagents) ? details.ampSubagents.filter(Boolean) : [];
|
|
2614
|
+
const ampEntities = Array.isArray(details.ampEntities) ? details.ampEntities.filter(Boolean) : [];
|
|
2615
|
+
const ampSignatures = Array.isArray(details.ampSignatures) ? details.ampSignatures.filter(Boolean) : [];
|
|
2616
|
+
const coreRoute = resolveRequestedRouteCore(config, targetModel, normalizedRequested, sourceFormat, routingIndex);
|
|
2617
|
+
if (coreRoute?.primary) {
|
|
2618
|
+
return decorateAmpResolvedRoute(coreRoute, {
|
|
2619
|
+
requestedModel: normalizedRequested,
|
|
2620
|
+
resolvedInputModel: targetModel,
|
|
2621
|
+
providerHint,
|
|
2622
|
+
mappedFrom,
|
|
2623
|
+
ampSubagents,
|
|
2624
|
+
ampEntities,
|
|
2625
|
+
ampSignatures,
|
|
2626
|
+
routeType: routeTypeOverride || coreRoute.routeType
|
|
2627
|
+
});
|
|
2628
|
+
}
|
|
2629
|
+
|
|
2630
|
+
if (!String(targetModel || "").includes("/")) {
|
|
2631
|
+
const bareRoute = resolveBareModelRoutePlan(config, targetModel, normalizedRequested, sourceFormat, routingIndex, {
|
|
2632
|
+
providerHint
|
|
2633
|
+
});
|
|
2634
|
+
if (bareRoute?.primary) {
|
|
2635
|
+
return decorateAmpResolvedRoute(bareRoute, {
|
|
2636
|
+
requestedModel: normalizedRequested,
|
|
2637
|
+
resolvedInputModel: targetModel,
|
|
2638
|
+
providerHint,
|
|
2639
|
+
mappedFrom,
|
|
2640
|
+
ampSubagents,
|
|
2641
|
+
ampEntities,
|
|
2642
|
+
ampSignatures,
|
|
2643
|
+
routeType: routeTypeOverride || bareRoute.routeType
|
|
2644
|
+
});
|
|
2645
|
+
}
|
|
2646
|
+
}
|
|
2647
|
+
|
|
2648
|
+
return coreRoute;
|
|
2649
|
+
};
|
|
2650
|
+
|
|
2651
|
+
const localRoute = resolveLocalRoute(effectiveRequested);
|
|
2652
|
+
|
|
2653
|
+
if (ampHasNewRoutingSchema(config)) {
|
|
2654
|
+
const matchingSignatures = findAmpMatchingSignatures(config, effectiveRequested);
|
|
2655
|
+
const matchingEntities = findAmpMatchingEntities(config, effectiveRequested, matchingSignatures);
|
|
2656
|
+
const signatureIds = matchingSignatures.map((entry) => entry.id);
|
|
2657
|
+
const entityIds = matchingEntities.map((entry) => entry.id);
|
|
2658
|
+
const routes = config?.amp?.routes && typeof config.amp.routes === "object"
|
|
2659
|
+
? config.amp.routes
|
|
2660
|
+
: {};
|
|
2661
|
+
const entityTarget = resolveAmpConfiguredDefinitionTarget(matchingEntities, routes);
|
|
2662
|
+
const signatureTarget = resolveAmpConfiguredDefinitionTarget(matchingSignatures, routes);
|
|
2663
|
+
const rawMappedTarget = resolveAmpRawModelMappedTarget(config, effectiveRequested);
|
|
2664
|
+
|
|
2665
|
+
const entityRoute = entityTarget.target
|
|
2666
|
+
? resolveLocalRoute(entityTarget.target, {
|
|
2667
|
+
mappedFrom: effectiveRequested,
|
|
2668
|
+
ampSubagents: entityIds,
|
|
2669
|
+
ampEntities: entityIds,
|
|
2670
|
+
ampSignatures: signatureIds,
|
|
2671
|
+
routeTypeOverride: "entity"
|
|
2672
|
+
})
|
|
2673
|
+
: null;
|
|
2674
|
+
const signatureRoute = signatureTarget.target
|
|
2675
|
+
? resolveLocalRoute(signatureTarget.target, {
|
|
2676
|
+
mappedFrom: effectiveRequested,
|
|
2677
|
+
ampSubagents: entityIds,
|
|
2678
|
+
ampEntities: entityIds,
|
|
2679
|
+
ampSignatures: signatureIds,
|
|
2680
|
+
routeTypeOverride: "signature"
|
|
2681
|
+
})
|
|
2682
|
+
: null;
|
|
2683
|
+
const rawMappedRoute = rawMappedTarget && rawMappedTarget !== effectiveRequested
|
|
2684
|
+
? resolveLocalRoute(rawMappedTarget, {
|
|
2685
|
+
mappedFrom: effectiveRequested,
|
|
2686
|
+
ampSubagents: entityIds,
|
|
2687
|
+
ampEntities: entityIds,
|
|
2688
|
+
ampSignatures: signatureIds,
|
|
2689
|
+
routeTypeOverride: "raw-model-route"
|
|
2690
|
+
})
|
|
2691
|
+
: null;
|
|
2692
|
+
|
|
2693
|
+
const ambiguous = entityTarget.ambiguous || signatureTarget.ambiguous;
|
|
2694
|
+
const fallbackAction = getAmpFallbackAction(config, { ambiguous });
|
|
2695
|
+
const ampDefaultTarget = String(config?.amp?.defaultRoute || "").trim();
|
|
2696
|
+
const globalDefaultTarget = getDefaultRouteReference(config, routingIndex);
|
|
2697
|
+
const selectedDefaultTarget = fallbackAction === "none" || fallbackAction === "upstream"
|
|
2698
|
+
? ""
|
|
2699
|
+
: (fallbackAction === "default-model"
|
|
2700
|
+
? globalDefaultTarget
|
|
2701
|
+
: (ampDefaultTarget || globalDefaultTarget));
|
|
2702
|
+
const selectedDefaultRouteType = fallbackAction === "default-model"
|
|
2703
|
+
? "default-model"
|
|
2704
|
+
: (ampDefaultTarget ? "default-route" : "default-model");
|
|
2705
|
+
const shouldFallbackToDefault = Boolean(
|
|
2706
|
+
selectedDefaultTarget
|
|
2707
|
+
&& selectedDefaultTarget !== effectiveRequested
|
|
2708
|
+
&& selectedDefaultTarget !== entityTarget.target
|
|
2709
|
+
&& selectedDefaultTarget !== signatureTarget.target
|
|
2710
|
+
&& selectedDefaultTarget !== rawMappedTarget
|
|
2711
|
+
&& !entityRoute?.primary
|
|
2712
|
+
&& !signatureRoute?.primary
|
|
2713
|
+
&& !rawMappedRoute?.primary
|
|
2714
|
+
&& !localRoute?.primary
|
|
2715
|
+
);
|
|
2716
|
+
const defaultRoute = shouldFallbackToDefault
|
|
2717
|
+
? resolveLocalRoute(selectedDefaultTarget, {
|
|
2718
|
+
mappedFrom: effectiveRequested,
|
|
2719
|
+
ampSubagents: entityIds,
|
|
2720
|
+
ampEntities: entityIds,
|
|
2721
|
+
ampSignatures: signatureIds,
|
|
2722
|
+
routeTypeOverride: selectedDefaultRouteType
|
|
2723
|
+
})
|
|
2724
|
+
: null;
|
|
2725
|
+
|
|
2726
|
+
if (entityRoute?.primary) return entityRoute;
|
|
2727
|
+
if (signatureRoute?.primary) return signatureRoute;
|
|
2728
|
+
|
|
2729
|
+
if (forceModelMappings) {
|
|
2730
|
+
if (rawMappedRoute?.primary) return rawMappedRoute;
|
|
2731
|
+
if (localRoute?.primary) return localRoute;
|
|
2732
|
+
} else {
|
|
2733
|
+
if (localRoute?.primary) return localRoute;
|
|
2734
|
+
if (rawMappedRoute?.primary) return rawMappedRoute;
|
|
2735
|
+
}
|
|
2736
|
+
|
|
2737
|
+
if (defaultRoute?.primary) return defaultRoute;
|
|
2738
|
+
|
|
2739
|
+
const allowAmpProxy = shouldAllowAmpUpstreamProxy(config, fallbackAction);
|
|
2740
|
+
const matchedLabel = ambiguous
|
|
2741
|
+
? `Matched AMP entities/signatures ambiguously (${entityIds.join(", ") || "none"}; ${signatureIds.join(", ") || "none"}).`
|
|
2742
|
+
: `No AMP route matched '${effectiveRequested}'.`;
|
|
2743
|
+
|
|
2744
|
+
const unresolvedCandidate = defaultRoute || rawMappedRoute || signatureRoute || entityRoute || localRoute;
|
|
2745
|
+
if (unresolvedCandidate) {
|
|
2746
|
+
return {
|
|
2747
|
+
...unresolvedCandidate,
|
|
2748
|
+
error: unresolvedCandidate.error || matchedLabel,
|
|
2749
|
+
allowAmpProxy
|
|
2750
|
+
};
|
|
2751
|
+
}
|
|
2752
|
+
|
|
2753
|
+
return buildAmpUnresolvedRoute(normalizedRequested, matchedLabel, { allowAmpProxy });
|
|
2754
|
+
}
|
|
2755
|
+
|
|
2756
|
+
const defaultAmpTarget = getDefaultRouteReference(config, routingIndex);
|
|
2757
|
+
const subagentMapped = resolveAmpSubagentMappedModel(config, effectiveRequested);
|
|
2758
|
+
const subagentRoute = subagentMapped.target
|
|
2759
|
+
? resolveLocalRoute(subagentMapped.target, {
|
|
2760
|
+
mappedFrom: effectiveRequested,
|
|
2761
|
+
ampSubagents: subagentMapped.subagents,
|
|
2762
|
+
routeTypeOverride: "subagent"
|
|
2763
|
+
})
|
|
2764
|
+
: null;
|
|
2765
|
+
const mappedModel = resolveAmpMappedModel(config, effectiveRequested);
|
|
2766
|
+
const mappedRoute = mappedModel && mappedModel !== effectiveRequested
|
|
2767
|
+
? resolveLocalRoute(mappedModel, { mappedFrom: effectiveRequested, routeTypeOverride: "mapped" })
|
|
2768
|
+
: null;
|
|
2769
|
+
const shouldFallbackToDefault = Boolean(
|
|
2770
|
+
defaultAmpTarget
|
|
2771
|
+
&& defaultAmpTarget !== effectiveRequested
|
|
2772
|
+
&& defaultAmpTarget !== mappedModel
|
|
2773
|
+
&& defaultAmpTarget !== subagentMapped.target
|
|
2774
|
+
&& !localRoute?.primary
|
|
2775
|
+
&& !subagentRoute?.primary
|
|
2776
|
+
&& !mappedRoute?.primary
|
|
2777
|
+
);
|
|
2778
|
+
const defaultRoute = shouldFallbackToDefault
|
|
2779
|
+
? resolveLocalRoute(defaultAmpTarget, { mappedFrom: effectiveRequested, routeTypeOverride: "default-model" })
|
|
2780
|
+
: null;
|
|
2781
|
+
|
|
2782
|
+
if (forceModelMappings) {
|
|
2783
|
+
if (subagentRoute?.primary) return subagentRoute;
|
|
2784
|
+
if (mappedRoute?.primary) return mappedRoute;
|
|
2785
|
+
if (localRoute?.primary) return localRoute;
|
|
2786
|
+
return defaultRoute || localRoute;
|
|
2787
|
+
}
|
|
2788
|
+
|
|
2789
|
+
if (subagentRoute?.primary) return subagentRoute;
|
|
2790
|
+
if (localRoute?.primary) return localRoute;
|
|
2791
|
+
if (mappedRoute?.primary) return mappedRoute;
|
|
2792
|
+
return defaultRoute || mappedRoute || subagentRoute || localRoute;
|
|
2793
|
+
}
|
|
2794
|
+
|
|
2795
|
+
export function resolveRequestedRoute(config, requestedModel, sourceFormat = FORMATS.CLAUDE, options = {}) {
|
|
2796
|
+
const routingIndex = getRoutingIndex(config);
|
|
1454
2797
|
const normalizedRequested = typeof requestedModel === "string" && requestedModel.trim()
|
|
1455
2798
|
? requestedModel.trim()
|
|
1456
2799
|
: "smart";
|
|
1457
|
-
const
|
|
2800
|
+
const smartRouteReference = normalizedRequested === "smart"
|
|
2801
|
+
? getSmartRouteReference(config, routingIndex)
|
|
2802
|
+
: "";
|
|
1458
2803
|
const effectiveRequested = normalizedRequested === "smart"
|
|
1459
|
-
?
|
|
2804
|
+
? (smartRouteReference || "smart")
|
|
1460
2805
|
: normalizedRequested;
|
|
1461
2806
|
|
|
1462
|
-
if (
|
|
2807
|
+
if (normalizedRequested === "smart" && !smartRouteReference) {
|
|
1463
2808
|
return {
|
|
1464
2809
|
requestedModel: normalizedRequested,
|
|
1465
2810
|
resolvedModel: null,
|
|
@@ -1471,23 +2816,36 @@ export function resolveRequestedRoute(config, requestedModel, sourceFormat = FOR
|
|
|
1471
2816
|
};
|
|
1472
2817
|
}
|
|
1473
2818
|
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
2819
|
+
if (options?.clientType === "amp") {
|
|
2820
|
+
const resolvedAmpRoute = resolveAmpRequestedRoute(
|
|
2821
|
+
config,
|
|
2822
|
+
effectiveRequested,
|
|
2823
|
+
normalizedRequested,
|
|
2824
|
+
sourceFormat,
|
|
2825
|
+
routingIndex,
|
|
2826
|
+
options
|
|
2827
|
+
);
|
|
2828
|
+
if (!resolvedAmpRoute?.primary && effectiveRequested === DEFAULT_MODEL_ALIAS_ID) {
|
|
2829
|
+
return {
|
|
2830
|
+
...resolvedAmpRoute,
|
|
2831
|
+
statusCode: 500
|
|
2832
|
+
};
|
|
2833
|
+
}
|
|
2834
|
+
return resolvedAmpRoute;
|
|
1478
2835
|
}
|
|
1479
2836
|
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
2837
|
+
const resolvedRoute = resolveRequestedRouteCore(config, effectiveRequested, normalizedRequested, sourceFormat, routingIndex);
|
|
2838
|
+
if (!resolvedRoute?.primary && effectiveRequested === DEFAULT_MODEL_ALIAS_ID) {
|
|
2839
|
+
return {
|
|
2840
|
+
...resolvedRoute,
|
|
2841
|
+
statusCode: 500
|
|
2842
|
+
};
|
|
2843
|
+
}
|
|
2844
|
+
return resolvedRoute;
|
|
1487
2845
|
}
|
|
1488
2846
|
|
|
1489
|
-
export function resolveRequestModel(config, requestedModel, sourceFormat = FORMATS.CLAUDE) {
|
|
1490
|
-
return resolveRequestedRoute(config, requestedModel, sourceFormat);
|
|
2847
|
+
export function resolveRequestModel(config, requestedModel, sourceFormat = FORMATS.CLAUDE, options = {}) {
|
|
2848
|
+
return resolveRequestedRoute(config, requestedModel, sourceFormat, options);
|
|
1491
2849
|
}
|
|
1492
2850
|
|
|
1493
2851
|
export function listConfiguredModels(config, { endpointFormat } = {}) {
|
|
@@ -1499,6 +2857,7 @@ export function listConfiguredModels(config, { endpointFormat } = {}) {
|
|
|
1499
2857
|
|
|
1500
2858
|
for (const model of (provider.models || [])) {
|
|
1501
2859
|
if (model.enabled === false) continue;
|
|
2860
|
+
const modelFormats = getRuntimeModelFormats(provider, model);
|
|
1502
2861
|
|
|
1503
2862
|
rows.push({
|
|
1504
2863
|
id: `${provider.id}/${model.id}`,
|
|
@@ -1507,13 +2866,13 @@ export function listConfiguredModels(config, { endpointFormat } = {}) {
|
|
|
1507
2866
|
owned_by: provider.id,
|
|
1508
2867
|
provider_id: provider.id,
|
|
1509
2868
|
provider_name: provider.name,
|
|
1510
|
-
formats:
|
|
2869
|
+
formats: modelFormats.length > 0 ? modelFormats : (provider.formats || []),
|
|
1511
2870
|
endpoint_format_supported: endpointFormat
|
|
1512
|
-
? (
|
|
2871
|
+
? (modelFormats.length > 0 ? modelFormats.includes(endpointFormat) : (provider.formats || []).includes(endpointFormat))
|
|
1513
2872
|
: undefined,
|
|
1514
2873
|
context_window: model.contextWindow,
|
|
1515
2874
|
cost: model.cost,
|
|
1516
|
-
model_formats:
|
|
2875
|
+
model_formats: modelFormats,
|
|
1517
2876
|
fallback_models: model.fallbackModels || []
|
|
1518
2877
|
});
|
|
1519
2878
|
}
|