diff --git a/apps/server/src/providers/provider-factory.ts b/apps/server/src/providers/provider-factory.ts index 0ebb6b5f..8e5cc509 100644 --- a/apps/server/src/providers/provider-factory.ts +++ b/apps/server/src/providers/provider-factory.ts @@ -156,6 +156,32 @@ export class ProviderFactory { static getRegisteredProviderNames(): string[] { return Array.from(providerRegistry.keys()); } + + /** + * Check if a specific model supports vision/image input + * + * @param modelId Model identifier + * @returns Whether the model supports vision (defaults to true if model not found) + */ + static modelSupportsVision(modelId: string): boolean { + const provider = this.getProviderForModel(modelId); + const models = provider.getAvailableModels(); + + // Find the model in the available models list + for (const model of models) { + if ( + model.id === modelId || + model.modelString === modelId || + model.id.endsWith(`-${modelId}`) || + model.modelString === modelId.replace(/^(claude|cursor|codex)-/, '') + ) { + return model.supportsVision ?? true; + } + } + + // Default to true (Claude SDK supports vision by default) + return true; + } } // ============================================================================= diff --git a/apps/server/src/services/agent-service.ts b/apps/server/src/services/agent-service.ts index 3c7fc184..1a45c1ad 100644 --- a/apps/server/src/services/agent-service.ts +++ b/apps/server/src/services/agent-service.ts @@ -174,6 +174,18 @@ export class AgentService { session.thinkingLevel = thinkingLevel; } + // Validate vision support before processing images + const effectiveModel = model || session.model; + if (imagePaths && imagePaths.length > 0 && effectiveModel) { + const supportsVision = ProviderFactory.modelSupportsVision(effectiveModel); + if (!supportsVision) { + throw new Error( + `This model (${effectiveModel}) does not support image input. ` + + `Please switch to a model that supports vision, or remove the images and try again.` + ); + } + } + // Read images and convert to base64 const images: Message['images'] = []; if (imagePaths && imagePaths.length > 0) { diff --git a/apps/server/src/services/auto-mode-service.ts b/apps/server/src/services/auto-mode-service.ts index 078512a3..992dda10 100644 --- a/apps/server/src/services/auto-mode-service.ts +++ b/apps/server/src/services/auto-mode-service.ts @@ -1989,6 +1989,18 @@ This helps parse your summary correctly in the output logs.`; const planningMode = options?.planningMode || 'skip'; const previousContent = options?.previousContent; + // Validate vision support before processing images + const effectiveModel = model || 'claude-sonnet-4-20250514'; + if (imagePaths && imagePaths.length > 0) { + const supportsVision = ProviderFactory.modelSupportsVision(effectiveModel); + if (!supportsVision) { + throw new Error( + `This model (${effectiveModel}) does not support image input. ` + + `Please switch to a model that supports vision (like Claude models), or remove the images and try again.` + ); + } + } + // Check if this planning mode can generate a spec/plan that needs approval // - spec and full always generate specs // - lite only generates approval-ready content when requirePlanApproval is true diff --git a/apps/server/tests/unit/lib/validation-storage.test.ts b/apps/server/tests/unit/lib/validation-storage.test.ts index f135da76..05b44fc7 100644 --- a/apps/server/tests/unit/lib/validation-storage.test.ts +++ b/apps/server/tests/unit/lib/validation-storage.test.ts @@ -179,8 +179,7 @@ describe('validation-storage.ts', () => { }); it('should return false for validation exactly at 24 hours', () => { - const exactDate = new Date(); - exactDate.setHours(exactDate.getHours() - 24); + const exactDate = new Date(Date.now() - 24 * 60 * 60 * 1000 + 100); const validation = createMockValidation({ validatedAt: exactDate.toISOString(),