skip to content
Python

SDK

Python

How to point the official openai and anthropic packages at Conifer.

terminal
pip install openai
app.py
from 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)

We document CONIFER_API_KEY as the variable name. If you set the drop-in pair below, the OpenAI client also reads OPENAI_API_KEY.

terminal
export OPENAI_BASE_URL=https://api.conifer.build/v1
export OPENAI_API_KEY=$CONIFER_API_KEY

Streaming

stream=True returns SSE, and tokens arrive as the model produces them. A few providers hand back the whole answer at the end instead, in the same chunk shape. The cost header is absent on a stream, so price the final usage chunk from the catalog when you need the figure.

stream.py
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. Before you do, check that the model’s caps field on GET /v1/models includes tools. If it does not, Conifer returns a 400 that names the missing capability. That 400 is not a 402 and not a 429. If a successful reply has no tool_calls, the model did not call a tool.

terminal
curl -s https://api.conifer.build/v1/models \
  -H "Authorization: Bearer $CONIFER_API_KEY" \
  | jq '.data[] | {id, caps, context_window, pricing}'
tools.py
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

The official OpenAI types do not declare these fields, but you can still read them off the message. The trace is at choices[0].message.reasoning. Some models use DeepSeek’s name instead, so also check reasoning_content. Pass reasoning through extra_body, because the typed client drops unknown kwargs. Not every model emits a trace. If the field is missing, the model did not produce one.

reasoning.py
res = client.chat.completions.create(
    model="claude-haiku-4-5",
    max_tokens=8192,
    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 counts appear under usage.prompt_tokens_details as cached_tokens and cache_write_tokens. Both are 0 when nothing was cached, and the details object itself may be absent. Read only the usage fields that exist, and do not assume a thinking-token count unless that field is on the body. To send cache directives, use the Anthropic package below.

cache.py
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

First save a resource in Add Azure OpenAI. Then set model to the deployment name or azure/<deployment>.

azure.py
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

terminal
pip install anthropic
terminal
export ANTHROPIC_BASE_URL=https://api.conifer.build
export ANTHROPIC_API_KEY=$CONIFER_API_KEY
messages.py
from 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)

The base URL is https://api.conifer.build, with no /v1 suffix. This route serves Anthropic model ids. Send cache_control breakpoints on this wire, and usage then reports cache_read_input_tokens and cache_creation_input_tokens.

Errors

errors.py
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. The code names are invalid_api_key, insufficient_quota, rate_limit_exceeded, model_not_found, and context_length_exceeded. A 402 stays a 402 and arrives as a plain APIStatusError, which the APIError branch catches. The full table is on Errors.