Structured outputs & tool calling

Free-form text is great for chat, but your code needs data: a category, an amount, a date. Modern LLM APIs offer two features for this: structured outputs (the model returns JSON matching a schema) and tool calling (the model asks your code to run a function and uses the result).

Structured outputs with a schema

Describe the shape you want as JSON Schema (or a Pydantic model), send it with the request, then validate the reply. The example uses the OpenAI-compatible Python SDK style, which many providers and local servers support; check your provider's docs because parameter names and model names change over time.

from openai import OpenAI
from pydantic import BaseModel, ValidationError

class Expense(BaseModel):
    vendor: str
    amount_inr: float
    gst_percent: float | None
    category: str

client = OpenAI(base_url="https://api.example-llm.com/v1", api_key="...")  # any compatible endpoint
resp = client.chat.completions.create(
    model="your-model-name",
    messages=[
        {"role": "system", "content": "Extract the expense. Reply only with JSON."},
        {"role": "user", "content": "Paid Sharma Stationers Rs 1,180 incl 18% GST for printer paper"},
    ],
    response_format={"type": "json_schema", "json_schema": {
        "name": "expense", "schema": Expense.model_json_schema()}},
    temperature=0,
)
try:
    expense = Expense.model_validate_json(resp.choices[0].message.content)
except ValidationError as err:
    expense = None      # retry once with the error message, then fall back to a human

Even with schema enforcement, validate business rules yourself (amount > 0, category in your allowed list).

Tool (function) calling

You describe tools with a name, description and JSON Schema for arguments. The model does not execute anything; it returns a request like {"name": "get_order_status", "arguments": "{...}"}. Your code runs the function and sends the result back in a follow-up message, and the model then writes the final answer.

import json

TOOLS = [{"type": "function", "function": {
    "name": "get_order_status",
    "description": "Look up a customer's order by order ID",
    "parameters": {"type": "object",
                   "properties": {"order_id": {"type": "string"}},
                   "required": ["order_id"]},
}}]

def get_order_status(order_id: str) -> dict:
    return {"order_id": order_id, "status": "Shipped", "city": "Kolkata"}   # really: a DB query

messages = [{"role": "user", "content": "Where is my order A-1042?"}]
first = client.chat.completions.create(model="your-model-name", messages=messages, tools=TOOLS)
msg = first.choices[0].message
if msg.tool_calls:
    messages.append(msg)
    for call in msg.tool_calls:
        args = json.loads(call.function.arguments)
        result = get_order_status(**args) if call.function.name == "get_order_status" else {"error": "unknown tool"}
        messages.append({"role": "tool", "tool_call_id": call.id, "content": json.dumps(result)})
    final = client.chat.completions.create(model="your-model-name", messages=messages, tools=TOOLS)
    print(final.choices[0].message.content)

The same idea over plain HTTP

Under the hood this is just a POST with JSON; the request body contains model, messages, tools and the response contains choices[0].message.tool_calls. Any language with an HTTP client can do it.

Feature Use for
Structured output Extraction, classification, form filling
Tool calling Fetching live data, taking actions, calculations
Plain text Explanations, drafts, chat replies

Common mistakes

  • Trusting tool arguments blindly. Check the user is allowed to see order A-1042 before querying.
  • Letting the model call dangerous tools (refunds, deletes) without confirmation.
  • Vague tool descriptions; the model chooses tools based on them.
  • Not handling the case where the model answers directly instead of calling a tool.

Practice

Build a GST invoice extractor: define a Pydantic model (GSTIN, invoice number, date, taxable value, CGST, SGST, total), extract from five sample invoice texts, and add a check that CGST + SGST + taxable value equals the total within INR 1.

B

Written by Bablu Kumar Jha

Founder, BeeProton AI · Mumbai, Maharashtra, India