Repository navigation
Description
Summary
When using SiliconFlow's Qwen3-8B model, tool call arguments may contain invalid JSON due to the </tool_call> XML tag being included in the function.arguments field. This affects both streaming and non-streaming modes because neither validates JSON before emitting tool-call events.
Environment
- OpenCode Version: v1.2.20+
- Provider: SiliconFlow (China)
- Model:
Qwen/Qwen3-8B - File:
packages/opencode/src/provider/sdk/copilot/chat/openai-compatible-chat-language-model.ts
Problem Description
Root Cause
Qwen3-8B internally uses <tool_call>...</tool_call> XML format to represent tool calls. When SiliconFlow's API converts this to OpenAI-compatible format, the </tool_call> tag may be included in function.arguments.
Code Analysis
Non-streaming mode (doGenerate) - Line 245-256:
if (choice.message.tool_calls != null) {
for (const toolCall of choice.message.tool_calls) {
content.push({
type: "tool-call",
toolCallId: toolCall.id ?? generateId(),
toolName: toolCall.function.name,
input: toolCall.function.arguments!, // No JSON validation!
// ...
})
}
}Streaming mode (flush) - Line 640-653:
flush(controller) {
// ...
for (const toolCall of toolCalls.filter((toolCall) => !toolCall.hasFinished)) {
controller.enqueue({
type: "tool-call",
toolCallId: toolCall.id ?? generateId(),
toolName: toolCall.function.name,
input: toolCall.function.arguments, // No JSON validation!
})
}
}Note: In streaming mode, there is JSON validation during chunk processing (line 603-606), but unfinished tool calls in flush bypass this check.
Error Message
Invalid input for tool question: JSON parsing failed: Text: {"questions": [...], "multiple": true}}
</tool_call>.
Error message: JSON Parse error: Expected ']'
Steps to Reproduce
- Configure OpenCode to use SiliconFlow with Qwen3-8B
- Send a message that triggers a tool call (e.g., ambiguous request that needs clarification)
- Observe that the tool call arguments contain
</tool_call>tag
Minimal reproduction with curl:
curl -s -N https://api.siliconflow.cn/v1/chat/completions \
-H "Authorization: Bearer $SILICONFLOW_CN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "Qwen/Qwen3-8B",
"messages": [{"role": "user", "content": "帮我写一个Python爬虫"}],
"tools": [{"type": "function", "function": {"name": "question", "description": "Ask user questions", "parameters": {"type": "object", "properties": {"questions": {"type": "array", "items": {"type": "object", "properties": {"question": {"type": "string"}, "options": {"type": "array", "items": {"type": "string"}}, "multiple": {"type": "boolean"}}}}}, "required": ["questions"]}}}],
"tool_choice": "auto",
"stream": true
}'Expected Behavior
Tool call arguments should always be valid JSON in both streaming and non-streaming modes.
Suggested Fix
Option 1: Add JSON validation in both modes
For non-streaming (doGenerate):
if (choice.message.tool_calls != null) {
for (const toolCall of choice.message.tool_calls) {
let args = toolCall.function.arguments!
// Strip XML artifacts from providers like SiliconFlow
args = args.replace(/<\/?tool_call>/g, '').trim()
if (!isParsableJson(args)) {
console.warn(`Invalid tool call arguments: ${args}`)
continue
}
content.push({
type: "tool-call",
toolCallId: toolCall.id ?? generateId(),
toolName: toolCall.function.name,
input: args,
// ...
})
}
}For streaming (flush):
flush(controller) {
// ...
for (const toolCall of toolCalls.filter((toolCall) => !toolCall.hasFinished)) {
let args = toolCall.function.arguments
// Strip XML artifacts from providers like SiliconFlow
args = args.replace(/<\/?tool_call>/g, '').trim()
if (!isParsableJson(args)) {
console.warn(`Invalid tool call arguments: ${args}`)
continue
}
controller.enqueue({
type: "tool-call",
toolCallId: toolCall.id ?? generateId(),
toolName: toolCall.function.name,
input: args,
})
}
}Option 2: Strip XML tags during chunk accumulation (streaming only)
if (toolCallDelta.function?.arguments != null) {
let args = toolCallDelta.function?.arguments ?? ""
// Strip XML tags that some providers include
args = args.replace(/<\/?tool_call>/g, '')
toolCall.function!.arguments += args
}Note: Option 2 only fixes streaming mode. Non-streaming mode still needs separate handling.
Related Code
packages/opencode/src/provider/sdk/copilot/chat/openai-compatible-chat-language-model.ts:245-256-doGeneratetool_calls handling (non-streaming)packages/opencode/src/provider/sdk/copilot/chat/openai-compatible-chat-language-model.ts:640-653-flushfunction (streaming)packages/opencode/src/provider/sdk/copilot/chat/openai-compatible-chat-language-model.ts:591-592- arguments accumulation (streaming)packages/opencode/src/session/llm.ts:178-197-experimental_repairToolCall(current workaround redirects to invalid tool)
Activity
github-actions commentedon Mar 16, 2026
This issue might be a duplicate of existing issues. Please check:
[-]Tool call arguments may contain invalid JSON in streaming mode[/-][+]Tool call arguments may contain invalid JSON[/+]jaykayenn commentedon May 24, 2026
Deepseek via Huggingface also results in "</" being appended to tool calls in Opencode, breaking tool usage.
github-actions commentedon Jul 24, 2026
To stay organized issues are automatically closed after 60 days of no activity. If the issue is still relevant please open a new one.