use std::sync::Arc;
use crate::api::llm::LlmRequest;
use crate::error::Result;
use crate::json::Json;
use super::request::AnnotatedLlmRequest;
use super::response::AnnotatedLlmResponse;
use super::streaming::StreamingCodec;
use super::traits::{LlmCodec, LlmResponseCodec};
use super::{anthropic, openai_chat, openai_responses};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProviderSurface {
OpenAIChat,
OpenAIResponses,
AnthropicMessages,
}
type RequestSurfaceDetector = fn(&serde_json::Map<String, Json>, Option<&str>) -> bool;
type ResponseSurfaceDetector = fn(&serde_json::Map<String, Json>) -> bool;
pub(crate) struct ProviderSurfaceDescriptor {
pub(crate) surface: ProviderSurface,
pub(crate) detect_request: RequestSurfaceDetector,
pub(crate) detect_response: ResponseSurfaceDetector,
pub(crate) decode_request: fn(&LlmRequest) -> Result<AnnotatedLlmRequest>,
pub(crate) decode_response: fn(&Json) -> Result<AnnotatedLlmResponse>,
pub(crate) codec_name: &'static str,
pub(crate) request_codec: fn() -> Arc<dyn LlmCodec>,
pub(crate) response_codec: fn() -> Arc<dyn LlmResponseCodec>,
pub(crate) streaming_codec: fn() -> Box<dyn StreamingCodec>,
}
pub(crate) static BUILTIN_PROVIDER_SURFACES: &[ProviderSurfaceDescriptor] = &[
openai_responses::PROVIDER_SURFACE,
anthropic::PROVIDER_SURFACE,
openai_chat::PROVIDER_SURFACE,
];
#[must_use]
pub fn detect_request_surface(body: &Json) -> Option<ProviderSurface> {
detect_request_surface_with_hint(body, None)
}
#[must_use]
pub fn detect_request_surface_with_hint(
body: &Json,
provider_hint: Option<&str>,
) -> Option<ProviderSurface> {
request_descriptor(body, provider_hint).map(|descriptor| descriptor.surface)
}
#[must_use]
pub fn detect_response_surface(raw: &Json) -> Option<ProviderSurface> {
response_descriptor(raw).map(|descriptor| descriptor.surface)
}
fn request_descriptor(
body: &Json,
provider_hint: Option<&str>,
) -> Option<&'static ProviderSurfaceDescriptor> {
let obj = body.as_object()?;
BUILTIN_PROVIDER_SURFACES
.iter()
.find(|descriptor| (descriptor.detect_request)(obj, provider_hint))
}
fn response_descriptor(raw: &Json) -> Option<&'static ProviderSurfaceDescriptor> {
let obj = raw.as_object()?;
let mut matches = BUILTIN_PROVIDER_SURFACES
.iter()
.filter(|descriptor| (descriptor.detect_response)(obj));
match (matches.next(), matches.next()) {
(Some(descriptor), None) => Some(descriptor),
_ => None,
}
}
#[must_use]
pub fn normalize_request(request: &LlmRequest) -> Option<AnnotatedLlmRequest> {
normalize_request_with_hint(request, None)
}
#[must_use]
pub fn normalize_request_with_hint(
request: &LlmRequest,
provider_hint: Option<&str>,
) -> Option<AnnotatedLlmRequest> {
let descriptor = request_descriptor(&request.content, provider_hint)?;
(descriptor.decode_request)(request).ok()
}
#[must_use]
pub fn normalize_response(raw: &Json) -> Option<AnnotatedLlmResponse> {
let descriptor = response_descriptor(raw)?;
(descriptor.decode_response)(raw).ok()
}
fn descriptor_for(surface: ProviderSurface) -> &'static ProviderSurfaceDescriptor {
match surface {
ProviderSurface::OpenAIChat => &openai_chat::PROVIDER_SURFACE,
ProviderSurface::OpenAIResponses => &openai_responses::PROVIDER_SURFACE,
ProviderSurface::AnthropicMessages => &anthropic::PROVIDER_SURFACE,
}
}
impl ProviderSurface {
#[must_use]
pub fn codec_name(self) -> &'static str {
descriptor_for(self).codec_name
}
#[must_use]
pub fn from_codec_name(name: &str) -> Option<Self> {
BUILTIN_PROVIDER_SURFACES
.iter()
.find(|descriptor| descriptor.codec_name == name)
.map(|descriptor| descriptor.surface)
}
}
#[must_use]
pub fn supported_codec_names() -> Vec<&'static str> {
BUILTIN_PROVIDER_SURFACES
.iter()
.map(|descriptor| descriptor.codec_name)
.collect()
}
#[must_use]
pub fn request_codec(surface: ProviderSurface) -> Arc<dyn LlmCodec> {
(descriptor_for(surface).request_codec)()
}
#[must_use]
pub fn response_codec(surface: ProviderSurface) -> Arc<dyn LlmResponseCodec> {
(descriptor_for(surface).response_codec)()
}
#[must_use]
pub fn streaming_codec(surface: ProviderSurface) -> Box<dyn StreamingCodec> {
(descriptor_for(surface).streaming_codec)()
}
#[cfg(test)]
#[path = "../../tests/unit/codec/resolve_tests.rs"]
mod tests;