From d314e82d173c057f6c8ec526b4045e87d475f99f Mon Sep 17 00:00:00 2001 From: Jake Turner Date: Thu, 5 Mar 2026 22:31:24 +0000 Subject: [PATCH] fix(AI): allow force refresh of models list --- admin/app/controllers/ollama_controller.ts | 1 + admin/app/services/ollama_service.ts | 19 ++++++++----- admin/app/validators/ollama.ts | 1 + admin/inertia/components/StyledButton.tsx | 4 ++- admin/inertia/lib/api.ts | 2 +- admin/inertia/pages/settings/models.tsx | 31 +++++++++++++++++++--- 6 files changed, 45 insertions(+), 13 deletions(-) diff --git a/admin/app/controllers/ollama_controller.ts b/admin/app/controllers/ollama_controller.ts index ee1640c..2b6cee3 100644 --- a/admin/app/controllers/ollama_controller.ts +++ b/admin/app/controllers/ollama_controller.ts @@ -22,6 +22,7 @@ export default class OllamaController { recommendedOnly: reqData.recommendedOnly, query: reqData.query || null, limit: reqData.limit || 15, + force: reqData.force, }) } diff --git a/admin/app/services/ollama_service.ts b/admin/app/services/ollama_service.ts index 84e4aff..24367a1 100644 --- a/admin/app/services/ollama_service.ts +++ b/admin/app/services/ollama_service.ts @@ -183,7 +183,7 @@ export class OllamaService { } async getAvailableModels( - { sort, recommendedOnly, query, limit }: { sort?: 'pulls' | 'name'; recommendedOnly?: boolean, query: string | null, limit?: number } = { + { sort, recommendedOnly, query, limit, force }: { sort?: 'pulls' | 'name'; recommendedOnly?: boolean, query: string | null, limit?: number, force?: boolean } = { sort: 'pulls', recommendedOnly: false, query: null, @@ -191,7 +191,7 @@ export class OllamaService { } ): Promise<{ models: NomadOllamaModel[], hasMore: boolean } | null> { try { - const models = await this.retrieveAndRefreshModels(sort) + const models = await this.retrieveAndRefreshModels(sort, force) if (!models) { // If we fail to get models from the API, return the fallback recommended models logger.warn( @@ -244,13 +244,18 @@ export class OllamaService { } private async retrieveAndRefreshModels( - sort?: 'pulls' | 'name' + sort?: 'pulls' | 'name', + force?: boolean ): Promise { try { - const cachedModels = await this.readModelsFromCache() - if (cachedModels) { - logger.info('[OllamaService] Using cached available models data') - return this.sortModels(cachedModels, sort) + if (!force) { + const cachedModels = await this.readModelsFromCache() + if (cachedModels) { + logger.info('[OllamaService] Using cached available models data') + return this.sortModels(cachedModels, sort) + } + } else { + logger.info('[OllamaService] Force refresh requested, bypassing cache') } logger.info('[OllamaService] Fetching fresh available models from API') diff --git a/admin/app/validators/ollama.ts b/admin/app/validators/ollama.ts index d9d48e3..2b754e8 100644 --- a/admin/app/validators/ollama.ts +++ b/admin/app/validators/ollama.ts @@ -19,5 +19,6 @@ export const getAvailableModelsSchema = vine.compile( recommendedOnly: vine.boolean().optional(), query: vine.string().trim().optional(), limit: vine.number().positive().optional(), + force: vine.boolean().optional(), }) ) diff --git a/admin/inertia/components/StyledButton.tsx b/admin/inertia/components/StyledButton.tsx index 43ee93e..5cc0387 100644 --- a/admin/inertia/components/StyledButton.tsx +++ b/admin/inertia/components/StyledButton.tsx @@ -20,6 +20,7 @@ const StyledButton: React.FC = ({ size = 'md', loading = false, fullWidth = false, + className, ...props }) => { const isDisabled = useMemo(() => { @@ -152,7 +153,8 @@ const StyledButton: React.FC = ({ getSizeClasses(), getVariantClasses(), isDisabled ? 'pointer-events-none opacity-60' : 'cursor-pointer', - 'items-center justify-center rounded-md font-semibold focus:outline-none focus:ring-2 focus:ring-desert-green-light focus:ring-offset-2 focus:ring-offset-desert-sand disabled:cursor-not-allowed disabled:shadow-none' + 'items-center justify-center rounded-md font-semibold focus:outline-none focus:ring-2 focus:ring-desert-green-light focus:ring-offset-2 focus:ring-offset-desert-sand disabled:cursor-not-allowed disabled:shadow-none', + className )} {...props} disabled={isDisabled} diff --git a/admin/inertia/lib/api.ts b/admin/inertia/lib/api.ts index c95def0..b710520 100644 --- a/admin/inertia/lib/api.ts +++ b/admin/inertia/lib/api.ts @@ -197,7 +197,7 @@ class API { })() } - async getAvailableModels(params: { query?: string; recommendedOnly?: boolean; limit?: number }) { + async getAvailableModels(params: { query?: string; recommendedOnly?: boolean; limit?: number; force?: boolean }) { return catchInternal(async () => { const response = await this.client.get<{ models: NomadOllamaModel[] diff --git a/admin/inertia/pages/settings/models.tsx b/admin/inertia/pages/settings/models.tsx index f21963f..ec7c38f 100644 --- a/admin/inertia/pages/settings/models.tsx +++ b/admin/inertia/pages/settings/models.tsx @@ -1,5 +1,5 @@ import { Head, router, usePage } from '@inertiajs/react' -import { useState } from 'react' +import { useRef, useState } from 'react' import StyledTable from '~/components/StyledTable' import SettingsLayout from '~/layouts/SettingsLayout' import { NomadOllamaModel } from '../../../types/ollama' @@ -16,7 +16,7 @@ import Switch from '~/components/inputs/Switch' import StyledSectionHeader from '~/components/StyledSectionHeader' import { useMutation, useQuery } from '@tanstack/react-query' import Input from '~/components/inputs/Input' -import { IconSearch } from '@tabler/icons-react' +import { IconSearch, IconRefresh } from '@tabler/icons-react' import useDebounce from '~/hooks/useDebounce' import ActiveModelDownloads from '~/components/ActiveModelDownloads' @@ -47,13 +47,19 @@ export default function ModelsPage(props: { setQuery(val) }, 300) - const { data: availableModelData, isFetching } = useQuery({ + const forceRefreshRef = useRef(false) + const [isForceRefreshing, setIsForceRefreshing] = useState(false) + + const { data: availableModelData, isFetching, refetch } = useQuery({ queryKey: ['ollama', 'availableModels', query, limit], queryFn: async () => { + const force = forceRefreshRef.current + forceRefreshRef.current = false const res = await api.getAvailableModels({ query, recommendedOnly: false, limit, + force: force || undefined, }) if (!res) { return { @@ -66,6 +72,14 @@ export default function ModelsPage(props: { initialData: { models: props.models.availableModels, hasMore: false }, }) + async function handleForceRefresh() { + forceRefreshRef.current = true + setIsForceRefreshing(true) + await refetch() + setIsForceRefreshing(false) + addNotification({ message: 'Model list refreshed from remote.', type: 'success' }) + } + async function handleInstallModel(modelName: string) { try { const res = await api.downloadModel(modelName) @@ -196,7 +210,7 @@ export default function ModelsPage(props: { -
+
} /> + + Refresh Models +
className="font-semibold mt-4"