diff --git a/changelog.txt b/changelog.txt new file mode 100644 index 00000000000..d78d856629d --- /dev/null +++ b/changelog.txt @@ -0,0 +1 @@ +Add message normalization for Ollama model compatibility with MCP tool calling (fixes Mistral Large 3, Ministral 3, Gemma3 27B) diff --git a/core/package-lock.json b/core/package-lock.json index 00b799d2085..68a1e97535e 100644 --- a/core/package-lock.json +++ b/core/package-lock.json @@ -220,6 +220,8 @@ "version": "1.32.0", "license": "Apache-2.0", "dependencies": { + "@ai-sdk/anthropic": "^1.0.10", + "@ai-sdk/openai": "^1.0.10", "@anthropic-ai/sdk": "^0.67.0", "@aws-sdk/client-bedrock-runtime": "^3.931.0", "@aws-sdk/credential-providers": "^3.931.0", @@ -227,6 +229,7 @@ "@continuedev/config-yaml": "^1.36.0", "@continuedev/fetch": "^1.6.0", "@google/genai": "^1.30.0", + "ai": "^4.0.33", "dotenv": "^16.5.0", "google-auth-library": "^10.4.1", "json-schema": "^0.4.0", diff --git a/extensions/cli/package-lock.json b/extensions/cli/package-lock.json index b86896f4f0a..b3d15419461 100644 --- a/extensions/cli/package-lock.json +++ b/extensions/cli/package-lock.json @@ -120,9 +120,9 @@ "license": "Apache-2.0", "dependencies": { "@anthropic-ai/sdk": "^0.62.0", - "@aws-sdk/client-bedrock-runtime": "^3.779.0", + "@aws-sdk/client-bedrock-runtime": "^3.931.0", "@aws-sdk/client-sagemaker-runtime": "^3.777.0", - "@aws-sdk/credential-providers": "^3.778.0", + "@aws-sdk/credential-providers": "^3.931.0", "@continuedev/config-types": "^1.0.13", "@continuedev/config-yaml": "file:../packages/config-yaml", "@continuedev/fetch": "file:../packages/fetch", @@ -275,8 +275,8 @@ "@ai-sdk/anthropic": "^1.0.10", "@ai-sdk/openai": "^1.0.10", "@anthropic-ai/sdk": "^0.67.0", - "@aws-sdk/client-bedrock-runtime": "^3.929.0", - "@aws-sdk/credential-providers": "^3.929.0", + "@aws-sdk/client-bedrock-runtime": "^3.931.0", + "@aws-sdk/credential-providers": "^3.931.0", "@continuedev/config-types": "^1.0.14", "@continuedev/config-yaml": "^1.36.0", "@continuedev/fetch": "^1.6.0", diff --git a/extensions/cli/src/stream/streamChatResponse.ts b/extensions/cli/src/stream/streamChatResponse.ts index b63541db14a..adf0b72f76e 100644 --- a/extensions/cli/src/stream/streamChatResponse.ts +++ b/extensions/cli/src/stream/streamChatResponse.ts @@ -19,6 +19,7 @@ import { withExponentialBackoff, } from "../util/exponentialBackoff.js"; import { logger } from "../util/logger.js"; +import { normalizeMessagesForModel } from "../util/messageNormalizer.js"; import { validateContextLength } from "../util/tokenizer.js"; import { getRequestTools, handleToolCalls } from "./handleToolCalls.js"; @@ -257,6 +258,15 @@ export async function processStreamingResponse( chatHistory, systemMessage, ) as ChatCompletionMessageParam[]; + + // Normalize messages for model-specific compatibility (GitHub Issue #9249) + // Fixes: Mistral "Unexpected role 'system' after role 'tool'" + // Gemma "Invalid 'tool_calls': unknown variant 'index'" + const normalizedMessages = normalizeMessagesForModel( + openaiChatHistory, + model.model, + ); + const requestStartTime = Date.now(); const streamFactory = async (retryAbortSignal: AbortSignal) => { @@ -269,7 +279,7 @@ export async function processStreamingResponse( llmApi, { model: model.model, - messages: openaiChatHistory, + messages: normalizedMessages, stream: true, tools, ...getDefaultCompletionOptions(model.defaultCompletionOptions), diff --git a/extensions/cli/src/util/messageNormalizer.ts b/extensions/cli/src/util/messageNormalizer.ts new file mode 100644 index 00000000000..3618cd7b962 --- /dev/null +++ b/extensions/cli/src/util/messageNormalizer.ts @@ -0,0 +1,106 @@ +/** + * Message Normalization for Model-Specific Compatibility + * + * Handles model-specific message formatting quirks to ensure compatibility + * across different LLM providers when using Ollama's OpenAI endpoint. + * + * Issues addressed: + * 1. Mistral/Ministral: "Unexpected role 'system' after role 'tool'" + * 2. Gemma3: "Invalid 'tool_calls': unknown variant 'index'" + * + * GitHub Issue: https://github.com/continuedev/continue/issues/9249 + */ + +import type { ChatCompletionMessageParam } from "openai/resources"; + +/** + * Normalize message list for model-specific requirements. + * + * @param messages - List of OpenAI-format messages + * @param modelName - Model identifier (e.g., 'mistral-large-3:675b-cloud') + * @returns Normalized message list safe for the target model + */ +export function normalizeMessagesForModel( + messages: ChatCompletionMessageParam[], + modelName: string, +): ChatCompletionMessageParam[] { + const modelLower = modelName.toLowerCase(); + + // Detect model family and apply appropriate normalization + if (modelLower.includes("mistral") || modelLower.includes("ministral")) { + return normalizeForMistral(messages); + } else if (modelLower.includes("gemma")) { + return normalizeForGemma(messages); + } + + // No normalization needed for other models + return messages; +} + +/** + * Fix Mistral's "Unexpected role 'system' after role 'tool'" error. + * + * Strategy: Move system messages before any tool interactions. + * If system message appears after tool, convert to user message. + */ +function normalizeForMistral( + messages: ChatCompletionMessageParam[], +): ChatCompletionMessageParam[] { + const normalized: ChatCompletionMessageParam[] = []; + const systemMessages: ChatCompletionMessageParam[] = []; + let hasToolInteraction = false; + + for (const msg of messages) { + const role = msg.role; + + if (role === "system") { + if (hasToolInteraction) { + // System after tool - convert to user message + normalized.push({ + role: "user", + content: `[System instruction]: ${typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content)}`, + }); + } else { + // System before tool - keep as system + systemMessages.push(msg); + } + } else if (role === "tool") { + hasToolInteraction = true; + normalized.push(msg); + } else { + normalized.push(msg); + } + } + + // Prepend system messages at the start + return [...systemMessages, ...normalized]; +} + +/** + * Fix Gemma's "Invalid 'tool_calls': unknown variant 'index'" error. + * + * Strategy: Remove 'index' field from tool_calls if present. + * Gemma expects only: id, type, function (name, arguments) + */ +function normalizeForGemma( + messages: ChatCompletionMessageParam[], +): ChatCompletionMessageParam[] { + return messages.map((msg) => { + // Only process assistant messages with tool_calls + if (msg.role !== "assistant" || !("tool_calls" in msg) || !msg.tool_calls) { + return msg; + } + + // Remove 'index' field from each tool call + const cleanedToolCalls = msg.tool_calls.map((call: any) => { + // Create a new object without the 'index' field + const { index: _index, ...cleanedCall } = call; + return cleanedCall; + }); + + return { + ...msg, + tool_calls: cleanedToolCalls, + }; + }); +} diff --git a/packages/config-yaml/package-lock.json b/packages/config-yaml/package-lock.json index 7bb2aa2d5cf..6f3d2028a66 100644 --- a/packages/config-yaml/package-lock.json +++ b/packages/config-yaml/package-lock.json @@ -83,6 +83,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.9.tgz", "integrity": "sha512-5e3FI4Q3M3Pbr21+5xJwCv6ZT6KmGkI0vw3Tozy5ODAQFTIWe37iT8Cr7Ice2Ntb+M3iSKCEWMB1MBgKrW3whg==", "dev": true, + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.24.7", @@ -995,6 +996,7 @@ "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.1.tgz", "integrity": "sha512-dKYCMuPO1bmrpuogcjQ8z7ICCH3FP6WmxpwC03yjzGfZhj9fTJg6+bS1+UAplekbN2C+M61UNllGOOoAfGCrdQ==", "dev": true, + "peer": true, "dependencies": { "@octokit/auth-token": "^4.0.0", "@octokit/graphql": "^7.1.0", @@ -1800,6 +1802,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.30.tgz", "integrity": "sha512-7zf4YyHA+jvBNfVrk2Gtvs6x7E8V+YDW05bNfG2XkWDJfYRXrTiP/DsB2zSYTaHX0bGIujTBQdMVAhb+j7mwpg==", "dev": true, + "peer": true, "dependencies": { "undici-types": "~6.19.2" } @@ -2143,6 +2146,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001640", "electron-to-chromium": "^1.4.820", @@ -3889,6 +3893,7 @@ "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, + "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -4753,6 +4758,7 @@ "resolved": "https://registry.npmjs.org/marked/-/marked-5.1.2.tgz", "integrity": "sha512-ahRPGXJpjMjwSOlBoTMZAK7ATXkli5qCPxZ21TG44rx1KEo44bii4ekgTDQPNRQ4Kh7JMb9Ub1PVk1NxRSsorg==", "dev": true, + "peer": true, "bin": { "marked": "bin/marked.js" }, @@ -9262,6 +9268,7 @@ "resolved": "https://registry.npmjs.org/semantic-release/-/semantic-release-21.1.2.tgz", "integrity": "sha512-kz76azHrT8+VEkQjoCBHE06JNQgTgsC4bT8XfCzb7DHcsk9vG3fqeMVik8h5rcWCYi2Fd+M3bwA7BG8Z8cRwtA==", "dev": true, + "peer": true, "dependencies": { "@semantic-release/commit-analyzer": "^10.0.0", "@semantic-release/error": "^4.0.0", @@ -10147,6 +10154,7 @@ "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, + "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -10511,6 +10519,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/packages/continue-sdk/package-lock.json b/packages/continue-sdk/package-lock.json index 0696bc47b35..65fb1c0dc2d 100644 --- a/packages/continue-sdk/package-lock.json +++ b/packages/continue-sdk/package-lock.json @@ -230,6 +230,7 @@ "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.9.tgz", "integrity": "sha512-zDntUTReRbAThIfSp3dQZ9kKqI+LjgLp5YZN5c1bgNRDuoeLySAoZg46Bg1a+uV8TMgIRziHocglKGNzr6l+bQ==", "license": "MIT", + "peer": true, "dependencies": { "file-type": "21.1.0", "iterare": "1.2.1", @@ -491,6 +492,7 @@ "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", "license": "MIT", + "peer": true, "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", @@ -2108,7 +2110,8 @@ "version": "0.2.2", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", - "license": "Apache-2.0" + "license": "Apache-2.0", + "peer": true }, "node_modules/require-directory": { "version": "2.1.1", @@ -2152,6 +2155,7 @@ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "license": "Apache-2.0", + "peer": true, "dependencies": { "tslib": "^2.1.0" } diff --git a/packages/fetch/package-lock.json b/packages/fetch/package-lock.json index ef54d2290a9..11a65f49277 100644 --- a/packages/fetch/package-lock.json +++ b/packages/fetch/package-lock.json @@ -538,6 +538,7 @@ "integrity": "sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@octokit/auth-token": "^4.0.0", "@octokit/graphql": "^7.1.0", @@ -3460,6 +3461,7 @@ "integrity": "sha512-ahRPGXJpjMjwSOlBoTMZAK7ATXkli5qCPxZ21TG44rx1KEo44bii4ekgTDQPNRQ4Kh7JMb9Ub1PVk1NxRSsorg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "marked": "bin/marked.js" }, @@ -7914,6 +7916,7 @@ "integrity": "sha512-kz76azHrT8+VEkQjoCBHE06JNQgTgsC4bT8XfCzb7DHcsk9vG3fqeMVik8h5rcWCYi2Fd+M3bwA7BG8Z8cRwtA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@semantic-release/commit-analyzer": "^10.0.0", "@semantic-release/error": "^4.0.0", @@ -8723,6 +8726,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, + "peer": true, "engines": { "node": ">=12" }, @@ -8812,6 +8816,7 @@ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -8919,6 +8924,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", "dev": true, + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -9029,6 +9035,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, + "peer": true, "engines": { "node": ">=12" }, diff --git a/packages/llm-info/package-lock.json b/packages/llm-info/package-lock.json index 4b7c0d122e0..a21ac25e22a 100644 --- a/packages/llm-info/package-lock.json +++ b/packages/llm-info/package-lock.json @@ -108,6 +108,7 @@ "integrity": "sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@octokit/auth-token": "^4.0.0", "@octokit/graphql": "^7.1.0", @@ -2434,6 +2435,7 @@ "integrity": "sha512-ahRPGXJpjMjwSOlBoTMZAK7ATXkli5qCPxZ21TG44rx1KEo44bii4ekgTDQPNRQ4Kh7JMb9Ub1PVk1NxRSsorg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "marked": "bin/marked.js" }, @@ -6756,6 +6758,7 @@ "integrity": "sha512-kz76azHrT8+VEkQjoCBHE06JNQgTgsC4bT8XfCzb7DHcsk9vG3fqeMVik8h5rcWCYi2Fd+M3bwA7BG8Z8cRwtA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@semantic-release/commit-analyzer": "^10.0.0", "@semantic-release/error": "^4.0.0", @@ -7545,6 +7548,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", "dev": true, + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/packages/openai-adapters/package-lock.json b/packages/openai-adapters/package-lock.json index b110cac0ea1..3a04dd51868 100644 --- a/packages/openai-adapters/package-lock.json +++ b/packages/openai-adapters/package-lock.json @@ -5900,7 +5900,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -7648,7 +7647,6 @@ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=0.8.19" } @@ -15738,7 +15736,6 @@ "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^4.0.1" @@ -15753,7 +15750,6 @@ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "dev": true, "license": "ISC", - "peer": true, "engines": { "node": ">=14" }, diff --git a/packages/terminal-security/package-lock.json b/packages/terminal-security/package-lock.json index 640f76919cf..fa5bb36e835 100644 --- a/packages/terminal-security/package-lock.json +++ b/packages/terminal-security/package-lock.json @@ -791,6 +791,7 @@ "integrity": "sha512-lSOjyS6vdO2G2g2CWrETTV3Jz2zlCXHpu1rcubLKpz9oj+z/1CceHlj+yq53W+9zgb98nSov/wjEKYDNauD+Hw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -1174,6 +1175,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -1395,6 +1397,7 @@ "integrity": "sha512-4cKBO9wR75r0BeIWWWId9XK9Lj6La5X846Zw9dFfzMRw38IlTk2iCcUt6hsyiDRcPidc55ZParFYDXi0nXOeLQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0",