Summary
Every OpenCode session that includes the goal tools fails on providers that strictly validate tool JSON Schemas, because the V2 update_goal_plan registration passes raw zod schema objects as the tool input. The host serializes them as-is, producing invalid JSON Schema, and the provider rejects the whole request with 400 Invalid tool schema.
Environment
- OpenCode v2.0.24,
@prevalentware/opencode-goal-plugin 0.1.59
- Provider: Mistral, model
mistral-large-4 (strict tool-schema validation)
- Other goal tools are unaffected; only
update_goal_plan leaks internals
What happens
goalToolsV2 builds the tool input with raw zod objects:
const planToolArgs = {
goal_id: z.string().min(1),
expected_revision: z.number().int().nonnegative(),
plan: GoalPlanInputSchema,
reason: z.string().trim().min(1).max(2000),
revisit_evidence: z.string().trim().min(1).max(2000).optional(),
}
// ...
input: v2ObjectSchema(planToolArgs), // <- properties are zod schemas, not JSON Schema
Captured from the actual provider request (GET on a local logging proxy), the schema OpenCode sends looks like this:
{
"type": "object",
"properties": {
"goal_id": {
"def": { "type": "string", "checks": [{}] },
"type": "string",
"format": null,
"minLength": 1,
"maxLength": null
},
"expected_revision": {
"def": { "type": "number", "checks": [ { "def": { "check": "number_format", "format": "safeint" }, ... } ] },
"type": "number",
"isFinite": true,
"minValue": 0,
"format": "safeint"
},
"plan": { "def": { "type": "object", "shape": { ... } } }
},
"required": [],
"additionalProperties": false
}
Invalid keywords include def, checks, shape, optional, isFinite, minValue, maxValue, isInt, and null values for keywords that must be integers/strings ("maxLength": null, "format": null).
The provider (Python jsonschema-style validation) rejects it with any of:
Invalid tool schema: None is not of type 'integer'
Invalid tool schema: None is not of type 'string'
Invalid tool schema: 'optional' is not valid under any of the given schemas
I verified directly against the API that mistral-large-4 strictly validates tool schemas while zai-glm-5-3 silently accepts the same invalid schema — which is why the bug only surfaces on specific models. Since the plugin registers the goal tools into every session (including subagents), any session on a strictly-validating model dies before the first token.
Suggested fix
Convert PlanToolSchema to JSON Schema once via z.toJSONSchema and use that as the V2 tool input (the V2 plugin API takes plain JSON Schema, as the other goal tools already do). Runtime validation is unaffected because planFromTool parses through PlanToolSchema itself.
One extra caution from testing: hand out a deep copy per registration rather than the shared module-level object. Bun's expect().toMatchObject() mutates the object it receives (it emptied properties and even unfroze a frozen object in my repro), and any host that mutates a received tool input would otherwise corrupt the schema for every other registration.
I have a PR ready with the fix + regression tests.
Summary
Every OpenCode session that includes the goal tools fails on providers that strictly validate tool JSON Schemas, because the V2
update_goal_planregistration passes raw zod schema objects as the toolinput. The host serializes them as-is, producing invalid JSON Schema, and the provider rejects the whole request with400 Invalid tool schema.Environment
@prevalentware/opencode-goal-plugin0.1.59mistral-large-4(strict tool-schema validation)update_goal_planleaks internalsWhat happens
goalToolsV2builds the tool input with raw zod objects:Captured from the actual provider request (
GETon a local logging proxy), the schema OpenCode sends looks like this:{ "type": "object", "properties": { "goal_id": { "def": { "type": "string", "checks": [{}] }, "type": "string", "format": null, "minLength": 1, "maxLength": null }, "expected_revision": { "def": { "type": "number", "checks": [ { "def": { "check": "number_format", "format": "safeint" }, ... } ] }, "type": "number", "isFinite": true, "minValue": 0, "format": "safeint" }, "plan": { "def": { "type": "object", "shape": { ... } } } }, "required": [], "additionalProperties": false }Invalid keywords include
def,checks,shape,optional,isFinite,minValue,maxValue,isInt, and null values for keywords that must be integers/strings ("maxLength": null,"format": null).The provider (Python jsonschema-style validation) rejects it with any of:
Invalid tool schema: None is not of type 'integer'Invalid tool schema: None is not of type 'string'Invalid tool schema: 'optional' is not valid under any of the given schemasI verified directly against the API that
mistral-large-4strictly validates tool schemas whilezai-glm-5-3silently accepts the same invalid schema — which is why the bug only surfaces on specific models. Since the plugin registers the goal tools into every session (including subagents), any session on a strictly-validating model dies before the first token.Suggested fix
Convert
PlanToolSchemato JSON Schema once viaz.toJSONSchemaand use that as the V2 toolinput(the V2 plugin API takes plain JSON Schema, as the other goal tools already do). Runtime validation is unaffected becauseplanFromToolparses throughPlanToolSchemaitself.One extra caution from testing: hand out a deep copy per registration rather than the shared module-level object. Bun's
expect().toMatchObject()mutates the object it receives (it emptiedpropertiesand even unfroze a frozen object in my repro), and any host that mutates a received tool input would otherwise corrupt the schema for every other registration.I have a PR ready with the fix + regression tests.