- 本倉庫:作為
gproxy-channel的一個模組,掛在獨立的 Cargo feature 後面,透過 PR 合入。合入後所有宿主都能使用,包括 Workers 宿主。見向本倉庫新增渠道。 - 自己的 crate:一個按公開契約實現
BaseChannel的型別,連結進一個除此之外與原版相同的原生二進位檔或 Worker。本倉庫無需任何改動。見在本倉庫之外。
什麼時候需要渠道
渠道要跟著別人的線協議持續維護,所以只有在一行供應商配置表達不了上游的需求時才值得新增:路徑隨操作變化、請求體需要改寫、有帳號介面要讀取,或者要模擬某個客戶端身份。 如果差別只是源地址和一個請求頭,用custom 供應商即可,見不需要渠道的廠商。
BaseChannel 契約
BaseChannel 是唯一必須實現的 trait,id 是其中唯一必須實現的方法。每個協議操作都有獨立的非同步方法和預設實現:用 prepare(HTTP)或 prepare_connect(WebSocket)構造請求,再交給宿主分配的 client 傳送。
兩個準備鉤子預設都返回「不支援」。因此渠道支援的操作就是它準備或覆寫的那些,沒有別的。
ProviderView:id、channel、可選的base_url,以及由渠道自行解碼為型別化設定的configJSON。CredentialView:id、provider_id、auth_kind、secretJSON(不實現Debug和Serialize)、宿主記錄的公開metadata、version、expires_at_ms。PrepareContext:prepare和prepare_connect的引數,包含兩個檢視、原生OperationKey、WireRequest和可選的端點覆蓋。OperationContext:操作方法的引數,包含兩個檢視、操作的方言、WireRequest、一個歸呼叫方持有的 client、跨請求狀態、宿主例項 id 和可選的端點覆蓋。CredentialContext:供應商、憑證和 client,用於重新整理、配額和服務。
prepare 外,渠道通常還要覆寫 native_dialects(上游對某個操作原生接受的方言;返回空表示交給配置決定)。如果渠道也接受其他方言的請求,再覆寫 default_conversion_target,指定宿主在呼叫 prepare 之前把請求轉換成哪種方言。
準備與錯誤的規則
- 準備是同步的純函式:不做 I/O,不保留隱藏狀態,不構造 client。操作覆寫可以發起多次交換,但只能透過
context.client;回應流結束後也可以繼續持有 client 完成收尾,例如釋放廠商側的會話。 - 來源鑑權不會到達上游。轉發助手會丟棄逐跳請求頭,以及
host、content-length、authorization、x-api-key、x-goog-api-key和api-key;strip_query_auth會從查詢串中刪除key、access_token和api_key。之後由渠道根據憑證加上自己的鑑權。 - 轉發的客戶端請求頭取全域性白名單、供應商
allowed_headers與渠道宣告頭的並集,content-type始終保留。列表缺失或為空時不增加任何允許項。 - 端點覆蓋是完整的方法 URL,同時替換 base URL 和渠道的預設路徑,不會再往後拼接。
- 非 2xx 應答作為回應返回,狀態碼、請求頭和惰性讀取的 body 保持原樣。錯誤變體只用於能力呼叫(登入、重新整理、配額),以及多次呼叫覆寫中的中間交換,因為那裡失敗的應答不是最終返回的回應。
RefreshRejected表示上游明確拒絕了這把憑證(invalid_grant、已撤銷)。宿主預設把憑證標記為失效。渠道可以覆寫persist_rejection,只針對當前記憶體中的憑證版本記住某個拒絕原因,下次嘗試前重新載入持久化的材料。短暫的傳輸失敗或 5xx 必須作為傳輸錯誤返回,讓宿主稍後重試;誤用RefreshRejected會讓一個正常的帳號失效。ContinuationElsewhere表示一條活動的上游連線由另一個宿主程序持有。宿主會把請求轉到那個例項,憑證本身沒有問題。
可選能力
每項能力都是獨立的 trait,透過預設返回None 的訪問器提供。渠道按上游實際提供的功能實現,各項之間互不依賴,例如可以報告配額視窗而不提供重置。
classify_rejection 是 BaseChannel 上的普通方法,不是訪問器。宿主對每個非 2xx 應答呼叫它,並傳入 body 的有限字首;2xx 流以錯誤幀開頭時也會呼叫。它返回這次拒絕對憑證意味著什麼(限流、配額耗盡、餘額不足、未認證、不提供該模型,或請求本身有誤),上游給出等待時間時一併返回。預設實現只讀取結構化的錯誤欄位,需要識別廠商自己的措辭時再覆寫。
憑證與重新整理
運維已有的金鑰走普通的憑證新增和更新介面,用validate_credential_secret 校驗格式。如果材料在首次使用前需要聯網換取(例如尚未換取令牌的服務帳號金鑰),由 initial_credential_expiry 返回 Some(0),並透過 CredentialRefresh 實現換取過程。
重新整理返回完整替換的金鑰和過期時間,以及可選的公開 metadata(None 表示保留原值)。宿主用同一個版本 CAS 一起儲存這三項。金鑰不得放進 metadata。
CredentialRefresh::proactive_refresh_reason 決定請求時是否續期。預設在訪問憑據五分鐘內過期,或重新整理憑據(金鑰中的 refresh_expires_at_ms)一天內過期時續期。渠道可以覆寫它以匹配上游的節奏;令牌介面接受續期原因時,再實現 refresh_with_reason。response_update 用於從上游回應頭中獲取輪換後的憑證;即使不接受任何更新,也必須從回應中刪除含金鑰的回應頭。
用量不需要任何能力:宿主從渠道整形後的標準回應中讀取。不報告用量的渠道記為「未報告」,而不是 0。
descriptor 能力與狀態
有兩項 descriptor 能力無法從訪問器推斷,具備的渠道需要在自己的descriptor 中宣告:websocket(覆寫了某個 socket 操作)和 paid_usage(其 QuotaModel::allows_paid_usage 讀取憑證 metadata 中的 allow_paid_usage 標誌,控制台據此顯示對應開關)。
跨請求狀態由宿主限定在一個供應商加一把憑證的範圍內,並以 CAS 寫入。binding 的預設狀態拒絕一切寫入,所以渠道只有在宿主提供狀態時才能使用。
登入狀態(provider_state)
兩種 OAuth 流程都帶有一個 provider_state 對映,存放渠道在使用者完成授權之後還需要的資訊:start(或 authorize)放進去的內容會原樣交回給 poll(或 exchange)。內容由渠道自行決定,例如非標準的裝置控制代碼,或登入過程為自己註冊的 client。
與宿主作為憑證 metadata 公開的 provider_fields 不同,這個對映可以存放金鑰。它只儲存在宿主的登入會話中,會話生命週期短、存於快取、從不渲染,登入結束即丟棄。需要在登入之後繼續保留的資訊必須由 exchange 返回:公開資訊放進 provider_fields(會成為 metadata),機密資訊放進 provider_secrets,後者與令牌一起加密封存,不會成為 metadata。
向本倉庫新增渠道
以custom(API key)和 codex(OAuth 帳號)為參考。最近新增的 opendesign 是一個小型 API key 渠道,可以從它的提交看到新渠道涉及的全部檔案。
-
在
crates/gproxy-channel/Cargo.toml中宣告 feature,只列出該渠道需要的可選依賴。僅 wasm 需要的依賴放在cfg(target_arch = "wasm32")的 target 表下。 -
在
src/channels/mod.rs的channels!列表中加一行:feature、模組,以及宿主要註冊的值。這一行同時宣告模組並把渠道加入compiled_in(),所有宿主都透過它找到渠道。註冊值是一個表示式,需要構造的渠道寫成claudeweb::ClaudeWeb::new();一個模組在同一套線協議上服務兩個產品時,兩個值都列出(opencode::OpenCode::ZEN, opencode::OpenCode::GO)。如果渠道用到shared,還要把它的 feature 加進該模組的cfg(any(...))。 -
按「一個檔案一件事」組織模組。小渠道只有一個
acme.rs,大渠道用目錄:模組文件要寫明每一條線協議事實的來源:廠商的 API 參考,或samples/下的 CLI 原始碼。其他渠道的程式碼不能作為來源。 -
實現
BaseChannel。最少需要id、native_dialects和prepare。結構體是無狀態的單元結構體,一個例項服務該渠道下的所有供應商。 同時覆寫descriptor。預設實現會根據訪問器填好能力,但顯示名稱以及從供應商configJSON 中解碼哪些鍵,只有渠道自己知道。管理介面只依據 descriptor 渲染供應商表單(啟用tsfeature 時它也是一個 TypeScript 型別),descriptor 中沒有的鍵就無法在介面上設定。追加HOST_CONFIG_KEYS來宣告宿主自己的鍵。 當上遊需要多次交換、本地合成的應答、另一種傳輸方式,或需要把回應改寫為宣告的原生方言時,覆寫具體的操作方法,而不是prepare。 -
宣告廠商客戶端傳送的內容。如果渠道模擬某個 CLI,匯出它的請求頭常量並據此構造白名單,這樣供應商的
allowed_headers去不掉 CLI 自己的請求頭。同時把這些身份請求頭傳給轉發助手的丟棄列表,防止客戶端偽造。上游會對客戶端做指紋識別時,由default_connection返回預設連線配置;憑證或供應商上顯式指定的配置仍然優先。 -
把能力實現為獨立的型別,由訪問器返回。每個 trait 都有自己的規則:重新整理返回完整替換,不做合併;配額維度來自憑證 metadata 中的套餐資訊,不訪問網路;用量觀察者做累積快照,它的
finish由宿主告知流是完整結束還是被中斷,因為讀到 EOF 並不代表用量完整。 -
可複用的線協議機制放在
src/channels/shared/,按使用它們的 feature 條件編譯。策略留在渠道中,shared 模組只執行渠道要求的操作。 -
測試放在程式碼旁邊的
tests/<id>.rs,用#![cfg(feature = "…")]控制。手工構造供應商和憑證檢視,用測試金鑰呼叫prepare,斷言 URL、注入的鑑權和被丟棄的請求頭。把抓取的幀交給用量觀察者,解析錄製的配額請求頭和用量 body。多次呼叫覆寫用指令碼化的出站 client 測試。這裡不測試引擎。 -
把 feature 轉發到每個宿主。引擎本身不列舉渠道:
gproxy-sdk註冊compiled_in()返回的全部渠道,所以只有當二進位檔的 feature 集合啟用了渠道的 feature 時,二進位檔才能使用它。用同一個名字把 feature 加到:註冊時重複的 id 會被拒絕。供應商引用未註冊的渠道屬於配置錯誤,不會回退到其他渠道。 -
補充文件:在
crates/gproxy-channel/README.md的 feature 表中加一行,並在文件子模組guides/providers.mdx的「支援的渠道」表中加一行,四種語言都要改。
檢查構建
gproxy-channel 預設不啟用任何渠道,所以這些命令必須帶 --all-features,否則一個渠道都不會編譯。每個渠道都必須能為原生目標和 wasm32-unknown-unknown 構建,Workers 宿主因此可以帶上完整的渠道集合。lint 報告的問題要透過修改程式碼解決,不要加 #[allow]。
合入後不需要其他步驟:新渠道上的供應商會連同渠道的 descriptor 出現在 GET /admin/api/channels 中,控制台據此渲染表單。文件中的渠道參考由 CI 根據已註冊的渠道重新生成。
在本倉庫之外
渠道可以放在獨立的 crate 中,不必 fork 就能發布,兩種宿主都支援。原生二進位檔仍是原版gproxy(命令、引數、控制台和資料庫都相同),Worker 仍是原版 GPROXY Worker,只是在內建渠道之外多註冊了你的渠道。examples/channel-plugin 是一套完整可執行的三個 crate:一個檔案實現的 OpenAI 相容 API key 渠道,以及每個宿主幾行程式碼的入口。CI 每次改動都會構建這個示例,所以它與當前契約保持一致。
目錄結構
gproxy 無法編譯到 wasm,Worker 也會因此無法編譯。
main_with 就是完整的原版 main:載入 .env,解析相同的引數,建立執行時並啟動。自帶執行時的程式改為呼叫 gproxy::run_with(cli, channels)。
fetch_with 就是完整的原版 fetch handler。一個 Worker 模組只能有一個 #[event(fetch)],所以 edge crate 依賴 gproxy-host-edge 時要關閉它的 entry feature,原版 handler 正是由這個 feature 宣告的。閉包在每個 isolate 中只執行一次,即第一個請求組裝例項時。
在兩種宿主中,id 與內建渠道相同的渠道都會替換該內建渠道。要發布修改過的現有渠道,就用這種方式。
依賴
按路徑依賴一份 gproxy checkout,版本與你要構建的目標一致:glib,只有桌面宿主會用到,倉庫外的 workspace 不需要這一項。第一次構建前,把 checkout 的 Cargo.lock 複製到根 Cargo.toml 旁邊,使依賴解析到 gproxy 測試過的版本。
之所以使用 checkout 而不是 git 依賴,是因為原生控制台從 crates/gproxy-host-axum/assets/web 編譯進二進位檔,而這個目錄沒有提交到倉庫。先在 checkout 的 console/ 中執行 pnpm install && pnpm build;或者從 features 中去掉 embedded-console,構建不帶控制台的二進位檔。下面的 Worker 構建同樣從 checkout 獲取控制台。
gproxy 和 gproxy-host-edge 的 feature 列表用法與原版構建相同:channels 啟用全部內建渠道,也可以只列出部署要用的渠道。
構建 Worker
checkout 中的deploy/cloudflare 可以構建任意 Worker crate,不限於原版。用絕對路徑指向你的 crate,然後照常部署:
worker-build,去掉除錯段,再把 Worker 複製到 checkout 的 wrangler.toml、控制台資源及其 headers 檔案旁邊。邊緣部署中的內容(D1 繫結、金鑰、GPROXY_CONFIG)全部照常適用。
用於 Worker 的渠道必須能編譯到 wasm32-unknown-unknown,所有內建渠道都滿足這一點。具體來說:不使用 tokio 網路,不訪問檔案系統,用 web-time 代替 std::time::Instant,所有請求都透過宿主提供的 client 傳送。用 cargo check --target wasm32-unknown-unknown -p my-channel 檢查。
公開契約
渠道實現或接收的一切都在gproxy_channel::channel 中公開:BaseChannel、上下文與檢視型別、所有能力 trait、descriptor 型別(包括 HOST_CONFIG_KEYS),以及構造請求所需的助手:處理客戶端請求頭的 forwardable 和 HeaderAllowlist、寫入運維配置的靜態請求頭的 insert_configured、清理查詢串的 strip_query_auth,以及 standard_reason_observer。協議詞彙(Dialect、Operation、OperationKey、HttpBody、WireRequest)來自 gproxy_protocol。
gproxy_channel::channels::shared 不公開。它針對本倉庫的渠道編寫,會隨這些渠道一起變化,需要時複製過去。
契約中的規則同樣適用,尤其是準備必須是純函式、來源鑑權不會到達上游。
與內建渠道的區別
- 控制台與對待其他渠道一樣,根據 descriptor 渲染你的渠道:供應商表單來自
config_keys,憑證表單來自credential_kinds,登入按鈕來自login_modes,配額、重置、重新整理和付費用量控制元件來自capabilities。缺少的是為內建 id 單獨編寫的內容,例如定製的提示文字或模型預設組。 - 原生二進位檔的自更新會拒絕安裝。發布的版本是本倉庫的構建,裝上之後會丟掉你的渠道,下次重啟後其上的所有供應商都會失效。請基於新版本重新構建。檢查更新功能照常可用。
- 契約隨 gproxy 的版本變化。實際變更都是增量的(多數協議型別帶
#[non_exhaustive],BaseChannel新增的方法都有預設實現),但仍應基於你實際執行的版本構建。寫ChannelCapabilities { quota_query: true, ..Default::default() },不要逐個列出欄位,這樣新增能力不會導致構建失敗。
給編碼 Agent 的清單
與上面相同的步驟,按 agent 執行的順序列出。英文版本節標題為 「Checklist for Coding Agents」,倉庫的AGENTS.md 指向它。
內建渠道(向本倉庫提 PR):
- 以
crates/gproxy-channel/src/channels/nvidia.rs(API key)或codex/(OAuth)為模板,並閱讀samples/下廠商的線協議原始碼或其 API 參考。 - 在
crates/gproxy-channel/Cargo.toml中加 feature,在src/channels/mod.rs的channels!中加一行,並把 feature 加進渠道用到的src/channels/shared/中的每個cfg(any(...))。 - 編寫
src/channels/<id>.rs或src/channels/<id>/,覆寫descriptor。 - 在第 9 步列出的四處轉發 feature:
crates/gproxy-sdk/Cargo.toml(feature 和channels列表)、crates/gproxy/Cargo.toml、crates/gproxy-host-edge/Cargo.toml,以及crates/gproxy-app/src/v3/channels.rs中的V4_CHANNELS(修改陣列長度)。 - 如果渠道支援
allow_paid_usage或提供 WebSocket,在 descriptor 的 capabilities 中設定paid_usage/websocket。 - 在
crates/gproxy-channel/README.md中加一行,並在docs/guides/providers.mdx、docs/zh-tw/guides/providers.mdx、docs/zh-tw/guides/providers.mdx、docs/ru/guides/providers.mdx的「支援的渠道」表中加一行(文件改動在子模組中提交)。 - 執行
pnpm --dir docs generate:reference,把已註冊的渠道匯出到參考文件。不要提交生成的 JSON,CI 會匯出並提交。 - 執行檢查構建中的命令。
- 把
examples/channel-plugin複製到 checkout 之外:channel/必須複製,native/和edge/按要部署的宿主選擇。加上前文帶[patch.crates-io]表的 workspace 根Cargo.toml,並複製Cargo.lock。 - 在每個 crate 的
Cargo.toml中,用包自己的version和edition = "2024"替換*.workspace = true這些鍵,並把每個workspace = true依賴改為前文所示的路徑依賴。 - 重新命名包、二進位檔、結構體和
ID。除非就是要替換某個內建渠道,否則選一個內建渠道沒有用過的 id。 - 只基於
gproxy_channel::channel實現,不要使用channels::shared。 - 執行
cargo clippy --all-targets -- -D warnings;部署 Worker 時再執行cargo check --target wasm32-unknown-unknown -p <channel> -p <edge>。 - 在全新的
GPROXY_DATA_DIR上執行原生二進位檔,或按前文構建後在deploy/cloudflare中用wrangler dev --local執行 Worker,確認GET /admin/api/channels列出了這個 id。