Skip to main content

nemo_relay/codec/
resolve.rs

1// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Provider-surface detection, best-effort normalization, and construction of
5//! the matching built-in codecs from a raw payload, surface, or codec name.
6
7use 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/// A built-in provider request/response surface.
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum ProviderSurface {
22    /// OpenAI Chat Completions.
23    OpenAIChat,
24    /// OpenAI Responses.
25    OpenAIResponses,
26    /// Anthropic Messages.
27    AnthropicMessages,
28}
29
30/// Request shape detector; the optional `&str` is a provider hint a codec may use
31/// to claim an otherwise-ambiguous shape.
32type RequestSurfaceDetector = fn(&serde_json::Map<String, Json>, Option<&str>) -> bool;
33
34/// Response shape detector; response routing is payload-only because provider
35/// responses carry stronger built-in discriminators than request bodies.
36type ResponseSurfaceDetector = fn(&serde_json::Map<String, Json>) -> bool;
37
38/// Built-in provider extraction strategy for one request/response surface.
39///
40/// The descriptor keeps surface detection next to the codec that owns the
41/// schema-specific decode logic while preserving the existing public
42/// [`LlmCodec`](super::traits::LlmCodec) and
43/// [`LlmResponseCodec`](super::traits::LlmResponseCodec) traits.
44/// `decode_response` is the provider response-extraction interface: built-in
45/// codecs populate [`AnnotatedLlmResponse`] with model names, finish reasons,
46/// tool calls, usage, cost, provider-specific fields, and replayable response
47/// data when the source payload supplies them.
48pub(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
60/// Built-in provider surfaces in request-detection priority order.
61///
62/// First match wins for requests because some shapes overlap. The order is
63/// authoritative: a hint-aware detector must stay after any stronger-signal
64/// surface it could shadow. Response detection requires exactly one match
65/// before decoding.
66pub(crate) static BUILTIN_PROVIDER_SURFACES: &[ProviderSurfaceDescriptor] = &[
67    openai_responses::PROVIDER_SURFACE,
68    anthropic::PROVIDER_SURFACE,
69    openai_chat::PROVIDER_SURFACE,
70];
71
72/// Detect the request surface from a raw request body by top-level key.
73///
74/// Priority: OpenAI Responses (`input`/`instructions`) > Anthropic Messages
75/// (`system`) > OpenAI Chat (`messages`). `None` when no key matches or `body`
76/// is not an object. This is a best-effort heuristic: an Anthropic request that
77/// omits the optional top-level `system` is indistinguishable from OpenAI Chat
78/// and classifies as `OpenAIChat`.
79#[must_use]
80pub fn detect_request_surface(body: &Json) -> Option<ProviderSurface> {
81    detect_request_surface_with_hint(body, None)
82}
83
84/// Like [`detect_request_surface`], but a recognized `provider_hint` resolves the
85/// one ambiguous shape (an Anthropic request without a top-level `system`,
86/// otherwise read as OpenAI Chat). Today, only the exact hints `"anthropic"`
87/// and `"anthropic.messages"` change detection; `None` or any other value is
88/// ignored and detection stays shape-only.
89#[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/// Detect the response surface from a raw provider response, classifying only
98/// when exactly one built-in shape matches (the built-in codecs accept minimal
99/// objects, so decode success alone is not a reliable classifier).
100#[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/// Best-effort decode of a raw request into [`AnnotatedLlmRequest`] (fail-open).
127#[must_use]
128pub fn normalize_request(request: &LlmRequest) -> Option<AnnotatedLlmRequest> {
129    normalize_request_with_hint(request, None)
130}
131
132/// Like [`normalize_request`], but a recognized `provider_hint` can
133/// disambiguate provider request shapes that are otherwise identical.
134#[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/// Best-effort decode of a raw response into [`AnnotatedLlmResponse`] (fail-open).
144#[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    /// The canonical codec name for this surface (e.g. `"openai_chat"`), the
160    /// inverse of [`Self::from_codec_name`].
161    #[must_use]
162    pub fn codec_name(self) -> &'static str {
163        descriptor_for(self).codec_name
164    }
165
166    /// Resolves a canonical codec name to its surface, or `None` when `name` is
167    /// not a built-in provider codec.
168    #[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/// The canonical codec names of every built-in provider surface, for config
178/// validation and "supported codec" messages.
179#[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/// Constructs the built-in bidirectional request codec ([`LlmCodec`]) for a surface.
188#[must_use]
189pub fn request_codec(surface: ProviderSurface) -> Arc<dyn LlmCodec> {
190    (descriptor_for(surface).request_codec)()
191}
192
193/// Constructs the built-in decode-only response codec ([`LlmResponseCodec`]) for a surface.
194#[must_use]
195pub fn response_codec(surface: ProviderSurface) -> Arc<dyn LlmResponseCodec> {
196    (descriptor_for(surface).response_codec)()
197}
198
199/// Constructs a fresh, single-use streaming codec ([`StreamingCodec`]) for a surface.
200///
201/// A [`StreamingCodec`] finalizer consumes its accumulator, so callers must
202/// construct one instance per managed streaming call.
203#[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;