openkind_api/
playground.rs1use axum::http::header::{CACHE_CONTROL, CONTENT_TYPE};
9use axum::http::HeaderValue;
10use axum::response::IntoResponse;
11
12const PLAYGROUND_HTML: &str = include_str!("../assets/playground.html");
14
15const PLAYGROUND_MARKER: &str = "openkind playground";
17
18pub async fn playground_page() -> impl IntoResponse {
24 debug_assert!(PLAYGROUND_HTML.contains(PLAYGROUND_MARKER));
25 (
26 [
27 (
28 CONTENT_TYPE,
29 HeaderValue::from_static("text/html; charset=utf-8"),
30 ),
31 (CACHE_CONTROL, HeaderValue::from_static("no-store")),
32 (axum::http::header::CONTENT_SECURITY_POLICY, HeaderValue::from_static("default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; connect-src 'self'; img-src 'self' data:; base-uri 'none'; frame-ancestors 'none'; form-action 'none'")),
33 (axum::http::header::X_CONTENT_TYPE_OPTIONS, HeaderValue::from_static("nosniff")),
34 ],
35 PLAYGROUND_HTML,
36 )
37}
38
39#[derive(serde::Serialize)]
41pub struct PlaygroundModel {
42 pub name: String,
44 pub description: String,
46 pub source: String,
48 pub loaded: bool,
50 pub manageable: bool,
52}
53
54#[async_trait::async_trait]
56pub trait PlaygroundModels: Send + Sync {
57 async fn list(&self) -> Result<Vec<PlaygroundModel>, crate::ApiError>;
59 async fn set_loaded(&self, name: String, loaded: bool) -> Result<(), crate::ApiError>;
61}
62
63#[derive(serde::Deserialize)]
64#[serde(deny_unknown_fields)]
65pub(crate) struct ModelAction {
66 name: String,
67 loaded: bool,
68}
69
70pub(crate) async fn list_models(
71 axum::extract::State(state): axum::extract::State<std::sync::Arc<crate::AppState>>,
72) -> Result<impl IntoResponse, crate::ApiError> {
73 let manager = state.playground_models.as_ref().ok_or_else(|| {
74 crate::ApiError::InvalidBody("Model controls are unavailable on this daemon".into())
75 })?;
76 Ok((
77 [(CACHE_CONTROL, "no-store")],
78 axum::Json(serde_json::json!({"models": manager.list().await?})),
79 ))
80}
81
82pub(crate) async fn change_model(
83 axum::extract::State(state): axum::extract::State<std::sync::Arc<crate::AppState>>,
84 headers: axum::http::HeaderMap,
85 axum::Json(action): axum::Json<ModelAction>,
86) -> Result<impl IntoResponse, crate::ApiError> {
87 if headers
90 .get("x-openkind-playground")
91 .and_then(|v| v.to_str().ok())
92 != Some("1")
93 || headers
94 .get("sec-fetch-site")
95 .is_some_and(|v| v == "cross-site")
96 {
97 return Err(crate::ApiError::Unauthorized);
98 }
99 let manager = state.playground_models.as_ref().ok_or_else(|| {
100 crate::ApiError::InvalidBody("Model controls are unavailable on this daemon".into())
101 })?;
102 manager.set_loaded(action.name, action.loaded).await?;
103 Ok((
104 [(CACHE_CONTROL, "no-store")],
105 axum::Json(serde_json::json!({"ok": true})),
106 ))
107}