One Default, Five Answers
Replace repeated model strings with one DEFAULT_MODEL while preserving explicit user configuration and a documented rollback.
By ConnectOnion Team · 4 min read · 667 words
The issue was one sentence: make Gemini 3.7 the default model.
I expected a one-line diff. Find the constant, bump the version, done. So I did what you do — grep for the current default:
grep -rn "co/gemini" --include='*.py' --include='*.yml' .
Fifty-nine lines came back. Not fifty-nine mentions of a constant. Fifty-nine
separate, hand-typed copies of the string co/gemini-3.6-flash, each one
independently claiming to be the default.
Where "the default" actually lived
Once you stop skimming and start reading the hits, they sort into places, and each place is its own small story:
connectonion/core/agent.py— theAgentclass signature. The one everybody would name if you asked "where is the default model defined?"connectonion/llm_do.py— the one-shot call helper, with its own copy in the signature and four more in its docstring.connectonion/core/llm.py— twice, actually:OpenOnionLLMfor the managed route, andGeminiLLMwith the baregemini-3.6-flashfor people bringing their own Google key.connectonion/cli/main.py— theco ai --modeloption default, typed out again inside atyper.Option(...)call.action.yml— the GitHub Action'smodelinput, in YAML, where no Python constant can reach. Plus its twin fallback incli/github_action.py, readingCO_ACTION_MODELwith — you guessed it — another literal.
And that's just the headline five. Keep reading the grep and the default also
lived in the subagent frontmatter loaders (useful_plugins/subagents.py,
subagents/loader.py, cli/co_ai/agents/registry.py), the project
scaffolding that writes .env files for new users
(cli/commands/init.py, create.py, project_cmd_lib.py), the eval plugin's
scoring model, the TUI status bar's example renders, the transcribe helper,
and about forty documentation lines that confidently told readers what the
default was.
Why defaults drift
None of this happened because anyone was careless. It happened because every one of those places was written on a different day, and on each of those days the author did the reasonable thing: they looked at what the default was that day and typed it in. A new plugin copies the current default at birth. Then the default moves on, and the copy doesn't.
We had already paid for this once. The free-models list in this repo used to exist as two copies in two branches of the same auth flow, and the test that guards it now carries the scar in its docstring: "both copies naming the retired model — the shape that has caused most of this release's bugs." Defaults drift for exactly the same reason lists drift. A literal is a snapshot; nobody schedules the snapshot's refresh.
The tell was right there in the grep output. If the repo had one source of truth, the search would have returned one definition and fifty-eight references. It returned fifty-nine definitions.
The fix is a name, not a sed
Running sed 's/3.6/3.7/' would have closed the issue and re-armed the trap.
The next default change would face the same fifty-nine-line grep, minus
whatever new copies had accumulated by then.
So the actual change is one new line in core/usage.py, next to the pricing
and context tables where the other model facts already live:
DEFAULT_MODEL = "co/gemini-3.7-flash"
Agent, llm_do, transcribe, both LLM classes, co ai, and the GitHub
Action fallback now import that name. The next time the default moves, the
diff is the one-liner I originally expected — plus the places that genuinely
cannot import Python and have to stay prose: action.yml, the scaffolded
.env templates, the docs. Those still exist, but they went from
fifty-something down to a handful, and the test suite now asserts that the
three entry points agree with each other, so a partial edit fails loudly
instead of shipping quietly.
Two things deliberately did not change. Anything a user configured
explicitly stays theirs — the defaults only apply when you configured nothing,
and the tests pin that. And co/gemini-3.6-flash stays on the free-models
list, priced and reachable, as the rollback: if 3.7 misbehaves, reverting is
one constant, no user migration.
What changing one number everywhere teaches you is that "everywhere" is the bug. A default you have to change in fifty-nine places isn't a default — it's fifty-nine opinions that currently happen to agree.
ConnectOnion