Back to blog

TroubleshootingGuide

AI answer cut off? Fix finish_reason "length" and max_tokens

Your API answer stops mid-sentence or returns broken JSON? Check finish_reason, raise max_tokens within the limit and add a continue step.

By CheapTokens TeamPublished 6 min read

Terminal-style cover showing finish_reason length in an API response
On this page
  1. How a cut-off answer looks on screen
  2. Step 1: read the field that tells you
  3. Step 2: raise max_tokens, but not past the ceiling
  4. Step 3: JSON that stops halfway
  5. Step 4: let the model continue
  6. What a long answer costs
  7. Before you ship: a short checklist

Your script ran, the model answered, and the function ends in the middle of a line. Or the JSON parser throws on a perfectly normal-looking reply. Most often the model did not fail: it hit the max_tokens limit you set (or a default you never saw) and the API stopped it mid-word.

The quick fix: read finish_reason in the response. If it is "length" (OpenAI-compatible API) or stop_reason is "max_tokens" (Anthropic Messages), the answer was cut. Raise max_tokens up to the model's output limit, or ask the model to continue. The rest of this page shows both in code you can paste.

If you want the plain-English version of why this happens, start with why an AI answer stops mid-sentence. This page is the debugging version.

How a cut-off answer looks on screen#

The symptoms differ by what you asked for, but the cause is the same. Try matching these to what you have seen:

Match the pairs

Match the symptom to its likely cause

  • A function ends with return resuThe code answer hit max_tokens mid-line
  • json.loads fails with "Unterminated string" or "Expecting ',' delimiter"The JSON was cut off mid-way
  • A translation stops after the first half of the textThe output limit is smaller than the translation
  • The reply is complete and the field says stopThe model finished on its own; look elsewhere

That last row matters. If finish_reason is stop, the limit is not your problem, and raising max_tokens will change nothing.

Step 1: read the field that tells you#

Every response says why generation ended. In an OpenAI-compatible response it is choices[0].finish_reason: "stop" when the model finished, "length" when it ran out of tokens. In the Anthropic Messages API the field is stop_reason, and the cut-off value is "max_tokens". Anthropic's stop-reason guide (checked 28 October 2026) recommends exactly the two fixes below: raise max_tokens or continue the response.

Here is the same check in three forms. The base URL is https://api.cheaptokens.dev/v1 for the OpenAI SDK and https://api.cheaptokens.dev (no /v1) for the Anthropic SDK. The max_tokens=200 is deliberately small, so you can see the cut for yourself.

One check, three ways

Python (OpenAI SDK)

python
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)

python
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

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",
    "max_tokens": 200,
    "messages": [{"role": "user", "content": "Write a Python class for a todo list with tests."}]
  }'

Look for "finish_reason": "length" in the JSON that comes back.

When it works as a diagnosis, you will see this pair: a low limit gives length, a roomy limit gives stop.

Same request with a low and a high max_tokens: finish_reason changes from length to stop
The two values of finish_reason you will meet most often

Step 2: raise max_tokens, but not past the ceiling#

The obvious fix is a bigger number. The catch: every model has its own cap on the answer. Do not set a value above it: the limit is per model, so check the table below first. These are the per-answer limits on CheapTokens (facts checked 28 October 2026):

Model Max output per answer Context
DeepSeek-V4-Flash 32,768 tokens 380K
GLM-5.3-Flash 8,192 tokens 390K
MiniMax-M2.7 8,192 tokens 180K

A 9,000-token translation needs a model with a larger per-answer limit — DeepSeek-V4-Flash takes up to 32,768 tokens per answer. For long code, long JSON or a whole document, deepseek-ai/DeepSeek-V4-Flash-0731 has the most room: up to 32,768 tokens per answer, one price at any hour of the day. Take the exact model ids from the CheapTokens docs and the models page, since providers name them differently.

A practical rule: set max_tokens to the longest answer you would accept, not to the maximum. You pay for tokens the model actually writes, so a high ceiling costs nothing when answers are short.

Step 3: JSON that stops halfway#

A cut-off JSON reply is the nastiest case, because the text looks right until the parser hits the end. Say you extract fields from 40 invoices in one call and ask for an array. The model writes 31 objects, reaches the limit, and the array never closes.

A json.loads error on an array cut off at the token limit
What a truncated JSON array looks like in a parser

Do not try to repair the string by adding ]}. You will get valid JSON with missing records, which is worse than an error. Check finish_reason first; if it is length, either raise the limit and rerun the call, or split the input into smaller batches. For forcing the format itself, see valid JSON from an LLM API.

Step 4: let the model continue#

Sometimes the answer genuinely is longer than any limit: a book chapter, a big refactor. Then feed what you have back and ask the model to go on. The loop below keeps the text it has so far, asks for the rest, and gives up after a few rounds so a bug cannot spin forever.

python
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

Two habits keep this honest. Return the True/False flag and show the user a warning on False, rather than presenting half a document as done. And remember that each round resends the earlier text as input, so a continued answer costs more than one big answer. For structured data, prefer a bigger limit or smaller batches over continuing.

What a long answer costs#

Say a long answer is 2,000 tokens in and 8,000 tokens out. On DeepSeek-V4-Flash that request costs $0.00083 at today's price, and in total, with input and output counted together, it is cheaper than OpenRouter. Change the numbers to your own load:

Cost calculator

A day of 50 long answers

Input tokens per request: 2,000 · Output tokens per request: 8,000 · Requests per day: 50

ModelPer dayPer month
GLM-5.3-Flash$0.018$0.54
DeepSeek-V4-Flash$0.0415$1.25
MiniMax-M2.7$0.065$1.95

The takeaway: the output side dominates in this scenario, so headroom in max_tokens is cheap insurance against a rerun. A cut answer that you have to regenerate is paid for twice. Background on the two sides of the bill is in input vs output tokens, and the full model list with prices is on the pricing page.

Before you ship: a short checklist#

Checklist

Truncation checklist for your code