An OpenCode plugin that keeps write
and edit usable with OpenAI-compatible local models, instead of having to turn
those tools off.
Related upstream: issue #29142, PR #30224, and the earlier report #18131.
// crafted for the agentic AI community — funding keeps it maintained
Models served through LM Studio, LiteLLM or similar (Qwen, DeepSeek, and other
OpenAI-compatible endpoints) fail write/edit calls with:
Error: The write tool was called with invalid arguments:
SchemaError(Missing key at ["content"]).
Please rewrite the input so it satisfies the expected schema.
The agent then retries the same broken shape and loops. The workaround people land on is to disable the tools outright:
permission:
edit: deny
write: deny…which removes one of OpenCode's core editing paths. This plugin exists so you don't have to do that.
This is the part worth understanding before installing anything, because only one of the two is a plugin problem.
| Cause | Tool input looks like | Fix |
|---|---|---|
| Mis-keyed arguments — model used a synonym | {"fileContent": "…", "path": "…"} |
this plugin |
| Empty arguments — model emitted no args at all | {} |
provider config, see below |
There are no keys to rename. An argument-less tool call means the model
degenerated, and on the system this was developed against that was caused by
context overflow. Custom providers default to limit.context = 0, and
isOverflow() in session/overflow.ts bails out early on exactly that:
if (input.model.limit.context === 0) return falseAuto-compaction therefore never fires, context grows without bound, and the model eventually emits tool calls with no arguments. Measured on one session: tool calls succeeded at ~66k input tokens on average and failed from ~80k up to 167k.
The fix is to declare limits on your provider's models:
limit.input is the useful knob: usable = limit.input − reserved, so you can
declare the true context window while still compacting early enough to stay in
the range where the model behaves.
1. Alias normalization — tool.execute.before
Renames known alias keys onto their canonical names in place, before
OpenCode's schema decode. In session/tools.ts the
plugin.trigger("tool.execute.before", …) call runs before item.execute, and
the hook receives the live args object — so a renamed key makes the call succeed
outright rather than just producing a nicer error.
Covers write, edit, bash, read, grep, glob. For example
fileContent/text/body → content; path/file_path/fileName →
filePath; cmd/script/shell → command; old_string/search →
oldString. A key the model already got right is never overwritten.
2. Field reorder — tool.definition
Re-emits the write schema with filePath declared before content. Local
models generate JSON fields in declaration order, so a model that runs out of
output budget partway through a large content value has already emitted the
required filePath. Same idea as PR
#29943, applied from
userland with no rebuild.
3. Diagnostics
Appends to ~/.local/share/opencode/plugin-schema-fix.log:
loaded— once per start; use this to confirm the plugin is actually picked uprepaired tool=… alias->canonical— an alias was renamedempty-args tool=…— the{}case, logged distinctly so it is never mistaken for a key mismatch
# global
git clone https://github.com/VRIL-LABS/better-write-opencode
cp better-write-opencode/write-edit-schema-fix.ts ~/.config/opencode/plugins/Or per project, into .opencode/plugins/. Discovery uses the glob
{plugin,plugins}/*.{ts,js} (see packages/opencode/src/config/plugin.ts), so
no config entry is needed.
Restart OpenCode and confirm it loaded:
cat ~/.local/share/opencode/plugin-schema-fix.log # expect a "loaded" lineAn empty or missing file means the plugin is not being picked up — check the
directory name and that the file is .ts or .js.
Requires a build with the tool.definition and tool.execute.before hooks
(developed against @opencode-ai/plugin 1.18.x).
Working in real use. With the plugin installed, write succeeds — verified
first-hand against a live OpenCode session backed by a local OpenAI-compatible
model. The plugin logs a loaded line on every start, and the schema reorder is
demonstrably in effect: the successful call emitted filePath ahead of
content, which is the order this plugin installs rather than OpenCode's
default.
Covered by tests against the built plugin:
{cmd: "hostname"}→{command: "hostname"}{fileContent, path}→{content, filePath}- already-correct arguments left untouched
- empty
{}passed through without throwing writeschema reordering to[filePath, content]
One caveat worth repeating: if your tool inputs are arriving as {} rather than
mis-keyed, that is the context-overflow case, and you need the config fix above
as well — no plugin can rename keys that were never sent.
PR #30224 improves the error message, appending expected and received keys so the model can self-correct on a subsequent turn. This plugin instead repairs the call so the first attempt succeeds, and needs no rebuild of OpenCode. They are complementary.
One note if you are comparing them: #30224 patches
packages/llm/src/tool-runtime.ts, while the error text seen in the running app
(The write tool was called with invalid arguments: …) is produced by
InvalidArgumentsError in packages/opencode/src/tool/tool.ts — a different
package. Worth confirming which path your build takes.
MIT — see LICENSE.