File size: 4,104 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
import { FREE_MODEL_BUDGETS } from "@omniroute/open-sse/config/freeModelCatalog";
import { resolveProviderId } from "@/shared/constants/providers";

/**
 * Free-model detection shared between the "import only free models" connection
 * option (Add API Key modal) and the model-sync import filter.
 *
 * A provider is considered to "have free models" when it appears in the
 * documented free-tier catalog (`FREE_MODEL_BUDGETS`). A single model is
 * considered free when its id carries the OpenRouter-style `:free` suffix, when
 * both its prompt and completion prices are zero, or when its id is listed as a
 * free model for that provider in the catalog.
 */

/** Provider ids that have at least one documented free model. */
export const PROVIDERS_WITH_FREE_MODELS: Set<string> = new Set(
  FREE_MODEL_BUDGETS.map((m) => m.provider)
);

const FREE_MODEL_IDS_BY_PROVIDER: Map<string, Set<string>> = (() => {
  const map = new Map<string, Set<string>>();
  for (const m of FREE_MODEL_BUDGETS) {
    let set = map.get(m.provider);
    if (!set) {
      set = new Set<string>();
      map.set(m.provider, set);
    }
    set.add(m.modelId);
  }
  return map;
})();

/** Whether the given provider exposes any documented free models. Accepts a provider id or alias. */
export function providerHasFreeModels(providerId: string | undefined | null): boolean {
  if (typeof providerId !== "string") return false;
  return (
    PROVIDERS_WITH_FREE_MODELS.has(providerId) ||
    PROVIDERS_WITH_FREE_MODELS.has(resolveProviderId(providerId))
  );
}

function isZeroPrice(value: unknown): boolean {
  if (typeof value === "number") return value === 0;
  if (typeof value !== "string") return false;
  const parsed = Number(value);
  return Number.isFinite(parsed) && parsed === 0;
}

export interface FreeModelCandidate {
  id?: string;
  pricing?: { prompt?: string | number; completion?: string | number };
}

/** Whether a single fetched model qualifies as free for the given provider (id or alias). */
export function isFreeModel(provider: string, model: FreeModelCandidate): boolean {
  if (typeof model.id === "string" && model.id.endsWith(":free")) return true;
  if (isZeroPrice(model.pricing?.prompt) && isZeroPrice(model.pricing?.completion)) return true;
  if (typeof model.id === "string") {
    const canonical = resolveProviderId(provider);
    if (
      FREE_MODEL_IDS_BY_PROVIDER.get(provider)?.has(model.id) ||
      FREE_MODEL_IDS_BY_PROVIDER.get(canonical)?.has(model.id)
    ) {
      return true;
    }
  }
  return false;
}

export interface SelectModelsForImportResult<T extends FreeModelCandidate> {
  models: T[];
  /**
   * True when the caller asked for free-only, models were fetched, but none of
   * them qualified as free — so nothing will be imported. Lets the UI show a
   * clear "no free models found" message instead of a silent empty import.
   */
  freeFilterEmpty: boolean;
}

/**
 * Stable "free first" sort: free models before paid, ties broken alphabetically
 * by a caller-supplied key so the order stays deterministic across re-renders and
 * data refetches (e.g. while "Test all" runs). Returns a new array; does not mutate.
 */
export function sortModelsFreeFirst<T>(
  items: T[],
  opts: { isFree: (item: T) => boolean; key: (item: T) => string }
): T[] {
  return [...items].sort((a, b) => {
    const fa = opts.isFree(a);
    const fb = opts.isFree(b);
    if (fa !== fb) return fa ? -1 : 1;
    return opts.key(a).localeCompare(opts.key(b));
  });
}

/**
 * Decide which fetched models to import. When `importFreeOnly` is false the list
 * passes through unchanged. When true, only free models are kept.
 */
export function selectModelsForImport<T extends FreeModelCandidate>(
  provider: string,
  fetchedModels: T[],
  importFreeOnly: boolean
): SelectModelsForImportResult<T> {
  if (!importFreeOnly) {
    return { models: fetchedModels, freeFilterEmpty: false };
  }
  const models = fetchedModels.filter((m) => isFreeModel(provider, m));
  const freeFilterEmpty = fetchedModels.length > 0 && models.length === 0;
  return { models, freeFilterEmpty };
}