> ## Documentation Index
> Fetch the complete documentation index at: https://docs.gloo.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Use Claude through the Anthropic Messages endpoint

> Point the official Anthropic SDKs, Claude Code or the Claude Agent SDK at Gloo. Send a message, stream it, turn on extended thinking and call a tool, in Python and TypeScript.

This tutorial shows how to use Claude models through Gloo with the official Anthropic SDKs. You'll set up the SDK, send a first message, stream a long answer, see Claude's thinking, run a tool call, and point Claude Code or the Claude Agent SDK at Gloo.

Want the concepts first? Read the [Anthropic Messages guide](https://docs.gloo.com/api-guides/anthropic-messages.md).

> [!NOTE]
>
> **Why Anthropic Messages?** If you already have code written for the Anthropic SDK, you change two lines (the base URL and the key) and it keeps working: the same request body, the same responses and [streaming](https://docs.gloo.com/ai-learning-center/gloo-ai-104/streaming-responses.md) events, and the same error types. It's a [direct endpoint](https://docs.gloo.com/api-guides/endpoint-types.md), so you pay the model's [published rate](https://docs.gloo.com/getting-started/pricing.md) with nothing added.

## Prerequisites

Before starting, ensure you have:

* A Gloo AI Studio account
* Your API key from the [API Keys page](https://docs.gloo.com/studio/manage-api-credentials.md), exported as `GLOO_API_KEY`
* **Authentication setup** - Complete the [Authentication Tutorial](https://docs.gloo.com/tutorials/authentication.md) first
* Python 3 or Node.js, and the Anthropic SDK for your language:

```bash Python theme={null}
pip install anthropic
```

```bash TypeScript theme={null}
npm install @anthropic-ai/sdk
```

```bash theme={null}
export GLOO_API_KEY="your_actual_api_key_here"
```

## Understanding the endpoint

All requests go to one endpoint:

**POST** `/ai/v2/direct/anthropic/v1/messages`

You don't type that path yourself. You give the SDK the base URL, and it adds `/v1/messages`.

| Setting | Value |
| :- | :- |
| Base URL | `https://platform.ai.gloo.com/ai/v2/direct/anthropic` |
| Auth | Your Gloo API key as the SDK's **auth token**, sent as `Authorization: Bearer <key>` |
| Models | Claude models only, by Gloo model ID (`gloo-anthropic-claude-opus-5.5`) or Anthropic's ID (`claude-opus-5-5`) |
| `anthropic-version` header | Optional. If you send it, `2023-06-01` (what the SDKs send) or `2026-10-21` |

Like the other direct endpoints, this one applies no [guardrails](https://docs.gloo.com/ai-learning-center/gloo-ai-101/application-patterns.md#guardrails), output moderation or routing: your request goes to the Claude model you name. See the [API reference](https://docs.gloo.com/api-reference/messages/anthropic.md) for every request and response field.

***

## Step 1: Set up the client

Create the client with the Gloo base URL and pass your key as the auth token:

```python Python theme={null}
import os
from anthropic import Anthropic

client = Anthropic(
    base_url="https://platform.ai.gloo.com/ai/v2/direct/anthropic",
    auth_token=os.environ["GLOO_API_KEY"],
)
```

```typescript TypeScript theme={null}
import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic({
  baseURL: "https://platform.ai.gloo.com/ai/v2/direct/anthropic",
  authToken: process.env.GLOO_API_KEY,
});
```

> [!WARNING]
>
> Use `auth_token` (`authToken` in TypeScript), not `api_key`. The auth token is sent as `Authorization: Bearer`, which is where this endpoint reads your key, in every SDK version. SDK versions before Python 2.0 and TypeScript 1.0 send `api_key` as an `x-api-key` header instead, and the request returns `401`.

### Or configure it from the environment

The SDKs also read the base URL and token from environment variables, so you can construct the client with no arguments:

```bash theme={null}
export ANTHROPIC_BASE_URL=https://platform.ai.gloo.com/ai/v2/direct/anthropic
export ANTHROPIC_AUTH_TOKEN="$GLOO_API_KEY"
```

```python Python theme={null}
from anthropic import Anthropic

client = Anthropic()  # reads ANTHROPIC_BASE_URL and ANTHROPIC_AUTH_TOKEN
```

```typescript TypeScript theme={null}
import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic(); // reads ANTHROPIC_BASE_URL and ANTHROPIC_AUTH_TOKEN
```

Keep the key in the environment or a secrets manager, never in source code.

***

## Step 2: Send your first message

```python Python theme={null}
message = client.messages.create(
    model="gloo-anthropic-claude-opus-5.5",
    max_tokens=8192,
    messages=[{"role": "user", "content": "Explain prompt caching in two sentences."}],
)

for block in message.content:
    if block.type == "text":
        print(block.text)

print("Model:", message.model)
print("Usage:", message.usage)
```

```typescript TypeScript theme={null}
const message = await client.messages.create({
  model: "gloo-anthropic-claude-opus-5.5",
  max_tokens: 8192,
  messages: [{ role: "user", content: "Explain prompt caching in two sentences." }],
});

for (const block of message.content) {
  if (block.type === "text") console.log(block.text);
}

console.log("Model:", message.model);
console.log("Usage:", message.usage);
```

```bash cURL theme={null}
curl https://platform.ai.gloo.com/ai/v2/direct/anthropic/v1/messages \
  -H "Authorization: Bearer $GLOO_API_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "gloo-anthropic-claude-opus-5.5",
    "max_tokens": 8192,
    "messages": [
      { "role": "user", "content": "Explain prompt caching in two sentences." }
    ]
  }'
```

### Understanding the response

The response is an Anthropic `message` object:

* **`content`** is a list of blocks. Read the `text` blocks for the answer. On a thinking model you may also see `thinking` blocks before the text; see [Step 4](#step-4-see-claudes-thinking).
* **`model`** is the model ID you sent, whichever form you used.
* **`stop_reason`** says why Claude stopped.
* **`usage`** reports `input_tokens`, `output_tokens`, and the [prompt caching](https://docs.gloo.com/ai-learning-center/gloo-ai-104/cost-and-speed.md#prompt-caching) fields `cache_creation_input_tokens` and `cache_read_input_tokens`, as Anthropic defines them.

`max_tokens` is required. It counts Claude's thinking as well as the answer, so leave room for both (see [Good to know](#good-to-know-about-thinking)).

### Pick a model

Name a Claude model by its Gloo model ID or by Anthropic's own ID. Both of these call Claude Opus 5.5:

* `gloo-anthropic-claude-opus-5.5`
* `claude-opus-5-5`

Other current choices are Claude Sonnet 5.5 (`gloo-anthropic-claude-sonnet-5.5`) and Claude Haiku 5.5 (`gloo-anthropic-claude-haiku-5.5`). See [Supported Models](https://docs.gloo.com/api-guides/supported-models.md) for every Claude model and its rates.

### What You'll See

A two-sentence explanation of prompt caching, the model ID you sent, and a usage summary with input and output token counts.

***

## Step 3: Stream the answer

For long answers, stream. The SDK's streaming helper prints text as it arrives and still gives you the full message at the end:

```python Python theme={null}
with client.messages.stream(
    model="gloo-anthropic-claude-opus-5.5",
    max_tokens=16000,
    messages=[{"role": "user", "content": "Write a short story about a lighthouse."}],
) as stream:
    for text in stream.text_stream:
        print(text, end="", flush=True)
    final = stream.get_final_message()

print()
print("Output tokens:", final.usage.output_tokens)
```

```typescript TypeScript theme={null}
const stream = client.messages.stream({
  model: "gloo-anthropic-claude-opus-5.5",
  max_tokens: 16000,
  messages: [{ role: "user", content: "Write a short story about a lighthouse." }],
});

stream.on("text", (text) => process.stdout.write(text));
const final = await stream.finalMessage();

console.log();
console.log("Output tokens:", final.usage.output_tokens);
```

```bash cURL theme={null}
curl -N https://platform.ai.gloo.com/ai/v2/direct/anthropic/v1/messages \
  -H "Authorization: Bearer $GLOO_API_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "gloo-anthropic-claude-opus-5.5",
    "max_tokens": 16000,
    "stream": true,
    "messages": [
      { "role": "user", "content": "Write a short story about a lighthouse." }
    ]
  }'
```

The events are Anthropic's own (`message_start`, `content_block_start`, `content_block_delta`, `content_block_stop`, `message_delta`, `message_stop` and `ping`), passed through as Claude sends them, so the SDK helpers work unchanged.

### How errors arrive on a stream

* **Before the stream starts**, a failure is a real HTTP error (`429`, `529` and so on). Nothing is sent until Claude's first event arrives, so your SDK can retry it like any other request. You never get an error inside a `200`. When someone is waiting, cap those retries as shown in [Reading errors](#reading-errors).
* **After the stream starts**, a failure ends the stream with an `event: error`, as on Anthropic's API. The SDK raises it while you iterate.

Once text has reached your user, retrying from scratch can repeat or replace what they've already read. [Handling Streaming Failures](https://docs.gloo.com/best-practices/completions-streaming-failures.md) covers when to keep partial output, when to retry and how to tell the user; the same thinking applies here.

### When to stream

A non-streaming request with `max_tokens` above 21,333 returns `400`, the same limit the SDKs apply, and long non-streaming requests can time out with `504`. Stream anything long.

### What You'll See

The story prints word by word, then the output token count.

***

## Step 4: See Claude's thinking

**Thinking** is Claude [reasoning through a problem](https://docs.gloo.com/ai-learning-center/gloo-ai-102/models-that-think.md) before it answers. On this endpoint you control it with Anthropic's own parameters, which Gloo sends to Claude as you write them:

* **`thinking`** turns thinking on or off, and `display: "summarized"` returns a readable summary of it.
* **`output_config.effort`** (`low`, `medium`, `high`, `xhigh`, `max`) sets how much effort Claude puts into the response, thinking included.

> [!NOTE]
>
> Gloo's provider-neutral [reasoning settings](https://docs.gloo.com/api-guides/reasoning.md) (`reasoning_effort` and the `reasoning` object) are for the Responses and Chat Completions endpoints. They aren't read here: a `reasoning_effort` field is listed in the `x-gloo-ignored-params` response header. Use `thinking` and `output_config.effort` on this endpoint.

### Ask for a summary of the thinking

Claude Opus 5.5 thinks by default, but you don't see the thinking unless you ask for it:

```python Python theme={null}
message = client.messages.create(
    model="gloo-anthropic-claude-opus-5.5",
    max_tokens=16000,
    thinking={"type": "adaptive", "display": "summarized"},
    output_config={"effort": "medium"},
    messages=[{"role": "user", "content": "How many prime numbers are there below 100?"}],
)

for block in message.content:
    if block.type == "thinking":
        print("Thinking:", block.thinking)
    elif block.type == "text":
        print("Answer:", block.text)
```

```typescript TypeScript theme={null}
const message = await client.messages.create({
  model: "gloo-anthropic-claude-opus-5.5",
  max_tokens: 16000,
  thinking: { type: "adaptive", display: "summarized" },
  output_config: { effort: "medium" },
  messages: [{ role: "user", content: "How many prime numbers are there below 100?" }],
});

for (const block of message.content) {
  if (block.type === "thinking") console.log("Thinking:", block.thinking);
  else if (block.type === "text") console.log("Answer:", block.text);
}
```

```bash cURL theme={null}
curl https://platform.ai.gloo.com/ai/v2/direct/anthropic/v1/messages \
  -H "Authorization: Bearer $GLOO_API_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "gloo-anthropic-claude-opus-5.5",
    "max_tokens": 16000,
    "thinking": { "type": "adaptive", "display": "summarized" },
    "output_config": { "effort": "medium" },
    "messages": [
      { "role": "user", "content": "How many prime numbers are there below 100?" }
    ]
  }'
```

With `adaptive`, Claude decides how much each request needs, and it can decide a request needs no thinking at all. The response then has no `thinking` block, only the answer, so treat the thinking block as optional when you read the response.

### Stream the thinking

When you stream with `display: "summarized"`, the thinking summary arrives before the answer. Read both from the stream's events:

```python Python theme={null}
with client.messages.stream(
    model="gloo-anthropic-claude-opus-5.5",
    max_tokens=16000,
    thinking={"type": "adaptive", "display": "summarized"},
    output_config={"effort": "high"},
    messages=[{"role": "user", "content": "Plan a four-week small-group study on the Psalms."}],
) as stream:
    for event in stream:
        if event.type == "content_block_delta":
            if event.delta.type == "thinking_delta":
                print(event.delta.thinking, end="", flush=True)
            elif event.delta.type == "text_delta":
                print(event.delta.text, end="", flush=True)
```

```typescript TypeScript theme={null}
const thinkingStream = client.messages.stream({
  model: "gloo-anthropic-claude-opus-5.5",
  max_tokens: 16000,
  thinking: { type: "adaptive", display: "summarized" },
  output_config: { effort: "high" },
  messages: [{ role: "user", content: "Plan a four-week small-group study on the Psalms." }],
});

for await (const event of thinkingStream) {
  if (event.type === "content_block_delta") {
    if (event.delta.type === "thinking_delta") process.stdout.write(event.delta.thinking);
    else if (event.delta.type === "text_delta") process.stdout.write(event.delta.text);
  }
}
```

### Turn thinking off, or on

What thinking does by default depends on the model:

| Model | If you send no `thinking` | To turn thinking off | To think more, or less |
| :- | :- | :- | :- |
| **Claude Opus 5.5** | Thinks adaptively | Not possible: `{"type": "disabled"}` returns `400` | `output_config.effort`; `low` keeps thinking short |
| **Claude Sonnet 5.5** | Thinks adaptively, and can skip thinking on easy requests | `thinking: {"type": "between_tools"}` | `output_config.effort` |
| **Claude Haiku 5.5, Sonnet 5 and Opus 5** | Thinks adaptively | `thinking: {"type": "disabled"}` | `output_config.effort` |
| **Claude Sonnet 4.6 and Opus 4.6 to 4.8** | Doesn't think | Already off | `thinking: {"type": "adaptive"}` |
| **Claude Haiku 4.5 and Opus 4.5** | Doesn't think | Already off | `thinking: {"type": "enabled", "budget_tokens": 2048}` |

For the fastest answer on a simple request, turn thinking off on a model that allows it. On an older model, turn it on with a token budget:

```python Python theme={null}
# Claude Haiku 5.5: no thinking, for the fastest answer
fast = client.messages.create(
    model="gloo-anthropic-claude-haiku-5.5",
    max_tokens=1024,
    thinking={"type": "disabled"},
    messages=[{"role": "user", "content": "Suggest a title for a talk on gratitude."}],
)

# Claude Haiku 4.5: turn thinking on with a token budget
deep = client.messages.create(
    model="gloo-anthropic-claude-haiku-4.5",
    max_tokens=4096,
    thinking={"type": "enabled", "budget_tokens": 2048},
    messages=[{"role": "user", "content": "How many prime numbers are there below 100?"}],
)
```

```typescript TypeScript theme={null}
// Claude Haiku 5.5: no thinking, for the fastest answer
const fast = await client.messages.create({
  model: "gloo-anthropic-claude-haiku-5.5",
  max_tokens: 1024,
  thinking: { type: "disabled" },
  messages: [{ role: "user", content: "Suggest a title for a talk on gratitude." }],
});

// Claude Haiku 4.5: turn thinking on with a token budget
const deep = await client.messages.create({
  model: "gloo-anthropic-claude-haiku-4.5",
  max_tokens: 4096,
  thinking: { type: "enabled", budget_tokens: 2048 },
  messages: [{ role: "user", content: "How many prime numbers are there below 100?" }],
});
```

The [Thinking section of the guide](https://docs.gloo.com/api-guides/anthropic-messages.md#thinking) lists which effort levels each model takes. A level the model doesn't take returns `400`.

### Good to know about thinking

* **Thinking is billed as output tokens**, whether or not you see it. It counts in `usage.output_tokens` and toward `max_tokens`.
* **`max_tokens` above the model's output limit is lowered** to that limit, and the response's `x-gloo-adjusted-params` header names the value used, for example `max_tokens=128000`.
* **Effort is your cost lever.** Lower effort means less thinking and fewer output tokens. [When More Effort Is Worth It](https://docs.gloo.com/ai-learning-center/gloo-ai-102/models-that-think.md#when-more-effort-is-worth-it) explains when a higher level pays off.

***

## Step 5: Check what Gloo adjusted

Gloo tells you in response headers when it dropped or changed part of your request. The request still runs:

| Header | What it lists |
| :- | :- |
| `x-gloo-ignored-params` | Top-level body fields that were dropped, and `messages.thinking` when earlier `thinking` blocks were removed from `messages` |
| `x-gloo-ignored-betas` | `anthropic-beta` values that were dropped |
| `x-gloo-adjusted-params` | Values Gloo changed, such as `max_tokens=128000` |
| `request-id` | The request's ID (`req_...`). Include it when you [contact support](https://docs.gloo.com/getting-started/support.md#contact-support) |

Use the SDK's raw-response helper to read them alongside the parsed message:

```python Python theme={null}
raw = client.messages.with_raw_response.create(
    model="gloo-anthropic-claude-opus-5.5",
    max_tokens=8192,
    messages=[{"role": "user", "content": "Give me one sentence of encouragement."}],
)
message = raw.parse()

print("Ignored:", raw.headers.get("x-gloo-ignored-params"))
print("Adjusted:", raw.headers.get("x-gloo-adjusted-params"))
print("Request ID:", raw.headers.get("request-id"))
```

```typescript TypeScript theme={null}
const { data: reply, response } = await client.messages
  .create({
    model: "gloo-anthropic-claude-opus-5.5",
    max_tokens: 8192,
    messages: [{ role: "user", content: "Give me one sentence of encouragement." }],
  })
  .withResponse();

console.log("Ignored:", response.headers.get("x-gloo-ignored-params"));
console.log("Adjusted:", response.headers.get("x-gloo-adjusted-params"));
console.log("Request ID:", response.headers.get("request-id"));
```

Log these during development. An unexpected entry usually means a field name typo or a setting meant for another endpoint, such as `reasoning_effort`.

***

## Step 6: Call a tool

[Tool use](https://docs.gloo.com/ai-learning-center/gloo-ai-101/application-patterns.md#tool-use-or-function-calling) lets Claude ask your application to run a function and use the result. Define your own tools in `tools` exactly as in Anthropic's API, run the one Claude asks for, and send the result back in a `tool_result` block. `tool_choice` is passed through.

This example gives Claude a `get_service_times` tool and loops until Claude answers:

```python Python theme={null}
import json

tools = [
    {
        "name": "get_service_times",
        "description": "Look up the weekend service times for a church campus.",
        "input_schema": {
            "type": "object",
            "properties": {
                "campus": {"type": "string", "description": "Campus name, for example 'Downtown'"}
            },
            "required": ["campus"],
        },
    }
]

def get_service_times(campus: str) -> dict:
    # Replace with a call to your own system.
    times = {"Downtown": ["9:00 AM", "11:00 AM"], "Northside": ["10:30 AM"]}
    return {"campus": campus, "times": times.get(campus, [])}

def run_tool(call) -> dict:
    """Run one tool call. Input that doesn't fit goes back to Claude as an error result."""
    campus = call.input.get("campus") if isinstance(call.input, dict) else None
    if call.name != "get_service_times" or not isinstance(campus, str):
        return {"type": "tool_result", "tool_use_id": call.id, "is_error": True,
                "content": "Call get_service_times with a string 'campus'."}
    return {"type": "tool_result", "tool_use_id": call.id,
            "content": json.dumps(get_service_times(campus))}

MAX_TURNS = 5  # stop a loop that never settles on an answer
messages = [{"role": "user", "content": "When are services at the Downtown campus this weekend?"}]

for _ in range(MAX_TURNS):
    response = client.messages.create(
        model="gloo-anthropic-claude-opus-5.5",
        max_tokens=4096,
        tools=tools,
        messages=messages,
    )
    # Send Claude's turn back as the SDK returned it.
    messages.append({"role": "assistant", "content": response.content})

    tool_calls = [block for block in response.content if block.type == "tool_use"]
    if not tool_calls:
        break

    # Return every result in one user message.
    messages.append({"role": "user", "content": [run_tool(call) for call in tool_calls]})
else:
    print(f"Stopped after {MAX_TURNS} turns without a final answer.")

for block in response.content:
    if block.type == "text":
        print(block.text)
```

```typescript TypeScript theme={null}
const tools: Anthropic.Tool[] = [
  {
    name: "get_service_times",
    description: "Look up the weekend service times for a church campus.",
    input_schema: {
      type: "object",
      properties: {
        campus: { type: "string", description: "Campus name, for example 'Downtown'" },
      },
      required: ["campus"],
    },
  },
];

function getServiceTimes(campus: string) {
  // Replace with a call to your own system.
  const times: Record<string, string[]> = {
    Downtown: ["9:00 AM", "11:00 AM"],
    Northside: ["10:30 AM"],
  };
  return { campus, times: times[campus] ?? [] };
}

/** Run one tool call. Input that doesn't fit goes back to Claude as an error result. */
function runTool(call: Anthropic.ToolUseBlock): Anthropic.ToolResultBlockParam {
  const campus = (call.input as { campus?: unknown } | null)?.campus;
  if (call.name !== "get_service_times" || typeof campus !== "string") {
    return {
      type: "tool_result",
      tool_use_id: call.id,
      is_error: true,
      content: "Call get_service_times with a string 'campus'.",
    };
  }
  return { type: "tool_result", tool_use_id: call.id, content: JSON.stringify(getServiceTimes(campus)) };
}

const MAX_TURNS = 5; // stop a loop that never settles on an answer
const messages: Anthropic.MessageParam[] = [
  { role: "user", content: "When are services at the Downtown campus this weekend?" },
];

let response: Anthropic.Message | undefined;
for (let turn = 1; turn <= MAX_TURNS; turn++) {
  response = await client.messages.create({
    model: "gloo-anthropic-claude-opus-5.5",
    max_tokens: 4096,
    tools,
    messages,
  });
  // Send Claude's turn back as the SDK returned it.
  messages.push({ role: "assistant", content: response.content });

  const toolCalls = response.content.filter(
    (block): block is Anthropic.ToolUseBlock => block.type === "tool_use",
  );
  if (toolCalls.length === 0) break;
  if (turn === MAX_TURNS) console.log(`Stopped after ${MAX_TURNS} turns without a final answer.`);

  // Return every result in one user message.
  messages.push({ role: "user", content: toolCalls.map(runTool) });
}

for (const block of response?.content ?? []) {
  if (block.type === "text") console.log(block.text);
}
```

### Multi-turn and thinking blocks

You can send Claude's turns back exactly as the SDK returns them, thinking blocks included, as the loop above does. Gloo doesn't pass earlier `thinking` blocks to the model: each new turn starts its own thinking, and the tool loop continues as normal. The `x-gloo-ignored-params` header then lists `messages.thinking`; that's expected, not an error.

### Anthropic-defined tools

Web search runs on Anthropic's servers, so there's no loop to write. Each search is billed in addition to tokens:

```python Python theme={null}
message = client.messages.create(
    model="gloo-anthropic-claude-opus-5.5",
    max_tokens=4096,
    tools=[{"type": "web_search_20250305", "name": "web_search", "max_uses": 3}],
    messages=[{"role": "user", "content": "What changed in the latest Python release?"}],
)

for block in message.content:
    if block.type == "text":
        print(block.text)
```

```typescript TypeScript theme={null}
const searched = await client.messages.create({
  model: "gloo-anthropic-claude-opus-5.5",
  max_tokens: 4096,
  tools: [{ type: "web_search_20250305", name: "web_search", max_uses: 3 }],
  messages: [{ role: "user", content: "What changed in the latest Python release?" }],
});

for (const block of searched.content) {
  if (block.type === "text") console.log(block.text);
}
```

The bash (`bash_*`) and text editor (`text_editor_*`) tools are supported too; they run in your application like any tool you define. Other Anthropic-defined tools, such as web fetch, code execution, computer use and the memory tool, return `400` with `tool_not_supported`.

### What You'll See

Claude calls `get_service_times` with `{"campus": "Downtown"}`, your code returns the times, and Claude answers with the 9:00 AM and 11:00 AM services. Claude writes the tool input, so `run_tool` reads only the `campus` field it expects and sends an `is_error` result back when the input doesn't fit, which lets Claude correct itself. `MAX_TURNS` stops a loop that never settles.

***

## Step 7: Use Claude Code or the Claude Agent SDK

Claude Code and the Claude Agent SDK are [agents](https://docs.gloo.com/ai-learning-center/gloo-ai-101/application-patterns.md#agent) built on the Messages API ([Coding Agents](https://docs.gloo.com/ai-learning-center/gloo-ai-104/coding-agents.md) explains how they work), and they read the same environment variables as the SDKs. Set them before you start a session, and name models by Anthropic's model IDs:

```bash theme={null}
export ANTHROPIC_BASE_URL=https://platform.ai.gloo.com/ai/v2/direct/anthropic
export ANTHROPIC_AUTH_TOKEN="$GLOO_API_KEY"
export ANTHROPIC_MODEL=claude-opus-5-5
export ANTHROPIC_DEFAULT_HAIKU_MODEL=claude-haiku-4-5
```

| Variable | What it sets |
| :- | :- |
| `ANTHROPIC_BASE_URL` | Sends requests to Gloo |
| `ANTHROPIC_AUTH_TOKEN` | Your Gloo API key, sent as `Authorization: Bearer` |
| `ANTHROPIC_MODEL` | The model for your session. `claude-sonnet-5` and `claude-haiku-4-5` work too |
| `ANTHROPIC_DEFAULT_HAIKU_MODEL` | The model Claude Code uses for quick background tasks |

Then start Claude Code, or run your Agent SDK program, from the same shell. Every request goes through your Gloo [organization](https://docs.gloo.com/getting-started/glossary.md#organization) and is billed at the model's published rate.

> [!TIP]
>
> Put the exports in a project-level `.envrc` or your shell profile so every session picks them up, and keep `GLOO_API_KEY` itself out of version control.

***

## Complete Example

This script puts the steps together: a message, a stream, thinking, and a tool call, with error handling that branches on the error code Gloo returns.

```python Python theme={null}
import json
import os

import anthropic
from anthropic import Anthropic

client = Anthropic(
    base_url="https://platform.ai.gloo.com/ai/v2/direct/anthropic",
    auth_token=os.environ["GLOO_API_KEY"],
)
MODEL = "gloo-anthropic-claude-opus-5.5"

TOOLS = [
    {
        "name": "get_service_times",
        "description": "Look up the weekend service times for a church campus.",
        "input_schema": {
            "type": "object",
            "properties": {"campus": {"type": "string"}},
            "required": ["campus"],
        },
    }
]


def get_service_times(campus: str) -> dict:
    times = {"Downtown": ["9:00 AM", "11:00 AM"], "Northside": ["10:30 AM"]}
    return {"campus": campus, "times": times.get(campus, [])}


def run_tool(call) -> dict:
    campus = call.input.get("campus") if isinstance(call.input, dict) else None
    if call.name != "get_service_times" or not isinstance(campus, str):
        return {"type": "tool_result", "tool_use_id": call.id, "is_error": True,
                "content": "Call get_service_times with a string 'campus'."}
    return {"type": "tool_result", "tool_use_id": call.id,
            "content": json.dumps(get_service_times(campus))}


def text_of(message) -> str:
    return "".join(block.text for block in message.content if block.type == "text")


def first_message():
    message = client.messages.create(
        model=MODEL,
        max_tokens=8192,
        messages=[{"role": "user", "content": "Explain prompt caching in two sentences."}],
    )
    print("1. Message:", text_of(message))


def streamed():
    print("2. Stream: ", end="")
    with client.messages.stream(
        model=MODEL,
        max_tokens=16000,
        messages=[{"role": "user", "content": "Write a four-line poem about a lighthouse."}],
    ) as stream:
        for text in stream.text_stream:
            print(text, end="", flush=True)
        final = stream.get_final_message()
    print(f"\n   ({final.usage.output_tokens} output tokens)")


def thinking():
    message = client.messages.create(
        model=MODEL,
        max_tokens=16000,
        thinking={"type": "adaptive", "display": "summarized"},
        output_config={"effort": "medium"},
        messages=[{"role": "user", "content": "How many prime numbers are there below 100?"}],
    )
    for block in message.content:
        if block.type == "thinking":
            print("3. Thinking:", block.thinking[:200], "...")
    print("   Answer:", text_of(message))


def tool_call():
    messages = [{"role": "user", "content": "When are services at the Downtown campus?"}]
    for _ in range(5):  # at most five turns
        response = client.messages.create(
            model=MODEL, max_tokens=4096, tools=TOOLS, messages=messages
        )
        messages.append({"role": "assistant", "content": response.content})
        calls = [block for block in response.content if block.type == "tool_use"]
        if not calls:
            break
        messages.append({"role": "user", "content": [run_tool(call) for call in calls]})
    else:
        print("4. Tool call: stopped after five turns without a final answer.")
        return
    print("4. Tool call:", text_of(response))


if __name__ == "__main__":
    try:
        first_message()
        streamed()
        thinking()
        tool_call()
    except anthropic.APIStatusError as err:
        # Gloo's error body: {"type": "error", "error": {"message", "details": {"code", ...}}}
        body = err.body if isinstance(err.body, dict) else {}
        details = body.get("error", {}).get("details", {})
        print(f"Request failed ({err.status_code}): {err.message}")
        print("Code:", details.get("code"), "| Request ID:", err.response.headers.get("request-id"))
```

```typescript TypeScript theme={null}
import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic({
  baseURL: "https://platform.ai.gloo.com/ai/v2/direct/anthropic",
  authToken: process.env.GLOO_API_KEY,
});
const MODEL = "gloo-anthropic-claude-opus-5.5";

const TOOLS: Anthropic.Tool[] = [
  {
    name: "get_service_times",
    description: "Look up the weekend service times for a church campus.",
    input_schema: {
      type: "object",
      properties: { campus: { type: "string" } },
      required: ["campus"],
    },
  },
];

function getServiceTimes(campus: string) {
  const times: Record<string, string[]> = {
    Downtown: ["9:00 AM", "11:00 AM"],
    Northside: ["10:30 AM"],
  };
  return { campus, times: times[campus] ?? [] };
}

function runTool(call: Anthropic.ToolUseBlock): Anthropic.ToolResultBlockParam {
  const campus = (call.input as { campus?: unknown } | null)?.campus;
  if (call.name !== "get_service_times" || typeof campus !== "string") {
    return { type: "tool_result", tool_use_id: call.id, is_error: true,
             content: "Call get_service_times with a string 'campus'." };
  }
  return { type: "tool_result", tool_use_id: call.id, content: JSON.stringify(getServiceTimes(campus)) };
}

const textOf = (message: Anthropic.Message) =>
  message.content.map((block) => (block.type === "text" ? block.text : "")).join("");

async function firstMessage() {
  const message = await client.messages.create({
    model: MODEL,
    max_tokens: 8192,
    messages: [{ role: "user", content: "Explain prompt caching in two sentences." }],
  });
  console.log("1. Message:", textOf(message));
}

async function streamed() {
  process.stdout.write("2. Stream: ");
  const stream = client.messages.stream({
    model: MODEL,
    max_tokens: 16000,
    messages: [{ role: "user", content: "Write a four-line poem about a lighthouse." }],
  });
  stream.on("text", (text) => process.stdout.write(text));
  const final = await stream.finalMessage();
  console.log(`\n   (${final.usage.output_tokens} output tokens)`);
}

async function thinking() {
  const message = await client.messages.create({
    model: MODEL,
    max_tokens: 16000,
    thinking: { type: "adaptive", display: "summarized" },
    output_config: { effort: "medium" },
    messages: [{ role: "user", content: "How many prime numbers are there below 100?" }],
  });
  for (const block of message.content) {
    if (block.type === "thinking") console.log("3. Thinking:", block.thinking.slice(0, 200), "...");
  }
  console.log("   Answer:", textOf(message));
}

async function toolCall() {
  const messages: Anthropic.MessageParam[] = [
    { role: "user", content: "When are services at the Downtown campus?" },
  ];
  for (let turn = 1; turn <= 5; turn++) { // at most five turns
    const response = await client.messages.create({ model: MODEL, max_tokens: 4096, tools: TOOLS, messages });
    messages.push({ role: "assistant", content: response.content });
    const calls = response.content.filter(
      (block): block is Anthropic.ToolUseBlock => block.type === "tool_use",
    );
    if (calls.length === 0) {
      console.log("4. Tool call:", textOf(response));
      return;
    }
    messages.push({ role: "user", content: calls.map(runTool) });
  }
  console.log("4. Tool call: stopped after five turns without a final answer.");
}

async function main() {
  try {
    await firstMessage();
    await streamed();
    await thinking();
    await toolCall();
  } catch (err) {
    if (err instanceof Anthropic.APIError) {
      // Gloo's error body: {"type": "error", "error": {"message", "details": {"code", ...}}}
      const body = err.error as { error?: { details?: { code?: string } } } | undefined;
      console.error(`Request failed (${err.status}): ${err.message}`);
      console.error("Code:", body?.error?.details?.code, "| Request ID:", err.headers?.get("request-id"));
      process.exitCode = 1;
    } else {
      throw err;
    }
  }
}

main();
```

Run it:

```bash Python theme={null}
python anthropic_messages.py
```

```bash TypeScript theme={null}
npx tsx anthropic_messages.ts
```

### Reading errors

Errors use Anthropic's error shape, so the SDK raises its usual classes (`RateLimitError`, `InternalServerError` and so on) and applies its usual retry rules. Gloo adds a `details` object:

```json theme={null}
{
  "type": "error",
  "error": {
    "type": "overloaded_error",
    "message": "The model is temporarily unavailable. Retry shortly.",
    "details": {
      "code": "upstream_capacity",
      "retryable": true
    }
  },
  "request_id": "req_..."
}
```

* **`error.message`** is safe to show your users.
* **`error.details.code`** is the value to branch on.
* **`details.developer_message`**, when present, tells you what to change.

Requests on this endpoint are served by Anthropic only, with no [failover](https://docs.gloo.com/api-guides/provider-failover.md) to another provider, so retry a `529` rather than treating it as final. The SDKs do this for you: by default they retry twice, honoring `retry-after`.

A `529` can ask for a long wait, and the SDKs wait for as long as `retry-after` says. When someone is waiting for the answer, follow the [retry rule](https://docs.gloo.com/best-practices/error-handling-and-retries.md#the-retry-rule): keep `max_retries` (`maxRetries`) at 2 or fewer and set `timeout` so one stuck attempt can't hold the screen. To fail fast instead of waiting out a long `retry-after`, set `max_retries` to `0` for that call and show `error.message`. Background jobs can keep the SDK defaults.

```python Python theme={null}
# Someone is waiting: at most 2 retries, and at most 60 seconds for any one attempt.
interactive = client.with_options(max_retries=2, timeout=60.0)
message = interactive.messages.create(
    model="gloo-anthropic-claude-opus-5.5",
    max_tokens=4096,
    messages=[{"role": "user", "content": "Suggest a theme for Sunday's welcome."}],
)
```

```typescript TypeScript theme={null}
// Someone is waiting: at most 2 retries, and at most 60 seconds for any one attempt.
const message = await client.messages.create(
  {
    model: "gloo-anthropic-claude-opus-5.5",
    max_tokens: 4096,
    messages: [{ role: "user", content: "Suggest a theme for Sunday's welcome." }],
  },
  { maxRetries: 2, timeout: 60_000 },
);
```

***

## Troubleshooting

**`401` on every request**
: The key isn't arriving as `Authorization: Bearer`. Construct the client with `auth_token` (`authToken`), not `api_key`, or set `ANTHROPIC_AUTH_TOKEN`. See [Step 1](#step-1-set-up-the-client).

**`404` with `model_not_available`**
: The model isn't a Claude model. This endpoint serves Claude only; use [Direct Responses](https://docs.gloo.com/api-guides/direct-responses.md) for other providers' models.

**`404` with `model_retired`**
: The Claude model you named is retired. The error message names the model to use instead. Gloo doesn't switch it for you on this endpoint, because your `thinking` and effort settings were written for the model you named.

**`404` with `endpoint_not_available`**
: Only `POST /v1/messages` is served. Token counting (`/v1/messages/count_tokens`) and the models list (`/v1/models`) aren't; use [`GET /platform/v2/models`](https://docs.gloo.com/api-reference/get-models/v2) for the model catalog. Message Batches and the Files API aren't available either.

**`400` when `max_tokens` is large**
: A non-streaming request with `max_tokens` above 21,333 returns `400`. Stream it ([Step 3](#step-3-stream-the-answer)).

**`400` with `"thinking": {"type": "disabled"}`**
: Claude Opus 5.5 always thinks, so it can't be turned off; lower `output_config.effort` to `low` instead. On Claude Sonnet 5.5, use `{"type": "between_tools"}`.

**`400` with `"type": "enabled"` and `budget_tokens`**
: Claude 5 models and Claude Opus 4.7 and 4.8 don't take a thinking budget. Use `{"type": "adaptive"}` and `output_config.effort`.

**`400` on `output_config.effort`**
: The model doesn't take that level. Claude Haiku 4.5 takes no effort level at all; see [Parameters](https://docs.gloo.com/api-guides/anthropic-messages.md#parameters) for each model's levels.

**`400` when you send `temperature`, `top_p` or `top_k`**
: Claude 5 models and Claude Opus 4.7 and 4.8 don't take them, and no model does with `anthropic-version: 2026-10-21`. Remove them.

**`thinking` blocks come back with empty text**
: Add `"display": "summarized"` to `thinking`. Without it, Claude 5 models and Claude Opus 4.7 and 4.8 return thinking blocks with empty text.

**`400` with `tool_not_supported`**
: You included an Anthropic-defined tool that isn't available here, such as web fetch or code execution. Web search, bash and text editor are supported; define anything else as your own tool.

**`429` with `rate_limit_error`**
: Your organization has too many Messages requests in flight. The response carries `retry-after: 1`, and the SDKs retry it for you. If the code is `spend_limit_reached` (with `x-should-retry: false`), the organization's [spending limit](https://docs.gloo.com/studio/billing.md#spend-limits) is reached: raise the limit rather than retrying.

**`504` with `timeout_error`**
: A non-streaming request ran too long. Stream it instead.

**`reasoning_effort` has no effect**
: That's a setting for the Responses and Chat Completions endpoints. Here it's dropped and listed in `x-gloo-ignored-params`; use `output_config.effort`.

**`413` on a large request**
: The body is over 10 MB. Send less, or use URL sources for large images and documents.

***

## Next Steps

> [card] [Anthropic Messages guide](https://docs.gloo.com/api-guides/anthropic-messages.md)
>
> Supported fields, betas, limits, errors and pricing.

> [card] [API Reference](https://docs.gloo.com/api-reference/messages/anthropic.md)
>
> Every request and response field for Anthropic Messages.

> [card] [Handling Streaming Failures](https://docs.gloo.com/best-practices/completions-streaming-failures.md)
>
> Keep partial output, retry safely and tell users what happened.

> [card] [Prompt Caching](https://docs.gloo.com/api-guides/prompt-caching.md)
>
> Use `cache_control` to cut the cost of repeated context.

> [card] [Models That Think](https://docs.gloo.com/ai-learning-center/gloo-ai-102/models-that-think.md)
>
> What thinking is, and when more effort pays off.

> [card] [Direct Responses](https://docs.gloo.com/api-guides/direct-responses.md)
>
> One request shape for every model in the catalog, Claude included.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.