Repository navigation
Description
Description
Thread-title generation runs on a metered model even when the provider offers free ones, because the title-model picker matches on hardcoded family names with no cost awareness. Free models on the same provider can never be selected.
When small_model is unset, SessionContext.selectTitle falls back to Model.small(providerID):
// SessionContext.selectTitle
if (X.model) return yield* Model.get(X.model.providerID, X.model.id); // small_model path
if (!primary) return;
return yield* Model.small(primary.ref.providerID); // fallbackModel.small filters the provider's available models and returns the first whose family matches a hardcoded list:
small: (providerID) =>
available
.filter(m => m.providerID === providerID && m.status === "active" && /* text i/o */)
.flatMap(h => ["gpt-luna","gemini-flash-lite","gemini-flash","claude-haiku"]
.flatMap(f => o.find(m => m.family === f)))
[0]On opencode-go the only family in that list present is gpt-luna, so the title request always lands on gpt-6-luna ($0.10/M input, $0.50/M output) — even though the same provider offers space-bunny-free at $0. That model is unreachable: it has family: null, and longcat-2.5-preview-free has family: "longcat", neither of which appears in the list. So a cost-0 model is never a candidate, regardless of what the provider offers.
Two related problems: the selection has no cost awareness at all, and it is invisible — GET /api/agent reports model: null for the hidden title agent, so the resolved model is never surfaced to the client or TUI.
Model.small has exactly one call site, so this affects the title request only; the compaction and summary agents inherit the session model and are unaffected.
Observed impact: one ~565-token request costing ~$0.000062 about a second after each new session starts, on a model the user never selected, while every call in the session itself is free.
Scope note: the accounting side of this (background LLM request costs missing from session data) is already tracked in #7175, which is now closed. This issue is only about model selection. For reference, in 2.0.18 the title cost does now reach session.cost (observed session total 0.042075 vs sum of message costs 0.042019), but it still has no per-message or per-model attribution.
Steps to reproduce
- Use a provider that exposes both free and metered models (reproduced with
opencode-go:space-bunny-freeat $0,gpt-6-lunaat $0.10/M in / $0.50/M out). - Leave
small_modelunset inopencode.json. - Start a new session using a free model.
- Check the provider-side request log about a second in: the title request is on
gpt-6-luna, while every session call uses the free model.
Workaround: set "small_model": "opencode-go/space-bunny-free". This maps to agents.title.model and applies on config reload without a restart. Verified — the title agent then reports model: {"providerID":"opencode-go","id":"space-bunny-free"}.
Possible fix direction: filter for cost-0 models before applying the family preference list, so a free model wins whenever the provider offers one; alternatively treat the family list as a tiebreaker within the cheapest tier rather than as an absolute filter.
Plugins
None
OpenCode version
2.0.18 (channel: latest)
Screenshot and/or share link
The provider dashboard shows a GPT-6 Luna row ($0.000062, ~565 tokens) about a second after the session start timestamp, interleaved with $0.00 rows for the free model actually in use. No share link — the row is on the provider's own billing page.
Operating System
Ubuntu (kernel 6.8.0-142-fips, x86_64)
Terminal
opencode web UI; host TERM=xterm-256color
Activity
[-]title generation: Model.small picks metered gpt-6-luna over free models, and its cost is unattributed[/-][+]title generation: Model.small selects a metered model even when the provider offers a free one[/+]Kreathyn commentedon Sep 29, 2026
Narrowed the scope and switched to the Bug report template (missing the required Description field).
On the duplicate flag: the bot linked #7175, which is about background LLM request costs not being accounted for in session data. That's a real overlap and I've said so in the issue — that half is already tracked there, so this issue now covers only model selection, which #7175 doesn't address. The two fixes are independent: one is about which model gets picked, the other about where the spend is recorded.
Everything in here was verified locally against a 2.0.18 binary rather than inferred; the two code excerpts are decompiled from
SessionContext.selectTitleand theModel.smallresolver, and the config/workaround behavior was confirmed viaGET /api/agent.github-actions commentedon Sep 29, 2026
Thanks for updating the issue with the Bug Report template, reproduction steps, and system details. It now meets our issue guidelines.
vedmitry7 commentedon Oct 16, 2026
I can reproduce this on OpenCode 1.18.32 with
small_modelunset. The primary model was explicitly set toopenrouter/cohere/north-mini-code:freeforopencode run; no image generation was requested. In one day's local OpenCode log, six new sessions selectedopenrouter/google/gemini-nano-banana-2.1withsmall=true agent=title mode=primary. Four had no stream error in that log; two later failed with OpenRouter's "This request requires at least $1.00 in balance for image or video output" error. All recorded normal assistant messages in those sessions used the free Cohere model. One provider-side generation was confirmed at $0.0008505 (547 input tokens, 4 output tokens, 0 generated images); I cannot attribute the user's roughly $0.04 observed spend to these six attempts from the available records.The selection path in this release is concrete:
SessionPrompt.ensureTitlecallsprovider.getSmallModel(input.providerID)when the hidden title agent has no configured model, falling back to the primary model only if no small model is found.Provider.getSmallModeluses a fixed family priority (gemini-flash,gpt-nano,claude-haikufor OpenRouter), sorts models by descending release date and ID, and picks the first match. It does not compare prices or exclude models with image output. The current local model catalog labelsgoogle/gemini-nano-banana-2.1asfamily: gemini-flash, released 2026-10-06, with both text and image output and nonzero input/output prices. The catalog was refreshed after these calls, so its exact metadata at request time is not independently preserved; the observed choice is consistent with this selection logic.opencode run --modelsupplies the primary model/provider but does not constrain title generation to the selected free model or cost tier. The earlier free Muse Spark title calls used a different primary provider (opencode), so the provider change explains the different fallback path.Title generation constructs its own request, streams only text deltas, and saves the first nonempty line. Two of the four sessions retained their default title; title prompts/responses are not stored as normal session messages, so their exact output and why those two titles were not saved cannot be established from the local DB. Their session totals are $0 despite the provider-side billed generation, making this background spend easy to miss.
Please make automatic title selection cost-aware and modality-aware: for a free primary model, use a suitable free text-output model (or the primary model / skip generation), never silently upgrade a background task to a metered model. Treat family and recency as tie-breakers among eligible models, and preserve explicit
small_modeloverrides. This is a financial risk even when every user-selected task call is free.opencodejr commentedon Oct 28, 2026
Thanks for the detailed write-up, @Kreathyn!
In short, you're asking for two things:
small_modelis unset, the title-generation fallback (Model.small) should consider cost, so a free model on the same provider wins over a metered one.titleagent should be shown to the client (for example inGET /api/agent).Right now
Model.smallintentionally picks the first model from a fixed family preference list (gpt-luna,gemini-flash-lite,gemini-flash,claude-haiku) and doesn't look at pricing. So this is working as currently designed, and I've filed it as a feature request for the maintainers to consider.Until then, the workaround you found is the right one: set
small_modelexplicitly, e.g."small_model": "opencode-go/space-bunny-free".Related: