Application code often needs named fields rather than an answer that must be parsed from free-form prose. LlamaIndex can bind an LLM response to a Pydantic model so routing, extraction, and agent logic receive a validated Python object.
The as_structured_llm() wrapper attaches the model class to an LLM. A completion then exposes the validated object through response.raw, while response.text remains the serialized response text.
A deterministic custom LLM keeps the schema handoff test independent of a provider key. The same TicketSummary model and wrapper apply to a provider-backed LLM; only the LLM construction changes.
$ python -m pip install llama-index-core pydantic Collecting llama-index-core ##### snipped ##### Successfully installed llama-index-core-0.14.23 ##### snipped #####
A provider-backed LLM also requires its integration package in the same environment.
Related: How to install LlamaIndex with pip
import json from collections.abc import Generator from typing import Any from llama_index.core.llms import CompletionResponse, CustomLLM, LLMMetadata from pydantic import BaseModel, Field class TicketSummary(BaseModel): team: str = Field(description="Team that should handle the ticket") priority: str = Field(description="Routing priority") next_action: str = Field(description="Next action for the team")
class TicketRouterLLM(CustomLLM): @property def metadata(self) -> LLMMetadata: return LLMMetadata(model_name="ticket-router", is_chat_model=False) def complete( self, prompt: str, formatted: bool = False, **kwargs: Any ) -> CompletionResponse: if "duplicate invoice" not in prompt.lower(): return CompletionResponse( text='{"team":"support","priority":"normal",' '"next_action":"review ticket"}' ) return CompletionResponse( text='{"team":"billing","priority":"high",' '"next_action":"review duplicate invoice"}' ) def stream_complete( self, prompt: str, formatted: bool = False, **kwargs: Any ) -> Generator[CompletionResponse, None, None]: yield self.complete(prompt, formatted=formatted, **kwargs)
The two prompt paths make the adapter deterministic while preserving an input-dependent result. A provider LLM replaces TicketRouterLLM in production.
structured_llm = TicketRouterLLM().as_structured_llm( output_cls=TicketSummary ) response = structured_llm.complete( "Route this ticket: customer reports duplicate invoice INV-1042." ) ticket = response.raw
response.raw holds the validated TicketSummary instance. Invalid or missing fields cause Pydantic validation to fail instead of silently returning an incomplete object.
assert isinstance(ticket, TicketSummary) assert ticket.team == "billing" assert ticket.priority == "high" print(f"structured_type={type(ticket).__name__}") print(json.dumps(ticket.model_dump(), indent=2))
$ python structured_output.py
structured_type=TicketSummary
{
"team": "billing",
"priority": "high",
"next_action": "review duplicate invoice"
}
The assertions stop execution when response.raw is not the expected model or when the routed fields differ from the requested ticket.