> ## Documentation Index
> Fetch the complete documentation index at: https://gproxy.leenhawk.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Добавление канала

> Как устроен канал v4: контракт BaseChannel, необязательные трейты возможностей, шаги от Cargo-фичи до зарегистрированного канала и выпуск канала вне этого репозитория.

Это руководство для тех, кто хочет подключить к GPROXY ещё не поддерживаемый
upstream: каналом в этом репозитории или каналом в собственном крейте.

Канал отвечает за одно семейство upstream-сервисов: его URL, способ
подстановки учётных данных, поддерживаемые диалекты протокола, приведение
ответов к стандартному ответу каждой операции (именно оттуда хост читает
расход) и, для сервисов с аккаунтами, вход, обновление токенов и чтение квот.
Маршрутизация, выбор учётных данных, переключение при отказе, преобразование
протоколов, расчёты и capture относятся к движку. Канал не читает базу данных
и не выбирает транспорт.

Каналы компилируются в программу, во время работы ничего не загружается.
Канал может находиться в одном из двух мест:

* В этом репозитории, как модуль `gproxy-channel` за собственной Cargo-фичей,
  добавленный через pull request. Тогда он доступен всем хостам, включая
  Workers. См. [Добавление канала в этот репозиторий](#adding-a-channel-to-this-repository).
* В вашем крейте, как тип, реализующий `BaseChannel` по публичному контракту и
  подключённый к нативному бинарнику или Worker, которые в остальном совпадают
  со стандартными. В этом репозитории ничего менять не нужно. См.
  [Вне этого репозитория](#outside-this-repository).

Трейт и способ его реализации в обоих случаях одинаковы, различается только
регистрация. По умолчанию ни один конкретный канал не компилируется.

## Когда нужен канал

Канал придётся поддерживать вслед за чужим протоколом, поэтому добавлять его
стоит, только если строка провайдера не может описать то, что нужно
upstream-сервису: путь, зависящий от операции; тело запроса, которое надо
переписать; интерфейс аккаунта, который надо читать; клиентскую идентичность,
которую надо предъявить.

Если отличие только в адресе и одном заголовке, используйте провайдер
`custom`. См. [Провайдеры, которым не нужен канал](https://gproxy.leenhawk.com/ru/guides/providers.md#compatible-vendors).

## Контракт BaseChannel

`BaseChannel` — единственный обязательный трейт, а `id` — его единственный
обязательный метод. У каждой операции протокола есть свой асинхронный метод с
реализацией по умолчанию: собрать запрос через `prepare` (HTTP) или
`prepare_connect` (WebSocket) и отправить его через клиент, назначенный хостом.

Оба метода подготовки по умолчанию возвращают «не поддерживается». Поэтому
канал поддерживает ровно те операции, которые он подготавливает или
переопределяет.

```text theme={null}
the host selects provider, credential, client
  → ChannelBinding::new(&channel, provider, credential, client)
  → binding.send(OperationKey, WireRequest<HttpBody>)
    binding.connect(OperationKey, WireRequest<()>)
  → the named operation method on BaseChannel
  → default: prepare + client.send  /  prepare_connect + client.connect
    or the channel's own multi-call flow through the same client
```

Хост передаёт заимствованные представления, по возможности публичные:

* `ProviderView`: `id`, `channel`, необязательный `base_url` и JSON `config`,
  который канал разбирает в собственные типизированные настройки.
* `CredentialView`: `id`, `provider_id`, `auth_kind`, JSON `secret` (без
  `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, и они не
зависят друг от друга: канал может сообщать окна квот, не предлагая сброса.

| Метод доступа | Назначение |
| - | - |
| `credential_refresh` | Выдать полную замену секрета, срок действия и необязательные публичные метаданные; хост сохраняет их через compare-and-swap по `version` |
| `oauth_authorization_code` | Построить URL авторизации по PKCE challenge и state от хоста; обменять код. Переопределите `pkce_verifier_bytes`, если клиент upstream создаёт verifier не из 32 байт |
| `oauth_device_code` | `start` и один шаг `poll`. Расписание опроса задаёт хост |
| `quota_model` | Синхронно и чисто: какие измерения квоты есть у учётных данных, по их `auth_kind` и метаданным |
| `quota_query` | Прочитать снимок квоты из эндпоинта расхода upstream |
| `quota_headers` | Превратить заголовки ответа в записи квот; пустой результат означает, что ничего не сообщено |
| `quota_reset` | Кредиты и ручной сброс у upstream, которые их продают |
| `usage_extras` | Собственные поля расхода поставщика, читаемые рядом со стандартным расходом, который хост уже взял из сформированного ответа |
| `response_reason_observer` | Классифицировать сырой ответ upstream (ограничение частоты, перегрузка, отказ и т. п.). По умолчанию разбираются стандартные JSON и SSE; переопределять нужно только для закрытого формата |
| `websocket_response_reason` | То же для одного полного входящего сообщения WebSocket |
| `services` | Маршруты плоскости управления поставщика; см. [CLI-клиенты](https://gproxy.leenhawk.com/ru/guides/cli-clients.md) |

`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-ключом; по его
коммиту видны все файлы, которые затрагивает новый канал.

1. Объявите фичу в `crates/gproxy-channel/Cargo.toml`, перечислив только те
   необязательные зависимости, которые нужны каналу. Зависимость только для
   wasm указывается в целевой таблице `cfg(target_arch = "wasm32")`.

   ```toml theme={null}
   [features]
   # One-line description of the upstream and what the channel covers.
   acme = ["dep:base64", "dep:web-time"]
   ```

2. Добавьте строку в список `channels!` в `src/channels/mod.rs`: фичу, модуль
   и значение, которое регистрирует хост. Эта строка одновременно объявляет
   модуль и добавляет канал в `compiled_in()`, через который его находят все
   хосты.

   ```rust theme={null}
   "acme" => acme, acme::Acme;
   ```

   Значение — это выражение, поэтому канал, который нужно сконструировать,
   записывается как `claudeweb::ClaudeWeb::new()`, а модуль, обслуживающий два
   продукта по одному протоколу, перечисляет оба
   (`opencode::OpenCode::ZEN, opencode::OpenCode::GO`). Если канал использует
   `shared`, добавьте его фичу и в `cfg(any(...))` этого модуля.

3. Разложите модуль по файлам, по одной задаче в каждом. Небольшой канал —
   один файл `acme.rs`, крупный — каталог:

   ```text theme={null}
   src/channels/acme/
     mod.rs        the id, the unit struct, re-exports, the module doc
     config.rs     the provider `config` JSON, serde(default), unknown keys ignored
     request.rs    prepare / prepare_connect / overridden operations
     oauth.rs      the login and refresh abilities
     quota.rs      the quota abilities
     usage.rs      the usage abilities
     services.rs   the vendor control-plane routes
   ```

   Документация модуля называет источник каждого факта о протоколе:
   справочник API поставщика или исходники CLI в `samples/`. Код другого
   канала источником не считается.

4. Реализуйте `BaseChannel`. Минимум — `id`, `native_dialects` и `prepare`.
   Структура — единичная и без состояния, так что один экземпляр обслуживает
   все провайдеры этого канала.

   Переопределите и `descriptor`. Реализация по умолчанию заполняет
   возможности по методам доступа, но отображаемое имя и ключи, которые канал
   читает из JSON `config` провайдера, известны только самому каналу.
   Интерфейс управления строит форму провайдера только по дескриптору (с фичей
   `ts` это ещё и тип TypeScript), так что ключ, которого в нём нет, из
   интерфейса задать нельзя. Добавьте `HOST_CONFIG_KEYS`, чтобы объявить ключи
   самого хоста.

   Переопределяйте метод операции вместо `prepare`, если upstream требует
   нескольких обменов, локально сформированного ответа, другого транспорта или
   переписывания ответа в объявленный нативный диалект.

5. Объявите, что отправляет клиент поставщика. Если канал имитирует CLI,
   экспортируйте константу с его заголовками и постройте из неё список
   разрешённых, чтобы `allowed_headers` провайдера не мог убрать собственные
   заголовки CLI. Передайте эти заголовки идентичности в список отбрасываемых
   у функции пересылки, чтобы клиент не мог их подделать. Если upstream
   распознаёт клиентов по отпечатку, верните подключение по умолчанию из
   `default_connection`; явно заданный профиль у учётных данных или провайдера
   всё равно имеет приоритет.

6. Реализуйте возможности отдельными типами и возвращайте их из методов
   доступа. У каждого трейта свои правила: обновление возвращает полную замену,
   а не слияние; измерения квоты берутся из сведений о тарифе в метаданных
   учётных данных, а не из сети; наблюдатель расхода делает накопительные
   снимки, а его `finish` получает от хоста признак, завершился ли поток
   полностью или был прерван, потому что достижение EOF само по себе не
   означает, что расход полный.

7. Переиспользуемые механизмы протокола помещайте в `src/channels/shared/` под
   условием фич, которые их используют. Политика остаётся в канале; общий
   модуль делает только то, что просит канал.

8. Тесты размещайте рядом с кодом, в `tests/<id>.rs` под
   `#![cfg(feature = "…")]`. Соберите представления провайдера и учётных
   данных вручную, вызовите `prepare` с тестовым секретом и проверьте URL,
   добавленную аутентификацию и отброшенные заголовки. Подайте записанные
   кадры наблюдателю расхода; разберите записанные заголовки квот и тела с
   расходом. Для многошаговых операций используйте сценарный исходящий клиент.
   Движок из этих тестов не тестируйте.

9. Пробросьте фичу во все хосты. Движок сам каналы не перечисляет:
   `gproxy-sdk` регистрирует всё, что возвращает `compiled_in()`, поэтому
   бинарник может использовать канал, только если его набор фич включает фичу
   канала. Добавьте фичу с тем же именем в:

   | Файл | Изменение |
   | - | - |
   | `crates/gproxy-sdk/Cargo.toml` | `acme = ["gproxy-channel/acme"]`, а также `"acme"` в список `channels` |
   | `crates/gproxy/Cargo.toml` | `acme = ["gproxy-sdk/acme"]` |
   | `crates/gproxy-host-edge/Cargo.toml` | `acme = ["gproxy-sdk/acme"]` |
   | `crates/gproxy-app/src/v3/channels.rs` | id в `V4_CHANNELS` (и длину массива), чтобы импорт v3 его принимал |

   Повторяющийся id отклоняется при регистрации. Провайдер, ссылающийся на
   незарегистрированный канал, — ошибка конфигурации; запасного варианта нет.

10. Опишите канал в документации: добавьте строку в таблицу фич в
    `crates/gproxy-channel/README.md` и в таблицу «Поддерживаемые каналы» в
    `guides/providers.mdx` подмодуля документации, на всех четырёх языках.

### Проверка сборки

```sh theme={null}
cargo fmt --all
cargo clippy -p gproxy-channel --all-features --all-targets -- -D warnings
cargo clippy -p gproxy-channel --all-features --target wasm32-unknown-unknown --lib -- -D warnings
cargo test   -p gproxy-channel --all-features
```

В `gproxy-channel` по умолчанию не включён ни один канал, поэтому этим
командам нужен `--all-features`; без него не скомпилируется ни один канал.
Каждый канал должен собираться и для нативных целей, и для
`wasm32-unknown-unknown`, благодаря чему хост Workers несёт полный набор
каналов. Замечания линтера исправляйте в коде, а не через `#[allow]`.

После слияния больше ничего не требуется: провайдеры нового канала появляются
в `GET /admin/api/channels` вместе с дескриптором канала, и консоль строит по
нему формы. Справочник каналов в этой документации CI пересобирает из
зарегистрированных каналов.

## Вне этого репозитория

Канал может жить в собственном крейте и выпускаться без форка, для любого из
двух хостов. Нативный бинарник при этом остаётся стандартным `gproxy` (те же
команды, флаги, консоль и база данных), а Worker — стандартным GPROXY Worker;
в каждом ваш канал зарегистрирован рядом со встроенными.
`examples/channel-plugin` содержит полный рабочий набор из трёх крейтов: канал с
API-ключом, совместимый с OpenAI, в одном файле, и короткую точку входа для
каждого хоста. CI собирает пример при каждом изменении, так что он
соответствует текущему контракту.

### Структура проекта

```text theme={null}
my-gproxy/
  channel/    the channel: a unit struct implementing BaseChannel, no host
  native/     the gproxy binary, carrying it
  edge/       the Cloudflare Worker, carrying it (only if you deploy there)
```

Канал вынесен в отдельный крейт, чтобы от него могли зависеть оба хоста. Сам
он не должен зависеть от нативного хоста: `gproxy` не собирается под wasm, и
тогда не собрался бы и Worker.

```rust theme={null}
// native/src/main.rs
use std::{process::ExitCode, sync::Arc};

fn main() -> ExitCode {
    gproxy::main_with(vec![Arc::new(my_channel::Acme)])
}
```

`main_with` — это весь стандартный `main`: загружает `.env`, разбирает те же
аргументы, создаёт среду выполнения и запускается. Программа со своей средой
выполнения вызывает вместо него `gproxy::run_with(cli, channels)`.

```rust theme={null}
// edge/src/lib.rs
#[cfg(target_arch = "wasm32")]
mod entry {
    use std::sync::Arc;

    use worker::{Context, Env, HttpRequest, event};

    #[event(fetch)]
    async fn fetch(
        request: HttpRequest,
        env: Env,
        ctx: Context,
    ) -> worker::Result<gproxy_host_edge::Response> {
        gproxy_host_edge::fetch_with(request, env, ctx, || vec![Arc::new(my_channel::Acme)])
            .await
    }
}
```

`fetch_with` — это весь стандартный обработчик fetch. В модуле Worker может
быть только один `#[event(fetch)]`, поэтому крейт edge зависит от
`gproxy-host-edge` без фичи `entry`, которая и объявляет стандартный
обработчик. Замыкание выполняется один раз на isolate, когда первый запрос
собирает экземпляр.

В обоих хостах канал, чей `id` совпадает со встроенным, заменяет встроенный.
Так выпускают исправленную копию существующего канала.

### Зависимости

Зависьте от рабочей копии gproxy по пути, той версии, под которую собираете:

```toml theme={null}
# channel/Cargo.toml
[dependencies]
gproxy-channel = { path = "../../gproxy/crates/gproxy-channel" }
gproxy-protocol = { path = "../../gproxy/crates/gproxy-protocol", features = ["exhaustive"] }
http = "1"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
```

```toml theme={null}
# native/Cargo.toml
[dependencies]
my-channel = { path = "../channel" }
gproxy = { path = "../../gproxy/crates/gproxy", features = ["channels", "memory", "fs", "embedded-console"] }
```

```toml theme={null}
# edge/Cargo.toml
[lib]
crate-type = ["cdylib", "rlib"]

[target.'cfg(target_arch = "wasm32")'.dependencies]
my-channel = { path = "../channel" }
gproxy-host-edge = { path = "../../gproxy/crates/gproxy-host-edge", default-features = false, features = ["channels", "d1", "libsql"] }
worker = { version = "0.8", default-features = false, features = ["http"] }
```

```toml theme={null}
# Cargo.toml, the workspace root
[workspace]
resolver = "3"
members = ["channel", "native", "edge"]

# Copied from the checkout's root Cargo.toml: a patch applies only in the
# workspace that declares it, and the build depends on these.
[patch.crates-io]
sqlx-sqlite = { path = "../gproxy/vendor/sqlx-sqlite" }
btls-sys = { git = "https://github.com/0x676e67/btls", rev = "609f955e6de4df4a215ee9b273d327af1c0771db" }
```

В таблице самой рабочей копии есть ещё патч `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, а не только
стандартный. Укажите на свой крейт абсолютным путём и разверните как обычно:

```sh theme={null}
cd ../gproxy/deploy/cloudflare
pnpm install
GPROXY_EDGE_CRATE=/absolute/path/to/my-gproxy/edge pnpm run build
pnpm run deploy
```

Команда запускает `worker-build` в вашем крейте, удаляет отладочные секции и
копирует Worker к `wrangler.toml` рабочей копии, ресурсам консоли и файлу их
заголовков. Всё из [руководства по edge-развёртыванию](https://gproxy.leenhawk.com/ru/deployment/edge.md)
(привязка 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` не публичен. Он подогнан под каналы этого
репозитория и меняется вместе с ними; копируйте нужное.

[Правила контракта](#rules-for-preparation-and-errors) действуют без
изменений, в первую очередь чистота подготовки и то, что аутентификация
источника не доходит до 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 в этот репозиторий):

1. Возьмите за образец `crates/gproxy-channel/src/channels/nvidia.rs`
   (API-ключ) или `codex/` (OAuth) и прочитайте источник протокола
   поставщика в `samples/` или его справочник API.
2. Добавьте фичу в `crates/gproxy-channel/Cargo.toml`, строку в `channels!` в
   `src/channels/mod.rs` и фичу в каждый `cfg(any(...))` в
   `src/channels/shared/`, который использует канал.
3. Напишите `src/channels/<id>.rs` или `src/channels/<id>/`; переопределите
   `descriptor`.
4. Пробросьте фичу в четыре места из шага 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` (увеличьте длину массива).
5. Если канал учитывает `allow_paid_usage` или обслуживает WebSocket,
   установите `paid_usage` / `websocket` в возможностях его дескриптора.
6. Добавьте строку канала в `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` (изменение документации коммитится в
   подмодуле).
7. Выполните `pnpm --dir docs generate:reference`; он экспортирует
   зарегистрированные каналы в справочник. Сгенерированный JSON не
   коммитьте: его экспортирует и коммитит CI.
8. Выполните команды из раздела [Проверка сборки](#checking-the-build).

Канал вне репозитория:

1. Скопируйте `examples/channel-plugin` за пределы рабочей копии: `channel/`
   всегда, `native/` и `edge/` — для нужных хостов. Добавьте корневой
   `Cargo.toml` workspace, показанный выше, с таблицей `[patch.crates-io]`, и
   скопируйте `Cargo.lock`.
2. В `Cargo.toml` каждого крейта задайте собственные `version` и
   `edition = "2024"` вместо ключей `*.workspace = true` и замените каждую
   зависимость `workspace = true` зависимостью по пути, как показано выше.
3. Переименуйте пакеты, бинарник, структуру и `ID`. Выберите id, который не
   использует ни один встроенный канал, если только цель не в том, чтобы его
   заменить.
4. Реализуйте только через `gproxy_channel::channel`; не используйте
   `channels::shared`.
5. Выполните `cargo clippy --all-targets -- -D warnings`; для Worker также
   `cargo check --target wasm32-unknown-unknown -p <channel> -p <edge>`.
6. Запустите нативный бинарник на новом `GPROXY_DATA_DIR` или Worker через
   `wrangler dev --local` из `deploy/cloudflare` после описанной выше сборки и
   убедитесь, что `GET /admin/api/channels` содержит этот id.


## Related topics

- [Сборка из исходного кода](https://gproxy.leenhawk.com/ru/development/building.md)
- [GPROXY](https://gproxy.leenhawk.com/ru/index.md)
- [Провайдеры и учётные данные](https://gproxy.leenhawk.com/ru/guides/providers.md)
- [Каталог каналов](https://gproxy.leenhawk.com/ru/reference/channel-catalog.md)
- [Модели и маршруты](https://gproxy.leenhawk.com/ru/guides/models.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.