cc_lb_plugin_api/traits.rs
1//! Object-safe plugin traits for each proxy lifecycle boundary.
2
3use std::sync::Arc;
4
5use async_trait::async_trait;
6use uuid::Uuid;
7
8use crate::errors::{DialectError, ObservabilityError, RouteError, SignerError, UpstreamError};
9use crate::types::{
10 ObserveEvent, PerCandidateReason, Principal, RequestContext, RetryDecision, RouteDecision,
11 ShapedRequest, ShapedRequestBuilder, SignedRequest, SigningCapability, SlotKey, Upstream,
12 UpstreamCandidate,
13};
14
15/// Filter plugin output containing upstream selection results and per-candidate reasons.
16#[derive(Clone, Debug, PartialEq, Eq)]
17pub struct FilterOutput {
18 /// Upstream IDs that passed the filter.
19 pub kept_upstream_ids: Vec<Uuid>,
20 /// Human-readable reason for the filtering decision.
21 pub reason: String,
22 /// Per-candidate filtering reasons.
23 pub per_candidate_reasons: Vec<PerCandidateReason>,
24}
25
26/// Filter plugin errors returned by [`FilterPlugin`].
27#[derive(Debug)]
28pub enum FilterError {
29 /// Runtime error during filtering.
30 Runtime {
31 /// Redacted runtime error reason.
32 reason: String,
33 },
34 /// Trap error (plugin crashed or returned invalid state).
35 Trap {
36 /// Redacted trap error reason.
37 reason: String,
38 },
39}
40
41impl std::fmt::Display for FilterError {
42 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43 match self {
44 FilterError::Runtime { reason } => write!(f, "filter runtime error: {}", reason),
45 FilterError::Trap { reason } => write!(f, "filter trap error: {}", reason),
46 }
47 }
48}
49
50impl std::error::Error for FilterError {}
51
52/// Filter plugin boundary for upstream candidate filtering and decision-making.
53///
54/// Filter plugins evaluate requests against custom criteria and decide which upstream
55/// candidates are acceptable. They have access to the request context, authenticated
56/// principal, and list of available upstream candidates, and return a filtered set
57/// of acceptable upstreams along with per-candidate reasoning.
58pub trait FilterPlugin: Send + Sync {
59 /// Filters upstream candidates based on request and principal.
60 ///
61 /// Returns a [`FilterOutput`] containing the kept upstream IDs and per-candidate
62 /// reasons, or a [`FilterError`] if filtering fails.
63 fn filter(
64 &self,
65 ctx: &RequestContext,
66 principal: &Principal,
67 candidates: &[UpstreamCandidate],
68 ) -> Result<FilterOutput, FilterError>;
69
70 /// Returns the stable plugin identifier.
71 fn plugin_id(&self) -> Uuid;
72
73 /// Returns the human-readable plugin name.
74 fn plugin_name(&self) -> &str;
75
76 /// Returns the composite key identifying this filter slot — the
77 /// principal it is bound to and the plugin name. Callers use this
78 /// to address cache eviction, hot reload, and per-slot metrics
79 /// uniformly across runtime implementations.
80 ///
81 /// The default impl returns a global slot key derived from
82 /// [`Self::plugin_name`]. Runtime impls bound to a specific
83 /// principal (e.g. wasmtime) override this to return the
84 /// principal × plugin pair they were instantiated with.
85 fn slot_key(&self) -> SlotKey {
86 SlotKey::global(self.plugin_name())
87 }
88}
89
90/// Router plugin boundary.
91pub trait RouterPlugin: Send + Sync {
92 /// Selects the upstream and dialect for an authenticated request.
93 ///
94 /// The `candidates` parameter provides the list of available upstreams that can be
95 /// selected. Candidates are sorted by `upstream_id` ascending (Uuid byte order) to enable
96 /// deterministic routing algorithms.
97 fn route(
98 &self,
99 ctx: &RequestContext,
100 principal: &Principal,
101 candidates: &[UpstreamCandidate],
102 ) -> Result<RouteDecision, RouteError>;
103}
104
105/// Upstream dialect boundary for request shaping and error normalization.
106pub trait UpstreamDialect: Send + Sync {
107 /// Shapes a downstream Anthropic-compatible request for the selected upstream.
108 fn shape(
109 &self,
110 ctx: &RequestContext,
111 upstream: &Upstream,
112 principal: &Principal,
113 builder: &mut ShapedRequestBuilder,
114 ) -> Result<ShapedRequest, DialectError>;
115}
116
117/// Signer boundary for applying credentials to shaped requests.
118#[async_trait]
119pub trait Signer: Send + Sync {
120 /// Consumes a shaped request and returns a sealed signed request.
121 async fn sign(
122 &self,
123 shaped: ShapedRequest,
124 capability: &mut SigningCapability,
125 ) -> Result<SignedRequest, SignerError>;
126
127 /// Handles an unauthorized upstream response, optionally refreshing credentials.
128 async fn on_unauthorized(&self, err: &UpstreamError) -> RetryDecision;
129}
130
131/// Factory that builds upstream-specific signers.
132#[async_trait]
133pub trait SignerFactory: Send + Sync {
134 /// Builds a signer for the selected upstream.
135 async fn build(&self, upstream: &Upstream) -> Result<Arc<dyn Signer>, SignerError>;
136}
137
138/// Factory extension that binds signer construction to the router-selected upstream.
139pub trait ApiKeyAwareSignerFactory: Send + Sync {
140 /// Returns a signer factory using the downstream API key and router-selected upstream name.
141 fn with_router_choice(
142 &self,
143 api_key: String,
144 router_chosen_upstream_name: String,
145 ) -> Arc<dyn SignerFactory>;
146}
147
148/// Non-blocking observability hook boundary.
149pub trait ObservabilityHook: Send + Sync {
150 /// Observes a lifecycle event.
151 fn observe(&self, event: ObserveEvent) -> Result<(), ObservabilityError>;
152}