Skip to content

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

Closed

Description

@garionberg

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 (zod 4.1.8)
  • Model: deepseek/deepseek-v4.1-flash-1017 via 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

  1. ctxMemoryArgsShape / ctxNoteArgsShape declare every parameter with .optional() (packages/plugin/src/tools/ctx-memory/tools.ts, ctx-note/tools.ts).
  2. The OpenCode plugin registry converts the shape with z.object(args) → z.toJSONSchema(...), which therefore emits no required array.
  3. 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 action plus the union of action-critical fields required (ids, content, category) so the schema carries a required array; 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: [] on write, content: "" on get, a filler category on get/archive must 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

  1. magic-alfonso commented on Oct 26, 2026

    @magic-alfonso

    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_memory and ctx_note parameter, including action, is declared optional (packages/plugin/src/tools/ctx-memory/tools.ts, ctx-note/tools.ts), so the schema the host sends carries no required array. 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.

  2. magic-alfonso commented on Oct 27, 2026

    @magic-alfonso

    Fixed in 0.49.24. Thanks @garionberg for the careful report.

    ctx_memory and ctx_note now 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, category and ids;
    • ctx_note: action, content and note_ids.

    Values that don't apply to the chosen action can be sent empty, for example ids: [] on a write, or an empty note_ids for 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions