Назад до блогу

API та SDKПосібник

Отримання валідного JSON від LLM API: response_format і повторні спроби

Копійований Python-запит, який повертає придатний для парсингу JSON, обгортка json.loads, що обробляє порожні або обрізані відповіді, та контрольний список причин збою JSON.

Автор: CheapTokens TeamОпубліковано 5 хв читання

Термінал, що показує запит на завершення чату з response_format json_object
На цій сторінці
  1. Задача: лист на вході, рядок на виході
  2. Чому приклад важливіший за прапорець
  3. Безпечний парсинг: порожній, обрізаний або просто зламаний
  4. Перевіряйте форму, а не лише синтаксис
  5. Скільки коштує пакет листів
  6. Швидка перевірка

Щоб отримати JSON від чат-моделі, зробіть три речі: передайте response_format={"type": "json_object"}, додайте слово "json" і приклад структури в запит, а також встановіть max_tokens достатньо високим для всього об'єкта. Потім ніколи не довіряйте відповіді наосліп: перевірте, що вона не порожня, перевірте finish_reason, і лише тоді викликайте json.loads.

Це коротка версія. Решта цього посібника — один повний скрипт для дуже поширеної задачі: вилучення полів з електронного листа. Приклади використовують базову URL-адресу https://api.cheaptokens.dev/v1 та ідентифікатор моделі deepseek-ai/DeepSeek-V4-Flash-0731 (документація).

Задача: лист на вході, рядок на виході#

Скажімо, ваш скрипт читає листи підтримки і має перетворити кожен на запис: відправник, дата, сума та те, що хоче відправник. Візьміть цей:

Привіт, це Анна Петрова з Lumen Ltd. 14 жовтня я сплатила рахунок №3381 на 1 250 євро, але квитанція досі показує його як несплачений. Будь ласка, виправте це до п'ятниці.

Ви хочете отримати це, і нічого іншого:

json
{"sender": "Anna Petrova", "date": "2026-10-14", "amount": 1250, "currency": "EUR", "action": "fix receipt status"}

Простий запит "вилучи поля" може дати вам дружнє речення, блок коду або і те, і інше. Ваш json.loads задихається на першому зворотному апострофі. Це проблема, яку response_format і хороший запит вирішують разом.

Той самий запит на Python і curl

Python

python
import json
import os

from openai import OpenAI

client = OpenAI(
    base_url="https://api.cheaptokens.dev/v1",
    api_key=os.environ["CHEAPTOKENS_API_KEY"],
)
MODEL = "deepseek-ai/DeepSeek-V4-Flash-0731"

SYSTEM = """Extract fields from the email and answer with json only.
Example of the json format:
{"sender": "Jane Roe", "date": "2026-01-31", "amount": 99.5, "currency": "USD", "action": "short phrase"}
Use null for a field that is not in the email."""

resp = client.chat.completions.create(
    model=MODEL,
    messages=[
        {"role": "system", "content": SYSTEM},
        {"role": "user", "content": email_text},
    ],
    response_format={"type": "json_object"},
    max_tokens=1000,
    temperature=0,
)
print(resp.choices[0].message.content)

curl

bash
curl https://api.cheaptokens.dev/v1/chat/completions \
  -H "Authorization: Bearer $CHEAPTOKENS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "deepseek-ai/DeepSeek-V4-Flash-0731",
    "messages": [
      {"role": "system", "content": "Extract fields from the email and answer with json only. Example: {\"sender\": \"Jane Roe\", \"amount\": 99.5}"},
      {"role": "user", "content": "Hi, this is Anna Petrova from Lumen Ltd. I paid invoice #3381 for 1,250 EUR..."}
    ],
    "response_format": {"type": "json_object"},
    "max_tokens": 1000
  }'

Довідка DeepSeek API для JSON Output (перевірено 27 жовтня 2026; сторінка не має дати публікації) визначає правила, яких дотримується цей запит: response_format встановлено на json_object, слово "json" десь у системному або користувацькому запиті, та приклад цільової структури. Без прикладу модель має вгадати назви ваших полів, і вона вгадає по-різному для наступного листа.

Чому приклад важливіший за прапорець#

response_format просить API повернути JSON-об'єкт. Він не каже моделі, який об'єкт. Уявіть, що ви кажете колезі "надішли мені таблицю", не вказуючи, які стовпці. Ви отримаєте таблицю, просто не свою.

Приклад у запиті — це рядок заголовків стовпців. Дві звички покращують його роботу:

  • Покажіть реальну форму, включаючи типи: 1250 — це число, "2026-10-14" — це рядок у фіксованому форматі дати.
  • Скажіть, що робити, коли поле відсутнє (null вище). Інакше модель вигадає значення, щоб заповнити прогалину.

Сприймайте response_format як сильну підказку, а не гарантію. Моделі іноді все ще повертають порожній рядок або обгортають об'єкт у текст. Ось чому існує наступна частина.

Безпечний парсинг: порожній, обрізаний або просто зламаний#

На практиці трапляються три збої, і кожен залишає різний слід:

  • Порожній вміст. Документація DeepSeek каже, що режим JSON може іноді повертати порожній вміст і радить переформулювати запит. З вашого боку це виглядає як message.content, що дорівнює "" або None, і json.loads("") викликає Expecting value: line 1 column 1 (char 0).
  • Обрізаний. max_tokens закінчився посеред об'єкта. finish_reason — це length, і текст закінчується посеред рядка. Детальніше про це в чому відповідь ШІ зупиняється на півслові.
  • Не JSON. Речення перед об'єктом або блок коду навколо нього.
json.loads на порожній відповіді викликає JSONDecodeError
Як виглядає порожня відповідь у режимі JSON на Python

Виправлення для першого та третього — повторна спроба з жорсткішим запитом. Для другого повторна спроба з тим самим лімітом нічого не змінює, тому обгортка викликає помилку і каже вам підвищити max_tokens:

python
def ask_json(email_text: str, retries: int = 1) -> dict:
    user = email_text
    for attempt in range(retries + 1):
        resp = client.chat.completions.create(
            model=MODEL,
            messages=[
                {"role": "system", "content": SYSTEM},
                {"role": "user", "content": user},
            ],
            response_format={"type": "json_object"},
            max_tokens=1000,
            temperature=0,
        )
        choice = resp.choices[0]
        if choice.finish_reason == "length":
            raise ValueError("JSON cut off by max_tokens: raise the limit")
        text = (choice.message.content or "").strip()
        if text:
            try:
                return json.loads(text)
            except json.JSONDecodeError:
                pass
        # empty or not JSON: ask again, more firmly
        user = email_text + "\n\nReply with one json object only, in the format of the example."
    raise ValueError("no valid JSON after retry")

Однієї повторної спроби достатньо для скрипта. Якщо вона не вдається двічі, зафіксуйте ідентифікатор листа і рухайтеся далі; третій ідентичний запит рідко допомагає.

З'єднайте пари

Який симптом вказує на яку причину?

  • finish_reason дорівнює "length"max_tokens замалий для об'єкта
  • content — порожній рядокрежим JSON не повернув нічого; переформулюйте запит і повторіть
  • JSONDecodeError на символі, який є зворотним апострофоммодель обгорнула JSON у блок коду
  • Назви полів відрізняються між листамиу запиті немає прикладу структури

Перевіряйте форму, а не лише синтаксис#

json.loads лише доводить, що текст є валідним JSON. Він не доводить, що в ньому є поля, які ви просили. Додайте дешеву перевірку відразу після парсингу:

python
REQUIRED = {"sender", "date", "amount", "currency", "action"}

data = ask_json(email_text)
missing = REQUIRED - data.keys()
if missing:
    raise ValueError(f"missing fields: {sorted(missing)}")

Звідси словник потрапляє прямо в рядок бази даних або таблицю. Якщо вам потрібно, щоб модель викликала ваші власні функції замість заповнення фіксованого запису, це інший інструмент: дивіться виклик функцій на Python.

Шлях від листа до перевіреного запису: запит з прикладом, json_object, парсинг, перевірка полів
Весь шлях за п'ять кроків

Скільки коштує пакет листів#

Коротке вилучення — це дешевий тип запиту: кілька сотень токенів на вході, кілька десятків на виході. Припустімо, 1 000 листів на день при близько 400 вхідних токенах (запит, приклад і лист) і близько 80 вихідних токенах (JSON). На DeepSeek-V4-Flash це $0.014 на день, і таблиця показує те саме навантаження на інших моделях:

Модель За запит 1 000 запитів OpenRouter, 1 000 запитів
GLM-5.3-Flash $0.000011 $0.0112 $0.0226
DeepSeek-V4-Flash $0.000014 $0.014 $0.0315
MiniMax-M2.7 $0.000032 $0.032 $0.216

400 вхідних і 80 вихідних токенів на запит. Ціни за 1 млн токенів актуальні та змінюються разом із нашими тарифами; стовпець OpenRouter показує довідкові ціни для того самого навантаження.

Загалом, з урахуванням входу та виходу разом, DeepSeek-V4-Flash виходить дешевшим за довідкову ціну OpenRouter для цього навантаження. Він також дозволяє до 32 768 вихідних токенів на відповідь, тому довгий список позицій не буде обрізаний, як це сталося б із малим лімітом, і ціна однакова в будь-яку годину. GLM-5.3-Flash (zai-org/GLM-5.3-Flash) — ще один варіант за тією ж адресою API: до 8 192 вихідних токенів, чого цілком достатньо для одного запису на лист. Поточні ціни на сторінці цін; сторінка моделі DeepSeek містить деталі для використаної тут моделі.

Швидка перевірка#

Швидка перевірка

Ваш JSON обрізано посеред рядка. На що ви дивитеся першим?

Показати відповідь

Відповідь: finish_reason і max_tokens

Обрізаний об'єкт закінчується з finish_reason, встановленим на length. Ліміт було досягнуто, тому підвищте max_tokens; зміна температури або адреси не допоможе.

Якщо ви ще не надсилали запит, почніть з вашого першого запиту до API, а потім поверніться і вставте код вище.