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
update_goal_plan input leaks raw Zod schemas → invalid JSON Schema ("type":"optional") rejected by strict providers #71
update_goal_plan is the only goal tool whose input schema is built from raw Zod v4 schemas instead of plain JSON Schema. When OpenCode serializes it for the model, Zod internals leak into the schema, including "type": "optional", which is not valid JSON Schema. Providers that validate tool schemas strictly reject the entire request.
Version / environment
@prevalentware/opencode-goal-plugin@0.1.59 (latest at time of writing)
OpenCode v2
Reproduced through the opencode provider, whose upstream validates tool schemas with Python jsonschema
v2ObjectSchema drops the raw Zod schemas straight into properties, so the tool input is a "JSON schema" whose property values are Zod instances. A Zod v4 schema serializes like this:
So the emitted schema contains revisit_evidence: { "def": {...}, "type": "optional" }.
Error observed
Upstream request failed: [invalid_request_error]
Error validating JSON Schema:
<ValidationError: "'optional' is not valid under any of the given schemas">
Reproduced locally with Python jsonschema 4.10.3:
fromjsonschemaimportDraft202012Validatorlist(Draft202012Validator(Draft202012Validator.META_SCHEMA).iter_errors({"type": "optional"}))
# MSG: 'optional' is not valid under any of the given schemas
Why only this tool
Every other goal tool builds input from plain JSON Schema helpers (e.g. v2GoalTextSchema(...), or inline { type: "string", enum: [...] }). update_goal_plan is the sole tool that passes the raw Zod planToolArgs through v2ObjectSchema.
Impact
Any OpenCode session that exposes update_goal_plan fails every model request when routed through a provider that validates tool schemas, because the provider rejects the whole request. The session cannot proceed.
Suggested fix
Build the update_goal_planinput with plain JSON Schema, matching the other tools:
plan → a fully expanded object schema (or convert with z.toJSONSchema(GoalPlanInputSchema) before wrapping)
Keep the Zod schemas for parsing/validation only (PlanToolSchema = z2.object(planToolArgs).strict()), but do not place them directly inside the tool input.
Independent confirmation on a third provider family, plus a verified patch.
Third reproduction path
muse-spark-1.3-contributor reached through an OpenAI-compatible router whose upstream node is a Responses API endpoint (strict tool-schema validation). The request is rejected before the first token:
[400] Invalid JSON schema: "optional" is not valid under any of the schemas listed in the 'anyOf' keyword
Router-side call logs show 5 requests rejected with exactly this error — every one of them carrying the update_goal_plan tool, while the other 24 tools in the same requests were clean. Tolerant upstreams (GLM-family nodes on the same router) silently accept the identical payload, which matches the "only some models die" symptom reported above.
What leaks
Deep-scanning the serialized update_goal_plan input found 24 occurrences of "type": "optional" used as a type value, plus "type": "default", "type": "nullable", "type": "enum", "type": "never", and Zod internals def, innerType, checks, and [MaxDepth] placeholder strings, e.g.:
In dist/server.js, goalToolsV2() registers input: v2ObjectSchema(planToolArgs) where every value of planToolArgs is a Zod instance. Replacing that line with a per-property z.toJSONSchema() conversion fixes it:
io: "input" maps .optional() props to the plain inner schema and .nullish() to anyOf: [ {...}, { "type": "null" } ] — both valid under strict validators.
required must be derived via .isOptional(); after conversion it correctly comes out as ["goal_id", "expected_revision", "plan", "reason"] with revisit_evidence omitted.
Because z2.toJSONSchema() returns a fresh object per call and the IIFE runs on each goalToolsV2() invocation, every registration gets its own deep copy — this also addresses the shared-object mutation concern raised earlier in the thread.
Verified end-to-end: after the patch, the exact request that returned 400 completes with 200 OK through the same strict upstream.
Environment: opencode v2.0.26, @prevalentware/opencode-goal-plugin@0.1.59, zod resolved at 4.6.5 inside the plugin cache.
This is the same update_goal_plan raw-Zod schema defect tracked in #69. PR #70 has merged the fix, including provider-safe JSON Schema and regression coverage. Thank you for the independent strict-provider reproduction and verification.
Activity
nixaut-codelabs commentedon Oct 9, 2026
Independent confirmation on a third provider family, plus a verified patch.
Third reproduction path
muse-spark-1.3-contributorreached through an OpenAI-compatible router whose upstream node is a Responses API endpoint (strict tool-schema validation). The request is rejected before the first token:Router-side call logs show 5 requests rejected with exactly this error — every one of them carrying the
update_goal_plantool, while the other 24 tools in the same requests were clean. Tolerant upstreams (GLM-family nodes on the same router) silently accept the identical payload, which matches the "only some models die" symptom reported above.What leaks
Deep-scanning the serialized
update_goal_planinput found 24 occurrences of"type": "optional"used as a type value, plus"type": "default","type": "nullable","type": "enum","type": "never", and Zod internalsdef,innerType,checks, and[MaxDepth]placeholder strings, e.g.:Verified fix
In
dist/server.js,goalToolsV2()registersinput: v2ObjectSchema(planToolArgs)where every value ofplanToolArgsis a Zod instance. Replacing that line with a per-propertyz.toJSONSchema()conversion fixes it:Notes for whoever lands the PR:
io: "input"maps.optional()props to the plain inner schema and.nullish()toanyOf: [ {...}, { "type": "null" } ]— both valid under strict validators.requiredmust be derived via.isOptional(); after conversion it correctly comes out as["goal_id", "expected_revision", "plan", "reason"]withrevisit_evidenceomitted.z2.toJSONSchema()returns a fresh object per call and the IIFE runs on eachgoalToolsV2()invocation, every registration gets its own deep copy — this also addresses the shared-object mutation concern raised earlier in the thread.Environment: opencode v2.0.26,
@prevalentware/opencode-goal-plugin@0.1.59, zod resolved at 4.6.5 inside the plugin cache.danyel117 commentedon Oct 10, 2026
This is the same update_goal_plan raw-Zod schema defect tracked in #69. PR #70 has merged the fix, including provider-safe JSON Schema and regression coverage. Thank you for the independent strict-provider reproduction and verification.