Skip to content

Commit e98a4de

Browse files
committed
Cache model catalog responses
1 parent 348b334 commit e98a4de

File tree

7 files changed

+639
-42
lines changed

7 files changed

+639
-42
lines changed

lib/public/js/components/models-tab/use-models.js

Lines changed: 74 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,35 @@ import {
88
} from "../../lib/api.js";
99
import { showToast } from "../toast.js";
1010
import { useCachedFetch } from "../../hooks/use-cached-fetch.js";
11+
import { usePolling } from "../../hooks/usePolling.js";
12+
import { invalidateCache } from "../../lib/api-cache.js";
13+
import {
14+
getModelCatalogModels,
15+
isModelCatalogRefreshing,
16+
kModelCatalogCacheKey,
17+
kModelCatalogPollIntervalMs,
18+
} from "../../lib/model-catalog.js";
1119

1220
let kModelsTabCache = null;
1321
const getCredentialValue = (value) =>
1422
String(value?.key || value?.token || value?.access || "").trim();
23+
const kNoModelsFoundError = "No models found";
24+
const kModelSettingsLoadError = "Failed to load model settings";
1525

1626
export const useModels = (agentId) => {
1727
const isScoped = !!agentId;
1828
const normalizedAgentId = String(agentId || "").trim();
1929
const useCache = !isScoped;
2030
const [catalog, setCatalog] = useState(() => (useCache && kModelsTabCache?.catalog) || []);
31+
const [catalogStatus, setCatalogStatus] = useState(
32+
() =>
33+
(useCache && kModelsTabCache?.catalogStatus) || {
34+
source: "",
35+
fetchedAt: null,
36+
stale: false,
37+
refreshing: false,
38+
},
39+
);
2140
const [primary, setPrimary] = useState(() => (useCache && kModelsTabCache?.primary) || "");
2241
const [configuredModels, setConfiguredModels] = useState(
2342
() => (useCache && kModelsTabCache?.configuredModels) || {},
@@ -48,7 +67,7 @@ export const useModels = (agentId) => {
4867
const modelsConfigCacheKey = normalizedAgentId
4968
? `/api/models/config?agentId=${encodeURIComponent(normalizedAgentId)}`
5069
: "/api/models/config";
51-
const catalogFetchState = useCachedFetch("/api/models", fetchModels, {
70+
const catalogFetchState = useCachedFetch(kModelCatalogCacheKey, fetchModels, {
5271
maxAgeMs: 30000,
5372
});
5473
const configFetchState = useCachedFetch(
@@ -59,6 +78,41 @@ export const useModels = (agentId) => {
5978
const codexFetchState = useCachedFetch("/api/codex/status", fetchCodexStatus, {
6079
maxAgeMs: 15000,
6180
});
81+
const catalogPoll = usePolling(fetchModels, kModelCatalogPollIntervalMs, {
82+
enabled: ready && isModelCatalogRefreshing(catalogStatus),
83+
pauseWhenHidden: true,
84+
cacheKey: kModelCatalogCacheKey,
85+
});
86+
87+
const syncCatalogError = useCallback((catalogModels) => {
88+
setError((current) => {
89+
if (catalogModels.length > 0) {
90+
return current === kNoModelsFoundError ? "" : current;
91+
}
92+
return current || kNoModelsFoundError;
93+
});
94+
}, []);
95+
96+
const applyCatalogResult = useCallback(
97+
(catalogResult) => {
98+
const catalogModels = getModelCatalogModels(catalogResult);
99+
const nextCatalogStatus = {
100+
source: String(catalogResult?.source || ""),
101+
fetchedAt: Number(catalogResult?.fetchedAt || 0) || null,
102+
stale: Boolean(catalogResult?.stale),
103+
refreshing: Boolean(catalogResult?.refreshing),
104+
};
105+
setCatalog(catalogModels);
106+
setCatalogStatus(nextCatalogStatus);
107+
updateCache({
108+
catalog: catalogModels,
109+
catalogStatus: nextCatalogStatus,
110+
});
111+
syncCatalogError(catalogModels);
112+
return catalogModels;
113+
},
114+
[syncCatalogError, updateCache],
115+
);
62116

63117
const refresh = useCallback(async () => {
64118
if (!ready) setLoading(true);
@@ -69,10 +123,7 @@ export const useModels = (agentId) => {
69123
configFetchState.refresh({ force: true }),
70124
codexFetchState.refresh({ force: true }),
71125
]);
72-
const catalogModels = Array.isArray(catalogResult.models)
73-
? catalogResult.models
74-
: [];
75-
setCatalog(catalogModels);
126+
const catalogModels = applyCatalogResult(catalogResult);
76127
const p = configResult.primary || "";
77128
const cm = configResult.configuredModels || {};
78129
const ap = configResult.authProfiles || [];
@@ -94,20 +145,31 @@ export const useModels = (agentId) => {
94145
authOrder: ao,
95146
codexStatus: codex || { connected: false },
96147
});
97-
if (!catalogModels.length) setError("No models found");
98148
} catch (err) {
99-
setError("Failed to load model settings");
100-
showToast(`Failed to load model settings: ${err.message}`, "error");
149+
setError(kModelSettingsLoadError);
150+
showToast(`${kModelSettingsLoadError}: ${err.message}`, "error");
101151
} finally {
102152
setReady(true);
103153
setLoading(false);
104154
}
105-
}, [catalogFetchState, codexFetchState, configFetchState, ready, updateCache, agentId, isScoped]);
155+
}, [
156+
applyCatalogResult,
157+
catalogFetchState,
158+
codexFetchState,
159+
configFetchState,
160+
ready,
161+
updateCache,
162+
]);
106163

107164
useEffect(() => {
108165
refresh();
109166
}, [agentId]);
110167

168+
useEffect(() => {
169+
if (!catalogPoll.data) return;
170+
applyCatalogResult(catalogPoll.data);
171+
}, [applyCatalogResult, catalogPoll.data]);
172+
111173
const stableStringify = (obj) =>
112174
JSON.stringify(Object.keys(obj).sort().reduce((acc, k) => { acc[k] = obj[k]; return acc; }, {}));
113175

@@ -261,6 +323,7 @@ export const useModels = (agentId) => {
261323
if (result.syncWarning) {
262324
showToast(`Saved, but git-sync failed: ${result.syncWarning}`, "warning");
263325
}
326+
invalidateCache(kModelCatalogCacheKey);
264327
await refresh();
265328
} catch (err) {
266329
showToast(err.message || "Failed to save changes", "error");
@@ -274,6 +337,8 @@ export const useModels = (agentId) => {
274337
profileEdits,
275338
orderEdits,
276339
authProfiles,
340+
isScoped,
341+
agentId,
277342
refresh,
278343
]);
279344

lib/public/js/lib/model-catalog.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { getFeaturedModels } from "./model-config.js";
2+
3+
export const kModelCatalogCacheKey = "/api/models";
4+
export const kModelCatalogPollIntervalMs = 3000;
5+
6+
export const getModelCatalogModels = (payload) =>
7+
Array.isArray(payload?.models) ? payload.models : [];
8+
9+
export const isModelCatalogRefreshing = (payload) =>
10+
Boolean(payload?.refreshing);
11+
12+
export const getInitialOnboardingModelKey = ({
13+
catalog = [],
14+
currentModelKey = "",
15+
} = {}) => {
16+
const normalizedCurrent = String(currentModelKey || "").trim();
17+
if (normalizedCurrent) return normalizedCurrent;
18+
const featuredModels = getFeaturedModels(catalog);
19+
return String(featuredModels[0]?.key || catalog[0]?.key || "");
20+
};

0 commit comments

Comments
 (0)