From 2a63d958507a88bca7201590ce0e74c44d306090 Mon Sep 17 00:00:00 2001 From: Jake Turner Date: Wed, 25 Feb 2026 06:13:40 +0000 Subject: [PATCH] feat(Models): paginate available models endpoint --- admin/app/controllers/ollama_controller.ts | 1 + admin/app/controllers/settings_controller.ts | 4 +-- admin/app/services/ollama_service.ts | 28 ++++++++++++---- admin/app/validators/ollama.ts | 1 + admin/inertia/lib/api.ts | 11 ++++--- admin/inertia/pages/easy-setup/index.tsx | 12 +++++-- admin/inertia/pages/settings/models.tsx | 34 ++++++++++++++++---- 7 files changed, 68 insertions(+), 23 deletions(-) diff --git a/admin/app/controllers/ollama_controller.ts b/admin/app/controllers/ollama_controller.ts index d163d3b..17c08db 100644 --- a/admin/app/controllers/ollama_controller.ts +++ b/admin/app/controllers/ollama_controller.ts @@ -21,6 +21,7 @@ export default class OllamaController { sort: reqData.sort, recommendedOnly: reqData.recommendedOnly, query: reqData.query || null, + limit: reqData.limit || 15, }) } diff --git a/admin/app/controllers/settings_controller.ts b/admin/app/controllers/settings_controller.ts index 079b3f7..35f8800 100644 --- a/admin/app/controllers/settings_controller.ts +++ b/admin/app/controllers/settings_controller.ts @@ -51,12 +51,12 @@ export default class SettingsController { } async models({ inertia }: HttpContext) { - const availableModels = await this.ollamaService.getAvailableModels({ sort: 'pulls', recommendedOnly: false, query: null }); + const availableModels = await this.ollamaService.getAvailableModels({ sort: 'pulls', recommendedOnly: false, query: null, limit: 15 }); const installedModels = await this.ollamaService.getModels(); const chatSuggestionsEnabled = await KVStore.getValue('chat.suggestionsEnabled') return inertia.render('settings/models', { models: { - availableModels: availableModels || [], + availableModels: availableModels?.models || [], installedModels: installedModels || [], settings: { chatSuggestionsEnabled: parseBoolean(chatSuggestionsEnabled) diff --git a/admin/app/services/ollama_service.ts b/admin/app/services/ollama_service.ts index d9022aa..84e4aff 100644 --- a/admin/app/services/ollama_service.ts +++ b/admin/app/services/ollama_service.ts @@ -183,12 +183,13 @@ export class OllamaService { } async getAvailableModels( - { sort, recommendedOnly, query }: { sort?: 'pulls' | 'name'; recommendedOnly?: boolean, query: string | null } = { + { sort, recommendedOnly, query, limit }: { sort?: 'pulls' | 'name'; recommendedOnly?: boolean, query: string | null, limit?: number } = { sort: 'pulls', recommendedOnly: false, query: null, + limit: 15, } - ): Promise { + ): Promise<{ models: NomadOllamaModel[], hasMore: boolean } | null> { try { const models = await this.retrieveAndRefreshModels(sort) if (!models) { @@ -196,12 +197,18 @@ export class OllamaService { logger.warn( '[OllamaService] Returning fallback recommended models due to failure in fetching available models' ) - return FALLBACK_RECOMMENDED_OLLAMA_MODELS + return { + models: FALLBACK_RECOMMENDED_OLLAMA_MODELS, + hasMore: false + } } if (!recommendedOnly) { const filteredModels = query ? this.fuseSearchModels(models, query) : models - return filteredModels + return { + models: filteredModels.slice(0, limit || 15), + hasMore: filteredModels.length > (limit || 15) + } } // If recommendedOnly is true, only return the first three models (if sorted by pulls, these will be the top 3) @@ -217,10 +224,17 @@ export class OllamaService { }) if (query) { - return this.fuseSearchModels(recommendedModels, query) + const filteredRecommendedModels = this.fuseSearchModels(recommendedModels, query) + return { + models: filteredRecommendedModels, + hasMore: filteredRecommendedModels.length > (limit || 15) + } } - return recommendedModels + return { + models: recommendedModels, + hasMore: recommendedModels.length > (limit || 15) + } } catch (error) { logger.error( `[OllamaService] Failed to get available models: ${error instanceof Error ? error.message : error}` @@ -253,7 +267,7 @@ export class OllamaService { } const rawModels = response.data.models as NomadOllamaModel[] - + // Filter out tags where cloud is truthy, then remove models with no remaining tags const noCloud = rawModels .map((model) => ({ diff --git a/admin/app/validators/ollama.ts b/admin/app/validators/ollama.ts index 6b9ae9a..d9d48e3 100644 --- a/admin/app/validators/ollama.ts +++ b/admin/app/validators/ollama.ts @@ -18,5 +18,6 @@ export const getAvailableModelsSchema = vine.compile( sort: vine.enum(['pulls', 'name'] as const).optional(), recommendedOnly: vine.boolean().optional(), query: vine.string().trim().optional(), + limit: vine.number().positive().optional(), }) ) diff --git a/admin/inertia/lib/api.ts b/admin/inertia/lib/api.ts index 2ebf780..8964a40 100644 --- a/admin/inertia/lib/api.ts +++ b/admin/inertia/lib/api.ts @@ -196,10 +196,13 @@ class API { })() } - async getAvailableModels(query: string | null, recommendedOnly: boolean): Promise { + async getAvailableModels(params: { query?: string; recommendedOnly?: boolean; limit?: number }) { return catchInternal(async () => { - const response = await this.client.get('/ollama/models', { - params: { sort: 'pulls', recommendedOnly, query }, + const response = await this.client.get<{ + models: NomadOllamaModel[] + hasMore: boolean + }>('/ollama/models', { + params: { sort: 'pulls', ...params }, }) return response.data })() @@ -506,7 +509,7 @@ class API { // For 409 Conflict errors, throw a specific error that the UI can handle if (error.response?.status === 409) { const err = new Error(error.response?.data?.error || 'This benchmark has already been submitted to the repository') - ;(err as any).status = 409 + ; (err as any).status = 409 throw err } // For other errors, extract the message and throw diff --git a/admin/inertia/pages/easy-setup/index.tsx b/admin/inertia/pages/easy-setup/index.tsx index c929ffe..2f23c58 100644 --- a/admin/inertia/pages/easy-setup/index.tsx +++ b/admin/inertia/pages/easy-setup/index.tsx @@ -152,7 +152,13 @@ export default function EasySetupWizard(props: { system: { services: ServiceSlim const { data: recommendedModels, isLoading: isLoadingRecommendedModels } = useQuery({ queryKey: ['recommended-ollama-models'], - queryFn: () => api.getAvailableModels(null, true), + queryFn: async () => { + const res = await api.getAvailableModels({ recommendedOnly: true }) + if (!res) { + return [] + } + return res.models + }, refetchOnWindowFocus: false, }) @@ -736,7 +742,7 @@ export default function EasySetupWizard(props: { system: { services: ServiceSlim className={classNames( 'relative', selectedMapCollections.includes(collection.slug) && - 'ring-4 ring-desert-green rounded-lg', + 'ring-4 ring-desert-green rounded-lg', collection.all_installed && 'opacity-75', !isOnline && 'opacity-50 cursor-not-allowed' )} @@ -760,7 +766,7 @@ export default function EasySetupWizard(props: { system: { services: ServiceSlim const renderStep3 = () => { // Check if AI or Information capabilities are selected OR already installed - const isAiSelected = selectedServices.includes(SERVICE_NAMES.OLLAMA) || + const isAiSelected = selectedServices.includes(SERVICE_NAMES.OLLAMA) || installedServices.some((s) => s.service_name === SERVICE_NAMES.OLLAMA) const isInformationSelected = selectedServices.includes(SERVICE_NAMES.KIWIX) || installedServices.some((s) => s.service_name === SERVICE_NAMES.KIWIX) diff --git a/admin/inertia/pages/settings/models.tsx b/admin/inertia/pages/settings/models.tsx index 997892d..c817fa3 100644 --- a/admin/inertia/pages/settings/models.tsx +++ b/admin/inertia/pages/settings/models.tsx @@ -37,21 +37,29 @@ export default function ModelsPage(props: { const [query, setQuery] = useState('') const [queryUI, setQueryUI] = useState('') + const [limit, setLimit] = useState(15) const debouncedSetQuery = debounce((val: string) => { setQuery(val) }, 300) - const { data: availableModels, isLoading } = useQuery({ - queryKey: ['ollama', 'availableModels', query], + const { data: availableModelData, isFetching } = useQuery({ + queryKey: ['ollama', 'availableModels', query, limit], queryFn: async () => { - const res = await api.getAvailableModels(query, false) + const res = await api.getAvailableModels({ + query, + recommendedOnly: false, + limit, + }) if (!res) { - return [] + return { + models: [], + hasMore: false, + } } return res }, - initialData: props.models.availableModels, + initialData: { models: props.models.availableModels, hasMore: false }, }) async function handleInstallModel(modelName: string) { @@ -209,8 +217,8 @@ export default function ModelsPage(props: { title: 'Last Updated', }, ]} - data={availableModels || []} - loading={isLoading} + data={availableModelData?.models || []} + loading={isFetching} expandable={{ expandedRowRender: (record) => (
@@ -283,6 +291,18 @@ export default function ModelsPage(props: { ), }} /> +
+ {availableModelData?.hasMore && ( + { + setLimit((prev) => prev + 15) + }} + > + Load More + + )} +