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
This commit is contained in:
Daniel 2025-06-22 09:55:16 -05:00 committed by GitHub
parent 922f48335a
commit 02a4823250
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 99 additions and 10 deletions

View file

@ -31,6 +31,31 @@ describe("Ollama Fetcher", () => {
description: "Family: qwen3, Context: 40960, Size: 32.8B",
})
})
it("should handle models with null families field", () => {
const modelDataWithNullFamilies = {
...ollamaModelsData["qwen3-2to16:latest"],
details: {
...ollamaModelsData["qwen3-2to16:latest"].details,
families: null,
},
}
const parsedModel = parseOllamaModel(modelDataWithNullFamilies as any)
expect(parsedModel).toEqual({
maxTokens: 40960,
contextWindow: 40960,
supportsImages: false,
supportsComputerUse: false,
supportsPromptCache: true,
inputPrice: 0,
outputPrice: 0,
cacheWritesPrice: 0,
cacheReadsPrice: 0,
description: "Family: qwen3, Context: 40960, Size: 32.8B",
})
})
})
describe("getOllamaModels", () => {
@ -129,5 +154,69 @@ describe("Ollama Fetcher", () => {
consoleInfoSpy.mockRestore() // Restore original console.info
})
it("should handle models with null families field in API response", async () => {
const baseUrl = "http://localhost:11434"
const modelName = "test-model:latest"
const mockApiTagsResponse = {
models: [
{
name: modelName,
model: modelName,
modified_at: "2025-06-03T09:23:22.610222878-04:00",
size: 14333928010,
digest: "6a5f0c01d2c96c687d79e32fdd25b87087feb376bf9838f854d10be8cf3c10a5",
details: {
family: "llama",
families: null, // This is the case we're testing
format: "gguf",
parameter_size: "23.6B",
parent_model: "",
quantization_level: "Q4_K_M",
},
},
],
}
const mockApiShowResponse = {
license: "Mock License",
modelfile: "FROM /path/to/blob\nTEMPLATE {{ .Prompt }}",
parameters: "num_ctx 4096\nstop_token <eos>",
template: "{{ .System }}USER: {{ .Prompt }}ASSISTANT:",
modified_at: "2025-06-03T09:23:22.610222878-04:00",
details: {
parent_model: "",
format: "gguf",
family: "llama",
families: null, // This is the case we're testing
parameter_size: "23.6B",
quantization_level: "Q4_K_M",
},
model_info: {
"ollama.context_length": 4096,
"some.other.info": "value",
},
capabilities: ["completion"],
}
mockedAxios.get.mockResolvedValueOnce({ data: mockApiTagsResponse })
mockedAxios.post.mockResolvedValueOnce({ data: mockApiShowResponse })
const result = await getOllamaModels(baseUrl)
expect(mockedAxios.get).toHaveBeenCalledTimes(1)
expect(mockedAxios.get).toHaveBeenCalledWith(`${baseUrl}/api/tags`)
expect(mockedAxios.post).toHaveBeenCalledTimes(1)
expect(mockedAxios.post).toHaveBeenCalledWith(`${baseUrl}/api/show`, { model: modelName })
expect(typeof result).toBe("object")
expect(result).not.toBeInstanceOf(Array)
expect(Object.keys(result).length).toBe(1)
expect(result[modelName]).toBeDefined()
// Verify the model was parsed correctly despite null families
expect(result[modelName].description).toBe("Family: llama, Context: 4096, Size: 23.6B")
})
})
})

View file

@ -4,26 +4,26 @@ import { z } from "zod"
const OllamaModelDetailsSchema = z.object({
family: z.string(),
families: z.array(z.string()),
format: z.string(),
families: z.array(z.string()).nullable().optional(),
format: z.string().optional(),
parameter_size: z.string(),
parent_model: z.string(),
quantization_level: z.string(),
parent_model: z.string().optional(),
quantization_level: z.string().optional(),
})
const OllamaModelSchema = z.object({
details: OllamaModelDetailsSchema,
digest: z.string(),
digest: z.string().optional(),
model: z.string(),
modified_at: z.string(),
modified_at: z.string().optional(),
name: z.string(),
size: z.number(),
size: z.number().optional(),
})
const OllamaModelInfoResponseSchema = z.object({
modelfile: z.string(),
parameters: z.string(),
template: z.string(),
modelfile: z.string().optional(),
parameters: z.string().optional(),
template: z.string().optional(),
details: OllamaModelDetailsSchema,
model_info: z.record(z.string(), z.any()),
capabilities: z.array(z.string()).optional(),