Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.

Commit c6bfa43

Browse files
committed
fix: strip null values from tool call args to prevent Jinja template errors
Local models using Jinja chat templates (e.g. llama.cpp, Ollama) cannot handle null values in tool call arguments, causing "Cannot convert value of type Optional<Any> to Jinja Value" errors when selecting follow-up answers. Changes: - Remove strict mode from ask_followup_question tool definition and make mode optional (not required), matching the read_command_output pattern - Update examples to omit mode instead of using null - Strip null mode values when building follow_up JSON in the tool - Strip null values from all tool call arguments during OpenAI message serialization as a general safety net - Add tests for null stripping behavior Addresses #12233
1 parent ad25634 commit c6bfa43

5 files changed

Lines changed: 90 additions & 8 deletions

File tree

src/api/transform/__tests__/openai-format.spec.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,39 @@ describe("convertToOpenAiMessages", () => {
112112
})
113113
})
114114

115+
it("should strip null values from tool call arguments to prevent Jinja template errors", () => {
116+
const anthropicMessages: Anthropic.Messages.MessageParam[] = [
117+
{
118+
role: "assistant",
119+
content: [
120+
{
121+
type: "tool_use",
122+
id: "followup-123",
123+
name: "ask_followup_question",
124+
input: {
125+
question: "Pick one",
126+
follow_up: [
127+
{ text: "Option A", mode: null },
128+
{ text: "Option B", mode: "code" },
129+
],
130+
},
131+
},
132+
],
133+
},
134+
]
135+
136+
const openAiMessages = convertToOpenAiMessages(anthropicMessages)
137+
const assistantMessage = openAiMessages[0] as OpenAI.Chat.ChatCompletionAssistantMessageParam
138+
const toolCall = assistantMessage.tool_calls![0] as any
139+
const args = JSON.parse(toolCall.function.arguments)
140+
141+
// null mode should be stripped (becomes undefined, omitted from JSON)
142+
expect(args.follow_up[0]).toEqual({ text: "Option A" })
143+
expect(args.follow_up[0].mode).toBeUndefined()
144+
// non-null mode should be preserved
145+
expect(args.follow_up[1]).toEqual({ text: "Option B", mode: "code" })
146+
})
147+
115148
it("should handle user messages with tool results (no normalization without normalizeToolCallId)", () => {
116149
const anthropicMessages: Anthropic.Messages.MessageParam[] = [
117150
{

src/api/transform/openai-format.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -467,8 +467,13 @@ export function convertToOpenAiMessages(
467467
type: "function",
468468
function: {
469469
name: toolMessage.name,
470-
// json string
471-
arguments: JSON.stringify(toolMessage.input),
470+
// Serialize as JSON, stripping null values to prevent Jinja template
471+
// errors on local models (e.g. "Cannot convert value of type
472+
// Optional<Any> to Jinja Value"). Null in tool args typically means
473+
// "not provided" and should be omitted instead.
474+
arguments: JSON.stringify(toolMessage.input, (_key, value) =>
475+
value === null ? undefined : value,
476+
),
472477
},
473478
}))
474479

src/core/prompts/tools/native-tools/ask_followup_question.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ Parameters:
77
- follow_up: (required) A list of 2-4 suggested answers. Suggestions must be complete, actionable answers without placeholders. Optionally include mode to switch modes (code/architect/etc.)
88
99
Example: Asking for file path
10-
{ "question": "What is the path to the frontend-config.json file?", "follow_up": [{ "text": "./src/frontend-config.json", "mode": null }, { "text": "./config/frontend-config.json", "mode": null }, { "text": "./frontend-config.json", "mode": null }] }
10+
{ "question": "What is the path to the frontend-config.json file?", "follow_up": [{ "text": "./src/frontend-config.json" }, { "text": "./config/frontend-config.json" }, { "text": "./frontend-config.json" }] }
1111
1212
Example: Asking with mode switch
1313
{ "question": "Would you like me to implement this feature?", "follow_up": [{ "text": "Yes, implement it now", "mode": "code" }, { "text": "No, just plan it out", "mode": "architect" }] }`
@@ -25,7 +25,12 @@ export default {
2525
function: {
2626
name: "ask_followup_question",
2727
description: ASK_FOLLOWUP_QUESTION_DESCRIPTION,
28-
strict: true,
28+
// Note: strict mode is intentionally disabled for this tool.
29+
// With strict: true, OpenAI requires ALL properties to be in the 'required' array,
30+
// which forces the LLM to always provide explicit values (even null) for optional params.
31+
// Local models using Jinja chat templates cannot handle null values in tool call arguments,
32+
// causing "Cannot convert value of type Optional<Any> to Jinja Value" errors.
33+
// By disabling strict mode, the LLM can omit the optional `mode` parameter entirely.
2934
parameters: {
3035
type: "object",
3136
properties: {
@@ -44,11 +49,11 @@ export default {
4449
description: FOLLOW_UP_TEXT_DESCRIPTION,
4550
},
4651
mode: {
47-
type: ["string", "null"],
52+
type: "string",
4853
description: FOLLOW_UP_MODE_DESCRIPTION,
4954
},
5055
},
51-
required: ["text", "mode"],
56+
required: ["text"],
5257
additionalProperties: false,
5358
},
5459
minItems: 1,

src/core/tools/AskFollowupQuestionTool.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,18 @@ export class AskFollowupQuestionTool extends BaseTool<"ask_followup_question"> {
3939
return
4040
}
4141

42-
// Transform follow_up suggestions to the format expected by task.ask
42+
// Transform follow_up suggestions to the format expected by task.ask.
43+
// Omit `mode` when it's null/undefined to avoid Jinja template errors
44+
// on local models that can't handle null values in tool call arguments.
4345
const follow_up_json = {
4446
question,
45-
suggest: follow_up.map((s) => ({ answer: s.text, mode: s.mode })),
47+
suggest: follow_up.map((s) => {
48+
const suggestion: { answer: string; mode?: string } = { answer: s.text }
49+
if (s.mode != null) {
50+
suggestion.mode = s.mode
51+
}
52+
return suggestion
53+
}),
4654
}
4755

4856
task.consecutiveMistakeCount = 0

src/core/tools/__tests__/askFollowupQuestionTool.spec.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,37 @@ describe("askFollowupQuestionTool", () => {
8282
)
8383
})
8484

85+
it("should strip null mode values from suggestions to prevent Jinja template errors", async () => {
86+
const block: ToolUse = {
87+
type: "tool_use",
88+
name: "ask_followup_question",
89+
params: {
90+
question: "What would you like to do?",
91+
},
92+
nativeArgs: {
93+
question: "What would you like to do?",
94+
follow_up: [
95+
{ text: "Option A", mode: null as any },
96+
{ text: "Option B", mode: "code" },
97+
],
98+
},
99+
partial: false,
100+
}
101+
102+
await askFollowupQuestionTool.handle(mockCline, block as ToolUse<"ask_followup_question">, {
103+
askApproval: vi.fn(),
104+
handleError: vi.fn(),
105+
pushToolResult: mockPushToolResult,
106+
})
107+
108+
// mode: null should be stripped, mode: "code" should be preserved
109+
expect(mockCline.ask).toHaveBeenCalledWith(
110+
"followup",
111+
expect.stringContaining('"suggest":[{"answer":"Option A"},{"answer":"Option B","mode":"code"}]'),
112+
false,
113+
)
114+
})
115+
85116
it("should handle mixed suggestions with and without mode attributes", async () => {
86117
const block: ToolUse = {
87118
type: "tool_use",

0 commit comments

Comments
 (0)