Доступ к 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 содержит необработанный ответ.

Что дает этот режим

Быстрый старт

Ниже приведены два полных плагина — один чат, один структурированный. Оба работают внутри одной функции 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 = 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 может подключить:

…и это всё. Аргументы 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 — плагину, которому доверяют выбор модели, всё равно запрещено менять провайдера, если он не получит также разрешение на провайдера.

Что шлюзу НЕ нужно контролировать

Что принадлежит хосту

Полный список того, что ctx.llm делает для плагина, чтобы вам не приходилось:

Что принадлежит плагину

Где это находится в поверхности плагина

Существующие методы 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.

Ссылки