Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.

Commit 14c254d

Browse files
committed
fix: add fallback vision detection for Ollama models with incomplete capabilities
1 parent ad25634 commit 14c254d

2 files changed

Lines changed: 158 additions & 1 deletion

File tree

src/api/providers/fetchers/__tests__/ollama.test.ts

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,123 @@ describe("Ollama Fetcher", () => {
114114
expect(parsedModel!.supportsImages).toBe(true)
115115
expect(parsedModel!.contextWindow).toBeGreaterThan(0)
116116
})
117+
118+
it("should detect vision via details.families when capabilities omits vision", () => {
119+
const modelData = {
120+
...ollamaModelsData["qwen3-2to16:latest"],
121+
details: {
122+
...ollamaModelsData["qwen3-2to16:latest"].details,
123+
families: ["gemma4", "clip"],
124+
},
125+
capabilities: ["completion", "tools"], // no "vision"
126+
}
127+
128+
const parsedModel = parseOllamaModel(modelData as any)
129+
130+
expect(parsedModel).not.toBeNull()
131+
expect(parsedModel!.supportsImages).toBe(true)
132+
})
133+
134+
it("should detect vision via model_info keys when capabilities and families lack vision indicators", () => {
135+
const modelData = {
136+
...ollamaModelsData["qwen3-2to16:latest"],
137+
details: {
138+
...ollamaModelsData["qwen3-2to16:latest"].details,
139+
families: ["gemma4"],
140+
},
141+
model_info: {
142+
...ollamaModelsData["qwen3-2to16:latest"].model_info,
143+
"gemma4_vision_encoder.block_count": 27,
144+
"gemma4_vision_encoder.embedding_length": 1152,
145+
},
146+
capabilities: ["completion", "tools"], // no "vision"
147+
}
148+
149+
const parsedModel = parseOllamaModel(modelData as any)
150+
151+
expect(parsedModel).not.toBeNull()
152+
expect(parsedModel!.supportsImages).toBe(true)
153+
})
154+
155+
it("should detect vision via siglip family in details.families", () => {
156+
const modelData = {
157+
...ollamaModelsData["qwen3-2to16:latest"],
158+
details: {
159+
...ollamaModelsData["qwen3-2to16:latest"].details,
160+
families: ["gemma4", "siglip"],
161+
},
162+
capabilities: ["completion", "tools"],
163+
}
164+
165+
const parsedModel = parseOllamaModel(modelData as any)
166+
167+
expect(parsedModel).not.toBeNull()
168+
expect(parsedModel!.supportsImages).toBe(true)
169+
})
170+
171+
it("should detect vision via mmproj family in details.families", () => {
172+
const modelData = {
173+
...ollamaModelsData["qwen3-2to16:latest"],
174+
details: {
175+
...ollamaModelsData["qwen3-2to16:latest"].details,
176+
families: ["llama", "mmproj"],
177+
},
178+
capabilities: ["completion", "tools"],
179+
}
180+
181+
const parsedModel = parseOllamaModel(modelData as any)
182+
183+
expect(parsedModel).not.toBeNull()
184+
expect(parsedModel!.supportsImages).toBe(true)
185+
})
186+
187+
it("should detect vision via mllama family in details.families", () => {
188+
const modelData = {
189+
...ollamaModelsData["qwen3-2to16:latest"],
190+
details: {
191+
...ollamaModelsData["qwen3-2to16:latest"].details,
192+
families: ["llama", "mllama"],
193+
},
194+
capabilities: ["completion", "tools"],
195+
}
196+
197+
const parsedModel = parseOllamaModel(modelData as any)
198+
199+
expect(parsedModel).not.toBeNull()
200+
expect(parsedModel!.supportsImages).toBe(true)
201+
})
202+
203+
it("should not detect vision when no indicators are present", () => {
204+
const modelData = {
205+
...ollamaModelsData["qwen3-2to16:latest"],
206+
details: {
207+
...ollamaModelsData["qwen3-2to16:latest"].details,
208+
families: ["qwen3"],
209+
},
210+
capabilities: ["completion", "tools"],
211+
}
212+
213+
const parsedModel = parseOllamaModel(modelData as any)
214+
215+
expect(parsedModel).not.toBeNull()
216+
expect(parsedModel!.supportsImages).toBe(false)
217+
})
218+
219+
it("should handle case-insensitive family matching for vision detection", () => {
220+
const modelData = {
221+
...ollamaModelsData["qwen3-2to16:latest"],
222+
details: {
223+
...ollamaModelsData["qwen3-2to16:latest"].details,
224+
families: ["gemma4", "CLIP"],
225+
},
226+
capabilities: ["completion", "tools"],
227+
}
228+
229+
const parsedModel = parseOllamaModel(modelData as any)
230+
231+
expect(parsedModel).not.toBeNull()
232+
expect(parsedModel!.supportsImages).toBe(true)
233+
})
117234
})
118235

119236
describe("getOllamaModels", () => {

src/api/providers/fetchers/ollama.ts

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,46 @@ type OllamaModelsResponse = z.infer<typeof OllamaModelsResponseSchema>
3737

3838
type OllamaModelInfoResponse = z.infer<typeof OllamaModelInfoResponseSchema>
3939

40+
/**
41+
* Known vision-related family names that appear in `details.families` for
42+
* multimodal models in Ollama. When a model's `capabilities` array omits
43+
* "vision" (as happens with some third-party quants like unsloth), we fall
44+
* back to checking these families.
45+
*/
46+
const VISION_FAMILIES = new Set(["clip", "siglip", "mmproj", "mllama"])
47+
48+
/**
49+
* Regex patterns matched against `model_info` keys to detect a vision
50+
* encoder even when `capabilities` and `details.families` are both silent.
51+
*/
52+
const VISION_MODEL_INFO_PATTERN = /vision|clip|siglip|mmproj|image_encoder/i
53+
54+
/**
55+
* Determines whether the model supports images by checking:
56+
* 1. The authoritative `capabilities` array (preferred).
57+
* 2. `details.families` for known vision encoder families.
58+
* 3. `model_info` keys for vision-related architecture indicators.
59+
*/
60+
const detectVisionSupport = (rawModel: OllamaModelInfoResponse): boolean => {
61+
// 1. Authoritative check
62+
if (rawModel.capabilities?.includes("vision")) {
63+
return true
64+
}
65+
66+
// 2. Families check
67+
const families = rawModel.details.families
68+
if (families?.some((f) => VISION_FAMILIES.has(f.toLowerCase()))) {
69+
return true
70+
}
71+
72+
// 3. model_info key check
73+
if (Object.keys(rawModel.model_info).some((k) => VISION_MODEL_INFO_PATTERN.test(k))) {
74+
return true
75+
}
76+
77+
return false
78+
}
79+
4080
export const parseOllamaModel = (rawModel: OllamaModelInfoResponse): ModelInfo | null => {
4181
const contextKey = Object.keys(rawModel.model_info).find((k) => k.includes("context_length"))
4282
const contextWindow =
@@ -52,7 +92,7 @@ export const parseOllamaModel = (rawModel: OllamaModelInfoResponse): ModelInfo |
5292
description: `Family: ${rawModel.details.family}, Context: ${contextWindow}, Size: ${rawModel.details.parameter_size}`,
5393
contextWindow: contextWindow || ollamaDefaultModelInfo.contextWindow,
5494
supportsPromptCache: true,
55-
supportsImages: rawModel.capabilities?.includes("vision"),
95+
supportsImages: detectVisionSupport(rawModel),
5696
maxTokens: contextWindow || ollamaDefaultModelInfo.contextWindow,
5797
})
5898

0 commit comments

Comments
 (0)