From 1a59f1ab56f78c041e00b7603a3f679f7861e678 Mon Sep 17 00:00:00 2001 From: shanevcantwell <153727980+shanevcantwell@users.noreply.github.com> Date: Thu, 12 Feb 2026 21:35:20 -0700 Subject: [PATCH] fix: harden system message tools instructions and wire toolOverrides to system message path The system message tools path (used when models don't support native tool calling) had two issues: 1. Weak format instructions - the prefix/suffix used vague prose that local models often ignored, reverting to XML or JSON tool call formats from their training priors. Replaced with explicit numbered rules that prohibit alternative formats. 2. toolOverrides silently ignored - applyToolOverrides() existed and ran on the native tools path (in BaseLLM.streamChat), but was never called on the system message path. Config YAML toolOverrides for disabled and description had no effect when tools were injected via system message. Fixed by: - Strengthening systemMessagePrefix/Suffix in toolCodeblocks framework - Adding toolOverrides to ModelDescription interface and serialization - Calling applyToolOverrides() in streamNormalInput before both paths Co-Authored-By: Claude Opus 4.6 --- core/config/load.ts | 1 + core/index.d.ts | 3 +++ .../toolCodeblocks/index.ts | 21 ++++++++++++------- gui/src/redux/thunks/streamNormalInput.ts | 17 +++++++++++++-- 4 files changed, 33 insertions(+), 9 deletions(-) diff --git a/core/config/load.ts b/core/config/load.ts index 4a88fbcce30..e4c7e13c4f0 100644 --- a/core/config/load.ts +++ b/core/config/load.ts @@ -651,6 +651,7 @@ function llmToSerializedModelDescription(llm: ILLM): ModelDescription { envSecretLocations: llm.envSecretLocations, sourceFile: llm.sourceFile, isFromAutoDetect: llm.isFromAutoDetect, + toolOverrides: llm.toolOverrides, }; } diff --git a/core/index.d.ts b/core/index.d.ts index 1e7c6ac4baf..ccdfcedaf07 100644 --- a/core/index.d.ts +++ b/core/index.d.ts @@ -1254,6 +1254,9 @@ export interface ModelDescription { sourceFile?: string; isFromAutoDetect?: boolean; + + /** Tool overrides for this model */ + toolOverrides?: ToolOverride[]; } export interface JSONEmbedOptions { diff --git a/core/tools/systemMessageTools/toolCodeblocks/index.ts b/core/tools/systemMessageTools/toolCodeblocks/index.ts index 97baa19fa06..a2cdc393d66 100644 --- a/core/tools/systemMessageTools/toolCodeblocks/index.ts +++ b/core/tools/systemMessageTools/toolCodeblocks/index.ts @@ -65,13 +65,20 @@ export class SystemMessageToolCodeblocksFramework return toolDefinition.trim(); } - systemMessagePrefix = `You have access to several "tools" that you can use at any time to retrieve information and/or perform tasks for the User. -To use a tool, respond with a tool code block (\`\`\`tool) using the syntax shown in the examples below:`; - - systemMessageSuffix = `If it seems like the User's request could be solved with one of the tools, choose the BEST one for the job based on the user's request and the tool descriptions -Then send the \`\`\`tool codeblock (YOU call the tool, not the user). Always start the codeblock on a new line. -Do not perform actions with/for hypothetical files. Ask the user or use tools to deduce which files are relevant. -You can only call ONE tool at at time. The tool codeblock should be the last thing you say; stop your response after the tool codeblock.`; + systemMessagePrefix = `You have access to tools. To call a tool, you MUST respond with EXACTLY this format — a tool code block (\`\`\`tool) using the syntax shown below. + +CRITICAL: Follow the exact syntax. Do not use XML tags, JSON objects, or any other format for tool calls.`; + + systemMessageSuffix = `RULES FOR TOOL USE: +1. To call a tool, output a \`\`\`tool code block using EXACTLY the format shown above. +2. Always start the code block on a new line. +3. You can only call ONE tool at a time. +4. The \`\`\`tool code block MUST be the last thing in your response. Stop immediately after the closing \`\`\`. +5. Do NOT wrap tool calls in XML tags like or . +6. Do NOT use JSON format for tool calls. +7. Do NOT invent tools that are not listed above. +8. If the user's request can be addressed with a listed tool, use it rather than guessing. +9. Do not perform actions with hypothetical files. Use tools to find relevant files.`; exampleDynamicToolDefinition = ` \`\`\`tool_definition diff --git a/gui/src/redux/thunks/streamNormalInput.ts b/gui/src/redux/thunks/streamNormalInput.ts index 13cc5ebbf60..559bcadf96e 100644 --- a/gui/src/redux/thunks/streamNormalInput.ts +++ b/gui/src/redux/thunks/streamNormalInput.ts @@ -22,6 +22,7 @@ import { ThunkApiType } from "../store"; import { constructMessages } from "../util/constructMessages"; import { modelSupportsNativeTools } from "core/llm/toolSupport"; +import { applyToolOverrides } from "core/tools/applyToolOverrides"; import { addSystemMessageToolsToSystemMessage } from "core/tools/systemMessageTools/buildToolsSystemMessage"; import { interceptSystemToolCalls } from "core/tools/systemMessageTools/interceptSystemToolCalls"; import { SystemMessageToolCodeblocksFramework } from "core/tools/systemMessageTools/toolCodeblocks"; @@ -93,8 +94,20 @@ export const streamNormalInput = createAsyncThunk< throw new Error("No chat model selected"); } - // Get tools and filter them based on the selected model - const activeTools = selectActiveTools(state); + // Get tools and apply model-level overrides (disabled, description, etc.) + let activeTools = selectActiveTools(state); + if (selectedChatModel.toolOverrides?.length) { + const { tools: overriddenTools, errors } = applyToolOverrides( + activeTools, + selectedChatModel.toolOverrides, + ); + activeTools = overriddenTools; + for (const error of errors) { + if (!error.fatal) { + console.warn(`Tool override warning: ${error.message}`); + } + } + } // Use the centralized selector to determine if system message tools should be used const useNativeTools = state.config.config.experimental