Models
Connect Anthropic, OpenAI or any other provider, with retries and fallbacks handled for you.
A model is one line of config: the provider's model id. Keys come from your environment unless you pass them.
| TypeScript | Python | |
|---|---|---|
| Anthropic | anthropic() from @threads/anthropic | anthropic() from threads.anthropic |
| OpenAI | openai() from @threads/openai | openai() from threads.openai |
| Everything else | aiSdk() from @threads/ai-sdk (any AI SDK provider) | litellm() from threads.litellm (LiteLLM's openai/ route) |
Anthropic
import { anthropic } from "@threads/anthropic";
const claude = anthropic("claude-sonnet-5");Reads ANTHROPIC_API_KEY.
Prompt caching
Anthropic models cache the prompt for 5 minutes by default: the system prompt and tools, and the conversation as it grows. A cached token costs a fraction of a normal input token to read back. For a thread with longer pauses, cache for an hour:
const claude = anthropic("claude-sonnet-5", { promptCache: "1h" });- Writing to the cache costs more than plain input: 1.25 x for 5 minutes, 2 x for an hour. If you give a
pricewithoutcache_write, threads fills it in frominputat that rate. Withoutcache_readit fills in 0.1 xinput, the highest read rate Anthropic lists, so a cost can come out high but never low; passcache_readfor your model's exact rate (0.05 x onclaude-opus-5-5). - A short one-off prompt may pay the write and never read it back.
promptCache: false(prompt_cache=False) turns caching off. "1h"can't be combined withhostedTools: Anthropic caches hosted tool results for 5 minutes, so writes would be billed at two rates.cache_controlinparamsor a hosted tool is refused too;promptCacheis the one way to set caching.- The setting is part of the thread's pinned config. Threads started before prompt caching existed continue only with
promptCache: false. citations: trueturns on citations for every document in a request. It can't be combined with a native structured-output field inparams(output_format,output_config.format); agentoutputis unaffected.
OpenAI
Uses the Responses API.
import { openai } from "@threads/openai";
const gpt = openai("gpt-5.5", {
params: { reasoning: { effort: "medium" } },
});Reads OPENAI_API_KEY. Both factories read the key at check() or the first run, never when you call them. OpenAI caches prompts on its own; set params: { prompt_cache_retention: "24h" } to keep them longer.
Limits
threads needs two numbers for every model: the most input tokens one request may carry (for compaction and cost budgets) and the most output tokens the model can produce. For the model ids below you pass neither: they come from a catalog each provider's own source was checked against.
| Model id | Max input tokens | Max output tokens | Checked against |
|---|---|---|---|
claude-fable-5-1, claude-opus-5-5, claude-sonnet-5 | 1,000,000 | 128,000 | Anthropic Models API |
claude-haiku-4-5-20251001 | 200,000 | 64,000 | Anthropic Models API |
gpt-5.5, gpt-5.5-2026-04-23, gpt-6-sol | 922,000 | 128,000 | OpenAI model pages* |
* Derived, not published: OpenAI's GPT-5.5 page gives a 1,050,000-token context window and a 128,000 output limit but no input limit, so threads uses the window minus the output limit, 922,000. OpenAI's GPT-6 Sol page publishes 922,000 as the input limit for the same window and output limit.
The id must match exactly: claude-haiku-4-5 is not claude-haiku-4-5-20251001. A dated snapshot id keeps a long-lived thread on one model. For any other id, pass both limits; leaving one out is a ConfigError that names it:
const next = anthropic("claude-next", {
maxInputTokens: 1_000_000,
maxOutputTokens: 128_000,
});Each request asks for at most maxTokens (max_tokens) output tokens: 8192 by default, or the model's output limit if that is lower. A longer answer continues in a new request. Raise it for long single answers, up to the model's output limit:
const writer = anthropic("claude-sonnet-5", { maxTokens: 32_000 });maxTokens is the only way to set the cap: putting max_tokens (or OpenAI's max_output_tokens) in params is refused. Output-token budgets read this cap, for every provider.
The limits are recorded when a thread starts. If a catalog entry is ever found wrong, it is withdrawn rather than edited: the factory then asks you for the limits, and its error prints both the corrected values (for new threads) and the old ones (to keep continuing a thread started with them).
Release note: threads started before the model catalog
A thread continues only with the same pinned limits and cap it started with. These threads, started before the catalog, can't be continued with the new defaults; they stay readable (timeline(), cost(), export):
- OpenAI threads (either language): the cap is now pinned as
max_tokens. They can't be continued. - TypeScript
aiSdkthreads: the cap is now pinned asmax_tokens. They can't be continued. - Python
anthropicandlitellmthreads that used the default cap: it was the model's whole output limit and is now 8192. Pass the old value to continue one, e.g.max_tokens=128_000. - Threads started with limits other than the catalog's (such as the README's old
200_000forclaude-sonnet-5): pass the oldmaxInputTokens,maxOutputTokensandmaxTokensto continue one.
Other providers
aiSdk() wraps any AI SDK provider. Build the model inside the factory with the fetch threads hands you, so every request goes through threads.
import { aiSdk } from "@threads/ai-sdk";
aiSdk({
model: (fetch) => createProvider({ fetch })("model-id"),
maxInputTokens: 128_000,
maxOutputTokens: 8192,
});createProvider stands for your provider's factory, such as createOpenAI or createMistral. Passing a ready-made model instead of a factory is refused at setup. The two limits are required: the model id alone doesn't say whose model it is. Other options: maxTokens, params (AI SDK call options like temperature), accepts (input types, default text only), price, and cacheTtlMs: how long the provider keeps prompt-cache entries, in milliseconds, or "none" (see Cache lifetimes).
Shared options
| Option (TS / Python) | What it does |
|---|---|
maxInputTokens / max_input_tokens | The most input tokens one request may carry. Default: the catalog's value; required for an unlisted id. Recorded as the model's context_window |
maxOutputTokens / max_output_tokens | The model's output limit. Default: the catalog's value; required for an unlisted id |
maxTokens / max_tokens | The output cap of each request. Default: 8192, or the output limit if lower |
params | Other provider request fields (temperature, reasoning, thinking, ...). Fields threads sets itself, like model, tools or the output cap, are refused |
price | Price per token, for cost budgets: { input, output, cache_read?, cache_write? } in nano-dollars (USD 3 per million tokens is 3000) |
hostedTools / hosted_tools | Provider-run tools. Only web search (both) and web fetch (Anthropic) are accepted |
apiKey / api_key | A string or a secret(). Default: the provider's environment variable (ANTHROPIC_API_KEY, OPENAI_API_KEY), read at check() or the first run. Never logged, and redacted from tool results |
baseUrl / base_url | A proxy or compatible endpoint |
Model settings are fixed for the life of a thread, which keeps the prompt prefix identical on every request so provider caches keep hitting.
Cache lifetimes
Cache breaks are judged by how long the provider keeps the cache. Each model declares it: anthropic() from promptCache, openai() from its retention, the scripted test model as none. An agent uses the lifetime its models agree on, so anthropic("claude-sonnet-5", { promptCache: "1h" }) needs nothing else.
check() (and the first run) asks you to set context.cache_ttl_ms when threads can't decide:
- the model and a fallback cache for different lifetimes;
- a model's lifetime is unknown: an
aiSdk()orlitellm()model that doesn't passcacheTtlMs/cache_ttl_ms, or aModelyou wrote yourself whoseinfohas nocache(declarecache: { ttl_ms: 300_000 }, or"none"if it never caches).
const assistant = agent({
model: anthropic("claude-sonnet-5", { promptCache: "1h" }),
fallback: [gpt],
context: { cache_ttl_ms: 3_600_000 },
});Retries
threads owns every retry; provider SDK retries are turned off. Rate limits, overloads and server errors are retried with exponential backoff, honoring retry-after. The defaults:
| Setting | Default |
|---|---|
max_retries | 8 |
base_delay_ms / max_delay_ms | 1,000 / 32,000 |
max_retry_after_ms | 60,000 |
max_total_wait_ms | 600,000 |
fallback_after | 3 overloaded errors in a row |
When retries run out, the run ends failed with code model_unavailable. In TypeScript, pass any subset: retry: { max_retries: 4 }. Python's retry= takes a complete Retry object from threads.log.
Fallback models
After fallback_after overloaded errors in a row, the agent switches to the next model in fallback and records the switch in the log. From then on requests go to that model, and its own context window sets the compaction thresholds.
const assistant = agent({
model: claude,
fallback: [gpt],
retry: { max_retries: 4, fallback_after: 2 },
});A fallback lasts for the rest of the turn. With the default fallback_scope: "turn", the thread's next input goes back to the settings it had before the fallback, recorded as a settings_changed with reason revert before that input's first request. With fallback_scope: "thread" the fallback stays.
The before_model_switch hook gates every switch, fallbacks and reverts alike. A deny keeps the current model: a denied fallback retries on the same model, and a denied revert keeps the fallback for that input's turn. The hook is asked once per input for a revert, even across a crash.
Every fallback model counts toward a budget: a model with no per-attempt bound for one of the budget's limits is refused at setup (budget_unenforceable).
Switch models mid-thread
An operator can switch a thread to another model it already knows (its model or one of its fallback models) with the thread's setModel (set_model). The change is recorded in the log and applies from the next request. See Thread.