How to Debug Requests to an LLM API: From curl to Error Codes

Most often the problem is the request, not the model: the URL, key, limit or body size. A step-by-step debugging routine and a table of Zukko error codes with fixes.

5 min · · development · errors · API keys

Debugging requests to an LLM API usually comes down to one question: what exactly did the server answer? SDKs often hide it behind a generic exception, and application logs only say "something went wrong". Below is a routine that finds the cause quickly: reproduce the request in curl, read the JSON error, match the code against the table, and set up retries and logging properly.

Step 1. Reproduce the request with curl

curl takes the SDK, the framework and your code out of the equation. If curl shows the same error, the problem is in the request or the account; if curl works, look for the difference in your code.

bash
curl -i https://api.zukko.pro/v1/chat/completions \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"gpt-5.6-sol","messages":[{"role":"user","content":"hello"}]}'

-i prints the response status and headers together with the body. -v also shows outgoing headers and connection details, which helps when you suspect a proxy, DNS or TLS. Remember that -v prints the Authorization header with your key too: do not paste that output into chats or tickets without masking it.

What to look for:

  • The status line: HTTP/2 200 or an error code.
  • Content-Type: application/json for a normal response, text/event-stream for streaming. HTML instead of JSON usually means the request went to the wrong address.
  • Zukko service headers on successful responses: X-Zukko-Cost-Micros and X-Fetch-Balance-Cents, the request cost and the remaining balance. For spend tracking, the "Usage" section of the dashboard is more reliable.

Check the JSON itself too: echo '<body>' | python3 -m json.tool. A trailing comma or an unescaped quote in a shell string is a common cause of a 400.

Step 2. Read the JSON error

Zukko errors come in the body in this shape:

bash
{
  "error": {
    "message": "Модель 'glm-5.3' временно недоступна. Актуальный список: GET /v1/models",
    "type": "service_unavailable",
    "code": "model_disabled"
  }
}
  • message is for humans. A line with a support link may be appended to the end of the message, so never compare message as a whole in code.
  • type is the category: authentication_error, permission_error, rate_limit_error, invalid_request_error, service_unavailable.
  • code is the machine-readable code. Branch your logic on it and on the HTTP status. Some responses (for example, 401) may have no code field; then rely on the status and type.

If the error comes from the model provider rather than the gateway, its shape may differ, but the status and text will still point to the cause. When contacting support, attach the full JSON rather than a paraphrase.

Zukko error codes and what to do

HTTPcodeCauseFix
401none (type: authentication_error)The key is wrong, revoked, or missingCheck the Authorization: Bearer fetch_... header and environment variables
402insufficient_balanceBalance below $0.01Top up in the dashboard
403email_unverifiedThe account email is not verifiedVerify your email
403model_not_allowed_for_keyThe model is not in the key's allowed listAdd the model in the key settings or use another key
413payload_too_largeThe request body exceeds 32 MBShrink the body; base64 images or huge attachments are the usual culprits
429key_daily_limit_reachedThe key's daily dollar limit is used upWait for the reset at 00:00 UTC or raise the limit in the dashboard
429key_monthly_limit_reachedThe key's monthly limit is used upRaise the limit in the dashboard
400model_retiredThe model has been retiredThe message names a replacement; update the model name
503model_disabledThe model is temporarily disabledSwitch to a model from GET /v1/models

An important note on 429: Zukko has no request-rate limit on its side. The gateway's own 429s only mean exhausted key limits, so retrying a second later is pointless. Other 5xx errors coming from the provider are usually temporary and worth retrying after a pause.

Step 3. Set up retries in code

By default, the official OpenAI SDK retries a request twice on connection errors, 408, 409, 429 and 5xx. For a key-limit 429 and a 503 model_disabled these are wasted requests and extra delay. Turn off built-in retries and decide yourself:

bash
import os
import time

import openai
from openai import OpenAI

client = OpenAI(
    base_url="https://api.zukko.pro/v1",
    api_key=os.environ["OPENAI_API_KEY"],
    max_retries=0,
)


def error_code(exc: Exception) -> str | None:

error_code also works with Anthropic SDK exceptions, which have status_code and body attributes as well. In the Anthropic SDK the base URL has no /v1: https://api.zukko.pro. More about the clients is in the Python SDK guide.

Step 4. Log safely

The rules are simple:

  • never write the key to logs, not in full and not partially; keep it in an environment variable;
  • log the request body selectively: model, number of messages, text length, not content with personal data;
  • log response headers and usage in full: they contain no secrets.

To get response headers in the OpenAI SDK, use with_raw_response:

bash
raw = client.chat.completions.with_raw_response.create(
    model="gpt-5.6-sol",
    messages=[{"role": "user", "content": "test"}],
)
completion = raw.parse()
print("status:", raw.status_code)
print("cost header:", raw.headers.get("X-Zukko-Cost-Micros"))
print("usage:", completion.usage)

Step 5. Check the base URL and the model name

A 404 on /chat/completions almost always means a wrong address:

  • OpenAI SDK and OpenAI-compatible clients: https://api.zukko.pro/v1; the SDK appends /chat/completions itself.
  • Anthropic SDK and Claude Code: https://api.zukko.pro without /v1; the client appends /v1/messages itself.

The list of enabled models comes from a public endpoint that needs no key:

bash
curl https://api.zukko.pro/v1/models
bash
{
  "object": "list",
  "data": [
    {"id": "claude-sonnet-5", "object": "model", "created": 1700000000, "owned_by": "zukko"},
    {"id": "gpt-5.6-sol", "object": "model", "created": 1700000000, "owned_by": "zukko"}
  ]
}

The list contains only models that are enabled right now. If the model you need is missing, a request to it will get model_disabled or model_retired. The current list is also on the "Models" page in the dashboard; the endpoint is described in the models reference, and keys and auth in the authentication section.

FAQ

How do I debug streaming?

Look at the raw events with curl using -N (no buffering) and "stream": true in the body. Each event is a data: {...} line; on Zukko the last chunk carries usage and an empty choices. So in code, check before accessing chunk.choices[0]:

bash
stream = client.chat.completions.create(
    model="gpt-5.6-sol",
    messages=[{"role": "user", "content": "test"}],
    stream=True,
)
for chunk in stream:
    if chunk.choices:
        print(chunk.choices[0].delta.content or "", end="", flush=True)

An error before the first event arrives as normal JSON with an HTTP status; a break mid-stream is a network error, and the whole request should be retried.

How do I count tokens before sending?

For Claude models there is POST https://api.zukko.pro/v1/messages/count_tokens in Anthropic format (with the anthropic-version: 2023-06-01 header). It returns input_tokens, the number of input tokens; output depends on the answer and cannot be counted in advance. See the count_tokens docs.

I get a 429 even though I send few requests. Why?

Because a 429 on Zukko is about money, not frequency: the key has used up its daily (key_daily_limit_reached) or monthly (key_monthly_limit_reached) limit. Check the key settings on the keys page.

Should I log message text?

Only if you cannot do without it, and with personal data in mind. Model, request size, usage, HTTP status and error.code are usually enough. If you need the text, store it separately with restricted access and a retention period.

zukko

Pay 25×
less

for Claude Code & ChatGPT

One fetch_* key and base URL api.zukko.pro — for Cursor, Claude Code, Codex, and any SDK.

Get API keyDocumentation

api.zukko.pro · fetch_*

Debugging LLM API Requests: curl, Logs, Error Codes · Zukko