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}