Bug Description
The AI SDK (@vercel/ai) generates tool call IDs that are not globally unique across multiple messages in a conversation. When language model providers don't include tool call IDs in their responses (or provide empty IDs), the AI SDK falls back to using an index-based identifier within each message. This results in non-unique IDs like bash:0, bash:1, read:0, grep:1, edit:0, wait_for_user:1, etc. The same ID pattern repeats across different messages, making it impossible for the OpenCode DCP plugin to uniquely identify a tool call using only the callID field from message.part.callID.
This causes the plugin to incorrectly associate tool outputs with tool calls from different messages, which can lead to modified tool output being applied to wrong tool calls and context pruning decisions based on misaligned tool execution history. Moreover, tool results will be immediately replaced by the text [Output removed to save context - information superseded...] before the model had the opportunity to read them in the same turn as the tools were called.
Expected Behavior
Tool call IDs should be globally unique across the entire conversation, allowing the OpenCode DCP plugin to unambiguously correlate tool calls with their outputs without needing additional context. Alternatively, the plugin should have a reliable way to correlate tool calls with their corresponding outputs across multiple messages.
Debug Context Logs
[
// ...
{
"role": "user",
"time": 1788528208168,
"parts": [
{
"type": "text",
"text": "I enabled debug logging in the plugin. Try causing the problem to occur once again so I get some meaningful output for a bug report.\n\n<dcp-message-id>m0701</dcp-message-id>"
}
]
},
{
"role": "assistant",
"time": 1788528208209,
"tokens": {
"input": 18380,
"output": 204,
"reasoning": 0,
"cache": {
"write": 0,
"read": 121344
}
},
"parts": [
{
"type": "reasoning",
"text": "User wants me to reproduce the stripping so the plugin's debug log captures it. Trigger a burst of small direct tool outputs in one turn."
},
{
"type": "text",
"text": "Triggering the failure mode now — a burst of short-lived direct tool outputs (the exact pattern that was being stripped):"
},
{
"type": "tool",
"tool": "bash",
"callID": "bash:0",
"status": "completed",
"input": {
"command": "date +%s"
},
"output": "[Output removed to save context - information superseded or no longer needed]\n<dcp-message-id>m0702</dcp-message-id>",
"metadata": {
"output": "1788528288\n",
"exit": 0,
"truncated": false
},
"title": "date +%s"
},
{
"type": "tool",
"tool": "bash",
"callID": "bash:1",
"status": "completed",
"input": {
"command": "echo marker-A-$(date +%s)"
},
"output": "[Output removed to save context - information superseded or no longer needed]\n<dcp-message-id>m0702</dcp-message-id>",
"metadata": {
"output": "marker-A-1788528288\n",
"exit": 0,
"truncated": false
},
"title": "echo marker-A-$(date +%s)"
},
{
"type": "tool",
"tool": "read",
"callID": "read:2",
"status": "completed",
"input": {
"filePath": "/mnt/files/README.md",
"limit": 5
},
"output": "<path>/mnt/files/README.md</path>\n<type>file</type>\n<content>\n[actual file content here]\n</content>\n<dcp-message-id>m0702</dcp-message-id>",
"metadata": {
"preview": "[actual file content here]",
"truncated": true,
"loaded": [],
"display": {
"type": "file",
"path": "/mnt/files/README.md",
"text": "[actual file content here]",
"lineStart": 1,
"lineEnd": 5,
"totalLines": 125,
"truncated": true
}
},
"title": "README.md"
}
]
},
{
"role": "assistant",
"time": 1788528289253,
"tokens": {
"input": 140901,
"output": 206,
"reasoning": 0,
"cache": {
"write": 0,
"read": 0
}
},
"parts": [
{
"type": "tool",
"tool": "bash",
"callID": "bash:0",
"status": "completed",
"input": {
"command": "echo probe-1-$(date +%s)"
},
"output": "[Output removed to save context - information superseded or no longer needed]\n<dcp-message-id>m0703</dcp-message-id>",
"metadata": {
"output": "probe-1-1788528306\n",
"exit": 0,
"truncated": false
},
"title": "echo probe-1-$(date +%s)"
},
{
"type": "tool",
"tool": "bash",
"callID": "bash:1",
"status": "completed",
"input": {
"command": "echo probe-2-$(date +%s) && echo probe-3 && echo probe-4"
},
"output": "[Output removed to save context - information superseded or no longer needed]\n<dcp-message-id>m0703</dcp-message-id>",
"metadata": {
"output": "probe-2-1788528306\nprobe-3\nprobe-4\n",
"exit": 0,
"truncated": false
},
"title": "echo probe-2-$(date +%s) && echo probe-3 && echo probe-4"
},
{
"type": "tool",
"tool": "bash",
"callID": "bash:2",
"status": "completed",
"input": {
"command": "uname -a; uptime"
},
"output": "Linux hostname 7.1.5-201.fc44.x86_64 #1 SMP PREEMPT_DYNAMIC Tue Jul 28 14:16:30 UTC 2026 x86_64 GNU/Linux\n 15:25:06 up 6 days, 4:32, 1 user, load average: 0,60, 0,64, 0,48\n\n<dcp-message-id>m0703</dcp-message-id>",
"metadata": {
"output": "Linux hostname 7.1.5-201.fc44.x86_64 #1 SMP PREEMPT_DYNAMIC Tue Jul 28 14:16:30 UTC 2026 x86_64 GNU/Linux\n 15:25:06 up 6 days, 4:32, 1 user, load average: 0,60, 0,64, 0,48\n",
"exit": 0,
"truncated": false
},
"title": "uname -a; uptime"
},
{
"type": "tool",
"tool": "bash",
"callID": "bash:3",
"status": "completed",
"input": {
"command": "echo fill-A; echo fill-B"
},
"output": "fill-A\nfill-B\n\n<dcp-message-id>m0703</dcp-message-id>",
"metadata": {
"output": "fill-A\nfill-B\n",
"exit": 0,
"truncated": false
},
"title": "echo fill-A; echo fill-B"
},
{
"type": "tool",
"tool": "bash",
"callID": "bash:4",
"status": "completed",
"input": {
"command": "echo tail-marker-END"
},
"output": "tail-marker-END\n\n<dcp-message-id>m0703</dcp-message-id>",
"metadata": {
"output": "tail-marker-END\n",
"exit": 0,
"truncated": false
},
"title": "echo tail-marker-END"
}
]
}
]
Tool Call Details
Example scenario:
- Message 1: Tool call with
callID: bash:0 for command "ls /"
- Message 2: Tool call with
callID: bash:0 for command "cd /tmp"
When processing tool outputs, if the plugin only uses the callID without message context, it cannot distinguish between these two separate tool calls. The output of the second bash:0 call might be incorrectly attributed to the first one.
DCP Version
3.1.15
Opencode Version
1.18.27
Model
Other (specify in description)
Additional Context
Root Cause:
This issue stems from the AI SDK's @vercel/ai package, specifically:
-
packages/openai-compatible/src/chat/openai-compatible-chat-language-model.ts (line 377):
toolCallId: toolCall.id || generateId(),
-
packages/provider-utils/src/streaming-tool-call-tracker.ts (lines 110-129):
processDelta(toolCallDelta: DELTA): void {
const { id, index } = toolCallDelta;
let toolCall =
id != null && id.length > 0
? this.toolCallsById.get(id)
: index != null
? this.toolCallsByIndex.get(index) // ← Fallback to index
: this.latestToolCall;
When the provider doesn't send id fields, the SDK tracks tool calls by their index within the current message, generating composite IDs that reset with each new message.
Recommended Solution for DCP:
Use a composite key to uniquely identify tool calls:
- Internally track tool calls using
{messageId}:{toolCallId} as the unique key
- Preserve original
callID for display purposes
- When matching tool calls with outputs, always correlate using both message ID and tool call ID
Resources:
Bug Description
The AI SDK (@vercel/ai) generates tool call IDs that are not globally unique across multiple messages in a conversation. When language model providers don't include tool call IDs in their responses (or provide empty IDs), the AI SDK falls back to using an index-based identifier within each message. This results in non-unique IDs like
bash:0,bash:1,read:0,grep:1,edit:0,wait_for_user:1, etc. The same ID pattern repeats across different messages, making it impossible for the OpenCode DCP plugin to uniquely identify a tool call using only thecallIDfield frommessage.part.callID.This causes the plugin to incorrectly associate tool outputs with tool calls from different messages, which can lead to modified tool output being applied to wrong tool calls and context pruning decisions based on misaligned tool execution history. Moreover, tool results will be immediately replaced by the text
[Output removed to save context - information superseded...]before the model had the opportunity to read them in the same turn as the tools were called.Expected Behavior
Tool call IDs should be globally unique across the entire conversation, allowing the OpenCode DCP plugin to unambiguously correlate tool calls with their outputs without needing additional context. Alternatively, the plugin should have a reliable way to correlate tool calls with their corresponding outputs across multiple messages.
Debug Context Logs
Tool Call Details
Example scenario:
callID: bash:0for command "ls /"callID: bash:0for command "cd /tmp"When processing tool outputs, if the plugin only uses the
callIDwithout message context, it cannot distinguish between these two separate tool calls. The output of the secondbash:0call might be incorrectly attributed to the first one.DCP Version
3.1.15
Opencode Version
1.18.27
Model
Other (specify in description)
Additional Context
Root Cause:
This issue stems from the AI SDK's @vercel/ai package, specifically:
packages/openai-compatible/src/chat/openai-compatible-chat-language-model.ts(line 377):packages/provider-utils/src/streaming-tool-call-tracker.ts(lines 110-129):When the provider doesn't send
idfields, the SDK tracks tool calls by theirindexwithin the current message, generating composite IDs that reset with each new message.Recommended Solution for DCP:
Use a composite key to uniquely identify tool calls:
{messageId}:{toolCallId}as the unique keycallIDfor display purposesResources: