Разработка плагинов
Система плагинов 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_applier | Thinking applier |
| Расширение | usage_plugin | Наблюдение за использованием |
| Расширение | command_line_plugin | Расширение командной строки |
| Расширение | management_api | Management API |
| Возможность хоста | host.* | Callback хоста |
Требования к запуску
Возможности плагинов требуют сборку с CGO. Ответы Management API содержат:
X-CPA-SUPPORT-PLUGIN: 11 означает, что текущий бинарник поддерживает плагины как динамические библиотеки, 0 — что не поддерживает. Этот заголовок показывает только возможность сборки. Он не означает, что плагины включены или что конкретный плагин загружен.
В конфигурации также нужно включить глобальный переключатель плагинов:
plugins:
enabled: true
dir: "plugins"
configs: {}Если plugins.enabled равен false, файлы плагинов и отдельная конфигурация плагинов могут существовать, но они не становятся фактически включёнными.
Обнаружение файлов плагинов
ID плагина берётся из имени файла динамической библиотеки без расширения. Например:
plugins/darwin/arm64/example-provider.dylibСоответствующий ключ конфигурации:
plugins:
configs:
example-provider:
enabled: true
priority: 1ID плагина должен соответствовать:
[A-Za-z0-9][A-Za-z0-9._-]{0,127}Хост последовательно ищет по текущей платформе:
plugins/<GOOS>/<GOARCH>-<variant>
plugins/<GOOS>/<GOARCH>
pluginsmacOS использует .dylib, Linux и FreeBSD используют .so, Windows использует .dll. Если один ID плагина найден в нескольких каталогах, срабатывает каталог с более высоким приоритетом.
Основы ABI
Каждый стандартный плагин как динамическая библиотека должен экспортировать:
int cliproxy_plugin_init(const cliproxy_host_api* host, cliproxy_plugin_api* plugin);При инициализации плагин заполняет свою таблицу функций:
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);Таблица функций, предоставленная хостом, нужна для обратных вызовов плагина в хост:
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.
Успешный ответ:
{
"ok": true,
"result": {}
}Ответ с ошибкой:
{
"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.
Ответ регистрации должен вернуть:
{
"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.
Семантика конфигурации
Рекомендуемая минимальная конфигурация:
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 applier | thinking.apply | Применяет проверенную thinking-конфигурацию. |
| Наблюдение за использованием | usage.handle | Получает записи использования завершённых запросов. |
| Расширение командной строки | command_line.* | Регистрирует и обрабатывает CLI-флаги плагина. |
| Management API | management.* | Регистрирует 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-возможностей:
- Собственные API плагина, которым нужно управлять учётными данными.
- Ресурсные страницы плагина, которые браузер может открыть напрямую.
Границы маршрутов различаются:
| Тип | Поле регистрации | Открытый путь | Аутентификация |
|---|---|---|---|
| Собственный Management API плагина | routes | /v0/management/... | Требует management key. |
| Ресурсная страница плагина | resources | /v0/resource/plugins/<pluginID>/... | Доступ как к resource route. |
Пример: ID плагина example-provider, путь ресурса /status, итоговый адрес:
http://localhost:8317/v0/resource/plugins/example-provider/statusПлагин возвращает маршруты и ресурсы через management.register:
{
"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 магазина плагинов по умолчанию:
https://raw.githubusercontent.com/router-for-me/CLIProxyAPI-Plugins-Store/main/registry.jsonМожно добавить сторонние источники в конфигурации:
plugins:
store-sources:
- "https://example.com/cliproxyapi-plugins/registry.json"Формат registry:
{
"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:
<pluginID>_<version>_<goos>_<goarch>.zip
checksums.txtКорень zip должен напрямую содержать целевую динамическую библиотеку:
example-provider.dylibНе помещайте динамическую библиотеку в подкаталог. checksums.txt использует обычный формат sha256:
<sha256> example-provider_0.1.0_darwin_arm64.zipСоветы по разработке
Рекомендуется начинать с примеров в репозитории:
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 или перезапустите сервис, чтобы старый экземпляр плагина больше не использовался.
Минимальный процесс проверки
После разработки локального плагина можно проверить его так:
- Соберите динамическую библиотеку для текущей платформы и поместите её в
plugins/<GOOS>/<GOARCH>/илиplugins/. - Включите
plugins.enabledвconfig.yamlи добавьтеplugins.configs.<pluginID>. - Запустите CLIProxyAPI.
- Выполните
GET /v0/management/pluginsи убедитесь, чтоregistered: trueиeffective_enabled: true. - Если у плагина есть resource pages, откройте
/v0/resource/plugins/<pluginID>/<path>. - Если у плагина есть Management API, запросите соответствующий маршрут
/v0/management/...с management key. - После изменения плагина установите или удалите его через management API либо перезапустите сервис и убедитесь, что старая динамическая библиотека больше не используется.