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 | Returns *StreamInterruptedError with Partial; retries when nothing was shown yet |
| The content filter refuses a request | Returns *ContentFilterError, or tries the next fallback model |
| A model reads text only and the request has images | Returns *ImagesNotSupportedError, or tries the next fallback model |
| A model's daily token budget is used up | Returns *ProviderBudgetError, or tries the next fallback model |
| A model ID is mistyped | Fixes a missing fta/ prefix; otherwise lists close IDs in Suggestions |
| The daily check-in isn't done | Returns *CheckinRequiredError at once, with CheckinURL() |
| Today's free requests are used up | Returns *DailyLimitError at once, with ResetsAt() |
The key has quotes, spaces, or Bearer around it |
Cleans it |
| The key is only part of a key | Returns *IncompleteKeyError from NewClient, before any request |
| Edge errors during a deploy (HTML 502 or 530 pages) | Retries with backoff, then returns *InternalServerError |
| Network errors and other 5xx answers | Retries with backoff (WithMaxRetries, default 3) |
A model puts its thinking inside <think> tags |
Moves it to ReasoningContent |
Errors that retrying cannot fix are returned at once, so you never wait on retries that cannot succeed.
Fallback models¶
client, _ := freetheai.NewClient(freetheai.WithFallbackModels("fta/zai/glm-5.3-flash", "fta/mmx/minimax-m3"))
reply, _ := client.Chat.Completions.New(ctx, freetheai.ChatCompletionParams{Model: "fta/kimi/k3", Messages: messages})
fmt.Println(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 daily budget, is unknown, or still fails after its retries. Set FallbackModels on a single call to override the list for that call.
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.WithMaxConcurrency(2).
A stream holds its slot until you finish reading it or call Close. Always defer stream.Close().
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 the stream transport on (the default), New asks the API for a stream and assembles it into one ordinary chat completion. Your code sees no difference.
Turn it off with freetheai.WithStreamTransport(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.