cd /news/artificial-intelligence/3-assumptions-that-broke-before-i-go… · home topics artificial-intelligence article
[ARTICLE · art-71096] src=dev.to ↗ pub= topic=artificial-intelligence verified=true sentiment=· neutral

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

A developer using Sakura AI Engine's free tier to access OpenAI's open-weight gpt-oss-120b model through the Anthropic SDK encountered two bugs: the SDK requires `auth_token` instead of `api_key` for Bearer auth, and response content must be filtered by type to avoid `'ThinkingBlock' object has no attribute 'text'`. Both have one-line fixes.

read7 min views1 publishedJul 23, 2026

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:

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:

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:

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].textAttributeError: '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.

── more in #artificial-intelligence 4 stories · sorted by recency
── more on @sakura ai engine 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/3-assumptions-that-b…] indexed:0 read:7min 2026-07-23 ·