플러그인 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(), 타입이 있는 JSON에는complete_structured(), asyncio 코드에는acomplete()와acomplete_structured()를 사용합니다. 인수와 결과 객체의 개념은 동일합니다. - 호스트 소유 자격 증명. OAuth 토큰, 리프레시 흐름, 자격 증명 풀,
작업별 보조 설정까지 Hermes가 이미 가진 인증 개념이 그대로 적용됩니다.
플러그인은 토큰을 직접 보지 않으며, 호스트가
result.audit를 통해 호출 출처를 기록합니다. - 범위가 제한된 호출. 동기 또는 비동기 단일 호출만 수행합니다. 스트리밍, 도구 루프, 대화 상태 관리는 없습니다. 입력을 지정하고 결과를 받아 반환합니다.
- 기본 차단형 신뢰 모델. 별도로 설정하지 않은 플러그인은 자체 제공자,
모델, 에이전트, 저장된 자격 증명을 선택할 수 없습니다. 기본 동작은
"사용자가 쓰는 모델을 그대로 사용"하는 것입니다. 운영자는
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() |
| 다중 턴 프롬프트(시스템 + few-shot 예시 + 사용자) | complete() |
| 스키마로 검증된 타입 있는 dict 응답 | complete_structured() |
| 이미지 또는 텍스트 입력에서 타입 있는 dict 응답 추출 | complete_structured() |
| 비동기 코드(게이트웨이 어댑터, 비동기 후크)의 동일한 호출 | acomplete() / acomplete_structured() |
그 밖의 제공자 선택, 모델 해석, 인증, 폴백, 타임아웃, 비전 라우팅은 네 메서드 모두에서 동일하게 처리됩니다.
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)
일반 Chat Completions 호출입니다. messages는 표준 OpenAI 형식인
{"role": "...", "content": "..."} dict 목록입니다. 시스템 메시지,
few-shot 사용자/어시스턴트 쌍, 최종 사용자 메시지를 포함한 다중 턴 프롬프트도
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를 제공하면 호스트는 response_format으로 JSON 출력을 요청하고,
필요하면 로컬에서 다시 파싱한 뒤 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
플러그인 ID는 플랫 플러그인의 매니페스트 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을 읽습니다. - 인증.
~/.hermes/auth.json또는 환경 변수에서 API 키, OAuth 토큰, 리프레시 토큰을 가져오며, 설정된 경우 자격 증명 풀도 사용합니다. 플러그인은 이 값을 직접 볼 수 없습니다. - 비전 라우팅. 이미지 입력이 제공됐지만 사용자의 활성 텍스트 모델이 텍스트 전용이면, 호스트가 설정된 비전 모델로 자동 라우팅합니다.
- 폴백 체인. 사용자의 기본 제공자가 5xx 또는 429를 반환하면, 플러그인에 오류를 돌려주기 전에 Hermes의 일반 aggregator-aware 폴백을 거칩니다.
- 타임아웃.
timeout=인수를 존중하고, 없으면auxiliary.<task>.timeout설정 또는 전역 보조 기본값으로 대체합니다. - JSON 형태 지정. JSON을 요청하면 제공자에
response_format을 보내고, 제공자가 코드 펜스에 감싼 응답을 반환하면 로컬에서 다시 파싱합니다. - 스키마 검증.
jsonschema가 설치되어 있으면json_schema로 검증합니다. 설치되어 있지 않으면 디버그 로그를 남기고 엄격 검증을 건너뜁니다. - 감사 로그. 각 호출은
agent.log에 플러그인 ID, 제공자/모델, 목적, 토큰 합계가 포함된 INFO 줄을 하나 남깁니다.
플러그인이 소유하는 것
- 요청 형태. 채팅에는
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 | image-gen 백엔드를 대체합니다. |
| 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()과 비동기
- 보조 클라이언트(후드 아래의 엔진): 참조 제공자 런타임.