From 300d70aa42395e9dac4b2a28c12bfc77449839c0 Mon Sep 17 00:00:00 2001 From: Steven Enamakel <31011319+senamakel@users.noreply.github.com> Date: Mon, 29 Jun 2026 10:18:39 -0700 Subject: [PATCH] feat(inference): register burst-v1 tier; SuperContext scout uses it (#4260) --- gitbooks/features/model-routing/README.md | 1 + gitbooks/features/super-context.md | 2 + src/openhuman/agent/cost.rs | 20 ++++++++++ .../agents/context_scout/agent.toml | 8 ++-- src/openhuman/agent_registry/agents/loader.rs | 8 ++++ src/openhuman/config/mod.rs | 8 ++-- src/openhuman/config/schema/identity_cost.rs | 10 ++++- src/openhuman/config/schema/types.rs | 4 ++ src/openhuman/inference/model_context.rs | 9 ++++- src/openhuman/inference/provider/factory.rs | 39 +++++++++++++++---- .../inference/provider/factory_tests.rs | 22 ++++++++++- 11 files changed, 113 insertions(+), 18 deletions(-) diff --git a/gitbooks/features/model-routing/README.md b/gitbooks/features/model-routing/README.md index 01682cb0b..33a978b34 100644 --- a/gitbooks/features/model-routing/README.md +++ b/gitbooks/features/model-routing/README.md @@ -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. diff --git a/gitbooks/features/super-context.md b/gitbooks/features/super-context.md index 17ebb8ce5..a517859fb 100644 --- a/gitbooks/features/super-context.md +++ b/gitbooks/features/super-context.md @@ -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 diff --git a/src/openhuman/agent/cost.rs b/src/openhuman/agent/cost.rs index e7bcf9b88..d9a13b0cc 100644 --- a/src/openhuman/agent/cost.rs +++ b/src/openhuman/agent/cost.rs @@ -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"); diff --git a/src/openhuman/agent_registry/agents/context_scout/agent.toml b/src/openhuman/agent_registry/agents/context_scout/agent.toml index 9c7b757d2..9bc984ca7 100644 --- a/src/openhuman/agent_registry/agents/context_scout/agent.toml +++ b/src/openhuman/agent_registry/agents/context_scout/agent.toml @@ -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 diff --git a/src/openhuman/agent_registry/agents/loader.rs b/src/openhuman/agent_registry/agents/loader.rs index e16ed6bf6..f1c8a256d 100644 --- a/src/openhuman/agent_registry/agents/loader.rs +++ b/src/openhuman/agent_registry/agents/loader.rs @@ -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)); diff --git a/src/openhuman/config/mod.rs b/src/openhuman/config/mod.rs index 966aaae9c..dd722d2b2 100644 --- a/src/openhuman/config/mod.rs +++ b/src/openhuman/config/mod.rs @@ -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, diff --git a/src/openhuman/config/schema/identity_cost.rs b/src/openhuman/config/schema/identity_cost.rs index 2f99a239d..b6e20de9a 100644 --- a/src/openhuman/config/schema/identity_cost.rs +++ b/src/openhuman/config/schema/identity_cost.rs @@ -150,7 +150,7 @@ impl Default for CostConfig { /// Default pricing for popular models (USD per 1M tokens) fn get_default_pricing() -> HashMap { 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 { 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 } diff --git a/src/openhuman/config/schema/types.rs b/src/openhuman/config/schema/types.rs index 62ad1a489..51eaa456f 100644 --- a/src/openhuman/config/schema/types.rs +++ b/src/openhuman/config/schema/types.rs @@ -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`. diff --git a/src/openhuman/inference/model_context.rs b/src/openhuman/inference/model_context.rs index 4da1b814a..2e3f614db 100644 --- a/src/openhuman/inference/model_context.rs +++ b/src/openhuman/inference/model_context.rs @@ -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 { 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) diff --git a/src/openhuman/inference/provider/factory.rs b/src/openhuman/inference/provider/factory.rs index bdd24a86e..72879bf4c 100644 --- a/src/openhuman/inference/provider/factory.rs +++ b/src/openhuman/inference/provider/factory.rs @@ -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(_) => { diff --git a/src/openhuman/inference/provider/factory_tests.rs b/src/openhuman/inference/provider/factory_tests.rs index 659733942..5c2d21668 100644 --- a/src/openhuman/inference/provider/factory_tests.rs +++ b/src/openhuman/inference/provider/factory_tests.rs @@ -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"));