Docs
Guides

Structured outputs

Get reliable JSON from Tase models: JSON mode with response_format, schema instructions in the system message, validation with retries, and when to use tool calling instead.

Chat is only half of what a language model is good for. The other half is turning messy human input into data your code can act on — a task with a due date, an expense with an amount and category, a form filled from a voice note. That requires the model to answer in JSON your program can parse every single time. This guide shows how to make that reliable.

JSON mode with response_format

Set response_format to { "type": "json_object" } and the model is constrained to emit syntactically valid JSON — no prose, no markdown fences, no trailing commentary.

curl https://api.tase.app/v1/chat/completions \
  -H "Authorization: Bearer tase_sk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "model": "tase-8b",
    "response_format": { "type": "json_object" },
    "messages": [
      {
        "role": "system",
        "content": "Extract task details from the user message. Reply with JSON only, using exactly these keys: title (string), due_date (YYYY-MM-DD or null), priority (one of: low, medium, high)."
      },
      {
        "role": "user",
        "content": "Remind me to renew my passport by August 15, it is urgent"
      }
    ]
  }'

The reply arrives in the usual place, choices[0].message.content, as a JSON string ready to parse:

{"title": "Renew passport", "due_date": "2026-08-15", "priority": "high"}

JSON mode guarantees syntax, not shape

The constraint ensures the output parses as JSON. It does not ensure the JSON has your keys, your types, or your enum values — that comes from your instructions. Always describe the schema in the prompt, and always mention JSON explicitly in your messages when using json_object mode.

Put the schema in the system message

The schema description is the contract. Be literal: name every key, give its type, list allowed values for enums, and say what to do when a value is unknown. A compact convention that works well:

Extract task details from the user message. Reply with JSON only,
using exactly these keys:

- title: string, a short imperative phrase
- due_date: string in YYYY-MM-DD format, or null if no date is mentioned
- priority: one of "low", "medium", "high"; default to "medium"

Do not add keys. Do not wrap the JSON in markdown.
Say what to output when data is missing (null, empty string, a default). Models fill gaps with guesses unless you give them an explicit escape hatch.

SDK examples

import OpenAI from 'openai';

const client = new OpenAI({
  apiKey: process.env.TASE_API_KEY,
  baseURL: 'https://api.tase.app/v1',
});

const completion = await client.chat.completions.create({
  model: 'tase-8b',
  response_format: { type: 'json_object' },
  temperature: 0,
  messages: [
    {
      role: 'system',
      content:
        'Extract task details from the user message. Reply with JSON only, ' +
        'using exactly these keys: title (string), due_date (YYYY-MM-DD or null), ' +
        'priority (one of: low, medium, high).',
    },
    { role: 'user', content: 'Remind me to renew my passport by August 15, it is urgent' },
  ],
});

const task = JSON.parse(completion.choices[0].message.content);
console.log(task.title, task.due_date, task.priority);
import json
import os

from openai import OpenAI

client = OpenAI(
    api_key=os.environ["TASE_API_KEY"],
    base_url="https://api.tase.app/v1",
)

completion = client.chat.completions.create(
    model="tase-8b",
    response_format={"type": "json_object"},
    temperature=0,
    messages=[
        {
            "role": "system",
            "content": (
                "Extract task details from the user message. Reply with JSON only, "
                "using exactly these keys: title (string), due_date (YYYY-MM-DD or null), "
                "priority (one of: low, medium, high)."
            ),
        },
        {"role": "user", "content": "Remind me to renew my passport by August 15, it is urgent"},
    ],
)

task = json.loads(completion.choices[0].message.content)
print(task["title"], task["due_date"], task["priority"])

Use temperature 0 for extraction

Structured extraction wants determinism, not creativity. A low temperature makes the same input produce the same JSON, which makes failures reproducible and tests meaningful.

Validate, then retry

Treat model output like user input: parse it, check the shape, and never let unvalidated JSON reach your database. When validation fails, do not silently retry the identical request — feed the bad output and the failure back into the conversation so the model can correct itself.

import OpenAI from 'openai';

const client = new OpenAI({
  apiKey: process.env.TASE_API_KEY,
  baseURL: 'https://api.tase.app/v1',
});

const PRIORITIES = ['low', 'medium', 'high'];

function validateTask(task) {
  if (typeof task.title !== 'string' || task.title.length === 0) {
    throw new Error('title must be a non-empty string');
  }
  if (task.due_date !== null && !/^\d{4}-\d{2}-\d{2}$/.test(task.due_date)) {
    throw new Error('due_date must be YYYY-MM-DD or null');
  }
  if (!PRIORITIES.includes(task.priority)) {
    throw new Error('priority must be low, medium, or high');
  }
  return task;
}

async function extractTask(text, attempts = 3) {
  const messages = [
    {
      role: 'system',
      content:
        'Extract task details from the user message. Reply with JSON only, ' +
        'using exactly these keys: title (string), due_date (YYYY-MM-DD or null), ' +
        'priority (one of: low, medium, high).',
    },
    { role: 'user', content: text },
  ];

  for (let attempt = 1; attempt <= attempts; attempt++) {
    const completion = await client.chat.completions.create({
      model: 'tase-8b',
      response_format: { type: 'json_object' },
      temperature: 0,
      messages,
    });

    const raw = completion.choices[0].message.content;

    try {
      return validateTask(JSON.parse(raw));
    } catch (err) {
      // Feed the failure back so the next attempt can correct itself.
      messages.push({ role: 'assistant', content: raw });
      messages.push({
        role: 'user',
        content:
          'That reply failed validation: ' + err.message + '. ' +
          'Reply again with JSON only, matching the schema exactly.',
      });
    }
  }

  throw new Error('No valid JSON after ' + attempts + ' attempts');
}

const task = await extractTask('Remind me to renew my passport by August 15, it is urgent');
console.log(task);
import json
import os
import re

from openai import OpenAI

client = OpenAI(
    api_key=os.environ["TASE_API_KEY"],
    base_url="https://api.tase.app/v1",
)

PRIORITIES = {"low", "medium", "high"}
SCHEMA_PROMPT = (
    "Extract task details from the user message. Reply with JSON only, "
    "using exactly these keys: title (string), due_date (YYYY-MM-DD or null), "
    "priority (one of: low, medium, high)."
)


def validate_task(task: dict) -> dict:
    if not isinstance(task.get("title"), str) or not task["title"]:
        raise ValueError("title must be a non-empty string")
    due = task.get("due_date")
    if due is not None and not re.fullmatch(r"\d{4}-\d{2}-\d{2}", due):
        raise ValueError("due_date must be YYYY-MM-DD or null")
    if task.get("priority") not in PRIORITIES:
        raise ValueError("priority must be low, medium, or high")
    return task


def extract_task(text: str, attempts: int = 3) -> dict:
    messages = [
        {"role": "system", "content": SCHEMA_PROMPT},
        {"role": "user", "content": text},
    ]

    for _ in range(attempts):
        completion = client.chat.completions.create(
            model="tase-8b",
            response_format={"type": "json_object"},
            temperature=0,
            messages=messages,
        )
        raw = completion.choices[0].message.content
        try:
            return validate_task(json.loads(raw))
        except (json.JSONDecodeError, ValueError) as err:
            messages.append({"role": "assistant", "content": raw})
            messages.append({
                "role": "user",
                "content": (
                    "That reply failed validation: " + str(err) + ". "
                    "Reply again with JSON only, matching the schema exactly."
                ),
            })

    raise RuntimeError("No valid JSON after " + str(attempts) + " attempts")


task = extract_task("Remind me to renew my passport by August 15, it is urgent")
print(task)

Structured outputs vs tool calling

The tools parameter also produces structured JSON — the model emits a function name plus arguments matching a JSON Schema you define. The two features solve different problems, and picking the wrong one is a common source of awkward prompts.

  • Use response_format when the JSON is the answer: extraction, classification, transforming text into a record. Every reply is JSON, unconditionally.
  • Use tools when the model should decide whether to act: it can answer in plain text, or call one of several functions with typed arguments, and you send the result back in a tool message for the next turn.
  • Combining them: a chat assistant typically uses tools for its actions, and plain conversational replies otherwise. A batch pipeline that parses documents into rows wants response_format with a strict schema prompt.
// Tool calling: the model decides IF and WITH WHAT arguments to act.
const completion = await client.chat.completions.create({
  model: 'tase-8b',
  messages: [{ role: 'user', content: 'Add milk to my shopping list' }],
  tools: [
    {
      type: 'function',
      function: {
        name: 'add_task',
        description: 'Add a task to the user task list',
        parameters: {
          type: 'object',
          properties: {
            title: { type: 'string' },
            due_date: { type: 'string', description: 'YYYY-MM-DD, optional' },
          },
          required: ['title'],
        },
      },
    },
  ],
  tool_choice: 'auto',
});

const message = completion.choices[0].message;
const toolCall = message.tool_calls?.[0];

if (toolCall) {
  const args = JSON.parse(toolCall.function.arguments);
  // Run your function, then send the result back as a { role: 'tool' } message.
} else {
  // The model chose to reply in plain text instead.
  console.log(message.content);
}
Do I still need to validate when using response_format?+

Yes. JSON mode guarantees the output parses, not that it matches your schema. Validate keys, types, and enum values before using the data, and retry with feedback on failure.

Can I combine response_format with streaming?+

Yes — the JSON arrives as deltas like any other reply. Accumulate the full string first and parse once after the stream completes; partial JSON does not parse.

Why does the model sometimes wrap JSON in markdown fences?+

That happens when JSON is requested only via the prompt. Setting response_format to json_object removes the fences, since the constraint forbids anything outside the JSON value.

Review the fundamentals

Roles, parameters, multi-turn history, and token usage — the base layer under every structured request.

Back to chat completions