> ## 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.

# Встраивание SDK

> Крейт gproxy-sdk для запуска шлюза GPROXY внутри программы на Rust: сборка, features, вызовы, вход, запись конфигурации, запросы, синхронизация и экспорт типов TypeScript.

`gproxy-sdk` — крейт, на котором построен сам шлюз GPROXY. Подключайте его
напрямую, если программе на Rust нужно отправлять запросы через пул учётных
данных с автоматическим обновлением, переключением между провайдерами и учётом
расходов, но без HTTP-сервера.

Дескриптору нужна база данных: состояние учётных данных нельзя хранить только в
памяти. Например, Claude меняет refresh token при каждом обновлении, и
дескриптор, державший учётные данные в памяти, потерял бы рабочие учётные
данные при первом обновлении после перезапуска.

## Что входит в крейт

sdk содержит сборку, запись конфигурации, вход, разрешение моделей, вызовы,
запросы к данным и синхронизацию. В нём нет:

* Идентификации. Пользователи, API-ключи, организации, команды, права,
  подписки, лимиты запросов и OAuth issuer принадлежат `gproxy-app`, который
  читает ту же устойчивую ревизию из того же пакета.
* Аутентификации нижестоящих клиентов. sdk не решает, кто вызывающий; его права
  передаются извне.
* Сервера: нет слушающего сокета, маршрутизатора, middleware и CLI.
* Консоли. Крейт генерирует типы TypeScript, по которым пишется UI; сам UI
  остаётся за приложением.
* Выполнения. Попытки, преобразование, перезапись, наблюдение, бюджеты и расчёт
  выполняет движок (`gproxy-core`); sdk решает только, что ему передать.

Место sdk между движком и app описано в разделе
[Архитектура](https://gproxy.leenhawk.com/ru/development/architecture.md).

## Подключение зависимости

Подключите `gproxy-sdk` через Git-тег или зависимость по пути:

```toml theme={null}
[dependencies]
gproxy-sdk = { git = "https://github.com/LeenHawk/gproxy", tag = "v4.0.0" }
```

В примере закреплён тег `v4.0.0`; укажите тег того выпуска, с которым
работаете. Workspace использует Rust edition 2024. Rust API и API управления в
v4 отличаются от v3.

`gproxy-sdk` распространяется под AGPL-3.0-or-later и на crates.io не
публикуется. Библиотеки под MIT (`gproxy-protocol`, `gproxy-client`,
`gproxy-cache`, `gproxy-seaorm`, `gproxy-tokenizer`, `gproxy-file`,
`gproxy-client-sdk`, `gproxy-store` и другие) публикуются там отдельно.

## Сборка дескриптора

```rust theme={null}
use gproxy_sdk::{GproxyBuilder, SyncMode};

let gproxy = GproxyBuilder::sqlite("gproxy.db")
    .await?
    .master_key([0u8; 32])
    .sync_mode(SyncMode::Background)
    .build()
    .await?;

for channel in gproxy.channels() {
    println!("{} ({})", channel.display_name, channel.id);
}
println!("serving revision {}", gproxy.revision().0);
```

Построитель создаётся через `GproxyBuilder::sqlite(path)`,
`GproxyBuilder::sqlite_memory()`, `GproxyBuilder::database_url(url)` или
`GproxyBuilder::connection(connection)` для соединения, открытого вами.

`build()` не открывает ничего, что ему не передали. Он синхронизирует схему
сущностей (если не задано `.sync_schema(false)`), создаёт строку настроек,
собирает движок, загружает первый снимок (если не задано
`.initial_reload(false)`) и в фоновом режиме запускает циклы подписки и опроса.
`build_unsynced()` собирает дескриптор без работы со схемой — для реплики или
хоста, который уже синхронизировал схему; соответствие схемы при этом не
проверяется.

Способ хранения секретов нужно выбрать явно. `.master_key(key)` шифрует
секреты учётных данных AES-256-GCM, `.plaintext_secrets()` хранит их
открытым текстом. Сборка без одного из двух отклоняется, так что дескриптор
никогда не хранит секреты открытым текстом по умолчанию.

Другие методы построителя:

| Метод | По умолчанию | Назначение |
| - | - | - |
| `.cache(cache)` | `MemoryCache` в нативной сборке, `StoreCache` в wasm | Общее временное состояние и канал инвалидации. Нескольким экземплярам нужен общий кеш: Redis в нативной сборке, `StoreCache` поверх D1 или libSQL на edge. |
| `.channel(channel)` | | Зарегистрировать дополнительный канал. Он имеет приоритет над встроенным каналом с тем же id. |
| `.without_default_channels()` | | Регистрировать только каналы, переданные через `.channel(...)`. |
| `.client_pool(pool)` | `ClientPool::default()` | Кеш исходящих клиентов. |
| `.file_storage(operator)` | | Объектное хранилище для публикуемых тел и скачанных словарей. |
| `.publication_url(builder)` | | Построитель ссылок хоста для публикаций по URL. |
| `.fetch_policy(policy)` | `DefaultFetchPolicy` | Какие URL движок может загружать от имени вызывающего. См. [Вызовы](#calling). |
| `.observer(observer)` | `StoreObserver` | Заменить сохранение использования и захватов в БД собственной реализацией. |
| `.instance_id(id)` | случайный | Стабильный идентификатор процесса, записываемый в продолжения, которые держат живое соединение с апстримом. |
| `.sync_mode(mode)` | `SyncMode::Background` | Кто управляет синхронизацией. См. [Синхронизация](#synchronization). |
| `.poll_interval(interval)` | 30 с | Как часто фоновый цикл проверяет устойчивую ревизию. |
| `.login_ttl(ttl)` | 600 с / 900 с | Сколько времени вход по коду авторизации / коду устройства можно завершить. |

## Cargo features

| Feature | Действие |
| - | - |
| id канала (`codex`, `kiro`, `openai`, …) | Скомпилировать канал. Построитель регистрирует его, если не вызван `without_default_channels()`. |
| `channels` | Все каналы, поставляемые в workspace |
| `postgres` / `mysql` | Дополнительные драйверы БД, только нативно. SQLite в нативной сборке доступен всегда. |
| `libsql` | libSQL/Turso через HTTP-конвейер Hrana, на всех целевых платформах |
| `d1` | Маркер привязки Cloudflare D1 (в wasm привязка доступна всегда) |
| `memory` (по умолчанию) | Кеш внутри процесса |
| `redis` | Кеш Redis для нескольких экземпляров |
| `fs` (по умолчанию) | Локальное файловое хранилище, только нативно |
| `s3` | Хранилище S3/R2 |
| `bundled-vocabulary` (по умолчанию) | Словарь DeepSeek для оценки токенов у моделей без собственного файла словаря |
| `ts` | Объявления `ts-rs` для каждого DTO и каждого дескриптора канала |
| `client-sdk` | `ClientBackend`: обслуживает `Client` из `gproxy-client-sdk` этим дескриптором внутри процесса (только нативные платформы) |

Набор features по умолчанию компилируется под `wasm32-unknown-unknown`, как и
`libsql`.

Нативная сборка использует транспорт `reqwest`. Каналы, которым нужен другой
бэкенд, включают его сами: `claudecode`, `kiro`, `antigravity` и другие
включают `wreq` для эмуляции TLS, а `codex` — нативный стек `reqwest`,
совпадающий по отпечатку с Codex CLI. Хост также может включить feature
бэкенда в `gproxy-client` напрямую или передать собственный пул клиентов
через `.client_pool`.

## Вызовы

```rust theme={null}
let execution = gproxy
    .call(
        OperationKey { operation: Operation::GenerateContent, dialect: Dialect::OpenAi },
        request,
    )
    .scope("user:u-1")
    .attribution(UsageAttribution { user_id: Some("u-1".into()), ..Default::default() })
    .budgets(vec![BudgetOwner::new("user", "u-1")])
    .credentials(["cred-1".to_owned()].into())
    .send()
    .await?;

let (response, usage) = execution.into_parts();
```

`call` принимает ключ операции и `WireRequest` и возвращает построитель; `send`
разрешает имя модели и перебирает полученный план, переключаясь между учётными
данными и провайдерами, как описано в разделе
[Архитектура](https://gproxy.leenhawk.com/ru/development/architecture.md#failover). `connect` — тот же
построитель для рукопожатия WebSocket.

`.scope(...)` обязателен и не имеет значения по умолчанию. Scope — граница
изоляции, внутри которой движок хранит привязку вызывающего к учётным данным и
его продолжения. Scope по умолчанию означал бы, что посторонние вызывающие
делят их между собой.

Остальные методы построителя передают то, что прикладной слой уже решил:

* `.attribution(...)`: на кого записывается использование.
* `.budgets(...)`: цепочка владельцев бюджетов, с которых списывается расход.
* `.providers(...)` и `.credentials(...)`: идентификаторы провайдеров и учётных
  данных, доступные вызывающему. `None` означает отсутствие ограничений, пустой
  набор — что не разрешено ничего.
* `.session(...)`, `.session_id(...)`, `.agent_session_id(...)`: идентификатор
  сессии для привязки.
* `.model(...)`: имя модели, если его нет в теле запроса.
* `.max_attempts(...)`, `.deadline(...)`, `.cancellation(...)`,
  `.request_id(...)`.

`.channel("claudecode")` сужает кандидатов до одного канала. Используйте его,
когда канал важен для вызывающего: `claudecode` расходует лимит OAuth-подписки,
а `claudeapi` — API-ключ с потокенной оплатой, и у них раздельные пулы квот и
биллинг. Провайдер же — строка конфигурации, о которой вызывающим вне
экземпляра знать не нужно. Даже с закреплённым каналом дескриптор сам выбирает
среди провайдеров этого канала и их учётных данных и переключается внутри
канала при отказе.

### Политика загрузки ресурсов

Если запрос ссылается на ресурс по URL, движок загружает его согласно
`FetchPolicy`. Политика по умолчанию, `DefaultFetchPolicy`, разрешает только
http и https к публичным адресам; прямые нативные соединения закрепляют
проверенные публичные IP-адреса. URL с доменным именем отклоняются, если прокси
разрешал бы имя самостоятельно. Встраивающий хост может явно задать
`AllowlistFetchPolicy` через `.fetch_policy(...)` для подконтрольных ему имён
хостов; тогда контроль адресов назначения ложится на его прокси. В wasm
назначения по имени хоста по-прежнему зависят от ограничений исходящего трафика
платформы.

## Вход

`gproxy.login()` превращает OAuth-вход в апстрим в строку учётных данных.
Поддерживаются два потока, каждый из пары вызовов:

* Код авторизации: `authcode_start` возвращает URL для открытия;
  `authcode_complete` принимает обратный вызов и создаёт учётные данные.
* Код устройства: `device_start` возвращает код, который вводит пользователь;
  `device_poll` выполняет один опрос и возвращает ответ апстрима, в том числе
  время ожидания до следующего опроса. sdk не спит и не крутит цикл — частотой
  опроса управляет вызывающий.

Верификатор PKCE и CSRF state создаёт и проверяет sdk. Обратный вызов с
несовпадающим state отклоняется, а сессия уничтожается. Незавершённая сессия
входа хранится в общем кеше с TTL входа, поэтому любой экземпляр развёртывания
может завершить вход, начатый другим. До успешного обмена ничего не
сохраняется, а секрет шифруется настроенным кодеком перед вставкой.

## Управление

`gproxy.manage()` — сторона записи. У каждого семейства конфигурации свой
метод доступа, и все они пишут через один примитив фиксации (см.
[Запись конфигурации и перезагрузка](https://gproxy.leenhawk.com/ru/development/architecture.md#configuration-writes-and-reload)).

| Семейство | Данные | Помимо CRUD |
| - | - | - |
| `providers()` | `providers` | `reset_routing_defaults` |
| `credentials()` | `credentials` | `reveal_secret`, `set_status`, `refresh`, `quota_probe`, `quota_diagnostics`, `quota_read`, `quota_observations`, `quota_reset`, `quota_reset_with`, `quota_reset_credits`, `health_reset`, `limit_status` |
| `models()` / `provider_models()` | каталог | |
| `routes()` / `route_members()` | маршрутизация | |
| `connection_profiles()` | исходящие стеки | |
| `settings()` | единственная строка настроек | только `get` / `update` |
| `rewrite()` | наборы правил, правила, привязки | `replace_rules` |
| `endpoints()` | `operation_rules`, `operation_endpoints` | `effective`, `set_mapping`, `reset_mapping` |
| `quotas()` | `quotas` | `budget_status`, `reset_budget`, `limit_status`, `reset_limit` |
| `pricing()` | правила, ставки, уровни | |
| `transfer()` | вся конфигурация | `export`, `import` |
| `catalog()` | ничего, пока не применено | `channels`, `default_models`, `apply_default_prices`, `tls_presets`, `rule_presets`, `apply_rule_preset` |
| `connectivity()` | `provider_models` | `test`, `model_test`, `discover_models`, `refresh_models`, `apply_discovered` |
| `tokenizer()` | файлы словарей | `vocabularies`, `fetch`, `import`, `progress`, `delete`, `auth`, `set_auth`, `reveal_auth` |
| `history()` | захваченные логи и записи использования | `delete_logs`, `delete_usage` |

Семейства CRUD предоставляют `list`, `get`, `create`, `update`, `delete` и
`batch`. Переданные идентификаторы используются как есть, отсутствующие
генерируются; метки времени — миллисекунды Unix; десятичные числа передаются
строками; секрет учётных данных никогда не попадает в DTO — для его чтения есть
`reveal_secret`.

Списки принимают `ListQuery` и возвращают `Page`. Страницы нумеруются с 1;
`page_size` по умолчанию 50 и ограничен диапазоном 1–500.

Запись перечисляет свои scope, и от них зависит последующая перезагрузка. Scope
состояния учётных данных ведёт по дешёвому пути перезагрузки и допустим только
для записи, затрагивающей секрет, срок его действия и статус жизненного цикла.
Всё остальное в учётных данных фиксируется в снимке выполнения при сборке и
требует полной перезагрузки; с дешёвым scope такое изменение попало бы в базу,
но оставалось бы невидимым для этого экземпляра до следующей посторонней
перезагрузки.

`history()` к конфигурации не относится. Удаление истории не увеличивает
ревизию и не вызывает перезагрузку, а рассчитанные счётчики квот, записи
биллинга и окна подписок остаются нетронутыми. Захват, который ещё
записывается, никогда не удаляется. Одно удаление указывает не более 500
идентификаторов.

## Запросы к данным

`gproxy.query()` — сторона чтения. Здесь ничего не записывается, поэтому
ревизия не меняется.

| Семейство | Методы |
| - | - |
| `usage()` | `records`, `summary`, `group`, `trend`, `aggregate` |
| `quota()` | `windows`, `settlements`, `credential_cycles`, `credential_observations`, `counted_windows`, `budget_status` |
| `logs()` | `list`, `upstream`, `capture`, `detail(request_id)` |

### Агрегаты использования

Фиксированные поля использования хранятся в отдельных столбцах, по одной строке
на каждый физический вызов апстрима, без сводных строк нижестоящего уровня.
Фильтры по провайдеру и учётным данным выполняются в SQL; затем `summary`,
`group` и `trend` сворачивают подходящие строки и пользовательские величины в
Rust, порциями в порядке ключа.

Без `max_scan_rows` агрегат читает весь подходящий диапазон порциями. С ним
сканирование останавливается после указанного числа строк (ограниченного
диапазоном 1–50 000), а результат возвращается с `truncated: true` и числом
просмотренных строк `scanned`, чтобы частичный итог не выдавался за полный.

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

Стоимость читается из отдельных столбцов в USD. `currency` равен `None`, если
ни одна запись не была тарифицирована. `quantities` содержит счётчики медиа,
инструментов и пользовательские счётчики в виде десятичных строк.

### Постраничный вывод

Записи использования, окна квот и списки управления листаются по смещению.
Журнал запросов листается по курсору: это поток, в который только добавляются
записи, и его начало сдвигается, пока его читают, поэтому страницы по смещению
повторяли бы или пропускали строки. Курсор состоит из двух частей,
`next_cursor` (метка времени) и `next_cursor_id` (идентификатор строки), и обе
нужно возвращать без изменений. Одной метки времени недостаточно, потому что
два запроса могут начаться в одну миллисекунду.

### Маскирование

`logs()` ничего не маскирует. Наблюдатель применил политику захвата
развёртывания при записи строк, поэтому сохранённое — это то, что можно
показывать. Хосту не стоит рассчитывать на повторную фильтрацию при чтении:
если секрет оказался в базе, значит, политика это разрешила.

## Синхронизация

Экземпляры одного развёртывания держат одну конфигурацию с помощью уведомлений
об инвалидации в общем кеше и опроса `settings.config_revision` (по умолчанию
каждые 30 секунд). Механизм описан в разделе
[Архитектура](https://gproxy.leenhawk.com/ru/development/architecture.md#multi-instance-sync).

Методы синхронизации дескриптора:

* `reload()`: перечитать конфигурацию и опубликовать новый снимок.
* `reload_credentials(ids)`: перечитать только данные учётных данных.
* `sync_now()`: один опрос ревизии; перезагрузка, если устойчивая ревизия
  новее.
* `drain_notifications()`: применить уже пришедшие уведомления, не обращаясь
  к базе. В `SyncMode::Background` ничего не делает.
* `tick()`: `drain_notifications()`, затем `sync_now()`.

В режиме `SyncMode::Background` дескриптор сам запускает оба цикла; для этого
нужен рантайм Tokio и процесс, живущий дольше запроса. В режиме
`SyncMode::Manual` ничего не происходит, пока хост не вызовет перечисленные
методы.

В wasm кеш по умолчанию — `StoreCache` поверх базы данных, а синхронизация
всегда ручная, потому что изолят не переживает свой запрос. Вызывайте `tick()`
в начале каждого запроса:

```rust theme={null}
// tick() = drain the delivered notifications, then poll the revision once.
instance.tick().await;
```

Забирать уведомления достаточно дёшево, чтобы делать это на каждом запросе;
опрос дороже, поэтому хост, которому нужна только дешёвая половина, может
вызывать `drain_notifications()` отдельно. В ручном режиме подписка создаётся
во время `build()`, а её начальная подсказка о ресинхронизации отбрасывается
(первая загрузка и есть эта ресинхронизация). Поэтому запись, пришедшая между
сборкой и первым `tick()`, не теряется.

## Экспорт типов TypeScript

С feature `ts` sdk экспортирует объявления TypeScript для каждого DTO и для
дескрипторов каналов:

```sh theme={null}
GPROXY_TS_OUT=console/src/generated \
  cargo test -p gproxy-sdk --features ts export_types
```

Без `GPROXY_TS_OUT` тест сразу завершается и ничего не пишет, поэтому
`cargo test --all-features` не имеет побочных эффектов. С переменной выходной
каталог сначала удаляется, чтобы не оставались объявления удалённых DTO, а в
конце записывается `index.ts`, реэкспортирующий всё.

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

Список экспорта ведётся вручную, поскольку Rust не умеет перечислять типы
модуля во время выполнения. В v3 этот список расходился с кодом: DTO
добавляли, не внося в список, и консоль оставалась без их типов. Второй тест,
`every_exported_dto_is_in_the_export_list`, читает исходный код модуля `dto` и
падает, если реэкспортируемого типа нет в списке.

## Слои над sdk

Приложение, которому нужна ещё и идентификация, подключает `gproxy-app`. Он
добавляет аутентификацию, допуск, семейства операций администрирования и
портала и OAuth issuer, но не содержит транспорта. `gproxy-host-axum`
предоставляет всё это по HTTP, `gproxy-host-edge` монтирует тот же
маршрутизатор в Cloudflare Worker, а `gproxy-host-tauri` обслуживает
административные части через IPC. Каждый из этих крейтов можно встраивать на
своём уровне.


## Related topics

- [Архитектура](https://gproxy.leenhawk.com/ru/development/architecture.md)
- [Провайдеры и учётные данные](https://gproxy.leenhawk.com/ru/guides/providers.md)
- [CLI-клиенты](https://gproxy.leenhawk.com/ru/guides/cli-clients.md)
- [Маршрутизация и конечные точки](https://gproxy.leenhawk.com/ru/reference/routing-table.md)
- [GPROXY](https://gproxy.leenhawk.com/ru/index.md)


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