diff --git a/CHANGELOG.md b/CHANGELOG.md index 277d5ff1..81f29be2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ adheres to [Semantic Versioning](https://semver.org/). ## Unreleased +- **`mistral_large_4` repriced to $0.70 / $2.10 per million tokens, cached $0.07** (was $1.36 / $4.18 / $0.14). Mistral's model page and models.dev both list the new rate, with no sale marking; the release price audit caught the drift. The pricing docs no longer describe Mistral's launch sale as running (it ended about 2026-10-20). - **Compaction keeps the task prompt and the latest ask** (#150). When the `keep_first` head and the `keep_recent` tail alone exceed the compaction target (a small budget, bulky tool results), level 3 kept only the newest messages that fit the target — dropping the task prompt first, and leaving only the `[Context compacted]` marker when the newest turn alone exceeded the target (#150's `22 msgs -> 1 msg`). It now keeps, in order, the head, the latest user message a person wrote, the newest turn and as much of the rest of the tail as fits — overshooting the target, up to the budget, rather than cutting into them; past the budget the head goes first, then the ask, and the newest turn is cut last (a turn bigger than the budget leaves the marker and the ask). A cut past the latest user message on the normal path keeps it too, right after the marker. The kept head also never ends partway through a multi-call turn's results, which could leave a tool call unanswered, and the guaranteed tail is never less than the newest turn: with a small `keep_recent` the cut could drop the turn whose results the model was waiting on. In the offline sweep (`docs/evals/compaction-defaults.md`) task-prompt loss goes from 100% to 0% at a 26K budget, with no marker-only compactions; tool-heavy sessions compact ~14% more often there (they now keep more). At the default 96K the only change is that tool-heavy sessions no longer lose the latest ask (2% of compactions before). ## 0.26.1 (2026-10-30) diff --git a/docs/concepts/pricing.md b/docs/concepts/pricing.md index 72641c8a..2d24df4f 100644 --- a/docs/concepts/pricing.md +++ b/docs/concepts/pricing.md @@ -186,10 +186,9 @@ every lookup is `None`. Once it has: `gpt_6_sol`, `gpt_6_1_sol`, `gpt_6_luna`, `gemini_4_argon`, `gemini_3_8_flash`, `grok_4_7`, `mistral_large_4`, `glm_5_3`, `glm_5_3_flash`, `qwen_3_8_max`, `qwen_3_8_flash`) are all listed in the - bundled snapshot. Three carry the **regular** rate during a vendor + bundled snapshot. Two carry the **regular** rate during a vendor promotion (`gemini_4_argon` to 2026-11-30, `gemini_3_8_flash` to - 2026-12-31, `mistral_large_4` to about 2026-10-20), so their costs read up - to twice the bill until it ends — an upper bound, like DeepSeek's peak rate. + 2026-12-31), so their costs read up to twice the bill until it ends — an upper bound, like DeepSeek's peak rate. That holds for the bundled snapshot: `enable_live*` installs models.dev's current, promotional rate over it (the true bill today), and a live table or cache fetched before a promotion ends keeps that rate until refetched. diff --git a/docs/providers/model-presets.md b/docs/providers/model-presets.md index b9ac891f..354e87cd 100644 --- a/docs/providers/model-presets.md +++ b/docs/providers/model-presets.md @@ -49,7 +49,7 @@ Use a preset when the provider is listed here. Use a custom `ModelConfig` when y The generic constructors (`google(id, name)`, `xai`, `mistral`, …) assume conservative limits; for a model with a named preset, use the preset — it carries the model's real context window and output limit (a generic `ModelConfig::xai("grok-4.7", ..)` compacts at 131K and stops replies at 4,096 tokens). The constructors do not validate model IDs. They send the `id` you pass through to the provider, which lets you use newly released model IDs before yoagent updates its examples. -**Nothing is priced until the process opts in** — `prices::enable_bundled()` (the bundled data file `src/provider/prices.json`, offline) or `prices::enable_live(..)` (native) — see [Model Pricing](../concepts/pricing.md#enabling-pricing). After that, prices are looked up by `(provider, id)` when the config is built. The named presets (`claude_fable_5`, `claude_fable_5_1`, `claude_opus_5_5`, `claude_opus_5`, `claude_opus_4_8`, `claude_sonnet_5`, `claude_sonnet_5_5`, `claude_haiku_5_5`, `claude_haiku_4_5`, `gpt_5_5`, `gpt_6_astra`, `gpt_6_sol`, `gpt_6_1_sol`, `gpt_6_luna`, `gemini_4_argon`, `gemini_3_8_flash`, `grok_4_7`, `mistral_large_4`, `glm_5_3`, `glm_5_3_flash`, `qwen_3_8_max`, `qwen_3_8_flash`) are all in the snapshot (`cost: Some(CostConfig)`). The OpenAI ones carry a context tier: above 272K prompt tokens the whole request bills at the long-context rates (`gpt_5_5`'s long-band cached-input rate is unverified; see its `note` in the data file); so do `claude_haiku_5_5` (above 100K) and `grok_4_7` (above 200K; xAI bills a prompt of exactly 200,000 tokens at the higher rate, see its `note`). `gemini_4_argon`, `gemini_3_8_flash` and `mistral_large_4` carry the **regular** rate while their vendors run a promotion (Gemini 4 Argon through 2026-11-30, 3.8 Flash through 2026-12-31, Mistral's launch sale to about 2026-10-20): until then their costs read up to twice the bill, an upper bound, as with DeepSeek's peak rate. The generic first-party constructors (`anthropic`, `openai`, `openai_responses`, `google`, `xai`, `groq`, `deepseek`, `mistral`, `zai`, `minimax`, `qwen`, `meta`) are priced when the file lists the id — `ModelConfig::anthropic("claude-sonnet-5", ..)` gets `claude_sonnet_5()`'s rates, `meta` prices `muse-spark-1.1`/`-1.2`, `deepseek` prices `deepseek-flash`/`deepseek-v4-pro` and the legacy `deepseek-v4-flash`/`deepseek-v4-flash-vision-exp` at the peak rate (see [DeepSeek Models](#deepseek-models)) — and carry `cost: None` otherwise. Gateways and custom endpoints (`custom`, `openai_compat`, `local`, `ollama`, `opencode_zen`, `opencode_go`) are always `cost: None`, since what they bill is not the vendor's list price. `None` means pricing **unknown**, not free; set `config.cost = Some(CostConfig::new(input, output))` yourself if you want cost reporting. The OpenCode presets select the API protocol from the model id — see [OpenCode Zen & Go](opencode.md). +**Nothing is priced until the process opts in** — `prices::enable_bundled()` (the bundled data file `src/provider/prices.json`, offline) or `prices::enable_live(..)` (native) — see [Model Pricing](../concepts/pricing.md#enabling-pricing). After that, prices are looked up by `(provider, id)` when the config is built. The named presets (`claude_fable_5`, `claude_fable_5_1`, `claude_opus_5_5`, `claude_opus_5`, `claude_opus_4_8`, `claude_sonnet_5`, `claude_sonnet_5_5`, `claude_haiku_5_5`, `claude_haiku_4_5`, `gpt_5_5`, `gpt_6_astra`, `gpt_6_sol`, `gpt_6_1_sol`, `gpt_6_luna`, `gemini_4_argon`, `gemini_3_8_flash`, `grok_4_7`, `mistral_large_4`, `glm_5_3`, `glm_5_3_flash`, `qwen_3_8_max`, `qwen_3_8_flash`) are all in the snapshot (`cost: Some(CostConfig)`). The OpenAI ones carry a context tier: above 272K prompt tokens the whole request bills at the long-context rates (`gpt_5_5`'s long-band cached-input rate is unverified; see its `note` in the data file); so do `claude_haiku_5_5` (above 100K) and `grok_4_7` (above 200K; xAI bills a prompt of exactly 200,000 tokens at the higher rate, see its `note`). `gemini_4_argon` and `gemini_3_8_flash` carry the **regular** rate while Google runs a promotion (Gemini 4 Argon through 2026-11-30, 3.8 Flash through 2026-12-31): until then their costs read up to twice the bill, an upper bound, as with DeepSeek's peak rate. The generic first-party constructors (`anthropic`, `openai`, `openai_responses`, `google`, `xai`, `groq`, `deepseek`, `mistral`, `zai`, `minimax`, `qwen`, `meta`) are priced when the file lists the id — `ModelConfig::anthropic("claude-sonnet-5", ..)` gets `claude_sonnet_5()`'s rates, `meta` prices `muse-spark-1.1`/`-1.2`, `deepseek` prices `deepseek-flash`/`deepseek-v4-pro` and the legacy `deepseek-v4-flash`/`deepseek-v4-flash-vision-exp` at the peak rate (see [DeepSeek Models](#deepseek-models)) — and carry `cost: None` otherwise. Gateways and custom endpoints (`custom`, `openai_compat`, `local`, `ollama`, `opencode_zen`, `opencode_go`) are always `cost: None`, since what they bill is not the vendor's list price. `None` means pricing **unknown**, not free; set `config.cost = Some(CostConfig::new(input, output))` yourself if you want cost reporting. The OpenCode presets select the API protocol from the model id — see [OpenCode Zen & Go](opencode.md). The `claude_*` presets also set `AnthropicCompat::native_structured_output`, so `Agent::prompt_structured` uses the API's native JSON outputs instead of a forced tool call. `claude_fable_5_1` and `claude_opus_5_5` depend on it, because both models reject forced `tool_choice`. `ModelConfig::anthropic(id, name)` infers it (and the thinking mode) from a Claude id, and leaves it off for any other id. See [Structured Outputs](../concepts/structured-outputs.md). diff --git a/src/provider/model.rs b/src/provider/model.rs index 84f46ba5..6c73eb08 100644 --- a/src/provider/model.rs +++ b/src/provider/model.rs @@ -1786,9 +1786,9 @@ impl ModelConfig { /// Reasoning (`reasoning_effort` `high` or `none`) is not sent by this /// preset: the model runs at its own default. /// - /// Priced at the **regular** rate, $1.36 / $4.18, cached $0.14 (read - /// 2026-10-18) — Mistral's launch sale bills half until about 2026-10-20. - /// See [`CostConfig`]. + /// Priced at $0.70 / $2.10, cached $0.07 (Mistral's model page, + /// 2026-10-30; it listed $1.36 / $4.18 after the launch sale ended). See + /// [`CostConfig`]. pub fn mistral_large_4() -> Self { Self { context_window: 1_000_000, diff --git a/src/provider/prices.json b/src/provider/prices.json index b4d3fdef..5e840c40 100644 --- a/src/provider/prices.json +++ b/src/provider/prices.json @@ -264,12 +264,12 @@ }, "mistral": { "mistral-large-4": { - "input": 1.36, - "output": 4.18, - "cache_read": 0.14, - "note": "Regular price; Mistral's launch sale (50% off from 2026-10-06) has ended.", + "input": 0.7, + "output": 2.1, + "cache_read": 0.07, + "note": "Mistral's model page lists $0.70 / $2.10 (cached $0.07) from about 2026-10-30, with no sale marking; it listed $1.36 / $4.18 after the launch sale (50% off, $0.68 in, from 2026-10-06) ended. No changelog entry announces the change.", "source": "https://docs.mistral.ai/models/mistral-large-4-0", - "verified": "2026-10-23" + "verified": "2026-10-30" } }, "qwen": { diff --git a/tests/fixtures/models_dev_slice.json b/tests/fixtures/models_dev_slice.json index 52315238..d640cddb 100644 --- a/tests/fixtures/models_dev_slice.json +++ b/tests/fixtures/models_dev_slice.json @@ -403,9 +403,9 @@ }, "status": "beta", "cost": { - "input": 1.36, - "output": 4.18, - "cache_read": 0.14 + "input": 0.7, + "output": 2.1, + "cache_read": 0.07 }, "canonical_model_id": "mistral/mistral-large-4" } diff --git a/tests/preset_prices_test.rs b/tests/preset_prices_test.rs index 5ca7eea9..7ea09504 100644 --- a/tests/preset_prices_test.rs +++ b/tests/preset_prices_test.rs @@ -123,7 +123,7 @@ fn pinned() -> Vec<(&'static str, ModelConfig, CostConfig)> { ( "mistral_large_4", ModelConfig::mistral_large_4(), - read_only(1.36, 4.18, 0.14), + read_only(0.7, 2.1, 0.07), ), ("glm_5_3", ModelConfig::glm_5_3(), read_only(1.4, 4.4, 0.26)), ( diff --git a/tests/price_audit.rs b/tests/price_audit.rs index c40ea463..4ad78928 100644 --- a/tests/price_audit.rs +++ b/tests/price_audit.rs @@ -971,7 +971,7 @@ fn the_audit_logic_runs_offline_against_the_fixture() { }); assert_eq!(stale_sale.len(), 1, "{stale_sale:?}"); assert!( - stale_sale[0].contains("input is 0.68 in the crate, 1.36 in models.dev"), + stale_sale[0].contains("input is 0.68 in the crate, 0.7 in models.dev"), "{stale_sale:?}" ); // Without the recorded gap, models.dev's `reasoning` rate is unknown diff --git a/tests/price_fetch_test.rs b/tests/price_fetch_test.rs index 34e3375a..b0423286 100644 --- a/tests/price_fetch_test.rs +++ b/tests/price_fetch_test.rs @@ -151,7 +151,7 @@ fn models_dev_fixture_maps_into_our_schema() { // Against the built-in data: every overlapping model agrees as billed // (gpt-5.5's implicit cache_write equals its explicit input-rate one), so // only new models are reported. - // (Mistral Large 4 agrees too since its launch sale ended, 2026-10-23.) + // (Mistral Large 4 agrees too: both carry its 2026-10-30 rate.) let changes = t.changes_from(&PriceTable::builtin()); assert!( changes.iter().all(|c| c.before.is_none()),