Reliability¶
Generic OpenAI clients treat every error the same. This client knows the FreeTheAI API's answers and handles each one the way it should be handled.
| What happens | What the client does |
|---|---|
A model is busy (503 with Retry-After) |
Tries the next fallback model at once, or waits Retry-After (at most 30 s) and retries |
| The account already has as many replies running as its plan allows (429) | Learns the limit and queues the rest instead of failing them |
| A long reply without streaming idles past a proxy timeout | Fetches non-streamed chats as a stream and assembles them (stream transport) |
| A stream ends without a finish reason | Raises StreamInterruptedError with .partial; retries when nothing was shown yet |
| The content filter refuses a request | Raises ContentFilterError, or tries the next fallback model |
| A model reads text only and the request has images | Raises ImagesNotSupportedError, or tries the next fallback model |
| A provider's daily token budget is used up | Raises ProviderBudgetError, or tries the next fallback model |
| A model ID is mistyped | Fixes a missing fta/ prefix; otherwise lists close IDs in ModelNotFoundError.suggestions |
| The daily check-in isn't done | Raises CheckinRequiredError at once, with checkin_url |
| Today's free requests are used up | Raises DailyLimitError at once, with resets_at |
The key has quotes, spaces, or Bearer around it |
Cleans it |
| The key is only part of a key | Raises IncompleteKeyError before any request is sent |
| Edge errors during a deploy (HTML 502 or 530 pages) | Retries with backoff, then raises InternalServerError |
| Network errors and other 5xx answers | Retries with backoff (max_retries, default 3) |
A model puts its thinking inside <think> tags |
Moves it to reasoning_content |
Errors that retrying cannot fix (a wrong key, the daily check-in, the daily limit, a bad request) are raised at once, so you never wait on retries that cannot succeed.
Fallback models¶
client = FreeTheAI(fallback_models=["fta/zai/glm-5.3-flash", "fta/mmx/minimax-m3"])
reply = client.chat.completions.create(model="fta/kimi/k3", messages=messages)
print(reply["model"]) # the model that answered
The chain is tried in order when a model is busy, filtered, can't read the request's images, is over its provider's budget, is unknown, or still fails after its retries. Pass fallback_models=[...] to create to override the list for one call, or fallback_models=[] to turn it off.
For streams, fallbacks only happen before the first piece of the reply arrives, so you never get two answers mixed.
Concurrency¶
Accounts run a limited number of replies at once (one or two on the free tier). The client starts with no limit. The first time the API refuses a call for concurrency, the client lowers its own limit to the number of replies that were running and queues further calls until a slot frees up. Ten minutes after the last refusal it tries more again, so a plan upgrade is picked up without a restart.
To set a fixed cap instead: FreeTheAI(max_concurrency=2).
A stream holds its slot until you finish reading it (or the iterator is closed), so read streams to the end or close them.
Stream transport¶
Some proxies close a connection that sends nothing for a while, and a non-streamed request sends nothing until the whole reply is written. With stream_transport=True (the default), create(...) without stream=True asks the API for a stream and assembles it into one ordinary chat.completion, with content, reasoning, tool calls, finish reason, and usage. Your code sees no difference.
Turn it off with FreeTheAI(stream_transport=False) to get the API's own non-streamed answer.
Error ids¶
Every API error carries an error_id (also sent as the X-Error-ID header). Quote it in Discord or a support ticket so the exact failure can be found.