use std::sync::Arc; use anyhow::Result; use async_trait::async_trait; use crate::chatbot::ChatbotClient; use crate::image_generate::{ImageGenerate, ImageGenerateModelRecord}; use crate::tts::{TextToSpeech, TtsModelRecord, RemoteTtsModelInfo}; use crate::transcribe::{Transcribe, TranscribeModelRecord, RemoteTranscribeModelInfo}; // ── LlmStrength ─────────────────────────────────────────────────────────────── #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "snake_case")] pub enum LlmStrength { VeryLow, Low, Average, High, VeryHigh, } // ── Provider record types (DB ↔ manager) ────────────────────────────────────── /// Full provider record (includes secrets — only expose over trusted local connections). #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct LlmProviderRecord { pub id: i64, pub name: String, /// Provider type_id string as stored in DB (e.g. "open_ai", "anthropic"). #[serde(rename = "type")] pub provider: String, pub api_key: Option, /// Only used by ollama and lm_studio. pub base_url: Option, pub description: Option, } /// Full model record. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct LlmModelRecord { pub id: i64, pub provider_id: i64, pub model_id: String, pub name: String, pub strength: Option, pub scope: Vec, pub is_default: bool, pub priority: i32, pub extra_params: Option, pub context_length: Option, pub max_output_tokens: Option, pub knowledge_cutoff: Option, pub capabilities: Vec, /// Selected reasoning value (interpreted per provider via `ReasoningMode`): /// a JSON string for a `ValueSet` (e.g. `"high"`, `"enabled"`) or a JSON /// number for a `Range` (e.g. `8000`). `None` = reasoning off / unset. pub reasoning: Option, } /// Remote model info returned by a provider's `list_llm_models()`. #[derive(Debug, Clone, serde::Serialize)] pub struct RemoteLlmModelInfo { pub id: String, pub name: String, pub context_length: Option, pub max_completion_tokens: Option, pub knowledge_cutoff: Option, pub capabilities: Vec, pub vision: Option, pub price_input_per_million: Option, pub price_output_per_million: Option, /// Reasoning control descriptor for this model, if it supports reasoning. /// Filled by the API layer from `ApiProvider::reasoning_mode`; providers' /// `list_llm_models` may leave it `None`. #[serde(default)] pub reasoning: Option, } // ── ReasoningMode ───────────────────────────────────────────────────────────── /// Describes how a model exposes its "reasoning"/"thinking" knob, so the UI can /// render the right control and the caller knows the space of valid values. /// Provider-owned (each `ApiProvider` returns it per model); the actual request /// translation is done by `ApiProvider::reasoning_request`. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] pub enum ReasoningMode { /// A fixed set of discrete string choices — e.g. `["low","medium","high"]` /// (effort levels) or `["disabled","enabled"]` (on/off toggle). ValueSet { values: Vec, default: Option, }, /// A continuous numeric range — e.g. Anthropic thinking `budget_tokens`. Range { min: i64, max: i64, step: Option, default: Option, unit: Option, }, } // ── ServiceType ─────────────────────────────────────────────────────────────── #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize)] #[serde(rename_all = "snake_case")] pub enum ServiceType { Llm, Transcribe, ImageGenerate, Tts, } // ── UI metadata ─────────────────────────────────────────────────────────────── #[derive(Debug, Clone, serde::Serialize)] pub struct ProviderUiMeta { pub type_id: &'static str, pub display_name: &'static str, pub description: Option<&'static str>, pub color: &'static str, pub icon: &'static str, pub fields: &'static [ProviderField], } #[derive(Debug, Clone, serde::Serialize)] pub struct ProviderField { pub key: &'static str, pub label: &'static str, pub required: bool, pub secret: bool, } // ── BuiltLlmClient ──────────────────────────────────────────────────────────── pub struct BuiltLlmClient { pub client: Arc, pub prompt_cache: bool, } // ── ApiProvider trait ───────────────────────────────────────────────────────── #[async_trait] pub trait ApiProvider: Send + Sync { /// Short stable identifier stored in the DB (e.g. "open_ai", "anthropic"). fn type_id(&self) -> &'static str; fn display_name(&self) -> &'static str; fn supported_types(&self) -> &'static [ServiceType]; async fn list_llm_models( &self, _record: &LlmProviderRecord, ) -> Result>> { Ok(None) } /// Reasoning control descriptor for a given model, or `None` if the model /// does not support reasoning. Drives the UI control (a `ValueSet` dropdown /// or a numeric `Range`). Provider-owned so each provider decides which of /// its models support reasoning and in what form. fn reasoning_mode( &self, _model_id: &str, _capabilities: &[String], ) -> Option { None } /// Translates a selected reasoning `value` (see `LlmModelRecord::reasoning`) /// into the provider-specific JSON fragment to merge into the request body /// (e.g. `{"reasoning":{"effort":"high"}}` or `{"thinking":{"type":"enabled"}}`). /// `None` = nothing to inject. fn reasoning_request( &self, _value: &serde_json::Value, ) -> Option { None } async fn llm_model_info( &self, _record: &LlmProviderRecord, _model_id: &str, ) -> Result> { Ok(None) } async fn list_tts_models( &self, _record: &LlmProviderRecord, ) -> Result>> { Ok(None) } async fn list_transcribe_models( &self, _record: &LlmProviderRecord, ) -> Result>> { Ok(None) } fn build_llm( &self, _record: &LlmProviderRecord, _model: &LlmModelRecord, ) -> Option> { None } fn build_tts( &self, _record: &LlmProviderRecord, _model: &TtsModelRecord, ) -> Option>> { None } fn build_transcriber( &self, _record: &LlmProviderRecord, _model: &TranscribeModelRecord, ) -> Option>> { None } fn build_image_generator( &self, _record: &LlmProviderRecord, _model: &ImageGenerateModelRecord, ) -> Option>> { None } fn ui_meta(&self) -> ProviderUiMeta; } // ── ApiProviderRegistry trait ───────────────────────────────────────────────── /// Write-side of the provider registry: register and remove plugin-provided /// `ApiProvider` implementations at runtime. /// /// Implemented by `ProviderRegistry` in the main crate. Plugins that supply /// their own API provider (e.g. `plugin-elevenlabs`) use this to register at /// start and unregister at stop. #[async_trait] pub trait ApiProviderRegistry: Send + Sync { fn register_plugin(&self, provider: Arc); fn unregister_plugin(&self, type_id: &str); }