Skip to content

Commit 02a4823

Browse files
authored
fix: mark unused Ollama schema properties as optional (#5014)
fix: handle null families field in Ollama model details schema - Updated OllamaModelDetailsSchema to make families field nullable and optional - Made all unused properties optional in Ollama schemas to prevent validation errors - Added test cases to verify handling of null families field - Only required properties that are actually used in the code are now mandatory - Fixes Zod validation error when Ollama returns null for families array
1 parent 922f483 commit 02a4823

File tree

2 files changed

+99
-10
lines changed

2 files changed

+99
-10
lines changed

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

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,31 @@ describe("Ollama Fetcher", () => {
3131
description: "Family: qwen3, Context: 40960, Size: 32.8B",
3232
})
3333
})
34+
35+
it("should handle models with null families field", () => {
36+
const modelDataWithNullFamilies = {
37+
...ollamaModelsData["qwen3-2to16:latest"],
38+
details: {
39+
...ollamaModelsData["qwen3-2to16:latest"].details,
40+
families: null,
41+
},
42+
}
43+
44+
const parsedModel = parseOllamaModel(modelDataWithNullFamilies as any)
45+
46+
expect(parsedModel).toEqual({
47+
maxTokens: 40960,
48+
contextWindow: 40960,
49+
supportsImages: false,
50+
supportsComputerUse: false,
51+
supportsPromptCache: true,
52+
inputPrice: 0,
53+
outputPrice: 0,
54+
cacheWritesPrice: 0,
55+
cacheReadsPrice: 0,
56+
description: "Family: qwen3, Context: 40960, Size: 32.8B",
57+
})
58+
})
3459
})
3560

3661
describe("getOllamaModels", () => {
@@ -129,5 +154,69 @@ describe("Ollama Fetcher", () => {
129154

130155
consoleInfoSpy.mockRestore() // Restore original console.info
131156
})
157+
158+
it("should handle models with null families field in API response", async () => {
159+
const baseUrl = "http://localhost:11434"
160+
const modelName = "test-model:latest"
161+
162+
const mockApiTagsResponse = {
163+
models: [
164+
{
165+
name: modelName,
166+
model: modelName,
167+
modified_at: "2025-06-03T09:23:22.610222878-04:00",
168+
size: 14333928010,
169+
digest: "6a5f0c01d2c96c687d79e32fdd25b87087feb376bf9838f854d10be8cf3c10a5",
170+
details: {
171+
family: "llama",
172+
families: null, // This is the case we're testing
173+
format: "gguf",
174+
parameter_size: "23.6B",
175+
parent_model: "",
176+
quantization_level: "Q4_K_M",
177+
},
178+
},
179+
],
180+
}
181+
const mockApiShowResponse = {
182+
license: "Mock License",
183+
modelfile: "FROM /path/to/blob\nTEMPLATE {{ .Prompt }}",
184+
parameters: "num_ctx 4096\nstop_token <eos>",
185+
template: "{{ .System }}USER: {{ .Prompt }}ASSISTANT:",
186+
modified_at: "2025-06-03T09:23:22.610222878-04:00",
187+
details: {
188+
parent_model: "",
189+
format: "gguf",
190+
family: "llama",
191+
families: null, // This is the case we're testing
192+
parameter_size: "23.6B",
193+
quantization_level: "Q4_K_M",
194+
},
195+
model_info: {
196+
"ollama.context_length": 4096,
197+
"some.other.info": "value",
198+
},
199+
capabilities: ["completion"],
200+
}
201+
202+
mockedAxios.get.mockResolvedValueOnce({ data: mockApiTagsResponse })
203+
mockedAxios.post.mockResolvedValueOnce({ data: mockApiShowResponse })
204+
205+
const result = await getOllamaModels(baseUrl)
206+
207+
expect(mockedAxios.get).toHaveBeenCalledTimes(1)
208+
expect(mockedAxios.get).toHaveBeenCalledWith(`${baseUrl}/api/tags`)
209+
210+
expect(mockedAxios.post).toHaveBeenCalledTimes(1)
211+
expect(mockedAxios.post).toHaveBeenCalledWith(`${baseUrl}/api/show`, { model: modelName })
212+
213+
expect(typeof result).toBe("object")
214+
expect(result).not.toBeInstanceOf(Array)
215+
expect(Object.keys(result).length).toBe(1)
216+
expect(result[modelName]).toBeDefined()
217+
218+
// Verify the model was parsed correctly despite null families
219+
expect(result[modelName].description).toBe("Family: llama, Context: 4096, Size: 23.6B")
220+
})
132221
})
133222
})

src/api/providers/fetchers/ollama.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,26 @@ import { z } from "zod"
44

55
const OllamaModelDetailsSchema = z.object({
66
family: z.string(),
7-
families: z.array(z.string()),
8-
format: z.string(),
7+
families: z.array(z.string()).nullable().optional(),
8+
format: z.string().optional(),
99
parameter_size: z.string(),
10-
parent_model: z.string(),
11-
quantization_level: z.string(),
10+
parent_model: z.string().optional(),
11+
quantization_level: z.string().optional(),
1212
})
1313

1414
const OllamaModelSchema = z.object({
1515
details: OllamaModelDetailsSchema,
16-
digest: z.string(),
16+
digest: z.string().optional(),
1717
model: z.string(),
18-
modified_at: z.string(),
18+
modified_at: z.string().optional(),
1919
name: z.string(),
20-
size: z.number(),
20+
size: z.number().optional(),
2121
})
2222

2323
const OllamaModelInfoResponseSchema = z.object({
24-
modelfile: z.string(),
25-
parameters: z.string(),
26-
template: z.string(),
24+
modelfile: z.string().optional(),
25+
parameters: z.string().optional(),
26+
template: z.string().optional(),
2727
details: OllamaModelDetailsSchema,
2828
model_info: z.record(z.string(), z.any()),
2929
capabilities: z.array(z.string()).optional(),

0 commit comments

Comments
 (0)