mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
feat(inference): register burst-v1 tier; SuperContext scout uses it (#4260)
This commit is contained in:
@@ -39,6 +39,7 @@ The router wraps several pre-created providers (Anthropic, OpenAI, Google, Groq,
|
||||
| `hint:vision` | A vision-capable model | Screenshots, image attachments, OCR |
|
||||
| `hint:summarize` | A model good at compression | Memory tree summary builders |
|
||||
| `hint:code` | A code-tuned model | Native coder turns |
|
||||
| `hint:burst` | A high-throughput, low-cost model | Cheap, latency-tolerant pre-flight sweeps — e.g. the SuperContext scout |
|
||||
|
||||
The exact mappings are configurable; the defaults ship sensible per-provider routes.
|
||||
|
||||
|
||||
@@ -44,6 +44,8 @@ New thread, first message
|
||||
|
||||
Because the scout is **read-only**, it can never take an action on a fresh thread — it only reads and summarizes. And because it runs in the harness rather than as an optional tool, the redundant `agent_prepare_context` tool is suppressed for that turn, so the agent doesn't do the work twice.
|
||||
|
||||
The scout runs on the **`burst` tier** (`hint = "burst"` → `burst-v1` on the managed backend) — a cheap, high-throughput, non-reasoning model. The sweep is a latency-tolerant pre-flight pass, so raw throughput on a fast model is a better fit than the pricier agentic/reasoning tiers. See [Automatic Model Routing](model-routing/README.md).
|
||||
|
||||
***
|
||||
|
||||
## Safety and robustness
|
||||
|
||||
@@ -101,6 +101,15 @@ pub const PRICING_TABLE: &[ModelPricing] = &[
|
||||
cached_input_per_mtok_usd: 0.003625,
|
||||
output_per_mtok_usd: 0.87,
|
||||
},
|
||||
// Burst tier — high-throughput, low-cost model; flat rate both directions,
|
||||
// no prompt cache (so cached rate mirrors the input rate). Used by the
|
||||
// SuperContext scout.
|
||||
ModelPricing {
|
||||
model: "burst-v1",
|
||||
input_per_mtok_usd: 0.208,
|
||||
cached_input_per_mtok_usd: 0.208,
|
||||
output_per_mtok_usd: 0.208,
|
||||
},
|
||||
// Vision tier — multimodal; estimate only. The backend's echoed
|
||||
// `charged_amount_usd` is authoritative when present.
|
||||
ModelPricing {
|
||||
@@ -252,6 +261,17 @@ mod tests {
|
||||
assert_eq!(p.output_per_mtok_usd, 15.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lookup_pricing_has_a_burst_row() {
|
||||
// The burst tier (SuperContext scout) must price from its own row —
|
||||
// NOT via the $3/$15 fallback, which would inflate first-turn scout cost
|
||||
// and could trip budget gates.
|
||||
let p = lookup_pricing("burst-v1");
|
||||
assert_eq!(p.model, "burst-v1");
|
||||
assert_eq!(p.input_per_mtok_usd, 0.208);
|
||||
assert_eq!(p.output_per_mtok_usd, 0.208);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lookup_pricing_falls_back_for_unknown_model() {
|
||||
let p = lookup_pricing("totally-unknown-model");
|
||||
|
||||
@@ -32,9 +32,11 @@ omit_profile = false
|
||||
omit_memory_md = false
|
||||
|
||||
[model]
|
||||
# Multi-step gathering loop (recall → maybe fetch → assess), so the agentic
|
||||
# tier fits — not the slow reasoning tier (the scout is meant to be cheap).
|
||||
hint = "agentic"
|
||||
# Multi-step gathering loop (recall → maybe fetch → assess). Rides the
|
||||
# high-throughput `burst` tier: the scout is a cheap, latency-tolerant,
|
||||
# non-reasoning pre-flight pass, so raw throughput on a fast model beats the
|
||||
# pricier agentic/reasoning tiers. Resolves to `burst-v1` on the managed backend.
|
||||
hint = "burst"
|
||||
|
||||
[tools]
|
||||
# Curated read-only context-gathering surface. No writes, no shell, no
|
||||
|
||||
@@ -988,6 +988,14 @@ mod tests {
|
||||
let def = find("context_scout");
|
||||
assert_eq!(def.agent_tier, AgentTier::Worker);
|
||||
assert_eq!(def.sandbox_mode, SandboxMode::ReadOnly);
|
||||
// Super-context scout rides the cheap, high-throughput `burst` tier
|
||||
// (resolves to `burst-v1` on the managed backend) — not the pricier
|
||||
// agentic/reasoning tiers.
|
||||
assert!(
|
||||
matches!(&def.model, ModelSpec::Hint(h) if h == "burst"),
|
||||
"context_scout must spawn on the burst tier, got {:?}",
|
||||
def.model
|
||||
);
|
||||
// Bundle cap — load-bearing for the parent's context budget. Leaves
|
||||
// room for the `recommended_skills` block alongside summary + plan.
|
||||
assert_eq!(def.max_result_chars, Some(5000));
|
||||
|
||||
@@ -48,10 +48,10 @@ pub use schema::{
|
||||
StorageProviderSection, StreamMode, TeamModelConfig, TelegramConfig, TokenjuiceConfig,
|
||||
UpdateConfig, UpdateRestartStrategy, VoiceActivationMode, VoiceServerConfig, WebSearchConfig,
|
||||
WebhookConfig, DEFAULT_CLOUD_LLM_MODEL, DEFAULT_MEMORY_SYNC_INTERVAL_SECS, DEFAULT_MODEL,
|
||||
MEMORY_SYNC_INTERVAL_PRESETS_SECS, MODEL_AGENTIC_V1, MODEL_CHAT_V1, MODEL_CODING_V1,
|
||||
MODEL_REASONING_QUICK_V1, MODEL_REASONING_V1, MODEL_SUMMARIZATION_V1, MODEL_VISION_V1,
|
||||
SEARCH_ENGINE_BRAVE, SEARCH_ENGINE_DISABLED, SEARCH_ENGINE_MANAGED, SEARCH_ENGINE_PARALLEL,
|
||||
SEARCH_ENGINE_QUERIT,
|
||||
MEMORY_SYNC_INTERVAL_PRESETS_SECS, MODEL_AGENTIC_V1, MODEL_BURST_V1, MODEL_CHAT_V1,
|
||||
MODEL_CODING_V1, MODEL_REASONING_QUICK_V1, MODEL_REASONING_V1, MODEL_SUMMARIZATION_V1,
|
||||
MODEL_VISION_V1, SEARCH_ENGINE_BRAVE, SEARCH_ENGINE_DISABLED, SEARCH_ENGINE_MANAGED,
|
||||
SEARCH_ENGINE_PARALLEL, SEARCH_ENGINE_QUERIT,
|
||||
};
|
||||
pub use schemas::{
|
||||
all_controller_schemas as all_config_controller_schemas,
|
||||
|
||||
@@ -150,7 +150,7 @@ impl Default for CostConfig {
|
||||
/// Default pricing for popular models (USD per 1M tokens)
|
||||
fn get_default_pricing() -> HashMap<String, ModelPricing> {
|
||||
use super::types::{
|
||||
MODEL_AGENTIC_V1, MODEL_CHAT_V1, MODEL_CODING_V1, MODEL_REASONING_QUICK_V1,
|
||||
MODEL_AGENTIC_V1, MODEL_BURST_V1, MODEL_CHAT_V1, MODEL_CODING_V1, MODEL_REASONING_QUICK_V1,
|
||||
MODEL_REASONING_V1,
|
||||
};
|
||||
|
||||
@@ -192,6 +192,14 @@ fn get_default_pricing() -> HashMap<String, ModelPricing> {
|
||||
output: 3.30,
|
||||
},
|
||||
);
|
||||
// Burst tier — high-throughput, low-cost model; flat rate both directions.
|
||||
prices.insert(
|
||||
MODEL_BURST_V1.into(),
|
||||
ModelPricing {
|
||||
input: 0.208,
|
||||
output: 0.208,
|
||||
},
|
||||
);
|
||||
|
||||
prices
|
||||
}
|
||||
|
||||
@@ -14,6 +14,10 @@ pub const MODEL_CHAT_V1: &str = "chat-v1";
|
||||
/// Legacy low-latency chat tier slug retained for older persisted configs.
|
||||
pub const MODEL_REASONING_QUICK_V1: &str = "reasoning-quick-v1";
|
||||
pub const MODEL_CODING_V1: &str = "coding-v1";
|
||||
/// High-throughput "burst" tier served by the managed backend. Cheap, fast,
|
||||
/// non-reasoning, text-only, 128k context, no prompt cache; used by the
|
||||
/// super-context scout. Managed-backend only (no BYOK knob).
|
||||
pub const MODEL_BURST_V1: &str = "burst-v1";
|
||||
pub const MODEL_SUMMARIZATION_V1: &str = "summarization-v1";
|
||||
/// Multimodal (image-input) tier. Managed backend serves this with the vision
|
||||
/// flag enabled; the vision sub-agent rides this tier via `hint:vision`.
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
//! metadata is not yet available.
|
||||
|
||||
use crate::openhuman::config::{
|
||||
MODEL_AGENTIC_V1, MODEL_CHAT_V1, MODEL_CODING_V1, MODEL_REASONING_QUICK_V1, MODEL_REASONING_V1,
|
||||
MODEL_AGENTIC_V1, MODEL_BURST_V1, MODEL_CHAT_V1, MODEL_CODING_V1, MODEL_REASONING_QUICK_V1,
|
||||
MODEL_REASONING_V1,
|
||||
};
|
||||
|
||||
/// Conservative default for OpenHuman abstract tier models (tokens).
|
||||
@@ -126,6 +127,9 @@ fn tier_context_window(model: &str) -> Option<u64> {
|
||||
MODEL_REASONING_V1 => Some(TIER_REASONING_CONTEXT),
|
||||
MODEL_AGENTIC_V1 | MODEL_CODING_V1 => Some(TIER_LARGE_CONTEXT),
|
||||
"summarization-v1" => Some(TIER_SUMMARIZATION_CONTEXT),
|
||||
// Burst tier advertises a 128k window on the managed backend. Matched on
|
||||
// the `burst-v1` alias before any substring fallbacks below.
|
||||
MODEL_BURST_V1 => Some(TIER_STANDARD_CONTEXT),
|
||||
MODEL_CHAT_V1 | MODEL_REASONING_QUICK_V1 | "chat" => Some(TIER_STANDARD_CONTEXT),
|
||||
m if m.starts_with("gemma") || m.contains(":1b") || m.contains("270m") => {
|
||||
Some(TIER_LOCAL_CONTEXT)
|
||||
@@ -277,6 +281,9 @@ mod tests {
|
||||
assert_eq!(context_window_for_model("reasoning-v1"), Some(1_000_000));
|
||||
assert_eq!(context_window_for_model("agentic-v1"), Some(200_000));
|
||||
assert_eq!(context_window_for_model("chat-v1"), Some(128_000));
|
||||
// Burst tier — 128k on the managed backend. Matched on the alias, not
|
||||
// the local-gemma 8k substring arm.
|
||||
assert_eq!(context_window_for_model("burst-v1"), Some(128_000));
|
||||
assert_eq!(
|
||||
context_window_for_model("reasoning-quick-v1"),
|
||||
Some(128_000)
|
||||
|
||||
@@ -77,7 +77,7 @@ pub(crate) const NO_MODEL_CONFIGURED_ANCHOR: &str = "resolved to an empty model
|
||||
|
||||
fn is_abstract_tier_model(model: &str) -> bool {
|
||||
use crate::openhuman::config::{
|
||||
MODEL_AGENTIC_V1, MODEL_CHAT_V1, MODEL_CODING_V1, MODEL_REASONING_QUICK_V1,
|
||||
MODEL_AGENTIC_V1, MODEL_BURST_V1, MODEL_CHAT_V1, MODEL_CODING_V1, MODEL_REASONING_QUICK_V1,
|
||||
MODEL_REASONING_V1, MODEL_SUMMARIZATION_V1, MODEL_VISION_V1,
|
||||
};
|
||||
let trimmed = model.trim();
|
||||
@@ -86,6 +86,7 @@ fn is_abstract_tier_model(model: &str) -> bool {
|
||||
|| trimmed == MODEL_CHAT_V1
|
||||
|| trimmed == MODEL_AGENTIC_V1
|
||||
|| trimmed == MODEL_CODING_V1
|
||||
|| trimmed == MODEL_BURST_V1
|
||||
|| trimmed == MODEL_VISION_V1
|
||||
|| trimmed == MODEL_SUMMARIZATION_V1
|
||||
}
|
||||
@@ -109,6 +110,7 @@ pub fn resolve_model_for_hint(hint_or_tier: &str, config: &Config) -> String {
|
||||
("chat", crate::openhuman::config::MODEL_CHAT_V1),
|
||||
("agentic", crate::openhuman::config::MODEL_AGENTIC_V1),
|
||||
("coding", crate::openhuman::config::MODEL_CODING_V1),
|
||||
("burst", crate::openhuman::config::MODEL_BURST_V1),
|
||||
("vision", crate::openhuman::config::MODEL_VISION_V1),
|
||||
(
|
||||
"summarization",
|
||||
@@ -125,6 +127,7 @@ pub fn resolve_model_for_hint(hint_or_tier: &str, config: &Config) -> String {
|
||||
(crate::openhuman::config::MODEL_REASONING_QUICK_V1, "chat"),
|
||||
(crate::openhuman::config::MODEL_AGENTIC_V1, "agentic"),
|
||||
(crate::openhuman::config::MODEL_CODING_V1, "coding"),
|
||||
(crate::openhuman::config::MODEL_BURST_V1, "burst"),
|
||||
(crate::openhuman::config::MODEL_VISION_V1, "vision"),
|
||||
(
|
||||
crate::openhuman::config::MODEL_SUMMARIZATION_V1,
|
||||
@@ -184,7 +187,7 @@ pub fn resolve_model_for_hint(hint_or_tier: &str, config: &Config) -> String {
|
||||
/// to the backend.
|
||||
pub(crate) fn is_known_openhuman_tier(model: &str) -> bool {
|
||||
use crate::openhuman::config::{
|
||||
MODEL_AGENTIC_V1, MODEL_CHAT_V1, MODEL_CODING_V1, MODEL_REASONING_QUICK_V1,
|
||||
MODEL_AGENTIC_V1, MODEL_BURST_V1, MODEL_CHAT_V1, MODEL_CODING_V1, MODEL_REASONING_QUICK_V1,
|
||||
MODEL_REASONING_V1, MODEL_SUMMARIZATION_V1, MODEL_VISION_V1,
|
||||
};
|
||||
matches!(
|
||||
@@ -193,6 +196,7 @@ pub(crate) fn is_known_openhuman_tier(model: &str) -> bool {
|
||||
| MODEL_CHAT_V1
|
||||
| MODEL_AGENTIC_V1
|
||||
| MODEL_CODING_V1
|
||||
| MODEL_BURST_V1
|
||||
| MODEL_REASONING_QUICK_V1
|
||||
| MODEL_SUMMARIZATION_V1
|
||||
| MODEL_VISION_V1
|
||||
@@ -200,6 +204,7 @@ pub(crate) fn is_known_openhuman_tier(model: &str) -> bool {
|
||||
| "hint:chat"
|
||||
| "hint:agentic"
|
||||
| "hint:coding"
|
||||
| "hint:burst"
|
||||
| "hint:summarization"
|
||||
| "hint:vision"
|
||||
)
|
||||
@@ -219,7 +224,7 @@ pub(crate) fn is_known_openhuman_tier(model: &str) -> bool {
|
||||
/// ([`crate::openhuman::inference::model_context::model_vision_enabled`]).
|
||||
pub(crate) fn oh_tier_supports_vision(model: &str) -> bool {
|
||||
use crate::openhuman::config::{
|
||||
MODEL_AGENTIC_V1, MODEL_CHAT_V1, MODEL_CODING_V1, MODEL_REASONING_QUICK_V1,
|
||||
MODEL_AGENTIC_V1, MODEL_BURST_V1, MODEL_CHAT_V1, MODEL_CODING_V1, MODEL_REASONING_QUICK_V1,
|
||||
MODEL_REASONING_V1, MODEL_SUMMARIZATION_V1, MODEL_VISION_V1,
|
||||
};
|
||||
match model {
|
||||
@@ -231,6 +236,8 @@ pub(crate) fn oh_tier_supports_vision(model: &str) -> bool {
|
||||
MODEL_REASONING_QUICK_V1 => false,
|
||||
MODEL_AGENTIC_V1 | "hint:agentic" => false,
|
||||
MODEL_CODING_V1 | "hint:coding" => false,
|
||||
// Burst is a text-only tier.
|
||||
MODEL_BURST_V1 | "hint:burst" => false,
|
||||
MODEL_SUMMARIZATION_V1 | "hint:summarization" => false,
|
||||
_ => false,
|
||||
}
|
||||
@@ -259,6 +266,11 @@ pub fn provider_for_role(role: &str, config: &Config) -> String {
|
||||
"reasoning" => config.reasoning_provider.as_deref(),
|
||||
"agentic" => config.agentic_provider.as_deref(),
|
||||
"coding" => config.coding_provider.as_deref(),
|
||||
// Burst is managed-backend only (no per-workload provider knob): it rides
|
||||
// the hosted high-throughput tier. Unset → falls through to
|
||||
// `primary_cloud` (→ managed `burst-v1`) like the other tier-specific
|
||||
// background workloads.
|
||||
"burst" => None,
|
||||
// Tier-specific multimodal model; like `agentic` it is NOT part of the
|
||||
// chat-tier BYOK inheritance below — when unset it falls through to
|
||||
// `primary_cloud` (→ managed `vision-v1`).
|
||||
@@ -299,13 +311,19 @@ pub fn provider_for_role(role: &str, config: &Config) -> String {
|
||||
if !matches!(role, "chat" | "reasoning" | "coding") {
|
||||
if let Some(chat) = config.chat_provider.as_deref() {
|
||||
if crate::openhuman::inference::local::profile::is_local_provider_string(chat) {
|
||||
// burst is managed-backend only — there is no `burst_provider`
|
||||
// knob, so don't suggest setting one.
|
||||
let override_hint = if role == "burst" {
|
||||
"managed-backend only; no per-workload override".to_string()
|
||||
} else {
|
||||
format!("set {role}_provider explicitly to override")
|
||||
};
|
||||
log::info!(
|
||||
"[providers][local-fallback] role={} using managed backend (chat is \
|
||||
local '{}' but background workloads require cloud — set \
|
||||
{}_provider explicitly to override)",
|
||||
local '{}' but background workloads require cloud — {})",
|
||||
role,
|
||||
chat,
|
||||
role
|
||||
override_hint
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -871,12 +889,18 @@ pub(crate) fn create_local_chat_provider_from_string(
|
||||
/// every attached image — leaving the managed vision sub-agent blind.
|
||||
fn managed_tier_for_role(role: &str) -> Option<&'static str> {
|
||||
use crate::openhuman::config::{
|
||||
MODEL_AGENTIC_V1, MODEL_CHAT_V1, MODEL_CODING_V1, MODEL_REASONING_V1, MODEL_VISION_V1,
|
||||
MODEL_AGENTIC_V1, MODEL_BURST_V1, MODEL_CHAT_V1, MODEL_CODING_V1, MODEL_REASONING_V1,
|
||||
MODEL_VISION_V1,
|
||||
};
|
||||
match role {
|
||||
"reasoning" => Some(MODEL_REASONING_V1),
|
||||
"agentic" => Some(MODEL_AGENTIC_V1),
|
||||
"coding" => Some(MODEL_CODING_V1),
|
||||
// Burst rides the managed backend's high-throughput tier. Pinned here
|
||||
// (rather than collapsing to `default_model`) so the `hint = "burst"`
|
||||
// sub-agent — the super-context scout — actually reaches `burst-v1`.
|
||||
// There is no `burst_provider` knob: burst is managed-only.
|
||||
"burst" => Some(MODEL_BURST_V1),
|
||||
"vision" => Some(MODEL_VISION_V1),
|
||||
// Background subconscious tick/triage: pinned to the lightweight chat
|
||||
// tier (see the doc above for why it is pinned despite being background).
|
||||
@@ -977,6 +1001,7 @@ fn make_openhuman_backend(
|
||||
Some("chat") => crate::openhuman::config::MODEL_CHAT_V1.to_string(),
|
||||
Some("agentic") => crate::openhuman::config::MODEL_AGENTIC_V1.to_string(),
|
||||
Some("coding") => crate::openhuman::config::MODEL_CODING_V1.to_string(),
|
||||
Some("burst") => crate::openhuman::config::MODEL_BURST_V1.to_string(),
|
||||
Some("summarization") => crate::openhuman::config::MODEL_SUMMARIZATION_V1.to_string(),
|
||||
Some("vision") => crate::openhuman::config::MODEL_VISION_V1.to_string(),
|
||||
Some(_) => {
|
||||
|
||||
@@ -404,7 +404,7 @@ fn create_chat_provider_uses_role() {
|
||||
#[test]
|
||||
fn managed_backend_pins_specialised_role_to_tier() {
|
||||
use crate::openhuman::config::{
|
||||
MODEL_AGENTIC_V1, MODEL_CODING_V1, MODEL_REASONING_V1, MODEL_VISION_V1,
|
||||
MODEL_AGENTIC_V1, MODEL_BURST_V1, MODEL_CODING_V1, MODEL_REASONING_V1, MODEL_VISION_V1,
|
||||
};
|
||||
// default_model is chat-v1 — the value the buggy path would have leaked.
|
||||
let config = Config::default();
|
||||
@@ -414,6 +414,7 @@ fn managed_backend_pins_specialised_role_to_tier() {
|
||||
("reasoning", MODEL_REASONING_V1),
|
||||
("agentic", MODEL_AGENTIC_V1),
|
||||
("coding", MODEL_CODING_V1),
|
||||
("burst", MODEL_BURST_V1),
|
||||
("vision", MODEL_VISION_V1),
|
||||
] {
|
||||
let (_, model) = create_chat_provider_from_string(role, "openhuman", &config)
|
||||
@@ -481,12 +482,16 @@ fn managed_backend_summarization_ignores_cloud_llm_model_override() {
|
||||
// `code_executor` agent (`hint = "coding"`) makes when it spawns.
|
||||
#[test]
|
||||
fn subagent_hint_resolves_to_tier_on_managed_backend() {
|
||||
use crate::openhuman::config::{MODEL_AGENTIC_V1, MODEL_CODING_V1, MODEL_REASONING_V1};
|
||||
use crate::openhuman::config::{
|
||||
MODEL_AGENTIC_V1, MODEL_BURST_V1, MODEL_CODING_V1, MODEL_REASONING_V1,
|
||||
};
|
||||
let config = Config::default();
|
||||
for (hint, expected_tier) in &[
|
||||
("coding", MODEL_CODING_V1),
|
||||
("agentic", MODEL_AGENTIC_V1),
|
||||
("reasoning", MODEL_REASONING_V1),
|
||||
// The super-context scout (`context_scout`) spawns with `hint = "burst"`.
|
||||
("burst", MODEL_BURST_V1),
|
||||
] {
|
||||
let (_, model) =
|
||||
create_chat_provider(hint, &config).expect("create_chat_provider must succeed");
|
||||
@@ -1010,10 +1015,23 @@ fn known_hints_pass() {
|
||||
assert!(is_known_openhuman_tier("hint:chat"));
|
||||
assert!(is_known_openhuman_tier("hint:agentic"));
|
||||
assert!(is_known_openhuman_tier("hint:coding"));
|
||||
assert!(is_known_openhuman_tier("hint:burst"));
|
||||
assert!(is_known_openhuman_tier("hint:summarization"));
|
||||
assert!(is_known_openhuman_tier("hint:vision"));
|
||||
}
|
||||
|
||||
// `hint:burst` is accepted by `is_known_openhuman_tier`, so it must also be
|
||||
// translated to `burst-v1` by the managed backend — otherwise a saved
|
||||
// `default_model = "hint:burst"` would be forwarded literally and 400.
|
||||
#[test]
|
||||
fn managed_backend_translates_hint_burst_to_burst_tier() {
|
||||
let mut config = Config::default();
|
||||
config.default_model = Some("hint:burst".to_string());
|
||||
let (_, model) = create_chat_provider_from_string("chat", "openhuman", &config)
|
||||
.expect("managed backend must build");
|
||||
assert_eq!(model, crate::openhuman::config::MODEL_BURST_V1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_models_fail() {
|
||||
assert!(!is_known_openhuman_tier("deepseek-v4-pro"));
|
||||
|
||||
Reference in New Issue
Block a user