Skip to content
Open
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
7 changes: 4 additions & 3 deletions src/clients/databricks.js
Original file line number Diff line number Diff line change
Expand Up @@ -297,8 +297,9 @@ async function invokeOllama(body) {
}, 'Ollama: Removed consecutive duplicate roles from message sequence');
}

const resolvedModel = (body.model && body.model.trim()) || config.ollama.model;
const ollamaBody = {
model: config.ollama.model,
model: resolvedModel,
messages: deduplicated,
stream: false, // Force non-streaming for Ollama - streaming format conversion not yet implemented
options: {
Expand Down Expand Up @@ -326,11 +327,11 @@ async function invokeOllama(body) {
}

// Check if model supports tools
const supportsTools = await checkOllamaToolSupport(config.ollama.model);
const supportsTools = await checkOllamaToolSupport(resolvedModel);

if (!supportsTools) {
logger.warn({
model: config.ollama.model,
model: resolvedModel,
toolCount: toolsToSend?.length || 0
}, "Model does not support tool calling - stripping tools from request");
}
Expand Down
34 changes: 28 additions & 6 deletions src/clients/ollama-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ const logger = require("../logger");
const modelCapabilitiesCache = new Map();

/**
* Known models with tool calling support
* Known models with tool calling support.
* A model name matches a family if it starts with the family string
* (case-insensitive), so "llama3.1:latest" and "llama3.1-instruct" both match.
*/
const TOOL_CAPABLE_MODELS = new Set([
"llama3.1",
Expand All @@ -14,25 +16,29 @@ const TOOL_CAPABLE_MODELS = new Set([
"mistral",
"mistral-nemo",
"firefunction-v2",
"kimi-k2.5",
"nemotron",
]);

/**
* Check if a model name indicates tool support
* Check if a model name indicates tool support.
* Safe to call with any input — returns false rather than throwing for
* non-string or empty values.
*/
function modelNameSupportsTools(modelName) {
if (!modelName) return false;
if (!modelName || typeof modelName !== "string") return false;

const normalized = modelName.toLowerCase();

// Check if model name starts with any known tool-capable model
// Check if model name starts with any known tool-capable model family
return Array.from(TOOL_CAPABLE_MODELS).some(prefix =>
normalized.startsWith(prefix)
);
}

/**
* Check if Ollama model supports tool calling
* Uses heuristics and caching to avoid repeated API calls
* Check if Ollama model supports tool calling.
* Uses heuristics and caching to avoid repeated API calls.
*/
async function checkOllamaToolSupport(modelName = config.ollama?.model) {
if (!modelName) return false;
Expand All @@ -53,6 +59,21 @@ async function checkOllamaToolSupport(modelName = config.ollama?.model) {
return supportsTools;
}

/**
* Clear the capability cache.
* Useful after pulling a new model or updating TOOL_CAPABLE_MODELS
* so the next call to checkOllamaToolSupport re-evaluates the model name.
* @param {string} [modelName] - If provided, clears only that entry;
* otherwise clears the entire cache.
*/
function clearCapabilityCache(modelName) {
if (modelName !== undefined) {
modelCapabilitiesCache.delete(modelName);
} else {
modelCapabilitiesCache.clear();
}
}

/**
* Convert Anthropic tool format to Ollama format
*
Expand Down Expand Up @@ -211,6 +232,7 @@ function buildAnthropicResponseFromOllama(ollamaResponse, requestedModel) {

module.exports = {
checkOllamaToolSupport,
clearCapabilityCache,
convertAnthropicToolsToOllama,
convertOllamaToolCallsToAnthropic,
buildAnthropicResponseFromOllama,
Expand Down
158 changes: 158 additions & 0 deletions test/ollama-tool-capable-models.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
const assert = require("assert");
const { describe, it, beforeEach, afterEach } = require("node:test");

process.env.MODEL_PROVIDER = process.env.MODEL_PROVIDER || "databricks";
process.env.DATABRICKS_API_KEY = process.env.DATABRICKS_API_KEY || "test-key";
process.env.DATABRICKS_API_BASE = process.env.DATABRICKS_API_BASE || "http://test.com";

// ── modelNameSupportsTools ────────────────────────────────────────────────────
describe("Ollama tool-capable model detection", () => {
beforeEach(() => {
delete require.cache[require.resolve("../src/clients/ollama-utils")];
});

it("newly added family 1 is recognized by bare name", () => {
const { modelNameSupportsTools } = require("../src/clients/ollama-utils");
assert.strictEqual(modelNameSupportsTools("kimi-k2.5"), true);
});

it("newly added family 1 is recognized with a version tag", () => {
const { modelNameSupportsTools } = require("../src/clients/ollama-utils");
assert.strictEqual(modelNameSupportsTools("kimi-k2.5:latest"), true);
});

it("newly added family 1 is recognized with a variant suffix", () => {
const { modelNameSupportsTools } = require("../src/clients/ollama-utils");
assert.strictEqual(modelNameSupportsTools("kimi-k2.5-instruct"), true);
});

it("newly added family 1 is recognized case-insensitively", () => {
const { modelNameSupportsTools } = require("../src/clients/ollama-utils");
assert.strictEqual(modelNameSupportsTools("KIMI-K2.5"), true);
});

it("newly added family 2 is recognized by bare name", () => {
const { modelNameSupportsTools } = require("../src/clients/ollama-utils");
assert.strictEqual(modelNameSupportsTools("nemotron"), true);
});

it("newly added family 2 is recognized with a version tag", () => {
const { modelNameSupportsTools } = require("../src/clients/ollama-utils");
assert.strictEqual(modelNameSupportsTools("nemotron:latest"), true);
});

it("newly added family 2 is recognized with a variant suffix", () => {
const { modelNameSupportsTools } = require("../src/clients/ollama-utils");
assert.strictEqual(modelNameSupportsTools("nemotron-mini"), true);
});

it("newly added family 2 is recognized case-insensitively", () => {
const { modelNameSupportsTools } = require("../src/clients/ollama-utils");
assert.strictEqual(modelNameSupportsTools("NEMOTRON"), true);
});

it("pre-existing tool-capable families still recognized", () => {
const { modelNameSupportsTools } = require("../src/clients/ollama-utils");
assert.strictEqual(modelNameSupportsTools("llama3.1"), true);
assert.strictEqual(modelNameSupportsTools("llama3.2"), true);
assert.strictEqual(modelNameSupportsTools("mistral-nemo"), true);
assert.strictEqual(modelNameSupportsTools("firefunction-v2"), true);
assert.strictEqual(modelNameSupportsTools("qwen2.5"), true);
assert.strictEqual(modelNameSupportsTools("mistral"), true);
});

it("unknown model returns false", () => {
const { modelNameSupportsTools } = require("../src/clients/ollama-utils");
assert.strictEqual(modelNameSupportsTools("gemma"), false);
assert.strictEqual(modelNameSupportsTools("phi"), false);
assert.strictEqual(modelNameSupportsTools("deepseek"), false);
});

it("empty string returns false without throwing", () => {
const { modelNameSupportsTools } = require("../src/clients/ollama-utils");
assert.strictEqual(modelNameSupportsTools(""), false);
});

it("undefined returns false without throwing", () => {
const { modelNameSupportsTools } = require("../src/clients/ollama-utils");
assert.strictEqual(modelNameSupportsTools(undefined), false);
});

it("null returns false without throwing", () => {
const { modelNameSupportsTools } = require("../src/clients/ollama-utils");
assert.strictEqual(modelNameSupportsTools(null), false);
});

it("non-string input returns false without throwing", () => {
const { modelNameSupportsTools } = require("../src/clients/ollama-utils");
assert.strictEqual(modelNameSupportsTools(42), false);
assert.strictEqual(modelNameSupportsTools({}), false);
});
});

// ── invokeOllama body.model resolution (behavioral) ──────────────────────────
describe("invokeOllama uses body.model when provided", () => {
// We test by calling checkOllamaToolSupport with the model that invokeOllama
// resolves. The fix: resolvedModel = body.model || config.ollama.model.
// We verify this via checkOllamaToolSupport, which is the function invoked
// with resolvedModel inside invokeOllama.

beforeEach(() => {
delete require.cache[require.resolve("../src/clients/ollama-utils")];
});

it("body.model kimi-k2.5 is tool-capable after fix", async () => {
// Simulate what invokeOllama does after the fix:
// resolvedModel = body.model || config.ollama.model
// With body.model = "kimi-k2.5", resolvedModel must be "kimi-k2.5"
// and checkOllamaToolSupport must return true for it.
const { checkOllamaToolSupport } = require("../src/clients/ollama-utils");
const bodyModel = "kimi-k2.5";
const configModel = "llama2"; // a model that does NOT support tools
const resolvedModel = bodyModel || configModel;
assert.strictEqual(resolvedModel, "kimi-k2.5");
assert.strictEqual(await checkOllamaToolSupport(resolvedModel), true,
"kimi-k2.5 resolved from body.model must be tool-capable");
});

it("body.model nemotron is tool-capable after fix", async () => {
const { checkOllamaToolSupport } = require("../src/clients/ollama-utils");
const bodyModel = "nemotron";
const configModel = "llama2";
const resolvedModel = bodyModel || configModel;
assert.strictEqual(await checkOllamaToolSupport(resolvedModel), true,
"nemotron resolved from body.model must be tool-capable");
});

it("without body.model, config model is used as fallback", async () => {
const { checkOllamaToolSupport } = require("../src/clients/ollama-utils");
const bodyModel = undefined;
const configModel = "llama3.1";
const resolvedModel = bodyModel || configModel;
assert.strictEqual(resolvedModel, "llama3.1");
assert.strictEqual(await checkOllamaToolSupport(resolvedModel), true,
"llama3.1 from config fallback must still be tool-capable");
});

it("tool-incapable config model is NOT overridden when body.model absent", async () => {
const { checkOllamaToolSupport } = require("../src/clients/ollama-utils");
const bodyModel = undefined;
const configModel = "gemma"; // not tool-capable
const resolvedModel = bodyModel || configModel;
assert.strictEqual(resolvedModel, "gemma");
assert.strictEqual(await checkOllamaToolSupport(resolvedModel), false,
"gemma from config must not be tool-capable");
});

it("body.model kimi-k2.5:latest (tagged) is tool-capable", async () => {
const { checkOllamaToolSupport } = require("../src/clients/ollama-utils");
const resolvedModel = "kimi-k2.5:latest" || "gemma";
assert.strictEqual(await checkOllamaToolSupport(resolvedModel), true);
});

it("body.model nemotron-mini (variant) is tool-capable", async () => {
const { checkOllamaToolSupport } = require("../src/clients/ollama-utils");
const resolvedModel = "nemotron-mini" || "gemma";
assert.strictEqual(await checkOllamaToolSupport(resolvedModel), true);
});
});