- В этом репозитории, как модуль
gproxy-channelза собственной Cargo-фичей, добавленный через pull request. Тогда он доступен всем хостам, включая Workers. См. Добавление канала в этот репозиторий. - В вашем крейте, как тип, реализующий
BaseChannelпо публичному контракту и подключённый к нативному бинарнику или Worker, которые в остальном совпадают со стандартными. В этом репозитории ничего менять не нужно. См. Вне этого репозитория.
Когда нужен канал
Канал придётся поддерживать вслед за чужим протоколом, поэтому добавлять его стоит, только если строка провайдера не может описать то, что нужно upstream-сервису: путь, зависящий от операции; тело запроса, которое надо переписать; интерфейс аккаунта, который надо читать; клиентскую идентичность, которую надо предъявить. Если отличие только в адресе и одном заголовке, используйте провайдерcustom. См. Провайдеры, которым не нужен канал.
Контракт BaseChannel
BaseChannel — единственный обязательный трейт, а id — его единственный
обязательный метод. У каждой операции протокола есть свой асинхронный метод с
реализацией по умолчанию: собрать запрос через prepare (HTTP) или
prepare_connect (WebSocket) и отправить его через клиент, назначенный хостом.
Оба метода подготовки по умолчанию возвращают «не поддерживается». Поэтому
канал поддерживает ровно те операции, которые он подготавливает или
переопределяет.
ProviderView:id,channel, необязательныйbase_urlи JSONconfig, который канал разбирает в собственные типизированные настройки.CredentialView:id,provider_id,auth_kind, JSONsecret(безDebugиSerialize), публичныеmetadata, записанные хостом,versionиexpires_at_ms.PrepareContext: аргументprepareиprepare_connect— оба представления, нативныйOperationKey,WireRequestи необязательное переопределение адреса.OperationContext: аргумент метода операции — оба представления, диалект операции,WireRequest, принадлежащий ему клиент, состояние между запросами, идентификатор экземпляра хоста и необязательное переопределение адреса.CredentialContext: провайдер, учётные данные и клиент для обновления, квот и сервисов.
prepare, канал обычно переопределяет native_dialects (диалекты,
которые upstream принимает для операции; пустой список означает, что решает
конфигурация), а если он принимает запросы и в других диалектах, то и
default_conversion_target: он указывает, в какой диалект хост преобразует
запрос до вызова prepare.
Правила подготовки и ошибок
- Подготовка синхронна и чиста: без ввода-вывода, скрытого состояния и
создания клиентов. Переопределённая операция может выполнить несколько
обменов, но только через
context.client. Она может удерживать клиент и после окончания потока ответа, чтобы завершить работу, например освободить диалог на стороне поставщика. - Аутентификация источника не доходит до upstream. Вспомогательная функция
пересылки отбрасывает hop-by-hop-заголовки, а также
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 метода. Оно заменяет и базовый URL, и путь канала по умолчанию; к нему ничего не дописывается.
- Ответ с кодом не 2xx возвращается как ответ: со статусом, заголовками и лениво читаемым телом. Вариант ошибки предназначен для вызовов возможностей (вход, обновление, квоты) и для промежуточного обмена внутри многошаговой операции, где неудачный ответ не является возвращаемым.
RefreshRejectedозначает, что upstream окончательно отклонил учётные данные (invalid_grant, отзыв). По умолчанию хост помечает их как нерабочие. Канал может переопределитьpersist_rejection, чтобы причина отказа запоминалась только для текущей версии учётных данных в памяти, а перед следующей попыткой долговременные данные перечитывались. Временный сбой транспорта или 5xx нужно возвращать как ошибку транспорта, чтобы хост повторил попытку позже;RefreshRejectedв этом случае выведет из строя рабочий аккаунт.ContinuationElsewhereозначает, что активное соединение с upstream держит другой процесс хоста. Хост перенаправит запрос туда; с учётными данными всё в порядке.
Необязательные возможности
Каждая возможность — отдельный трейт за методом доступа, который по умолчанию возвращаетNone. Канал реализует те, что есть у его upstream, и они не
зависят друг от друга: канал может сообщать окна квот, не предлагая сброса.
classify_rejection — обычный метод BaseChannel, а не метод доступа. Хост
вызывает его для каждого ответа не 2xx, передавая ограниченный префикс тела, а
также для кадра ошибки в начале потока 2xx. Метод сообщает, что отказ значит для
учётных данных (ограничение частоты, исчерпанная квота, нет средств, нет
аутентификации, модель не обслуживается или ошибка в самом запросе) и, если
upstream это указал, сколько ждать. Реализация по умолчанию читает только
структурированные поля ошибки; переопределите её, чтобы распознавать
формулировки конкретного поставщика.
Учётные данные и обновление
Секреты, которые у оператора уже есть, проходят через обычный API создания и изменения учётных данных. Проверяйте их формат вvalidate_credential_secret.
Если перед первым использованием нужен сетевой обмен (например, ключ
сервисного аккаунта ещё не обменян на токен), верните Some(0) из
initial_credential_expiry и реализуйте обмен через CredentialRefresh.
Обновление возвращает полную замену секрета и срока действия, а также
необязательные публичные метаданные (None сохраняет прежние). Хост сохраняет
все три значения одним compare-and-swap по версии. Секретам не место в
метаданных.
CredentialRefresh::proactive_refresh_reason решает, обновлять ли учётные
данные при запросе. По умолчанию обновление происходит, если токен доступа
истекает в течение пяти минут или токен обновления (refresh_expires_at_ms в
секрете) — в течение суток. Канал может переопределить это под расписание
upstream и реализовать refresh_with_reason, если эндпоинт токенов принимает
причину обновления. response_update позволяет каналу взять обновлённые
учётные данные из заголовка ответа upstream; секретные заголовки он должен
удалить из ответа, даже если обновление не принято.
Для расхода возможность не нужна: хост читает его из стандартного ответа,
который формирует канал. Если канал ничего не сообщает, расход считается
неизвестным, а не нулевым.
Возможности дескриптора и состояние
Две возможности дескриптора нельзя вывести из методов доступа, поэтому канал, у которого они есть, указывает их в своёмdescriptor: websocket (он
переопределяет операцию через сокет) и paid_usage (его
QuotaModel::allows_paid_usage читает флаг allow_paid_usage в метаданных
учётных данных; по нему консоль показывает соответствующий переключатель).
Состояние между запросами хост ограничивает одним провайдером и одними
учётными данными и записывает через compare-and-swap. Состояние привязки по
умолчанию отклоняет любую запись, так что канал получает состояние, только если
его предоставил хост.
Состояние входа (provider_state)
Оба потока OAuth передают словарь provider_state для сведений, которые нужны
каналу после того, как пользователь дал согласие: то, что положил start (или
authorize), без изменений возвращается в poll (или exchange).
Содержимое определяет канал, например нестандартный идентификатор устройства
или клиент, который вход зарегистрировал для себя.
В отличие от provider_fields, которые хост публикует как метаданные учётных
данных, этот словарь может содержать секреты. Он живёт только в сеансе входа
на хосте, который недолговечен, хранится в кеше и никогда не отображается, и
удаляется по окончании входа. Всё, что должно пережить вход, нужно вернуть из
обмена: публичные сведения в provider_fields (они станут метаданными),
секретные — в provider_secrets, которые шифруются вместе с токенами и
метаданными не становятся.
Добавление канала в этот репозиторий
Ориентируйтесь наcustom (API-ключ) и codex (аккаунт OAuth). Последний
добавленный канал, opendesign, — небольшой канал с API-ключом; по его
коммиту видны все файлы, которые затрагивает новый канал.
-
Объявите фичу в
crates/gproxy-channel/Cargo.toml, перечислив только те необязательные зависимости, которые нужны каналу. Зависимость только для wasm указывается в целевой таблицеcfg(target_arch = "wasm32"). -
Добавьте строку в список
channels!вsrc/channels/mod.rs: фичу, модуль и значение, которое регистрирует хост. Эта строка одновременно объявляет модуль и добавляет канал вcompiled_in(), через который его находят все хосты.Значение — это выражение, поэтому канал, который нужно сконструировать, записывается какclaudeweb::ClaudeWeb::new(), а модуль, обслуживающий два продукта по одному протоколу, перечисляет оба (opencode::OpenCode::ZEN, opencode::OpenCode::GO). Если канал используетshared, добавьте его фичу и вcfg(any(...))этого модуля. -
Разложите модуль по файлам, по одной задаче в каждом. Небольшой канал —
один файл
acme.rs, крупный — каталог:Документация модуля называет источник каждого факта о протоколе: справочник API поставщика или исходники CLI вsamples/. Код другого канала источником не считается. -
Реализуйте
BaseChannel. Минимум —id,native_dialectsиprepare. Структура — единичная и без состояния, так что один экземпляр обслуживает все провайдеры этого канала. Переопределите иdescriptor. Реализация по умолчанию заполняет возможности по методам доступа, но отображаемое имя и ключи, которые канал читает из JSONconfigпровайдера, известны только самому каналу. Интерфейс управления строит форму провайдера только по дескриптору (с фичейtsэто ещё и тип TypeScript), так что ключ, которого в нём нет, из интерфейса задать нельзя. ДобавьтеHOST_CONFIG_KEYS, чтобы объявить ключи самого хоста. Переопределяйте метод операции вместоprepare, если upstream требует нескольких обменов, локально сформированного ответа, другого транспорта или переписывания ответа в объявленный нативный диалект. -
Объявите, что отправляет клиент поставщика. Если канал имитирует CLI,
экспортируйте константу с его заголовками и постройте из неё список
разрешённых, чтобы
allowed_headersпровайдера не мог убрать собственные заголовки CLI. Передайте эти заголовки идентичности в список отбрасываемых у функции пересылки, чтобы клиент не мог их подделать. Если upstream распознаёт клиентов по отпечатку, верните подключение по умолчанию изdefault_connection; явно заданный профиль у учётных данных или провайдера всё равно имеет приоритет. -
Реализуйте возможности отдельными типами и возвращайте их из методов
доступа. У каждого трейта свои правила: обновление возвращает полную замену,
а не слияние; измерения квоты берутся из сведений о тарифе в метаданных
учётных данных, а не из сети; наблюдатель расхода делает накопительные
снимки, а его
finishполучает от хоста признак, завершился ли поток полностью или был прерван, потому что достижение EOF само по себе не означает, что расход полный. -
Переиспользуемые механизмы протокола помещайте в
src/channels/shared/под условием фич, которые их используют. Политика остаётся в канале; общий модуль делает только то, что просит канал. -
Тесты размещайте рядом с кодом, в
tests/<id>.rsпод#![cfg(feature = "…")]. Соберите представления провайдера и учётных данных вручную, вызовитеprepareс тестовым секретом и проверьте URL, добавленную аутентификацию и отброшенные заголовки. Подайте записанные кадры наблюдателю расхода; разберите записанные заголовки квот и тела с расходом. Для многошаговых операций используйте сценарный исходящий клиент. Движок из этих тестов не тестируйте. -
Пробросьте фичу во все хосты. Движок сам каналы не перечисляет:
gproxy-sdkрегистрирует всё, что возвращаетcompiled_in(), поэтому бинарник может использовать канал, только если его набор фич включает фичу канала. Добавьте фичу с тем же именем в:Повторяющийся id отклоняется при регистрации. Провайдер, ссылающийся на незарегистрированный канал, — ошибка конфигурации; запасного варианта нет. -
Опишите канал в документации: добавьте строку в таблицу фич в
crates/gproxy-channel/README.mdи в таблицу «Поддерживаемые каналы» вguides/providers.mdxподмодуля документации, на всех четырёх языках.
Проверка сборки
gproxy-channel по умолчанию не включён ни один канал, поэтому этим
командам нужен --all-features; без него не скомпилируется ни один канал.
Каждый канал должен собираться и для нативных целей, и для
wasm32-unknown-unknown, благодаря чему хост Workers несёт полный набор
каналов. Замечания линтера исправляйте в коде, а не через #[allow].
После слияния больше ничего не требуется: провайдеры нового канала появляются
в GET /admin/api/channels вместе с дескриптором канала, и консоль строит по
нему формы. Справочник каналов в этой документации CI пересобирает из
зарегистрированных каналов.
Вне этого репозитория
Канал может жить в собственном крейте и выпускаться без форка, для любого из двух хостов. Нативный бинарник при этом остаётся стандартнымgproxy (те же
команды, флаги, консоль и база данных), а Worker — стандартным GPROXY Worker;
в каждом ваш канал зарегистрирован рядом со встроенными.
examples/channel-plugin содержит полный рабочий набор из трёх крейтов: канал с
API-ключом, совместимый с OpenAI, в одном файле, и короткую точку входа для
каждого хоста. CI собирает пример при каждом изменении, так что он
соответствует текущему контракту.
Структура проекта
gproxy не собирается под wasm, и
тогда не собрался бы и Worker.
main_with — это весь стандартный main: загружает .env, разбирает те же
аргументы, создаёт среду выполнения и запускается. Программа со своей средой
выполнения вызывает вместо него gproxy::run_with(cli, channels).
fetch_with — это весь стандартный обработчик fetch. В модуле Worker может
быть только один #[event(fetch)], поэтому крейт edge зависит от
gproxy-host-edge без фичи entry, которая и объявляет стандартный
обработчик. Замыкание выполняется один раз на isolate, когда первый запрос
собирает экземпляр.
В обоих хостах канал, чей id совпадает со встроенным, заменяет встроенный.
Так выпускают исправленную копию существующего канала.
Зависимости
Зависьте от рабочей копии gproxy по пути, той версии, под которую собираете:glib; он нужен только
настольному хосту, и внешнему workspace эта запись не требуется. Перед первой
сборкой скопируйте Cargo.lock рабочей копии рядом с корневым Cargo.toml,
чтобы зависимости разрешились в версии, с которыми тестировался gproxy.
Рабочая копия используется вместо git-зависимости потому, что нативная
консоль встраивается в бинарник из crates/gproxy-host-axum/assets/web, а этот
каталог не хранится в репозитории. Сначала выполните pnpm install && pnpm build
в console/ рабочей копии или уберите embedded-console из фич, чтобы собрать
бинарник без консоли. Сборка Worker ниже тоже берёт консоль из рабочей копии.
Списки фич gproxy и gproxy-host-edge работают так же, как в стандартных
сборках: channels включает все встроенные каналы, а можно перечислить только
те, что нужны развёртыванию.
Сборка Worker
deploy/cloudflare из рабочей копии собирает любой крейт Worker, а не только
стандартный. Укажите на свой крейт абсолютным путём и разверните как обычно:
worker-build в вашем крейте, удаляет отладочные секции и
копирует Worker к wrangler.toml рабочей копии, ресурсам консоли и файлу их
заголовков. Всё из руководства по edge-развёртыванию
(привязка D1, секреты, GPROXY_CONFIG) применяется без изменений.
Канал для Worker должен собираться под wasm32-unknown-unknown, как и все
встроенные каналы. На практике это значит: без сетевого tokio, без доступа к
файловой системе, web-time вместо std::time::Instant, и все запросы — через
клиент, предоставленный хостом. Проверка:
cargo check --target wasm32-unknown-unknown -p my-channel.
Публичный контракт
Всё, что канал реализует или получает, публично вgproxy_channel::channel:
BaseChannel, типы контекстов и представлений, все трейты возможностей, типы
дескриптора (включая HOST_CONFIG_KEYS) и вспомогательные функции для
запросов: forwardable и HeaderAllowlist для клиентских заголовков,
insert_configured для статического заголовка, заданного оператором,
strip_query_auth для строки запроса и standard_reason_observer. Словарь
протокола (Dialect, Operation, OperationKey, HttpBody, WireRequest)
находится в gproxy_protocol.
gproxy_channel::channels::shared не публичен. Он подогнан под каналы этого
репозитория и меняется вместе с ними; копируйте нужное.
Правила контракта действуют без
изменений, в первую очередь чистота подготовки и то, что аутентификация
источника не доходит до upstream.
Отличия от встроенных каналов
- Консоль строит интерфейс вашего канала по дескриптору, как и для любого
другого: форму провайдера по
config_keys, форму учётных данных поcredential_kinds, кнопки входа поlogin_modes, элементы квот, сброса, обновления и платного расхода поcapabilities. Нет только доработок, написанных для встроенных id, например особого текста подсказки или группы предустановленных моделей. - Самообновление в нативном бинарнике отказывается устанавливать новую версию. Опубликованный релиз — это сборка этого репозитория; после установки ваш канал пропал бы, и все провайдеры на нём перестали бы работать после следующего перезапуска. Пересоберите бинарник под новую версию. Проверка обновлений работает.
- Контракт следует версии gproxy. На практике изменения только добавляются
(большинство типов протокола помечены
#[non_exhaustive], а новые методыBaseChannelимеют реализации по умолчанию), но собирайте под ту версию, которую запускаете. ПишитеChannelCapabilities { quota_query: true, ..Default::default() }, а не перечисляйте все поля, чтобы новая возможность не сломала сборку.
Контрольный список для агентов
Те же шаги в порядке, в котором их должен выполнять агент. В английской версии этот раздел называется «Checklist for Coding Agents», и на него ссылаетсяAGENTS.md репозитория.
Встроенный канал (pull request в этот репозиторий):
- Возьмите за образец
crates/gproxy-channel/src/channels/nvidia.rs(API-ключ) илиcodex/(OAuth) и прочитайте источник протокола поставщика вsamples/или его справочник API. - Добавьте фичу в
crates/gproxy-channel/Cargo.toml, строку вchannels!вsrc/channels/mod.rsи фичу в каждыйcfg(any(...))вsrc/channels/shared/, который использует канал. - Напишите
src/channels/<id>.rsилиsrc/channels/<id>/; переопределитеdescriptor. - Пробросьте фичу в четыре места из шага 9:
crates/gproxy-sdk/Cargo.toml(фича и списокchannels),crates/gproxy/Cargo.toml,crates/gproxy-host-edge/Cargo.tomlиV4_CHANNELSвcrates/gproxy-app/src/v3/channels.rs(увеличьте длину массива). - Если канал учитывает
allow_paid_usageили обслуживает WebSocket, установитеpaid_usage/websocketв возможностях его дескриптора. - Добавьте строку канала в
crates/gproxy-channel/README.mdи в таблицу поддерживаемых каналов вdocs/guides/providers.mdx,docs/zh-cn/guides/providers.mdx,docs/zh-tw/guides/providers.mdxиdocs/ru/guides/providers.mdx(изменение документации коммитится в подмодуле). - Выполните
pnpm --dir docs generate:reference; он экспортирует зарегистрированные каналы в справочник. Сгенерированный JSON не коммитьте: его экспортирует и коммитит CI. - Выполните команды из раздела Проверка сборки.
- Скопируйте
examples/channel-pluginза пределы рабочей копии:channel/всегда,native/иedge/— для нужных хостов. Добавьте корневойCargo.tomlworkspace, показанный выше, с таблицей[patch.crates-io], и скопируйтеCargo.lock. - В
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или Worker черезwrangler dev --localизdeploy/cloudflareпосле описанной выше сборки и убедитесь, чтоGET /admin/api/channelsсодержит этот id.