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
6 changes: 4 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ All notable changes to `yoagent` are documented here. The format loosely
follows [Keep a Changelog](https://keepachangelog.com/), and the project
adheres to [Semantic Versioning](https://semver.org/).

## Unreleased
## 0.25.13 (2026-10-23)

Two provider fixes; no API change.

- **Bedrock, Gemini and Vertex no longer send an empty message** (#369). An assistant turn that errored, was refused or was cancelled with no content (the loop's error message is one empty text block, which the builders filter) — or whose only content is thinking the provider does not replay (on Gemini and Vertex any thinking; on Bedrock unsigned reasoning to a Claude model, or another API's) — went out as `content: []` / `parts: []`, which both APIs reject (as did a user message with only empty text); the Anthropic provider already skipped such an assistant turn. So the prompt after a failed or cancelled run was rejected too. Empty messages of either role are now dropped before consecutive same-role messages are merged (#368), so the user turns around a dropped one go back as one.
- **Bedrock, Gemini and Vertex: a parallel tool call's results go back in one message** (#223). Each tool result was sent as its own user-role message, and a steering message, stop marker or compaction summary after them added another, while Bedrock's Converse requires roles to alternate and Gemini requires a turn's function responses in one content, as many as its function calls. So the request after any turn with two or more tool calls (the default parallel strategy) could be rejected. The three request builders now merge consecutive same-role messages, keeping every block in order: the results in call order, then whatever followed; on Gemini and Vertex a merged turn's function responses come first, then any images and text (images among the responses are a reported cause of Gemini's response-count 400). Pinned by request-body tests; not run against a live endpoint.

## yoagent-rutis Unreleased
## yoagent-rutis 0.8.2 (2026-10-23)

yoagent-rutis only; requires yoagent 0.25, rutis 0.12 and Rust 1.88, as 0.8.1. Additive.

Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "yoagent"
version = "0.25.12"
version = "0.25.13"
edition = "2021"
# MSRV-aware dependency resolution (cargo 1.84+): prefer dependency versions
# whose own rust-version fits ours. Without it CI re-resolves to whatever is
Expand Down
2 changes: 1 addition & 1 deletion integrations/yoagent-rutis/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "yoagent-rutis"
version = "0.8.1"
version = "0.8.2"
edition = "2021"
resolver = "3"
description = "Extend yoagent agents at runtime with rutis plugins, as one yoagent Extension: tools, tool policy, output filters, turn notes, input checks, verifiers and events"
Expand Down
11 changes: 10 additions & 1 deletion skills/yoagent/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: Use when building, extending or upgrading an agent on the yoagent R

# yoagent: which feature for which job

Written for **yoagent 0.25.12** (and yoagent-rutis 0.8.1); the recipes
Written for **yoagent 0.25.13** (and yoagent-rutis 0.8.2); the recipes
below are compiled by `tests/skill_test.rs`, line for line. Check what you
build against: `cargo tree -p yoagent` or `Cargo.lock`. If you are on an
older version, read [What changed](#what-changed) first.
Expand Down Expand Up @@ -198,6 +198,11 @@ and its examples.

Newest first; the full list is `CHANGELOG.md`.

- **0.25.13** — Bedrock, Gemini and Vertex: a parallel tool call's
results go back in one message (consecutive same-role messages are
merged; on Gemini/Vertex the function responses come first), and an
empty message (an errored or cancelled turn) is no longer sent, so the
request after a multi-tool turn or a failed run is accepted.
- **0.25.12** — `provider::api_key_variables()`: every environment
variable yoagent reads a credential from (providers', Bedrock's `AWS_*`,
the `YOAGENT_API_KEY` / `API_KEY` fallback, the decision presets'). To
Expand Down Expand Up @@ -274,6 +279,10 @@ Newest first; the full list is `CHANGELOG.md`.
- **0.24.2** — built-in tool sandbox fixes (path escapes, `search` flag
injection, Gemini key out of the URL, tool panics contained).
- **0.24.1** — `retry_safe_events`.
- **yoagent-rutis 0.8.2** — feature `embedded-adapters`:
`adapters::files()` is the pi and DSH adapter files built into the
binary, for hosts installed without the crate's source (write them under
one folder per version, then `npm ci` in its `pi/` and `dsh/`).
- **yoagent-rutis 0.8.1** — `probe::pi_with` / `probe::dsh_with` with
`ProbeOptions::default().env(name, value)`: variables for the probe's
runtime (an empty value blanks one; the DSH probe's own homes win).
Expand Down
4 changes: 2 additions & 2 deletions src/provider/prices.json
Original file line number Diff line number Diff line change
Expand Up @@ -267,9 +267,9 @@
"input": 1.36,
"output": 4.18,
"cache_read": 0.14,
"note": "Regular price. Mistral's launch sale (50% off for two weeks from 2026-10-06) bills half of it until about 2026-10-20.",
"note": "Regular price; Mistral's launch sale (50% off from 2026-10-06) has ended.",
"source": "https://docs.mistral.ai/models/mistral-large-4-0",
"verified": "2026-10-18"
"verified": "2026-10-23"
}
},
"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": 0.68,
"output": 2.09,
"cache_read": 0.07
"input": 1.36,
"output": 4.18,
"cache_read": 0.14
},
"canonical_model_id": "mistral/mistral-large-4"
}
Expand Down
22 changes: 12 additions & 10 deletions tests/price_audit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,8 @@ const PEAK_IS_TWICE_UPSTREAM: &[&str] = &["deepseek"];
const PROMO_IS_HALF_REGULAR: &[(&str, &str)] = &[
("google", "gemini-4-argon"),
("google", "gemini-3.8-flash"),
("mistral", "mistral-large-4"),
// Mistral Large 4 left on 2026-10-23: its launch sale ended, and
// models.dev lists the regular rate the crate carries.
];

/// Whether the crate's `input`/`output`/`cache_read` are twice models.dev's
Expand Down Expand Up @@ -909,11 +910,12 @@ fn the_audit_logic_runs_offline_against_the_fixture() {
// Muse Spark 1.2, and the three DeepSeek entries; and Mistral Large 4's,
// unset on both sides.
assert_eq!(o.absent, 7);
// A promotional entry is compared at twice models.dev's rate.
// Mistral Large 4's launch sale is over: compared at models.dev's own
// rate, with no promotion note.
assert!(
o.notes
!o.notes
.iter()
.any(|n| n.starts_with("mistral/mistral-large-4: regular rate")),
.any(|n| n.starts_with("mistral/mistral-large-4")),
"{:?}",
o.notes
);
Expand Down Expand Up @@ -960,17 +962,17 @@ fn the_audit_logic_runs_offline_against_the_fixture() {
.any(|d| d.contains("twice models.dev's off-peak")),
"{off_peak:?}"
);
// A promotional entry carried at the promotional rate is drift: the
// crate carries the regular one.
let promo = drift_after(&|p| {
// An entry left at a sale price after the sale ended is drift, compared
// directly (Mistral Large 4 left `PROMO_IS_HALF_REGULAR` on 2026-10-23).
let stale_sale = drift_after(&|p| {
if p.model == "mistral-large-4" {
p.cost.input_per_million = 0.68;
}
});
assert_eq!(promo.len(), 1, "{promo:?}");
assert_eq!(stale_sale.len(), 1, "{stale_sale:?}");
assert!(
promo[0].contains("twice models.dev's off-peak or promotional rate"),
"{promo:?}"
stale_sale[0].contains("input is 0.68 in the crate, 1.36 in models.dev"),
"{stale_sale:?}"
);
// Without the recorded gap, models.dev's `reasoning` rate is unknown
// structure.
Expand Down
12 changes: 1 addition & 11 deletions tests/price_fetch_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,18 +151,8 @@ 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.)
let changes = t.changes_from(&PriceTable::builtin());
// Except a model on a vendor promotion: the snapshot carries the regular
// rate, models.dev the promotional half (what a live fetch installs).
let promo = |c: &yoagent::provider::prices::PriceChange| {
c.provider == "mistral" && c.model == "mistral-large-4"
};
let mistral = changes.iter().find(|c| promo(c)).expect("the promotion");
assert_eq!(
mistral.after.as_ref().map(|a| a.input_per_million),
Some(0.68)
);
let changes: Vec<_> = changes.into_iter().filter(|c| !promo(c)).collect();
assert!(
changes.iter().all(|c| c.before.is_none()),
"{:?}",
Expand Down
Loading