OBLETHdocs

Claude Code and Anthropic SDKs

Run Claude Code and Anthropic SDK clients on your own models through the gateway's Anthropic Messages API.

5 min readUpdated for v2.4.0Edit on GitHub

You needA gateway key and a chat model with function calling
Set on the clientANTHROPIC_BASE_URL and ANTHROPIC_API_KEY
RoutesPOST /v1/messages, POST /v1/messages/count_tokens
Billed asRequest type messages

The gateway serves the Messages API itself: each request is translated to a chat request, runs through the same admission, budgets, boons and billing as /v1/chat/completions, and the reply is translated back. Your registered models answer; no Anthropic service is involved.

Set up Claude Code

  1. 1

    Point it at the gateway

    Set ANTHROPIC_BASE_URL to the data-plane origin without a path (the client adds /v1/messages), and a gateway key as the credential:

    export ANTHROPIC_BASE_URL=http://localhost:8088
    export ANTHROPIC_API_KEY=sk_...
    claude
    

    ANTHROPIC_API_KEY is sent as x-api-key and ANTHROPIC_AUTH_TOKEN as Authorization: Bearer; the gateway accepts both, and an identity token works in either. To keep the settings, put them in the env block of ~/.claude/settings.json, not in a project's .claude/settings.json, which is usually committed.

  2. 2

    Pick the model that answers

    Claude Code sends Claude model ids that your gateway does not serve. Map them with one of the options in the next section.

  3. 3

    Check it

    curl -s "$ANTHROPIC_BASE_URL/v1/messages" \
      -H "x-api-key: $ANTHROPIC_API_KEY" \
      -H "anthropic-version: 2023-06-01" \
      -H "content-type: application/json" \
      -d '{"model": "qwen3-coder", "max_tokens": 64,
           "messages": [{"role": "user", "content": "ping"}]}'
    

    A message object back means the key, the model and its backend all work. A not_found_error means the model name did not resolve.

The Anthropic SDKs take the same two values:

from anthropic import Anthropic

client = Anthropic(base_url="http://localhost:8088", api_key="sk_...")
message = client.messages.create(
    model="qwen3-coder",
    max_tokens=1024,
    messages=[{"role": "user", "content": "Hello"}],
)
print(message.content[0].text)

Map Claude model ids to your models

Use any of these; they combine.

  • Pin the model on the client. Set ANTHROPIC_MODEL (or claude --model) for the main session, and ANTHROPIC_DEFAULT_SONNET_MODEL, ANTHROPIC_DEFAULT_OPUS_MODEL, ANTHROPIC_DEFAULT_HAIKU_MODEL and CLAUDE_CODE_SUBAGENT_MODEL for the rest. The value is any gateway model name, alias or auto.
  • Add Claude ids as aliases. Add the ids the client sends, such as claude-sonnet-4-6, to the aliases of the model that should answer. Aliases match exactly, so a client release with new ids needs new aliases.
  • Set a default for Anthropic clients. In Settings › Routing, choose a model under Default for Anthropic clients, and save. Any name that does not resolve on /v1/messages is then served by that model; it can be auto. Over the API it is messages_default_model on PUT /api/v1/settings/auto-router, and an empty string clears it.

For each request the gateway uses auto as sent, then a name or alias it knows, then the default. With none of these it returns 404 not_found_error with model: <name>. Routing, pools, budgets and usage follow the model that served the request, while the response's model field echoes the name the client sent. The default does not stand in for a model that exists but is disabled; that returns 403.

Claude Code on your gateway is only as good as the model you route it to. Use a model with function calling on (supports_function_calling), a large context window, and an accurate context_window setting, which the gateway uses to fit max_tokens.

What is translated

AnthropicChat completions
system, and system-role entries inside messagesOne leading system message, in order
Text and image blocks (base64 or URL)Text and image_url parts
tool_use and tool_result blockstool_calls and tool messages
tools, tool_choiceFunction tools; auto, required, a named function or none
disable_parallel_tool_use: trueparallel_tool_calls: false
stop_sequences, metadata.user_idstop, user
stream: trueA chat stream, re-emitted as Anthropic events
Other top-level fields (temperature, top_p, …)Passed through

Replies come back as a message object with thinking (from the model's reasoning output), text and tool_use blocks, and stop_reason of end_turn, max_tokens or tool_use. Streams send ping every 15 seconds while the backend is quiet.

Usage follows Anthropic's fields. input_tokens is the input the backend did not serve from its prefix cache, cache_read_input_tokens the part it did, and cache_creation_input_tokens is always 0, since a prefix cache has no write charge. A stream's message_start is sent before usage is known, so its counts are 0; the final message_delta carries them.

Token counting

POST /v1/messages/count_tokens takes the same body (max_tokens optional) and returns {"input_tokens": 1834}, the gateway tokenizer's estimate of the message text. It takes no slot, reserves no budget and writes no usage row.

Not supported

  • Prompt cache control. cache_control markers are dropped. Caching is up to the backend's own prefix cache, and usage reports what it served.
  • Extended thinking budgets. thinking and output_config.effort are read as a reasoning effort level (disabled is none; a budget is bucketed into low, medium or high) and reach the model only through its effort map or a model family. The budget itself is not passed on. Reasoning a model produces still comes back as thinking blocks.
  • Images inside tool results. Only the text of a tool_result reaches the model.
  • Server-side tools. Web search, code execution, computer use, container, mcp_servers and the batch API. The model's own boons, including the gateway tool loop, still run.
  • Which stop sequence fired. stop_sequence is always null.
  • A per-tenant default. The default for Anthropic clients is one gateway-wide setting.
Fitting max_tokens and "prompt is too long"

Anthropic clients size max_tokens for a Claude model, which can be more than a smaller model holds. Before sending, the gateway estimates the prompt (message text, plus about 3 characters per token for the tools array, plus 1500 tokens per image) and lowers max_tokens to what fits in the model's context_window, less a margin of 256 tokens or one eighth of the estimate, whichever is larger. For auto it uses the largest window among healthy chat models. With a context_window of 0 it sends max_tokens unchanged.

When the prompt alone does not fit, the request fails with 400 prompt is too long: <n> tokens > <window> maximum. That is Anthropic's wording, which Claude Code uses to start compacting the conversation. A backend's own context-length 400 is reworded the same way.

Request rules and errors

model, a positive max_tokens and a non-empty messages array are required. Roles other than user, assistant and system, and block types other than text, image, tool_use, tool_result, thinking and redacted_thinking, return 400. thinking blocks in the history are dropped. The anthropic-beta header and beta query parameter are removed before the request goes on. The body limit is 32 MiB.

Errors use the Anthropic envelope, with the pipeline's status and message, and keep Retry-After and x-obleth-request-id:

Statuserror.type
400invalid_request_error
401authentication_error
403permission_error
404not_found_error
413request_too_large
429rate_limit_error
502, 503, 504overloaded_error (an admission timeout is a 503 with Retry-After: 5)
Otherapi_error