use crate::compact::{CompactRequest, CompactResponse};
use crate::credential_schema::CredentialFormSchema;
use crate::error::{AgentLoopError, LlmErrorKind, Result};
use crate::tool_types::{ToolCall, ToolDefinition};
use async_trait::async_trait;
use futures::Stream;
use std::collections::HashMap;
use std::pin::Pin;
use std::sync::Arc;
pub type LlmResponseStream = Pin<Box<dyn Stream<Item = Result<LlmStreamEvent>> + Send>>;
pub use crate::provider_managed::{ProviderCheckpointCandidate, ProviderOpaqueContext};
pub use crate::stream_error::LlmStreamError;
pub use crate::stream_event::{
HostedToolCall, HostedToolCallStatus, LlmCompletionMetadata, LlmStreamEvent,
disjoint_prompt_tokens,
};
pub use crate::model_discovery::DiscoveredModel;
#[async_trait]
pub trait ChatDriver: Send + Sync {
fn native_async_driver(
&self,
_model: &str,
_tools: std::collections::BTreeMap<String, Option<serde_json::Value>>,
_continuation: Option<crate::native_async::Delivery>,
) -> Option<Arc<dyn ChatDriver>> {
None
}
async fn chat_completion_stream(
&self,
endpoint: &crate::runtime_provider::ProviderEndpoint,
messages: Vec<Message>,
config: &LlmCallConfig,
) -> Result<LlmResponseStream>;
async fn chat_completion(
&self,
endpoint: &crate::runtime_provider::ProviderEndpoint,
messages: Vec<Message>,
config: &LlmCallConfig,
) -> Result<LlmResponse> {
let limits = config.limits;
let (stream, spent) = crate::turn_collector::connect_within(
&limits,
self.chat_completion_stream(endpoint, messages, config),
)
.await?;
Ok(
crate::turn_collector::collect_turn(stream, &limits.after(spent), |_| {})
.await?
.into_response(),
)
}
fn supports_native_non_streaming(&self) -> bool {
false
}
async fn chat_completion_non_streaming(
&self,
endpoint: &crate::runtime_provider::ProviderEndpoint,
messages: Vec<Message>,
config: &LlmCallConfig,
) -> Result<LlmResponse> {
self.chat_completion(endpoint, messages, config).await
}
async fn list_models(
&self,
_endpoint: &crate::runtime_provider::ProviderEndpoint,
) -> Result<Option<Vec<DiscoveredModel>>> {
Ok(None)
}
fn supports_compact(&self) -> bool {
false
}
fn supports_stateful_responses(&self) -> bool {
false
}
fn effective_context_window(&self, _model: &str) -> Option<usize> {
None
}
fn supports_parallel_tool_calls(&self, _model: &str) -> bool {
false
}
fn supports_response_format(&self, _model: &str) -> bool {
false
}
fn provider_managed_reduction_option(
&self,
_endpoint: &crate::runtime_provider::ProviderEndpoint,
_model: &str,
_budget_tokens: usize,
) -> Option<(String, serde_json::Value)> {
None
}
fn provider_managed_reduction_fallback_reason(
&self,
_endpoint: &crate::runtime_provider::ProviderEndpoint,
_config: &LlmCallConfig,
) -> Option<&'static str> {
None
}
fn validate_provider_opaque_context(&self, _context: &ProviderOpaqueContext) -> bool {
true
}
async fn compact(
&self,
_endpoint: &crate::runtime_provider::ProviderEndpoint,
_request: CompactRequest,
) -> Result<Option<CompactResponse>> {
Ok(None)
}
}
#[async_trait]
impl ChatDriver for Box<dyn ChatDriver> {
fn native_async_driver(
&self,
model: &str,
tools: std::collections::BTreeMap<String, Option<serde_json::Value>>,
continuation: Option<crate::native_async::Delivery>,
) -> Option<Arc<dyn ChatDriver>> {
(**self).native_async_driver(model, tools, continuation)
}
async fn chat_completion_stream(
&self,
endpoint: &crate::runtime_provider::ProviderEndpoint,
messages: Vec<Message>,
config: &LlmCallConfig,
) -> Result<LlmResponseStream> {
(**self)
.chat_completion_stream(endpoint, messages, config)
.await
}
async fn chat_completion(
&self,
endpoint: &crate::runtime_provider::ProviderEndpoint,
messages: Vec<Message>,
config: &LlmCallConfig,
) -> Result<LlmResponse> {
(**self).chat_completion(endpoint, messages, config).await
}
fn supports_native_non_streaming(&self) -> bool {
(**self).supports_native_non_streaming()
}
async fn chat_completion_non_streaming(
&self,
endpoint: &crate::runtime_provider::ProviderEndpoint,
messages: Vec<Message>,
config: &LlmCallConfig,
) -> Result<LlmResponse> {
(**self)
.chat_completion_non_streaming(endpoint, messages, config)
.await
}
async fn list_models(
&self,
endpoint: &crate::runtime_provider::ProviderEndpoint,
) -> Result<Option<Vec<DiscoveredModel>>> {
(**self).list_models(endpoint).await
}
fn supports_compact(&self) -> bool {
(**self).supports_compact()
}
fn supports_stateful_responses(&self) -> bool {
(**self).supports_stateful_responses()
}
fn effective_context_window(&self, model: &str) -> Option<usize> {
(**self).effective_context_window(model)
}
fn supports_parallel_tool_calls(&self, model: &str) -> bool {
(**self).supports_parallel_tool_calls(model)
}
fn supports_response_format(&self, model: &str) -> bool {
(**self).supports_response_format(model)
}
fn provider_managed_reduction_option(
&self,
endpoint: &crate::runtime_provider::ProviderEndpoint,
model: &str,
budget_tokens: usize,
) -> Option<(String, serde_json::Value)> {
(**self).provider_managed_reduction_option(endpoint, model, budget_tokens)
}
fn provider_managed_reduction_fallback_reason(
&self,
endpoint: &crate::runtime_provider::ProviderEndpoint,
config: &LlmCallConfig,
) -> Option<&'static str> {
(**self).provider_managed_reduction_fallback_reason(endpoint, config)
}
fn validate_provider_opaque_context(&self, context: &ProviderOpaqueContext) -> bool {
(**self).validate_provider_opaque_context(context)
}
async fn compact(
&self,
endpoint: &crate::runtime_provider::ProviderEndpoint,
request: CompactRequest,
) -> Result<Option<CompactResponse>> {
(**self).compact(endpoint, request).await
}
}
pub use crate::message::{
LlmContentPart, Message, MessageContent, MessageRole, fold_system_messages,
};
mod configuration;
pub use configuration::*;
#[derive(Debug, Clone)]
pub struct LlmResponse {
pub text: String,
pub reasoning: Vec<crate::reasoning::ReasoningContentPart>,
pub tool_calls: Option<Vec<ToolCall>>,
pub metadata: LlmCompletionMetadata,
}
pub use crate::llm_call_config_builder::LlmCallConfigBuilder;
pub use crate::provider::DriverId;
#[derive(Clone, Default, PartialEq, Eq)]
pub struct ProviderMetadata {
pub refresh_token: Option<String>,
pub account_id: Option<String>,
pub extra: Option<serde_json::Value>,
}
impl std::fmt::Debug for ProviderMetadata {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ProviderMetadata")
.field(
"refresh_token",
&self.refresh_token.as_ref().map(|_| "<configured>"),
)
.field("account_id", &self.account_id)
.field("extra", &self.extra.as_ref().map(|_| "<configured>"))
.finish()
}
}
#[derive(Clone)]
#[non_exhaustive]
pub struct ProviderConfig {
pub provider: crate::runtime_provider::ProviderKey,
pub provider_type: DriverId,
pub api_key: Option<String>,
pub base_url: Option<String>,
pub metadata: ProviderMetadata,
pub request_options: crate::provider::ProviderRequestOptions,
}
impl ProviderConfig {
pub fn new(provider_type: DriverId) -> Self {
let provider = crate::runtime_provider::ProviderKey::new(provider_type.as_str());
Self {
provider,
provider_type,
api_key: None,
base_url: None,
metadata: ProviderMetadata::default(),
request_options: Default::default(),
}
}
pub fn for_provider(
provider: impl Into<crate::runtime_provider::ProviderKey>,
provider_type: DriverId,
) -> Self {
Self {
provider: provider.into(),
provider_type,
api_key: None,
base_url: None,
metadata: ProviderMetadata::default(),
request_options: Default::default(),
}
}
pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
self.api_key = Some(api_key.into());
self
}
pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {
self.base_url = Some(base_url.into());
self
}
pub fn with_metadata(mut self, metadata: ProviderMetadata) -> Self {
self.metadata = metadata;
self
}
pub fn with_request_options(
mut self,
request_options: crate::provider::ProviderRequestOptions,
) -> Self {
self.request_options = request_options;
self
}
}
impl std::fmt::Debug for ProviderConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ProviderConfig")
.field("provider", &self.provider)
.field("provider_type", &self.provider_type)
.field("auth", &self.api_key.as_ref().map(|_| "<configured>"))
.field("base_url", &self.base_url.as_ref().map(|_| "<configured>"))
.field(
"metadata",
&self.metadata.extra.as_ref().map(|_| "<configured>"),
)
.finish()
}
}
#[derive(Clone)]
pub struct DriverConfig {
pub provider: crate::runtime_provider::ProviderKey,
pub provider_type: DriverId,
pub api_key: Option<String>,
pub credentials: std::collections::BTreeMap<String, String>,
pub base_url: Option<String>,
pub metadata: ProviderMetadata,
}
impl DriverConfig {
pub fn from_provider_config(config: &ProviderConfig) -> Self {
Self {
provider: config.provider.clone(),
provider_type: config.provider_type.clone(),
credentials: crate::credential_schema::parse_credential_document(
config.api_key.as_deref(),
),
api_key: config.api_key.clone(),
base_url: config.base_url.clone(),
metadata: config.metadata.clone(),
}
}
pub fn credential(&self, name: &str) -> Option<&str> {
self.credentials
.get(name)
.map(String::as_str)
.filter(|s| !s.is_empty())
}
}
impl std::fmt::Debug for DriverConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DriverConfig")
.field("provider", &self.provider)
.field("provider_type", &self.provider_type)
.field("auth", &self.api_key.as_ref().map(|_| "<configured>"))
.field(
"credential_fields",
&self.credentials.keys().collect::<Vec<_>>(),
)
.field("base_url", &self.base_url.as_ref().map(|_| "<configured>"))
.finish()
}
}
pub type BoxedChatDriver = Box<dyn ChatDriver>;
#[derive(Debug, Clone)]
pub struct EmbedRequest {
pub texts: Vec<String>,
pub model: String,
}
#[derive(Debug, Clone)]
pub struct EmbedResponse {
pub embeddings: Vec<Vec<f32>>,
pub usage_tokens: Option<u32>,
pub actual_cost_usd: Option<f64>,
}
#[derive(Debug, thiserror::Error)]
pub enum EmbeddingsDriverError {
#[error("embeddings provider returned an error: {0}")]
Provider(String),
#[error("embeddings request failed: {0}")]
Transport(String),
}
#[async_trait]
pub trait EmbeddingsDriver: Send + Sync {
async fn embed(
&self,
endpoint: &crate::runtime_provider::ProviderEndpoint,
request: EmbedRequest,
) -> std::result::Result<EmbedResponse, EmbeddingsDriverError>;
}
#[async_trait]
impl EmbeddingsDriver for Box<dyn EmbeddingsDriver> {
async fn embed(
&self,
endpoint: &crate::runtime_provider::ProviderEndpoint,
request: EmbedRequest,
) -> std::result::Result<EmbedResponse, EmbeddingsDriverError> {
(**self).embed(endpoint, request).await
}
}
pub type BoxedEmbeddingsDriver = Box<dyn EmbeddingsDriver>;
pub type EmbeddingsDriverFactory =
Arc<dyn Fn(&DriverConfig) -> BoxedEmbeddingsDriver + Send + Sync>;
pub type DriverFactory = Arc<dyn Fn(&DriverConfig) -> BoxedChatDriver + Send + Sync>;
struct CredentialGateDriver {
inner: BoxedChatDriver,
message: String,
}
impl CredentialGateDriver {
fn error(&self) -> AgentLoopError {
AgentLoopError::llm_kind(LlmErrorKind::Authentication, self.message.clone())
}
}
#[async_trait]
impl ChatDriver for CredentialGateDriver {
async fn chat_completion_stream(
&self,
_endpoint: &crate::runtime_provider::ProviderEndpoint,
_messages: Vec<Message>,
_config: &LlmCallConfig,
) -> Result<LlmResponseStream> {
Err(self.error())
}
async fn list_models(
&self,
_endpoint: &crate::runtime_provider::ProviderEndpoint,
) -> Result<Option<Vec<DiscoveredModel>>> {
Err(self.error())
}
fn supports_compact(&self) -> bool {
self.inner.supports_compact()
}
fn supports_stateful_responses(&self) -> bool {
self.inner.supports_stateful_responses()
}
fn effective_context_window(&self, model: &str) -> Option<usize> {
self.inner.effective_context_window(model)
}
fn supports_parallel_tool_calls(&self, model: &str) -> bool {
self.inner.supports_parallel_tool_calls(model)
}
fn supports_response_format(&self, model: &str) -> bool {
self.inner.supports_response_format(model)
}
async fn compact(
&self,
_endpoint: &crate::runtime_provider::ProviderEndpoint,
_request: CompactRequest,
) -> Result<Option<CompactResponse>> {
Err(self.error())
}
}
struct RequestOptionsDriver {
inner: Arc<dyn ChatDriver>,
options: crate::provider::ProviderRequestOptions,
}
impl RequestOptionsDriver {
fn wrap(
driver: BoxedChatDriver,
options: &crate::provider::ProviderRequestOptions,
) -> BoxedChatDriver {
if options.is_empty() {
return driver;
}
Box::new(Self {
inner: Arc::from(driver),
options: options.clone(),
})
}
fn apply(&self, config: &LlmCallConfig) -> LlmCallConfig {
let mut config = config.clone();
config.extra_headers.extend(self.options.header_pairs());
if self.options.cache_diagnostics {
config.cache_diagnostics = Some(CacheDiagnosticsConfig {
enabled: true,
previous_message_id: config.previous_response_id.clone(),
});
}
config
}
}
#[async_trait]
impl ChatDriver for RequestOptionsDriver {
fn native_async_driver(
&self,
model: &str,
tools: std::collections::BTreeMap<String, Option<serde_json::Value>>,
continuation: Option<crate::native_async::Delivery>,
) -> Option<Arc<dyn ChatDriver>> {
Some(Arc::new(Self {
inner: self.inner.native_async_driver(model, tools, continuation)?,
options: self.options.clone(),
}))
}
async fn chat_completion_stream(
&self,
endpoint: &crate::runtime_provider::ProviderEndpoint,
messages: Vec<Message>,
config: &LlmCallConfig,
) -> Result<LlmResponseStream> {
self.inner
.chat_completion_stream(endpoint, messages, &self.apply(config))
.await
}
async fn chat_completion(
&self,
endpoint: &crate::runtime_provider::ProviderEndpoint,
messages: Vec<Message>,
config: &LlmCallConfig,
) -> Result<LlmResponse> {
self.inner
.chat_completion(endpoint, messages, &self.apply(config))
.await
}
fn supports_native_non_streaming(&self) -> bool {
self.inner.supports_native_non_streaming()
}
async fn chat_completion_non_streaming(
&self,
endpoint: &crate::runtime_provider::ProviderEndpoint,
messages: Vec<Message>,
config: &LlmCallConfig,
) -> Result<LlmResponse> {
self.inner
.chat_completion_non_streaming(endpoint, messages, &self.apply(config))
.await
}
async fn list_models(
&self,
endpoint: &crate::runtime_provider::ProviderEndpoint,
) -> Result<Option<Vec<DiscoveredModel>>> {
self.inner.list_models(endpoint).await
}
fn supports_compact(&self) -> bool {
self.inner.supports_compact()
}
fn supports_stateful_responses(&self) -> bool {
self.inner.supports_stateful_responses()
}
fn effective_context_window(&self, model: &str) -> Option<usize> {
self.inner.effective_context_window(model)
}
fn supports_parallel_tool_calls(&self, model: &str) -> bool {
self.inner.supports_parallel_tool_calls(model)
}
fn supports_response_format(&self, model: &str) -> bool {
self.inner.supports_response_format(model)
}
async fn compact(
&self,
endpoint: &crate::runtime_provider::ProviderEndpoint,
request: CompactRequest,
) -> Result<Option<CompactResponse>> {
self.inner.compact(endpoint, request).await
}
}
pub use crate::model_profile_data::ServiceKind;
pub use crate::driver_oauth::DriverOAuthFlow;
#[derive(Debug, Clone)]
pub struct DriverOAuthConfig {
pub authorize_url: String,
pub token_url: String,
pub flow: DriverOAuthFlow,
}
impl DriverOAuthConfig {
pub fn openrouter() -> Self {
Self {
authorize_url: "https://openrouter.ai/auth".to_string(),
token_url: "https://openrouter.ai/api/v1/auth/keys".to_string(),
flow: DriverOAuthFlow::OpenRouterPkce,
}
}
}
pub type ProviderFactory = Arc<dyn Fn(&DriverConfig) -> crate::Provider + Send + Sync>;
#[derive(Clone)]
pub struct DriverDescriptor {
pub id: DriverId,
pub display_name: String,
pub services: Vec<ServiceKind>,
pub credential_schema: CredentialFormSchema,
pub base_url_env: Option<String>,
pub oauth: Option<DriverOAuthConfig>,
pub chat: Option<DriverFactory>,
pub embeddings: Option<EmbeddingsDriverFactory>,
pub provider: Option<ProviderFactory>,
}
impl DriverDescriptor {
pub fn chat_only<F>(id: impl Into<DriverId>, factory: F) -> Self
where
F: Fn(&DriverConfig) -> BoxedChatDriver + Send + Sync + 'static,
{
let id = id.into();
Self {
display_name: default_display_name(&id),
credential_schema: default_credential_schema(&id),
base_url_env: None,
services: vec![ServiceKind::Chat],
oauth: None,
chat: Some(Arc::new(factory)),
embeddings: None,
provider: None,
id,
}
}
pub fn with_base_url_env(mut self, base_url_env: impl Into<String>) -> Self {
self.base_url_env = Some(base_url_env.into());
self
}
pub fn supports(&self, service: ServiceKind) -> bool {
self.services.contains(&service)
}
pub fn declared_env_vars(&self) -> Vec<String> {
self.credential_schema
.fields
.iter()
.flat_map(|field| field.env.iter().cloned())
.chain(self.base_url_env.clone())
.collect()
}
pub fn base_url_from_env<F>(&self, lookup: F) -> Option<String>
where
F: Fn(&str) -> Option<String>,
{
self.base_url_env
.as_deref()
.and_then(lookup)
.filter(|value| !value.trim().is_empty())
}
}
impl std::fmt::Debug for DriverDescriptor {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DriverDescriptor")
.field("id", &self.id)
.field("display_name", &self.display_name)
.field("services", &self.services)
.field("oauth", &self.oauth.is_some())
.field("chat", &self.chat.is_some())
.field("embeddings", &self.embeddings.is_some())
.finish()
}
}
fn default_display_name(id: &DriverId) -> String {
id.as_str().replace(['_', '-'], " ")
}
fn default_credential_schema(id: &DriverId) -> CredentialFormSchema {
if id == &DriverId::LlmSim {
CredentialFormSchema::empty()
} else {
CredentialFormSchema {
fields: vec![
crate::credential_schema::FormField::password("api_key", "API Key").required(),
],
instructions_markdown: String::new(),
}
}
}
#[derive(Clone, Default)]
pub struct DriverRegistry {
descriptors: HashMap<DriverId, DriverDescriptor>,
providers: crate::runtime_provider::RuntimeProviderRegistry,
}
impl DriverRegistry {
pub fn new() -> Self {
Self {
descriptors: HashMap::new(),
providers: crate::runtime_provider::RuntimeProviderRegistry::new(),
}
}
pub fn register_provider(
&mut self,
provider: crate::runtime_provider::RuntimeProvider,
) -> Result<()> {
self.providers.register(provider)
}
pub fn replace_provider(
&mut self,
provider: crate::runtime_provider::RuntimeProvider,
) -> Option<Arc<crate::runtime_provider::RuntimeProvider>> {
self.providers.replace(provider)
}
pub fn provider(
&self,
id: &crate::runtime_provider::ProviderKey,
) -> Option<Arc<crate::runtime_provider::RuntimeProvider>> {
self.providers.get(id)
}
pub fn register_descriptor(&mut self, descriptor: DriverDescriptor) {
if self.descriptors.contains_key(&descriptor.id) {
panic!(
"driver already registered for provider '{}'; \
use register_descriptor_or_replace to overwrite intentionally",
descriptor.id
);
}
self.descriptors.insert(descriptor.id.clone(), descriptor);
}
pub fn register_descriptor_or_replace(&mut self, descriptor: DriverDescriptor) {
self.descriptors.insert(descriptor.id.clone(), descriptor);
}
pub fn register<F>(&mut self, provider_type: impl Into<DriverId>, factory: F)
where
F: Fn(&DriverConfig) -> BoxedChatDriver + Send + Sync + 'static,
{
self.register_descriptor(DriverDescriptor::chat_only(provider_type, factory));
}
pub fn register_or_replace<F>(&mut self, provider_type: impl Into<DriverId>, factory: F)
where
F: Fn(&DriverConfig) -> BoxedChatDriver + Send + Sync + 'static,
{
self.register_descriptor_or_replace(DriverDescriptor::chat_only(provider_type, factory));
}
pub fn register_external<F>(&mut self, id: impl AsRef<str>, factory: F)
where
F: Fn(&DriverConfig) -> BoxedChatDriver + Send + Sync + 'static,
{
let mut descriptor = DriverDescriptor::chat_only(DriverId::external(id), factory);
descriptor.credential_schema = CredentialFormSchema::empty();
self.register_descriptor(descriptor);
}
pub fn create_chat_driver(&self, config: &ProviderConfig) -> Result<BoxedChatDriver> {
if let Some(provider) = self.providers.get(&config.provider) {
return Ok(RequestOptionsDriver::wrap(
(*provider).clone().into_boxed_driver(),
&config.request_options,
));
}
let descriptor = self.descriptors.get(&config.provider_type).ok_or_else(|| {
AgentLoopError::driver_not_registered(config.provider_type.to_string())
})?;
let factory = descriptor.chat.as_ref().ok_or_else(|| {
AgentLoopError::llm(format!(
"Provider driver '{}' does not implement the chat service.",
config.provider_type
))
})?;
let driver_config = DriverConfig::from_provider_config(config);
let driver = if let Some(provider) = &descriptor.provider {
provider(&driver_config).into_boxed_driver()
} else {
factory(&driver_config)
};
let mut credential_fields = driver_config.credentials.clone();
if let Some(serde_json::Value::Object(extra)) = &driver_config.metadata.extra {
for (name, value) in extra {
if let Some(value) = value.as_str() {
credential_fields
.entry(name.clone())
.or_insert_with(|| value.to_string());
}
}
}
let credential_errors = descriptor.credential_schema.validate(&credential_fields);
if credential_errors.is_empty() {
Ok(RequestOptionsDriver::wrap(driver, &config.request_options))
} else {
let message = if descriptor.credential_schema.fields.len() == 1
&& descriptor.credential_schema.fields[0].name == "api_key"
{
"API key is required. Configure the API key in provider settings.".to_string()
} else {
format!(
"Provider credentials are required. Configure provider settings: {}",
credential_errors.join(" ")
)
};
Ok(Box::new(CredentialGateDriver {
inner: driver,
message,
}))
}
}
pub fn has_driver(&self, provider_type: &DriverId) -> bool {
self.descriptors.contains_key(provider_type)
}
pub fn descriptor(&self, provider_type: &DriverId) -> Option<&DriverDescriptor> {
self.descriptors.get(provider_type)
}
pub fn supports(&self, provider_type: &DriverId, service: ServiceKind) -> bool {
self.descriptors
.get(provider_type)
.is_some_and(|d| d.supports(service))
}
pub fn providers_for(&self, service: ServiceKind) -> Vec<DriverId> {
self.descriptors
.values()
.filter(|d| d.supports(service))
.map(|d| d.id.clone())
.collect()
}
pub fn registered_providers(&self) -> Vec<DriverId> {
self.descriptors.keys().cloned().collect()
}
pub fn registered_provider_ids(&self) -> Vec<String> {
self.providers.ids()
}
pub fn create_provider(&self, config: &ProviderConfig) -> Result<crate::Provider> {
if let Some(provider) = self.providers.get(&config.provider) {
return Ok((*provider).clone());
}
let descriptor = self.descriptor(&config.provider_type).ok_or_else(|| {
AgentLoopError::driver_not_registered(config.provider_type.to_string())
})?;
let headers = config.request_options.headers.clone();
let config = DriverConfig::from_provider_config(config);
let errors = descriptor.credential_schema.validate(&config.credentials);
if !errors.is_empty() {
return Err(AgentLoopError::Configuration(
"Provider credentials are required. Configure provider settings.".into(),
));
}
if let Some(factory) = &descriptor.provider {
let mut provider = factory(&config);
for header in &headers {
provider = provider.header(&header.name, &header.value);
}
return Ok(provider);
}
let factory = descriptor
.chat
.as_ref()
.ok_or_else(|| AgentLoopError::Configuration("Provider has no model service".into()))?;
Ok(
crate::Provider::from_driver(config.provider.clone(), factory(&config).into())
.with_driver_id(config.provider_type.clone()),
)
}
pub fn create_embeddings_driver(
&self,
config: &ProviderConfig,
) -> std::result::Result<BoxedEmbeddingsDriver, EmbeddingsDriverError> {
if let Some(provider) = self.providers.get(&config.provider) {
return (*provider).clone().into_embeddings_driver();
}
if self
.descriptor(&config.provider_type)
.is_some_and(|d| d.provider.is_some())
{
return self
.create_provider(config)
.map_err(|e| EmbeddingsDriverError::Provider(e.to_string()))?
.into_embeddings_driver();
}
let requires_api_key = config.provider_type != DriverId::LlmSim;
if requires_api_key && config.api_key.is_none() {
return Err(EmbeddingsDriverError::Provider(
"API key is required. Configure the API key in provider settings.".to_string(),
));
}
let descriptor = self.descriptors.get(&config.provider_type).ok_or_else(|| {
EmbeddingsDriverError::Provider(format!(
"No driver registered for provider '{}'",
config.provider_type
))
})?;
let factory = descriptor.embeddings.as_ref().ok_or_else(|| {
EmbeddingsDriverError::Provider(format!(
"Provider driver '{}' does not implement the embeddings service.",
config.provider_type
))
})?;
let driver_config = DriverConfig::from_provider_config(config);
Ok(factory(&driver_config))
}
}
const MAX_TOOL_RESULT_BYTES: usize = 64 * 1024;
const TRUNCATION_SUFFIX: &str =
"\n\n[Output truncated — exceeded 64 KiB limit. Try quiet flags, pipes, or redirect to file.]";
pub fn truncate_tool_result(text: String) -> String {
if text.len() <= MAX_TOOL_RESULT_BYTES {
return text;
}
let content_budget = MAX_TOOL_RESULT_BYTES.saturating_sub(TRUNCATION_SUFFIX.len());
let mut end = content_budget;
while end > 0 && !text.is_char_boundary(end) {
end -= 1;
}
let mut truncated = text[..end].to_string();
truncated.push_str(TRUNCATION_SUFFIX);
truncated
}
#[cfg(test)]
#[path = "driver_registry_tests.rs"]
mod tests;