providers-kit 0.0.2

Portable enum-dispatch Kit over multiple model provider APIs.
Documentation
use providers_kit::generation::{JsonSchema, ReasoningConfig, ReasoningEffort, ResponseFormat};
use serde_json::json;

use super::{LiveProvider, Mode, Service};
use crate::live_assertions::GenerationEvidence;
use crate::live_failures::CaseFailure;

impl LiveProvider {
    async fn current_model_generation(&self, model_id: &str) -> Result<(), CaseFailure> {
        let model = self.selected(model_id)?;
        let nonce = Self::nonce();
        let mut request = Self::text_request(&format!(
            "Reply with exactly this text and nothing else: {nonce}"
        ))?
        .with_reasoning(ReasoningConfig {
            effort: Some(ReasoningEffort::Minimal),
            ..ReasoningConfig::default()
        });
        if model_id == "gpt-6-luna" {
            request = request.with_generation(
                Self::generation_config()
                    .with_temperature(0.2)?
                    .with_top_p(0.9)?,
            )?;
        }
        let response = self.generate_on(&model, &request, Mode::Unary).await?;
        GenerationEvidence::exact_text(&response, &nonce, false).map_err(CaseFailure::from)
    }

    async fn current_reasoning_boundaries(&self, model_id: &str) -> Result<(), CaseFailure> {
        let model = self.selected(model_id)?;
        for (mode, effort) in [
            (Mode::Unary, None),
            (Mode::Streaming, Some(ReasoningEffort::Maximum)),
        ] {
            let nonce = Self::nonce();
            let mut request = Self::text_request(&format!(
                "Reply with exactly this text and nothing else: {nonce}"
            ))?;
            if let Some(effort) = effort {
                request = request.with_reasoning(ReasoningConfig {
                    effort: Some(effort),
                    ..ReasoningConfig::default()
                });
            }
            let response = self.generate_on(&model, &request, mode).await?;
            GenerationEvidence::exact_text(&response, &nonce, false)?;
        }
        Ok(())
    }

    async fn current_structured_output(&self, model_id: &str) -> Result<(), CaseFailure> {
        let model = self.selected(model_id)?;
        let schema = JsonSchema::new(json!({
            "type":"object", "properties":{"ack":{"type":"string"}},
            "required":["ack"], "additionalProperties":false,
        }))?;
        for mode in [Mode::Unary, Mode::Streaming] {
            let nonce = Self::nonce();
            let request = Self::text_request(&format!("Return a JSON object with exactly one property, ack, whose string value is {nonce}."))?
                .with_response_format(ResponseFormat::JsonSchema(schema.clone()));
            let response = self.generate_on(&model, &request, mode).await?;
            GenerationEvidence::completed(&response)?;
            if !response.hosted_tool_outputs.is_empty() {
                return Err("structured generation returned unrequested hosted output".into());
            }
            let value: serde_json::Value =
                serde_json::from_str(&GenerationEvidence::text(&response)?).map_err(|_| {
                    CaseFailure::from("structured generation returned invalid JSON")
                })?;
            if value != json!({"ack": nonce}) {
                return Err(
                    "structured generation violated the exact object schema or requested value"
                        .into(),
                );
            }
        }
        Ok(())
    }

    async fn current_reasoning_continuation(&self, model_id: &str) -> Result<(), CaseFailure> {
        let model = self.selected(model_id)?;
        self.function_continuation_on(
            &model,
            Mode::Streaming,
            Some(ReasoningConfig {
                effort: Some(ReasoningEffort::High),
                ..ReasoningConfig::default()
            }),
            Self::generation_config(),
            None,
        )
        .await
    }
}

#[tokio::test]
async fn openai_current_astra_generation() -> Result<(), CaseFailure> {
    let live = LiveProvider::new(Service::OpenAi)?;
    let result = live.current_model_generation("gpt-6-astra").await;
    live.finish(result).await;
    Ok(())
}

#[tokio::test]
async fn openai_current_sol_6_1_generation() -> Result<(), CaseFailure> {
    let live = LiveProvider::new(Service::OpenAi)?;
    let result = live.current_model_generation("gpt-6.1-sol").await;
    live.finish(result).await;
    Ok(())
}

#[tokio::test]
async fn openai_current_luna_generation() -> Result<(), CaseFailure> {
    let live = LiveProvider::new(Service::OpenAi)?;
    let result = live.current_model_generation("gpt-6-luna").await;
    live.finish(result).await;
    Ok(())
}

#[tokio::test]
async fn openai_retained_sol_6_generation() -> Result<(), CaseFailure> {
    let live = LiveProvider::new(Service::OpenAi)?;
    let result = live.current_model_generation("gpt-6-sol").await;
    live.finish(result).await;
    Ok(())
}

#[tokio::test]
async fn anthropic_current_fable_generation() -> Result<(), CaseFailure> {
    let live = LiveProvider::new(Service::Anthropic)?;
    let result = live.current_model_generation("claude-fable-5-1").await;
    live.finish(result).await;
    Ok(())
}

#[tokio::test]
async fn anthropic_current_opus_generation() -> Result<(), CaseFailure> {
    let live = LiveProvider::new(Service::Anthropic)?;
    let result = live.current_model_generation("claude-opus-5-5").await;
    live.finish(result).await;
    Ok(())
}

#[tokio::test]
async fn anthropic_current_sonnet_generation() -> Result<(), CaseFailure> {
    let live = LiveProvider::new(Service::Anthropic)?;
    let result = live.current_model_generation("claude-sonnet-5-5").await;
    live.finish(result).await;
    Ok(())
}

#[tokio::test]
async fn anthropic_current_haiku_generation() -> Result<(), CaseFailure> {
    let live = LiveProvider::new(Service::Anthropic)?;
    let result = live.current_model_generation("claude-haiku-5-5").await;
    live.finish(result).await;
    Ok(())
}

#[tokio::test]
async fn gemini_current_argon_generation() -> Result<(), CaseFailure> {
    let live = LiveProvider::new(Service::Gemini)?;
    let result = live.current_model_generation("models/gemini-4-argon").await;
    live.finish(result).await;
    Ok(())
}

#[tokio::test]
async fn gemini_current_flash_generation() -> Result<(), CaseFailure> {
    let live = LiveProvider::new(Service::Gemini)?;
    let result = live
        .current_model_generation("models/gemini-3.8-flash")
        .await;
    live.finish(result).await;
    Ok(())
}

#[tokio::test]
async fn openai_current_astra_reasoning_boundaries() -> Result<(), CaseFailure> {
    let live = LiveProvider::new(Service::OpenAi)?;
    let result = live.current_reasoning_boundaries("gpt-6-astra").await;
    live.finish(result).await;
    Ok(())
}

#[tokio::test]
async fn openai_current_luna_reasoning_boundaries() -> Result<(), CaseFailure> {
    let live = LiveProvider::new(Service::OpenAi)?;
    let result = live.current_reasoning_boundaries("gpt-6-luna").await;
    live.finish(result).await;
    Ok(())
}

#[tokio::test]
async fn anthropic_current_reasoning_boundaries() -> Result<(), CaseFailure> {
    let live = LiveProvider::new(Service::Anthropic)?;
    let result = live.current_reasoning_boundaries("claude-haiku-5-5").await;
    live.finish(result).await;
    Ok(())
}

#[tokio::test]
async fn gemini_current_reasoning_boundaries() -> Result<(), CaseFailure> {
    let live = LiveProvider::new(Service::Gemini)?;
    let result = live
        .current_reasoning_boundaries("models/gemini-3.8-flash")
        .await;
    live.finish(result).await;
    Ok(())
}

#[tokio::test]
async fn openai_current_structured_output() -> Result<(), CaseFailure> {
    let live = LiveProvider::new(Service::OpenAi)?;
    let result = live.current_structured_output("gpt-6-luna").await;
    live.finish(result).await;
    Ok(())
}

#[tokio::test]
async fn anthropic_current_structured_output() -> Result<(), CaseFailure> {
    let live = LiveProvider::new(Service::Anthropic)?;
    let result = live.current_structured_output("claude-haiku-5-5").await;
    live.finish(result).await;
    Ok(())
}

#[tokio::test]
async fn gemini_current_structured_output() -> Result<(), CaseFailure> {
    let live = LiveProvider::new(Service::Gemini)?;
    let result = live
        .current_structured_output("models/gemini-3.8-flash")
        .await;
    live.finish(result).await;
    Ok(())
}

#[tokio::test]
async fn openai_current_reasoning_stream_continuation() -> Result<(), CaseFailure> {
    let live = LiveProvider::new(Service::OpenAi)?;
    let result = live.current_reasoning_continuation("gpt-6-luna").await;
    live.finish(result).await;
    Ok(())
}

#[tokio::test]
async fn anthropic_current_reasoning_stream_continuation() -> Result<(), CaseFailure> {
    let live = LiveProvider::new(Service::Anthropic)?;
    let result = live
        .current_reasoning_continuation("claude-haiku-5-5")
        .await;
    live.finish(result).await;
    Ok(())
}

#[tokio::test]
async fn gemini_current_reasoning_stream_continuation() -> Result<(), CaseFailure> {
    let live = LiveProvider::new(Service::Gemini)?;
    let result = live
        .current_reasoning_continuation("models/gemini-3.8-flash")
        .await;
    live.finish(result).await;
    Ok(())
}