File size: 10,081 Bytes
cd8bd0a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
/**
 * Universal model naming template for the OmniRoute plugin.
 *
 * Naming pipeline:
 *   [tag] <provider-label><separator><display-name><suffix>
 *
 *   [Free] <provider> - <name> Β· <budget>     ← free model
 *   Auto: <variant> (<N>p)                     ← auto combo
 *   Combo: <name>                              ← DB combo
 *   <provider> - <name>                        ← regular model
 */

// ── Constants ────────────────────────────────────────────────────────────

/** Separator between provider label and model display name. */
export const PROVIDER_TAG_SEPARATOR = " - ";

/** Threshold beyond which providerDisplayName is abbreviated. */
const PROVIDER_LABEL_MAX_CHARS = 12;

/** Aliases longer than this get title-case instead of UPPER. */
const ALIAS_UPPER_MAX_CHARS = 5;

// ── Auto Combo Types ─────────────────────────────────────────────────────

export type AutoVariant =
  | "coding"
  | "fast"
  | "cheap"
  | "offline"
  | "smart"
  | "lkgp";

export const AUTO_VARIANTS: AutoVariant[] = [
  "coding",
  "fast",
  "cheap",
  "offline",
  "smart",
  "lkgp",
];

export const AUTO_VARIANT_DESCRIPTIONS: Record<
  AutoVariant | "default",
  string
> = {
  default: "Best provider via scoring",
  coding: "Quality-first for code tasks",
  fast: "Latency-optimized routing",
  cheap: "Cost-optimized routing",
  offline: "Offline-friendly providers",
  smart: "Quality-first with exploration",
  lkgp: "Last-Known-Good-Provider routing",
};

// ── Free Model Types ─────────────────────────────────────────────────────

export type FreeModelFreeType =
  | "recurring-daily"
  | "recurring-monthly"
  | "recurring-credit"
  | "one-time-initial"
  | "keyless"
  | "discontinued";

// ── Provider Label ────────────────────────────────────────────────────────

/**
 * Title-case a long, lowercase-looking alias.
 * `antigravity` β†’ `Antigravity`
 */
function titleCaseAlias(alias: string): string {
  if (alias.length === 0) return alias;
  return alias.charAt(0).toUpperCase() + alias.slice(1).toLowerCase();
}

/**
 * Pick the short label for an upstream provider.
 *
 * Rules:
 *   1. Trim `providerDisplayName`. If ≀12 chars β†’ use verbatim.
 *   2. Alias ≀5 chars β†’ UPPER(alias). Alias >5 β†’ titleCase.
 *   3. Neither β†’ undefined.
 */
export function shortProviderLabel(
  enrichment:
    | { providerDisplayName?: string; providerAlias?: string }
    | undefined,
): string | undefined {
  if (!enrichment) return undefined;
  const raw =
    typeof enrichment.providerDisplayName === "string"
      ? enrichment.providerDisplayName.trim()
      : "";
  if (raw.length > 0 && raw.length <= PROVIDER_LABEL_MAX_CHARS) return raw;
  const alias =
    typeof enrichment.providerAlias === "string"
      ? enrichment.providerAlias.trim()
      : "";
  if (alias.length > 0) {
    return alias.length <= ALIAS_UPPER_MAX_CHARS
      ? alias.toUpperCase()
      : titleCaseAlias(alias);
  }
  // Long displayName with no alias to fall back on: keep the long label
  // rather than dropping the provider prefix entirely.
  return raw.length > 0 ? raw : undefined;
}

// ── Free Label ────────────────────────────────────────────────────────────

/**
 * Normalise display name so free-tier models get a consistent `[Free] ` prefix.
 *
 * "GPT-4.1 (Free)"          β†’ "[Free] GPT-4.1"
 * "DeepSeek V4 Flash Free"  β†’ "[Free] DeepSeek V4 Flash"
 * "Claude Opus 4.7"         β†’ "Claude Opus 4.7"  (unchanged)
 */
export function normaliseFreeLabel(name: string): string {
  // Bounded whitespace quantifiers ({0,8}/{1,8}) avoid the polynomial-ReDoS
  // backtracking that unbounded \s* before an anchored \s*$ would allow on
  // attacker-influenced display names. 8 covers any realistic label spacing.
  const cleaned = name
    .replace(/\s{0,8}\(free\)\s{0,8}$/i, "")
    .replace(/[\s-]{1,8}free\s{0,8}$/i, "")
    .trim();
  const wasFree = cleaned.length < name.trim().length;
  if (!wasFree) return name;
  return `[Free] ${cleaned}`;
}

// ── Free Budget Formatting ────────────────────────────────────────────────

function fmtTokens(n: number): string {
  if (n >= 1e9) return (n / 1e9).toFixed(1).replace(/\.0$/, "") + "B";
  if (n >= 1e6) return (n / 1e6).toFixed(1).replace(/\.0$/, "") + "M";
  if (n >= 1e3) return (n / 1e3).toFixed(1).replace(/\.0$/, "") + "K";
  return String(n);
}

/**
 * Format a free model budget into a short human-readable suffix.
 *
 * recurring-daily   β†’ "25M tokens/day"
 * recurring-monthly β†’ "25M tokens/month"
 * recurring-credit  β†’ "10M credits"
 * one-time-initial  β†’ "1M credits (one-time)"
 * keyless           β†’ "(keyless)"
 * discontinued      β†’ "(discontinued)"
 */
export function formatFreeBudget(params: {
  freeType: FreeModelFreeType;
  monthlyTokens?: number;
  creditTokens?: number;
}): string {
  const { freeType, monthlyTokens = 0, creditTokens = 0 } = params;

  switch (freeType) {
    case "recurring-daily":
      return `${fmtTokens(monthlyTokens)} tokens/day`;
    case "recurring-monthly":
      return `${fmtTokens(monthlyTokens)} tokens/month`;
    case "recurring-credit":
      return `${fmtTokens(creditTokens)} credits`;
    case "one-time-initial":
      return `${fmtTokens(creditTokens)} credits (one-time)`;
    case "keyless":
      return "(keyless)";
    case "discontinued":
      return "(discontinued)";
    default:
      return "";
  }
}

// ── Auto Combo Naming ─────────────────────────────────────────────────────

/**
 * Format auto combo display name.
 *
 * "Auto: Coding (4p)"
 * "Auto: Default (6p)"
 * "Auto" (no candidate count when unknown)
 */
export function formatAutoComboName(
  variant: AutoVariant | undefined,
  candidateCount?: number,
): string {
  const label = variant
    ? variant.charAt(0).toUpperCase() + variant.slice(1)
    : "Default";
  const count =
    typeof candidateCount === "number" && candidateCount > 0
      ? ` (${candidateCount}p)`
      : "";
  return `Auto: ${label}${count}`;
}

/**
 * Build the model ID for an auto combo entry.
 * "auto/coding", "auto/fast", "auto" (default).
 */
export function autoComboModelId(variant: AutoVariant | undefined): string {
  return variant ? `auto/${variant}` : "auto";
}

// ── Universal Display Name Builder ────────────────────────────────────────

export interface ModelDisplayNameParams {
  /** Raw model ID (e.g. "cc/claude-sonnet-4-6"). */
  rawId: string;
  /** Enrichment display name (e.g. "Claude Sonnet 4.6"). */
  enrichmentName?: string;
  /** Provider tag enrichment. */
  providerAlias?: string;
  /** Human-readable upstream provider label. */
  providerDisplayName?: string;
  /** Whether model is free tier. */
  isFree?: boolean;
  /** Free model budget info. */
  freeType?: FreeModelFreeType;
  /** Monthly token budget (for recurring free models). */
  monthlyTokens?: number;
  /** Credit token budget (for credit-based free models). */
  creditTokens?: number;
  /** Whether this is a combo entry (skip provider tag). */
  isCombo?: boolean;
  /** Whether this is an auto combo entry. */
  isAutoCombo?: boolean;
  /** Auto combo variant. */
  autoVariant?: AutoVariant;
  /** Auto combo candidate count. */
  autoCandidateCount?: number;
}

/**
 * Build the final display name following the universal template.
 *
 * Priority:
 *   1. Auto combo β†’ "Auto: <variant> (<N>p)"
 *   2. DB combo β†’ "Combo: <name>"
 *   3. Free + enrichment + provider tag β†’ "[Free] <label> - <name> Β· <budget>"
 *   4. Free + enrichment β†’ "[Free] <name> Β· <budget>"
 *   5. Free + raw β†’ "[Free] <rawId> Β· <budget>"
 *   6. Enrichment + provider tag β†’ "<label> - <name>"
 *   7. Enrichment only β†’ "<name>"
 *   8. Raw fallback β†’ normaliseFreeLabel(rawId)
 */
export function buildModelDisplayName(params: ModelDisplayNameParams): string {
  // Auto combos
  if (params.isAutoCombo) {
    return formatAutoComboName(params.autoVariant, params.autoCandidateCount);
  }

  // Determine base name β€” strip any existing free suffix first
  const rawBase =
    params.enrichmentName && params.enrichmentName.trim().length > 0
      ? params.enrichmentName
      : params.rawId;
  const cleanedBase = rawBase
    .replace(/\s*\(free\)\s*$/i, "")
    .replace(/[\s-]+free\s*$/i, "")
    .trim();
  const wasFree = cleanedBase.length < rawBase.trim().length;
  const isFree = !!params.isFree || wasFree;

  let baseName = cleanedBase;

  // Provider tag (skip for combos)
  if (!params.isCombo) {
    const label = shortProviderLabel({
      providerDisplayName: params.providerDisplayName,
      providerAlias: params.providerAlias,
    });
    if (label) {
      const prefix = `${label}${PROVIDER_TAG_SEPARATOR}`;
      if (!baseName.startsWith(prefix)) {
        baseName = `${prefix}${baseName}`;
      }
    }
  }

  // Prepend [Free] if applicable (AFTER provider tag for correct ordering)
  if (isFree) {
    baseName = `[Free] ${baseName}`;
  }

  // Free budget suffix
  if (isFree && params.freeType) {
    const budget = formatFreeBudget({
      freeType: params.freeType,
      monthlyTokens: params.monthlyTokens,
      creditTokens: params.creditTokens,
    });
    if (budget) {
      baseName = `${baseName} Β· ${budget}`;
    }
  }

  return baseName;
}