va-ai-api-bridge 0.1.7

Protocol translation primitives for AI API request and response shapes.
Documentation
use serde_json::{json, Value};

use crate::ReasoningConfig;

use super::super::common::empty_extensions;

pub(crate) fn reasoning_from_anthropic_thinking(thinking: Value) -> ReasoningConfig {
    let mut extensions = empty_extensions();
    let thinking_type = thinking
        .get("type")
        .and_then(Value::as_str)
        .map(ToString::to_string);
    if let Some(thinking_type) = &thinking_type {
        extensions.insert(
            "anthropicThinkingType".to_string(),
            Value::String(thinking_type.clone()),
        );
    }

    ReasoningConfig {
        effort: None,
        budget_tokens: thinking.get("budget_tokens").and_then(Value::as_u64),
        visible: None,
        extensions,
    }
}

pub(crate) fn anthropic_thinking_from_reasoning(
    reasoning: &ReasoningConfig,
    max_tokens: Option<u64>,
) -> Option<Value> {
    let effort = reasoning.effort.as_deref();
    if matches!(effort, Some("none" | "disabled")) {
        return None;
    }

    let wants_thinking = reasoning.budget_tokens.is_some()
        || reasoning.visible == Some(true)
        || matches!(
            effort,
            Some("minimal" | "low" | "medium" | "high" | "xhigh" | "enabled" | "adaptive")
        )
        || reasoning
            .extensions
            .get("anthropicThinkingType")
            .and_then(Value::as_str)
            .is_some_and(|kind| matches!(kind, "enabled" | "adaptive"));
    if !wants_thinking {
        return None;
    }

    let effective_max_tokens = max_tokens.or(Some(4096));
    let desired_budget = reasoning
        .budget_tokens
        .or_else(|| anthropic_budget_for_effort(effort));
    let budget_tokens = desired_budget
        .and_then(|budget| clamp_anthropic_thinking_budget(budget, effective_max_tokens))?;

    Some(json!({
        "type": "enabled",
        "budget_tokens": budget_tokens
    }))
}

fn anthropic_budget_for_effort(effort: Option<&str>) -> Option<u64> {
    Some(match effort {
        Some("minimal" | "low") => 1024,
        Some("high") => 4096,
        Some("xhigh") => 8192,
        Some("medium" | "enabled" | "adaptive") | None => 2048,
        Some(_) => return None,
    })
}

fn clamp_anthropic_thinking_budget(budget: u64, max_tokens: Option<u64>) -> Option<u64> {
    let Some(max_tokens) = max_tokens else {
        return Some(budget.max(1024));
    };
    if max_tokens <= 1024 {
        return None;
    }
    let cap = if max_tokens >= 4096 {
        max_tokens / 2
    } else {
        max_tokens - 1
    };
    Some(budget.max(1024).min(cap).min(max_tokens - 1))
}