Every failure raises a typed exception. All of them inherit from TraceError,
so one except TraceError catches everything the SDK can throw.
from trace_sdk import Trace, TraceError
client = Trace()
try:
completion = client.chat.completions.create(
messages=[{"role": "user", "content": "Summarise this chapter."}],
)
except TraceError as error:
print(error) # the message, which always says what to do next
print(error.status_code) # 402
print(error.code) # "insufficient_credits"
The hierarchy#
TraceError
├── APIConnectionError the request never reached Trace
│ └── APITimeoutError Trace did not answer in time
├── APIStatusError an error status with no more specific class
├── AuthenticationError 401
├── PermissionDeniedError 403
├── BadRequestError 400
├── ModelNotFoundError 404, unknown model id
├── InsufficientCreditsError 402
├── RateLimitError 429
└── ServiceUnavailableError 5xx
Fields on every error#
| Field | Meaning |
|---|---|
str(error) | The message. Multi-line: what happened, then what to do. |
error.message | The same text. |
error.status_code | The HTTP status, or None for connection failures. |
error.code | A stable machine-readable code, e.g. "model_not_found". |
error.type | The error family, e.g. "invalid_request_error". |
error.request_id | Trace's id for the request, when there is one. |
error.body | The raw error body as a dict. |
Match on error.code, not on the message text. Messages are written for humans
and will be improved; codes are a contract.
AuthenticationError — 401#
No key, an unknown key, a revoked key, or a credential that is not a Trace key at all.
That Trace API key is not valid or has been revoked. Run `trace login` to get a new one,
or create one at /account/developer.
Run `trace login` to sign in, or set TRACE_API_KEY to a key from your account page.
Fix: run trace login. If you are setting TRACE_API_KEY yourself, check
it starts with trace_sk_ and has not been revoked at /account/developer.
Codes: missing_api_key, invalid_api_key.
InsufficientCreditsError — 402#
The reservation for this request is larger than the balance.
except InsufficientCreditsError as error:
error.balance # 0.5
error.estimated_cost # 4.2
Fix, in order of what to try:
- Lower
max_tokens. The reservation is your prompt plus the fullmax_tokens, so a generous cap can refuse a request you could actually afford. - Use a cheaper model — see Models and pricing.
- Ask your teacher for a grant.
Nothing is charged for a refused request; no ledger row is written. See Credits and limits.
ModelNotFoundError — 404#
The model id is not on the roster, or you passed a chat model to
embeddings.create() (or an embedding model to chat.completions.create()).
except ModelNotFoundError as error:
print(error.available_models)
# ['deepseek/deepseek-v4-flash-0731:nitro', 'xiaomi/mimo-v2.5', ...]
The message lists the valid ids, so the fix is in the traceback. Usually it is a typo. Occasionally a model has been switched off temporarily, in which case it is missing from the list rather than misspelled.
Fix: copy an id from error.available_models, or run trace models.
RateLimitError — 429#
More than 30 requests in one minute from one key.
import time
try:
response = client.chat.completions.create(model=model, messages=messages)
except RateLimitError as error:
time.sleep(error.retry_after)
# then retry the request
retry_after is in seconds and is always set. The SDK auto-retries a 429 only on
requests that are safe to repeat (GETs); model completions and streaming requests
are not retried automatically, so a 429 there surfaces to you directly.
Fix: put a short sleep inside your loop. Catching the error and retrying immediately makes it worse.
BadRequestError — 400#
The request itself is wrong. The message names the specific problem.
| Code | Cause |
|---|---|
max_tokens_too_large | Over the 4000 cap. |
unsupported_parameter | n greater than 1. |
missing_messages | messages was empty. |
invalid_message | A message with no role. |
invalid_tool | A tool without function.name. |
missing_input | embeddings.create() was given nothing to embed. |
invalid_input | input was not a string or a list of strings. |
too_many_inputs | More than 128 texts to embeddings.create(). |
request_too_large | Body over 256 KB. |
Fix: read the message; it says the limit and the value you sent.
ServiceUnavailableError — 5xx#
The provider behind Trace failed, timed out, or is not configured.
The model provider rejected this request.
This is a problem on Trace's side, not in your code. Try again in a moment.
The SDK retries these automatically. Nothing is charged: the reservation is returned in full.
Fix: wait and try again. If it persists across models, Trace has a problem.
APIConnectionError and APITimeoutError#
The request never reached Trace, or Trace did not answer inside the client
timeout. These are network conditions, not API errors — status_code is None.
Could not reach Trace at http://localhost:8000/api/sdk/v1.
Check your internet connection, and check TRACE_BASE_URL if you set it.
Fix: check your connection. If you set TRACE_BASE_URL, check it points at
a running Trace. If you are on a school network, a proxy may be blocking it.
For a timeout, raise the client timeout or make the request smaller:
client = Trace(timeout=120.0)
Errors during a stream#
An error can arrive after some text has already been yielded. The iterator
raises at that point, so wrap the for loop, not just the call that creates it:
try:
for chunk in client.chat.completions.create(messages=messages, stream=True):
print(chunk.text, end="")
except TraceError as error:
print("\nstream failed:", error)
See Streaming.
Which errors are retried#
The SDK retries 429 and 5xx responses, and connection failures, up to
max_retries times (default 2) with no delay between attempts — but only for
requests that are safe to repeat. Model completions (POST) and streaming
requests are not retried, because a retry could double-charge or duplicate a
partially delivered response; handle those yourself as shown above.
It never retries 400, 401, 402, 403, or 404. Repeating a request the account cannot pay for, or one that is simply malformed, only burns the rate limit.
client = Trace(max_retries=0) # retry nothing