Agent LLM Interface — Client Developer Documentation
Introduction
Agents call LLMs and other AI models through two libraries:
| Library | Package | Use it for |
|---|---|---|
| LLM Interface | agents_llm (libs/llm-interface) |
Full inference client. Calls AIOS blocks over gRPC or REST, AIOS vDAGs, OpenAI, or custom backends. Supports infer, chat completions, completions, multipart inference, async calls, block pre/post-processing policies, latency metrics and LLM-based selection |
| LLM Interface Lite | agents_llm_lite (libs/llm-interface-lite) |
A simple prompt-in / text-out registry of inference providers (AIOS or OpenAI). Includes generate-and-execute for code |
A typical workflow is:
- Declare a ModelItem in the Subject Specification (optional) to record which models the agent uses.
- Create a client and register one or more backends under names.
- Run inference by backend name.
Registering LLMs in Subject Specification
Models can be recorded in the subject's integrations.models list using ModelItem.
Data Class Reference
from dataclasses import dataclass, field
from typing import Dict, Any
@dataclass
class ModelItem:
llm_type: str
llm_block_id: str
llm_selection_query: Dict[str, Any] = field(default_factory=dict)
llm_parameters: Dict[str, Any] = field(default_factory=dict)
Example Configuration
{
"llm_type": "inference",
"llm_block_id": "gemma3-27b-block",
"llm_selection_query": {},
"llm_parameters": {
"network_host": "34.93.251.223",
"inference_server_id": "inference-server-1",
"mode": "auto"
}
}
Field Descriptions
| Field | Type | Description |
|---|---|---|
| llm_type | str |
Model kind, e.g. "inference". |
| llm_block_id | str |
AIOS block ID (model) or provider model name. |
| llm_selection_query | dict |
Optional selector for picking a deployment. |
| llm_parameters | dict |
Backend settings, e.g. network_host, inference_server_id, mode. |
The agents SDK does not build LLM clients from
ModelItementries automatically. Read them fromself.subject.integrations.modelsand register backends yourself (see below).
Part 1 — agents_llm (LLM Interface)
Import and Setup
from agents_llm import AIGridClient
from agents_llm.custom import AbstractBlockInferenceSystem, OpenAIBlockInferenceSystem
client = AIGridClient()
AIGridClient keeps a registry of inference systems by name. Methods called without name= use the system registered as "default".
Registering Backends
A. AIOS block (gRPC or REST)
client.add_block(
name="gemma",
network_host="34.93.251.223", # host of the AIOS registries
inference_server_id="inference-server-1",
model="gemma3-27b-block", # block ID
mode="auto", # "auto" | "grpc" | "rest"
)
How the block is resolved:
- Inference server. If
inference_server_idis set, the server record is fetched from the inference server registry athttp://<network_host>:30105. Itsinference_server_urlsgive the REST URL (rest/http/https, elseinference_server_public_url) and the gRPC address (grpc/grpc_address/grpc_url). - Direct URLs. If
inference_server_id="", passurls={"rest": "http://...:31504"}and/orurls={"grpc": "host:port"}instead. - Transport.
mode="auto"picks gRPC when available, else REST. Asking for a transport that isn't available raisesRuntimeError. - Block data. The block record is fetched from the blocks registry at
http://<network_host>:30100. If it defines policies namedagents_pre_processing/agents_post_processing, they run on every request payload and response.
# Direct REST URL, no inference server registry lookup
client.add_block(
name="gemma-rest",
network_host="34.93.251.223",
inference_server_id="",
model="gemma3-27b-block",
mode="rest",
urls={"rest": "http://35.232.150.117:31504"},
)
default_headers (optional) are sent to the inference server registry.
B. AIOS vDAG
client.add_vdag(
name="pipeline",
registry_base_url="http://<vdag-controller-registry>",
vdag_uri="my-vdag:1.0-stable",
mode="auto",
)
All vDAG controllers registered for vdag_uri are listed and one is picked at random. Its REST and gRPC endpoints come from config.urls (rest/http/https, else public_url; grpc, else config.grpc_address/config.grpc_url).
C. OpenAI
openai_llm = OpenAIBlockInferenceSystem(
model="gpt-4o-mini", # default
api_key=None, # default: OPENAI_API_KEY (required)
base_url=None, # optional OpenAI-compatible endpoint
organization=None,
project=None,
)
client.add_custom_block(name="openai", system=openai_llm)
- Only OpenAI-supported keys are forwarded from
data. Chat:messages, temperature, top_p, max_tokens, n, stop, presence_penalty, frequency_penalty, logit_bias, user, tools, tool_choice, response_format, seed, metadata. Text: the same, minustools/tool_choice/response_format, plusprompt. infer()routes to chat whendatacontainsmessages(or is a list of role/content dicts), and to text completions when it containsprompt(or is a string or list). If the model does not support text completions, the call is retried as a chat completion.- Multipart inference is not supported.
D. Custom backends
Managing backends
client.list() # ['gemma', 'openai', ...]
system = client.get("gemma")
client.remove("gemma")
Running Inference
Chat completions
resp = client.chat_completions(
name="openai",
session_id="sess-001",
messages=[{"role": "user", "content": "Explain quantum computing in one paragraph."}],
data={"temperature": 0.2, "max_tokens": 256},
)
print(resp["choices"][0]["message"]["content"])
For AIOS blocks, messages is merged into data (data["messages"] = messages). The pre-processing policy (if any) runs on data, and the result is sent to POST /v1/chat/completions (REST) or the gRPC chat_completions call. Generation settings for the block go in data, for example:
resp = client.chat_completions(
name="gemma-rest",
session_id="session-1214",
messages=[{"role": "user", "content": "What is 2 + 2?"}],
data={
"mode": "chat",
"generation_config": {"max_new_tokens": 512, "do_sample": False, "top_p": 0.95, "temperature": 1.0},
},
)
Completions
resp = client.completions(name="gemma", session_id="sess-002",
prompts="Write a haiku about the ocean", data={})
For AIOS blocks, always pass a dict as data (the prompt is added to it as data["prompt"]).
client.completions(...)currently fails with aTypeErrorfor OpenAI and other custom systems:AIGridClientalways passesdata=, whichAbstractBlockInferenceSystem.completionsdoes not accept. Call the system directly instead —client.get("openai").completions(prompt="...", session_id="sess-002")— or use chat completions.
Raw inference
resp = client.infer(name="gemma", session_id="sess-003", data={"mode": "chat", "message": "Hello"})
For AIOS blocks this calls POST /v1/infer (or gRPC infer) with model, session_id, seq_no, data, files, graph and selection_query.
Multipart inference (files)
resp = client.infer_multipart(name="gemma-rest", session_id="sess-004", data={"mode": "vision"})
Multipart is only available over REST (POST /v1/infer-multipart).
Full-parameter calls
The simplified methods above always send seq_no=0 and no files, graph, selection_query, frame_ptr or output_ptr. To set these, use the internal_* methods. They pass keyword arguments straight to the backend:
client.internal_infer(
name="gemma",
session_id="sess-005",
seq_no=7,
data={"mode": "chat", "message": "Describe this image"},
files=["/tmp/cat.png"],
selection_query={},
graph={},
)
vDAG systems take a smaller set of arguments (for example,
infer(session_id, seq_no, data, files, frame_ptr, ts)), so call them throughinternal_*methods orclient.get(name)rather than the simplified methods.
Async calls
Every call has an async variant (async_infer, async_chat_completions, async_completions, async_infer_multipart, plus internal_async_*). Each runs in a background thread and returns a ResultWaiter:
waiter = client.async_chat_completions(
name="openai",
session_id="sess-006",
messages=[{"role": "user", "content": "What is 2 + 2?"}],
data={},
)
waiter.ready() # True once finished
resp = waiter.wait(timeout=30) # raises TimeoutError, or re-raises the call's exception
Metrics
Each system keeps rolling latency metrics:
client.get("gemma").get_metrics()
# {"count": 12, "last_latency": 0.41, "min_latency": 0.32, "max_latency": 1.2, "avg_latency": 0.47}
LLM-based Selection (SelectionEngine)
SelectionEngine asks a model to pick the single best candidate for a query. Candidates are any objects with an id and a get_searchable_representation() method.
from dataclasses import dataclass
from typing import List
@dataclass
class Shoe:
id: str
title: str
tags: List[str]
def get_searchable_representation(self) -> str:
return f"title={self.title}; tags={','.join(self.tags)}"
engine = client.create_selector(name="openai", base_query="Select the best product for the user.")
result = engine.select(
entities=[Shoe("A1", "Red Running Shoes", ["running"]), Shoe("B2", "Trail Shoes", ["trail"])],
user_query="I need running shoes for daily jogging.",
session_id="sess-007",
extra_data={"max_tokens": 64},
)
result.selected_id # "A1"
result.raw_json # {"id": "A1"}
The engine sends a chat completion with strict JSON-only instructions and response_format={"type": "json_object"}, temperature=0.0, max_tokens=32 (overridable through extra_data). It parses {"id": ...} from the reply. ValueError is raised if the reply isn't valid JSON or the ID is not one of the candidates.
Writing a Custom Backend
Subclass AbstractBlockInferenceSystem and implement the four _do_* hooks. The base class provides the public infer / chat_completions / completions / infer_multipart methods, their async variants, and latency metrics.
from typing import Any, Dict, List, Optional
from agents_llm.custom import AbstractBlockInferenceSystem
class EchoLLM(AbstractBlockInferenceSystem):
def __init__(self):
super().__init__(model="echo")
def _do_infer(self, *, session_id, seq_no, data, files, selection_query, graph, frame_ptr, output_ptr):
return {"reply": data}
def _do_chat_completions(self, *, messages, session_id, seq_no, selection_query, graph,
frame_ptr, output_ptr, data=None):
last = messages[-1]["content"] if messages else ""
return {"choices": [{"message": {"role": "assistant", "content": last}}]}
def _do_completions(self, *, prompt, session_id, seq_no, selection_query, graph,
frame_ptr, output_ptr, data=None):
return {"choices": [{"text": prompt}]}
def _do_infer_multipart(self, *, session_id, seq_no, data, ts, frame_ptr, files):
raise RuntimeError("not supported")
client.add_custom_block(name="echo", system=EchoLLM())
Optional hooks:
_finalize_mode()— resolvemode("auto"→"grpc"/"rest") at construction._check_mode(operation)— reject operations the transport doesn't support.
Block Discovery
BlockSearchClient queries the AIOS search service for blocks:
from agents_llm.discovery import BlockSearchClient
search = BlockSearchClient("http://<search-service>")
search.filter_query(filter={"tags": "llm"}) # POST /api/filter
search.search(policyRuleURI="policies/rank-blocks:1.0-stable", # POST /api/search
filter={"tags": "llm"}, policy_params={})
Part 2 — agents_llm_lite (LLM Interface Lite)
agents_llm_lite is a registry of simple text-generation providers. Every provider implements:
infer(*, prompt, callback, temperature=0.1, top_p=0.95, max_tokens=4096, system_message=None, **kwargs) -> str
callback(raw_response) -> str extracts the text from the provider's raw response. Markdown fences are stripped from the result.
from agents_llm_lite.inference import AgentInferenceManager
from agents_llm_lite.custom import BaseInference, OpenAIInference
llm = AgentInferenceManager()
AIOS provider
llm.register_new_inference(
name="aios",
model="magistral-small-2506-llama-cpp-block",
inference_server_id="http://35.232.150.117:31504", # or a registry ID
aios_url_map={"inference_server_url": "http://<registry>", "blocks_db_url": "http://<blocks-db>"},
system_message="You are a concise assistant.",
)
text = llm.infer(
name="aios",
prompt="Summarize the benefits of vector databases.",
callback=lambda raw: raw["data"]["reply"],
)
The request is POST /v1/infer with data = {"mode": "chat", "gen_params": {temperature, top_p, max_tokens}, "message": prompt, "system_message": ...}.
OpenAI provider
llm.register_custom_inference(name="openai", provider=OpenAIInference(model="gpt-4o-mini"))
text = llm.infer(
name="openai",
prompt="Write a haiku about the ocean",
callback=lambda resp: resp.choices[0].message.content,
)
OpenAIInference uses OPENAI_API_KEY unless you pass client=OpenAI(...). It sends max_tokens as max_completion_tokens, and does not forward temperature or top_p.
Generate and execute
infer_and_execute works like the Code Generator SDK's generate_and_execute: it generates code, installs declared requirements, loads the code as a module, and calls main (or class_name().method_name, or a single callable class):
result = llm.infer_and_execute(
name="aios",
prompt="Write `def main(a, b)` returning the element-wise sum of two lists.",
callback=lambda raw: raw["data"]["reply"],
args=[[1, 2], [3, 4]],
)
Managing providers
llm.list_inferences()
llm.has_inference("openai")
llm.get_inference("openai")
llm.unregister_inference("openai")
llm.clear_inferences()
Custom providers subclass BaseInference (keyword-only model, session_id, logger, default_system_message) and implement infer. Lite providers can also be used as the llm of AgentRAG (see Accessing Vector Databases).
Best Practices
- Keep API keys in environment variables (
OPENAI_API_KEY) rather than in code or subject specs. - Use
async_*calls withResultWaiter.wait(timeout=...)to bound waiting time. - Use block pre/post-processing policies to adapt payloads per model without changing agent code.
- Use
get_metrics()to monitor backend latency.