Skip to main content

converge_core/traits/
llm.rs

1// Copyright 2024-2026 Reflective Labs
2// SPDX-License-Identifier: MIT
3
4//! LLM capability traits.
5//!
6//! The canonical chat contract now lives in `converge-provider`. `converge-core`
7//! re-exports that surface during the migration so downstreams can move without
8//! a flag day. Embedding remains here for now.
9
10use super::error::{CapabilityError, ErrorCategory};
11pub use converge_provider::chat::{
12    BoxFuture, ChatBackend, ChatMessage, ChatRequest, ChatResponse, ChatRole, DynChatBackend,
13    FinishReason, LlmError, ResponseFormat, TokenUsage, ToolCall, ToolDefinition,
14};
15use std::future::Future;
16use std::time::Duration;
17
18/// Request for embedding generation.
19#[derive(Debug, Clone)]
20pub struct EmbedRequest {
21    pub inputs: Vec<String>,
22    pub model: Option<String>,
23    pub dimensions: Option<u32>,
24}
25
26/// Response from embedding generation.
27#[derive(Debug, Clone)]
28pub struct EmbedResponse {
29    pub embeddings: Vec<Vec<f32>>,
30    pub usage: Option<TokenUsage>,
31    pub model: Option<String>,
32}
33
34impl CapabilityError for LlmError {
35    fn category(&self) -> ErrorCategory {
36        match self {
37            Self::RateLimited { .. } => ErrorCategory::RateLimit,
38            Self::Timeout { .. } => ErrorCategory::Timeout,
39            Self::AuthDenied { .. } => ErrorCategory::Auth,
40            Self::InvalidRequest { .. } => ErrorCategory::InvalidInput,
41            Self::ModelNotFound { .. } => ErrorCategory::NotFound,
42            Self::ContextLengthExceeded { .. } => ErrorCategory::InvalidInput,
43            Self::ContentFiltered { .. } => ErrorCategory::InvalidInput,
44            Self::ResponseFormatMismatch { .. } => ErrorCategory::Internal,
45            Self::ProviderError { .. } => ErrorCategory::Internal,
46            Self::NetworkError { .. } => ErrorCategory::Unavailable,
47        }
48    }
49
50    fn is_transient(&self) -> bool {
51        matches!(
52            self,
53            Self::RateLimited { .. } | Self::Timeout { .. } | Self::NetworkError { .. }
54        )
55    }
56
57    fn is_retryable(&self) -> bool {
58        self.is_transient() || matches!(self, Self::ProviderError { .. })
59    }
60
61    fn retry_after(&self) -> Option<Duration> {
62        match self {
63            Self::RateLimited { retry_after, .. } => Some(*retry_after),
64            _ => None,
65        }
66    }
67}
68
69/// Embedding generation capability.
70pub trait EmbedBackend: Send + Sync {
71    type EmbedFut<'a>: Future<Output = Result<EmbedResponse, LlmError>> + Send + 'a
72    where
73        Self: 'a;
74
75    fn embed<'a>(&'a self, req: EmbedRequest) -> Self::EmbedFut<'a>;
76}
77
78/// Umbrella trait combining chat and embedding capabilities.
79pub trait LlmBackend: ChatBackend + EmbedBackend {}
80
81impl<T: ChatBackend + EmbedBackend> LlmBackend for T {}
82
83/// Dyn-safe embed backend for runtime polymorphism.
84pub trait DynEmbedBackend: Send + Sync {
85    fn embed(&self, req: EmbedRequest) -> BoxFuture<'_, Result<EmbedResponse, LlmError>>;
86}
87
88impl<T: EmbedBackend> DynEmbedBackend for T {
89    fn embed(&self, req: EmbedRequest) -> BoxFuture<'_, Result<EmbedResponse, LlmError>> {
90        Box::pin(EmbedBackend::embed(self, req))
91    }
92}