Возможности
ВОЗМОЖНОСТИ

Структурированный вывод

Получение ответа строго в формате JSON по заданной схеме — без парсинга текста и без шанса получить «почти-JSON».

Структурированный вывод гарантирует, что модель вернёт валидный JSON и, при необходимости, соответствующий конкретной JSON Schema. Это убирает целый класс ошибок: «модель прислала JSON с лишним текстом», «закрывающая скобка съехала», «вместо числа — строка с числом».

Поддерживается большинством современных моделей. Жёсткое следование схеме (strict: true) — у OpenAI gpt-4o и новее, Claude 3.5 Sonnet и новее, Gemini 2.0+. Карточка модели в каталоге подскажет.

Два режима

Параметр response_format принимает один из трёх вариантов:

ТипЧто гарантирует
{ "type": "text" }Свободный текст. То же, что не передавать response_format вовсе
{ "type": "json_object" }Ответ — синтаксически валидный JSON. Структура произвольная
{ "type": "json_schema", ... }Ответ — JSON, соответствующий вашей схеме

Режим json_object достаточен, если вы сами в системном промпте описали ожидаемые поля. Режим json_schema — строже: апстрим валидирует ответ модели и при strict: true гарантирует точное соответствие схеме.

json_object — простой случай

curl -s https://api.hubris.pw/v1/chat/completions \
  -H "Authorization: Bearer sk-gw-..." \
  -H "Content-Type: application/json" \
  -d '{
    "model": "openai/gpt-4o-mini",
    "messages": [
      {"role": "system", "content": "Верни поля: name (строка), age (число)."},
      {"role": "user", "content": "Меня зовут Алексей, мне 34 года."}
    ],
    "response_format": { "type": "json_object" }
  }'

Ответ — гарантированно валидный JSON, но конкретные поля зависят от модели:

{
  "choices": [{
    "message": {
      "role": "assistant",
      "content": "{\"name\": \"Алексей\", \"age\": 34}"
    }
  }]
}

Контент по-прежнему приходит строкой — JSON.parse(...) обязателен.

json_schema — строгий режим

Передайте схему в response_format.json_schema. Поле strict: true включает жёсткую валидацию: апстрим заранее ограничивает токены, которые модель может выдать, чтобы получить именно соответствующий вашей схеме JSON.

curl -s https://api.hubris.pw/v1/chat/completions \
  -H "Authorization: Bearer sk-gw-..." \
  -d '{
    "model": "openai/gpt-4o-mini",
    "messages": [
      {"role": "user", "content": "Извлеки данные из квитанции: пицца 750₽, кола 150₽, чаевые 100₽."}
    ],
    "response_format": {
      "type": "json_schema",
      "json_schema": {
        "name": "receipt",
        "strict": true,
        "schema": {
          "type": "object",
          "properties": {
            "items": {
              "type": "array",
              "items": {
                "type": "object",
                "properties": {
                  "name": {"type": "string"},
                  "price_rub": {"type": "number"}
                },
                "required": ["name", "price_rub"],
                "additionalProperties": false
              }
            },
            "tip_rub": {"type": "number"},
            "total_rub": {"type": "number"}
          },
          "required": ["items", "tip_rub", "total_rub"],
          "additionalProperties": false
        }
      }
    }
  }'

Ответ:

{
  "choices": [{
    "message": {
      "content": "{\"items\":[{\"name\":\"пицца\",\"price_rub\":750},{\"name\":\"кола\",\"price_rub\":150}],\"tip_rub\":100,\"total_rub\":1000}"
    },
    "finish_reason": "stop"
  }]
}

Требования к схеме при strict: true

  • Корневой объект — обязательно "type": "object".
  • Все поля, которые могут присутствовать в ответе, должны быть в required[]. Опциональные поля нужно делать union с null: {"type": ["string", "null"]}.
  • На каждом уровне объекта обязателен "additionalProperties": false.
  • Нельзя использовать oneOf, allOf, not, регулярные выражения в pattern, рекурсивные ссылки.

Если схема не проходит этим требованиям — апстрим вернёт ошибку валидации до старта генерации.

OpenAI SDK

Python (pydantic + helper parse):

from openai import OpenAI
from pydantic import BaseModel

client = OpenAI(
    base_url="https://api.hubris.pw/v1",
    api_key="sk-gw-...",
)

class ReceiptItem(BaseModel):
    name: str
    price_rub: float

class Receipt(BaseModel):
    items: list[ReceiptItem]
    tip_rub: float
    total_rub: float

resp = client.chat.completions.parse(
    model="openai/gpt-4o-mini",
    messages=[{"role": "user", "content": "Пицца 750, кола 150, чаевые 100."}],
    response_format=Receipt,
)

receipt = resp.choices[0].message.parsed
print(receipt.total_rub)  # 1000.0

TypeScript (zod + helper parse):

import OpenAI from 'openai';
import { z } from 'zod';
import { zodResponseFormat } from 'openai/helpers/zod';

const client = new OpenAI({
  baseURL: 'https://api.hubris.pw/v1',
  apiKey: 'sk-gw-...',
});

const Receipt = z.object({
  items: z.array(z.object({
    name: z.string(),
    price_rub: z.number(),
  })),
  tip_rub: z.number(),
  total_rub: z.number(),
});

const resp = await client.chat.completions.parse({
  model: 'openai/gpt-4o-mini',
  messages: [{ role: 'user', content: 'Пицца 750, кола 150, чаевые 100.' }],
  response_format: zodResponseFormat(Receipt, 'receipt'),
});

const receipt = resp.choices[0].message.parsed;
console.log(receipt?.total_rub); // 1000

Хелперы parse сами добавят правильный response_format и при успешном ответе вернут уже распарсенный и провалидированный объект в .parsed.

Что делать с отказами

Если модель решит, что задача нерешаема (например, в квитанции совсем нет цен), она вернёт refusal вместо контента:

{
  "choices": [{
    "message": {
      "role": "assistant",
      "content": null,
      "refusal": "Не могу извлечь цены — в тексте нет числовых данных."
    },
    "finish_reason": "stop"
  }]
}

В коде проверяйте оба поля — иначе при refusal ваш JSON.parse(content) упадёт на null.

Стриминг

stream: true работает. Чанки в delta.content приходят кусками всё ещё валидной JSON-строки — собирайте, как обычный текст, парсите финал. Для прогрессивного парсинга (показывать ответ по мере прихода) понадобится партиальный JSON-парсер на клиенте.

Биллинг

Структурированный вывод не имеет отдельного тарифа. Платите за обычные prompt- и completion-токены:

  • Описание схемы попадает в prompt-токены каждого запроса.
  • Финальный JSON-ответ — в completion-токенах.

Длинные схемы (десятки полей) могут заметно раздувать prompt-расход — учитывайте при тарификации.

Что дальше

Обновлено:

Структурированный вывод · Hubris