Repository navigation
0.49.23 (opencode 1.19.4): ctx_memory/ctx_note silently lose arguments on OpenRouter InferenceNet — all-optional schemas are not provider-safe #793
Description
Summary
On OpenRouter's InferenceNet provider (slugs inference-net and inference-net/us), tool-call arguments declared .optional() are dropped: the model emits only the first property of the schema. Because every ctx_memory / ctx_note parameter is optional, calls arrive truncated and the handler rejects them:
Error: 'ids' must contain at least one integer memory ID when action is 'get'
Error: 'content' is required when action is 'write'
Error: 'ids' must contain at least one integer memory ID when action is 'update'
The same model, same prompt, same schema on DeepInfra emits complete arguments and the calls succeed. So the all-optional surface — which appears to be deliberate, see #460 — is not safe across providers.
This is not a model bug: with required added, InferenceNet emits the fields correctly (matrix below).
Environment
- Magic Context:
0.49.23 - OpenCode:
1.19.4 @opencode-ai/plugin:1.14.18(zod4.1.8)- Model:
deepseek/deepseek-v4.1-flash-1017via OpenRouter - Provider routing:
order: ["inference-net/us", "deepinfra"],allow_fallbacks: false,zdr: true - OS: Linux x64
Reproduction
One chat completion with the schema createCtxMemoryTool() exposes today (no required array), prompt: "Call ctx_memory to read memory 1473."
| Provider | Schema | Emitted arguments |
|---|---|---|
InferenceNet (inference-net/us) |
all optional (as shipped) | {"action": "get"} |
| DeepInfra | all optional (as shipped) | {"action": "get", "ids": [1473]} |
| OpenRouter auto (routed to InferenceNet) | all optional (as shipped) | {"action": "get"} |
| InferenceNet | required: ["action"] |
{"action": "get"} |
| InferenceNet | required: ["action", "ids"] |
{"ids": [1473], "action": "get"} |
| InferenceNet | anyOf per-action required |
{"action": "get"} (ignored) |
| InferenceNet | all-optional + description guidance ("action=get requires ids") | {"action": "get"} (ignored) |
Three further prompts on InferenceNet with the shipped schema:
| Prompt | Emitted |
|---|---|
| "Record a durable memory: … Choose the most appropriate category." | {"action": "write"} |
| "Update memory 1473 with the content … Keep its category." | {"action": "get"} |
| "Read memory 1473." | {"action": "get"} |
So on InferenceNet the tool is unusable for anything except a bare action.
Root cause
ctxMemoryArgsShape/ctxNoteArgsShapedeclare every parameter with.optional()(packages/plugin/src/tools/ctx-memory/tools.ts,ctx-note/tools.ts).- The OpenCode plugin registry converts the shape with
z.object(args)→z.toJSONSchema(...), which therefore emits norequiredarray. - InferenceNet treats non-required properties as omittable and emits only the first one. DeepInfra fills them.
This is the mirror image of #460: there, a required-all surface forced the model to fill action-irrelevant fields (note_id: 0, note_ids: [1]). Both failure modes come from the same place — a single multi-action tool whose parameter set varies by action, exposed as one flat object schema. All-optional protects against #460 but is vulnerable to argument-dropping providers; required-all protects against dropping but produces filler. Neither is provider-safe in general.
Suggested fix
Since the plugin API exposes a raw Zod shape rather than a discriminated union (as #460 notes), the narrowest robust fix is the same pattern #460 established — make the fields the caller must decide required, and normalize the rest by action:
- Mark
actionplus the union of action-critical fields required (ids,content,category) so the schema carries arequiredarray; providers that drop optionals then still emit them. - Rely on (and extend) the ctx_note write fails when required-all tool schemas fill both ID fields #460 action normalization to ignore action-irrelevant values:
ids: []onwrite,content: ""onget, a fillercategoryonget/archivemust not fail the call.
Verified locally that this combination works on InferenceNet for get, write and update (arguments arrive complete, and the existing #460 normalization accepts the filler).
Alternatively, if all-optional is preferred, document the provider limitation and recommend DeepInfra (or any provider that fills optional args) for the primary session model — but a silent failure mode that looks like "the tool is broken" is worth avoiding in the schema itself.
Regression coverage worth adding: an assertion that the provider-facing schema has a non-empty required array, plus a handler test proving get succeeds when content/category are present as filler (and write when ids: [] is present).
Workaround in use
A local patch unwraps .optional() on action, content, category, ids for ctx_memory and action, content, note_ids for ctx_note before the schema is built. With ["inference-net/us","deepinfra"] primary it restores all three actions; the plugin is otherwise unmodified. It has to be re-applied after every plugin update, which is why this report exists.
Triage note
The tool-call arguments persisted by OpenCode (~/.local/share/opencode/opencode.db, part.data.state.input) are lossy — a bash call records only {"command": …} even though description is a required parameter that was definitely sent. They therefore cannot be used to diagnose missing-argument bugs; the provider probe above is what settled it.
Activity
magic-alfonso commentedon Oct 26, 2026
Thanks for the provider matrix. Comparing the same model and schema on two providers is exactly what settles this.
Confirmed on our side: every
ctx_memoryandctx_noteparameter, includingaction, is declared optional (packages/plugin/src/tools/ctx-memory/tools.ts,ctx-note/tools.ts), so the schema the host sends carries norequiredarray. As you describe, this is the other side of #460, where requiring everything made models fill in irrelevant fields.Changing these schemas changes the tool definitions every session sends, so we're deciding the shape before changing anything. Your local patch is a reasonable workaround until then, and so is routing the primary model to a provider that fills optional arguments. I'll update this issue once the direction is settled.
magic-alfonso commentedon Oct 27, 2026
Fixed in 0.49.24. Thanks @garionberg for the careful report.
ctx_memoryandctx_notenow mark their key arguments as required, so providers that send only the first argument when nothing is required (such as InferenceNet on OpenRouter) send them all:ctx_memory:action,content,categoryandids;ctx_note:action,contentandnote_ids.Values that don't apply to the chosen action can be sent empty, for example
ids: []on a write, or an emptynote_idsfor a plain note listing. To keep a memory's category on an update or merge, the model passes its current category.The tool definitions changed, so each open session rebuilds its prompt cache once on its next turn after you update and restart.