@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
@@ -0,0 +1,616 @@
1
+ import { DEFAULT_MODEL_ALIAS_ID } from "../../runtime/config.js";
2
+ import {
3
+ buildRateLimitBucketsFromDraftRows,
4
+ RATE_LIMIT_ALL_MODELS_SELECTOR
5
+ } from "./rate-limit-utils.js";
6
+
7
+ function safeClone(value) {
8
+ return JSON.parse(JSON.stringify(value ?? {}));
9
+ }
10
+
11
+ function dedupeStrings(values = []) {
12
+ const seen = new Set();
13
+ const result = [];
14
+ for (const value of values) {
15
+ const trimmed = String(value || "").trim();
16
+ if (!trimmed || seen.has(trimmed)) continue;
17
+ seen.add(trimmed);
18
+ result.push(trimmed);
19
+ }
20
+ return result;
21
+ }
22
+
23
+ function normalizeAliasLookup(value) {
24
+ const text = String(value || "").trim();
25
+ if (!text) return "";
26
+ return text.startsWith("alias:") ? text.slice("alias:".length).trim() : text;
27
+ }
28
+
29
+ function buildDefaultAlias(config = {}, aliases = {}) {
30
+ const existingDefault = aliases?.[DEFAULT_MODEL_ALIAS_ID];
31
+ if (existingDefault && typeof existingDefault === "object" && !Array.isArray(existingDefault)) {
32
+ return {
33
+ ...existingDefault,
34
+ id: DEFAULT_MODEL_ALIAS_ID,
35
+ strategy: String(existingDefault.strategy || "ordered").trim() || "ordered",
36
+ targets: rewriteAliasTargetList(existingDefault.targets || [], (ref) => ref),
37
+ fallbackTargets: rewriteAliasTargetList(existingDefault.fallbackTargets || [], (ref) => ref)
38
+ };
39
+ }
40
+
41
+ const configuredDefault = String(config?.defaultModel || "").trim();
42
+ if (!configuredDefault || configuredDefault === "smart") {
43
+ return {
44
+ id: DEFAULT_MODEL_ALIAS_ID,
45
+ strategy: "ordered",
46
+ targets: [],
47
+ fallbackTargets: []
48
+ };
49
+ }
50
+
51
+ const aliasFromDefault = normalizeAliasLookup(configuredDefault);
52
+ if (aliasFromDefault && aliases?.[aliasFromDefault]) {
53
+ const sourceAlias = aliases[aliasFromDefault];
54
+ return {
55
+ ...sourceAlias,
56
+ id: DEFAULT_MODEL_ALIAS_ID,
57
+ strategy: String(sourceAlias.strategy || "ordered").trim() || "ordered",
58
+ targets: rewriteAliasTargetList(sourceAlias.targets || [], (ref) => ref),
59
+ fallbackTargets: rewriteAliasTargetList(sourceAlias.fallbackTargets || [], (ref) => ref)
60
+ };
61
+ }
62
+
63
+ return {
64
+ id: DEFAULT_MODEL_ALIAS_ID,
65
+ strategy: "ordered",
66
+ targets: configuredDefault ? [{ ref: configuredDefault }] : [],
67
+ fallbackTargets: []
68
+ };
69
+ }
70
+
71
+ function ensureDefaultAlias(config = {}) {
72
+ if (!config.modelAliases || typeof config.modelAliases !== "object" || Array.isArray(config.modelAliases)) {
73
+ config.modelAliases = {};
74
+ }
75
+ config.modelAliases[DEFAULT_MODEL_ALIAS_ID] = buildDefaultAlias(config, config.modelAliases);
76
+ return config;
77
+ }
78
+
79
+ function collectManagedRouteRefs(config = {}) {
80
+ const refs = new Set();
81
+ for (const aliasId of Object.keys(config?.modelAliases || {})) {
82
+ const normalizedAliasId = String(aliasId || "").trim();
83
+ if (normalizedAliasId) refs.add(normalizedAliasId);
84
+ }
85
+ for (const provider of (config?.providers || [])) {
86
+ const providerId = String(provider?.id || "").trim();
87
+ if (!providerId) continue;
88
+ for (const model of (provider?.models || [])) {
89
+ const modelId = String(model?.id || "").trim();
90
+ if (!modelId) continue;
91
+ refs.add(`${providerId}/${modelId}`);
92
+ }
93
+ }
94
+ return refs;
95
+ }
96
+
97
+ function pickFallbackDefaultModel(config = {}) {
98
+ const aliasIds = Object.keys(config?.modelAliases || {}).map((aliasId) => String(aliasId || "").trim()).filter(Boolean);
99
+ if (aliasIds.includes(DEFAULT_MODEL_ALIAS_ID)) return DEFAULT_MODEL_ALIAS_ID;
100
+ if (aliasIds.length > 0) return aliasIds[0];
101
+
102
+ for (const provider of (config?.providers || [])) {
103
+ const providerId = String(provider?.id || "").trim();
104
+ if (!providerId) continue;
105
+ for (const model of (provider?.models || [])) {
106
+ const modelId = String(model?.id || "").trim();
107
+ if (!modelId) continue;
108
+ return `${providerId}/${modelId}`;
109
+ }
110
+ }
111
+
112
+ return "";
113
+ }
114
+
115
+ function ensureTopLevelRoutes(config = {}) {
116
+ ensureDefaultAlias(config);
117
+ const knownRoutes = collectManagedRouteRefs(config);
118
+ config.defaultModel = DEFAULT_MODEL_ALIAS_ID;
119
+
120
+ if (!config.amp || typeof config.amp !== "object" || Array.isArray(config.amp)) {
121
+ return config;
122
+ }
123
+
124
+ const nextAmpDefaultRoute = String(config.amp.defaultRoute || "").trim();
125
+ if (!nextAmpDefaultRoute || !knownRoutes.has(normalizeAliasLookup(nextAmpDefaultRoute))) {
126
+ const fallback = String(config.defaultModel || "").trim() || pickFallbackDefaultModel(config);
127
+ if (fallback) {
128
+ config.amp.defaultRoute = fallback;
129
+ } else {
130
+ delete config.amp.defaultRoute;
131
+ }
132
+ }
133
+
134
+ return config;
135
+ }
136
+
137
+ function rewriteProviderModelRef(ref, providerId, renameMap, validModelIds) {
138
+ const text = String(ref || "").trim();
139
+ if (!text) return "";
140
+ const prefix = `${providerId}/`;
141
+ if (!text.startsWith(prefix)) return text;
142
+
143
+ const modelId = text.slice(prefix.length);
144
+ const nextModelId = renameMap.get(modelId) || modelId;
145
+ if (!validModelIds.has(nextModelId)) return "";
146
+ return `${providerId}/${nextModelId}`;
147
+ }
148
+
149
+ function rewriteProviderReference(ref, currentProviderId, nextProviderId = "") {
150
+ const text = String(ref || "").trim();
151
+ if (!text) return "";
152
+ const prefix = `${currentProviderId}/`;
153
+ if (!text.startsWith(prefix)) return text;
154
+ if (!nextProviderId) return "";
155
+ return `${nextProviderId}/${text.slice(prefix.length)}`;
156
+ }
157
+
158
+ function normalizeEndpointCandidates(values = []) {
159
+ return dedupeStrings(Array.isArray(values) ? values : [values]);
160
+ }
161
+
162
+ function rewriteProviderEndpoints(provider = {}, endpoints = []) {
163
+ const nextProvider = { ...provider };
164
+ const nextEndpoints = normalizeEndpointCandidates(endpoints);
165
+ const primaryEndpoint = String(nextEndpoints[0] || "").trim();
166
+ let nextMetadata = nextProvider.metadata && typeof nextProvider.metadata === "object" && !Array.isArray(nextProvider.metadata)
167
+ ? { ...nextProvider.metadata }
168
+ : null;
169
+ const shouldPersistEndpointCandidates = nextEndpoints.length > 1
170
+ || Boolean(nextMetadata && Object.prototype.hasOwnProperty.call(nextMetadata, "endpointCandidates"));
171
+
172
+ if (!primaryEndpoint) {
173
+ delete nextProvider.baseUrl;
174
+ delete nextProvider.baseUrlByFormat;
175
+ if (nextMetadata && Object.prototype.hasOwnProperty.call(nextMetadata, "endpointCandidates")) {
176
+ delete nextMetadata.endpointCandidates;
177
+ }
178
+ } else {
179
+ nextProvider.baseUrl = primaryEndpoint;
180
+ const baseUrlByFormat = nextProvider.baseUrlByFormat && typeof nextProvider.baseUrlByFormat === "object" && !Array.isArray(nextProvider.baseUrlByFormat)
181
+ ? nextProvider.baseUrlByFormat
182
+ : null;
183
+ if (baseUrlByFormat && Object.keys(baseUrlByFormat).length > 0) {
184
+ nextProvider.baseUrlByFormat = Object.fromEntries(Object.keys(baseUrlByFormat).map((format) => [format, primaryEndpoint]));
185
+ }
186
+ if (shouldPersistEndpointCandidates) {
187
+ nextMetadata = nextMetadata || {};
188
+ nextMetadata.endpointCandidates = nextEndpoints;
189
+ }
190
+ }
191
+
192
+ if (nextMetadata && Object.keys(nextMetadata).length > 0) {
193
+ nextProvider.metadata = nextMetadata;
194
+ } else if (nextMetadata) {
195
+ delete nextProvider.metadata;
196
+ }
197
+
198
+ return nextProvider;
199
+ }
200
+
201
+ function normalizePositiveInteger(value, fallback) {
202
+ const parsed = Number.parseInt(String(value || ""), 10);
203
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
204
+ }
205
+
206
+ function buildLegacyRateLimitDraftRows(draftProvider = {}) {
207
+ const hasLegacyFields =
208
+ draftProvider?.rateLimitLimit !== undefined
209
+ || draftProvider?.rateLimitWindowValue !== undefined
210
+ || draftProvider?.rateLimitWindowUnit !== undefined;
211
+ if (!hasLegacyFields) return [];
212
+ return [{
213
+ sourceId: String(draftProvider?.rateLimitSourceId || "").trim(),
214
+ models: [RATE_LIMIT_ALL_MODELS_SELECTOR],
215
+ requests: draftProvider?.rateLimitLimit,
216
+ windowValue: draftProvider?.rateLimitWindowValue,
217
+ windowUnit: draftProvider?.rateLimitWindowUnit
218
+ }];
219
+ }
220
+
221
+ function rewriteRateLimits(provider = {}, draftProvider = {}, providerId = "") {
222
+ const nextProvider = { ...provider };
223
+ const currentRateLimits = Array.isArray(nextProvider.rateLimits)
224
+ ? nextProvider.rateLimits.map((rateLimit) => (rateLimit && typeof rateLimit === "object" ? safeClone(rateLimit) : rateLimit)).filter(Boolean)
225
+ : [];
226
+ const draftRows = Array.isArray(draftProvider?.rateLimitRows)
227
+ ? draftProvider.rateLimitRows
228
+ : buildLegacyRateLimitDraftRows(draftProvider);
229
+ const currentPrimary = currentRateLimits[0] && typeof currentRateLimits[0] === "object" ? currentRateLimits[0] : null;
230
+ const currentRequests = Number(currentPrimary?.requests ?? currentPrimary?.limit) || 60;
231
+ const currentWindowSize = Number(currentPrimary?.window?.size ?? currentPrimary?.window?.value) || 1;
232
+ const currentWindowUnit = String(currentPrimary?.window?.unit || "minute").trim() || "minute";
233
+ const existingBucketsBySourceId = new Map(
234
+ currentRateLimits
235
+ .map((bucket) => [String(bucket?.id || "").trim(), bucket])
236
+ .filter(([bucketId]) => Boolean(bucketId))
237
+ );
238
+
239
+ nextProvider.rateLimits = buildRateLimitBucketsFromDraftRows(draftRows, {
240
+ existingBucketsBySourceId,
241
+ fallbackRequests: currentRequests,
242
+ fallbackWindowValue: currentWindowSize,
243
+ fallbackWindowUnit: currentWindowUnit
244
+ });
245
+ return nextProvider;
246
+ }
247
+
248
+ function rewriteAliasReference(ref, fromAliasId, toAliasId = "") {
249
+ const text = String(ref || "").trim();
250
+ if (!text) return "";
251
+ if (text === fromAliasId) return toAliasId;
252
+ if (text === `alias:${fromAliasId}`) return toAliasId ? `alias:${toAliasId}` : "";
253
+ return text;
254
+ }
255
+
256
+ function rewriteAliasTargetList(targets = [], rewriter) {
257
+ const seenRefs = new Set();
258
+ const nextTargets = [];
259
+
260
+ for (const target of (Array.isArray(targets) ? targets : [])) {
261
+ const currentRef = String(target?.ref || "").trim();
262
+ if (!currentRef) continue;
263
+ const nextRef = String(rewriter(currentRef) || "").trim();
264
+ if (!nextRef || seenRefs.has(nextRef)) continue;
265
+ seenRefs.add(nextRef);
266
+ nextTargets.push({
267
+ ...target,
268
+ ref: nextRef
269
+ });
270
+ }
271
+
272
+ return nextTargets;
273
+ }
274
+
275
+ function cleanupAliasReferences(config = {}) {
276
+ const aliases = config?.modelAliases && typeof config.modelAliases === "object" && !Array.isArray(config.modelAliases)
277
+ ? config.modelAliases
278
+ : {};
279
+ const nextAliases = {};
280
+
281
+ for (const [aliasId, alias] of Object.entries(aliases)) {
282
+ const targets = rewriteAliasTargetList(alias?.targets || [], (ref) => ref);
283
+ const fallbackTargets = rewriteAliasTargetList(alias?.fallbackTargets || [], (ref) => ref);
284
+ nextAliases[aliasId] = {
285
+ ...alias,
286
+ targets,
287
+ fallbackTargets
288
+ };
289
+ }
290
+
291
+ config.modelAliases = nextAliases;
292
+ return ensureTopLevelRoutes(config);
293
+ }
294
+
295
+ function normalizeProviderModelRows(rows = []) {
296
+ const seenIds = new Set();
297
+ const normalizedRows = [];
298
+
299
+ for (const row of (Array.isArray(rows) ? rows : [])) {
300
+ const id = String(row?.id || "").trim();
301
+ const sourceId = String(row?.sourceId || row?.originalId || "").trim();
302
+ if (!id || seenIds.has(id)) continue;
303
+ seenIds.add(id);
304
+ normalizedRows.push({ id, sourceId });
305
+ }
306
+
307
+ return normalizedRows;
308
+ }
309
+
310
+ function normalizeAliasTargetRows(rows = []) {
311
+ const seenRefs = new Set();
312
+ const normalizedRows = [];
313
+
314
+ for (const row of (Array.isArray(rows) ? rows : [])) {
315
+ const ref = String(row?.ref || "").trim();
316
+ const sourceRef = String(row?.sourceRef || row?.originalRef || "").trim();
317
+ const rawWeight = String(row?.weight ?? "").trim();
318
+ const parsedWeight = Math.floor(Number(rawWeight));
319
+ if (!ref || seenRefs.has(ref)) continue;
320
+ seenRefs.add(ref);
321
+ normalizedRows.push({
322
+ ref,
323
+ sourceRef,
324
+ ...(rawWeight ? { weight: (Number.isFinite(parsedWeight) && parsedWeight > 0 ? parsedWeight : 1) } : {})
325
+ });
326
+ }
327
+
328
+ return normalizedRows;
329
+ }
330
+
331
+ export function applyProviderModelEdits(config = {}, providerId, rows = []) {
332
+ const nextConfig = safeClone(config);
333
+ const providerList = Array.isArray(nextConfig.providers) ? nextConfig.providers : [];
334
+ const providerIndex = providerList.findIndex((provider) => provider?.id === providerId);
335
+ if (providerIndex === -1) return nextConfig;
336
+
337
+ const provider = providerList[providerIndex];
338
+ const existingModels = Array.isArray(provider?.models) ? provider.models : [];
339
+ const existingModelMap = new Map(existingModels.map((model) => [String(model?.id || "").trim(), model]));
340
+ const normalizedRows = normalizeProviderModelRows(rows);
341
+ const usedSourceIds = new Set();
342
+ const renameMap = new Map();
343
+ const nextModels = [];
344
+
345
+ for (const row of normalizedRows) {
346
+ const existingBySourceId = row.sourceId && existingModelMap.has(row.sourceId) && !usedSourceIds.has(row.sourceId)
347
+ ? row.sourceId
348
+ : "";
349
+ const existingByCurrentId = !existingBySourceId && existingModelMap.has(row.id) && !usedSourceIds.has(row.id)
350
+ ? row.id
351
+ : "";
352
+ const matchedSourceId = existingBySourceId || existingByCurrentId;
353
+
354
+ if (matchedSourceId) {
355
+ usedSourceIds.add(matchedSourceId);
356
+ if (matchedSourceId !== row.id) {
357
+ renameMap.set(matchedSourceId, row.id);
358
+ }
359
+ nextModels.push({
360
+ ...existingModelMap.get(matchedSourceId),
361
+ id: row.id
362
+ });
363
+ continue;
364
+ }
365
+
366
+ nextModels.push({ id: row.id });
367
+ }
368
+
369
+ const validModelIds = new Set(nextModels.map((model) => String(model?.id || "").trim()).filter(Boolean));
370
+
371
+ provider.models = nextModels;
372
+ provider.rateLimits = (Array.isArray(provider.rateLimits) ? provider.rateLimits : []).map((bucket) => {
373
+ const bucketModels = Array.isArray(bucket?.models) ? bucket.models : [];
374
+ return {
375
+ ...bucket,
376
+ models: dedupeStrings(bucketModels.map((modelId) => {
377
+ if (String(modelId || "").trim() === "all") return "all";
378
+ const nextModelId = renameMap.get(String(modelId || "").trim()) || String(modelId || "").trim();
379
+ return validModelIds.has(nextModelId) ? nextModelId : "";
380
+ }))
381
+ };
382
+ });
383
+
384
+ nextConfig.providers = providerList.map((entry, index) => (index === providerIndex ? provider : entry)).map((entry) => ({
385
+ ...entry,
386
+ models: (Array.isArray(entry?.models) ? entry.models : []).map((model) => {
387
+ if (!Array.isArray(model?.fallbackModels)) return model;
388
+ return {
389
+ ...model,
390
+ fallbackModels: dedupeStrings(model.fallbackModels.map((ref) => rewriteProviderModelRef(ref, providerId, renameMap, validModelIds)))
391
+ };
392
+ })
393
+ }));
394
+
395
+ const existingAliases = nextConfig.modelAliases && typeof nextConfig.modelAliases === "object" && !Array.isArray(nextConfig.modelAliases)
396
+ ? nextConfig.modelAliases
397
+ : {};
398
+ const nextAliases = {};
399
+
400
+ for (const [aliasId, alias] of Object.entries(existingAliases)) {
401
+ const targets = rewriteAliasTargetList(alias?.targets || [], (ref) => rewriteProviderModelRef(ref, providerId, renameMap, validModelIds));
402
+ const fallbackTargets = rewriteAliasTargetList(alias?.fallbackTargets || [], (ref) => rewriteProviderModelRef(ref, providerId, renameMap, validModelIds));
403
+ nextAliases[aliasId] = {
404
+ ...alias,
405
+ targets,
406
+ fallbackTargets
407
+ };
408
+ }
409
+
410
+ nextConfig.modelAliases = nextAliases;
411
+ nextConfig.defaultModel = rewriteProviderModelRef(nextConfig.defaultModel, providerId, renameMap, validModelIds);
412
+ if (nextConfig?.amp && typeof nextConfig.amp === "object" && !Array.isArray(nextConfig.amp)) {
413
+ nextConfig.amp.defaultRoute = rewriteProviderModelRef(nextConfig.amp.defaultRoute, providerId, renameMap, validModelIds);
414
+ }
415
+
416
+ return ensureTopLevelRoutes(nextConfig);
417
+ }
418
+
419
+ export function applyProviderInlineEdits(config = {}, currentProviderId = "", draftProvider = {}) {
420
+ const nextConfig = safeClone(config);
421
+ const providerList = Array.isArray(nextConfig.providers) ? nextConfig.providers : [];
422
+ const providerIndex = providerList.findIndex((provider) => provider?.id === currentProviderId);
423
+ if (providerIndex === -1) return nextConfig;
424
+
425
+ const currentProvider = providerList[providerIndex] || {};
426
+ const nextProviderId = String(draftProvider?.id || currentProviderId || "").trim();
427
+ const nextProviderName = String(draftProvider?.name ?? currentProvider?.name ?? "").trim();
428
+ const nextEndpoints = normalizeEndpointCandidates(
429
+ Array.isArray(draftProvider?.endpoints) ? draftProvider.endpoints : [draftProvider?.endpoint]
430
+ );
431
+ const isSubscription = currentProvider?.type === "subscription";
432
+ const renamedProviderId = nextProviderId || currentProviderId;
433
+
434
+ let nextProvider = {
435
+ ...currentProvider,
436
+ id: renamedProviderId,
437
+ name: nextProviderName || renamedProviderId
438
+ };
439
+
440
+ if (!isSubscription) {
441
+ nextProvider = rewriteProviderEndpoints(nextProvider, nextEndpoints);
442
+ nextProvider = rewriteRateLimits(nextProvider, draftProvider, renamedProviderId);
443
+ }
444
+
445
+ providerList[providerIndex] = nextProvider;
446
+ nextConfig.providers = providerList.map((provider) => ({
447
+ ...provider,
448
+ models: (Array.isArray(provider?.models) ? provider.models : []).map((model) => {
449
+ if (!Array.isArray(model?.fallbackModels)) return model;
450
+ return {
451
+ ...model,
452
+ fallbackModels: dedupeStrings(model.fallbackModels.map((ref) => rewriteProviderReference(ref, currentProviderId, renamedProviderId)))
453
+ };
454
+ })
455
+ }));
456
+
457
+ if (renamedProviderId !== currentProviderId) {
458
+ const aliasMap = nextConfig.modelAliases && typeof nextConfig.modelAliases === "object" && !Array.isArray(nextConfig.modelAliases)
459
+ ? nextConfig.modelAliases
460
+ : {};
461
+ const nextAliases = {};
462
+
463
+ for (const [aliasId, alias] of Object.entries(aliasMap)) {
464
+ nextAliases[aliasId] = {
465
+ ...alias,
466
+ targets: rewriteAliasTargetList(alias?.targets || [], (ref) => rewriteProviderReference(ref, currentProviderId, renamedProviderId)),
467
+ fallbackTargets: rewriteAliasTargetList(alias?.fallbackTargets || [], (ref) => rewriteProviderReference(ref, currentProviderId, renamedProviderId))
468
+ };
469
+ }
470
+
471
+ nextConfig.modelAliases = nextAliases;
472
+ nextConfig.defaultModel = rewriteProviderReference(nextConfig.defaultModel, currentProviderId, renamedProviderId);
473
+ if (nextConfig?.amp && typeof nextConfig.amp === "object" && !Array.isArray(nextConfig.amp)) {
474
+ nextConfig.amp.defaultRoute = rewriteProviderReference(nextConfig.amp.defaultRoute, currentProviderId, renamedProviderId);
475
+ }
476
+ }
477
+
478
+ return ensureTopLevelRoutes(nextConfig);
479
+ }
480
+
481
+ export function applyModelAliasEdits(config = {}, currentAliasId = "", draftAlias = {}) {
482
+ const nextConfig = safeClone(config);
483
+ const aliasMap = nextConfig.modelAliases && typeof nextConfig.modelAliases === "object" && !Array.isArray(nextConfig.modelAliases)
484
+ ? nextConfig.modelAliases
485
+ : {};
486
+ const existingAlias = currentAliasId ? aliasMap[currentAliasId] : undefined;
487
+ const nextAliasId = currentAliasId === DEFAULT_MODEL_ALIAS_ID
488
+ ? DEFAULT_MODEL_ALIAS_ID
489
+ : String(draftAlias?.id || currentAliasId || "").trim();
490
+ if (!nextAliasId) return nextConfig;
491
+
492
+ const currentTargetsByRef = new Map((existingAlias?.targets || []).map((target) => [String(target?.ref || "").trim(), target]));
493
+ const currentFallbackTargetsByRef = new Map((existingAlias?.fallbackTargets || []).map((target) => [String(target?.ref || "").trim(), target]));
494
+
495
+ const usedPrimarySources = new Set();
496
+ const targets = normalizeAliasTargetRows(draftAlias?.targets).map((row) => {
497
+ const matchedSourceRef = row.sourceRef && currentTargetsByRef.has(row.sourceRef) && !usedPrimarySources.has(row.sourceRef)
498
+ ? row.sourceRef
499
+ : (currentTargetsByRef.has(row.ref) && !usedPrimarySources.has(row.ref) ? row.ref : "");
500
+ if (matchedSourceRef) usedPrimarySources.add(matchedSourceRef);
501
+ return {
502
+ ...(matchedSourceRef ? currentTargetsByRef.get(matchedSourceRef) : {}),
503
+ ref: row.ref,
504
+ ...(row.weight !== undefined ? { weight: row.weight } : {})
505
+ };
506
+ });
507
+
508
+ const usedFallbackSources = new Set();
509
+ const fallbackTargets = normalizeAliasTargetRows(draftAlias?.fallbackTargets).map((row) => {
510
+ const matchedSourceRef = row.sourceRef && currentFallbackTargetsByRef.has(row.sourceRef) && !usedFallbackSources.has(row.sourceRef)
511
+ ? row.sourceRef
512
+ : (currentFallbackTargetsByRef.has(row.ref) && !usedFallbackSources.has(row.ref) ? row.ref : "");
513
+ if (matchedSourceRef) usedFallbackSources.add(matchedSourceRef);
514
+ return {
515
+ ...(matchedSourceRef ? currentFallbackTargetsByRef.get(matchedSourceRef) : {}),
516
+ ref: row.ref,
517
+ ...(row.weight !== undefined ? { weight: row.weight } : {})
518
+ };
519
+ });
520
+
521
+ if (currentAliasId && currentAliasId !== nextAliasId) {
522
+ delete aliasMap[currentAliasId];
523
+ }
524
+
525
+ aliasMap[nextAliasId] = {
526
+ ...(existingAlias?.metadata ? { metadata: existingAlias.metadata } : {}),
527
+ ...(draftAlias?.metadata && typeof draftAlias.metadata === "object" && !Array.isArray(draftAlias.metadata)
528
+ ? { metadata: draftAlias.metadata }
529
+ : {}),
530
+ id: nextAliasId,
531
+ strategy: String(draftAlias?.strategy || existingAlias?.strategy || "ordered").trim() || "ordered",
532
+ targets,
533
+ fallbackTargets
534
+ };
535
+
536
+ nextConfig.modelAliases = aliasMap;
537
+
538
+ if (currentAliasId && currentAliasId !== nextAliasId) {
539
+ for (const [aliasId, alias] of Object.entries(aliasMap)) {
540
+ if (aliasId === nextAliasId) continue;
541
+ alias.targets = rewriteAliasTargetList(alias?.targets || [], (ref) => rewriteAliasReference(ref, currentAliasId, nextAliasId));
542
+ alias.fallbackTargets = rewriteAliasTargetList(alias?.fallbackTargets || [], (ref) => rewriteAliasReference(ref, currentAliasId, nextAliasId));
543
+ }
544
+ nextConfig.defaultModel = rewriteAliasReference(nextConfig.defaultModel, currentAliasId, nextAliasId);
545
+ if (nextConfig?.amp && typeof nextConfig.amp === "object" && !Array.isArray(nextConfig.amp)) {
546
+ nextConfig.amp.defaultRoute = rewriteAliasReference(nextConfig.amp.defaultRoute, currentAliasId, nextAliasId);
547
+ }
548
+ }
549
+
550
+ return cleanupAliasReferences(nextConfig);
551
+ }
552
+
553
+ export function removeModelAlias(config = {}, aliasId = "") {
554
+ const nextConfig = safeClone(config);
555
+ const targetAliasId = String(aliasId || "").trim();
556
+ if (!targetAliasId) return nextConfig;
557
+
558
+ const aliasMap = nextConfig.modelAliases && typeof nextConfig.modelAliases === "object" && !Array.isArray(nextConfig.modelAliases)
559
+ ? nextConfig.modelAliases
560
+ : {};
561
+ if (!Object.prototype.hasOwnProperty.call(aliasMap, targetAliasId)) return nextConfig;
562
+
563
+ if (targetAliasId === DEFAULT_MODEL_ALIAS_ID) {
564
+ aliasMap[targetAliasId] = {
565
+ ...aliasMap[targetAliasId],
566
+ id: DEFAULT_MODEL_ALIAS_ID,
567
+ strategy: String(aliasMap[targetAliasId]?.strategy || "ordered").trim() || "ordered",
568
+ targets: [],
569
+ fallbackTargets: []
570
+ };
571
+ nextConfig.modelAliases = aliasMap;
572
+ return ensureTopLevelRoutes(nextConfig);
573
+ }
574
+
575
+ delete aliasMap[targetAliasId];
576
+ nextConfig.modelAliases = aliasMap;
577
+
578
+ for (const alias of Object.values(aliasMap)) {
579
+ alias.targets = rewriteAliasTargetList(alias?.targets || [], (ref) => rewriteAliasReference(ref, targetAliasId));
580
+ alias.fallbackTargets = rewriteAliasTargetList(alias?.fallbackTargets || [], (ref) => rewriteAliasReference(ref, targetAliasId));
581
+ }
582
+
583
+ nextConfig.defaultModel = rewriteAliasReference(nextConfig.defaultModel, targetAliasId);
584
+ if (nextConfig?.amp && typeof nextConfig.amp === "object" && !Array.isArray(nextConfig.amp)) {
585
+ nextConfig.amp.defaultRoute = rewriteAliasReference(nextConfig.amp.defaultRoute, targetAliasId);
586
+ }
587
+
588
+ return cleanupAliasReferences(nextConfig);
589
+ }
590
+
591
+ export function createAliasDraftState(aliasId = "", alias = {}) {
592
+ return {
593
+ id: String(aliasId || alias?.id || "").trim(),
594
+ strategy: String(alias?.strategy || "ordered").trim() || "ordered",
595
+ targets: (Array.isArray(alias?.targets) ? alias.targets : []).map((target, index) => ({
596
+ key: `target-${aliasId || "alias"}-${index}-${String(target?.ref || "").trim() || "empty"}`,
597
+ ref: String(target?.ref || "").trim(),
598
+ sourceRef: String(target?.ref || "").trim(),
599
+ weight: String(Number.isFinite(Number(target?.weight)) && Number(target.weight) > 0 ? Math.floor(Number(target.weight)) : 1)
600
+ })),
601
+ fallbackTargets: (Array.isArray(alias?.fallbackTargets) ? alias.fallbackTargets : []).map((target, index) => ({
602
+ key: `fallback-${aliasId || "alias"}-${index}-${String(target?.ref || "").trim() || "empty"}`,
603
+ ref: String(target?.ref || "").trim(),
604
+ sourceRef: String(target?.ref || "").trim(),
605
+ weight: String(Number.isFinite(Number(target?.weight)) && Number(target.weight) > 0 ? Math.floor(Number(target.weight)) : 1)
606
+ }))
607
+ };
608
+ }
609
+
610
+ export function createProviderModelDraftRows(provider = {}) {
611
+ return (Array.isArray(provider?.models) ? provider.models : []).map((model, index) => ({
612
+ key: `model-${provider?.id || "provider"}-${index}-${String(model?.id || "").trim() || "empty"}`,
613
+ id: String(model?.id || "").trim(),
614
+ sourceId: String(model?.id || "").trim()
615
+ }));
616
+ }
@@ -0,0 +1,6 @@
1
+ import { clsx } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ export function cn(...inputs) {
5
+ return twMerge(clsx(inputs));
6
+ }