First Version
This commit is contained in:
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user