Доступ плагина к LLM
ctx.llm — это поддерживаемый способ для плагина выполнить LLM-вызов.
Завершение чата, структурированное извлечение, синхронно, асинхронно, с
изображениями или без — единый интерфейс, единый защитный шлюз, единые
учётные данные хоста.
Плагины обращаются к этому, когда им нужно сделать что-то, что включает модель, но не является частью диалога агента. Хук, который переписывает ошибку инструмента в понятную для не-инженера форму. Адаптер шлюза, который переводит входящее сообщение перед постановкой в очередь. Slash-команда, которая суммирует длинную вставку. Запланированная задача, которая оценивает вчерашнюю активность и пишет одну строку на доску статуса. Предварительный фильтр, который решает, стоит ли вообще будить агента для этого сообщения.
Это задачи, в которые агент не должен быть вовлечён. Им нужен один 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
purpose — это свободная строка для аудита — она отображается в
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-токены, процедуры обновления, пул
учётных данных, вспомогательные переопределения для задач — все
концепции учётных данных, которые уже есть в VibeOS, применимы.
Плагин никогда не видит токен; хост атрибутирует вызов обратно через
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-дата или пусто"},
},
"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 "(задачи не найдены)"
Третий рабочий пример, на этот раз с вводом изображения, находится в
репозитории
vibeos-example-plugins
(сопутствующий репозиторий для эталонных плагинов — не входит в
vibeos-agent). Для асинхронного интерфейса (acomplete() /
acomplete_structured() с asyncio.gather()) смотрите
plugin-llm-async-example
в том же репозитории.
Когда что использовать
| Вам нужно… | Используйте |
|---|---|
| Ответ в свободной текстовой форме (перевод, суммаризация, переписывание, генерация) | complete() |
| Многовитковый промпт (системный + несколько примеров + пользовательский) | complete() |
| Типизированный словарь, проверенный по схеме | 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 провайдера VibeOS (например, "openrouter")
model=None, # опционально, с ограничением — любая строка, которую ожидает провайдер
temperature=None,
max_tokens=None,
timeout=None, # секунды
agent_id=None, # опционально, с ограничением
profile=None, # опционально, с ограничением — явное имя профиля аутентификации
purpose="опциональная-строка-аудита",
)
# → 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 как URL data:). Когда
указаны 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,timeout,system_prompt,purpose,messages,instructions,input,json_schema);
…и это всё. Аргументы provider=, model=, agent_id= и profile=
вызывают PluginLlmTrustError, пока оператор не даст согласие.
Большинству плагинов этот раздел никогда не понадобится. Плагин,
который просто вызывает ctx.llm.complete(messages=...) без
переопределений, работает с тем, что активно у пользователя, и не
требует настройки. Блок ниже актуален только тогда, когда плагин
специально хочет закрепиться за другой моделью или провайдером,
отличным от пользовательского.
plugins:
entries:
my-plugin:
llm:
# Разрешить этому плагину выбирать другого провайдера VibeOS
# (должен быть тем, о котором VibeOS уже знает — те же имена,
# что и в `vibeos model` и config.yaml model.provider).
allow_provider_override: true
# Опционально ограничить список провайдеров. Используйте ["*"] для любых.
allowed_providers:
- openrouter
- anthropic
# Разрешить этому плагину запрашивать конкретную модель.
allow_model_override: true
# Опционально ограничить список моделей. Используйте ["*"] для любых.
# Модели сопоставляются буквально с той строкой, которую отправляет
# плагин — VibeOS ничего не ищет.
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-токены или токены
обновления из
~/.vibeos/auth.json/ env, включая пул учётных данных, если он настроен. Плагин их никогда не видит. - Маршрутизация изображений. Когда предоставлен ввод изображения и активная текстовая модель пользователя является только текстовой, хост автоматически переключается на настроенную модель для изображений.
- Цепочка запасных вариантов. Если основной провайдер пользователя возвращает 5xx или 429, запрос проходит через обычную цепочку запасных вариантов VibeOS с учётом агрегатора, прежде чем вернуть ошибку плагину.
- Тайм-аут. Соблюдает ваш аргумент
timeout=, возвращаясь к конфигурацииauxiliary.<task>.timeoutили глобальному значению по умолчанию для вспомогательных задач. - Формирование JSON. Отправляет
response_formatпровайдеру, когда вы запрашиваете JSON, затем повторно парсит локально из ответа, заключённого в код-фенс, если провайдер вернул такой. - Валидация схемы. Проверяет по вашей
json_schema, если установленjsonschema; записывает отладочную строку и пропускает строгую валидацию в противном случае. - Журнал аудита. Каждый вызов записывает одну строку INFO в
agent.logс идентификатором плагина, провайдером/моделью, назначением и общим количеством токенов.
Что принадлежит плагину
- Форма запроса.
messagesдля чата,instructions+inputдля структурированного вывода. Плагин строит промпт; хост его выполняет. - Схема. Любая форма, которую вы хотите получить. Хост не угадывает её за вас.
- Обработка ошибок.
complete_structured()вызываетValueErrorпри пустых входных данных и при ошибке валидации схемы.PluginLlmTrustErrorсрабатывает, когда защитный шлюз отклоняет переопределение. Всё остальное (5xx провайдера, отсутствие настроенных учётных данных, тайм-аут) вызывает то, что вызываетauxiliary_client.call_llm(). - Стоимость. Каждый вызов выполняется с использованием платного
провайдера пользователя. Не зацикливайтесь на
complete()для каждого сообщения шлюза, не думая о расходе токенов.
Где это вписывается в поверхность плагина
Существующие методы ctx.* расширяют существующую подсистему VibeOS:
| 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.