fix(cli): canonicalize --model to lowercase before dispatch (#73715)#74945
fix(cli): canonicalize --model to lowercase before dispatch (#73715)#74945lonexreb wants to merge 4 commits intoopenclaw:mainfrom
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d43dc0ab56
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| return { model: trimmed.toLowerCase() }; | ||
| } | ||
| return { | ||
| provider: trimmed.slice(0, slash), | ||
| model: trimmed.slice(slash + 1), | ||
| model: trimmed.slice(slash + 1).toLowerCase(), |
There was a problem hiding this comment.
Preserve case-sensitive model IDs in override parsing
Lowercasing the parsed model segment here changes user-supplied IDs before dispatch, which breaks providers/catalog entries whose model IDs intentionally include uppercase characters (for example mixed-case refs like .../DeepSeek-R1 and .../Qwen3-30B-A3B-6bit are preserved by the shared model-ref parser contract). With this change, --model overrides in CLI paths using resolveModelRefOverride can silently target a different/nonexistent model and fail at runtime.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Addressed in b86ff1b. Reverted the model-segment lowercasing in resolveModelRefOverride so mixed-case canonical ids (e.g. deepseek/DeepSeek-R1, openrouter/qwen/Qwen3-30B-A3B-6bit) resolve on the strict path and are never modified. The fix moved to a strict-first + lowercase-fallback approach in runModelRun local path: only retry with a lowercased model id if the first prepareSimpleCompletionModelForAgent call returned an error. New regression test does not retry when strict resolution succeeds for mixed-case model ids locks this behavior.
| // (#73715). Canonical model ids in OpenClaw's catalog are lowercase, and | ||
| // auth profile suffixes are also lowercase by convention, so a whole-string | ||
| // lower() is safe. | ||
| const modelRef = params.model?.trim().toLowerCase() || undefined; |
There was a problem hiding this comment.
Keep auth profile suffix casing in model-run refs
runModelRun now lowercases the entire --model string, which also lowercases trailing auth profile suffixes (for example @Work -> @work). Profile IDs are stored and resolved by exact key, so users with non-lowercase profile names can no longer resolve credentials when using --model <ref>@<profile> and will get profile-not-found auth errors even though the profile exists.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Addressed in b86ff1b. Removed the whole-string lowercasing. The fallback retry now uses a new lowercaseModelIdSegment helper that splits via splitTrailingAuthProfile and lowercases only the model-id segment, leaving @<profile> suffixes verbatim. So anthropic/CLAUDE-OPUS-4-7@Work retries as anthropic/claude-opus-4-7@Work (@Work preserved). New regression test preserves auth profile suffix casing when retrying with lowercased model id covers this.
…enclaw#73715) Address Codex P1 + P2 review findings on PR openclaw#74945. P1: lowercasing the model segment in resolveModelRefOverride breaks configs that intentionally use mixed-case canonical model ids (for example deepseek/DeepSeek-R1, openrouter/qwen/Qwen3-30B-A3B-6bit). P2: whole-string lowercasing in runModelRun also lowercases the trailing auth profile suffix (e.g. @work -> @work). Profile keys are resolved by exact match, so non-lowercase profile names stop resolving credentials. New approach (strict-first, lowercase fallback): - resolveModelRefOverride preserves the model id verbatim. - runModelRun trims --model but does not pre-normalize case. - On the local-transport path, if prepareSimpleCompletionModelForAgent returns an error, retry once with only the model-id segment lowercased (auth profile suffix preserved). The lowered candidate is computed via splitTrailingAuthProfile so 'anthropic/CLAUDE-OPUS-4-7@Work' becomes 'anthropic/claude-opus-4-7@Work'. Mixed-case canonical ids resolve on the first attempt and never enter the fallback path. Auth profile suffixes are never lowercased. Tests: 3 regression cases in capability-cli.test.ts: - retries with lowercased model id when strict resolution fails - does not retry when strict resolution succeeds for mixed-case ids - preserves auth profile suffix casing during retry 51/51 tests pass; tsgo:core and tsgo:core:test clean.
|
Codex review: needs real behavior proof before merge. Summary Reproducibility: yes. The linked issue gives concrete CLI steps and current main source still forwards raw Real behavior proof Next step before merge Security Review findings
Review detailsBest possible solution: Use a side-effect-free or read-only catalog lookup shared by local and gateway dispatch, preserve canonical mixed-case model IDs and auth profile suffixes, then land one coordinated fix with real CLI output proof. Do we have a high-confidence way to reproduce the issue? Yes. The linked issue gives concrete CLI steps and current main source still forwards raw Is this the best way to solve the issue? No, not as-is. Catalog-backed canonicalization is the right direction, but this PR should use a read-only or narrower lookup and still needs after-fix real behavior proof before merge. Full review comments:
Overall correctness: patch is incorrect Security concerns:
What I checked:
Likely related people:
Remaining risk / open question:
Codex review notes: model gpt-5.5, reasoning high; reviewed against 5fae1c32b5f8. Re-review progress:
|
…law#73715) Address clawsweeper P1 + P2 review findings on PR openclaw#74945. P1: the strict-first + lowercase-fallback retry only fires when prepareSimpleCompletionModelForAgent returns an error, but for the reproducer (anthropic/CLAUDE-OPUS-4-7) preparation can synthesize a generic configured-provider model with the typed mixed-case id, never entering the retry path. P2: the previous patch left the gateway transport unchanged, so '--gateway --model anthropic/CLAUDE-OPUS-4-7' still forwarded the case-mismatched id verbatim to ingress execution. New approach: canonicalize the user-supplied --model against the model catalog BEFORE dispatch, shared by both local and gateway transports. The lookup is case-insensitive but returns the catalog's canonical casing, so: - Strict matches (DeepSeek-R1, Qwen3-30B-A3B-6bit) are returned verbatim — intentionally mixed-case canonical ids survive. - Case-only mismatches (CLAUDE-OPUS-4-7) get rewritten to the canonical lowercase form (claude-opus-4-7) before any provider call. - Refs with no catalog match (custom configured models, dynamic plugin models) pass through unchanged so downstream resolution still owns the decision. - Auth profile suffixes (@<profile>) are split via splitTrailingAuthProfile, never modified, and reattached, so @work stays @work even when the model id is canonicalized. Tests: 5 regression cases in capability-cli.test.ts cover canonicalization on local dispatch, mixed-case preservation, auth-profile preservation, gateway transport coverage, and verbatim pass-through for unknown refs. 53/53 tests pass; tsgo:core and tsgo:core:test clean.
|
@codex review Addressed both clawsweeper findings in da99c31 by replacing the strict-first + retry pattern with a catalog-aware canonicalization step shared by both transports. P1 — local dispatch reaching provider with typed id: the previous fallback only ran on error, so configured-provider generic-model synthesis bypassed the fix. New code calls P2 — gateway path missed: the canonicalized 5 new tests:
53/53 tests pass. |
da99c31 to
88c229b
Compare
…enclaw#73715) Address Codex P1 + P2 review findings on PR openclaw#74945. P1: lowercasing the model segment in resolveModelRefOverride breaks configs that intentionally use mixed-case canonical model ids (for example deepseek/DeepSeek-R1, openrouter/qwen/Qwen3-30B-A3B-6bit). P2: whole-string lowercasing in runModelRun also lowercases the trailing auth profile suffix (e.g. @work -> @work). Profile keys are resolved by exact match, so non-lowercase profile names stop resolving credentials. New approach (strict-first, lowercase fallback): - resolveModelRefOverride preserves the model id verbatim. - runModelRun trims --model but does not pre-normalize case. - On the local-transport path, if prepareSimpleCompletionModelForAgent returns an error, retry once with only the model-id segment lowercased (auth profile suffix preserved). The lowered candidate is computed via splitTrailingAuthProfile so 'anthropic/CLAUDE-OPUS-4-7@Work' becomes 'anthropic/claude-opus-4-7@Work'. Mixed-case canonical ids resolve on the first attempt and never enter the fallback path. Auth profile suffixes are never lowercased. Tests: 3 regression cases in capability-cli.test.ts: - retries with lowercased model id when strict resolution fails - does not retry when strict resolution succeeds for mixed-case ids - preserves auth profile suffix casing during retry 51/51 tests pass; tsgo:core and tsgo:core:test clean.
…law#73715) Address clawsweeper P1 + P2 review findings on PR openclaw#74945. P1: the strict-first + lowercase-fallback retry only fires when prepareSimpleCompletionModelForAgent returns an error, but for the reproducer (anthropic/CLAUDE-OPUS-4-7) preparation can synthesize a generic configured-provider model with the typed mixed-case id, never entering the retry path. P2: the previous patch left the gateway transport unchanged, so '--gateway --model anthropic/CLAUDE-OPUS-4-7' still forwarded the case-mismatched id verbatim to ingress execution. New approach: canonicalize the user-supplied --model against the model catalog BEFORE dispatch, shared by both local and gateway transports. The lookup is case-insensitive but returns the catalog's canonical casing, so: - Strict matches (DeepSeek-R1, Qwen3-30B-A3B-6bit) are returned verbatim — intentionally mixed-case canonical ids survive. - Case-only mismatches (CLAUDE-OPUS-4-7) get rewritten to the canonical lowercase form (claude-opus-4-7) before any provider call. - Refs with no catalog match (custom configured models, dynamic plugin models) pass through unchanged so downstream resolution still owns the decision. - Auth profile suffixes (@<profile>) are split via splitTrailingAuthProfile, never modified, and reattached, so @work stays @work even when the model id is canonicalized. Tests: 5 regression cases in capability-cli.test.ts cover canonicalization on local dispatch, mixed-case preservation, auth-profile preservation, gateway transport coverage, and verbatim pass-through for unknown refs. 53/53 tests pass; tsgo:core and tsgo:core:test clean.
…enclaw#73715) Address Codex P1 + P2 review findings on PR openclaw#74945. P1: lowercasing the model segment in resolveModelRefOverride breaks configs that intentionally use mixed-case canonical model ids (for example deepseek/DeepSeek-R1, openrouter/qwen/Qwen3-30B-A3B-6bit). P2: whole-string lowercasing in runModelRun also lowercases the trailing auth profile suffix (e.g. @work -> @work). Profile keys are resolved by exact match, so non-lowercase profile names stop resolving credentials. New approach (strict-first, lowercase fallback): - resolveModelRefOverride preserves the model id verbatim. - runModelRun trims --model but does not pre-normalize case. - On the local-transport path, if prepareSimpleCompletionModelForAgent returns an error, retry once with only the model-id segment lowercased (auth profile suffix preserved). The lowered candidate is computed via splitTrailingAuthProfile so 'anthropic/CLAUDE-OPUS-4-7@Work' becomes 'anthropic/claude-opus-4-7@Work'. Mixed-case canonical ids resolve on the first attempt and never enter the fallback path. Auth profile suffixes are never lowercased. Tests: 3 regression cases in capability-cli.test.ts: - retries with lowercased model id when strict resolution fails - does not retry when strict resolution succeeds for mixed-case ids - preserves auth profile suffix casing during retry 51/51 tests pass; tsgo:core and tsgo:core:test clean.
88c229b to
2928efe
Compare
…law#73715) Address clawsweeper P1 + P2 review findings on PR openclaw#74945. P1: the strict-first + lowercase-fallback retry only fires when prepareSimpleCompletionModelForAgent returns an error, but for the reproducer (anthropic/CLAUDE-OPUS-4-7) preparation can synthesize a generic configured-provider model with the typed mixed-case id, never entering the retry path. P2: the previous patch left the gateway transport unchanged, so '--gateway --model anthropic/CLAUDE-OPUS-4-7' still forwarded the case-mismatched id verbatim to ingress execution. New approach: canonicalize the user-supplied --model against the model catalog BEFORE dispatch, shared by both local and gateway transports. The lookup is case-insensitive but returns the catalog's canonical casing, so: - Strict matches (DeepSeek-R1, Qwen3-30B-A3B-6bit) are returned verbatim — intentionally mixed-case canonical ids survive. - Case-only mismatches (CLAUDE-OPUS-4-7) get rewritten to the canonical lowercase form (claude-opus-4-7) before any provider call. - Refs with no catalog match (custom configured models, dynamic plugin models) pass through unchanged so downstream resolution still owns the decision. - Auth profile suffixes (@<profile>) are split via splitTrailingAuthProfile, never modified, and reattached, so @work stays @work even when the model id is canonicalized. Tests: 5 regression cases in capability-cli.test.ts cover canonicalization on local dispatch, mixed-case preservation, auth-profile preservation, gateway transport coverage, and verbatim pass-through for unknown refs. 53/53 tests pass; tsgo:core and tsgo:core:test clean.
…enclaw#73715) Address Codex P1 + P2 review findings on PR openclaw#74945. P1: lowercasing the model segment in resolveModelRefOverride breaks configs that intentionally use mixed-case canonical model ids (for example deepseek/DeepSeek-R1, openrouter/qwen/Qwen3-30B-A3B-6bit). P2: whole-string lowercasing in runModelRun also lowercases the trailing auth profile suffix (e.g. @work -> @work). Profile keys are resolved by exact match, so non-lowercase profile names stop resolving credentials. New approach (strict-first, lowercase fallback): - resolveModelRefOverride preserves the model id verbatim. - runModelRun trims --model but does not pre-normalize case. - On the local-transport path, if prepareSimpleCompletionModelForAgent returns an error, retry once with only the model-id segment lowercased (auth profile suffix preserved). The lowered candidate is computed via splitTrailingAuthProfile so 'anthropic/CLAUDE-OPUS-4-7@Work' becomes 'anthropic/claude-opus-4-7@Work'. Mixed-case canonical ids resolve on the first attempt and never enter the fallback path. Auth profile suffixes are never lowercased. Tests: 3 regression cases in capability-cli.test.ts: - retries with lowercased model id when strict resolution fails - does not retry when strict resolution succeeds for mixed-case ids - preserves auth profile suffix casing during retry 51/51 tests pass; tsgo:core and tsgo:core:test clean.
…law#73715) Address clawsweeper P1 + P2 review findings on PR openclaw#74945. P1: the strict-first + lowercase-fallback retry only fires when prepareSimpleCompletionModelForAgent returns an error, but for the reproducer (anthropic/CLAUDE-OPUS-4-7) preparation can synthesize a generic configured-provider model with the typed mixed-case id, never entering the retry path. P2: the previous patch left the gateway transport unchanged, so '--gateway --model anthropic/CLAUDE-OPUS-4-7' still forwarded the case-mismatched id verbatim to ingress execution. New approach: canonicalize the user-supplied --model against the model catalog BEFORE dispatch, shared by both local and gateway transports. The lookup is case-insensitive but returns the catalog's canonical casing, so: - Strict matches (DeepSeek-R1, Qwen3-30B-A3B-6bit) are returned verbatim — intentionally mixed-case canonical ids survive. - Case-only mismatches (CLAUDE-OPUS-4-7) get rewritten to the canonical lowercase form (claude-opus-4-7) before any provider call. - Refs with no catalog match (custom configured models, dynamic plugin models) pass through unchanged so downstream resolution still owns the decision. - Auth profile suffixes (@<profile>) are split via splitTrailingAuthProfile, never modified, and reattached, so @work stays @work even when the model id is canonicalized. Tests: 5 regression cases in capability-cli.test.ts cover canonicalization on local dispatch, mixed-case preservation, auth-profile preservation, gateway transport coverage, and verbatim pass-through for unknown refs. 53/53 tests pass; tsgo:core and tsgo:core:test clean.
2928efe to
a7cd8c1
Compare
|
Both findings already addressed in current HEAD (codex was reviewing the original commit
Also rebased onto current |
…enclaw#73715) Address Codex P1 + P2 review findings on PR openclaw#74945. P1: lowercasing the model segment in resolveModelRefOverride breaks configs that intentionally use mixed-case canonical model ids (for example deepseek/DeepSeek-R1, openrouter/qwen/Qwen3-30B-A3B-6bit). P2: whole-string lowercasing in runModelRun also lowercases the trailing auth profile suffix (e.g. @work -> @work). Profile keys are resolved by exact match, so non-lowercase profile names stop resolving credentials. New approach (strict-first, lowercase fallback): - resolveModelRefOverride preserves the model id verbatim. - runModelRun trims --model but does not pre-normalize case. - On the local-transport path, if prepareSimpleCompletionModelForAgent returns an error, retry once with only the model-id segment lowercased (auth profile suffix preserved). The lowered candidate is computed via splitTrailingAuthProfile so 'anthropic/CLAUDE-OPUS-4-7@Work' becomes 'anthropic/claude-opus-4-7@Work'. Mixed-case canonical ids resolve on the first attempt and never enter the fallback path. Auth profile suffixes are never lowercased. Tests: 3 regression cases in capability-cli.test.ts: - retries with lowercased model id when strict resolution fails - does not retry when strict resolution succeeds for mixed-case ids - preserves auth profile suffix casing during retry 51/51 tests pass; tsgo:core and tsgo:core:test clean.
a7cd8c1 to
7742ff2
Compare
…law#73715) Address clawsweeper P1 + P2 review findings on PR openclaw#74945. P1: the strict-first + lowercase-fallback retry only fires when prepareSimpleCompletionModelForAgent returns an error, but for the reproducer (anthropic/CLAUDE-OPUS-4-7) preparation can synthesize a generic configured-provider model with the typed mixed-case id, never entering the retry path. P2: the previous patch left the gateway transport unchanged, so '--gateway --model anthropic/CLAUDE-OPUS-4-7' still forwarded the case-mismatched id verbatim to ingress execution. New approach: canonicalize the user-supplied --model against the model catalog BEFORE dispatch, shared by both local and gateway transports. The lookup is case-insensitive but returns the catalog's canonical casing, so: - Strict matches (DeepSeek-R1, Qwen3-30B-A3B-6bit) are returned verbatim — intentionally mixed-case canonical ids survive. - Case-only mismatches (CLAUDE-OPUS-4-7) get rewritten to the canonical lowercase form (claude-opus-4-7) before any provider call. - Refs with no catalog match (custom configured models, dynamic plugin models) pass through unchanged so downstream resolution still owns the decision. - Auth profile suffixes (@<profile>) are split via splitTrailingAuthProfile, never modified, and reattached, so @work stays @work even when the model id is canonicalized. Tests: 5 regression cases in capability-cli.test.ts cover canonicalization on local dispatch, mixed-case preservation, auth-profile preservation, gateway transport coverage, and verbatim pass-through for unknown refs. 53/53 tests pass; tsgo:core and tsgo:core:test clean.
…#73715) `pnpm openclaw infer model run --model anthropic/CLAUDE-OPUS-4-7` accepted the case-mismatched id, dispatched it to the provider, and returned the misleading error 'No text output returned for provider "anthropic" model "CLAUDE-OPUS-4-7"'. Provider half is case-insensitive (normalizeProviderId lowercases it) but the model half was preserved as typed. Canonical model ids in OpenClaw's catalog are lowercase, and auth profile suffixes are also lowercase by convention. Lowercase the `--model` value once at the top of `runModelRun` so both the local and gateway transport paths see the canonical id, and lowercase the model half inside the shared `resolveModelRefOverride` helper to keep its other call sites consistent. Tests: 2 new regression cases in capability-cli.test.ts asserting the mocked `prepareSimpleCompletionModelForAgent` receives a lowercased `modelRef` for both `anthropic/CLAUDE-OPUS-4-7` and bare `GPT-5.4`. 50/50 tests pass. Closes openclaw#73715
…enclaw#73715) Address Codex P1 + P2 review findings on PR openclaw#74945. P1: lowercasing the model segment in resolveModelRefOverride breaks configs that intentionally use mixed-case canonical model ids (for example deepseek/DeepSeek-R1, openrouter/qwen/Qwen3-30B-A3B-6bit). P2: whole-string lowercasing in runModelRun also lowercases the trailing auth profile suffix (e.g. @work -> @work). Profile keys are resolved by exact match, so non-lowercase profile names stop resolving credentials. New approach (strict-first, lowercase fallback): - resolveModelRefOverride preserves the model id verbatim. - runModelRun trims --model but does not pre-normalize case. - On the local-transport path, if prepareSimpleCompletionModelForAgent returns an error, retry once with only the model-id segment lowercased (auth profile suffix preserved). The lowered candidate is computed via splitTrailingAuthProfile so 'anthropic/CLAUDE-OPUS-4-7@Work' becomes 'anthropic/claude-opus-4-7@Work'. Mixed-case canonical ids resolve on the first attempt and never enter the fallback path. Auth profile suffixes are never lowercased. Tests: 3 regression cases in capability-cli.test.ts: - retries with lowercased model id when strict resolution fails - does not retry when strict resolution succeeds for mixed-case ids - preserves auth profile suffix casing during retry 51/51 tests pass; tsgo:core and tsgo:core:test clean.
…law#73715) Address clawsweeper P1 + P2 review findings on PR openclaw#74945. P1: the strict-first + lowercase-fallback retry only fires when prepareSimpleCompletionModelForAgent returns an error, but for the reproducer (anthropic/CLAUDE-OPUS-4-7) preparation can synthesize a generic configured-provider model with the typed mixed-case id, never entering the retry path. P2: the previous patch left the gateway transport unchanged, so '--gateway --model anthropic/CLAUDE-OPUS-4-7' still forwarded the case-mismatched id verbatim to ingress execution. New approach: canonicalize the user-supplied --model against the model catalog BEFORE dispatch, shared by both local and gateway transports. The lookup is case-insensitive but returns the catalog's canonical casing, so: - Strict matches (DeepSeek-R1, Qwen3-30B-A3B-6bit) are returned verbatim — intentionally mixed-case canonical ids survive. - Case-only mismatches (CLAUDE-OPUS-4-7) get rewritten to the canonical lowercase form (claude-opus-4-7) before any provider call. - Refs with no catalog match (custom configured models, dynamic plugin models) pass through unchanged so downstream resolution still owns the decision. - Auth profile suffixes (@<profile>) are split via splitTrailingAuthProfile, never modified, and reattached, so @work stays @work even when the model id is canonicalized. Tests: 5 regression cases in capability-cli.test.ts cover canonicalization on local dispatch, mixed-case preservation, auth-profile preservation, gateway transport coverage, and verbatim pass-through for unknown refs. 53/53 tests pass; tsgo:core and tsgo:core:test clean.
7742ff2 to
2868947
Compare
Follow-up notes for reviewers — canonicalization properties and adjacent surfaceAdding a slightly deeper note on the canonicalization contract, since model-id normalization is one of those quietly load-bearing CLI seams where downstream consumers tend to assume idempotence without it being written down. Canonicalization properties pinned by the testsLet
Why both call sites get the lowercase, not just one
What is not lowercased, on purposePer the PR description, model ids in:
are intentionally untouched. The boundary is exactly the CLI input — anyone with a custom uppercase id in config keeps it. This matches the existing memory I'm carrying that auth-profile suffixes are case-sensitive and mixed-case canonical ids like Adjacent surface worth a follow-up (out of scope for this PR)The same misleading "No text output returned for provider X model Y" symptom can be reproduced when:
Would be happy to file either as a separate PR if reviewers want this one to stay strictly tied to #73715. Note on the misleading error pathWorth noting for the maintainer: even after this fix, a typo-level mismatch (e.g. |
Summary
pnpm openclaw infer model run --model anthropic/CLAUDE-OPUS-4-7 --prompt "Just say OK"accepted the case-mismatched model id, ran the provider call, and surfaced the misleading errorError: No text output returned for provider "anthropic" model "CLAUDE-OPUS-4-7".. The provider half of--modelis already case-insensitive (Anthropic/...,ANTHROPIC/...both succeed) but the model half was preserved as typed and dispatched to the provider, producing the confusing "No text output" symptom that [Bug]:infer model run --local --prompt ""reaches the provider;--gatewaycorrectly rejects it (Claude shows "No text output returned"; DeepSeek silently bills) #73185 also flagged.--model <provider>/<model>is the least-surprise rule, the user's report explicitly suggested this fix as one of two equally acceptable options, and the misleading error wastes debug time.--modelvalue once at the top ofrunModelRunso both the local and gateway transport paths see the canonical id, and lowercase the model half inside the sharedresolveModelRefOverridehelper so its other call sites (commands/model status,models tools, etc.) get the same behavior. Two regression tests insrc/cli/capability-cli.test.tslock the new behavior.agents.defaults.model.primary, session state, ormodels.providers.<id>.models[].idconfigured by the user are untouched. Anyone with a custom uppercase model id in their config keeps it as-is. The change only affects the CLI input boundary.Change Type
Scope
Linked Issue/PR
Root Cause
resolveModelRefOverrideinsrc/cli/capability-cli.tsreturned the model id verbatim, while the provider half was lowercased downstream bynormalizeProviderId. Localmodel rundispatch additionally bypassedresolveModelRefOverrideentirely and passedparams.modeldirectly intoprepareSimpleCompletionModelForAgent({ modelRef }), so even a CLI-level lowercase inresolveModelRefOverridealone wouldn't have covered that path. Fix lowercases at both points.Regression Test Plan
2 new tests in
src/cli/capability-cli.test.ts:anthropic/CLAUDE-OPUS-4-7→ modelRef"anthropic/claude-opus-4-7"— the exact reproducer in the issue.GPT-5.4→ modelRef"gpt-5.4"— covers the no-slash code path.Test Plan
pnpm test src/cli/capability-cli.test.ts— 50/50 pass (48 existing + 2 new)pnpm tsgo:core— cleanpnpm tsgo:core:test— cleanpnpm exec oxfmt --write --threads=1 …— cleanReal behavior proof