feat(cli): reject model overrides the agent's CLI won't accept - #695
Merged
Conversation
`ty create --model` accepted any string: IsValidModel returned true unconditionally, on the theory that the Claude CLI validates the name itself. It does — but inside tmux, after the task has launched, where nobody sees it. The task just sits there looking busy. The "claude" executor slug an early default baked into every row (see the clear-model repair in sqlite.go) is that failure mode. Validate the override up front instead. A model passes when it is one of the executor's aliases (opus/sonnet/haiku/fable/opusplan, with an optional [1m] variant) or a well-formed ID carrying the executor's vendor prefix, so claude-opus-5 and grok-4-fast work without ty having to track every release. Typos, other vendors' models, and the executor slug are rejected with a message naming the alternatives. Executors with no --model flag at all (codex, gemini, pi, opencode, openclaw) now say so rather than silently dropping the override. Two things stay unchecked on purpose: a task routed at a proxy — a CLAUDE_CONFIG_DIR override or ANTHROPIC_BASE_URL, the ollama shape — names the proxy's models (glm-5.2:cloud), which ty cannot know; and the TUI form, whose picker only offers valid values anyway. Workflow YAML gets the same check at parse time, with the same proxy escape hatch, since a bad model there stalls a step just as quietly. Shell completion now offers the chosen executor's models. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The CLI check left the HTTP path open: PATCH /api/tasks/{id} wrote
whatever `model` it was handed, so the GUI (and any client) could
still park a task on a model the agent's CLI rejects.
Rather than repeat the CLI's escape-hatch logic, give both paths one
entry point: db.ValidateTaskModel resolves the proxy hatch from the
task's own config dir and env, its project's config dir, and an
ambient ANTHROPIC_BASE_URL, then defers to ValidateModel. `ty create`
now builds a probe task and calls it, dropping its local helper.
The API validates only when the request is actually setting a model,
and against the executor as it stands after the update — an edit that
touches the title must not be blocked by a model stored before this
check existed.
MCP needs nothing: taskyou_create_task takes neither model nor
executor, so there is no unvalidated write there.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
ty create --model <anything>was accepted.db.IsValidModelwas literallyreturn true, on the documented theory that the Claude CLI validates the model name itself.It does — but inside tmux, after the task has launched, where nobody is looking. The task moves to running, claude exits on the bad flag, and the card sits there looking busy. The
claudeexecutor slug that an early default baked into every row (the one-time repair insqlite.go) is exactly this failure.What changed
Validation up front, in
internal/db/models.go:opus,sonnet,haiku,fable,opusplan, with an optional[1m]variant suffix.claude-opus-5,claude-haiku-4-5-20251001andgrok-4-fastpass without ty tracking every release. A model that ships next month works with no code change.opuss), other vendors (gpt-5on claude,opuson grok), and the executor slug (claude). The error names the alternatives.--modelflag — codex, gemini, pi, opencode, openclaw never pass the flag to their CLI, so an override there is dead config. They now say so instead of dropping it silently.Every write path that takes a model now goes through one entry point,
db.ValidateTaskModel:ty create --modelPATCH /api/tasks/{id}(GUI and any HTTP client)ty pipeline)step "Code": unknown claude model …taskyou_create_task(MCP)The API validates only when the request is actually setting a model, and against the executor as it stands after the update, so an edit that only touches the title is never blocked by a model stored before this check existed.
What stays unchecked, deliberately
CLAUDE_CONFIG_DIRoverride orANTHROPIC_BASE_URL(the ollama shape, already exercised inpipeline_test.gowithglm-5.2:cloud) means the model names are the proxy's, which ty cannot know.ValidateTaskModelresolves that hatch from the task's own config dir and env, its project's config dir, and an ambientANTHROPIC_BASE_URL— a workflow step'sconfig_dir/envdoes the same at parse time.CreateTask. The check lives at the write boundaries, not inCreateTask, so the form (whose picker only offers valid values) and any proxy-routed caller are unaffected.IsValidModelsurvives as the looser executor-less form the form's remembered per-project default uses — it now rejects theclaudeslug instead of waving it through.Files
internal/db/models.go(new) — constants, per-executor registry,ValidateModel,ValidateTaskModel,ModelBackendIsCustom; the model block moves here out oftasks.go.cmd/task/main.go— validate inty create, executor-aware shell completion.internal/web/handlers.go— validate inhandleUpdateTask.internal/pipeline/definition_file.go— validate each step's model at parse time.internal/db/models_test.go(new),internal/web/model_validation_test.go(new),internal/pipeline/custom_test.go,cmd/task/cli_test.go.Verification
go vet ./...clean; the fullgo test ./...passes package-by-package (the suite OOMs when every package builds at once on this box — unrelated to this change;-p 1works). Smoke-tested against a real binary: every rejection above exits 1, andopus/claude-opus-5/claude-opus-9/grok-4/ no-override /ANTHROPIC_BASE_URL=… --model glm-5.2:cloudall create.Maintenance note
New Claude Code aliases still need a line in
executorModelAliases— that's howfablearrived alongside opus/sonnet/haiku. New full model IDs need nothing.🤖 Generated with Claude Code