feat(Models): paginate available models endpoint

This commit is contained in:
Jake Turner 2026-02-25 06:13:40 +00:00 committed by Jake Turner
parent a3f10dd158
commit 6874a2824f
7 changed files with 68 additions and 23 deletions

View File

@ -21,6 +21,7 @@ export default class OllamaController {
sort: reqData.sort,
recommendedOnly: reqData.recommendedOnly,
query: reqData.query || null,
limit: reqData.limit || 15,
})
}

View File

@ -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)

View File

@ -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<NomadOllamaModel[] | null> {
): 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}`

View File

@ -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(),
})
)

View File

@ -196,10 +196,13 @@ class API {
})()
}
async getAvailableModels(query: string | null, recommendedOnly: boolean): Promise<NomadOllamaModel[] | undefined> {
async getAvailableModels(params: { query?: string; recommendedOnly?: boolean; limit?: number }) {
return catchInternal(async () => {
const response = await this.client.get<NomadOllamaModel[]>('/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

View File

@ -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'
)}

View File

@ -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) => (
<div className="pl-14">
@ -283,6 +291,18 @@ export default function ModelsPage(props: {
),
}}
/>
<div className="flex justify-center mt-6">
{availableModelData?.hasMore && (
<StyledButton
variant="primary"
onClick={() => {
setLimit((prev) => prev + 15)
}}
>
Load More
</StyledButton>
)}
</div>
</main>
</div>
</SettingsLayout>