I expected this to be a one-line change. When Gemini 3.8 Flash went generally available on September 2, the first thing I checked was the price column: $0.75 per 1M input tokens, $3.75 per 1M output. Identical to 3.7 Flash. Nothing to think about, I decided.
Then I read the paragraph underneath. On hard problems, 3.8 Flash works harder by design — it takes smaller reasoning steps, calls tools iteratively, and checks its own answers as it goes. Which means it can spend more tokens getting there.
Same price per token, more tokens. Written out like that it is obvious, but I had been comparing two numbers in a table, and I very nearly missed it.
The unit price and the invoice are two different numbers
The facts first. Gemini 3.8 Flash, 3.7 Flash, and 3.6 Flash all sit on introductory pricing through December 31, 2026, and move to standard pricing on January 1, 2027.
| Item | Introductory (through 2026-12-31) | Standard (from 2027-01-01) |
|---|---|---|
| Input per 1M tokens | $0.75 | $1.50 |
| Output per 1M tokens | $3.75 | $7.50 |
The model ID is gemini-3.8-flash, the default thinking level is medium, the context window is 1M tokens, and max output is 64k. All of that is on the Gemini API page for 3.8 Flash.
What matters here is that the number on your invoice is unit price multiplied by token count. If the price column is frozen across 3.7 and 3.8, then the only thing left that can move is the token count.
What grows is the number of steps the model takes
3.8 Flash lets you pick a thinking level of low, medium, or high. The default is medium, and minimal returns an error — so an old config carried over unchanged will fail on the very first call.
from google import genai
client = genai.Client()
interaction = client.interactions.create(
model="gemini-3.8-flash",
input="Extract only the date, line items, and pre-tax amounts from this invoice PDF as JSON.",
generation_config={
# A fixed-shape extraction, so low is plenty (medium is the default)
# minimal is not supported on 3.8 Flash and will error
"thinking_level": "low"
},
)
print(interaction.output_text)The documentation is notably unhurried about all this. It says you can lower the effort for everyday tasks to cut token consumption, and it says 3.7 Flash remains fully supported for efficiency-first workloads. The announcement post makes the same point.
I read that as "pick by the shape of the work," not "newer is better." As an indie developer running the Lab sites, most of what I actually send through the API is not hard judgment — it is the same transformation repeated at volume. Rough translation passes, article metadata tidying, trimming store listings to a character budget. None of it needs a model that double-checks itself.
Measure output tokens for one week before you switch
An estimate needs a starting number. This does not call for instrumentation work — one extra line on an existing call is enough.
import json
import time
def log_usage(interaction, route: str, path: str = "usage.jsonl") -> None:
"""Append the token counts for a single call. Aggregate later."""
u = interaction.usage # attribute names vary a little between SDK versions
record = {
"ts": time.time(),
"route": route, # always record which path this call came from
"model": interaction.model,
"input_tokens": getattr(u, "input_tokens", None),
"output_tokens": getattr(u, "output_tokens", None),
}
with open(path, "a", encoding="utf-8") as f:
f.write(json.dumps(record, ensure_ascii=False) + "\n")Keep the route field. Decisions like "leave the translation path on 3.7" are impossible without a per-route average. I logged only the model name at first, watched every call collapse into a single mean, and had to start the week over.
Price it twice — once for this year, once for January
Once you have a week of averages, the rest is multiplication. This script prints the monthly cost of staying on 3.7 and of moving to 3.8, under both price regimes.
INTRO = {"input": 0.75, "output": 3.75} # through 2026-12-31
STANDARD = {"input": 1.50, "output": 7.50} # from 2027-01-01
def monthly_cost(calls_per_day, in_tokens, out_tokens, price, days=30):
total_in = calls_per_day * days * in_tokens
total_out = calls_per_day * days * out_tokens
return (total_in * price["input"] + total_out * price["output"]) / 1_000_000
def compare(calls_per_day, in_tokens, out_tokens, growth):
"""growth is how much output tokens expand after moving to 3.8."""
out_38 = out_tokens * growth
rows = []
for label, price in (("introductory", INTRO), ("standard", STANDARD)):
keep = monthly_cost(calls_per_day, in_tokens, out_tokens, price)
move = monthly_cost(calls_per_day, in_tokens, out_38, price)
rows.append((label, keep, move, move - keep))
return rows
CALLS, IN, OUT, GROWTH = 500, 1200, 800, 1.35
for label, keep, move, diff in compare(CALLS, IN, OUT, GROWTH):
print(f"{label}: stay ${keep:.2f} / move ${move:.2f} / delta ${diff:.2f}")
# introductory: stay $58.50 / move $74.25 / delta $15.75
# standard: stay $117.00 / move $148.50 / delta $31.50Take the shape I used above — 500 calls a day, 1,200 input tokens, 800 output tokens — and assume output grows 1.35x on 3.8. The monthly cost goes from $58.50 to $74.25, a difference of $15.75.
Here is the part my intuition got wrong until I ran the numbers. Output growing 1.35x does not make the bill 1.35x. In this shape the total rose by 26.9%, because output only accounts for 76.9% of the bill. A 35% increase multiplied by 76.9% lands at 26.9%.
So the heavier your input side — long context shipped on every call — the less a chattier model moves your total. Flip it around, and a short prompt that produces long prose passes almost the entire increase straight through. "Move to 3.8" is not one decision with one price tag; it is a different decision per route.
The other half is the January boundary. Under standard pricing both columns double, which means the gap doubles too: the $15.75 above becomes $31.50. Deciding purely on "that difference is fine" means making the same decision again when the first invoice of the new year arrives.
I have written up how output tokens propagate into the total before, in what a 17% output reduction actually does to the bill. For handling a price change itself, offsetting the 3.7 Flash increase by moving work to the batch tier is the closer read.
Where I drew the line, and what to strip when you switch
For now my rule is this. Send 3.8 the work that deserves extra steps, and leave the repetitive shapes on 3.7. Multi-stage investigation, or a fix spanning several files — anywhere I actually want the model to verify itself mid-flight — is where 3.8's design earns its tokens. On paths where the answer has a fixed shape, extra diligence barely moves quality and moves the invoice alone.
When you do swap in gemini-3.8-flash, do not carry the old config across. The removal list is short and specific:
- Drop
temperature,top_p, andtop_kfrom the generation config - Replace
thinking_budgetwith the stringthinking_level(minimalis unavailable) - Remove
candidate_count, which Gemini 3 and later do not support
On the sampling parameters, what tripped me up when they were deprecated was not determinism but the diversity side of the behavior. I wrote that down separately in the deprecation that cost me variety, not repeatability.
This week, pick your single highest-volume route and add the output-token log to that one path. Seven days of numbers still leaves plenty of room before December 31.
Thank you for reading. I was the one nearly reassured by two matching numbers in a price table, so if sharing that saves you a surprise, it was worth writing down.