{"record":{"id":"80f77d1bd8947b0c","repo":"Hmbown/CodeWhale","slug":"invalid-default-text-model-model-for-provider","errorCode":null,"errorMessage":"Invalid default_text_model '{model}' for provider '{}': expected auto or a model ID this provider serves{hint}.","messagePattern":"Invalid default_text_model '(.+?)' for provider '(.+?)': expected auto or a model ID this provider serves(.+?)\\.","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/tui/src/config.rs","lineNumber":4410,"sourceCode":"        // (GLM via Z.ai, Kimi, MiniMax, …) and passes unknown ids through, so\n        // it rejects only what the provider genuinely cannot serve. Validating\n        // with the DeepSeek-only `normalize_model_name` bricked every config\n        // whose provider owns a non-DeepSeek family — including ones our own\n        // setup wizard writes (`provider = \"zai\"`, `GLM-5.2`). (#4829)\n        if let Some(model) = self.default_text_model.as_deref()\n            && !model.trim().eq_ignore_ascii_case(\"auto\")\n            && !provider_passes_model_through(self.api_provider())\n            && !self.active_provider_preserves_custom_base_url_model()\n            && canonical_model_id_for_provider(self.api_provider(), model).is_none()\n        {\n            let provider = self.api_provider();\n            let known = model_completion_names_for_provider(provider);\n            let hint = if known.is_empty() {\n                String::new()\n            } else {\n                format!(\" (for example: {})\", known.join(\", \"))\n            };\n            anyhow::bail!(\n                \"Invalid default_text_model '{model}' for provider '{}': expected auto or a model ID this provider serves{hint}.\",\n                provider.as_str()\n            );\n        }\n        if let Some(policy) = self.approval_policy.as_deref() {\n            let normalized = policy.trim().to_ascii_lowercase();\n            if !matches!(\n                normalized.as_str(),\n                \"on-request\" | \"untrusted\" | \"never\" | \"auto\" | \"suggest\"\n            ) {\n                anyhow::bail!(\n                    \"Invalid approval_policy '{policy}': expected on-request, untrusted, never, auto, or suggest.\"\n                );\n            }\n        }\n        if let Some(v) = self.verbosity.as_deref() {\n            let normalized = v.trim().to_ascii_lowercase();\n            if !matches!(normalized.as_str(), \"normal\" | \"concise\") {","sourceCodeStart":4392,"sourceCodeEnd":4428,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/8880682c63083a91624de936797efa3ce9e498fd/crates/tui/src/config.rs#L4392-L4428","documentation":"Thrown by Config::validate() (crates/tui/src/config.rs:4352) when default_text_model is neither 'auto' (case-insensitive, trimmed) nor a model ID the active provider serves. The check is skipped for providers that pass model IDs straight through or when a custom base URL preserves the model. When the provider has a known catalog, the message appends example model names via model_completion_names_for_provider().","triggerScenarios":"Setting default_text_model to another vendor's model ID (e.g. 'gpt-4o' while provider = 'deepseek'); switching the active provider without updating a stale model name; a typo such as 'deepseek-chatt'; canonical_model_id_for_provider() returning None for the pair.","commonSituations":"Copy-pasting config snippets written for a different provider; flipping providers with --provider while a shared ~/.codewhale/config.toml pins the old model; model catalog renames after an upgrade.","solutions":["Set default_text_model = \"auto\" and let the provider pick its default.","Replace the value with a model ID this provider serves (the message hint lists examples, e.g. deepseek-chat).","If the endpoint serves arbitrary model names, route through a custom provider entry or custom base_url so the passthrough/custom-base-url exemptions apply.","Re-run validation after editing to confirm the config loads."],"exampleFix":"# before (~/.codewhale/config.toml)\nprovider = \"deepseek\"\ndefault_text_model = \"gpt-4o\"\n\n# after\nprovider = \"deepseek\"\ndefault_text_model = \"auto\"   # or \"deepseek-chat\" / \"deepseek-reasoner\"","handlingStrategy":"validation","validationCode":"fn default_text_model_is_safe(provider: ApiProvider, model: &str) -> bool {\n    let m = model.trim();\n    m.eq_ignore_ascii_case(\"auto\")\n        || canonical_model_id_for_provider(provider, m).is_some()\n        // passthrough/custom-base-url routes exempt from the catalog check\n        || provider_passes_model_through(provider)\n}\n\n// run before startup:\nlet model = config.default_text_model.as_deref().unwrap_or(\"auto\");\nassert!(default_text_model_is_safe(config.api_provider(), model), \"fix default_text_model\");","typeGuard":"fn is_valid_default_text_model(provider: ApiProvider, model: &str) -> bool {\n    matches!(model.trim(), \"auto\" if model.trim().eq_ignore_ascii_case(\"auto\"))\n        || canonical_model_id_for_provider(provider, model.trim()).is_some()\n}","tryCatchPattern":"match config.validate() {\n    Ok(()) => {}\n    Err(e) if e.to_string().starts_with(\"Invalid default_text_model\") => {\n        // normalize to \"auto\" and retry validation before surfacing to the user\n        config.default_text_model = Some(\"auto\".into());\n        config.validate()?;\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["Keep default_text_model = \"auto\" in shared/team configs; pin IDs only in per-provider configs.","Run codewhale config validation in CI for every committed config.toml.","When switching providers, re-check the pinned model ID against the new provider's catalog."],"tags":["rust","config","toml","validation","model-id","provider"],"backgroundTag":null,"analyzedSha":"8880682c63083a91624de936797efa3ce9e498fd","analyzedAt":"2026-08-16T11:31:27.956Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}