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.
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 200or an error code. - Content-Type:
application/jsonfor a normal response,text/event-streamfor streaming. HTML instead of JSON usually means the request went to the wrong address. - Zukko service headers on successful responses:
X-Zukko-Cost-MicrosandX-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:
{
"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
messageas 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
codefield; then rely on the status andtype.
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
| HTTP | code | Cause | Fix |
|---|---|---|---|
| 401 | none (type: authentication_error) | The key is wrong, revoked, or missing | Check the Authorization: Bearer fetch_... header and environment variables |
| 402 | insufficient_balance | Balance below $0.01 | Top up in the dashboard |
| 403 | email_unverified | The account email is not verified | Verify your email |
| 403 | model_not_allowed_for_key | The model is not in the key's allowed list | Add the model in the key settings or use another key |
| 413 | payload_too_large | The request body exceeds 32 MB | Shrink the body; base64 images or huge attachments are the usual culprits |
| 429 | key_daily_limit_reached | The key's daily dollar limit is used up | Wait for the reset at 00:00 UTC or raise the limit in the dashboard |
| 429 | key_monthly_limit_reached | The key's monthly limit is used up | Raise the limit in the dashboard |
| 400 | model_retired | The model has been retired | The message names a replacement; update the model name |
| 503 | model_disabled | The model is temporarily disabled | Switch 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:
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:
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/completionsitself. - Anthropic SDK and Claude Code:
https://api.zukko.prowithout/v1; the client appends/v1/messagesitself.
The list of enabled models comes from a public endpoint that needs no key:
curl https://api.zukko.pro/v1/models{
"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]:
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.
Related articles
- OpenAI API Keys in the Open: Why You Should Not Use ThemWhere free keys on GitHub come from, why using them is dangerous for you and for the owner, and how to protect your own key from the same leak.
- GLM API Errors: Why the Request Fails and How to Fix ItA wrong model id, an overflowing 128K context, the key, the balance, limits: the typical causes of GLM API errors and a quick way to find yours.
- This organization has been disabled in the Claude API: What to DoAn Anthropic key suddenly answers "organization has been disabled". What the error is, what to check, and how to move the SDK and Claude Code to another provider.
- Your Own Neural Network Proxy on Node.js: Keep the API Key Off the FrontendA small Express server between your frontend and the model API: the key stays on the server, and models, request size and rate are capped.