import type { SettingsService } from './SettingsService.js'; // v0.3.1 Cut F final fix — gemma 시리즈 default 정정. 본인 dogfood 환경 = gemma4:e4b // (텍스트). vision 변종은 gemma3 (현재 vision-capable) 또는 gemma4 (향후 출시 시). // 양 family 모두 hint 에 포함 — capability detection 이 future-proof. const VISION_FAMILIES = new Set(['gemma3', 'gemma4', 'llava', 'llama3.2-vision', 'minicpm-v', 'pixtral']); const VISION_NAME_HINTS = ['vision', 'vl', 'multimodal', 'gemma3', 'gemma4']; export interface OllamaModel { name: string; details?: { family?: string; families?: string[] }; } export function isVisionCapable(model: OllamaModel): boolean { if (model.details?.family && VISION_FAMILIES.has(model.details.family)) return true; if (model.details?.families?.some((f) => VISION_FAMILIES.has(f))) return true; const lower = model.name.toLowerCase(); return VISION_NAME_HINTS.some((h) => lower.includes(h)); } export interface RefreshDeps { settings: SettingsService; endpoint: string; now?: () => Date; fetchImpl?: typeof fetch; } export async function refreshVisionCache( deps: RefreshDeps ): Promise<{ ok: true; models: string[] } | { ok: false; reason: string }> { if (!(await deps.settings.isAiEnabled())) { return { ok: false, reason: 'ai_disabled' }; } const fetchFn = deps.fetchImpl ?? fetch; let body: { models?: OllamaModel[] }; try { const r = await fetchFn(`${deps.endpoint}/api/tags`); if (!r.ok) return { ok: false, reason: `tags http ${r.status}` }; body = (await r.json()) as { models?: OllamaModel[] }; } catch (e) { return { ok: false, reason: `unreachable: ${(e as Error).message}` }; } const capable = (body.models ?? []).filter(isVisionCapable).map((m) => m.name); const now = deps.now ? deps.now() : new Date(); await deps.settings.setVisionCapableCache(capable, now); return { ok: true, models: capable }; }