From 6185f52db62cf0d63189d0662cafc856216312ef Mon Sep 17 00:00:00 2001 From: Claude Code Date: Mon, 26 Oct 2026 11:38:21 -0700 Subject: [PATCH 1/2] List a granted private model in the harnesses' account model list Found testing the 0.8.14 harnesses against PRODUCTION with only an API key (WALLY_CLOUD_KEY): `wally deepsec|vvah|shannon -m deepseek-v4.1-flash-1017- uncensored` refused it, "not available on this account", although the same key calls it (200) and the catalogue lists it. The CLI built its list from GET /v1/models alone, and the gateway strips every `listed = false` (private, grant-only) model from that list for EVERY key, so the one account entitled to it could never pick it. The control plane's catalogue (GET /v1/models/catalog) is grant-aware -- it names a private model only to an account that holds it -- and account_models_with already fetched it for prices. It now also adds any non-decisions-only model the catalogue names that /v1/models omitted (no limits: the catalogue carries price only, so the entry has no window, as primary_first does for a model it knows by id). An account without the grant still never sees the model (the catalogue does not list it). Fixed in the CLI rather than the gateway filter: that filter is an ASGI wrapper over response bytes with no caller identity, and a wrong change there would leak the private id to every key. Red before / green after: a_granted_private_model_the_gateway_list_omits_is_ added_from_the_catalogue FAILED, then passed; the no-grant and listed-once guards pass throughout. cargo test --lib (serial): 621 passed; clippy -D warnings and fmt clean. Verified live against production with an API key: all three harnesses now accept the model. Co-Authored-By: Claude Sonnet 5.5 --- src/harness/catalog_models.rs | 91 +++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/src/harness/catalog_models.rs b/src/harness/catalog_models.rs index f5879712..abfb81e9 100644 --- a/src/harness/catalog_models.rs +++ b/src/harness/catalog_models.rs @@ -153,6 +153,27 @@ pub fn account_models_with( reasoning: info.reasoning.clone(), }); } + // A private, grant-only model is stripped from the gateway's /v1/models for + // every key (so the id never reaches an account that was not granted it), + // but the CATALOGUE is grant-aware: it lists a private model only to an + // account that holds it. Add what the catalogue names that /v1/models left + // out, so a granted account can pick the model it can already call. The + // catalogue carries price but not limits, so these entries get no window + // (the same shape `primary_first` uses for a model it only knows by id). + // Decisions-only ids stay out: they are the one thing the catalogue names + // that is deliberately not a chat model. + for price in &prices { + if price.id.is_empty() || price.decisions_only || out.iter().any(|m| m.id == price.id) { + continue; + } + out.push(CatalogModel { + id: price.id.clone(), + input_per_mtok: price.input_per_mtok, + output_per_mtok: price.output_per_mtok, + cached_input_per_mtok: price.cached_input_per_mtok, + ..Default::default() + }); + } (result, out, error) } @@ -257,3 +278,73 @@ mod effort_tests { assert_eq!(body["reasoning_effort"], "minimal"); } } + +#[cfg(test)] +mod granted_model_tests { + use super::*; + use crate::account::console::{HttpResponse, Transport}; + use std::sync::Arc; + + const GATED: &str = "deepseek-v4.1-flash-1017-uncensored"; + + /// A control plane where `/v1/models` is the gateway's list (a private, + /// grant-only model is stripped from it) and the catalogue is the + /// grant-aware one (it names the model the account was granted). + fn console(models_body: &'static str, catalog_body: &'static str) -> ConsoleClient { + let transport: Transport = Arc::new(move |request| { + let body = if request.url.ends_with("/v1/models/catalog") { + catalog_body + } else if request.url.ends_with("/v1/models") { + models_body + } else { + return Err(format!("unexpected url {}", request.url)); + }; + Ok(HttpResponse { + status: 200, + body: body.to_string(), + headers: Default::default(), + }) + }); + ConsoleClient::new(Some(transport)) + } + + const MODELS: &str = r#"{"object":"list","data":[{"id":"glm-5.3-flash","object":"model"}]}"#; + const CATALOG_WITH_GRANT: &str = r#"{"pricing_version":"v","effective_from":"2026-10-26T00:00:00Z","models":[ + {"id":"glm-5.3-flash","display_name":"GLM","input_per_mtok":272000,"output_per_mtok":650000,"cached_input_per_mtok":76000,"decisions_only":false,"decisions_image_input":false}, + {"id":"deepseek-v4.1-flash-1017-uncensored","display_name":"U","input_per_mtok":230000,"output_per_mtok":900000,"cached_input_per_mtok":22000,"decisions_only":false,"decisions_image_input":false}]}"#; + const CATALOG_WITHOUT_GRANT: &str = r#"{"pricing_version":"v","effective_from":"2026-10-26T00:00:00Z","models":[ + {"id":"glm-5.3-flash","display_name":"GLM","input_per_mtok":272000,"output_per_mtok":650000,"cached_input_per_mtok":76000,"decisions_only":false,"decisions_image_input":false}]}"#; + + #[test] + fn a_granted_private_model_the_gateway_list_omits_is_added_from_the_catalogue() { + let (result, models, _) = account_models_with( + &console(MODELS, CATALOG_WITH_GRANT), + "https://c.example", + "k", + ); + assert!(matches!(result, IdentityResult::Ok)); + let gated = models + .iter() + .find(|m| m.id == GATED) + .expect("granted model is listed"); + assert_eq!(gated.input_per_mtok, 230000, "priced from the catalogue"); + } + + #[test] + fn an_account_without_the_grant_never_gets_the_private_model() { + let (_, models, _) = account_models_with( + &console(MODELS, CATALOG_WITHOUT_GRANT), + "https://c.example", + "k", + ); + assert!(models.iter().all(|m| m.id != GATED), "no grant, no listing"); + } + + #[test] + fn a_model_in_both_lists_is_listed_once() { + let both = r#"{"object":"list","data":[{"id":"glm-5.3-flash","object":"model"},{"id":"deepseek-v4.1-flash-1017-uncensored","object":"model"}]}"#; + let (_, models, _) = + account_models_with(&console(both, CATALOG_WITH_GRANT), "https://c.example", "k"); + assert_eq!(models.iter().filter(|m| m.id == GATED).count(), 1); + } +} From a73c11637ccb0e626b421e78be156763cb48b776 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Mon, 26 Oct 2026 11:50:58 -0700 Subject: [PATCH 2/2] release: 0.8.15 0.8.14 shipped the three hosted harnesses (shannon, deepsec, vvah) before the account model-list fix in the previous commit: with only an API key (WALLY_CLOUD_KEY) every harness refused the gated deepseek-v4.1-flash-1017-uncensored although the key calls it. 0.8.15 carries that fix, so the gated model works from the CLI for API-key users. versions.toml, Cargo.toml, Cargo.lock and the Homebrew formula move together; scripts/ci/check-versions.py: "versions.toml consistent: product 0.8.15". Co-Authored-By: Claude Sonnet 5.5 --- Cargo.lock | 2 +- Cargo.toml | 2 +- Formula/wally.rb | 6 +++--- versions.toml | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bceef897..6d7ee988 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1410,7 +1410,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "wally" -version = "0.8.14" +version = "0.8.15" dependencies = [ "brotli-decompressor", "ctrlc", diff --git a/Cargo.toml b/Cargo.toml index b941c579..c5ba3de8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ [package] name = "wally" # Must equal [product] version in versions.toml (scripts/ci/check-versions.py). -version = "0.8.14" +version = "0.8.15" edition = "2021" rust-version = "1.93" license = "Apache-2.0" diff --git a/Formula/wally.rb b/Formula/wally.rb index 5b7bc9ea..5841fa12 100644 --- a/Formula/wally.rb +++ b/Formula/wally.rb @@ -3,15 +3,15 @@ class Wally < Formula desc "Run models on your machine or your RunAnywhere account" homepage "https://github.com/RunanywhereAI/wally" - version "0.8.14" + version "0.8.15" license "MIT" # macOS arm64 ships the Swift MLX host # (llama.cpp + ONNX + Sherpa + MLX). Linux is not in this cut. on_macos do on_arm do - url "https://github.com/RunanywhereAI/wally/releases/download/v0.8.14/wally-0.8.14-macos-arm64.tar.gz" - # Placeholder -- the v0.8.14 release has not published a wally-named asset + url "https://github.com/RunanywhereAI/wally/releases/download/v0.8.15/wally-0.8.15-macos-arm64.tar.gz" + # Placeholder -- the v0.8.15 release has not published a wally-named asset # yet. scripts/release/update-tap.sh re-stamps this from the real release # checksum; a stale value here fails brew install's own hash check # rather than installing something unverified. diff --git a/versions.toml b/versions.toml index d5174e0c..279ed8ae 100644 --- a/versions.toml +++ b/versions.toml @@ -7,7 +7,7 @@ [product] # wally's own release version; git tag is v (auto-tag.yml reads it here). -version = "0.8.14" +version = "0.8.15" [contracts] # Vendored from InferenceInfra at this exact revision.