Skip to main content

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}