@khanglvm/llm-router 2.3.1 → 2.3.2

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 (35) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/README.md +2 -2
  3. package/package.json +1 -1
  4. package/src/cli/router-module.js +32 -5
  5. package/src/node/coding-tool-config.js +138 -25
  6. package/src/node/large-request-log.js +54 -0
  7. package/src/node/litellm-context-catalog.js +13 -1
  8. package/src/node/local-server.js +10 -0
  9. package/src/node/ollama-client.js +195 -0
  10. package/src/node/ollama-hardware.js +94 -0
  11. package/src/node/ollama-install.js +230 -0
  12. package/src/node/provider-probe.js +69 -5
  13. package/src/node/web-console-client.js +36 -36
  14. package/src/node/web-console-server.js +478 -8
  15. package/src/node/web-console-styles.generated.js +1 -1
  16. package/src/node/web-console-ui/amp-utils.js +272 -0
  17. package/src/node/web-console-ui/api-client.js +128 -0
  18. package/src/node/web-console-ui/capability-utils.js +36 -0
  19. package/src/node/web-console-ui/config-editor-utils.js +20 -5
  20. package/src/node/web-console-ui/constants.js +140 -0
  21. package/src/node/web-console-ui/context-window-utils.js +262 -0
  22. package/src/node/web-console-ui/hooks/use-reorder-layout-animation.js +65 -0
  23. package/src/node/web-console-ui/provider-presets.js +211 -0
  24. package/src/node/web-console-ui/quick-start-utils.js +790 -0
  25. package/src/node/web-console-ui/utils.js +353 -0
  26. package/src/node/web-console-ui/web-search-utils.js +460 -0
  27. package/src/runtime/config.js +96 -9
  28. package/src/runtime/handler/fallback.js +71 -0
  29. package/src/runtime/handler/field-filter.js +39 -0
  30. package/src/runtime/handler/large-request-log.js +211 -0
  31. package/src/runtime/handler/provider-call.js +185 -15
  32. package/src/runtime/handler/reasoning-effort.js +11 -1
  33. package/src/runtime/handler/tool-name-sanitizer.js +258 -0
  34. package/src/runtime/handler.js +16 -3
  35. package/src/shared/coding-tool-bindings.js +3 -0
@@ -0,0 +1,790 @@
1
+ import { QUICK_START_FALLBACK_USER_AGENT, QUICK_START_PROVIDER_ID_PATTERN, QUICK_START_ALIAS_ID_PATTERN, QUICK_START_CONNECTION_CATEGORIES, QUICK_START_DEFAULT_ENDPOINT_BY_PROTOCOL } from "./constants.js";
2
+ import { CODEX_SUBSCRIPTION_MODELS, CLAUDE_CODE_SUBSCRIPTION_MODELS } from "../../runtime/subscription-constants.js";
3
+ import { DEFAULT_MODEL_ALIAS_ID } from "../../runtime/config.js";
4
+ import { buildRateLimitBucketsFromDraftRows, validateRateLimitDraftRows, RATE_LIMIT_ALL_MODELS_SELECTOR } from "./rate-limit-utils.js";
5
+ import { safeClone, looksLikeEnvVarName, slugifyProviderId, createMasterKey, createRateLimitDraftRows, createRateLimitDraftRow, mergeChipValuesAndDraft, resolveRateLimitDraftRows } from "./utils.js";
6
+ import { findPresetByKey, findPresetByHost, PROVIDER_PRESET_BY_KEY, PROVIDER_PRESET_FREE_TIER_RPM_BY_HOST, presetModelCache } from "./provider-presets.js";
7
+
8
+ // ── Header / endpoint utils ──
9
+
10
+ export function getQuickStartDefaultHeaderRows(defaultProviderUserAgent = QUICK_START_FALLBACK_USER_AGENT) {
11
+ return [{ name: "User-Agent", value: String(defaultProviderUserAgent || QUICK_START_FALLBACK_USER_AGENT) }];
12
+ }
13
+
14
+ export function normalizeQuickStartHeaderRows(rows = []) {
15
+ return (Array.isArray(rows) ? rows : []).map((row) => ({
16
+ name: String(row?.name || ""),
17
+ value: row?.value === undefined || row?.value === null ? "" : String(row.value)
18
+ }));
19
+ }
20
+
21
+ export function headerObjectToRows(headers, defaultProviderUserAgent = QUICK_START_FALLBACK_USER_AGENT) {
22
+ if (!headers || typeof headers !== "object" || Array.isArray(headers)) {
23
+ return getQuickStartDefaultHeaderRows(defaultProviderUserAgent);
24
+ }
25
+
26
+ const rows = Object.entries(headers)
27
+ .map(([name, value]) => ({
28
+ name: String(name || ""),
29
+ value: value === undefined || value === null ? "" : String(value)
30
+ }))
31
+ .filter((row) => row.name.trim());
32
+
33
+ return rows.length > 0 ? rows : getQuickStartDefaultHeaderRows(defaultProviderUserAgent);
34
+ }
35
+
36
+ export function headerRowsToObject(rows = []) {
37
+ const output = {};
38
+ for (const row of normalizeQuickStartHeaderRows(rows)) {
39
+ const name = String(row.name || "").trim();
40
+ if (!name) continue;
41
+ const isUserAgent = name.toLowerCase() === "user-agent";
42
+ const value = String(row.value || "").trim();
43
+ if (!value && !isUserAgent) continue;
44
+ output[name] = value;
45
+ }
46
+ return output;
47
+ }
48
+
49
+ export function buildQuickStartApiSignature(quickStart = {}) {
50
+ return JSON.stringify({
51
+ connectionType: String(quickStart?.connectionType || ""),
52
+ endpoints: Array.isArray(quickStart?.endpoints) ? quickStart.endpoints.map((entry) => String(entry || "").trim()).filter(Boolean) : [],
53
+ apiKeyEnv: String(quickStart?.apiKeyEnv || "").trim(),
54
+ headers: headerRowsToObject(quickStart?.headerRows || [])
55
+ });
56
+ }
57
+
58
+ export function isProviderReference(value, providerId) {
59
+ return String(value || "").trim().startsWith(`${providerId}/`);
60
+ }
61
+
62
+ export function pickFallbackDefaultModel(config) {
63
+ const aliasIds = Object.keys(config?.modelAliases || {});
64
+ if (aliasIds.includes(DEFAULT_MODEL_ALIAS_ID)) return DEFAULT_MODEL_ALIAS_ID;
65
+ if (aliasIds.length > 0) return aliasIds[0];
66
+ const provider = (config?.providers || []).find((entry) => (entry?.models || []).length > 0);
67
+ return provider?.models?.[0]?.id ? `${provider.id}/${provider.models[0].id}` : undefined;
68
+ }
69
+
70
+ // ── Quick start state / config ──
71
+
72
+ export function removeProviderFromConfig(config, providerId) {
73
+ const next = safeClone(config && typeof config === "object" ? config : {});
74
+ next.providers = (Array.isArray(next.providers) ? next.providers : []).filter((provider) => provider?.id !== providerId);
75
+
76
+ const remainingModelRefs = new Set(
77
+ (next.providers || []).flatMap((provider) =>
78
+ (provider.models || []).map((model) => `${provider.id}/${model.id}`)
79
+ )
80
+ );
81
+
82
+ const aliases = next.modelAliases && typeof next.modelAliases === "object" ? next.modelAliases : {};
83
+ const nextAliases = {};
84
+ for (const [aliasId, alias] of Object.entries(aliases)) {
85
+ const targets = Array.isArray(alias?.targets)
86
+ ? alias.targets.filter((target) => !isProviderReference(target?.ref, providerId))
87
+ : [];
88
+ const fallbackTargets = Array.isArray(alias?.fallbackTargets)
89
+ ? alias.fallbackTargets.filter((target) => !isProviderReference(target?.ref, providerId))
90
+ : [];
91
+
92
+ nextAliases[aliasId] = {
93
+ ...alias,
94
+ ...(Array.isArray(alias?.targets) ? { targets } : {}),
95
+ ...(Array.isArray(alias?.fallbackTargets) ? { fallbackTargets } : {})
96
+ };
97
+ }
98
+ next.modelAliases = nextAliases;
99
+
100
+ if (next.defaultModel && !next.modelAliases?.[next.defaultModel] && !remainingModelRefs.has(next.defaultModel)) {
101
+ next.defaultModel = DEFAULT_MODEL_ALIAS_ID;
102
+ }
103
+
104
+ if (next?.amp?.defaultRoute && !next.modelAliases?.[next.amp.defaultRoute] && !remainingModelRefs.has(next.amp.defaultRoute)) {
105
+ next.amp = {
106
+ ...next.amp,
107
+ defaultRoute: next.defaultModel || pickFallbackDefaultModel(next)
108
+ };
109
+ }
110
+
111
+ return next;
112
+ }
113
+
114
+ export function collectQuickStartEndpoints(provider = {}) {
115
+ const fromMetadata = Array.isArray(provider?.metadata?.endpointCandidates) ? provider.metadata.endpointCandidates : [];
116
+ const fromConfig = [provider?.baseUrl, ...Object.values(provider?.baseUrlByFormat || {})];
117
+ return Array.from(new Set((fromMetadata.length > 0 ? fromMetadata : fromConfig)
118
+ .map((entry) => String(entry || "").trim())
119
+ .filter(Boolean)));
120
+ }
121
+
122
+ export function getStoredProviderCredentialPayload(provider = {}) {
123
+ const apiKeyEnv = String(provider?.apiKeyEnv || "").trim();
124
+ if (apiKeyEnv) return { apiKeyEnv };
125
+
126
+ const apiKey = String(provider?.apiKey || provider?.credential || "").trim();
127
+ return apiKey ? { apiKey } : {};
128
+ }
129
+
130
+ export function getDraftProviderCredentialPayload(draftProvider = {}, provider = {}) {
131
+ if (draftProvider && Object.prototype.hasOwnProperty.call(draftProvider, "credentialInput")) {
132
+ const credentialInput = String(draftProvider?.credentialInput || "").trim();
133
+ if (!credentialInput) return {};
134
+ return looksLikeEnvVarName(credentialInput)
135
+ ? { apiKeyEnv: credentialInput }
136
+ : { apiKey: credentialInput };
137
+ }
138
+
139
+ return getStoredProviderCredentialPayload(provider);
140
+ }
141
+
142
+ export function inferQuickStartConnectionType(provider = {}) {
143
+ if (provider?.type === "subscription") {
144
+ return "subscription";
145
+ }
146
+ return "api";
147
+ }
148
+
149
+ export function inferQuickStartPresetKey(provider = {}) {
150
+ if (provider?.type === "subscription") {
151
+ return provider?.subscriptionType === "claude-code" ? "oauth-claude" : "oauth-gpt";
152
+ }
153
+ const endpoints = collectQuickStartEndpoints(provider);
154
+ for (const ep of endpoints) {
155
+ try {
156
+ const host = new URL(String(ep || "")).hostname;
157
+ const preset = findPresetByHost(host);
158
+ if (preset) return preset.key;
159
+ } catch { /* ignore */ }
160
+ }
161
+ return "custom";
162
+ }
163
+
164
+ export function createProviderInlineDraftState(provider = {}) {
165
+ const endpoints = collectQuickStartEndpoints(provider);
166
+ const connectionType = inferQuickStartConnectionType(provider);
167
+ const presetKey = inferQuickStartPresetKey(provider);
168
+ const rateLimitDefaults = getQuickStartRateLimitDefaults(presetKey);
169
+ return {
170
+ id: String(provider?.id || "").trim(),
171
+ name: String(provider?.name || provider?.id || "").trim(),
172
+ credentialInput: connectionType === "api"
173
+ ? String(provider?.apiKeyEnv || provider?.apiKey || provider?.credential || "").trim()
174
+ : "",
175
+ endpoints: connectionType === "api" ? endpoints : [],
176
+ endpointDraft: "",
177
+ rateLimitRows: connectionType === "api"
178
+ ? createRateLimitDraftRows(provider?.rateLimits, {
179
+ keyPrefix: `provider-${provider?.id || "new"}-rate-limit`,
180
+ defaults: rateLimitDefaults,
181
+ includeDefault: true
182
+ })
183
+ : []
184
+ };
185
+ }
186
+
187
+ export function getQuickStartConnectionLabel(presetKey) {
188
+ const preset = findPresetByKey(presetKey);
189
+ return preset.label || "API Key";
190
+ }
191
+
192
+ export function detectPresetHostFromEndpoints(endpoints) {
193
+ for (const ep of (endpoints || [])) {
194
+ try {
195
+ const host = new URL(String(ep || "")).hostname;
196
+ if (PROVIDER_PRESET_FREE_TIER_RPM_BY_HOST[host]) return host;
197
+ } catch { /* ignore */ }
198
+ }
199
+ return null;
200
+ }
201
+
202
+ export function buildPresetFreeTierRateLimitRows(presetHost, modelIds) {
203
+ const limits = PROVIDER_PRESET_FREE_TIER_RPM_BY_HOST[presetHost];
204
+ if (!limits || !Array.isArray(modelIds) || modelIds.length === 0) return null;
205
+ const defaultLimit = limits._default || 30;
206
+ return modelIds.map((modelId, index) => createRateLimitDraftRow({
207
+ models: [String(modelId)],
208
+ requests: limits[modelId] || defaultLimit,
209
+ window: { size: 1, unit: "minute" }
210
+ }, { keyPrefix: `preset-rl`, index }));
211
+ }
212
+
213
+ export function pickFreeTierProbeModels(modelIds) {
214
+ const tiers = new Map();
215
+ for (const id of modelIds) {
216
+ const lower = id.toLowerCase();
217
+ const tier = lower.includes("flash-lite") ? "flash-lite"
218
+ : lower.includes("flash") ? "flash"
219
+ : lower.includes("pro") ? "pro"
220
+ : lower;
221
+ if (!tiers.has(tier)) tiers.set(tier, id);
222
+ }
223
+ return [...tiers.values()];
224
+ }
225
+
226
+ export function getQuickStartSuggestedModelIds(presetKey, protocol = "openai") {
227
+ const cached = presetModelCache.get(presetKey);
228
+ if (cached?.length) return [...cached];
229
+ const preset = findPresetByKey(presetKey);
230
+ const models = preset.defaultModels;
231
+ if (models && typeof models === "object" && !Array.isArray(models)) {
232
+ return [...(models[protocol] || models.openai || [])];
233
+ }
234
+ return Array.isArray(models) ? [...models] : [];
235
+ }
236
+
237
+ export function getQuickStartRateLimitDefaults(presetKey) {
238
+ const preset = findPresetByKey(presetKey);
239
+ return preset.rateLimitDefaults || PROVIDER_PRESET_BY_KEY.custom.rateLimitDefaults;
240
+ }
241
+
242
+ export function deduplicateProviderId(baseId, baseName, existingProviders = []) {
243
+ const ids = new Set((existingProviders || []).map((p) => p?.id).filter(Boolean));
244
+ if (!ids.has(baseId)) return { providerId: baseId, providerName: baseName };
245
+ let n = 2;
246
+ while (ids.has(`${baseId}-${n}`)) n++;
247
+ return { providerId: `${baseId}-${n}`, providerName: `${baseName} ${n}` };
248
+ }
249
+
250
+ export function getQuickStartConnectionDefaults(presetKey, protocol = "openai") {
251
+ const preset = findPresetByKey(presetKey);
252
+ const rateLimitDefaults = getQuickStartRateLimitDefaults(presetKey);
253
+ const isApi = preset.category === "api";
254
+
255
+ return {
256
+ providerName: preset.providerName,
257
+ providerId: preset.providerId,
258
+ endpoints: isApi && preset.endpoint ? [preset.endpoint] : [],
259
+ apiKeyEnv: isApi ? (preset.apiKeyEnv || "") : "",
260
+ subscriptionProfile: isApi ? "" : "",
261
+ modelIds: presetKey === "custom" ? [] : getQuickStartSuggestedModelIds(presetKey, protocol),
262
+ rateLimitRows: isApi
263
+ ? createRateLimitDraftRows([], {
264
+ keyPrefix: `quick-start-${presetKey}-rate-limit`,
265
+ defaults: rateLimitDefaults,
266
+ includeDefault: true
267
+ })
268
+ : []
269
+ };
270
+ }
271
+
272
+ export function applyQuickStartConnectionPreset(current = {}, {
273
+ baseConfig = {},
274
+ nextCategory,
275
+ nextPresetKey,
276
+ defaultProviderUserAgent = QUICK_START_FALLBACK_USER_AGENT
277
+ } = {}) {
278
+ const preset = findPresetByKey(nextPresetKey);
279
+ const isApi = nextCategory === "api";
280
+ const currentPresetKey = current.selectedConnection || "custom";
281
+ const currentPreset = findPresetByKey(currentPresetKey);
282
+ const currentDefaults = getQuickStartConnectionDefaults(currentPresetKey);
283
+ const existingProviders = Array.isArray(baseConfig?.providers) ? baseConfig.providers : [];
284
+ const deduped = deduplicateProviderId(preset.providerId, preset.providerName, existingProviders);
285
+ const currentHeaderDefaults = current.connectionType === "api"
286
+ ? getQuickStartDefaultHeaderRows(defaultProviderUserAgent)
287
+ : [];
288
+ const nextHeaderDefaults = isApi
289
+ ? getQuickStartDefaultHeaderRows(defaultProviderUserAgent)
290
+ : [];
291
+ const currentHeaderRows = normalizeQuickStartHeaderRows(current.headerRows || []);
292
+ const providerIdWasAuto = !current.providerId
293
+ || current.providerId === currentPreset.providerId
294
+ || current.providerId === currentDefaults.providerId
295
+ || current.providerId === slugifyProviderId(current.providerName || "");
296
+ const providerNameWasAuto = !current.providerName
297
+ || current.providerName === currentPreset.providerName
298
+ || current.providerName === currentDefaults.providerName;
299
+ const profileWasAuto = !current.subscriptionProfile || current.subscriptionProfile === currentDefaults.subscriptionProfile;
300
+ const currentDefaultModels = Array.isArray(currentPreset.defaultModels) ? currentPreset.defaultModels : [];
301
+ const modelsWereDefault = (current.modelIds || []).length === 0
302
+ || JSON.stringify(current.modelIds || []) === JSON.stringify(currentDefaultModels)
303
+ || JSON.stringify(current.modelIds || []) === JSON.stringify(currentDefaults.modelIds || []);
304
+ const headerRowsWereDefault = JSON.stringify(currentHeaderRows) === JSON.stringify(currentHeaderDefaults);
305
+ const currentEndpoints = mergeChipValuesAndDraft(current.endpoints, current.endpointDraft);
306
+ const currentDefaultEndpoints = Array.isArray(currentDefaults.endpoints) ? currentDefaults.endpoints : [];
307
+ const endpointsWereAuto = currentEndpoints.length === 0
308
+ || JSON.stringify(currentEndpoints) === JSON.stringify(currentDefaultEndpoints);
309
+ const currentApiKeyEnv = String(current.apiKeyEnv || "").trim();
310
+ const apiKeyEnvWasAuto = !currentApiKeyEnv
311
+ || currentApiKeyEnv === String(currentPreset.apiKeyEnv || "").trim()
312
+ || currentApiKeyEnv === String(currentDefaults.apiKeyEnv || "").trim();
313
+ const presetModels = Array.isArray(preset.defaultModels)
314
+ ? [...preset.defaultModels]
315
+ : [];
316
+ const nextDefaults = getQuickStartConnectionDefaults(nextPresetKey);
317
+
318
+ return {
319
+ ...current,
320
+ connectionType: nextCategory,
321
+ selectedConnection: nextPresetKey,
322
+ providerName: providerNameWasAuto ? deduped.providerName : current.providerName,
323
+ providerId: providerIdWasAuto ? deduped.providerId : current.providerId,
324
+ endpoints: isApi
325
+ ? (endpointsWereAuto ? [...nextDefaults.endpoints] : (Array.isArray(current.endpoints) ? current.endpoints : []))
326
+ : [],
327
+ endpointDraft: isApi
328
+ ? (endpointsWereAuto ? "" : String(current.endpointDraft || ""))
329
+ : "",
330
+ apiKeyEnv: isApi
331
+ ? (apiKeyEnvWasAuto ? (preset.apiKeyEnv || "") : current.apiKeyEnv)
332
+ : "",
333
+ subscriptionProfile: isApi
334
+ ? ""
335
+ : (profileWasAuto ? nextDefaults.subscriptionProfile : current.subscriptionProfile),
336
+ modelIds: modelsWereDefault ? (presetModels.length > 0 ? presetModels : nextDefaults.modelIds) : current.modelIds,
337
+ modelContextWindows: modelsWereDefault ? {} : (current.modelContextWindows || {}),
338
+ modelDraft: "",
339
+ rateLimitRows: isApi
340
+ ? createRateLimitDraftRows([], {
341
+ keyPrefix: `quick-start-${nextPresetKey}-rate-limit`,
342
+ defaults: preset.rateLimitDefaults || PROVIDER_PRESET_BY_KEY.custom.rateLimitDefaults,
343
+ includeDefault: true
344
+ })
345
+ : [],
346
+ headerRows: isApi
347
+ ? ((currentHeaderRows.length === 0 || headerRowsWereDefault) ? nextHeaderDefaults : currentHeaderRows)
348
+ : []
349
+ };
350
+ }
351
+
352
+ export function findQuickStartAliasEntry(baseConfig = {}, providerId = "", { aliasId = "" } = {}) {
353
+ if (!providerId) return null;
354
+ const aliases = baseConfig?.modelAliases && typeof baseConfig.modelAliases === "object" && !Array.isArray(baseConfig.modelAliases)
355
+ ? baseConfig.modelAliases
356
+ : {};
357
+ const entries = aliasId
358
+ ? [[aliasId, aliases[aliasId]]]
359
+ : Object.entries(aliases);
360
+
361
+ for (const [candidateAliasId, alias] of entries) {
362
+ if (!alias || typeof alias !== "object") continue;
363
+ const targets = Array.isArray(alias?.targets) ? alias.targets : [];
364
+ const fallbackTargets = Array.isArray(alias?.fallbackTargets) ? alias.fallbackTargets : [];
365
+ if ([...targets, ...fallbackTargets].some((target) => isProviderReference(target?.ref, providerId))) {
366
+ return { aliasId: candidateAliasId, alias };
367
+ }
368
+ }
369
+ return null;
370
+ }
371
+
372
+ export function getQuickStartAliasTargetModelIds(aliasEntry, providerId = "", fallbackModelIds = []) {
373
+ const normalizedFallbackModelIds = Array.from(new Set((fallbackModelIds || []).map((modelId) => String(modelId || "").trim()).filter(Boolean)));
374
+ if (!providerId || !aliasEntry?.alias) return normalizedFallbackModelIds;
375
+
376
+ const allowedModelIds = new Set(normalizedFallbackModelIds);
377
+ const aliasTargets = Array.isArray(aliasEntry.alias?.targets) ? aliasEntry.alias.targets : [];
378
+ const orderedAliasModelIds = [];
379
+
380
+ for (const target of aliasTargets) {
381
+ const ref = String(target?.ref || "").trim();
382
+ if (!isProviderReference(ref, providerId)) continue;
383
+ const modelId = ref.slice(providerId.length + 1);
384
+ if (!allowedModelIds.has(modelId) || orderedAliasModelIds.includes(modelId)) continue;
385
+ orderedAliasModelIds.push(modelId);
386
+ }
387
+
388
+ return orderedAliasModelIds.length > 0 ? orderedAliasModelIds : normalizedFallbackModelIds;
389
+ }
390
+
391
+ export function syncQuickStartAliasModelIds(aliasModelIds = [], modelIds = []) {
392
+ const normalizedModelIds = Array.from(new Set((modelIds || []).map((modelId) => String(modelId || "").trim()).filter(Boolean)));
393
+ const remaining = new Set(normalizedModelIds);
394
+ const ordered = [];
395
+
396
+ for (const modelId of (aliasModelIds || []).map((entry) => String(entry || "").trim()).filter(Boolean)) {
397
+ if (!remaining.has(modelId)) continue;
398
+ ordered.push(modelId);
399
+ remaining.delete(modelId);
400
+ }
401
+
402
+ return [
403
+ ...ordered,
404
+ ...normalizedModelIds.filter((modelId) => remaining.has(modelId))
405
+ ];
406
+ }
407
+
408
+ export function rewriteQuickStartAliasTarget(target, { fromProviderId, toProviderId, allowedModelIds }) {
409
+ if (!target || typeof target !== "object") return target;
410
+ const ref = String(target.ref || "").trim();
411
+ if (!fromProviderId || !isProviderReference(ref, fromProviderId)) return target;
412
+ const modelId = ref.slice(fromProviderId.length + 1);
413
+ if (!allowedModelIds.has(modelId)) return null;
414
+ return {
415
+ ...target,
416
+ ref: `${toProviderId}/${modelId}`
417
+ };
418
+ }
419
+
420
+ export function rewriteQuickStartAlias(alias, options) {
421
+ if (!alias || typeof alias !== "object") return alias;
422
+ const nextAlias = { ...alias };
423
+ const hasTargets = Array.isArray(alias.targets);
424
+ const hasFallbackTargets = Array.isArray(alias.fallbackTargets);
425
+
426
+ if (hasTargets) {
427
+ nextAlias.targets = alias.targets
428
+ .map((target) => rewriteQuickStartAliasTarget(target, options))
429
+ .filter(Boolean);
430
+ }
431
+ if (hasFallbackTargets) {
432
+ nextAlias.fallbackTargets = alias.fallbackTargets
433
+ .map((target) => rewriteQuickStartAliasTarget(target, options))
434
+ .filter(Boolean);
435
+ }
436
+
437
+ return nextAlias;
438
+ }
439
+
440
+ export function rewriteQuickStartProviderRef(value, { fromProviderId, toProviderId, allowedModelIds }) {
441
+ const ref = String(value || "").trim();
442
+ if (!ref || !fromProviderId || !isProviderReference(ref, fromProviderId)) return ref;
443
+ const modelId = ref.slice(fromProviderId.length + 1);
444
+ if (!allowedModelIds.has(modelId)) return "";
445
+ return `${toProviderId}/${modelId}`;
446
+ }
447
+
448
+ export function collectQuickStartProviderRefs(providers = []) {
449
+ return new Set(
450
+ (providers || []).flatMap((provider) =>
451
+ (provider?.models || []).map((model) => `${provider.id}/${model.id}`)
452
+ )
453
+ );
454
+ }
455
+
456
+ export function createQuickStartState(baseConfig = {}, { seedMode = "blank", targetProviderId = "", defaultProviderUserAgent = QUICK_START_FALLBACK_USER_AGENT } = {}) {
457
+ const providerList = Array.isArray(baseConfig?.providers) ? baseConfig.providers : [];
458
+ const targetedProvider = targetProviderId
459
+ ? providerList.find((entry) => entry?.id === targetProviderId) || null
460
+ : null;
461
+ const useExistingProvider = seedMode === "existing" || Boolean(targetedProvider);
462
+ const provider = useExistingProvider ? (targetedProvider || providerList[0] || {}) : {};
463
+ const connectionType = useExistingProvider ? inferQuickStartConnectionType(provider) : "api";
464
+ const presetKey = useExistingProvider ? inferQuickStartPresetKey(provider) : "custom";
465
+ const protocol = provider?.format === "claude" ? "claude" : "openai";
466
+ const defaults = getQuickStartConnectionDefaults(presetKey, protocol);
467
+ const rateLimitDefaults = getQuickStartRateLimitDefaults(presetKey);
468
+ const providerModels = Array.isArray(provider?.models)
469
+ ? provider.models.map((model) => model?.id).filter(Boolean)
470
+ : [];
471
+ const resolvedProviderId = String(provider?.id || defaults.providerId || slugifyProviderId(provider?.name || defaults.providerName || "my-provider") || "my-provider");
472
+ const aliasEntry = useExistingProvider
473
+ ? findQuickStartAliasEntry(baseConfig, resolvedProviderId, { aliasId: DEFAULT_MODEL_ALIAS_ID })
474
+ : null;
475
+ const resolvedModelIds = providerModels.length > 0 ? providerModels : [...defaults.modelIds];
476
+ const modelContextWindows = Object.fromEntries(
477
+ (Array.isArray(provider?.models) ? provider.models : [])
478
+ .map((model) => {
479
+ const modelId = String(model?.id || "").trim();
480
+ const contextWindow = Number(model?.contextWindow);
481
+ if (!modelId || !Number.isFinite(contextWindow) || contextWindow <= 0) return null;
482
+ return [modelId, Math.floor(contextWindow)];
483
+ })
484
+ .filter(Boolean)
485
+ );
486
+ const headerRows = connectionType === "api"
487
+ ? headerObjectToRows(provider?.headers, defaultProviderUserAgent)
488
+ : [];
489
+
490
+ return {
491
+ connectionType,
492
+ selectedConnection: presetKey,
493
+ providerName: String(provider?.name || defaults.providerName),
494
+ providerId: resolvedProviderId,
495
+ endpoints: collectQuickStartEndpoints(provider).length > 0 ? collectQuickStartEndpoints(provider) : defaults.endpoints,
496
+ endpointDraft: "",
497
+ apiKeyEnv: String(provider?.apiKeyEnv || provider?.apiKey || defaults.apiKeyEnv),
498
+ subscriptionProfile: String(provider?.subscriptionProfile || defaults.subscriptionProfile),
499
+ modelIds: resolvedModelIds,
500
+ modelContextWindows,
501
+ modelDraft: "",
502
+ aliasModelIds: getQuickStartAliasTargetModelIds(aliasEntry, resolvedProviderId, resolvedModelIds),
503
+ headerRows,
504
+ rateLimitRows: connectionType === "api"
505
+ ? createRateLimitDraftRows(provider?.rateLimits, {
506
+ keyPrefix: `quick-start-${resolvedProviderId}-rate-limit`,
507
+ defaults: rateLimitDefaults,
508
+ includeDefault: true
509
+ })
510
+ : [],
511
+ enableAlias: true,
512
+ aliasId: DEFAULT_MODEL_ALIAS_ID,
513
+ sourceAliasId: aliasEntry?.aliasId || "",
514
+ useAliasAsDefault: useExistingProvider ? Boolean(aliasEntry) : true
515
+ };
516
+ }
517
+
518
+ export function buildQuickStartModelEntries(modelIds, modelPreferredFormat = {}, modelContextWindows = {}) {
519
+ return (modelIds || []).map((id) => {
520
+ const preferred = modelPreferredFormat[id];
521
+ const contextWindow = Number(modelContextWindows?.[id]);
522
+ return {
523
+ id,
524
+ ...(preferred ? { formats: [preferred] } : {}),
525
+ ...(Number.isFinite(contextWindow) && contextWindow > 0 ? { contextWindow: Math.floor(contextWindow) } : {})
526
+ };
527
+ });
528
+ }
529
+
530
+ export function resolveQuickStartSubscriptionProfile(quickStart = {}) {
531
+ const providerId = slugifyProviderId(quickStart?.providerId || quickStart?.providerName || "") || "";
532
+ return String(quickStart?.subscriptionProfile || providerId || "default").trim() || providerId || "default";
533
+ }
534
+
535
+ export function buildQuickStartConfig(baseConfig = {}, quickStart, testedProviderConfig = null, { targetProviderId = "" } = {}) {
536
+ const next = safeClone(baseConfig && typeof baseConfig === "object" ? baseConfig : {});
537
+ const providerId = slugifyProviderId(quickStart?.providerId || quickStart?.providerName || "my-provider") || "my-provider";
538
+ const providerName = String(quickStart?.providerName || providerId).trim() || providerId;
539
+ const modelIds = Array.isArray(quickStart?.modelIds) ? quickStart.modelIds.map((id) => String(id || "").trim()).filter(Boolean) : [];
540
+ const orderedModelIds = syncQuickStartAliasModelIds(quickStart?.aliasModelIds, modelIds);
541
+ const effectiveRateLimitDefaults = getQuickStartRateLimitDefaults(quickStart?.selectedConnection || quickStart?.connectionType);
542
+ const resolvedRateLimitRows = Array.isArray(quickStart?.rateLimitRows)
543
+ ? quickStart.rateLimitRows
544
+ : [];
545
+ const effectiveApiRateLimitRows = resolvedRateLimitRows.length > 0
546
+ ? resolvedRateLimitRows
547
+ : [{
548
+ models: [RATE_LIMIT_ALL_MODELS_SELECTOR],
549
+ requests: effectiveRateLimitDefaults.limit,
550
+ windowValue: effectiveRateLimitDefaults.windowValue,
551
+ windowUnit: effectiveRateLimitDefaults.windowUnit
552
+ }];
553
+ const hadProviders = Array.isArray(baseConfig?.providers) && baseConfig.providers.length > 0;
554
+ const sourceProviderId = String(targetProviderId || "").trim();
555
+ const sourceAliasId = String(quickStart?.sourceAliasId || "").trim();
556
+ let provider;
557
+
558
+ if (quickStart?.connectionType === "api") {
559
+ const endpoints = Array.isArray(quickStart?.endpoints) ? quickStart.endpoints.map((entry) => String(entry || "").trim()).filter(Boolean) : [];
560
+ const workingFormats = Array.isArray(testedProviderConfig?.workingFormats)
561
+ ? testedProviderConfig.workingFormats.filter(Boolean)
562
+ : [];
563
+ const preferredFormat = testedProviderConfig?.preferredFormat || workingFormats[0] || "openai";
564
+ const baseUrlByFormat = testedProviderConfig?.baseUrlByFormat && typeof testedProviderConfig.baseUrlByFormat === "object"
565
+ ? testedProviderConfig.baseUrlByFormat
566
+ : (endpoints[0] ? { [preferredFormat]: endpoints[0] } : undefined);
567
+ const baseUrl = (preferredFormat && baseUrlByFormat?.[preferredFormat])
568
+ || baseUrlByFormat?.openai
569
+ || baseUrlByFormat?.claude
570
+ || endpoints[0]
571
+ || "";
572
+ const confirmedModelIds = Array.isArray(testedProviderConfig?.models) && testedProviderConfig.models.length > 0
573
+ ? orderedModelIds.filter((id) => testedProviderConfig.models.includes(id))
574
+ : orderedModelIds;
575
+ const effectiveModelIds = confirmedModelIds.length > 0 ? confirmedModelIds : orderedModelIds;
576
+ const providerMetadata = endpoints.length > 1 ? { endpointCandidates: endpoints } : undefined;
577
+
578
+ const credentialInput = String(quickStart?.apiKeyEnv || "").trim();
579
+ const providerCredential = looksLikeEnvVarName(credentialInput)
580
+ ? { apiKeyEnv: credentialInput }
581
+ : (credentialInput ? { apiKey: credentialInput } : {});
582
+ const customHeaders = headerRowsToObject(quickStart?.headerRows || []);
583
+
584
+ provider = {
585
+ id: providerId,
586
+ name: providerName,
587
+ baseUrl,
588
+ baseUrlByFormat,
589
+ ...providerCredential,
590
+ ...(Object.keys(customHeaders).length > 0 ? { headers: customHeaders } : {}),
591
+ format: preferredFormat,
592
+ formats: workingFormats.length > 0 ? workingFormats : [preferredFormat],
593
+ models: buildQuickStartModelEntries(
594
+ effectiveModelIds,
595
+ testedProviderConfig?.modelPreferredFormat || {},
596
+ quickStart?.modelContextWindows || {}
597
+ ),
598
+ ...(providerMetadata ? { metadata: providerMetadata } : {})
599
+ };
600
+ } else {
601
+ const preset = findPresetByKey(quickStart?.selectedConnection || "oauth-gpt");
602
+ const subscriptionType = preset.subscriptionType || "chatgpt-codex";
603
+ const providerFormat = preset.format || "openai";
604
+
605
+ provider = {
606
+ id: providerId,
607
+ name: providerName,
608
+ type: "subscription",
609
+ subscriptionType,
610
+ subscriptionProfile: resolveQuickStartSubscriptionProfile(quickStart),
611
+ format: providerFormat,
612
+ formats: [providerFormat],
613
+ models: buildQuickStartModelEntries(orderedModelIds, {}, quickStart?.modelContextWindows || {})
614
+ };
615
+ }
616
+
617
+ provider.rateLimits = quickStart?.connectionType === "api"
618
+ ? buildRateLimitBucketsFromDraftRows(effectiveApiRateLimitRows, {
619
+ fallbackRequests: effectiveRateLimitDefaults.limit,
620
+ fallbackWindowValue: effectiveRateLimitDefaults.windowValue,
621
+ fallbackWindowUnit: effectiveRateLimitDefaults.windowUnit
622
+ })
623
+ : buildRateLimitBucketsFromDraftRows([{
624
+ models: [RATE_LIMIT_ALL_MODELS_SELECTOR],
625
+ requests: effectiveRateLimitDefaults.limit,
626
+ windowValue: effectiveRateLimitDefaults.windowValue,
627
+ windowUnit: effectiveRateLimitDefaults.windowUnit
628
+ }], {
629
+ fallbackRequests: effectiveRateLimitDefaults.limit,
630
+ fallbackWindowValue: effectiveRateLimitDefaults.windowValue,
631
+ fallbackWindowUnit: effectiveRateLimitDefaults.windowUnit
632
+ });
633
+
634
+ if (!String(next.masterKey || "").trim()) {
635
+ next.masterKey = createMasterKey();
636
+ }
637
+
638
+ next.version = typeof next.version === "number" ? next.version : 2;
639
+
640
+ const existingProviders = Array.isArray(next.providers) ? next.providers : [];
641
+ let providerIndex = sourceProviderId
642
+ ? existingProviders.findIndex((entry) => entry?.id === sourceProviderId)
643
+ : -1;
644
+ if (providerIndex === -1) {
645
+ providerIndex = existingProviders.findIndex((entry) => entry?.id === providerId);
646
+ }
647
+ next.providers = providerIndex === -1
648
+ ? [...existingProviders, provider]
649
+ : existingProviders.map((entry, index) => (index === providerIndex ? provider : entry));
650
+
651
+ const existingAliases = next.modelAliases && typeof next.modelAliases === "object" && !Array.isArray(next.modelAliases)
652
+ ? next.modelAliases
653
+ : {};
654
+ const allowedModelIds = new Set((provider.models || []).map((model) => model.id));
655
+ const shouldManageDefaultAlias = !hadProviders || quickStart?.useAliasAsDefault === true;
656
+ const nextAliases = {};
657
+
658
+ for (const [aliasId, alias] of Object.entries(existingAliases)) {
659
+ const rewrittenAlias = sourceProviderId
660
+ ? rewriteQuickStartAlias(alias, { fromProviderId: sourceProviderId, toProviderId: providerId, allowedModelIds })
661
+ : alias;
662
+ nextAliases[aliasId] = rewrittenAlias;
663
+ }
664
+
665
+ const primaryRef = provider.models?.[0]?.id ? `${providerId}/${provider.models[0].id}` : "";
666
+ const aliasTargetModelIds = (orderedModelIds.length > 0 ? orderedModelIds : (provider.models || []).map((model) => model.id))
667
+ .filter((modelId) => allowedModelIds.has(modelId));
668
+ const defaultAliasId = DEFAULT_MODEL_ALIAS_ID;
669
+ const existingDefaultAlias = nextAliases[defaultAliasId] && typeof nextAliases[defaultAliasId] === "object"
670
+ ? nextAliases[defaultAliasId]
671
+ : { id: defaultAliasId, strategy: "ordered", targets: [], fallbackTargets: [] };
672
+ nextAliases[defaultAliasId] = shouldManageDefaultAlias
673
+ ? {
674
+ ...existingDefaultAlias,
675
+ id: defaultAliasId,
676
+ strategy: "ordered",
677
+ targets: aliasTargetModelIds.map((modelId) => ({ ref: `${providerId}/${modelId}` })),
678
+ fallbackTargets: []
679
+ }
680
+ : {
681
+ ...existingDefaultAlias,
682
+ id: defaultAliasId,
683
+ strategy: String(existingDefaultAlias.strategy || "ordered").trim() || "ordered",
684
+ targets: Array.isArray(existingDefaultAlias.targets) ? existingDefaultAlias.targets : [],
685
+ fallbackTargets: Array.isArray(existingDefaultAlias.fallbackTargets) ? existingDefaultAlias.fallbackTargets : []
686
+ };
687
+ next.modelAliases = nextAliases;
688
+
689
+ const remainingModelRefs = collectQuickStartProviderRefs(next.providers);
690
+ next.defaultModel = DEFAULT_MODEL_ALIAS_ID;
691
+
692
+ if (!next.amp || typeof next.amp !== "object" || Array.isArray(next.amp)) {
693
+ next.amp = { restrictManagementToLocalhost: true, overrides: { entities: [] } };
694
+ }
695
+ if (next.amp.restrictManagementToLocalhost === undefined) {
696
+ next.amp.restrictManagementToLocalhost = true;
697
+ }
698
+ if (!next.amp.overrides || typeof next.amp.overrides !== "object" || Array.isArray(next.amp.overrides)) {
699
+ next.amp.overrides = { entities: [] };
700
+ }
701
+ if (!Array.isArray(next.amp.overrides.entities)) {
702
+ next.amp.overrides.entities = [];
703
+ }
704
+
705
+ let nextAmpDefaultRoute = String(next.amp.defaultRoute || "").trim();
706
+ if (sourceProviderId) {
707
+ nextAmpDefaultRoute = rewriteQuickStartProviderRef(nextAmpDefaultRoute, {
708
+ fromProviderId: sourceProviderId,
709
+ toProviderId: providerId,
710
+ allowedModelIds
711
+ });
712
+ }
713
+ if (shouldManageDefaultAlias || !nextAmpDefaultRoute || (!next.modelAliases?.[nextAmpDefaultRoute] && !remainingModelRefs.has(nextAmpDefaultRoute))) {
714
+ nextAmpDefaultRoute = DEFAULT_MODEL_ALIAS_ID;
715
+ }
716
+ if (nextAmpDefaultRoute) {
717
+ next.amp.defaultRoute = nextAmpDefaultRoute;
718
+ }
719
+
720
+ if (!next.metadata || typeof next.metadata !== "object" || Array.isArray(next.metadata)) {
721
+ next.metadata = {};
722
+ }
723
+
724
+ return next;
725
+ }
726
+
727
+ export function hasCompletedProviderSetup(config = {}) {
728
+ const providers = Array.isArray(config?.providers) ? config.providers : [];
729
+ return providers.some((provider) => {
730
+ if (provider?.enabled === false) return false;
731
+ const models = Array.isArray(provider?.models)
732
+ ? provider.models.filter((model) => String(model?.id || "").trim())
733
+ : [];
734
+ const rateLimits = Array.isArray(provider?.rateLimits) ? provider.rateLimits : [];
735
+ return models.length > 0 && rateLimits.length > 0;
736
+ });
737
+ }
738
+
739
+ export function getQuickStartStepError(stepIndex, quickStart, baseConfig = {}, { targetProviderId = "" } = {}) {
740
+ const providerId = slugifyProviderId(quickStart?.providerId || quickStart?.providerName || "");
741
+ const modelIds = mergeChipValuesAndDraft(quickStart?.modelIds, quickStart?.modelDraft);
742
+ const aliasModelIds = syncQuickStartAliasModelIds(quickStart?.aliasModelIds, modelIds);
743
+ const rateLimitRows = resolveRateLimitDraftRows(quickStart?.rateLimitRows);
744
+ const endpoints = quickStart?.connectionType === "api"
745
+ ? mergeChipValuesAndDraft(quickStart?.endpoints, quickStart?.endpointDraft)
746
+ : [];
747
+ const providerList = Array.isArray(baseConfig?.providers) ? baseConfig.providers : [];
748
+ const aliasMap = baseConfig?.modelAliases && typeof baseConfig.modelAliases === "object" && !Array.isArray(baseConfig.modelAliases)
749
+ ? baseConfig.modelAliases
750
+ : {};
751
+
752
+ if (stepIndex === 0) {
753
+ if (!String(quickStart?.providerName || "").trim()) return "Add a provider name to continue.";
754
+ if (!providerId || !QUICK_START_PROVIDER_ID_PATTERN.test(providerId)) return "Provider id must start with a letter and use lowercase letters, numbers, or hyphens.";
755
+ if (providerList.some((provider) => provider?.id === providerId && provider?.id !== targetProviderId)) {
756
+ return "Provider id already exists. Choose another id or edit that provider instead.";
757
+ }
758
+ if (quickStart?.connectionType === "api") {
759
+ if (endpoints.length === 0) return "Add at least one endpoint to continue.";
760
+ if (!String(quickStart?.apiKeyEnv || "").trim()) return "API key or env is required before testing config.";
761
+ }
762
+ }
763
+
764
+ if (stepIndex === 1) {
765
+ if (modelIds.length === 0) return "Add at least one model id.";
766
+ if (quickStart?.connectionType === "api") {
767
+ const rateLimitIssue = validateRateLimitDraftRows(rateLimitRows, {
768
+ knownModelIds: modelIds,
769
+ requireAtLeastOne: true
770
+ });
771
+ if (rateLimitIssue) return rateLimitIssue;
772
+ }
773
+ }
774
+
775
+ if (stepIndex === 2) {
776
+ if (quickStart?.useAliasAsDefault && Object.prototype.hasOwnProperty.call(aliasMap, String(quickStart?.aliasId || DEFAULT_MODEL_ALIAS_ID).trim()) === false) {
777
+ return "";
778
+ }
779
+ }
780
+
781
+ return "";
782
+ }
783
+
784
+ // ── Internal dependency notes ──
785
+ // The following are referenced from app.jsx scope and not yet in separate modules:
786
+ // safeClone, looksLikeEnvVarName, slugifyProviderId, createRateLimitDraftRows,
787
+ // createRateLimitDraftRow, mergeChipValuesAndDraft, resolveRateLimitDraftRows,
788
+ // validateRateLimitDraftRows, createMasterKey,
789
+ // findPresetByKey, findPresetByHost, PROVIDER_PRESET_BY_KEY,
790
+ // PROVIDER_PRESET_FREE_TIER_RPM_BY_HOST, presetModelCache