{"record":{"id":"785e96d501ed48c5","repo":"zeroclaw-labs/zeroclaw","slug":"custom-model-provider-prefix-url-requires-a","errorCode":null,"errorMessage":"Custom model_provider `{prefix}:<url>` requires a URL beginning with http:// or https://. Set `[providers.models.custom.<alias>] uri = \"https://your-api.com\"` or pass a valid URL.","messagePattern":"Custom model_provider `(.+?):<url>` requires a URL beginning with http:// or https://\\. Set `\\[providers\\.models\\.custom\\.<alias>\\] uri = \"https://your-api\\.com\"` or pass a valid URL\\.","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-providers/src/lib.rs","lineNumber":1281,"sourceCode":"}\n\n/// Factory: create model_provider with optional base URL and runtime options.\n#[allow(clippy::too_many_lines)]\nfn create_model_provider_inner(\n    config: Option<&zeroclaw_config::schema::Config>,\n    raw_name: &str,\n    alias: &str,\n    api_key: Option<&str>,\n    api_url: Option<&str>,\n    options: &ModelProviderRuntimeOptions,\n) -> anyhow::Result<Box<dyn ModelProvider>> {\n    if let Some(idx) = raw_name.find(':') {\n        let prefix = &raw_name[..idx];\n        let url = raw_name[idx + 1..].trim();\n        if matches!(prefix, \"custom\" | \"anthropic-custom\")\n            && (url.is_empty() || !(url.starts_with(\"http://\") || url.starts_with(\"https://\")))\n        {\n            anyhow::bail!(\n                \"Custom model_provider `{prefix}:<url>` requires a URL beginning with http:// or https://. \\\n                 Set `[providers.models.custom.<alias>] uri = \\\"https://your-api.com\\\"` or pass a valid URL.\"\n            );\n        }\n    }\n    let (split_name, split_url) = split_v2_colon_url(raw_name);\n    let legacy_kimi_code = is_legacy_kimi_code_alias(split_name);\n    let api_url = api_url.or(split_url);\n    let name = canonicalize_v2_model_provider_name(split_name);\n    let provider_kind = options\n        .provider_kind\n        .as_deref()\n        .map(str::trim)\n        .filter(|value| !value.is_empty())\n        .map(canonicalize_v2_model_provider_name)\n        .unwrap_or(name);\n\n    // V2 spelled OpenAI Codex as `openai-codex` / `openai_codex` / `codex`.","sourceCodeStart":1263,"sourceCodeEnd":1299,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-providers/src/lib.rs#L1263-L1299","documentation":"create_model_provider_inner parses inline provider names of the form `custom:<url>` and `anthropic-custom:<url>`. When the part after the colon is empty (after trimming) or does not start with http:// or https://, construction fails immediately, before any dispatch or network activity. The message suggests both the inline form and the explicit profile form with a `uri` field.","triggerScenarios":"model_provider = \"custom:api.example.com/v1\" (missing scheme); \"custom:\" (empty URL); \"anthropic-custom:ftp://host\" (wrong scheme); a template concatenation leaving a trailing colon.","commonSituations":"Moving from the profile `uri` field to the inline name form and dropping the scheme; copy-paste that loses `https://`; environment-variable-built provider names with empty URL segments.","solutions":["Include the scheme: `custom:https://api.example.com/v1`","Prefer the explicit profile: `[providers.models.custom.<alias>]` with `uri = \"https://your-api.com\"`, then reference the alias","Use https:// unless the endpoint is a local plaintext proxy that genuinely requires http://"],"exampleFix":"# before\nmodel_provider = \"custom:my-gateway.internal/v1\"\n\n# after\nmodel_provider = \"custom:https://my-gateway.internal/v1\"","handlingStrategy":"validation","validationCode":"fn custom_provider_name_ok(raw: &str) -> bool {\n    match raw.split_once(':') {\n        Some((prefix, url)) if matches!(prefix, \"custom\" | \"anthropic-custom\") => {\n            let url = url.trim();\n            !url.is_empty() && (url.starts_with(\"http://\") || url.starts_with(\"https://\"))\n        }\n        _ => true,\n    }\n}","typeGuard":"fn is_valid_custom_url(raw: &str) -> Option<(&str, &str)> {\n    let (prefix, url) = raw.split_once(':')?;\n    let url = url.trim();\n    if matches!(prefix, \"custom\" | \"anthropic-custom\")\n        && (url.starts_with(\"http://\") || url.starts_with(\"https://\"))\n    { Some((prefix, url)) } else { None }\n}","tryCatchPattern":"if !custom_provider_name_ok(&model_provider) {\n    return Err(format!(\"invalid custom provider name `{model_provider}`; expected custom:https://...\"));\n}\ncreate_resilient_model_provider(&model_provider, api_key, api_url, &reliability).await","preventionTips":["Always include the scheme in inline custom provider names","Prefer the explicit profile form with a `uri` field for anything configured long-term","Validate provider names in config linting before deploy"],"tags":["custom-provider","url","scheme","validation"],"backgroundTag":"invalid-endpoint-url","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}