aura-gui 0.2.2

UI nativa acelerada por GPU para escritorio, Android e iOS, en un solo crate: estilo tipo Tailwind, componentes, sombras, animaciones y agentes de IA. Reposo a cero. Licencia AGPL-3.0 o comercial.
//! OpenCode Go y Zen: piden que el cliente se identifique con su propio User-Agent y mande una sesión estable por
//! conversación (`x-opencode-session`) para enrutar y aprovechar la caché del prompt
//! (<https://opencode.ai/docs/go/>). Sus direcciones y claves están en `catalog`.

use crate::ai::Request;
use std::hash::{Hash, Hasher};

/// Chat Completions de Go (los modelos de protocolo Anthropic o Responses no pasan por aquí).
pub const BASE_URL: &str = "https://opencode.ai/inference/go/openai/v1";
/// La lista de modelos de Go vive fuera de `/openai`.
const GO_MODELS_URL: &str = "https://opencode.ai/inference/go/v1/models";

/// `true` si la dirección es de OpenCode (también si el usuario la escribió como `base_url`).
pub(crate) fn is_opencode(base_url: &str) -> bool {
    base_url.starts_with("https://opencode.ai/")
}

/// Lista de modelos de Go; Zen la tiene en el sitio de siempre (`{base}/models`).
pub(crate) fn models_url(base_url: &str) -> Option<String> {
    base_url.starts_with("https://opencode.ai/inference/go").then(|| GO_MODELS_URL.to_owned())
}

/// User-Agent de Aura y la sesión: un hash del primer mensaje, que no cambia en toda la conversación.
pub(crate) fn headers(request: &Request) -> [(String, String); 2] {
    let mut hasher = std::collections::hash_map::DefaultHasher::new();
    if let Some(first) = request.messages.first() {
        format!("{:?}", first.blocks).hash(&mut hasher);
    }
    [
        ("user-agent".to_owned(), concat!("aura/", env!("CARGO_PKG_VERSION")).to_owned()),
        ("x-opencode-session".to_owned(), format!("aura-{:016x}", hasher.finish())),
    ]
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::ai::{Message, Provider, ProviderKind};

    fn request(messages: Vec<Message>) -> Request {
        Request { model: "deepseek-v4.1-flash".into(), system: None, messages, tools: Vec::new(), max_tokens: 1 }
    }

    #[test]
    fn la_sesion_es_estable_en_la_conversacion_y_distinta_entre_conversaciones() {
        let first = headers(&request(vec![Message::user("hola")]));
        let later = headers(&request(vec![Message::user("hola"), Message::user("sigue")]));
        let other = headers(&request(vec![Message::user("otra cosa")]));
        assert_eq!(first[1], later[1], "el mismo hilo conserva la sesión");
        assert_ne!(first[1], other[1]);
        assert!(first[0].1.starts_with("aura/"), "se identifica como Aura y no como la librería HTTP");
    }

    #[test]
    fn go_usa_su_lista_de_modelos_y_zen_la_de_siempre() {
        let at = |base_url: &str| {
            Provider { kind: ProviderKind::OpenAiCompatible, base_url: base_url.into(), api_key: None, effort: None }
                .models_endpoint()
                .0
        };
        assert_eq!(at(BASE_URL), GO_MODELS_URL);
        assert_eq!(at("https://opencode.ai/zen/v1"), "https://opencode.ai/zen/v1/models");
        assert!(is_opencode(BASE_URL) && !is_opencode("https://api.openai.com/v1"));
    }
}