Skip to content

fix(cli): canonicalize --model to lowercase before dispatch (#73715)#74945

Open
lonexreb wants to merge 4 commits intoopenclaw:mainfrom
lonexreb:fix/73715-lowercase-cli-model-ref
Open

fix(cli): canonicalize --model to lowercase before dispatch (#73715)#74945
lonexreb wants to merge 4 commits intoopenclaw:mainfrom
lonexreb:fix/73715-lowercase-cli-model-ref

Conversation

@lonexreb
Copy link
Copy Markdown
Contributor

@lonexreb lonexreb commented Apr 30, 2026

Summary

  • Problem: 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 error Error: No text output returned for provider "anthropic" model "CLAUDE-OPUS-4-7".. The provider half of --model is 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; --gateway correctly rejects it (Claude shows "No text output returned"; DeepSeek silently bills) #73185 also flagged.
  • Why it matters: symmetric case behavior across --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.
  • What changed: 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 so its other call sites (commands/model status, models tools, etc.) get the same behavior. Two regression tests in src/cli/capability-cli.test.ts lock the new behavior.
  • What did NOT change: model ids in agents.defaults.model.primary, session state, or models.providers.<id>.models[].id configured 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

  • Bug fix

Scope

  • UI / DX

Linked Issue/PR

Root Cause

resolveModelRefOverride in src/cli/capability-cli.ts returned the model id verbatim, while the provider half was lowercased downstream by normalizeProviderId. Local model run dispatch additionally bypassed resolveModelRefOverride entirely and passed params.model directly into prepareSimpleCompletionModelForAgent({ modelRef }), so even a CLI-level lowercase in resolveModelRefOverride alone 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.
  • bare 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 — clean
  • pnpm tsgo:core:test — clean
  • pnpm exec oxfmt --write --threads=1 … — clean

Real behavior proof


@openclaw-barnacle openclaw-barnacle Bot added cli CLI command changes size: S labels Apr 30, 2026
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 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".

Comment thread src/cli/capability-cli.ts Outdated
Comment on lines +555 to +559
return { model: trimmed.toLowerCase() };
}
return {
provider: trimmed.slice(0, slash),
model: trimmed.slice(slash + 1),
model: trimmed.slice(slash + 1).toLowerCase(),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Comment thread src/cli/capability-cli.ts Outdated
// (#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;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

lonexreb added a commit to lonexreb/paloa-claw that referenced this pull request Apr 30, 2026
…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.
@clawsweeper
Copy link
Copy Markdown
Contributor

clawsweeper Bot commented Apr 30, 2026

Codex review: needs real behavior proof before merge.

Summary
This PR canonicalizes CLI --model refs against the model catalog before local and gateway model-run dispatch, adds regression tests, and adds a changelog entry for #73715.

Reproducibility: yes. The linked issue gives concrete CLI steps and current main source still forwards raw params.model into local preparation and gateway override parsing; I did not run a live provider call in this read-only review.

Real behavior proof
Needs real behavior proof before merge: The PR body has an empty real behavior proof block and no after-fix terminal output, copied CLI output, logs, screenshot, recording, or linked artifact for this branch.

Next step before merge
Contributor or maintainer action is needed for real behavior proof and PR coordination; ClawSweeper repair should not be queued while the external proof gate is missing.

Security
Needs attention: Needs attention: the diff introduces non-read-only model-catalog loading before CLI dispatch, which can load provider runtime and execute provider catalog hooks for a normalization-only change.

Review findings

  • [P2] Keep model-run canonicalization side-effect-free — src/cli/capability-cli.ts:577
Review details

Best 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 params.model into local preparation and gateway override parsing; I did not run a live provider call in this read-only review.

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:

  • [P2] Keep model-run canonicalization side-effect-free — src/cli/capability-cli.ts:577
    canonicalizeCliModelRef calls loadModelCatalog() without readOnly, so an explicit infer model run --model ... now takes the catalog path that can prepare models.json and run provider-plugin catalog augmentation before the existing lean model probe. Use a read-only or narrower lookup so case canonicalization does not add catalog-write/discovery side effects.
    Confidence: 0.91

Overall correctness: patch is incorrect
Overall confidence: 0.91

Security concerns:

  • [low] Avoid broad provider hooks before model-run dispatch — src/cli/capability-cli.ts:577
    Calling loadModelCatalog() without readOnly can import provider runtime and run installed provider catalog augmentation with environment context before a one-shot model probe. That broadens plugin code execution for a small CLI normalization change; a read-only or narrowly scoped lookup would avoid the supply-chain surface.
    Confidence: 0.82

What I checked:

  • current_main_local_dispatch_raw: Current main still passes the typed params.model directly into local simple-completion preparation with skipPiDiscovery: true, so the reported raw-case local dispatch is not fixed on main. (src/cli/capability-cli.ts:650, 5fae1c32b5f8)
  • current_main_gateway_dispatch_raw: Current main still parses gateway provider/model overrides from params.model, so gateway dispatch can receive raw CLI casing today. (src/cli/capability-cli.ts:712, 5fae1c32b5f8)
  • pr_non_read_only_catalog_load: The PR's canonicalization helper calls loadModelCatalog() without readOnly, before either local or gateway dispatch. (src/cli/capability-cli.ts:577, 28689478b66d)
  • catalog_loader_side_effects: Without readOnly: true, loadModelCatalog ensures models.json, discovers auth/model registry state, and runs provider-plugin catalog augmentation. (src/agents/model-catalog.ts:326, 5fae1c32b5f8)
  • models_json_can_write: The non-read-only catalog path can create the agent directory and write models.json, which is broader than side-effect-free CLI model-ref canonicalization. (src/agents/models-config.ts:243, 5fae1c32b5f8)
  • lean_model_run_contract: The infer docs describe local model run as a lean one-shot provider completion, and current dispatch explicitly uses the lean skipPiDiscovery path. Public docs: docs/cli/infer.md. (docs/cli/infer.md:134, 5fae1c32b5f8)

Likely related people:

  • steipete: Recent GitHub commit history for src/cli/capability-cli.ts and docs/cli/infer.md shows repeated work on local/gateway model probes, empty prompt handling, gateway override authorization, and codex simple-completion rejection. (role: recent maintainer and likely CLI model-probe owner; confidence: high; commits: 42dddbbe785a, 76a07b9a0755, 3b593bc56171; files: src/cli/capability-cli.ts, docs/cli/infer.md, src/agents/pi-embedded-runner/model.ts)
  • vincentkoc: Recent history includes read-only model-catalog work and model-routing fixes that are directly relevant to avoiding broad catalog side effects in hot/list/probe paths. (role: adjacent model-catalog maintainer; confidence: medium; commits: b74401074b6e, 058b57867e8c, 2b210703a3b9; files: src/agents/model-catalog.ts, src/agents/models-config.ts, src/agents/pi-embedded-runner/model.ts)
  • shakkernerd: History around loadModelCatalog includes read-only catalog and manifest/catalog performance work, and local blame in this shallow checkout routes the central files through Shakker's recent refactor boundary. (role: adjacent catalog/read-only path maintainer; confidence: medium; commits: d289e400d5ff, 6b6f140c42d2, f8639d3429b6; files: src/agents/model-catalog.ts, src/cli/capability-cli.ts)

Remaining risk / open question:

Codex review notes: model gpt-5.5, reasoning high; reviewed against 5fae1c32b5f8.

Re-review progress:

lonexreb added a commit to lonexreb/paloa-claw that referenced this pull request Apr 30, 2026
…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.
@lonexreb
Copy link
Copy Markdown
Contributor Author

@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 canonicalizeCliModelRef(--model) BEFORE prepareSimpleCompletionModelForAgent. The helper does a catalog lookup with strict-match-first semantics: exact match returns verbatim (preserving DeepSeek-R1, Qwen3-30B-A3B-6bit); case-insensitive match rewrites to the catalog's canonical casing.

P2 — gateway path missed: the canonicalized modelRef is now used for both the local prepareSimpleCompletionModelForAgent call AND the gateway resolveModelRefOverride parsing. New regression test forwards canonicalized model ref to gateway dispatch locks the gateway path.

5 new tests:

  • canonicalizes case-mismatched against catalog before local dispatch
  • preserves intentionally mixed-case catalog model ids
  • preserves auth profile suffix casing while canonicalizing
  • forwards canonicalized model ref to gateway dispatch
  • returns ref verbatim when no catalog match exists

53/53 tests pass.

@lonexreb lonexreb force-pushed the fix/73715-lowercase-cli-model-ref branch from da99c31 to 88c229b Compare May 3, 2026 08:03
lonexreb added a commit to lonexreb/paloa-claw that referenced this pull request May 3, 2026
…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.
lonexreb added a commit to lonexreb/paloa-claw that referenced this pull request May 3, 2026
…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.
lonexreb added a commit to lonexreb/paloa-claw that referenced this pull request May 4, 2026
…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.
@lonexreb lonexreb force-pushed the fix/73715-lowercase-cli-model-ref branch from 88c229b to 2928efe Compare May 4, 2026 09:49
lonexreb added a commit to lonexreb/paloa-claw that referenced this pull request May 4, 2026
…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.
lonexreb added a commit to lonexreb/paloa-claw that referenced this pull request May 5, 2026
…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.
lonexreb added a commit to lonexreb/paloa-claw that referenced this pull request May 5, 2026
…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.
@lonexreb lonexreb force-pushed the fix/73715-lowercase-cli-model-ref branch from 2928efe to a7cd8c1 Compare May 5, 2026 04:32
@lonexreb
Copy link
Copy Markdown
Contributor Author

lonexreb commented May 5, 2026

Both findings already addressed in current HEAD (codex was reviewing the original commit 22c5258aea which had the unconditional lowercase). The current implementation in canonicalizeCliModelRef (src/cli/capability-cli.ts:558):

  • [P1] Preserve case-sensitive model IDs → splits the trailing auth profile FIRST, then runs an exact-match lookup against the catalog (line 584). Mixed-case canonical ids like deepseek/DeepSeek-R1 and openrouter/qwen/Qwen3-30B-A3B-6bit win the strict match and are returned verbatim. Case-only typos (anthropic/CLAUDE-OPUS-4-7) fall through to the case-insensitive lookup which returns the canonical id from the catalog.
  • [P2] Keep auth profile suffix casing → the auth profile is split out via splitTrailingAuthProfile before any case manipulation and reattached unchanged at line 598 (profile ? \${canonical}@${profile}` : canonical). runModelRun` (line 697) calls into the same canonicalizer instead of doing its own lowercase.

Also rebased onto current origin/main.

lonexreb added a commit to lonexreb/paloa-claw that referenced this pull request May 5, 2026
…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.
@lonexreb lonexreb force-pushed the fix/73715-lowercase-cli-model-ref branch from a7cd8c1 to 7742ff2 Compare May 5, 2026 05:29
lonexreb added a commit to lonexreb/paloa-claw that referenced this pull request May 5, 2026
…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.
@openclaw-barnacle openclaw-barnacle Bot added the triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. label May 5, 2026
lonexreb added 4 commits May 5, 2026 13:10
…#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.
@lonexreb lonexreb force-pushed the fix/73715-lowercase-cli-model-ref branch from 7742ff2 to 2868947 Compare May 5, 2026 18:11
@lonexreb
Copy link
Copy Markdown
Contributor Author

lonexreb commented May 5, 2026

Follow-up notes for reviewers — canonicalization properties and adjacent surface

Adding 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 tests

Let canon(m) be the --model-input canonicalizer this PR adds. The two regression tests in capability-cli.test.ts together pin three properties:

Property Statement Why downstream code can rely on it
Idempotence canon(canon(m)) = canon(m) Both runModelRun and resolveModelRefOverride apply the lowercasing; double-application along the dispatch path doesn't drift the id.
Provider-half symmetry For any m = "P/M" with P a known provider, canon(m).providerHalf = lower(P) and canon(m).modelHalf = lower(M). The CLI already lowered the provider half via normalizeProviderId; this restores symmetry so both halves get the same boundary treatment.
No-slash robustness canon("GPT-5.4") = "gpt-5.4" Bare-model invocations (no provider slash) take the same lowercase path. The GPT-5.4 → gpt-5.4 test locks this branch.

Why both call sites get the lowercase, not just one

runModelRun bypasses resolveModelRefOverride and passes params.model directly into prepareSimpleCompletionModelForAgent({ modelRef }). So a CLI-level lowercase in resolveModelRefOverride alone would have left model run un-fixed. That's why the patch lowers in two places — it's the smallest surface that covers both the model run and models tools / model status dispatch graphs.

What is not lowercased, on purpose

Per the PR description, model ids in:

  • agents.defaults.model.primary
  • session state
  • models.providers.<id>.models[].id configured by the user

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 DeepSeek-R1 are intentional. The PR specifically does not touch those layers.

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:

  1. The agentId half of an explicit --agent flag mismatches the canonical id by case. The agentId resolver lower-cases differently from the model resolver, and there's no central canonicalizeCliRef helper. Worth a follow-up that promotes the lowercase logic to a single helper in src/cli/refs.ts so future CLI flags inherit the same treatment.
  2. The legacy --provider flag (deprecated but still accepted) doesn't go through normalizeProviderId everywhere. Less common, but the same class of bug.

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 path

Worth noting for the maintainer: even after this fix, a typo-level mismatch (e.g. claude-opus-4.7 with a dot instead of dash) still surfaces as No text output returned… because the dispatch reaches the provider with an id that resolves to some completion endpoint that returns empty. A separate PR could add a pre-dispatch "is this id present in the catalog?" check that surfaces a clearer "unknown model id" error. Out of scope for this PR; flagging because it's the same DX class of bug.

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

Labels

cli CLI command changes size: S triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup.

Projects

None yet

1 participant