Agent and AI engineering · Practical technical guide · 18 October 2026
Providers now retire models by routing an old model string to a newer model instead of returning an error. What that does to evaluations, cost and output formats, how to detect and pin, and a migration runbook using the Gemini deprecations of 8 October 2026 as the worked example.
Deprecation used to mean an error; now it can mean a different model
The Gemini API release notes for 8 October 2026 record two deprecations with a new shape. gemini-3.7-flash is deprecated and replaced by gemini-3.8-flash; it can no longer be selected in Google AI Studio, and API requests that name gemini-3.7-flash are automatically routed to gemini-3.8-flash. The same note applies to gemini-3.5-flash, which now routes to gemini-3.6-flash. The deprecations page phrases it precisely: the endpoint is not shutting down and automatically routes API requests to the replacement. The same notes also record a hard shutdown, the deep-research-pro-preview-12-2025 agent, which stops on 23 October 2026, and on 13 October the general availability of Gemini 4 Argon, which many teams will now evaluate as a migration target.
Two regimes therefore coexist. A hard shutdown produces errors on a date you were told about, and the OpenAI deprecations page works the same way, listing a shutdown date and a recommended replacement per model. Automatic re-routing produces no errors at all: your code keeps working, your bill and your outputs change, and nothing in the response tells you unless you look. Both regimes need the same engineering discipline; the second one needs it before the date, because there is no failure to react to afterwards.
What re-routing changes underneath you
| Property | Why it moves when the served model changes | Where it shows up first |
|---|---|---|
| Price per token | Each model has its own pricing table | Monthly invoice, or a per-request cost metric if you have one |
| Token counts | Tokenizers and system prompt handling differ between model generations | Context budget alarms, truncation of long inputs |
| Output format adherence | JSON and schema behaviour improves or shifts; field ordering and null handling change | Parse failures and validation errors downstream |
| Tool calling behaviour | Different propensity to call tools, to call several at once, or to ask questions first | Agent step counts, tool error rates |
| Latency and throughput | Different serving characteristics and default reasoning settings | p95 latency, timeouts, rate limit errors |
| Safety filtering | Filter thresholds and categories are model-specific | Blocked responses in categories that never blocked before |
| Evaluation scores | Every number above feeds the eval; the baseline no longer describes what runs | Nowhere, until you re-run the suite |
The last row is the dangerous one. An evaluation suite that was run against the old model and never re-run is now a historical document. A team that reads a stable dashboard and concludes nothing changed has measured the wrong thing: dashboards track your traffic, not the model behind it.
Detect it: log the served model, not the requested one
Most model APIs echo the identifier of the model that served a request in the response object. Record it on every call next to the string you requested, and alert when the two diverge for the first time. That one comparison turns a silent re-route into an event with a timestamp. The wrapper below is provider-agnostic: an adapter extracts the served identifier, the wrapper counts divergences, and a kill switch lets you fail the call instead of accepting a re-routed response for workloads where that is the right choice.
Illustrative Python; the served identifier field differs per provider and SDK, so read it from your SDK response type.
import time
from collections import Counter
class ModelCall:
"""Wraps one provider client. `served_id` extracts the model id the API echoed."""
def __init__(self, client, served_id, strict=False, on_divergence=None):
self.client, self.served_id = client, served_id
self.strict, self.on_divergence = strict, on_divergence
self.divergences = Counter()
def __call__(self, requested_model, **request):
started = time.monotonic()
response = self.client.generate(model=requested_model, **request)
served = self.served_id(response) or "unknown"
record = {"requested": requested_model, "served": served,
"latency_ms": round((time.monotonic() - started) * 1000)}
if served != requested_model and not served.startswith(requested_model):
self.divergences[(requested_model, served)] += 1
if self.on_divergence:
self.on_divergence(record)
if self.strict:
raise RuntimeError(f"{requested_model} was served by {served}; refusing re-routed response")
return response, recordTreat the first divergence as an incident, not a log line: it means the evaluation baseline for that workload is stale from that moment. Strict mode is for workloads where a wrong-but-plausible output costs more than an outage, for example extraction that feeds financial records. For everything else, let the call succeed and page a person.
Pin what you can, and know what pinning means
Pinning a specific model version buys time, not permanence. The Gemini models page distinguishes stable releases from preview models and lists aliases that move; the deprecations page then gives, per model, a release date and the earliest possible shutdown date, with a note that exact dates are communicated with notice. OpenAI publishes dated snapshots and a deprecations list with shutdown dates. In both cases a pinned identifier protects you from unannounced drift between versions of the same model, and it gives you a date to plan around. It does not protect you from the date itself, and in the re-routing regime the pinned string may be exactly the one that starts answering with a different model. Pin, record the shutdown date in the same configuration file as the model string, and treat that date as a deadline for the migration below.
Re-baseline evaluations before the routing changes
Run the evaluation suite against both the current and the replacement model, on the same inputs, and compare per task. Aggregate accuracy hides the failures that matter: a replacement can score two points higher overall and break one field of a schema every time. Measure at least task score, format validity rate, p95 latency and cost per task, and set thresholds before you look at the numbers.
Illustrative python, checked for syntax; adapt names and storage to your system.
def compare_models(results_a, results_b, max_regression=0.02, max_cost_increase=0.25):
"""results_*: {task_id: {"score": float, "valid_format": bool, "cost": float, "latency_ms": float}}"""
tasks = sorted(set(results_a) & set(results_b))
if not tasks:
raise ValueError("no shared tasks")
worse = [t for t in tasks if results_b[t]["score"] < results_a[t]["score"] - 1e-9]
score_a = sum(results_a[t]["score"] for t in tasks) / len(tasks)
score_b = sum(results_b[t]["score"] for t in tasks) / len(tasks)
valid_a = sum(results_a[t]["valid_format"] for t in tasks) / len(tasks)
valid_b = sum(results_b[t]["valid_format"] for t in tasks) / len(tasks)
cost_a = sum(results_a[t]["cost"] for t in tasks)
cost_b = sum(results_b[t]["cost"] for t in tasks)
report = {"tasks": len(tasks), "score_delta": round(score_b - score_a, 4),
"format_validity": (round(valid_a, 4), round(valid_b, 4)),
"cost_ratio": round(cost_b / cost_a, 3) if cost_a else None,
"regressed_tasks": worse[:20]}
report["blocked"] = (report["score_delta"] < -max_regression
or valid_b < valid_a
or (report["cost_ratio"] or 0) > 1 + max_cost_increase)
return reportThe regressed task list is the useful output: it tells you which prompts to adjust before cutting over, and it is usually short. Keep the comparison in version control next to the model strings, so the next deprecation starts from a known state rather than from memory.
Worked example: an extraction pipeline on a re-routed model
Consider an illustrative team running nightly invoice extraction with a JSON schema on gemini-3.7-flash, with a cost alert on the monthly total and an eval suite last run in July. From 8 October their requests are served by gemini-3.8-flash. What they would see: the cost line moves, in either direction, with no deploy to explain it; a downstream job starts rejecting a small share of records because one optional field now arrives as an empty string instead of being omitted; and their dashboards show nothing, because throughput and error rates are unchanged.
The runbook for them is short. Pin gemini-3.8-flash explicitly so that the configuration matches reality. Re-run the suite on the golden set and read the regressed task list, which points at the optional field. Adjust the schema or the prompt, re-run, and update the cost alert from a monthly total to cost per thousand documents, which would have fired on day one. None of the figures here are measurements; they describe the shape of the problem so you can recognise it.
Migration runbook
- Inventory every model string in configuration, feature flags, fallback chains and notebooks that run in production. The usual count is higher than anyone expects.
- Subscribe to the release notes and deprecation pages of each provider and copy the shutdown or re-routing date next to each model string.
- Add served-model logging and the divergence alert before anything else; it is the only detector for the re-routing regime.
- Run the paired evaluation against the replacement and fix the regressed tasks.
- Cut over with a percentage canary on real traffic, watching format validity and cost per task rather than totals.
- Update fallback chains, which often still point at the retired model, as model fallback in ADK notes.
- Delete the old string everywhere, and record the date you did so.
Failure modes
- The alias pinned in seven places. One is missed and keeps running the retired model through re-routing. The inventory step exists for this.
- An eval set too small to see per-field regressions. If every task is a single number, add format checks and field-level comparisons.
- Cost alerts on totals. A per-token price change hides inside normal traffic variation. Alert on cost per task.
- Prompts that depend on quirks. Workarounds for one model become bugs on the next. The regressed task list finds them.
- Tests that mock the model. They pass before and after. Only the eval suite against the live model sees the change.
Practice and related reading
Further reading:
Sources and scope
Checked against these sources on 18 October 2026. Examples and figures are illustrative, not measurements.