1use serde::{Deserialize, Serialize};
8use std::sync::Arc;
9
10use siumai_core::{
11 error::LlmError,
12 streaming::ChatStreamEvent,
13 types::{ChatRequest, ChatResponse},
14};
15
16#[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 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
39pub enum BridgeMode {
40 Strict,
41 #[default]
42 BestEffort,
43 ProviderTolerant,
44}
45
46#[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 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#[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#[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#[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#[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
307pub enum BridgePrimitiveKind {
308 ToolDefinition,
309 ToolChoice,
310 ToolCall,
311 ToolResult,
312}
313
314#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
316pub enum RequestBridgePhase {
317 NormalizeSource,
318 SerializeTarget,
319}
320
321#[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#[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#[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#[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
439pub 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
469pub 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
481pub 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
492pub 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
505pub enum BridgeLossAction {
506 Continue,
507 Reject,
508}
509
510pub 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
604pub 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#[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#[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#[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}