You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by wy...@apache.org on 2026/10/21 03:29:44 UTC

(maka) branch rust-preview updated: fix(providers): encode options for the resolved protocol

This is an automated email from the ASF dual-hosted git repository.

M4n5ter pushed a commit to branch rust-preview
in repository https://gitbox.apache.org/repos/asf/maka.git


The following commit(s) were added to refs/heads/rust-preview by this push:
     new 4829d2a150 fix(providers): encode options for the resolved protocol
4829d2a150 is described below

commit 4829d2a1506443ba4c96c3e9d37056d7949f9c2b
Author: Wang <m4...@gmail.com>
AuthorDate: Wed Oct 21 11:29:38 2026 +0800

    fix(providers): encode options for the resolved protocol
    
    Route thinking and parallel policy through the selected provider contract so aliases and auxiliary title/recap calls preserve declared choices. Retain safe local preparation reasons without exposing arbitrary provider error text.
    
    Generated-by: Codex
---
 plugins/agent/native/src/model/diagnostics.rs      | 115 ++++++-
 plugins/agent/native/src/owner/tests/recap.rs      | 116 +++++++
 plugins/agent/native/src/owner/tests/selection.rs  |   2 +
 plugins/agent/native/src/owner/tests/title.rs      | 119 +++++++
 .../agent/native/src/owner/tests/tool_schemas.rs   |  40 ++-
 plugins/agent/ports/src/provider.rs                |  44 ++-
 plugins/agent/providers/README.md                  |   2 +
 plugins/agent/providers/README.zh-CN.md            |   2 +
 plugins/agent/providers/src/api.rs                 |  12 +-
 plugins/agent/providers/src/api/options.rs         |  88 +++---
 .../agent/providers/src/api/options/compatible.rs  |   3 +-
 plugins/agent/providers/src/api/options/relay.rs   |   3 +-
 plugins/agent/providers/src/api/output.rs          |   4 +-
 plugins/agent/providers/src/api/route.rs           |  25 +-
 plugins/agent/providers/tests/api.rs               |  51 ++-
 plugins/agent/providers/tests/api/effects.rs       |  15 +-
 plugins/agent/providers/tests/api/policy.rs        | 345 +++++++++++++++++++++
 17 files changed, 880 insertions(+), 106 deletions(-)

diff --git a/plugins/agent/native/src/model/diagnostics.rs b/plugins/agent/native/src/model/diagnostics.rs
index 479a3b3935..49e6d504c2 100644
--- a/plugins/agent/native/src/model/diagnostics.rs
+++ b/plugins/agent/native/src/model/diagnostics.rs
@@ -29,15 +29,42 @@ pub(super) fn provider(
     connection_id: &str,
     provider: &str,
 ) {
-    let cause = match error {
+    let (cause, reason) = match error {
         ProviderError::Cancelled => return,
-        ProviderError::Unavailable => "provider_unavailable",
-        ProviderError::AuthenticationRequired => "provider_authentication_required",
-        ProviderError::Rejected(_) => "provider_rejected",
-        ProviderError::Invalid(_) => "provider_invalid",
-        ProviderError::OutcomeUnknown => "provider_outcome_unknown",
-        ProviderError::Transport(_) => "provider_transport",
-        ProviderError::Http(_) => "provider_http",
+        ProviderError::Unavailable => ("provider_unavailable", None),
+        ProviderError::AuthenticationRequired => ("provider_authentication_required", None),
+        ProviderError::Rejected(_) => ("provider_rejected", None),
+        ProviderError::Invalid(_) => ("provider_invalid", None),
+        ProviderError::OutcomeUnknown => ("provider_outcome_unknown", None),
+        ProviderError::Transport(_) => ("provider_transport", None),
+        ProviderError::Http(_) => ("provider_http", None),
+        ProviderError::InvalidConfiguration => ("provider_invalid", Some("invalid_configuration")),
+        ProviderError::InvalidModel => ("provider_invalid", Some("invalid_model")),
+        ProviderError::InvalidPreparedModel => ("provider_invalid", Some("invalid_prepared_model")),
+        ProviderError::SelectedModelChanged => ("provider_invalid", Some("selected_model_changed")),
+        ProviderError::UnsupportedThinkingLevel => {
+            ("provider_invalid", Some("unsupported_thinking_level"))
+        }
+        ProviderError::UnsupportedProtocol => ("provider_invalid", Some("unsupported_protocol")),
+        ProviderError::UnsupportedExecutionProfile => {
+            ("provider_invalid", Some("unsupported_execution_profile"))
+        }
+        ProviderError::InvalidEndpoint => ("provider_invalid", Some("invalid_endpoint")),
+        ProviderError::MissingResponsesContract => {
+            ("provider_invalid", Some("missing_responses_contract"))
+        }
+        ProviderError::UnsupportedParallelPolicy => {
+            ("provider_invalid", Some("unsupported_parallel_policy"))
+        }
+        ProviderError::InsufficientOutputBudget => {
+            ("provider_invalid", Some("insufficient_output_budget"))
+        }
+        ProviderError::ProviderFactsUnavailable => {
+            ("provider_invalid", Some("provider_facts_unavailable"))
+        }
+        ProviderError::UnsupportedProbeAuthentication => {
+            ("provider_invalid", Some("unsupported_probe_authentication"))
+        }
     };
     let status = match error {
         ProviderError::Http(status) => Some(*status),
@@ -46,6 +73,7 @@ pub(super) fn provider(
     tracing::warn!(
         stage,
         cause,
+        reason,
         connection_id,
         provider,
         status,
@@ -126,3 +154,74 @@ pub(super) fn network(stage: &'static str, error: &maka_network::Error, connecti
     };
     tracing::warn!(stage, cause, connection_id, "Model preparation failed");
 }
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn arbitrary_provider_error_strings_stay_private_at_diagnostic_boundary() {
+        use std::io::{Read, Seek};
+        let mut file = tempfile::tempfile().unwrap();
+        let subscriber = tracing_subscriber::fmt()
+            .with_writer(file.try_clone().unwrap())
+            .with_max_level(tracing::Level::WARN)
+            .with_ansi(false)
+            .without_time()
+            .finish();
+        let _guard = tracing::subscriber::set_default(subscriber);
+        for (stage, error) in [
+            (
+                "boundary_invalid",
+                ProviderError::Invalid("PRIVATE_INVALID_VALIDATION_SENTINEL".into()),
+            ),
+            (
+                "boundary_rejected",
+                ProviderError::Rejected("PRIVATE_REJECTED_REMOTE_SENTINEL".into()),
+            ),
+            (
+                "boundary_transport",
+                ProviderError::Transport("PRIVATE_TRANSPORT_URL_KEY_SENTINEL".into()),
+            ),
+            ("boundary_thinking", ProviderError::UnsupportedThinkingLevel),
+            (
+                "boundary_profile",
+                ProviderError::UnsupportedExecutionProfile,
+            ),
+        ] {
+            provider(stage, &error, "original-connection", "kimi-coding-plan");
+        }
+        file.rewind().unwrap();
+        let mut log = String::new();
+        file.read_to_string(&mut log).unwrap();
+        for (stage, cause) in [
+            ("boundary_invalid", "provider_invalid"),
+            ("boundary_rejected", "provider_rejected"),
+            ("boundary_transport", "provider_transport"),
+        ] {
+            let line = log.lines().find(|line| line.contains(stage)).unwrap();
+            assert!(line.contains(&format!("cause=\"{cause}\"")), "{line}");
+            assert!(line.contains("original-connection"));
+            assert!(line.contains("kimi-coding-plan"));
+            assert!(!line.contains("reason="));
+        }
+        for (stage, reason) in [
+            ("boundary_thinking", "unsupported_thinking_level"),
+            ("boundary_profile", "unsupported_execution_profile"),
+        ] {
+            let line = log.lines().find(|line| line.contains(stage)).unwrap();
+            assert!(line.contains("cause=\"provider_invalid\""), "{line}");
+            assert!(line.contains(&format!("reason=\"{reason}\"")), "{line}");
+        }
+        for secret in [
+            "PRIVATE_INVALID_VALIDATION_SENTINEL",
+            "PRIVATE_REJECTED_REMOTE_SENTINEL",
+            "PRIVATE_TRANSPORT_URL_KEY_SENTINEL",
+        ] {
+            assert!(
+                !log.contains(secret),
+                "arbitrary provider error payload escaped sanitization"
+            );
+        }
+    }
+}
diff --git a/plugins/agent/native/src/owner/tests/recap.rs b/plugins/agent/native/src/owner/tests/recap.rs
index 2a13718f63..59ddd0427a 100644
--- a/plugins/agent/native/src/owner/tests/recap.rs
+++ b/plugins/agent/native/src/owner/tests/recap.rs
@@ -53,6 +53,122 @@ async fn attempts(f: &Fixture, session: &str) -> Vec<maka_agent_facts::accountin
         .unwrap()
         .attempts
 }
+
+#[tokio::test]
+async fn kimi_recap_uses_low_and_retains_original_session_operation() {
+    tokio::time::timeout(Duration::from_secs(60), async {
+    let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
+    let f = Fixture::configured_provider(
+        &format!("http://{}/v1", listener.local_addr().unwrap()),
+        None,
+        None,
+        None,
+        |settings| settings.recaps = true,
+        "kimi-coding-plan",
+    )
+    .await;
+    selection::edit(&f, |changes| {
+        changes.model_overrides = Patch::Set(BTreeMap::from([(
+            "fixture-model".into(),
+            ModelOverride {
+                thinking_levels: Some(vec![
+                    ThinkingLevel::Low,
+                    ThinkingLevel::High,
+                    ThinkingLevel::Max,
+                ]),
+                ..Default::default()
+            },
+        )]));
+    })
+    .await;
+    let session = f.create("Kimi recap source").await;
+    let sent = f.send(&session).await.unwrap();
+    let (mut socket, _) = listener.accept().await.unwrap();
+    let main_request = tool_schemas::read_wire(&mut socket).await;
+    tool_schemas::anthropic_text_reply(&mut socket, "Tests passed; deployment is still pending.")
+        .await;
+    f.drained().await;
+    let operation = Uuid::new_v4();
+    let input = json!({"sessionId":session,"model":f.selection().await});
+    let generation = f.mutation(2, "generateRecap", input.clone(), operation);
+    tokio::pin!(generation);
+    // A preparation rejection completes the real mutation without sending HTTP.
+    // Collect either branch so baseline RED still settles every original resource.
+    let (result, recap_request) = tokio::select! {
+        result = &mut generation => (result, None),
+        accepted = listener.accept() => {
+            let (mut socket, _) = accepted.unwrap();
+            let body = tool_schemas::read_wire(&mut socket).await;
+            let pending = read(&f, &session).await;
+            tool_schemas::anthropic_text_reply(&mut socket, "Tests passed. Deployment is next.").await;
+            (generation.await, Some((body, pending)))
+        }
+    };
+    f.drained().await;
+    let cached = read(&f, &session).await;
+    let recovered = if result.is_ok() {
+        Some(f.mutation(2, "generateRecap", input, operation).await)
+    } else {
+        None
+    };
+    let by_id = match &result {
+        Ok(receipt) => Some(f.query("recap", json!({"sessionId":session,"operationId":receipt["operationId"]})).await.unwrap()),
+        Err(_) => None,
+    };
+    let physical = attempts(&f, &session).await;
+    let boundary = f
+        .store
+        .invocation_boundary(&session, sent["invocationId"].as_str().unwrap())
+        .await
+        .unwrap()
+        .unwrap()
+        .0;
+    f.app.shutdown().await.unwrap();
+    f.config.shutdown().await.unwrap();
+
+    assert!(main_request["output_config"]["effort"].is_null());
+    let result = result.expect("supported Kimi auxiliary Low must prepare and complete recap");
+    let (outgoing, pending) = recap_request.expect("recap must reach actual Anthropic HTTP");
+    assert_eq!(outgoing["output_config"]["effort"], "low");
+    assert_eq!(outgoing["max_tokens"], 1024);
+    assert!(
+        outgoing["system"]
+            .to_string()
+            .contains("The user is returning to this session")
+    );
+    assert!(
+        outgoing["messages"]
+            .to_string()
+            .contains("deployment is still pending")
+    );
+    assert_eq!(result["kind"], "ready");
+    assert_eq!(result["text"], "Tests passed. Deployment is next.");
+    assert_eq!(pending["kind"], "pending");
+    assert_eq!(result["operationId"], pending["operationId"]);
+    assert_eq!(by_id.unwrap(), result);
+    assert_eq!(cached, result);
+    assert_eq!(recovered.unwrap().unwrap(), result);
+    assert!(matches!(
+        boundary.state,
+        maka_event_log::turns::InvocationState::Ended {
+            outcome: maka_agent_facts::event::InvocationOutcome::Completed,
+            ..
+        }
+    ));
+    assert_eq!(physical.len(), 2);
+    let auxiliary = physical
+        .iter()
+        .find(|attempt| matches!(attempt.origin, Origin::Auxiliary { .. }))
+        .unwrap();
+    assert_eq!(auxiliary.session_id.as_deref(), Some(session.as_str()));
+    assert!(matches!(&auxiliary.origin, Origin::Auxiliary {
+        source: AuxiliarySource::ApplicationOperation { operation: original, .. }
+    } if *original == operation));
+    assert_eq!(auxiliary.outcome, Outcome::Success);
+    assert_eq!(auxiliary.usage.input_tokens, Some(1));
+    assert_eq!(auxiliary.usage.output_tokens, Some(2));
+    }).await.unwrap();
+}
 #[tokio::test]
 async fn recap_native_original_receipt_cached_access_accounting_and_restart() {
     tokio::time::timeout(Duration::from_secs(60), async {
diff --git a/plugins/agent/native/src/owner/tests/selection.rs b/plugins/agent/native/src/owner/tests/selection.rs
index b79525df7d..307f27a19d 100644
--- a/plugins/agent/native/src/owner/tests/selection.rs
+++ b/plugins/agent/native/src/owner/tests/selection.rs
@@ -784,6 +784,8 @@ async fn unsupported_thinking_logs_original_provider_cause_without_admitting_exe
             .expect(
                 "actual typed provider rejection must be logged before its public cause is erased",
             );
+        assert!(cause.contains("reason=\"unsupported_thinking_level\""),
+            "the original deterministic preparation reason must survive public sanitization: {cause}");
         assert!(
             cause.contains("session_id=") && cause.contains(&session),
             "warning must identify the existing session: {cause}"
diff --git a/plugins/agent/native/src/owner/tests/title.rs b/plugins/agent/native/src/owner/tests/title.rs
index 5a4445ec89..deebe4c007 100644
--- a/plugins/agent/native/src/owner/tests/title.rs
+++ b/plugins/agent/native/src/owner/tests/title.rs
@@ -74,6 +74,125 @@ async fn attempts(f: &Fixture, session: &str) -> Vec<maka_agent_facts::accountin
         .attempts
 }
 
+#[tokio::test]
+async fn kimi_automatic_title_uses_low_without_defaulting_main_thinking() {
+    tokio::time::timeout(Duration::from_secs(60), async {
+    let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
+    let f = Fixture::configured_provider(
+        &format!("http://{}/v1", listener.local_addr().unwrap()),
+        None,
+        None,
+        None,
+        |settings| settings.automatic_titles = true,
+        "kimi-coding-plan",
+    )
+    .await;
+    selection::edit(&f, |changes| {
+        changes.model_overrides = Patch::Set(BTreeMap::from([(
+            "fixture-model".into(),
+            ModelOverride {
+                thinking_levels: Some(vec![
+                    ThinkingLevel::Low,
+                    ThinkingLevel::High,
+                    ThinkingLevel::Max,
+                ]),
+                ..Default::default()
+            },
+        )]));
+    })
+    .await;
+    let session = automatic(&f).await;
+    let mut commits = f.store.subscribe_commits();
+    let sent = f.send(&session).await.unwrap();
+    let mut main_request = None;
+    let mut title_request = None;
+    // Baseline rejects auxiliary Low before HTTP. Observe that durable failure
+    // instead of waiting for a second request which cannot arrive.
+    loop {
+        commits.borrow_and_update();
+        let diagnostics = f
+            .query(
+                "diagnostics",
+                json!({"sessionId":session,"position":{"kind":"latest"}}),
+            )
+            .await
+            .unwrap();
+        let failed = diagnostics["entries"]
+            .as_array()
+            .unwrap()
+            .iter()
+            .any(|entry| entry["detail"]["kind"] == "session_title");
+        if main_request.is_some() && (title_request.is_some() || failed) {
+            break;
+        }
+        tokio::select! {
+            accepted = listener.accept() => {
+                let (mut socket, _) = accepted.unwrap();
+                let body = tool_schemas::read_wire(&mut socket).await;
+                if body["system"].to_string().contains("Generate a short session title") {
+                    tool_schemas::anthropic_text_reply(&mut socket, "Kimi auxiliary title").await;
+                    title_request = Some(body);
+                } else {
+                    tool_schemas::anthropic_text_reply(&mut socket, "Kimi main completed").await;
+                    main_request = Some(body);
+                }
+            }
+            changed = commits.changed() => { changed.unwrap(); }
+        }
+    }
+    f.drained().await;
+    let record = f.owner.session(&session).await.unwrap();
+    let physical = attempts(&f, &session).await;
+    let diagnostics = f
+        .query(
+            "diagnostics",
+            json!({"sessionId":session,"position":{"kind":"latest"}}),
+        )
+        .await
+        .unwrap();
+    let boundary = f
+        .store
+        .invocation_boundary(&session, sent["invocationId"].as_str().unwrap())
+        .await
+        .unwrap()
+        .unwrap()
+        .0;
+    f.app.shutdown().await.unwrap();
+    f.config.shutdown().await.unwrap();
+
+    assert!(main_request.unwrap()["output_config"]["effort"].is_null());
+    assert_eq!(
+        record.configuration.title, "Kimi auxiliary title",
+        "auxiliary preparation must succeed: {diagnostics}"
+    );
+    assert!(!record.configuration.title_is_manual);
+    let title_request = title_request.expect("supported Kimi Low must reach actual Anthropic HTTP");
+    assert_eq!(title_request["output_config"]["effort"], "low");
+    assert_eq!(title_request["max_tokens"], 2048);
+    assert!(title_request["tools"].is_null() || title_request["tools"] == json!([]));
+    assert_eq!(diagnostics["entries"], json!([]));
+    assert!(matches!(
+        boundary.state,
+        maka_event_log::turns::InvocationState::Ended {
+            outcome: maka_agent_facts::event::InvocationOutcome::Completed,
+            ..
+        }
+    ));
+    assert_eq!(physical.len(), 2);
+    let auxiliary = physical
+        .iter()
+        .find(|attempt| matches!(attempt.origin, Origin::Auxiliary { .. }))
+        .unwrap();
+    assert_eq!(auxiliary.session_id.as_deref(), Some(session.as_str()));
+    assert!(matches!(&auxiliary.origin, Origin::Auxiliary {
+        source: AuxiliarySource::SessionTitle { invocation }
+    } if invocation == &boundary.invocation));
+    assert_eq!(auxiliary.outcome, Outcome::Success);
+    assert_eq!(auxiliary.usage.input_tokens, Some(1));
+    assert_eq!(auxiliary.usage.output_tokens, Some(2));
+    }).await.unwrap();
+}
+
 #[tokio::test]
 async fn title_survives_main_completion_between_metadata_read_and_write() {
     verify_title_metadata_conflict(false).await;
diff --git a/plugins/agent/native/src/owner/tests/tool_schemas.rs b/plugins/agent/native/src/owner/tests/tool_schemas.rs
index a80fcadbf9..7b3223aa6c 100644
--- a/plugins/agent/native/src/owner/tests/tool_schemas.rs
+++ b/plugins/agent/native/src/owner/tests/tool_schemas.rs
@@ -39,9 +39,10 @@ struct Captured {
     schemas: BTreeMap<String, Value>,
     violations: Vec<String>,
     results: Vec<Value>,
+    tool_choice: Value,
 }
 
-async fn read_wire(socket: &mut TcpStream) -> Value {
+pub(super) async fn read_wire(socket: &mut TcpStream) -> Value {
     let mut bytes = Vec::new();
     let boundary = loop {
         let mut chunk = [0; 4096];
@@ -74,6 +75,22 @@ async fn send_body(socket: &mut TcpStream, status: &str, content_type: &str, bod
     socket.write_all(format!("HTTP/1.1 {status}\r\nContent-Type: {content_type}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", body.len()).as_bytes()).await.unwrap();
 }
 
+pub(super) async fn anthropic_text_reply(socket: &mut TcpStream, text: &str) {
+    let events = [
+        json!({"type":"message_start","message":{"id":"auxiliary-reply","type":"message","role":"assistant","model":"fixture-model","content":[],"usage":{"input_tokens":1,"output_tokens":0}}}),
+        json!({"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}),
+        json!({"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":text}}),
+        json!({"type":"content_block_stop","index":0}),
+        json!({"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":2}}),
+        json!({"type":"message_stop"}),
+    ];
+    let body = events
+        .iter()
+        .map(|event| format!("data: {event}\n\n"))
+        .collect();
+    send_body(socket, "200 OK", "text/event-stream", body).await;
+}
+
 async fn model_reply(socket: &mut TcpStream, wire: Wire, call: Option<(&str, &str, Value)>) {
     let tool_call = call.is_some();
     let events = match wire {
@@ -156,6 +173,7 @@ async fn strict_provider(listener: TcpListener, wire: Wire) -> Captured {
         schemas: BTreeMap::new(),
         violations: Vec::new(),
         results: Vec::new(),
+        tool_choice: request["tool_choice"].clone(),
     };
     for tool in request["tools"].as_array().unwrap() {
         let (name, schema) = match wire {
@@ -279,6 +297,20 @@ async fn coordinator_catalog_completes(wire: Wire, provider: &str) {
             provider,
         )
         .await;
+        if matches!(wire, Wire::Anthropic) {
+            selection::edit(&fixture, |changes| {
+                changes.model_overrides = Patch::Set(BTreeMap::from([(
+                    "fixture-model".into(),
+                    ModelOverride {
+                        capabilities: Some(
+                            serde_json::from_value(json!({"parallelToolCalls":false})).unwrap(),
+                        ),
+                        ..Default::default()
+                    },
+                )]));
+            })
+            .await;
+        }
         workhub::activate_workers(&mut fixture, true, true, true).await;
         let ready =
             workhub::resolve(&fixture, workhub::selection(&fixture).await, Uuid::new_v4()).await;
@@ -314,6 +346,12 @@ async fn coordinator_catalog_completes(wire: Wire, provider: &str) {
         let captured = server.await.unwrap();
         fixture.app.shutdown().await.unwrap();
         fixture.config.shutdown().await.unwrap();
+        if matches!(wire, Wire::Anthropic) {
+            assert_eq!(
+                captured.tool_choice["disable_parallel_tool_use"], true,
+                "the actual Anthropic request must retain the declared parallel opt-out"
+            );
+        }
         for name in AFFECTED.into_iter().chain(EMPTY) {
             assert!(
                 captured.schemas.contains_key(name),
diff --git a/plugins/agent/ports/src/provider.rs b/plugins/agent/ports/src/provider.rs
index 11549719b3..72d16d8309 100644
--- a/plugins/agent/ports/src/provider.rs
+++ b/plugins/agent/ports/src/provider.rs
@@ -60,6 +60,32 @@ pub enum Error {
     Rejected(String),
     #[error("invalid provider input or output: {0}")]
     Invalid(String),
+    #[error("invalid provider configuration")]
+    InvalidConfiguration,
+    #[error("invalid selected model")]
+    InvalidModel,
+    #[error("invalid prepared model")]
+    InvalidPreparedModel,
+    #[error("provider changed the selected model identity")]
+    SelectedModelChanged,
+    #[error("unsupported model thinking level")]
+    UnsupportedThinkingLevel,
+    #[error("provider execution profile or semantic features are not supported")]
+    UnsupportedExecutionProfile,
+    #[error("provider adapter or protocol is not supported")]
+    UnsupportedProtocol,
+    #[error("invalid provider endpoint")]
+    InvalidEndpoint,
+    #[error("Responses contract is not declared")]
+    MissingResponsesContract,
+    #[error("provider protocol does not expose parallel-call policy")]
+    UnsupportedParallelPolicy,
+    #[error("model output limit does not leave a positive text budget")]
+    InsufficientOutputBudget,
+    #[error("provider facts are unavailable")]
+    ProviderFactsUnavailable,
+    #[error("provider probe authentication is not supported")]
+    UnsupportedProbeAuthentication,
     #[error("provider request outcome is unknown")]
     OutcomeUnknown,
     #[error("provider transport failed: {0}")]
@@ -156,9 +182,7 @@ impl Definition {
     /// stored connection never substitutes a newer plugin's endpoint or defaults.
     pub fn configure(&self, input: serde_json::Value) -> Result<serde_json::Value, Error> {
         let serde_json::Value::Object(input) = input else {
-            return Err(Error::Invalid(
-                "provider configuration must be an object".into(),
-            ));
+            return Err(Error::InvalidConfiguration);
         };
         let mut configuration = self
             .descriptor
@@ -176,14 +200,12 @@ impl Definition {
     /// drain its actual implementation owner; this value is not an admission grant.
     pub async fn prepare(&self, request: Resolve) -> Result<Model, Error> {
         self.validate_configuration(&request.connection.configuration)?;
-        request.model.validate().map_err(Error::Invalid)?;
+        request.model.validate().map_err(|_| Error::InvalidModel)?;
         let expected = request.model.id.clone();
         let model = self.implementation.resolve(request).await?;
-        model.validate()?;
+        model.validate().map_err(|_| Error::InvalidPreparedModel)?;
         if model.info.id != expected {
-            return Err(Error::Invalid(
-                "provider changed the selected model identity".into(),
-            ));
+            return Err(Error::SelectedModelChanged);
         }
         Ok(model)
     }
@@ -271,14 +293,12 @@ impl Definition {
 
     pub fn validate_configuration(&self, configuration: &serde_json::Value) -> Result<(), Error> {
         if serde_json::to_vec(configuration)
-            .map_err(|_| Error::Invalid("invalid configuration".into()))?
+            .map_err(|_| Error::InvalidConfiguration)?
             .len()
             > 64 * 1024
             || !self.configuration.is_valid(configuration)
         {
-            return Err(Error::Invalid(
-                "provider configuration does not satisfy its schema".into(),
-            ));
+            return Err(Error::InvalidConfiguration);
         }
         Ok(())
     }
diff --git a/plugins/agent/providers/README.md b/plugins/agent/providers/README.md
index 93ab72caba..7f314f2e22 100644
--- a/plugins/agent/providers/README.md
+++ b/plugins/agent/providers/README.md
@@ -23,4 +23,6 @@
 
 Supplies compiled model-provider definitions. `api` covers API-backed providers and `codex` implements the Codex provider integration. The native service selects a definition explicitly. Provider transport and credentials use the caller's capabilities and configuration.
 
+API request options follow the resolved protocol, including provider aliases, model overrides and protocol alternates. Declared thinking choices use the same preparation policy for selectable metadata, main requests, automatic titles and recaps. An explicit Anthropic parallel-tool opt-out is encoded in the request. Native preparation diagnostics retain fixed local rejection reasons while keeping arbitrary provider error text private.
+
 [Source](src/lib.rs) · [Cargo manifest](Cargo.toml)
diff --git a/plugins/agent/providers/README.zh-CN.md b/plugins/agent/providers/README.zh-CN.md
index c8c9bd1261..7cd3436049 100644
--- a/plugins/agent/providers/README.zh-CN.md
+++ b/plugins/agent/providers/README.zh-CN.md
@@ -23,4 +23,6 @@
 
 提供编译进程序的模型提供商定义。`api` 覆盖 API 提供商,`codex` 实现 Codex 提供商集成。原生服务显式选择定义,传输与凭据使用调用方提供的能力和配置。
 
+API 请求选项遵循解析后的实际协议,覆盖提供商别名、模型覆盖和备选协议。模型可选元数据、主请求、自动标题和回顾使用相同的思考档位准备策略;显式禁用 Anthropic 并行工具调用会编码进请求。原生准备诊断保留固定的本地拒绝原因,同时隐藏提供商的任意错误文本。
+
 [源码](src/lib.rs) · [Cargo 配置](Cargo.toml)
diff --git a/plugins/agent/providers/src/api.rs b/plugins/agent/providers/src/api.rs
index 7865124766..e48edb25b8 100644
--- a/plugins/agent/providers/src/api.rs
+++ b/plugins/agent/providers/src/api.rs
@@ -128,11 +128,11 @@ struct Configuration {
 impl Configuration {
     fn read(connection: &Connection) -> Result<Self, Error> {
         let mut configuration: Self = serde_json::from_value(connection.configuration.clone())
-            .map_err(|_| Error::Invalid("invalid API provider configuration".into()))?;
+            .map_err(|_| Error::InvalidConfiguration)?;
         configuration.base_url = maka_agent_facts::configuration::validation::normalize_base_url(
             &configuration.base_url,
         )
-        .map_err(Error::Invalid)?;
+        .map_err(|_| Error::InvalidConfiguration)?;
         Ok(configuration)
     }
 }
@@ -146,10 +146,6 @@ struct Selection<'a> {
     overrides: Option<&'a ModelOverride>,
 }
 
-fn unavailable(message: impl Into<String>) -> Error {
-    Error::Invalid(message.into())
-}
-
 impl ApiProvider {
     fn definition(&self) -> Result<Definition, String> {
         let authentication =
@@ -218,7 +214,7 @@ impl ApiProvider {
         route.check_execution()?;
         let level = request.thinking_level.or(default_thinking_level);
         if level.is_some_and(|level| !thinking_levels.contains(&level)) {
-            return Err(Error::Invalid("unsupported model thinking level".into()));
+            return Err(Error::UnsupportedThinkingLevel);
         }
         let options = options::resolve(&selection, self.facts, level, &route)?;
         let output = output::resolve(&selection, route.wire, &options)?;
@@ -243,7 +239,7 @@ impl ApiProvider {
             provider_options: options,
             main_output_limit: output,
         };
-        model.validate()?;
+        model.validate().map_err(|_| Error::InvalidPreparedModel)?;
         Ok(model)
     }
 }
diff --git a/plugins/agent/providers/src/api/options.rs b/plugins/agent/providers/src/api/options.rs
index 2477de014b..0ece6b0f2d 100644
--- a/plugins/agent/providers/src/api/options.rs
+++ b/plugins/agent/providers/src/api/options.rs
@@ -17,8 +17,8 @@
  * under the License.
  */
 
+use super::Wire;
 use super::{Selection, route::Route};
-use super::{Wire, unavailable};
 use crate::facts::{ProviderFacts, ThinkingOffBehavior};
 use maka_agent_facts::execution::ThinkingLevel;
 use maka_agent_ports::model::ProviderKind;
@@ -55,51 +55,47 @@ pub(super) fn resolve(
                 .and_then(|item| item.metadata.capabilities)
                 .and_then(|caps| caps.parallel_tool_calls)
         });
-    if let ProviderKind::OpenResponses(contract) = &route.kind {
-        if parallel == Some(false) {
-            return Err(unavailable(
-                "Open Responses does not expose parallel-call policy",
-            ));
+    match &route.kind {
+        ProviderKind::Anthropic => Ok(anthropic(facts, model, thinking_level, parallel)),
+        ProviderKind::OpenaiResponses if row.provider == "openai-responses-compatible" => {
+            relay::responses(row, thinking_level, parallel.unwrap_or(true))
         }
-        let mut options = json!({});
-        if let Some(level) = thinking_level {
-            options["reasoningEffort"] = if level == ThinkingLevel::Off {
-                json!("none")
-            } else {
-                json!(level)
-            };
+        ProviderKind::OpenaiChat | ProviderKind::OpenaiResponses => {
+            // An explicit account capability can opt out; it never gates rendering.
+            let summary = stored
+                .and_then(|model| model.supports_reasoning_summary)
+                .unwrap_or(false);
+            Ok(openai(
+                wire,
+                thinking_level,
+                summary,
+                parallel.unwrap_or(true),
+            ))
         }
-        if contract.reasoning_replay
-            == maka_agent_facts::model::PlaintextReasoningReplay::PlaintextSummary
-            && thinking_level != Some(ThinkingLevel::Off)
-        {
-            options["reasoningSummary"] = json!("auto");
+        ProviderKind::OpenaiCompatible { name } => {
+            compatible::chat(name, model, thinking_level, parallel)
         }
-        return Ok(json!({"openResponses":options}));
-    }
-    if row.provider == "anthropic" {
-        return Ok(anthropic(facts, model, thinking_level));
-    }
-    if row.provider == "openai-responses-compatible" && wire == Wire::OpenaiResponses {
-        return relay::responses(row, thinking_level, parallel.unwrap_or(true));
-    }
-    if let ProviderKind::OpenaiCompatible { name } = &route.kind {
-        return compatible::chat(name, model, thinking_level, parallel);
-    }
-    if row.provider != "openai" {
-        if thinking_level.is_some() || parallel == Some(false) {
-            return Err(unavailable(
-                "Thinking or parallel-call policy is not supported for this provider",
-            ));
+        ProviderKind::OpenResponses(contract) => {
+            if parallel == Some(false) {
+                return Err(Error::UnsupportedParallelPolicy);
+            }
+            let mut options = json!({});
+            if let Some(level) = thinking_level {
+                options["reasoningEffort"] = if level == ThinkingLevel::Off {
+                    json!("none")
+                } else {
+                    json!(level)
+                };
+            }
+            if contract.reasoning_replay
+                == maka_agent_facts::model::PlaintextReasoningReplay::PlaintextSummary
+                && thinking_level != Some(ThinkingLevel::Off)
+            {
+                options["reasoningSummary"] = json!("auto");
+            }
+            Ok(json!({"openResponses":options}))
         }
-        return Ok(json!({}));
     }
-    // An explicit account capability can opt out; it never gates rendering.
-    let summary = stored
-        .and_then(|model| model.supports_reasoning_summary)
-        .unwrap_or(false);
-    let options = openai(wire, thinking_level, summary, parallel.unwrap_or(true));
-    Ok(options)
 }
 
 fn default_medium(facts: &ProviderFacts, model: &str) -> bool {
@@ -127,7 +123,12 @@ fn openai(wire: Wire, level: Option<ThinkingLevel>, summary: bool, parallel: boo
     json!({"openai": options})
 }
 
-fn anthropic(facts: &ProviderFacts, model: &str, level: Option<ThinkingLevel>) -> Value {
+fn anthropic(
+    facts: &ProviderFacts,
+    model: &str,
+    level: Option<ThinkingLevel>,
+    parallel: Option<bool>,
+) -> Value {
     let family = claude_family(model);
     let known = facts.models.get(model);
     let family_facts = facts.models.get(family.as_ref());
@@ -154,6 +155,9 @@ fn anthropic(facts: &ProviderFacts, model: &str, level: Option<ThinkingLevel>) -
                 == Some(true)
             || bare_legacy);
     let mut options = json!({"cacheControl":{"type":"ephemeral"}});
+    if parallel == Some(false) {
+        options["disableParallelToolUse"] = json!(true);
+    }
     if level == Some(ThinkingLevel::Off)
         && thinking.and_then(|thinking| thinking.off_behavior)
             == Some(ThinkingOffBehavior::AnthropicThinkingDisabled)
diff --git a/plugins/agent/providers/src/api/options/compatible.rs b/plugins/agent/providers/src/api/options/compatible.rs
index 2d0acb3c48..1408ee1f4f 100644
--- a/plugins/agent/providers/src/api/options/compatible.rs
+++ b/plugins/agent/providers/src/api/options/compatible.rs
@@ -26,8 +26,7 @@ pub(super) fn chat(
     level: Option<ThinkingLevel>,
     parallel: Option<bool>,
 ) -> Result<Value, Error> {
-    let facts = provider_facts("openai")
-        .map_err(|_| super::unavailable("OpenAI provider facts are unavailable"))?;
+    let facts = provider_facts("openai").map_err(|_| Error::ProviderFactsUnavailable)?;
     let level =
         level.or_else(|| super::default_medium(facts, model).then_some(ThinkingLevel::Medium));
     let mut options = json!({});
diff --git a/plugins/agent/providers/src/api/options/relay.rs b/plugins/agent/providers/src/api/options/relay.rs
index 21625678c5..a0e896cbd2 100644
--- a/plugins/agent/providers/src/api/options/relay.rs
+++ b/plugins/agent/providers/src/api/options/relay.rs
@@ -28,8 +28,7 @@ pub(super) fn responses(
     let model = row.model.id.as_str();
     // Relay catalogs may have no models. Defaults and summaries follow canonical
     // OpenAI family facts, never the relay's declared thinking levels.
-    let facts = provider_facts("openai")
-        .map_err(|_| super::unavailable("OpenAI provider facts are unavailable"))?;
+    let facts = provider_facts("openai").map_err(|_| Error::ProviderFactsUnavailable)?;
     let mut options = super::openai(
         Wire::OpenaiResponses,
         level.or_else(|| super::default_medium(facts, model).then_some(ThinkingLevel::Medium)),
diff --git a/plugins/agent/providers/src/api/output.rs b/plugins/agent/providers/src/api/output.rs
index cceaeecb45..94d3a3de53 100644
--- a/plugins/agent/providers/src/api/output.rs
+++ b/plugins/agent/providers/src/api/output.rs
@@ -18,7 +18,7 @@
  */
 
 use super::Selection;
-use super::{Wire, unavailable};
+use super::Wire;
 use maka_agent_ports::provider::Error;
 use serde_json::Value;
 
@@ -64,5 +64,5 @@ fn subtract_budget(limit: u64, options: &Value) -> Result<u64, Error> {
     limit
         .checked_sub(fixed)
         .filter(|limit| *limit > 0)
-        .ok_or_else(|| unavailable("Model output limit does not leave a positive text budget"))
+        .ok_or(Error::InsufficientOutputBudget)
 }
diff --git a/plugins/agent/providers/src/api/route.rs b/plugins/agent/providers/src/api/route.rs
index bfc49ebe13..ad21f43b6d 100644
--- a/plugins/agent/providers/src/api/route.rs
+++ b/plugins/agent/providers/src/api/route.rs
@@ -73,9 +73,7 @@ fn check_execution(adapter: &RuntimeAdapter, wire: Wire) -> Result<(), Error> {
         _ => false,
     };
     if !supported {
-        return Err(unavailable(
-            "Provider execution profile or semantic features are not supported",
-        ));
+        return Err(Error::UnsupportedExecutionProfile);
     }
     Ok(())
 }
@@ -96,9 +94,7 @@ impl Route<'_> {
                 ..
             }
         ) {
-            return Err(unavailable(
-                "Provider probe authentication is not supported",
-            ));
+            return Err(Error::UnsupportedProbeAuthentication);
         }
         Ok(())
     }
@@ -108,14 +104,10 @@ impl Route<'_> {
     }
 }
 
-fn unavailable(message: impl Into<String>) -> Error {
-    Error::Invalid(message.into())
-}
-
 fn adapter(value: &RuntimeAdapter) -> Result<&RuntimeAdapter, Error> {
     match value.kind {
         AdapterKind::Google { .. } | AdapterKind::Cohere | AdapterKind::Unavailable => {
-            Err(unavailable("Provider adapter or protocol is not supported"))
+            Err(Error::UnsupportedProtocol)
         }
         _ => Ok(value),
     }
@@ -168,9 +160,7 @@ pub(crate) fn resolve<'a>(
         }
     };
     if !supports(selected, wire) {
-        return Err(unavailable(
-            "Provider does not support the declared model protocol",
-        ));
+        return Err(Error::UnsupportedProtocol);
     }
     let resolved_endpoint = if row.base_url == facts.base_url && uses_override {
         override_
@@ -179,10 +169,9 @@ pub(crate) fn resolve<'a>(
     } else {
         row.base_url
     };
-    let mut url =
-        url::Url::parse(resolved_endpoint).map_err(|_| unavailable("Invalid provider endpoint"))?;
+    let mut url = url::Url::parse(resolved_endpoint).map_err(|_| Error::InvalidEndpoint)?;
     if !matches!(url.scheme(), "http" | "https") || url.host_str().is_none() {
-        return Err(unavailable("Invalid provider endpoint"));
+        return Err(Error::InvalidEndpoint);
     }
     let path = url.path().trim_end_matches('/');
     let normalize_base_url = match selected.kind {
@@ -233,7 +222,7 @@ pub(crate) fn resolve<'a>(
                     responses: Some(responses),
                     ..
                 } => responses,
-                _ => return Err(unavailable("Responses contract is not declared")),
+                _ => return Err(Error::MissingResponsesContract),
             };
             match responses {
                 ResponsesContract::Openai { .. } => ProviderKind::OpenaiResponses,
diff --git a/plugins/agent/providers/tests/api.rs b/plugins/agent/providers/tests/api.rs
index 155fb53fae..27c8f06cd7 100644
--- a/plugins/agent/providers/tests/api.rs
+++ b/plugins/agent/providers/tests/api.rs
@@ -37,6 +37,8 @@ use serde_json::json;
 mod discovery;
 #[path = "api/effects.rs"]
 mod effects;
+#[path = "api/policy.rs"]
+mod policy;
 
 fn binding(name: &str) -> Definition {
     ApiProviders::definition(name).unwrap()
@@ -192,7 +194,10 @@ async fn account_metadata_and_reply_capacity_survive_plugin_policy_resolution()
     );
     assert_eq!(model.main_output_limit, Some(2976));
     input.overrides.as_mut().unwrap().max_output_tokens = Some(1000);
-    assert!(anthropic.prepare(input).await.is_err());
+    assert_eq!(
+        serde_json::to_value(anthropic.prepare(input).await.unwrap_err()).unwrap(),
+        json!({"kind":"insufficient_output_budget"})
+    );
     let openai = binding("openai");
     for (id, label) in [("gpt-6-sol", "GPT-6 Sol"), ("gpt-6-luna", "GPT-6 Luna")] {
         let mut input = request(&openai, id, ModelOverride::default());
@@ -300,23 +305,55 @@ async fn selectable_thinking_choices_respect_native_encoder_and_invalid_default(
         input.overrides.clone(),
     )
     .unwrap();
-    assert!(metadata.thinking_levels.is_empty());
+    assert_eq!(metadata.thinking_levels, [High]);
     assert_eq!(metadata.default_thinking_level, None);
-    binding.prepare(input.clone()).await.unwrap();
+    assert_eq!(
+        binding
+            .prepare(input.clone())
+            .await
+            .unwrap()
+            .provider_options,
+        json!({"anthropic":{"cacheControl":{"type":"ephemeral"}}})
+    );
     input.thinking_level = Some(High);
-    assert!(binding.prepare(input.clone()).await.is_err());
+    assert_eq!(
+        binding
+            .prepare(input.clone())
+            .await
+            .unwrap()
+            .provider_options,
+        json!({"anthropic":{"cacheControl":{"type":"ephemeral"},"effort":"high"}})
+    );
     input.overrides.as_mut().unwrap().default_thinking_level = Some(High);
+    let metadata = ApiProviders::model_metadata(
+        "anthropic-compatible",
+        input.connection.clone(),
+        input.model.clone(),
+        input.overrides.clone(),
+    )
+    .unwrap();
+    assert_eq!(metadata.thinking_levels, [High]);
+    assert_eq!(metadata.default_thinking_level, Some(High));
+    input.thinking_level = None;
+    assert_eq!(
+        binding
+            .prepare(input.clone())
+            .await
+            .unwrap()
+            .provider_options,
+        json!({"anthropic":{"cacheControl":{"type":"ephemeral"},"effort":"high"}})
+    );
+    input.overrides.as_mut().unwrap().thinking_levels = Some(vec![Low]);
     assert!(
         ApiProviders::model_metadata(
             "anthropic-compatible",
             input.connection.clone(),
             input.model.clone(),
-            input.overrides.clone()
+            input.overrides.clone(),
         )
         .is_err()
     );
-    input.overrides.as_mut().unwrap().thinking_levels = Some(vec![Low]);
-    // Invalid configured defaults are also rejected for encoders that do support thinking.
+    // A default outside the declared choices is invalid on either protocol.
     assert!(
         ApiProviders::model_metadata(
             "openai-compatible",
diff --git a/plugins/agent/providers/tests/api/effects.rs b/plugins/agent/providers/tests/api/effects.rs
index 56514f03c1..9e013896f3 100644
--- a/plugins/agent/providers/tests/api/effects.rs
+++ b/plugins/agent/providers/tests/api/effects.rs
@@ -311,7 +311,6 @@ async fn discovery_survives_persistence_and_verification_does_not_require_stream
 
 #[tokio::test]
 async fn compiled_definition_authenticates_without_legacy_registration_or_secret_diagnostics() {
-    use maka_agent_ports::provider::Error;
     use maka_agent_ports::provider::authentication::Authenticate;
     let definition = ApiProviders::definition("openai-compatible").unwrap();
     let transport = Arc::new(Exchange::default());
@@ -339,23 +338,31 @@ async fn compiled_definition_authenticates_without_legacy_registration_or_secret
         .unwrap();
     assert_eq!(result.secret, key);
     assert_eq!(result.refresh_at, None);
-    for (method, input, configuration) in [
+    for (method, input, configuration, error_kind) in [
         (
             "unknown",
             json!({"apiKey":key}),
             connection.configuration.clone(),
+            "invalid",
         ),
         (
             "api-key",
             json!({"apiKey":key,"unknown":key}),
             connection.configuration.clone(),
+            "invalid",
         ),
         (
             "api-key",
             json!({"apiKey":format!("{key}\n{key}")}),
             connection.configuration.clone(),
+            "invalid",
+        ),
+        (
+            "api-key",
+            json!({"apiKey":key}),
+            json!({"unknown":key}),
+            "invalid_configuration",
         ),
-        ("api-key", json!({"apiKey":key}), json!({"unknown":key})),
     ] {
         let error = match definition
             .authenticate(
@@ -374,7 +381,7 @@ async fn compiled_definition_authenticates_without_legacy_registration_or_secret
             Ok(_) => panic!("invalid authentication admitted"),
             Err(error) => error,
         };
-        assert!(matches!(error, Error::Invalid(_)));
+        assert_eq!(serde_json::to_value(&error).unwrap()["kind"], error_kind);
         assert!(!error.to_string().contains(key));
         assert!(!serde_json::to_string(&error).unwrap().contains(key));
     }
diff --git a/plugins/agent/providers/tests/api/policy.rs b/plugins/agent/providers/tests/api/policy.rs
new file mode 100644
index 0000000000..924bf4cda9
--- /dev/null
+++ b/plugins/agent/providers/tests/api/policy.rs
@@ -0,0 +1,345 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+use super::*;
+use maka_agent_facts::configuration::ModelOverrideCapabilities;
+
+#[tokio::test]
+async fn kimi_catalog_thinking_levels_prepare_without_losing_the_selected_route() {
+    let binding = binding("kimi-coding-plan");
+    for (level, options) in [
+        (
+            None,
+            json!({"anthropic":{"cacheControl":{"type":"ephemeral"}}}),
+        ),
+        (
+            Some(Low),
+            json!({"anthropic":{"cacheControl":{"type":"ephemeral"},"effort":"low"}}),
+        ),
+        (
+            Some(High),
+            json!({"anthropic":{"cacheControl":{"type":"ephemeral"},"effort":"high"}}),
+        ),
+        (
+            Some(Max),
+            json!({"anthropic":{"cacheControl":{"type":"ephemeral"},"effort":"max"}}),
+        ),
+    ] {
+        let mut input = request(&binding, "k3", ModelOverride::default());
+        input.thinking_level = level;
+        let model = binding.prepare(input).await.unwrap();
+        assert!(matches!(model.protocol, ProviderKind::Anthropic));
+        assert_eq!(model.adapter, "anthropic-messages");
+        assert_eq!(model.info.id, "k3");
+        assert_eq!(model.base_url, "https://relay.test/v1");
+        assert_eq!(model.thinking_levels, [Low, High, Max]);
+        assert_eq!(model.provider_options, options);
+        assert_eq!(model.main_output_limit, Some(131072));
+    }
+}
+
+#[tokio::test]
+async fn kimi_catalog_choices_and_supported_default_remain_selectable() {
+    let binding = binding("kimi-coding-plan");
+    for default in [None, Some(High)] {
+        let input = request(
+            &binding,
+            "k3",
+            ModelOverride {
+                default_thinking_level: default,
+                ..Default::default()
+            },
+        );
+        let metadata = ApiProviders::model_metadata(
+            "kimi-coding-plan",
+            input.connection.clone(),
+            input.model.clone(),
+            input.overrides.clone(),
+        )
+        .unwrap();
+        assert_eq!(metadata.thinking_levels, [Low, High, Max]);
+        assert_eq!(metadata.default_thinking_level, default);
+        let model = binding.prepare(input).await.unwrap();
+        assert_eq!(
+            model.provider_options,
+            if default.is_some() {
+                json!({"anthropic":{"cacheControl":{"type":"ephemeral"},"effort":"high"}})
+            } else {
+                json!({"anthropic":{"cacheControl":{"type":"ephemeral"}}})
+            }
+        );
+    }
+}
+
+#[tokio::test]
+async fn anthropic_alias_override_and_alternate_use_the_selected_protocol_policy() {
+    // These inputs distinguish direct aliases, model adapter overrides and an
+    // explicit protocol alternate. Account endpoints must retain precedence.
+    for (provider, id, protocol, level, options, budget) in [
+        (
+            "minimax-coding-plan",
+            "MiniMax-M2.5",
+            None,
+            High,
+            json!({"anthropic":{"cacheControl":{"type":"ephemeral"},"effort":"high"}}),
+            4000,
+        ),
+        (
+            "opencode",
+            "claude-opus-4-6",
+            None,
+            High,
+            json!({"anthropic":{"cacheControl":{"type":"ephemeral"},"thinking":{"type":"enabled","budgetTokens":1024},"effort":"high"}}),
+            2976,
+        ),
+        (
+            "zenmux",
+            "anthropic/claude-opus-4.5",
+            None,
+            High,
+            json!({"anthropic":{"cacheControl":{"type":"ephemeral"},"thinking":{"type":"enabled","budgetTokens":1024},"effort":"high"}}),
+            2976,
+        ),
+        (
+            "opencode",
+            "custom-model",
+            Some(ApiProtocol::AnthropicMessages),
+            High,
+            json!({"anthropic":{"cacheControl":{"type":"ephemeral"},"effort":"high"}}),
+            4000,
+        ),
+        (
+            "anthropic",
+            "claude-opus-4-6",
+            None,
+            High,
+            json!({"anthropic":{"cacheControl":{"type":"ephemeral"},"thinking":{"type":"adaptive","display":"summarized"},"effort":"high"}}),
+            4000,
+        ),
+        (
+            "anthropic",
+            "claude-sonnet-4-5",
+            None,
+            Off,
+            json!({"anthropic":{"cacheControl":{"type":"ephemeral"},"thinking":{"type":"disabled"}}}),
+            4000,
+        ),
+    ] {
+        let binding = binding(provider);
+        let mut input = request(
+            &binding,
+            id,
+            ModelOverride {
+                api_protocol: protocol,
+                thinking_levels: Some(vec![level]),
+                max_output_tokens: Some(4000),
+                ..Default::default()
+            },
+        );
+        input.thinking_level = Some(level);
+        let model = binding.prepare(input).await.unwrap();
+        assert!(
+            matches!(model.protocol, ProviderKind::Anthropic),
+            "{provider}/{id}"
+        );
+        assert_eq!(model.adapter, "anthropic-messages");
+        assert_eq!(model.base_url, "https://relay.test/v1");
+        assert_eq!(model.info.id, id);
+        assert_eq!(model.provider_options, options, "{provider}/{id}");
+        assert_eq!(model.main_output_limit, Some(budget), "{provider}/{id}");
+    }
+    // With the catalog endpoint unchanged, the model's endpoint override owns
+    // routing; a custom account endpoint above deliberately takes precedence.
+    let binding = binding("zenmux");
+    let mut input = request(
+        &binding,
+        "anthropic/claude-opus-4.5",
+        ModelOverride {
+            max_output_tokens: Some(4000),
+            ..Default::default()
+        },
+    );
+    input.connection.configuration = binding
+        .configure(json!({"baseUrl":"https://zenmux.ai/api/v1"}))
+        .unwrap();
+    let model = binding.prepare(input).await.unwrap();
+    assert!(matches!(model.protocol, ProviderKind::Anthropic));
+    assert_eq!(model.base_url, "https://zenmux.ai/api/anthropic/v1");
+    assert_eq!(
+        model.provider_options,
+        json!({"anthropic":{"cacheControl":{"type":"ephemeral"},"thinking":{"type":"enabled","budgetTokens":1024}}})
+    );
+    assert_eq!(model.main_output_limit, Some(2976));
+}
+
+#[tokio::test]
+async fn resolved_openai_routes_preserve_explicit_effort_and_parallel_policy() {
+    for (provider, id, wire) in [
+        (
+            "volcengine-agent-plan",
+            "ark-code-latest",
+            ApiProtocol::OpenaiResponses,
+        ),
+        ("xai", "grok-4.5", ApiProtocol::OpenaiResponses),
+        ("opencode", "gpt-5.2", ApiProtocol::OpenaiResponses),
+        ("opencode", "custom-model", ApiProtocol::OpenaiResponses),
+        ("zenmux", "openai/gpt-5.2", ApiProtocol::OpenaiChat),
+    ] {
+        let binding = binding(provider);
+        let mut input = request(
+            &binding,
+            id,
+            ModelOverride {
+                api_protocol: Some(wire),
+                thinking_levels: Some(vec![High]),
+                max_output_tokens: Some(4000),
+                capabilities: Some(ModelOverrideCapabilities {
+                    parallel_tool_calls: Some(false),
+                    ..Default::default()
+                }),
+                ..Default::default()
+            },
+        );
+        input.thinking_level = Some(High);
+        input.model.supports_reasoning_summary = Some(false);
+        let model = binding.prepare(input).await.unwrap();
+        match wire {
+            ApiProtocol::OpenaiResponses => {
+                assert!(matches!(model.protocol, ProviderKind::OpenaiResponses));
+                assert_eq!(model.adapter, "responses");
+            }
+            ApiProtocol::OpenaiChat => {
+                assert!(matches!(model.protocol, ProviderKind::OpenaiChat));
+                assert_eq!(model.adapter, "chat-completions");
+            }
+            ApiProtocol::AnthropicMessages => unreachable!(),
+        }
+        assert_eq!(model.base_url, "https://relay.test/v1");
+        assert_eq!(model.info.id, id);
+        assert_eq!(
+            model.provider_options,
+            json!({"openai":{"store":false,"parallelToolCalls":false,"reasoningEffort":"high"}}),
+            "{provider}/{id}"
+        );
+        assert_eq!(model.main_output_limit, Some(4000));
+    }
+}
+
+#[tokio::test]
+async fn anthropic_parallel_opt_out_is_encoded_on_canonical_and_alias_routes() {
+    for provider in ["anthropic", "anthropic-compatible"] {
+        let binding = binding(provider);
+        for parallel in [None, Some(false)] {
+            let input = request(
+                &binding,
+                "custom-model",
+                ModelOverride {
+                    capabilities: Some(ModelOverrideCapabilities {
+                        parallel_tool_calls: parallel,
+                        ..Default::default()
+                    }),
+                    ..Default::default()
+                },
+            );
+            let model = binding.prepare(input).await.unwrap();
+            assert!(matches!(model.protocol, ProviderKind::Anthropic));
+            assert_eq!(
+                model.provider_options,
+                if parallel == Some(false) {
+                    json!({"anthropic":{"cacheControl":{"type":"ephemeral"},"disableParallelToolUse":true}})
+                } else {
+                    json!({"anthropic":{"cacheControl":{"type":"ephemeral"}}})
+                },
+                "{provider}/{parallel:?}"
+            );
+        }
+    }
+}
+
+#[tokio::test]
+async fn supported_options_do_not_enable_unsupported_execution_contracts() {
+    let mut observed = Vec::new();
+    for (provider, id, wire) in [
+        ("MiniMax", "custom-model", ApiProtocol::AnthropicMessages),
+        (
+            "commandcode",
+            "custom-model",
+            ApiProtocol::AnthropicMessages,
+        ),
+        ("zenmux", "openai/gpt-5.2", ApiProtocol::OpenaiResponses),
+        ("kimi-coding-plan", "k3", ApiProtocol::OpenaiChat),
+        (
+            "anthropic-compatible",
+            "custom-model",
+            ApiProtocol::OpenaiResponses,
+        ),
+    ] {
+        let binding = binding(provider);
+        let input = request(
+            &binding,
+            id,
+            ModelOverride {
+                api_protocol: Some(wire),
+                thinking_levels: Some(vec![High]),
+                default_thinking_level: Some(High),
+                ..Default::default()
+            },
+        );
+        observed.push(json!({
+            "case": provider,
+            "error": binding.prepare(input).await.err(),
+        }));
+    }
+    let binding = binding("deepseek");
+    let input = request(
+        &binding,
+        "custom-model",
+        ModelOverride {
+            api_protocol: Some(ApiProtocol::OpenaiResponses),
+            capabilities: Some(ModelOverrideCapabilities {
+                parallel_tool_calls: Some(false),
+                ..Default::default()
+            }),
+            ..Default::default()
+        },
+    );
+    observed.push(json!({
+        "case": "open-responses-parallel",
+        "error": binding.prepare(input).await.err(),
+    }));
+    let binding = super::binding("kimi-coding-plan");
+    let mut input = request(&binding, "k3", ModelOverride::default());
+    input.thinking_level = Some(Medium);
+    observed.push(json!({
+        "case": "undeclared-kimi-level",
+        "error": binding.prepare(input).await.err(),
+    }));
+    assert_eq!(
+        json!(observed),
+        json!([
+            {"case":"MiniMax","error":{"kind":"unsupported_execution_profile"}},
+            {"case":"commandcode","error":{"kind":"unsupported_execution_profile"}},
+            {"case":"zenmux","error":{"kind":"unsupported_execution_profile"}},
+            {"case":"kimi-coding-plan","error":{"kind":"unsupported_execution_profile"}},
+            {"case":"anthropic-compatible","error":{"kind":"unsupported_protocol"}},
+            {"case":"open-responses-parallel","error":{"kind":"unsupported_parallel_policy"}},
+            {"case":"undeclared-kimi-level","error":{"kind":"unsupported_thinking_level"}},
+        ])
+    );
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org