Skip to content

fix(opencode): reject unknown prompt variants instead of recording them - #51918

Open
monody0007 wants to merge 4 commits into
anomalyco:devfrom
monody0007:reject-unknown-variant
Open

monody0007 wants to merge 4 commits into
anomalyco:devfrom
monody0007:reject-unknown-variant

Conversation

@monody0007

@monody0007 monody0007 commented Sep 28, 2026 •

Copy link
Copy Markdown

Issue for this PR

Closes #40182

Type of change

  • Bug fix

What does this PR do?

opencode run --variant <typo> currently succeeds without applying the variant, then records the unapplied value. This checks explicit variants before persistence and returns HTTP 400 with the model's valid values. The default sentinel remains supported; prototype names and an empty explicit value are rejected.

The same check applies to every caller that passes a variant, so session.command and the GitHub action (VARIANT input) now fail on an unknown variant instead of silently running without it.

A prompt's agent, model and variant are now resolved once, before a pending undo is cleaned up, and the user message is created from that same selection. A rejected prompt therefore keeps the undone messages and the revert marker instead of deleting them. For a session without its own model (a fork, for example), the model fallback skips the messages the pending undo will drop, so the check sees the same model the message is recorded with.

Two compatibility paths are covered: pinned-model commands discard only a valid variant inherited from a different model, and background results forward the parent's current model together with its variant.

This follows the closed #40250 by @C0d3N1nja97342. Related #46106 changes the fallback for prompts without an explicit model; this patch supplies the model explicitly.

How did you verify your code works?

  • Real prompt admission, background TaskTool execution, and source CLI subprocess tests with a local fake provider. Baseline failures and targeted mutations verify the checks.
  • Undo regressions for both prompt and command: with an explicit model, and in a fork without a session model where only the undone message is on a model that declares the variant. An unknown variant leaves the undone messages and the revert marker intact and restorable. Controls check that a valid prompt or command still replaces them and runs on the model left after the undo.
  • A background result reaches a parent whose agent pins a different model than the one the user switched to.
  • On the latest head: bun test test/session (428 pass, 7 skip, 1 todo), bun test test/tool/task.test.ts test/session/revert-compact.test.ts (31 pass) and bun typecheck pass. On the previous revision, whose production code is unchanged here: test/tool (342 pass), test/acp (139 pass), test/cli run serially (372 pass, 5 skip) and the prompt/session server tests.
  • With default concurrency, five run-process subprocess cases in test/cli time out; all pass when run serially.
  • No release-binary, live UI, paid-provider or cross-platform validation.

Implementation and independent review used AI assistance; these checks ran locally.

Screenshots / recordings

N/A, no UI change.

Checklist

  • Tested locally
  • No unrelated changes

monody0007 and others added 2 commits September 28, 2026 09:57
An explicit variant that the model does not declare was dropped when the
request was built, but still written to the user message and the session
model, so `opencode run --variant typo` exited 0 at the default effort
while the session claimed the typo. Validate explicit variants in
createUserMessage before anything is persisted ("default" stays a
passthrough sentinel).

Two internal callers relied on the silent drop and are fixed with it:
- commands that pin their own model no longer forward the caller's
  variant when the pinned model does not offer it
- background task results follow the parent session's current model
  selection instead of the spawn-time variant

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Follow-up to the unknown-variant rejection:

- membership uses the model's own declared keys, so "constructor",
  "toString", "__proto__" and an explicit empty string are rejected
  instead of passing a truthiness check ("default" is still the sentinel)
- a command that pins another model only drops a caller variant that the
  caller's model actually declares; anything else is rejected
- the rejection is a typed SessionPrompt.VariantNotFoundError that the
  prompt and command routes return as a 400 BadRequest with the message,
  so `opencode run` shows the valid variants instead of a generic 500
- background task results carry the parent's current model together with
  its variant, so an agent pinned to another model cannot reject them

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

The following comment was made by an LLM, it may be inaccurate:

prompt() runs SessionRevert.cleanup before building the user message. A
prompt rejected for an unknown variant therefore still deleted the undone
messages and cleared the revert marker, and nothing could restore them.

Resolve the agent, model and variant once, before cleanup, and create the
user message from that same selection, so a rejected prompt leaves the
session untouched and an accepted one is recorded with exactly what was
checked.

A session without its own model (a fork, for example) takes its model
from the newest user message. Before cleanup that could be an undone
message, so the check and the recorded message could see different
models. currentModel now skips the messages a pending revert will drop,
using the same cutoff as SessionRevert.cleanup, so every caller resolves
the model the session is left on.
A command without its own model resolves the caller's model before the
prompt drops undone messages. Check that it rejects a variant the model
left after the undo does not declare, keeps the undone messages and the
revert marker, and otherwise runs on that model with the variant.

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

--variant silently ignored when the value is not in the model's reasoning_options

1 participant