From 482cd59cba895b510cbee3e387815e7f787babd6 Mon Sep 17 00:00:00 2001 From: Reconstruct Bot Date: Sat, 1 Aug 2026 18:43:09 +0530 Subject: [PATCH 1/2] feat: add llama3.3 and qwen3 to TOOL_CAPABLE_MODELS in ollama-utils --- src/clients/ollama-utils.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/clients/ollama-utils.js b/src/clients/ollama-utils.js index b4d4a84..6d97cd3 100644 --- a/src/clients/ollama-utils.js +++ b/src/clients/ollama-utils.js @@ -10,7 +10,9 @@ const modelCapabilitiesCache = new Map(); const TOOL_CAPABLE_MODELS = new Set([ "llama3.1", "llama3.2", + "llama3.3", "qwen2.5", + "qwen3", "mistral", "mistral-nemo", "firefunction-v2", From 4c95d913d5f6670fe70ef70b464eea9d8356640e Mon Sep 17 00:00:00 2001 From: Reconstruct Bot Date: Thu, 6 Aug 2026 13:16:19 +0530 Subject: [PATCH 2/2] Fix audit logging (#27): add kimi-k2.5/nemotron to tool-capable models and fix body.model resolution in invokeOllama --- src/clients/databricks.js | 7 +- src/clients/ollama-utils.js | 36 ++++-- test/ollama-tool-capable-models.test.js | 158 ++++++++++++++++++++++++ 3 files changed, 190 insertions(+), 11 deletions(-) create mode 100644 test/ollama-tool-capable-models.test.js diff --git a/src/clients/databricks.js b/src/clients/databricks.js index ebfb3fd..7c9973f 100644 --- a/src/clients/databricks.js +++ b/src/clients/databricks.js @@ -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: { @@ -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"); } diff --git a/src/clients/ollama-utils.js b/src/clients/ollama-utils.js index 6d97cd3..3422193 100644 --- a/src/clients/ollama-utils.js +++ b/src/clients/ollama-utils.js @@ -5,36 +5,40 @@ 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", "llama3.2", - "llama3.3", "qwen2.5", - "qwen3", "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; @@ -55,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 * @@ -213,6 +232,7 @@ function buildAnthropicResponseFromOllama(ollamaResponse, requestedModel) { module.exports = { checkOllamaToolSupport, + clearCapabilityCache, convertAnthropicToolsToOllama, convertOllamaToolCallsToAnthropic, buildAnthropicResponseFromOllama, diff --git a/test/ollama-tool-capable-models.test.js b/test/ollama-tool-capable-models.test.js new file mode 100644 index 0000000..ae6038d --- /dev/null +++ b/test/ollama-tool-capable-models.test.js @@ -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); + }); +});