Skip to main content

jamjet_models/
adapter.rs

1//! Unified model adapter trait and shared types.
2
3use async_trait::async_trait;
4use serde::{Deserialize, Serialize};
5use thiserror::Error;
6
7// ── Error ─────────────────────────────────────────────────────────────────────
8
9#[derive(Debug, Error)]
10pub enum ModelError {
11    #[error("provider API error ({status}): {body}")]
12    Api { status: u16, body: String },
13
14    #[error("rate limited — retry after {retry_after_secs}s")]
15    RateLimited { retry_after_secs: u64 },
16
17    #[error("context window exceeded: {input_tokens} tokens > {limit} limit")]
18    ContextWindowExceeded { input_tokens: u64, limit: u64 },
19
20    #[error("network error: {0}")]
21    Network(String),
22
23    #[error("serialization error: {0}")]
24    Serialization(String),
25
26    #[error("timeout")]
27    Timeout,
28}
29
30// ── Shared types ──────────────────────────────────────────────────────────────
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
33#[serde(rename_all = "lowercase")]
34pub enum ChatRole {
35    System,
36    User,
37    Assistant,
38    Tool,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct ChatMessage {
43    pub role: ChatRole,
44    pub content: String,
45}
46
47impl ChatMessage {
48    pub fn system(content: impl Into<String>) -> Self {
49        Self {
50            role: ChatRole::System,
51            content: content.into(),
52        }
53    }
54    pub fn user(content: impl Into<String>) -> Self {
55        Self {
56            role: ChatRole::User,
57            content: content.into(),
58        }
59    }
60    pub fn assistant(content: impl Into<String>) -> Self {
61        Self {
62            role: ChatRole::Assistant,
63            content: content.into(),
64        }
65    }
66}
67
68/// Configuration for a single model call (overrides adapter defaults).
69#[derive(Debug, Clone, Default, Serialize, Deserialize)]
70pub struct ModelConfig {
71    /// Model name (e.g. "claude-sonnet-4-6", "gpt-4o").
72    pub model: Option<String>,
73    /// Max tokens to generate.
74    pub max_tokens: Option<u32>,
75    /// Sampling temperature (0.0–1.0).
76    pub temperature: Option<f32>,
77    /// System prompt to prepend (overrides messages).
78    pub system_prompt: Option<String>,
79    /// Stop sequences.
80    pub stop_sequences: Option<Vec<String>>,
81}
82
83/// A request to a chat model.
84#[derive(Debug, Clone)]
85pub struct ModelRequest {
86    pub messages: Vec<ChatMessage>,
87    pub config: ModelConfig,
88}
89
90impl ModelRequest {
91    pub fn new(messages: Vec<ChatMessage>) -> Self {
92        Self {
93            messages,
94            config: ModelConfig::default(),
95        }
96    }
97
98    pub fn with_config(mut self, config: ModelConfig) -> Self {
99        self.config = config;
100        self
101    }
102}
103
104/// A request for structured (JSON) output.
105#[derive(Debug, Clone)]
106pub struct StructuredRequest {
107    pub messages: Vec<ChatMessage>,
108    pub config: ModelConfig,
109    /// JSON Schema describing the expected output object.
110    pub output_schema: serde_json::Value,
111}
112
113/// A response from a chat model.
114#[derive(Debug, Clone)]
115pub struct ModelResponse {
116    /// The generated text content.
117    pub content: String,
118    /// The model that actually served the request (may differ from requested).
119    pub model: String,
120    /// Finish reason: "stop", "length", "tool_calls", "content_filter".
121    pub finish_reason: String,
122    /// Input tokens consumed.
123    pub input_tokens: u64,
124    /// Output tokens generated.
125    pub output_tokens: u64,
126    /// Structured output parsed from JSON (for `structured_output()` calls).
127    pub structured: Option<serde_json::Value>,
128}
129
130// ── Trait ─────────────────────────────────────────────────────────────────────
131
132/// Unified interface for LLM providers.
133///
134/// Implement this trait to add a new model provider.
135/// The `system` string returned by `system_name()` is used as the
136/// `gen_ai.system` OTel attribute.
137#[async_trait]
138pub trait ModelAdapter: Send + Sync {
139    /// OTel GenAI system name (e.g. "anthropic", "openai").
140    fn system_name(&self) -> &'static str;
141
142    /// Default model for this adapter (e.g. "claude-sonnet-4-6").
143    fn default_model(&self) -> &str;
144
145    /// Send a chat request and return the response.
146    async fn chat(&self, request: ModelRequest) -> Result<ModelResponse, ModelError>;
147
148    /// Send a structured output request, returning a JSON value.
149    ///
150    /// The response is validated against `request.output_schema` if possible.
151    async fn structured_output(
152        &self,
153        request: StructuredRequest,
154    ) -> Result<ModelResponse, ModelError>;
155}