Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
5 changes: 2 additions & 3 deletions docs/concepts/pricing.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion docs/providers/model-presets.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand Down
6 changes: 3 additions & 3 deletions src/provider/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
10 changes: 5 additions & 5 deletions src/provider/prices.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
6 changes: 3 additions & 3 deletions tests/fixtures/models_dev_slice.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Expand Down
2 changes: 1 addition & 1 deletion tests/preset_prices_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)),
(
Expand Down
2 changes: 1 addition & 1 deletion tests/price_audit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion tests/price_fetch_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()),
Expand Down
Loading