Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 25 additions & 20 deletions src/api/providers/fetchers/__tests__/ollama.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ describe("Ollama Fetcher", () => {
describe("parseOllamaModel", () => {
it("should correctly parse Ollama model info", () => {
const modelData = ollamaModelsData["qwen3-2to16:latest"]
const parsedModel = parseOllamaModel(modelData)
const { modelInfo } = parseOllamaModel(modelData)

expect(parsedModel).toEqual({
expect(modelInfo).toEqual({
maxTokens: 40960,
contextWindow: 40960,
supportsImages: false,
Expand All @@ -39,9 +39,9 @@ describe("Ollama Fetcher", () => {
},
}

const parsedModel = parseOllamaModel(modelDataWithNullFamilies as any)
const { modelInfo } = parseOllamaModel(modelDataWithNullFamilies as any)

expect(parsedModel).toEqual({
expect(modelInfo).toEqual({
maxTokens: 40960,
contextWindow: 40960,
supportsImages: false,
Expand All @@ -54,16 +54,18 @@ describe("Ollama Fetcher", () => {
})
})

it("should return null when capabilities does not include 'tools'", () => {
it("should return null with reason when capabilities does not include 'tools'", () => {
const modelDataWithoutTools = {
...ollamaModelsData["qwen3-2to16:latest"],
capabilities: ["completion"], // No "tools" capability
}

const parsedModel = parseOllamaModel(modelDataWithoutTools as any)
const { modelInfo, filteredReason } = parseOllamaModel(modelDataWithoutTools as any, "test-model")

// Models without tools capability are filtered out (return null)
expect(parsedModel).toBeNull()
expect(modelInfo).toBeNull()
expect(filteredReason).toContain("test-model")
expect(filteredReason).toContain("do not include 'tools'")
})

it("should return model info when capabilities includes 'tools'", () => {
Expand All @@ -72,22 +74,25 @@ describe("Ollama Fetcher", () => {
capabilities: ["completion", "tools"], // Has "tools" capability
}

const parsedModel = parseOllamaModel(modelDataWithTools as any)
const { modelInfo, filteredReason } = parseOllamaModel(modelDataWithTools as any)

expect(parsedModel).not.toBeNull()
expect(parsedModel!.contextWindow).toBeGreaterThan(0)
expect(modelInfo).not.toBeNull()
expect(modelInfo!.contextWindow).toBeGreaterThan(0)
expect(filteredReason).toBeUndefined()
})

it("should return null when capabilities is undefined (no tool support)", () => {
it("should return null with reason when capabilities is undefined (no tool support)", () => {
const modelDataWithoutCapabilities = {
...ollamaModelsData["qwen3-2to16:latest"],
capabilities: undefined, // No capabilities array
}

const parsedModel = parseOllamaModel(modelDataWithoutCapabilities as any)
const { modelInfo, filteredReason } = parseOllamaModel(modelDataWithoutCapabilities as any, "test-model")

// Models without explicit tools capability are filtered out
expect(parsedModel).toBeNull()
expect(modelInfo).toBeNull()
expect(filteredReason).toContain("test-model")
expect(filteredReason).toContain("no capabilities reported")
})

it("should return null when model has vision but no tools capability", () => {
Expand All @@ -96,10 +101,10 @@ describe("Ollama Fetcher", () => {
capabilities: ["completion", "vision"],
}

const parsedModel = parseOllamaModel(modelDataWithVision as any)
const { modelInfo } = parseOllamaModel(modelDataWithVision as any)

// No "tools" capability means filtered out
expect(parsedModel).toBeNull()
expect(modelInfo).toBeNull()
})

it("should return model with both vision and tools when both capabilities present", () => {
Expand All @@ -108,11 +113,11 @@ describe("Ollama Fetcher", () => {
capabilities: ["completion", "vision", "tools"],
}

const parsedModel = parseOllamaModel(modelDataWithBoth as any)
const { modelInfo } = parseOllamaModel(modelDataWithBoth as any)

expect(parsedModel).not.toBeNull()
expect(parsedModel!.supportsImages).toBe(true)
expect(parsedModel!.contextWindow).toBeGreaterThan(0)
expect(modelInfo).not.toBeNull()
expect(modelInfo!.supportsImages).toBe(true)
expect(modelInfo!.contextWindow).toBeGreaterThan(0)
})
})

Expand Down Expand Up @@ -177,7 +182,7 @@ describe("Ollama Fetcher", () => {
expect(Object.keys(result).length).toBe(1)
expect(result[modelName]).toBeDefined()

const expectedParsedDetails = parseOllamaModel(mockApiShowResponse as any)
const { modelInfo: expectedParsedDetails } = parseOllamaModel(mockApiShowResponse as any)
expect(result[modelName]).toEqual(expectedParsedDetails)
})

Expand Down
28 changes: 24 additions & 4 deletions src/api/providers/fetchers/ollama.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,21 @@ type OllamaModelsResponse = z.infer<typeof OllamaModelsResponseSchema>

type OllamaModelInfoResponse = z.infer<typeof OllamaModelInfoResponseSchema>

export const parseOllamaModel = (rawModel: OllamaModelInfoResponse): ModelInfo | null => {
export const parseOllamaModel = (
rawModel: OllamaModelInfoResponse,
modelName?: string,
): { modelInfo: ModelInfo | null; filteredReason?: string } => {
const contextKey = Object.keys(rawModel.model_info).find((k) => k.includes("context_length"))
const contextWindow =
contextKey && typeof rawModel.model_info[contextKey] === "number" ? rawModel.model_info[contextKey] : undefined

// Filter out models that don't support tools. Models without tool capability won't work.
const supportsTools = rawModel.capabilities?.includes("tools") ?? false
if (!supportsTools) {
return null
const reason = rawModel.capabilities
? `Model '${modelName || "unknown"}' capabilities (${rawModel.capabilities.join(", ")}) do not include 'tools'`
: `Model '${modelName || "unknown"}' has no capabilities reported (Ollama may need to be updated)`
return { modelInfo: null, filteredReason: reason }
}

const modelInfo: ModelInfo = Object.assign({}, ollamaDefaultModelInfo, {
Expand All @@ -56,7 +62,7 @@ export const parseOllamaModel = (rawModel: OllamaModelInfoResponse): ModelInfo |
maxTokens: contextWindow || ollamaDefaultModelInfo.contextWindow,
})

return modelInfo
return { modelInfo }
}

export async function getOllamaModels(
Expand Down Expand Up @@ -84,6 +90,8 @@ export async function getOllamaModels(
let modelInfoPromises = []

if (parsedResponse.success) {
const filteredModels: string[] = []

for (const ollamaModel of parsedResponse.data.models) {
modelInfoPromises.push(
axios
Expand All @@ -95,16 +103,28 @@ export async function getOllamaModels(
{ headers },
)
.then((ollamaModelInfo) => {
const modelInfo = parseOllamaModel(ollamaModelInfo.data)
const { modelInfo, filteredReason } = parseOllamaModel(
ollamaModelInfo.data,
ollamaModel.name,
)
// Only include models that support native tools
if (modelInfo) {
models[ollamaModel.name] = modelInfo
} else if (filteredReason) {
filteredModels.push(filteredReason)
}
}),
)
}

await Promise.all(modelInfoPromises)

// Log filtered models to help users understand why models aren't appearing
if (filteredModels.length > 0) {
console.warn(
`[Ollama] ${filteredModels.length} model(s) filtered out due to missing tool support:\n${filteredModels.join("\n")}`,
)
}
} else {
console.error(`Error parsing Ollama models response: ${JSON.stringify(parsedResponse.error, null, 2)}`)
}
Expand Down
15 changes: 14 additions & 1 deletion src/api/providers/native-ollama.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,9 +206,22 @@ export class NativeOllamaHandler extends BaseProvider implements SingleCompletio
metadata?: ApiHandlerCreateMessageMetadata,
): ApiStream {
const client = this.ensureClient()
const { id: modelId } = await this.fetchModel()
const { id: modelId, info: modelInfo } = await this.fetchModel()
const useR1Format = modelId.toLowerCase().includes("deepseek-r1")

// Log request info for debugging
const baseUrl = this.options.ollamaBaseUrl || "http://localhost:11434"
console.log(`[Ollama] Starting request to model '${modelId}' at ${baseUrl}`)

// Warn if the model is not in the fetched models list (may indicate missing tool support)
if (!this.models[modelId]) {
console.warn(
`[Ollama] Warning: Model '${modelId}' was not found in the list of tool-capable models. ` +
`This may indicate the model does not support native tool calling. ` +
`Check if your Ollama version reports capabilities by running: ollama show ${modelId}`,
)
}

const ollamaMessages: Message[] = [
{ role: "system", content: systemPrompt },
...convertToOllamaMessages(messages),
Expand Down
Loading