252 lines
9.0 KiB
Rust
252 lines
9.0 KiB
Rust
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<String>,
|
|
/// Only used by ollama and lm_studio.
|
|
pub base_url: Option<String>,
|
|
pub description: Option<String>,
|
|
}
|
|
|
|
/// 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<LlmStrength>,
|
|
pub scope: Vec<String>,
|
|
pub is_default: bool,
|
|
pub priority: i32,
|
|
pub extra_params: Option<serde_json::Value>,
|
|
pub context_length: Option<i64>,
|
|
pub max_output_tokens: Option<i64>,
|
|
pub knowledge_cutoff: Option<String>,
|
|
pub capabilities: Vec<String>,
|
|
/// 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<serde_json::Value>,
|
|
}
|
|
|
|
/// 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<u64>,
|
|
pub max_completion_tokens: Option<u64>,
|
|
pub knowledge_cutoff: Option<String>,
|
|
pub capabilities: Vec<String>,
|
|
pub vision: Option<bool>,
|
|
pub price_input_per_million: Option<f64>,
|
|
pub price_output_per_million: Option<f64>,
|
|
/// 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>,
|
|
}
|
|
|
|
// ── 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<String>,
|
|
default: Option<String>,
|
|
},
|
|
/// A continuous numeric range — e.g. Anthropic thinking `budget_tokens`.
|
|
Range {
|
|
min: i64,
|
|
max: i64,
|
|
step: Option<i64>,
|
|
default: Option<i64>,
|
|
unit: Option<String>,
|
|
},
|
|
}
|
|
|
|
// ── 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<dyn ChatbotClient>,
|
|
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<Option<Vec<RemoteLlmModelInfo>>> {
|
|
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<ReasoningMode> {
|
|
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<serde_json::Value> {
|
|
None
|
|
}
|
|
|
|
async fn llm_model_info(
|
|
&self,
|
|
_record: &LlmProviderRecord,
|
|
_model_id: &str,
|
|
) -> Result<Option<RemoteLlmModelInfo>> {
|
|
Ok(None)
|
|
}
|
|
|
|
async fn list_tts_models(
|
|
&self,
|
|
_record: &LlmProviderRecord,
|
|
) -> Result<Option<Vec<RemoteTtsModelInfo>>> {
|
|
Ok(None)
|
|
}
|
|
|
|
async fn list_transcribe_models(
|
|
&self,
|
|
_record: &LlmProviderRecord,
|
|
) -> Result<Option<Vec<RemoteTranscribeModelInfo>>> {
|
|
Ok(None)
|
|
}
|
|
|
|
fn build_llm(
|
|
&self,
|
|
_record: &LlmProviderRecord,
|
|
_model: &LlmModelRecord,
|
|
) -> Option<Result<BuiltLlmClient>> {
|
|
None
|
|
}
|
|
|
|
fn build_tts(
|
|
&self,
|
|
_record: &LlmProviderRecord,
|
|
_model: &TtsModelRecord,
|
|
) -> Option<Result<Arc<dyn TextToSpeech>>> {
|
|
None
|
|
}
|
|
|
|
fn build_transcriber(
|
|
&self,
|
|
_record: &LlmProviderRecord,
|
|
_model: &TranscribeModelRecord,
|
|
) -> Option<Result<Arc<dyn Transcribe>>> {
|
|
None
|
|
}
|
|
|
|
fn build_image_generator(
|
|
&self,
|
|
_record: &LlmProviderRecord,
|
|
_model: &ImageGenerateModelRecord,
|
|
) -> Option<Result<Arc<dyn ImageGenerate>>> {
|
|
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<dyn ApiProvider>);
|
|
fn unregister_plugin(&self, type_id: &str);
|
|
}
|