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
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
6 changes: 3 additions & 3 deletions Formula/wally.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
91 changes: 91 additions & 0 deletions src/harness/catalog_models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The new catalogue-only IDs appear in Codex’s picker but are not routable: Codex keeps only cached IDs in Runtime.catalog, so selecting one silently falls back to the launch model. Populate the runtime’s routable IDs from models as well as the cache.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At src/harness/catalog_models.rs, line 170:

<comment>The new catalogue-only IDs appear in Codex’s picker but are not routable: Codex keeps only cached IDs in `Runtime.catalog`, so selecting one silently falls back to the launch model. Populate the runtime’s routable IDs from `models` as well as the cache.</comment>

<file context>
@@ -153,6 +153,27 @@ pub fn account_models_with(
+            continue;
+        }
+        out.push(CatalogModel {
+            id: price.id.clone(),
+            input_per_mtok: price.input_per_mtok,
+            output_per_mtok: price.output_per_mtok,
</file context>

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)
}

Expand Down Expand Up @@ -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);
}
}
2 changes: 1 addition & 1 deletion versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

[product]
# wally's own release version; git tag is v<version> (auto-tag.yml reads it here).
version = "0.8.14"
version = "0.8.15"

[contracts]
# Vendored from InferenceInfra at this exact revision.
Expand Down
Loading