darwincode 1.9.91

The open source terminal AI coding agent
use anyhow::Result;
use std::thread;
use std::time::Duration;

pub fn read_error(error: ureq::Error) -> anyhow::Error {
    match error {
        ureq::Error::Status(code, response) => {
            let message = response
                .into_string()
                .unwrap_or_else(|_| "unknown error".to_owned());
            anyhow::anyhow!("API request failed with HTTP {code}: {message}")
        }
        ureq::Error::Transport(error) => anyhow::anyhow!("API request failed: {error}"),
    }
}

pub fn lowercase_types(value: &mut serde_json::Value) {
    if let Some(obj) = value.as_object_mut() {
        if let Some(s) = obj
            .get("type")
            .and_then(|t| t.as_str())
            .map(|s| s.to_lowercase())
        {
            obj.insert("type".to_owned(), serde_json::json!(s));
        }
        for val in obj.values_mut() {
            lowercase_types(val);
        }
    } else if let Some(arr) = value.as_array_mut() {
        for val in arr {
            lowercase_types(val);
        }
    }
}

pub fn model_supports_vision(model: &str, base_url: &str) -> bool {
    let m = model.to_lowercase();
    let b = base_url.to_lowercase();

    if m.contains("deepseek")
        || b.contains("deepseek")
        || m.contains("coder")
        || m.contains("reasoner")
        || m.contains("r1")
    {
        return false;
    }
    true
}

pub fn execute_with_retry<F>(
    agent: &ureq::Agent,
    make_request: F,
) -> Result<ureq::Response, anyhow::Error>
where
    F: Fn(&ureq::Agent) -> Result<ureq::Response, ureq::Error>,
{
    let mut backoff = Duration::from_millis(500);
    let max_backoff = Duration::from_secs(30);
    let mut attempt = 0;
    loop {
        attempt += 1;
        match make_request(agent) {
            Ok(resp) => return Ok(resp),
            Err(err) => {
                let is_ret = match &err {
                    ureq::Error::Transport(_) => true,
                    ureq::Error::Status(429, _) => true,
                    ureq::Error::Status(code, _) => *code >= 500 && *code < 600,
                };
                if is_ret && attempt < 5 {
                    thread::sleep(backoff);
                    backoff = (backoff * 2).min(max_backoff);
                } else {
                    return Err(read_error(err));
                }
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_model_supports_vision() {
        assert!(model_supports_vision("gpt-4o", "https://api.openai.com/v1"));
        assert!(model_supports_vision(
            "claude-3-5-sonnet",
            "https://api.anthropic.com/v1"
        ));
        assert!(model_supports_vision(
            "gemini-1.5-flash",
            "https://generativelanguage.googleapis.com"
        ));
        assert!(model_supports_vision(
            "big-pickle",
            "https://opencode.ai/zen/v1"
        ));
        assert!(!model_supports_vision(
            "deepseek-chat",
            "https://api.deepseek.com/v1"
        ));
        assert!(!model_supports_vision(
            "deepseek-coder",
            "https://api.deepseek.com/v1"
        ));
        assert!(!model_supports_vision(
            "deepseek-reasoner",
            "https://api.deepseek.com/v1"
        ));
        assert!(!model_supports_vision(
            "qwen2.5-coder",
            "https://api.openai.com/v1"
        ));
        assert!(!model_supports_vision(
            "big-pickle",
            "https://api.deepseek.com/v1"
        ));
    }
}