Skip to content

feat(desktop): add editable Runtime Host-backed pricing settings #2015

Description

@me2seeks
English

Goal

Turn the existing Settings → Usage → Pricing tab into an editable, Runtime Host-backed price-override editor. It should show the prices Maka will use for newly activated model work, distinguish bundled prices from user overrides, and preserve the Host as the only pricing authority.

This keeps Pricing next to the cost data it explains without adding another top-level Settings destination. When the Pricing tab is active, the Usage range/summary controls should not imply that prices are time-scoped; show Pricing-specific copy and refresh controls instead.

Current code facts

  • The current Pricing tab is disconnected, not merely read-only. It renders UsageStats.pricing, while readUsageStats() currently returns pricing: [] unconditionally.
  • The Runtime Host authority implementation owns persisted overrides and one root-scoped revision shared by all pricing keys in runtime.sqlite. pricing.query returns revision-pinned override pages; pricing.mutate performs CAS upsert/delete and returns committed, unchanged, or revision_conflict. This is not yet the active Desktop production path before M5.
  • Runtime cost settlement uses override ?? builtin. The bundled table lives in Runtime. When pricing resolves, a completed ModelCallAttempt freezes the resolved costUsd and pricingRates; otherwise its cost basis remains unpriced.
  • Execution snapshots effective pricing when an AiSdkBackend is activated. A mutation refreshes idle backends, but an active Run keeps its existing snapshot until it settles; this issue does not introduce mid-Run repricing.
  • pricing.query currently has no production client. The Desktop adapter tracked by [Tracking] Runtime Host M4 Desktop-first client adapters #2010 is still planned, and Desktop production still has an embedded Runtime owner until the atomic M5 cutover governed by feat(runtime): RFC - one Runtime Host for every interactive Maka surface #853.
  • Open PR refactor(desktop): remove unused Usage preload bridge #2038 proposes to remove the unused speculative maka.usage preload bridge while retaining the temporary Main handlers. This feature must not restore that bridge or treat those handlers as its long-term contract.

Product behavior

The first version lists this exact key set:

bundled pricing keys ∪ persisted override keys

It does not join the Provider model catalog yet. Users can add an exact model key manually.

Example:

Pricing                                  [Refresh] [Add price]
USD per 1M tokens. Applies to newly activated model work; an active run keeps its starting prices.
Historical costs are not recalculated. Provider billing is authoritative.

Model key                    Source                  Input  Output  Cache read  Cache write  Action
openai:gpt-4o                Built-in                $2.50  $10.00  $1.25       Not set      Customize
anthropic:claude-sonnet-4-5  Custom · has fallback  $2.00  $12.00  $0.30       $3.75        Edit · Reset
acme:coder-v2                Custom-only             $0.80  $2.40   Not set     Not set      Edit · Delete
  • Editing a Built-in row creates a complete override and changes its source to Custom.
  • Editing a Custom row updates that override.
  • Resetting a Custom row with a bundled fallback deletes the override and restores Built-in pricing.
  • Deleting a Custom-only row removes the override; newly activated model work becomes unpriced for that key, not $0, while work already active keeps the snapshot it started with.
  • Provenance comes from override presence, not value equality. An override numerically equal to the bundled value is still Custom.
  • The row and confirmation copy must make the destructive outcome explicit: reset restores a bundled value, while deleting a Custom-only row leaves newly activated model work unpriced; work already active keeps its starting snapshot.
  • An omitted cache rate and an explicit 0 must remain distinct in the form. The current cost calculator treats an omitted cache rate as no cache charge, so show copy such as Not set (no cache charge in Maka estimates) instead of an ambiguous dash.
  • Display formatting must not become stored input. Editing must round-trip the canonical value without losing precision, and a positive rate must never be rendered as $0. The form may use a friendlier display format only while preserving the exact underlying value.
  • The manual model-key field explains that this is the exact Runtime lookup key and shows existing examples. For a Custom-only model, users must be able to copy the exact key from an unpriced Host-backed Usage or Inspector call fact; use the canonical providerId and modelId, never the connection slug. A shared projection/helper should own key construction so the renderer does not guess the lookup rule. A direct prefilled deep link is a later convenience, not a prerequisite for obtaining the key.

Host read model

Prefer evolving the currently unused pricing.query response instead of adding parallel raw-override and catalog operations.

The Host-owned projection should contain one revision-consistent page of effective entries. Conceptually:

type EffectivePricingEntry =
  | {
      pricing: PricingConfig;
      source: 'builtin';
    }
  | {
      pricing: PricingConfig;
      source: 'custom';
      resetEffect: 'restore_builtin' | 'become_unpriced';
    };
  • The Host combines the same bundled table used by execution with persisted overrides and keeps canonical model-key ordering and existing page bounds.
  • An override is a complete PricingConfig; fields do not partially inherit from the bundled row.
  • Pagination remains pinned to the override revision. If a later page reports revision_changed, the adapter discards the whole partial snapshot and restarts from page one with a bounded retry count.
  • A snapshot and its save revision are valid only for the current Host connection/epoch. Reconnect or Host replacement preserves any user draft but discards partial pages and the mutation base, then requires a fresh authoritative load and review before another save. This handles bundled-table changes across Host builds without inventing a second catalog revision.
  • Once the renderer uses this projection, remove the dead UsageStats.pricing field rather than retaining a second pricing read model.

Desktop adapter boundary

Coordinate the concrete Electron/preload shape with #2010; this issue specifies only two semantic capabilities:

load one complete Pricing Settings snapshot
apply one upsert/delete against an expected revision

The Desktop adapter owns Host transport, page assembly, bounded restart, and typed error mapping. The renderer owns only presentation, an in-memory editor draft, and user feedback. It must not:

  • open a Pricing Store or TelemetryRepo;
  • import/copy the bundled pricing table;
  • maintain a persistent canonical mirror;
  • receive Host pagination cursors;
  • call the retained legacy Main handlers as a fallback;
  • restore window.maka.usage.

Before or as M5 atomically activates the M4-built adapter, remove or statically fence usage:pricing:list, usage:pricing:put, and usage:pricing:reset so the legacy direct Store routes cannot coexist with the active Host path. This does not remove the separate settings:usageStats read path.

No live pricing subscription is required for V1. Load on entering the tab, manual refresh, and authoritative reload after every mutation/conflict/uncertain outcome are sufficient; CAS prevents silent overwrites.

M4 may land the Host projection, production-shaped Desktop adapter, and isolated UI coverage independently. User-visible activation must still obey #853's M5 atomic ownership cutover and must not introduce a second production Runtime owner.

Input rules

  • Reuse canonical modelKey validation. Its current boundary trims, rejects empty input, and requires JavaScript string.length <= 128; do not lowercase or Unicode-normalize it. Matching is exact and existing keys are case-sensitive.
  • Input/output rates are required.
  • Cache-read/cache-write rates are optional; blank means omitted and 0 is a valid explicit value.
  • Every present rate must be finite and non-negative. Do not add a UI-only upper limit.
  • Renderer validation is immediate feedback only. Host canonical validation remains authoritative.
  • Editing an existing row keeps its model key fixed. Renaming is delete + add, not an atomic mutation invented by the client.

Concurrency and uncertain outcomes

Every save carries the revision from the loaded snapshot.

For committed and unchanged, reload the authoritative snapshot before presenting the final state. If that read fails, the mutation outcome is still known: say that the save completed but the latest prices could not be loaded, keep the draft, show no speculative final list, and disable further writes until a fresh snapshot is available. Do not reclassify it as an uncertain commit.

For revision_conflict:

  1. never replay the mutation automatically;
  2. keep the user's draft;
  3. reload the latest authoritative snapshot;
  4. compare the intended state, including whether an override is present, with fresh authority;
  5. if it already matches, report that the current authoritative state is synchronized without issuing another write;
  6. otherwise show the latest value beside the draft and require an explicit review and second Save against the new revision.

Treat commit_outcome_unknown, a response-losing disconnect, and a timeout after dispatch as uncertain rather than ordinary failure:

  1. do not retry automatically or claim success/failure;
  2. keep the draft and block another write;
  3. reload authority when the Host is available;
  4. compare the complete target state: override presence/absence plus every canonical field. Reset matches only when the override is absent and the row resolves to Built-in; Custom-only delete matches only when that key is absent; an upsert equal to a bundled price matches only when provenance still proves an override exists;
  5. if the complete target matches, report that current authority matches the draft without claiming which command committed it;
  6. otherwise require the user to review before an explicit retry.

Delivery slices

  1. Host projection — replace the unused override-only pricing.query projection with effective pricing + provenance, including bounded pages and two-client/CAS tests.
  2. [Tracking] Runtime Host M4 Desktop-first client adapters #2010 Pricing adapter — aggregate pages, invalidate state across Host epochs, map conflict/uncertain outcomes, and replace/fence the three legacy Pricing Main handlers without reaching Stores or activating another production owner.
  3. Desktop Pricing UI + exact-key source — evolve the existing Usage/Pricing tab after rebasing over fix(ui): clarify the Usage page hierarchy #2024, and expose a copyable exact Pricing key from an unpriced Host-backed Usage/Inspector fact. Activate the Host-backed editor only at the M5 ownership switch.
  4. Optional follow-up — link that unpriced call directly to a prefilled Add-price editor. This shortens the journey but is not required to discover/copy the canonical key.

Acceptance

  1. The page reads one Host-backed effective snapshot and distinguishes Built-in, overridden Built-in, and Custom-only rows.
  2. The Pricing tab is visibly not time-scoped; the Usage date range cannot be mistaken for a Pricing scope.
  3. Add/edit/reset/delete cover input, output, cache-read, and cache-write rates, including blank optionals and explicit zero.
  4. Reset restores the bundled value when one exists; deleting a Custom-only override makes the key unpriced.
  5. Effective provenance is based on override presence, including an override equal to the bundled values.
  6. A user can obtain a copyable exact key for an observed unpriced model without guessing or substituting a connection slug.
  7. Multi-page reads never mix revisions, fail clearly after bounded restart under continuous churn, and discard both snapshot and save base after a Host epoch change while preserving the draft.
  8. committed/unchanged (including post-commit reload failure), revision_conflict, commit_outcome_unknown, and response loss follow the behavior above; no mutation is blindly replayed.
  9. Newly activated model work uses the new effective rates; work already active keeps the pricing snapshot it started with until that Run settles, and historical costUsd/pricingRates remain unchanged.
  10. Loading, read-error/retry, mutation-pending, and refresh states are explicit; destructive reset/delete copy states whether the result is Built-in or unpriced.
  11. Field errors are associated with their controls, validation/conflict/pending status is announced to assistive technology, and closing the editor restores focus to its trigger.
  12. Static adapter gates prove the renderer path cannot open Host-owned Stores or an embedded Runtime owner, and the active Host path cannot coexist with the three legacy Pricing handlers/direct Store routes.
  13. Focused Host/protocol, adapter, renderer state/interaction, Storybook (loading, read error, built-in-only, mixed, Custom-only, validation, conflict, uncertain, narrow), and Desktop E2E coverage passes.

Dependencies

Non-goals

  • provider billing, payments, or claiming estimates are invoices;
  • repricing historical records;
  • dynamically repricing a Run that is already active;
  • dynamically fetching vendor prices;
  • currency conversion, bulk import/export, or pricing history;
  • joining all configured models into the V1 catalog;
  • a second raw/catalog query, generic Store RPC, or live invalidation subscription without a demonstrated consumer;
  • restoring Cursor OAuth or designing future Cursor CLI/SDK support;
  • claiming the broader [Tracking] Runtime Host M4 Desktop-first client adapters #2010 Desktop adapter or the M5 cutover.
简体中文

目标

把现有 设置 → 使用统计 → 定价配置 标签页改造成可编辑、由 Runtime Host 支撑的价格覆盖编辑器。页面应展示 Maka 将用于新激活模型工作的真实生效价格,区分内置价格与用户 override,并保持 Host 是唯一 Pricing authority。

Pricing 与它所解释的费用数据继续放在一起,不新增顶层 Settings 导航项。打开 Pricing 标签时,Usage 的时间范围/汇总控件不应让用户误以为价格受时间范围约束;此时改为展示 Pricing 专属说明和刷新操作。

当前代码事实

  • 当前 Pricing 标签不是“只有只读数据”,而是完全断开:它渲染 UsageStats.pricing,但 readUsageStats() 目前始终写死返回 pricing: []
  • Runtime Host authority 实现已在 runtime.sqlite 中拥有持久化 override,以及由当前 storage root 全部 pricing key 共享的一份 revision。pricing.query 返回绑定 revision 的 override 分页;pricing.mutate 通过 CAS 执行 upsert/delete,并返回 committedunchangedrevision_conflict。在 M5 前,它还不是 Desktop production 的 active path。
  • Runtime 结算成本时使用 override ?? builtin,内置价格表位于 Runtime。能够解析价格时,完成的 ModelCallAttempt 会冻结当时解析出的 costUsdpricingRates;否则其费用依据保持未定价。
  • Execution 会在 AiSdkBackend 激活时取得一份 effective pricing snapshot。Mutation 会刷新 idle backend,但 active Run 在结束前继续使用原 snapshot;本 Issue 不引入运行途中的动态重定价。
  • pricing.query 当前没有生产 Client。[Tracking] Runtime Host M4 Desktop-first client adapters #2010 追踪的 Desktop adapter 仍为 Planned;在 feat(runtime): RFC - one Runtime Host for every interactive Maka surface #853 规定的 M5 原子切换前,Desktop production 仍拥有 embedded Runtime。
  • 尚未合并的 PR refactor(desktop): remove unused Usage preload bridge #2038 提议删除未使用的 speculative maka.usage preload bridge,同时暂时保留 Main handlers。本功能不得恢复该 bridge,也不得把这些 handler 当成长期产品契约。

产品行为

V1 的列表严格定义为:

内置 pricing key ∪ 已持久化 override key

首版不与 Provider model catalog 做 join;用户可以手动添加精确 model key。

页面示例:

定价配置                                [刷新] [添加价格]
单位为 USD / 百万 Token,适用于新激活的模型工作;正在运行的 Run 保持启动时的价格。
历史费用不会重算;最终账单以模型供应商为准。

模型 Key                    来源                  输入    输出    缓存读取    缓存写入    操作
openai:gpt-4o                内置                  $2.50   $10.00  $1.25       未设置      自定义
anthropic:claude-sonnet-4-5  自定义 · 有内置回退    $2.00   $12.00  $0.30       $3.75       编辑 · 恢复
acme:coder-v2                仅自定义               $0.80   $2.40   未设置      未设置      编辑 · 删除
  • 编辑内置行会创建完整 override,来源变为“自定义”。
  • 编辑自定义行会更新该 override。
  • 对具有内置 fallback 的自定义行执行重置,会删除 override 并恢复内置价格。
  • 删除没有内置 fallback 的自定义行,会移除 override;新激活的模型工作对该 key 变为“未定价”而不是 $0,已经运行中的工作继续使用其启动时的 snapshot。
  • provenance 根据 override 是否存在判断,而不是比较数值。即使 override 与内置数值完全相同,它仍是“自定义”。
  • 行内操作与确认文案必须说清破坏性操作的后果:reset 会恢复内置值;删除仅自定义行会让新激活的模型工作变成未定价,已经运行中的工作保持启动时的 snapshot。
  • 表单必须区分“省略 cache rate”和“显式填写 0”。当前成本计算器把省略的 cache rate 当作不计 cache 费用,因此应显示类似“未设置(Maka 估算时不计 cache 费用)”的说明,不能只显示含糊的横线。
  • 展示格式不能反过来成为存储输入。进入编辑时必须无损 round-trip canonical value;任何正费率都不能被显示成 $0。页面可以使用更友好的显示格式,但必须保留底层精确值。
  • 手工 model key 字段需说明它是 Runtime 精确查找 key,并展示仓库中已有的例子。对于仅自定义模型,用户必须能从 Host-backed Usage 或 Inspector 的未定价调用事实中复制精确 key;使用 canonical providerIdmodelId,绝不能拿 connection slug 代替。key 的构造由共享 projection/helper 持有,Renderer 不猜测 lookup rule。直接跳转并预填表单只是后续便利功能,不是获取 key 的前置条件。

Host read model

优先演进当前无人使用的 pricing.query response,而不是并行增加 raw override 与 catalog 两套相似 operation。

Host-owned projection 应以同一 revision 输出生效价格。概念结构如下:

type EffectivePricingEntry =
  | {
      pricing: PricingConfig;
      source: 'builtin';
    }
  | {
      pricing: PricingConfig;
      source: 'custom';
      resetEffect: 'restore_builtin' | 'become_unpriced';
    };
  • Host 使用执行期相同的内置表与持久化 override 合成结果,并保留规范 model-key 排序和现有分页上限。
  • override 是完整 PricingConfig,字段不会从内置行做部分继承。
  • 分页继续绑定 override revision。后续页返回 revision_changed 时,adapter 必须丢弃全部部分结果,并以有界次数从第一页重读。
  • snapshot 及其保存 revision 只在当前 Host connection/epoch 内有效。发生重连或 Host replacement 时,保留用户 draft,但丢弃未完成分页和 mutation base;必须先重新读取 authority 并让用户复核,才能再次保存。这样即可处理 Host build 更换后内置表变化,无需再发明一份 catalog revision。
  • Renderer 改用此 projection 后,应删除已经失效的 UsageStats.pricing,而不是保留第二套 Pricing read model。

Desktop adapter 边界

Electron/preload 的具体形状继续与 #2010 协调;本 Issue 只规定两个语义能力:

读取一份完整 Pricing Settings snapshot
基于 expected revision 应用一次 upsert/delete

Desktop adapter 拥有 Host transport、分页组装、有界重读与 typed error mapping。Renderer 只拥有展示、内存中的编辑草稿和用户反馈。它不得:

  • 打开 Pricing Store 或 TelemetryRepo;
  • 导入/复制内置价格表;
  • 维护持久化 canonical mirror;
  • 接收 Host 分页 cursor;
  • 把暂时保留的旧 Main handlers 当作 fallback;
  • 恢复 window.maka.usage

在 M5 原子激活由 M4 构建的 adapter 之前或切换当下,必须删除或用静态门禁封死 usage:pricing:listusage:pricing:putusage:pricing:reset,防止旧的 Store 直达路径与 active Host path 共存。这不影响独立的 settings:usageStats 读取路径。

V1 不需要 live pricing subscription。进入标签页、手动刷新、每次 mutation/冲突/不确定结果后重新读取已经足够;CAS 可以阻止静默覆盖。

M4 可以分别落地 Host projection、production-shaped Desktop adapter 与隔离的 UI 覆盖。用户可见的正式启用仍须服从 #853 的 M5 原子 ownership 切换,不能引入第二个 production Runtime owner。

输入规则

  • 复用 canonical modelKey validation。当前边界会 trim、拒绝空值,并要求 JavaScript string.length <= 128;不得 lowercase 或 Unicode normalize。匹配是精确的,现有 key 也区分大小写。
  • Input/output 必填。
  • Cache read/cache write 可选;空白表示省略,0 是合法的显式值。
  • 每个已填写费率必须有限且非负;不要增加仅存在于 UI 的上限。
  • Renderer 校验只用于即时反馈,Host canonical validation 仍是权威。
  • 编辑已有行时 model key 固定;改名是 delete + add,Client 不应发明原子 rename mutation。

并发与不确定结果

每次保存都携带已加载 snapshot 的 revision。

收到 committedunchanged 后,先重新读取 authority,再展示最终状态。如果读取失败,mutation outcome 仍然是已知的:提示“保存已完成,但无法加载最新价格”,保留 draft,不展示推测出的最终列表,并在拿到 fresh snapshot 前禁止后续写入;不能把它重新归类成 commit outcome uncertain。

收到 revision_conflict 时:

  1. 禁止自动重放 mutation;
  2. 保留用户 draft;
  3. 重新读取最新 authority;
  4. 对照 fresh authority 比较目标状态,包括 override 是否存在;
  5. 如果目标已经成立,则不再写入,只说明当前权威状态已同步;
  6. 否则并列显示最新值和 draft,用户检查后再使用新 revision 明确保存。

commit_outcome_unknown、发送后丢失 response 的断线,以及无法证明 command 未到达 Host 的 timeout,都属于“不确定结果”,不能当作普通失败:

  1. 不自动重试,也不宣称成功/失败;
  2. 保留 draft 并阻止下一次写入;
  3. Host 可用后重新读取 authority;
  4. 比较完整目标状态:override 是否存在,以及全部 canonical fields。Reset 只有在 override 已不存在且该行解析为内置价时才匹配;删除仅自定义行只有在该 key 已不存在时才匹配;数值等于内置价的 upsert 也只有在 provenance 仍证明 override 存在时才匹配;
  5. 完整目标匹配时,只说明当前 authority 与草稿一致,不声称是哪次 command 提交;
  6. 不匹配时,必须让用户检查后显式重试。

交付 slices

  1. Host projection:把无人消费的 override-only pricing.query 替换为 effective pricing + provenance,并覆盖有界分页与双 Client/CAS 测试。
  2. [Tracking] Runtime Host M4 Desktop-first client adapters #2010 Pricing adapter:组装分页、在 Host epoch 变化时失效旧状态、映射 conflict/uncertain outcome,并替换或封死三个旧 Pricing Main handlers;不接触 Store,也不激活第二个 production owner。
  3. Desktop Pricing UI + 精确 key 来源:在 fix(ui): clarify the Usage page hierarchy #2024 之上 rebase 后演进现有 Usage/Pricing 标签,并从 Host-backed Usage/Inspector 的未定价事实中提供可复制的精确 Pricing key。Host-backed editor 只在 M5 ownership 切换时正式启用。
  4. 可选后续:从该未定价调用直接打开预填 key 的 Add-price editor。它能缩短路径,但不是发现/复制 canonical key 的前置条件。

验收条件

  1. 页面读取同一份 Host-backed effective snapshot,并区分纯内置、覆盖内置与仅自定义三类行。
  2. Pricing 标签明确不受时间范围约束;Usage 日期范围不能被误解成 Pricing scope。
  3. 添加、编辑、重置、删除覆盖 input、output、cache read、cache write,并正确处理空白 optional 与显式零值。
  4. Reset 在存在内置值时恢复内置价格;删除仅自定义 override 后,该 key 变为未定价。
  5. provenance 按 override 存在性判断,包括数值与内置完全相同的 override。
  6. 用户能为实际出现过的未定价模型复制精确 key,不需要猜测,也不会误用 connection slug。
  7. 多页读取绝不混合 revision;持续 churn 超过有界重读次数后明确失败;Host epoch 变化后丢弃 snapshot 与保存基线,同时保留 draft。
  8. committed/unchanged(包括提交后 reload 失败)、revision_conflictcommit_outcome_unknown 与 response loss 均遵循上述行为,任何 mutation 都不会被盲目重放。
  9. 新激活的模型工作使用新费率;已经运行中的 Run 保持启动时的 pricing snapshot,直到自身结束;历史 costUsd/pricingRates 也保持不变。
  10. loading、读取失败/重试、mutation pending 与刷新状态都有明确表现;reset/delete 文案说明结果是恢复内置价还是变为未定价。
  11. 字段错误与控件关联,校验/冲突/pending 状态可被辅助技术感知;关闭 editor 后焦点返回触发控件。
  12. 静态 adapter gate 证明 Renderer 路径无法打开 Host-owned Store 或 embedded Runtime owner,并且 active Host path 不会与三个旧 Pricing handler/Store 直达路径共存。
  13. focused Host/protocol、adapter、renderer state/interaction、Storybook(loading、读取失败、纯内置、混合、自定义-only、校验、冲突、不确定、窄窗口)与 Desktop E2E 全部通过。

依赖

非目标

  • 供应商账单、支付,或把估算值声明为账单;
  • 重算历史记录;
  • 对已经 active 的 Run 动态重定价;
  • 动态抓取供应商价格;
  • 货币换算、批量导入/导出或定价历史;
  • 在 V1 中 join 全部已配置模型;
  • 在没有真实消费者前增加第二套 raw/catalog query、通用 Store RPC 或 live invalidation subscription;
  • 恢复 Cursor OAuth 或设计未来 Cursor CLI/SDK;
  • 认领更大范围的 [Tracking] Runtime Host M4 Desktop-first client adapters #2010 Desktop adapter 或 M5 cutover。

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requesthelp wantedExtra attention is needed

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions