File size: 4,730 Bytes
7824fcd
 
 
 
 
c231bc0
7824fcd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4a940a5
 
 
 
c231bc0
 
7824fcd
 
5416ffb
4a940a5
94a5578
4a940a5
7824fcd
 
 
 
 
 
4a940a5
 
 
 
 
94a5578
 
 
 
 
 
 
 
 
 
4a940a5
 
94a5578
4a940a5
 
 
 
94a5578
 
4a940a5
 
 
 
 
 
 
94a5578
 
 
 
4a940a5
 
 
 
 
94a5578
 
 
4a940a5
7824fcd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5416ffb
 
7824fcd
 
 
4a940a5
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
import { useState, useEffect, useCallback, useMemo } from "preact/hooks";

export interface CatalogModel {
  id: string;
  displayName: string;
  isDefault: boolean;
  supportedReasoningEfforts: { reasoningEffort: string; description: string }[];
  defaultReasoningEffort: string;
}

export interface ModelFamily {
  id: string;
  displayName: string;
  efforts: { reasoningEffort: string; description: string }[];
  defaultEffort: string;
}

/**
 * Extract model family ID from a model ID.
 * gpt-5.3-codex-high → gpt-5.3-codex
 * gpt-5.3-codex-spark → gpt-5.3-codex-spark (spark is a distinct family)
 * gpt-5.4 → gpt-5.4
 */
function getFamilyId(id: string): string {
  // Bare model: gpt-5.4
  if (/^gpt-\d+(?:\.\d+)?$/.test(id)) return id;
  // Spark family: gpt-X.Y-codex-spark
  if (/^gpt-\d+(?:\.\d+)?-codex-spark$/.test(id)) return id;
  // Mini family: gpt-X.Y-codex-mini
  if (/^gpt-\d+(?:\.\d+)?-codex-mini$/.test(id)) return id;
  // Codex base or tier variant (high/mid/low/max): family = gpt-X.Y-codex
  const m = id.match(/^(gpt-\d+(?:\.\d+)?-codex)(?:-(?:high|mid|low|max))?$/);
  if (m) return m[1];
  // Legacy: gpt-5-codex, gpt-5-codex-mini
  const legacy = id.match(/^(gpt-\d+-codex)(?:-(?:high|mid|low|max|mini))?$/);
  if (legacy) return legacy[1];
  return id;
}

/** Check if a model ID is a tier variant (not the base family model). */
function isTierVariant(id: string): boolean {
  return /^gpt-\d+(?:\.\d+)?-codex-(?:high|mid|low|max)$/.test(id);
}

export function useStatus(accountCount: number) {
  const [baseUrl, setBaseUrl] = useState("Loading...");
  const [apiKey, setApiKey] = useState("Loading...");
  const [models, setModels] = useState<string[]>([]);
  const [selectedModel, setSelectedModel] = useState("");
  const [modelCatalog, setModelCatalog] = useState<CatalogModel[]>([]);
  const [selectedEffort, setSelectedEffort] = useState("medium");
  const [selectedSpeed, setSelectedSpeed] = useState<string | null>(null);

  const fetchModels = useCallback(async (isInitial: boolean) => {
    try {
      // Fetch full catalog for effort info
      const catalogResp = await fetch("/v1/models/catalog");
      const catalogData: CatalogModel[] = await catalogResp.json();
      setModelCatalog(catalogData);

      // Also fetch model list (includes aliases)
      const resp = await fetch("/v1/models");
      const data = await resp.json();
      const ids: string[] = data.data.map((m: { id: string }) => m.id);
      if (ids.length > 0) {
        setModels(ids);
        if (isInitial) {
          const defaultModel = catalogData.find((m) => m.isDefault)?.id ?? ids[0] ?? "";
          setSelectedModel(defaultModel);
        } else {
          // On refresh: only reset if current selection is no longer available
          setSelectedModel((prev) => {
            if (ids.includes(prev)) return prev;
            return catalogData.find((m) => m.isDefault)?.id ?? ids[0] ?? prev;
          });
        }
      }
    } catch {
      if (isInitial) setModels([]);
    }
  }, []);

  useEffect(() => {
    let intervalId: ReturnType<typeof setInterval> | null = null;

    async function loadStatus() {
      try {
        const resp = await fetch("/auth/status");
        const data = await resp.json();
        if (!data.authenticated) return;
        setBaseUrl(`${window.location.origin}/v1`);
        setApiKey(data.proxy_api_key || "any-string");
        await fetchModels(true);

        // Refresh model list every 60s to pick up dynamic backend changes
        intervalId = setInterval(() => { fetchModels(false); }, 60_000);
      } catch (err) {
        console.error("Status load error:", err);
      }
    }
    loadStatus();

    return () => { if (intervalId) clearInterval(intervalId); };
  }, [fetchModels, accountCount]);

  // Build model families — group catalog by family, excluding tier variants
  const modelFamilies = useMemo((): ModelFamily[] => {
    if (modelCatalog.length === 0) return [];

    const familyMap = new Map<string, ModelFamily>();
    for (const m of modelCatalog) {
      const fid = getFamilyId(m.id);
      // Only use the base family model (not tier variants) to define the family
      if (isTierVariant(m.id)) continue;
      if (familyMap.has(fid)) continue;
      familyMap.set(fid, {
        id: fid,
        displayName: m.displayName,
        efforts: m.supportedReasoningEfforts,
        defaultEffort: m.defaultReasoningEffort,
      });
    }
    return [...familyMap.values()];
  }, [modelCatalog]);

  return {
    baseUrl,
    apiKey,
    models,
    selectedModel,
    setSelectedModel,
    selectedEffort,
    setSelectedEffort,
    selectedSpeed,
    setSelectedSpeed,
    modelFamilies,
    modelCatalog,
  };
}