SDK
Python
The official openai and anthropic packages, pointed at Conifer.
pip install openaifrom openai import OpenAI
import os
client = OpenAI(
base_url=os.environ.get("OPENAI_BASE_URL", "https://api.conifer.build/v1"),
api_key=os.environ["CONIFER_API_KEY"],
)
res = client.chat.completions.create(
model="claude-haiku-4-5",
max_tokens=1024,
messages=[{"role": "user", "content": "three names for a build cache"}],
)
print(res.choices[0].message.content)CONIFER_API_KEY is the name we document. The OpenAI client also reads OPENAI_API_KEY if you set the drop-in pair:
export OPENAI_BASE_URL=https://api.conifer.build/v1
export OPENAI_API_KEY=$CONIFER_API_KEYStreaming
stream=True returns well-formed SSE. On the chat wire most models currently deliver the completion as one burst. Do not build a UI on incremental token timing.
stream = client.chat.completions.create(
model="claude-haiku-4-5",
max_tokens=1024,
stream=True,
messages=[{"role": "user", "content": "three names for a build cache"}],
)
for chunk in stream:
delta = chunk.choices[0].delta
if delta and delta.content:
print(delta.content, end="", flush=True)Tools
Send OpenAI-shaped tools and handle tool_calls in the reply. Check caps on GET /v1/models first for the string tools. A model without that cap is a 400 that says so. That 400 is not a 402 and not a 429. Missing tool_calls on a successful reply means the model did not call a tool.
curl -s https://api.conifer.build/v1/models \
-H "Authorization: Bearer $CONIFER_API_KEY" \
| jq '.data[] | {id, caps, context_window, pricing}'res = client.chat.completions.create(
model="claude-haiku-4-5",
max_tokens=1024,
tools=[{
"type": "function",
"function": {
"name": "list_files",
"description": "List files in a directory",
"parameters": {
"type": "object",
"properties": {"path": {"type": "string"}},
"required": ["path"],
},
},
}],
messages=[{"role": "user", "content": "what is in /tmp?"}],
)
print(getattr(res.choices[0].message, "tool_calls", None) or [])Reasoning traces
Official OpenAI types do not declare these fields. Read choices[0].message.reasoning, and reasoning_content when upstream used DeepSeek’s name. Pass reasoning through extra_body; the typed client drops unknown kwargs. Not every model emits a trace. An omitted field is the absence signal, not an SDK bug.
res = client.chat.completions.create(
model="claude-haiku-4-5",
max_tokens=1024,
extra_body={"reasoning": {"effort": "medium"}},
messages=[{"role": "user", "content": "three names for a build cache"}],
)
msg = res.choices[0].message
trace = getattr(msg, "reasoning", None) or getattr(msg, "reasoning_content", None)
if trace:
print(trace)Cache metadata
On the chat wire, cache lands on usage.prompt_tokens_details: cached_tokens and cache_write_tokens (0 when the object is present and nothing cached). The details object itself may be omitted. Do not assume a thinking-token count on usage unless that field is present. Cache directives themselves ride the Anthropic package below.
details = getattr(getattr(res, "usage", None), "prompt_tokens_details", None)
if details is not None:
print(getattr(details, "cached_tokens", None))
print(getattr(details, "cache_write_tokens", None))Azure BYOK
After you save a resource in Add Azure OpenAI, set model to the deployment name or azure/<deployment>.
res = client.chat.completions.create(
model="azure/YOUR_DEPLOYMENT_NAME",
max_tokens=1024,
messages=[{"role": "user", "content": "three names for a build cache"}],
)Anthropic package
pip install anthropicexport ANTHROPIC_BASE_URL=https://api.conifer.build
export ANTHROPIC_API_KEY=$CONIFER_API_KEYfrom anthropic import Anthropic
import os
client = Anthropic(
base_url=os.environ.get("ANTHROPIC_BASE_URL", "https://api.conifer.build"),
api_key=os.environ["CONIFER_API_KEY"],
)
res = client.messages.create(
model="claude-haiku-4-5",
max_tokens=1024,
messages=[{"role": "user", "content": "three names for a build cache"}],
)
print(res.content)Base URL is https://api.conifer.build with no /v1. ANTHROPIC_AUTH_TOKEN also works and takes precedence when both are set. Anthropic model ids only. Send cache_control breakpoints on this wire; usage reports cache_read_input_tokens and cache_creation_input_tokens.
Errors
from openai import APIError, AuthenticationError, NotFoundError, RateLimitError
try:
client.chat.completions.create(...)
except AuthenticationError as e:
print("401", e.code)
except NotFoundError as e:
print("404", e.code, e.body)
except RateLimitError as e:
print("429", e.code)
except APIError as e:
print(e.status_code, e.code, e.body)Branch on status and code: invalid_api_key, insufficient_quota, rate_limit_exceeded, model_not_found, context_length_exceeded. 402 stays 402 and arrives as a generic APIError. The full table is on Errors.