DEV Community

Sho Naka
Sho Naka

Posted on

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

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.

Quick answer

  • Free tier billing: Sakura AI Engine's free "foundation model" plan only rate-limits past 3,000 chat completions/month. It does not bill for overage — only a separate, independent pay-as-you-go plan meters and charges. The credit-card prompt at sign-up is identity verification, not a billing trigger.
  • Anthropic SDK 401 - Unauthorized: use 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': don't index resp.content[0]. Filter instead: [b.text for b in resp.content if b.type == "text"].

What actually happened

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)
Enter fullscreen mode Exit fullscreen mode

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?"}],
)
Enter fullscreen mode Exit fullscreen mode

That raised:

anthropic.AuthenticationError: Error code: 401 - {'error': {'message': 'Unauthorized'}}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

That returned 200, and printed:

[ThinkingBlock(thinking='...', type='thinking'), TextBlock(text='ChatGPT', type='text')]
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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)")
Enter fullscreen mode Exit fullscreen mode

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.

Three blockers, three fixes

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.

The rule I extracted

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.

FAQ

Does Sakura AI Engine charge me if I go over the free 3,000 requests/month?

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.

Why does the Anthropic Python SDK return a 401 against a third-party endpoint even though the key is valid?

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.

Why is 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.

Do the OpenAI-compatible and Anthropic-compatible endpoints return the same model output?

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.

Try it yourself: a 3-step checklist

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

  1. Grep your own client code for 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.
  2. If a third-party endpoint 401s against Anthropic's SDK with a key that works fine elsewhere, try auth_token= before assuming the key itself is wrong.
  3. Replace any 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
Enter fullscreen mode Exit fullscreen mode
$ python sakura_hello.py
[ThinkingBlock(thinking='...', type='thinking'), TextBlock(text='ChatGPT', type='text')]
ChatGPT
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)