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    pub documents: DocumentCapabilities,
22}
23
24#[derive(Debug, Serialize)]
25#[serde(rename_all = "camelCase")]
26pub struct DocumentCapabilities {
27    /// Document parser types this instance can apply. `["pdf"]` when PDF
28    /// support is compiled in and enabled; empty otherwise. The SaaS gates the
29    /// `parsers` option and the upload UI on this.
30    pub parsers: Vec<&'static str>,
31    /// File-upload (`POST /v2/parse`) availability + limits.
32    pub file_upload: FileUploadCapabilities,
33}
34
35#[derive(Debug, Serialize)]
36#[serde(rename_all = "camelCase")]
37pub struct FileUploadCapabilities {
38    pub supported: bool,
39    pub endpoint: &'static str,
40    pub max_bytes: usize,
41    pub types: Vec<&'static str>,
42    /// pdf-inspector has no OCR — scanned/image PDFs yield empty/partial text.
43    pub ocr: bool,
44}
45
46#[derive(Debug, Serialize)]
47#[serde(rename_all = "camelCase")]
48pub struct LlmCapabilities {
49    /// Provider tags the server's dispatch knows about.
50    pub providers: Vec<&'static str>,
51    pub supports_base_url: bool,
52    /// True when a server-wide LLM key is configured (self-hosted /
53    /// no-SaaS deploys). SaaS-fronted deploys set
54    /// `CRW_DISABLE_SERVER_LLM_KEY=1` and rely on per-request BYOK.
55    pub server_key_configured: bool,
56    /// Configured server-side fan-out cap for LLM calls. 0 when no
57    /// server-side LLM config is present.
58    pub max_concurrency: usize,
59    /// Header name the server will look for on LLM-touching requests
60    /// (`None` means no header guard).
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub require_byok_header: Option<String>,
63}
64
65#[derive(Debug, Serialize)]
66#[serde(rename_all = "camelCase")]
67pub struct FormatCapabilities {
68    pub supported: Vec<&'static str>,
69    /// Change-tracking diff modes this instance supports. Empty when the
70    /// `changeTracking` format is unavailable. The SaaS capability-gate checks
71    /// `supported` contains `"changeTracking"` before emitting monitor scrapes.
72    pub change_tracking_modes: Vec<&'static str>,
73}
74
75#[derive(Debug, Serialize)]
76#[serde(rename_all = "camelCase")]
77pub struct SearchCapabilities {
78    pub answer: bool,
79    pub summarize_results: bool,
80}
81
82pub async fn capabilities(State(state): State<AppState>) -> Json<Capabilities> {
83    let llm_cfg = state.config.extraction.llm.as_ref();
84    Json(Capabilities {
85        version: env!("CARGO_PKG_VERSION"),
86        llm: LlmCapabilities {
87            providers: vec![
88                "anthropic",
89                "openai",
90                "deepseek",
91                "openai-compatible",
92                "azure",
93            ],
94            supports_base_url: true,
95            server_key_configured: llm_cfg.map(|c| !c.api_key.is_empty()).unwrap_or(false),
96            max_concurrency: llm_cfg.map(|c| c.max_concurrency).unwrap_or(0),
97            require_byok_header: llm_cfg.and_then(|c| c.require_byok_header.clone()),
98        },
99        formats: FormatCapabilities {
100            supported: vec![
101                "markdown",
102                "html",
103                "rawHtml",
104                "plainText",
105                "links",
106                "json",
107                "summary",
108                "changeTracking",
109            ],
110            change_tracking_modes: vec!["gitDiff", "json"],
111        },
112        search: SearchCapabilities {
113            answer: true,
114            summarize_results: true,
115        },
116        documents: {
117            let pdf_on = crw_extract::pdf::PDF_SUPPORTED && state.config.document.enabled;
118            DocumentCapabilities {
119                parsers: if pdf_on { vec!["pdf"] } else { vec![] },
120                file_upload: FileUploadCapabilities {
121                    supported: pdf_on,
122                    endpoint: "/v2/parse",
123                    max_bytes: state.config.document.max_upload_bytes,
124                    types: if pdf_on {
125                        vec!["application/pdf"]
126                    } else {
127                        vec![]
128                    },
129                    ocr: false,
130                },
131            }
132        },
133    })
134}