Skip to main content

everruns_provider/
driver_registry.rs

1// Chat Driver Abstractions
2//
3// This module encapsulates all abstractions needed to interact with LLM Providers:
4// - ChatDriver trait and types for provider-agnostic LLM interactions
5// - DriverRegistry for dynamic driver registration at startup
6// - Message types for LLM calls
7//
8// Supports both simple text content and multipart content (text, images, audio).
9//
10// IMPORTANT: API keys must be provided from the database. The registry does NOT read
11// from environment variables. Keys should be decrypted and passed via ProviderConfig.
12//
13// Design: Dependency inversion - provider crates (everruns-anthropic, everruns-openai)
14// depend on core and register their drivers at startup. Core has no knowledge of
15// specific provider implementations.
16
17use crate::credential_schema::CredentialFormSchema;
18use crate::error::{AgentLoopError, LlmErrorKind, Result};
19use crate::openresponses_protocol::{CompactOutputItem, CompactRequest, CompactResponse};
20use crate::tool_types::{ToolCall, ToolDefinition};
21use async_trait::async_trait;
22use chrono::{DateTime, Utc};
23use futures::Stream;
24use serde::{Deserialize, Serialize};
25use std::collections::HashMap;
26use std::pin::Pin;
27use std::sync::Arc;
28
29// ============================================================================
30// ChatDriver Trait
31// ============================================================================
32
33/// Type alias for the LLM response stream
34pub type LlmResponseStream = Pin<Box<dyn Stream<Item = Result<LlmStreamEvent>> + Send>>;
35
36/// Ordered provider-owned context returned by a native compaction operation.
37///
38/// The runtime carries this value without interpreting or exposing its opaque
39/// payload. The matching provider driver is responsible for putting the items
40/// back on the wire exactly as returned.
41#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
42#[serde(tag = "type", rename_all = "snake_case")]
43pub enum ProviderOpaqueContext {
44    /// Standalone `output` returned by OpenAI `/responses/compact`.
45    OpenResponsesCompact { output: Vec<CompactOutputItem> },
46}
47
48impl std::fmt::Debug for ProviderOpaqueContext {
49    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50        match self {
51            Self::OpenResponsesCompact { output } => f
52                .debug_struct("OpenResponsesCompact")
53                .field("item_count", &output.len())
54                .finish_non_exhaustive(),
55        }
56    }
57}
58
59/// Structured provider error emitted inside an accepted response stream.
60///
61/// Providers should preserve the wire error code and HTTP status when they are
62/// available. Runtime retry classification uses those fields before falling
63/// back to the human-readable message for legacy drivers.
64#[derive(Debug, Clone, PartialEq, Eq)]
65pub struct LlmStreamError {
66    /// Stable machine-readable provider error code, when supplied.
67    pub code: Option<String>,
68    /// HTTP status associated with the stream error, when supplied.
69    pub status: Option<u16>,
70    /// Human-readable diagnostic text.
71    pub message: String,
72}
73
74impl LlmStreamError {
75    pub fn new(message: impl Into<String>) -> Self {
76        Self {
77            code: None,
78            status: None,
79            message: message.into(),
80        }
81    }
82
83    /// Build a stream error while preserving provider-supplied structure.
84    pub fn provider(
85        code: Option<impl Into<String>>,
86        status: Option<u16>,
87        message: impl Into<String>,
88    ) -> Self {
89        Self {
90            code: code.map(Into::into),
91            status,
92            message: message.into(),
93        }
94    }
95
96    /// Map the preserved structure to Everruns' semantic provider error kind.
97    pub fn kind(&self) -> LlmErrorKind {
98        if let Some(code) = self.code.as_deref()
99            && let Some(kind) = LlmErrorKind::from_provider_code(code)
100        {
101            return kind;
102        }
103        if let Some(status) = self.status {
104            return LlmErrorKind::from_provider_status(status, &self.message);
105        }
106        LlmErrorKind::from_error_text(&self.message)
107    }
108}
109
110impl std::error::Error for LlmStreamError {}
111
112impl std::fmt::Display for LlmStreamError {
113    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
114        match (&self.code, self.status) {
115            (Some(code), Some(status)) => write!(f, "{code} ({status}): {}", self.message),
116            (Some(code), None) => write!(f, "{code}: {}", self.message),
117            (None, Some(status)) => write!(f, "({status}): {}", self.message),
118            (None, None) => f.write_str(&self.message),
119        }
120    }
121}
122
123impl From<String> for LlmStreamError {
124    fn from(message: String) -> Self {
125        Self::new(message)
126    }
127}
128
129impl From<&str> for LlmStreamError {
130    fn from(message: &str) -> Self {
131        Self::new(message)
132    }
133}
134
135/// Events emitted during LLM streaming
136#[derive(Debug, Clone)]
137pub enum LlmStreamEvent {
138    /// Text delta (incremental content)
139    TextDelta(String),
140    /// Thinking delta (incremental reasoning content from extended thinking models)
141    ThinkingDelta(String),
142    /// Cryptographic signature for thinking content (Anthropic Claude)
143    /// Emitted when a thinking block completes, before the Done event
144    ThinkingSignature(String),
145    /// Opaque assistant reasoning response item (OpenAI Responses).
146    /// Carries provider-supplied opaque/encrypted reasoning artifacts plus safe
147    /// summary text and per-item metadata. Plaintext hidden reasoning content is
148    /// intentionally excluded so callers can persist this without exposing
149    /// chain-of-thought.
150    ReasonItem {
151        /// Provider name (e.g., "openai").
152        provider: String,
153        /// Model identifier reported by the provider, if known.
154        model: Option<String>,
155        /// Provider-assigned identifier for the reasoning item.
156        item_id: String,
157        /// Provider-encrypted reasoning context, if supplied.
158        encrypted_content: Option<String>,
159        /// Safe summary text segments curated by the provider.
160        summary: Vec<String>,
161        /// Per-item reasoning token count, when the provider reports one.
162        token_count: Option<u32>,
163    },
164    /// Tool calls from the LLM
165    ToolCalls(Vec<ToolCall>),
166    /// Provider-native execution phase for the current assistant message,
167    /// surfaced mid-stream before completion (EVE-774).
168    ///
169    /// Only emitted by providers whose stream carries a native phase ahead of
170    /// the terminal `Done` metadata (OpenAI Responses exposes it on
171    /// `response.output_item.added`). Consumers use it as a best-effort hint to
172    /// classify streamed assistant text as commentary vs final answer; the
173    /// authoritative value is still the completed `Message.phase`. Other
174    /// providers never emit this and stay unclassified until completion.
175    MessagePhase(crate::execution_phase::ExecutionPhase),
176    /// Streaming completed
177    Done(Box<LlmCompletionMetadata>),
178    /// Error during streaming
179    Error(LlmStreamError),
180}
181
182/// Model information discovered from a provider's list_models API
183///
184/// Represents a model available from a provider. Used for dynamic model discovery
185/// to sync available models from provider APIs into the database.
186///
187/// The `discovered_profile` field carries structured capability/limit metadata
188/// parsed from the provider's API response (e.g., Anthropic's capabilities object).
189/// During model sync, this profile is merged with hardcoded profiles: hardcoded
190/// values take precedence (they include cost data not available from APIs),
191/// but discovered data fills gaps for models without hardcoded profiles.
192#[derive(Debug, Clone)]
193pub struct DiscoveredModel {
194    /// Model identifier (e.g., "gpt-5.2", "claude-opus-4-5-20251101")
195    pub model_id: String,
196    /// Human-readable display name (if provided by API)
197    pub display_name: Option<String>,
198    /// When the model was created/released
199    pub created_at: Option<DateTime<Utc>>,
200    /// Owner or organization (e.g., "openai", "system")
201    pub owned_by: Option<String>,
202    /// Structured profile built from provider API metadata (capabilities, limits).
203    /// Populated by drivers that return rich model metadata (e.g., Anthropic /v1/models).
204    pub discovered_profile: Option<crate::model::ModelProfile>,
205}
206
207/// Metadata about LLM completion
208///
209/// Contains token usage and completion information from the LLM response.
210///
211/// Token buckets are **disjoint** by convention (see the `TokenUsage` event): drivers
212/// normalize provider wire formats at the boundary so `prompt_tokens` carries
213/// only non-cached input, with `cache_read_tokens` / `cache_creation_tokens`
214/// additive on top. Inclusive providers (OpenAI Responses / Chat Completions,
215/// Gemini) subtract their cached count from the reported prompt total via
216/// [`disjoint_prompt_tokens`]; Anthropic / Bedrock already report disjoint
217/// buckets and pass values through unchanged.
218///
219#[derive(Debug, Clone, Default)]
220pub struct LlmCompletionMetadata {
221    /// Total tokens used (non-cached prompt + cache read/creation + completion)
222    pub total_tokens: Option<u32>,
223    /// Non-cached prompt tokens (cached reads are excluded; see struct docs)
224    pub prompt_tokens: Option<u32>,
225    /// Completion tokens
226    pub completion_tokens: Option<u32>,
227    /// Tokens read from cache (reduces cost), disjoint from `prompt_tokens`
228    pub cache_read_tokens: Option<u32>,
229    /// Tokens written to cache (Anthropic-specific), disjoint from `prompt_tokens`
230    pub cache_creation_tokens: Option<u32>,
231    /// Authoritative cost of this generation in USD, when the provider reports
232    /// it inline (e.g. OpenRouter's `usage.cost`). `None` for providers that do
233    /// not return a cost.
234    pub provider_cost_usd: Option<f64>,
235    /// Model used
236    pub model: Option<String>,
237    /// Finish reason
238    pub finish_reason: Option<String>,
239    /// Retry metadata (present if rate limit retries occurred)
240    pub retry_metadata: Option<crate::llm_retry::RetryMetadata>,
241    /// Provider's response ID (e.g., OpenAI response ID from response.completed).
242    /// Used for `previous_response_id` chaining and OTel tracing.
243    pub response_id: Option<String>,
244    /// Execution phase from the provider's response (e.g., "commentary", "final_answer").
245    /// When present, this value should be preserved on the assistant message and sent
246    /// back as-is in subsequent requests. Only set by providers with native phase support.
247    pub phase: Option<String>,
248}
249
250/// Normalize an inclusive provider's reported prompt-token count to the disjoint
251/// `TokenUsage` convention by subtracting the cached-read subset.
252///
253/// OpenAI (Responses & Chat Completions) and Gemini report a prompt token count
254/// that *includes* cached reads; callers pass that raw count plus the provider's
255/// cached-read count to get the non-cached remainder. Saturating subtraction
256/// guards against a provider reporting `cache_read > reported_input`. Anthropic /
257/// Bedrock already report disjoint buckets and must not call this.
258///
259pub fn disjoint_prompt_tokens(reported_input: u32, cache_read: Option<u32>) -> u32 {
260    reported_input.saturating_sub(cache_read.unwrap_or(0))
261}
262
263/// Trait for LLM drivers
264///
265/// Implementations handle provider-specific API calls and response parsing.
266///
267/// # Error contract
268///
269/// Drivers surface provider failures as `AgentLoopError` and classify them
270/// semantically at the provider boundary, where HTTP status and response body
271/// are still available:
272///
273/// - request-too-large conditions => `AgentLoopError::request_too_large`
274/// - missing/unknown model => `AgentLoopError::model_not_available`
275/// - everything else => `AgentLoopError::llm_kind(LlmErrorKind::..., msg)`,
276///   using `LlmErrorKind::from_provider_status` (HTTP drivers) or
277///   `LlmErrorKind::from_error_text` (SDK drivers without a status). Plain
278///   `AgentLoopError::llm` is reserved for unclassifiable errors; downstream
279///   then falls back to string classification.
280///
281/// Quota/billing exhaustion (`LlmErrorKind::QuotaExhausted`) is non-transient
282/// and must not be retried by driver retry loops even when the provider
283/// reports it under a transient status like 429.
284#[async_trait]
285pub trait ChatDriver: Send + Sync {
286    /// Call the LLM with streaming response
287    async fn chat_completion_stream(
288        &self,
289        messages: Vec<LlmMessage>,
290        config: &LlmCallConfig,
291    ) -> Result<LlmResponseStream>;
292
293    /// Call the LLM without streaming (convenience method)
294    async fn chat_completion(
295        &self,
296        messages: Vec<LlmMessage>,
297        config: &LlmCallConfig,
298    ) -> Result<LlmResponse> {
299        use futures::StreamExt;
300
301        let mut stream = self.chat_completion_stream(messages, config).await?;
302        let mut text = String::new();
303        let mut thinking = String::new();
304        let mut thinking_signature: Option<String> = None;
305        let mut tool_calls = Vec::new();
306        let mut metadata = LlmCompletionMetadata::default();
307
308        while let Some(event) = stream.next().await {
309            match event? {
310                LlmStreamEvent::TextDelta(delta) => text.push_str(&delta),
311                LlmStreamEvent::ThinkingDelta(delta) => thinking.push_str(&delta),
312                LlmStreamEvent::ThinkingSignature(sig) => thinking_signature = Some(sig),
313                LlmStreamEvent::ReasonItem {
314                    encrypted_content, ..
315                } => {
316                    if let Some(sig) = encrypted_content {
317                        thinking_signature = Some(sig);
318                    }
319                }
320                LlmStreamEvent::ToolCalls(calls) => tool_calls = calls,
321                // Streamed phase hint is a mid-stream refinement only; the
322                // non-streaming collector relies on the terminal Done metadata.
323                LlmStreamEvent::MessagePhase(_) => {}
324                LlmStreamEvent::Done(meta) => metadata = *meta,
325                LlmStreamEvent::Error(err) => {
326                    return Err(crate::error::AgentLoopError::llm_kind(
327                        err.kind(),
328                        err.to_string(),
329                    ));
330                }
331            }
332        }
333
334        Ok(LlmResponse {
335            text,
336            thinking: if thinking.is_empty() {
337                None
338            } else {
339                Some(thinking)
340            },
341            thinking_signature,
342            tool_calls: if tool_calls.is_empty() {
343                None
344            } else {
345                Some(tool_calls)
346            },
347            metadata,
348        })
349    }
350
351    /// List available models from the provider
352    ///
353    /// Returns `Ok(Some(models))` if the provider supports model listing,
354    /// or `Ok(None)` if not supported (e.g., custom endpoints, proxies).
355    ///
356    /// Implementations should filter to chat/completion models only,
357    /// excluding embedding models, TTS, whisper, etc.
358    async fn list_models(&self) -> Result<Option<Vec<DiscoveredModel>>> {
359        // Default: not supported. Providers override if they support listing.
360        Ok(None)
361    }
362
363    /// Check if this driver supports the compact endpoint
364    ///
365    /// The compact endpoint compresses conversation history by replacing
366    /// assistant messages, tool calls, and tool results with an encrypted
367    /// compaction item. User messages are kept verbatim.
368    ///
369    /// Returns `true` if the driver supports compaction, `false` otherwise.
370    /// Currently only supported by OpenAI's Responses API.
371    fn supports_compact(&self) -> bool {
372        // Default: not supported
373        false
374    }
375
376    /// Whether this driver persists Responses API state and can resolve tool
377    /// calls that are reachable only through `previous_response_id`.
378    ///
379    /// Stateless and custom drivers default to `false`; they must receive a
380    /// self-contained tool call/result transcript on every request.
381    fn supports_stateful_responses(&self) -> bool {
382        false
383    }
384
385    /// Effective context window for `model`, when the driver has authoritative
386    /// model metadata that is not represented by Everruns' built-in profiles.
387    ///
388    /// External drivers should override this so host policy does not guess from
389    /// a provider/model table that cannot describe their runtime model aliases.
390    fn effective_context_window(&self, _model: &str) -> Option<usize> {
391        None
392    }
393
394    /// Whether this driver can express the request-level `parallel_tool_calls`
395    /// preference on the wire for `model`.
396    ///
397    /// Drivers that map the preference onto a request field (OpenAI/Anthropic
398    /// families) return `true`; drivers whose provider API has no such control
399    /// (Gemini, Bedrock) return `false`. When `false`, the preference is omitted
400    /// from the request and is honored only by the local tool scheduler, so an
401    /// `avoid` preference still serializes tool execution on every provider.
402    ///
403    /// The default is `false` (conservative: omit unless a driver opts in).
404    fn supports_parallel_tool_calls(&self, _model: &str) -> bool {
405        false
406    }
407
408    /// Compact a conversation to reduce context size
409    ///
410    /// This method compresses conversation history by calling the provider's
411    /// compact endpoint. User messages are kept verbatim, while assistant
412    /// messages, tool calls, and tool results are replaced by an encrypted
413    /// compaction item that preserves latent context but is opaque.
414    ///
415    /// # Arguments
416    ///
417    /// * `request` - The compact request containing the model and input items
418    ///
419    /// # Returns
420    ///
421    /// Returns `Ok(Some(response))` if compaction succeeded,
422    /// `Ok(None)` if compaction is not supported by this driver,
423    /// or `Err` if an error occurred.
424    ///
425    /// The response contains the compacted output items which can be used
426    /// directly as input for the next chat completion call.
427    async fn compact(&self, _request: CompactRequest) -> Result<Option<CompactResponse>> {
428        // Default: not supported
429        Ok(None)
430    }
431}
432
433/// Implement ChatDriver for `Box<dyn ChatDriver>` to allow dynamic dispatch
434#[async_trait]
435impl ChatDriver for Box<dyn ChatDriver> {
436    async fn chat_completion_stream(
437        &self,
438        messages: Vec<LlmMessage>,
439        config: &LlmCallConfig,
440    ) -> Result<LlmResponseStream> {
441        (**self).chat_completion_stream(messages, config).await
442    }
443
444    async fn chat_completion(
445        &self,
446        messages: Vec<LlmMessage>,
447        config: &LlmCallConfig,
448    ) -> Result<LlmResponse> {
449        (**self).chat_completion(messages, config).await
450    }
451
452    async fn list_models(&self) -> Result<Option<Vec<DiscoveredModel>>> {
453        (**self).list_models().await
454    }
455
456    fn supports_compact(&self) -> bool {
457        (**self).supports_compact()
458    }
459
460    fn supports_stateful_responses(&self) -> bool {
461        (**self).supports_stateful_responses()
462    }
463
464    fn effective_context_window(&self, model: &str) -> Option<usize> {
465        (**self).effective_context_window(model)
466    }
467
468    fn supports_parallel_tool_calls(&self, model: &str) -> bool {
469        (**self).supports_parallel_tool_calls(model)
470    }
471
472    async fn compact(&self, request: CompactRequest) -> Result<Option<CompactResponse>> {
473        (**self).compact(request).await
474    }
475}
476
477// ============================================================================
478// Message Types
479// ============================================================================
480
481/// Message format for LLM calls (provider-agnostic)
482#[derive(Debug, Clone)]
483pub struct LlmMessage {
484    pub role: LlmMessageRole,
485    pub content: LlmMessageContent,
486    pub tool_calls: Option<Vec<ToolCall>>,
487    pub tool_call_id: Option<String>,
488    /// Execution phase for assistant messages.
489    /// Helps models distinguish between intermediate working commentary (`Commentary`)
490    /// and completed answers (`FinalAnswer`) in multi-step tool-calling flows.
491    /// Only set on assistant messages. Must be preserved when replaying conversation history.
492    pub phase: Option<crate::execution_phase::ExecutionPhase>,
493    /// Thinking content from extended thinking models (Anthropic Claude)
494    /// Must be included in subsequent API calls when thinking is enabled
495    pub thinking: Option<String>,
496    /// Cryptographic signature for thinking content (Anthropic Claude)
497    /// Required when sending thinking back in subsequent API calls
498    pub thinking_signature: Option<String>,
499}
500
501impl LlmMessage {
502    /// Create a message with text content
503    pub fn text(role: LlmMessageRole, content: impl Into<String>) -> Self {
504        Self {
505            role,
506            content: LlmMessageContent::Text(content.into()),
507            tool_calls: None,
508            tool_call_id: None,
509            phase: None,
510            thinking: None,
511            thinking_signature: None,
512        }
513    }
514
515    /// Create a message with content parts (text, images, audio)
516    pub fn parts(role: LlmMessageRole, parts: Vec<LlmContentPart>) -> Self {
517        Self {
518            role,
519            content: LlmMessageContent::Parts(parts),
520            tool_calls: None,
521            tool_call_id: None,
522            phase: None,
523            thinking: None,
524            thinking_signature: None,
525        }
526    }
527
528    /// Get content as plain text string (for simple cases)
529    pub fn content_as_text(&self) -> String {
530        self.content.to_text()
531    }
532
533    /// Prepend a prefix to the first text content.
534    ///
535    /// Used by ReasonAtom to inject external actor identity (e.g. `"[Alice] "`)
536    /// into user messages from external channels.
537    pub fn prepend_text_prefix(&mut self, prefix: &str) {
538        match &mut self.content {
539            LlmMessageContent::Text(text) => {
540                *text = format!("{}{}", prefix, text);
541            }
542            LlmMessageContent::Parts(parts) => {
543                for part in parts.iter_mut() {
544                    if let LlmContentPart::Text { text } = part {
545                        *text = format!("{}{}", prefix, text);
546                        return;
547                    }
548                }
549                // No text part found — prepend one
550                parts.insert(
551                    0,
552                    LlmContentPart::Text {
553                        text: prefix.to_string(),
554                    },
555                );
556            }
557        }
558    }
559}
560
561/// Fold every `System`-role message into a single string, joined in order with
562/// blank lines.
563///
564/// Multiple system messages legitimately occur in one request: the agent system
565/// prompt plus, e.g., `infinity_context`'s hidden-history notice or
566/// `compaction`'s `[CONVERSATION_SUMMARY]`. Drivers that map the system role into
567/// a dedicated top-level field (Anthropic `system`, Gemini `system_instruction`,
568/// OpenResponses `instructions`) must accumulate rather than overwrite — otherwise
569/// the real agent system prompt is silently dropped and only the last notice
570/// survives. Returns `None` when there are no system messages.
571pub fn fold_system_messages(messages: &[LlmMessage]) -> Option<String> {
572    let mut system: Option<String> = None;
573    for msg in messages {
574        if msg.role == LlmMessageRole::System {
575            let text = msg.content.to_text();
576            system = Some(match system.take() {
577                Some(existing) if !existing.is_empty() => format!("{existing}\n\n{text}"),
578                _ => text,
579            });
580        }
581    }
582    system
583}
584
585/// Message content - either a simple string or array of content parts
586#[derive(Debug, Clone)]
587pub enum LlmMessageContent {
588    /// Simple text content
589    Text(String),
590    /// Array of content parts (text, images, audio)
591    Parts(Vec<LlmContentPart>),
592}
593
594impl LlmMessageContent {
595    /// Convert to plain text (concatenates text parts, ignores media)
596    pub fn to_text(&self) -> String {
597        match self {
598            LlmMessageContent::Text(s) => s.clone(),
599            LlmMessageContent::Parts(parts) => parts
600                .iter()
601                .filter_map(|p| match p {
602                    LlmContentPart::Text { text } => Some(text.clone()),
603                    _ => None,
604                })
605                .collect::<Vec<_>>()
606                .join(""),
607        }
608    }
609
610    /// Check if content is simple text
611    pub fn is_text(&self) -> bool {
612        matches!(self, LlmMessageContent::Text(_))
613    }
614
615    /// Check if content has multiple parts
616    pub fn is_parts(&self) -> bool {
617        matches!(self, LlmMessageContent::Parts(_))
618    }
619}
620
621impl From<String> for LlmMessageContent {
622    fn from(s: String) -> Self {
623        LlmMessageContent::Text(s)
624    }
625}
626
627impl From<&str> for LlmMessageContent {
628    fn from(s: &str) -> Self {
629        LlmMessageContent::Text(s.to_string())
630    }
631}
632
633/// A single content part within a message
634#[derive(Debug, Clone)]
635pub enum LlmContentPart {
636    /// Text content
637    Text { text: String },
638    /// Image content (base64 data URL or HTTP URL)
639    Image { url: String },
640    /// Audio content (base64 data URL)
641    Audio { url: String },
642}
643
644impl LlmContentPart {
645    /// Create a text content part
646    pub fn text(text: impl Into<String>) -> Self {
647        LlmContentPart::Text { text: text.into() }
648    }
649
650    /// Create an image content part from URL (can be data URL or HTTP URL)
651    pub fn image(url: impl Into<String>) -> Self {
652        LlmContentPart::Image { url: url.into() }
653    }
654
655    /// Create an audio content part from URL (typically a data URL)
656    pub fn audio(url: impl Into<String>) -> Self {
657        LlmContentPart::Audio { url: url.into() }
658    }
659}
660
661/// Message role for LLM calls
662#[derive(Debug, Clone, PartialEq, Eq)]
663pub enum LlmMessageRole {
664    System,
665    User,
666    Assistant,
667    Tool,
668}
669
670// ============================================================================
671// Configuration and Response Types
672// ============================================================================
673
674/// Configuration for tool_search (deferred tool loading).
675///
676/// When enabled, the driver groups tools into namespaces and marks them with
677/// `defer_loading: true` so the model only loads full schemas on-demand.
678/// This reduces token usage for agents with many tools.
679#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
680pub struct ToolSearchConfig {
681    /// Enable tool_search for this request (requires model support)
682    pub enabled: bool,
683    /// Minimum number of tools before activating tool_search.
684    /// Below this threshold, full schemas are sent even when enabled.
685    pub threshold: usize,
686}
687
688/// Strategy for prompt caching.
689#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
690#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
691#[serde(rename_all = "snake_case")]
692pub enum PromptCacheStrategy {
693    /// Let each driver choose the safest provider-specific behavior.
694    #[default]
695    Auto,
696}
697
698/// Configuration for prompt caching.
699///
700/// Drivers translate this into provider-specific request options when possible.
701/// Unsupported providers or models should ignore it without failing the call.
702#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
703#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
704pub struct PromptCacheConfig {
705    /// Enable prompt caching for this request.
706    pub enabled: bool,
707    /// Strategy the driver should use when enabling prompt caching.
708    #[serde(default)]
709    pub strategy: PromptCacheStrategy,
710    /// Existing Gemini cached content resource name (`cachedContents/{id}`).
711    ///
712    /// When set, the Gemini driver uses explicit caching via the
713    /// `cachedContent` request field. When absent, Gemini falls back to its
714    /// default provider behavior (for example implicit caching on supported
715    /// models).
716    #[serde(default, skip_serializing_if = "Option::is_none")]
717    pub gemini_cached_content: Option<String>,
718}
719
720/// High-level intent presets that compile into OpenRouter provider-routing
721/// controls. Presets let callers express quality, cost, privacy, and capability
722/// goals without knowing every OpenRouter `provider` flag.
723///
724/// Multiple presets may be combined. When a preset and an explicit `provider`
725/// field target the same control, the explicit field wins. Presets applied
726/// earlier in the list may be overridden by later ones for the same field.
727///
728/// Compilation happens in `OpenRouterRoutingConfig::apply_presets()`.
729#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
730#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
731#[serde(tag = "kind", rename_all = "snake_case")]
732pub enum OpenRouterRoutingPreset {
733    /// Prefer the cheapest providers that support function-calling parameters.
734    CheapestWithTools,
735    /// Prefer the highest-throughput providers for quick review or triage tasks.
736    LowestLatencyReview,
737    /// Route only to zero-data-retention (ZDR) endpoints.
738    ZdrOnly,
739    /// Try BYOK-registered providers first; fall back to shared capacity.
740    ByokFirst,
741    /// Deny all provider-side data collection (logs and training).
742    NoDataCollection,
743    /// Route only to providers that support strict JSON / structured output.
744    StrictJson,
745    /// Route only to providers that natively support reasoning/thinking models.
746    ReasoningRequired,
747    /// Cap per-token provider cost. Values are USD per million tokens; `None`
748    /// means no cap on that dimension.
749    MaxPrice {
750        /// Maximum prompt cost in USD per million tokens.
751        #[serde(default, skip_serializing_if = "Option::is_none")]
752        prompt_usd_per_million: Option<f64>,
753        /// Maximum completion cost in USD per million tokens.
754        #[serde(default, skip_serializing_if = "Option::is_none")]
755        completion_usd_per_million: Option<f64>,
756    },
757}
758
759/// OpenRouter model fallback and provider routing controls.
760///
761/// Organization-level strategy for how OpenRouter should allocate compute capacity.
762///
763/// Controls whether requests use OpenRouter shared credits, prefer customer-owned
764/// upstream keys (BYOK), or require BYOK-only routing. Compiled into OpenRouter
765/// `provider` routing controls before dispatch; not sent verbatim on the wire.
766#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
767#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
768#[serde(rename_all = "snake_case")]
769pub enum OpenRouterCapacityStrategy {
770    /// Use OpenRouter shared capacity (credits). No routing changes. Default.
771    #[default]
772    SharedCapacity,
773    /// Prefer providers where the org has registered its own upstream key.
774    /// Falls back to shared capacity when BYOK providers are unavailable.
775    /// Sets `provider.allow_fallbacks = true` unless the caller overrides it.
776    ByokFirst,
777    /// Require a provider where the org has its own upstream key.
778    /// Routing fails if `provider.only` is not explicitly configured with at
779    /// least one BYOK provider slug.
780    /// Sets `provider.allow_fallbacks = false`.
781    ByokOnly,
782}
783
784/// One of OpenRouter's provider-executed "server tools" (beta).
785///
786/// Server tools are tools OpenRouter runs server-side — it loops internally and
787/// returns the final answer, so unlike client-executed function tools the agent
788/// loop never dispatches them. The only client-visible artifact is
789/// `usage.server_tool_use`. See
790/// <https://openrouter.ai/docs/guides/features/server-tools>.
791#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
792#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
793#[serde(rename_all = "snake_case")]
794pub enum OpenRouterServerToolKind {
795    WebSearch,
796    WebFetch,
797    Datetime,
798    ImageGeneration,
799    ApplyPatch,
800    Fusion,
801    Advisor,
802    Subagent,
803}
804
805impl OpenRouterServerToolKind {
806    /// Every known server tool, in catalog order.
807    pub const ALL: [OpenRouterServerToolKind; 8] = [
808        Self::WebSearch,
809        Self::WebFetch,
810        Self::Datetime,
811        Self::ImageGeneration,
812        Self::ApplyPatch,
813        Self::Fusion,
814        Self::Advisor,
815        Self::Subagent,
816    ];
817
818    /// Bare tool name (no prefix), e.g. `"web_search"`.
819    pub fn name(&self) -> &'static str {
820        match self {
821            Self::WebSearch => "web_search",
822            Self::WebFetch => "web_fetch",
823            Self::Datetime => "datetime",
824            Self::ImageGeneration => "image_generation",
825            Self::ApplyPatch => "apply_patch",
826            Self::Fusion => "fusion",
827            Self::Advisor => "advisor",
828            Self::Subagent => "subagent",
829        }
830    }
831
832    /// Human-readable English display name, used for UI schema titles.
833    pub fn display_name(&self) -> &'static str {
834        match self {
835            Self::WebSearch => "Web Search",
836            Self::WebFetch => "Web Fetch",
837            Self::Datetime => "Date & Time",
838            Self::ImageGeneration => "Image Generation",
839            Self::ApplyPatch => "Apply Patch",
840            Self::Fusion => "Fusion",
841            Self::Advisor => "Advisor",
842            Self::Subagent => "Subagent",
843        }
844    }
845
846    /// The `type` discriminator OpenRouter expects in the request `tools` array,
847    /// e.g. `"openrouter:web_search"`.
848    pub fn wire_type(&self) -> String {
849        format!("openrouter:{}", self.name())
850    }
851
852    /// Parse a bare tool name (no `openrouter:` prefix).
853    pub fn from_name(name: &str) -> Option<Self> {
854        Self::ALL.into_iter().find(|kind| kind.name() == name)
855    }
856}
857
858/// One activated OpenRouter server tool plus optional tool-specific parameters
859/// (e.g. web_search `max_results`). Parameters are forwarded verbatim under the
860/// wire entry's `parameters` field.
861#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
862#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
863pub struct OpenRouterServerTool {
864    pub kind: OpenRouterServerToolKind,
865    #[serde(default, skip_serializing_if = "Option::is_none")]
866    #[cfg_attr(feature = "openapi", schema(value_type = Option<Object>))]
867    pub parameters: Option<serde_json::Value>,
868}
869
870impl OpenRouterServerTool {
871    /// A server tool with no parameters.
872    pub fn new(kind: OpenRouterServerToolKind) -> Self {
873        Self {
874            kind,
875            parameters: None,
876        }
877    }
878
879    /// A server tool carrying parameters forwarded verbatim to OpenRouter.
880    pub fn with_parameters(kind: OpenRouterServerToolKind, parameters: serde_json::Value) -> Self {
881        Self {
882            kind,
883            parameters: Some(parameters),
884        }
885    }
886}
887
888/// These fields mirror OpenRouter's request-level routing extensions. Drivers
889/// must only forward this config to OpenRouter-compatible endpoints.
890#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]
891#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
892pub struct OpenRouterRoutingConfig {
893    /// Candidate models to try in OpenRouter's fallback order.
894    #[serde(default, skip_serializing_if = "Vec::is_empty")]
895    pub models: Vec<String>,
896    /// OpenRouter route strategy. Currently `fallback` is the stable route
897    /// value used with `models`.
898    #[serde(default, skip_serializing_if = "Option::is_none")]
899    pub route: Option<OpenRouterRoute>,
900    /// Provider ordering, policy, and sorting preferences.
901    #[serde(default, skip_serializing_if = "Option::is_none")]
902    pub provider: Option<OpenRouterProviderRouting>,
903    /// Optional plugin activations (web search, file reader).
904    #[serde(default, skip_serializing_if = "Option::is_none")]
905    pub plugins: Option<OpenRouterPluginConfig>,
906    /// Org-level capacity strategy. Compiled into `provider` routing before
907    /// dispatch; not forwarded verbatim. `None` and `SharedCapacity` are
908    /// equivalent (no routing changes).
909    #[serde(default, skip_serializing_if = "Option::is_none")]
910    pub capacity_strategy: Option<OpenRouterCapacityStrategy>,
911    /// High-level routing quality/policy presets. Compiled into `provider`
912    /// flags by `apply_presets()` before the request is serialized.
913    /// Explicit `provider` fields override preset-derived values.
914    #[serde(default, skip_serializing_if = "Vec::is_empty")]
915    pub presets: Vec<OpenRouterRoutingPreset>,
916    /// OpenRouter server tools (beta) the model may invoke. Provider-executed;
917    /// appended to the request `tools` array as `{"type":"openrouter:<name>"}`.
918    #[serde(default, skip_serializing_if = "Vec::is_empty")]
919    pub server_tools: Vec<OpenRouterServerTool>,
920}
921
922impl OpenRouterRoutingConfig {
923    pub fn is_empty(&self) -> bool {
924        self.models.is_empty()
925            && self.route.is_none()
926            && self.provider.is_none()
927            && self.plugins.as_ref().is_none_or(|p| p.is_empty())
928            && matches!(
929                self.capacity_strategy,
930                None | Some(OpenRouterCapacityStrategy::SharedCapacity)
931            )
932            && self.presets.is_empty()
933            && self.server_tools.is_empty()
934    }
935
936    /// Build an ordered model-fallback routing config.
937    pub fn fallback_models(models: impl IntoIterator<Item = impl Into<String>>) -> Self {
938        let models = models.into_iter().map(Into::into).collect::<Vec<_>>();
939        let route = (!models.is_empty()).then_some(OpenRouterRoute::Fallback);
940        Self {
941            models,
942            route,
943            provider: None,
944            plugins: None,
945            capacity_strategy: None,
946            presets: vec![],
947            server_tools: vec![],
948        }
949    }
950
951    pub fn validate_for_primary_model(
952        &self,
953        primary_model: &str,
954    ) -> std::result::Result<(), String> {
955        if self.route == Some(OpenRouterRoute::Fallback) && self.models.is_empty() {
956            return Err(
957                "OpenRouter fallback routing requires at least one model in `models`".to_string(),
958            );
959        }
960
961        if let Some(first_model) = self.models.first()
962            && first_model != primary_model
963        {
964            return Err(format!(
965                "OpenRouter routing models[0] ('{first_model}') must match primary model ('{primary_model}')"
966            ));
967        }
968
969        Ok(())
970    }
971
972    /// Apply the capacity strategy, returning a derived config with `provider`
973    /// routing adjusted accordingly.
974    ///
975    /// - `SharedCapacity` / `None` — returns `self` unchanged.
976    /// - `ByokFirst` — sets `provider.allow_fallbacks = true` when not already set.
977    /// - `ByokOnly` — requires `provider.only` to list at least one provider slug;
978    ///   sets `provider.allow_fallbacks = false`.
979    ///
980    /// Returns `Err` when the strategy constraints cannot be satisfied.
981    pub fn apply_capacity_strategy(&self) -> std::result::Result<Self, String> {
982        match self.capacity_strategy {
983            None | Some(OpenRouterCapacityStrategy::SharedCapacity) => Ok(self.clone()),
984            Some(OpenRouterCapacityStrategy::ByokFirst) => {
985                let mut result = self.clone();
986                let provider = result.provider.get_or_insert_with(Default::default);
987                if provider.allow_fallbacks.is_none() {
988                    provider.allow_fallbacks = Some(true);
989                }
990                Ok(result)
991            }
992            Some(OpenRouterCapacityStrategy::ByokOnly) => {
993                let only_is_empty = self.provider.as_ref().is_none_or(|p| p.only.is_empty());
994                if only_is_empty {
995                    return Err(
996                        "OpenRouter BYOK-only strategy requires provider.only to list at least \
997                         one upstream provider slug. Configure the provider list to match the \
998                         BYOK providers registered in your OpenRouter workspace."
999                            .to_string(),
1000                    );
1001                }
1002                let mut result = self.clone();
1003                let provider = result.provider.get_or_insert_with(Default::default);
1004                provider.allow_fallbacks = Some(false);
1005                Ok(result)
1006            }
1007        }
1008    }
1009
1010    /// Compile `presets` into `OpenRouterProviderRouting` flags and merge with
1011    /// any explicit `provider` overrides. Returns a derived config with the
1012    /// `presets` list cleared and `provider` reflecting the merged result.
1013    ///
1014    /// Explicit `provider` fields always win over preset-derived values. When
1015    /// multiple presets target the same provider field, later presets in the
1016    /// list override earlier ones.
1017    ///
1018    /// Returns `Err` if any preset values are invalid (e.g. negative `MaxPrice` values).
1019    pub fn apply_presets(&self) -> std::result::Result<Self, String> {
1020        if self.presets.is_empty() {
1021            return Ok(self.clone());
1022        }
1023
1024        let mut derived = OpenRouterProviderRouting::default();
1025
1026        for preset in &self.presets {
1027            match preset {
1028                OpenRouterRoutingPreset::CheapestWithTools => {
1029                    derived.require_parameters = Some(true);
1030                    derived.sort = Some(OpenRouterProviderSort::Simple(
1031                        OpenRouterProviderSortBy::Price,
1032                    ));
1033                }
1034                OpenRouterRoutingPreset::LowestLatencyReview => {
1035                    derived.sort = Some(OpenRouterProviderSort::Simple(
1036                        OpenRouterProviderSortBy::Throughput,
1037                    ));
1038                }
1039                OpenRouterRoutingPreset::ZdrOnly => {
1040                    derived.zdr = Some(true);
1041                }
1042                OpenRouterRoutingPreset::ByokFirst => {
1043                    if derived.allow_fallbacks.is_none() {
1044                        derived.allow_fallbacks = Some(true);
1045                    }
1046                }
1047                OpenRouterRoutingPreset::NoDataCollection => {
1048                    derived.data_collection = Some(OpenRouterDataCollection::Deny);
1049                }
1050                OpenRouterRoutingPreset::StrictJson
1051                | OpenRouterRoutingPreset::ReasoningRequired => {
1052                    derived.require_parameters = Some(true);
1053                }
1054                OpenRouterRoutingPreset::MaxPrice {
1055                    prompt_usd_per_million,
1056                    completion_usd_per_million,
1057                } => {
1058                    if prompt_usd_per_million.is_some_and(|v| v < 0.0)
1059                        || completion_usd_per_million.is_some_and(|v| v < 0.0)
1060                    {
1061                        return Err(
1062                            "MaxPrice preset values must be non-negative USD per million tokens"
1063                                .to_string(),
1064                        );
1065                    }
1066                    if prompt_usd_per_million.is_some() || completion_usd_per_million.is_some() {
1067                        let mp = derived.max_price.get_or_insert_with(Default::default);
1068                        if let Some(p) = prompt_usd_per_million {
1069                            mp.prompt = Some(p / 1_000_000.0);
1070                        }
1071                        if let Some(c) = completion_usd_per_million {
1072                            mp.completion = Some(c / 1_000_000.0);
1073                        }
1074                    }
1075                }
1076            }
1077        }
1078
1079        // Explicit provider fields override preset-derived values.
1080        let merged = merge_provider_routing(derived, self.provider.clone().unwrap_or_default());
1081
1082        let mut result = self.clone();
1083        result.presets = vec![];
1084        result.provider = if merged.is_empty() {
1085            None
1086        } else {
1087            Some(merged)
1088        };
1089        Ok(result)
1090    }
1091}
1092
1093/// Merge preset-derived provider routing with explicit provider overrides.
1094/// Explicit fields always win; preset-derived fields fill gaps where explicit
1095/// fields are absent (None / empty Vec).
1096fn merge_provider_routing(
1097    derived: OpenRouterProviderRouting,
1098    explicit: OpenRouterProviderRouting,
1099) -> OpenRouterProviderRouting {
1100    OpenRouterProviderRouting {
1101        order: if !explicit.order.is_empty() {
1102            explicit.order
1103        } else {
1104            derived.order
1105        },
1106        only: if !explicit.only.is_empty() {
1107            explicit.only
1108        } else {
1109            derived.only
1110        },
1111        ignore: if !explicit.ignore.is_empty() {
1112            explicit.ignore
1113        } else {
1114            derived.ignore
1115        },
1116        allow_fallbacks: explicit.allow_fallbacks.or(derived.allow_fallbacks),
1117        require_parameters: explicit.require_parameters.or(derived.require_parameters),
1118        data_collection: explicit.data_collection.or(derived.data_collection),
1119        zdr: explicit.zdr.or(derived.zdr),
1120        enforce_distillable_text: explicit
1121            .enforce_distillable_text
1122            .or(derived.enforce_distillable_text),
1123        quantizations: if !explicit.quantizations.is_empty() {
1124            explicit.quantizations
1125        } else {
1126            derived.quantizations
1127        },
1128        sort: explicit.sort.or(derived.sort),
1129        max_price: explicit.max_price.or(derived.max_price),
1130    }
1131}
1132
1133/// OpenRouter route strategy.
1134#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
1135#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1136#[serde(rename_all = "snake_case")]
1137pub enum OpenRouterRoute {
1138    Fallback,
1139}
1140
1141/// OpenRouter provider routing preferences.
1142#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]
1143#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1144pub struct OpenRouterProviderRouting {
1145    /// Provider slugs to try first, in order.
1146    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1147    pub order: Vec<String>,
1148    /// Restrict routing to these provider slugs.
1149    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1150    pub only: Vec<String>,
1151    /// Provider slugs to skip.
1152    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1153    pub ignore: Vec<String>,
1154    /// Whether OpenRouter may fall back outside the ordered/allowed providers.
1155    #[serde(default, skip_serializing_if = "Option::is_none")]
1156    pub allow_fallbacks: Option<bool>,
1157    /// Require routed providers to support all request parameters.
1158    #[serde(default, skip_serializing_if = "Option::is_none")]
1159    pub require_parameters: Option<bool>,
1160    /// Restrict routing by provider data-retention policy.
1161    #[serde(default, skip_serializing_if = "Option::is_none")]
1162    pub data_collection: Option<OpenRouterDataCollection>,
1163    /// Restrict routing to zero-data-retention endpoints.
1164    #[serde(default, skip_serializing_if = "Option::is_none")]
1165    pub zdr: Option<bool>,
1166    /// Restrict routing to distillable-text endpoints.
1167    #[serde(default, skip_serializing_if = "Option::is_none")]
1168    pub enforce_distillable_text: Option<bool>,
1169    /// Restrict routing to provider quantization levels.
1170    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1171    pub quantizations: Vec<String>,
1172    /// Sort provider endpoints by price, throughput, or latency.
1173    #[serde(default, skip_serializing_if = "Option::is_none")]
1174    pub sort: Option<OpenRouterProviderSort>,
1175    /// Maximum accepted per-unit provider price.
1176    #[serde(default, skip_serializing_if = "Option::is_none")]
1177    pub max_price: Option<OpenRouterMaxPrice>,
1178}
1179
1180impl OpenRouterProviderRouting {
1181    pub fn is_empty(&self) -> bool {
1182        self.order.is_empty()
1183            && self.only.is_empty()
1184            && self.ignore.is_empty()
1185            && self.allow_fallbacks.is_none()
1186            && self.require_parameters.is_none()
1187            && self.data_collection.is_none()
1188            && self.zdr.is_none()
1189            && self.enforce_distillable_text.is_none()
1190            && self.quantizations.is_empty()
1191            && self.sort.is_none()
1192            && self.max_price.is_none()
1193    }
1194}
1195
1196/// OpenRouter provider data-retention preference.
1197#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
1198#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1199#[serde(rename_all = "snake_case")]
1200pub enum OpenRouterDataCollection {
1201    Allow,
1202    Deny,
1203}
1204
1205/// OpenRouter provider sort preference.
1206#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
1207#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1208#[serde(untagged)]
1209pub enum OpenRouterProviderSort {
1210    Simple(OpenRouterProviderSortBy),
1211    Advanced(OpenRouterProviderSortOptions),
1212}
1213
1214/// OpenRouter provider sorting dimension.
1215#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
1216#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1217#[serde(rename_all = "snake_case")]
1218pub enum OpenRouterProviderSortBy {
1219    Price,
1220    Throughput,
1221    Latency,
1222}
1223
1224/// OpenRouter advanced provider sort options.
1225#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
1226#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1227pub struct OpenRouterProviderSortOptions {
1228    pub by: OpenRouterProviderSortBy,
1229    #[serde(default, skip_serializing_if = "Option::is_none")]
1230    pub partition: Option<OpenRouterSortPartition>,
1231}
1232
1233/// How OpenRouter sorts endpoints when multiple fallback models are present.
1234#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
1235#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1236#[serde(rename_all = "snake_case")]
1237pub enum OpenRouterSortPartition {
1238    Model,
1239    None,
1240}
1241
1242/// Maximum accepted OpenRouter provider pricing, expressed in dollars per
1243/// million prompt/completion tokens or per request/image where supported.
1244#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]
1245#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1246pub struct OpenRouterMaxPrice {
1247    #[serde(default, skip_serializing_if = "Option::is_none")]
1248    pub prompt: Option<f64>,
1249    #[serde(default, skip_serializing_if = "Option::is_none")]
1250    pub completion: Option<f64>,
1251    #[serde(default, skip_serializing_if = "Option::is_none")]
1252    pub request: Option<f64>,
1253    #[serde(default, skip_serializing_if = "Option::is_none")]
1254    pub image: Option<f64>,
1255}
1256
1257/// OpenRouter web-search plugin configuration.
1258///
1259/// Instructs OpenRouter to retrieve and inject web search results before the
1260/// model sees the prompt. Only sent when the resolved provider type is
1261/// OpenRouter.
1262#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]
1263#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1264pub struct OpenRouterWebSearchPlugin {
1265    /// Maximum number of search results to include.
1266    #[serde(default, skip_serializing_if = "Option::is_none")]
1267    pub max_results: Option<u32>,
1268    /// Custom search prompt hint passed to the web-search step.
1269    #[serde(default, skip_serializing_if = "Option::is_none")]
1270    pub search_prompt: Option<String>,
1271}
1272
1273/// OpenRouter file-reader plugin configuration.
1274///
1275/// Instructs OpenRouter to read and attach file contents before the model
1276/// sees the prompt. Only sent when the resolved provider type is OpenRouter.
1277#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]
1278#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1279pub struct OpenRouterFilePlugin {}
1280
1281/// OpenRouter plugin configuration bundling optional plugin activations.
1282///
1283/// Any `None` plugin is omitted from the wire request. When all plugins are
1284/// `None`, no `plugins` field is emitted.
1285#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]
1286#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1287pub struct OpenRouterPluginConfig {
1288    /// Web-search plugin.
1289    #[serde(default, skip_serializing_if = "Option::is_none")]
1290    pub web: Option<OpenRouterWebSearchPlugin>,
1291    /// File-reader plugin.
1292    #[serde(default, skip_serializing_if = "Option::is_none")]
1293    pub file: Option<OpenRouterFilePlugin>,
1294}
1295
1296impl OpenRouterPluginConfig {
1297    pub fn is_empty(&self) -> bool {
1298        self.web.is_none() && self.file.is_none()
1299    }
1300}
1301
1302/// Metadata key consumed by the OpenRouter driver as `HTTP-Referer`.
1303pub const OPENROUTER_HTTP_REFERER_METADATA_KEY: &str = "openrouter.http_referer";
1304/// Metadata key consumed by the OpenRouter driver as `X-Title`.
1305pub const OPENROUTER_X_TITLE_METADATA_KEY: &str = "openrouter.x_title";
1306
1307/// Configuration for an LLM call
1308#[derive(Debug, Clone)]
1309pub struct LlmCallConfig {
1310    pub model: String,
1311    pub temperature: Option<f32>,
1312    pub max_tokens: Option<u32>,
1313    pub tools: Vec<ToolDefinition>,
1314    /// Reasoning effort level (for models that support it: low, medium, high)
1315    pub reasoning_effort: Option<String>,
1316    /// Speed (service tier) for this call: "flex", "default", or "priority".
1317    /// Serialized as OpenAI `service_tier`; omitted when `None` so the
1318    /// provider keeps its default ("auto") routing.
1319    pub speed: Option<String>,
1320    /// Verbosity for this call: "low", "medium", or "high". Serialized as
1321    /// OpenAI `verbosity`; omitted when `None` so the provider keeps its
1322    /// default ("medium") output length.
1323    pub verbosity: Option<String>,
1324    /// Metadata to send with the API request for tracking and debugging.
1325    /// Keys and values are strings. Both OpenAI and Anthropic support metadata fields.
1326    /// Typically includes: session_id, agent_id, org_id, turn_id, exec_id.
1327    pub metadata: HashMap<String, String>,
1328    /// Previous response ID for stateful continuation (OpenAI Responses API).
1329    /// When set, the provider can skip re-encoding cached context.
1330    pub previous_response_id: Option<String>,
1331    /// Standalone, ordered native compact output for this request.
1332    ///
1333    /// This is mutually exclusive with `previous_response_id`. Provider
1334    /// drivers must serialize it as the request input without transcript-delta
1335    /// trimming or structural pruning.
1336    pub provider_opaque_context: Option<ProviderOpaqueContext>,
1337    /// Tool search configuration for deferred tool loading
1338    pub tool_search: Option<ToolSearchConfig>,
1339    /// Prompt caching configuration for provider-specific cache controls.
1340    pub prompt_cache: Option<PromptCacheConfig>,
1341    /// OpenRouter-only model fallback and provider routing controls.
1342    pub openrouter_routing: Option<OpenRouterRoutingConfig>,
1343    /// Request-level parallel tool calling preference (EVE-598).
1344    ///
1345    /// Serialized onto the provider request when `Some(_)`: OpenAI sets
1346    /// `parallel_tool_calls`; Anthropic maps `Some(false)` →
1347    /// `tool_choice.disable_parallel_tool_use = true`. `None` preserves
1348    /// provider defaults (no field sent).
1349    pub parallel_tool_calls: Option<bool>,
1350    /// Number of trailing messages that are volatile (regenerated every turn)
1351    /// and must not anchor a message-level prompt-cache breakpoint.
1352    ///
1353    /// `ReasonAtom` sets this to the count of live `<facts>` messages it appends
1354    /// at the conversation tail. Drivers that place a message cache breakpoint
1355    /// on the last block (Anthropic) skip this many trailing messages so the
1356    /// breakpoint lands on the last *stable* block — otherwise a tail that
1357    /// changes each turn would evict the conversation-history cache. `0` (the
1358    /// default) preserves the previous behavior exactly.
1359    pub volatile_suffix_len: usize,
1360}
1361
1362impl LlmCallConfig {
1363    /// Resolve the effective wire value for `parallel_tool_calls`, gated by
1364    /// whether the driver/model can express it on the request.
1365    ///
1366    /// Returns `None` (omit the field, keep the provider default) when the
1367    /// preference is unset or `supported` is `false`. Drivers call this with
1368    /// `self.supports_parallel_tool_calls(&config.model)` so the preference is
1369    /// only serialized where the provider has a control for it. The local tool
1370    /// scheduler honors the preference independently, so `Some(false)` still
1371    /// serializes execution even when this returns `None`.
1372    pub fn resolved_parallel_tool_calls(&self, supported: bool) -> Option<bool> {
1373        if supported {
1374            self.parallel_tool_calls
1375        } else {
1376            None
1377        }
1378    }
1379}
1380
1381// The `From<&RuntimeAgent>` adapter for LlmCallConfig lives in
1382// everruns-core (`llm_conversions`), since RuntimeAgent is a core domain type.
1383
1384/// Response from an LLM call (non-streaming)
1385#[derive(Debug, Clone)]
1386pub struct LlmResponse {
1387    pub text: String,
1388    /// Thinking content from extended thinking models (e.g., Claude with thinking enabled)
1389    pub thinking: Option<String>,
1390    /// Cryptographic signature for thinking content (Anthropic Claude)
1391    pub thinking_signature: Option<String>,
1392    pub tool_calls: Option<Vec<ToolCall>>,
1393    pub metadata: LlmCompletionMetadata,
1394}
1395
1396/// Builder for LlmCallConfig with fluent API
1397///
1398/// Chain methods like `reasoning_effort()`, `temperature()`, etc. and call
1399/// `build()` to get the final config. To start from a core `RuntimeAgent`, use
1400/// `everruns_core::llm_conversions::llm_call_config_builder_from_agent`.
1401pub struct LlmCallConfigBuilder {
1402    config: LlmCallConfig,
1403}
1404
1405impl LlmCallConfigBuilder {
1406    /// Construct a builder wrapping an existing config.
1407    pub fn from_config(config: LlmCallConfig) -> Self {
1408        Self { config }
1409    }
1410
1411    /// Set reasoning effort level (for models that support it: low, medium, high)
1412    pub fn reasoning_effort(mut self, effort: impl Into<String>) -> Self {
1413        self.config.reasoning_effort = Some(effort.into());
1414        self
1415    }
1416
1417    /// Set speed (service tier): "flex", "default", or "priority"
1418    pub fn speed(mut self, speed: impl Into<String>) -> Self {
1419        self.config.speed = Some(speed.into());
1420        self
1421    }
1422
1423    /// Set verbosity: "low", "medium", or "high"
1424    pub fn verbosity(mut self, verbosity: impl Into<String>) -> Self {
1425        self.config.verbosity = Some(verbosity.into());
1426        self
1427    }
1428
1429    /// Set the model
1430    pub fn model(mut self, model: impl Into<String>) -> Self {
1431        self.config.model = model.into();
1432        self
1433    }
1434
1435    /// Set temperature
1436    pub fn temperature(mut self, temp: f32) -> Self {
1437        self.config.temperature = Some(temp);
1438        self
1439    }
1440
1441    /// Set max tokens
1442    pub fn max_tokens(mut self, tokens: u32) -> Self {
1443        self.config.max_tokens = Some(tokens);
1444        self
1445    }
1446
1447    /// Set tools
1448    pub fn tools(mut self, tools: Vec<ToolDefinition>) -> Self {
1449        self.config.tools = tools;
1450        self
1451    }
1452
1453    /// Set metadata for API tracking
1454    ///
1455    /// This metadata is sent to the LLM provider for tracking and debugging.
1456    /// Typically includes session_id, agent_id, org_id, turn_id, exec_id.
1457    pub fn metadata(mut self, metadata: HashMap<String, String>) -> Self {
1458        self.config.metadata = metadata;
1459        self
1460    }
1461
1462    /// Add a single metadata key-value pair
1463    pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
1464        self.config.metadata.insert(key.into(), value.into());
1465        self
1466    }
1467
1468    /// Set previous response ID for stateful continuation
1469    pub fn previous_response_id(mut self, id: Option<String>) -> Self {
1470        self.config.previous_response_id = id;
1471        self
1472    }
1473
1474    /// Set standalone provider-owned compact context for the request.
1475    pub fn provider_opaque_context(mut self, context: Option<ProviderOpaqueContext>) -> Self {
1476        self.config.provider_opaque_context = context;
1477        self
1478    }
1479
1480    /// Set tool_search configuration
1481    pub fn tool_search(mut self, config: ToolSearchConfig) -> Self {
1482        self.config.tool_search = Some(config);
1483        self
1484    }
1485
1486    /// Set prompt caching configuration
1487    pub fn prompt_cache(mut self, config: PromptCacheConfig) -> Self {
1488        self.config.prompt_cache = Some(config);
1489        self
1490    }
1491
1492    /// Set OpenRouter model fallback and provider routing controls.
1493    pub fn openrouter_routing(mut self, config: OpenRouterRoutingConfig) -> Self {
1494        self.config.openrouter_routing = (!config.is_empty()).then_some(config);
1495        self
1496    }
1497
1498    /// Set the request-level parallel tool calling preference (EVE-598).
1499    pub fn parallel_tool_calls(mut self, parallel_tool_calls: Option<bool>) -> Self {
1500        self.config.parallel_tool_calls = parallel_tool_calls;
1501        self
1502    }
1503
1504    /// Set the number of trailing volatile messages that must not anchor a
1505    /// message-level prompt-cache breakpoint (see
1506    /// [`LlmCallConfig::volatile_suffix_len`]).
1507    pub fn volatile_suffix_len(mut self, len: usize) -> Self {
1508        self.config.volatile_suffix_len = len;
1509        self
1510    }
1511
1512    /// Build the configuration
1513    pub fn build(self) -> LlmCallConfig {
1514        self.config
1515    }
1516}
1517
1518// The Message->LlmMessage adapters (plain, with-images, and image-file
1519// helpers) live in everruns-core (`llm_conversions`): they depend on core
1520// domain types (Message, ContentPart, ResolvedImage).
1521
1522// ============================================================================
1523// Driver Factory Types
1524// ============================================================================
1525
1526pub use crate::provider::DriverId;
1527
1528/// Extra provider-specific authentication/metadata beyond an API key.
1529///
1530/// Built-in providers ignore this; embedder-defined ([`DriverId::External`])
1531/// providers use it to carry OAuth tokens, account ids, or arbitrary extras
1532/// their driver factory needs.
1533#[derive(Debug, Clone, Default, PartialEq, Eq)]
1534pub struct ProviderMetadata {
1535    /// OAuth refresh token, when the provider authenticates via OAuth.
1536    pub refresh_token: Option<String>,
1537    /// Provider-side account identifier, when required.
1538    pub account_id: Option<String>,
1539    /// Arbitrary extra fields the driver factory understands.
1540    pub extra: Option<serde_json::Value>,
1541}
1542
1543/// Configuration for creating an LLM provider
1544#[derive(Debug, Clone)]
1545pub struct ProviderConfig {
1546    /// Type of provider
1547    pub provider_type: DriverId,
1548    /// API key for authentication
1549    pub api_key: Option<String>,
1550    /// Base URL override (optional)
1551    pub base_url: Option<String>,
1552    /// Extra provider-specific metadata (OAuth tokens, account ids, etc.).
1553    pub metadata: ProviderMetadata,
1554}
1555
1556impl ProviderConfig {
1557    /// Create a new provider config
1558    pub fn new(provider_type: DriverId) -> Self {
1559        Self {
1560            provider_type,
1561            api_key: None,
1562            base_url: None,
1563            metadata: ProviderMetadata::default(),
1564        }
1565    }
1566
1567    /// Set the API key
1568    pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
1569        self.api_key = Some(api_key.into());
1570        self
1571    }
1572
1573    /// Set the base URL
1574    pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {
1575        self.base_url = Some(base_url.into());
1576        self
1577    }
1578
1579    /// Set provider-specific metadata.
1580    pub fn with_metadata(mut self, metadata: ProviderMetadata) -> Self {
1581        self.metadata = metadata;
1582        self
1583    }
1584}
1585
1586/// Everything a [`DriverFactory`] receives to build a driver instance.
1587///
1588/// Replaces the old `(api_key, base_url)` factory arguments so that
1589/// embedder-defined providers can receive richer auth via [`ProviderMetadata`]
1590/// without changing the factory signature again.
1591#[derive(Debug, Clone)]
1592pub struct DriverConfig {
1593    /// Provider type being created.
1594    pub provider_type: DriverId,
1595    /// Raw credential document, when one is configured. `None` for keyless
1596    /// providers (LlmSim, or external providers that authenticate via
1597    /// [`ProviderMetadata`]). For single-key drivers this is the API key
1598    /// verbatim; multi-field drivers should read [`DriverConfig::credentials`]
1599    /// instead of parsing this string.
1600    pub api_key: Option<String>,
1601    /// Typed credential fields parsed from the stored credential document (see
1602    /// [`crate::credential_schema::parse_credential_document`]). Multi-field
1603    /// drivers (Bedrock AWS keys, MAI Entra OAuth) read their declared fields
1604    /// from here instead of hand-parsing JSON out of `api_key`. Empty for
1605    /// keyless providers.
1606    pub credentials: std::collections::BTreeMap<String, String>,
1607    /// Base URL override, when configured.
1608    pub base_url: Option<String>,
1609    /// Extra provider-specific metadata.
1610    pub metadata: ProviderMetadata,
1611}
1612
1613impl DriverConfig {
1614    /// Build a driver config from a resolved [`ProviderConfig`], parsing the
1615    /// credential document into the typed [`DriverConfig::credentials`] map.
1616    /// This is the single point where the stored credential string becomes
1617    /// typed fields, so every driver-creation path (server, worker, sync, dev)
1618    /// gets the same typed view.
1619    pub fn from_provider_config(config: &ProviderConfig) -> Self {
1620        Self {
1621            provider_type: config.provider_type.clone(),
1622            credentials: crate::credential_schema::parse_credential_document(
1623                config.api_key.as_deref(),
1624            ),
1625            api_key: config.api_key.clone(),
1626            base_url: config.base_url.clone(),
1627            metadata: config.metadata.clone(),
1628        }
1629    }
1630
1631    /// A declared credential field's non-empty value, if present.
1632    pub fn credential(&self, name: &str) -> Option<&str> {
1633        self.credentials
1634            .get(name)
1635            .map(String::as_str)
1636            .filter(|s| !s.is_empty())
1637    }
1638}
1639
1640// The `From<&ResolvedModel>` adapter for ProviderConfig lives in
1641// everruns-core (`llm_conversions`), since ResolvedModel is a core domain type.
1642
1643/// Boxed chat driver for dynamic dispatch
1644pub type BoxedChatDriver = Box<dyn ChatDriver>;
1645
1646// ============================================================================
1647// EmbeddingsDriver Trait
1648// ============================================================================
1649
1650/// Request to embed a batch of text strings into dense vectors.
1651#[derive(Debug, Clone)]
1652pub struct EmbedRequest {
1653    /// Texts to embed. All texts in a batch share the same model.
1654    pub texts: Vec<String>,
1655    /// Provider-side model id (e.g. `text-embedding-3-small`).
1656    pub model: String,
1657}
1658
1659/// Response from an embedding request.
1660#[derive(Debug, Clone)]
1661pub struct EmbedResponse {
1662    /// One float vector per input text, in the same order.
1663    pub embeddings: Vec<Vec<f32>>,
1664    /// Total tokens consumed (for usage tracking). `None` if the provider
1665    /// does not report token counts.
1666    pub usage_tokens: Option<u32>,
1667}
1668
1669/// Error returned by [`EmbeddingsDriver::embed`].
1670#[derive(Debug, thiserror::Error)]
1671pub enum EmbeddingsDriverError {
1672    #[error("embeddings provider returned an error: {0}")]
1673    Provider(String),
1674    #[error("embeddings request failed: {0}")]
1675    Transport(String),
1676}
1677
1678/// Driver trait for text embedding services.
1679///
1680/// Implementors call their provider's embedding API and return dense float
1681/// vectors. Used by knowledge-base hybrid retrieval (see specs/knowledge-bases.md
1682/// and specs/providers.md phase 6).
1683#[async_trait]
1684pub trait EmbeddingsDriver: Send + Sync {
1685    /// Embed a batch of texts and return one vector per input.
1686    async fn embed(
1687        &self,
1688        request: EmbedRequest,
1689    ) -> std::result::Result<EmbedResponse, EmbeddingsDriverError>;
1690}
1691
1692/// Boxed embeddings driver for dynamic dispatch.
1693pub type BoxedEmbeddingsDriver = Box<dyn EmbeddingsDriver>;
1694
1695/// Factory function type for creating embeddings drivers.
1696pub type EmbeddingsDriverFactory =
1697    Arc<dyn Fn(&DriverConfig) -> BoxedEmbeddingsDriver + Send + Sync>;
1698
1699// ============================================================================
1700// Driver Registry
1701// ============================================================================
1702
1703/// Factory function type for creating chat drivers.
1704///
1705/// Receives a [`DriverConfig`] (provider type, optional key/base URL, and
1706/// provider metadata) and returns a boxed driver.
1707pub type DriverFactory = Arc<dyn Fn(&DriverConfig) -> BoxedChatDriver + Send + Sync>;
1708
1709/// A typed service a provider driver can offer (see specs/providers.md).
1710///
1711/// Declared in code by each driver, never stored in the database. Only `Chat`
1712/// has a driver trait today; the set is additive and new kinds gain factories
1713/// on [`DriverDescriptor`] when their first consumer lands.
1714#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1715#[serde(rename_all = "snake_case")]
1716pub enum ServiceKind {
1717    /// Chat completion ([`ChatDriver`]).
1718    Chat,
1719    /// Text embeddings (planned: knowledge-base hybrid retrieval).
1720    Embeddings,
1721    /// Realtime voice sessions (server-side adapter using provider credentials).
1722    Realtime,
1723    /// Image generation.
1724    Images,
1725    /// Search-result reranking.
1726    Rerank,
1727}
1728
1729impl std::fmt::Display for ServiceKind {
1730    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1731        let s = match self {
1732            ServiceKind::Chat => "chat",
1733            ServiceKind::Embeddings => "embeddings",
1734            ServiceKind::Realtime => "realtime",
1735            ServiceKind::Images => "images",
1736            ServiceKind::Rerank => "rerank",
1737        };
1738        f.write_str(s)
1739    }
1740}
1741
1742/// Wire flavor of a driver's interactive OAuth connect flow.
1743///
1744/// A driver may let an org admin connect a provider by authorizing in the
1745/// browser instead of pasting an API key. The flow always yields a long-lived
1746/// credential that lands in `providers.credentials_encrypted`, exactly like a
1747/// hand-entered key — so runtime resolution is unchanged and non-admin users
1748/// are unaffected (see specs/providers.md "OAuth provider connection").
1749///
1750/// Only OpenRouter's PKCE flavor exists today. Adding OAuth to another driver
1751/// means a new variant here (which the server matches on) plus a
1752/// [`DriverOAuthConfig`] on that driver's descriptor — never a parallel set of
1753/// endpoints.
1754#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1755pub enum DriverOAuthFlow {
1756    /// OpenRouter one-click PKCE
1757    /// (<https://openrouter.ai/docs/guides/overview/auth/oauth>): redirect the
1758    /// admin to `authorize_url?callback_url=..&code_challenge=..&code_challenge_method=S256`,
1759    /// then POST JSON `{code, code_verifier, code_challenge_method}` to
1760    /// `token_url`; the `key` field of the response is the user-controlled API
1761    /// key to store. No client registration or secret is required (public PKCE
1762    /// client).
1763    OpenRouterPkce,
1764}
1765
1766/// A driver's declared OAuth connect flow.
1767///
1768/// Presence of this on a [`DriverDescriptor`] is what makes "Connect with
1769/// {provider}" available; absence means credentials must be entered manually.
1770#[derive(Debug, Clone)]
1771pub struct DriverOAuthConfig {
1772    /// Authorization endpoint the admin's browser is redirected to.
1773    pub authorize_url: String,
1774    /// Endpoint that exchanges the returned authorization code for a credential.
1775    pub token_url: String,
1776    /// Wire flavor of the two steps above.
1777    pub flow: DriverOAuthFlow,
1778}
1779
1780impl DriverOAuthConfig {
1781    /// OpenRouter's one-click PKCE connect flow.
1782    pub fn openrouter() -> Self {
1783        Self {
1784            authorize_url: "https://openrouter.ai/auth".to_string(),
1785            token_url: "https://openrouter.ai/api/v1/auth/keys".to_string(),
1786            flow: DriverOAuthFlow::OpenRouterPkce,
1787        }
1788    }
1789}
1790
1791/// A registered provider driver: identity, declared services, the credential
1792/// shape its providers must supply, and per-service factories.
1793///
1794/// The descriptor is the code-side unit of the providers domain model
1795/// (specs/providers.md): one descriptor per driver id, instantiated as many
1796/// org-scoped providers.
1797#[derive(Clone)]
1798pub struct DriverDescriptor {
1799    /// Driver id (also the registry key).
1800    pub id: DriverId,
1801    /// Human-readable driver name (e.g. "OpenAI", "AWS Bedrock").
1802    pub display_name: String,
1803    /// Services this driver's providers can power. Declared, not stored.
1804    pub services: Vec<ServiceKind>,
1805    /// Credential fields a provider instance must supply.
1806    pub credential_schema: CredentialFormSchema,
1807    /// Optional interactive OAuth connect flow. `Some` makes "Connect with
1808    /// {provider}" available as an alternative to entering a key by hand.
1809    pub oauth: Option<DriverOAuthConfig>,
1810    /// Chat service factory. `None` for drivers that only offer other services.
1811    pub chat: Option<DriverFactory>,
1812    /// Embeddings service factory. `None` for drivers that do not support embeddings.
1813    pub embeddings: Option<EmbeddingsDriverFactory>,
1814}
1815
1816impl DriverDescriptor {
1817    /// Descriptor for a chat-only driver with the default credential schema
1818    /// for the driver id (a single required `api_key` field for real
1819    /// providers; empty for `LlmSim` and `External`, which may authenticate
1820    /// via [`ProviderMetadata`]) and a display name derived from the id.
1821    pub fn chat_only<F>(id: impl Into<DriverId>, factory: F) -> Self
1822    where
1823        F: Fn(&DriverConfig) -> BoxedChatDriver + Send + Sync + 'static,
1824    {
1825        let id = id.into();
1826        Self {
1827            display_name: default_display_name(&id),
1828            credential_schema: default_credential_schema(&id),
1829            services: vec![ServiceKind::Chat],
1830            oauth: None,
1831            chat: Some(Arc::new(factory)),
1832            embeddings: None,
1833            id,
1834        }
1835    }
1836
1837    /// Whether the driver declares the given service.
1838    pub fn supports(&self, service: ServiceKind) -> bool {
1839        self.services.contains(&service)
1840    }
1841}
1842
1843impl std::fmt::Debug for DriverDescriptor {
1844    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1845        f.debug_struct("DriverDescriptor")
1846            .field("id", &self.id)
1847            .field("display_name", &self.display_name)
1848            .field("services", &self.services)
1849            .field("oauth", &self.oauth.is_some())
1850            .field("chat", &self.chat.is_some())
1851            .field("embeddings", &self.embeddings.is_some())
1852            .finish()
1853    }
1854}
1855
1856fn default_display_name(id: &DriverId) -> String {
1857    match id {
1858        DriverId::OpenAI => "OpenAI".to_string(),
1859        DriverId::OpenRouter => "OpenRouter".to_string(),
1860        DriverId::AzureOpenAI => "Azure OpenAI".to_string(),
1861        DriverId::OpenAICompletions => "OpenAI (Chat Completions)".to_string(),
1862        DriverId::Anthropic => "Anthropic".to_string(),
1863        DriverId::Gemini => "Google Gemini".to_string(),
1864        DriverId::Bedrock => "AWS Bedrock".to_string(),
1865        DriverId::Mai => "Microsoft MAI".to_string(),
1866        DriverId::Fireworks => "Fireworks AI".to_string(),
1867        DriverId::Meta => "Meta Model API".to_string(),
1868        DriverId::LlmSim => "LLM Simulator".to_string(),
1869        DriverId::External(id) => id.to_string(),
1870    }
1871}
1872
1873fn default_credential_schema(id: &DriverId) -> CredentialFormSchema {
1874    match id {
1875        // Keyless: simulator always; external drivers may auth via metadata.
1876        DriverId::LlmSim | DriverId::External(_) => CredentialFormSchema::empty(),
1877        _ => CredentialFormSchema::api_key(String::new()),
1878    }
1879}
1880
1881/// Registry for LLM drivers
1882///
1883/// Enables dependency inversion: provider crates (everruns-anthropic, everruns-openai)
1884/// register their drivers at startup. The core has no direct knowledge of implementations.
1885///
1886/// # Example
1887///
1888/// ```ignore
1889/// use everruns_core::{DriverRegistry, DriverId};
1890/// use everruns_anthropic::register_driver;
1891/// use everruns_openai::register_driver as register_openai;
1892///
1893/// let mut registry = DriverRegistry::new();
1894/// everruns_anthropic::register_driver(&mut registry);
1895/// everruns_openai::register_driver(&mut registry);
1896///
1897/// // Later, create a driver from config
1898/// let driver = registry.create_chat_driver(&config)?;
1899/// ```
1900#[derive(Clone, Default)]
1901pub struct DriverRegistry {
1902    descriptors: HashMap<DriverId, DriverDescriptor>,
1903}
1904
1905impl DriverRegistry {
1906    /// Create a new empty registry
1907    pub fn new() -> Self {
1908        Self {
1909            descriptors: HashMap::new(),
1910        }
1911    }
1912
1913    /// Register a full driver descriptor.
1914    ///
1915    /// Panics if a descriptor is already registered for the same driver id —
1916    /// silent overwrites hide double-registration bugs. Use
1917    /// [`Self::register_descriptor_or_replace`] to overwrite intentionally.
1918    pub fn register_descriptor(&mut self, descriptor: DriverDescriptor) {
1919        if self.descriptors.contains_key(&descriptor.id) {
1920            panic!(
1921                "driver already registered for provider '{}'; \
1922                 use register_descriptor_or_replace to overwrite intentionally",
1923                descriptor.id
1924            );
1925        }
1926        self.descriptors.insert(descriptor.id.clone(), descriptor);
1927    }
1928
1929    /// Register a full driver descriptor, replacing any existing one.
1930    pub fn register_descriptor_or_replace(&mut self, descriptor: DriverDescriptor) {
1931        self.descriptors.insert(descriptor.id.clone(), descriptor);
1932    }
1933
1934    /// Register a driver factory for a provider type.
1935    ///
1936    /// Panics if a factory is already registered for `provider_type` — silent
1937    /// overwrites hide double-registration bugs. Use
1938    /// [`Self::register_or_replace`] to overwrite intentionally.
1939    pub fn register<F>(&mut self, provider_type: impl Into<DriverId>, factory: F)
1940    where
1941        F: Fn(&DriverConfig) -> BoxedChatDriver + Send + Sync + 'static,
1942    {
1943        self.register_descriptor(DriverDescriptor::chat_only(provider_type, factory));
1944    }
1945
1946    /// Register a driver factory, replacing any existing one for the provider.
1947    ///
1948    /// Use when overwriting is intentional (e.g. swapping in an `LlmSim` driver
1949    /// for tests). Prefer [`Self::register`] otherwise so duplicates surface.
1950    pub fn register_or_replace<F>(&mut self, provider_type: impl Into<DriverId>, factory: F)
1951    where
1952        F: Fn(&DriverConfig) -> BoxedChatDriver + Send + Sync + 'static,
1953    {
1954        self.register_descriptor_or_replace(DriverDescriptor::chat_only(provider_type, factory));
1955    }
1956
1957    /// Register a driver factory for an embedder-defined external provider,
1958    /// keyed by its canonical id. The id is normalized to lowercase (via
1959    /// [`DriverId::external`]) so it matches parsed lookups regardless of
1960    /// the casing stored in the database or sent on the wire.
1961    pub fn register_external<F>(&mut self, id: impl Into<Arc<str>>, factory: F)
1962    where
1963        F: Fn(&DriverConfig) -> BoxedChatDriver + Send + Sync + 'static,
1964    {
1965        self.register(DriverId::external(id), factory);
1966    }
1967
1968    /// Create an LLM driver based on configuration
1969    ///
1970    /// API keys must be provided in the config for real providers. This function does NOT fall back to
1971    /// environment variables. Keys should be decrypted from the database and passed here.
1972    /// Exception: `LlmSim` and `External` providers do not require an API key
1973    /// (external providers may authenticate via [`ProviderMetadata`]).
1974    ///
1975    /// Returns `DriverNotRegistered` error if no driver is registered for the provider type.
1976    pub fn create_chat_driver(&self, config: &ProviderConfig) -> Result<BoxedChatDriver> {
1977        // API key is required for real built-in providers, but not for LlmSim
1978        // (testing), External providers, or Mai (which may all authenticate via
1979        // metadata-based auth — Mai supports Entra ID OAuth without an api_key).
1980        let requires_api_key = !matches!(
1981            config.provider_type,
1982            DriverId::LlmSim | DriverId::External(_) | DriverId::Mai
1983        );
1984        if requires_api_key && config.api_key.is_none() {
1985            return Err(AgentLoopError::llm(
1986                "API key is required. Configure the API key in provider settings.",
1987            ));
1988        }
1989
1990        // Look up the descriptor and its chat factory for this provider type
1991        let descriptor = self.descriptors.get(&config.provider_type).ok_or_else(|| {
1992            AgentLoopError::driver_not_registered(config.provider_type.to_string())
1993        })?;
1994        let factory = descriptor.chat.as_ref().ok_or_else(|| {
1995            AgentLoopError::llm(format!(
1996                "Provider driver '{}' does not implement the chat service.",
1997                config.provider_type
1998            ))
1999        })?;
2000
2001        // Create the driver using the factory
2002        let driver_config = DriverConfig::from_provider_config(config);
2003        Ok(factory(&driver_config))
2004    }
2005
2006    /// Check if a driver is registered for a provider type
2007    pub fn has_driver(&self, provider_type: &DriverId) -> bool {
2008        self.descriptors.contains_key(provider_type)
2009    }
2010
2011    /// Get the registered descriptor for a provider type.
2012    pub fn descriptor(&self, provider_type: &DriverId) -> Option<&DriverDescriptor> {
2013        self.descriptors.get(provider_type)
2014    }
2015
2016    /// Whether the registered driver declares the given service.
2017    pub fn supports(&self, provider_type: &DriverId, service: ServiceKind) -> bool {
2018        self.descriptors
2019            .get(provider_type)
2020            .is_some_and(|d| d.supports(service))
2021    }
2022
2023    /// Driver ids whose descriptors declare the given service.
2024    pub fn providers_for(&self, service: ServiceKind) -> Vec<DriverId> {
2025        self.descriptors
2026            .values()
2027            .filter(|d| d.supports(service))
2028            .map(|d| d.id.clone())
2029            .collect()
2030    }
2031
2032    /// Get the list of registered provider types
2033    pub fn registered_providers(&self) -> Vec<DriverId> {
2034        self.descriptors.keys().cloned().collect()
2035    }
2036
2037    /// Create an embeddings driver based on configuration.
2038    ///
2039    /// API keys must be provided in the config for real providers. Exception:
2040    /// `LlmSim` and `External` providers do not require an API key.
2041    ///
2042    /// Returns an error if the driver is not registered or does not implement
2043    /// the embeddings service.
2044    pub fn create_embeddings_driver(
2045        &self,
2046        config: &ProviderConfig,
2047    ) -> std::result::Result<BoxedEmbeddingsDriver, EmbeddingsDriverError> {
2048        let requires_api_key = !matches!(
2049            config.provider_type,
2050            DriverId::LlmSim | DriverId::External(_)
2051        );
2052        if requires_api_key && config.api_key.is_none() {
2053            return Err(EmbeddingsDriverError::Provider(
2054                "API key is required. Configure the API key in provider settings.".to_string(),
2055            ));
2056        }
2057        let descriptor = self.descriptors.get(&config.provider_type).ok_or_else(|| {
2058            EmbeddingsDriverError::Provider(format!(
2059                "No driver registered for provider '{}'",
2060                config.provider_type
2061            ))
2062        })?;
2063        let factory = descriptor.embeddings.as_ref().ok_or_else(|| {
2064            EmbeddingsDriverError::Provider(format!(
2065                "Provider driver '{}' does not implement the embeddings service.",
2066                config.provider_type
2067            ))
2068        })?;
2069        let driver_config = DriverConfig::from_provider_config(config);
2070        Ok(factory(&driver_config))
2071    }
2072}
2073
2074/// Maximum tool result size in bytes before truncation (64 KiB).
2075/// Defense-in-depth backstop for tool results that bypass ActAtom hooks
2076/// (e.g. client-submitted or stored events). The primary hard limit is
2077/// enforced by `OutputHardLimitHook` (EVE-225) at tool execution time.
2078const MAX_TOOL_RESULT_BYTES: usize = 64 * 1024;
2079
2080const TRUNCATION_SUFFIX: &str =
2081    "\n\n[Output truncated — exceeded 64 KiB limit. Try quiet flags, pipes, or redirect to file.]";
2082
2083pub fn truncate_tool_result(text: String) -> String {
2084    if text.len() <= MAX_TOOL_RESULT_BYTES {
2085        return text;
2086    }
2087    let content_budget = MAX_TOOL_RESULT_BYTES.saturating_sub(TRUNCATION_SUFFIX.len());
2088    let mut end = content_budget;
2089    while end > 0 && !text.is_char_boundary(end) {
2090        end -= 1;
2091    }
2092    let mut truncated = text[..end].to_string();
2093    truncated.push_str(TRUNCATION_SUFFIX);
2094    truncated
2095}
2096
2097// ============================================================================
2098// Tests
2099// ============================================================================
2100
2101#[cfg(test)]
2102mod tests {
2103    use super::*;
2104
2105    #[test]
2106    fn test_disjoint_prompt_tokens_subtracts_cached_subset() {
2107        // Inclusive providers report a prompt count that includes cached reads;
2108        // normalization yields the non-cached remainder.
2109        assert_eq!(disjoint_prompt_tokens(1000, Some(800)), 200);
2110        // No cache reported => prompt count passes through unchanged.
2111        assert_eq!(disjoint_prompt_tokens(1000, None), 1000);
2112        assert_eq!(disjoint_prompt_tokens(1000, Some(0)), 1000);
2113        // Saturating: a provider reporting cache > input never underflows.
2114        assert_eq!(disjoint_prompt_tokens(800, Some(1000)), 0);
2115    }
2116
2117    #[test]
2118    fn test_chat_driver_defaults_are_conservative_and_boxed_capabilities_forward() {
2119        // Default trait impl is conservative: drivers opt in.
2120        struct DefaultDriver;
2121        #[async_trait]
2122        impl ChatDriver for DefaultDriver {
2123            async fn chat_completion_stream(
2124                &self,
2125                _messages: Vec<LlmMessage>,
2126                _config: &LlmCallConfig,
2127            ) -> Result<LlmResponseStream> {
2128                unreachable!()
2129            }
2130        }
2131        assert!(!DefaultDriver.supports_parallel_tool_calls("any-model"));
2132        assert!(!DefaultDriver.supports_stateful_responses());
2133
2134        struct StatefulDriver;
2135        #[async_trait]
2136        impl ChatDriver for StatefulDriver {
2137            async fn chat_completion_stream(
2138                &self,
2139                _messages: Vec<LlmMessage>,
2140                _config: &LlmCallConfig,
2141            ) -> Result<LlmResponseStream> {
2142                unreachable!()
2143            }
2144
2145            fn supports_stateful_responses(&self) -> bool {
2146                true
2147            }
2148        }
2149        let boxed: BoxedChatDriver = Box::new(StatefulDriver);
2150        assert!(boxed.supports_stateful_responses());
2151    }
2152
2153    #[test]
2154    fn test_fold_system_messages_none_when_absent() {
2155        let messages = vec![
2156            LlmMessage::text(LlmMessageRole::User, "hi"),
2157            LlmMessage::text(LlmMessageRole::Assistant, "ok"),
2158        ];
2159        assert_eq!(fold_system_messages(&messages), None);
2160    }
2161
2162    #[test]
2163    fn test_fold_system_messages_single() {
2164        let messages = vec![
2165            LlmMessage::text(LlmMessageRole::System, "AGENT-PROMPT"),
2166            LlmMessage::text(LlmMessageRole::User, "hi"),
2167        ];
2168        assert_eq!(
2169            fold_system_messages(&messages),
2170            Some("AGENT-PROMPT".to_string())
2171        );
2172    }
2173
2174    #[test]
2175    fn test_fold_system_messages_accumulates_in_order() {
2176        // The agent system prompt plus a later notice/summary System message
2177        // (infinity_context / compaction) must both survive, in order — the
2178        // later one must not overwrite the real agent system prompt.
2179        let messages = vec![
2180            LlmMessage::text(LlmMessageRole::System, "A"),
2181            LlmMessage::text(LlmMessageRole::User, "hi"),
2182            LlmMessage::text(LlmMessageRole::Assistant, "ok"),
2183            LlmMessage::text(LlmMessageRole::System, "B"),
2184        ];
2185        assert_eq!(fold_system_messages(&messages), Some("A\n\nB".to_string()));
2186    }
2187
2188    #[test]
2189    fn test_fold_system_messages_concatenates_parts() {
2190        let messages = vec![LlmMessage::parts(
2191            LlmMessageRole::System,
2192            vec![
2193                LlmContentPart::text("foo"),
2194                LlmContentPart::image("data:image/png;base64,xxx"),
2195                LlmContentPart::text("bar"),
2196            ],
2197        )];
2198        assert_eq!(fold_system_messages(&messages), Some("foobar".to_string()));
2199    }
2200
2201    #[test]
2202    fn test_openrouter_fallback_models_empty_is_empty() {
2203        let routing = OpenRouterRoutingConfig::fallback_models(std::iter::empty::<String>());
2204
2205        assert!(routing.is_empty());
2206        assert_eq!(routing.route, None);
2207    }
2208
2209    #[test]
2210    fn test_openrouter_routing_validates_primary_model() {
2211        let routing = OpenRouterRoutingConfig::fallback_models([
2212            "openai/gpt-5-mini",
2213            "anthropic/claude-sonnet-4.5",
2214        ]);
2215
2216        assert!(
2217            routing
2218                .validate_for_primary_model("openai/gpt-5-mini")
2219                .is_ok()
2220        );
2221        let err = routing
2222            .validate_for_primary_model("anthropic/claude-sonnet-4.5")
2223            .unwrap_err();
2224        assert!(err.contains("models[0]"));
2225    }
2226
2227    #[test]
2228    fn test_openrouter_routing_rejects_fallback_without_models() {
2229        let routing = OpenRouterRoutingConfig {
2230            route: Some(OpenRouterRoute::Fallback),
2231            ..Default::default()
2232        };
2233
2234        let err = routing
2235            .validate_for_primary_model("openai/gpt-5-mini")
2236            .unwrap_err();
2237        assert!(err.contains("requires at least one model"));
2238    }
2239
2240    #[test]
2241    fn test_openrouter_routing_serializes_request_fields() {
2242        let routing = OpenRouterRoutingConfig {
2243            models: vec![
2244                "openai/gpt-5-mini".to_string(),
2245                "anthropic/claude-sonnet-4.5".to_string(),
2246            ],
2247            route: Some(OpenRouterRoute::Fallback),
2248            provider: Some(OpenRouterProviderRouting {
2249                order: vec!["anthropic".to_string(), "openai".to_string()],
2250                allow_fallbacks: Some(false),
2251                require_parameters: Some(true),
2252                data_collection: Some(OpenRouterDataCollection::Deny),
2253                zdr: Some(true),
2254                sort: Some(OpenRouterProviderSort::Advanced(
2255                    OpenRouterProviderSortOptions {
2256                        by: OpenRouterProviderSortBy::Throughput,
2257                        partition: Some(OpenRouterSortPartition::None),
2258                    },
2259                )),
2260                max_price: Some(OpenRouterMaxPrice {
2261                    prompt: Some(1.0),
2262                    completion: Some(2.0),
2263                    ..Default::default()
2264                }),
2265                ..Default::default()
2266            }),
2267            ..Default::default()
2268        };
2269
2270        let json = serde_json::to_value(routing).unwrap();
2271
2272        assert_eq!(
2273            json,
2274            serde_json::json!({
2275                "models": [
2276                    "openai/gpt-5-mini",
2277                    "anthropic/claude-sonnet-4.5"
2278                ],
2279                "route": "fallback",
2280                "provider": {
2281                    "order": ["anthropic", "openai"],
2282                    "allow_fallbacks": false,
2283                    "require_parameters": true,
2284                    "data_collection": "deny",
2285                    "zdr": true,
2286                    "sort": {
2287                        "by": "throughput",
2288                        "partition": "none"
2289                    },
2290                    "max_price": {
2291                        "prompt": 1.0,
2292                        "completion": 2.0
2293                    }
2294                }
2295            })
2296        );
2297    }
2298
2299    #[test]
2300    fn test_provider_type_parsing() {
2301        assert_eq!("openai".parse::<DriverId>().unwrap(), DriverId::OpenAI);
2302        assert_eq!(
2303            "openrouter".parse::<DriverId>().unwrap(),
2304            DriverId::OpenRouter
2305        );
2306        assert_eq!(
2307            "openai_completions".parse::<DriverId>().unwrap(),
2308            DriverId::OpenAICompletions
2309        );
2310        assert_eq!(
2311            "azure_openai".parse::<DriverId>().unwrap(),
2312            DriverId::AzureOpenAI
2313        );
2314        assert_eq!(
2315            "anthropic".parse::<DriverId>().unwrap(),
2316            DriverId::Anthropic
2317        );
2318        assert_eq!("gemini".parse::<DriverId>().unwrap(), DriverId::Gemini);
2319        // Unknown ids parse to External rather than erroring.
2320        assert_eq!(
2321            "ollama".parse::<DriverId>().unwrap(),
2322            DriverId::external("ollama")
2323        );
2324        assert_eq!(
2325            "custom".parse::<DriverId>().unwrap(),
2326            DriverId::external("custom")
2327        );
2328    }
2329
2330    #[test]
2331    fn test_external_provider_id_is_case_insensitive() {
2332        // Built-in matching and external normalization are both case-folding,
2333        // so the same id in different casing resolves to one provider.
2334        assert_eq!("OpenAI".parse::<DriverId>().unwrap(), DriverId::OpenAI);
2335        assert_eq!(
2336            "Ollama".parse::<DriverId>().unwrap(),
2337            "ollama".parse::<DriverId>().unwrap()
2338        );
2339        assert_eq!(DriverId::external("OpenAI-Codex").as_str(), "openai-codex");
2340        // Registration and parsed lookup agree regardless of casing.
2341        assert_eq!(
2342            DriverId::external("MyProvider"),
2343            "myprovider".parse::<DriverId>().unwrap()
2344        );
2345    }
2346
2347    #[test]
2348    fn test_provider_type_display() {
2349        assert_eq!(DriverId::OpenAI.to_string(), "openai");
2350        assert_eq!(DriverId::OpenRouter.to_string(), "openrouter");
2351        assert_eq!(DriverId::AzureOpenAI.to_string(), "azure_openai");
2352        assert_eq!(
2353            DriverId::OpenAICompletions.to_string(),
2354            "openai_completions"
2355        );
2356        assert_eq!(DriverId::Anthropic.to_string(), "anthropic");
2357        assert_eq!(DriverId::Gemini.to_string(), "gemini");
2358    }
2359
2360    #[test]
2361    fn test_provider_config_builder() {
2362        let config = ProviderConfig::new(DriverId::Anthropic)
2363            .with_api_key("test-key")
2364            .with_base_url("https://custom.api.com");
2365
2366        assert_eq!(config.provider_type, DriverId::Anthropic);
2367        assert_eq!(config.api_key, Some("test-key".to_string()));
2368        assert_eq!(config.base_url, Some("https://custom.api.com".to_string()));
2369    }
2370
2371    #[test]
2372    fn test_driver_registry_requires_api_key() {
2373        // Register a mock factory
2374        let mut registry = DriverRegistry::new();
2375        registry.register(DriverId::OpenAI, |_config| {
2376            // Return a mock driver - just need something that compiles
2377            struct MockDriver;
2378            #[async_trait]
2379            impl ChatDriver for MockDriver {
2380                async fn chat_completion_stream(
2381                    &self,
2382                    _messages: Vec<LlmMessage>,
2383                    _config: &LlmCallConfig,
2384                ) -> Result<LlmResponseStream> {
2385                    unimplemented!()
2386                }
2387            }
2388            Box::new(MockDriver)
2389        });
2390
2391        // Driver without API key should fail
2392        let config = ProviderConfig::new(DriverId::OpenAI);
2393        let result = registry.create_chat_driver(&config);
2394        assert!(result.is_err());
2395
2396        // Driver with API key should succeed
2397        let config_with_key = ProviderConfig::new(DriverId::OpenAI).with_api_key("test-key");
2398        let result = registry.create_chat_driver(&config_with_key);
2399        assert!(result.is_ok());
2400    }
2401
2402    #[test]
2403    fn test_driver_registry_returns_error_for_unregistered_provider() {
2404        let registry = DriverRegistry::new();
2405        let config = ProviderConfig::new(DriverId::Anthropic).with_api_key("test-key");
2406
2407        let result = registry.create_chat_driver(&config);
2408
2409        // Should fail with DriverNotRegistered error
2410        if let Err(AgentLoopError::DriverNotRegistered(provider)) = result {
2411            assert_eq!(provider, "anthropic");
2412        } else {
2413            panic!("Expected DriverNotRegistered error");
2414        }
2415    }
2416
2417    #[test]
2418    fn test_driver_registry_registration() {
2419        let mut registry = DriverRegistry::new();
2420
2421        assert!(!registry.has_driver(&DriverId::OpenAI));
2422        assert!(!registry.has_driver(&DriverId::Anthropic));
2423
2424        registry.register(DriverId::OpenAI, |_config| {
2425            struct MockDriver;
2426            #[async_trait]
2427            impl ChatDriver for MockDriver {
2428                async fn chat_completion_stream(
2429                    &self,
2430                    _messages: Vec<LlmMessage>,
2431                    _config: &LlmCallConfig,
2432                ) -> Result<LlmResponseStream> {
2433                    unimplemented!()
2434                }
2435            }
2436            Box::new(MockDriver)
2437        });
2438
2439        assert!(registry.has_driver(&DriverId::OpenAI));
2440        assert!(!registry.has_driver(&DriverId::Anthropic));
2441    }
2442
2443    #[test]
2444    fn test_register_external_and_create_driver_without_api_key() {
2445        struct MockDriver;
2446        #[async_trait]
2447        impl ChatDriver for MockDriver {
2448            async fn chat_completion_stream(
2449                &self,
2450                _messages: Vec<LlmMessage>,
2451                _config: &LlmCallConfig,
2452            ) -> Result<LlmResponseStream> {
2453                unimplemented!()
2454            }
2455        }
2456
2457        let mut registry = DriverRegistry::new();
2458        registry.register_external("openai-codex", |config| {
2459            // External providers may authenticate via metadata, not an api_key.
2460            assert_eq!(config.provider_type, DriverId::external("openai-codex"));
2461            Box::new(MockDriver)
2462        });
2463
2464        assert!(registry.has_driver(&DriverId::external("openai-codex")));
2465
2466        // No api_key required for external providers.
2467        let config = ProviderConfig::new(DriverId::external("openai-codex")).with_metadata(
2468            ProviderMetadata {
2469                refresh_token: Some("rt".into()),
2470                ..Default::default()
2471            },
2472        );
2473        assert!(registry.create_chat_driver(&config).is_ok());
2474    }
2475
2476    #[test]
2477    fn test_register_defaults_to_chat_only_descriptor() {
2478        struct MockDriver;
2479        #[async_trait]
2480        impl ChatDriver for MockDriver {
2481            async fn chat_completion_stream(
2482                &self,
2483                _messages: Vec<LlmMessage>,
2484                _config: &LlmCallConfig,
2485            ) -> Result<LlmResponseStream> {
2486                unimplemented!()
2487            }
2488        }
2489
2490        let mut registry = DriverRegistry::new();
2491        registry.register(DriverId::Anthropic, |_config| Box::new(MockDriver));
2492
2493        let descriptor = registry.descriptor(&DriverId::Anthropic).unwrap();
2494        assert_eq!(descriptor.display_name, "Anthropic");
2495        assert_eq!(descriptor.services, vec![ServiceKind::Chat]);
2496        assert!(descriptor.chat.is_some());
2497        // Default credential shape is a single required api_key field.
2498        assert_eq!(descriptor.credential_schema.fields.len(), 1);
2499        assert_eq!(descriptor.credential_schema.fields[0].name, "api_key");
2500        assert!(descriptor.credential_schema.fields[0].required);
2501
2502        // Keyless drivers default to an empty schema.
2503        registry.register(DriverId::LlmSim, |_config| Box::new(MockDriver));
2504        let sim = registry.descriptor(&DriverId::LlmSim).unwrap();
2505        assert!(sim.credential_schema.fields.is_empty());
2506    }
2507
2508    #[test]
2509    fn test_descriptor_services_and_lookup() {
2510        struct MockDriver;
2511        #[async_trait]
2512        impl ChatDriver for MockDriver {
2513            async fn chat_completion_stream(
2514                &self,
2515                _messages: Vec<LlmMessage>,
2516                _config: &LlmCallConfig,
2517            ) -> Result<LlmResponseStream> {
2518                unimplemented!()
2519            }
2520        }
2521
2522        let mut registry = DriverRegistry::new();
2523        registry.register_descriptor(DriverDescriptor {
2524            services: vec![ServiceKind::Chat, ServiceKind::Realtime],
2525            ..DriverDescriptor::chat_only(DriverId::OpenAI, |_config| Box::new(MockDriver))
2526        });
2527        registry.register(DriverId::Anthropic, |_config| Box::new(MockDriver));
2528
2529        assert!(registry.supports(&DriverId::OpenAI, ServiceKind::Chat));
2530        assert!(registry.supports(&DriverId::OpenAI, ServiceKind::Realtime));
2531        assert!(!registry.supports(&DriverId::Anthropic, ServiceKind::Realtime));
2532        assert!(!registry.supports(&DriverId::Gemini, ServiceKind::Chat));
2533
2534        let realtime = registry.providers_for(ServiceKind::Realtime);
2535        assert_eq!(realtime, vec![DriverId::OpenAI]);
2536        let mut chat = registry.providers_for(ServiceKind::Chat);
2537        chat.sort_by_key(|p| p.to_string());
2538        assert_eq!(chat, vec![DriverId::Anthropic, DriverId::OpenAI]);
2539    }
2540
2541    #[test]
2542    fn test_create_chat_driver_fails_without_chat_factory() {
2543        let mut registry = DriverRegistry::new();
2544        registry.register_descriptor(DriverDescriptor {
2545            id: DriverId::external("embeddings-only"),
2546            display_name: "Embeddings Only".to_string(),
2547            services: vec![ServiceKind::Embeddings],
2548            credential_schema: CredentialFormSchema::empty(),
2549            oauth: None,
2550            chat: None,
2551            embeddings: None,
2552        });
2553
2554        let config = ProviderConfig::new(DriverId::external("embeddings-only"));
2555        let err = match registry.create_chat_driver(&config) {
2556            Ok(_) => panic!("expected error for missing chat factory"),
2557            Err(err) => err,
2558        };
2559        assert!(
2560            err.to_string()
2561                .contains("does not implement the chat service"),
2562            "unexpected error: {err}"
2563        );
2564    }
2565
2566    #[test]
2567    #[should_panic(expected = "already registered")]
2568    fn test_register_duplicate_panics() {
2569        struct MockDriver;
2570        #[async_trait]
2571        impl ChatDriver for MockDriver {
2572            async fn chat_completion_stream(
2573                &self,
2574                _messages: Vec<LlmMessage>,
2575                _config: &LlmCallConfig,
2576            ) -> Result<LlmResponseStream> {
2577                unimplemented!()
2578            }
2579        }
2580
2581        let mut registry = DriverRegistry::new();
2582        registry.register(DriverId::OpenAI, |_config| Box::new(MockDriver));
2583        // Second registration for the same provider must panic.
2584        registry.register(DriverId::OpenAI, |_config| Box::new(MockDriver));
2585    }
2586
2587    #[test]
2588    fn test_register_or_replace_overwrites() {
2589        struct MockDriver;
2590        #[async_trait]
2591        impl ChatDriver for MockDriver {
2592            async fn chat_completion_stream(
2593                &self,
2594                _messages: Vec<LlmMessage>,
2595                _config: &LlmCallConfig,
2596            ) -> Result<LlmResponseStream> {
2597                unimplemented!()
2598            }
2599        }
2600
2601        let mut registry = DriverRegistry::new();
2602        registry.register(DriverId::LlmSim, |_config| Box::new(MockDriver));
2603        // Replacing intentionally must not panic.
2604        registry.register_or_replace(DriverId::LlmSim, |_config| Box::new(MockDriver));
2605        assert!(registry.has_driver(&DriverId::LlmSim));
2606    }
2607
2608    #[test]
2609    fn test_prepend_text_prefix_simple_text() {
2610        let mut msg = LlmMessage::text(LlmMessageRole::User, "Hello bot");
2611        msg.prepend_text_prefix("[Alice] ");
2612        assert_eq!(msg.content_as_text(), "[Alice] Hello bot");
2613    }
2614
2615    #[test]
2616    fn test_prepend_text_prefix_parts() {
2617        let mut msg = LlmMessage::parts(
2618            LlmMessageRole::User,
2619            vec![
2620                LlmContentPart::Text {
2621                    text: "Hello".to_string(),
2622                },
2623                LlmContentPart::Image {
2624                    url: "data:image/png;base64,abc".to_string(),
2625                },
2626            ],
2627        );
2628        msg.prepend_text_prefix("[Bob] ");
2629        match &msg.content {
2630            LlmMessageContent::Parts(parts) => {
2631                if let LlmContentPart::Text { text } = &parts[0] {
2632                    assert_eq!(text, "[Bob] Hello");
2633                } else {
2634                    panic!("Expected text part");
2635                }
2636            }
2637            _ => panic!("Expected parts content"),
2638        }
2639    }
2640
2641    #[test]
2642    fn test_prepend_text_prefix_parts_no_text() {
2643        let mut msg = LlmMessage::parts(
2644            LlmMessageRole::User,
2645            vec![LlmContentPart::Image {
2646                url: "data:image/png;base64,abc".to_string(),
2647            }],
2648        );
2649        msg.prepend_text_prefix("[Eve] ");
2650        match &msg.content {
2651            LlmMessageContent::Parts(parts) => {
2652                assert_eq!(parts.len(), 2);
2653                if let LlmContentPart::Text { text } = &parts[0] {
2654                    assert_eq!(text, "[Eve] ");
2655                } else {
2656                    panic!("Expected prepended text part");
2657                }
2658            }
2659            _ => panic!("Expected parts content"),
2660        }
2661    }
2662
2663    #[test]
2664    fn test_openrouter_plugin_config_is_empty() {
2665        assert!(OpenRouterPluginConfig::default().is_empty());
2666        assert!(
2667            !OpenRouterPluginConfig {
2668                web: Some(OpenRouterWebSearchPlugin::default()),
2669                file: None,
2670            }
2671            .is_empty()
2672        );
2673        assert!(
2674            !OpenRouterPluginConfig {
2675                web: None,
2676                file: Some(OpenRouterFilePlugin {}),
2677            }
2678            .is_empty()
2679        );
2680    }
2681
2682    #[test]
2683    fn test_openrouter_routing_is_empty_with_plugins() {
2684        let with_plugins = OpenRouterRoutingConfig {
2685            plugins: Some(OpenRouterPluginConfig {
2686                web: Some(OpenRouterWebSearchPlugin::default()),
2687                file: None,
2688            }),
2689            ..Default::default()
2690        };
2691        assert!(!with_plugins.is_empty());
2692
2693        let empty_plugins = OpenRouterRoutingConfig {
2694            plugins: Some(OpenRouterPluginConfig::default()),
2695            ..Default::default()
2696        };
2697        assert!(empty_plugins.is_empty());
2698    }
2699
2700    #[test]
2701    fn test_openrouter_web_search_plugin_serialization() {
2702        let plugin = OpenRouterWebSearchPlugin {
2703            max_results: Some(10),
2704            search_prompt: Some("search for Rust crates".to_string()),
2705        };
2706        let json = serde_json::to_value(&plugin).unwrap();
2707        assert_eq!(json["max_results"], 10);
2708        assert_eq!(json["search_prompt"], "search for Rust crates");
2709    }
2710
2711    #[test]
2712    fn test_openrouter_web_search_plugin_omits_none_fields() {
2713        let plugin = OpenRouterWebSearchPlugin::default();
2714        let json = serde_json::to_value(&plugin).unwrap();
2715        assert!(json.get("max_results").is_none());
2716        assert!(json.get("search_prompt").is_none());
2717    }
2718
2719    #[test]
2720    fn test_capacity_strategy_shared_capacity_is_noop() {
2721        let base = OpenRouterRoutingConfig {
2722            models: vec!["openai/gpt-5-mini".to_string()],
2723            capacity_strategy: Some(OpenRouterCapacityStrategy::SharedCapacity),
2724            ..Default::default()
2725        };
2726        let result = base.apply_capacity_strategy().unwrap();
2727        assert_eq!(
2728            result.capacity_strategy,
2729            Some(OpenRouterCapacityStrategy::SharedCapacity)
2730        );
2731        assert!(result.provider.is_none());
2732    }
2733
2734    #[test]
2735    fn test_capacity_strategy_none_is_noop() {
2736        let base = OpenRouterRoutingConfig {
2737            models: vec!["openai/gpt-5-mini".to_string()],
2738            capacity_strategy: None,
2739            ..Default::default()
2740        };
2741        let result = base.apply_capacity_strategy().unwrap();
2742        assert!(result.provider.is_none());
2743    }
2744
2745    #[test]
2746    fn test_capacity_strategy_byok_first_sets_allow_fallbacks() {
2747        let base = OpenRouterRoutingConfig {
2748            models: vec!["openai/gpt-5-mini".to_string()],
2749            capacity_strategy: Some(OpenRouterCapacityStrategy::ByokFirst),
2750            ..Default::default()
2751        };
2752        let result = base.apply_capacity_strategy().unwrap();
2753        let provider = result.provider.as_ref().expect("provider set by ByokFirst");
2754        assert_eq!(provider.allow_fallbacks, Some(true));
2755    }
2756
2757    #[test]
2758    fn test_capacity_strategy_byok_first_preserves_explicit_allow_fallbacks() {
2759        // If allow_fallbacks was already set explicitly, ByokFirst must not override it.
2760        let base = OpenRouterRoutingConfig {
2761            models: vec!["openai/gpt-5-mini".to_string()],
2762            capacity_strategy: Some(OpenRouterCapacityStrategy::ByokFirst),
2763            provider: Some(OpenRouterProviderRouting {
2764                allow_fallbacks: Some(false),
2765                ..Default::default()
2766            }),
2767            ..Default::default()
2768        };
2769        let result = base.apply_capacity_strategy().unwrap();
2770        let provider = result.provider.as_ref().unwrap();
2771        assert_eq!(provider.allow_fallbacks, Some(false));
2772    }
2773
2774    #[test]
2775    fn test_capacity_strategy_byok_only_requires_provider_only() {
2776        let base = OpenRouterRoutingConfig {
2777            models: vec!["openai/gpt-5-mini".to_string()],
2778            capacity_strategy: Some(OpenRouterCapacityStrategy::ByokOnly),
2779            ..Default::default()
2780        };
2781        let err = base.apply_capacity_strategy().unwrap_err();
2782        assert!(
2783            err.contains("provider.only"),
2784            "error should mention provider.only: {err}"
2785        );
2786    }
2787
2788    #[test]
2789    fn test_capacity_strategy_byok_only_disables_fallbacks() {
2790        let base = OpenRouterRoutingConfig {
2791            models: vec!["openai/gpt-5-mini".to_string()],
2792            capacity_strategy: Some(OpenRouterCapacityStrategy::ByokOnly),
2793            provider: Some(OpenRouterProviderRouting {
2794                only: vec!["my-byok-provider".to_string()],
2795                ..Default::default()
2796            }),
2797            ..Default::default()
2798        };
2799        let result = base.apply_capacity_strategy().unwrap();
2800        let provider = result.provider.as_ref().unwrap();
2801        assert_eq!(provider.allow_fallbacks, Some(false));
2802        assert_eq!(provider.only, vec!["my-byok-provider"]);
2803    }
2804
2805    #[test]
2806    fn test_capacity_strategy_byok_only_not_empty_in_is_empty() {
2807        let with_strategy = OpenRouterRoutingConfig {
2808            capacity_strategy: Some(OpenRouterCapacityStrategy::ByokOnly),
2809            ..Default::default()
2810        };
2811        assert!(!with_strategy.is_empty());
2812
2813        let byok_first = OpenRouterRoutingConfig {
2814            capacity_strategy: Some(OpenRouterCapacityStrategy::ByokFirst),
2815            ..Default::default()
2816        };
2817        assert!(!byok_first.is_empty());
2818
2819        let shared = OpenRouterRoutingConfig {
2820            capacity_strategy: Some(OpenRouterCapacityStrategy::SharedCapacity),
2821            ..Default::default()
2822        };
2823        assert!(shared.is_empty());
2824    }
2825
2826    // -------------------------------------------------------------------------
2827
2828    // OpenRouterRoutingPreset tests
2829
2830    // -------------------------------------------------------------------------
2831
2832    #[test]
2833    fn test_preset_no_presets_is_noop() {
2834        let base = OpenRouterRoutingConfig {
2835            models: vec!["openai/gpt-5-mini".to_string()],
2836            ..Default::default()
2837        };
2838        let result = base.apply_presets().unwrap();
2839        assert_eq!(result, base);
2840    }
2841
2842    #[test]
2843    fn test_preset_cheapest_with_tools_sets_require_parameters_and_sort_price() {
2844        let base = OpenRouterRoutingConfig {
2845            presets: vec![OpenRouterRoutingPreset::CheapestWithTools],
2846            ..Default::default()
2847        };
2848        let result = base.apply_presets().unwrap();
2849        assert!(result.presets.is_empty(), "presets cleared after apply");
2850        let provider = result.provider.expect("provider set by preset");
2851        assert_eq!(provider.require_parameters, Some(true));
2852        assert_eq!(
2853            provider.sort,
2854            Some(OpenRouterProviderSort::Simple(
2855                OpenRouterProviderSortBy::Price
2856            ))
2857        );
2858    }
2859
2860    #[test]
2861    fn test_preset_lowest_latency_review_sets_sort_throughput() {
2862        let base = OpenRouterRoutingConfig {
2863            presets: vec![OpenRouterRoutingPreset::LowestLatencyReview],
2864            ..Default::default()
2865        };
2866        let result = base.apply_presets().unwrap();
2867        let provider = result.provider.expect("provider set by preset");
2868        assert_eq!(
2869            provider.sort,
2870            Some(OpenRouterProviderSort::Simple(
2871                OpenRouterProviderSortBy::Throughput
2872            ))
2873        );
2874    }
2875
2876    #[test]
2877    fn test_preset_zdr_only_sets_zdr() {
2878        let base = OpenRouterRoutingConfig {
2879            presets: vec![OpenRouterRoutingPreset::ZdrOnly],
2880            ..Default::default()
2881        };
2882        let result = base.apply_presets().unwrap();
2883        let provider = result.provider.expect("provider set");
2884        assert_eq!(provider.zdr, Some(true));
2885    }
2886
2887    #[test]
2888    fn test_preset_byok_first_sets_allow_fallbacks() {
2889        let base = OpenRouterRoutingConfig {
2890            presets: vec![OpenRouterRoutingPreset::ByokFirst],
2891            ..Default::default()
2892        };
2893        let result = base.apply_presets().unwrap();
2894        let provider = result.provider.expect("provider set");
2895        assert_eq!(provider.allow_fallbacks, Some(true));
2896    }
2897
2898    #[test]
2899    fn test_preset_no_data_collection_sets_data_collection_deny() {
2900        let base = OpenRouterRoutingConfig {
2901            presets: vec![OpenRouterRoutingPreset::NoDataCollection],
2902            ..Default::default()
2903        };
2904        let result = base.apply_presets().unwrap();
2905        let provider = result.provider.expect("provider set");
2906        assert_eq!(
2907            provider.data_collection,
2908            Some(OpenRouterDataCollection::Deny)
2909        );
2910    }
2911
2912    #[test]
2913    fn test_preset_strict_json_sets_require_parameters() {
2914        let base = OpenRouterRoutingConfig {
2915            presets: vec![OpenRouterRoutingPreset::StrictJson],
2916            ..Default::default()
2917        };
2918        let result = base.apply_presets().unwrap();
2919        let provider = result.provider.expect("provider set");
2920        assert_eq!(provider.require_parameters, Some(true));
2921    }
2922
2923    #[test]
2924    fn test_preset_reasoning_required_sets_require_parameters() {
2925        let base = OpenRouterRoutingConfig {
2926            presets: vec![OpenRouterRoutingPreset::ReasoningRequired],
2927            ..Default::default()
2928        };
2929        let result = base.apply_presets().unwrap();
2930        let provider = result.provider.expect("provider set");
2931        assert_eq!(provider.require_parameters, Some(true));
2932    }
2933
2934    #[test]
2935    fn test_preset_max_price_converts_usd_per_million() {
2936        let base = OpenRouterRoutingConfig {
2937            presets: vec![OpenRouterRoutingPreset::MaxPrice {
2938                prompt_usd_per_million: Some(5.0),
2939                completion_usd_per_million: Some(15.0),
2940            }],
2941            ..Default::default()
2942        };
2943        let result = base.apply_presets().unwrap();
2944        let provider = result.provider.expect("provider set");
2945        let max_price = provider.max_price.expect("max_price set");
2946        // 5.0 USD/M → 5.0 / 1_000_000 per token
2947        let prompt = max_price.prompt.expect("prompt set");
2948        assert!((prompt - 5.0 / 1_000_000.0).abs() < f64::EPSILON);
2949        let completion = max_price.completion.expect("completion set");
2950        assert!((completion - 15.0 / 1_000_000.0).abs() < f64::EPSILON);
2951    }
2952
2953    #[test]
2954    fn test_preset_max_price_rejects_negative_values() {
2955        let base = OpenRouterRoutingConfig {
2956            presets: vec![OpenRouterRoutingPreset::MaxPrice {
2957                prompt_usd_per_million: Some(-1.0),
2958                completion_usd_per_million: None,
2959            }],
2960            ..Default::default()
2961        };
2962        let err = base.apply_presets().unwrap_err();
2963        assert!(
2964            err.contains("non-negative"),
2965            "error should mention non-negative: {err}"
2966        );
2967    }
2968
2969    #[test]
2970    fn test_preset_max_price_both_none_no_provider_field() {
2971        let base = OpenRouterRoutingConfig {
2972            presets: vec![OpenRouterRoutingPreset::MaxPrice {
2973                prompt_usd_per_million: None,
2974                completion_usd_per_million: None,
2975            }],
2976            ..Default::default()
2977        };
2978        let result = base.apply_presets().unwrap();
2979        assert!(
2980            result.provider.is_none(),
2981            "MaxPrice with no dimensions should not produce a provider field"
2982        );
2983    }
2984
2985    #[test]
2986    fn test_preset_explicit_provider_overrides_preset() {
2987        let base = OpenRouterRoutingConfig {
2988            presets: vec![OpenRouterRoutingPreset::CheapestWithTools],
2989            provider: Some(OpenRouterProviderRouting {
2990                // Caller explicitly wants throughput sort, overriding Price preset
2991                sort: Some(OpenRouterProviderSort::Simple(
2992                    OpenRouterProviderSortBy::Throughput,
2993                )),
2994                ..Default::default()
2995            }),
2996            ..Default::default()
2997        };
2998        let result = base.apply_presets().unwrap();
2999        let provider = result.provider.expect("provider set");
3000        // Explicit sort wins
3001        assert_eq!(
3002            provider.sort,
3003            Some(OpenRouterProviderSort::Simple(
3004                OpenRouterProviderSortBy::Throughput
3005            ))
3006        );
3007        // But preset-derived require_parameters still set (not overridden by explicit)
3008        assert_eq!(provider.require_parameters, Some(true));
3009    }
3010
3011    #[test]
3012    fn test_preset_multiple_presets_combined() {
3013        let base = OpenRouterRoutingConfig {
3014            presets: vec![
3015                OpenRouterRoutingPreset::ZdrOnly,
3016                OpenRouterRoutingPreset::NoDataCollection,
3017                OpenRouterRoutingPreset::LowestLatencyReview,
3018            ],
3019            ..Default::default()
3020        };
3021        let result = base.apply_presets().unwrap();
3022        let provider = result.provider.expect("provider set");
3023        assert_eq!(provider.zdr, Some(true));
3024        assert_eq!(
3025            provider.data_collection,
3026            Some(OpenRouterDataCollection::Deny)
3027        );
3028        assert_eq!(
3029            provider.sort,
3030            Some(OpenRouterProviderSort::Simple(
3031                OpenRouterProviderSortBy::Throughput
3032            ))
3033        );
3034    }
3035
3036    #[test]
3037    fn test_preset_later_preset_overrides_sort() {
3038        let base = OpenRouterRoutingConfig {
3039            presets: vec![
3040                OpenRouterRoutingPreset::CheapestWithTools, // sets Price sort
3041                OpenRouterRoutingPreset::LowestLatencyReview, // overrides to Throughput
3042            ],
3043            ..Default::default()
3044        };
3045        let result = base.apply_presets().unwrap();
3046        let provider = result.provider.expect("provider set");
3047        // Later preset wins for sort
3048        assert_eq!(
3049            provider.sort,
3050            Some(OpenRouterProviderSort::Simple(
3051                OpenRouterProviderSortBy::Throughput
3052            ))
3053        );
3054        // require_parameters still set by CheapestWithTools
3055        assert_eq!(provider.require_parameters, Some(true));
3056    }
3057
3058    #[test]
3059    fn test_preset_non_empty_in_is_empty() {
3060        let with_preset = OpenRouterRoutingConfig {
3061            presets: vec![OpenRouterRoutingPreset::ZdrOnly],
3062            ..Default::default()
3063        };
3064        assert!(!with_preset.is_empty());
3065
3066        let without = OpenRouterRoutingConfig::default();
3067        assert!(without.is_empty());
3068    }
3069}