Skip to main content

loadmaster_probe/
detect.rs

1//! Which runtime is behind an OpenAI-compatible URL, from its identifying endpoints.
2
3use serde::Serialize;
4use serde_json::Value;
5
6use crate::client::Client;
7
8#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
9pub struct Detected {
10    /// `llama.cpp`, `ollama`, `vllm`, `lmstudio`, `sglang`, `tabbyapi`, or `unknown`.
11    pub runtime: String,
12    pub version: Option<String>,
13    /// The chat template the runtime reports using (llama.cpp `/props`).
14    #[serde(skip)]
15    pub chat_template: Option<String>,
16    pub models: Vec<String>,
17}
18
19fn str_of(v: &Value, k: &str) -> Option<String> {
20    v.get(k).and_then(Value::as_str).map(str::to_owned)
21}
22
23/// Probe in an order where no runtime matches another's check: llama.cpp `/props`,
24/// SGLang `/get_model_info`, LM Studio `/api/v0/models`, Ollama `/api/version`, TabbyAPI
25/// `/v1/model`, vLLM `/version`; then `owned_by` in `/v1/models`.
26pub async fn detect(c: &Client) -> Detected {
27    let models_body = c.get("/v1/models").await;
28    let models: Vec<String> = models_body
29        .as_ref()
30        .and_then(|(_, _, b)| b["data"].as_array().cloned())
31        .unwrap_or_default()
32        .iter()
33        .filter_map(|m| str_of(m, "id"))
34        .collect();
35    let found = |runtime: &str, version: Option<String>, chat_template: Option<String>| Detected {
36        runtime: runtime.into(),
37        version,
38        chat_template,
39        models: models.clone(),
40    };
41    let ok = |r: &Option<(u16, reqwest::header::HeaderMap, Value)>| -> Option<Value> {
42        r.as_ref()
43            .filter(|(s, _, b)| *s == 200 && b.is_object())
44            .map(|(_, _, b)| b.clone())
45    };
46
47    if let Some(b) = ok(&c.get("/props").await)
48        && (b.get("chat_template").is_some()
49            || b.get("build_info").is_some()
50            || b.get("default_generation_settings").is_some())
51    {
52        return found(
53            "llama.cpp",
54            str_of(&b, "build_info"),
55            str_of(&b, "chat_template"),
56        );
57    }
58    if let Some(b) = ok(&c.get("/get_model_info").await)
59        && b.get("model_path").is_some()
60    {
61        return found("sglang", str_of(&b, "version"), None);
62    }
63    if let Some(b) = ok(&c.get("/api/v0/models").await)
64        && b.get("data").is_some()
65    {
66        return found("lmstudio", None, None);
67    }
68    if let Some(b) = ok(&c.get("/api/version").await)
69        && b.get("version").is_some()
70    {
71        return found("ollama", str_of(&b, "version"), None);
72    }
73    if let Some(b) = ok(&c.get("/v1/model").await)
74        && (b["owned_by"] == "tabbyAPI" || b.get("parameters").is_some())
75    {
76        return found("tabbyapi", None, None);
77    }
78    if let Some(b) = ok(&c.get("/version").await)
79        && let Some(v) = str_of(&b, "version")
80    {
81        return found("vllm", Some(v), None);
82    }
83    let owned = models_body
84        .as_ref()
85        .and_then(|(_, _, b)| b["data"][0]["owned_by"].as_str().map(str::to_owned))
86        .unwrap_or_default();
87    let runtime = match owned.as_str() {
88        "llamacpp" => "llama.cpp",
89        "vllm" => "vllm",
90        "library" => "ollama",
91        "sglang" => "sglang",
92        "tabbyAPI" => "tabbyapi",
93        _ => "unknown",
94    };
95    found(runtime, None, None)
96}