The Gemini API now serves Gemini 4 Carbon to paying developers, and Google’s launch post 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.
That 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 shows, so the project checks which model IDs your key can see before it sends a single prompt.
Demand for this skill is easy to measure. Stack Overflow’s 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.
Don't miss new tech stories on Google
Add Shattered once in the Google app and our stories show up in your news suggestions.
Where the Gemini API Stands on November 15, 2026 #
Google 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 describes the Ultra-first pattern. The API identifier is gemini-4-carbon.
Google also introduced Gemini 4 Hydrogen 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.
| Item | Current state (November 15, 2026) |
|---|---|
| Model ID | gemini-4-carbon |
| General availability | November 10, 2026, for paying developers and enterprises |
| Input price, through November 30 | $2 per 1M tokens |
| Output price, through November 30 | $10 per 1M tokens |
| Input price, from December 1 | $4 per 1M tokens |
| Output price, from December 1 | $20 per 1M tokens |
| Cached input | 95% off the input price |
| Intro price matches | Gemini 4 Argon ($2 and $10 per 1M) |
| Where it runs | Gemini API, Google AI Studio, Android Studio, Gemini for business, Antigravity |
Two 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.
For model choice beyond this project, the independent comparison of Claude Opus 5.5 and Gemini 4 Carbon covers where each wins. The Gemini 4 launch timeline explains how Argon, Carbon and Hydrogen reached developers in that order.
Prerequisites and Versions #
The project needs very little. Everything runs on a laptop, and the test suite works offline with no key at all.
- Python 3.12 or newer. Python 3.13 also works. Check with
python3 --version. - The google-genai SDK, latest version. This is Google’s current Python client. The older
google-generativeaipackage is the legacy library and does not mix well with the new one in a single environment. - pydantic, python-dotenv and pytest, latest versions. Pydantic gives you typed output, dotenv loads the key, pytest runs the checks.
- 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.
- A Linux, macOS or WSL shell. Windows PowerShell works too, but the commands below use POSIX syntax.
- About $1 of credit. At intro prices the whole walkthrough costs a few cents, yet a funded account avoids rate-limit surprises.
Pin 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.
Steps 1 and 2: Create a Key and Scaffold the Project #
Step 1: Create the API key
Open 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.
Never commit the key. Create the project folder and an environment file that git ignores:
mkdir carbon-lab && cd carbon-lab
python3 -m venv .venv
source .venv/bin/activate
pip install google-genai pydantic python-dotenv pytest
printf "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
printf ".env\n.venv/\nusage.jsonl\n__pycache__/\n" > .gitignore
printf "GEMINI_API_KEY=\nCARBON_MODEL=gemini-4-carbon\nFAST_MODEL=gemini-4-hydrogen\nTHINKING_LEVEL=low\nDAILY_BUDGET_USD=2.00\n" > .env.example
The 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 shows the pattern.
Beyond 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.
Step 2: Lay out the project
Create the package and test folders so the later steps drop files into the right places:
mkdir -p carbonlab tests
touch carbonlab/__init__.py tests/__init__.py
pip freeze | grep -iE "google-genai|pydantic|python-dotenv|pytest" > requirements.txt
cat requirements.txt
Expected output (your versions will differ):
google-genai==<resolved version>
pydantic==<resolved version>
python-dotenv==<resolved version>
pytest==<resolved version>
The final layout looks like this, and each later step adds one file:
carbon-lab/
carbonlab/
config.py step 3
client.py steps 4 and 10
usage.py steps 5 and 9
tools.py step 7
schemas.py step 8
cli.py steps 3 to 11
tests/
test_usage.py step 12
test_retry.py step 12
Steps 3 and 4: Load Settings and Make the First Call #
Step 3: Load settings and verify the model ID
Hard-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.
import os
from dataclasses import dataclass
from dotenv import load_dotenv
load_dotenv()
@dataclass(frozen=True)
class Settings:
api_key: str
model: str
fast_model: str
thinking_level: str
daily_budget_usd: float
def load_settings() -> Settings:
key = os.environ.get("GEMINI_API_KEY", "").strip()
if not key:
raise SystemExit("GEMINI_API_KEY is not set. Copy .env.example to .env and fill it in.")
return Settings(
api_key=key,
model=os.environ.get("CARBON_MODEL", "gemini-4-carbon"),
fast_model=os.environ.get("FAST_MODEL", "gemini-4-hydrogen"),
thinking_level=os.environ.get("THINKING_LEVEL", "low"),
daily_budget_usd=float(os.environ.get("DAILY_BUDGET_USD", "2.00")),
)
Now add the first piece of the CLI, a models command that lists what your key can see and flags the two IDs you configured:
import argparse
import sys
from google import genai
from carbonlab.config import load_settings
def visible_models(client) -> set[str]:
return {m.name.removeprefix("models/") for m in client.models.list()}
def cmd_models(args) -> int:
settings = load_settings()
client = genai.Client(api_key=settings.api_key)
names = visible_models(client)
for wanted in (settings.model, settings.fast_model):
flag = "OK " if wanted in names else "MISSING"
print(f"{flag} {wanted}")
gemini4 = sorted(n for n in names if n.startswith("gemini-4"))
print("gemini-4 models your key can see:", ", ".join(gemini4) or "none")
return 0 if settings.model in names else 1
Run it once the CLI entry point exists (the final assembly appears in step 11). Sample output on a paid key:
$ python -m carbonlab.cli models
OK gemini-4-carbon
OK gemini-4-hydrogen
gemini-4 models your key can see: gemini-4-argon, gemini-4-carbon, gemini-4-hydrogen
If 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.
The 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.
Step 4: Send the first request
A 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.
from google import genai
from google.genai import types
def make_client(api_key: str) -> genai.Client:
return genai.Client(api_key=api_key)
def build_config(thinking_level: str, max_output_tokens: int = 1024, system: str | None = None):
return types.GenerateContentConfig(
system_instruction=system,
max_output_tokens=max_output_tokens,
temperature=0.4,
thinking_config=types.ThinkingConfig(thinking_level=thinking_level),
)
def generate(client, model: str, prompt: str, config=None):
return client.models.generate_content(model=model, contents=prompt, config=config)
Wire an ask command into the CLI that prints the answer and a one-line usage summary:
from carbonlab.client import build_config, generate, make_client
def cmd_ask(args) -> int:
settings = load_settings()
client = make_client(settings.api_key)
config = build_config(args.thinking or settings.thinking_level, args.max_tokens)
resp = generate(client, settings.model, args.prompt, config)
print(resp.text)
u = resp.usage_metadata
print(f"\n[prompt={u.prompt_token_count} output={u.candidates_token_count} "
f"thinking={u.thoughts_token_count}]", file=sys.stderr)
return 0
Run it:
python -m carbonlab.cli ask "Explain HTTP 429 in two sentences."
HTTP 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.
[prompt=9 output=52 thinking=118]
The 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.
Step 5: Read Token Usage and Price Thinking Tokens #
Carbon is a reasoning model, and reasoning is not free. Real Python’s explainer on Carbon frames it as a model that thinks longer, and the pricing page analysis from Apidog 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.
That 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.
The usage object returned with every response carries the numbers you need:
prompt_token_countis everything you sent, including the system instruction, the conversation history and any cached content.cached_content_token_countis the part of the prompt served from cache, which bills at the cached rate.candidates_token_countis the visible answer.thoughts_token_countis the hidden reasoning, billed with output.total_token_countis the sum, useful as a cross-check against the other fields.
If 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.
import json
from dataclasses import dataclass
from datetime import date, datetime, timezone
from decimal import Decimal
from pathlib import Path
INTRO_ENDS = date(2026, 11, 30)
LEDGER = Path("usage.jsonl")
@dataclass(frozen=True)
class Rate:
input: Decimal
cached: Decimal
output: Decimal
INTRO = Rate(Decimal("2"), Decimal("0.10"), Decimal("10"))
STANDARD = Rate(Decimal("4"), Decimal("0.20"), Decimal("20"))
class BudgetExceeded(RuntimeError):
pass
def rate_for(day: date) -> Rate:
return INTRO if day <= INTRO_ENDS else STANDARD
def cost_usd(usage, day: date | None = None) -> Decimal:
rate = rate_for(day or date.today())
prompt = usage.prompt_token_count or 0
cached = usage.cached_content_token_count or 0
out = (usage.candidates_token_count or 0) + (usage.thoughts_token_count or 0)
fresh = max(prompt - cached, 0)
total = Decimal(fresh) * rate.input + Decimal(cached) * rate.cached + Decimal(out) * rate.output
return total / Decimal(1_000_000)
def record(model: str, usage, cost: Decimal, label: str = "") -> None:
row = {
"ts": datetime.now(timezone.utc).isoformat(),
"model": model,
"label": label,
"prompt": usage.prompt_token_count or 0,
"cached": usage.cached_content_token_count or 0,
"output": usage.candidates_token_count or 0,
"thinking": usage.thoughts_token_count or 0,
"cost_usd": str(cost),
}
with LEDGER.open("a", encoding="utf-8") as fh:
fh.write(json.dumps(row) + "\n")
def spent_today() -> Decimal:
if not LEDGER.exists():
return Decimal("0")
today = datetime.now(timezone.utc).date().isoformat()
total = Decimal("0")
for line in LEDGER.read_text(encoding="utf-8").splitlines():
row = json.loads(line)
if row["ts"].startswith(today):
total += Decimal(row["cost_usd"])
return total
def check_budget(limit: float) -> None:
spent = spent_today()
if spent >= Decimal(str(limit)):
raise BudgetExceeded(f"Daily budget ${limit:.2f} reached (spent ${spent:.4f}).")
Two 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.
Now call the ledger from ask. Add these lines right after the response comes back:
from carbonlab.usage import check_budget, cost_usd, record
check_budget(settings.daily_budget_usd) # place this BEFORE the generate call
c = cost_usd(resp.usage_metadata)
record(settings.model, resp.usage_metadata, c, label="ask")
print(f"[cost=${c:.6f}]", file=sys.stderr)
A second run now prints a cost line and the ledger grows by one row:
[prompt=9 output=52 thinking=118]
[cost=$0.001718]
Carbon 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.
Step 6: Stream Responses for Faster Perceived Latency #
Reasoning models 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.
def stream(client, model: str, prompt: str, config=None):
last = None
for chunk in client.models.generate_content_stream(model=model, contents=prompt, config=config):
if chunk.text:
yield chunk.text
last = chunk
yield ("__usage__", getattr(last, "usage_metadata", None))
The sentinel tuple is a small hack to carry usage out of a generator without a class. Consume it in the CLI like this:
import time
from carbonlab.client import stream
def cmd_stream(args) -> int:
settings = load_settings()
check_budget(settings.daily_budget_usd)
client = make_client(settings.api_key)
config = build_config(args.thinking or settings.thinking_level, args.max_tokens)
started = time.perf_counter()
first = None
usage = None
for piece in stream(client, settings.model, args.prompt, config):
if isinstance(piece, tuple):
usage = piece[1]
continue
if first is None:
first = time.perf_counter() - started
print(piece, end="", flush=True)
total = time.perf_counter() - started
print(f"\n\n[first token {first:.2f}s, total {total:.2f}s]", file=sys.stderr)
if usage:
c = cost_usd(usage)
record(settings.model, usage, c, label="stream")
print(f"[cost=${c:.6f}]", file=sys.stderr)
return 0
Sample run:
$ python -m carbonlab.cli stream "List three failure modes of retry loops."
1. Retry storms: every client retries at once and amplifies the outage.
2. Non-idempotent calls: a retried POST charges the customer twice.
3. Unbounded attempts: the loop never gives up and hides a permanent error.
[first token 2.84s, total 4.10s]
[cost=$0.002290]
Your timings will differ with thinking level, load and region.
Streaming 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 gives you a fair way to measure it.
One 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.
Step 7: Add Function Calling With a Real Tool #
Tool 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.
Use a tool that ties into this project’s own purpose, a price checker that answers “what would this request cost on a given day?”:
from datetime import date
from types import SimpleNamespace
from carbonlab.usage import cost_usd
def price_check(prompt_tokens: int, output_tokens: int, on_date: str) -> dict:
"""Estimate the Gemini 4 Carbon API cost in USD for one request.
Args:
prompt_tokens: Input tokens sent with the request.
output_tokens: Output tokens including thinking tokens.
on_date: Calendar date in YYYY-MM-DD format used to pick the price period.
"""
usage = SimpleNamespace(
prompt_token_count=prompt_tokens,
cached_content_token_count=0,
candidates_token_count=output_tokens,
thoughts_token_count=0,
)
day = date.fromisoformat(on_date)
return {"on_date": on_date, "cost_usd": str(cost_usd(usage, day))}
Hook it into a tool command:
from google.genai import types
from carbonlab.tools import price_check
def cmd_tool(args) -> int:
settings = load_settings()
check_budget(settings.daily_budget_usd)
client = make_client(settings.api_key)
config = types.GenerateContentConfig(
tools=[price_check],
max_output_tokens=args.max_tokens,
thinking_config=types.ThinkingConfig(thinking_level=settings.thinking_level),
)
resp = client.models.generate_content(model=settings.model, contents=args.prompt, config=config)
print(resp.text)
c = cost_usd(resp.usage_metadata)
record(settings.model, resp.usage_metadata, c, label="tool")
return 0
bash
$ 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."
On 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.
When 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.
Step 8: Force Structured JSON Output #
Free-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.
from typing import Literal
from pydantic import BaseModel
class TicketTriage(BaseModel):
category: Literal["billing", "bug", "feature", "other"]
urgency: int
summary: str
needs_human: bool
python
from carbonlab.schemas import TicketTriage
def cmd_triage(args) -> int:
settings = load_settings()
check_budget(settings.daily_budget_usd)
client = make_client(settings.api_key)
config = types.GenerateContentConfig(
system_instruction="Classify support tickets. Urgency runs from 1 (low) to 5 (outage).",
response_mime_type="application/json",
response_schema=TicketTriage,
max_output_tokens=args.max_tokens,
thinking_config=types.ThinkingConfig(thinking_level="low"),
)
resp = client.models.generate_content(model=settings.model, contents=args.prompt, config=config)
ticket = resp.parsed
if not isinstance(ticket, TicketTriage) or not 1 <= ticket.urgency <= 5:
raise SystemExit(f"Invalid triage output: {resp.text!r}")
print(ticket.model_dump_json(indent=2))
c = cost_usd(resp.usage_metadata)
record(settings.model, resp.usage_metadata, c, label="triage")
return 0
bash
$ python -m carbonlab.cli triage "Our invoice shows two charges for November and the dashboard is down for the whole team."
{
"category": "billing",
"urgency": 4,
"summary": "Duplicate November charge reported alongside a team-wide dashboard outage.",
"needs_human": true
}
Notice 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.
Schema 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.
Step 9: Build the Cost Calculator Before December 1 #
Google’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.
| Scenario | Through Nov 30 | From Dec 1 |
|---|---|---|
| 1 request | Not listed | Not listed |
| 1,000 requests | Not listed | Not listed |
| 10,000 requests | $66.80 | $133.60 |
| 100,000 requests | Not listed | Not listed |
| 10,000 requests, thinking cut to 100 tokens | Not listed | Not listed |
| 10,000 requests, 800 of 1,000 input tokens cached | Not listed | Not listed |
The 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:
from datetime import date
from types import SimpleNamespace
def cmd_cost(args) -> int:
usage = SimpleNamespace(
prompt_token_count=args.prompt_tokens,
cached_content_token_count=args.cached,
candidates_token_count=args.output_tokens,
thoughts_token_count=0,
)
per_req_intro = cost_usd(usage, date(2026, 11, 30))
per_req_std = cost_usd(usage, date(2026, 12, 1))
n = args.requests
print(f"Through Nov 30: ${per_req_intro * n:,.2f} for {n:,} requests")
print(f"From Dec 1: ${per_req_std * n:,.2f} for {n:,} requests")
print(f"Increase: ${(per_req_std - per_req_intro) * n:,.2f}")
return 0
bash
$ python -m carbonlab.cli cost --prompt-tokens 1000 --output-tokens 468 --requests 10000
Through Nov 30: $66.80 for 10,000 requests
From Dec 1: $133.60 for 10,000 requests
Increase: $66.80
If 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 sits far below Carbon’s per-token rates for that narrow job.
One 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.
Steps 10 and 11: Retries, Budget Guards and Cost Controls #
Step 10: Retry transient failures with backoff and jitter
Rate 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.
import random
import sys
import time
from google.genai import errors
RETRYABLE = {429, 500, 502, 503, 504}
def generate(client, model: str, prompt: str, config=None, max_attempts: int = 5, sleep=time.sleep):
delay = 1.0
for attempt in range(1, max_attempts + 1):
try:
return client.models.generate_content(model=model, contents=prompt, config=config)
except errors.APIError as exc:
if exc.code not in RETRYABLE or attempt == max_attempts:
raise
wait = delay + random.uniform(0, delay / 2)
print(f"retry {attempt}/{max_attempts - 1} after HTTP {exc.code}, waiting {wait:.1f}s",
file=sys.stderr)
sleep(wait)
delay = min(delay * 2, 30.0)
The sleep parameter exists so tests can pass a no-op and finish instantly. A failing run prints something like this:
retry 1/4 after HTTP 503, waiting 1.3s
retry 2/4 after HTTP 503, waiting 2.6s
A bad request or a permission error is not retryable. Retrying a bad request wastes time, and retrying a permission error can trip abuse protections.
Two 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.
Step 11: Route cheap work away from Carbon and cap thinking
Three 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.
def pick_model(settings, prompt: str, needs_deep_reasoning: bool) -> tuple[str, str]:
if needs_deep_reasoning or len(prompt) > 4000:
return settings.model, "medium"
return settings.fast_model, "low"
This 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, helps you decide which prompts deserve the premium model.
Finally, assemble the entry point so every command works from one file:
def main(argv=None) -> int:
p = argparse.ArgumentParser(prog="carbonlab")
sub = p.add_subparsers(dest="cmd", required=True)
sub.add_parser("models").set_defaults(fn=cmd_models)
for name, fn in (("ask", cmd_ask), ("stream", cmd_stream), ("tool", cmd_tool), ("triage", cmd_triage)):
sp = sub.add_parser(name)
sp.add_argument("prompt")
sp.add_argument("--max-tokens", type=int, default=1024)
sp.add_argument("--thinking", choices=["low", "medium", "high"])
sp.set_defaults(fn=fn)
c = sub.add_parser("cost")
c.add_argument("--prompt-tokens", type=int, required=True)
c.add_argument("--output-tokens", type=int, required=True)
c.add_argument("--cached", type=int, default=0)
c.add_argument("--requests", type=int, default=1)
c.set_defaults(fn=cmd_cost)
args = p.parse_args(argv)
return args.fn(args)
if __name__ == "__main__":
raise SystemExit(main())
Because 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.
Step 12: Test the Project and Wire It Into CI #
Tests 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.
from datetime import date
from decimal import Decimal
from types import SimpleNamespace
import pytest
from carbonlab import usage
def u(prompt=1000, cached=0, out=171, think=297):
return SimpleNamespace(prompt_token_count=prompt, cached_content_token_count=cached,
candidates_token_count=out, thoughts_token_count=think)
def test_intro_price_last_day():
assert usage.cost_usd(u(), date(2026, 11, 30)) == Decimal("0.00668")
def test_price_doubles_on_dec_first():
assert usage.cost_usd(u(), date(2026, 12, 1)) == Decimal("0.01336")
def test_cached_tokens_use_cached_rate():
cost = usage.cost_usd(u(prompt=1000, cached=800, out=0, think=0), date(2026, 11, 30))
assert cost == Decimal("0.00048")
def test_budget_guard(tmp_path, monkeypatch):
monkeypatch.setattr(usage, "LEDGER", tmp_path / "usage.jsonl")
usage.record("gemini-4-carbon", u(), Decimal("1.50"))
with pytest.raises(usage.BudgetExceeded):
usage.check_budget(1.00)
usage.check_budget(5.00)
python
import pytest
from google.genai import errors
from carbonlab.client import generate
class FlakyModels:
def __init__(self, failures, code):
self.failures, self.code, self.calls = failures, code, 0
def generate_content(self, **kwargs):
self.calls += 1
if self.calls <= self.failures:
raise errors.APIError(self.code, {"error": {"message": "boom"}})
return "ok"
class FakeClient:
def __init__(self, failures, code):
self.models = FlakyModels(failures, code)
def test_retries_then_succeeds():
client = FakeClient(failures=2, code=503)
assert generate(client, "m", "hi", sleep=lambda s: None) == "ok"
assert client.models.calls == 3
def test_does_not_retry_client_errors():
client = FakeClient(failures=1, code=400)
with pytest.raises(errors.APIError):
generate(client, "m", "hi", sleep=lambda s: None)
assert client.models.calls == 1
Run them:
python -m pytest -q
...... [100%]
6 passed in 0.21s
The 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:
name: test
on: [push, pull_request]
jobs:
pytest:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install -r requirements.txt
- run: python -m pytest -q
Store the real key as a repository secret only if you add the live test, and never echo it in a log step.
Common Pitfalls #
Most 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.
| Pitfall | Symptom | Fix |
|---|---|---|
| Hard-coding the model ID | Calls fail after a model is replaced or its access is restricted | Read the ID from .env and run themodels check on startup |
| Ignoring thinking tokens | Bills far above what visible text suggests | Log thoughts_token_count and lowerTHINKING_LEVEL |
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 |
| Budgeting with intro prices after November 30 | Spend doubles on December 1 | Use the dated rate table in usage.py and a daily cap |
| Retrying every error | Wasted calls on client errors that can never succeed | Retry only 429 and 5xx codes, with jitter |
| 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 |
| Committing the key | Surprise charges and a revoked key | Keep .env in.gitignore and rotate any exposed key at once |
A 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.
Troubleshooting #
Work through these in order when something breaks. Most problems resolve in the first three.
- Model not found error for
gemini-4-carbon. Run themodelscommand. 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 updateCARBON_MODEL. - 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.
- 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.
- 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.
- Empty
resp.textorNone. Check the finish reason. A MAX_TOKENS stop usually means thinking consumed the budget. Raisemax_output_tokensor lower the thinking level. A safety block returns no text either, so inspectresp.candidates. resp.parsedisNonein triage. The model returned JSON that failed the schema. Printresp.text, simplify the schema, and keep enum values short and unambiguous.- 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.
- ImportError on
from google import genai. You probably have the legacygoogle-generativeaipackage shadowing the new SDK. Create a clean virtual environment and install onlygoogle-genai. - “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.
- 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.
- 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.
Advanced Tips #
Once the basics run clean, a few upgrades pay off quickly.
-
Count before you send. Call
client.models.count_tokenson large documents and refuse requests whose estimated cost exceeds a per-request ceiling. This catches an accidental 500-page paste before it bills. -
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.
-
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.
-
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.
-
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.
-
Use Carbon for hard problems and say so. Carbon has also been applied to formal verification, as in the report onCarbon checking Signal’s library with Lean proofs , which shows what the long-thinking mode is for. Short chat tasks rarely need it.
-
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. -
Review the ledger weekly. Group rows by
labeland 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. -
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.
For wider context on model releases, benchmarks and safety, browse the AI and machine learning coverage.
FAQ #
How do I get a Gemini API key?
Sign 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.
How much does the Gemini 4 Carbon API cost?
Google 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.
Does the price go up for existing users on December 1?
Google’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.
Is the Gemini API free?
Google’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.
Does a Google AI Ultra or Pro subscription include API access?
Not 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.
Which Python SDK should I use?
Use 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.
Should I use Carbon or Hydrogen?
Use 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.