This commit is contained in:
roomote-v0[bot] 2026-04-21 02:26:35 +00:00 • committed by GitHub
commit c56db83e0d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 63 additions and 25 deletions

View file

@ -15,9 +15,9 @@ describe("Ollama Fetcher", () => {
describe("parseOllamaModel", () => { describe("parseOllamaModel", () => {
it("should correctly parse Ollama model info", () => { it("should correctly parse Ollama model info", () => {
const modelData = ollamaModelsData["qwen3-2to16:latest"] const modelData = ollamaModelsData["qwen3-2to16:latest"]
const parsedModel = parseOllamaModel(modelData) const { modelInfo } = parseOllamaModel(modelData)
expect(parsedModel).toEqual({ expect(modelInfo).toEqual({
maxTokens: 40960, maxTokens: 40960,
contextWindow: 40960, contextWindow: 40960,
supportsImages: false, supportsImages: false,
@ -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, maxTokens: 40960,
contextWindow: 40960, contextWindow: 40960,
supportsImages: false, supportsImages: false,
@ -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 = { const modelDataWithoutTools = {
...ollamaModelsData["qwen3-2to16:latest"], ...ollamaModelsData["qwen3-2to16:latest"],
capabilities: ["completion"], // No "tools" capability 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) // 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'", () => { it("should return model info when capabilities includes 'tools'", () => {
@ -72,22 +74,25 @@ describe("Ollama Fetcher", () => {
capabilities: ["completion", "tools"], // Has "tools" capability capabilities: ["completion", "tools"], // Has "tools" capability
} }
const parsedModel = parseOllamaModel(modelDataWithTools as any) const { modelInfo, filteredReason } = parseOllamaModel(modelDataWithTools as any)
expect(parsedModel).not.toBeNull() expect(modelInfo).not.toBeNull()
expect(parsedModel!.contextWindow).toBeGreaterThan(0) 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 = { const modelDataWithoutCapabilities = {
...ollamaModelsData["qwen3-2to16:latest"], ...ollamaModelsData["qwen3-2to16:latest"],
capabilities: undefined, // No capabilities array 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 // 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", () => { it("should return null when model has vision but no tools capability", () => {
@ -96,10 +101,10 @@ describe("Ollama Fetcher", () => {
capabilities: ["completion", "vision"], capabilities: ["completion", "vision"],
} }
const parsedModel = parseOllamaModel(modelDataWithVision as any) const { modelInfo } = parseOllamaModel(modelDataWithVision as any)
// No "tools" capability means filtered out // 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", () => { it("should return model with both vision and tools when both capabilities present", () => {
@ -108,11 +113,11 @@ describe("Ollama Fetcher", () => {
capabilities: ["completion", "vision", "tools"], capabilities: ["completion", "vision", "tools"],
} }
const parsedModel = parseOllamaModel(modelDataWithBoth as any) const { modelInfo } = parseOllamaModel(modelDataWithBoth as any)
expect(parsedModel).not.toBeNull() expect(modelInfo).not.toBeNull()
expect(parsedModel!.supportsImages).toBe(true) expect(modelInfo!.supportsImages).toBe(true)
expect(parsedModel!.contextWindow).toBeGreaterThan(0) expect(modelInfo!.contextWindow).toBeGreaterThan(0)
}) })
}) })
@ -177,7 +182,7 @@ describe("Ollama Fetcher", () => {
expect(Object.keys(result).length).toBe(1) expect(Object.keys(result).length).toBe(1)
expect(result[modelName]).toBeDefined() expect(result[modelName]).toBeDefined()
const expectedParsedDetails = parseOllamaModel(mockApiShowResponse as any) const { modelInfo: expectedParsedDetails } = parseOllamaModel(mockApiShowResponse as any)
expect(result[modelName]).toEqual(expectedParsedDetails) expect(result[modelName]).toEqual(expectedParsedDetails)
}) })

View file

@ -37,7 +37,10 @@ type OllamaModelsResponse = z.infer<typeof OllamaModelsResponseSchema>
type OllamaModelInfoResponse = z.infer<typeof OllamaModelInfoResponseSchema> 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 contextKey = Object.keys(rawModel.model_info).find((k) => k.includes("context_length"))
const contextWindow = const contextWindow =
contextKey && typeof rawModel.model_info[contextKey] === "number" ? rawModel.model_info[contextKey] : undefined contextKey && typeof rawModel.model_info[contextKey] === "number" ? rawModel.model_info[contextKey] : undefined
@ -45,7 +48,10 @@ export const parseOllamaModel = (rawModel: OllamaModelInfoResponse): ModelInfo |
// Filter out models that don't support tools. Models without tool capability won't work. // Filter out models that don't support tools. Models without tool capability won't work.
const supportsTools = rawModel.capabilities?.includes("tools") ?? false const supportsTools = rawModel.capabilities?.includes("tools") ?? false
if (!supportsTools) { 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, { const modelInfo: ModelInfo = Object.assign({}, ollamaDefaultModelInfo, {
@ -56,7 +62,7 @@ export const parseOllamaModel = (rawModel: OllamaModelInfoResponse): ModelInfo |
maxTokens: contextWindow || ollamaDefaultModelInfo.contextWindow, maxTokens: contextWindow || ollamaDefaultModelInfo.contextWindow,
}) })
return modelInfo return { modelInfo }
} }
export async function getOllamaModels( export async function getOllamaModels(
@ -84,6 +90,8 @@ export async function getOllamaModels(
let modelInfoPromises = [] let modelInfoPromises = []
if (parsedResponse.success) { if (parsedResponse.success) {
const filteredModels: string[] = []
for (const ollamaModel of parsedResponse.data.models) { for (const ollamaModel of parsedResponse.data.models) {
modelInfoPromises.push( modelInfoPromises.push(
axios axios
@ -95,16 +103,28 @@ export async function getOllamaModels(
{ headers }, { headers },
) )
.then((ollamaModelInfo) => { .then((ollamaModelInfo) => {
const modelInfo = parseOllamaModel(ollamaModelInfo.data) const { modelInfo, filteredReason } = parseOllamaModel(
ollamaModelInfo.data,
ollamaModel.name,
)
// Only include models that support native tools // Only include models that support native tools
if (modelInfo) { if (modelInfo) {
models[ollamaModel.name] = modelInfo models[ollamaModel.name] = modelInfo
} else if (filteredReason) {
filteredModels.push(filteredReason)
} }
}), }),
) )
} }
await Promise.all(modelInfoPromises) 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 { } else {
console.error(`Error parsing Ollama models response: ${JSON.stringify(parsedResponse.error, null, 2)}`) console.error(`Error parsing Ollama models response: ${JSON.stringify(parsedResponse.error, null, 2)}`)
} }

View file

@ -206,9 +206,22 @@ export class NativeOllamaHandler extends BaseProvider implements SingleCompletio
metadata?: ApiHandlerCreateMessageMetadata, metadata?: ApiHandlerCreateMessageMetadata,
): ApiStream { ): ApiStream {
const client = this.ensureClient() 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") 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[] = [ const ollamaMessages: Message[] = [
{ role: "system", content: systemPrompt }, { role: "system", content: systemPrompt },
...convertToOllamaMessages(messages), ...convertToOllamaMessages(messages),