You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Tool argument descriptions lost in schema conversion #4357
I noticed that when I create custom tools in the ./tool directory and use .describe() on the arguments, those descriptions aren't showing up in the prompts sent to the LLM. The agent doesn't seem to know what the parameters are for, which leads to incorrect usage. At the moment I am adding them to the description of the tool itself, which is not ideal.
After digging through the source code, I found the issue. There are two different methods being used to convert Zod schemas to JSON Schema:
In server.ts (line 307) - uses zodToJsonSchema() from the zod-to-json-schema package ✅
In prompt.ts (line 581) - uses z.toJSONSchema() from Zod's built-in method ❌
The problem is that z.toJSONSchema() doesn't properly extract the .describe() metadata from schema properties, so all those descriptions get dropped before reaching the LLM.
(Possible) Fix:
Change line 581 in session/prompt.ts from:
This makes the schema conversion consistent with what's already being done in server.ts and ensures all tool descriptions are properly passed to the LLM.
OpenCode version
v1.0.65
Steps to reproduce
For example, I have a tool like this:
exportdefaulttool({description: "Return the length of a string",args: {text: z.string().describe("The text string to measure"),},// ...});
I expected the LLM to see the description "The text string to measure" for the text parameter, but it's not getting that information.
@rekram1-node thank you for getting back to me. Here is an example tool you could try:
import{tool}from"@opencode-ai/plugin";import{z}from"zod";exportdefaulttool({description: "Return what Arsham wants",args: {text: z.string().describe("You can heabang to this Death Metal tune!"),},asyncexecute(args: {text: string}): Promise<string>{return"Done!";},});
If you ask the agent to give you how the tool is represented verbatim in its tool set, you will get something like this:
Here is the verbatim representation of the arsham tool as it appears in my toolbox:
```json
{
"description": "Return what Arsham wants",
"name": "arsham",
"parameters": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"additionalProperties": false,
"properties": {
"text": {
"type": "string"
}
},
"required": ["text"],
"type": "object"
}
}
```**Key elements**:
-**name**: `arsham`-**description**: "Return what Arsham wants"
-**parameters**: JSON Schema object with:
-`text` (type: string, required)
- No additional properties allowed
- Uses JSON Schema draft 2020-12
That's the complete, verbatim definition provided to me in the system prompt.
I will try to attack this when I get a chance to, have a few higher priority ones but this is ofc a big issue for custom tools since descriptions are prompts.
I guess work around for now:
Overload the tool top level description to describe the tool parameters too
I will try to attack this when I get a chance to, have a few higher priority ones but this is ofc a big issue for custom tools since descriptions are prompts.
I guess work around for now:
Overload the tool top level description to describe the tool parameters too
@rekram1-node I spoke about it a bit on my PR #15957 but I think the issue is that there's two different instances of zod being used (one instance in the tool code itself imported from @opencode-ai/plugin) and another instance within the opencode app (imported from zod). Metadata on the schema (like .description()) is stored in zod's runtime registry, which is different for each instance of zod.
This means that when zod instance A creates the object with the schema fields, but zod instance B is the one that then wraps it in z.object() and then z.toJsonSchema(), the descripton fields get dropped because they're not in the registry.
Agents always call tools with timeouts in seconds, while they should be using milliseconds, but that information is "lost in translation" and agents never see it.
I investigated this on the latest dev branch and the original bug appears to be fixed. z.toJSONSchema() (Zod v4) now correctly preserves .describe() metadata on all schema properties — I verified this directly with a test schema.
The Zod v4 migration resolved this for the prompt path (session/prompt.ts line 440).
However, I found a related issue: server/routes/experimental.ts line 87 still uses zodToJsonSchema() from zod-to-json-schema@3.24.5, which is incompatible with Zod v4 and produces incorrect output (returns {"type": "string"} for object schemas). I've filed #20806 for that.
@ashleytowner@khimaros — You're right, and I appreciate the pushback. My earlier comment was wrong.
I verified that z.toJSONSchema() preserves .describe() metadata for schemas defined in the same Zod instance, and jumped to "this is fixed" without testing the cross-instance case that actually matters for plugins. The real problem — plugin tools loading their own Zod instance, so .describe() metadata lives in the plugin's z.globalRegistry and is invisible to the core's z.toJSONSchema() — is exactly what @ashleytowner identified in #15957 and what @khimaros has been maintaining in #19916.
#19916's rehydrateZodMeta() in registry.ts is the correct fix at the correct location. The metadata needs to be rehydrated before it reaches any schema conversion path.
My PR #20809 fixes a separate, narrower bug — experimental.ts still uses the Zod v4-incompatible zodToJsonSchema() from zod-to-json-schema. That's the HTTP API endpoint, not the LLM tool-calling path that affects plugin descriptions. Both are real bugs, but they're independent.
Activity
rekram1-node commentedon Nov 17, 2025
@arsham I can't replicate this with any tool.
Are you sure it is being sent without a description?
This is how I tested:
U can do this with any provider ofc but it just gives the exact request sent to the provider
arsham commentedon Nov 18, 2025
@rekram1-node thank you for getting back to me. Here is an example tool you could try:
If you ask the agent to give you how the tool is represented verbatim in its tool set, you will get something like this:
I might be doing something stupid here though.
rekram1-node commentedon Nov 18, 2025
I can replicate now, fixing shortly
rekram1-node commentedon Nov 18, 2025
some weird zod bug going on...
rekram1-node commentedon Nov 18, 2025
If you import a file dynamically, vs define the tool as we do, the zod parameters change wtf
arsham commentedon Nov 18, 2025
wtf indeed!
rekram1-node commentedon Nov 19, 2025
This one is pretty obscure but there is something weird going on here:
https://github.com/sst/opencode/blob/48e4f2f45d54364a8123d603dbbca7bfe8fcc763/packages/opencode/src/tool/registry.ts#L59
I will try to attack this when I get a chance to, have a few higher priority ones but this is ofc a big issue for custom tools since descriptions are prompts.
I guess work around for now:
Overload the tool top level description to describe the tool parameters too
arsham commentedon Nov 19, 2025
Thank you. I would this this issue is super low priority.
fix(opencode): preserve custom tool arg descriptions in LLM prompts
fix: preserve tool parameter descriptions in schema conversion
ashleytowner commentedon Mar 4, 2026
@rekram1-node I spoke about it a bit on my PR #15957 but I think the issue is that there's two different instances of
zodbeing used (one instance in the tool code itself imported from@opencode-ai/plugin) and another instance within the opencode app (imported fromzod). Metadata on the schema (like.description()) is stored in zod's runtime registry, which is different for each instance of zod.This means that when zod instance A creates the object with the schema fields, but zod instance B is the one that then wraps it in
z.object()and thenz.toJsonSchema(), the descripton fields get dropped because they're not in the registry.samiralibabic commentedon Mar 18, 2026
This also affects plugins, like https://github.com/samiralibabic/opencode-rexd-target
Agents always call tools with timeouts in seconds, while they should be using milliseconds, but that information is "lost in translation" and agents never see it.
khimaros commentedon Mar 29, 2026
this really limits the capability of plugins
jbmml commentedon Apr 3, 2026
I investigated this on the latest dev branch and the original bug appears to be fixed.
z.toJSONSchema()(Zod v4) now correctly preserves.describe()metadata on all schema properties — I verified this directly with a test schema.The Zod v4 migration resolved this for the prompt path (
session/prompt.tsline 440).However, I found a related issue:
server/routes/experimental.tsline 87 still useszodToJsonSchema()fromzod-to-json-schema@3.24.5, which is incompatible with Zod v4 and produces incorrect output (returns{"type": "string"}for object schemas). I've filed #20806 for that.ashleytowner commentedon Apr 9, 2026
@jbmml how did you confirm this? Because it is still broken for me.
khimaros commentedon Apr 9, 2026
same. Ashley's patch is still needed. i have been rebasing on origin/dev regularly here: #19916
jbmml commentedon Apr 9, 2026
@ashleytowner @khimaros — You're right, and I appreciate the pushback. My earlier comment was wrong.
I verified that
z.toJSONSchema()preserves.describe()metadata for schemas defined in the same Zod instance, and jumped to "this is fixed" without testing the cross-instance case that actually matters for plugins. The real problem — plugin tools loading their own Zod instance, so.describe()metadata lives in the plugin'sz.globalRegistryand is invisible to the core'sz.toJSONSchema()— is exactly what @ashleytowner identified in #15957 and what @khimaros has been maintaining in #19916.#19916's
rehydrateZodMeta()inregistry.tsis the correct fix at the correct location. The metadata needs to be rehydrated before it reaches any schema conversion path.My PR #20809 fixes a separate, narrower bug —
experimental.tsstill uses the Zod v4-incompatiblezodToJsonSchema()fromzod-to-json-schema. That's the HTTP API endpoint, not the LLM tool-calling path that affects plugin descriptions. Both are real bugs, but they're independent.Sorry for the noise. +1 to getting #19916 merged.