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 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}