providers-kit 0.0.2

Portable enum-dispatch Kit over multiple model provider APIs.
Documentation
use serde_json::{Map, Value, json};

use crate::KitError;
use crate::error::ValidationViolation;
use crate::generation::{GenerationConfig, ReasoningConfig, ReasoningEffort, ReasoningSummaryMode};

#[derive(Clone, Copy, Debug)]
pub(crate) enum GeminiThinkingProfile {
    Levels { minimal: bool, fixed_sampling: bool },
    Budget,
    Unknown,
}

impl GeminiThinkingProfile {
    pub(crate) fn for_model(model: &str) -> Self {
        match model {
            "models/gemini-4-argon" | "models/gemini-3.8-flash" => Self::Levels {
                minimal: false,
                fixed_sampling: true,
            },
            "models/gemini-3.1-pro-preview" => Self::Levels {
                minimal: false,
                fixed_sampling: false,
            },
            "models/gemini-3-flash-preview" => Self::Levels {
                minimal: true,
                fixed_sampling: false,
            },
            "models/gemini-2.5-pro"
            | "models/gemini-2.5-flash"
            | "models/gemini-2.5-flash-lite" => Self::Budget,
            _ => Self::Unknown,
        }
    }

    pub(crate) fn validate_sampling(self, generation: &GenerationConfig) -> Result<(), KitError> {
        if matches!(
            self,
            Self::Levels {
                fixed_sampling: true,
                ..
            }
        ) && (generation.temperature().is_some()
            || generation.top_p().is_some()
            || generation.top_k().is_some())
        {
            return Err(KitError::unsupported(
                ValidationViolation::Unsupported {
                    field: "generation",
                    rule: "selected model ignores sampling overrides",
                },
                "Gemini sampling overrides for the selected model",
            ));
        }
        Ok(())
    }

    pub(crate) fn config(
        self,
        reasoning: &ReasoningConfig,
    ) -> Result<Option<Map<String, Value>>, KitError> {
        let mut thinking = Map::new();
        match self {
            Self::Levels { minimal, .. } => {
                if reasoning.max_reasoning_tokens.is_some() {
                    return Err(KitError::unsupported(
                        ValidationViolation::Unsupported {
                            field: "reasoning.max_reasoning_tokens",
                            rule: "selected model uses effort levels",
                        },
                        "Gemini selected model uses thinkingLevel rather than a token budget",
                    ));
                }
                if let Some(effort) = reasoning.effort {
                    let level = match effort {
                        ReasoningEffort::Minimal if minimal => "minimal",
                        ReasoningEffort::Minimal | ReasoningEffort::Low => "low",
                        ReasoningEffort::Medium => "medium",
                        ReasoningEffort::High | ReasoningEffort::Maximum => "high",
                    };
                    thinking.insert("thinkingLevel".into(), json!(level));
                }
            }
            Self::Budget => {
                if reasoning.effort.is_some() {
                    return Err(KitError::unsupported(
                        ValidationViolation::Unsupported {
                            field: "reasoning.effort",
                            rule: "selected model uses token budgets",
                        },
                        "Gemini 2.5 models use thinkingBudget rather than effort levels",
                    ));
                }
                if let Some(tokens) = reasoning.max_reasoning_tokens {
                    thinking.insert("thinkingBudget".into(), json!(tokens));
                }
            }
            Self::Unknown => {
                return Err(KitError::unsupported(
                    ValidationViolation::Unsupported {
                        field: "reasoning",
                        rule: "selected model lacks recognized reasoning controls",
                    },
                    "Gemini reasoning controls for an unrecognized model ID",
                ));
            }
        }
        match reasoning.summary_mode {
            ReasoningSummaryMode::Hidden => {
                thinking.insert("includeThoughts".into(), Value::Bool(false));
            }
            ReasoningSummaryMode::Summary | ReasoningSummaryMode::Verbose => {
                thinking.insert("includeThoughts".into(), Value::Bool(true));
            }
            ReasoningSummaryMode::Auto => {}
        }
        Ok((!thinking.is_empty()).then_some(thinking))
    }
}