Skip to main content

core_api/node/contracts/
llm.rs

1use serde::{Deserialize, Serialize};
2
3pub const VERSION: &str = crate::node::V1;
4pub const NODE_TYPE: &str = "llm";
5pub const RUNTIME_TARGET_PREFIX: &str = "/llm/app/";
6pub const RUNTIME_STATUS: &str = "runtime/status";
7pub const RUNTIME_RETRY: &str = "runtime/retry";
8pub const MODEL_LIST: &str = "model/list";
9pub const MODEL_DOWNLOAD: &str = "model/download";
10pub const MODEL_DOWNLOAD_STATUS: &str = "model/download/status";
11pub const MODEL_DOWNLOAD_CANCEL: &str = "model/download/cancel";
12pub const MODEL_IMPORT_START: &str = "model/import/start";
13pub const MODEL_IMPORT_STATUS: &str = "model/import/status";
14pub const MODEL_IMPORT_FINALIZE: &str = "model/import/finalize";
15pub const MODEL_IMPORT_CANCEL: &str = "model/import/cancel";
16pub const MODEL_IMPORT_UPLOAD_PATH_PREFIX: &str = "/llm/model/import/upload";
17pub const MODEL_IMPORT_OFFSET_HEADER: &str = "x-upload-offset";
18pub const MODEL_IMPORT_CHUNK_SHA256_HEADER: &str = "x-chunk-sha256";
19pub const MODEL_DELETE: &str = "model/delete";
20
21#[derive(Debug, Clone, Default, Serialize, Deserialize)]
22#[serde(rename_all = "camelCase", deny_unknown_fields)]
23pub struct EmptyRequest {}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
26#[serde(rename_all = "camelCase", deny_unknown_fields)]
27pub struct ModelDownloadRequest {
28    pub model_id: String,
29    pub idempotency_key: String,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
33#[serde(rename_all = "camelCase", deny_unknown_fields)]
34pub struct ModelRequest {
35    pub model_id: String,
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize)]
39#[serde(rename_all = "camelCase", deny_unknown_fields)]
40pub struct ModelImportStartRequest {
41    pub display_name: String,
42    pub file_name: String,
43    pub artifact_sha256: String,
44    pub size_bytes: u64,
45    pub idempotency_key: String,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
49#[serde(rename_all = "camelCase", deny_unknown_fields)]
50pub struct ModelImportOperationRequest {
51    pub operation_id: String,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
55#[serde(rename_all = "camelCase")]
56pub enum ModelImportState {
57    Uploading,
58    Importing,
59    Verifying,
60    Available,
61    Cancelling,
62    Cancelled,
63    Error,
64}
65
66impl ModelImportState {
67    pub fn is_terminal(&self) -> bool {
68        matches!(self, Self::Available | Self::Cancelled | Self::Error)
69    }
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
73#[serde(rename_all = "camelCase")]
74pub struct ModelImportError {
75    pub code: String,
76    pub detail: String,
77    pub retryable: bool,
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize)]
81#[serde(rename_all = "camelCase")]
82pub struct ModelImportSnapshot {
83    pub operation_id: String,
84    pub model_id: String,
85    pub display_name: String,
86    pub artifact_sha256: String,
87    pub size_bytes: u64,
88    pub received_bytes: u64,
89    pub state: ModelImportState,
90    pub status: String,
91    pub started_at_epoch_ms: i64,
92    pub updated_at_epoch_ms: i64,
93    pub cancellable: bool,
94    #[serde(default, skip_serializing_if = "Option::is_none")]
95    pub error: Option<ModelImportError>,
96}
97
98#[derive(Debug, Clone, Serialize, Deserialize)]
99#[serde(rename_all = "camelCase")]
100pub struct ModelImportUploadGrant {
101    pub bearer_token: String,
102    pub chunk_size_bytes: u64,
103    pub expires_at_epoch_ms: i64,
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize)]
107#[serde(rename_all = "camelCase")]
108pub struct ModelImportStartResponse {
109    pub ok: bool,
110    pub started: bool,
111    pub operation: ModelImportSnapshot,
112    #[serde(default, skip_serializing_if = "Option::is_none")]
113    pub upload: Option<ModelImportUploadGrant>,
114    #[serde(default, skip_serializing_if = "Option::is_none")]
115    pub reason: Option<String>,
116    #[serde(default, skip_serializing_if = "Option::is_none")]
117    pub detail: Option<String>,
118}
119
120#[derive(Debug, Clone, Serialize, Deserialize)]
121#[serde(rename_all = "camelCase")]
122pub struct ModelImportStatusResponse {
123    pub ok: bool,
124    #[serde(default, skip_serializing_if = "Option::is_none")]
125    pub operation: Option<ModelImportSnapshot>,
126    #[serde(default, skip_serializing_if = "Option::is_none")]
127    pub reason: Option<String>,
128    #[serde(default, skip_serializing_if = "Option::is_none")]
129    pub detail: Option<String>,
130}
131
132#[derive(Debug, Clone, Serialize, Deserialize)]
133#[serde(rename_all = "camelCase")]
134pub struct ModelImportChunkResponse {
135    pub ok: bool,
136    pub operation_id: String,
137    pub received_bytes: u64,
138    #[serde(default, skip_serializing_if = "Option::is_none")]
139    pub reason: Option<String>,
140    #[serde(default, skip_serializing_if = "Option::is_none")]
141    pub detail: Option<String>,
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147
148    #[test]
149    fn import_start_request_is_strict_and_uses_public_names() {
150        let value = serde_json::to_value(ModelImportStartRequest {
151            display_name: "Local model".to_string(),
152            file_name: "model.gguf".to_string(),
153            artifact_sha256: "a".repeat(64),
154            size_bytes: 1024,
155            idempotency_key: "import-1".to_string(),
156        })
157        .expect("serialize import request");
158        assert_eq!(value["artifactSha256"], "a".repeat(64));
159        assert!(value.get("sourcePath").is_none());
160        assert!(value.get("scopeId").is_none());
161
162        let mut invalid = value;
163        invalid["sourcePath"] = serde_json::json!("/private/model.gguf");
164        assert!(serde_json::from_value::<ModelImportStartRequest>(invalid).is_err());
165    }
166
167    #[test]
168    fn import_terminal_states_are_explicit() {
169        assert!(ModelImportState::Available.is_terminal());
170        assert!(ModelImportState::Cancelled.is_terminal());
171        assert!(ModelImportState::Error.is_terminal());
172        assert!(!ModelImportState::Uploading.is_terminal());
173    }
174}