Решение проблемИнструкция
Ответ ИИ обрывается? Разбираем finish_reason "length" и max_tokens
Ответ API оборвался на полуслове или JSON сломан? Проверьте finish_reason, поднимите max_tokens в пределах лимита и добавьте продолжение.

Содержание
Скрипт отработал, модель ответила, а функция заканчивается посреди строки. Или JSON-парсер падает на вполне обычном на вид ответе. Чаще всего модель не ошиблась: она упёрлась в лимит max_tokens, который вы задали (или в значение по умолчанию, которого вы не видели), и API остановил её на полуслове.
Коротко: посмотрите finish_reason в ответе. Если там "length" (OpenAI-совместимый API) или stop_reason равен "max_tokens" (Anthropic Messages), ответ обрезан. Поднимите max_tokens до лимита модели или попросите её продолжить. Ниже оба способа в коде, который можно скопировать.
Если нужно объяснение попроще, почему так бывает, начните со статьи почему ответ ИИ обрывается на полуслове. Эта страница — про отладку.
Как обрезанный ответ выглядит на экране#
Симптомы зависят от того, что вы просили, а причина одна. Попробуйте сопоставить то, что видели вы:
Найдите пары
Сопоставьте симптом и вероятную причину
Нажмите на элемент слева, затем на его пару
- Функция заканчивается на
return resuКод упёрся в max_tokens посреди строки json.loadsпадает с "Unterminated string" или "Expecting ',' delimiter"JSON оборвался на середине- Перевод заканчивается на середине текстаЛимит ответа меньше, чем нужен переводу
- Ответ целый, а в поле стоит
stopМодель закончила сама, причину ищите в другом месте
Последняя строка важна. Если finish_reason равен stop, дело не в лимите, и увеличение max_tokens ничего не изменит.
Шаг 1: прочитайте поле, которое всё говорит#
В каждом ответе указана причина остановки. В OpenAI-совместимом ответе это choices[0].: "stop", если модель закончила, и "length", если кончились токены. В Anthropic Messages поле называется stop_reason, а значение для обрыва — "max_tokens". Руководство Anthropic по причинам остановки (проверено 28 октября 2026) советует ровно два способа из этой статьи: поднять max_tokens или продолжить ответ.
Вот одна и та же проверка в трёх видах. Базовый адрес — https:/ для OpenAI SDK и https:/ (без /v1) для Anthropic SDK. Значение max_tokens=200 нарочно маленькое, чтобы обрыв было видно сразу.
Одна проверка, три способа
Python (OpenAI SDK)
import os
from openai import OpenAI
client = OpenAI(
base_url="https://api.cheaptokens.dev/v1",
api_key=os.environ["CHEAPTOKENS_API_KEY"],
)
resp = client.chat.completions.create(
model="deepseek-ai/DeepSeek-V4-Flash-0731",
max_tokens=200,
messages=[{"role": "user", "content": "Write a Python class for a todo list with tests."}],
)
choice = resp.choices[0]
if choice.finish_reason == "length":
print("Cut off at the token limit")
print(choice.message.content)
print(resp.usage.completion_tokens)Python (Anthropic SDK)
import os
import anthropic
client = anthropic.Anthropic(
base_url="https://api.cheaptokens.dev",
api_key=os.environ["CHEAPTOKENS_API_KEY"],
)
msg = client.messages.create(
model="deepseek-ai/DeepSeek-V4-Flash-0731",
max_tokens=200,
messages=[{"role": "user", "content": "Write a Python class for a todo list with tests."}],
)
if msg.stop_reason == "max_tokens":
print("Cut off at the token limit")
print(msg.content[0].text)curl
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",
"max_tokens": 200,
"messages": [{"role": "user", "content": "Write a Python class for a todo list with tests."}]
}'В JSON, который вернётся, ищите "finish_.
Когда диагностика сработала, вы увидите такую пару: маленький лимит даёт length, просторный — stop.

Шаг 2: поднимите max_tokens, но не выше потолка#
Очевидное решение — число побольше. Подвох в том, что у каждой модели свой предел ответа. Не ставьте значение выше предела: лимит у каждой модели свой, сверьтесь с таблицей ниже. Вот лимиты на один ответ в CheapTokens (данные на 28 октября 2026):
Прокрутите таблицу вправо →
| Модель | Максимум в ответе | Контекст |
|---|---|---|
| DeepSeek-V4-Flash | 32 768 токенов | 380K |
| GLM-5.3-Flash | 8 192 токена | 390K |
| MiniMax-M2.7 | 8 192 токена | 180K |
Перевод на 9 000 токенов требует модели с большим лимитом ответа — DeepSeek-V4-Flash отдаёт до 32 768 токенов за ответ. Для длинного кода, длинного JSON или целого документа больше всего места у deepseek-: до 32 768 токенов в ответе и одна цена в любое время суток. Точные id моделей берите из документации CheapTokens и страницы моделей: у разных провайдеров названия отличаются.
Практичное правило: ставьте max_tokens равным самому длинному ответу, который вас устроит, а не максимуму. Платите вы за токены, которые модель реально написала, поэтому высокий потолок ничего не стоит, пока ответы короткие.
Шаг 3: JSON, который оборвался на середине#
Обрезанный JSON — самый неприятный случай: текст выглядит правильным, пока парсер не дойдёт до конца. Допустим, вы вытаскиваете поля из 40 счетов за один вызов и просите массив. Модель пишет 31 объект, упирается в лимит, и массив так и не закрывается.

Не пытайтесь «починить» строку, дописав ]}. Получится валидный JSON с недостающими записями, а это хуже ошибки. Сначала проверьте finish_reason; если там length, поднимите лимит и повторите вызов или разбейте вход на меньшие пачки. О том, как заставить модель держать формат, — в статье валидный JSON от LLM API.
Шаг 4: пусть модель продолжит#
Иногда ответ действительно длиннее любого лимита: глава книги, большой рефакторинг. Тогда возвращаем модели то, что уже есть, и просим продолжить. Цикл ниже хранит накопленный текст, просит остаток и сдаётся через несколько кругов, чтобы ошибка не крутилась вечно.
def complete(base_messages, model, max_tokens=4096, rounds=4):
text = ""
for _ in range(rounds):
messages = list(base_messages)
if text:
messages += [
{"role": "assistant", "content": text},
{"role": "user", "content": "Continue exactly where you stopped. Do not repeat anything."},
]
r = client.chat.completions.create(model=model, max_tokens=max_tokens, messages=messages)
choice = r.choices[0]
text += choice.message.content or ""
if choice.finish_reason != "length":
return text, True # finished on its own
return text, False # still cut after all roundsДве привычки не дают себя обмануть. Возвращайте флаг True/False и показывайте пользователю предупреждение при False, а не выдавайте половину документа за готовый. И помните: каждый круг заново отправляет прежний текст как вход, поэтому продолженный ответ обходится дороже одного большого. Для структурированных данных лучше поднять лимит или резать вход на пачки, чем продолжать.
Сколько стоит длинный ответ#
Допустим, длинный ответ — это 2 000 токенов на входе и 8 000 на выходе. На DeepSeek-V4-Flash такой запрос стоит $0.00083 по сегодняшней цене, а в сумме, с учётом входа и выхода, это дешевле, чем у OpenRouter. Подставьте свою нагрузку:
Калькулятор стоимости
День из 50 длинных ответов
Входные токены на запрос: 2 000 · Выходные токены на запрос: 8 000 · Запросов в день: 50
| Модель | В день | В месяц |
|---|---|---|
| GLM-5.3-Flash$0.02$0.04 | $0.018 | $0.54 |
| DeepSeek-V4-Flash$0.015$0.10 | $0.0415 | $1.25 |
| MiniMax-M2.7$0.05$0.15 | $0.065 | $1.95 |
Вывод: в таком сценарии главная часть счёта — выход, поэтому запас в max_tokens — дешёвая страховка от повторного запуска. Обрезанный ответ, который приходится генерировать заново, оплачивается дважды. О двух сторонах счёта — в статье входные и выходные токены, а список моделей с ценами — на странице цен.
Перед выпуском: короткий чек-лист#
Чек-лист
Чек-лист против обрезанных ответов



