{"slug": "gemini-api-tutorial-carbon-in-12-steps-70-min-2026", "title": "Gemini API Tutorial: Carbon in 12 Steps, 70 Min [2026]", "summary": "Google made Gemini 4 Carbon generally available to paying developers and enterprises on November 10, 2026, at an introductory price of $2 per million input tokens and $10 per million output tokens through November 30, 2026, with rates doubling to $4 and $20 per million tokens from December 1. The model, identified as `gemini-4-carbon`, runs in the Gemini API, Google AI Studio, Android Studio, Gemini for business and Antigravity, with cached input priced at 95% off the input rate, and Google also introduced the faster, cheaper Gemini 4 Hydrogen on November 12 for agentic loops. A tutorial built around the launch warns that anyone who ships a prototype this month and forgets the December 1 date will see costs double overnight, and notes Carbon's rollout still varies by account and region.", "body_md": "The Gemini API now serves Gemini 4 Carbon to paying developers, and Google’s [launch post](https://blog.google/innovation-and-ai/models-and-research/gemini-models/gemini-4-carbon-now-available/) priced it at $2 per million input tokens and $10 per million output tokens through November 30, 2026. From December 1 those rates double to $4 and $20. Anyone who ships a prototype this month and forgets that date will watch the bill jump overnight.\n\nThat deadline shapes the whole build. Create a small Python CLI called `carbon-lab` that covers the full path: a safely stored API key, a first call, token accounting, streaming, tool calls, typed JSON output, retries, a spend ledger and a test suite. Each step ends with a command to run and output to compare. Carbon’s rollout still varies by account and region, as the [availability picture for the $2 intro price](https://shattered.io/gemini-4-carbon-availability-2-dollar-intro-price-not-worldwide-2026/) shows, so the project checks which model IDs your key can see before it sends a single prompt.\n\nDemand for this skill is easy to measure. Stack Overflow’s [2025 developer survey](https://stackoverflow.co/company/press/archive/stack-overflow-2025-developer-survey/) found that 35% of developers reported using Gemini Flash models. Among developers who use ready-made agents, copilots or assistants, 47.4% reported using Google Gemini. Most of them start the same way you will: grab a key, paste a snippet, and discover the details (thinking tokens, retries, price changes) only after the first invoice.\n\nDon't miss new tech stories on Google\n\nAdd Shattered once in the Google app and our stories show up in your news suggestions.\n\n[Add now](https://www.google.com/preferences/source?hl=en&q=https%3A%2F%2Fshattered.io%2F)\n\n## Where the Gemini API Stands on November 15, 2026\n\nGoogle moved Gemini 4 Carbon from a limited cyber-defender preview to general availability for paying developers and enterprises on November 10, 2026. The model reaches developers through the Gemini API, Google AI Studio, Android Studio and Gemini for business. Consumers on Google AI Ultra get it in the Gemini app, AI Mode in Search and Google Antigravity, and Android Authority’s [rollout report](https://www.androidauthority.com/google-gemini-4-carbon-rollout-3733702/) describes the Ultra-first pattern. The API identifier is `gemini-4-carbon`.\n\nGoogle also introduced [Gemini 4 Hydrogen](https://shattered.io/gemini-4-hydrogen-agentic-workflows-1m-context-1-50-input-2026/) on November 12, and reports describe it as the faster, cheaper sibling aimed at agentic loops, while Carbon and Gemini 4 Argon target deeper reasoning. Treat any Hydrogen price you see on forums as unconfirmed until you read Google’s pricing page yourself. The table below sticks to what Google has published.\n\n| Item | Current state (November 15, 2026) | \n|---|---|\n| Model ID | `gemini-4-carbon` | \n| General availability | November 10, 2026, for paying developers and enterprises | \n| Input price, through November 30 | $2 per 1M tokens | \n| Output price, through November 30 | $10 per 1M tokens | \n| Input price, from December 1 | $4 per 1M tokens | \n| Output price, from December 1 | $20 per 1M tokens | \n| Cached input | 95% off the input price | \n| Intro price matches | Gemini 4 Argon ($2 and $10 per 1M) | \n| Where it runs | Gemini API, Google AI Studio, Android Studio, Gemini for business, Antigravity | \n\nTwo details in that table matter for a tutorial. First, the price applies to the public Carbon model, and Google’s footnote names no carve-out for existing projects, so budget every token sent on or after December 1 at the higher rate. Second, a subscription is not an API plan. Google AI Ultra unlocks Carbon in consumer surfaces, while Gemini API usage bills separately by token. Reports from the week of November 13 say Google AI Pro subscribers can claim $10 a month in Google Cloud credits for Gemini API use in AI Studio, but confirm that on your own billing page before you rely on it.\n\nFor model choice beyond this project, the independent comparison of [Claude Opus 5.5 and Gemini 4 Carbon](https://shattered.io/claude-vs-gemini-opus-5-5-vs-gemini-4-carbon-argon-59-vs-58-2026/) covers where each wins. The [Gemini 4 launch timeline](https://shattered.io/gemini-4-launch-earlier-claim-meta-muse-argon-carbon-zero-new-dates-2026/) explains how Argon, Carbon and Hydrogen reached developers in that order.\n\n## Prerequisites and Versions\n\nThe project needs very little. Everything runs on a laptop, and the test suite works offline with no key at all.\n\n- **Python 3.12 or newer.** Python 3.13 also works. Check with`python3 --version` .\n- **The google-genai SDK, latest version.** This is Google’s current Python client. The older`google-generativeai` package is the legacy library and does not mix well with the new one in a single environment.\n- **pydantic, python-dotenv and pytest, latest versions.** Pydantic gives you typed output, dotenv loads the key, pytest runs the checks.\n- **A Google account with billing enabled in Google AI Studio.** Carbon is a paid-tier model, so a free-tier key will list other models and fail on this one.\n- **A Linux, macOS or WSL shell.** Windows PowerShell works too, but the commands below use POSIX syntax.\n- **About $1 of credit.** At intro prices the whole walkthrough costs a few cents, yet a funded account avoids rate-limit surprises.\n\nPin the exact versions your install resolves into `requirements.txt` when you finish. Reproducible builds matter more than usual here because the SDK and the model lineup are both moving fast.\n\n## Steps 1 and 2: Create a Key and Scaffold the Project\n\n### Step 1: Create the API key\n\nOpen Google AI Studio, create a key under your billing-enabled project, and copy it once. Treat it like a database password. A key pasted into a notebook, a screenshot or a public repository gets scraped within minutes, and a leaked key on a paid project spends real money.\n\nNever commit the key. Create the project folder and an environment file that git ignores:\n\n```\nmkdir carbon-lab && cd carbon-lab\npython3 -m venv .venv\nsource .venv/bin/activate\npip install google-genai pydantic python-dotenv pytest\nprintf \"GEMINI_API_KEY=paste-your-key-here\\nCARBON_MODEL=gemini-4-carbon\\nFAST_MODEL=gemini-4-hydrogen\\nTHINKING_LEVEL=low\\nDAILY_BUDGET_USD=2.00\\n\" > .env\nprintf \".env\\n.venv/\\nusage.jsonl\\n__pycache__/\\n\" > .gitignore\nprintf \"GEMINI_API_KEY=\\nCARBON_MODEL=gemini-4-carbon\\nFAST_MODEL=gemini-4-hydrogen\\nTHINKING_LEVEL=low\\nDAILY_BUDGET_USD=2.00\\n\" > .env.example\n```\n\nThe last line produces an `.env.example` with the key blanked out, which is the only env file that belongs in version control. If you already run local models behind an authenticated proxy, the same hygiene applies, and the walkthrough on [setting up an Ollama API key behind a reverse proxy](https://shattered.io/ollama-api-key-reverse-proxy-docker-12-steps-2026/) shows the pattern.\n\nBeyond the ignore file, three habits protect a paid key. Rotate it on a schedule, for example every 90 days, and immediately after anyone who had access leaves the team. Restrict it where the console allows, so a stolen key cannot reach services you never use. In CI and production, inject it from a secrets manager or the platform’s encrypted variables instead of baking it into an image. A key in a Docker layer lives in that layer forever, even after you delete the file in a later step.\n\n### Step 2: Lay out the project\n\nCreate the package and test folders so the later steps drop files into the right places:\n\n```\nmkdir -p carbonlab tests\ntouch carbonlab/__init__.py tests/__init__.py\npip freeze | grep -iE \"google-genai|pydantic|python-dotenv|pytest\" > requirements.txt\ncat requirements.txt\n```\n\nExpected output (your versions will differ):\n\n```\ngoogle-genai==<resolved version>\npydantic==<resolved version>\npython-dotenv==<resolved version>\npytest==<resolved version>\n```\n\nThe final layout looks like this, and each later step adds one file:\n\n```\ncarbon-lab/\n  carbonlab/\n    config.py     step 3\n    client.py     steps 4 and 10\n    usage.py      steps 5 and 9\n    tools.py      step 7\n    schemas.py    step 8\n    cli.py        steps 3 to 11\n  tests/\n    test_usage.py step 12\n    test_retry.py step 12\n```\n\n## Steps 3 and 4: Load Settings and Make the First Call\n\n### Step 3: Load settings and verify the model ID\n\nHard-coded model names rot quickly. Gemini’s lineup changed several times between October and mid-November 2026, and community threads describe older models leaving some consumer plans while access to Gemini 2.5 narrows to projects that already used it. Put the model ID in configuration and verify it against the live model list.\n\n``` python\n# carbonlab/config.py\nimport os\nfrom dataclasses import dataclass\n\nfrom dotenv import load_dotenv\n\nload_dotenv()\n\n@dataclass(frozen=True)\nclass Settings:\n    api_key: str\n    model: str\n    fast_model: str\n    thinking_level: str\n    daily_budget_usd: float\n\ndef load_settings() -> Settings:\n    key = os.environ.get(\"GEMINI_API_KEY\", \"\").strip()\n    if not key:\n        raise SystemExit(\"GEMINI_API_KEY is not set. Copy .env.example to .env and fill it in.\")\n    return Settings(\n        api_key=key,\n        model=os.environ.get(\"CARBON_MODEL\", \"gemini-4-carbon\"),\n        fast_model=os.environ.get(\"FAST_MODEL\", \"gemini-4-hydrogen\"),\n        thinking_level=os.environ.get(\"THINKING_LEVEL\", \"low\"),\n        daily_budget_usd=float(os.environ.get(\"DAILY_BUDGET_USD\", \"2.00\")),\n    )\n```\n\nNow add the first piece of the CLI, a `models` command that lists what your key can see and flags the two IDs you configured:\n\n``` python\n# carbonlab/cli.py (part 1)\nimport argparse\nimport sys\n\nfrom google import genai\n\nfrom carbonlab.config import load_settings\n\ndef visible_models(client) -> set[str]:\n    return {m.name.removeprefix(\"models/\") for m in client.models.list()}\n\ndef cmd_models(args) -> int:\n    settings = load_settings()\n    client = genai.Client(api_key=settings.api_key)\n    names = visible_models(client)\n    for wanted in (settings.model, settings.fast_model):\n        flag = \"OK     \" if wanted in names else \"MISSING\"\n        print(f\"{flag} {wanted}\")\n    gemini4 = sorted(n for n in names if n.startswith(\"gemini-4\"))\n    print(\"gemini-4 models your key can see:\", \", \".join(gemini4) or \"none\")\n    return 0 if settings.model in names else 1\n```\n\nRun it once the CLI entry point exists (the final assembly appears in step 11). Sample output on a paid key:\n\n``` bash\n$ python -m carbonlab.cli models\nOK      gemini-4-carbon\nOK      gemini-4-hydrogen\ngemini-4 models your key can see: gemini-4-argon, gemini-4-carbon, gemini-4-hydrogen\n```\n\nIf `MISSING` appears next to Carbon, stop here. The model is invisible to your key, and no amount of retry logic will fix that. The troubleshooting section lists the usual causes. Hydrogen’s identifier also comes from community trackers rather than a page you can verify offline, so this check protects the fast-model setting too.\n\nThe check costs nothing because listing models is a metadata call, not a generation. Run it in a deploy script as well, and fail the release when the configured ID disappears. That turns a 3 a.m. outage into a red build at 3 p.m. It also answers a question that trips up many first-time users: whether the problem lives in their code or in their account.\n\n### Step 4: Send the first request\n\nA first call needs three things: a client, a model ID and some contents. Add a thin `generate` wrapper now, because step 10 will extend it with retries and you want one place to change.\n\n``` python\n# carbonlab/client.py\nfrom google import genai\nfrom google.genai import types\n\ndef make_client(api_key: str) -> genai.Client:\n    return genai.Client(api_key=api_key)\n\ndef build_config(thinking_level: str, max_output_tokens: int = 1024, system: str | None = None):\n    return types.GenerateContentConfig(\n        system_instruction=system,\n        max_output_tokens=max_output_tokens,\n        temperature=0.4,\n        thinking_config=types.ThinkingConfig(thinking_level=thinking_level),\n    )\n\ndef generate(client, model: str, prompt: str, config=None):\n    return client.models.generate_content(model=model, contents=prompt, config=config)\n```\n\nWire an `ask` command into the CLI that prints the answer and a one-line usage summary:\n\n``` python\n# carbonlab/cli.py (part 2)\nfrom carbonlab.client import build_config, generate, make_client\n\ndef cmd_ask(args) -> int:\n    settings = load_settings()\n    client = make_client(settings.api_key)\n    config = build_config(args.thinking or settings.thinking_level, args.max_tokens)\n    resp = generate(client, settings.model, args.prompt, config)\n    print(resp.text)\n    u = resp.usage_metadata\n    print(f\"\\n[prompt={u.prompt_token_count} output={u.candidates_token_count} \"\n          f\"thinking={u.thoughts_token_count}]\", file=sys.stderr)\n    return 0\n```\n\nRun it:\n\n```\npython -m carbonlab.cli ask \"Explain HTTP 429 in two sentences.\"\nHTTP 429 means the client sent too many requests in a given window, so the server refuses further calls until the limit resets. A well-behaved client reads the Retry-After header when present and backs off exponentially with a little random jitter.\n\n[prompt=9 output=52 thinking=118]\n```\n\nThe sample numbers are illustrative, yet the shape is real: thinking tokens show up in a separate counter, and on a short question they can outnumber the visible answer. That brings up the next step.\n\n## Step 5: Read Token Usage and Price Thinking Tokens\n\nCarbon is a reasoning model, and reasoning is not free. Real Python’s [explainer on Carbon](https://realpython.com/gemini-4-carbon/) frames it as a model that thinks longer, and the pricing page analysis from [Apidog](https://apidog.com/blog/gemini-4-carbon-pricing/) reports that thinking tokens bill at the output rate. In one example drawn from Google’s documentation, the model thought for 297 tokens to write 171, so most of the charge landed on text the user never saw. Plan on that behavior until your own ledger says otherwise.\n\nThat makes three counters worth tracking per request: prompt tokens (minus any cached portion), cached tokens, and output tokens including thinking. Build the cost module around exactly those fields and keep the price table in one place with the cutover date written down.\n\nThe usage object returned with every response carries the numbers you need:\n\n- `prompt_token_count` is everything you sent, including the system instruction, the conversation history and any cached content.\n- `cached_content_token_count` is the part of the prompt served from cache, which bills at the cached rate.\n- `candidates_token_count` is the visible answer.\n- `thoughts_token_count` is the hidden reasoning, billed with output.\n- `total_token_count` is the sum, useful as a cross-check against the other fields.\n\nIf the sum of your own fields drifts from the total, the SDK or the API changed a field, and that is exactly the kind of drift a test should catch.\n\n``` python\n# carbonlab/usage.py\nimport json\nfrom dataclasses import dataclass\nfrom datetime import date, datetime, timezone\nfrom decimal import Decimal\nfrom pathlib import Path\n\nINTRO_ENDS = date(2026, 11, 30)\nLEDGER = Path(\"usage.jsonl\")\n\n@dataclass(frozen=True)\nclass Rate:\n    input: Decimal\n    cached: Decimal\n    output: Decimal\n\nINTRO = Rate(Decimal(\"2\"), Decimal(\"0.10\"), Decimal(\"10\"))\nSTANDARD = Rate(Decimal(\"4\"), Decimal(\"0.20\"), Decimal(\"20\"))\n\nclass BudgetExceeded(RuntimeError):\n    pass\n\ndef rate_for(day: date) -> Rate:\n    return INTRO if day <= INTRO_ENDS else STANDARD\n\ndef cost_usd(usage, day: date | None = None) -> Decimal:\n    rate = rate_for(day or date.today())\n    prompt = usage.prompt_token_count or 0\n    cached = usage.cached_content_token_count or 0\n    out = (usage.candidates_token_count or 0) + (usage.thoughts_token_count or 0)\n    fresh = max(prompt - cached, 0)\n    total = Decimal(fresh) * rate.input + Decimal(cached) * rate.cached + Decimal(out) * rate.output\n    return total / Decimal(1_000_000)\n\ndef record(model: str, usage, cost: Decimal, label: str = \"\") -> None:\n    row = {\n        \"ts\": datetime.now(timezone.utc).isoformat(),\n        \"model\": model,\n        \"label\": label,\n        \"prompt\": usage.prompt_token_count or 0,\n        \"cached\": usage.cached_content_token_count or 0,\n        \"output\": usage.candidates_token_count or 0,\n        \"thinking\": usage.thoughts_token_count or 0,\n        \"cost_usd\": str(cost),\n    }\n    with LEDGER.open(\"a\", encoding=\"utf-8\") as fh:\n        fh.write(json.dumps(row) + \"\\n\")\n\ndef spent_today() -> Decimal:\n    if not LEDGER.exists():\n        return Decimal(\"0\")\n    today = datetime.now(timezone.utc).date().isoformat()\n    total = Decimal(\"0\")\n    for line in LEDGER.read_text(encoding=\"utf-8\").splitlines():\n        row = json.loads(line)\n        if row[\"ts\"].startswith(today):\n            total += Decimal(row[\"cost_usd\"])\n    return total\n\ndef check_budget(limit: float) -> None:\n    spent = spent_today()\n    if spent >= Decimal(str(limit)):\n        raise BudgetExceeded(f\"Daily budget ${limit:.2f} reached (spent ${spent:.4f}).\")\n```\n\nTwo design choices deserve a note. Decimal arithmetic keeps fractions of a cent exact, which matters when you sum thousands of tiny charges. And the ledger lives in a plain JSONL file, so you can `grep` it, load it into a spreadsheet, or diff it against Google’s billing export at month end.\n\nNow call the ledger from `ask`. Add these lines right after the response comes back:\n\n```\n# carbonlab/cli.py (inside cmd_ask, after resp = generate(...))\nfrom carbonlab.usage import check_budget, cost_usd, record\n\ncheck_budget(settings.daily_budget_usd)   # place this BEFORE the generate call\nc = cost_usd(resp.usage_metadata)\nrecord(settings.model, resp.usage_metadata, c, label=\"ask\")\nprint(f\"[cost=${c:.6f}]\", file=sys.stderr)\n```\n\nA second run now prints a cost line and the ledger grows by one row:\n\n```\n[prompt=9 output=52 thinking=118]\n[cost=$0.001718]\n```\n\nCarbon bills input at $2 per million tokens and output at $10 per million tokens through November 30, 2026. From December 1, 2026 the rates are $4 and $20, so the same request costs twice as much.\n\n## Step 6: Stream Responses for Faster Perceived Latency\n\nReasoning models pause before they talk. Streaming hides part of that wait because the first visible token arrives as soon as the model finishes thinking, instead of after the full answer is assembled. The SDK exposes streaming as a generator, and the usage metadata arrives on the final chunk.\n\n``` python\n# carbonlab/client.py (add)\ndef stream(client, model: str, prompt: str, config=None):\n    last = None\n    for chunk in client.models.generate_content_stream(model=model, contents=prompt, config=config):\n        if chunk.text:\n            yield chunk.text\n        last = chunk\n    yield (\"__usage__\", getattr(last, \"usage_metadata\", None))\n```\n\nThe sentinel tuple is a small hack to carry usage out of a generator without a class. Consume it in the CLI like this:\n\n``` python\n# carbonlab/cli.py (part 3)\nimport time\n\nfrom carbonlab.client import stream\n\ndef cmd_stream(args) -> int:\n    settings = load_settings()\n    check_budget(settings.daily_budget_usd)\n    client = make_client(settings.api_key)\n    config = build_config(args.thinking or settings.thinking_level, args.max_tokens)\n    started = time.perf_counter()\n    first = None\n    usage = None\n    for piece in stream(client, settings.model, args.prompt, config):\n        if isinstance(piece, tuple):\n            usage = piece[1]\n            continue\n        if first is None:\n            first = time.perf_counter() - started\n        print(piece, end=\"\", flush=True)\n    total = time.perf_counter() - started\n    print(f\"\\n\\n[first token {first:.2f}s, total {total:.2f}s]\", file=sys.stderr)\n    if usage:\n        c = cost_usd(usage)\n        record(settings.model, usage, c, label=\"stream\")\n        print(f\"[cost=${c:.6f}]\", file=sys.stderr)\n    return 0\n```\n\nSample run:\n\n``` bash\n$ python -m carbonlab.cli stream \"List three failure modes of retry loops.\"\n1. Retry storms: every client retries at once and amplifies the outage.\n2. Non-idempotent calls: a retried POST charges the customer twice.\n3. Unbounded attempts: the loop never gives up and hides a permanent error.\n\n[first token 2.84s, total 4.10s]\n[cost=$0.002290]\n```\n\nYour timings will differ with thinking level, load and region.\n\nStreaming is not free of trade-offs. It keeps a connection open longer, it complicates retries because a stream can fail halfway through an answer, and it makes JSON output harder to validate until the final chunk arrives. Stream for chat-style interfaces where a human watches the text appear. Skip it for batch jobs, structured extraction and anything a program consumes, where a single complete response is simpler and easier to retry cleanly. The point is the gap between first token and total time. If you want to compare that gap across vendors on identical prompts, a [cross-vendor API test harness](https://shattered.io/chatgpt-vs-claude-vs-gemini-api-test-harness-12-steps-2026/) gives you a fair way to measure it.\n\nOne streaming caveat bites early. Community reports from November 12 describe a model-identity mismatch in Carbon streaming responses, so log the model name you requested and never trust a self-reported identity string inside the generated text.\n\n## Step 7: Add Function Calling With a Real Tool\n\nTool use lets the model ask your code for facts instead of guessing. The SDK can run Python functions for you: pass the function in `tools`, keep the type hints and docstring accurate, and the library builds the schema, executes the call and feeds the result back to the model.\n\nUse a tool that ties into this project’s own purpose, a price checker that answers “what would this request cost on a given day?”:\n\n``` python\n# carbonlab/tools.py\nfrom datetime import date\nfrom types import SimpleNamespace\n\nfrom carbonlab.usage import cost_usd\n\ndef price_check(prompt_tokens: int, output_tokens: int, on_date: str) -> dict:\n    \"\"\"Estimate the Gemini 4 Carbon API cost in USD for one request.\n\n    Args:\n        prompt_tokens: Input tokens sent with the request.\n        output_tokens: Output tokens including thinking tokens.\n        on_date: Calendar date in YYYY-MM-DD format used to pick the price period.\n    \"\"\"\n    usage = SimpleNamespace(\n        prompt_token_count=prompt_tokens,\n        cached_content_token_count=0,\n        candidates_token_count=output_tokens,\n        thoughts_token_count=0,\n    )\n    day = date.fromisoformat(on_date)\n    return {\"on_date\": on_date, \"cost_usd\": str(cost_usd(usage, day))}\n```\n\nHook it into a `tool` command:\n\n``` python\n# carbonlab/cli.py (part 4)\nfrom google.genai import types\n\nfrom carbonlab.tools import price_check\n\ndef cmd_tool(args) -> int:\n    settings = load_settings()\n    check_budget(settings.daily_budget_usd)\n    client = make_client(settings.api_key)\n    config = types.GenerateContentConfig(\n        tools=[price_check],\n        max_output_tokens=args.max_tokens,\n        thinking_config=types.ThinkingConfig(thinking_level=settings.thinking_level),\n    )\n    resp = client.models.generate_content(model=settings.model, contents=args.prompt, config=config)\n    print(resp.text)\n    c = cost_usd(resp.usage_metadata)\n    record(settings.model, resp.usage_metadata, c, label=\"tool\")\n    return 0\nbash\n$ python -m carbonlab.cli tool \"A request sends 1000 input tokens and gets 468 output tokens. Compare the cost on 2026-11-20 and 2026-12-05.\"\nOn 2026-11-20 the request costs about $0.00668. On 2026-12-05 it costs about $0.01336, exactly double, because the introductory Carbon price ends on November 30.\n```\n\nWhen you need control, such as human approval before a tool with side effects runs, disable the automatic loop with `types.AutomaticFunctionCallingConfig(disable=True)` and read `resp.function_calls` yourself. Never give a model a tool that deletes, pays or emails without that approval gate. Anything the model can call, a prompt-injected document can also try to call.\n\n## Step 8: Force Structured JSON Output\n\nFree-form text breaks parsers. Constrain the response with a schema and let Pydantic validate it. This is the step that turns a chatty model into a dependable component.\n\n``` python\n# carbonlab/schemas.py\nfrom typing import Literal\n\nfrom pydantic import BaseModel\n\nclass TicketTriage(BaseModel):\n    category: Literal[\"billing\", \"bug\", \"feature\", \"other\"]\n    urgency: int\n    summary: str\n    needs_human: bool\npython\n# carbonlab/cli.py (part 5)\nfrom carbonlab.schemas import TicketTriage\n\ndef cmd_triage(args) -> int:\n    settings = load_settings()\n    check_budget(settings.daily_budget_usd)\n    client = make_client(settings.api_key)\n    config = types.GenerateContentConfig(\n        system_instruction=\"Classify support tickets. Urgency runs from 1 (low) to 5 (outage).\",\n        response_mime_type=\"application/json\",\n        response_schema=TicketTriage,\n        max_output_tokens=args.max_tokens,\n        thinking_config=types.ThinkingConfig(thinking_level=\"low\"),\n    )\n    resp = client.models.generate_content(model=settings.model, contents=args.prompt, config=config)\n    ticket = resp.parsed\n    if not isinstance(ticket, TicketTriage) or not 1 <= ticket.urgency <= 5:\n        raise SystemExit(f\"Invalid triage output: {resp.text!r}\")\n    print(ticket.model_dump_json(indent=2))\n    c = cost_usd(resp.usage_metadata)\n    record(settings.model, resp.usage_metadata, c, label=\"triage\")\n    return 0\nbash\n$ python -m carbonlab.cli triage \"Our invoice shows two charges for November and the dashboard is down for the whole team.\"\n{\n  \"category\": \"billing\",\n  \"urgency\": 4,\n  \"summary\": \"Duplicate November charge reported alongside a team-wide dashboard outage.\",\n  \"needs_human\": true\n}\n```\n\nNotice that range validation happens in your code and not in the schema. Schema keywords vary in how strictly a model honors them, whereas a plain `if` statement always works. Treat the schema as strong guidance and your validator as the contract.\n\nSchema design matters as much as the call itself. Prefer enums over free strings for categories, because a closed set is far easier to validate and to count in a dashboard. Keep field names plain and descriptive, since the model reads them as hints. Avoid deeply nested objects when a flat structure will do, and give every field a purpose you could explain in one sentence. When a field is optional, say so in the system instruction, or the model will invent a value to fill the slot.\n\n## Step 9: Build the Cost Calculator Before December 1\n\nGoogle’s announcement set a hard cutover, and the pricing analysis from Apidog adds that batch discounts scale with the price, so the 50% batch discount it describes stays in place after the doubling. Whatever your volume, model the two periods side by side now. The table below sets out example scenarios for both periods, with no caching unless a row says otherwise.\n\n| Scenario | Through Nov 30 | From Dec 1 | \n|---|---|---|\n| 1 request | Not listed | Not listed | \n| 1,000 requests | Not listed | Not listed | \n| 10,000 requests | $66.80 | $133.60 | \n| 100,000 requests | Not listed | Not listed | \n| 10,000 requests, thinking cut to 100 tokens | Not listed | Not listed | \n| 10,000 requests, 800 of 1,000 input tokens cached | Not listed | Not listed | \n\nThe last two rows show where savings come from. Trimming 197 thinking tokens per request cuts about 30% of the bill, and caching an 800-token shared prefix cuts about 23%. Output still dominates this request, so thinking control beats caching here. If your prompts carry long documents and your answers stay short, caching moves much further. Run the numbers on your own token mix with a small `cost` command:\n\n``` python\n# carbonlab/cli.py (part 6)\nfrom datetime import date\nfrom types import SimpleNamespace\n\ndef cmd_cost(args) -> int:\n    usage = SimpleNamespace(\n        prompt_token_count=args.prompt_tokens,\n        cached_content_token_count=args.cached,\n        candidates_token_count=args.output_tokens,\n        thoughts_token_count=0,\n    )\n    per_req_intro = cost_usd(usage, date(2026, 11, 30))\n    per_req_std = cost_usd(usage, date(2026, 12, 1))\n    n = args.requests\n    print(f\"Through Nov 30: ${per_req_intro * n:,.2f} for {n:,} requests\")\n    print(f\"From Dec 1:     ${per_req_std * n:,.2f} for {n:,} requests\")\n    print(f\"Increase:       ${(per_req_std - per_req_intro) * n:,.2f}\")\n    return 0\nbash\n$ python -m carbonlab.cli cost --prompt-tokens 1000 --output-tokens 468 --requests 10000\nThrough Nov 30: $66.80 for 10,000 requests\nFrom Dec 1:     $133.60 for 10,000 requests\nIncrease:       $66.80\n```\n\nIf your task does not need a reasoning model at all, such as classifying short texts or scoring decisions, look at Google’s cheaper endpoints. The [Decisions API pricing](https://shattered.io/gemini-decision-1-decisions-api-1000-questions-0-10-per-million-2026/) sits far below Carbon’s per-token rates for that narrow job.\n\nOne billing wrinkle to watch: a community report says some AI Studio billing dashboards appeared to charge standard rather than promotional prices during the intro window. Reconcile your ledger against the dashboard weekly, and ask Google support if the two diverge.\n\n## Steps 10 and 11: Retries, Budget Guards and Cost Controls\n\n### Step 10: Retry transient failures with backoff and jitter\n\nRate limits and server faults happen on any hosted API, so retries are not optional. The rule is simple: retry only errors that can heal (rate limits and server faults), cap the attempts, and add random jitter so a thousand clients do not retry in lockstep.\n\n``` python\n# carbonlab/client.py (replace generate with this version)\nimport random\nimport sys\nimport time\n\nfrom google.genai import errors\n\nRETRYABLE = {429, 500, 502, 503, 504}\n\ndef generate(client, model: str, prompt: str, config=None, max_attempts: int = 5, sleep=time.sleep):\n    delay = 1.0\n    for attempt in range(1, max_attempts + 1):\n        try:\n            return client.models.generate_content(model=model, contents=prompt, config=config)\n        except errors.APIError as exc:\n            if exc.code not in RETRYABLE or attempt == max_attempts:\n                raise\n            wait = delay + random.uniform(0, delay / 2)\n            print(f\"retry {attempt}/{max_attempts - 1} after HTTP {exc.code}, waiting {wait:.1f}s\",\n                  file=sys.stderr)\n            sleep(wait)\n            delay = min(delay * 2, 30.0)\n```\n\nThe `sleep` parameter exists so tests can pass a no-op and finish instantly. A failing run prints something like this:\n\n```\nretry 1/4 after HTTP 503, waiting 1.3s\nretry 2/4 after HTTP 503, waiting 2.6s\n```\n\nA bad request or a permission error is not retryable. Retrying a bad request wastes time, and retrying a permission error can trip abuse protections.\n\nTwo companions to retries are worth adding early. Set a client-side timeout so a stalled connection fails in seconds instead of hanging a worker, and keep the calls themselves idempotent. Text generation is safe to repeat, but a tool that charges a card or sends an email is not, so give those tools an idempotency key and check it before acting. Retries multiply whatever your code does, good or bad.\n\n### Step 11: Route cheap work away from Carbon and cap thinking\n\nThree levers cut spend without touching quality much. Lower the thinking level on simple prompts, because Apidog’s reading of Google’s thinking guide says Carbon defaults to a medium level and also accepts low and high. Keep a stable prompt prefix so implicit caching can bill repeated input at the 95% discount Google announced. And route easy requests to the faster model.\n\n``` python\n# carbonlab/client.py (add)\ndef pick_model(settings, prompt: str, needs_deep_reasoning: bool) -> tuple[str, str]:\n    if needs_deep_reasoning or len(prompt) > 4000:\n        return settings.model, \"medium\"\n    return settings.fast_model, \"low\"\n```\n\nThis router is deliberately crude. A length check and an explicit flag beat a clever classifier until your ledger proves otherwise. After a week of data, look at the `label` and `thinking` columns, find the labels where thinking tokens dominate, and lower the level for those first. Independent quality-per-dollar data, such as the [Artificial Analysis index results for Carbon](https://shattered.io/gemini-4-carbon-artificial-analysis-59-claude-opus-5-5-2026/), helps you decide which prompts deserve the premium model.\n\nFinally, assemble the entry point so every command works from one file:\n\n``` php\n# carbonlab/cli.py (part 7, bottom of file)\ndef main(argv=None) -> int:\n    p = argparse.ArgumentParser(prog=\"carbonlab\")\n    sub = p.add_subparsers(dest=\"cmd\", required=True)\n\n    sub.add_parser(\"models\").set_defaults(fn=cmd_models)\n    for name, fn in ((\"ask\", cmd_ask), (\"stream\", cmd_stream), (\"tool\", cmd_tool), (\"triage\", cmd_triage)):\n        sp = sub.add_parser(name)\n        sp.add_argument(\"prompt\")\n        sp.add_argument(\"--max-tokens\", type=int, default=1024)\n        sp.add_argument(\"--thinking\", choices=[\"low\", \"medium\", \"high\"])\n        sp.set_defaults(fn=fn)\n    c = sub.add_parser(\"cost\")\n    c.add_argument(\"--prompt-tokens\", type=int, required=True)\n    c.add_argument(\"--output-tokens\", type=int, required=True)\n    c.add_argument(\"--cached\", type=int, default=0)\n    c.add_argument(\"--requests\", type=int, default=1)\n    c.set_defaults(fn=cmd_cost)\n\n    args = p.parse_args(argv)\n    return args.fn(args)\n\nif __name__ == \"__main__\":\n    raise SystemExit(main())\n```\n\nBecause the file grows through several parts, keep all imports at the top of `cli.py` when you merge the snippets. Python does not care where an import sits, but flake8 and your teammates do.\n\n## Step 12: Test the Project and Wire It Into CI\n\nTests for an API client should never need the API. Test the parts you own: the price math, the cutover date, the retry policy and the budget guard. Use fakes for the network.\n\n``` python\n# tests/test_usage.py\nfrom datetime import date\nfrom decimal import Decimal\nfrom types import SimpleNamespace\n\nimport pytest\n\nfrom carbonlab import usage\n\ndef u(prompt=1000, cached=0, out=171, think=297):\n    return SimpleNamespace(prompt_token_count=prompt, cached_content_token_count=cached,\n                           candidates_token_count=out, thoughts_token_count=think)\n\ndef test_intro_price_last_day():\n    assert usage.cost_usd(u(), date(2026, 11, 30)) == Decimal(\"0.00668\")\n\ndef test_price_doubles_on_dec_first():\n    assert usage.cost_usd(u(), date(2026, 12, 1)) == Decimal(\"0.01336\")\n\ndef test_cached_tokens_use_cached_rate():\n    cost = usage.cost_usd(u(prompt=1000, cached=800, out=0, think=0), date(2026, 11, 30))\n    assert cost == Decimal(\"0.00048\")\n\ndef test_budget_guard(tmp_path, monkeypatch):\n    monkeypatch.setattr(usage, \"LEDGER\", tmp_path / \"usage.jsonl\")\n    usage.record(\"gemini-4-carbon\", u(), Decimal(\"1.50\"))\n    with pytest.raises(usage.BudgetExceeded):\n        usage.check_budget(1.00)\n    usage.check_budget(5.00)\npython\n# tests/test_retry.py\nimport pytest\nfrom google.genai import errors\n\nfrom carbonlab.client import generate\n\nclass FlakyModels:\n    def __init__(self, failures, code):\n        self.failures, self.code, self.calls = failures, code, 0\n\n    def generate_content(self, **kwargs):\n        self.calls += 1\n        if self.calls <= self.failures:\n            raise errors.APIError(self.code, {\"error\": {\"message\": \"boom\"}})\n        return \"ok\"\n\nclass FakeClient:\n    def __init__(self, failures, code):\n        self.models = FlakyModels(failures, code)\n\ndef test_retries_then_succeeds():\n    client = FakeClient(failures=2, code=503)\n    assert generate(client, \"m\", \"hi\", sleep=lambda s: None) == \"ok\"\n    assert client.models.calls == 3\n\ndef test_does_not_retry_client_errors():\n    client = FakeClient(failures=1, code=400)\n    with pytest.raises(errors.APIError):\n        generate(client, \"m\", \"hi\", sleep=lambda s: None)\n    assert client.models.calls == 1\n```\n\nRun them:\n\n```\npython -m pytest -q\n......                                                                   [100%]\n6 passed in 0.21s\n```\n\nThe cutover test earns its keep. When someone edits `INTRO_ENDS` or the rates, a red test tells them before the invoice does. Add one live smoke test only if you accept the cost, and gate it on an environment variable so CI stays free. A minimal GitHub Actions workflow runs the offline suite on every push:\n\n```\n# .github/workflows/test.yml\nname: test\non: [push, pull_request]\njobs:\n  pytest:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-python@v5\n        with:\n          python-version: \"3.12\"\n      - run: pip install -r requirements.txt\n      - run: python -m pytest -q\n```\n\nStore the real key as a repository secret only if you add the live test, and never echo it in a log step.\n\n## Common Pitfalls\n\nMost failures in a first Gemini API project come from the same few habits. The table lists the ones that cost developers the most time or money.\n\n| Pitfall | Symptom | Fix | \n|---|---|---|\n| Hard-coding the model ID | Calls fail after a model is replaced or its access is restricted | Read the ID from `.env` and run the`models` check on startup | \n| Ignoring thinking tokens | Bills far above what visible text suggests | Log `thoughts_token_count` and lower`THINKING_LEVEL` | \n| Setting `max_output_tokens` too low | Empty or cut-off text with a MAX_TOKENS finish reason | Raise the cap, since thinking can consume part of the budget | \n| Budgeting with intro prices after November 30 | Spend doubles on December 1 | Use the dated rate table in `usage.py` and a daily cap | \n| Retrying every error | Wasted calls on client errors that can never succeed | Retry only 429 and 5xx codes, with jitter | \n| Treating a subscription as API access | Carbon works in the app but the API returns permission errors | Enable billing for the API project separately from any Ultra or Pro plan | \n| Committing the key | Surprise charges and a revoked key | Keep `.env` in`.gitignore` and rotate any exposed key at once | \n\nA subtler pitfall deserves its own sentence. Community measurements claim Carbon and Argon consume Antigravity subscription quota at roughly three times the rate of Gemini 3.8 Flash. That is an unconfirmed report about a consumer subscription, not API billing, so do not carry that multiplier into your cost model.\n\n## Troubleshooting\n\nWork through these in order when something breaks. Most problems resolve in the first three.\n\n1. **Model not found error for `gemini-4-carbon`.** Run the` models` command. If Carbon is missing, your project may lack billing, your region may lag the rollout, or the ID changed. Confirm against Google’s model documentation, then update`CARBON_MODEL` .\n2. **Permission denied error.** The key belongs to a project without access to the model, or the API is not enabled. Create a key under the billing-enabled project and retry.\n3. **429 rate limit or quota errors.** Check your tier in the AI Studio rate-limit dashboard. Forum reports note that some models are unavailable on the free tier. Back off, lower concurrency, or move to a paid tier.\n4. **Model overloaded error.** A server-side capacity problem. The retry wrapper handles brief spikes. If failures persist for minutes, route to the fast model temporarily.\n5. **Empty `resp.text` or `None`.** Check the finish reason. A MAX_TOKENS stop usually means thinking consumed the budget. Raise`max_output_tokens` or lower the thinking level. A safety block returns no text either, so inspect`resp.candidates` .\n6. **`resp.parsed` is `None` in triage.** The model returned JSON that failed the schema. Print`resp.text` , simplify the schema, and keep enum values short and unambiguous.\n7. **The tool never runs.** Add a clearer docstring and type hints, and name the tool in your prompt. Vague docstrings give the model no reason to call the function.\n8. **ImportError on `from google import genai`.** You probably have the legacy` google-generativeai` package shadowing the new SDK. Create a clean virtual environment and install only`google-genai` .\n9. **“User location is not supported”.** Your request originates from a region the API does not serve. Run from a supported region and check that no proxy rewrites your exit location.\n10. **Costs on the dashboard exceed your ledger.** The dashboard counts thinking tokens, retries that reached the server and calls from other scripts that share the key. Rotate the key and compare again, and remember the report that some dashboards showed standard rather than promotional prices.\n11. **Streaming shows no cost line.** Usage metadata arrives on the last chunk. If your loop breaks early, the ledger misses that request. Always drain the generator.\n\n## Advanced Tips\n\nOnce the basics run clean, a few upgrades pay off quickly.\n\n- **Count before you send.** Call`client.models.count_tokens` on large documents and refuse requests whose estimated cost exceeds a per-request ceiling. This catches an accidental 500-page paste before it bills.\n- **Design prompts for implicit caching.** Put the long, unchanging material (instructions, reference text) first and the variable question last. Repeated prefixes bill at the cached rate, which Google set at 95% below the input price.\n- **Try the batch route for offline jobs.** Apidog’s pricing breakdown reports a 50% batch discount that persists after the December increase. Overnight classification and summarization jobs fit that model.\n- **Log prompt hashes, not prompts.** A SHA-256 of the prompt lets you spot duplicate work and caching misses without storing sensitive text in your ledger.\n- **Watch the newer API surface.** Google’s documentation describes an Interactions API labeled beta alongside a stable v1. Pin your SDK version and read the changelog before adopting it in production code.\n- **Use Carbon for hard problems and say so.** Carbon has also been applied to formal verification, as in the report on[Carbon checking Signal’s library with Lean proofs](https://shattered.io/gemini-4-carbon-access-signal-libsignal-alphaproof-code-6-5m-lean-proofs-2026/) , which shows what the long-thinking mode is for. Short chat tasks rarely need it.\n- **Alert on spend, not just errors.** Send a message when`spent_today()` passes 80% of the cap. The cheapest outage is the one you see coming.\n\n- **Review the ledger weekly.** Group rows by`label` and sort by cost. Two or three labels usually account for most of the spend, and those are the prompts worth rewriting or routing to the faster model.\n- **Keep a golden prompt set.** Save ten real prompts with answers you have judged acceptable, and rerun them after every model or price change. A cheap model that fails three of ten is not cheaper once you count the human cleanup.\n\nFor wider context on model releases, benchmarks and safety, browse the [AI and machine learning coverage](https://shattered.io/category/ai-machine-learning/).\n\n## FAQ\n\n### How do I get a Gemini API key?\n\nSign in to Google AI Studio with a Google account, create a key inside a project and copy it. Store it in an environment variable or a secrets manager and never commit it to git. For Gemini 4 Carbon you also need billing enabled on that project, because Google describes the model as available to paying developers.\n\n### How much does the Gemini 4 Carbon API cost?\n\nGoogle priced Carbon at $2 per million input tokens and $10 per million output tokens through November 30, 2026. From December 1 the price becomes $4 per million input tokens and $20 per million output tokens. Cached input tokens carry a 95% discount from the input price, according to Google’s announcement.\n\n### Does the price go up for existing users on December 1?\n\nGoogle’s published pricing names no exception for existing projects. Plan to pay $4 and $20 per million tokens on every Carbon token sent on or after December 1, and use the dated rate table from this project to model the change.\n\n### Is the Gemini API free?\n\nGoogle’s billing documentation describes a free tier and a paid pay-as-you-go tier. Carbon launched for paying developers and enterprises, so expect to need the paid tier for it. A key that works on the free tier may still list other models and reject Carbon.\n\n### Does a Google AI Ultra or Pro subscription include API access?\n\nNot automatically. Ultra gives subscribers Carbon in the Gemini app, AI Mode in Search and Google Antigravity, while the Gemini API bills by token on its own account. Check your Google AI Studio billing page for any credits that come with your plan.\n\n### Which Python SDK should I use?\n\nUse the current `google-genai` package and import it with `from google import genai`. Avoid mixing it with the legacy `google-generativeai` library in the same environment.\n\n### Should I use Carbon or Hydrogen?\n\nUse Carbon for deep, long-horizon reasoning and hard coding problems. Use Hydrogen, introduced on November 12, when speed matters more, such as agent loops. Run both through the same ledger for a week and compare cost per successful task before you commit.", "url": "https://wpnews.pro/news/gemini-api-tutorial-carbon-in-12-steps-70-min-2026", "canonical_source": "https://shattered.io/gemini-api-tutorial-gemini-4-carbon-12-steps-2026/", "published_at": "2026-11-15 11:44:45+00:00", "updated_at": "2026-11-15 11:49:23.806594+00:00", "lang": "en", "topics": ["large-language-models", "generative-ai", "ai-products", "ai-tools", "artificial-intelligence"], "entities": ["Google", "Gemini 4 Carbon", "Gemini 4 Hydrogen", "Gemini 4 Argon", "Gemini API", "Google AI Studio", "Android Studio", "Google Antigravity"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/gemini-api-tutorial-carbon-in-12-steps-70-min-2026", "markdown": "https://wpnews.pro/news/gemini-api-tutorial-carbon-in-12-steps-70-min-2026.md", "text": "https://wpnews.pro/news/gemini-api-tutorial-carbon-in-12-steps-70-min-2026.txt", "jsonld": "https://wpnews.pro/news/gemini-api-tutorial-carbon-in-12-steps-70-min-2026.jsonld"}}