Доступ плагина к 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": "Rewrite errors as one short sentence a non-engineer can act on."},
{"role": "user", "content": traceback_text},
],
max_tokens=64,
purpose="hooks.error-rewrite",
)
return result.text
purpose — это свободная аудируемая строка; она отображается в agent.log
и в result.audit, чтобы операторы могли видеть, какой плагин и какой
вызов совершил. Опционально, но рекомендуется для всего, что выполняется часто.
Структурированный вывод
Когда плагину нужен типизированный ответ, переключайтесь на структурированный канал:
result = ctx.llm.complete_structured(
instructions="Score this support reply for urgency (0–1) and pick a category.",
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. -
Ограниченный объём. Одиночный синхронный или асинхронный вызов. Ни стриминга, ни циклов с инструментами, ни состояния диалога для управления. Передайте входные данные, получите результат, верните.
-
Безопасный по умолчанию (fail-closed). Плагин, который вы никогда не настраивали, не может выбрать свой провайдер, модель, агента или сохранённые учётные данные. По умолчанию — «использовать то, что использует пользователь». Операторы дают согласие на конкретные переопределения, для каждого плагина, в
config.yaml.
Быстрый старт
Ниже приведены два полных плагина — один для чата, один для структурированного вывода. Оба размещаются
внутри одной функции register(ctx) и не требуют никакой внешней
настройки для работы с любой моделью, активной у пользователя.
Чат-завершение — /tldr
def register(ctx):
ctx.register_command(
name="tldr",
handler=lambda raw: _tldr(ctx, raw),
description="Summarise the supplied text in one paragraph.",
args_hint="<text>",
)
def _tldr(ctx, raw_args: str) -> str:
text = raw_args.strip()
if not text:
return "Usage: /tldr <text to summarise>"
result = ctx.llm.complete(
messages=[
{"role": "system",
"content": "Summarise the user's text in one tight paragraph. No preamble."},
{"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="Turn freeform meeting notes into structured tasks.",
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 "Usage: /paste-to-tasks <meeting notes>"
result = ctx.llm.complete_structured(
instructions=(
"Extract concrete action items from these meeting notes. "
"One task per actionable line. If no owner is named, leave 'owner' blank."
),
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"Couldn't parse a response. Raw output:\n{result.text}"
lines = [f"- [{t.get('owner') or '?'}] {t['action']}" for t in result.parsed["tasks"]]
return "\n".join(lines) or "(no tasks found)"
Третий рабочий пример, на этот раз с вводом изображения, находится в
репозитории
hermes-example-plugins
(сопутствующий репозиторий с эталонными плагинами — не входит в
состав hermes-agent). Для асинхронного интерфейса (acomplete() /
acomplete_structured() с asyncio.gather()) см.
plugin-llm-async-example
в том же репозитории.
Когда что использовать
| Вам нужно… | Используйте |
|---|---|
| Произвольный текстовый ответ (перевод, краткое содержание, переписывание, генерация) | complete() |
| Многошаговый промпт (системный + несколько примеров + пользователь) | complete() |
| Типизированный словарь с проверкой по схеме | complete_structured() |
| Ввод изображения или текста с типизированным словарём на выходе | complete_structured() |
| Тот же вызов из асинхронного кода (адаптеры шлюзов, асинхронные хуки) | acomplete() / acomplete_structured() |
Всё остальное — выбор провайдера, определение модели, аутентификация, fallback, таймаут, маршрутизация изображений — одинаково для всех четырёх методов.
API-поверхность
ctx.llm — это экземпляр agent.plugin_llm.PluginLlm.
complete()
result = ctx.llm.complete(
messages=[{"role": "user", "content": "Hi"}],
provider=None, # optional, gated — Hermes provider id (e.g. "openrouter")
model=None, # optional, gated — whatever string that provider expects
temperature=None,
max_tokens=None,
timeout=None, # seconds
agent_id=None, # optional, gated
profile=None, # optional, gated — explicit auth-profile name
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="What you want extracted.",
input=[
{"type": "text", "text": "..."},
{"type": "image", "data": b"...", "mime_type": "image/png"},
{"type": "image", "url": "https://..."},
],
json_schema={...}, # optional — triggers parsed result + validation
json_mode=False, # set True without a schema to ask for JSON anyway
schema_name=None, # optional human-readable schema name
system_prompt=None,
provider=None, # optional, gated
model=None, # optional, gated
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 # the assistant's response
provider: str # e.g. "openrouter", "anthropic"
model: str # whatever the provider returned for this call
agent_id: str # whose model/auth was used
usage: PluginLlmUsage # tokens + cache + cost estimate
audit: Dict[str, Any] # plugin_id, purpose, profile
@dataclass
class PluginLlmStructuredResult(PluginLlmCompleteResult):
parsed: Optional[Any] # JSON object when content_type == "json"
content_type: str # "json" or "text"
# audit also carries schema_name when supplied
usage содержит input_tokens, output_tokens, total_tokens,
cache_read_tokens, cache_write_tokens и cost_usd, если
провайдер возвращает эти поля.
Шлюз доверия
Поведение по умолчанию — fail-closed (безопасный отказ). Без блока конфигурации
plugins.entries плагин может:
-
запускать любой из четырёх методов против активного провайдера и модели пользователя,
-
устанавливать аргументы формирования запроса (
temperature,max_tokens,timeout,system_prompt,purpose,messages,instructions,input,json_schema),
…и это всё. Аргументы provider=, model=, agent_id= и profile=
вызывают PluginLlmTrustError до тех пор, пока оператор не даст согласие.
Большинству плагинов этот раздел не нужен. Плагин, который просто
вызывает ctx.llm.complete(messages=...) без переопределений, работает
с тем, что активно у пользователя, и не требует настройки. Блок ниже
актуален только когда плагину необходимо закрепить за собой
другую модель или провайдера, отличных от пользовательских.
plugins:
entries:
my-plugin:
llm:
# Allow this plugin to choose a different Hermes provider
# (must be one Hermes already knows about — same names as
# `hermes model` and config.yaml model.provider).
allow_provider_override: true
# Optionally restrict which providers. Use ["*"] for any.
allowed_providers:
- openrouter
- anthropic
# Allow this plugin to ask for a specific model.
allow_model_override: true
# Optionally restrict which models. Use ["*"] for any.
# Models are matched literally against whatever string the
# plugin sends — Hermes does not look anything up.
allowed_models:
- openai/gpt-4o-mini
- anthropic/claude-3-5-haiku
# Allow cross-agent calls (rare).
allow_agent_id_override: false
# Allow the plugin to request a specific stored auth profile
# (e.g. a different OAuth account on the same provider).
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, включая пул учётных данных, если он настроен. Плагин их никогда не видит. -
Маршрутизация изображений. Когда предоставлен ввод изображения и активная текстовая модель пользователя является только текстовой, хост автоматически переключается на настроенную модель для изображений.
-
Цепочка fallback. Если основной провайдер пользователя отвечает 5xx или 429, запрос проходит через обычный fallback-механизм Hermes с учётом агрегатора, прежде чем вернуть ошибку плагину.
-
Таймаут. Соблюдает ваш аргумент
timeout=, откатываясь к конфигурацииauxiliary.<task>.timeoutили глобальному значению aux по умолчанию. -
Формирование 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 | заменяет backend генерации изображений |
|| ctx.register_memory_provider | заменяет backend памяти |
|| ctx.register_context_engine | заменяет компрессор контекста |
|| ctx.register_hook | наблюдает за событием жизненного цикла |
ctx.llm — это первая поверхность, позволяющая плагину запускать ту же
модель, с которой общается пользователь, вне основного канала, без всего
вышеперечисленного. Это её единственная задача. Если вашему плагину нужно
зарегистрировать инструмент, который вызывает агент, используйте
register_tool. Если нужно реагировать на событие жизненного цикла, используйте
register_hook. Если нужно выполнить собственный
вызов модели — по любой причине, структурированный или нет — ctx.llm.
Справочные материалы
-
Реализация:
agent/plugin_llm.py -
Эталонные плагины (сопутствующий репозиторий):
plugin-llm-example— синхронное структурированное извлечение с вводом изображения-
plugin-llm-async-example— асинхронный вызов сasyncio.gather() -
Вспомогательный клиент (движок под капотом): см. Provider Runtime.