# Claude Code & Codex

> Route Claude Code, Codex and the agent SDKs through INFRO by changing the base URL and the key: Anthropic's Messages API and OpenAI's Responses API.

Source: https://infro.io/docs/coding-agents

> **Note:** INFRO is in private early access. Running these examples requires an early-access account, an active trial or subscription entitlement, and your own provider connection. Self-serve purchasing is not generally open. See [Early access](https://infro.io/early-access).

> **Warning:** **On the roadmap — not available yet.** `/v1/messages` and `/v1/responses` are built and in testing, and not deployed: `https://api.infro.io` does not answer them today. This page describes the contract they will ship with.

Coding agents speak their provider's own API, not Chat Completions. INFRO answers both: `POST /v1/messages` in Anthropic's Messages shape and `POST /v1/responses` in OpenAI's Responses shape. Point the agent at INFRO with an INFRO gateway key, and every request it makes goes through the same path as any other: your routes and failover, model allowlists, budgets and spend limits checked **before** the provider is called, guardrails, the request log with an estimated provider cost, and attribution. Your provider still bills you for the tokens; INFRO adds nothing to them.

> **Note:** Give each developer, or each agent, its own gateway key with a [spend limit](https://infro.io/docs/api/limits). A request that limit cannot cover is refused with `402` before the provider sees it, and the request log shows spend per key.

## Claude Code

Set the base URL and an INFRO key. `ANTHROPIC_AUTH_TOKEN` is sent as `Authorization: Bearer`; `ANTHROPIC_API_KEY` as `x-api-key`. INFRO accepts either.

```bash
export ANTHROPIC_BASE_URL="https://api.infro.io"
export ANTHROPIC_AUTH_TOKEN="sk_infro_..."
# Optional: attribute every request (newline-separated "Name: Value" pairs)
export ANTHROPIC_CUSTOM_HEADERS="X-INFRO-Workflow: claude-code"
```

- **Model names.** Claude Code sends Anthropic's own ids (`claude-sonnet-5-5`, `claude-haiku-4-5-20251001`); INFRO reads them as the catalog's `anthropic/claude-sonnet-5.5`. A [route](https://infro.io/docs/routes) slug works too (`ANTHROPIC_MODEL=your-route`), but Claude Code does not recognize the name and assumes a 200K context window.
- **Lossless on Anthropic.** On an Anthropic connection, or Claude on Vertex AI, the body Claude Code sends reaches the provider as written (thinking blocks and their signatures, `cache_control` breakpoints, server tools, documents) and the answer comes back as the provider sent it. The `anthropic-beta` header is forwarded to Anthropic; `anthropic-version` is accepted and pinned.
- **Streaming** is relayed event by event. A provider that opens the stream and reports an error before any content (an `overloaded_error`) is failed over to the next target, like one that refused outright.
- **Token counting.** `POST /v1/messages/count_tokens` asks the first Anthropic connection that serves the model; it is free and leaves no request row. With no such connection the answer is an estimate, and `X-INFRO-Token-Count: estimated` says so.
- **Model list.** With `CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1`, Claude Code reads `GET /v1/models` and adds the Claude models your key can call to its `/model` picker.
- **Sessions.** Claude Code's `x-claude-code-session-id` header becomes the request's `session_id` when you set none, so one session groups in the request log.

The **Claude Agent SDK** runs Claude Code, so it reads the same variables.

## Codex

Add INFRO as a model provider in `~/.codex/config.toml`. Codex sends the key from `env_key` as a bearer token, to `<base_url>/responses`.

```toml
model = "gpt-5.6-sol"
model_provider = "infro"

[model_providers.infro]
name = "INFRO"
base_url = "https://api.infro.io/v1"
env_key = "INFRO_API_KEY"
wire_api = "responses"
http_headers = { "X-INFRO-Workflow" = "codex" }
```

Codex sends the whole conversation each turn (`store: false`, its reasoning carried back as encrypted content), which is what INFRO needs: see [state](#state-stays-with-the-caller) below. On an OpenAI or Azure OpenAI connection the body goes to the provider as written, Codex's custom `apply_patch` tool and reasoning items included.

## OpenAI Agents SDK

```python
from openai import AsyncOpenAI
from agents import set_default_openai_client

set_default_openai_client(
    AsyncOpenAI(base_url="https://api.infro.io/v1", api_key="sk_infro_..."),
    use_for_tracing=False,  # the INFRO key is not an OpenAI key; trace with your own, or set_tracing_disabled(True)
)
```

## Which providers serve it, and how

| Endpoint | Sent as written (lossless) | Translated | Not served |
| --- | --- | --- | --- |
| `/v1/messages` | Anthropic; Claude on Vertex AI; KIE's Claude models | Every other provider with chat: text, images and your own tools only | Providers without chat |
| `/v1/responses` | OpenAI; Azure OpenAI; KIE's Codex models | Every other provider with chat: messages, images, function tools and JSON-schema output only | Providers without chat |

A translation drops what has no meaning for the other model (thinking settings and past thinking blocks, `top_k`, reasoning items, `cache_control`), names it in `X-INFRO-Params-Dropped`, and answers in the dialect you called. What it cannot carry without changing the request (a server tool such as `web_search`, a document block, a custom or hosted tool, MCP servers) makes that target ineligible for the request: it is left out of the chain, never sent a reduced request. When no target is left, the request is refused with `400 unsupported_on_targets` naming what stood in the way. `X-INFRO-Ingress` says which way a request was served.

> **Warning:** Anthropic does not support Claude Code on non-Claude models. INFRO can translate a Claude Code request to another model, but for failover give Claude Code's routes Claude targets on more than one provider (Anthropic and Claude on Vertex AI): those are served as written.

## What is the same, and what differs

- **Budgets and caps** reserve the request's worst case before any provider is called, exactly as on chat completions. A refusal is `402` in the dialect's envelope, and Claude Code shows its message.
- **Guardrails** read every text block, tool input and tool result. Thinking blocks are not inspected: their signature covers the text, so a redaction would make Anthropic refuse the next turn. An output rule needs the whole answer: a streamed request it applies to is read whole from the provider, checked, then streamed to you, with SSE comments every few seconds meanwhile so the client does not time out.
- **The answer keeps the provider's shape.** INFRO's facts ride on response headers: `X-INFRO-Request-Id`, `X-INFRO-Model`, `X-INFRO-Provider`, `X-INFRO-Route`, `X-INFRO-Ingress`, and on an unstreamed answer `X-INFRO-Cost-Estimate` (USD, or `unpriced`). The [request log](https://infro.io/docs/observability/traces) has the rest.
- **The response cache** does not serve these endpoints yet. Where it is on, the request log says `uncacheable` with the reason `endpoint`.
- **Attribution.** Send INFRO's `X-INFRO-Trace-Id`, `X-INFRO-Workflow`, `X-INFRO-Step`, `X-INFRO-Feature` and `X-INFRO-User-Id` headers. On `/v1/messages`, `metadata.user_id` is Anthropic's field and is passed to Anthropic; INFRO's end customer is `X-INFRO-User-Id`. On `/v1/responses`, INFRO's reserved `metadata` keys are read and not forwarded.

## State stays with the caller

INFRO keeps no responses. `previous_response_id` and `conversation` are refused with `400 unsupported_parameter` on every target, because a failover to another account or another model could not resolve them; `background: true` is refused for the same reason. Send the whole conversation in `input` each turn. `store` is passed to a provider that takes the request as written: it is your own account.

## Errors

Errors keep the [documented taxonomy](https://infro.io/docs/api/errors) — `type`, `code`, `message` — inside each API's envelope: on `/v1/messages`, Anthropic's `{"type": "error", "error": {…}}`; on `/v1/responses`, the same `{"error": {…}}` as everywhere on `/v1`. A provider's refusal of the request carries the provider's own words in `message`, which is what Claude Code's automatic recovery reads. After a stream has started, a failure is the API's own stream event: `error` on `/v1/messages`, `response.failed` on `/v1/responses`.
