pub mod audio;
#[cfg(target_os = "macos")]
pub mod audio_real;
mod auth;
mod bounded_resolve {
use std::num::NonZeroUsize;
use std::sync::Arc;
use tokio::sync::Semaphore;
use crate::registry::{ModelRegistry, Resolution};
const DEFAULT_MAX_CONCURRENT_RESOLUTIONS: usize = 4;
#[derive(Clone, Copy)]
pub(crate) enum ResolutionKind {
Chat,
Embedding,
}
pub(crate) enum ResolveError {
Saturated,
WorkerFailed,
}
#[derive(Clone)]
pub(crate) struct BoundedResolveExecutor {
slots: Arc<Semaphore>,
}
impl Default for BoundedResolveExecutor {
fn default() -> Self {
Self::new(
NonZeroUsize::new(DEFAULT_MAX_CONCURRENT_RESOLUTIONS)
.expect("default resolution limit is nonzero"),
)
}
}
impl BoundedResolveExecutor {
pub(crate) fn new(max_concurrent: NonZeroUsize) -> Self {
Self {
slots: Arc::new(Semaphore::new(max_concurrent.get())),
}
}
pub(crate) async fn resolve(
&self,
registry: Arc<dyn ModelRegistry>,
requested: Option<String>,
kind: ResolutionKind,
) -> Result<Resolution, ResolveError> {
let permit = Arc::clone(&self.slots)
.try_acquire_owned()
.map_err(|_| ResolveError::Saturated)?;
tokio::task::spawn_blocking(move || {
let _permit = permit;
match kind {
ResolutionKind::Chat => registry.resolve(requested.as_deref()),
ResolutionKind::Embedding => registry.resolve_embedding(requested.as_deref()),
}
})
.await
.map_err(|_| ResolveError::WorkerFailed)
}
}
}
mod cancel;
mod completions;
mod embeddings;
#[cfg(target_os = "macos")]
mod encoder_model;
mod guardrails;
mod handler;
mod images;
mod messages;
mod model;
pub mod observe;
mod ollama;
mod queue;
#[cfg(target_os = "macos")]
mod real_model;
#[cfg(target_os = "macos")]
#[cfg(test)]
#[path = "real_model_tests.rs"]
mod real_model_tests;
pub mod registry;
mod response;
mod responses;
pub mod vision;
pub(crate) const DEGRADATION_HEADER: &str = "x-anyllm-degradation";
const MAX_REQUEST_BODY_BYTES: usize = 25 * 1024 * 1024;
pub const SERVER_ROUTE_PATHS: &[&str] = &[
"/health",
"/v1/chat/completions",
"/v1/completions",
"/v1/responses",
"/v1/images/generations",
"/v1/images/edits",
"/v1/messages",
"/v1/messages/count_tokens",
"/v1/models",
"/v1/models/{model}",
"/v1/embeddings",
"/api/tags",
"/api/version",
"/api/show",
"/api/chat",
"/api/generate",
"/api/embeddings",
"/api/embed",
"/v1/audio/transcriptions",
"/v1/audio/translations",
"/v1/audio/transcriptions/realtime",
"/v1/audio/speech",
"/v1/audio/generate",
"/v1/audio/jobs/{id}",
"/v1/audio/jobs/{id}/result",
"/v1/audio/models",
"/v1/audio/models/{id}",
"/v1/metrics",
];
pub use audio::{
AudioError, AudioModelInfo, AudioProvider, AudioTask, GenerateRequest, GeneratedAudio,
SpeechRequest, SpeechStream, TranscribeRequest, TranscribedSegment, Transcription,
};
#[cfg(target_os = "macos")]
pub use encoder_model::RealEncoderModel;
pub use guardrails::GuardrailConfig;
pub use handler::AppState;
pub use images::{ImageError, ImageGenerateRequest, ImageProvider, MAX_IMAGE_PROMPT_BYTES};
pub use model::{ChatModel, ScriptedChatModel};
pub use queue::{GenerationPermit, GenerationQueue};
#[cfg(target_os = "macos")]
pub use real_model::RealChatModel;
pub use anyllm_translate::anthropic::{MessageCreateRequest, MessageResponse};
pub use anyllm_translate::openai::streaming::ChatCompletionChunk;
pub use anyllm_translate::openai::{
ChatCompletionRequest, ChatCompletionResponse, ChatMessage, ChatUsage, Choice,
};
use std::num::NonZeroUsize;
use std::sync::Arc;
use axum::routing::{get, post};
use axum::Router;
#[derive(Default, Clone)]
pub struct RouterOptions {
pub api_key: Option<String>,
pub observer: Option<Arc<dyn observe::ServerObserver>>,
}
#[derive(Clone)]
pub struct ServerState {
pub(crate) registry: Arc<dyn registry::ModelRegistry>,
pub(crate) resolve_executor: bounded_resolve::BoundedResolveExecutor,
pub(crate) observer: Option<Arc<dyn observe::ServerObserver>>,
pub(crate) ids: Arc<observe::RequestIds>,
pub(crate) image_provider: Option<Arc<dyn ImageProvider>>,
pub(crate) audio: Option<audio::AudioState>,
}
impl ServerState {
pub fn new(registry: Arc<dyn registry::ModelRegistry>) -> Self {
Self {
registry,
resolve_executor: bounded_resolve::BoundedResolveExecutor::default(),
observer: None,
ids: Arc::new(observe::RequestIds::default()),
image_provider: None,
audio: None,
}
}
pub fn with_image_provider(mut self, provider: Arc<dyn ImageProvider>) -> Self {
self.image_provider = Some(provider);
self
}
pub fn with_audio_provider(mut self, provider: Arc<dyn AudioProvider>) -> Self {
self.audio = Some(audio::AudioState::new(provider));
self
}
}
impl ServerState {
pub fn with_max_concurrent_resolutions(mut self, max_concurrent: NonZeroUsize) -> Self {
self.resolve_executor = bounded_resolve::BoundedResolveExecutor::new(max_concurrent);
self
}
}
impl From<Arc<dyn ChatModel>> for ServerState {
fn from(model: Arc<dyn ChatModel>) -> Self {
Self::new(Arc::new(registry::SingleModel::new(model)))
}
}
impl From<Arc<dyn registry::ModelRegistry>> for ServerState {
fn from(registry: Arc<dyn registry::ModelRegistry>) -> Self {
Self::new(registry)
}
}
pub fn build_router(state: impl Into<ServerState>) -> Router {
build_router_with_options(state, RouterOptions::default())
}
pub fn build_router_with_options(state: impl Into<ServerState>, options: RouterOptions) -> Router {
let mut state = state.into();
state.observer = options.observer.clone();
let protected = Router::new()
.route("/v1/chat/completions", post(handler::chat_completions))
.route("/v1/completions", post(completions::completions))
.route("/v1/responses", post(responses::responses))
.route(
"/v1/images/generations",
post(images::generations).layer(axum::extract::DefaultBodyLimit::max(
images::MAX_IMAGE_REQUEST_BODY_BYTES,
)),
)
.route("/v1/images/edits", post(images::edits_unsupported))
.route("/v1/messages", post(messages::messages))
.route("/v1/messages/count_tokens", post(messages::count_tokens))
.route("/v1/models", get(handler::models))
.route("/v1/models/:model", get(handler::model_detail))
.route("/v1/embeddings", post(embeddings::embeddings))
.route("/api/tags", get(ollama::tags))
.route("/api/version", get(ollama::version))
.route("/api/show", post(ollama::show))
.route("/api/chat", post(ollama::chat))
.route("/api/generate", post(ollama::generate))
.route("/api/embeddings", post(embeddings::ollama_embeddings))
.route("/api/embed", post(embeddings::ollama_embed))
.layer(axum::extract::DefaultBodyLimit::max(MAX_REQUEST_BODY_BYTES));
let protected = if state.audio.is_some() {
protected.merge(audio_router())
} else {
protected
};
let protected = match options.api_key {
Some(key) => protected.layer(axum::middleware::from_fn_with_state(
auth::ApiKey(key.into()),
auth::require_api_key,
)),
None => protected,
};
let router = Router::new()
.route("/health", get(handler::health))
.merge(protected);
let router = match options.observer {
Some(_) => router.layer(axum::middleware::from_fn_with_state(
state.clone(),
observe_layer,
)),
None => router,
};
router
.layer(axum::middleware::from_fn(handler::request_lease_layer))
.with_state(state)
}
fn audio_router() -> Router<ServerState> {
use axum::routing::delete;
Router::new()
.route("/v1/audio/transcriptions", post(audio::transcriptions))
.route(
"/v1/audio/translations",
post(audio::translations_unsupported),
)
.route("/v1/audio/transcriptions/realtime", get(audio::realtime))
.route("/v1/audio/speech", post(audio::speech))
.route("/v1/audio/generate", post(audio::generate))
.route(
"/v1/audio/jobs/:id",
get(audio::get_job).delete(audio::delete_job),
)
.route("/v1/audio/jobs/:id/result", get(audio::get_job_result))
.route(
"/v1/audio/models",
get(audio::list_models).post(audio::load_model),
)
.route("/v1/audio/models/:id", delete(audio::delete_model))
.route("/v1/metrics", get(audio::metrics_endpoint))
.layer(axum::extract::DefaultBodyLimit::max(
audio::MAX_AUDIO_UPLOAD_BYTES,
))
}
async fn observe_layer(
axum::extract::State(state): axum::extract::State<ServerState>,
mut request: axum::extract::Request,
next: axum::middleware::Next,
) -> axum::response::Response {
let id = state.ids.next();
let method = request.method().to_string();
let path = request.uri().path().to_string();
request.extensions_mut().insert(observe::RequestTag(id));
observe::record(&state.observer, || observe::ServerEvent::RequestStarted {
id,
at_ms: observe::now_ms(),
method,
path,
});
let started = std::time::Instant::now();
let mut response = next.run(request).await;
let status = response.status().as_u16();
let duration_ms = started.elapsed().as_millis() as u32;
let error = if status >= 400 {
let (parts, body) = response.into_parts();
match axum::body::to_bytes(body, 64 * 1024).await {
Ok(bytes) => {
let err = extract_error_message(&bytes);
response =
axum::response::Response::from_parts(parts, axum::body::Body::from(bytes));
err
}
Err(_) => {
response = axum::response::Response::from_parts(parts, axum::body::Body::empty());
None
}
}
} else {
None
};
observe::record(&state.observer, || observe::ServerEvent::RequestFinished {
id,
status,
duration_ms,
error,
});
response
}
fn extract_error_message(bytes: &[u8]) -> Option<String> {
if bytes.is_empty() {
return None;
}
if let Ok(val) = serde_json::from_slice::<serde_json::Value>(bytes) {
if let Some(err_obj) = val.get("error") {
if let Some(msg) = err_obj.get("message").and_then(|m| m.as_str()) {
return Some(msg.to_string());
}
if let Some(msg) = err_obj.as_str() {
return Some(msg.to_string());
}
}
if let Some(msg) = val.get("message").and_then(|m| m.as_str()) {
return Some(msg.to_string());
}
}
if let Ok(text) = std::str::from_utf8(bytes) {
let trimmed = text.trim();
if !trimmed.is_empty() {
let capped: String = trimmed.chars().take(1024).collect();
return Some(capped);
}
}
None
}
pub use foundation::TokenId;