Skip to content

Разработка плагинов

Система плагинов CLIProxyAPI подключает к потоку хоста возможности моделей, учётных данных, планирования, преобразования, перехвата, наблюдения за использованием, расширения командной строки и management-страниц. Плагины запускаются как нативные динамические библиотеки внутри процесса CLIProxyAPI. Хост вызывает плагины через стабильный C ABI, а плагины могут вызывать callback хоста, чтобы переиспользовать существующие возможности CLIProxyAPI для HTTP, выполнения моделей, файлов учётных данных и логирования.

Область применения

Плагины подходят для следующих задач:

  • Предоставление списка моделей, разбора учётных данных, обновления входа и выполнения запросов для нового upstream.
  • Преобразование запросов, нормализация запросов, выбор планирования или перехват запросов перед upstream.
  • Преобразование ответов, нормализация ответов или перехват streaming chunk перед возвратом ответа клиенту.
  • Получение записей использования или добавление страниц и диагностических endpoints, принадлежащих только этому плагину, в management-часть.
  • Вызов существующего пути выполнения моделей хоста вместо копирования secret, proxy, логирования, статистики использования и логики маршрутизации в плагин.

Плагины не подходят для запуска недоверенного кода. Стандартные плагины как динамические библиотеки находятся в одном процессе с бинарником сервиса. Хост может восстановиться после части panic, но не может запретить плагину завершить процесс, повредить память, изменить глобальное состояние процесса или раскрыть чувствительные данные.

Документы возможностей

У каждой возможности есть отдельная страница. Материал собран по sdk/pluginapi/types.go, sdk/pluginabi/types.go, путям вызовов internal/pluginhost и примерам examples/plugin.

КатегорияВозможностьДокумент
Входная возможностьmodel_registrarРегистратор моделей
Входная возможностьmodel_providerПровайдер моделей
Входная возможностьauth_providerПровайдер учётных данных
Входная возможностьfrontend_auth_providerПровайдер фронтенд-аутентификации
Входная возможностьfrontend_auth_provider_exclusiveЭксклюзивный режим фронтенд-аутентификации
Входная возможностьschedulerПланировщик
Входная возможностьexecutorИсполнитель
Обработка запросовrequest_translatorПреобразование запросов
Обработка запросовrequest_normalizerНормализация запросов
Обработка запросовrequest_interceptorПерехват запросов
Обработка ответовresponse_translatorПреобразование ответов
Обработка ответовresponse_before_translatorНормализация ответа перед преобразованием
Обработка ответовresponse_after_translatorНормализация ответа после преобразования
Обработка ответовresponse_interceptorПерехват ответов
Обработка ответовresponse_stream_interceptorПерехват стриминговых ответов
Расширениеthinking_applierThinking applier
Расширениеusage_pluginНаблюдение за использованием
Расширениеcommand_line_pluginРасширение командной строки
Расширениеmanagement_apiManagement API
Возможность хостаhost.*Callback хоста

Требования к запуску

Возможности плагинов требуют сборку с CGO. Ответы Management API содержат:

http
X-CPA-SUPPORT-PLUGIN: 1

1 означает, что текущий бинарник поддерживает плагины как динамические библиотеки, 0 — что не поддерживает. Этот заголовок показывает только возможность сборки. Он не означает, что плагины включены или что конкретный плагин загружен.

В конфигурации также нужно включить глобальный переключатель плагинов:

yaml
plugins:
  enabled: true
  dir: "plugins"
  configs: {}

Если plugins.enabled равен false, файлы плагинов и отдельная конфигурация плагинов могут существовать, но они не становятся фактически включёнными.

Обнаружение файлов плагинов

ID плагина берётся из имени файла динамической библиотеки без расширения. Например:

text
plugins/darwin/arm64/example-provider.dylib

Соответствующий ключ конфигурации:

yaml
plugins:
  configs:
    example-provider:
      enabled: true
      priority: 1

ID плагина должен соответствовать:

text
[A-Za-z0-9][A-Za-z0-9._-]{0,127}

Хост последовательно ищет по текущей платформе:

text
plugins/<GOOS>/<GOARCH>-<variant>
plugins/<GOOS>/<GOARCH>
plugins

macOS использует .dylib, Linux и FreeBSD используют .so, Windows использует .dll. Если один ID плагина найден в нескольких каталогах, срабатывает каталог с более высоким приоритетом.

Основы ABI

Каждый стандартный плагин как динамическая библиотека должен экспортировать:

c
int cliproxy_plugin_init(const cliproxy_host_api* host, cliproxy_plugin_api* plugin);

При инициализации плагин заполняет свою таблицу функций:

c
int call(char* method, uint8_t* request, size_t request_len, cliproxy_buffer* response);
void free_buffer(void* ptr, size_t len);
void shutdown(void);

Таблица функций, предоставленная хостом, нужна для обратных вызовов плагина в хост:

c
int call(void* host_ctx, char* method, uint8_t* request, size_t request_len, cliproxy_buffer* response);
void free_buffer(void* ptr, size_t len);

C ABI передаёт только имя метода, массив байтов и длину. Он не передаёт Go interface, Go slice, Go map, Go channel, context.Context или Go error. Запросы и ответы используют JSON-конверт, а raw byte поля автоматически представлены в JSON как base64.

Успешный ответ:

json
{
  "ok": true,
  "result": {}
}

Ответ с ошибкой:

json
{
  "ok": false,
  "error": {
    "code": "invalid_request",
    "message": "request is invalid"
  }
}

Жизненный цикл

Хост вызывает базовые методы:

МетодНаправлениеНазначение
plugin.registerХост вызывает плагинПервая загрузка плагина: чтение метаданных, полей конфигурации и объявлений возможностей.
plugin.reconfigureХост вызывает плагинПовторная передача конфигурации плагину после изменения конфигурации.
plugin.shutdownХост вызывает плагинОсвобождение ресурсов при выгрузке плагина или остановке хоста.

Запросы plugin.register и plugin.reconfigure содержат config_yaml. Он берётся из plugins.configs.<pluginID>. Хост сохраняет YAML-поля самого плагина и разбирает только принадлежащие хосту enabled и priority.

Ответ регистрации должен вернуть:

json
{
  "schema_version": 1,
  "metadata": {
    "Name": "example-provider",
    "Version": "0.1.0",
    "Author": "router-for-me",
    "GitHubRepository": "https://github.com/router-for-me/example-provider",
    "Logo": "https://example.com/logo.png",
    "ConfigFields": [
      {
        "Name": "mode",
        "Type": "enum",
        "EnumValues": ["safe", "fast"],
        "Description": "Execution mode."
      }
    ]
  },
  "capabilities": {
    "request_normalizer": true,
    "management_api": true
  }
}

ConfigFields используется management-частью для отображения конфигурации плагина. Это не заменяет собственную проверку конфигурации плагином. Плагин всё равно должен проверять важные для него поля в plugin.register или plugin.reconfigure.

Семантика конфигурации

Рекомендуемая минимальная конфигурация:

yaml
plugins:
  enabled: true
  dir: "plugins"
  configs:
    example-provider:
      enabled: true
      priority: 1
      mode: "safe"

Значения полей:

ПолеОписание
plugins.enabledГлобальный переключатель загрузки плагинов.
plugins.dirКаталог обнаружения плагинов, по умолчанию plugins.
plugins.store-sourcesСписок дополнительных URL registry магазина плагинов.
plugins.configs.<pluginID>.enabledПереключатель отдельного плагина. Если не указан, считается включённым.
plugins.configs.<pluginID>.priorityПорядок запуска, регистрации и маршрутизации плагинов. Плагины с более высоким приоритетом обрабатываются первыми.
Другие поляСобственная конфигурация плагина, хост сохраняет её как есть и передаёт плагину.

При обновлении конфигурации через Management API хост старается сохранить исходное YAML-дерево и изменить только запрошенные поля. После установки плагина из магазина хост записывает динамическую библиотеку и выставляет конфигурацию этого плагина в enabled: true, но не включает принудительно plugins.enabled.

Модель возможностей

Плагин объявляет реализованные возможности через capabilities. Частые возможности:

ВозможностьНаправление методаНазначение
Регистратор моделейmodel.registerРегистрирует статические метаданные моделей в хосте.
Провайдер моделейmodel.static / model.for_authПредоставляет статические модели или модели по записям учётных данных.
Провайдер учётных данныхauth.*Разбирает, логинит, опрашивает и обновляет учётные данные провайдера плагина.
Провайдер фронтенд-аутентификацииfrontend_auth.*Аутентифицирует клиентские запросы до обработки proxy.
Планировщикscheduler.pickВыбирает учётные данные из кандидатов или делегирует встроенному планировщику.
Исполнительexecutor.*Непосредственно выполняет upstream-запросы или stream-запросы.
Преобразование запросовrequest.translateПреобразует канонический запрос в upstream-протокол.
Нормализация запросовrequest.normalizeНормализует запросы перед входом в путь выполнения.
Перехват запросовrequest.intercept_before / request.intercept_afterИзменяет запросы выполнения до или после выбора учётных данных.
Преобразование ответовresponse.translateПреобразует канонический ответ в клиентский протокол.
Нормализация ответовresponse.normalize_before / response.normalize_afterНормализует ответы до или после встроенного преобразования.
Перехват ответовresponse.intercept_afterИзменяет нестриминговые ответы.
Перехват стриминговых ответовresponse.intercept_stream_chunkИзменяет chunk стримингового ответа.
Thinking applierthinking.applyПрименяет проверенную thinking-конфигурацию.
Наблюдение за использованиемusage.handleПолучает записи использования завершённых запросов.
Расширение командной строкиcommand_line.*Регистрирует и обрабатывает CLI-флаги плагина.
Management APImanagement.*Регистрирует management routes или браузерные ресурсы плагина.

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

Callback хоста

Callback хоста — это вызовы плагина в хост, а не вызовы хоста в плагин. Они подходят для переиспользования proxy, учётных данных, маршрутизации моделей, логирования, статистики использования и управления ресурсами, которые уже обрабатывает хост.

Частые callback:

CallbackНазначение
host.http.doВыполняет один обычный HTTP-запрос через хост.
host.http.do_stream / host.http.stream_read / host.http.stream_closeВыполняет стриминговый HTTP-запрос через хост и читает или закрывает поток.
host.model.executeЗапускает нестриминговый запрос модели через путь выполнения моделей хоста.
host.model.execute_stream / host.model.stream_read / host.model.stream_closeЗапускает стриминговый запрос модели через путь выполнения моделей хоста и читает или закрывает поток.
host.stream.emit / host.stream.closeПозволяет executor plugin отправлять chunk в stream bridge хоста или закрывать поток.
host.logПишет через логгер хоста.
host.auth.listПеречисляет записи учётных данных хоста.
host.auth.getЧитает физический JSON-файл учётных данных.
host.auth.get_runtimeЧитает runtime-информацию об учётных данных.
host.auth.saveЗаписывает JSON учётных данных и обновляет runtime-запись.

Если плагин вызывает host.model.execute или host.model.execute_stream из management.handle или другого контекста, вызванного хостом, он должен переслать host_callback_id из запроса. Хост использует его, чтобы определить источник callback, и при вложенном выполнении модели пропускает request, response и stream interceptor того же плагина, предотвращая рекурсивный вызов самого себя. Другие включённые плагины всё ещё могут обработать вложенный запрос.

Для streaming callback рекомендуется явно вызывать соответствующий метод *_close. Хост может очистить часть ресурсов в конце области RPC, но явное закрытие быстрее освобождает потоковые ресурсы и упрощает поиск ошибок.

Management API и ресурсы плагинов

Плагин может объявлять два типа management-возможностей:

  1. Собственные API плагина, которым нужно управлять учётными данными.
  2. Ресурсные страницы плагина, которые браузер может открыть напрямую.

Границы маршрутов различаются:

ТипПоле регистрацииОткрытый путьАутентификация
Собственный Management API плагинаroutes/v0/management/...Требует management key.
Ресурсная страница плагинаresources/v0/resource/plugins/<pluginID>/...Доступ как к resource route.

Пример: ID плагина example-provider, путь ресурса /status, итоговый адрес:

text
http://localhost:8317/v0/resource/plugins/example-provider/status

Плагин возвращает маршруты и ресурсы через management.register:

json
{
  "resources": [
    {
      "Path": "/status",
      "Menu": "Example Provider",
      "Description": "Show plugin status."
    }
  ],
  "routes": [
    {
      "Method": "POST",
      "Path": "/plugins/example-provider/run"
    }
  ]
}

Хост передаёт совпавшие запросы в management.handle. Запрос содержит method, path, headers, query и body; ответ содержит status code, headers и body.

Важно:

  • Собственные Management API маршруты плагина проверяются на конфликт с существующими маршрутами хоста /v0/management; конфликтующие маршруты плагина пропускаются.
  • Пути ресурсов плагина всегда монтируются под /v0/resource/plugins/<pluginID>/.
  • Устаревшие GET management routes с Menu обрабатываются как браузерные ресурсы и больше не раскрываются как management API.
  • Resource path не может содержать пробелы, :, * или ...

Management endpoints

Все следующие endpoints находятся под /v0/management и требуют management key.

Метод и путьНазначение
GET /pluginsПеречисляет найденные, настроенные и зарегистрированные плагины, возвращает plugins_enabled, effective_enabled, меню, метаданные и поля конфигурации.
PATCH /plugins/{pluginID}/enabledОбновляет только plugins.configs.<pluginID>.enabled, не изменяя глобальный plugins.enabled.
GET /plugins/{pluginID}/configПолучает сохранённый объект конфигурации плагина.
PUT /plugins/{pluginID}/configПолностью заменяет объект конфигурации плагина.
PATCH /plugins/{pluginID}/configПоверхностно объединяет объект конфигурации; значения null удаляют поля.
DELETE /plugins/{pluginID}Точечно выгружает целевой плагин, удаляет локальную динамическую библиотеку и сохранённую конфигурацию.
GET /plugin-storeПоказывает плагины в магазине и их локальный статус установки.
POST /plugin-store/{pluginID}/installУстанавливает или обновляет плагин из магазина; при нескольких источниках с одинаковым ID используйте ?source=<sourceID>.

Не смешивайте поля статуса из GET /plugins:

  • plugins_enabled: глобальный переключатель плагинов.
  • enabled: переключатель конфигурации отдельного плагина.
  • registered: динамическая библиотека плагина загружена и регистрация завершена.
  • effective_enabled: фактическое включённое состояние, когда одновременно выполнены глобальный переключатель, переключатель плагина и состояние регистрации.

При установке или обновлении плагина хост сначала скачивает release asset и проверяет checksums.txt, затем точечно выгружает целевой плагин перед перезаписью динамической библиотеки, записывает новый файл и запускает hot reload конфигурации. Если платформа или блокировка файла не позволяют перезаписать уже загруженную динамическую библиотеку, endpoint возвращает conflict response, требующий перезапуска.

Формат публикации магазина плагинов

Registry магазина плагинов по умолчанию:

text
https://raw.githubusercontent.com/router-for-me/CLIProxyAPI-Plugins-Store/main/registry.json

Можно добавить сторонние источники в конфигурации:

yaml
plugins:
  store-sources:
    - "https://example.com/cliproxyapi-plugins/registry.json"

Формат registry:

json
{
  "schema_version": 1,
  "plugins": [
    {
      "id": "example-provider",
      "name": "Example Provider",
      "description": "Example plugin provider.",
      "author": "router-for-me",
      "version": "0.1.0",
      "repository": "https://github.com/router-for-me/example-provider",
      "logo": "https://example.com/logo.png",
      "homepage": "https://example.com",
      "license": "MIT",
      "tags": ["provider"]
    }
  ]
}

Требования:

  • schema_version должен быть 1.
  • id, name, description, author, repository обязательны.
  • repository должен быть https://github.com/{owner}/{repo}.
  • version — fallback для отображения. Фактическая устанавливаемая версия берётся из GitHub latest release tag. Tag может начинаться с v; хост удаляет ведущий v перед проверкой версии.

Release плагина должен предоставлять zip asset для текущей платформы и checksums.txt:

text
<pluginID>_<version>_<goos>_<goarch>.zip
checksums.txt

Корень zip должен напрямую содержать целевую динамическую библиотеку:

text
example-provider.dylib

Не помещайте динамическую библиотеку в подкаталог. checksums.txt использует обычный формат sha256:

text
<sha256>  example-provider_0.1.0_darwin_arm64.zip

Советы по разработке

Рекомендуется начинать с примеров в репозитории:

bash
make -C examples/plugin list
make -C examples/plugin build

Частые примеры:

ПримерФокус
examples/plugin/simpleПолные ABI skeleton на Go, C и Rust.
examples/plugin/codex-service-tierПлагин нормализации запросов.
examples/plugin/schedulerПлагин планировщика.
examples/plugin/management-apiСобственные management routes и resource pages плагина.
examples/plugin/host-callback-auth-filesВызов callback файлов учётных данных хоста.
examples/plugin/host-model-callbackВызов callback выполнения моделей хоста и демонстрация защиты от рекурсии.

При разработке:

  • Объявляйте только те возможности, которые плагин действительно реализует.
  • Собственные HTTP-запросы плагина лучше выполнять через host.http.*, чтобы не обходить proxy, логирование и транспортную политику хоста.
  • Когда нужен запрос модели, предпочитайте host.model.*; не копируйте учётные данные хоста в плагин.
  • Явно закрывайте streaming resources после использования.
  • Сохраняйте обратную совместимость собственных полей конфигурации плагина; при удалении полей поддерживайте старую конфигурацию.
  • Не логируйте secret, token, исходный JSON учётных данных или чувствительные тела пользовательских запросов.
  • После изменения динамической библиотеки используйте plugin management API или перезапустите сервис, чтобы старый экземпляр плагина больше не использовался.

Минимальный процесс проверки

После разработки локального плагина можно проверить его так:

  1. Соберите динамическую библиотеку для текущей платформы и поместите её в plugins/<GOOS>/<GOARCH>/ или plugins/.
  2. Включите plugins.enabled в config.yaml и добавьте plugins.configs.<pluginID>.
  3. Запустите CLIProxyAPI.
  4. Выполните GET /v0/management/plugins и убедитесь, что registered: true и effective_enabled: true.
  5. Если у плагина есть resource pages, откройте /v0/resource/plugins/<pluginID>/<path>.
  6. Если у плагина есть Management API, запросите соответствующий маршрут /v0/management/... с management key.
  7. После изменения плагина установите или удалите его через management API либо перезапустите сервис и убедитесь, что старая динамическая библиотека больше не используется.

Лицензия MIT.