cortiq-gateway 0.2.45

Universal LLM gateway with intelligent routing and an embedded multilingual admin console
//! Inbound OpenAI Models adapter: `GET /v1/models`.
//! Lists everything a client can address: the virtual `cortiq-auto`, the
//! configured `[[models]]` pool, and the managed local `[[cmf.servers]]`.
//!
//! The managed CMF servers belong here because they are routable model ids like
//! any other — omitting them left OpenAI clients (Open WebUI among them) unable
//! to pin a local model even though the gateway would happily serve it.

use crate::error::{GatewayError, Result};
use crate::state::SharedState;
use axum::{extract::State, response::IntoResponse, routing::get, Json, Router};

pub fn routes() -> Router<SharedState> {
    Router::new().route("/v1/models", get(handler))
}

fn now_secs() -> u64 {
    std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap_or_default()
        .as_secs()
}

async fn handler(State(state): State<SharedState>) -> Result<impl IntoResponse> {
    let live = state.live();
    if !live.cfg.protocols.openai_models {
        return Err(GatewayError::InvalidRequest(
            "openai_models protocol is disabled".into(),
        ));
    }
    let created = now_secs();
    let mut data = vec![serde_json::json!({
        "id": "cortiq-auto",
        "object": "model",
        "created": created,
        "owned_by": "cortiq",
    })];
    let mut seen: std::collections::HashSet<String> =
        std::collections::HashSet::from(["cortiq-auto".to_string()]);

    for m in &live.cfg.models {
        if !seen.insert(m.id.clone()) {
            continue;
        }
        data.push(serde_json::json!({
            "id": m.id,
            "object": "model",
            "created": created,
            "owned_by": m.provider,
        }));
    }

    // Managed local CMF servers. Health is reported alongside (a non-standard
    // field clients ignore) rather than used as a filter: a large `.cmf` takes
    // minutes to load, and a model that vanishes from the list while it warms up
    // is worse than one marked "not ready yet".
    let cmf = state.cmf.status();
    for s in live.cfg.cmf.effective_servers() {
        if !seen.insert(s.id.clone()) {
            continue; // a static [[models]] entry with the same id wins
        }
        let status = cmf.servers.iter().find(|st| st.id == s.id);
        data.push(serde_json::json!({
            "id": s.id,
            "object": "model",
            "created": created,
            "owned_by": "cortiq-cmf",
            "cortiq": {
                "kind": "cmf",
                "local": true,
                "running": status.map(|st| st.running).unwrap_or(false),
                "healthy": status.map(|st| st.healthy).unwrap_or(false),
            },
        }));
    }

    Ok(Json(serde_json::json!({ "object": "list", "data": data })))
}