SAGEA LogoDocs & API
Studio

Structured outputs

Structured outputs turn free-form chat into data your code can trust — invoices, forms, and tickets parsed as JSON every time.

In this guide

  • Design a schema before you write the prompt
  • Request json_object mode for strict JSON replies
  • Validate responses and repair them automatically
  • Extract Nepali invoice fields end to end

Start with the schema, not the prompt

Decide the exact keys, types, and required fields first. Small, flat schemas with explicit formats (YYYY-MM-DD, NPR numbers) fail far less often than clever nested ones.

For the invoice case below, the contract is fixed: merchant as string, date as ISO date, total_npr as number, and line_items as an array of name plus price objects.

INVOICE_SCHEMA = {
    "type": "object",
    "required": ["merchant", "date", "total_npr", "line_items"],
    "properties": {
        "merchant": {"type": "string"},
        "date": {"type": "string", "format": "date"},
        "total_npr": {"type": "number"},
        "line_items": {
            "type": "array",
            "items": {
                "type": "object",
                "required": ["name", "price_npr"],
                "properties": {
                    "name": {"type": "string"},
                    "price_npr": {"type": "number"},
                },
            },
        },
    },
}

Paste this schema into your prompt description so the model sees field names verbatim. Keep key names snake_case English even when values are Nepali — it simplifies downstream code.

Ask for JSON object mode

Set response_format to json_object so the model returns parseable JSON instead of chatty prose. State the schema in the system message and give one short example of the shape you expect.

curl -X POST https://api.sagea.space/v1/chat/completions \
  -H "Authorization: Bearer $SAGEA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "sage-2-4-actus",
    "temperature": 0.2,
    "response_format": {"type": "json_object"},
    "messages": [
      {"role": "system", "content": "Extract invoices as JSON with keys: merchant, date, total_npr, line_items[{name, price_npr}]. Return JSON only."},
      {"role": "user", "content": "BhatBhateni, 2026-03-14, Dal 350, Chamal 1200, Total NPR 1550"}
    ]
  }'

Expected shape for the call above:

{
  "merchant": "BhatBhateni",
  "date": "2026-03-14",
  "total_npr": 1550,
  "line_items": [
    {"name": "Dal", "price_npr": 350},
    {"name": "Chamal", "price_npr": 1200}
  ]
}

See request and response fields in the Chat Completions reference.

Validate, then repair

Never trust raw model JSON. Parse it, validate types and totals, and on failure send the error back for a one-shot repair call. This validate-then-repair loop fixes most malformed replies without human review.

import json
 
def parse_invoice(text):
    data = json.loads(text)
    assert isinstance(data["total_npr"], (int, float)), "total_npr must be numeric"
    summed = sum(item["price_npr"] for item in data["line_items"])
    assert summed == data["total_npr"], f"line total {summed} != total {data['total_npr']}"
    return data
 
try:
    invoice = parse_invoice(response.choices[0].message.content)
except (ValueError, AssertionError, KeyError) as err:
    repair = client.chat.completions.create(
        model="sage-2-5-celer",
        temperature=0.2,
        response_format={"type": "json_object"},
        messages=[
            {"role": "system", "content": "Return valid invoice JSON only."},
            {"role": "user", "content": f"Fix this JSON: {response.choices[0].message.content}\nError: {err}"},
        ],
    )
    invoice = parse_invoice(repair.choices[0].message.content)

Limit repairs to one retry, then route to a human queue. Log both attempts so you can tighten the prompt or schema.

Extract Nepali invoice text

Real receipts mix Nepali and English: "भाटभटेनी, मिति २०८२-१२-०२, दाल रु ३५०". Normalize first — ask the model to transliterate merchant names, convert Bikram Sambat dates to ISO when possible, and strip currency symbols into plain total_npr numbers.

Use sage-2-4-actus for messy handwriting-OCR text and sage-2-5-celer for clean, high-volume POS lines. When sourcing text from scans, pair this guide with Process documents.

Best practices

  • Keep temperature at 0.2 or below for extraction tasks.
  • Demand JSON-only replies — no markdown fences or commentary.
  • Validate enums, dates, and NPR arithmetic in code, not in prose.
  • Version your schemas (invoice_v1) so prompts and parsers evolve together.
  • Sample and review 5% of outputs weekly to catch drift.

What's next

On this page