{"record":{"id":"055f66f9a82f35c3","repo":"aaif-goose/goose","slug":"apikey-cannot-be-empty","errorCode":null,"errorMessage":"apiKey cannot be empty","messagePattern":"apiKey cannot be empty","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/goose/src/config/declarative_providers.rs","lineNumber":177,"sourceCode":"    pub headers: Option<HashMap<String, String>>,\n    pub requires_auth: bool,\n    pub catalog_provider_id: Option<String>,\n    pub base_path: Option<String>,\n    pub preserves_thinking: Option<bool>,\n}\n\npub fn create_custom_provider(\n    params: CreateCustomProviderParams,\n) -> Result<DeclarativeProviderConfig> {\n    let id = generate_id(&params.display_name);\n    validate_provider_id(&id)?;\n\n    let api_key_env = if params.requires_auth {\n        let api_key = params\n            .api_key\n            .as_deref()\n            .filter(|api_key| !api_key.trim().is_empty())\n            .ok_or_else(|| anyhow::anyhow!(\"apiKey cannot be empty\"))?;\n        let api_key_name = generate_api_key_name(&id);\n        let config = Config::global();\n        config.set_secret(&api_key_name, &api_key)?;\n        api_key_name\n    } else {\n        String::new()\n    };\n\n    let model_infos: Vec<ModelInfo> = params\n        .models\n        .into_iter()\n        .map(|name| ModelInfo::new(name, 128000))\n        .collect();\n\n    let engine = ProviderEngine::from_str(&params.engine)?;\n    let preserves_thinking = params\n        .preserves_thinking\n        .unwrap_or_else(|| should_preserve_thinking_by_default(&engine));","sourceCodeStart":159,"sourceCodeEnd":195,"githubUrl":"https://github.com/aaif-goose/goose/blob/3810898a7447ec3299be72e223d3570a7aabf0ab/crates/goose/src/config/declarative_providers.rs#L159-L195","documentation":"create_custom_provider stores the API key as a secret named `<ID>_API_KEY` (generate_api_key_name) when requires_auth is true. Before storing, it requires a non-blank api_key (None or whitespace-only is rejected with this error) so the secret is never written empty; nothing is persisted on failure.","triggerScenarios":"Calling create_custom_provider with CreateCustomProviderParams { requires_auth: true, api_key: None } or api_key: Some(\"   \") — the value is filtered on trim().is_empty().","commonSituations":"UI or script creating an auth-required provider before the user pasted the key; passing an unset environment variable; whitespace paste from a clipboard.","solutions":["Supply a real api_key in the create params","If the endpoint needs no auth (local gateways like Ollama), create the provider with requires_auth: false"],"exampleFix":"// before\nlet params = CreateCustomProviderParams { requires_auth: true, api_key: None, .. };\n\n// after\nlet params = CreateCustomProviderParams {\n    requires_auth: true,\n    api_key: Some(std::env::var(\"ACME_API_KEY\")?),\n    ..\n};","handlingStrategy":"validation","validationCode":"let params = CreateCustomProviderParams { /* ... */ };\nif params.requires_auth {\n    let key = params.api_key.as_deref().unwrap_or(\"\").trim();\n    assert!(!key.is_empty(), \"an API key is required for auth-enabled providers\");\n}","typeGuard":"fn has_auth_inputs(params: &CreateCustomProviderParams) -> bool {\n    !params.requires_auth\n        || params.api_key.as_deref().is_some_and(|k| !k.trim().is_empty())\n}","tryCatchPattern":null,"preventionTips":["Collect and validate the API key in the UI before enabling requires_auth","Trim pasted keys and reject blanks client-side","Use requires_auth: false for local no-auth endpoints"],"tags":["providers","auth","secrets","custom-providers"],"backgroundTag":null,"analyzedSha":"3810898a7447ec3299be72e223d3570a7aabf0ab","analyzedAt":"2026-08-16T10:14:26.282Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}