diff --git a/admin/app/jobs/download_model_job.ts b/admin/app/jobs/download_model_job.ts index ccc5207..61908e4 100644 --- a/admin/app/jobs/download_model_job.ts +++ b/admin/app/jobs/download_model_job.ts @@ -1,4 +1,4 @@ -import { Job } from 'bullmq' +import { Job, UnrecoverableError } from 'bullmq' import { QueueService } from '#services/queue_service' import { createHash } from 'crypto' import logger from '@adonisjs/core/services/logger' @@ -63,6 +63,10 @@ export class DownloadModelJob { logger.error( `[DownloadModelJob] Failed to initiate download for model ${modelName}: ${result.message}` ) + // Don't retry errors that will never succeed (e.g., Ollama version too old) + if (result.retryable === false) { + throw new UnrecoverableError(result.message) + } throw new Error(`Failed to initiate download for model: ${result.message}`) } @@ -85,6 +89,15 @@ export class DownloadModelJob { const queue = queueService.getQueue(this.queue) const jobId = this.getJobId(params.modelName) + // Clear any previous failed job so a fresh attempt can be dispatched + const existing = await queue.getJob(jobId) + if (existing) { + const state = await existing.getState() + if (state === 'failed') { + await existing.remove() + } + } + try { const job = await queue.add(this.key, params, { jobId, @@ -104,9 +117,9 @@ export class DownloadModelJob { } } catch (error) { if (error.message.includes('job already exists')) { - const existing = await queue.getJob(jobId) + const active = await queue.getJob(jobId) return { - job: existing, + job: active, created: false, message: `Job already exists for model ${params.modelName}`, } diff --git a/admin/app/services/ollama_service.ts b/admin/app/services/ollama_service.ts index 24367a1..fa7b9f9 100644 --- a/admin/app/services/ollama_service.ts +++ b/admin/app/services/ollama_service.ts @@ -51,7 +51,7 @@ export class OllamaService { * @param model Model name to download * @returns Success status and message */ - async downloadModel(model: string, progressCallback?: (percent: number) => void): Promise<{ success: boolean; message: string }> { + async downloadModel(model: string, progressCallback?: (percent: number) => void): Promise<{ success: boolean; message: string; retryable?: boolean }> { try { await this._ensureDependencies() if (!this.ollama) { @@ -86,11 +86,21 @@ export class OllamaService { logger.info(`[OllamaService] Model "${model}" downloaded successfully.`) return { success: true, message: 'Model downloaded successfully.' } } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) logger.error( - `[OllamaService] Failed to download model "${model}": ${error instanceof Error ? error.message : error - }` + `[OllamaService] Failed to download model "${model}": ${errorMessage}` ) - return { success: false, message: 'Failed to download model.' } + + // Check for version mismatch (Ollama 412 response) + const isVersionMismatch = errorMessage.includes('newer version of Ollama') + const userMessage = isVersionMismatch + ? 'This model requires a newer version of Ollama. Please update AI Assistant from the Apps page.' + : `Failed to download model: ${errorMessage}` + + // Broadcast failure to connected clients so UI can show the error + this.broadcastDownloadError(model, userMessage) + + return { success: false, message: userMessage, retryable: !isVersionMismatch } } } @@ -379,6 +389,15 @@ export class OllamaService { return models } + private broadcastDownloadError(model: string, error: string) { + transmit.broadcast(BROADCAST_CHANNELS.OLLAMA_MODEL_DOWNLOAD, { + model, + percent: -1, + error, + timestamp: new Date().toISOString(), + }) + } + private broadcastDownloadProgress(model: string, percent: number) { transmit.broadcast(BROADCAST_CHANNELS.OLLAMA_MODEL_DOWNLOAD, { model, diff --git a/admin/inertia/components/ActiveModelDownloads.tsx b/admin/inertia/components/ActiveModelDownloads.tsx index d1d0b85..c927126 100644 --- a/admin/inertia/components/ActiveModelDownloads.tsx +++ b/admin/inertia/components/ActiveModelDownloads.tsx @@ -1,6 +1,7 @@ import useOllamaModelDownloads from '~/hooks/useOllamaModelDownloads' import HorizontalBarChart from './HorizontalBarChart' import StyledSectionHeader from './StyledSectionHeader' +import { IconAlertTriangle } from '@tabler/icons-react' interface ActiveModelDownloadsProps { withHeader?: boolean @@ -17,19 +18,31 @@ const ActiveModelDownloads = ({ withHeader = false }: ActiveModelDownloadsProps) downloads.map((download) => (
- + {download.error ? ( +
+ +
+

{download.model}

+

{download.error}

+
+
+ ) : ( + + )}
)) ) : ( diff --git a/admin/inertia/hooks/useOllamaModelDownloads.ts b/admin/inertia/hooks/useOllamaModelDownloads.ts index 4417321..8fc5460 100644 --- a/admin/inertia/hooks/useOllamaModelDownloads.ts +++ b/admin/inertia/hooks/useOllamaModelDownloads.ts @@ -5,6 +5,7 @@ export type OllamaModelDownload = { model: string percent: number timestamp: string + error?: string } export default function useOllamaModelDownloads() { @@ -17,7 +18,19 @@ export default function useOllamaModelDownloads() { setDownloads((prev) => { const updated = new Map(prev) - if (data.percent >= 100) { + if (data.percent === -1) { + // Download failed — show error state, auto-remove after 15 seconds + updated.set(data.model, data) + const errorTimeout = setTimeout(() => { + timeoutsRef.current.delete(errorTimeout) + setDownloads((current) => { + const next = new Map(current) + next.delete(data.model) + return next + }) + }, 15000) + timeoutsRef.current.add(errorTimeout) + } else if (data.percent >= 100) { // If download is complete, keep it for a short time before removing to allow UI to show 100% progress updated.set(data.model, data) const timeout = setTimeout(() => {