{"record":{"id":"51db042007667bd0","repo":"Hmbown/CodeWhale","slug":"custom-provider-name-must-not-shadow-a-built-in-provider","errorCode":null,"errorMessage":"custom provider name must not shadow a built-in provider","messagePattern":"custom provider name must not shadow a built-in provider","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/config_persistence.rs","lineNumber":696,"sourceCode":"            }\n        }\n        Ok(())\n    })?;\n    Ok(path)\n}\n\nfn normalize_custom_provider_id(raw: &str) -> anyhow::Result<String> {\n    use anyhow::bail;\n\n    let value = raw.trim();\n    if value.is_empty() {\n        bail!(\"custom provider name is required\");\n    }\n    if value == \"__custom__\" {\n        bail!(\"custom provider name is reserved\");\n    }\n    if crate::config::ApiProvider::parse(value).is_some() {\n        bail!(\"custom provider name must not shadow a built-in provider\");\n    }\n    if !value\n        .chars()\n        .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '_' | '-'))\n    {\n        bail!(\"custom provider name may only use letters, numbers, '-' and '_'\");\n    }\n    Ok(value.to_string())\n}\n\nfn normalize_custom_provider_base_url(raw: &str) -> anyhow::Result<String> {\n    use anyhow::bail;\n\n    let value = raw.trim().trim_end_matches('/');\n    if value.is_empty() {\n        bail!(\"custom provider base URL is required\");\n    }\n    let parsed = reqwest::Url::parse(value)","sourceCodeStart":678,"sourceCodeEnd":714,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/config_persistence.rs#L678-L714","documentation":"A custom provider name must not collide with a built-in provider. normalize_custom_provider_id checks crate::config::ApiProvider::parse and bails if the name resolves to a known built-in provider, preventing shadowing of built-in config keys.","triggerScenarios":"Calling persist_custom_provider with a name like \"openai\", \"anthropic\", or \"deepseek\" that parses as a built-in ApiProvider.","commonSituations":"A user names a custom gateway \"openai\" to point it at a proxy; the persistence layer refuses to create a shadowing table.","solutions":["Choose a distinct name not matching any built-in provider id (e.g. \"openai_proxy\").","Check the name against ApiProvider::parse in the UI before submitting."],"exampleFix":"// before\npersist_custom_provider(path, \"openai\", &url, ...)?;\n// after\npersist_custom_provider(path, \"openai_proxy\", &url, ...)?;","handlingStrategy":"validation","validationCode":"if crate::config::ApiProvider::parse(name.trim()).is_some() { return Err(anyhow::anyhow!(\"name shadows a built-in provider\")); }","typeGuard":"fn shadows_builtin(name: &str) -> bool { crate::config::ApiProvider::parse(name.trim()).is_some() }","tryCatchPattern":"match persist_custom_provider(path, name, &url, ...) {\n    Err(e) if e.to_string().contains(\"shadow\") => suggest_alternate_name(name),\n    other => other,\n}","preventionTips":["Suggest names with a suffix like _proxy or -custom when a built-in name is typed.","Check against ApiProvider::parse in form validation."],"tags":["validation","custom-provider","name-collision"],"backgroundTag":"invalid-identifier","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}