API reference · v1

API documentation

ChromaLab is a gateway: you bring the client, it handles protocol translation, routing, context management and billing. Anthropic Messages is the primary surface because that is what most agent harnesses already speak. The OpenAI, Gemini and MARI shapes reach the same catalog with the same key.

Base URLhttps://chromalab.ink

Introduction

Every request goes through the same path: your key is looked up, the model is checked against the key's filters and your plan, the conversation is trimmed to fit the model's context window without splitting a tool call from its result, the request is sent upstream, and usage is recorded once the response completes. Nothing is stored between requests — chroma has no conversation identity, so each call carries its own full transcript.

  • Quick start — set up a client with two environment variables and a key.
  • Messages — the primary protocol.
  • Request units — how a call is priced against a plan or the wallet.

Authentication

Keys are created on the API keys page and look like chroma-xxxxxxxxxxx-xxxxxxxxxxx. Every gateway endpoint requires one. Which header carries it depends on the protocol you are speaking, because each SDK sends its own — chroma accepts all three, so the client does not need to be patched.

Accepted authentication headers per endpoint.
HeaderAccepted onSent by
Authorization: BearerEvery endpointOpenAI SDKs, cURL, anything generic
x-api-key/v1/messagesThe Anthropic SDK and Claude Code
x-goog-api-key/v1beta/models/*The Google GenAI SDK
Any endpoint
curl https://chromalab.ink/v1/models \
  -H "Authorization: Bearer chroma-xxxxxxxxxxx-xxxxxxxxxxx"
Keys are secrets, and they are shown in full only on your dashboard.

Keep them out of client-side code and committed config. A leaked key spends your plan window. Rotate one by deleting it and creating a replacement — deletion takes effect on the next request.

A key can be given an expiry date, a model whitelist or blacklist, and a daily token budget. Those are enforced before the request leaves chroma — see per-key controls.


Quick start

Create an account, make a key, and point your client at the base URL. Nothing else changes: the model id, the message shape and the streaming format stay whatever your client already sends.

Shell
export ANTHROPIC_BASE_URL="https://chromalab.ink"
export ANTHROPIC_AUTH_TOKEN="chroma-xxxxxxxxxxx-xxxxxxxxxxx"
export ANTHROPIC_MODEL="anthropic/claude-sonnet-4.6"

claude
The Anthropic SDK appends /v1 itself.

Give it https://chromalab.ink with no path. OpenAI SDKs do not, so they get https://chromalab.ink/v1. Passing the wrong one produces a 404 on /v1/v1/messages.


Choosing a protocol

All five surfaces convert to the same internal request and reach the same catalog. They differ in how well they stream and how much of a tool loop survives the round trip. If you have a choice, use Messages.

Endpoints and their streaming behaviour.
EndpointFormatStreamingTools
POST /v1/messagesAnthropic MessagesIncrementalFull round trip
POST /v1/chat/completionsOpenAI Chat CompletionsIncrementalFull round trip
POST /v1/responsesOpenAI ResponsesIncrementalPlus web_search_preview
POST /v1beta/models/*Google GeminiBufferedText only
POST /mari/MARI (chroma-native)IncrementalFull round trip

Buffered means the upstream response is read to completion before chroma re-encodes it into that format, so the client sees one delayed burst rather than a live stream. Only Gemini is still on that path.


MessagesPOST /v1/messages

The Anthropic Messages API. This is the surface chroma optimises first, so pointing the Anthropic SDK or Claude Code at chroma reaches any catalog model, not only Anthropic ones. Three things this path does that a naive proxy does not:

  • tool_use and tool_result content blocks round-trip intact in both directions, along with tools and tool_choice.
  • Tool-argument deltas stream as they arrive (input_json_delta) rather than being buffered and re-encoded at the end.
  • cache_control markers on system and text blocks are forwarded upstream rather than stripped, so client-side prompt caching keeps working.

Request body

ParameterTypeDescription
modelrequiredstringAny catalog id, e.g. anthropic/claude-sonnet-4.6
messagesrequiredarrayTurns with role and string or block content
max_tokensrequiredintegerOutput cap. A key's own maxOutputTokens lowers it further
systemstring | arraySystem prompt; block form accepts cache_control
toolsarrayTool definitions, converted to OpenAI function tools upstream
tool_choiceobjectauto, any or a named tool
temperature, top_pnumberSampling controls, passed through
stop_sequencesarrayStop strings
streambooleanEmit Anthropic SSE events as the provider produces them

Example

Python
import anthropic

client = anthropic.Anthropic(
    api_key="chroma-xxxxxxxxxxx-xxxxxxxxxxx",
    base_url="https://chromalab.ink",  # no /v1 — the SDK appends it
)

message = client.messages.create(
    model="anthropic/claude-sonnet-4.6",
    max_tokens=1024,
    system="You are a terse code reviewer.",
    messages=[{"role": "user", "content": "Why is this loop O(n^2)?"}],
)

print(message.content[0].text)
Streamed usage counts are a floor, not a meter reading.

Not every provider emits per-chunk usage, so a streamed response's prompt tokens are estimated from the outgoing messages. Buffered responses carry the provider's real numbers.


Chat CompletionsPOST /v1/chat/completions

The OpenAI-native surface. Anything that speaks the OpenAI SDK — most tooling, most plugins, most quick scripts — works here unchanged.

Request body

ParameterTypeDescription
modelrequiredstringCatalog id, e.g. openai/gpt-5.1-codex
messagesrequiredarraysystem, user, assistant and tool turns
tools, tool_choicearray, string | objectFunction tools in OpenAI shape
max_tokensintegerOutput cap
temperature, top_pnumberSampling controls
streambooleanEmit OpenAI SSE chunks

Example response

JSON response
{
  "id": "chatcmpl-abc123",
  "object": "chat.completion",
  "created": 1677652288,
  "model": "openai/gpt-5.1-codex",
  "choices": [{
    "index": 0,
    "message": { "role": "assistant", "content": "Done." },
    "finish_reason": "stop"
  }],
  "usage": { "prompt_tokens": 9, "completion_tokens": 12, "total_tokens": 21 }
}

ResponsesPOST /v1/responses

The OpenAI Responses API, with its own incremental stream converter. Passing a web_search_preview tool routes the request through chroma's search mode — see web search for what that actually does.

ParameterTypeDescription
modelrequiredstringCatalog id
inputrequiredstring | arrayA prompt, or an array of input items
instructionsstringSystem / developer prompt
toolsarrayPass {"type":"web_search_preview"} to enable search
max_output_tokensintegerOutput cap
streambooleanEmit Responses-format SSE events
Python
from openai import OpenAI

client = OpenAI(
    base_url="https://chromalab.ink/v1",
    api_key="chroma-xxxxxxxxxxx-xxxxxxxxxxx",
)

response = client.responses.create(
    model="openai/gpt-5.4",
    input="Rewrite this regex so it is readable.",
)

print(response.output[0].content[0].text)

GeminiPOST /v1beta/models/{model}:generateContent

The Google generateContent and streamGenerateContent shapes, so the official GenAI SDK works without code changes. This path is deprioritised: it is still buffered rather than incrementally streamed, and tool calls are not converted.

ParameterTypeDescription
contentsrequiredarrayTurns with role (user/model) and parts
systemInstructionobject{"parts":[{"text":"..."}]}
generationConfig.maxOutputTokensintegerOutput cap
generationConfig.temperature, .topPnumberSampling controls
generationConfig.stopSequencesarrayStop strings
cURL
curl https://chromalab.ink/v1beta/models/gemini-2.5-flash:generateContent \
  -H "Content-Type: application/json" \
  -H "x-goog-api-key: chroma-xxxxxxxxxxx-xxxxxxxxxxx" \
  -d '{
    "contents": [{ "role": "user", "parts": [{ "text": "Hello!" }] }],
    "generationConfig": { "maxOutputTokens": 512 }
  }'
Model names on this path are prefixed for you.

The slug in the URL is sent upstream as google/{model}, so use plain Gemini slugs such as gemini-2.5-flash or gemini-2.5-pro. Reaching a non-Google model needs one of the other endpoints.


MARIPOST /mari/

Chroma's own request shape. It exists because the four standard formats each drop something on conversion — MARI carries reasoning (cot), multimodal attachments and tool calls as first-class fields rather than as content-block conventions. It is optional; use it only if you are writing the client yourself and want that shape.

Request body

FieldTypeDescription
modelrequiredstringCatalog id
turnsrequiredarrayConversation turns, see below
toolsobjectMap of tool name to definition
tool_choicestring | objectauto, none, required, or {"name":"..."}
streambooleanEmit MARI SSE events

Turn and response fields

FieldWhereDescription
roleturn, responseuser, assistant or tool; responses are always assistant
contentturn, responseText; null on a response carrying tool calls
attachmentsturn, response{type, attachment} — image or audio, base64
tool_callsassistant turn, response{id, name, arguments}
tool_call_idtool turnWhich call this turn answers
cotresponseReasoning content, when the model emits any
tokens_outresponseCompletion tokens generated
end_reasonresponsestop, max_tokens or tool_calls
cURL
curl https://chromalab.ink/mari/ \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer chroma-xxxxxxxxxxx-xxxxxxxxxxx" \
  -d '{
    "model": "openai/gpt-5.4",
    "turns": [{ "role": "user", "content": "What can you do?" }]
  }'

Content chunks arrive as plain data: events; tool-call chunks are preceded by event: tool_call_delta. The stream ends with data: [DONE].


List modelsGET /v1/models

Returns every id the gateway will accept, in OpenAI catalog shape. It needs no key and sends permissive CORS headers, so a browser can read it directly. Pricing, context length and the per-request unit price are not in this response — the models page shows those, refreshed from the upstream catalog every fifteen minutes.

cURL
curl https://chromalab.ink/v1/models

Tool calling

Tool definitions and tool results convert between all the tool-capable formats: an Anthropic tool_use block becomes an OpenAI tool_calls entry going out, and a tool_result block becomes one role: "tool" message per result coming back, because OpenAI requires that shape. Nothing is dropped in either direction.

Not every model in the catalog handles tools equally well. Each entry carries a tool_call_reliability tag of high or unverified. Treat high as plausible rather than certified — it reflects provider documentation, not a live test against an agent loop. A model that emits XML-shaped tool calls in plain content instead of the structured field is detected and counted, but not repaired; if a harness misbehaves on an unfamiliar model, that is the first thing to check.


Context and memory

Every request is trimmed to a safety ceiling before it goes upstream. The trim groups messages into tool-call and tool-result units first, so it can never split a call from its result at the boundary — a common way for a proxy to break an agent loop.

Past that floor, and only when the conversation still does not fit, a key can opt into more aggressive handling. Four tiers, selected by two per-key toggles:

TierSelected byWhat it adds
litedefaultThe mandatory tool-aware trim, nothing else
standardneither toggleTool-output compaction
deepragModeRetrieval of previously compacted tool output
fulldeepMemoryA task and repo state summary
  • Compaction truncates large, non-recent tool outputs in place, leaving the first 300 characters plus a [compacted tool output, N chars omitted, ref:<hash>] marker. The two most recent tool turns are always left alone.
  • Retrieval reinjects the full cached text when your next turn refers back to a compacted output, either by echoing its ref: marker or by matching a distinctive word in the visible head. This is lexical, not embedding-based, and it says so in the metrics rather than pretending otherwise.
  • Task state extracts file paths and shell commands from the tool calls already in context into a single [Task state] system block. No extra model call, no extra latency.
Compaction is scoped to the request that carries it.

The cache lives in the gateway process and only covers content resent in the current request's own history. There is no cross-request store, because chroma has no session identity — every call is stateless. Content your client has already dropped is gone.


Per-key controls

Each key carries its own settings, edited on the API keys page. They are enforced at admission, before anything is sent upstream, so a blocked request costs nothing.

SettingValuesEffect
Model filternone, whitelist, blacklistRestricts which catalog ids this key may call
ExpirydateRequests after it fail with api_key_expired
Token budgetdisabled or a numberDaily token ceiling for this key, reset at UTC midnight. Suits bursty tool loops better than a request count
Max output tokensintegerCaps max_tokens on /v1/messages regardless of what the client asks for
Tool choice enforcementnone, requiredrequired forces tool_choice: "required" whenever the request carries tools
Web searchtool or offAdds the search and fetch tools
Memory tierragMode, deepMemorySelects the deep or full memory tier
Economy modenone, light, medium, heavyReroutes every 20th / 10th / 5th turn to a cheaper fallback model. Off unless you turn it on
Chats are collected for training unless you opt out.

Collected chats are stored without your account or key and used to train Lucidity models. Turn collection off on your Account page and nothing about later conversations is written down. The memory features work on the transcript inside the request you sent, not on a stored history.


Request units

A plan is sold in request units rather than requests, because the catalog spans a 200x price range and a flat request count would price a cheap model and a flagship the same. One unit buys a baseline model's worth of inference; a model costing N times the baseline costs N units.

The baseline is $0.15 per million tokens blended — input weighted three parts to output one, which is the conventional single-number quote and, on agent traffic where prompt tokens dominate, a conservative one. Prices come from the live catalog, refreshed every fifteen minutes, so the ladder moves with them rather than going stale.

Blended price to request-unit cost.
Blended priceUnits per callNotes
$0.00 / M0Free models draw a separate daily counter instead
$0.15 / M1×The baseline. Cheaper models still cost one unit
$1.25 / M8×At and above this, the memory pipeline treats a model as premium
$6.00 / M40×Needs Plus or above to draw from a plan

A model with no readable price is charged one unit, not zero — an unknown price is still a price. Meta-routing entries whose cost depends on where they route are treated the same way.


Sessions, windows and plans

Allowance is a rolling coding session, not a calendar day. A session opens on your first request and runs five hours; a longer weekly cap stops one account living inside back-to-back sessions. A UTC-midnight bucket counts agent traffic badly in both directions — a loop that starts at 22:00 gets two hours and then a cliff — which is why it is shaped this way.

Plan allowances.
PlanPriceUnits / 5h sessionUnits / weekModel ceilingFree-model calls
No plan—Wallet credits only—Any, if funded200 / day
Basic$52503,00015×500 / day
Plus$106007,50050×Unlimited
Pro$151,20015,000No ceilingUnlimited
Enterprise$505,00060,000No ceilingUnlimited

A model above your plan's ceiling is not blocked outright — the account can still pay for it from wallet credits, it just cannot draw it from the plan. Adding credits also raises the free-model limit from 200 to 500 calls a day. Full pricing lives on the plans page.


Errors

Errors come back in the shape the endpoint speaks: an OpenAI-style error object on the OpenAI paths, an Anthropic {"type":"error"} envelope on /v1/messages. The code field is the stable part — match on it rather than on the message text, which is written for humans and changes.

JSON error
{
  "error": {
    "message": "Model foo/bar was not found. Check the model ID or use /v1/models.",
    "type": "invalid_request_error",
    "code": "model_not_found"
  }
}
StatusCodeWhat to do
401invalid_api_keyThe key is missing, mistyped or deleted. Check the header the client sends
403api_key_expiredPast the key's expiry date. Create a replacement
403model_not_allowed, model_blockedThis key's filter excludes the model
403account_bannedContact support through feedback
404model_not_foundNot a catalog id. Check GET /v1/models
429rate_limit_exceededThe free-model daily limit is spent. Wait, add credits, or take a plan
429token_budget_exceededThis key's own daily token budget is spent. Resets at UTC midnight
402coding_window_exhaustedThe session's units are gone. Wait for the window, or fund the wallet
402plan_multiplier_lockedThe model is above your plan's ceiling. Upgrade, or pay from credits
402insufficient_creditsNo plan and no funded wallet
402spending_limit_reachedThe account's daily spend ceiling is reached
500api_errorUpstream or adapter failure. Retry; if it persists, send us the request

Lab

Install Lab, sign in and start your coding agent from the Lab setup page. You can also open Lab directly from the dashboard sidebar.


Claude Code

Claude Code speaks Anthropic Messages, the path chroma optimises first. Setup is two environment variables and a model id.

  1. Install the CLI

    Shell
    npm install -g @anthropic-ai/claude-code
  2. Point it at chroma

    Shell
    export ANTHROPIC_BASE_URL="https://chromalab.ink"
    export ANTHROPIC_AUTH_TOKEN="chroma-xxxxxxxxxxx-xxxxxxxxxxx"
  3. Choose the models it uses

    Any catalog id works, not only Anthropic ones. A cheap model for the background tasks keeps the unit spend down.

    Shell
    export ANTHROPIC_MODEL="anthropic/claude-sonnet-4.6"
    export ANTHROPIC_SMALL_FAST_MODEL="anthropic/claude-haiku-4.5"
    
    claude
Prompt caching keeps working.

Claude Code sets cache_control markers automatically. Chroma forwards them upstream instead of stripping them, so cache-hit pricing still applies on Anthropic-backed models.


Cursor and VS Code

Cursor's custom OpenAI setting, and most VS Code AI extensions, take a base URL and a key. In Cursor: Settings → Models → OpenAI API Key, enable Override OpenAI Base URL, then add the model ids you want by hand — Cursor does not read the catalog endpoint.

Settings
Base URL: https://chromalab.ink/v1
API key:  chroma-xxxxxxxxxxx-xxxxxxxxxxx
Model:    openai/gpt-5.1-codex

OpenCode

OpenCode treats chroma as a custom OpenAI-compatible provider. Install it, then declare the provider in your config with the models you intend to use.

Shell
curl -fsSL https://opencode.ai/install | bash
~/.config/opencode/opencode.json
{
  "provider": {
    "chroma": {
      "npm": "@ai-sdk/openai-compatible",
      "options": {
        "baseURL": "https://chromalab.ink/v1",
        "apiKey": "chroma-xxxxxxxxxxx-xxxxxxxxxxx"
      },
      "models": {
        "anthropic/claude-sonnet-4.6": { "name": "Sonnet 4.6" },
        "deepseek/deepseek-v3.2": { "name": "DeepSeek v3.2" }
      }
    }
  }
}

aider

aider routes through LiteLLM, so chroma is reached with the openai/ provider prefix on top of the catalog id. The doubled prefix is expected: the first names the protocol, the rest is the model.

Shell
export OPENAI_API_BASE="https://chromalab.ink/v1"
export OPENAI_API_KEY="chroma-xxxxxxxxxxx-xxxxxxxxxxx"

aider --model openai/anthropic/claude-sonnet-4.6

Open WebUI

Add chroma as an OpenAI connection under Settings → Connections → OpenAI API. Open WebUI reads GET /v1/models, so the full catalog appears in the model picker on its own — expect a long list.

Settings
API Base URL: https://chromalab.ink/v1
API Key:      chroma-xxxxxxxxxxx-xxxxxxxxxxx

The same two fields work for Ollama-style front ends and anything else that reads OPENAI_API_BASE and OPENAI_API_KEY from the environment.


SDKs and your own loop

Any official SDK works by changing the base URL. Pick the one matching the protocol you want: the Anthropic SDK for Messages, an OpenAI SDK for Chat Completions or Responses, the Google GenAI SDK for Gemini.

TypeScript
import OpenAI from 'openai';

const client = new OpenAI({
  baseURL: 'https://chromalab.ink/v1',
  apiKey: process.env.CHROMA_API_KEY,
});

const models = await client.models.list();
console.log(models.data.length);
  • Create a key — set its filters, budget and memory tier.
  • Model catalog — pricing, context length and unit cost per model.
  • Feedback — report a bug, with the request if you have it.