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