nemo_relay/codec/
resolve.rs1use std::sync::Arc;
8
9use crate::api::llm::LlmRequest;
10use crate::error::Result;
11use crate::json::Json;
12
13use super::request::AnnotatedLlmRequest;
14use super::response::AnnotatedLlmResponse;
15use super::streaming::StreamingCodec;
16use super::traits::{LlmCodec, LlmResponseCodec};
17use super::{anthropic, openai_chat, openai_responses};
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum ProviderSurface {
22 OpenAIChat,
24 OpenAIResponses,
26 AnthropicMessages,
28}
29
30type RequestSurfaceDetector = fn(&serde_json::Map<String, Json>, Option<&str>) -> bool;
33
34type ResponseSurfaceDetector = fn(&serde_json::Map<String, Json>) -> bool;
37
38pub(crate) struct ProviderSurfaceDescriptor {
49 pub(crate) surface: ProviderSurface,
50 pub(crate) detect_request: RequestSurfaceDetector,
51 pub(crate) detect_response: ResponseSurfaceDetector,
52 pub(crate) decode_request: fn(&LlmRequest) -> Result<AnnotatedLlmRequest>,
53 pub(crate) decode_response: fn(&Json) -> Result<AnnotatedLlmResponse>,
54 pub(crate) codec_name: &'static str,
55 pub(crate) request_codec: fn() -> Arc<dyn LlmCodec>,
56 pub(crate) response_codec: fn() -> Arc<dyn LlmResponseCodec>,
57 pub(crate) streaming_codec: fn() -> Box<dyn StreamingCodec>,
58}
59
60pub(crate) static BUILTIN_PROVIDER_SURFACES: &[ProviderSurfaceDescriptor] = &[
67 openai_responses::PROVIDER_SURFACE,
68 anthropic::PROVIDER_SURFACE,
69 openai_chat::PROVIDER_SURFACE,
70];
71
72#[must_use]
80pub fn detect_request_surface(body: &Json) -> Option<ProviderSurface> {
81 detect_request_surface_with_hint(body, None)
82}
83
84#[must_use]
90pub fn detect_request_surface_with_hint(
91 body: &Json,
92 provider_hint: Option<&str>,
93) -> Option<ProviderSurface> {
94 request_descriptor(body, provider_hint).map(|descriptor| descriptor.surface)
95}
96
97#[must_use]
101pub fn detect_response_surface(raw: &Json) -> Option<ProviderSurface> {
102 response_descriptor(raw).map(|descriptor| descriptor.surface)
103}
104
105fn request_descriptor(
106 body: &Json,
107 provider_hint: Option<&str>,
108) -> Option<&'static ProviderSurfaceDescriptor> {
109 let obj = body.as_object()?;
110 BUILTIN_PROVIDER_SURFACES
111 .iter()
112 .find(|descriptor| (descriptor.detect_request)(obj, provider_hint))
113}
114
115fn response_descriptor(raw: &Json) -> Option<&'static ProviderSurfaceDescriptor> {
116 let obj = raw.as_object()?;
117 let mut matches = BUILTIN_PROVIDER_SURFACES
118 .iter()
119 .filter(|descriptor| (descriptor.detect_response)(obj));
120 match (matches.next(), matches.next()) {
121 (Some(descriptor), None) => Some(descriptor),
122 _ => None,
123 }
124}
125
126#[must_use]
128pub fn normalize_request(request: &LlmRequest) -> Option<AnnotatedLlmRequest> {
129 normalize_request_with_hint(request, None)
130}
131
132#[must_use]
135pub fn normalize_request_with_hint(
136 request: &LlmRequest,
137 provider_hint: Option<&str>,
138) -> Option<AnnotatedLlmRequest> {
139 let descriptor = request_descriptor(&request.content, provider_hint)?;
140 (descriptor.decode_request)(request).ok()
141}
142
143#[must_use]
145pub fn normalize_response(raw: &Json) -> Option<AnnotatedLlmResponse> {
146 let descriptor = response_descriptor(raw)?;
147 (descriptor.decode_response)(raw).ok()
148}
149
150fn descriptor_for(surface: ProviderSurface) -> &'static ProviderSurfaceDescriptor {
151 match surface {
152 ProviderSurface::OpenAIChat => &openai_chat::PROVIDER_SURFACE,
153 ProviderSurface::OpenAIResponses => &openai_responses::PROVIDER_SURFACE,
154 ProviderSurface::AnthropicMessages => &anthropic::PROVIDER_SURFACE,
155 }
156}
157
158impl ProviderSurface {
159 #[must_use]
162 pub fn codec_name(self) -> &'static str {
163 descriptor_for(self).codec_name
164 }
165
166 #[must_use]
169 pub fn from_codec_name(name: &str) -> Option<Self> {
170 BUILTIN_PROVIDER_SURFACES
171 .iter()
172 .find(|descriptor| descriptor.codec_name == name)
173 .map(|descriptor| descriptor.surface)
174 }
175}
176
177#[must_use]
180pub fn supported_codec_names() -> Vec<&'static str> {
181 BUILTIN_PROVIDER_SURFACES
182 .iter()
183 .map(|descriptor| descriptor.codec_name)
184 .collect()
185}
186
187#[must_use]
189pub fn request_codec(surface: ProviderSurface) -> Arc<dyn LlmCodec> {
190 (descriptor_for(surface).request_codec)()
191}
192
193#[must_use]
195pub fn response_codec(surface: ProviderSurface) -> Arc<dyn LlmResponseCodec> {
196 (descriptor_for(surface).response_codec)()
197}
198
199#[must_use]
204pub fn streaming_codec(surface: ProviderSurface) -> Box<dyn StreamingCodec> {
205 (descriptor_for(surface).streaming_codec)()
206}
207
208#[cfg(test)]
209#[path = "../../tests/unit/codec/resolve_tests.rs"]
210mod tests;