Skip to content

feat(cost): display authoritative models.dev cost_usd in desktop session header #7

Open

Description

@dezren39

Context & Background

In OpenCode upstream commit 7b33bcea4e (feat(desktop): show session tokens and cost on the session view (#53785)), upstream introduced a UI display in the session view header showing accumulated tokens and estimated cost for the active session.

In our fork (developing-today/opencode), we calculate exact per-request USD costs derived from the official models.dev token catalog pricing and store it directly in the route_attempt table under column cost_usd (migration 20261014094820_route_attempt_cost_usd.ts). Currently, upstream's new desktop session cost view computes a rough estimate or reads client-side estimates rather than our authoritative server-calculated database value.

Feature Objective

Wire our authoritative cost_usd database metric from route_attempt directly into OpenCode's session metadata and the desktop session view, ensuring that users see exact dollar costs calculated from the official models.dev pricing snapshot.


Technical Specifications

1. Aggregate Session Cost in Session Service

  • In packages/core/src/session.ts or packages/core/src/session/generate.ts:
    • When returning or subscribing to SessionInfo / session state:
    • Query total cost_usd by summing all attempts for session_id in route_attempt:
      SELECT COALESCE(SUM(cost_usd), 0.0) AS total_cost_usd
      FROM route_attempt
      WHERE session_id = ? AND outcome = 'success';
    • Populate session.cost with this exact value (formatted in USD currency, e.g. $0.042).

2. Protocol & Client Contract

  • In packages/protocol/src/groups/session.ts and packages/schema/src/session.ts:
    • Ensure SessionInfo.cost is typed as a number (USD) and documented as authoritative models.dev computed cost.
    • When new LLM attempts finish in publish-llm-event.ts, emit an updated session telemetry event carrying the latest total cost_usd.

3. Desktop Session UI Component

  • In packages/app/src/session/ (or packages/desktop/src/ where #53785 renders):
    • Ensure the header component formats the authoritative cost_usd value:
      • Values < $0.01: <$0.01
      • Standard values: $0.04, $1.28
    • On hover, provide a tooltip breaking down input token cost, output token cost, and cache savings.

Key Files & References

  • packages/core/src/database/migration/20261014094820_route_attempt_cost_usd.ts: cost_usd column definition
  • packages/core/src/model-usage.ts & model-plan.ts: Token catalog pricing and cost calculation formulas
  • packages/core/src/session/runner/publish-llm-event.ts: Emitting session cost updates
  • packages/desktop/ and packages/app/src/session/: Session header view components

Acceptance Criteria

  1. The session view header displays the exact sum of cost_usd recorded for that session in route_attempt.
  2. As new turns complete, the displayed dollar cost updates in real-time.
  3. Pricing matches official token pricing from the bundled models.dev catalog.
  4. Unit tests in packages/core/test/ verify session cost aggregation from multiple attempts.

Activity

  1. added
    kind/featureNew feature or capability request
    source/humanFiled directly by a human
    priority/normalOrdinary priority (the default)
    effort/mMedium: expected under 1 day of work
    app/corePackage: core (pkgs/ or apps/ in this repo)
    meta/enrichedIntake review done: acceptance criteria, links, and normalized labels added
    on Oct 27, 2026
  2. dezren39 commented on Oct 27, 2026

    @dezren39
    Author

    Intake review

    Still live (verified this run): no aggregation of the authoritative route_attempt.cost_usd into session metadata exists in packages/core/src (no SUM(cost_usd)-style query found), so the desktop session header still cannot be showing the server-calculated figure the filing asks for. The cost_usd column itself is live and written per attempt (it is the same column opencode #549's observer-retryable finding concerns).

    Related: opencode #8 (continuation cost stitching — the rollup semantics there affect what a session total should include), opencode #9 (cache savings reporting shares the route_stats surface).

    Notes / risks: the smallest of the five opencode feature specs in this batch (effort/m): one aggregate query, one field on session info, one header binding. Risk is mostly definitional — session total should be the sum over all attempts including continuations and retries, and the UI should say so.

    Suggested approach: add the aggregate in the session service, expose it on SessionInfo, bind the desktop header to it, and cover the sum with a core test using seeded route_attempt rows.

    — fleet monitor intake (repo-fleet-monitor, 2026-10-26 20:19 CDT run)

  3. dezren39 commented on Oct 28, 2026

    @dezren39
    Author

    🤖 Agent note · triage confirmed — real gap, decomposes into two packets (not one small fix)

    • Agent: repo-worker-opencode (Lane B, job repo-queue-worker-shift-opencode-b)
    • Session / ID: task 2032, shift 2026-10-28 14:21 CDT
    • Verdict: CONFIRMED at current main (b0633cc697 lineage). The desktop session header does not display the authoritative route_attempt.cost_usd sum, and no existing surface carries that exact number into the session view. Code evidence:
      • route_attempt.cost_usd exists and is written per attempt: migration packages/core/src/database/migration/20261014094820_route_attempt_cost_usd.ts; write site packages/core/src/session/runner/step.ts:419 (cost_usd: SessionUsage.calculateCost(input.model.cost, tokens)).
      • Session.Info.cost exists (packages/schema/src/session.ts:48, Money.USD) but is a different number: it is accumulated by the session projector from per-step LLM-event costs (packages/core/src/session/projector.ts:62, cost = session.cost + event.cost), i.e. the event-carried estimate the issue describes — not a SUM(route_attempt.cost_usd). Other consumers depend on this field (e.g. compaction spend, packages/core/src/session/compaction.ts:354), so silently re-sourcing it is a design decision, not a patch.
      • The authoritative aggregate exists only in route-log stats: packages/core/src/model-route-log.ts:656 (costUsd: coalesce(sum(cost_usd),0) in the shared usageColumns) and sessionUsage at :659 aggregates it per session — but (a) it is exposed only to route-usage/stats surfaces, not to Session.Info or the app, and (b) its costUsd sums all outcomes, while this issue's spec sums outcome = 'success' only — the filter is part of the work.
      • No header display exists to correct: packages/app/src/session/header/session-header.tsx and session-header-actions.tsx contain no cost or token rendering at HEAD (grep: zero hits) — upstream feat(desktop): show session tokens and cost on the session view anomalyco/opencode#53785's header display is not present in this fork's app header, so the UI piece is build-new, not re-wire.
    • Routing: confirmed but NOT routable as a single small_fix packet — it is effort/m across two surfaces with one genuine design choice. No follow-up packet enqueued from this triage; the decomposition for the producer is:
      1. Core/protocol packet: expose the authoritative per-session cost — decide between a new Session.Info field (e.g. costAuthoritative) sourced from a success-only route_attempt sum vs re-sourcing Info.cost (must audit projector/compaction consumers first); success-only aggregation can extend ModelRouteLog.sessionUsage's columns or a session-scoped query; add the aggregation tests this issue's criterion 4 asks for in packages/core/test/.
      2. App packet: build the header cost display in packages/app/src/session/header/ consuming that field, with the issue's formatting rules (<$0.01 under a cent, $0.04/$1.28 standard) and the hover breakdown; live update follows the session-info subscription once packet 1 emits it.
    • Links: issue feat(cost): display authoritative models.dev cost_usd in desktop session header #7
  4. dezren39 commented on Oct 30, 2026

    @dezren39
    Author

    🤖 Agent note · picked up

  5. dezren39 commented on Oct 30, 2026

    @dezren39
    Author

    Triage verdict: parts 1–2 LANDED (by a different mechanism), part 3 (header UI) remains — routed as task 3778

    Verified on main 3a1c3a0c78 (2026-10-30, issue-sweep-rotating):

    Part 1 — aggregate session cost: LANDED, no SQL SUM needed.

    • SessionInfo.cost exists: packages/schema/src/session.ts:48 (cost: Money.USD), served from the session row at packages/core/src/session/info.ts:37.
    • It is accumulated by the session projector from per-step LLM event costs (packages/core/src/session/projector.ts:62,723).
    • Those event costs are the same authoritative figure this issue wants: packages/core/src/session/runner/step.ts:663 computes the event cost and :489 computes route_attempt.cost_usd, both as SessionUsage.calculateCost(input.model.cost, tokens) — identical catalog pricing, two accumulations. A SUM(route_attempt.cost_usd) would add nothing (and would miss non-runner cost events such as compaction, packages/core/src/session/compaction.ts:658, which also feed session cost).

    Part 2 — protocol & client contract: LANDED.

    • The client already exposes a family selector: packages/client/src/solid/data.ts:1505-1513 — cost(sessionID) returns the session's own cost for a subsession, or the root family's summed cost (root + subsessions).

    Part 3 — session header rendering: NOT landed.

    • Nothing in packages/app calls that selector or renders session cost: grep for a .cost( call in packages/app/src = 0 hits. Every cost reference in the app is model catalog pricing (providers/models), not session spend.

    Routing: the UI-only remainder is enqueued as queue task 3778 (repo-health-opencode-pilot, small_fix, packet hidden_files/packet-7-fix.json): render the family cost in the session header via the existing selector, <$0.01 under a cent, $X.XX otherwise, reactive as steps land. The tooltip breakdown from the original spec is a stretch goal in the packet only if the data is already in the client store — no new protocol surface.

    🤖 Agent note · triaged

  6. 21 remaining items

  7. dezren39 commented on Nov 4, 2026

    @dezren39
    Author

    🤖 Agent note · picked-up

    • Agent: repo-worker-opencode (OpenChamber scheduled lane)
    • Session / ID: lane 3fe37e8a-c61b-419c-9e97-769ecd264d5d, task 3778
    • Task: Revalidate the focused session-header cost feature before selecting a safe action.
    • Expectation: Preserve the prior resume brief; no competing PR or hard-stop label found.
    • Links: feat(cost): display authoritative models.dev cost_usd in desktop session header #7 · prior verified resume note from 2026-11-04}
  8. added
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    status/parkedDeliberately paused with a recorded resume condition
    and removed
    queue/claimedA queue worker has claimed the task for this item
    on Nov 4, 2026
  9. dezren39 commented on Nov 4, 2026

    @dezren39
    Author

    🤖 Agent note · parked

    • Agent: repo-worker-opencode (OpenChamber scheduled lane)
    • Session / ID: lane 3fe37e8a-c61b-419c-9e97-769ecd264d5d, task 3778
    • Task: Parked for four hours for a dedicated focused implementation session.
    • Why: This requires resolving the success-only route-attempt sum versus projector Session.Info.cost, building a core test harness, then adding UI, tooltip, and live-update coverage. The scheduled lane cannot safely finish that scope; no code edits or worktree were started.
    • Resume: Use the verified checkpoint and the prior issue resume brief; first revalidate main and ownership, then create one dedicated task worktree.
    • Links: https://github.com/developing-today/opencode/issues/7}
  10. added
    queue/claimedA queue worker has claimed the task for this item
    and removed
    status/parkedDeliberately paused with a recorded resume condition
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    on Nov 4, 2026
  11. dezren39 commented on Nov 4, 2026

    @dezren39
    Author

    🤖 Agent note · blocked — packet_defect (task 3778)

    • Agent: repo-worker-opencode (event-hook worker, lane opencode-a)
    • Session / ID: task 3778, run 2026-11-04 18:54Z; worktree ~/git/fleet/3778-7-cost-header created from origin/main cd60d2db90, no code edits made
    • Verdict: The packet cannot be implemented as written; three material defects, verified on current main:
      1. Accounting rule conflict. The packet pins SUM(route_attempt.cost_usd WHERE outcome='success'), the issue's own intake review says the total should be "the sum over all attempts including continuations and retries", and the current projector total additionally includes compaction spend (packages/core/src/session/compaction.ts, UsageRecorded with no route_attempt row). These are three different totals; the packet must name one and state what happens to the other consumers of Session.Info.cost (e.g. compaction spend).
      2. Tooltip data is not in the contract the packet forbids changing. Session.Info (packages/schema/src/session.ts) carries only total cost: Money.USD plus token counts — no input-token cost, no output-token cost, and no cache_savings_usd (that column exists only on route_attempt, aggregated only in packages/core/src/model-route-log.ts for stats surfaces). The required hover breakdown (input cost / output cost / cache savings) therefore needs either a schema/protocol addition — which the packet explicitly excludes — or client-side repricing, which the issue itself rejects as the non-authoritative estimate.
      3. Live-update mechanism undefined if re-sourced. Session.Info.cost updates today because the projector accumulates UsageRecorded events and publishes UsageUpdated. Route attempts are written directly in the runner with no corresponding event, so a SQL-sum source has no defined trigger to refresh the header "as new turns complete" (acceptance 2).
    • Needed to unblock: a revised packet that (a) picks the accounting rule and its consumer impact, (b) either lifts the schema/protocol exclusion so a breakdown can be exposed or drops/redefines the tooltip, and (c) names the refresh trigger for a re-sourced total.
    • Links: feat(cost): display authoritative models.dev cost_usd in desktop session header #7
  12. added
    queue/blockedQueue task is blocked (reason recorded on the task)
    status/blockedBlocked on something named in the issue/PR
    and removed
    queue/claimedA queue worker has claimed the task for this item
    on Nov 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    app/corePackage: core (pkgs/ or apps/ in this repo)effort/mMedium: expected under 1 day of workkind/featureNew feature or capability requestmeta/enrichedIntake review done: acceptance criteria, links, and normalized labels addedpriority/normalOrdinary priority (the default)queue/blockedQueue task is blocked (reason recorded on the task)source/humanFiled directly by a humanstatus/blockedBlocked on something named in the issue/PRtask/triageQueue packet kind: verify, reproduce, and route this itemwork/assignedAn agent holds this item via the queue (claimed task)

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions