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 bytes::Bytes;
7use http::StatusCode;
8use uuid::Uuid;
9
10use crate::errors::{
11 DialectError, ObservabilityError, RouteError, RuntimeError, SignerError, UpstreamError,
12};
13use crate::types::{
14 ObserveEvent, PerCandidateReason, PluginManifest, Principal, RequestContext, RetryDecision,
15 RouteDecision, ShapedRequest, ShapedRequestBuilder, SignedRequest, SigningCapability, SlotKey,
16 Upstream, UpstreamCandidate,
17};
18
19/// Filter plugin output containing upstream selection results and per-candidate reasons.
20#[derive(Clone, Debug, PartialEq, Eq)]
21pub struct FilterOutput {
22 /// Upstream IDs that passed the filter.
23 pub kept_upstream_ids: Vec<Uuid>,
24 /// Human-readable reason for the filtering decision.
25 pub reason: String,
26 /// Per-candidate filtering reasons.
27 pub per_candidate_reasons: Vec<PerCandidateReason>,
28}
29
30/// Filter plugin errors returned by [`FilterPlugin`].
31#[derive(Debug)]
32pub enum FilterError {
33 /// Runtime error during filtering.
34 Runtime {
35 /// Redacted runtime error reason.
36 reason: String,
37 },
38 /// Trap error (plugin crashed or returned invalid state).
39 Trap {
40 /// Redacted trap error reason.
41 reason: String,
42 },
43}
44
45impl std::fmt::Display for FilterError {
46 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47 match self {
48 FilterError::Runtime { reason } => write!(f, "filter runtime error: {}", reason),
49 FilterError::Trap { reason } => write!(f, "filter trap error: {}", reason),
50 }
51 }
52}
53
54impl std::error::Error for FilterError {}
55
56/// Filter plugin boundary for upstream candidate filtering and decision-making.
57///
58/// Filter plugins evaluate requests against custom criteria and decide which upstream
59/// candidates are acceptable. They have access to the request context, authenticated
60/// principal, and list of available upstream candidates, and return a filtered set
61/// of acceptable upstreams along with per-candidate reasoning.
62pub trait FilterPlugin: Send + Sync {
63 /// Filters upstream candidates based on request and principal.
64 ///
65 /// Returns a [`FilterOutput`] containing the kept upstream IDs and per-candidate
66 /// reasons, or a [`FilterError`] if filtering fails.
67 fn filter(
68 &self,
69 ctx: &RequestContext,
70 principal: &Principal,
71 candidates: &[UpstreamCandidate],
72 ) -> Result<FilterOutput, FilterError>;
73
74 /// Returns the stable plugin identifier.
75 fn plugin_id(&self) -> Uuid;
76
77 /// Returns the human-readable plugin name.
78 fn plugin_name(&self) -> &str;
79
80 /// Returns the composite key identifying this filter slot — the
81 /// principal it is bound to and the plugin name. Callers use this
82 /// to address cache eviction, hot reload, and per-slot metrics
83 /// uniformly across runtime implementations.
84 ///
85 /// The default impl returns a global slot key derived from
86 /// [`Self::plugin_name`]. Runtime impls bound to a specific
87 /// principal (e.g. wasmtime) override this to return the
88 /// principal × plugin pair they were instantiated with.
89 fn slot_key(&self) -> SlotKey {
90 SlotKey::global(self.plugin_name())
91 }
92}
93
94/// Router plugin boundary.
95#[deprecated(since = "0.2.0", note = "Use FilterPlugin via wire v3")]
96pub trait RouterPlugin: Send + Sync {
97 /// Selects the upstream and dialect for an authenticated request.
98 ///
99 /// The `candidates` parameter provides the list of available upstreams that can be
100 /// selected. Candidates are sorted by `upstream_id` ascending (Uuid byte order) to enable
101 /// deterministic routing algorithms.
102 fn route(
103 &self,
104 ctx: &RequestContext,
105 principal: &Principal,
106 candidates: &[UpstreamCandidate],
107 ) -> Result<RouteDecision, RouteError>;
108}
109
110/// Upstream dialect boundary for request shaping and error normalization.
111pub trait UpstreamDialect: Send + Sync {
112 /// Shapes a downstream Anthropic-compatible request for the selected upstream.
113 fn shape(
114 &self,
115 ctx: &RequestContext,
116 upstream: &Upstream,
117 principal: &Principal,
118 builder: &mut ShapedRequestBuilder,
119 ) -> Result<ShapedRequest, DialectError>;
120
121 /// Normalizes an upstream error body to Anthropic error shape when possible.
122 fn normalize_error(&self, status: StatusCode, body: &Bytes) -> Option<Bytes>;
123}
124
125/// Signer boundary for applying credentials to shaped requests.
126#[async_trait]
127pub trait Signer: Send + Sync {
128 /// Consumes a shaped request and returns a sealed signed request.
129 async fn sign(
130 &self,
131 shaped: ShapedRequest,
132 capability: &mut SigningCapability,
133 ) -> Result<SignedRequest, SignerError>;
134
135 /// Handles an unauthorized upstream response, optionally refreshing credentials.
136 async fn on_unauthorized(&self, err: &UpstreamError) -> RetryDecision;
137}
138
139/// Factory that builds upstream-specific signers.
140#[async_trait]
141pub trait SignerFactory: Send + Sync {
142 /// Builds a signer for the selected upstream.
143 async fn build(&self, upstream: &Upstream) -> Result<Arc<dyn Signer>, SignerError>;
144}
145
146/// Factory extension that binds signer construction to the router-selected upstream.
147pub trait ApiKeyAwareSignerFactory: Send + Sync {
148 /// Returns a signer factory using the downstream API key and router-selected upstream name.
149 fn with_router_choice(
150 &self,
151 api_key: String,
152 router_chosen_upstream_name: String,
153 ) -> Arc<dyn SignerFactory>;
154}
155
156/// Non-blocking observability hook boundary.
157pub trait ObservabilityHook: Send + Sync {
158 /// Observes a lifecycle event.
159 fn observe(&self, event: ObserveEvent) -> Result<(), ObservabilityError>;
160}
161
162/// Runtime abstraction for concrete plugin systems (currently wasmtime).
163pub trait PluginRuntime: Send + Sync {
164 /// Instantiates a router plugin.
165 #[allow(deprecated)]
166 fn instantiate_router(
167 &self,
168 manifest: &PluginManifest,
169 ) -> Result<Arc<dyn RouterPlugin>, RuntimeError>;
170
171 /// Instantiates an upstream dialect plugin.
172 fn instantiate_dialect(
173 &self,
174 manifest: &PluginManifest,
175 ) -> Result<Arc<dyn UpstreamDialect>, RuntimeError>;
176
177 /// Instantiates a signer factory plugin.
178 ///
179 /// Signer plugin extension is no longer supported — cc-lb-server uses
180 /// built-in `AnthropicKeySigner` / `AnthropicOAuthSigner` directly.
181 /// New runtime impls should leave the default `unimplemented!()` body
182 /// in place. The trait method is retained as a deprecated shim so
183 /// that out-of-tree runtimes do not see a hard trait-shape break;
184 /// it is scheduled for removal in the next semver-major bump.
185 fn instantiate_signer_factory(
186 &self,
187 _manifest: &PluginManifest,
188 ) -> Result<Arc<dyn SignerFactory>, RuntimeError> {
189 unimplemented!(
190 "signer plugin extension dropped; use built-in \
191 AnthropicKeySigner / AnthropicOAuthSigner instead"
192 )
193 }
194
195 /// Instantiates an observability hook plugin.
196 fn instantiate_observability(
197 &self,
198 manifest: &PluginManifest,
199 ) -> Result<Arc<dyn ObservabilityHook>, RuntimeError>;
200}