Skip to content

fix(backends): honor inherited model label + warn on engine/model incompatibility - #1533

Merged
aaight merged 1 commit into
devfrom
fix/mng-1772-agent-model-picker-fallback
Aug 5, 2026
Merged

fix(backends): honor inherited model label + warn on engine/model incompatibility#1533
aaight merged 1 commit into
devfrom
fix/mng-1772-agent-model-picker-fallback

Conversation

@aaight

@aaight aaight commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fixes MNG-1772. The agent detail panel's Model picker showed a fictional fallback (Default (Sonnet 5)) that the runtime never applies. The runtime chain (src/agents/shared/modelResolution.ts) is override → per-agent → project.model with no engine-default step, so an empty override actually inherits project.model — which defaults to openrouter:google/gemini-3-flash-preview and then fails the claude-code engine guard mid-run. This PR makes the UI honest and surfaces the incompatibility at configuration time.

What changed

1. Honor defaultLabel on the ModelField select branch (web/src/components/settings/model-field.tsx)

  • New exported pure helper resolveSelectEmptyLabel(defaultLabel, engineDefaultValueLabel)defaultLabel ?? engineDefaultValueLabel.
  • The empty (_none) option now renders the caller's inheritance-aware label (Inherit from project (X)) instead of the engine catalog's hardcoded default — matching the agents list. The _none → '' save mapping is unchanged.

2. Derive defaultValueLabel from the default-model constants (src/backends/catalog.ts, claude-code/models.ts, codex/models.ts)

  • New helper defaultModelLabel(models, defaultId)Default (${label ?? id}).
  • Replaced the hardcoded 'Default (Sonnet 5)' / 'Default (GPT-5.4)' literals with values derived from DEFAULT_CLAUDE_CODE_MODEL / DEFAULT_CODEX_MODEL + catalog lookup, killing the hand-sync drift class.

3. Normalize the project harness-form model default label (web/src/components/projects/project-harness-form.tsx)

  • Project-level Model field now passes Default (${defaults.model}) for a clear empty-option read.

4. Config-time engine/model incompatibility warning (src/backends/*, web/src/components/projects/*)

  • Single-sourced the runtime acceptance rules as CLAUDE_CODE_ACCEPTED_PREFIXES = ['claude-', 'anthropic:'] / CODEX_ACCEPTED_PREFIXES = ['openai:'] next to the model lists, consumed by resolveClaudeModel / resolveCodexModel (no behavior change).
  • Exposed them on the serializable engine definition as modelSelection.acceptedModelPrefixes?: readonly string[] (plain array — serializes across the agentConfigs.engines tRPC query).
  • New pure FE helper isModelCompatibleWithEngine(model, engine) (catalog-membership OR accepted-prefix match; free-text/unknown engines always compatible).
  • The agent-config detail panel computes the effective engine (agentEngine || inheritedEngine) + effective model (model || inheritedModel) and shows an inline amber AlertTriangle warning when a select-type engine would fail on the inherited model. Turns a post-dispatch crash into a config-time signal.

5. Tests

  • resolveSelectEmptyLabel, isModelCompatibleWithEngine, defaultModelLabel unit tests.
  • Anti-drift guard in tests/unit/backends/catalog.test.ts asserting displayed default == resolved default for every select engine — fails loudly on the next model bump.
  • Existing resolveClaudeModel / resolveCodexModel tests remain green (refactor is behavior-preserving).

6. Docsdocs/adding-engines.md modelSelection example now derives defaultValueLabel from the default-model constant and documents the new optional acceptedModelPrefixes.

Scope notes

  • Intentionally does not add an engine-default step to modelResolution.ts (that is the direction-4 product decision). This PR makes the UI honest about the existing chain.
  • The deeper PROJECT_DEFAULTS claude-code + openrouter default mismatch is flagged by the new warning rather than silently changed — a follow-up product call.
  • No DB / migration / runtime-dispatch changes: purely presentational plus a shared read-only predicate and a behavior-preserving refactor.

Testing

  • npm run typecheck (root) + tsc -b (web) — clean
  • biome check on changed files — clean
  • npx vitest run for catalog.test.ts, model-field.test.ts, agent-config-utils.test.ts, claude-code.test.ts, codex.test.ts, engine-contract, registry, resolution, all tests/unit/web/, and architecture-docs.test.ts — all passing

🤖 Generated with Claude Code

🕵️ claude-code · claude-opus-4-8 · run details

@codecov

codecov Bot commented Aug 5, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@nhopeatall nhopeatall left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

APPROVE — this makes the model picker honest about the actual override → per-agent → project.model resolution chain (no engine-default step) and adds a config-time engine/model incompatibility warning. All six MNG-1772 steps are implemented; the resolveClaudeModel / resolveCodexModel refactor is behavior-preserving; web + backend typechecks and the new unit/guard tests all pass.

What I verified

  • Behavior-preserving refactor — the CLAUDE_CODE_ACCEPTED_PREFIXES / CODEX_ACCEPTED_PREFIXES rewrite of the inline startsWith checks is a faithful translation (anthropic: still strips the prefix; openai: still requires a catalog-id bare match). 194 existing claude-code/codex tests stay green.
  • SerializationacceptedModelPrefixes is a plain (readonly) string[], so it survives the agentConfigs.engines tRPC query, and the FE isModelCompatibleWithEngine mirror reads it. engines is cast as Engine[] from the query data, and the runtime catalog populates the field.
  • No call-site regressions — the three ModelField sites are consistent: the global settings dialog (agent-config-form-dialog.tsx) passes no defaultLabel (unchanged engine-default fallback), while the detail panel and harness form pass inheritance-aware labels.
  • Anti-drift guardcatalog.test.ts asserts label is defined and defaultValueLabel === Default (<label>) for each select engine, so bumping DEFAULT_*_MODEL to a non-catalog id fails loudly.
  • Warning fires on the target case — claude-code inheriting openrouter:google/gemini-3-flash-preview correctly surfaces the amber warning; catalog / claude- / anthropic: models clear it.

Code Issues

Nitpick (non-blocking)

  • agent-config-detail.tsx:252 — the warning copy always reads "Inherited model X" even when X is an explicit per-agent override rather than an inherited value. This is reachable: with the engine left as "Inherit" (agentEngine=''), ModelField renders the free-text OpenRouter combobox, so a user can set an incompatible override; then effectiveModel = model (the override) but the copy still says "Inherited". Wording like "Model X will fail on the {engine} engine" stays accurate in both the inherited and explicit-override cases. Purely cosmetic — the signal itself is correct and actionable.

🕵️ claude-code · claude-opus-4-8 · run details

<p className="flex items-start gap-1.5 text-xs text-amber-600 dark:text-amber-400">
<AlertTriangle className="mt-0.5 h-3.5 w-3.5 shrink-0" aria-hidden="true" />
<span>
Inherited model <strong>{effectiveModel}</strong> will fail on the{' '}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking wording nit: this always says "Inherited model" even when effectiveModel is an explicit per-agent override. When the engine is left as "Inherit" (agentEngine=''), ModelField renders the free-text OpenRouter combobox (the select branch is skipped for an empty engine id), so a user can set an incompatible override — effectiveModel = model || inheritedModel resolves to the override, yet the copy calls it "Inherited". Consider "Model {effectiveModel} will fail on the {engine} engine" so it reads correctly in both the inherited and explicit-override cases.

@aaight
aaight merged commit ba2649f into dev Aug 5, 2026
9 checks passed
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.

2 participants