@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.
Files changed (43) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +337 -41
  3. package/package.json +19 -3
  4. package/src/cli/router-module.js +7331 -3805
  5. package/src/cli/wrangler-toml.js +1 -1
  6. package/src/cli-entry.js +162 -24
  7. package/src/node/amp-client-config.js +426 -0
  8. package/src/node/coding-tool-config.js +763 -0
  9. package/src/node/config-store.js +49 -18
  10. package/src/node/instance-state.js +213 -12
  11. package/src/node/listen-port.js +5 -37
  12. package/src/node/local-server-settings.js +122 -0
  13. package/src/node/local-server.js +3 -2
  14. package/src/node/provider-probe.js +13 -0
  15. package/src/node/start-command.js +282 -40
  16. package/src/node/startup-manager.js +64 -29
  17. package/src/node/web-command.js +106 -0
  18. package/src/node/web-console-assets.js +26 -0
  19. package/src/node/web-console-client.js +56 -0
  20. package/src/node/web-console-dev-assets.js +258 -0
  21. package/src/node/web-console-server.js +3146 -0
  22. package/src/node/web-console-styles.generated.js +1 -0
  23. package/src/node/web-console-ui/config-editor-utils.js +616 -0
  24. package/src/node/web-console-ui/lib/utils.js +6 -0
  25. package/src/node/web-console-ui/rate-limit-utils.js +144 -0
  26. package/src/node/web-console-ui/select-search-utils.js +36 -0
  27. package/src/runtime/codex-request-transformer.js +46 -5
  28. package/src/runtime/codex-response-transformer.js +268 -35
  29. package/src/runtime/config.js +1394 -35
  30. package/src/runtime/handler/amp-gemini.js +913 -0
  31. package/src/runtime/handler/amp-response.js +308 -0
  32. package/src/runtime/handler/amp.js +290 -0
  33. package/src/runtime/handler/auth.js +17 -2
  34. package/src/runtime/handler/provider-call.js +168 -50
  35. package/src/runtime/handler/provider-translation.js +937 -26
  36. package/src/runtime/handler/request.js +149 -6
  37. package/src/runtime/handler/route-debug.js +22 -1
  38. package/src/runtime/handler.js +449 -9
  39. package/src/runtime/subscription-auth.js +1 -6
  40. package/src/shared/local-router-defaults.js +62 -0
  41. package/src/translator/index.js +3 -1
  42. package/src/translator/request/openai-to-claude.js +217 -6
  43. package/src/translator/response/openai-to-claude.js +206 -58
@@ -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(entry.requests),
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 modelAliases = modelAliasResult.aliases;
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 = typeof raw.defaultModel === "string"
708
- ? raw.defaultModel
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
- metadata: raw.metadata && typeof raw.metadata === "object" ? raw.metadata : {}
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 = dedupeStrings([...(model?.formats || []), model?.format])
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 = dedupeStrings([...(model.formats || []), model.format])
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: `Alias '${aliasId}' has no resolvable target candidates.`
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
- export function resolveRequestedRoute(config, requestedModel, sourceFormat = FORMATS.CLAUDE) {
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 defaultModel = config?.defaultModel || "smart";
2800
+ const smartRouteReference = normalizedRequested === "smart"
2801
+ ? getSmartRouteReference(config, routingIndex)
2802
+ : "";
1458
2803
  const effectiveRequested = normalizedRequested === "smart"
1459
- ? defaultModel
2804
+ ? (smartRouteReference || "smart")
1460
2805
  : normalizedRequested;
1461
2806
 
1462
- if (effectiveRequested === "smart") {
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
- const routingIndex = getRoutingIndex(config);
1475
- const parsed = parseRouteReference(effectiveRequested);
1476
- if (parsed.type === "alias") {
1477
- return resolveAliasRoutePlan(config, parsed.aliasId, normalizedRequested, sourceFormat, routingIndex);
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
- return resolveDirectRoutePlan(
1481
- config,
1482
- effectiveRequested,
1483
- normalizedRequested,
1484
- sourceFormat,
1485
- routingIndex
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: (model.formats && model.formats.length > 0) ? model.formats : (provider.formats || []),
2869
+ formats: modelFormats.length > 0 ? modelFormats : (provider.formats || []),
1511
2870
  endpoint_format_supported: endpointFormat
1512
- ? ((model.formats && model.formats.length > 0) ? model.formats.includes(endpointFormat) : (provider.formats || []).includes(endpointFormat))
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: model.formats || [],
2875
+ model_formats: modelFormats,
1517
2876
  fallback_models: model.fallbackModels || []
1518
2877
  });
1519
2878
  }