skip to content
TypeScript

SDK

TypeScript

How to point the official openai package at Conifer.

terminal
npm install openai

Install the package, then paste this handler into an App Router route such as app/api/chat/route.ts. Outside Next.js, keep conifer() in a shared module and call it from any server code.

route.ts
import OpenAI from "openai";

export function conifer() {
  const apiKey = process.env.CONIFER_API_KEY;
  if (!apiKey) {
    throw new Error("CONIFER_API_KEY is missing. Set it before calling the gateway.");
  }
  return new OpenAI({
    baseURL: process.env.OPENAI_BASE_URL ?? "https://api.conifer.build/v1",
    apiKey,
  });
}

export async function POST(req: Request) {
  const res = await conifer().chat.completions.create({
    model: "claude-haiku-4-5",
    max_tokens: 1024,
    messages: [{ role: "user", content: "three names for a build cache" }],
  });

  return Response.json({ content: res.choices[0].message.content });
}

We document CONIFER_API_KEY as the variable name. If you prefer a drop-in setup, the OpenAI constructor 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 send stream_options: { include_usage: true } to get a final chunk with usage and price the token counts from the catalog.

stream.ts
const stream = await 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 await (const chunk of stream) {
  const delta = chunk.choices[0]?.delta;
  if (delta?.content) process.stdout.write(delta.content);
}

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.ts
const res = await 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?" }],
});

console.log(res.choices[0].message.tool_calls ?? []);

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. Both fields also stream on choices[0].delta. Not every model emits a trace. If the field is missing, the model did not produce one.

reasoning.ts
const res = await client.chat.completions.create({
  model: "claude-haiku-4-5",
  max_tokens: 8192,
  reasoning: { effort: "medium" },
  messages: [{ role: "user", content: "three names for a build cache" }],
});

const msg = res.choices[0].message;
const trace = msg.reasoning ?? msg.reasoning_content;
if (trace) console.log(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 /v1/messages.

cache.ts
const details = res.usage?.prompt_tokens_details;
console.log(details?.cached_tokens);
console.log(details?.cache_write_tokens);

Azure BYOK

First save a resource in Add Azure OpenAI. Then set model to the deployment name or azure/<deployment>. Your deployments do not appear in GET /v1/models.

azure.ts
const res = await 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
npm install @anthropic-ai/sdk
app/api/messages/route.ts
import Anthropic from "@anthropic-ai/sdk";

export function conifer() {
  const apiKey = process.env.CONIFER_API_KEY;
  if (!apiKey) {
    throw new Error("CONIFER_API_KEY is missing. Set it before calling the gateway.");
  }
  return new Anthropic({
    baseURL: process.env.ANTHROPIC_BASE_URL ?? "https://api.conifer.build",
    apiKey,
  });
}

export async function POST(req: Request) {
  const res = await conifer().messages.create({
    model: "claude-haiku-4-5",
    max_tokens: 1024,
    messages: [{ role: "user", content: "three names for a build cache" }],
  });

  return Response.json({ content: res.content });
}

The base URL is https://api.conifer.build, with no /v1 suffix. This route serves Anthropic model ids.

Errors

errors.ts
import OpenAI, { APIError } from "openai";

try {
  await client.chat.completions.create({ /* … */ });
} catch (err) {
  if (err instanceof APIError) {
    console.error(err.status, err.code, err.error);
  }
  throw err;
}

Branch on err.status and err.code. The OpenAI-compat code names are invalid_api_key, insufficient_quota, rate_limit_exceeded, model_not_found, and context_length_exceeded. A 402 stays a 402. The full table is on Errors.