●FLASH — Gemini 3.8 Flash landed on September 2, tuned for coding and agentic work. It is the third Flash release in roughly six weeks●PRICING — $0.75 in and $3.75 out per million tokens, but only through the end of 2026. Reports put the January 2027 rates at $1.50 and $7.50●BENCH — Terminal-Bench 2.1 climbed from 81.6% on 3.7 Flash to 90.8%. Finance-agent tasks come in at 61.4% and legal at 10.0%●CAVEAT — That 10.0% on legal does beat the 6.7% it is measured against, though both numbers are low in absolute terms. Reporting only the winner would mislead●LIMIT — On the hardest long-horizon software engineering, Opus 5 still leads. The sweet spot here looks like mid-difficulty agentic work, not everything●WORKSPACE — Alongside the model: one-click co-presenters in Meet, new Workspace Studio automation steps, wider Gemini custom instructions, and video summaries in Google Vids●FLASH — Gemini 3.8 Flash landed on September 2, tuned for coding and agentic work. It is the third Flash release in roughly six weeks●PRICING — $0.75 in and $3.75 out per million tokens, but only through the end of 2026. Reports put the January 2027 rates at $1.50 and $7.50●BENCH — Terminal-Bench 2.1 climbed from 81.6% on 3.7 Flash to 90.8%. Finance-agent tasks come in at 61.4% and legal at 10.0%●CAVEAT — That 10.0% on legal does beat the 6.7% it is measured against, though both numbers are low in absolute terms. Reporting only the winner would mislead●LIMIT — On the hardest long-horizon software engineering, Opus 5 still leads. The sweet spot here looks like mid-difficulty agentic work, not everything●WORKSPACE — Alongside the model: one-click co-presenters in Meet, new Workspace Studio automation steps, wider Gemini custom instructions, and video summaries in Google Vids
Writing my first Gemini cost estimate in two columns, one for now and one for January
Introductory pricing for Gemini Flash ends on December 31, 2026, and standard pricing starts on January 1, 2027. Staying on an older generation does not avoid it. Here is the small, working estimator I use to see both prices at once.
I was reworking next year's numbers late one evening, looking at the 0.75 I had typed by hand into the corner of a spreadsheet. The batch jobs I run as an indie developer are all small ones, but the number of them that fire every day keeps growing, and the end-of-month invoice always lands a little outside what I expected.
What I was thinking at that moment was simple. If I don't move up to the newer Flash, I keep this rate.
I opened the migration docs one more time, just to be sure, and my hand stopped. I had read it wrong.
The end of the introductory price is attached to the calendar, not to the model
Google's What's new in Gemini 3.8 Flash states the pricing plainly. The introductory rate is $0.75 per 1M input tokens and $3.75 per 1M output tokens, through December 31, 2026. Standard pricing of $1.50 and $7.50 takes effect on January 1, 2027.
Here is the part I had misread. The same page says that introductory pricing runs through the end of 2026 for Gemini 3.8 Flash, Gemini 3.7 Flash, and Gemini 3.6 Flash. The deadline sits on the calendar, not on the generation you happen to be calling.
Staying put on an older model does not move that date. My assumption — that not upgrading would keep the rate where it was — never held in the first place.
That isn't a hostile design. It was my own assumption. But a spreadsheet built on that assumption will lie to me in January without saying a word, because spreadsheets don't raise their hand.
Estimates break the moment a price is written as a number
For a long time I kept these estimates in a spreadsheet. Type 0.75 into the rate cell, multiply by tokens, read the month. I thought that was enough.
It stopped being enough once I was calling more than one model. The month I started using different models for the classification batch on my wallpaper app and for the first pass of translations on my sites, I could no longer tell what the rate in that cell had been for. A cell holds a number and nothing else. Which date it came from, which model it belonged to, when it was last touched — none of that lives inside the digits.
At first I tried patching around it with a notes column. That did not go well. Notes stop being updated while the number keeps walking around on its own.
So I drew a different line. A price is not a number; it is a record with a start date and an end date attached. Being able to doubt the 0.75 I typed is the whole point. Once the number knows when it is valid, the January calculation comes back into my hands.
What follows is the smallest version of that idea. Python, standard library only.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦You will be able to build one small estimator of your own that keeps working across the day the price changes
✦You will be able to decide when to move models without the false comfort of thinking an older generation keeps your rate frozen
✦You will avoid the long detour of hard-coding a rate and only noticing next January, when the invoice tells you
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
A to of null means "no end has been announced yet." When one is announced, you write the date in and you are done.
Look at the two rows for 3.7 Flash and you can see my misreading laid out as data. Older generation, identical expiry. Once it is a table, there is no room left for the assumption to hide in.
Keeping per_tokens outside the formula is worth the extra line, too. I once mistook a per-1M rate for a per-1K rate and estimated a month at a thousand times its real size. Units belong in the data, not in the arithmetic.
Pass the date in, rather than assuming today
Next, the script that reads the table and adds things up. The point here is that the date you are pricing against comes in from outside. Stop treating today as an implicit default and next year's shape falls out of the same code.
import json, sysfrom datetime import datedef load_prices(path): raw = json.load(open(path, encoding="utf-8")) unit = raw["per_tokens"] # per-1M rate -> per-token rate rows = [] for r in raw["rows"]: rows.append({ "model": r["model"], "start": date.fromisoformat(r["from"]), "end": date.fromisoformat(r["to"]) if r["to"] else date.max, "input": r["input"] / unit, "output": r["output"] / unit, }) return rowsclass PriceMissing(Exception): """No price row covers this date. Keeps us from quietly charging zero."""def price_on(rows, model, on): for r in rows: if r["model"] == model and r["start"] <= on <= r["end"]: return r raise PriceMissing(f"{model} has no price row covering {on}")def total(ledger, rows, on): per_model = {} for line in open(ledger, encoding="utf-8"): rec = json.loads(line) p = price_on(rows, rec["model"], on) cost = rec["input_tokens"] * p["input"] + rec["output_tokens"] * p["output"] per_model[rec["model"]] = per_model.get(rec["model"], 0.0) + cost return per_modeldef main(): rows = load_prices("prices.json") days = [date.fromisoformat(d) for d in sys.argv[1:]] or [date.today()] tables = {d: total("usage.jsonl", rows, d) for d in days} models = sorted({m for t in tables.values() for m in t}) head = "model".ljust(20) + "".join(str(d).rjust(14) for d in days) print(head) print("-" * len(head)) for m in models: print(m.ljust(20) + "".join(f"${tables[d].get(m, 0):.4f}".rjust(14) for d in days)) print("-" * len(head)) print("TOTAL".ljust(20) + "".join(f"${sum(tables[d].values()):.4f}".rjust(14) for d in days))main()
The input, usage.jsonl, is a plain ledger with one line per day. For this article I am running it against five sample days I put together by hand.
Pass two dates: today, and any day after the change.
python3 estimate.py 2026-09-08 2027-01-15
Running it here gave me this.
model 2026-09-08 2027-01-15------------------------------------------------gemini-3.7-flash $1.1325 $2.2650gemini-3.8-flash $1.3800 $2.7600------------------------------------------------TOTAL $2.5125 $5.0250
The ledger is a sample, so the amounts themselves mean nothing. What means something is the shape. The 3.7 Flash row grows by the same factor as the 3.8 Flash row. Staying put is not shelter — and you see that as a table rather than as an argument.
Since I started laying it out this way, I spend far less time agonizing over which generation to run. The gap between the two columns is the same either way, so the comparison moves to where it belongs: tokens per job, and how often a job has to be redone.
Output tokens cost five times what input tokens cost. Carrying that ratio around in your head changes how you feel about designs that produce long answers. In my own work, keeping outputs short paid better than dropping down a model tier.
When the table has a hole, don't quietly bill zero
The real danger in a first estimator is not that the calculation fails. It is that it succeeds with the wrong number.
Suppose you forget the 2027 row for 3.7 Flash. A typical implementation finds no matching rate, contributes zero, and prints a total that looks perfectly plausible. An estimate that came out cheap rarely gets questioned, and you find out when the invoice arrives.
The code above raises instead. Delete one row and run it:
__main__.PriceMissing: gemini-3.7-flash has no price row covering 2027-01-15
It exits with status 1, so anything you have on a schedule stops right there.
Handling a missing price row
What happens now
What happens later
Count it as zero
You get an estimate
Nothing tells you until the invoice does
Skip the line
The total drifts slightly low
No way to tell which lines vanished
Raise and stop
You get no estimate
Fix the table and it is correct from that day on
I have written the swallow-it-quietly version more than once, because stopping felt inconvenient. I have reversed that rule since. Anything missing that touches money gets to make noise.
Feed it the tokens you actually spent
Everything so far ran on a sample ledger. The last piece is an opening for the real numbers: take the usage metadata off the response and append it.
import jsonfrom datetime import datefrom google import genaiclient = genai.Client() # reads GEMINI_API_KEY from the environmentMODEL = "gemini-3.8-flash" # keep the model ID in exactly one placedef ask(prompt: str, ledger: str = "usage.jsonl") -> str: interaction = client.interactions.create(model=MODEL, input=prompt) usage = getattr(interaction, "usage", None) if usage is None: # If it isn't there, record that fact rather than guessing a value record = {"day": str(date.today()), "model": MODEL, "usage": "missing"} else: record = { "day": str(date.today()), "model": MODEL, "input_tokens": getattr(usage, "input_tokens", 0), "output_tokens": getattr(usage, "output_tokens", 0), } with open(ledger, "a", encoding="utf-8") as f: f.write(json.dumps(record, ensure_ascii=False) + "\n") return interaction.output_text
Not writing a zero when the counts are unavailable is a small thing that earns its keep. A zero reads as "spent nothing," and later you cannot tell the two apart. Record the absence instead, and when you reconcile at month end you can see exactly where the counting stopped.
With the deadline sitting on the calendar, the lever you hold is not the rate. It is the volume.
The docs give you two things to work with. Gemini 3.8 Flash is designed to spend more tokens on long, difficult, multi-step work, and you can lower the reasoning effort for everyday tasks. And Gemini 3.7 Flash remains fully supported.
Neither "newer is always better" nor "older is always cheaper" survives contact with that. For short-answer work such as classification I lean on lower effort settings, and I reserve careful attention for the jobs where the output itself runs long — summaries, first-pass translations.
Easy to miss when migrating
How 3.8 Flash treats it
temperature / top_p / top_k
Strip them from the generation config
thinking_budget
Replaced by thinking_level (minimal is unsupported)
candidate_count
Not used from Gemini 3 onward
If you assume the migration is a one-line model string swap, this is where you trip. For the same reason I moved prices into a file, I keep the model ID in one place — so the blast radius of a swap is visible at a glance.
There is one thing worth doing today. Open whichever sheet or script holds your estimate, and add from and to next to the rate. The moment your numbers know when they are valid, January stops being a day you get surprised and becomes a day you simply check.
Thank you for following along with a fairly plain little script. I am still re-reading my own ledger at the end of every month.
Share
Thank You for Reading
Gemini Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.