# 3 Assumptions That Broke Before I Got gpt-oss-120b Working Through the Anthropic SDK

> Source: <https://dev.to/nomurasan/3-assumptions-that-broke-before-i-got-gpt-oss-120b-working-through-the-anthropic-sdk-m55>
> Published: 2026-07-23 22:33:31+00:00

I signed up for a free-tier LLM API, hit a credit-card prompt before I'd made a single call, and assumed it meant I was about to get billed. I was wrong about that one. I was not wrong that the SDK code I wrote next would break twice before I got a real response back.

**TL;DR: Sakura AI Engine's free tier (3,000 chat completions/month, hosting OpenAI's open-weight gpt-oss-120b) broke 3 assumptions in a row: that a credit-card prompt on a free plan means billing, that Anthropic's Python SDK authenticates the same way against every compatible endpoint, and that content[0] in a response is always the text you asked for. Only the last two are real bugs. Both have a one-line fix once you know they exist.**

`401 - Unauthorized`

`auth_token="<key>"`

instead of `api_key="<key>"`

when the endpoint expects Bearer auth instead of an `x-api-key`

header. Same mechanism as Claude Code's `ANTHROPIC_AUTH_TOKEN`

.`'ThinkingBlock' object has no attribute 'text'`

`resp.content[0]`

. Filter instead: `[b.text for b in resp.content if b.type == "text"]`

.Sakura Internet, a Japanese cloud provider, opened a free tier for an API called "AI Engine": 3,000 chat completions and 50 audio-related requests per month, hosting `gpt-oss-120b`

(OpenAI's open-weight model) behind both an OpenAI-compatible endpoint and an Anthropic-Messages-API-compatible endpoint. Sign-up asked for 5 things in order: a member ID, a project, a phone number verified by SMS, a credit card, and finally an account token in the form `<UUID>:<secret>`

, shown exactly once.

The credit card step made me stop. A "free" plan asking for a card reads like a billing trap. It isn't, in this case — the free "foundation model" plan only rate-limits you past 3,000 requests/month, and a separate pay-as-you-go plan is the only one that meters and bills overage. The two plans don't auto-convert into each other; the card is identity verification, not a billing trigger. That part turned out to be a non-issue. What actually blocked me was the code.

The OpenAI-compatible endpoint worked on the first try:

``` python
from openai import OpenAI

client = OpenAI(
    api_key="<UUID>:<secret>",
    base_url="https://api.ai.sakura.ad.jp/v1",
)
resp = client.chat.completions.create(
    model="gpt-oss-120b",
    messages=[{"role": "user", "content": "Hello, who are you? Introduce yourself in one line."}],
)
print(resp.choices[0].message.content)
```

Output: `ChatGPT`

. (`gpt-oss-120b`

is an open-weight model OpenAI released; its training data appears to include ChatGPT-authored text, which is the likely reason it introduces itself that way.)

Then I pointed the same account token at the Anthropic-compatible endpoint, using Anthropic's own Python SDK, and swapped in the field name I already knew:

``` python
from anthropic import Anthropic

client = Anthropic(
    api_key="<UUID>:<secret>",
    base_url="https://api.ai.sakura.ad.jp",
)
resp = client.messages.create(
    model="gpt-oss-120b",
    max_tokens=500,
    messages=[{"role": "user", "content": "Hello, who are you?"}],
)
```

That raised:

```
anthropic.AuthenticationError: Error code: 401 - {'error': {'message': 'Unauthorized'}}
```

Same token, same model, same provider account — just a different SDK, and a 401. The Anthropic SDK's `api_key=`

sends an `x-api-key`

header by default. Sakura AI Engine's Anthropic-compatible endpoint expects Bearer auth instead, which the SDK only sends when you use `auth_token=`

. This is the exact mechanism Claude Code itself uses when it's pointed at a gateway through the `ANTHROPIC_AUTH_TOKEN`

environment variable — so once I remembered that, the fix was a one-word field swap:

``` python
from anthropic import Anthropic

client = Anthropic(
    auth_token="<UUID>:<secret>",  # not api_key=
    base_url="https://api.ai.sakura.ad.jp",
)
resp = client.messages.create(
    model="gpt-oss-120b",
    max_tokens=500,
    messages=[{"role": "user", "content": "Hello, who are you?"}],
)
print(resp.content)
```

That returned 200, and printed:

```
[ThinkingBlock(thinking='...', type='thinking'), TextBlock(text='ChatGPT', type='text')]
```

Which is where the second bug showed up. My original code, written before I'd seen this shape, read `resp.content[0].text`

. `gpt-oss-120b`

is a reasoning model, and the Anthropic-compatible endpoint puts a `ThinkingBlock`

first in the `content`

array, not the answer. `ThinkingBlock`

has no `.text`

attribute, so that line threw:

```
AttributeError: 'ThinkingBlock' object has no attribute 'text'
```

The fix is to stop assuming position and filter by type instead:

```
text_blocks = [b.text for b in resp.content if b.type == "text"]
print(text_blocks[0] if text_blocks else "(no text block)")
```

That printed `ChatGPT`

— the same answer the OpenAI-compatible endpoint gave, confirming both endpoints were routing to the same model the whole time. The auth header and the response shape were the only things standing in the way.

| blocker | what broke | the fix |
|---|---|---|
| Credit card on a free plan | Looked like a billing trigger before any call was made | Non-issue: free "foundation model" plan only rate-limits past 3,000 req/month; the metered plan is separate and doesn't auto-activate |
| Anthropic SDK auth |
`api_key=` → `AuthenticationError: Error code: 401 - {'error': {'message': 'Unauthorized'}}`
|
`auth_token=` (Bearer auth — same mechanism as Claude Code's `ANTHROPIC_AUTH_TOKEN` ) |
| Reasoning model response shape |
`resp.content[0].text` → `AttributeError: 'ThinkingBlock' object has no attribute 'text'`
|
`[b.text for b in resp.content if b.type == "text"]` |

The first row cost time worrying, not debugging. The other two cost actual stack traces, and both trace back to the same root cause once you line them up.

Call it **the compatibility mirage**: an endpoint that accepts the same request shape and the same model name as the SDK you already know isn't necessarily compatible with everything that SDK assumes. The parts that silently differ are the parts you can't see in a quick glance at the docs — the auth handshake and the response schema.

Matching the URL and the payload shape isn't enough. You also have to match how the client authenticates and how it parses what comes back — and a "compatible" endpoint can diverge on both without ever changing the request you send.

I'd guess this isn't unique to Sakura AI Engine. Any provider exposing an Anthropic-Messages-API-shaped endpoint with Bearer auth will hit the same 401 until `auth_token=`

replaces `api_key=`

. And any client code written against `content[0]`

will break the moment a reasoning model puts a `ThinkingBlock`

first — regardless of which provider is serving it.

No, not on the free "foundation model" plan — you get rate-limited, not billed. Only a separate pay-as-you-go plan meters and charges for overage, and the two plans don't auto-convert into each other.

Because `api_key=`

sends an `x-api-key`

header by default, and endpoints that expect Bearer auth (`Authorization: Bearer <token>`

) need `auth_token=`

instead. This is the same mechanism Claude Code uses through the `ANTHROPIC_AUTH_TOKEN`

environment variable when talking to a gateway.

`resp.content[0]`

not always the text response?
Reasoning models such as `gpt-oss-120b`

can return a `ThinkingBlock`

as the first element of the `content`

array instead of the answer. `ThinkingBlock`

has no `.text`

attribute, so indexing position 0 and reading `.text`

throws `AttributeError`

. Filter by `block.type == "text"`

instead of assuming index 0 is the answer.

Yes — in this case both endpoints routed to the same underlying `gpt-oss-120b`

model and returned the same text (`ChatGPT`

) once the auth header and the response parsing were both fixed.

If you're integrating a reasoning model behind any Anthropic-Messages-API-compatible endpoint:

`content[0]`

or `.content[0].text`

. If you find it, you have the same latent bug this article does — it just hasn't fired yet because your model hasn't returned a thinking block first.`auth_token=`

before assuming the key itself is wrong.`content[0]`

assumption with a type filter: `[b.text for b in resp.content if b.type == "text"]`

.Minimal repro, end to end: save the fixed `auth_token=`

client code above as `sakura_hello.py`

, then:

```
pip install anthropic
python sakura_hello.py
bash
$ python sakura_hello.py
[ThinkingBlock(thinking='...', type='thinking'), TextBlock(text='ChatGPT', type='text')]
ChatGPT
```

None of these three took more than a few minutes to fix once I found them. All three would have been avoidable with five minutes of reading the response schema first — which is exactly the five minutes I skipped.

*Sho Naka (nomurasan). I test with a real API key before I trust what the docs imply. Verified end-to-end against Sakura AI Engine's live API and a real account token.*

This is an adapted English rewrite of an essay I first wrote in Japanese, on Qiita. I worked with AI to shape and restructure the piece for a dev.to audience; the code, the errors, and the conclusions are my own, verified against a live API key.
