Skip to main content

gproxy_channel_api/
channel.rs

1//! Core channel adapter contract.
2
3use std::sync::Arc;
4
5use bytes::Bytes;
6use http::{HeaderMap, StatusCode};
7use serde_json::Value;
8
9use crate::context::{PrepareCtx, RefreshCtx, ShapeCtx, TransportKind};
10use crate::control::{CredentialControlOperation, CredentialControlResponse};
11use crate::disposition::Disposition;
12use crate::error::ChannelError;
13use crate::metadata::ChannelMetadata;
14use crate::prepared::PreparedRequest;
15use crate::transport::{ByteStreamDecoder as ChannelStreamDecoder, UpstreamClient};
16use crate::usage::{RateLimitResetCreditConsumeResponse, UsageSnapshot, UsageWindowDescriptor};
17
18/// A model catalogue plus the wire family used by its serialized body.
19#[derive(Debug, Clone)]
20pub struct ModelCatalog {
21    pub family: crate::protocol::Provider,
22    pub body: Bytes,
23}
24
25/// Pure upstream access adapter (§6.3). Implementors provide `id`,
26/// `routing_table` and `prepare`; the rest have sensible defaults.
27#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
28#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
29pub trait Channel: Send + Sync {
30    /// Stable channel id used as the registry key (matches `Provider.channel`).
31    fn id(&self) -> &'static str;
32
33    /// Metadata for runtime discovery and generic configuration UIs.
34    fn metadata(&self) -> ChannelMetadata {
35        ChannelMetadata::new(self.id())
36    }
37
38    /// The channel's explicit routing surface (ported from its capabilities).
39    fn routing_table(&self) -> crate::routes::RouteList;
40
41    /// Inject auth, resolve endpoint + method, set an ABSOLUTE upstream URL.
42    /// Pure access — no transform/rules, no body mutation. Moves `ctx.body` in.
43    fn prepare(&self, ctx: PrepareCtx<'_>) -> Result<PreparedRequest, ChannelError>;
44
45    /// Map an upstream response to the 5-state [`Disposition`]. Default is the
46    /// generic HTTP-status mapping; override only for provider-specific signals.
47    /// For streaming, `body` is empty (status + headers suffice).
48    fn classify(&self, status: StatusCode, headers: &HeaderMap, _body: &Bytes) -> Disposition {
49        Disposition::from_http(status, headers)
50    }
51
52    /// Whether a model-bound auth rejection (401/402/403) kills the WHOLE
53    /// credential rather than only the exact (credential, model) pair. `true`
54    /// for subscription-account channels (codex, claudecode) whose token is
55    /// account-wide. Default: model-scoped.
56    fn credential_wide_auth(&self) -> bool {
57        false
58    }
59
60    /// Whether first-time cookie exchange must use the native browser profile.
61    fn cookie_login_requires_browser(&self) -> bool {
62        false
63    }
64
65    /// Whether refreshing this secret must use the native browser profile.
66    fn refresh_requires_browser(&self, _secret: &Value) -> bool {
67        false
68    }
69
70    /// Whether this model draws from the channel's account-wide MAIN quota
71    /// pool. The main limit governs the whole account, so a 429 here cools the
72    /// WHOLE credential (separate-limit models included). Models with an
73    /// ADDITIONAL scoped limit on top of the main pool (codex spark, claude
74    /// fable) return `false`: their own 429 means only the scoped limit is hit
75    /// and stays model-scoped. Default: `false` (per-model quota, api-key
76    /// channels).
77    fn shares_account_quota(&self, _upstream_model_id: &str) -> bool {
78        false
79    }
80
81    /// Channel-specific REQUEST-body shaping (整形): runs after protocol
82    /// transform + process rules, before [`prepare`](Channel::prepare). Pure
83    /// field hygiene (strip unsupported fields, cap/rename, role/tools
84    /// normalize, remove header tokens). Default: identity.
85    fn shape_request(&self, body: Bytes, _headers: &mut HeaderMap, _ctx: &ShapeCtx) -> Bytes {
86        body
87    }
88
89    /// Channel-specific RESPONSE-body shaping (整形) on the raw buffered upstream
90    /// body, before protocol transform. Operation-aware via `ctx` so a channel
91    /// can reshape model lists, fix non-standard fields, unwrap envelopes, etc.
92    /// Runs on ALL statuses (error bodies included). Default: identity.
93    fn shape_response(&self, body: Bytes, _ctx: &ShapeCtx) -> Bytes {
94        body
95    }
96
97    /// A channel-bundled static model catalogue, for channels whose upstream
98    /// exposes no model-list endpoint (e.g. vertexexpress). When `Some`, the
99    /// admin model-pull returns it directly — no credential / upstream call. The
100    /// returned catalogue identifies its own canonical model-list wire family.
101    /// Default: none.
102    fn bundled_models(&self) -> Option<ModelCatalog> {
103        None
104    }
105
106    /// A credential-scoped model catalogue discovered while authenticating or
107    /// refreshing the secret. Unlike [`bundled_models`](Self::bundled_models),
108    /// this hook is evaluated only after the credential has been decrypted and
109    /// refreshed, so account-specific catalogues can be returned without an
110    /// extra upstream model-list request. Default: none.
111    fn credential_models(&self, _secret: &Value) -> Option<ModelCatalog> {
112        None
113    }
114
115    /// Optional channel-specific stream decoder (envelope unwrap / binary →
116    /// SSE), applied to the raw upstream byte stream before any protocol
117    /// transform. Default: none (passthrough).
118    fn stream_decoder(&self) -> Option<Box<dyn ChannelStreamDecoder>> {
119        None
120    }
121
122    /// Whether the DECRYPTED secret must be refreshed before use (e.g. OAuth
123    /// access token near expiry). Default: never.
124    fn needs_refresh(&self, _secret: &Value) -> bool {
125        false
126    }
127
128    /// Refresh the credential against the provider, returning the new PLAINTEXT
129    /// secret Value. The pipeline re-seals + persists + publishes — the channel
130    /// never touches cipher/persistence (purity §6.3). Default: unsupported.
131    async fn refresh(
132        &self,
133        _client: &Arc<dyn UpstreamClient>,
134        _ctx: RefreshCtx<'_>,
135    ) -> Result<Value, ChannelError> {
136        Err(ChannelError::Unsupported("refresh"))
137    }
138
139    fn transport(&self) -> TransportKind {
140        TransportKind::Http
141    }
142
143    /// Build one credential-scoped account/control request. The default bridges
144    /// the two legacy usage/reset hooks so existing channels keep working; new
145    /// account operations opt in explicitly per channel.
146    fn prepare_credential_control_request(
147        &self,
148        operation: &CredentialControlOperation,
149        secret: &Value,
150        settings: &Value,
151    ) -> Result<Option<http::Request<Bytes>>, ChannelError> {
152        match operation {
153            CredentialControlOperation::Usage => self.prepare_usage_request(secret, settings),
154            CredentialControlOperation::ConsumeRateLimitResetCredit { idempotency_key } => {
155                self.prepare_rate_limit_reset_credit_request(secret, settings, idempotency_key)
156            }
157            _ => Ok(None),
158        }
159    }
160
161    /// Parse a response to [`prepare_credential_control_request`].
162    fn parse_credential_control_response(
163        &self,
164        operation: &CredentialControlOperation,
165        status: StatusCode,
166        headers: &HeaderMap,
167        body: &Bytes,
168    ) -> Option<CredentialControlResponse> {
169        match operation {
170            CredentialControlOperation::Usage => self
171                .parse_usage(status, headers, body)
172                .map(CredentialControlResponse::Usage),
173            CredentialControlOperation::ConsumeRateLimitResetCredit { .. } => self
174                .parse_rate_limit_reset_credit(status, headers, body)
175                .map(CredentialControlResponse::RateLimitResetCreditConsume),
176            _ => None,
177        }
178    }
179
180    /// Build a request to this channel's per-credential upstream usage / quota
181    /// endpoint, given an already-fresh decrypted `secret` and provider
182    /// `settings`. `None` (the default) means the channel exposes no usage
183    /// endpoint (api-key / vertex channels). The driver sends it through the
184    /// credential's resolved client (same proxy + TLS profile as traffic) and
185    /// feeds the response to [`parse_usage`](Channel::parse_usage). Pure access:
186    /// no persistence, no body shaping beyond what the endpoint needs.
187    fn prepare_usage_request(
188        &self,
189        _secret: &Value,
190        _settings: &Value,
191    ) -> Result<Option<http::Request<Bytes>>, ChannelError> {
192        Ok(None)
193    }
194
195    /// Parse this channel's usage-endpoint response into the normalized
196    /// [`UsageSnapshot`]. Called only with the response to the request from
197    /// [`prepare_usage_request`](Channel::prepare_usage_request). `None` on a
198    /// non-success status or an unparseable body.
199    fn parse_usage(
200        &self,
201        _status: StatusCode,
202        _headers: &HeaderMap,
203        _body: &Bytes,
204    ) -> Option<UsageSnapshot> {
205        None
206    }
207
208    /// Describe the stable identity, scope, meter and period boundary of one
209    /// normalized usage window. The host calls this only with an index from
210    /// `snapshot.windows`; the conservative default keeps existing external
211    /// channel implementations source-compatible.
212    fn describe_usage_window(
213        &self,
214        snapshot: &UsageSnapshot,
215        index: usize,
216    ) -> UsageWindowDescriptor {
217        snapshot
218            .windows
219            .get(index)
220            .map(UsageWindowDescriptor::from_window)
221            .unwrap_or_else(|| {
222                UsageWindowDescriptor::from_window(&crate::usage::UsageWindow {
223                    name: format!("window_{index}"),
224                    ..Default::default()
225                })
226            })
227    }
228
229    /// Build a request to consume one earned rate-limit reset credit. Only
230    /// channels whose upstream exposes this account action return a request.
231    fn prepare_rate_limit_reset_credit_request(
232        &self,
233        _secret: &Value,
234        _settings: &Value,
235        _idempotency_key: &str,
236    ) -> Result<Option<http::Request<Bytes>>, ChannelError> {
237        Ok(None)
238    }
239
240    /// Parse the response from
241    /// [`prepare_rate_limit_reset_credit_request`](Self::prepare_rate_limit_reset_credit_request).
242    fn parse_rate_limit_reset_credit(
243        &self,
244        _status: StatusCode,
245        _headers: &HeaderMap,
246        _body: &Bytes,
247    ) -> Option<RateLimitResetCreditConsumeResponse> {
248        None
249    }
250}