{"slug": "3-assumptions-that-broke-before-i-got-gpt-oss-120b-working-through-the-anthropic", "title": "3 Assumptions That Broke Before I Got gpt-oss-120b Working Through the Anthropic SDK", "summary": "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.", "body_md": "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.\n\n**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.**\n\n`401 - Unauthorized`\n\n`auth_token=\"<key>\"`\n\ninstead of `api_key=\"<key>\"`\n\nwhen the endpoint expects Bearer auth instead of an `x-api-key`\n\nheader. Same mechanism as Claude Code's `ANTHROPIC_AUTH_TOKEN`\n\n.`'ThinkingBlock' object has no attribute 'text'`\n\n`resp.content[0]`\n\n. Filter instead: `[b.text for b in resp.content if b.type == \"text\"]`\n\n.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`\n\n(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>`\n\n, shown exactly once.\n\nThe 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.\n\nThe OpenAI-compatible endpoint worked on the first try:\n\n``` python\nfrom openai import OpenAI\n\nclient = OpenAI(\n    api_key=\"<UUID>:<secret>\",\n    base_url=\"https://api.ai.sakura.ad.jp/v1\",\n)\nresp = client.chat.completions.create(\n    model=\"gpt-oss-120b\",\n    messages=[{\"role\": \"user\", \"content\": \"Hello, who are you? Introduce yourself in one line.\"}],\n)\nprint(resp.choices[0].message.content)\n```\n\nOutput: `ChatGPT`\n\n. (`gpt-oss-120b`\n\nis 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.)\n\nThen 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:\n\n``` python\nfrom anthropic import Anthropic\n\nclient = Anthropic(\n    api_key=\"<UUID>:<secret>\",\n    base_url=\"https://api.ai.sakura.ad.jp\",\n)\nresp = client.messages.create(\n    model=\"gpt-oss-120b\",\n    max_tokens=500,\n    messages=[{\"role\": \"user\", \"content\": \"Hello, who are you?\"}],\n)\n```\n\nThat raised:\n\n```\nanthropic.AuthenticationError: Error code: 401 - {'error': {'message': 'Unauthorized'}}\n```\n\nSame token, same model, same provider account — just a different SDK, and a 401. The Anthropic SDK's `api_key=`\n\nsends an `x-api-key`\n\nheader by default. Sakura AI Engine's Anthropic-compatible endpoint expects Bearer auth instead, which the SDK only sends when you use `auth_token=`\n\n. This is the exact mechanism Claude Code itself uses when it's pointed at a gateway through the `ANTHROPIC_AUTH_TOKEN`\n\nenvironment variable — so once I remembered that, the fix was a one-word field swap:\n\n``` python\nfrom anthropic import Anthropic\n\nclient = Anthropic(\n    auth_token=\"<UUID>:<secret>\",  # not api_key=\n    base_url=\"https://api.ai.sakura.ad.jp\",\n)\nresp = client.messages.create(\n    model=\"gpt-oss-120b\",\n    max_tokens=500,\n    messages=[{\"role\": \"user\", \"content\": \"Hello, who are you?\"}],\n)\nprint(resp.content)\n```\n\nThat returned 200, and printed:\n\n```\n[ThinkingBlock(thinking='...', type='thinking'), TextBlock(text='ChatGPT', type='text')]\n```\n\nWhich is where the second bug showed up. My original code, written before I'd seen this shape, read `resp.content[0].text`\n\n. `gpt-oss-120b`\n\nis a reasoning model, and the Anthropic-compatible endpoint puts a `ThinkingBlock`\n\nfirst in the `content`\n\narray, not the answer. `ThinkingBlock`\n\nhas no `.text`\n\nattribute, so that line threw:\n\n```\nAttributeError: 'ThinkingBlock' object has no attribute 'text'\n```\n\nThe fix is to stop assuming position and filter by type instead:\n\n```\ntext_blocks = [b.text for b in resp.content if b.type == \"text\"]\nprint(text_blocks[0] if text_blocks else \"(no text block)\")\n```\n\nThat printed `ChatGPT`\n\n— 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.\n\n| blocker | what broke | the fix |\n|---|---|---|\n| 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 |\n| Anthropic SDK auth |\n`api_key=` → `AuthenticationError: Error code: 401 - {'error': {'message': 'Unauthorized'}}`\n|\n`auth_token=` (Bearer auth — same mechanism as Claude Code's `ANTHROPIC_AUTH_TOKEN` ) |\n| Reasoning model response shape |\n`resp.content[0].text` → `AttributeError: 'ThinkingBlock' object has no attribute 'text'`\n|\n`[b.text for b in resp.content if b.type == \"text\"]` |\n\nThe 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.\n\nCall 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.\n\nMatching 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.\n\nI'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=`\n\nreplaces `api_key=`\n\n. And any client code written against `content[0]`\n\nwill break the moment a reasoning model puts a `ThinkingBlock`\n\nfirst — regardless of which provider is serving it.\n\nNo, 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.\n\nBecause `api_key=`\n\nsends an `x-api-key`\n\nheader by default, and endpoints that expect Bearer auth (`Authorization: Bearer <token>`\n\n) need `auth_token=`\n\ninstead. This is the same mechanism Claude Code uses through the `ANTHROPIC_AUTH_TOKEN`\n\nenvironment variable when talking to a gateway.\n\n`resp.content[0]`\n\nnot always the text response?\nReasoning models such as `gpt-oss-120b`\n\ncan return a `ThinkingBlock`\n\nas the first element of the `content`\n\narray instead of the answer. `ThinkingBlock`\n\nhas no `.text`\n\nattribute, so indexing position 0 and reading `.text`\n\nthrows `AttributeError`\n\n. Filter by `block.type == \"text\"`\n\ninstead of assuming index 0 is the answer.\n\nYes — in this case both endpoints routed to the same underlying `gpt-oss-120b`\n\nmodel and returned the same text (`ChatGPT`\n\n) once the auth header and the response parsing were both fixed.\n\nIf you're integrating a reasoning model behind any Anthropic-Messages-API-compatible endpoint:\n\n`content[0]`\n\nor `.content[0].text`\n\n. 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=`\n\nbefore assuming the key itself is wrong.`content[0]`\n\nassumption with a type filter: `[b.text for b in resp.content if b.type == \"text\"]`\n\n.Minimal repro, end to end: save the fixed `auth_token=`\n\nclient code above as `sakura_hello.py`\n\n, then:\n\n```\npip install anthropic\npython sakura_hello.py\nbash\n$ python sakura_hello.py\n[ThinkingBlock(thinking='...', type='thinking'), TextBlock(text='ChatGPT', type='text')]\nChatGPT\n```\n\nNone 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.\n\n*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.*\n\nThis 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.", "url": "https://wpnews.pro/news/3-assumptions-that-broke-before-i-got-gpt-oss-120b-working-through-the-anthropic", "canonical_source": "https://dev.to/nomurasan/3-assumptions-that-broke-before-i-got-gpt-oss-120b-working-through-the-anthropic-sdk-m55", "published_at": "2026-07-23 22:33:31+00:00", "updated_at": "2026-07-23 23:01:49.965146+00:00", "lang": "en", "topics": ["artificial-intelligence", "large-language-models", "developer-tools"], "entities": ["Sakura AI Engine", "Anthropic", "OpenAI", "gpt-oss-120b", "Anthropic SDK"], "alternates": {"html": "https://wpnews.pro/news/3-assumptions-that-broke-before-i-got-gpt-oss-120b-working-through-the-anthropic", "markdown": "https://wpnews.pro/news/3-assumptions-that-broke-before-i-got-gpt-oss-120b-working-through-the-anthropic.md", "text": "https://wpnews.pro/news/3-assumptions-that-broke-before-i-got-gpt-oss-120b-working-through-the-anthropic.txt", "jsonld": "https://wpnews.pro/news/3-assumptions-that-broke-before-i-got-gpt-oss-120b-working-through-the-anthropic.jsonld"}}