api.rs
Language Rust6.8 KB236 lines
//! The shapes that cross from Rust to the interface (commands and events).use serde::{Deserialize, Serialize};use ts_rs::TS;use crate::pricing::ModelInfo;use crate::storage::{Conversation, Message, UsageRow};use crate::types::Usage;/// Events on a reply's stream, in order: deltas, maybe usage, then `done` or `error`.#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]#[serde(tag = "type", rename_all = "snake_case")]#[ts(export)]pub enum StreamEvent {Delta {text: String,},Reasoning {text: String,},Usage {usage: Usage,},/// The finished (or stopped) reply, as saved.Done {message: Message,},/// The reply failed; `message` is the saved row, with any partial text.Error {kind: String,error: String,message: Message,},}#[derive(Debug, Clone, Serialize, Deserialize, TS)]#[ts(export)]pub struct SendRequest {pub conversation_id: String,/// Empty when regenerating the last reply.pub text: String,pub attachment_ids: Vec<String>,}#[derive(Debug, Clone, Serialize, Deserialize, TS)]#[ts(export)]pub struct SendAck {pub stream_id: String,pub user_message: Option<Message>,pub assistant_message: Message,}#[derive(Debug, Clone, Serialize, Deserialize, TS)]#[ts(export)]pub struct ConversationView {pub conversation: Conversation,pub messages: Vec<Message>,}#[derive(Debug, Clone, Serialize, Deserialize, TS)]#[ts(export)]pub struct ModelsResponse {pub models: Vec<ModelInfo>,/// `live`, `snapshot` or `local`pub source: String,/// When the list was fetched (ms), or the snapshot's date.#[ts(type = "number | null")]pub fetched_at: Option<i64>,pub snapshot_date: Option<String>,/// Why a refresh failed, if it did (the list above is then the fallback).pub warning: Option<String>,}#[derive(Debug, Clone, Serialize, Deserialize, TS)]#[ts(export)]pub struct Estimate {#[ts(type = "number")]pub prompt_tokens: i64,/// The prompt's cost at the model's input price.#[ts(type = "number")]pub prompt_cost_nanousd: i64,#[ts(type = "number | null")]pub context_length: Option<i64>,}#[derive(Debug, Clone, Serialize, Deserialize, TS)]#[ts(export)]pub struct SpendSummary {#[ts(type = "number")]pub month_nanousd: i64,pub month_estimated: bool,pub budget_usd: Option<f64>,/// 0 below 80% of the budget, 1 from 80%, 2 at or over it.pub budget_level: u8,}#[derive(Debug, Clone, Serialize, Deserialize, TS)]#[ts(export)]pub struct UsageReport {pub by_model: Vec<UsageRow>,pub by_day: Vec<UsageRow>,#[ts(type = "number")]pub since_ms: i64,}#[derive(Debug, Clone, Serialize, Deserialize, TS)]#[ts(export)]pub struct AppInfo {pub version: String,/// `macos`, `linux` or `windows`pub platform: String,/// True when the window has native vibrancy behind it.pub vibrancy: bool,pub data_dir: String,}/// What happened when a key was saved. A key the provider refuses is not saved/// at all; that comes back as an error.#[derive(Debug, Clone, Serialize, Deserialize, TS)]#[ts(export)]pub struct KeySaved {/// False when the key was stored without being checked; `detail` says why.pub accepted: bool,/// Why the key could not be checked (the provider was unreachable, say).pub detail: Option<String>,}#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]#[serde(default)]#[ts(export)]pub struct Settings {/// `system`, `light` or `dark`pub theme: String,pub reduce_transparency: bool,/// macOS window material: `sidebar`, `window`, `fullscreen` or `hud`.pub material: String,/// How strongly panels tint the material, 0 to 100 percent.pub glass_tint: u8,pub shortcut: String,pub default_provider_id: String,pub default_model_id: String,pub quick_provider_id: String,pub quick_model_id: String,pub monthly_budget_usd: Option<f64>,pub confirm_over_budget: bool,pub vault_dir: Option<String>,/// `wiki` for `![[x.png]]`, `markdown` for ``pub image_links: String,pub favorite_models: Vec<String>,/// Local model ids the person has marked as able to see images.pub vision_models: Vec<String>,pub send_on_enter: bool,}impl Default for Settings {fn default() -> Self {Self {theme: "system".into(),reduce_transparency: false,material: "sidebar".into(),glass_tint: 100,shortcut: "CommandOrControl+Shift+Space".into(),default_provider_id: crate::storage::DEEPINFRA_ID.into(),default_model_id: "Qwen/Qwen3-235B-A22B-Instruct-2507".into(),quick_provider_id: crate::storage::DEEPINFRA_ID.into(),quick_model_id: "google/gemma-3-12b-it".into(),monthly_budget_usd: Some(10.0),confirm_over_budget: false,vault_dir: None,image_links: "wiki".into(),favorite_models: vec![],vision_models: vec![],send_on_enter: true,}}}#[derive(Debug, Clone, Serialize, Deserialize, TS)]#[ts(export)]pub struct SaveSettingsResult {pub settings: Settings,/// Set when the new shortcut could not be registered.pub shortcut_error: Option<String>,}#[derive(Debug, Clone, Serialize, Deserialize, TS)]#[ts(export)]pub struct ExportResult {pub path: String,#[ts(type = "number")]pub files_copied: i64,}pub fn budget_level(spent_nanousd: i64, budget_usd: Option<f64>) -> u8 {match budget_usd {Some(b) if b > 0.0 => {let spent = crate::cost::to_usd(spent_nanousd);if spent >= b {2} else if spent >= b * 0.8 {1} else {0}}_ => 0,}}#[cfg(test)]mod tests {use super::*;#[test]fn budget_levels() {assert_eq!(budget_level(0, Some(10.0)), 0);assert_eq!(budget_level(7_999_999_999, Some(10.0)), 0);assert_eq!(budget_level(8_000_000_000, Some(10.0)), 1);assert_eq!(budget_level(10_000_000_000, Some(10.0)), 2);assert_eq!(budget_level(99_000_000_000, None), 0);assert_eq!(budget_level(1, Some(0.0)), 0);}#[test]fn settings_tolerate_missing_fields() {let s: Settings = serde_json::from_str(r#"{"theme":"dark"}"#).unwrap();assert_eq!(s.theme, "dark");assert_eq!(s.shortcut, Settings::default().shortcut);}#[test]fn stream_event_wire_shape() {let v = serde_json::to_value(StreamEvent::Delta { text: "hi".into() }).unwrap();assert_eq!(v, serde_json::json!({"type": "delta", "text": "hi"}));}}