pub mod anthropic;
pub mod catalog_generated;
pub mod convert;
pub mod oauth;
pub mod openai;
pub mod preset;
use std::collections::HashMap;
use std::sync::Arc;
use async_trait::async_trait;
use infinity_provider_protocol::{
CompletionError, CompletionRequest, ErrorClass, ModelEntry, ModelProvider, ModelStream,
};
use preset::{Flavor, ResolvedBackend};
type BoxError = Box<dyn std::error::Error + Send + Sync>;
pub const USER_AGENT: &str = concat!("infinity-provider-models-dev/", env!("CARGO_PKG_VERSION"));
#[derive(Clone, Debug)]
struct Route {
entry: ModelEntry,
backend_idx: usize,
upstream_model: String,
default_max_tokens: Option<u64>,
order: usize,
}
pub struct MultiProvider {
client: reqwest::Client,
backends: Vec<ResolvedBackend>,
routes: HashMap<String, Route>,
}
impl MultiProvider {
#[must_use]
pub fn from_env() -> Self {
Self::from_backends(preset::resolve_backends())
}
#[must_use]
pub fn from_backends(backends: Vec<ResolvedBackend>) -> Self {
let client = reqwest::Client::new();
let mut routes = HashMap::new();
let mut order = 0_usize;
for (idx, backend) in backends.iter().enumerate() {
for m in &backend.models {
let model_id = format!("{}/{}", backend.preset_id, m.upstream);
if routes.contains_key(&model_id) {
tracing::warn!("duplicate model id {model_id}, keeping first");
continue;
}
let entry = ModelEntry {
model_id: model_id.clone(),
display_name: format!("{} ({})", m.display, backend.preset_id),
context_window: m.context_window,
max_output_tokens: m.max_output_tokens,
supports_image_input: m.supports_images,
};
order = order.saturating_add(1);
routes.insert(
model_id,
Route {
entry,
backend_idx: idx,
upstream_model: m.upstream.clone(),
default_max_tokens: m.max_output_tokens,
order,
},
);
}
}
for b in &backends {
tracing::info!(
"backend {}: {} model(s) at {}",
b.preset_id,
b.models.len(),
b.base_url,
);
}
if backends.is_empty() {
tracing::warn!(
"no backends configured: set one of the pi/opencode API-key env vars \
(e.g. OPENAI_API_KEY, ANTHROPIC_API_KEY, GROQ_API_KEY, OPENROUTER_API_KEY)"
);
}
Self {
client,
backends,
routes,
}
}
#[must_use]
pub fn backends(&self) -> &[ResolvedBackend] {
&self.backends
}
#[must_use]
pub fn into_arc(self) -> Arc<Self> {
Arc::new(self)
}
}
#[async_trait]
impl ModelProvider for MultiProvider {
async fn list_models(&self) -> Result<Vec<ModelEntry>, BoxError> {
let mut routes: Vec<&Route> = self.routes.values().collect();
routes.sort_by_key(|r| r.order);
let mut entries: Vec<ModelEntry> = routes.into_iter().map(|r| r.entry.clone()).collect();
let mut known: std::collections::HashSet<String> =
entries.iter().map(|e| e.model_id.clone()).collect();
let probed = futures_util::future::join_all(
self.backends.iter().filter(|b| b.discover).map(|b| async {
let ids =
crate::openai::discover_models(&self.client, &b.base_url, &b.api_key).await;
(b.preset_id, ids)
}),
)
.await;
for (preset_id, ids) in probed {
for id in ids {
let model_id = format!("{preset_id}/{id}");
if known.contains(&model_id) {
continue;
}
entries.push(ModelEntry {
model_id: model_id.clone(),
display_name: format!("{id} ({preset_id})"),
context_window: 200_000,
max_output_tokens: Some(32_000),
supports_image_input: false,
});
known.insert(model_id);
}
}
Ok(entries)
}
async fn invoke_model(
&self,
model_id: &str,
request: CompletionRequest,
) -> Result<ModelStream, CompletionError> {
let route = self.routes.get(model_id).cloned().or_else(|| {
model_id.split_once('/').and_then(|(prefix, rest)| {
self.backends.iter().enumerate().find_map(|(idx, b)| {
if b.preset_id == prefix && !rest.is_empty() {
Some(Route {
entry: ModelEntry {
model_id: model_id.to_owned(),
display_name: model_id.to_owned(),
context_window: 200_000,
max_output_tokens: Some(32_000),
supports_image_input: false,
},
backend_idx: idx,
upstream_model: rest.to_owned(),
default_max_tokens: Some(32_000),
order: usize::MAX,
})
} else {
None
}
})
})
});
let Some(route) = route else {
return Err(CompletionError::provider(
ErrorClass::Fatal,
format!("unknown model: {model_id}"),
));
};
let Some(backend) = self.backends.get(route.backend_idx) else {
return Err(CompletionError::provider(
ErrorClass::Fatal,
format!("unknown backend for model: {model_id}"),
));
};
match backend.flavor {
Flavor::OpenAi => {
let target = openai::Backend {
client: &self.client,
base_url: &backend.base_url,
api_key: &backend.api_key,
extra_headers: &backend.extra_headers,
supports_images: route.entry.supports_image_input,
};
openai::invoke(
&target,
&route.upstream_model,
route.default_max_tokens,
request,
)
.await
}
Flavor::Anthropic => {
let target = anthropic::Backend {
client: &self.client,
base_url: &backend.base_url,
api_key: &backend.api_key,
extra_headers: &backend.extra_headers,
oauth: backend.oauth.as_ref(),
};
anthropic::invoke(
&target,
&route.upstream_model,
route.default_max_tokens,
request,
route.entry.supports_image_input,
)
.await
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn backend(id: &'static str, models: Vec<(&str, &str)>) -> ResolvedBackend {
ResolvedBackend {
preset_id: id,
flavor: Flavor::OpenAi,
base_url: "https://example.test/v1".to_owned(),
api_key: "test".to_owned(),
models: models
.into_iter()
.map(|(m, d)| crate::preset::BackendModel {
upstream: m.to_owned(),
display: d.to_owned(),
context_window: 128_000,
max_output_tokens: Some(8_000),
supports_images: true,
})
.collect(),
oauth: None,
discover: false,
extra_headers: Vec::new(),
}
}
#[tokio::test]
async fn lists_namespaced_models_in_registration_order() {
let p = MultiProvider::from_backends(vec![
backend("openai", vec![("gpt-5.4", "GPT")]),
backend("groq", vec![("llama-3.3-70b-versatile", "Llama")]),
]);
let models = p.list_models().await.unwrap();
let ids: Vec<_> = models.iter().map(|m| m.model_id.as_str()).collect();
assert_eq!(ids, vec!["openai/gpt-5.4", "groq/llama-3.3-70b-versatile"]);
}
#[tokio::test]
async fn unknown_model_is_fatal() {
let p = MultiProvider::from_backends(vec![]);
let res = p
.invoke_model(
"nope/nothing",
CompletionRequest {
chat_history: vec![infinity_provider_protocol::Message::user("hi")],
..Default::default()
},
)
.await;
let Err(err) = res else {
panic!("unknown model must fail");
};
assert_eq!(err.class(), ErrorClass::Fatal);
}
#[tokio::test]
async fn unlisted_upstream_model_passes_through_on_known_backend() {
let p = MultiProvider::from_backends(vec![backend(
"groq",
vec![("llama-3.3-70b-versatile", "Llama")],
)]);
let res = p
.invoke_model(
"groq/some-future-model",
CompletionRequest {
chat_history: vec![infinity_provider_protocol::Message::user("hi")],
..Default::default()
},
)
.await;
let Err(err) = res else {
panic!("unreachable example endpoint must fail");
};
assert_ne!(err.class(), ErrorClass::Fatal);
}
}