Skip to main content

siumai_bridge/
contracts.rs

1//! Protocol bridge contracts.
2//!
3//! This module defines the protocol-agnostic contract used by request,
4//! response, and stream bridges. Concrete bridge implementations should live
5//! in protocol crates or gateway adapters built on top of this contract.
6
7use serde::{Deserialize, Serialize};
8use std::sync::Arc;
9
10use siumai_core::{
11    error::LlmError,
12    streaming::ChatStreamEvent,
13    types::{ChatRequest, ChatResponse},
14};
15
16/// Named protocol targets that bridge implementations can produce or consume.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
18pub enum BridgeTarget {
19    OpenAiResponses,
20    OpenAiChatCompletions,
21    AnthropicMessages,
22    GeminiGenerateContent,
23}
24
25impl BridgeTarget {
26    /// Return a stable identifier for diagnostics and metrics.
27    pub const fn as_str(self) -> &'static str {
28        match self {
29            Self::OpenAiResponses => "openai-responses",
30            Self::OpenAiChatCompletions => "openai-chat-completions",
31            Self::AnthropicMessages => "anthropic-messages",
32            Self::GeminiGenerateContent => "gemini-generate-content",
33        }
34    }
35}
36
37/// Bridge strictness policy.
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
39pub enum BridgeMode {
40    Strict,
41    #[default]
42    BestEffort,
43    ProviderTolerant,
44}
45
46/// Overall bridge outcome.
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
48pub enum BridgeDecision {
49    #[default]
50    Exact,
51    Lossy,
52    Rejected,
53}
54
55impl BridgeDecision {
56    /// Combine two decisions, keeping the strongest outcome.
57    pub const fn combine(self, other: Self) -> Self {
58        match (self, other) {
59            (Self::Rejected, _) | (_, Self::Rejected) => Self::Rejected,
60            (Self::Lossy, _) | (_, Self::Lossy) => Self::Lossy,
61            _ => Self::Exact,
62        }
63    }
64}
65
66/// Typed warning categories emitted during bridging.
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
68pub enum BridgeWarningKind {
69    LossyField,
70    DroppedField,
71    UnsupportedCapability,
72    ProviderMetadataCarried,
73    ProviderMetadataDropped,
74    SemanticMismatch,
75    Custom,
76}
77
78/// A structured warning attached to a bridge report.
79#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
80pub struct BridgeWarning {
81    pub kind: BridgeWarningKind,
82    pub path: Option<String>,
83    pub message: String,
84}
85
86impl BridgeWarning {
87    pub fn new<M: Into<String>>(kind: BridgeWarningKind, message: M) -> Self {
88        Self {
89            kind,
90            path: None,
91            message: message.into(),
92        }
93    }
94
95    pub fn with_path<P: Into<String>, M: Into<String>>(
96        kind: BridgeWarningKind,
97        path: P,
98        message: M,
99    ) -> Self {
100        Self {
101            kind,
102            path: Some(path.into()),
103            message: message.into(),
104        }
105    }
106}
107
108/// Structured bridge diagnostics.
109#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
110pub struct BridgeReport {
111    pub source: Option<BridgeTarget>,
112    pub target: BridgeTarget,
113    pub mode: BridgeMode,
114    pub decision: BridgeDecision,
115    pub warnings: Vec<BridgeWarning>,
116    pub lossy_fields: Vec<String>,
117    pub dropped_fields: Vec<String>,
118    pub unsupported_capabilities: Vec<String>,
119    pub carried_provider_metadata: Vec<String>,
120}
121
122impl BridgeReport {
123    pub fn new(target: BridgeTarget, mode: BridgeMode) -> Self {
124        Self::with_source(None, target, mode)
125    }
126
127    pub fn with_source(
128        source: Option<BridgeTarget>,
129        target: BridgeTarget,
130        mode: BridgeMode,
131    ) -> Self {
132        Self {
133            source,
134            target,
135            mode,
136            decision: BridgeDecision::Exact,
137            warnings: Vec::new(),
138            lossy_fields: Vec::new(),
139            dropped_fields: Vec::new(),
140            unsupported_capabilities: Vec::new(),
141            carried_provider_metadata: Vec::new(),
142        }
143    }
144
145    pub const fn is_exact(&self) -> bool {
146        matches!(self.decision, BridgeDecision::Exact)
147    }
148
149    pub const fn is_lossy(&self) -> bool {
150        matches!(self.decision, BridgeDecision::Lossy)
151    }
152
153    pub const fn is_rejected(&self) -> bool {
154        matches!(self.decision, BridgeDecision::Rejected)
155    }
156
157    pub const fn has_warnings(&self) -> bool {
158        !self.warnings.is_empty()
159    }
160
161    pub fn add_warning(&mut self, warning: BridgeWarning) {
162        let impact = match warning.kind {
163            BridgeWarningKind::LossyField
164            | BridgeWarningKind::DroppedField
165            | BridgeWarningKind::UnsupportedCapability
166            | BridgeWarningKind::ProviderMetadataDropped
167            | BridgeWarningKind::SemanticMismatch => BridgeDecision::Lossy,
168            BridgeWarningKind::ProviderMetadataCarried | BridgeWarningKind::Custom => {
169                BridgeDecision::Exact
170            }
171        };
172        self.decision = self.decision.combine(impact);
173        self.warnings.push(warning);
174    }
175
176    pub fn mark_lossy(&mut self) {
177        self.decision = self.decision.combine(BridgeDecision::Lossy);
178    }
179
180    pub fn reject<M: Into<String>>(&mut self, message: M) {
181        self.decision = BridgeDecision::Rejected;
182        self.warnings
183            .push(BridgeWarning::new(BridgeWarningKind::Custom, message));
184    }
185
186    pub fn reject_with_warning(&mut self, warning: BridgeWarning) {
187        self.decision = BridgeDecision::Rejected;
188        self.warnings.push(warning);
189    }
190
191    pub fn record_lossy_field<P: Into<String>, M: Into<String>>(&mut self, path: P, message: M) {
192        let path = path.into();
193        self.lossy_fields.push(path.clone());
194        self.add_warning(BridgeWarning::with_path(
195            BridgeWarningKind::LossyField,
196            path,
197            message,
198        ));
199    }
200
201    pub fn record_dropped_field<P: Into<String>, M: Into<String>>(&mut self, path: P, message: M) {
202        let path = path.into();
203        self.dropped_fields.push(path.clone());
204        self.add_warning(BridgeWarning::with_path(
205            BridgeWarningKind::DroppedField,
206            path,
207            message,
208        ));
209    }
210
211    pub fn record_unsupported_capability<C: Into<String>, M: Into<String>>(
212        &mut self,
213        capability: C,
214        message: M,
215    ) {
216        let capability = capability.into();
217        self.unsupported_capabilities.push(capability.clone());
218        self.add_warning(BridgeWarning::with_path(
219            BridgeWarningKind::UnsupportedCapability,
220            capability,
221            message,
222        ));
223    }
224
225    pub fn record_carried_provider_metadata<N: Into<String>, M: Into<String>>(
226        &mut self,
227        namespace: N,
228        message: M,
229    ) {
230        let namespace = namespace.into();
231        self.carried_provider_metadata.push(namespace.clone());
232        self.add_warning(BridgeWarning::with_path(
233            BridgeWarningKind::ProviderMetadataCarried,
234            namespace,
235            message,
236        ));
237    }
238
239    pub fn merge(&mut self, other: Self) {
240        self.decision = self.decision.combine(other.decision);
241        self.warnings.extend(other.warnings);
242        self.lossy_fields.extend(other.lossy_fields);
243        self.dropped_fields.extend(other.dropped_fields);
244        self.unsupported_capabilities
245            .extend(other.unsupported_capabilities);
246        self.carried_provider_metadata
247            .extend(other.carried_provider_metadata);
248        if self.source.is_none() {
249            self.source = other.source;
250        }
251    }
252}
253
254/// A bridge result paired with its diagnostic report.
255///
256/// Rejected bridges return `value = None` and `report.decision = Rejected`.
257#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
258pub struct BridgeResult<T> {
259    pub value: Option<T>,
260    pub report: BridgeReport,
261}
262
263impl<T> BridgeResult<T> {
264    pub fn new(value: T, report: BridgeReport) -> Self {
265        Self {
266            value: Some(value),
267            report,
268        }
269    }
270
271    pub fn rejected(report: BridgeReport) -> Self {
272        Self {
273            value: None,
274            report,
275        }
276    }
277
278    pub const fn is_rejected(&self) -> bool {
279        self.value.is_none() || self.report.is_rejected()
280    }
281
282    pub fn map<U, F>(self, f: F) -> BridgeResult<U>
283    where
284        F: FnOnce(T) -> U,
285    {
286        BridgeResult {
287            value: self.value.map(f),
288            report: self.report,
289        }
290    }
291
292    pub fn into_parts(self) -> (Option<T>, BridgeReport) {
293        (self.value, self.report)
294    }
295
296    #[allow(clippy::result_large_err)]
297    pub fn into_result(self) -> Result<(T, BridgeReport), BridgeReport> {
298        match (self.value, self.report) {
299            (Some(value), report) if !report.is_rejected() => Ok((value, report)),
300            (_, report) => Err(report),
301        }
302    }
303}
304
305/// Typed bridge primitive categories exposed to remap policies.
306#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
307pub enum BridgePrimitiveKind {
308    ToolDefinition,
309    ToolChoice,
310    ToolCall,
311    ToolResult,
312}
313
314/// Request bridge lifecycle phase.
315#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
316pub enum RequestBridgePhase {
317    NormalizeSource,
318    SerializeTarget,
319}
320
321/// Request bridge lifecycle context.
322#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
323pub struct RequestBridgeContext {
324    pub phase: RequestBridgePhase,
325    pub source: Option<BridgeTarget>,
326    pub target: BridgeTarget,
327    pub mode: BridgeMode,
328    pub route_label: Option<String>,
329    pub path_label: Option<String>,
330}
331
332impl RequestBridgeContext {
333    pub fn new(
334        phase: RequestBridgePhase,
335        source: Option<BridgeTarget>,
336        target: BridgeTarget,
337        mode: BridgeMode,
338        route_label: Option<String>,
339        path_label: Option<String>,
340    ) -> Self {
341        Self {
342            phase,
343            source,
344            target,
345            mode,
346            route_label,
347            path_label,
348        }
349    }
350}
351
352/// Response bridge lifecycle context.
353#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
354pub struct ResponseBridgeContext {
355    pub source: Option<BridgeTarget>,
356    pub target: BridgeTarget,
357    pub mode: BridgeMode,
358    pub route_label: Option<String>,
359    pub path_label: Option<String>,
360}
361
362impl ResponseBridgeContext {
363    pub fn new(
364        source: Option<BridgeTarget>,
365        target: BridgeTarget,
366        mode: BridgeMode,
367        route_label: Option<String>,
368        path_label: Option<String>,
369    ) -> Self {
370        Self {
371            source,
372            target,
373            mode,
374            route_label,
375            path_label,
376        }
377    }
378}
379
380/// Stream bridge lifecycle context.
381#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
382pub struct StreamBridgeContext {
383    pub source: Option<BridgeTarget>,
384    pub target: BridgeTarget,
385    pub mode: BridgeMode,
386    pub route_label: Option<String>,
387    pub path_label: Option<String>,
388}
389
390impl StreamBridgeContext {
391    pub fn new(
392        source: Option<BridgeTarget>,
393        target: BridgeTarget,
394        mode: BridgeMode,
395        route_label: Option<String>,
396        path_label: Option<String>,
397    ) -> Self {
398        Self {
399            source,
400            target,
401            mode,
402            route_label,
403            path_label,
404        }
405    }
406}
407
408/// Primitive remap context shared by request/response/stream remappers.
409#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
410pub struct BridgePrimitiveContext {
411    pub source: Option<BridgeTarget>,
412    pub target: BridgeTarget,
413    pub mode: BridgeMode,
414    pub route_label: Option<String>,
415    pub path_label: Option<String>,
416    pub primitive: BridgePrimitiveKind,
417}
418
419impl BridgePrimitiveContext {
420    pub fn new(
421        source: Option<BridgeTarget>,
422        target: BridgeTarget,
423        mode: BridgeMode,
424        route_label: Option<String>,
425        path_label: Option<String>,
426        primitive: BridgePrimitiveKind,
427    ) -> Self {
428        Self {
429            source,
430            target,
431            mode,
432            route_label,
433            path_label,
434            primitive,
435        }
436    }
437}
438
439/// Request bridge customization hook shared by source-normalization and target-serialization paths.
440pub trait RequestBridgeHook: Send + Sync {
441    fn transform_request(
442        &self,
443        _ctx: &RequestBridgeContext,
444        _request: &mut ChatRequest,
445        _report: &mut BridgeReport,
446    ) -> Result<(), LlmError> {
447        Ok(())
448    }
449
450    fn transform_json(
451        &self,
452        _ctx: &RequestBridgeContext,
453        _body: &mut serde_json::Value,
454        _report: &mut BridgeReport,
455    ) -> Result<(), LlmError> {
456        Ok(())
457    }
458
459    fn validate_json(
460        &self,
461        _ctx: &RequestBridgeContext,
462        _body: &serde_json::Value,
463        _report: &mut BridgeReport,
464    ) -> Result<(), LlmError> {
465        Ok(())
466    }
467}
468
469/// Response bridge customization hook.
470pub trait ResponseBridgeHook: Send + Sync {
471    fn transform_response(
472        &self,
473        _ctx: &ResponseBridgeContext,
474        _response: &mut ChatResponse,
475        _report: &mut BridgeReport,
476    ) -> Result<(), LlmError> {
477        Ok(())
478    }
479}
480
481/// Stream bridge customization hook.
482pub trait StreamBridgeHook: Send + Sync {
483    fn map_event(
484        &self,
485        _ctx: &StreamBridgeContext,
486        event: ChatStreamEvent,
487    ) -> Vec<ChatStreamEvent> {
488        vec![event]
489    }
490}
491
492/// Primitive remapper for small, reusable semantic rewrites.
493pub trait BridgePrimitiveRemapper: Send + Sync {
494    fn remap_tool_name(&self, _ctx: &BridgePrimitiveContext, _name: &str) -> Option<String> {
495        None
496    }
497
498    fn remap_tool_call_id(&self, _ctx: &BridgePrimitiveContext, _id: &str) -> Option<String> {
499        None
500    }
501}
502
503/// Final decision emitted by loss policies.
504#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
505pub enum BridgeLossAction {
506    Continue,
507    Reject,
508}
509
510/// Unified bridge customization trait.
511///
512/// This is an ergonomic bundle over the lower-level hook/remapper/policy traits. Applications that
513/// need coordinated request/response/stream customization can implement one object and attach it
514/// with `BridgeOptions::with_customization(...)` instead of wiring several trait objects manually.
515///
516/// The lower-level traits remain available and are still the best fit when the customization only
517/// targets one narrow bridge phase.
518pub trait BridgeCustomization: Send + Sync {
519    fn transform_request(
520        &self,
521        _ctx: &RequestBridgeContext,
522        _request: &mut ChatRequest,
523        _report: &mut BridgeReport,
524    ) -> Result<(), LlmError> {
525        Ok(())
526    }
527
528    fn transform_request_json(
529        &self,
530        _ctx: &RequestBridgeContext,
531        _body: &mut serde_json::Value,
532        _report: &mut BridgeReport,
533    ) -> Result<(), LlmError> {
534        Ok(())
535    }
536
537    fn validate_request_json(
538        &self,
539        _ctx: &RequestBridgeContext,
540        _body: &serde_json::Value,
541        _report: &mut BridgeReport,
542    ) -> Result<(), LlmError> {
543        Ok(())
544    }
545
546    fn transform_response(
547        &self,
548        _ctx: &ResponseBridgeContext,
549        _response: &mut ChatResponse,
550        _report: &mut BridgeReport,
551    ) -> Result<(), LlmError> {
552        Ok(())
553    }
554
555    fn map_stream_event(
556        &self,
557        _ctx: &StreamBridgeContext,
558        event: ChatStreamEvent,
559    ) -> Vec<ChatStreamEvent> {
560        vec![event]
561    }
562
563    fn remap_tool_name(&self, _ctx: &BridgePrimitiveContext, _name: &str) -> Option<String> {
564        None
565    }
566
567    fn remap_tool_call_id(&self, _ctx: &BridgePrimitiveContext, _id: &str) -> Option<String> {
568        None
569    }
570
571    fn request_action(
572        &self,
573        ctx: &RequestBridgeContext,
574        report: &BridgeReport,
575    ) -> BridgeLossAction {
576        if report.is_rejected() || (matches!(ctx.mode, BridgeMode::Strict) && report.is_lossy()) {
577            BridgeLossAction::Reject
578        } else {
579            BridgeLossAction::Continue
580        }
581    }
582
583    fn response_action(
584        &self,
585        ctx: &ResponseBridgeContext,
586        report: &BridgeReport,
587    ) -> BridgeLossAction {
588        if report.is_rejected() || (matches!(ctx.mode, BridgeMode::Strict) && report.is_lossy()) {
589            BridgeLossAction::Reject
590        } else {
591            BridgeLossAction::Continue
592        }
593    }
594
595    fn stream_action(&self, ctx: &StreamBridgeContext, report: &BridgeReport) -> BridgeLossAction {
596        if report.is_rejected() || (matches!(ctx.mode, BridgeMode::Strict) && report.is_lossy()) {
597            BridgeLossAction::Reject
598        } else {
599            BridgeLossAction::Continue
600        }
601    }
602}
603
604/// Policy for deciding whether a bridge report should continue or reject.
605pub trait BridgeLossPolicy: Send + Sync {
606    fn request_action(&self, ctx: &RequestBridgeContext, report: &BridgeReport)
607    -> BridgeLossAction;
608
609    fn response_action(
610        &self,
611        ctx: &ResponseBridgeContext,
612        report: &BridgeReport,
613    ) -> BridgeLossAction;
614
615    fn stream_action(&self, ctx: &StreamBridgeContext, report: &BridgeReport) -> BridgeLossAction;
616}
617
618/// Default mode-aware loss policy.
619#[derive(Debug, Default)]
620pub struct DefaultBridgeLossPolicy;
621
622fn default_loss_action(mode: BridgeMode, report: &BridgeReport) -> BridgeLossAction {
623    if report.is_rejected() || (matches!(mode, BridgeMode::Strict) && report.is_lossy()) {
624        BridgeLossAction::Reject
625    } else {
626        BridgeLossAction::Continue
627    }
628}
629
630impl BridgeLossPolicy for DefaultBridgeLossPolicy {
631    fn request_action(
632        &self,
633        ctx: &RequestBridgeContext,
634        report: &BridgeReport,
635    ) -> BridgeLossAction {
636        default_loss_action(ctx.mode, report)
637    }
638
639    fn response_action(
640        &self,
641        ctx: &ResponseBridgeContext,
642        report: &BridgeReport,
643    ) -> BridgeLossAction {
644        default_loss_action(ctx.mode, report)
645    }
646
647    fn stream_action(&self, ctx: &StreamBridgeContext, report: &BridgeReport) -> BridgeLossAction {
648        default_loss_action(ctx.mode, report)
649    }
650}
651
652#[derive(Clone)]
653struct BridgeCustomizationAdapter {
654    customization: Arc<dyn BridgeCustomization>,
655}
656
657impl BridgeCustomizationAdapter {
658    fn new(customization: Arc<dyn BridgeCustomization>) -> Self {
659        Self { customization }
660    }
661}
662
663impl RequestBridgeHook for BridgeCustomizationAdapter {
664    fn transform_request(
665        &self,
666        ctx: &RequestBridgeContext,
667        request: &mut ChatRequest,
668        report: &mut BridgeReport,
669    ) -> Result<(), LlmError> {
670        self.customization.transform_request(ctx, request, report)
671    }
672
673    fn transform_json(
674        &self,
675        ctx: &RequestBridgeContext,
676        body: &mut serde_json::Value,
677        report: &mut BridgeReport,
678    ) -> Result<(), LlmError> {
679        self.customization.transform_request_json(ctx, body, report)
680    }
681
682    fn validate_json(
683        &self,
684        ctx: &RequestBridgeContext,
685        body: &serde_json::Value,
686        report: &mut BridgeReport,
687    ) -> Result<(), LlmError> {
688        self.customization.validate_request_json(ctx, body, report)
689    }
690}
691
692impl ResponseBridgeHook for BridgeCustomizationAdapter {
693    fn transform_response(
694        &self,
695        ctx: &ResponseBridgeContext,
696        response: &mut ChatResponse,
697        report: &mut BridgeReport,
698    ) -> Result<(), LlmError> {
699        self.customization.transform_response(ctx, response, report)
700    }
701}
702
703impl StreamBridgeHook for BridgeCustomizationAdapter {
704    fn map_event(&self, ctx: &StreamBridgeContext, event: ChatStreamEvent) -> Vec<ChatStreamEvent> {
705        self.customization.map_stream_event(ctx, event)
706    }
707}
708
709impl BridgePrimitiveRemapper for BridgeCustomizationAdapter {
710    fn remap_tool_name(&self, ctx: &BridgePrimitiveContext, name: &str) -> Option<String> {
711        self.customization.remap_tool_name(ctx, name)
712    }
713
714    fn remap_tool_call_id(&self, ctx: &BridgePrimitiveContext, id: &str) -> Option<String> {
715        self.customization.remap_tool_call_id(ctx, id)
716    }
717}
718
719impl BridgeLossPolicy for BridgeCustomizationAdapter {
720    fn request_action(
721        &self,
722        ctx: &RequestBridgeContext,
723        report: &BridgeReport,
724    ) -> BridgeLossAction {
725        self.customization.request_action(ctx, report)
726    }
727
728    fn response_action(
729        &self,
730        ctx: &ResponseBridgeContext,
731        report: &BridgeReport,
732    ) -> BridgeLossAction {
733        self.customization.response_action(ctx, report)
734    }
735
736    fn stream_action(&self, ctx: &StreamBridgeContext, report: &BridgeReport) -> BridgeLossAction {
737        self.customization.stream_action(ctx, report)
738    }
739}
740
741/// Shared bridge configuration surface.
742#[derive(Clone)]
743pub struct BridgeOptions {
744    pub mode: BridgeMode,
745    pub route_label: Option<String>,
746    pub request_hook: Option<Arc<dyn RequestBridgeHook>>,
747    pub response_hook: Option<Arc<dyn ResponseBridgeHook>>,
748    pub stream_hook: Option<Arc<dyn StreamBridgeHook>>,
749    pub primitive_remapper: Option<Arc<dyn BridgePrimitiveRemapper>>,
750    pub loss_policy: Arc<dyn BridgeLossPolicy>,
751}
752
753/// Partial bridge configuration override.
754///
755/// This is primarily useful for route-level or gateway-level customization where callers want
756/// to override only selected fields, without having to restate the base `BridgeMode`.
757#[derive(Clone, Default)]
758pub struct BridgeOptionsOverride {
759    pub mode: Option<BridgeMode>,
760    pub route_label: Option<String>,
761    pub request_hook: Option<Arc<dyn RequestBridgeHook>>,
762    pub response_hook: Option<Arc<dyn ResponseBridgeHook>>,
763    pub stream_hook: Option<Arc<dyn StreamBridgeHook>>,
764    pub primitive_remapper: Option<Arc<dyn BridgePrimitiveRemapper>>,
765    pub loss_policy: Option<Arc<dyn BridgeLossPolicy>>,
766}
767
768impl Default for BridgeOptions {
769    fn default() -> Self {
770        Self::new(BridgeMode::default())
771    }
772}
773
774impl BridgeOptions {
775    pub fn new(mode: BridgeMode) -> Self {
776        Self {
777            mode,
778            route_label: None,
779            request_hook: None,
780            response_hook: None,
781            stream_hook: None,
782            primitive_remapper: None,
783            loss_policy: Arc::new(DefaultBridgeLossPolicy),
784        }
785    }
786
787    pub fn with_route_label(mut self, route_label: impl Into<String>) -> Self {
788        self.route_label = Some(route_label.into());
789        self
790    }
791
792    pub fn with_request_hook(mut self, hook: Arc<dyn RequestBridgeHook>) -> Self {
793        self.request_hook = Some(hook);
794        self
795    }
796
797    pub fn with_response_hook(mut self, hook: Arc<dyn ResponseBridgeHook>) -> Self {
798        self.response_hook = Some(hook);
799        self
800    }
801
802    pub fn with_stream_hook(mut self, hook: Arc<dyn StreamBridgeHook>) -> Self {
803        self.stream_hook = Some(hook);
804        self
805    }
806
807    pub fn with_primitive_remapper(mut self, remapper: Arc<dyn BridgePrimitiveRemapper>) -> Self {
808        self.primitive_remapper = Some(remapper);
809        self
810    }
811
812    pub fn with_loss_policy(mut self, policy: Arc<dyn BridgeLossPolicy>) -> Self {
813        self.loss_policy = policy;
814        self
815    }
816
817    pub fn with_customization(mut self, customization: Arc<dyn BridgeCustomization>) -> Self {
818        let adapter = Arc::new(BridgeCustomizationAdapter::new(customization));
819        self.request_hook = Some(adapter.clone());
820        self.response_hook = Some(adapter.clone());
821        self.stream_hook = Some(adapter.clone());
822        self.primitive_remapper = Some(adapter.clone());
823        self.loss_policy = adapter;
824        self
825    }
826
827    pub fn merged_with(mut self, overlay: BridgeOptions) -> Self {
828        self.mode = overlay.mode;
829        if overlay.route_label.is_some() {
830            self.route_label = overlay.route_label;
831        }
832        if overlay.request_hook.is_some() {
833            self.request_hook = overlay.request_hook;
834        }
835        if overlay.response_hook.is_some() {
836            self.response_hook = overlay.response_hook;
837        }
838        if overlay.stream_hook.is_some() {
839            self.stream_hook = overlay.stream_hook;
840        }
841        if overlay.primitive_remapper.is_some() {
842            self.primitive_remapper = overlay.primitive_remapper;
843        }
844        self.loss_policy = overlay.loss_policy;
845        self
846    }
847
848    pub fn merged_with_override(mut self, overlay: BridgeOptionsOverride) -> Self {
849        if let Some(mode) = overlay.mode {
850            self.mode = mode;
851        }
852        if overlay.route_label.is_some() {
853            self.route_label = overlay.route_label;
854        }
855        if overlay.request_hook.is_some() {
856            self.request_hook = overlay.request_hook;
857        }
858        if overlay.response_hook.is_some() {
859            self.response_hook = overlay.response_hook;
860        }
861        if overlay.stream_hook.is_some() {
862            self.stream_hook = overlay.stream_hook;
863        }
864        if overlay.primitive_remapper.is_some() {
865            self.primitive_remapper = overlay.primitive_remapper;
866        }
867        if let Some(loss_policy) = overlay.loss_policy {
868            self.loss_policy = loss_policy;
869        }
870        self
871    }
872}
873
874impl BridgeOptionsOverride {
875    pub fn new() -> Self {
876        Self::default()
877    }
878
879    pub fn with_mode(mut self, mode: BridgeMode) -> Self {
880        self.mode = Some(mode);
881        self
882    }
883
884    pub fn with_route_label(mut self, route_label: impl Into<String>) -> Self {
885        self.route_label = Some(route_label.into());
886        self
887    }
888
889    pub fn with_request_hook(mut self, hook: Arc<dyn RequestBridgeHook>) -> Self {
890        self.request_hook = Some(hook);
891        self
892    }
893
894    pub fn with_response_hook(mut self, hook: Arc<dyn ResponseBridgeHook>) -> Self {
895        self.response_hook = Some(hook);
896        self
897    }
898
899    pub fn with_stream_hook(mut self, hook: Arc<dyn StreamBridgeHook>) -> Self {
900        self.stream_hook = Some(hook);
901        self
902    }
903
904    pub fn with_primitive_remapper(mut self, remapper: Arc<dyn BridgePrimitiveRemapper>) -> Self {
905        self.primitive_remapper = Some(remapper);
906        self
907    }
908
909    pub fn with_loss_policy(mut self, policy: Arc<dyn BridgeLossPolicy>) -> Self {
910        self.loss_policy = Some(policy);
911        self
912    }
913
914    pub fn with_customization(mut self, customization: Arc<dyn BridgeCustomization>) -> Self {
915        let adapter = Arc::new(BridgeCustomizationAdapter::new(customization));
916        self.request_hook = Some(adapter.clone());
917        self.response_hook = Some(adapter.clone());
918        self.stream_hook = Some(adapter.clone());
919        self.primitive_remapper = Some(adapter.clone());
920        self.loss_policy = Some(adapter);
921        self
922    }
923}
924
925impl From<BridgeOptions> for BridgeOptionsOverride {
926    fn from(value: BridgeOptions) -> Self {
927        Self {
928            mode: Some(value.mode),
929            route_label: value.route_label,
930            request_hook: value.request_hook,
931            response_hook: value.response_hook,
932            stream_hook: value.stream_hook,
933            primitive_remapper: value.primitive_remapper,
934            loss_policy: Some(value.loss_policy),
935        }
936    }
937}
938
939#[cfg(test)]
940mod tests {
941    use super::*;
942    use serde_json::json;
943    use siumai_core::types::{ChatRequest, ChatStreamPart, MessageContent};
944
945    struct CompositeCustomization;
946
947    impl BridgeCustomization for CompositeCustomization {
948        fn transform_request(
949            &self,
950            _ctx: &RequestBridgeContext,
951            request: &mut ChatRequest,
952            report: &mut BridgeReport,
953        ) -> Result<(), LlmError> {
954            request.common_params.max_tokens = Some(42);
955            report.add_warning(BridgeWarning::new(
956                BridgeWarningKind::Custom,
957                "customization transformed request",
958            ));
959            Ok(())
960        }
961
962        fn transform_request_json(
963            &self,
964            _ctx: &RequestBridgeContext,
965            body: &mut serde_json::Value,
966            _report: &mut BridgeReport,
967        ) -> Result<(), LlmError> {
968            body["metadata"] = json!({ "customized": true });
969            Ok(())
970        }
971
972        fn validate_request_json(
973            &self,
974            _ctx: &RequestBridgeContext,
975            body: &serde_json::Value,
976            report: &mut BridgeReport,
977        ) -> Result<(), LlmError> {
978            assert_eq!(body["metadata"]["customized"], json!(true));
979            report.add_warning(BridgeWarning::new(
980                BridgeWarningKind::Custom,
981                "customization validated request json",
982            ));
983            Ok(())
984        }
985
986        fn transform_response(
987            &self,
988            _ctx: &ResponseBridgeContext,
989            response: &mut ChatResponse,
990            _report: &mut BridgeReport,
991        ) -> Result<(), LlmError> {
992            response.content = MessageContent::Text("[customized]".to_string());
993            Ok(())
994        }
995
996        fn map_stream_event(
997            &self,
998            _ctx: &StreamBridgeContext,
999            event: ChatStreamEvent,
1000        ) -> Vec<ChatStreamEvent> {
1001            match event {
1002                ChatStreamEvent::Part {
1003                    part:
1004                        ChatStreamPart::TextDelta {
1005                            id,
1006                            delta,
1007                            provider_metadata,
1008                        },
1009                } => {
1010                    vec![ChatStreamEvent::Part {
1011                        part: ChatStreamPart::TextDelta {
1012                            id,
1013                            delta: delta.to_uppercase(),
1014                            provider_metadata,
1015                        },
1016                    }]
1017                }
1018                ChatStreamEvent::PartWithReplay {
1019                    part:
1020                        ChatStreamPart::TextDelta {
1021                            id,
1022                            delta,
1023                            provider_metadata,
1024                        },
1025                    replay,
1026                } => {
1027                    vec![ChatStreamEvent::PartWithReplay {
1028                        part: ChatStreamPart::TextDelta {
1029                            id,
1030                            delta: delta.to_uppercase(),
1031                            provider_metadata,
1032                        },
1033                        replay,
1034                    }]
1035                }
1036                other => vec![other],
1037            }
1038        }
1039
1040        fn remap_tool_name(&self, _ctx: &BridgePrimitiveContext, name: &str) -> Option<String> {
1041            Some(format!("gw_{name}"))
1042        }
1043
1044        fn remap_tool_call_id(&self, _ctx: &BridgePrimitiveContext, id: &str) -> Option<String> {
1045            Some(format!("gw_{id}"))
1046        }
1047
1048        fn request_action(
1049            &self,
1050            _ctx: &RequestBridgeContext,
1051            report: &BridgeReport,
1052        ) -> BridgeLossAction {
1053            if report.is_lossy() {
1054                BridgeLossAction::Reject
1055            } else {
1056                BridgeLossAction::Continue
1057            }
1058        }
1059    }
1060
1061    #[test]
1062    fn report_starts_exact_and_metadata_warning_keeps_exact_decision() {
1063        let mut report = BridgeReport::with_source(
1064            Some(BridgeTarget::OpenAiResponses),
1065            BridgeTarget::AnthropicMessages,
1066            BridgeMode::BestEffort,
1067        );
1068
1069        assert!(report.is_exact());
1070        assert!(!report.has_warnings());
1071
1072        report.record_carried_provider_metadata(
1073            "openai",
1074            "provider metadata namespace was preserved for downstream inspection",
1075        );
1076
1077        assert!(report.is_exact());
1078        assert!(report.has_warnings());
1079        assert_eq!(report.carried_provider_metadata, vec!["openai"]);
1080        assert_eq!(report.warnings.len(), 1);
1081        assert_eq!(
1082            report.warnings[0].kind,
1083            BridgeWarningKind::ProviderMetadataCarried
1084        );
1085    }
1086
1087    #[test]
1088    fn lossy_and_dropped_fields_mark_report_lossy() {
1089        let mut report = BridgeReport::with_source(
1090            Some(BridgeTarget::AnthropicMessages),
1091            BridgeTarget::OpenAiChatCompletions,
1092            BridgeMode::Strict,
1093        );
1094
1095        report.record_lossy_field(
1096            "messages[0].thinking",
1097            "thinking blocks were flattened into plain assistant text",
1098        );
1099        report.record_dropped_field(
1100            "messages[0].cache_control",
1101            "target protocol does not support prompt caching on this route",
1102        );
1103        report.record_unsupported_capability(
1104            "computer-use",
1105            "target protocol cannot express interactive computer use actions",
1106        );
1107
1108        assert!(report.is_lossy());
1109        assert_eq!(report.lossy_fields, vec!["messages[0].thinking"]);
1110        assert_eq!(report.dropped_fields, vec!["messages[0].cache_control"]);
1111        assert_eq!(report.unsupported_capabilities, vec!["computer-use"]);
1112        assert_eq!(report.warnings.len(), 3);
1113    }
1114
1115    #[test]
1116    fn merge_aggregates_details_and_promotes_stronger_decision() {
1117        let mut aggregate =
1118            BridgeReport::new(BridgeTarget::OpenAiResponses, BridgeMode::BestEffort);
1119        aggregate.record_carried_provider_metadata(
1120            "anthropic",
1121            "request id metadata was forwarded to the target payload",
1122        );
1123
1124        let mut lossy = BridgeReport::new(BridgeTarget::OpenAiResponses, BridgeMode::BestEffort);
1125        lossy.record_lossy_field(
1126            "output[0].tool_result",
1127            "tool result details were normalized before serialization",
1128        );
1129
1130        aggregate.merge(lossy);
1131
1132        assert!(aggregate.is_lossy());
1133        assert_eq!(aggregate.warnings.len(), 2);
1134        assert_eq!(aggregate.carried_provider_metadata, vec!["anthropic"]);
1135        assert_eq!(aggregate.lossy_fields, vec!["output[0].tool_result"]);
1136    }
1137
1138    #[test]
1139    fn rejected_result_has_no_value_and_converts_into_error_result() {
1140        let mut report = BridgeReport::with_source(
1141            Some(BridgeTarget::OpenAiResponses),
1142            BridgeTarget::AnthropicMessages,
1143            BridgeMode::Strict,
1144        );
1145        report.reject("target route requires exact compatibility");
1146
1147        let result = BridgeResult::<String>::rejected(report.clone());
1148
1149        assert!(result.is_rejected());
1150        assert!(result.value.is_none());
1151
1152        let err = result.into_result().expect_err("expected rejected bridge");
1153        assert!(err.is_rejected());
1154        assert_eq!(err.warnings.len(), 1);
1155        assert_eq!(err.warnings[0].kind, BridgeWarningKind::Custom);
1156    }
1157
1158    #[test]
1159    fn default_bridge_options_use_best_effort_mode() {
1160        let options = BridgeOptions::default();
1161
1162        assert_eq!(options.mode, BridgeMode::BestEffort);
1163        assert!(options.route_label.is_none());
1164        assert!(options.request_hook.is_none());
1165        assert!(options.response_hook.is_none());
1166        assert!(options.stream_hook.is_none());
1167        assert!(options.primitive_remapper.is_none());
1168    }
1169
1170    #[test]
1171    fn default_loss_policy_rejects_strict_lossy_reports() {
1172        let ctx = RequestBridgeContext::new(
1173            RequestBridgePhase::SerializeTarget,
1174            Some(BridgeTarget::AnthropicMessages),
1175            BridgeTarget::OpenAiChatCompletions,
1176            BridgeMode::Strict,
1177            None,
1178            Some("via-normalized".to_string()),
1179        );
1180        let mut report = BridgeReport::with_source(ctx.source, ctx.target, ctx.mode);
1181        report.record_lossy_field(
1182            "messages[0].thinking",
1183            "thinking blocks were flattened into plain assistant text",
1184        );
1185
1186        assert_eq!(
1187            DefaultBridgeLossPolicy.request_action(&ctx, &report),
1188            BridgeLossAction::Reject
1189        );
1190    }
1191
1192    #[test]
1193    fn bridge_options_overlay_replaces_mode_and_present_hooks() {
1194        let base = BridgeOptions::new(BridgeMode::BestEffort).with_route_label("base");
1195        let overlay = BridgeOptions::new(BridgeMode::Strict);
1196        let merged = base.merged_with(overlay);
1197
1198        assert_eq!(merged.mode, BridgeMode::Strict);
1199        assert_eq!(merged.route_label.as_deref(), Some("base"));
1200    }
1201
1202    #[test]
1203    fn bridge_options_override_only_replaces_present_fields() {
1204        let base = BridgeOptions::new(BridgeMode::Strict).with_route_label("base");
1205        let merged =
1206            base.merged_with_override(BridgeOptionsOverride::new().with_route_label("route"));
1207
1208        assert_eq!(merged.mode, BridgeMode::Strict);
1209        assert_eq!(merged.route_label.as_deref(), Some("route"));
1210    }
1211
1212    #[test]
1213    fn bridge_options_override_can_set_mode_without_rebuilding_full_options() {
1214        let base = BridgeOptions::new(BridgeMode::BestEffort).with_route_label("base");
1215        let merged =
1216            base.merged_with_override(BridgeOptionsOverride::new().with_mode(BridgeMode::Strict));
1217
1218        assert_eq!(merged.mode, BridgeMode::Strict);
1219        assert_eq!(merged.route_label.as_deref(), Some("base"));
1220    }
1221
1222    #[test]
1223    fn bridge_options_customization_bundles_all_extension_points() {
1224        let options = BridgeOptions::new(BridgeMode::BestEffort)
1225            .with_route_label("tests.customization")
1226            .with_customization(Arc::new(CompositeCustomization));
1227
1228        assert!(options.request_hook.is_some());
1229        assert!(options.response_hook.is_some());
1230        assert!(options.stream_hook.is_some());
1231        assert!(options.primitive_remapper.is_some());
1232
1233        let request_ctx = RequestBridgeContext::new(
1234            RequestBridgePhase::SerializeTarget,
1235            Some(BridgeTarget::AnthropicMessages),
1236            BridgeTarget::OpenAiResponses,
1237            BridgeMode::BestEffort,
1238            Some("tests.customization".to_string()),
1239            Some("via-normalized".to_string()),
1240        );
1241        let mut request = ChatRequest::new(Vec::new());
1242        let mut request_report =
1243            BridgeReport::with_source(request_ctx.source, request_ctx.target, request_ctx.mode);
1244        options
1245            .request_hook
1246            .as_ref()
1247            .expect("request hook")
1248            .transform_request(&request_ctx, &mut request, &mut request_report)
1249            .expect("transform request");
1250        assert_eq!(request.common_params.max_tokens, Some(42));
1251
1252        let mut body = json!({});
1253        options
1254            .request_hook
1255            .as_ref()
1256            .expect("request hook")
1257            .transform_json(&request_ctx, &mut body, &mut request_report)
1258            .expect("transform request json");
1259        options
1260            .request_hook
1261            .as_ref()
1262            .expect("request hook")
1263            .validate_json(&request_ctx, &body, &mut request_report)
1264            .expect("validate request json");
1265
1266        let primitive_ctx = BridgePrimitiveContext::new(
1267            request_ctx.source,
1268            request_ctx.target,
1269            request_ctx.mode,
1270            request_ctx.route_label.clone(),
1271            request_ctx.path_label.clone(),
1272            BridgePrimitiveKind::ToolCall,
1273        );
1274        assert_eq!(
1275            options
1276                .primitive_remapper
1277                .as_ref()
1278                .expect("remapper")
1279                .remap_tool_name(&primitive_ctx, "weather")
1280                .as_deref(),
1281            Some("gw_weather")
1282        );
1283        assert_eq!(
1284            options
1285                .primitive_remapper
1286                .as_ref()
1287                .expect("remapper")
1288                .remap_tool_call_id(&primitive_ctx, "call_1")
1289                .as_deref(),
1290            Some("gw_call_1")
1291        );
1292
1293        let response_ctx = ResponseBridgeContext::new(
1294            request_ctx.source,
1295            request_ctx.target,
1296            request_ctx.mode,
1297            request_ctx.route_label.clone(),
1298            Some("normalized-response".to_string()),
1299        );
1300        let mut response = ChatResponse::new(MessageContent::Text("visible".to_string()));
1301        let mut response_report =
1302            BridgeReport::with_source(response_ctx.source, response_ctx.target, response_ctx.mode);
1303        options
1304            .response_hook
1305            .as_ref()
1306            .expect("response hook")
1307            .transform_response(&response_ctx, &mut response, &mut response_report)
1308            .expect("transform response");
1309        assert_eq!(
1310            response.content,
1311            MessageContent::Text("[customized]".to_string())
1312        );
1313
1314        let stream_ctx = StreamBridgeContext::new(
1315            request_ctx.source,
1316            request_ctx.target,
1317            request_ctx.mode,
1318            request_ctx.route_label.clone(),
1319            Some("stream".to_string()),
1320        );
1321        let stream_events = options
1322            .stream_hook
1323            .as_ref()
1324            .expect("stream hook")
1325            .map_event(&stream_ctx, ChatStreamEvent::text_delta_part("0", "hello"));
1326        assert_eq!(stream_events.len(), 1);
1327        let Some(ChatStreamPart::TextDelta { delta, .. }) = stream_events[0].part_ref() else {
1328            panic!("expected text delta part");
1329        };
1330        assert_eq!(delta, "HELLO");
1331
1332        let mut lossy_report =
1333            BridgeReport::with_source(request_ctx.source, request_ctx.target, request_ctx.mode);
1334        lossy_report.record_lossy_field("messages[0].thinking", "lossy");
1335        assert_eq!(
1336            options
1337                .loss_policy
1338                .request_action(&request_ctx, &lossy_report),
1339            BridgeLossAction::Reject
1340        );
1341    }
1342}