SDK
The Conifer SDK
The open-source client. It gives you the exact cost of every call, a hard spend ceiling, and errors you can branch on.
The Conifer SDK is open source at ConiferKit/use-conifer, in TypeScript and Python. It is a thin client over the same wire you could call with curl, so nothing here is required. The official OpenAI and Anthropic packages work against Conifer as they are. Use this one when you want cost and spend control in the code itself.
When to use it
| What you get | What it does |
|---|---|
| Receipts | The exact cost of each call, in integer nanodollars, on the response itself. No stats endpoint and no estimate from token counts. |
| A spend ceiling | maxCostNanoUsd is enforced before the model runs. A request that could go over it is refused, and nothing is charged. |
| Named refusals | Out of credit, over your ceiling, and a key at its own cap are three different problems. The SDK raises a separate error type for each. |
| Fallbacks | Name a backup model or two and the turn is retried on them if the first cannot answer. The receipt says which one did. |
Install
npm i conifer-sdkpip install "conifer-sdk[tls]"One call
Set CONIFER_API_KEY and construct the client with no arguments. Both languages take the same shape.
import { Conifer, textOf } from "conifer-sdk";
const conifer = new Conifer(); // reads CONIFER_API_KEY
const answer = await conifer.chat({
model: "claude-haiku-4-5",
messages: [{ role: "user", content: "three names for a build cache" }],
maxTokens: 200,
maxCostNanoUsd: 5_000_000, // refuse the turn if it could cost over $0.005
});
console.log(textOf(answer));
console.log(answer.receipt.costUsd); // "0.001250000", this exact callfrom conifer_sdk import Conifer, ChatRequest
conifer = Conifer() # reads CONIFER_API_KEY
answer = conifer.chat(ChatRequest(
model="claude-haiku-4-5",
messages=[{"role": "user", "content": "three names for a build cache"}],
max_tokens=200,
max_cost_nano_usd=5_000_000,
))
print(answer.text, answer.receipt.cost_usd)Streaming
A streamed turn carries no cost header, because the response starts before the cost is known. The SDK asks for the final usage chunk on every stream so you can price the token counts from the catalog. A buffered call carries the settled cost directly.
Errors you can branch on
import { Conifer, ConiferCostCeilingError, ConiferPaymentError } from "conifer-sdk";
try {
await conifer.chat({ model: "claude-opus-5", messages, maxCostNanoUsd: 1_000 });
} catch (err) {
if (err instanceof ConiferCostCeilingError) {
// your own ceiling refused the turn. Raise it, or pick a cheaper model
} else if (err instanceof ConiferPaymentError) {
// the account is out of credit. Top up
}
}The wire-level names behind these types, such as insufficient_quota and invalid_api_key, are on Errors.
Fallbacks
Server chain: serverFallbackModels
Name up to three backup models. If the model you asked for cannot answer, Conifer tries the chain in order inside the same request, and you pay once, for whichever model answered. The receipt names it.
const answer = await conifer.chat({
model: "deepseek-v4-flash",
messages: [{ role: "user", content: "classify: refund request" }],
maxTokens: 200,
serverFallbackModels: ["glm-5.3-flash", "gemini-3.5-flash"],
});
answer.receipt.effectiveModel; // the model that answeredcurl https://api.conifer.build/v1/chat/completions \
-H "authorization: Bearer $CONIFER_API_KEY" \
-H "x-conifer-fallback-models: glm-5.3-flash,gemini-3.5-flash" \
-H "content-type: application/json" \
-d '{"model":"deepseek-v4-flash","messages":[{"role":"user","content":"hi"}],"max_tokens":200}'Every member is checked against the catalog before anything is spent, so a typo is a clear refusal up front rather than a surprise later.
Client chain: fallbackModels
The SDK itself re-sends to the next model after a retryable failure. Each attempt that reaches a model is a separate billed request, which is why allowClientFallback: true is required. A 402 stops the chain. A ConiferCapabilityError, the unbilled refusal for something the model cannot do, such as an image on a model without vision, moves the TypeScript chain on to the next model.
const answer = await conifer.chat({
model: "deepseek-v4-flash",
messages,
fallbackModels: ["glm-5.3-flash"],
allowClientFallback: true, // required: each attempt is a separate billed request
});
answer.fallbackIndex; // 0 = primary answered, 1 = first fallback answered| Goal | Use |
|---|---|
| Stay up through a provider outage | serverFallbackModels |
| Absorb a capability mismatch | fallbackModels + allowClientFallback |
| Both | They compose. Each client attempt can carry its own server chain. |
Reasoning effort
One reasoning_effort field works across every provider. The levels are none, minimal, low, medium, high, xhigh and max. Where a model offers fewer levels, Conifer maps yours to the nearest one it has, so a level you ask for is never a refusal. On Anthropic models the effort becomes a thinking budget, so give the turn max_tokens room for the answer after the thinking.
curl https://api.conifer.build/v1/chat/completions \
-H "authorization: Bearer $CONIFER_API_KEY" \
-H "content-type: application/json" \
-d '{
"model": "glm-5.3",
"max_tokens": 3000,
"reasoning_effort": "medium",
"messages": [{"role": "user", "content": "why is the sky blue?"}]
}'Empty answers
A reasoning model spends maxTokens on its thinking first. A budget that looks generous for a short answer can run out before the visible answer starts, and you get empty content with finish_reason: "length". That looks the same as a refusal or a content filter. emptyReason(answer) in TypeScript and answer.empty_reason in Python tell them apart. Both return nothing when there is text, and nothing for a tool call, because empty text beside a tool call is a correct answer.
Embeddings
Embeddings use the same key, the same receipts and the same ceiling. The SDK fetches vectors in a compact form and decodes them for you, so you get plain numbers. Embeddings bill on input only, so the cost is on the response itself.
const result = await conifer.embeddings.create({
model: "text-embedding-3-small",
input: ["alpha", "beta"], // one vector per input, in order
});
result.data[0].embedding.length; // 1536
result.receipt.costUsd; // the settled cost of this callDeferred jobs
Submit work that does not need an answer now, such as an overnight re-index or a bulk classification, and collect it later. The completion window is at least 24 hours.
const job = await conifer.defer({
model: "claude-haiku-4-5",
messages: [{ role: "user", content: "classify these 400 tickets…" }],
});
const answer = await conifer.jobs.wait(job.jobId);wait() polls with backoff until the job finishes. On timeout it raises without cancelling, so a client-side clock never kills work you already paid for.
Receipts in any client
The cost comes back in response headers on every wire. The Conifer SDK also copies it into the body as usage.cost and usage.cost_nanousd. An absent cost means not yet known, never zero.
| Header | Meaning |
|---|---|
x-conifer-cost-nanousd | Settled cost of this call, in integer nanodollars. Absent on a stream. |
x-conifer-requested-model, x-conifer-effective-model | What you asked for and what answered. They differ when you let Conifer choose the model, when a fallback you named served the turn, or when you sent a namespaced spelling. |
x-conifer-request-id | Quote this in support requests. |
The OpenAI and Anthropic packages, LangChain, LiteLLM and the Vercel AI SDK all discard headers. Each takes an injected fetch or http_client, so hand it one that reads the receipt on the way past. It reads headers only and never touches the body.
import OpenAI from "openai";
import { ReceiptCollector } from "conifer-sdk";
const receipts = new ReceiptCollector();
const openai = new OpenAI({
baseURL: "https://api.conifer.build/v1",
apiKey: process.env.CONIFER_API_KEY,
fetch: receipts.fetch, // the only line that changes
});
await openai.chat.completions.create({ model: "claude-haiku-4-5", messages });
receipts.last.costNanoUsd; // 580000, that exact call
receipts.total.costUsd; // "0.001170000", the whole sessionStreams
No cost header on a streamed turn. Ask for the final usage chunk and price the token counts from the catalog, or make the call buffered when you need the settled figure.
const stream = await openai.chat.completions.create({
model: "claude-haiku-4-5",
messages,
stream: true,
stream_options: { include_usage: true },
});
for await (const chunk of stream) {
if (chunk.usage) chunk.usage.completion_tokens; // the final usage chunk: token counts
}
// No cost header on a stream. Price the counts from the catalog, or read
// x-conifer-cost-nanousd on a buffered call.SpendBudget: a session ceiling
import { SpendBudget } from "conifer-sdk";
const budget = new SpendBudget(50_000_000); // $0.05 in integer nanodollars
const openai = new OpenAI({
baseURL: "https://api.conifer.build/v1",
apiKey: process.env.CONIFER_API_KEY,
fetch: budget.fetch,
});
budget.spentNanoUsd; // observed on real receipts
budget.remainingNanoUsd; // never negative
budget.exhausted; // once true, the next call throws before any requestSpendBudget is client-side. It stops the next call once the total you set has been spent. The hard server-side bound is the per-request maxCostNanoUsd, sent as the x-conifer-max-cost-nanousd header and enforced before the model runs. Use both.
Contribute
Integration bugs are the most valuable reports. File them at https://github.com/ConiferKit/use-conifer/issues. The overview explains how to run the test suites.