Skip to main content

crw_server/routes/
capabilities.rs

1//! `GET /v1/capabilities` — surface what this opencore instance supports.
2//!
3//! SaaS / dashboard frontends call this on boot to decide which provider
4//! buttons / formats to surface. Closes the "SaaS UI shipped before
5//! opencore rollout" silent-failure mode by giving callers a way to ask
6//! "do you actually do this?" before making a real request.
7
8use axum::Json;
9use axum::extract::State;
10use serde::Serialize;
11
12use crate::state::AppState;
13
14#[derive(Debug, Serialize)]
15#[serde(rename_all = "camelCase")]
16pub struct Capabilities {
17    pub version: &'static str,
18    pub llm: LlmCapabilities,
19    pub formats: FormatCapabilities,
20    pub search: SearchCapabilities,
21}
22
23#[derive(Debug, Serialize)]
24#[serde(rename_all = "camelCase")]
25pub struct LlmCapabilities {
26    /// Provider tags the server's dispatch knows about.
27    pub providers: Vec<&'static str>,
28    pub supports_base_url: bool,
29    /// True when a server-wide LLM key is configured (self-hosted /
30    /// no-SaaS deploys). SaaS-fronted deploys set
31    /// `CRW_DISABLE_SERVER_LLM_KEY=1` and rely on per-request BYOK.
32    pub server_key_configured: bool,
33    /// Configured server-side fan-out cap for LLM calls. 0 when no
34    /// server-side LLM config is present.
35    pub max_concurrency: usize,
36    /// Header name the server will look for on LLM-touching requests
37    /// (`None` means no header guard).
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub require_byok_header: Option<String>,
40}
41
42#[derive(Debug, Serialize)]
43#[serde(rename_all = "camelCase")]
44pub struct FormatCapabilities {
45    pub supported: Vec<&'static str>,
46    /// Change-tracking diff modes this instance supports. Empty when the
47    /// `changeTracking` format is unavailable. The SaaS capability-gate checks
48    /// `supported` contains `"changeTracking"` before emitting monitor scrapes.
49    pub change_tracking_modes: Vec<&'static str>,
50}
51
52#[derive(Debug, Serialize)]
53#[serde(rename_all = "camelCase")]
54pub struct SearchCapabilities {
55    pub answer: bool,
56    pub summarize_results: bool,
57}
58
59pub async fn capabilities(State(state): State<AppState>) -> Json<Capabilities> {
60    let llm_cfg = state.config.extraction.llm.as_ref();
61    Json(Capabilities {
62        version: env!("CARGO_PKG_VERSION"),
63        llm: LlmCapabilities {
64            providers: vec![
65                "anthropic",
66                "openai",
67                "deepseek",
68                "openai-compatible",
69                "azure",
70            ],
71            supports_base_url: true,
72            server_key_configured: llm_cfg.map(|c| !c.api_key.is_empty()).unwrap_or(false),
73            max_concurrency: llm_cfg.map(|c| c.max_concurrency).unwrap_or(0),
74            require_byok_header: llm_cfg.and_then(|c| c.require_byok_header.clone()),
75        },
76        formats: FormatCapabilities {
77            supported: vec![
78                "markdown",
79                "html",
80                "rawHtml",
81                "plainText",
82                "links",
83                "json",
84                "summary",
85                "changeTracking",
86            ],
87            change_tracking_modes: vec!["gitDiff", "json"],
88        },
89        search: SearchCapabilities {
90            answer: true,
91            summarize_results: true,
92        },
93    })
94}