providers now per-agent: z.ai rides, bifrost stays home
Vanguard gets his own inference pipe. z.ai GLM Coding Plan, /paas/v4. BifrostClient learned to leave versioned URLs alone. ProviderRegistry dispatches per agent; every engine asks who it's talking to before dialing. Annie and Hal none the wiser.
This commit is contained in:
parent
19b8566e4d
commit
80bfb3a599
15 changed files with 326 additions and 57 deletions
7
.gitignore
vendored
7
.gitignore
vendored
|
|
@ -58,3 +58,10 @@ souveraine.log.prev
|
|||
|
||||
# Config (use souveraine.example.toml as template)
|
||||
souveraine.toml
|
||||
|
||||
# Local-only project scaffolding — kept on disk, never committed to any remote
|
||||
.superpowers/
|
||||
saf/
|
||||
journal/
|
||||
scripts/
|
||||
website/
|
||||
|
|
|
|||
13
Cargo.lock
generated
13
Cargo.lock
generated
|
|
@ -1308,6 +1308,15 @@ dependencies = [
|
|||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chord_macro"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chord_macro"
|
||||
version = "0.1.0"
|
||||
|
|
@ -8419,8 +8428,10 @@ dependencies = [
|
|||
"anyhow",
|
||||
"arboard",
|
||||
"async-trait",
|
||||
"axis2d",
|
||||
"axum",
|
||||
"base64 0.22.1",
|
||||
"chord_macro 0.1.0",
|
||||
"chrono",
|
||||
"clap 4.6.1",
|
||||
"clap_complete",
|
||||
|
|
@ -10253,7 +10264,7 @@ version = "0.1.5"
|
|||
dependencies = [
|
||||
"axis2d",
|
||||
"base64-simd",
|
||||
"chord_macro",
|
||||
"chord_macro 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"image",
|
||||
"libc",
|
||||
"nonmax",
|
||||
|
|
|
|||
|
|
@ -94,6 +94,22 @@ pub struct SouveraineConfig {
|
|||
pub archivist_enabled: bool,
|
||||
pub archivist_threshold: f32,
|
||||
pub sensorium_bandwidth: String,
|
||||
/// Per-agent model override for the subconscious (N+1) pass. When set,
|
||||
/// it wins over the global `[subconscious] model`. None falls back to
|
||||
/// the global setting, then the agent's own primary model.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub subconscious_model: Option<String>,
|
||||
/// Per-agent model override for the reflection (N+25) pass.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub reflection_model: Option<String>,
|
||||
/// Per-agent model override for the archivist (N+100) synthesis pass.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub archivist_model: Option<String>,
|
||||
/// Per-agent inference provider name (e.g. "zai"). When set, the agent's
|
||||
/// requests route through the named provider instead of the global
|
||||
/// `[inference] provider`. None falls back to the global default.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub provider: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
|
|
|
|||
|
|
@ -11,8 +11,12 @@ use crate::bridge::provider::LlmProvider;
|
|||
/// Bifrost is an OpenAI-compatible API gateway: http://127.0.0.1:3360/v1
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BifrostClient {
|
||||
/// Base URL including /v1 (e.g. "http://127.0.0.1:3360/v1")
|
||||
/// Base URL including its version path (e.g. "http://127.0.0.1:3360/v1"
|
||||
/// or "https://api.z.ai/api/coding/paas/v4").
|
||||
base_url: String,
|
||||
/// Provider identity label (logs/catalog). Defaults to "bifrost"; a
|
||||
/// differently-branded OpenAI-compatible endpoint (e.g. z.ai) sets its own.
|
||||
id: String,
|
||||
/// Bearer token for auth
|
||||
api_key: String,
|
||||
/// Optional virtual key for x-bf-vk header
|
||||
|
|
@ -428,10 +432,28 @@ enum ErrorClass {
|
|||
Permanent,
|
||||
}
|
||||
|
||||
/// True if the URL's final path segment is a version tag like `v1`, `v4`.
|
||||
/// Used to avoid appending `/v1` to an already-versioned base URL.
|
||||
fn last_segment_is_version(url: &str) -> bool {
|
||||
match url.rsplit('/').next() {
|
||||
Some(seg) if seg.len() >= 2 => {
|
||||
seg.starts_with('v') && seg[1..].chars().all(|c| c.is_ascii_digit())
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
impl BifrostClient {
|
||||
pub fn new(base_url: &str, api_key: &str, virtual_key: &str, default_model: &str, timeout_secs: u64) -> Result<Self> {
|
||||
let base = base_url.trim_end_matches('/').to_string();
|
||||
let base_url = if base.ends_with("/v1") { base } else { format!("{}/v1", base) };
|
||||
// Only append "/v1" when the base URL has no version path segment.
|
||||
// A URL already ending in "/v<digits>" (Bifrost's "/v1", z.ai's
|
||||
// "/api/coding/paas/v4") is left untouched.
|
||||
let base_url = if last_segment_is_version(&base) {
|
||||
base
|
||||
} else {
|
||||
format!("{}/v1", base)
|
||||
};
|
||||
|
||||
info!(
|
||||
"🌉 Bifrost client initialized — model: {}, endpoint: {}, timeout: {}s",
|
||||
|
|
@ -439,6 +461,7 @@ impl BifrostClient {
|
|||
);
|
||||
Ok(Self {
|
||||
base_url,
|
||||
id: "bifrost".to_string(),
|
||||
api_key: api_key.to_string(),
|
||||
virtual_key: virtual_key.to_string(),
|
||||
client: reqwest::Client::builder()
|
||||
|
|
@ -450,6 +473,14 @@ impl BifrostClient {
|
|||
})
|
||||
}
|
||||
|
||||
/// Set the provider identity label (logs/catalog merge). Use when
|
||||
/// constructing this generic OpenAI-compatible client for a non-Bifrost
|
||||
/// provider such as z.ai.
|
||||
pub fn with_id(mut self, id: impl Into<String>) -> Self {
|
||||
self.id = id.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_fallbacks(mut self, fallbacks: Vec<String>) -> Self {
|
||||
self.retry_policy.fallback_models = fallbacks;
|
||||
self
|
||||
|
|
@ -674,7 +705,7 @@ impl BifrostClient {
|
|||
#[async_trait]
|
||||
impl LlmProvider for BifrostClient {
|
||||
fn id(&self) -> &str {
|
||||
"bifrost"
|
||||
&self.id
|
||||
}
|
||||
|
||||
fn default_model(&self) -> &str {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
/// Connects Souveraine to inference providers behind the [`LlmProvider`] trait.
|
||||
/// `BifrostClient` speaks to any OpenAI-compatible gateway; the OAuth-riding
|
||||
/// ChatGPT provider lives in [`providers`]. [`build_provider`] selects the
|
||||
/// active one from config.
|
||||
/// active one from config; [`ProviderRegistry`] resolves a provider per agent.
|
||||
pub mod bifrost;
|
||||
pub mod model_router;
|
||||
pub mod oauth;
|
||||
|
|
@ -13,27 +13,56 @@ pub mod providers;
|
|||
pub use bifrost::BifrostClient;
|
||||
pub use provider::LlmProvider;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::api::models::AgentState;
|
||||
use crate::core::config::ConsciousnessConfig;
|
||||
|
||||
/// Build the active LLM provider from config.
|
||||
/// Build the active (global default) LLM provider from config.
|
||||
///
|
||||
/// `[inference] provider` selects the implementation:
|
||||
/// - `"bifrost"` (default) — any OpenAI-compatible gateway via `BifrostClient`.
|
||||
/// - `"openai-oauth"` — ride the Codex CLI's ChatGPT login and drive
|
||||
/// `backend-api/codex/responses`.
|
||||
///
|
||||
/// Kept for call sites with no agent context (model pickers, CLI probes). For
|
||||
/// inference, prefer [`build_registry`] + [`ProviderRegistry::for_agent`].
|
||||
pub fn build_provider(config: &ConsciousnessConfig) -> anyhow::Result<Arc<dyn LlmProvider>> {
|
||||
let bf = &config.bifrost;
|
||||
match config.inference.provider.as_str() {
|
||||
build_provider_named(&config.inference.provider, config)
|
||||
}
|
||||
|
||||
/// Build a specific named provider. Adding a provider = adding a case here and
|
||||
/// a config section. Reuses `BifrostClient` (generic OpenAI-compatible) for any
|
||||
/// gateway-style provider; the OAuth provider is the endpoint-specific special
|
||||
/// case.
|
||||
pub fn build_provider_named(
|
||||
name: &str,
|
||||
config: &ConsciousnessConfig,
|
||||
) -> anyhow::Result<Arc<dyn LlmProvider>> {
|
||||
match name {
|
||||
"openai-oauth" | "openai-codex" => {
|
||||
let provider = providers::openai_oauth::OpenAiOAuthProvider::from_codex_login(
|
||||
bf.primary_model.clone(),
|
||||
bf.timeout_secs,
|
||||
config.bifrost.primary_model.clone(),
|
||||
config.bifrost.timeout_secs,
|
||||
)?;
|
||||
Ok(Arc::new(provider))
|
||||
}
|
||||
"zai" => {
|
||||
let z = &config.zai;
|
||||
let client = BifrostClient::new(
|
||||
&z.base_url,
|
||||
&z.api_key,
|
||||
"", // no virtual-key header for z.ai
|
||||
&z.primary_model,
|
||||
z.timeout_secs,
|
||||
)?
|
||||
.with_id("zai");
|
||||
Ok(Arc::new(client))
|
||||
}
|
||||
_ => {
|
||||
// "bifrost" (and any unrecognized name) → OpenAI-compatible gateway.
|
||||
let bf = &config.bifrost;
|
||||
let client = BifrostClient::new(
|
||||
&bf.base_url,
|
||||
&bf.api_key,
|
||||
|
|
@ -45,3 +74,66 @@ pub fn build_provider(config: &ConsciousnessConfig) -> anyhow::Result<Arc<dyn Ll
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the per-agent provider registry: the global default provider (from
|
||||
/// `[inference] provider`) plus every additionally-configured named provider
|
||||
/// (e.g. `zai` when `[zai]` is set). Engines resolve a provider per agent from
|
||||
/// this instead of holding one global client.
|
||||
pub fn build_registry(config: &ConsciousnessConfig) -> anyhow::Result<ProviderRegistry> {
|
||||
let default_name = config.inference.provider.clone();
|
||||
let default = build_provider_named(&default_name, config)?;
|
||||
let mut map = HashMap::new();
|
||||
map.insert(default_name.clone(), default.clone());
|
||||
|
||||
// z.ai (GLM Coding Plan) — register when configured.
|
||||
if !config.zai.base_url.is_empty() && !config.zai.api_key.is_empty() {
|
||||
map.insert("zai".to_string(), build_provider_named("zai", config)?);
|
||||
}
|
||||
|
||||
Ok(ProviderRegistry {
|
||||
map,
|
||||
default_name,
|
||||
default,
|
||||
})
|
||||
}
|
||||
|
||||
/// Maps a provider name to a live [`LlmProvider`], resolving per agent.
|
||||
#[derive(Clone)]
|
||||
pub struct ProviderRegistry {
|
||||
map: HashMap<String, Arc<dyn LlmProvider>>,
|
||||
default_name: String,
|
||||
default: Arc<dyn LlmProvider>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for ProviderRegistry {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("ProviderRegistry")
|
||||
.field("providers", &self.map.keys().collect::<Vec<_>>())
|
||||
.field("default", &self.default_name)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl ProviderRegistry {
|
||||
/// Resolve the provider for an agent: its `_souveraine.provider` override,
|
||||
/// else the global default. An unknown/missing name falls back to the
|
||||
/// default rather than failing.
|
||||
pub fn for_agent(&self, agent: &AgentState) -> Arc<dyn LlmProvider> {
|
||||
match agent.souveraine.provider.as_deref() {
|
||||
Some(name) if name != self.default_name => {
|
||||
self.map.get(name).cloned().unwrap_or_else(|| self.default.clone())
|
||||
}
|
||||
_ => self.default.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The global default provider (for contexts with no agent).
|
||||
pub fn default_provider(&self) -> Arc<dyn LlmProvider> {
|
||||
self.default.clone()
|
||||
}
|
||||
|
||||
/// Name of the global default provider.
|
||||
pub fn default_name(&self) -> &str {
|
||||
&self.default_name
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ use serde::{Deserialize, Serialize};
|
|||
|
||||
use crate::bridge::bifrost::{ChatCompletionRequest, Message};
|
||||
use crate::bridge::LlmProvider;
|
||||
use crate::bridge::ProviderRegistry;
|
||||
use crate::core::config::{ArchivistConfig, SynthesisElement};
|
||||
use crate::core::memory::MemoryRepo;
|
||||
use crate::server::AgentInventory;
|
||||
|
|
@ -95,7 +96,7 @@ impl SynthesisReport {
|
|||
|
||||
pub struct ArchivistEngine {
|
||||
agents: Arc<AgentInventory>,
|
||||
bifrost: Arc<dyn LlmProvider>,
|
||||
providers: Arc<ProviderRegistry>,
|
||||
rate_delay: Arc<AtomicU64>,
|
||||
config: ArchivistConfig,
|
||||
/// Subconscious model handle — used to resolve `compression_model: "auto"`.
|
||||
|
|
@ -105,24 +106,35 @@ pub struct ArchivistEngine {
|
|||
impl ArchivistEngine {
|
||||
pub fn new(
|
||||
agents: Arc<AgentInventory>,
|
||||
bifrost: Arc<dyn LlmProvider>,
|
||||
providers: Arc<ProviderRegistry>,
|
||||
rate_delay: Arc<AtomicU64>,
|
||||
config: ArchivistConfig,
|
||||
subconscious_model: Option<String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
agents,
|
||||
bifrost,
|
||||
providers,
|
||||
rate_delay,
|
||||
config,
|
||||
subconscious_model,
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve the compression model. `"auto"` (or empty) falls back to the
|
||||
/// subconscious model, then to a sensible default. A real capability-aware
|
||||
/// auto-selection algorithm is deferred (task Phase 4).
|
||||
fn resolve_model(&self) -> String {
|
||||
/// Resolve the compression model. The agent's own
|
||||
/// `_souveraine.archivist_model` override wins outright. Otherwise the
|
||||
/// configured `compression_model` is used, with `"auto"` (or empty)
|
||||
/// falling back to the subconscious model, then a sensible default.
|
||||
/// A real capability-aware auto-selection algorithm is deferred.
|
||||
async fn resolve_model(&self, agent_id: &str) -> String {
|
||||
if let Some(per_agent) = self
|
||||
.agents
|
||||
.get(agent_id)
|
||||
.await
|
||||
.ok()
|
||||
.and_then(|a| a.souveraine.archivist_model.clone())
|
||||
{
|
||||
return per_agent;
|
||||
}
|
||||
let configured = self.config.compression_model.trim();
|
||||
if configured.is_empty() || configured.eq_ignore_ascii_case("auto") {
|
||||
self.subconscious_model
|
||||
|
|
@ -197,9 +209,16 @@ impl ArchivistEngine {
|
|||
let raw = format_journal_input(&entries);
|
||||
let approx_tokens_compressed = raw.len() / 4;
|
||||
|
||||
let model = self.resolve_model();
|
||||
let model = self.resolve_model(agent_id).await;
|
||||
let llm: Arc<dyn LlmProvider> = self
|
||||
.agents
|
||||
.get(agent_id)
|
||||
.await
|
||||
.ok()
|
||||
.map(|a| self.providers.for_agent(&a))
|
||||
.unwrap_or_else(|| self.providers.default_provider());
|
||||
let synthesis = self
|
||||
.run_synthesis(&model, &raw, start_date, end_date)
|
||||
.run_synthesis(&llm, &model, &raw, start_date, end_date)
|
||||
.await?;
|
||||
|
||||
let output_label = format!("{SYNTHESIS_DIR}/{end_date}");
|
||||
|
|
@ -229,6 +248,7 @@ impl ArchivistEngine {
|
|||
/// No tool loop — the Archivist produces a record, it doesn't act.
|
||||
async fn run_synthesis(
|
||||
&self,
|
||||
llm: &Arc<dyn LlmProvider>,
|
||||
model: &str,
|
||||
raw_journal: &str,
|
||||
start: NaiveDate,
|
||||
|
|
@ -253,7 +273,7 @@ impl ArchivistEngine {
|
|||
};
|
||||
|
||||
tracing::info!(model = %model, "archivist synthesis call starting");
|
||||
let (response, strain) = self.bifrost.chat_completion_with_strain(request).await?;
|
||||
let (response, strain) = llm.chat_completion_with_strain(request).await?;
|
||||
|
||||
for event in &strain {
|
||||
if let crate::bridge::bifrost::InferenceStrain::Transient { status, model, .. } = event {
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ use tokio::sync::RwLock;
|
|||
|
||||
use crate::bridge::model_router::TokenCounter;
|
||||
use crate::bridge::LlmProvider;
|
||||
use crate::bridge::ProviderRegistry;
|
||||
use crate::core::config::ConsciousnessConfig;
|
||||
use crate::core::memory::MemoryRepo;
|
||||
use crate::core::session::ConversationMessage;
|
||||
|
|
@ -85,7 +86,7 @@ pub trait CompactionEngine: Send + Sync {
|
|||
pub struct DefaultCompactionEngine {
|
||||
pub config: Arc<RwLock<ConsciousnessConfig>>,
|
||||
pub counter: TokenCounter,
|
||||
pub bifrost: Option<Arc<dyn LlmProvider>>,
|
||||
pub providers: Option<Arc<ProviderRegistry>>,
|
||||
pub model: Option<String>,
|
||||
pub clock: Arc<dyn Clock>,
|
||||
pub get_messages: Arc<dyn Fn(&str) -> Option<Vec<ConversationMessage>> + Send + Sync>,
|
||||
|
|
@ -151,8 +152,9 @@ impl CompactionEngine for DefaultCompactionEngine {
|
|||
|
||||
// Build strategy and get plan
|
||||
let plan = match strategy_kind {
|
||||
CompactionStrategyKind::Summary => match &self.bifrost {
|
||||
Some(client) => {
|
||||
CompactionStrategyKind::Summary => match &self.providers {
|
||||
Some(reg) => {
|
||||
let client = reg.default_provider();
|
||||
let model = self
|
||||
.model
|
||||
.as_deref()
|
||||
|
|
@ -178,8 +180,9 @@ impl CompactionEngine for DefaultCompactionEngine {
|
|||
let s = SlidingWindowStrategy;
|
||||
s.plan(&messages, &cfg, &self.counter).await?
|
||||
}
|
||||
CompactionStrategyKind::SlidingReflect => match &self.bifrost {
|
||||
Some(client) => {
|
||||
CompactionStrategyKind::SlidingReflect => match &self.providers {
|
||||
Some(reg) => {
|
||||
let client = reg.default_provider();
|
||||
let model = self
|
||||
.model
|
||||
.as_deref()
|
||||
|
|
@ -208,7 +211,7 @@ impl CompactionEngine for DefaultCompactionEngine {
|
|||
s.plan(&messages, &cfg, &self.counter).await?
|
||||
}
|
||||
None => {
|
||||
tracing::warn!("[sliding_reflect] no bifrost client, falling back to plain sliding window");
|
||||
tracing::warn!("[sliding_reflect] no provider registry, falling back to plain sliding window");
|
||||
SlidingWindowStrategy.plan(&messages, &cfg, &self.counter).await?
|
||||
}
|
||||
},
|
||||
|
|
@ -334,7 +337,7 @@ mod tests {
|
|||
|
||||
let engine = DefaultCompactionEngine {
|
||||
counter: TokenCounter::new(),
|
||||
bifrost: None,
|
||||
providers: None,
|
||||
model: None,
|
||||
clock: Arc::new(TestClock),
|
||||
config,
|
||||
|
|
|
|||
|
|
@ -15,6 +15,11 @@ pub struct ConsciousnessConfig {
|
|||
#[serde(default)]
|
||||
pub bifrost: BifrostConfig,
|
||||
|
||||
/// z.ai (GLM Coding Plan) inference config — an OpenAI-compatible
|
||||
/// provider reachable per-agent via `_souveraine.provider = "zai"`.
|
||||
#[serde(default)]
|
||||
pub zai: ZaiConfig,
|
||||
|
||||
/// Per-model physics configs
|
||||
#[serde(default)]
|
||||
pub models: HashMap<String, ModelConfig>,
|
||||
|
|
@ -219,6 +224,47 @@ pub struct BifrostConfig {
|
|||
pub timeout_secs: u64,
|
||||
}
|
||||
|
||||
// ── z.ai ──
|
||||
|
||||
/// z.ai (GLM Coding Plan) provider configuration. An OpenAI-compatible
|
||||
/// endpoint served from `https://api.z.ai/api/coding/paas/v4`. Selected
|
||||
/// per-agent via `_souveraine.provider = "zai"`; otherwise unused. Empty by
|
||||
/// default so configs without a `[zai]` section still parse.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ZaiConfig {
|
||||
/// Coding Plan endpoint, e.g. "https://api.z.ai/api/coding/paas/v4".
|
||||
#[serde(default)]
|
||||
pub base_url: String,
|
||||
|
||||
/// Bearer token (Zhipu `{id}.{secret}` key).
|
||||
#[serde(default)]
|
||||
pub api_key: String,
|
||||
|
||||
/// Default model for z.ai agents (e.g. "glm-5.2").
|
||||
#[serde(default)]
|
||||
pub primary_model: String,
|
||||
|
||||
/// Per-attempt timeout in seconds. glm-5.2 is a reasoning model and can
|
||||
/// take a while; default generously.
|
||||
#[serde(default = "default_zai_timeout")]
|
||||
pub timeout_secs: u64,
|
||||
}
|
||||
|
||||
impl Default for ZaiConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
base_url: String::new(),
|
||||
api_key: String::new(),
|
||||
primary_model: String::new(),
|
||||
timeout_secs: default_zai_timeout(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn default_zai_timeout() -> u64 {
|
||||
300
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct BifrostModelConfig {
|
||||
#[serde(default = "default_128k")]
|
||||
|
|
@ -628,6 +674,7 @@ impl Default for ConsciousnessConfig {
|
|||
Self {
|
||||
inference: InferenceConfig::default(),
|
||||
bifrost: BifrostConfig::default(),
|
||||
zai: ZaiConfig::default(),
|
||||
models: default_models(),
|
||||
subconscious: SubconsciousConfig::default(),
|
||||
reflection: ReflectionConfig::default(),
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ use crate::bridge::bifrost::{
|
|||
ChatCompletionRequest, Message, ToolDefinition, ToolFunction,
|
||||
};
|
||||
use crate::bridge::LlmProvider;
|
||||
use crate::bridge::ProviderRegistry;
|
||||
use crate::core::session::{ContentBlock, ConversationMessage, MessageRole};
|
||||
use crate::core::tools::defs::ToolContext;
|
||||
use crate::server::AgentInventory;
|
||||
|
|
@ -70,7 +71,7 @@ pub struct ReflectionReport {
|
|||
|
||||
pub struct ReflectionEngine {
|
||||
agents: Arc<AgentInventory>,
|
||||
bifrost: Arc<dyn LlmProvider>,
|
||||
providers: Arc<ProviderRegistry>,
|
||||
rate_delay: Arc<AtomicU64>,
|
||||
/// Model handle for reflection passes. None falls back to the
|
||||
/// subconscious model, then to a sensible default.
|
||||
|
|
@ -81,14 +82,14 @@ pub struct ReflectionEngine {
|
|||
impl ReflectionEngine {
|
||||
pub fn new(
|
||||
agents: Arc<AgentInventory>,
|
||||
bifrost: Arc<dyn LlmProvider>,
|
||||
providers: Arc<ProviderRegistry>,
|
||||
rate_delay: Arc<AtomicU64>,
|
||||
model: Option<String>,
|
||||
max_tokens: Option<u32>,
|
||||
) -> Self {
|
||||
Self {
|
||||
agents,
|
||||
bifrost,
|
||||
providers,
|
||||
rate_delay,
|
||||
model,
|
||||
max_tokens,
|
||||
|
|
@ -118,10 +119,21 @@ impl ReflectionEngine {
|
|||
.filter(|m| matches!(m.role, MessageRole::User | MessageRole::Assistant))
|
||||
.count();
|
||||
|
||||
let model = self
|
||||
.model
|
||||
.as_deref()
|
||||
.unwrap_or("openai/glm-5.1");
|
||||
// Per-agent reflection model: the agent's own
|
||||
// `_souveraine.reflection_model` override wins, then the engine's
|
||||
// configured reflection/subconscious model, then the agent's primary
|
||||
// model, then a sensible default.
|
||||
let agent = self.agents.get(agent_id).await.ok();
|
||||
let model = agent
|
||||
.as_ref()
|
||||
.and_then(|a| a.souveraine.reflection_model.clone())
|
||||
.or_else(|| self.model.clone())
|
||||
.or_else(|| agent.as_ref().map(|a| a.llm_config.model.clone()))
|
||||
.unwrap_or_else(|| "openai/glm-5.1".to_string());
|
||||
let llm: Arc<dyn LlmProvider> = agent
|
||||
.as_ref()
|
||||
.map(|a| self.providers.for_agent(a))
|
||||
.unwrap_or_else(|| self.providers.default_provider());
|
||||
|
||||
// ── Tools ───────────────────────────────────────────────────
|
||||
let all_defs = crate::core::tools::tool_definitions().await;
|
||||
|
|
@ -181,7 +193,7 @@ impl ReflectionEngine {
|
|||
tools: Some(reflection_tools.clone()),
|
||||
};
|
||||
|
||||
let (response, strain) = self.bifrost.chat_completion_with_strain(request).await?;
|
||||
let (response, strain) = llm.chat_completion_with_strain(request).await?;
|
||||
|
||||
for event in &strain {
|
||||
if let crate::bridge::bifrost::InferenceStrain::Transient { status, model, .. } =
|
||||
|
|
|
|||
|
|
@ -315,6 +315,10 @@ impl AgentInventory {
|
|||
archivist_enabled: true,
|
||||
archivist_threshold: 0.7,
|
||||
sensorium_bandwidth: "high".to_string(),
|
||||
subconscious_model: None,
|
||||
reflection_model: None,
|
||||
archivist_model: None,
|
||||
provider: None,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@
|
|||
|
||||
use crate::bridge::bifrost::{ChatCompletionRequest, Message, ToolDefinition, ToolFunction};
|
||||
use crate::bridge::LlmProvider;
|
||||
use crate::bridge::ProviderRegistry;
|
||||
use crate::bridge::model_router::TokenCounter;
|
||||
use crate::core::compact::CompactionEngine;
|
||||
use crate::core::session::{ContentBlock, ConversationMessage, MessageRole};
|
||||
|
|
@ -59,7 +60,7 @@ const SUBCONSCIOUS_INTER_ROUND_DELAY_MS: u64 = 300;
|
|||
pub struct ConsciousnessEngine {
|
||||
agents: Arc<AgentInventory>,
|
||||
sessions: Arc<SessionManager>,
|
||||
bifrost: Arc<dyn LlmProvider>,
|
||||
providers: Arc<ProviderRegistry>,
|
||||
counter: TokenCounter,
|
||||
/// Optional model override for the subconscious pass (e.g. "openai/glm-5.1").
|
||||
/// If None, uses the primary agent's model.
|
||||
|
|
@ -156,7 +157,7 @@ impl ConsciousnessEngine {
|
|||
pub fn new(
|
||||
agents: Arc<AgentInventory>,
|
||||
sessions: Arc<SessionManager>,
|
||||
bifrost: Arc<dyn LlmProvider>,
|
||||
providers: Arc<ProviderRegistry>,
|
||||
subconscious_model: Option<String>,
|
||||
reflection_model: Option<String>,
|
||||
max_tokens: Option<u32>,
|
||||
|
|
@ -167,14 +168,14 @@ impl ConsciousnessEngine {
|
|||
) -> Self {
|
||||
let reflection = Arc::new(crate::core::reflection::ReflectionEngine::new(
|
||||
agents.clone(),
|
||||
bifrost.clone(),
|
||||
providers.clone(),
|
||||
rate_delay.clone(),
|
||||
reflection_model.or_else(|| subconscious_model.clone()),
|
||||
max_tokens,
|
||||
));
|
||||
let archivist = Arc::new(crate::core::archivist::ArchivistEngine::new(
|
||||
agents.clone(),
|
||||
bifrost.clone(),
|
||||
providers.clone(),
|
||||
rate_delay.clone(),
|
||||
archivist_config,
|
||||
subconscious_model.clone(),
|
||||
|
|
@ -182,7 +183,7 @@ impl ConsciousnessEngine {
|
|||
Self {
|
||||
agents,
|
||||
sessions,
|
||||
bifrost,
|
||||
providers,
|
||||
counter: TokenCounter::new(),
|
||||
subconscious_model,
|
||||
subconscious_system_prompt,
|
||||
|
|
@ -556,10 +557,21 @@ impl ConsciousnessEngine {
|
|||
occasion: Occasion,
|
||||
stream_tx: Option<&mpsc::Sender<anyhow::Result<BackendEvent>>>,
|
||||
) -> anyhow::Result<SubconsciousPassOutcome> {
|
||||
let model = self
|
||||
.subconscious_model
|
||||
.as_deref()
|
||||
.unwrap_or("openai/kimi-k2.6");
|
||||
// Resolve the subconscious model per-agent: the agent's own
|
||||
// `_souveraine.subconscious_model` override wins, then the global
|
||||
// `[subconscious] model`, then the agent's primary model, then a
|
||||
// sensible default.
|
||||
let primary_agent = self.agents.get(primary_id).await.ok();
|
||||
let model = primary_agent
|
||||
.as_ref()
|
||||
.and_then(|a| a.souveraine.subconscious_model.clone())
|
||||
.or_else(|| self.subconscious_model.clone())
|
||||
.or_else(|| primary_agent.as_ref().map(|a| a.llm_config.model.clone()))
|
||||
.unwrap_or_else(|| "openai/kimi-k2.6".to_string());
|
||||
let llm: Arc<dyn LlmProvider> = primary_agent
|
||||
.as_ref()
|
||||
.map(|a| self.providers.for_agent(a))
|
||||
.unwrap_or_else(|| self.providers.default_provider());
|
||||
|
||||
// ── System prompt — load from subconscious agent's own memfs ──
|
||||
let sub_memory_root = self.agents.subconscious_memory_root(primary_id);
|
||||
|
|
@ -776,7 +788,7 @@ Resolve entries: `[YYYY-MM-DD HH:MM] RESOLVED — note`"#;
|
|||
|
||||
tracing::info!(model = %model, round = _round, "subconscious LLM call starting");
|
||||
let subconscious_start = std::time::Instant::now();
|
||||
let (response, strain) = self.bifrost.chat_completion_with_strain(request).await?;
|
||||
let (response, strain) = llm.chat_completion_with_strain(request).await?;
|
||||
tracing::info!(
|
||||
elapsed = ?subconscious_start.elapsed(),
|
||||
tool_calls = response.tool_calls.len(),
|
||||
|
|
@ -941,8 +953,7 @@ Resolve entries: `[YYYY-MM-DD HH:MM] RESOLVED — note`"#;
|
|||
stream: None,
|
||||
tools: None,
|
||||
};
|
||||
let (final_response, _strain) = self
|
||||
.bifrost
|
||||
let (final_response, _strain) = llm
|
||||
.chat_completion_with_strain(final_request)
|
||||
.await?;
|
||||
let content = final_response.content.trim().to_string();
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use crate::bridge::{build_provider, LlmProvider};
|
||||
use crate::bridge::{build_provider, build_registry, ProviderRegistry, LlmProvider};
|
||||
use crate::core::compact::{CompactionEngine, DefaultCompactionEngine, UtcClock};
|
||||
use crate::core::config::ConsciousnessConfig;
|
||||
use crate::core::identity::SeedId;
|
||||
|
|
@ -41,7 +41,7 @@ pub struct SouveraineServer {
|
|||
pub sessions: Arc<SessionManager>,
|
||||
pub consciousness: Arc<ConsciousnessEngine>,
|
||||
pub compaction_engine: Arc<dyn CompactionEngine>,
|
||||
pub bifrost: Arc<dyn LlmProvider>,
|
||||
pub providers: Arc<ProviderRegistry>,
|
||||
pub config: Arc<RwLock<ServerConfig>>,
|
||||
pub data_dir: PathBuf,
|
||||
pub memory: Option<Arc<ServerMemory>>,
|
||||
|
|
@ -133,9 +133,8 @@ impl SouveraineServer {
|
|||
let event_bus = crate::core::nervous::EventBus::default();
|
||||
let sessions = Arc::new(SessionManager::with_persistence(data_dir.join("agents")));
|
||||
|
||||
// Select the active LLM provider (OpenAI-compatible gateway or the
|
||||
// OAuth-riding ChatGPT provider) from config.
|
||||
let bifrost: Arc<dyn LlmProvider> = build_provider(&config)?;
|
||||
// Build the per-agent provider registry from config. z.ai, bifrost, etc.
|
||||
let providers = Arc::new(build_registry(&config)?);
|
||||
|
||||
let rate_delay = Arc::new(AtomicU64::new(1000));
|
||||
tracing::info!("rate delay initialized at 1000ms");
|
||||
|
|
@ -188,7 +187,7 @@ impl SouveraineServer {
|
|||
let compaction_engine: Arc<dyn CompactionEngine> = Arc::new(DefaultCompactionEngine {
|
||||
config: app_cfg,
|
||||
counter: crate::bridge::model_router::TokenCounter::new(),
|
||||
bifrost: Some(bifrost.clone()),
|
||||
providers: Some(providers.clone()),
|
||||
model: config.compaction.model.clone().or_else(|| config.subconscious.model.clone()),
|
||||
clock: Arc::new(UtcClock),
|
||||
get_messages,
|
||||
|
|
@ -200,7 +199,7 @@ impl SouveraineServer {
|
|||
let consciousness = Arc::new(ConsciousnessEngine::new(
|
||||
agents.clone(),
|
||||
sessions.clone(),
|
||||
bifrost.clone(),
|
||||
providers.clone(),
|
||||
config.subconscious.model.clone(),
|
||||
config.reflection.model.clone(),
|
||||
config.subconscious.max_tokens,
|
||||
|
|
@ -295,7 +294,7 @@ impl SouveraineServer {
|
|||
sessions,
|
||||
consciousness,
|
||||
compaction_engine,
|
||||
bifrost,
|
||||
providers,
|
||||
config: Arc::new(RwLock::new(server_config)),
|
||||
data_dir,
|
||||
memory,
|
||||
|
|
|
|||
|
|
@ -144,7 +144,7 @@ impl SubagentRunner for ServerSubagentRunner {
|
|||
tools: Some(bifrost_tools.clone()),
|
||||
};
|
||||
|
||||
let response = self.server.bifrost.chat_completion(req).await.map_err(|e| {
|
||||
let response = self.server.providers.default_provider().chat_completion(req).await.map_err(|e| {
|
||||
crate::core::tools::defs::ToolError::invalid_input(&format!(
|
||||
"Subagent LLM call failed: {e}"
|
||||
))
|
||||
|
|
|
|||
|
|
@ -350,13 +350,14 @@ pub(crate) async fn run_turn(
|
|||
|
||||
// Race the LLM call against cancellation so Esc drops the in-flight
|
||||
// request without waiting for it to complete.
|
||||
let llm = server.providers.for_agent(&agent);
|
||||
let (response, strain) = tokio::select! {
|
||||
biased;
|
||||
_ = cancel.cancelled() => {
|
||||
interrupted = true;
|
||||
break;
|
||||
}
|
||||
res = server.bifrost.chat_completion_with_strain(req) => res?,
|
||||
res = llm.chat_completion_with_strain(req) => res?,
|
||||
};
|
||||
|
||||
tracing::info!(
|
||||
|
|
|
|||
|
|
@ -176,6 +176,21 @@ impl DelegateWidget for SettingsScreen {
|
|||
};
|
||||
}
|
||||
|
||||
// When the model picker sub-page is active, route input to it. The
|
||||
// picker handles its own navigation, fuzzy filtering, Enter-to-select,
|
||||
// and 'r' to refresh. Esc with an empty query bubbles back as Rejected
|
||||
// — close the sub-page here.
|
||||
if self.active_model_picker_field.is_some() {
|
||||
let is_esc = matches!(key, Key::Esc);
|
||||
let result = self.get_delegate_mut().on_input(queue);
|
||||
if is_esc && matches!(result, InputResult::Rejected) {
|
||||
queue.next();
|
||||
self.process_action(SettingsAction::PopPage);
|
||||
return InputResult::Handled;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
match key {
|
||||
Key::Arrow(Direction2D::Up) => {
|
||||
queue.next();
|
||||
|
|
|
|||
Loading…
Reference in a new issue