mattrguerra/cortexmainPrivate on GitHub, published here in full

api.rs

Language Rust6.8 KB236 lines

crates/cortex-core/src/api.rs

  1. //! The shapes that cross from Rust to the interface (commands and events).
  2. use serde::{Deserialize, Serialize};
  3. use ts_rs::TS;
  4. use crate::pricing::ModelInfo;
  5. use crate::storage::{Conversation, Message, UsageRow};
  6. use crate::types::Usage;
  7. /// Events on a reply's stream, in order: deltas, maybe usage, then `done` or `error`.
  8. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
  9. #[serde(tag = "type", rename_all = "snake_case")]
  10. #[ts(export)]
  11. pub enum StreamEvent {
  12. Delta {
  13. text: String,
  14. },
  15. Reasoning {
  16. text: String,
  17. },
  18. Usage {
  19. usage: Usage,
  20. },
  21. /// The finished (or stopped) reply, as saved.
  22. Done {
  23. message: Message,
  24. },
  25. /// The reply failed; `message` is the saved row, with any partial text.
  26. Error {
  27. kind: String,
  28. error: String,
  29. message: Message,
  30. },
  31. }
  32. #[derive(Debug, Clone, Serialize, Deserialize, TS)]
  33. #[ts(export)]
  34. pub struct SendRequest {
  35. pub conversation_id: String,
  36. /// Empty when regenerating the last reply.
  37. pub text: String,
  38. pub attachment_ids: Vec<String>,
  39. }
  40. #[derive(Debug, Clone, Serialize, Deserialize, TS)]
  41. #[ts(export)]
  42. pub struct SendAck {
  43. pub stream_id: String,
  44. pub user_message: Option<Message>,
  45. pub assistant_message: Message,
  46. }
  47. #[derive(Debug, Clone, Serialize, Deserialize, TS)]
  48. #[ts(export)]
  49. pub struct ConversationView {
  50. pub conversation: Conversation,
  51. pub messages: Vec<Message>,
  52. }
  53. #[derive(Debug, Clone, Serialize, Deserialize, TS)]
  54. #[ts(export)]
  55. pub struct ModelsResponse {
  56. pub models: Vec<ModelInfo>,
  57. /// `live`, `snapshot` or `local`
  58. pub source: String,
  59. /// When the list was fetched (ms), or the snapshot's date.
  60. #[ts(type = "number | null")]
  61. pub fetched_at: Option<i64>,
  62. pub snapshot_date: Option<String>,
  63. /// Why a refresh failed, if it did (the list above is then the fallback).
  64. pub warning: Option<String>,
  65. }
  66. #[derive(Debug, Clone, Serialize, Deserialize, TS)]
  67. #[ts(export)]
  68. pub struct Estimate {
  69. #[ts(type = "number")]
  70. pub prompt_tokens: i64,
  71. /// The prompt's cost at the model's input price.
  72. #[ts(type = "number")]
  73. pub prompt_cost_nanousd: i64,
  74. #[ts(type = "number | null")]
  75. pub context_length: Option<i64>,
  76. }
  77. #[derive(Debug, Clone, Serialize, Deserialize, TS)]
  78. #[ts(export)]
  79. pub struct SpendSummary {
  80. #[ts(type = "number")]
  81. pub month_nanousd: i64,
  82. pub month_estimated: bool,
  83. pub budget_usd: Option<f64>,
  84. /// 0 below 80% of the budget, 1 from 80%, 2 at or over it.
  85. pub budget_level: u8,
  86. }
  87. #[derive(Debug, Clone, Serialize, Deserialize, TS)]
  88. #[ts(export)]
  89. pub struct UsageReport {
  90. pub by_model: Vec<UsageRow>,
  91. pub by_day: Vec<UsageRow>,
  92. #[ts(type = "number")]
  93. pub since_ms: i64,
  94. }
  95. #[derive(Debug, Clone, Serialize, Deserialize, TS)]
  96. #[ts(export)]
  97. pub struct AppInfo {
  98. pub version: String,
  99. /// `macos`, `linux` or `windows`
  100. pub platform: String,
  101. /// True when the window has native vibrancy behind it.
  102. pub vibrancy: bool,
  103. pub data_dir: String,
  104. }
  105. /// What happened when a key was saved. A key the provider refuses is not saved
  106. /// at all; that comes back as an error.
  107. #[derive(Debug, Clone, Serialize, Deserialize, TS)]
  108. #[ts(export)]
  109. pub struct KeySaved {
  110. /// False when the key was stored without being checked; `detail` says why.
  111. pub accepted: bool,
  112. /// Why the key could not be checked (the provider was unreachable, say).
  113. pub detail: Option<String>,
  114. }
  115. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
  116. #[serde(default)]
  117. #[ts(export)]
  118. pub struct Settings {
  119. /// `system`, `light` or `dark`
  120. pub theme: String,
  121. pub reduce_transparency: bool,
  122. /// macOS window material: `sidebar`, `window`, `fullscreen` or `hud`.
  123. pub material: String,
  124. /// How strongly panels tint the material, 0 to 100 percent.
  125. pub glass_tint: u8,
  126. pub shortcut: String,
  127. pub default_provider_id: String,
  128. pub default_model_id: String,
  129. pub quick_provider_id: String,
  130. pub quick_model_id: String,
  131. pub monthly_budget_usd: Option<f64>,
  132. pub confirm_over_budget: bool,
  133. pub vault_dir: Option<String>,
  134. /// `wiki` for `![[x.png]]`, `markdown` for `![x](attachments/x.png)`
  135. pub image_links: String,
  136. pub favorite_models: Vec<String>,
  137. /// Local model ids the person has marked as able to see images.
  138. pub vision_models: Vec<String>,
  139. pub send_on_enter: bool,
  140. }
  141. impl Default for Settings {
  142. fn default() -> Self {
  143. Self {
  144. theme: "system".into(),
  145. reduce_transparency: false,
  146. material: "sidebar".into(),
  147. glass_tint: 100,
  148. shortcut: "CommandOrControl+Shift+Space".into(),
  149. default_provider_id: crate::storage::DEEPINFRA_ID.into(),
  150. default_model_id: "Qwen/Qwen3-235B-A22B-Instruct-2507".into(),
  151. quick_provider_id: crate::storage::DEEPINFRA_ID.into(),
  152. quick_model_id: "google/gemma-3-12b-it".into(),
  153. monthly_budget_usd: Some(10.0),
  154. confirm_over_budget: false,
  155. vault_dir: None,
  156. image_links: "wiki".into(),
  157. favorite_models: vec![],
  158. vision_models: vec![],
  159. send_on_enter: true,
  160. }
  161. }
  162. }
  163. #[derive(Debug, Clone, Serialize, Deserialize, TS)]
  164. #[ts(export)]
  165. pub struct SaveSettingsResult {
  166. pub settings: Settings,
  167. /// Set when the new shortcut could not be registered.
  168. pub shortcut_error: Option<String>,
  169. }
  170. #[derive(Debug, Clone, Serialize, Deserialize, TS)]
  171. #[ts(export)]
  172. pub struct ExportResult {
  173. pub path: String,
  174. #[ts(type = "number")]
  175. pub files_copied: i64,
  176. }
  177. pub fn budget_level(spent_nanousd: i64, budget_usd: Option<f64>) -> u8 {
  178. match budget_usd {
  179. Some(b) if b > 0.0 => {
  180. let spent = crate::cost::to_usd(spent_nanousd);
  181. if spent >= b {
  182. 2
  183. } else if spent >= b * 0.8 {
  184. 1
  185. } else {
  186. 0
  187. }
  188. }
  189. _ => 0,
  190. }
  191. }
  192. #[cfg(test)]
  193. mod tests {
  194. use super::*;
  195. #[test]
  196. fn budget_levels() {
  197. assert_eq!(budget_level(0, Some(10.0)), 0);
  198. assert_eq!(budget_level(7_999_999_999, Some(10.0)), 0);
  199. assert_eq!(budget_level(8_000_000_000, Some(10.0)), 1);
  200. assert_eq!(budget_level(10_000_000_000, Some(10.0)), 2);
  201. assert_eq!(budget_level(99_000_000_000, None), 0);
  202. assert_eq!(budget_level(1, Some(0.0)), 0);
  203. }
  204. #[test]
  205. fn settings_tolerate_missing_fields() {
  206. let s: Settings = serde_json::from_str(r#"{"theme":"dark"}"#).unwrap();
  207. assert_eq!(s.theme, "dark");
  208. assert_eq!(s.shortcut, Settings::default().shortcut);
  209. }
  210. #[test]
  211. fn stream_event_wire_shape() {
  212. let v = serde_json::to_value(StreamEvent::Delta { text: "hi".into() }).unwrap();
  213. assert_eq!(v, serde_json::json!({"type": "delta", "text": "hi"}));
  214. }
  215. }