{"record":{"id":"b1bec247dd347ab7","repo":"zeroclaw-labs/zeroclaw","slug":"acp-returned-unknown-optionid-option-id","errorCode":null,"errorMessage":"ACP returned unknown optionId: {option_id}","messagePattern":"ACP returned unknown optionId: (.+?)","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-channels/src/acp_channel.rs","lineNumber":117,"sourceCode":"\n        // Response shape: { outcome: { outcome: \"selected\", optionId: \"...\" } | { outcome: \"cancelled\" } }\n        let outcome = response.get(\"outcome\");\n        let kind = outcome\n            .and_then(|o| o.get(\"outcome\"))\n            .and_then(|s| s.as_str())\n            .unwrap_or(\"\");\n        match kind {\n            \"selected\" => {\n                let option_id = outcome\n                    .and_then(|o| o.get(\"optionId\"))\n                    .and_then(|s| s.as_str())\n                    .unwrap_or(\"\");\n                let idx = option_id\n                    .strip_prefix(\"choice-\")\n                    .and_then(|s| s.parse::<usize>().ok());\n                match idx.and_then(|i| choices.get(i)) {\n                    Some(text) => Ok(Some(text.clone())),\n                    None => anyhow::bail!(\"ACP returned unknown optionId: {option_id}\"),\n                }\n            }\n            \"cancelled\" => Ok(None),\n            other => anyhow::bail!(\"ACP returned unexpected outcome: {other}\"),\n        }\n    }\n\n    /// Form-mode elicitation path — issues `elicitation/create` with a\n    /// single-select schema. Used when the client advertises\n    /// `clientCapabilities.elicitation.form`.\n    async fn request_choice_via_elicitation(\n        &self,\n        question: &str,\n        choices: &[String],\n        timeout: Duration,\n    ) -> anyhow::Result<Option<String>> {\n        let req = ElicitationRequest {\n            session_id: self.session_id.clone(),","sourceCodeStart":99,"sourceCodeEnd":135,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-channels/src/acp_channel.rs#L99-L135","documentation":"OpenAiCompatibleBuilder::build() requires display_name(), base_url(), and auth_style() — the doc comment states all three carry no sensible default and every real call site sets them. This expect fires when the display name (name field) was not set before build(). It is reached from make_model_provider and the streaming/test paths listed under RAISED IN.","triggerScenarios":"Calling build() without .display_name(...) — typically a generic OpenAI-compatible provider config entry added without a display label, so the mapping to the builder skips the name setter.","commonSituations":"A user adds an openai-compatible provider (Ollama, vLLM, OpenRouter-style) and omits the display-name key; a config schema change renames the field so the mapping silently drops it; new code constructs the builder for tests and forgets one of the three required setters.","solutions":["Set display_name() (along with base_url() and auth_style()) before build().","Validate compatible-provider config entries at load time for all three required keys and fail with a message naming the provider.","Restructure the builder so the three required values are constructor parameters, leaving Option setters for optional behavior.","Run the builder-related tests (build_native_tool_chat_request_*, capable_endpoint_*) after changes."],"exampleFix":"// before\nlet provider = OpenAiCompatibleModelProvider::builder()\n    .base_url(base_url)\n    .auth_style(auth_style)\n    .build(); // panics: display_name() is required\n\n// after\nlet provider = OpenAiCompatibleModelProvider::builder()\n    .display_name(\"my-ollama\")\n    .base_url(base_url)\n    .auth_style(auth_style)\n    .build();","handlingStrategy":"validation","validationCode":"// Validate all three required keys for compatible providers before build():\nfn compatible_entry_ok(cfg: &serde_json::Value) -> Result<(), String> {\n    for key in [\"display_name\", \"base_url\", \"auth_style\"] {\n        if cfg.get(key).and_then(|v| v.as_str()).map_or(true, |s| s.is_empty()) {\n            return Err(format!(\"openai-compatible provider: `{key}` missing\"));\n        }\n    }\n    Ok(())\n}","typeGuard":"fn compatible_provider_complete(cfg: &serde_json::Value) -> bool {\n    [\"display_name\", \"base_url\", \"auth_style\"].iter().all(|k| {\n        cfg.get(*k).and_then(|v| v.as_str()).map_or(false, |s| !s.trim().is_empty())\n    })\n}","tryCatchPattern":"let p = std::panic::catch_unwind(|| {\n    OpenAiCompatibleModelProvider::builder()\n        .display_name(name.clone())\n        .base_url(url.clone())\n        .auth_style(style)\n        .build()\n});\nmatch p {\n    Ok(provider) => provider,\n    Err(_) => { /* report which of the three required setters was skipped */ }\n}","preventionTips":["Require a human-readable display name in provider config templates and examples","Validate the three required keys at config load with per-key messages","Model required builder fields as constructor parameters in your own APIs","Run the builder regression tests after any provider-config refactor"],"tags":["rust","builder-pattern","openai-compatible","config-validation","panic"],"backgroundTag":"builder-missing-required-field","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}