From 2ee69cbae2d3f6a483046f4d1f66e16bcbb409ae Mon Sep 17 00:00:00 2001 From: thestreamcode Date: Sun, 8 Nov 2026 20:34:45 +0100 Subject: [PATCH 1/2] fix: report pay-as-you-go and missing media categories --- CHANGELOG.md | 15 +++ Cargo.lock | 8 +- README.md | 4 + .../chutes-build-core/src/account_summary.rs | 92 +++++++++++++++---- crates/codegen/chutes-build/Cargo.toml | 2 +- crates/codegen/chutes-pager/Cargo.toml | 2 +- .../docs/user-guide/04-slash-commands.md | 10 +- .../docs/user-guide/25-chutes-ecosystem.md | 35 ++++--- .../codegen/chutes-pager/src/account_cmd.rs | 53 +++++++++++ crates/codegen/chutes-shell/Cargo.toml | 2 +- .../chutes-tools-api/src/slash_commands.rs | 6 +- .../src/implementations/chutes/media.rs | 38 +++++++- crates/codegen/chutes-version/Cargo.toml | 2 +- docs/product-spec.md | 11 ++- package.json | 14 +-- 15 files changed, 230 insertions(+), 64 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4834f1dd..7e678f29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,21 @@ All notable changes to Chutes Build will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.0.11] - 2026-11-08 + +### Changed + +- Chutes now bills pay-as-you-go: `chutes-build account` and `/account` report + **Pay-as-you-go**, omit monthly price/cap values, hide the Plus/Pro plan + reference, and keep the rolling four-hour and daily request windows plus the + PAYG balance. + +### Fixed + +- `list_media_models` now returns an explicit message when a category has no + available model (for example video), and `/imagine`/`/imagine-video` tell the + user that generation is unavailable instead of inventing a model. + ## [2.0.10] - 2026-10-31 ### Fixed diff --git a/Cargo.lock b/Cargo.lock index 703c30a2..cdf1c269 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1853,7 +1853,7 @@ dependencies = [ [[package]] name = "chutes-build" -version = "2.0.10" +version = "2.0.11" dependencies = [ "anyhow", "chutes-acp-lib", @@ -2814,7 +2814,7 @@ dependencies = [ [[package]] name = "chutes-pager" -version = "2.0.10" +version = "2.0.11" dependencies = [ "agent-client-protocol", "ansi-to-tui", @@ -3316,7 +3316,7 @@ dependencies = [ [[package]] name = "chutes-shell" -version = "2.0.10" +version = "2.0.11" dependencies = [ "agent-client-protocol", "anyhow", @@ -3919,7 +3919,7 @@ dependencies = [ [[package]] name = "chutes-version" -version = "2.0.10" +version = "2.0.11" dependencies = [ "semver", ] diff --git a/README.md b/README.md index f27f8432..75f19510 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,10 @@ The TUI degrades decorative glyphs on the legacy Windows console: arrows, checkm Browser automation needs Chrome or Edge, or an explicit `CHUTES_BROWSER_EXECUTABLE`. Video inspection needs FFmpeg, or an explicit `CHUTES_FFMPEG_EXECUTABLE`; install it and restart the process. +### No model available for a media category + +Chutes media models come and go. If the live catalog has no model for a requested category, `/imagine`, `/imagine-video`, and `generate_media` report that no model is currently available and do not invent one. For example, when no video model is published, video generation cannot run until Chutes adds one; the same applies to image, music, and speech. Pick another category or retry later. + ## Development Clone the repository and build with the pinned toolchain: diff --git a/crates/chutes-build-core/src/account_summary.rs b/crates/chutes-build-core/src/account_summary.rs index a370753e..4f114f64 100644 --- a/crates/chutes-build-core/src/account_summary.rs +++ b/crates/chutes-build-core/src/account_summary.rs @@ -486,7 +486,7 @@ fn normalize_usage_windows( ); let label = pick_string(nested, payload, &[], &["label"]) .unwrap_or_else(|| candidate.label.to_string()); - if nested.is_none() && used.is_none() && limit.is_none() && remaining.is_none() { + if used.is_none() && limit.is_none() && remaining.is_none() { return None; } Some(UsageWindow { @@ -600,24 +600,11 @@ fn normalize_plan(payload: &serde_json::Value, windows: &[UsageWindow]) -> Optio let get = |map: &serde_json::Map, key: &str| { map.get(key).and_then(|v| as_number(Some(v))) }; - let plan_name = source - .get("name") - .and_then(|v| as_string(Some(v))) - .or_else(|| as_string(object.get("plan_name"))) - .or_else(|| derive_plan_name(object)); - let monthly_price_usd = - get(source, "monthly_price").or_else(|| as_number(object.get("monthly_price"))); - let monthly_cap_usd = get(source, "monthly_cap_usd") - .or_else(|| as_number(object.get("monthly_cap_usd"))) - .or_else(|| as_number(object.get("billing_cycle_limit"))) - .or_else(|| { - object - .get("monthly") - .and_then(|v| v.as_object()) - .and_then(|inner| as_number(inner.get("cap"))) - }) - .or_else(|| billing_cap.and_then(|inner| as_number(inner.get("limit")))) - .or_else(|| billing_window.and_then(|w| w.limit)); + let monthly = object.get("monthly").and_then(|v| v.as_object()); + let pay_as_you_go = monthly + .and_then(|inner| inner.get("uncapped")) + .and_then(|v| v.as_bool()) + == Some(true); let four_hour_cap_usd = get(source, "four_hour_cap_usd") .or_else(|| as_number(object.get("four_hour_cap_usd"))) .or_else(|| as_number(object.get("four_hour_limit"))) @@ -637,6 +624,31 @@ fn normalize_plan(payload: &serde_json::Value, windows: &[UsageWindow]) -> Optio let payg_discount_percent = get(source, "payg_discount_percent") .or_else(|| as_number(object.get("payg_discount_percent"))); + if pay_as_you_go { + return Some(PlanInfo { + plan_name: Some("Pay-as-you-go".to_string()), + monthly_price_usd: None, + monthly_cap_usd: None, + four_hour_cap_usd, + daily_request_limit, + payg_discount_percent, + }); + } + + let plan_name = source + .get("name") + .and_then(|v| as_string(Some(v))) + .or_else(|| as_string(object.get("plan_name"))) + .or_else(|| derive_plan_name(object)); + let monthly_price_usd = + get(source, "monthly_price").or_else(|| as_number(object.get("monthly_price"))); + let monthly_cap_usd = get(source, "monthly_cap_usd") + .or_else(|| as_number(object.get("monthly_cap_usd"))) + .or_else(|| as_number(object.get("billing_cycle_limit"))) + .or_else(|| monthly.and_then(|inner| as_number(inner.get("cap")))) + .or_else(|| billing_cap.and_then(|inner| as_number(inner.get("limit")))) + .or_else(|| billing_window.and_then(|w| w.limit)); + if plan_name.is_none() && monthly_price_usd.is_none() && monthly_cap_usd.is_none() @@ -683,6 +695,10 @@ fn derive_plan_name(payload: &serde_json::Map) -> Opt } fn normalize_plan_limits(plan: Option<&PlanInfo>) -> Vec { + if plan.and_then(|p| p.plan_name.as_deref()) == Some("Pay-as-you-go") { + return Vec::new(); + } + let references = [("Plus", 10.0, 6.0), ("Pro", 20.0, 10.0)]; let mut entries: Vec = references .iter() @@ -816,6 +832,44 @@ mod tests { assert_eq!(daily.reset_label.as_deref(), Some("Possible sync delay")); } + #[test] + fn uncapped_monthly_payload_is_pay_as_you_go() { + let snapshot = serde_json::json!({ + "subscription_usage": { + "subscription": true, + "custom": true, + "monthly_price": 20, + "monthly": {"uncapped": true}, + "four_hour": {"usage": 0, "cap": 8.333333333333332, "remaining": 8.333333333333332} + }, + "quotas": [{"chute_id": "*", "quota": 5001, "model": "All Models"}], + "quota_usage": {"used": 0, "quota": 5001}, + "model_stats": serde_json::Value::Null, + }); + let profile = serde_json::json!({"balance": 53.50094759099999}); + let summary = summarize_snapshot(&snapshot, &profile); + let plan = summary.plan.expect("plan"); + assert_eq!(plan.plan_name.as_deref(), Some("Pay-as-you-go")); + assert_eq!(plan.monthly_price_usd, None); + assert_eq!(plan.monthly_cap_usd, None); + assert_eq!(plan.four_hour_cap_usd, Some(8.333333333333332)); + assert_eq!(plan.daily_request_limit, Some(5001.0)); + assert!( + summary + .windows + .iter() + .all(|w| w.kind != UsageWindowKind::BillingCycle) + ); + assert!( + summary + .windows + .iter() + .any(|w| w.kind == UsageWindowKind::Rolling4h) + ); + assert!(summary.plan_limits.is_empty()); + assert_eq!(summary.payg_credit_usd, Some(53.50094759099999)); + } + #[test] fn plan_derives_from_price_and_flags() { let snapshot = serde_json::json!({ diff --git a/crates/codegen/chutes-build/Cargo.toml b/crates/codegen/chutes-build/Cargo.toml index 870cb47a..4f7e55c6 100644 --- a/crates/codegen/chutes-build/Cargo.toml +++ b/crates/codegen/chutes-build/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "chutes-build" -version = "2.0.10" +version = "2.0.11" edition.workspace = true license = "Apache-2.0" authors = ["Michael Gasperini (Mikesoft)"] diff --git a/crates/codegen/chutes-pager/Cargo.toml b/crates/codegen/chutes-pager/Cargo.toml index f19dbba4..c883e314 100644 --- a/crates/codegen/chutes-pager/Cargo.toml +++ b/crates/codegen/chutes-pager/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "chutes-pager" -version = "2.0.10" +version = "2.0.11" edition.workspace = true license = "Apache-2.0" authors = ["Chutes Build contributors"] diff --git a/crates/codegen/chutes-pager/docs/user-guide/04-slash-commands.md b/crates/codegen/chutes-pager/docs/user-guide/04-slash-commands.md index ca840fc9..36897812 100644 --- a/crates/codegen/chutes-pager/docs/user-guide/04-slash-commands.md +++ b/crates/codegen/chutes-pager/docs/user-guide/04-slash-commands.md @@ -248,7 +248,9 @@ list changes over time, so no model is hardcoded: `/imagine ` picks a currently available image model from the live Chutes catalog, and `/imagine ` uses the model you name (`--model ` is the unambiguous form). Bare `/imagine` lists the image models that are -available right now. +available right now. If no image model is available, it says so explicitly +instead of inventing a model; image generation stays unavailable until Chutes +publishes one. ``` /imagine a golden sunset over a calm ocean with silhouetted palm trees @@ -263,7 +265,9 @@ generates source images, and animates them with `generate_media`. As with `/imagine`, the model is chosen from the live catalog unless you name one: `/imagine-video ` auto-selects, `/imagine-video ` (or `--model `) uses your choice, and bare -`/imagine-video` lists the video models available right now. +`/imagine-video` lists the video models available right now. If no video model +is available, it says so explicitly instead of inventing a model; video +generation stays unavailable until Chutes publishes one. ``` /imagine-video a cat playing piano in a jazz club @@ -444,7 +448,7 @@ Unavailable: this build has no account sign-in. ### `/account` -Show the Chutes plan, quotas, usage windows, and PAYG credit for the +Show the Chutes billing mode, quotas, usage windows, and PAYG credit for the configured API key. Same data as `chutes-build account` (which also offers `--json` for scripts). diff --git a/crates/codegen/chutes-pager/docs/user-guide/25-chutes-ecosystem.md b/crates/codegen/chutes-pager/docs/user-guide/25-chutes-ecosystem.md index 2de8b8ad..36344dda 100644 --- a/crates/codegen/chutes-pager/docs/user-guide/25-chutes-ecosystem.md +++ b/crates/codegen/chutes-pager/docs/user-guide/25-chutes-ecosystem.md @@ -50,21 +50,20 @@ stats are resolved by `chute_id` against the live catalogue, so model names are shown instead of private chute labels. When a saved `default` pool exists, Auto targets `default:latency`, so the pool configured in the Chutes web app wins over the client-side inline pool. TEE and PAYG tool-capable rows are pooled -together now that subscriptions are ending. +together now that Chutes bills pay-as-you-go. Pool writes are not part of the supported surface yet: the Chutes alias endpoint is eventually consistent and intermittently returns 502. `/routing` and the plain `routing` command stay read-only. Some catalog entries have `confidential_compute: false`: they run on an external -provider and require a positive PAYG balance, separately from any subscription -quota. The beta models in this group (Qwen3.8-Max, DeepSeek-V4.1-Flash, GLM-5.3, -Kimi-K2.7-Code, GLM-5.2-Fast, Qwen3.8-2.4T-A95B) are PAYG-only and not TEE. When -that balance is not positive Chutes returns -`402 A positive balance is required for external provider execution`. This is a -PAYG funding condition, not the weekly or monthly subscription limit; top up the -PAYG balance at https://chutes.ai/?_s=usage or use a TEE model. The picker marks -those entries with a `PAYG` notice. +provider and require a positive PAYG balance. The beta models in this group +(Qwen3.8-Max, DeepSeek-V4.1-Flash, GLM-5.3, Kimi-K2.7-Code, GLM-5.2-Fast, +Qwen3.8-2.4T-A95B) are PAYG-only and not TEE. When that balance is not positive +Chutes returns `402 A positive balance is required for external provider +execution`. This is a PAYG funding condition, not the rolling 4-hour or daily +request limit; top up the PAYG balance at https://chutes.ai/?_s=usage or use a +TEE model. The picker marks those entries with a `PAYG` notice. TEE models run inside hardware enclaves and expose attestation evidence through `GET https://api.chutes.ai/chutes/{chute_id}/evidence`; do not claim verified @@ -158,14 +157,14 @@ the gap and separates documented facts from inference. ## Plan and quota indicator -The status bar shows the account plan plus the rolling four-hour and monthly -percentages when both are available. Its severity follows the highest consumed -active window: green is below 80%, warning color begins at 80%, and the error -color is used at 100% or above. Click the indicator or run `/usage` to inspect -every active window and its reset time. +The status bar shows the account billing mode (for example **Pay-as-you-go**) +plus the rolling four-hour and daily percentages when available. Its severity +follows the highest consumed active window: green is below 80%, warning color +begins at 80%, and the error color is used at 100% or above. Click the indicator +or run `/usage` to inspect every active window and its reset time. -Subscription usage and quota metadata are loaded concurrently. When aggregate -quota usage is not available, Chutes Build queries the documented per-chute -quota endpoints concurrently and combines their used/limit values. The compact +Billing usage and quota metadata are loaded concurrently. When aggregate quota +usage is not available, Chutes Build queries the documented per-chute quota +endpoints concurrently and combines their used/limit values. The compact indicator and detailed view preserve whether each window is daily, four-hour, -weekly, or monthly. +weekly, or any other window the API returns. diff --git a/crates/codegen/chutes-pager/src/account_cmd.rs b/crates/codegen/chutes-pager/src/account_cmd.rs index 762e0366..68255e94 100644 --- a/crates/codegen/chutes-pager/src/account_cmd.rs +++ b/crates/codegen/chutes-pager/src/account_cmd.rs @@ -208,6 +208,59 @@ mod tests { } } + #[test] + fn human_dashboard_reports_pay_as_you_go_without_monthly_caps() { + let summary = AccountSummary { + windows: vec![ + UsageWindow { + id: "four_hour_window".to_string(), + kind: UsageWindowKind::Rolling4h, + label: "4-Hour Window".to_string(), + unit: UsageUnit::Usd, + used: Some(0.0), + limit: Some(8.33), + remaining: Some(8.33), + percent_used: Some(0.0), + reset_label: None, + status: None, + data_source: None, + }, + UsageWindow { + id: "quotas-daily".to_string(), + kind: UsageWindowKind::DailyRequests, + label: "Daily Quota".to_string(), + unit: UsageUnit::Requests, + used: Some(0.0), + limit: Some(5001.0), + remaining: Some(5001.0), + percent_used: Some(0.0), + reset_label: None, + status: Some("trusted".to_string()), + data_source: Some("quota-usage".to_string()), + }, + ], + quotas: Vec::new(), + plan: Some(PlanInfo { + plan_name: Some("Pay-as-you-go".to_string()), + monthly_price_usd: None, + monthly_cap_usd: None, + four_hour_cap_usd: Some(8.33), + daily_request_limit: Some(5001.0), + payg_discount_percent: None, + }), + plan_limits: Vec::new(), + payg_credit_usd: Some(53.5), + }; + + let text = format_human(&summary); + assert!(text.contains("Plan: Pay-as-you-go"), "{text}"); + assert!(!text.contains("/mo"), "{text}"); + assert!(!text.contains("monthly cap"), "{text}"); + assert!(text.contains("4-hour cap"), "{text}"); + assert!(text.contains("daily requests"), "{text}"); + assert!(text.contains("Credit: $53.50 PAYG balance"), "{text}"); + } + #[test] fn human_dashboard_degrades_without_data() { let text = format_human(&AccountSummary { diff --git a/crates/codegen/chutes-shell/Cargo.toml b/crates/codegen/chutes-shell/Cargo.toml index 6ff66248..e7ea76f3 100644 --- a/crates/codegen/chutes-shell/Cargo.toml +++ b/crates/codegen/chutes-shell/Cargo.toml @@ -1,7 +1,7 @@ [package] license = "Apache-2.0" name = "chutes-shell" -version = "2.0.10" +version = "2.0.11" edition.workspace = true [features] diff --git a/crates/codegen/chutes-tools-api/src/slash_commands.rs b/crates/codegen/chutes-tools-api/src/slash_commands.rs index 38d08dbb..1cb8f309 100644 --- a/crates/codegen/chutes-tools-api/src/slash_commands.rs +++ b/crates/codegen/chutes-tools-api/src/slash_commands.rs @@ -170,7 +170,7 @@ fn imagine_model_list_instruction(kind: &str) -> String { "imagine-video" }; format!( - "Call list_media_models with kind \"{kind}\" and limit 20, then present the available {kind} models as a compact list: slug (or name), id, and a one-line reason to pick it. Do not generate anything yet. Close with the three forms the user can run: `/{command} ` lets the tool pick an available model automatically, `/{command} ` chooses one of the listed models, and `/{command} --model ` is the unambiguous form. Only list models that appear in the tool result; never invent one." + "Call list_media_models with kind \"{kind}\" and limit 20, then present the available {kind} models as a compact list: slug (or name), id, and a one-line reason to pick it. Do not generate anything yet. If the tool result has count 0, tell the user clearly that no {kind} model is currently available and that {kind} generation cannot be performed right now; do not invent a model or claim that generation succeeded. Close with the three forms the user can run: `/{command} ` lets the tool pick an available model automatically, `/{command} ` chooses one of the listed models, and `/{command} --model ` is the unambiguous form. Only list models that appear in the tool result; never invent one." ) } @@ -343,6 +343,8 @@ mod tests { let text = imagine_instruction(""); assert!(text.contains("list_media_models")); assert!(!text.contains("generate_media")); + assert!(text.contains("count 0")); + assert!(text.contains("no image model is currently available")); } #[test] @@ -376,6 +378,8 @@ mod tests { let text = imagine_video_instruction(""); assert!(text.contains("list_media_models")); assert!(text.contains("/imagine-video ")); + assert!(text.contains("count 0")); + assert!(text.contains("no video model is currently available")); } #[test] diff --git a/crates/codegen/chutes-tools/src/implementations/chutes/media.rs b/crates/codegen/chutes-tools/src/implementations/chutes/media.rs index a64349ea..42254f34 100644 --- a/crates/codegen/chutes-tools/src/implementations/chutes/media.rs +++ b/crates/codegen/chutes-tools/src/implementations/chutes/media.rs @@ -175,13 +175,22 @@ impl chutes_tool_runtime::Tool for ListMediaModelsTool { }) .take(limit) .collect::>(); - pretty_text( - serde_json::json!({ "count": models.len(), "models": models }), - "list_media_models", - ) + let count = models.len(); + let mut result = serde_json::json!({ "count": count, "models": models }); + if count == 0 { + result["message"] = serde_json::Value::String(empty_media_message(input.kind)); + } + pretty_text(result, "list_media_models") } } +fn empty_media_message(kind: Option) -> String { + let category = kind.map_or("media", MediaKind::as_str); + format!( + "No {category} models are currently available in the Chutes catalog. {category} generation cannot be performed right now.", + ) +} + #[derive(Debug, Default)] pub struct DescribeMediaModelTool; @@ -2432,6 +2441,27 @@ mod tests { assert_eq!(catalog_identifier(&catalog, "missing"), None); } + #[test] + fn empty_media_message_names_the_missing_category() { + assert!( + empty_media_message(Some(MediaKind::Video)) + .contains("No video models are currently available") + ); + assert!( + empty_media_message(Some(MediaKind::Image)) + .contains("No image models are currently available") + ); + assert!( + empty_media_message(Some(MediaKind::Music)) + .contains("No music models are currently available") + ); + assert!( + empty_media_message(Some(MediaKind::Speech)) + .contains("No speech models are currently available") + ); + assert!(empty_media_message(None).contains("No media models are currently available")); + } + /// Video chutes call themselves "diffusion" and mention images (i2v), so the /// classifier must decide video first and never offer them for image requests. #[test] diff --git a/crates/codegen/chutes-version/Cargo.toml b/crates/codegen/chutes-version/Cargo.toml index 1b5a30e2..6d278f5d 100644 --- a/crates/codegen/chutes-version/Cargo.toml +++ b/crates/codegen/chutes-version/Cargo.toml @@ -1,7 +1,7 @@ [package] license = "Apache-2.0" name = "chutes-version" -version = "2.0.10" +version = "2.0.11" edition.workspace = true description = "Lockstepped chutes-build CLI version." diff --git a/docs/product-spec.md b/docs/product-spec.md index 8e82b0ea..836dc445 100644 --- a/docs/product-spec.md +++ b/docs/product-spec.md @@ -177,10 +177,11 @@ hop. primary is unavailable or failing, and MUST return an honest error when no fallback exists at all — never a silent wrong result. -4.4. Account data (plan, quotas, usage windows, PAYG credit) MUST be +4.4. Account data (billing mode, quotas, usage windows, PAYG credit) MUST be derivable from the API key alone and visible via `chutes-build account` -(human and `--json`) and `/account`. Failures (no key, network error) -MUST report honestly, never as fake zeros. +(human and `--json`) and `/account`. An uncapped monthly payload MUST render +as pay-as-you-go, without a monthly price, monthly cap, or Plus/Pro reference. +Failures (no key, network error) MUST report honestly, never as fake zeros. 4.5. Inference speed MUST be reported as output tokens per API second (`tok/s`): after each turn next to the elapsed time, and as a Speed row @@ -195,7 +196,9 @@ the model to it. The legacy `image_gen`/`image_edit`/`image_to_video`/ not serve and were deleted in 2.0.3 (see §7); no legacy media tool, client, or upstream Imagine model id may be reintroduced. Media batch caps are per tool name, and `generate_media` takes the image budget (8) -by name for all its calls. +by name for all its calls. When the live catalog has no model for a requested +kind, the tools MUST report that the category is unavailable instead of +inventing a model or attempting generation. ## 5. Docs and naming truth diff --git a/package.json b/package.json index 6f989899..377ad559 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "chutes-build", - "version": "2.0.10", + "version": "2.0.11", "description": "Privacy-first, open-source coding agent for the Chutes ecosystem", "license": "Apache-2.0", "author": { "name": "Michael Gasperini", "email": "michael@mikesoft.it", "url": "https://mikesoft.it" }, @@ -37,12 +37,12 @@ "check": "npm test && npm run verify:release && npm pack --dry-run" }, "optionalDependencies": { - "chutes-build-darwin-arm64": "2.0.10", - "chutes-build-darwin-x64": "2.0.10", - "chutes-build-linux-arm64-gnu": "2.0.10", - "chutes-build-linux-x64-gnu": "2.0.10", - "chutes-build-win32-arm64": "2.0.10", - "chutes-build-win32-x64": "2.0.10" + "chutes-build-darwin-arm64": "2.0.11", + "chutes-build-darwin-x64": "2.0.11", + "chutes-build-linux-arm64-gnu": "2.0.11", + "chutes-build-linux-x64-gnu": "2.0.11", + "chutes-build-win32-arm64": "2.0.11", + "chutes-build-win32-x64": "2.0.11" }, "keywords": [ "ai", From 3abbe630add12a68a127f6f898f57c3947ef6246 Mon Sep 17 00:00:00 2001 From: thestreamcode Date: Sun, 8 Nov 2026 20:58:47 +0100 Subject: [PATCH 2/2] fix(media): avoid indexing the catalog result --- .../src/implementations/chutes/media.rs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/crates/codegen/chutes-tools/src/implementations/chutes/media.rs b/crates/codegen/chutes-tools/src/implementations/chutes/media.rs index 42254f34..a30aab1b 100644 --- a/crates/codegen/chutes-tools/src/implementations/chutes/media.rs +++ b/crates/codegen/chutes-tools/src/implementations/chutes/media.rs @@ -176,10 +176,15 @@ impl chutes_tool_runtime::Tool for ListMediaModelsTool { .take(limit) .collect::>(); let count = models.len(); - let mut result = serde_json::json!({ "count": count, "models": models }); - if count == 0 { - result["message"] = serde_json::Value::String(empty_media_message(input.kind)); - } + let result = if count == 0 { + serde_json::json!({ + "count": count, + "models": models, + "message": empty_media_message(input.kind), + }) + } else { + serde_json::json!({ "count": count, "models": models }) + }; pretty_text(result, "list_media_models") } }