zeroclaw-labs/zeroclaw · error · anyhow::Error

ACP returned unknown optionId: {option_id}

Error message

ACP returned unknown optionId: {option_id}

What it means

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.

Source

Thrown at crates/zeroclaw-channels/src/acp_channel.rs:117

        // Response shape: { outcome: { outcome: "selected", optionId: "..." } | { outcome: "cancelled" } }
        let outcome = response.get("outcome");
        let kind = outcome
            .and_then(|o| o.get("outcome"))
            .and_then(|s| s.as_str())
            .unwrap_or("");
        match kind {
            "selected" => {
                let option_id = outcome
                    .and_then(|o| o.get("optionId"))
                    .and_then(|s| s.as_str())
                    .unwrap_or("");
                let idx = option_id
                    .strip_prefix("choice-")
                    .and_then(|s| s.parse::<usize>().ok());
                match idx.and_then(|i| choices.get(i)) {
                    Some(text) => Ok(Some(text.clone())),
                    None => anyhow::bail!("ACP returned unknown optionId: {option_id}"),
                }
            }
            "cancelled" => Ok(None),
            other => anyhow::bail!("ACP returned unexpected outcome: {other}"),
        }
    }

    /// Form-mode elicitation path — issues `elicitation/create` with a
    /// single-select schema. Used when the client advertises
    /// `clientCapabilities.elicitation.form`.
    async fn request_choice_via_elicitation(
        &self,
        question: &str,
        choices: &[String],
        timeout: Duration,
    ) -> anyhow::Result<Option<String>> {
        let req = ElicitationRequest {
            session_id: self.session_id.clone(),

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Set display_name() (along with base_url() and auth_style()) before build().
  2. Validate compatible-provider config entries at load time for all three required keys and fail with a message naming the provider.
  3. Restructure the builder so the three required values are constructor parameters, leaving Option setters for optional behavior.
  4. Run the builder-related tests (build_native_tool_chat_request_*, capable_endpoint_*) after changes.

Example fix

// before
let provider = OpenAiCompatibleModelProvider::builder()
    .base_url(base_url)
    .auth_style(auth_style)
    .build(); // panics: display_name() is required

// after
let provider = OpenAiCompatibleModelProvider::builder()
    .display_name("my-ollama")
    .base_url(base_url)
    .auth_style(auth_style)
    .build();
Defensive patterns

Strategy: validation

Validate before calling

// Validate all three required keys for compatible providers before build():
fn compatible_entry_ok(cfg: &serde_json::Value) -> Result<(), String> {
    for key in ["display_name", "base_url", "auth_style"] {
        if cfg.get(key).and_then(|v| v.as_str()).map_or(true, |s| s.is_empty()) {
            return Err(format!("openai-compatible provider: `{key}` missing"));
        }
    }
    Ok(())
}

Type guard

fn compatible_provider_complete(cfg: &serde_json::Value) -> bool {
    ["display_name", "base_url", "auth_style"].iter().all(|k| {
        cfg.get(*k).and_then(|v| v.as_str()).map_or(false, |s| !s.trim().is_empty())
    })
}

Try / catch

let p = std::panic::catch_unwind(|| {
    OpenAiCompatibleModelProvider::builder()
        .display_name(name.clone())
        .base_url(url.clone())
        .auth_style(style)
        .build()
});
match p {
    Ok(provider) => provider,
    Err(_) => { /* report which of the three required setters was skipped */ }
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/b1bec247dd347ab7. Report an issue: GitHub.