Files
DeepSeek-Reasonix/docs/BILLING.md
T
SivanCola c1cec65e69 feat(provider,config,billing): adapt official DeepSeek to the V4.1 Flash launch
Problem
-------
The official DeepSeek provider still modelled the pre-V4.1 world. DeepSeek
shipped V4.1 Flash as `deepseek-flash` with native image input, retired
`deepseek-v4-flash-vision-exp`, cut the Flash price from 2026-09-10 12:00
Beijing, and left `deepseek-v4-flash` / `deepseek-v4-pro` as transition
aliases. The desktop model list therefore showed `deepseek-flash` as "image
capability unknown", `deepseek-v4-flash` was hard-blocked from images even
though it now serves a multimodal model, and every cost quote was priced on
the superseded August table.

Root cause
----------
Image capability had no single owner: the settings resolver read
`BuiltinModelInfo` and the curated preset, the runtime gate read the
pinned-SKU check inside `officialDeepSeekEffectiveVision`, and a separate
hard text-only block keyed on an exact two-model list. Adding a SKU meant
editing several lists that could silently disagree. Pricing had the same
shape: `ResolveScheduledRate` hardcoded one schedule id, and `LookupCatalog`
returned the first peak row, so a second dated schedule could not be
expressed.

Fix
---
- `internal/provider/deepseek_models.go` owns the official DeepSeek model
  lists. `IsOfficialDeepSeekImageModel` covers V4.1 Flash and the ids the
  vendor still routes to it; `IsOfficialDeepSeekTextModel` is now V4 Pro
  only. The capability resolver, the runtime vision gate and the local model
  catalog all consult it, so a new SKU is added in one place.
- An explicitly emptied `VisionModels` list still disables image input for a
  provider; only a stale non-empty list is allowed to omit a model the
  vendor reports as capable.
- `internal/billing/catalog.go` gains `ScheduleDeepSeekV4September2026`
  (2026-09-10 04:00 UTC). The August rows close at the cutover so the
  superseded prices stay queryable, and the retired ids are billed at the
  Flash price. `LookupCatalog` selects the newest peak anchor, and the new
  `MatchesOfficialPeakAnchor` accepts an anchor from any generation, so a
  config saved before the price cut is still quoted at the live rate.
- `internal/config/pricing.go` carries the new Flash table and recognises
  the August generation as an official default instead of a custom rate.

The config version is deliberately unchanged:
`ApplyUserConfigUpgradesOnStartup` stamps
`deepSeekOfficialChatUpgradeConfigVersion` (9) for any config below the
current version, which would downgrade a v10 config while also skipping the
OpenCode Go upgrade that follows it. The refresh is done at quote time
instead, so no user config is rewritten on startup.

Verification
------------
- `go test ./...` (root): 131 packages ok. The 3 remaining failures reproduce
  on a pristine `origin/main-v2` checkout: untracked `wt*/` worktrees inside
  the repo are scanned by the sub-agent boundary test, and
  `cmd/reasonix-legacy-migrator` reads a real installation on this machine.
- `cd desktop && go test ./...`: ok.
- `golangci-lint` 2.12.2: 0 issues. `repolint`: clean, baseline unchanged.
- New coverage: `TestOfficialDeepSeekMultimodalSKUsEmbedUserImages` (image
  parts reach the wire), `TestDeepSeekSeptemberScheduleResolvesFlashPriceCut`
  (peak/off-peak, both currencies, legacy ids, V4 Pro unchanged, August
  schedule closed), and `TestStaleDeepSeekAnchorQuotesAtTheLiveRate`
  (end-to-end quote from a config still holding the August anchor).
2026-09-10 18:48:49 +08:00

4.5 KiB
Raw Blame History

Billing, display currency, and cost quotes

Reasonix keeps three facts separate:

  1. original: an estimate from the selected public/custom rate card in its pricing-table currency. It is not an invoice or a provider debit.
  2. valuations: occurrence-time identity and, when available, an official_table estimate for the same model in the other official region.
  3. Wallet balances: the exact original-currency values returned by a provider.

Reasonix has no runtime FX download, cache, refresh goroutine, or wallet conversion. Old fx/rateSnapshot fields remain readable for history only; new quotes never generate them.

[billing]
display_currency = "auto"   # auto | CNY | USD

[[providers]]
billing_currency = "USD"    # pricing-table basis, not settlement currency
billing_mode = "payg"       # payg | subscription_equivalent

Legacy [desktop].currency remains readable and migrates to [billing].display_currency. auto is intentionally unresolved in config: one valid wallet currency may become a tab/session hint; otherwise a single original currency is selected or mixed currencies are shown as buckets. A language, browser locale, or host region never changes a rate card.

CostQuote

usage.costQuote is the canonical host-side usage payload:

Field Meaning
original Original-currency rate-card estimate
originalTotals[] ISO-sorted original buckets for mixed aggregates
valuations.*.basis identity or official_table for new quotes
selected A single amount only when a display total exists
costComplete Usage and pricing facts are complete
displayComplete A requested single-currency total exists
complete Compatibility alias mirroring displayComplete
displayStatus matched, fallback_original, bucketed, or unavailable
aggregateMode single_currency, common_valuation, or currency_buckets
rateBand DeepSeek occurrence-time band: peak, off_peak, or aggregate mixed
ratedAt UTC request-completion time used to select a scheduled rate

If a requested currency is unavailable but every original is the same, the original amount is shown with fallback_original. Mixed originals produce originalTotals and no scalar zero. — is reserved for missing usage or pricing (unavailable). Legacy scalar aliases (cost, costUsd, total_cost) are written only when selected exists.

DeepSeek scheduled rates

For official DeepSeek OpenAI, Responses, and Anthropic endpoints, Flash and V4 Pro use occurrence-time pricing. Peak windows are 09:00–12:00 and 14:00–18:00 Beijing time; boundaries are left-closed/right-open and all other times are off-peak. The request-completion timestamp is used because the provider does not report per-token billing time. Images sent to a vision-capable model are billed as input tokens from provider usage.

Two dated schedules are recorded. From 2026-08-17 00:00 Beijing time, V4 Flash, deepseek-v4-flash-vision-exp (same list price as Flash), and V4 Pro. From 2026-09-10 12:00 Beijing time, deepseek-flash carries a lower Flash price, and the retired deepseek-v4-flash and deepseek-v4-flash-vision-exp ids are served by that model and billed at the same rate. V4 Pro keeps its own price until the vendor routes it to V4.1 Flash. A quote resolves against the schedule in effect at its occurrence time, so the superseded August rates stay queryable.

The stored provider price remains the peak anchor. Dynamic resolution is enabled only for PAYG configurations whose complete rate card exactly matches an official anchor. A config that still holds the previous generation's anchor is recognized as an untouched official row and billed against the live schedule, so a dated vendor price change reaches it without a re-save. Custom endpoints, edited prices, and unrecognized models remain static. Persisted session, ledger, and stats quotes are never repriced.

Wallets and diagnostics

Wallets are never converted or cross-added. An explicit target uses the exact matching wallet; if it is absent, the real currency is shown with an ISO prefix. Automatic mode uses a single valid wallet currency only as a runtime hint. Multiple/unknown/error responses do not affect the cost facts.

reasonix doctor billing
reasonix doctor billing --json

The compatible fx report is always enabled=false and has no cache. The report also lists the automatic selection policy, pricing-table currencies, and official-catalog matches.