DEV Community

Jordan Huang
Jordan Huang

Posted on

A 200 Means the Socket Worked: Five Completion Myths

Did your last free-model call actually complete the thought?

I keep seeing the same sad Slack screenshot. Status two hundred. A chopped answer. Then a shrug.

People call that the model being random. Is it random, or unread?

This FAQ is about completion, not vibes. You leave with a gate script. You also leave with a decision table.

Why this keeps biting teams

LLM HTTP APIs look like REST. They are not CRUD.

A database commit is atomic. A completion is a stop condition.

You can get HTTP 200 with an unfinished thought. You can get a body with zero choices.

So what do we actually inspect after the socket closes?

Myth 1: HTTP 200 means the job finished

The claim: Green status. Ship the text.

The evidence: I read the body, not the status line. I look for choices, finish_reason, and usage.

A 200 means the server wrote bytes. It does not mean a stop token won.

Corrected model: Treat 200 as transport success only. Treat the body as the real status code.

# proposal: never accept status alone
def transport_ok(status: int) -> bool:
    return 200 <= status < 300
Enter fullscreen mode Exit fullscreen mode

Would you merge a Git commit from a TCP ACK? Then stop merging LLM text from a 200.

Myth 2: Empty content means the endpoint is down

The claim: Blank content equals an outage. Page someone.

The evidence: Filters, length caps, and tool calls produce empty text. The socket can still be fine.

Ask yourself one rude question. Did you log finish_reason at all?

If the reason is content_filter or length, downtime is the wrong story. You got a business result with no prose.

Corrected model: Empty text is a result type. It is not a health check failure.

def empty_text_is_outage(choice: dict) -> bool:
    text = (choice.get("message") or {}).get("content") or ""
    reason = choice.get("finish_reason")
    if text.strip():
        return False
    # proposal: these reasons are outcomes, not downtime
    return reason not in {"length", "content_filter", "tool_calls", "stop"}
Enter fullscreen mode Exit fullscreen mode

Still unsure? Check your own request id header. Then check the provider status page.

Myth 3: Truncation is always a prompt quality problem

The claim: I should have written a nicer prompt. Then it would fit.

Sometimes that is true. Not as a default diagnosis.

finish_reason: length is a budget event. It is not a style event.

Corrected model: Cap hits need a budget change. Or an explicit continuation call. Do not rephrase and pray.

# proposal: print the stop condition, every time
curl -sS "$LLM_URL" \
  -H "Authorization: Bearer $LLM_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"messages":[{"role":"user","content":"Summarize this log."}]}' \
  | python -c 'import sys,json; b=json.load(sys.stdin); print(b["choices"][0].get("finish_reason"), len((b["choices"][0].get("message") or {}).get("content") or ""))'
Enter fullscreen mode Exit fullscreen mode

Did the reason say length? Raise max_tokens or split the task. Prompt poetry will not invent extra context window.

Myth 4: The stream ended, so I can persist

The claim: data: [DONE] means commit. Write the row.

Streams end for many boring reasons. Client cancel. Idle timeout. Proxy flush. Laptop sleep.

Did you see a terminal finish_reason on the last chunk? If not, you have a partial.

Corrected model: Persist partials as partials. Do not merge them into prod copy.

# proposal: a stream is complete only with a terminal reason
TERMINAL = {"stop", "length", "content_filter", "tool_calls"}

def stream_complete(last_chunk: dict) -> bool:
    choices = last_chunk.get("choices") or []
    if not choices:
        return False
    return choices[0].get("finish_reason") in TERMINAL
Enter fullscreen mode Exit fullscreen mode

I refuse to treat a hung SSE pipe as a successful essay. Do you still do that?

Myth 5: Free calls skip these fields, so skip the checks

The claim: Free means best-effort. Skip the boring fields.

That is how you teach yourself a bad client. Cheap traffic is where habits form.

I run the same parser on free model access. Same exit codes. Same logs.

Disclosure: This article was prepared as part of MonkeyCode's product outreach.

MonkeyCode's free model access and free server option are a gym for this gate. Not a production SLA. The gym still returns a body. Parse it.

Corrected model: Free is for rehearsing the client. It is not a license to skip stop conditions.

The artifact: a completion gate

Do not debate the myths in standup. Run a gate.

Label this a proposed local check. Wire it in front of any persist step.

Decision table

What you see Do not conclude Do instead
HTTP 200 The task finished Read choices and finish_reason
Empty content The API is down Map the reason to an outcome
Cut-off sentence The prompt is bad Check length, then raise budget
Stream [DONE] Safe to persist Require a terminal reason
Missing usage block Free tiers hide cost Log tokens when present; fail closed if you need them
No request id Nothing to debug Record your own idempotency key

Checker script

Save this as completion_gate.py. Point it at a saved JSON body.

#!/usr/bin/env python3
"""Proposal: fail closed on incomplete LLM bodies."""
import json
import sys

TERMINAL = {"stop", "length", "content_filter", "tool_calls"}
SOFT_FAIL = {"length", "content_filter"}


def load(path: str) -> dict:
    with open(path, encoding="utf-8") as fh:
        return json.load(fh)


def gate(body: dict) -> list[str]:
    errors = []
    choices = body.get("choices") or []
    if not choices:
        errors.append("no choices array")
        return errors

    choice = choices[0]
    reason = choice.get("finish_reason")
    message = choice.get("message") or {}
    text = message.get("content") or ""

    if reason is None:
        errors.append("missing finish_reason")
    elif reason not in TERMINAL:
        errors.append(f"non-terminal finish_reason={reason!r}")

    if reason in SOFT_FAIL:
        errors.append(f"soft-fail finish_reason={reason!r}")

    if reason == "stop" and not str(text).strip():
        errors.append("stop with empty content")

    usage = body.get("usage") or {}
    if "completion_tokens" in usage and usage["completion_tokens"] == 0:
        errors.append("zero completion_tokens")

    return errors


def main() -> int:
    if len(sys.argv) != 2:
        print("usage: completion_gate.py BODY.json", file=sys.stderr)
        return 2
    body = load(sys.argv[1])
    errors = gate(body)
    if errors:
        for item in errors:
            print(f"FAIL: {item}")
        return 1
    reason = body["choices"][0].get("finish_reason")
    print(f"PASS: finish_reason={reason}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
Enter fullscreen mode Exit fullscreen mode

Fixtures you should keep

Do not wait for a live outage. Keep three files in git.

{
  "id": "fixture-length",
  "choices": [
    {
      "message": {"role": "assistant", "content": "The stack trace begins in"},
      "finish_reason": "length"
    }
  ],
  "usage": {"prompt_tokens": 128, "completion_tokens": 64}
}
Enter fullscreen mode Exit fullscreen mode
{
  "id": "fixture-empty-filter",
  "choices": [
    {
      "message": {"role": "assistant", "content": ""},
      "finish_reason": "content_filter"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode
{
  "id": "fixture-ok",
  "choices": [
    {
      "message": {"role": "assistant", "content": "Restart the worker. Then tail stderr."},
      "finish_reason": "stop"
    }
  ],
  "usage": {"prompt_tokens": 40, "completion_tokens": 12}
}
Enter fullscreen mode Exit fullscreen mode

Run them like CI. No notebook. No eyeballing.

python completion_gate.py fixture-length.json; echo $?
python completion_gate.py fixture-empty-filter.json; echo $?
python completion_gate.py fixture-ok.json; echo $?
Enter fullscreen mode Exit fullscreen mode

Expect non-zero on the first two. Expect zero on the third.

One-hour rehearsal plan

  1. Save one live body from your current client.
  2. Run the gate against that file.
  3. Add the three fixtures above.
  4. Fail the persist path when the gate exits 1.
  5. Log finish_reason beside your own request id.

That is the whole method. No dashboard required.

What this is not

This gate does not grade answer quality. It does not prove factual truth.

It does not pin a model version. It does not replace contract tests on JSON fields.

It only answers one question. Did this completion end on purpose?

OpenAI-compatible bodies often carry finish_reason. Some stacks rename it. Read your schema before you copy mine.

I am not claiming a named model, a quota, or a hardware profile here. I am claiming a client habit.

Who should not use this

Skip this if you only paste into a chat UI. You have no persist path to protect.

Skip this if a human already edits every line. The merge is the real gate then.

Skip this if your provider gives no stop condition and no usage block. You need a different adapter, not a louder script.

Do not point this gym workflow at customer data. Free boxes are for fixtures and fake logs.

The mental model I want stuck

Status lines describe sockets. Stop reasons describe thoughts.

Empty text can still be a finished policy decision. A cut sentence is often a budget decision.

A closed stream is not a commit. A free call is still a call.

If your client cannot print finish_reason, you do not have a client yet. You have a curl souvenir.

Want a disposable box for the rehearsal? Use free model access on a free server, then keep the gate. That is the whole ask.

Top comments (0)