codex-proxy / shared /hooks /use-status.ts
icebear0828
feat: model list auto-sync + theme contrast fix
94a5578
raw
history blame
4.73 kB
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,
};
}