# `finish_reason=length` Returned Empty Content — and the Error Message Lied to Me

> Source: <https://dev.to/emmalane/finishreasonlength-returned-empty-content-and-the-error-message-lied-to-me-168n>
> Published: 2026-07-30 01:26:11+00:00

The error said the model returned empty content. I assumed the response stream had broken. It hadn't.

My multi-agent content pipeline hit a wall: one of the generation steps was throwing `"model returned empty content"`

and the whole run was aborting. The message pointed at the output layer, so I started there.

I spent a few rounds instrumenting the streaming path — checking whether the SSE connection was dropping mid-response, whether the provider was rate-limiting and silently closing the socket, whether my deserialization was eating bytes. The logs looked clean every time. Request went out, response came back, full round-trip, no timeout. The only thing missing was actual content in the assistant turn.

I then suspected a provider-side issue: maybe the endpoint was returning a malformed chunk that my client was silently discarding. I added more logging at the HTTP layer. Still nothing. The request was completing successfully. `finish_reason: "length"`

. Zero tokens in the content field.

I'd been chasing the wrong layer entirely.

The model I was calling — deepseek-v4 over an OpenAI-compatible channel — is a reasoning model. It has an internal chain-of-thought that runs before it writes visible output. That reasoning lives in `reasoning_content`

, not `content`

.

The token budget I'd set was being consumed entirely by the reasoning chain. By the time the model finished thinking, there was nothing left for the actual response. So `content`

came back as `""`

— legitimately empty, not a transport error. The `reasoning_content`

field had plenty of text. The model had done work. It just ran out of budget before it could write a single token of visible output.

The misleading part: my orchestration wrapper saw `content: ""`

, threw `"model returned empty content"`

, and that error looked exactly like a network fault. Nothing in the error message mentioned token budgets or reasoning. It just said empty. So I went looking for why the content was empty at the transport layer, when the real answer was sitting one level up in the API response the whole time.

The signal was there if I'd looked at the raw response first. `finish_reason: "length"`

combined with a non-empty `reasoning_content`

and an empty `content`

is a specific fingerprint. It doesn't mean the stream dropped. It means the model thought itself into a corner.

Two parts.

**First: raise the token budget.** The fix wasn't a clever parameter split — it was simply that `maxTokens`

was too low for a reasoning model on a complex task. Reasoning models burn tokens differently than standard chat models. A budget that's fine for a non-reasoning model can be entirely consumed by chain-of-thought before a single output token gets written.

**Second: make the error tell the truth.** The more durable fix was changing what the error actually says. I added a `reasoningOnly`

flag to the `ChatResult`

type:

```
reasoningOnly: !content.trim() && hasReasoning
```

When that flag is true, the error now says something like `"reasoning consumed full token budget — no output generated"`

instead of the generic `"model returned empty content"`

. Same underlying condition, completely different diagnostic direction.

The fallback chain also changed. The second tier used to be "swap to a different model." Now it's "double the budget and retry" — because if `reasoningOnly`

is true, a model swap doesn't help. You'd hit the same wall with a different model family if the budget is still too small.

The commit is `193aba6`

, changes in `packages/llm-client/src/index.ts`

and `produce.ts`

.

This fix holds when you control the token budget directly in the API call. If you're routing through a proxy or a managed endpoint that doesn't expose budget parameters — or if your orchestration layer abstracts them away — you need a different gate. The `reasoningOnly`

detection still works for classification, but you can't fix what you can't configure. That's a separate problem.

The hours I lost weren't really about the bug. They were about the error message. `"model returned empty content"`

is technically accurate and completely useless. It describes the symptom at the wrong layer. The moment I looked at the raw `finish_reason`

and both content fields together, the cause was obvious. The error just never pointed me there.
