Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
# Changelog

## Unreleased

### Fixed
- **DeepSeek V4 pro/flash now price** — DeepSeek's V4 pro and flash
models (including dated snapshots like `deepseek-v4-pro-0813` and
`deepseek-v4-flash-0731`) weren't in any public catalog, so usage
through Hermes/AihubMix counted tokens but showed $0 and folded out of
the spend ring. Their AihubMix rates are baked in until the catalogs
learn them (self-retiring), and cached history repricies.
- **Grok 4.6 prices from day one** — xAI's launch-day rates
($2 in / $0.50 cached / $6 out per MTok, doubling for ≥200k-token
prompts; the fast variant at 2x) are baked in until the public
catalogs learn the model, so spend from Grok 4.6 sessions shows
dollars instead of the unpriced ⚠.
Comment on lines +5 to +16

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Pull request bundles three unrelated changes despite the one-change rule

This change ships launch pricing for one vendor's new model, a separate vendor-prefix stripping rule, and a third vendor's model family in a single pull request (see the changelog entries added at CHANGELOG.md:5-16), which the project's contribution rules forbid.
Impact: Reviewers cannot accept or revert one part without the others.

CONTRIBUTING.md rule

CONTRIBUTING.md states under Pull requests: "Keep PRs focused: one change per PR." The PR title itself names three separate items (Grok 4.6 launch pricing, Cursor slug handling, DeepSeek v4 builtins), and the code changes touch independent builtin entries in src-tauri/src/pricing.rs:667-716 plus a backstop table in src-tauri/src/spend.rs:414-432.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


## 0.4.33 — 2026-08-12

### Fixed
Expand Down
84 changes: 83 additions & 1 deletion src-tauri/src/pricing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ pub fn generation() -> u64 {
/// fingerprinted below — an app update that reprices the same files would
/// otherwise leave history at the old dollars until upstream happens to
/// rewrite a catalog.
const CORRECTIONS_REV: u32 = 3; // 3: zero-rate catalog placeholders no longer price at $0.00
const CORRECTIONS_REV: u32 = 4; // 4: deepseek-v4 pro/flash builtin prices

/// Stable fingerprint of the effective pricing inputs: the on-disk catalog
/// files plus this binary's corrections revision. The persistent spend
Expand Down Expand Up @@ -668,13 +668,49 @@ fn builtin_price(canonical: &str) -> Option<Price> {
let bare = canonical
.strip_prefix("moonshot/")
.or_else(|| canonical.strip_prefix("moonshot-ai/"))
.or_else(|| canonical.strip_prefix("xai/"))
.or_else(|| canonical.strip_prefix("deepseek/"))
// Cursor's CSV brands third-party slugs ("cursor-grok-4.6-xhigh");
// the supplement's alias rules normally translate these, but a
// launch-day model needs the baked rates before the supplement
// learns the new slug.
.or_else(|| canonical.strip_prefix("cursor-"))
.unwrap_or(canonical);
// DeepSeek ships dated snapshots ("deepseek-v4-pro-0813"); price them as
// the base model so a new date doesn't silently go unpriced.
let bare = match bare.strip_suffix(|c: char| c.is_ascii_digit()) {
Some(_) if bare.starts_with("deepseek-") => bare.rsplit_once('-').map_or(bare, |(h, t)| {
if t.chars().all(|c| c.is_ascii_digit()) && t.len() >= 4 { h } else { bare }
}),
_ => bare,
};
Comment on lines +679 to +686

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔍 Dated-snapshot stripping is scoped tightly, but only to deepseek-

The snapshot trimmer at src-tauri/src/pricing.rs:681-686 only fires when the slug ends in a digit, starts with deepseek-, and the trailing dash-segment is ≥4 digits, so deepseek-v4 (t=v4) and deepseek-coder-6.7b are untouched and non-DeepSeek families are unaffected. Note the guard is on the post-prefix-strip bare, so deepseek/deepseek-v4-pro-0813 works but a gateway spelling like aihubmix/deepseek-v4-pro-0813 would not be prefix-stripped at all and stays unpriced — worth confirming which exact strings Hermes/AihubMix logs emit.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

match bare {
// AihubMix DeepSeek V4 family (USD/MTok, aihubmix.com/model/…): no
// cache-write rate published, so writes bill at the input rate.
// Used through Hermes/AihubMix; public catalogs don't carry these
// slugs yet. pro: /deepseek-v4-pro-0813 · flash: /deepseek-v4-flash.
"deepseek-v4-pro" => Some(Price::flat(0.464, 0.928, 0.004, 0.464)),
"deepseek-v4-flash" => Some(Price::flat(0.142, 0.284, 0.0284, 0.142)),
Comment on lines +692 to +693

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Cached input on one DeepSeek model is billed at roughly a tenth of the plausible rate

The cached-input rate for DeepSeek's pro model is set to $0.004 per million (Price::flat(0.464, 0.928, 0.004, 0.464) at src-tauri/src/pricing.rs:692), about 1/116 of its regular input rate, while its sibling flash model uses 1/5, which points to a dropped digit, so cached usage is charged far too little.
Impact: Spend totals for that model under-report the money actually spent whenever prompt caching is used.

Rate-card ratio inconsistency inside the same baked-in family

Both new entries mirror each other everywhere else (output = 2× input, cache write = input). The flash entry's cache read 0.0284 is exactly 20% of its input 0.142; the pro entry's cache read 0.004 is 0.86% of 0.464. A vendor cache-hit price of 1/10 input would be 0.04640.004 looks like a truncated 0.0464. The value is asserted in the new test at src-tauri/src/pricing.rs:772, so the test would need updating alongside. Please re-check the aihubmix.com rate card before merge.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

"kimi-k3" | "kimi-k3-code" => Some(Price::flat(3.0, 15.0, 0.3, 3.0)),
// Alibaba Model Studio, GA'd 2026-08-03 (USD/MTok): input $2,
// output $6, implicit cache read $0.25, explicit cache write $2.50.
// Public catalogs still carry 0/0 placeholders for these slugs.
"qwen3.8-max" | "qwen3.8-max-preview" => Some(Price::flat(2.0, 6.0, 0.25, 2.5)),
// Grok 4.6, released 2026-08-12 — docs.x.ai/docs/pricing (USD/MTok):
// $2 in / $0.50 cached / $6 out; prompts ≥200k bill $4 / $1 / $12
// for the WHOLE request (xAI's long-context rule matches
// request_cost's tiering, and Grok spend passes the default 200k
// threshold). xAI bills no separate cache-write rate — writes are
// plain input. The announced 2x "-fast" variant needs no entry:
// the -fast resolution path applies the default 2x multiplier to
// this rate card. Public catalogs don't carry 4.6 yet.
"grok-4.6" | "grok-4-6" => Some(Price {
input_200k: Some(4.0),
output_200k: Some(12.0),
cache_read_200k: Some(1.0),
cache_write_200k: Some(4.0),
..Price::flat(2.0, 6.0, 0.5, 2.0)
}),
_ => None,
}
}
Expand Down Expand Up @@ -725,6 +761,25 @@ mod tests {
assert!((baked.input - 2.0).abs() < 1e-9);
}

#[test]
fn deepseek_v4_pro_is_priced_including_dated_snapshots() {
let store = super::Store::default();
// Bare slug and the AihubMix dated snapshot both price identically.
for slug in ["deepseek-v4-pro", "deepseek-v4-pro-0813", "deepseek/deepseek-v4-pro-0813"] {
let p = super::resolve(&store, slug, 0).unwrap_or_else(|| panic!("{slug} unpriced"));
assert!((p.input - 0.464).abs() < 1e-9, "{slug}");
assert!((p.output - 0.928).abs() < 1e-9, "{slug}");
assert!((p.cache_read - 0.004).abs() < 1e-9, "{slug}");
}
// The flash sibling is priced too (both are Hermes-logged slugs),
// including its dated snapshot.
let flash = super::resolve(&store, "deepseek-v4-flash-0731", 0).unwrap();
assert!((flash.input - 0.142).abs() < 1e-9);
assert!((flash.output - 0.284).abs() < 1e-9);
// A slug outside the family stays unpriced (no over-broad match).
assert!(super::resolve(&store, "deepseek-v9-imaginary", 0).is_none());
}

#[test]
fn priority_slugs_price_at_base_times_priority_multiplier() {
let mut store = super::Store::default();
Expand Down Expand Up @@ -768,6 +823,33 @@ mod tests {
assert_eq!((terra.input, terra.output), (1.75, 11.0));
}

#[test]
fn grok_46_builtin_prices_with_long_context_tier() {
// Vendor rates (docs.x.ai/docs/pricing): $2/$0.50/$6, doubling for
// ≥200k prompts — resolvable in every spelling before the public
// catalogs learn the model. Empty store = builtin only.
let store = super::Store::default();
// cursor-grok-4.6-xhigh is the exact slug Cursor's CSV logged on
// launch day — 20.6M real tokens showed $0.00 until it resolved.
for slug in
["grok-4.6", "grok-4-6", "xai/grok-4.6", "grok-4.6-high", "cursor-grok-4.6-xhigh"]
{
let p = super::resolve(&store, slug, 0)
.unwrap_or_else(|| panic!("{slug} did not price"));
assert_eq!((p.input, p.output, p.cache_read, p.cache_write), (2.0, 6.0, 0.5, 2.0), "{slug}");
assert_eq!(
(p.input_200k, p.output_200k, p.cache_read_200k),
(Some(4.0), Some(12.0), Some(1.0)),
"{slug}"
);
}
// The fast variant is "twice the price" (launch post): the -fast
// path scales the whole rate card, long-context tier included.
let fast = super::resolve(&store, "grok-4.6-fast", 0).unwrap();
assert_eq!((fast.input, fast.output, fast.cache_read), (4.0, 12.0, 1.0));
assert_eq!(fast.input_200k, Some(8.0));
}

#[test]
fn kimi_k3_builtin_prices_every_spelling() {
// Vendor-documented rates (platform.kimi.ai): $3 in, $15 out, $0.30
Expand Down
13 changes: 12 additions & 1 deletion src-tauri/src/spend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -413,7 +413,18 @@ fn codex_price(model: &str) -> (f64, f64, f64) {

fn grok_price(model: &str) -> (f64, f64) {
let m = model.to_lowercase();
if m.contains("code") || m.contains("fast") {
// Grok 4.6's "fast" is a 2x PREMIUM speed tier (launch post: "twice
// the price"), the opposite of the older grok-4-fast/grok-code-fast
// line where "fast" meant a smaller, cheaper model — 4.6 slugs must
// never fall into that cheap branch. (Normally unreachable: the
// baked-in catalog entry resolves 4.6 before this backstop.)
if m.contains("4.6") || m.contains("4-6") {
if m.contains("fast") {
(4.0, 12.0)
} else {
(2.0, 6.0)
}
} else if m.contains("code") || m.contains("fast") {
(0.2, 1.5)
} else {
(3.0, 15.0)
Expand Down