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 screenshot: ScreenshotCapabilities,
pub renderers: RendererCapabilities,
pub extract: ExtractCapabilities,
pub documents: DocumentCapabilities,
pub limits: Limits,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ExtractCapabilities {
pub supported: bool,
pub max_urls: usize,
pub per_field_attribution: bool,
pub max_output_tokens: u32,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DocumentCapabilities {
pub parsers: Vec<&'static str>,
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>,
pub ocr: bool,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct LlmCapabilities {
pub providers: Vec<&'static str>,
pub supports_base_url: bool,
pub server_key_configured: bool,
pub max_concurrency: usize,
#[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>,
pub llm_required: Vec<&'static str>,
pub change_tracking_modes: Vec<&'static str>,
pub change_tracking_modes_llm_required: Vec<&'static str>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SearchCapabilities {
pub supported: bool,
pub answer: bool,
pub summarize_results: bool,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ScreenshotCapabilities {
pub supported: bool,
pub full_page: bool,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RendererCapabilities {
pub available: Vec<String>,
pub mode: crw_core::config::RendererMode,
#[serde(skip_serializing_if = "Option::is_none")]
pub render_js_default: Option<bool>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Limits {
pub max_batch_urls: usize,
pub max_extract_urls: usize,
pub search_default_limit: u32,
pub search_max_limit: u32,
pub max_upload_bytes: usize,
}
const BASE_FORMATS: &[&str] = &[
"markdown",
"html",
"rawHtml",
"plainText",
"links",
"json",
"summary",
"changeTracking",
];
const LLM_REQUIRED_FORMATS: &[&str] = &["json", "summary"];
const CHANGE_TRACKING_MODES: &[&str] = &["gitDiff", "json"];
const LLM_REQUIRED_CHANGE_TRACKING_MODES: &[&str] = &["json"];
pub async fn capabilities(State(state): State<AppState>) -> Json<Capabilities> {
let llm_cfg = state.config.extraction.llm.as_ref();
let server_key_configured = llm_cfg.map(|c| !c.api_key.is_empty()).unwrap_or(false);
let byok_header_required = llm_cfg.is_some_and(|c| c.require_byok_header.is_some());
let llm_ready_without_caller_key = server_key_configured && !byok_header_required;
let search_supported = state.searxng.is_some();
let search_llm_ready = search_supported && server_key_configured;
let screenshot_supported = state.renderer.supports_screenshot();
let mut formats: Vec<&'static str> = BASE_FORMATS.to_vec();
if screenshot_supported {
formats.push("screenshot");
}
let pdf_on = crw_extract::pdf::PDF_SUPPORTED && state.config.document.enabled;
let max_upload_bytes = crate::routes::v2::parse::effective_max_upload_bytes(&state.config);
let upload_on = pdf_on && max_upload_bytes > 0;
Json(Capabilities {
version: env!("CARGO_PKG_VERSION"),
llm: LlmCapabilities {
providers: crw_extract::llm::SUPPORTED_PROVIDERS.to_vec(),
supports_base_url: true,
server_key_configured,
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: formats,
llm_required: LLM_REQUIRED_FORMATS.to_vec(),
change_tracking_modes: CHANGE_TRACKING_MODES.to_vec(),
change_tracking_modes_llm_required: LLM_REQUIRED_CHANGE_TRACKING_MODES.to_vec(),
},
search: SearchCapabilities {
supported: search_supported,
answer: search_llm_ready,
summarize_results: search_llm_ready,
},
screenshot: ScreenshotCapabilities {
supported: screenshot_supported,
full_page: screenshot_supported,
},
renderers: RendererCapabilities {
available: state
.renderer
.js_renderer_names()
.into_iter()
.map(String::from)
.collect(),
mode: state.config.renderer.mode,
render_js_default: state.config.renderer.render_js_default,
},
extract: ExtractCapabilities {
supported: llm_ready_without_caller_key,
max_urls: state.config.crawler.max_extract_urls,
per_field_attribution: true,
max_output_tokens: state
.config
.extraction
.llm
.as_ref()
.map_or(4096, |c| c.max_tokens),
},
documents: DocumentCapabilities {
parsers: if pdf_on { vec!["pdf"] } else { vec![] },
file_upload: FileUploadCapabilities {
supported: upload_on,
endpoint: "/v2/parse",
max_bytes: max_upload_bytes,
types: if upload_on {
vec!["application/pdf"]
} else {
vec![]
},
ocr: false,
},
},
limits: Limits {
max_batch_urls: state.config.crawler.max_batch_urls,
max_extract_urls: state.config.crawler.max_extract_urls,
search_default_limit: state.config.search.default_limit,
search_max_limit: state.config.search.max_limit,
max_upload_bytes,
},
})
}