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

Что даёт этот канал

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

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

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

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

Что берёт на себя хост

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

Что остаётся плагину

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

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

Справочные материалы