First Version

This commit is contained in:
2026-07-10 15:02:09 +01:00
commit 38494a85a9
562 changed files with 196313 additions and 0 deletions
+251
View File
@@ -0,0 +1,251 @@
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);
}