Visitar URL original
feat(tui): estimate session spend from catalog prices when the provider reports $0 by simonsilie · Pull Request #59864 · anomalyco/opencode · GitHub
Skip to content

feat(tui): estimate session spend from catalog prices when the provider reports $0 - #59864

Open
simonsilie wants to merge 3 commits into
anomalyco:devfrom
simonsilie:catalog-cost-estimate
Open

simonsilie wants to merge 3 commits into
anomalyco:devfrom
simonsilie:catalog-cost-estimate

Conversation

@simonsilie

@simonsilie simonsilie commented Oct 28, 2026 •

Copy link
Copy Markdown

Issue for this PR

Closes #59861
Closes #60653

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

Adds an opt-in Cost estimate experiment (experimental.cost-estimate) that displays a catalog-price estimate when the provider reports $0. Subscription-included sessions, such as OpenCode Go, can show ≈ $5.25 spent without changing the authoritative recorded cost.

Root sessions estimate the family total; subagent views estimate only the selected session, matching the existing reported-cost scope. Flat-rate models use accumulated token totals. Context-tiered models price loaded assistant calls at their matching tiers. Remaining usage—including title generation, aggregated compactions, and unloaded history—is estimated at untiered rates rather than treating lifetime totals as one long-context call. Inherited fork messages do not add tier surcharges. No extra history is fetched for the estimate.

Positive reported costs take precedence and skip estimation. If any scoped session lacks catalog pricing, the estimate is omitted rather than displaying a partial family total. Free models remain unlabelled; positive estimates below one cent display ≈ <$0.01. Known approximation: all usage in a session is still priced at its latest model.

How did you verify your code works?

From packages/tui:

bun test --timeout 30000 test/util/session.test.ts test/feature-plugins/sidebar-context.test.tsx test/feature-plugins/prompt-footer.test.tsx --rerun-each 10 --randomize --seed 59864

26 focused cases passed ten randomized runs with seed 59864 (260 passes, zero failures). Coverage includes flat pricing, two context tiers and their boundaries, differently-priced providers/models within a family, partial history, unloaded children, inherited fork messages, root/child scope, missing catalog pricing, sub-cent display, reported-cost precedence without unnecessary estimation, and experiment off.

Before integrating the mini/run footer, bun run check from the repository root and bun run typecheck:tsc from packages/tui passed.

After integrating the mini/run footer from #60659:

bun test --timeout 30000 --only-failures test/mini test/util/session.test.ts test/feature-plugins/sidebar-context.test.tsx test/feature-plugins/prompt-footer.test.tsx

456 tests passed, 2 skipped, zero failures. bun run check and bun run typecheck:tsc were rerun, but could not pass in the current local environment: the full check encounters missing dependencies in @opencode/data-core, and the TUI TypeScript build reports dependency/API errors in unchanged Core and Util files (including missing @effect/sql-sqlite-node and @effect/platform-node/NodeWS).

This experiment applies to the main TUI sidebar, prompt footer, and mini/run footer. The mini/run footer estimates only the current session from session totals and observed assistant steps; a step's reported cost remains unchanged in both experiment modes.

Visual

<svg xmlns="http://www.w3.org/2000/svg" width="240" height="72" viewBox="0 0 240 72">
  <style>
    .t { font: 10px ui-monospace, SFMono-Regular, Menlo, monospace; fill: currentColor; }
    .est { animation: in 3.2s ease infinite; }
    @keyframes in { 0% { opacity: 0; } 25%, 85% { opacity: 1; } 100% { opacity: 0; } }
  </style>
  <text class="t" x="12" y="20" opacity="0.9">Context</text>
  <text class="t" x="12" y="38" opacity="0.55">248,320 tokens</text>
  <text class="t" x="12" y="54" opacity="0.55">25% used</text>
  <text class="t est" x="12" y="70">≈ $5.25 spent</text>
</svg>

Screenshots / recordings

No screenshot attached. Text preview of the sidebar output, verified by renderer assertions:

Context
≈ $5.25 spent

Mini/run footer with the experiment enabled:

Build · gpt-5 · 159.6K (16%) · ≈ $4.23 · ctrl+p menu

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

Added by the opencode bot (AI-generated).

Related: #59634, #46320, #59653, #45417

Public API: adds the opt-in TUI config key experimental["cost-estimate"] (also read by mini/run) and exports calculateCost from @opencode/schema/model (backwards compatible)

@github-actions github-actions Bot added needs:compliance This means the issue will auto-close after 2 hours. and removed needs:compliance This means the issue will auto-close after 2 hours. labels Oct 28, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Thanks for updating your PR! It now meets our contributing guidelines. 👍

@opencodejr opencodejr Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The estimate works as described. With catalog prices on the simulated model and a reported cost: 0, the base shows no money figure, while this PR shows ≈ $X spent in the sidebar and ≈ $X in the footer. The reported cost still wins when it is above 0. The three touched test files pass (13 tests) and tsc -b in packages/tui is clean.

Public API: adds the TUI config key experimental["cost-estimate"] (boolean, opt-in, listed in the Experiments dialog). This is backwards compatible: experimental is already a string-to-boolean record. No SDK, server or plugin API changes.

Concerns:

  • Subagent sessions show the whole family's estimate. session.cost() returns only the child's own cost when the session has a parentID. The estimate always sums family(), which resolves to the root, so a subagent view would show the root's family total. See the inline comments.
  • The pricing math is copied from core's SessionUsage.calculateCost. The copy has already dropped core's finite() guard for malformed catalog rates (#43248). Tier or rate changes in core will not reach the TUI. The client package cannot import from core, but moving the function into @opencode/schema (next to the Model cost schema) would let core and the TUI share one implementation. Otherwise, at least keep the finite() guard.

Related: #59634, #46320, #59653, #45417

Before

After

const data = props.context.data.session
const models = props.context.data.location.model.list(session()?.location)
return costEstimate(
data.family(props.sessionID).map((id) => data.get(id)).filter((info) => info !== undefined),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

data.family() resolves to the root's family, but data.session.cost() returns only the session's own cost when it has a parentID. So on a subagent session this estimate covers the root and every sibling, while the reported figure it stands in for covers only that subagent. To match cost(), estimate just [session()] when session()?.parentID is set.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in b1bcc8a. The sidebar now estimates only the selected session when it has a parentID, while root sessions retain the family total. Regression tests cover a child with its own usage and a zero-usage child, with parent and sibling usage present in both cases.

const estimate = enabled()
? costEstimate(
props.context.data.session
.family(props.sessionID)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same mismatch as in the sidebar: for a child session, cost(props.sessionID) is the child's own cost, but this sums the whole family resolved from the root. A shared helper next to costEstimate that follows cost()'s root/child rule would keep the two call sites consistent.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in b1bcc8a. The footer now follows the same root/child rule as session.cost(): family totals for a root, only the selected session for a child. The parameterized renderer test covers root totals, child-only estimates, and a child with zero usage.

Comment thread packages/tui/src/util/session.ts Outdated
* authoritative (a reported zero is intentional, e.g. subscription billing), so this only fills the
* display gap for sessions whose recorded cost is 0. Tokens price at each session's latest model.
*/
export function costEstimate(sessions: ReadonlyArray<SessionInfo>, models: ReadonlyArray<ModelInfo> | undefined) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This copies core's SessionUsage.calculateCost but drops its finite() guard on catalog rates (added for malformed model costs in #43248), and it will drift from core when tiering changes. Since @opencode/client and the TUI can't depend on core, consider moving the calculation into @opencode/schema beside the Model cost schema so both sides share one implementation.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The duplication is still present; b1bcc8a fixes per-call tier selection and subagent scope, but does not extract the pricing calculation or add the finite() guards. One distinction is that Model.Cost rates use Money.USDPerMillionTokens, which is backed by Schema.Finite, so malformed rates should be rejected at the catalog/API boundary rather than reaching this helper. That does not address the drift concern. I have kept the shared-calculation refactor out of this TUI change for now rather than expanding the schema/core surface, and am leaving this concern outstanding rather than claiming it was fixed.

@opencodejr opencodejr Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The subagent scope is fixed: in both the sidebar and the footer, a child session now estimates only itself, matching session.cost(). The flat-price path still works (≈ $0.90 spent with a reported cost: 0).

The new per-call tier handling hides the estimate for tiered models in practice. With the same simulated model plus a 200k context tier, nothing is shown after a single exchange. Title generation (and compaction) add usage to session.tokens without a matching assistant message, so the completeness check fails as soon as a title has been generated. On root views, one subagent whose messages aren't loaded, or don't add up, also removes the estimate for the whole family. See the inline comment.

The point about the copied pricing math (the missing finite() guard, drift from core) still stands.

Public API: unchanged since the last review. It only adds the opt-in TUI config key experimental["cost-estimate"] (backwards compatible).

After

After

Comment thread packages/tui/src/util/session.ts Outdated
? messages(session.id).flatMap((message) => ("tokens" in message && message.tokens ? [message.tokens] : []))
: [session.tokens]
// Lifetime totals cannot select per-call tiers. Paginated or unloaded history must not produce a partial estimate.
if (usages.reduce((sum, tokens) => sum + TokenUsage.total(tokens), 0) !== TokenUsage.total(session.tokens)) return

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This equality rarely holds in real sessions. Core adds title-generation usage (SessionTitle → recordUsage) and compaction usage to session.tokens, and neither has an assistant message, so after the first exchange the summed message tokens are less than the session total. With a tiered model I get no estimate at all after one prompt. Returning undefined also short-circuits the whole family reduce, so on a root view one child whose messages aren't loaded hides the estimate for everyone. One option is to price the per-message usages by tier and the leftover (session.tokens minus the message sum) at the untiered rate. Another is to fall back to the session total rather than omit the estimate.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ad549b3. The estimate no longer requires message usage to equal session totals. It starts with aggregate usage at untiered rates, then replaces the base charge with the matching tier charge for each loaded assistant call. Title usage, aggregated compactions, and unloaded history stay at base rates, so one unloaded child no longer suppresses the family estimate. Compaction totals are not treated as single long-context calls. Re-review also caught inherited fork messages adding phantom surcharges; those are now excluded from tier adjustments.

Added deterministic regression coverage for these cases, tier/cache boundaries, mixed providers, free models, and reactive UI updates. All 51 focused tests passed ten randomized runs with a fixed seed; package typecheck:tsc and the root check pass. The full TUI run has two failures in unchanged Go-art/spinner rendering tests, both reproduced in isolation; those files were left untouched.

@opencodejr opencodejr Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tiered-model issue is fixed. With the simulated model priced with a 200k context tier and reporting cost: 0, the sidebar and footer now show ≈ $0.90 where the previous head showed nothing. The session total is priced at the untiered rate and tier surcharges are added per loaded reply, so title, compaction and unloaded usage no longer hide the estimate. The subagent scope fix from the previous round still holds. The three touched test files pass (51 tests) and tsc -b in packages/tui is clean.

Public API: unchanged since the last review. It only adds the opt-in TUI config key experimental["cost-estimate"] (backwards compatible).

Still open: the pricing math is a copy of core's calculateCost without its finite() guard; the new tokenCost helper carries the same copy.

New: the tests have grown to about 700 lines for about 100 lines of code (16 cases in session.test.ts, 10 in sidebar-context.test.tsx, several test.each matrices, including a width-parametrised reactivity test). Please cut them down to the core cases: flat price, tier surcharge with partial history, child-only scope, reported cost wins, and experiment off. Small costEstimate unit tests cover the rest more cheaply than renderer tests.

Before

After

@simonsilie

Copy link
Copy Markdown
Author

Addressed the latest review in 40efeba.

Reduced the three TUI test files from 51 cases to 15, removing about 390 net lines. Coverage retains flat pricing, tier surcharges with partial history, child-only scope, reported-cost precedence, and the experiment-off behavior. Partial history, compaction, unloaded children, and inherited fork usage are covered by small costEstimate unit tests rather than repeated renderer matrices.

Moved the pricing calculation into Model.calculateCost in @opencode/schema/model. Core and the TUI now share the same tier selection and finite-rate guards; the duplicated tokenCost helper is gone.

Validation: all 15 focused TUI tests passed 10 randomized runs (seed 59864); the three existing core pricing tests, including malformed rates, passed; TUI typecheck:tsc and root bun run check passed.

@opencodejr opencodejr Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The duplicated pricing math is resolved. Model.calculateCost in @opencode/schema now holds the single implementation, with the finite() guard. Core's SessionUsage.calculateCost re-exports it and the TUI estimate uses it, so the tier surcharge is just the difference between the tiered and untiered price. The trimmed tests are a reasonable size now. I re-ran the tiered-model setup in the TUI (≈ $0.90 shown, nothing on the base), the touched TUI tests and core's session-usage/session-step tests (all pass), and tsc in tui, schema and core (clean).

Public API: adds the opt-in TUI config key experimental["cost-estimate"] and a new export, calculateCost from @opencode/schema/model. SessionUsage.calculateCost keeps its signature. Both are additions, so this is backwards compatible.

No further concerns.

Before

After

@opencodejr opencodejr Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This push only merges dev; the PR's own changes are identical to the version I last reviewed. The touched TUI tests and core's session-usage tests pass on the new head, and tsc -b in packages/tui is clean.

Public API: unchanged since the last review. It adds the opt-in TUI config key experimental["cost-estimate"] and exports calculateCost from @opencode/schema/model; both are backwards compatible.

No further concerns.

@opencodejr opencodejr Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This push rebases the change onto a newer dev as one commit. The source changes are identical to the version I last reviewed; only the footer test fixtures were adjusted for the new base. The touched TUI tests and core's session-usage tests pass, tsc is clean in tui and schema, and the TUI still shows ≈ $0.90 spent for a tiered model reporting cost: 0.

Public API: unchanged since the last review. It adds the opt-in TUI config key experimental["cost-estimate"] and exports calculateCost from @opencode/schema/model; both are backwards compatible.

No further concerns.

After

@augustovicente augustovicente left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rebased head 1a082e65 has the same source as the version I reviewed before (only the base moved). The math holds up against core: projectFork zeroes cost/tokens and copies parent time_created, so pricing new fork calls at their own tier and skipping inherited messages is correct, and root/child scoping matches session.cost(). No blockers.

Before merge:

  1. The description is stale. It still says "51 focused tests ... 510 passes" and claims mixed-provider coverage, but after the trim the three files hold ~16 cases (5 in util/session.test.ts, 6 in sidebar-context.test.tsx, 5 in prompt-footer.test.tsx), and the only provider case asserts an unmatched provider returns 0. Please re-run the stated command and update the numbers, or drop the claim.

  2. Coverage gaps worth closing at unit level: a model with more than one context tier, a family containing two differently-priced models, and footer cases for experiment-off and reported-cost-wins (the sidebar already has both).

  3. Scope: mini/run mode doesn't get the estimate. Its footer only reads usage.cost from the per-step event, and RunTuiConfig doesn't expose experimental. I think mini should get it too (see inline).

Nits, not blocking:

  • costEstimate returns a bare number while the rest of the spend path uses Money.USD; accumulating branded money would keep the unit explicit at the call sites.
  • Model.calculateCost is now permanent public API of a published package for a temporary TUI experiment. Fine if intentional — just worth calling out.

Comment thread packages/tui/src/util/session.ts Outdated
(candidate) =>
session.model && candidate.providerID === session.model.providerID && candidate.id === session.model.id,
)
if (!model) return total

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When a session's model isn't in the catalog this drops it from the total and still renders ≈ $X. For a root the caller passes the whole family, so a single child on a custom or removed model silently under-reports the family estimate while the ≈ implies the figure is complete. Consider returning undefined and rendering nothing when any session in scope is unpriced, or state explicitly that only catalog-priced sessions are included.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 169fe04. costEstimate now returns undefined if any scoped session has no matching catalog model or has an empty price list, and both views omit the estimate. Regression coverage includes a priced root with an unpriced child; explicitly free models still return zero.

const spent = createMemo(() => {
if (cost() > 0) return `${money.format(cost())} spent`
// The tilde marks the catalog-price figure as an estimate; the recorded cost stays authoritative.
return estimate() > 0 ? `≈ ${money.format(estimate())} spent` : undefined

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

estimate > 0 still renders ≈ $0.00 spent for sub-cent sessions (anything under ~$0.005), so an active session can look free. Same path in the footer (prompt/footer.tsx:63). Worth a floor — <$0.01, or hide below a cent.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 169fe04. Positive estimates below one cent now display ≈ <$0.01 in both the sidebar and prompt footer, with renderer coverage for each. Reported costs keep their existing formatting.

session.revert?.messageID,
)
const cost = props.context.data.session.cost(props.sessionID)
const estimate = enabled()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The estimate is computed before the cost > 0 check on line 63, so every root with a reported cost now walks the family — and for tiered models, every message — on each status tick, only to throw the result away. Moving it behind cost > 0 matches the sidebar, where the memo is only read on the else branch.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 169fe04. The footer skips estimation when reported cost is positive. The sidebar now has the same explicit guard: Solid memos evaluate eagerly, so only conditionally reading the memo did not prevent that work. The footer regression fixture rejects family-session reads when the estimate is unused.

Comment on lines +28 to +32
{
id: "cost-estimate",
title: "Cost estimate",
description: "Estimate session spend from model catalog prices when the provider reports $0",
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the experiment on, mini/run mode still shows nothing for providers that report $0. mini/footer.view.tsx:217 computes cost from usage()?.cost, which comes from the per-step session.step.ended event (stream-v2.transport.ts maps event.data.cost || undefined), so a reported zero is indistinguishable from "cost not reported" and the line disappears. I think mini should get the same estimate as the main footer — otherwise the feature is invisible there.

Two things it would need that aren't there today:

  • The flag isn't exposed to mini: RunTuiConfig in mini/types.ts picks a fixed set of config fields and doesn't include experimental, so the experiment reads as off.
  • state.usage is per-step, not session totals. The estimate needs the session's accumulated tokens/model (a session.get, which the runtime already does elsewhere); per-call tiers would also need the loaded messages. And decide whether mini estimates just the current session or the family — the main footer gets the family for free from the client store, while mini would have to list children.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keeping mini/run support outside this PR. Matching the main TUI would require accumulated session/family usage, catalog prices, experiment configuration, and a history-loading policy rather than using the existing per-step usage. The PR description now explicitly limits this experiment to the main TUI sidebar and prompt footer; mini/run remains unchanged.

)
}

const finite = (value: number) => (Number.isFinite(value) ? value : 0)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: finite is declared after the function that uses it. It's safe today (function declaration + call-time lookup), but moving it above calculateCost avoids a TDZ trap if this module ever gains top-level evaluation.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving this unchanged. There is no top-level call before finite is initialized, and the repository convention puts supporting helpers below the exported function. A future top-level caller would need to respect initialization order, but there is no current TDZ failure to fix.

...pricing,
cost: [
...pricing.cost,
{ tier: { type: "context", size: 200_000 }, input: 6, output: 22.5, cache: { read: 0.6, write: 7.5 } },

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tiered fixture only has one tier, so the toSorted + "largest matching tier" path and the enclosing-tier boundary aren't exercised. A second tier (e.g. size: 400_000) would cover the selection logic that the per-call surcharge depends on.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in 169fe04: the fixture now has 200k and 400k context tiers, with cases exactly at and immediately above both thresholds. The above-400k case requires selecting the largest matching tier rather than the first match.

@simonsilie

Copy link
Copy Markdown
Author

@augustovicente Addressed in 169fe04 and replied to each inline thread. The PR description now reflects the current scope and verification: 26 focused cases passed ten randomized runs (260 passes), and root checks plus TUI typecheck:tsc passed. Added the two-tier boundaries, differently-priced family models, and footer experiment-off/reported-cost-wins coverage. costEstimate now accumulates branded Money.USD and returns undefined for incomplete catalog pricing.

Mini/run remains outside this PR because it needs separate accumulated session/family usage plumbing; that limitation is explicit in the description. The finite ordering is unchanged because there is no current initialization-order issue. Keeping Model.calculateCost as shared public API is intentional: core also uses it independently of the TUI experiment, so abandoning the experiment would not make that function obsolete.

@opencodejr opencodejr Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new commit works as described. The estimate is only computed when the recorded cost is 0, a family with any session lacking catalog pricing shows no estimate instead of a partial total, and amounts under a cent display as ≈ <$0.01. The touched TUI tests pass (26), tsc -b in packages/tui is clean, and the TUI still shows ≈ $0.90 spent for a tiered model reporting cost: 0.

Public API: unchanged since the last review. It adds the opt-in TUI config key experimental["cost-estimate"] and exports calculateCost from @opencode/schema/model; both are backwards compatible.

No further concerns.

After

@augustovicente augustovicente left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@augustovicente

augustovicente commented Oct 30, 2026 •

Copy link
Copy Markdown

Follow-ups for the mini/run footer, which this PR leaves out of scope: issue #60653 and PR #60659, stacked on this branch. I'll rebase it onto dev once this merges.

@simonsilie
simonsilie force-pushed the catalog-cost-estimate branch from 169fe04 to b227b3c Compare November 13, 2026 06:26

@opencodejr opencodejr Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This push rebases the change onto a newer dev; the PR's own changes are identical to the version I last reviewed. On the rebased head, the touched TUI tests (26) and core's session-usage tests pass, tsc -b in packages/tui is clean, and the TUI still shows ≈ $0.90 spent for a tiered model reporting cost: 0.

Public API: unchanged since the last review. It adds the opt-in TUI config key experimental["cost-estimate"] and exports calculateCost from @opencode/schema/model; both are backwards compatible.

No further concerns.

After

@simonsilie

Copy link
Copy Markdown
Author

@augustovicente Thanks! Integrated your prepared commit via fast-forward and pushed it as 1e6f074. Updated the PR description to include mini/run mode and Closes #60653.

@opencodejr opencodejr Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The mini/run footer addition reads correctly. Mini now gets the experimental config and the model catalog. It prices the current session from its server totals, keeps them updated from session.usage.updated and session.model.selected, and counts each session.step.ended once (keyed by event ID) for tier surcharges, all through the shared costEstimate. So a reported cost wins, an unpriced session shows no estimate, and amounts under a cent show ≈ <$0.01, matching #60653's acceptance criteria. I couldn't drive mini mode here (the drive harness only starts the full TUI), so this is checked by tests: test/mini plus the cost-estimate tests pass (458, 2 skipped), and tsc -b in packages/tui is clean. A short recording of the mini footer with the experiment on would help maintainers.

Public API: unchanged since the last review. It adds the opt-in TUI config key experimental["cost-estimate"], now also read by mini/run mode, and exports calculateCost from @opencode/schema/model; both are backwards compatible.

No further concerns.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

tui: show the cost estimate in the mini/run footer tui: optional catalog-price estimate for providers that report $0 session cost

2 participants