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