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.model}
+{download.error}
+