Доступ к LLM из плагина
ctx.llm — это способ вызова LLM из плагина.
Чат-завершение, структурированное извлечение, синхронно, асинхронно, с изображениями или без —
один и тот же интерфейс, один и тот же доверительный шлюз, одни и те же учётные данные хоста.
Плагины обращаются к этому, когда им нужно сделать что-то, связанное с моделью, но не являющееся частью диалога агента. Хук, который переписывает ошибочный инструмент в формате, понятном неинженеру. Адаптер шлюза, который переводит входящее сообщение перед постановкой в очередь. Слэш-команда, которая суммирует длинный текст. Запланированная задача, которая измеряет вчерашнюю нашу активность и записывает один сигнал на доску. Предварительный фильтр, который решает, стоит ли вообще вызывать агента для данных сообщений.
Это задача, в которой агент не должен участвовать. Мне нужен один вызов LLM, типизированный ответ и всё.
Самый простой возможный вызов
result = ctx.llm.complete(messages=[{"role": "user", "content": "ping"}])
return result.text
Вот и весь API в одной строке. Никаких ключей, никакой конфигурации провайдера, никакой организации SDK. Плагин работает с темным провайдером и моделью, которую использует пользователь — когда он меняет провайдера, плагин автоматически переключается на них.
Более полный пример чата
result = ctx.llm.complete(
messages=[
{"role": "system", "content": "Перепиши ошибки в виде одного короткого предложения, понятного не-инженеру."},
{"role": "user", "content": traceback_text},
],
max_tokens=64,
purpose="hooks.error-rewrite",
)
return result.text
назначение — это свободная строка для аудита — она отображается в agent.log и result.audit, так что операторы могут видеть, какой плагин сделал какой вызов. Опционально, но рекомендуется для всего, что встречается часто.
Структурированный вывод
Когда подключаетесь, нужен типизированный ответ, переключайтесь на структурированный режим:
result = ctx.llm.complete_structured(
instructions="Оцени этот ответ поддержки по срочности (0–1) и выбери категорию.",
input=[{"type": "text", "text": message_body}],
json_schema=TRIAGE_SCHEMA,
purpose="support.triage",
temperature=0.0,
max_tokens=128,
)
if result.parsed["urgency"] > 0.8:
await dispatch_to_oncall(result.parsed["category"], message_body)
Хост запрашивает JSON-вывод у провайдера, локально разбирает его как запасной вариант, теперь ваша схема, если установлена jsonschema, и возвращает объект Python в result.parsed. Если модель не смогла создать действительный JSON, result.parsed равен None, а result.text содержит необработанный ответ.
Что дает этот режим
- Один вызов, четыре формы.
complete()для чата,complete_structured()для типизированного JSON,acomplete()иacomplete_structured()для asyncio. Те же аргументы, те же результаты. - Учётные данные на стороне хоста. OAuth-токены, обновления процедур, пул учётных данных, вспомогательные переопределения для задач — все концепции учётных данных Hermes применимы. Плагин никогда не видит токен; хост привязывает вызов через
result.audit. - Ограниченность. Одиночный синхронный или асинхронный вызов. Никакой потоковой передачи, никаких циклических инструментов, никакого диалога состояния для управления. Укажите входные данные, получите результат, верните.
- Блокировка по умолчанию. Плагин, который вы никогда не настраивали, не может выбрать свой собственный провайдер, модель, агента или сохранённые учётные данные. Поведение по умолчанию — «использовать то, что использует пользователь». Операторы соглашаются на каждое переопределение каждого плагина в
config.yaml.
Быстрый старт
Ниже приведены два полных плагина — один чат, один структурированный. Оба работают внутри одной функции register(ctx) и не требуют внешних настроек для работы с моделью, выбранной пользователем.
Чат-завершение — /tldr
def register(ctx):
ctx.register_command(
name="tldr",
handler=lambda raw: _tldr(ctx, raw),
description="Суммируй предоставленный текст в одном абзаце.",
args_hint="<text>",
)
def _tldr(ctx, raw_args: str) -> str:
text = raw_args.strip()
if not text:
return "Использование: /tldr <текст для суммирования>"
result = ctx.llm.complete(
messages=[
{"role": "system",
"content": "Суммируй текст пользователя в одном коротком абзаце. Без предисловий."},
{"role": "user", "content": text},
],
max_tokens=256,
temperature=0.3,
purpose="tldr",
)
return result.text
result.text — это ответ модели; result.usage содержит количество токенов; result.provider и result.model содержат информацию о происхождении.
Структурированное извлечение — /paste-to-tasks
def register(ctx):
ctx.register_command(
name="paste-to-tasks",
handler=lambda raw: _paste_to_tasks(ctx, raw),
description="Преобразуй произвольные заметки с встречи в структурированные задачи.",
args_hint="<text>",
)
_TASKS_SCHEMA = {
"type": "object",
"properties": {
"tasks": {
"type": "array",
"items": {
"type": "object",
"properties": {
"owner": {"type": "string"},
"action": {"type": "string"},
"due": {"type": "string", "description": "ISO date or empty"},
},
"required": ["action"],
},
},
},
"required": ["tasks"],
}
def _paste_to_tasks(ctx, raw_args: str) -> str:
if not raw_args.strip():
return "Использование: /paste-to-tasks <заметки с встречи>"
result = ctx.llm.complete_structured(
instructions=(
"Извлеки конкретные действия из этих заметок с встречи. "
"Одна задача на каждую строку с действием. Если владелец не указан, оставь поле 'owner' пустым."
),
input=[{"type": "text", "text": raw_args}],
json_schema=_TASKS_SCHEMA,
schema_name="meeting.tasks",
purpose="paste-to-tasks",
temperature=0.0,
max_tokens=512,
)
if result.parsed is None:
return f"Не удалось разобрать ответ. Сырой вывод:\n{result.text}"
lines = [f"- [{t.get('owner') or '?'}] {t['action']}" for t in result.parsed["tasks"]]
return "\n".join(lines) or "(задачи не найдены)"
Третий рабочий пример, данный раз с изображением, находится в репозитории.
hermes-example-plugins
(сопутствующий репозиторий для эталонных плагинов — не входит в состав Hermes-Agent). Для асинхронного интерфейса (acomplete() /
acomplete_structured() с asyncio.gather()) см.
plugin-llm-async-example
в том же репозитории.
Когда что использовать
| Вам нужно… | Использовать |
|---|---|
| Свободный текстовый ответ (перевод, окончание, переписывание, генерация) | полный() |
| Многошаговый промпт (система + несколько примеров + пользователь) | полный() |
| Типизированный диктат, проверенный по схеме | complete_structured() |
| Ввод с изображением или текстом и типизированный текст на выходе | complete_structured() |
| Тот же вызов из асинхронного кода (адаптеры шлюзов, асинхронные хуки) | acomplete() / acomplete_structured() |
Все остальное — выбор провайдера, модели разрешения, аутентификация, запасной вариант, тайм-аут, маршрутизация изображений — одинаково для всех четырех.
API поверхности
ctx.llm — это экземпляр agent.plugin_llm.PluginLlm.
complete()
result = ctx.llm.complete(
messages=[{"role": "user", "content": "Привет"}],
provider=None, # опционально, шлюз — id провайдера Hermes (напр. "openrouter")
model=None, # опционально, шлюз — любая строка, которую ожидает этот провайдер
temperature=None,
max_tokens=None,
timeout=None, # секунды
agent_id=None, # опционально, шлюз
profile=None, # опционально, шлюз — явное имя профиля аутентификации
purpose="optional-audit-string",
)
# → PluginLlmCompleteResult(text, provider, model, agent_id, usage, audit)
Простое чат-завершение. messages имеет стандартную форму OpenAI — список слов {"role": "...", "content": "..."}. Многошаговые промпты (система + несколько пар пользователей/ассистент + конечный пользователь) работают точно так же, как и с OpenAI SDK.
provider= и model= независимы и имеют ту же форму, что и основная часть хоста (model.provider + model.model). Укажите только model=, чтобы использовать активный провайдер пользователя с другой моделью. Установите оба, чтобы полностью сменить провайдера. Любой из аргументов без согласия оператора вызывает PluginLlmTrustError.
complete_structured()
result = ctx.llm.complete_structured(
instructions="Что нужно извлечь.",
input=[
{"type": "text", "text": "..."},
{"type": "image", "data": b"...", "mime_type": "image/png"},
{"type": "image", "url": "https://..."},
],
json_schema={...}, # опционально — включает разобранный результат + валидацию
json_mode=False, # установите True без схемы, чтобы всё равно запросить JSON
schema_name=None, # опциональное человекочитаемое имя схемы
system_prompt=None,
provider=None, # опционально, шлюз
model=None, # опционально, шлюз
temperature=None,
max_tokens=None,
timeout=None,
agent_id=None,
profile=None,
purpose=None,
)
# → PluginLlmStructuredResult(text, provider, model, agent_id,
# usage, parsed, content_type, audit)
Входные данные — это типизированные текстовые или графические блоки (сырые байты автоматически кодируются в base64 как data: URL). Когда указан json_schema или json_mode=True, хост запрашивает JSON-вывод через response_format, локально разбирает его как запасной вариант и затем вашу схему, если выбран jsonschema.
result.content_type == "json"—result.parsed— это объект Python, ваша соответствующая схема.result.content_type == "text"— разбор или валидация не удалось; проверьтеresult.textдля получения необработанного ответа модели.
Асинхронные вызовы
result = await ctx.llm.acomplete(messages=...)
result = await ctx.llm.acomplete_structured(instructions=..., input=...)
Те же аргументы и результаты результатов, что и в синхронных аналогиях. Используйте их из адаптеров шлюзов, асинхронных хуков или любого плагина кода, уже работающего в цикле asyncio.
Атрибуты результата
@dataclass
class PluginLlmCompleteResult:
text: str # ответ ассистента
provider: str # например "openrouter", "anthropic"
model: str # то, что провайдер вернул для этого вызова
agent_id: str # чья модель/аутентификация использовалась
usage: PluginLlmUsage # токены + кэш + оценка стоимости
audit: Dict[str, Any] # plugin_id, purpose, profile
@dataclass
class PluginLlmStructuredResult(PluginLlmCompleteResult):
parsed: Optional[Any] # объект JSON, когда content_type == "json"
content_type: str # "json" или "text"
# audit также содержит schema_name, если он был указан
usage содержит input_tokens, output_tokens, total_tokens,
cache_read_tokens, cache_write_tokens и cost_usd, если провайдер получает эти поля.
Доверительный шлюз
Поведение по умолчанию — блокировка. Без блока конфигурации plugins.entries
может подключить:
- запускать любой из четырех методов с активным провайдером и моделью пользователя,
- сохранить аргументы формирования запроса (
temperature,max_tokens,таймаут,system_prompt,цель,сообщения,инструкции,вход,json_schema),
…и это всё. Аргументы provider=, model=, agent_id= и profile=
вызывает PluginLlmTrustError, пока оператор не даст согласия.
Большинству плагинов этот раздел никогда не нужен. Плагин, который просто вызывает ctx.llm.complete(messages=...) без переопределений, работает с темой, которая активно у пользователя, и не требует настройки. Приведенный ниже блок актуален только тогда, когда плагин хочет явно привязаться к другой модели или провайдеру, отличному от пользователя.
plugins:
entries:
my-plugin:
llm:
# Разрешить этому плагину выбирать другого провайдера Hermes
# (должен быть тем, которого Hermes уже знает — те же имена,
# что и в `hermes model` и config.yaml model.provider).
allow_provider_override: true
# Опционально ограничить список провайдеров. Используйте ["*"] для любых.
allowed_providers:
- openrouter
- anthropic
# Разрешить этому плагину запрашивать конкретную модель.
allow_model_override: true
# Опционально ограничить список моделей. Используйте ["*"] для любых.
# Модели сопоставляются буквально со строкой, которую отправляет
# плагин — Hermes ничего не ищет.
allowed_models:
- openai/gpt-4o-mini
- anthropic/claude-3-5-haiku
# Разрешить межагентные вызовы (редко).
allow_agent_id_override: false
# Разрешить плагину запрашивать конкретный сохранённый профиль
# аутентификации (например, другую учётную запись OAuth у того же провайдера).
allow_profile_override: false
Идентификатор плагина — это поле name: манифеста для плоских плагинов или ключ, полученный из пути, для вложенных плагинов (image_gen/openai,
memory/honcho и т.д.).
Что обеспечивает шлюз
| Переопределение | По умолчанию | Ключ конфигурации |
|---|---|---|
provider= |
запрещено | allow_provider_override: true |
| ↳ белый список | — | allowed_providers: [...] |
model= |
запрещено | allow_model_override: true |
| ↳ белый список | — | allowed_models: [...] |
agent_id= |
запрещено | allow_agent_id_override: true |
profile= |
запрещено | allow_profile_override: true |
Каждое переопределение защищено независимо. Предоставление allow_model_override не даёт автоматически allow_provider_override — плагину, которому доверяют выбор модели, всё равно запрещено менять провайдера, если он не получит также разрешение на провайдера.
Что шлюзу НЕ нужно контролировать
- Аргументы формирования запроса —
temperature,max_tokens,timeout,system_prompt,purpose,messages,instructions,input,json_schema,schema_name,json_mode— всегда разрешены; они не выбирают учётные данные или маршруты. - Политика запрета по умолчанию означает, что ненастроенный плагин всё ещё может выполнять полезную работу — он просто использует активного провайдера и модель. Операторам нужно думать о
plugins.entriesтолько для плагинов, которым нужна более точная маршрутизация.
Что принадлежит хосту
Полный список того, что ctx.llm делает для плагина, чтобы вам не приходилось:
- Разрешение провайдера. Читает
model.provider+model.modelиз конфигурации пользователя (или явные переопределения, когда доверено). - Аутентификация. Извлекает API-ключи, OAuth-токены или токены обновления из
~/.hermes/auth.json/ env, включая пул учётных данных, если он настроен. Плагин их никогда не видит. - Маршрутизация изображений. Когда предоставлено изображение и активная текстовая модель пользователя является только текстовой, хост автоматически переключается на настроенную модель для изображений.
- Цепочка запасных вариантов. Если основной провайдер пользователя возвращает 5xx или 429, запрос проходит через обычную цепочку запасных вариантов Hermes с учётом агрегатора, прежде чем вернуть ошибку плагину.
- Тайм-аут. Соблюдает аргумент
timeout=, возвращаясь к конфигурацииauxiliary.<task>.timeoutили глобальному значению по умолчанию для вспомогательных задач. - Формирование JSON. Отправляет
response_formatпровайдеру, когда вы запрашиваете JSON, затем повторно разбирает локально из ответа, заключённого в code-блок, если провайдер вернул такой. - Валидация схемы. Проверяет вашу
json_schema, если установленjsonschema; в противном случае записывает отладочную строку и пропускает строгую валидацию. - Журнал аудита. Каждый вызов записывает одну строку уровня INFO в
agent.logс идентификатором плагина, провайдером/моделью, целью и количеством токенов.
Что принадлежит плагину
- Форма запроса.
messagesдля чата,instructions+inputдля структурированного. Плагин строит промпт; хост его выполняет. - Схема. Любая форма, которую вы хотите получить. Хост не угадывает её за вас.
- Обработка ошибок.
complete_structured()вызываетValueErrorпри пустых входных данных и при неудаче валидации схемы.PluginLlmTrustErrorсрабатывает, когда доверительный шлюз отклоняет переопределение. Всё остальное (5xx провайдера, отсутствие настроенных учётных данных, тайм-аут) вызывает то, что вызываетauxiliary_client.call_llm(). - Стоимость. Каждый вызов выполняется с платным провайдером пользователя. Не вызывайте
complete()в цикле для каждого сообщения шлюза, не думая о расходе токенов.
Где это находится в поверхности плагина
Существующие методы ctx.* расширяют существующую подсистему Hermes:
| ctx.register_tool | добавляет инструмент, который может вызывать агент |
| ctx.register_platform | подключает новый адаптер шлюза |
| ctx.register_image_gen_provider | заменяет бэкенд генерации изображений |
| ctx.register_memory_provider | заменяет бэкенд памяти |
| ctx.register_context_engine | заменяет компрессор контекста |
| ctx.register_hook | наблюдает за событием жизненного цикла |
ctx.llm — это первая поверхность, позволяющая плагину запускать ту же модель, с которой общается пользователь, вне основного канала, без всего вышеперечисленного. Это его единственная задача. Если вашему плагину нужно зарегистрировать инструмент, который вызывает агент, используйте register_tool. Если ему нужно реагировать на событие жизненного цикла, используйте register_hook. Если ему нужно сделать собственный вызов модели — по любой причине, структурированный или нет — используйте ctx.llm.
Ссылки
- Реализация:
agent/plugin_llm.py - Тесты:
tests/agent/test_plugin_llm.py - Эталонные плагины (сопутствующий репозиторий):
plugin-llm-example— синхронное структурированное извлечение с изображениемplugin-llm-async-example— асинхронный вариант сasyncio.gather()- Вспомогательный клиент (движок под капотом): см. Provider Runtime.