Skip to content

Commit d80845c

Browse files
shanevcantwellclaudeRomneyDa
authored
feat: add tool prompt override support in .continuerc.json (#9314)
* feat: add tool prompt overrides support in YAML model config Adds per-model tool prompt overrides under chatOptions in YAML config: ```yaml models: - name: my-model chatOptions: toolPromptOverrides: run_terminal_command: description: "Custom description" view_diff: disabled: true ``` This replaces the JSON-only implementation with YAML-only support as requested by maintainers. The JSON implementation is preserved on the `feature/tool-prompt-overrides-json` branch for reference. Changes: - Add toolOverrideSchema to chatOptionsSchema (packages/config-yaml) - Add ToolOverride interface and toolPromptOverrides to LLMOptions (core) - Store and apply overrides in BaseLLM.streamChat() - Add applyToolOverrides utility with comprehensive tests 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat(cli): add tool prompt overrides support for CLI parity Apply toolPromptOverrides from YAML model config in CLI, ensuring feature parity with VS Code extension. - Add applyChatCompletionToolOverrides helper for CLI tool format - Apply overrides in streamChatResponse before LLM calls - Support description changes and disabling tools - Add unit tests for override logic Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * chore: package lock update * ci: retrigger after hub 502 * refactor: rename toolPromptOverrides to toolOverrides - Rename property across core, config-yaml, and CLI - Consolidate ToolOverride type to extend ToolOverrideConfig from config-yaml instead of duplicating 7 fields - Inline record-to-array conversion to match file patterns Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * chore: retrigger CI (clean run) * chore: revert gui package-lock to main Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: Dallin Romney <dallinromney@gmail.com>
1 parent dcac626 commit d80845c

File tree

9 files changed

+431
-3
lines changed

9 files changed

+431
-3
lines changed

core/config/yaml/models.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,12 @@ async function modelConfigToBaseLLM({
6767
baseAgentSystemMessage: model.chatOptions?.baseAgentSystemMessage,
6868
basePlanSystemMessage: model.chatOptions?.basePlanSystemMessage,
6969
baseChatSystemMessage: model.chatOptions?.baseSystemMessage,
70+
toolOverrides: model.chatOptions?.toolOverrides
71+
? Object.entries(model.chatOptions.toolOverrides).map(([name, o]) => ({
72+
name,
73+
...o,
74+
}))
75+
: undefined,
7076
capabilities: {
7177
tools: model.capabilities?.includes("tool_use"),
7278
uploadImage: model.capabilities?.includes("image_input"),

core/index.d.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
DataDestination,
33
ModelRole,
44
PromptTemplates,
5+
ToolOverrideConfig,
56
} from "@continuedev/config-yaml";
67
import Parser from "web-tree-sitter";
78
import { CodebaseIndexer } from "./indexing/CodebaseIndexer";
@@ -695,6 +696,9 @@ export interface LLMOptions {
695696

696697
sourceFile?: string;
697698
isFromAutoDetect?: boolean;
699+
700+
/** Tool overrides for this model */
701+
toolOverrides?: ToolOverride[];
698702
}
699703

700704
type RequireAtLeastOne<T, Keys extends keyof T = keyof T> = Pick<
@@ -1142,6 +1146,15 @@ export interface Tool {
11421146
) => ToolPolicy;
11431147
}
11441148

1149+
/**
1150+
* Configuration for overriding built-in tool prompts.
1151+
* Extends ToolOverrideConfig with required name for array usage.
1152+
*/
1153+
export type ToolOverride = ToolOverrideConfig & {
1154+
/** Tool name to override (matches function.name, e.g., "read_file") */
1155+
name: string;
1156+
};
1157+
11451158
interface ToolChoice {
11461159
type: "function";
11471160
function: {

core/llm/index.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
RequestOptions,
2929
TabAutocompleteOptions,
3030
TemplateType,
31+
ToolOverride,
3132
Usage,
3233
} from "../index.js";
3334
import { isLemonadeInstalled } from "../util/lemonadeHelper.js";
@@ -65,6 +66,8 @@ import {
6566
toCompleteBody,
6667
toFimBody,
6768
} from "./openaiTypeConverters.js";
69+
import { applyToolOverrides } from "../tools/applyToolOverrides.js";
70+
6871
export class LLMError extends Error {
6972
constructor(
7073
message: string,
@@ -196,6 +199,9 @@ export abstract class BaseLLM implements ILLM {
196199

197200
isFromAutoDetect?: boolean;
198201

202+
/** Tool overrides for this model */
203+
toolOverrides?: ToolOverride[];
204+
199205
lastRequestId: string | undefined;
200206

201207
private _llmOptions: LLMOptions;
@@ -303,6 +309,7 @@ export abstract class BaseLLM implements ILLM {
303309
this.autocompleteOptions = options.autocompleteOptions;
304310
this.sourceFile = options.sourceFile;
305311
this.isFromAutoDetect = options.isFromAutoDetect;
312+
this.toolOverrides = options.toolOverrides;
306313
}
307314

308315
get contextLength() {
@@ -1111,8 +1118,28 @@ export abstract class BaseLLM implements ILLM {
11111118
messageOptions?: MessageOption,
11121119
): AsyncGenerator<ChatMessage, PromptLog> {
11131120
this.lastRequestId = undefined;
1121+
1122+
// Apply per-model tool overrides if configured
1123+
let effectiveTools = options.tools;
1124+
if (this.toolOverrides?.length && options.tools?.length) {
1125+
const { tools: overriddenTools, errors } = applyToolOverrides(
1126+
options.tools,
1127+
this.toolOverrides,
1128+
);
1129+
effectiveTools = overriddenTools;
1130+
// Log any warnings for unknown tool names
1131+
for (const error of errors) {
1132+
if (!error.fatal) {
1133+
console.warn(`Tool override warning: ${error.message}`);
1134+
}
1135+
}
1136+
}
1137+
1138+
// Use effectiveTools for the rest of this method
1139+
const optionsWithOverrides = { ...options, tools: effectiveTools };
1140+
11141141
let { completionOptions, logEnabled } =
1115-
this._parseCompletionOptions(options);
1142+
this._parseCompletionOptions(optionsWithOverrides);
11161143
const interaction = logEnabled
11171144
? this.logger?.createInteractionLog()
11181145
: undefined;
@@ -1130,7 +1157,7 @@ export abstract class BaseLLM implements ILLM {
11301157
knownContextLength: this._contextLength,
11311158
maxTokens: completionOptions.maxTokens ?? DEFAULT_MAX_TOKENS,
11321159
supportsImages: this.supportsImages(),
1133-
tools: options.tools,
1160+
tools: optionsWithOverrides.tools,
11341161
});
11351162

11361163
messages = compiledChatMessages;

core/tools/applyToolOverrides.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { ConfigValidationError } from "@continuedev/config-yaml";
2+
import { Tool, ToolOverride } from "..";
3+
4+
export interface ApplyToolOverridesResult {
5+
tools: Tool[];
6+
errors: ConfigValidationError[];
7+
}
8+
9+
/**
10+
* Applies tool overrides from config to the list of tools.
11+
* Overrides can modify tool descriptions, display titles, action phrases,
12+
* system message descriptions, or disable tools entirely.
13+
*/
14+
export function applyToolOverrides(
15+
tools: Tool[],
16+
overrides: ToolOverride[] | undefined,
17+
): ApplyToolOverridesResult {
18+
if (!overrides?.length) {
19+
return { tools, errors: [] };
20+
}
21+
22+
const errors: ConfigValidationError[] = [];
23+
const toolsByName = new Map(tools.map((t) => [t.function.name, t]));
24+
25+
for (const override of overrides) {
26+
const tool = toolsByName.get(override.name);
27+
28+
if (!tool) {
29+
errors.push({
30+
fatal: false,
31+
message: `Tool override "${override.name}" does not match any known tool. Available tools: ${Array.from(toolsByName.keys()).join(", ")}`,
32+
});
33+
continue;
34+
}
35+
36+
if (override.disabled) {
37+
toolsByName.delete(override.name);
38+
continue;
39+
}
40+
41+
const updatedTool: Tool = {
42+
...tool,
43+
function: {
44+
...tool.function,
45+
description: override.description ?? tool.function.description,
46+
},
47+
displayTitle: override.displayTitle ?? tool.displayTitle,
48+
wouldLikeTo: override.wouldLikeTo ?? tool.wouldLikeTo,
49+
isCurrently: override.isCurrently ?? tool.isCurrently,
50+
hasAlready: override.hasAlready ?? tool.hasAlready,
51+
};
52+
53+
if (override.systemMessageDescription) {
54+
updatedTool.systemMessageDescription = {
55+
prefix:
56+
override.systemMessageDescription.prefix ??
57+
tool.systemMessageDescription?.prefix ??
58+
"",
59+
exampleArgs:
60+
override.systemMessageDescription.exampleArgs ??
61+
tool.systemMessageDescription?.exampleArgs,
62+
};
63+
}
64+
65+
toolsByName.set(override.name, updatedTool);
66+
}
67+
68+
return { tools: Array.from(toolsByName.values()), errors };
69+
}
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
import { describe, expect, it } from "vitest";
2+
import { Tool, ToolOverride } from "..";
3+
import { applyToolOverrides } from "./applyToolOverrides";
4+
5+
const mockTool = (name: string, description: string): Tool => ({
6+
type: "function",
7+
displayTitle: name,
8+
readonly: true,
9+
group: "test",
10+
function: { name, description },
11+
});
12+
13+
describe("applyToolOverrides", () => {
14+
it("should return tools unchanged when no overrides provided", () => {
15+
const tools = [mockTool("read_file", "Read a file")];
16+
const result = applyToolOverrides(tools, undefined);
17+
expect(result.tools).toEqual(tools);
18+
expect(result.errors).toHaveLength(0);
19+
});
20+
21+
it("should return tools unchanged when empty overrides array provided", () => {
22+
const tools = [mockTool("read_file", "Read a file")];
23+
const result = applyToolOverrides(tools, []);
24+
expect(result.tools).toEqual(tools);
25+
expect(result.errors).toHaveLength(0);
26+
});
27+
28+
it("should override description when specified", () => {
29+
const tools = [mockTool("read_file", "Original description")];
30+
const overrides: ToolOverride[] = [
31+
{ name: "read_file", description: "New description" },
32+
];
33+
const result = applyToolOverrides(tools, overrides);
34+
expect(result.tools[0].function.description).toBe("New description");
35+
expect(result.errors).toHaveLength(0);
36+
});
37+
38+
it("should override displayTitle when specified", () => {
39+
const tools = [mockTool("read_file", "Read a file")];
40+
const overrides: ToolOverride[] = [
41+
{ name: "read_file", displayTitle: "Custom Read File" },
42+
];
43+
const result = applyToolOverrides(tools, overrides);
44+
expect(result.tools[0].displayTitle).toBe("Custom Read File");
45+
});
46+
47+
it("should override action phrases when specified", () => {
48+
const tools = [mockTool("read_file", "Read a file")];
49+
tools[0].wouldLikeTo = "read {{{ filepath }}}";
50+
tools[0].isCurrently = "reading {{{ filepath }}}";
51+
tools[0].hasAlready = "read {{{ filepath }}}";
52+
53+
const overrides: ToolOverride[] = [
54+
{
55+
name: "read_file",
56+
wouldLikeTo: "open {{{ filepath }}}",
57+
isCurrently: "opening {{{ filepath }}}",
58+
hasAlready: "opened {{{ filepath }}}",
59+
},
60+
];
61+
const result = applyToolOverrides(tools, overrides);
62+
expect(result.tools[0].wouldLikeTo).toBe("open {{{ filepath }}}");
63+
expect(result.tools[0].isCurrently).toBe("opening {{{ filepath }}}");
64+
expect(result.tools[0].hasAlready).toBe("opened {{{ filepath }}}");
65+
});
66+
67+
it("should disable tools when disabled: true", () => {
68+
const tools = [
69+
mockTool("read_file", "Read"),
70+
mockTool("write_file", "Write"),
71+
];
72+
const overrides: ToolOverride[] = [{ name: "read_file", disabled: true }];
73+
const result = applyToolOverrides(tools, overrides);
74+
expect(result.tools).toHaveLength(1);
75+
expect(result.tools[0].function.name).toBe("write_file");
76+
expect(result.errors).toHaveLength(0);
77+
});
78+
79+
it("should warn when override references unknown tool", () => {
80+
const tools = [mockTool("read_file", "Read")];
81+
const overrides: ToolOverride[] = [
82+
{ name: "unknown_tool", description: "test" },
83+
];
84+
const result = applyToolOverrides(tools, overrides);
85+
expect(result.tools).toHaveLength(1);
86+
expect(result.errors).toHaveLength(1);
87+
expect(result.errors[0].message).toContain("unknown_tool");
88+
expect(result.errors[0].fatal).toBe(false);
89+
});
90+
91+
it("should preserve unmodified fields", () => {
92+
const tools = [mockTool("read_file", "Original")];
93+
tools[0].readonly = true;
94+
tools[0].group = "Built-In";
95+
96+
const overrides: ToolOverride[] = [
97+
{ name: "read_file", description: "New description" },
98+
];
99+
const result = applyToolOverrides(tools, overrides);
100+
expect(result.tools[0].readonly).toBe(true);
101+
expect(result.tools[0].group).toBe("Built-In");
102+
expect(result.tools[0].displayTitle).toBe("read_file");
103+
});
104+
105+
it("should override systemMessageDescription", () => {
106+
const tools = [mockTool("read_file", "Read")];
107+
tools[0].systemMessageDescription = {
108+
prefix: "old prefix",
109+
exampleArgs: [["filepath", "/old/path"]],
110+
};
111+
112+
const overrides: ToolOverride[] = [
113+
{
114+
name: "read_file",
115+
systemMessageDescription: {
116+
prefix: "new prefix",
117+
exampleArgs: [["filepath", "/new/path"]],
118+
},
119+
},
120+
];
121+
const result = applyToolOverrides(tools, overrides);
122+
expect(result.tools[0].systemMessageDescription?.prefix).toBe("new prefix");
123+
expect(result.tools[0].systemMessageDescription?.exampleArgs).toEqual([
124+
["filepath", "/new/path"],
125+
]);
126+
});
127+
128+
it("should partially override systemMessageDescription", () => {
129+
const tools = [mockTool("read_file", "Read")];
130+
tools[0].systemMessageDescription = {
131+
prefix: "old prefix",
132+
exampleArgs: [["filepath", "/old/path"]],
133+
};
134+
135+
const overrides: ToolOverride[] = [
136+
{
137+
name: "read_file",
138+
systemMessageDescription: {
139+
prefix: "new prefix",
140+
// exampleArgs not specified - should preserve original
141+
},
142+
},
143+
];
144+
const result = applyToolOverrides(tools, overrides);
145+
expect(result.tools[0].systemMessageDescription?.prefix).toBe("new prefix");
146+
expect(result.tools[0].systemMessageDescription?.exampleArgs).toEqual([
147+
["filepath", "/old/path"],
148+
]);
149+
});
150+
151+
it("should apply multiple overrides", () => {
152+
const tools = [
153+
mockTool("read_file", "Read"),
154+
mockTool("write_file", "Write"),
155+
mockTool("delete_file", "Delete"),
156+
];
157+
158+
const overrides: ToolOverride[] = [
159+
{ name: "read_file", description: "Custom read" },
160+
{ name: "write_file", disabled: true },
161+
{ name: "delete_file", displayTitle: "Remove File" },
162+
];
163+
164+
const result = applyToolOverrides(tools, overrides);
165+
expect(result.tools).toHaveLength(2);
166+
expect(result.tools[0].function.description).toBe("Custom read");
167+
expect(result.tools[1].displayTitle).toBe("Remove File");
168+
expect(result.errors).toHaveLength(0);
169+
});
170+
171+
it("should not mutate original tools array", () => {
172+
const tools = [mockTool("read_file", "Original")];
173+
const originalDescription = tools[0].function.description;
174+
175+
const overrides: ToolOverride[] = [
176+
{ name: "read_file", description: "New description" },
177+
];
178+
const result = applyToolOverrides(tools, overrides);
179+
180+
// Original should be unchanged
181+
expect(tools[0].function.description).toBe(originalDescription);
182+
// Result should have new description
183+
expect(result.tools[0].function.description).toBe("New description");
184+
});
185+
});

extensions/cli/src/stream/streamChatResponse.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { pruneLastMessage } from "../compaction.js";
1212
import { services } from "../services/index.js";
1313
import { posthogService } from "../telemetry/posthogService.js";
1414
import { telemetryService } from "../telemetry/telemetryService.js";
15+
import { applyChatCompletionToolOverrides } from "../tools/applyToolOverrides.js";
1516
import { ToolCall } from "../tools/index.js";
1617
import {
1718
chatCompletionStreamWithBackoff,
@@ -460,7 +461,11 @@ export async function streamChatResponse(
460461
);
461462

462463
// Recompute tools on each iteration to handle mode changes during streaming
463-
const tools = await getRequestTools(isHeadless);
464+
const rawTools = await getRequestTools(isHeadless);
465+
const tools = applyChatCompletionToolOverrides(
466+
rawTools,
467+
model.chatOptions?.toolOverrides,
468+
);
464469

465470
// Pre-API auto-compaction checkpoint (now includes tools)
466471
const preCompactionResult = await handlePreApiCompaction(chatHistory, {

0 commit comments

Comments
 (0)