diff --git a/ROADMAP.md b/ROADMAP.md index 2e9af1c..3b70234 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -5934,3 +5934,29 @@ This creates a confusing gap: users build successfully but then get "command not **Blocker:** None. Pure documentation. **Source:** Clawhip nudge 2026-04-21 21:27 KST — onboarding gap from #claw-code observations earlier this month. + +## Pinpoint #154. Model syntax error doesn't hint at env var when multiple credentials present + +**Gap.** When a user types `claw --model gpt-4` but only has `ANTHROPIC_API_KEY` set (no `OPENAI_API_KEY`), the error is: +``` +error: invalid model syntax: 'gpt-4'. Expected provider/model (e.g., anthropic/claude-opus-4-6) or known alias (opus, sonnet, haiku) +``` + +But USAGE.md documents that "The error message now includes a hint that names the detected env var" — **this hint is not actually emitted.** The user gets a generic syntax error and has to re-read USAGE.md to discover they should type `openai/gpt-4` instead. + +**Expected behavior (from USAGE.md):** When the user has multiple providers' env vars set, or when a model name looks like it belongs to a different provider (e.g., `gpt-4` looks like OpenAI), the error should hint: +- "Did you mean `openai/gpt-4`? (but `OPENAI_API_KEY` is not set)" +- or "You have `ANTHROPIC_API_KEY` set but `gpt-4` looks like an OpenAI model. Try `openai/gpt-4` with `OPENAI_API_KEY` exported" + +**Current behavior:** Generic syntax error, user has to infer the fix from USAGE.md or guess. + +**Fix shape (~20 lines).** Enhance `FormatError::InvalidModelSyntax` or the model-parsing validation to: +1. Detect if the model name looks like it belongs to a known provider (prefix `gpt-`, `openai/`, `qwen`, etc.) +2. If it does, check if that provider's env var is missing +3. Append a hint: "Did you mean \`{inferred_prefix}/{model}\`? (requires `{PROVIDER_KEY}` env var)" + +**Acceptance:** `claw --model gpt-4` produces a hint about OpenAI prefix and missing `OPENAI_API_KEY`. Same for `qwen-plus` → hint about `DASHSCOPE_API_KEY`, etc. + +**Blocker:** None. Pure error-message UX improvement. + +**Source:** Clawhip nudge 2026-04-21 21:37 KST — discovered during dogfood probing of model validation. diff --git a/rust/crates/rusty-claude-cli/src/main.rs b/rust/crates/rusty-claude-cli/src/main.rs index 5262a1b..8500259 100644 --- a/rust/crates/rusty-claude-cli/src/main.rs +++ b/rust/crates/rusty-claude-cli/src/main.rs @@ -1401,10 +1401,27 @@ fn validate_model_syntax(model: &str) -> Result<(), String> { // Check provider/model format: provider_id/model_id let parts: Vec<&str> = trimmed.split('/').collect(); if parts.len() != 2 || parts[0].is_empty() || parts[1].is_empty() { - return Err(format!( + // #154: hint if the model looks like it belongs to a different provider + let mut err_msg = format!( "invalid model syntax: '{}'. Expected provider/model (e.g., anthropic/claude-opus-4-6) or known alias (opus, sonnet, haiku)", trimmed - )); + ); + if trimmed.starts_with("gpt-") || trimmed.starts_with("gpt_") { + err_msg.push_str("\nDid you mean `openai/"); + err_msg.push_str(trimmed); + err_msg.push_str("`? (Requires OPENAI_API_KEY env var)"); + } + else if trimmed.starts_with("qwen") { + err_msg.push_str("\nDid you mean `qwen/"); + err_msg.push_str(trimmed); + err_msg.push_str("`? (Requires DASHSCOPE_API_KEY env var)"); + } + else if trimmed.starts_with("grok") { + err_msg.push_str("\nDid you mean `xai/"); + err_msg.push_str(trimmed); + err_msg.push_str("`? (Requires XAI_API_KEY env var)"); + } + return Err(err_msg); } Ok(()) } @@ -10301,6 +10318,34 @@ mod tests { .expect_err("`doctor garbage` should fail without --json hint"); assert!(!err_other.contains("--output-format json"), "unrelated args should not trigger --json hint: {err_other}"); + // #154: model syntax error should hint at provider prefix when applicable + let err_gpt = parse_args(&["prompt".to_string(), "test".to_string(), "--model".to_string(), "gpt-4".to_string()]) + .expect_err("`--model gpt-4` should fail with OpenAI hint"); + assert!( + err_gpt.contains("Did you mean `openai/gpt-4`?"), + "GPT model error should hint openai/ prefix: {err_gpt}" + ); + assert!( + err_gpt.contains("OPENAI_API_KEY"), + "GPT model error should mention env var: {err_gpt}" + ); + let err_qwen = parse_args(&["prompt".to_string(), "test".to_string(), "--model".to_string(), "qwen-plus".to_string()]) + .expect_err("`--model qwen-plus` should fail with DashScope hint"); + assert!( + err_qwen.contains("Did you mean `qwen/qwen-plus`?"), + "Qwen model error should hint qwen/ prefix: {err_qwen}" + ); + assert!( + err_qwen.contains("DASHSCOPE_API_KEY"), + "Qwen model error should mention env var: {err_qwen}" + ); + // Unrelated invalid model should NOT get a hint + let err_garbage = parse_args(&["prompt".to_string(), "test".to_string(), "--model".to_string(), "asdfgh".to_string()]) + .expect_err("`--model asdfgh` should fail"); + assert!( + !err_garbage.contains("Did you mean"), + "Unrelated model errors should not get a hint: {err_garbage}" + ); } #[test]