diff --git a/src/api/models.rs b/src/api/models.rs index a4fc745..6ae31ef 100644 --- a/src/api/models.rs +++ b/src/api/models.rs @@ -362,7 +362,7 @@ pub struct CreateConversationRequest { /// /// Untagged on purpose: a bare JSON string is exactly the wire it has always /// been, so every existing client keeps working unchanged; an array carries -/// ordered typed parts. `ContentValue` in `src/bridge/bifrost.rs` is this same +/// ordered typed parts. `ContentValue` in `src/bridge/openai_compatible.rs` is this same /// shape facing the provider. This is the inward-facing half, written in the /// substrate's own [`ContentBlock`] vocabulary rather than OpenAI's, because /// `ContentBlock` is what the session stores, persists, and replays. diff --git a/src/backend/local/mod.rs b/src/backend/local/mod.rs index d919c86..68846c1 100644 --- a/src/backend/local/mod.rs +++ b/src/backend/local/mod.rs @@ -2,7 +2,7 @@ //! In-process Backend impl. Same engine as the HTTP server, no socket. //! //! Constructed once with a `ConsciousnessConfig`; spins up an `AgentInventory` -//! (SQLite under `~/.souveraine/server/`), `SessionManager`, `BifrostClient`, +//! (SQLite under `~/.souveraine/server/`), `SessionManager`, `OpenAiCompatibleClient`, //! and `ConsciousnessEngine`. `send` mirrors the server's `stream_messages` //! handler, but emits `BackendEvent`s directly instead of SSE frames. //! diff --git a/src/bridge/claude_subscription.rs b/src/bridge/claude_subscription.rs index f5d7890..26c9cbb 100644 --- a/src/bridge/claude_subscription.rs +++ b/src/bridge/claude_subscription.rs @@ -4,7 +4,7 @@ //! Speaks the Claude Code (claude.ai OAuth) wire protocol directly against //! `api.anthropic.com`, using the OAuth credentials Claude Code already stored //! at `~/.claude/.credentials.json`. Implements [`LlmProvider`] so it drops -//! into the existing provider registry alongside Bifrost and OpenAI OAuth. +//! into the existing provider registry alongside the OpenAI-compatible client and OpenAI OAuth. //! //! Internally it translates OpenAI chat-completion requests (Souveraine's //! internal shape) to Anthropic `/v1/messages`, applies the subscription wire @@ -31,7 +31,7 @@ use sha2::{Digest, Sha256}; use tokio::sync::Mutex; use tracing::{info, warn}; -use super::bifrost::{ +use super::openai_compatible::{ ChatCompletionRequest, CompletionResult, InferenceStrain, Message, ParsedToolCall, ToolDefinition, Usage, }; @@ -1484,7 +1484,7 @@ fn status_hint(status: u16) -> &'static str { #[cfg(test)] mod tests { use super::*; - use crate::bridge::bifrost::{ + use crate::bridge::openai_compatible::{ ContentValue, MessageToolCall, MessageToolCallFunction, ToolFunction, }; diff --git a/src/bridge/mod.rs b/src/bridge/mod.rs index c8ce04f..bc1778d 100644 --- a/src/bridge/mod.rs +++ b/src/bridge/mod.rs @@ -2,17 +2,17 @@ /// Bridge module — LLM inference providers. /// /// Connects Souveraine to inference providers behind the [`LlmProvider`] trait. -/// `BifrostClient` speaks to any OpenAI-compatible gateway; the OAuth-riding +/// `OpenAiCompatibleClient` speaks to any OpenAI-compatible gateway; the OAuth-riding /// ChatGPT provider lives in [`providers`]. [`build_provider`] selects the /// active one from config; [`ProviderRegistry`] resolves a provider per agent. -pub mod bifrost; +pub mod openai_compatible; pub mod claude_subscription; pub mod model_router; pub mod oauth; pub mod provider; pub mod providers; -pub use bifrost::BifrostClient; +pub use openai_compatible::OpenAiCompatibleClient; pub use provider::LlmProvider; use std::collections::HashMap; @@ -60,14 +60,14 @@ pub fn build_provider_from_config( } _ => { // "openai-compatible" and any unrecognized type → OpenAI-compatible gateway. - let client = BifrostClient::new( + let client = OpenAiCompatibleClient::new( + name, &cfg.base_url, &cfg.api_key, &cfg.virtual_key, &cfg.primary_model, cfg.timeout_secs, - )? - .with_id(name); + )?; Ok(Arc::new(client)) } } diff --git a/src/bridge/model_router.rs b/src/bridge/model_router.rs index 067095b..89c1d7e 100644 --- a/src/bridge/model_router.rs +++ b/src/bridge/model_router.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use tokio::sync::RwLock; use tracing::{debug, info}; -use crate::bridge::bifrost::BifrostClient; +use crate::bridge::openai_compatible::OpenAiCompatibleClient; use crate::core::config::{ModelConfig, TaskType}; /// Context pressure — how full the context window is @@ -83,8 +83,8 @@ pub struct ModelRouter { configs: HashMap, current_usage: Arc>, token_counter: TokenCounter, - bifrost_client: Option, - bifrost_models: Vec, + provider_client: Option, + discovered_models: Vec, selected_model: String, } @@ -105,19 +105,19 @@ impl ModelRouter { configs, current_usage: Arc::new(RwLock::new(TokenUsage::default())), token_counter: TokenCounter::new(), - bifrost_client: None, - bifrost_models: Vec::new(), + provider_client: None, + discovered_models: Vec::new(), selected_model: String::new(), } } - /// Create a ModelRouter with Bifrost client for dynamic model discovery - pub fn with_bifrost( + /// Create a ModelRouter with a provider client for dynamic model discovery + pub fn with_provider_client( configs: HashMap, - bifrost_client: BifrostClient, + provider_client: OpenAiCompatibleClient, ) -> Self { let mut router = Self::new(configs); - router.bifrost_client = Some(bifrost_client); + router.provider_client = Some(provider_client); router } @@ -210,17 +210,17 @@ impl ModelRouter { providers } - /// Fetch models from Bifrost dynamically - pub async fn fetch_bifrost_models(&mut self) -> anyhow::Result> { - if let Some(client) = &self.bifrost_client { + /// Fetch the provider's advertised model list + pub async fn fetch_provider_models(&mut self) -> anyhow::Result> { + if let Some(client) = &self.provider_client { match client.list_models().await { Ok(models) => { - self.bifrost_models = models.clone(); - info!("🌐 Fetched {} models from Bifrost ", models.len()); + self.discovered_models = models.clone(); + info!("🌐 Fetched {} models from the provider", models.len()); Ok(models) } Err(e) => { - tracing::warn!("Failed to fetch models from Bifrost: {}", e); + tracing::warn!("Failed to fetch the provider's model list: {}", e); Ok(vec![]) } } @@ -229,9 +229,9 @@ impl ModelRouter { } } - /// Get all models (Bifrost-discovered + configured) + /// Get all models (provider-discovered + configured) pub fn all_models(&self) -> Vec { - let mut models = self.bifrost_models.clone(); + let mut models = self.discovered_models.clone(); for name in self.configs.keys() { if !models.contains(name) { models.push(name.clone()); @@ -264,9 +264,9 @@ impl ModelRouter { } } - /// Check if model is from Bifrost - pub fn is_bifrost_model(&self, name: &str) -> bool { - self.bifrost_models.contains(&name.to_string()) + /// Check if the model came from provider discovery + pub fn is_discovered_model(&self, name: &str) -> bool { + self.discovered_models.contains(&name.to_string()) } /// Get model info for display @@ -277,7 +277,7 @@ impl ModelRouter { context_limit: cfg.context_limit, output_limit: cfg.output_limit, preferred_for: cfg.preferred_for.clone(), - from_bifrost: self.is_bifrost_model(name), + from_discovery: self.is_discovered_model(name), }) } } @@ -290,7 +290,7 @@ pub struct ModelInfo { pub context_limit: usize, pub output_limit: usize, pub preferred_for: Vec, - pub from_bifrost: bool, + pub from_discovery: bool, } #[cfg(test)] diff --git a/src/bridge/bifrost.rs b/src/bridge/openai_compatible.rs similarity index 90% rename from src/bridge/bifrost.rs rename to src/bridge/openai_compatible.rs index 2aa9c0b..7e95ff0 100644 --- a/src/bridge/bifrost.rs +++ b/src/bridge/openai_compatible.rs @@ -7,16 +7,17 @@ use tracing::{debug, info, warn}; use crate::bridge::provider::LlmProvider; -/// Bifrost Inference Client -/// -/// Bifrost is an OpenAI-compatible API gateway: http://127.0.0.1:3360/v1 +/// Client for any endpoint speaking the OpenAI `/chat/completions` shape — +/// DeepSeek, z.ai, Google's OpenAI-compat root, a local llama-swap, and the +/// Bifrost gateway among them. It is not specific to Bifrost, and its logs +/// must not imply a request traversed that service. #[derive(Debug, Clone)] -pub struct BifrostClient { +pub struct OpenAiCompatibleClient { /// 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. + /// Configured `[providers.]` name — what logs and errors call this + /// endpoint, so a failure names the provider the operator configured. id: String, /// Bearer token for auth api_key: String, @@ -339,7 +340,7 @@ pub struct Usage { pub cache_write_tokens: u32, } -/// Stream chunk from Bifrost (OpenAI SSE format) +/// Stream chunk in OpenAI SSE format #[derive(Debug, Clone, Deserialize)] pub struct StreamChunk { pub choices: Vec, @@ -441,8 +442,9 @@ pub enum InferenceStrain { }, } -/// A Bifrost upstream timeout (`504` with `request_timed_out` / `"type":"timeout"`) -/// is *deterministic* — the same slow model on the same request will time out +/// An upstream timeout (`504` with `request_timed_out` / `"type":"timeout"`, as +/// the Bifrost gateway reports it) is *deterministic* — the same slow model on +/// the same request will time out /// again. Retrying it the full 6 times just multiplies one ~30s failure into a /// multi-minute stall that was never going to succeed. Cap those at a single /// retry (2 attempts total). Genuine transient blips — `503` overloaded, @@ -465,7 +467,7 @@ fn retry_cap(body: &str, max_retries: u32) -> u32 { /// /// `claude_subscription.rs` learned this first and answers it by rotating to /// another login, so there an *absent* `retry-after` also counts as exhausted. -/// Bifrost has nothing to rotate to, so an absent header keeps the ordinary +/// This client has nothing to rotate to, so an absent header keeps the ordinary /// jittered backoff — only an explicitly long one fails fast. const BURST_RETRY_CEILING_SECS: u64 = 60; @@ -539,8 +541,9 @@ fn has_own_path(url: &str) -> bool { reqwest::Url::parse(url).is_ok_and(|u| u.path() != "/") } -impl BifrostClient { +impl OpenAiCompatibleClient { pub fn new( + id: &str, base_url: &str, api_key: &str, virtual_key: &str, @@ -555,31 +558,23 @@ impl BifrostClient { }; info!( - "🌉 Bifrost client initialized — model: {}, endpoint: {}, timeout: {}s", - default_model, base_url, timeout_secs + "provider {} ready — model: {}, endpoint: {}, timeout: {}s", + id, default_model, base_url, timeout_secs ); Ok(Self { base_url, - id: "bifrost".to_string(), + id: id.to_string(), api_key: api_key.to_string(), virtual_key: virtual_key.to_string(), client: reqwest::Client::builder() .timeout(Duration::from_secs(timeout_secs)) .build() - .context("building bifrost reqwest client")?, + .with_context(|| format!("building reqwest client for provider {id}"))?, default_model: default_model.to_string(), retry_policy: RetryPolicy::default(), }) } - /// 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) -> Self { - self.id = id.into(); - self - } - pub fn with_fallbacks(mut self, fallbacks: Vec) -> Self { self.retry_policy.fallback_models = fallbacks; self @@ -603,7 +598,7 @@ impl BifrostClient { headers } - /// List available models from Bifrost + /// List the models this provider advertises at `/models`. pub async fn list_models(&self) -> Result> { let url = format!("{}/models", self.base_url); let resp = self @@ -612,7 +607,7 @@ impl BifrostClient { .headers(self.auth_headers()) .send() .await - .with_context(|| "Failed to fetch Bifrost models")?; + .with_context(|| format!("provider {} — failed to fetch models", self.id))?; let body: serde_json::Value = resp.json().await?; let models = body["data"] @@ -719,12 +714,18 @@ impl BifrostClient { Ok(r) => r, Err(e) if e.is_timeout() || e.is_connect() => { if attempt == policy.max_retries { - anyhow::bail!("Bifrost unreachable after {} attempts: {}", attempt + 1, e); + anyhow::bail!( + "provider {} unreachable at {} after {} attempts: {}", + self.id, + url, + attempt + 1, + e + ); } let delay = jittered_delay(attempt, policy); warn!( - "Bifrost connection failed (attempt {}), retrying in {:?}: {}", - attempt, delay, e + "provider {} connection failed at {} (attempt {}), retrying in {:?}: {}", + self.id, url, attempt, delay, e ); strain_events.push(InferenceStrain::Transient { attempt, @@ -745,14 +746,14 @@ impl BifrostClient { let body_text = resp .text() .await - .context("Failed to read Bifrost response body")?; - return Self::parse_completion_response(&body_text); + .with_context(|| format!("provider {} — unreadable response body", self.id))?; + return self.parse_completion_response(&body_text); } let body_text = resp .text() .await - .context("Failed to read Bifrost error body")?; + .with_context(|| format!("provider {} — unreadable error body", self.id))?; match classify_status(status, &body_text) { ErrorClass::Transient @@ -761,7 +762,8 @@ impl BifrostClient { { let delay = retry_after.unwrap_or_else(|| jittered_delay(attempt, policy)); warn!( - "Bifrost {} on {} (attempt {}), retrying in {:?}", + "provider {} returned {} on {} (attempt {}), retrying in {:?}", + self.id, status.as_u16(), model, attempt, @@ -783,7 +785,8 @@ impl BifrostClient { body: body_text[..body_text.len().min(300)].to_string(), }); anyhow::bail!( - "Bifrost returned {} after {} attempt(s) on {}{}: {}", + "provider {} returned {} after {} attempt(s) on {}{}: {}", + self.id, status, attempt + 1, model, @@ -794,21 +797,24 @@ impl BifrostClient { } } - anyhow::bail!("Bifrost retry loop exhausted without returning a result") + anyhow::bail!( + "provider {} exhausted its retry loop without returning a result", + self.id + ) } - fn parse_completion_response(body_text: &str) -> Result { + fn parse_completion_response(&self, body_text: &str) -> Result { let parsed: ChatCompletionResponse = serde_json::from_str(body_text).with_context(|| { let preview = &body_text[..body_text.len().min(200)]; - format!("Failed to parse Bifrost response: {preview}") + format!("provider {} — unparseable response: {preview}", self.id) })?; let choice = parsed .choices .into_iter() .next() - .context("Bifrost returned empty choices")?; + .with_context(|| format!("provider {} returned empty choices", self.id))?; let content = choice.message.content.unwrap_or_default(); let reasoning = choice.message.reasoning; @@ -839,11 +845,11 @@ impl BifrostClient { } } -/// `BifrostClient` is the OpenAI-compatible gateway implementation of the +/// `OpenAiCompatibleClient` is the OpenAI-compatible gateway implementation of the /// provider seam. The inherent methods do the work; the trait just exposes them /// behind `dyn LlmProvider` so the engine can hold any provider uniformly. #[async_trait] -impl LlmProvider for BifrostClient { +impl LlmProvider for OpenAiCompatibleClient { fn id(&self) -> &str { &self.id } @@ -853,18 +859,18 @@ impl LlmProvider for BifrostClient { } async fn list_models(&self) -> Result> { - BifrostClient::list_models(self).await + OpenAiCompatibleClient::list_models(self).await } async fn chat_completion_with_strain( &self, request: ChatCompletionRequest, ) -> Result<(CompletionResult, Vec)> { - BifrostClient::chat_completion_with_strain(self, request).await + OpenAiCompatibleClient::chat_completion_with_strain(self, request).await } async fn chat_completion(&self, request: ChatCompletionRequest) -> Result { - BifrostClient::chat_completion(self, request).await + OpenAiCompatibleClient::chat_completion(self, request).await } } @@ -897,7 +903,7 @@ mod tests { #[test] fn exhausted_window_ignores_absent_headers_and_other_statuses() { - // Bifrost has no account to rotate to, so a 429 with no header keeps + // This client has no account to rotate to, so a 429 with no header keeps // the ordinary jittered backoff — this is where it diverges from // `claude_subscription::is_exhausted_window` on purpose. assert!(!is_exhausted_window( @@ -923,7 +929,8 @@ mod tests { /// Appending `/v1` there produced a path that hangs on POST. #[test] fn google_nested_version_path_is_not_doubled() { - let client = BifrostClient::new( + let client = OpenAiCompatibleClient::new( + "gemini", "https://generativelanguage.googleapis.com/v1beta/openai", "", "", @@ -967,14 +974,15 @@ mod tests { ("http://10.10.20.19:8080", "http://10.10.20.19:8080/v1"), ]; for (base, want) in cases { - let client = BifrostClient::new(base, "", "", "m", 30).unwrap(); + let client = OpenAiCompatibleClient::new("p", base, "", "", "m", 30).unwrap(); assert_eq!(client.base_url, want, "base {base}"); } } #[test] fn test_client_creation() { - let client = BifrostClient::new( + let client = OpenAiCompatibleClient::new( + "bifrost", "http://127.0.0.1:3360", "sk-bf-test", "", @@ -983,6 +991,7 @@ mod tests { ) .unwrap(); assert!(client.base_url.ends_with("/v1")); + assert_eq!(client.id, "bifrost"); } #[test] diff --git a/src/bridge/provider.rs b/src/bridge/provider.rs index d6f5728..226f47a 100644 --- a/src/bridge/provider.rs +++ b/src/bridge/provider.rs @@ -14,7 +14,7 @@ use anyhow::Result; use async_trait::async_trait; -use super::bifrost::{ChatCompletionRequest, CompletionResult, InferenceStrain}; +use super::openai_compatible::{ChatCompletionRequest, CompletionResult, InferenceStrain}; #[async_trait] pub trait LlmProvider: Send + Sync { diff --git a/src/bridge/providers/mod.rs b/src/bridge/providers/mod.rs index e9d7386..61dec40 100644 --- a/src/bridge/providers/mod.rs +++ b/src/bridge/providers/mod.rs @@ -1,7 +1,7 @@ //! Concrete [`LlmProvider`](crate::bridge::provider::LlmProvider) implementations. //! -//! `BifrostClient` (the OpenAI-compatible gateway client) implements the trait -//! in `bridge::bifrost`; the OAuth-riding ChatGPT provider lives here. +//! `OpenAiCompatibleClient` (the OpenAI-compatible gateway client) implements the trait +//! in `bridge::openai_compatible`; the OAuth-riding ChatGPT provider lives here. pub mod openai_oauth; pub mod responses; diff --git a/src/bridge/providers/openai_oauth.rs b/src/bridge/providers/openai_oauth.rs index 44c94f4..24a69d2 100644 --- a/src/bridge/providers/openai_oauth.rs +++ b/src/bridge/providers/openai_oauth.rs @@ -15,7 +15,7 @@ use async_trait::async_trait; use tokio::sync::Mutex; use tracing::warn; -use crate::bridge::bifrost::{ +use crate::bridge::openai_compatible::{ ChatCompletionRequest, CompletionResult, InferenceStrain, RetryPolicy, }; use crate::bridge::oauth::codex_creds::{self, CodexCredentials}; @@ -44,7 +44,7 @@ impl OpenAiOAuthProvider { .timeout(Duration::from_secs(timeout_secs)) .build() .context("building OpenAI OAuth reqwest client")?; - // The configured `primary_model` may be a Bifrost-namespaced id + // The configured `primary_model` may be a provider-namespaced id // (`openai/…`); pin the provider default to a model this backend serves. let default_model = catalog::resolve(&default_model, catalog::DEFAULT_MODEL); Ok(Self { @@ -81,7 +81,7 @@ impl LlmProvider for OpenAiOAuthProvider { &self, request: ChatCompletionRequest, ) -> Result<(CompletionResult, Vec)> { - // Translate the engine's (possibly Bifrost-namespaced) model id onto a + // Translate the engine's (possibly provider-namespaced) model id onto a // model the codex backend actually serves before building the payload. let mut request = request; request.model = catalog::resolve(&request.model, &self.default_model); diff --git a/src/bridge/providers/responses.rs b/src/bridge/providers/responses.rs index 82b5599..f7f437c 100644 --- a/src/bridge/providers/responses.rs +++ b/src/bridge/providers/responses.rs @@ -9,7 +9,7 @@ use anyhow::{anyhow, Result}; use serde_json::{json, Value}; -use crate::bridge::bifrost::{ChatCompletionRequest, CompletionResult, ParsedToolCall, Usage}; +use crate::bridge::openai_compatible::{ChatCompletionRequest, CompletionResult, ParsedToolCall, Usage}; /// Models that take a `reasoning` block (GPT-5.x / o-series). fn is_reasoning_model(model: &str) -> bool { diff --git a/src/core/archivist/mod.rs b/src/core/archivist/mod.rs index 5ba4398..aeeae22 100644 --- a/src/core/archivist/mod.rs +++ b/src/core/archivist/mod.rs @@ -36,7 +36,7 @@ use anyhow::Result; use chrono::{NaiveDate, Utc}; use serde::{Deserialize, Serialize}; -use crate::bridge::bifrost::{ChatCompletionRequest, Message}; +use crate::bridge::openai_compatible::{ChatCompletionRequest, Message}; use crate::bridge::LlmProvider; use crate::bridge::ProviderRegistry; use crate::core::cadence::Cadence; @@ -376,7 +376,7 @@ impl ArchivistEngine { let (response, strain) = llm.chat_completion_with_strain(request).await?; for event in &strain { - if let crate::bridge::bifrost::InferenceStrain::Transient { status, model, .. } = event + if let crate::bridge::openai_compatible::InferenceStrain::Transient { status, model, .. } = event { tracing::info!("archivist felt inference strain: {} on {}", status, model); if *status == 429 { diff --git a/src/core/compact/strategy.rs b/src/core/compact/strategy.rs index d4b5895..4f5836a 100644 --- a/src/core/compact/strategy.rs +++ b/src/core/compact/strategy.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use async_trait::async_trait; -use crate::bridge::bifrost::{ChatCompletionRequest, Message}; +use crate::bridge::openai_compatible::{ChatCompletionRequest, Message}; use crate::bridge::model_router::TokenCounter; use crate::bridge::LlmProvider; use crate::core::session::ConversationMessage; @@ -51,8 +51,8 @@ pub trait CompactionStrategy: Send + Sync { ) -> anyhow::Result; } -/// Helper: call a Bifrost model with system+user prompt, get text response. -async fn bifrost_complete( +/// Helper: call a provider model with system+user prompt, get text response. +async fn provider_complete( client: &Arc, model: &str, system: &str, @@ -151,7 +151,7 @@ impl CompactionStrategy for SummaryStrategy { ), }; - let summary = bifrost_complete( + let summary = provider_complete( &self.client, &self.model, SUMMARY_SYSTEM_PROMPT, @@ -533,7 +533,7 @@ impl CompactionStrategy for SlidingWindowStrategy { /// even after the originals are gone. /// /// Uses whichever model the engine provides (subconscious model if subconscious is -/// enabled, compaction model / primary otherwise). If no Bifrost client is +/// enabled, compaction model / primary otherwise). If no provider client is /// available, falls back to plain SlidingWindow (no threads lost is better /// than no compaction at all). pub struct SlidingReflectStrategy { @@ -620,7 +620,7 @@ impl CompactionStrategy for SlidingReflectStrategy { ), }; - let reflection = bifrost_complete( + let reflection = provider_complete( &self.client, &self.model, &system_prompt, diff --git a/src/core/reflection/mod.rs b/src/core/reflection/mod.rs index 2cc5d86..b425560 100644 --- a/src/core/reflection/mod.rs +++ b/src/core/reflection/mod.rs @@ -31,7 +31,7 @@ use anyhow::Result; use chrono::Utc; use serde::{Deserialize, Serialize}; -use crate::bridge::bifrost::{ChatCompletionRequest, Message, ToolDefinition, ToolFunction}; +use crate::bridge::openai_compatible::{ChatCompletionRequest, Message, ToolDefinition, ToolFunction}; use crate::bridge::LlmProvider; use crate::bridge::ProviderRegistry; use crate::core::cadence::Cadence; @@ -326,7 +326,7 @@ impl ReflectionEngine { let (response, strain) = llm.chat_completion_with_strain(request).await?; for event in &strain { - if let crate::bridge::bifrost::InferenceStrain::Transient { + if let crate::bridge::openai_compatible::InferenceStrain::Transient { status, model, .. } = event { @@ -354,11 +354,11 @@ impl ReflectionEngine { }); } - let calls: Vec = response + let calls: Vec = response .tool_calls .iter() .map(|tc| { - crate::bridge::bifrost::MessageToolCall::function( + crate::bridge::openai_compatible::MessageToolCall::function( tc.id.clone(), tc.name.clone(), tc.arguments.to_string(), diff --git a/src/core/session/mod.rs b/src/core/session/mod.rs index 9f67ac7..e30c02e 100644 --- a/src/core/session/mod.rs +++ b/src/core/session/mod.rs @@ -319,12 +319,12 @@ impl Session { Ok(Some(session)) } - /// Convert to Bifrost API message format. + /// Convert to the OpenAI wire message format. /// /// Delegates to [`replay_messages`] — the one projection of stored history /// onto the wire. This wrapper degrades images, because the callers that /// can see natively build their own policy from the agent's config. - pub fn to_bifrost_messages(&self) -> Vec { + pub fn to_wire_messages(&self) -> Vec { replay_messages(&self.messages, ImagePolicy::Degrade) } } @@ -388,8 +388,8 @@ const UNFINISHED_CALL: &str = pub fn replay_messages( messages: &[ConversationMessage], images: ImagePolicy, -) -> Vec { - use crate::bridge::bifrost::{ContentPart, ImageUrlSource, Message, MessageToolCall}; +) -> Vec { + use crate::bridge::openai_compatible::{ContentPart, ImageUrlSource, Message, MessageToolCall}; use std::collections::HashSet; let ids = |f: fn(&ContentBlock) -> Option<&str>| -> HashSet<&str> { @@ -721,10 +721,10 @@ mod tests { } #[test] - fn test_bifrost_conversion() { + fn test_wire_conversion() { let mut session = Session::new("ani"); session.add_message(ConversationMessage::user_text("hello")); - let msgs = session.to_bifrost_messages(); + let msgs = session.to_wire_messages(); assert_eq!(msgs.len(), 1); assert_eq!(msgs[0].role, "user"); assert_eq!(msgs[0].content.as_text(), "hello"); @@ -763,7 +763,7 @@ mod tests { timestamp: None, }); - let msgs = session.to_bifrost_messages(); + let msgs = session.to_wire_messages(); // Every call is declared, and every tool message answers a declared // call. A half-pair in either direction is rejected by the wire. diff --git a/src/core/tools/mod.rs b/src/core/tools/mod.rs index 0e5118d..2589b17 100644 --- a/src/core/tools/mod.rs +++ b/src/core/tools/mod.rs @@ -59,7 +59,7 @@ pub const SUBCONSCIOUS_ONLY_TOOLS: &[&str] = &["halt", "intrusive"]; // ── Re-export for backward compat ─────────────────────────────── -/// Serializable tool definition sent to the model (bridges to Bifrost). +/// Serializable tool definition sent to the model (bridges to the OpenAI wire format). #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ToolDefinition { pub name: String, diff --git a/src/server/consciousness_engine.rs b/src/server/consciousness_engine.rs index f69836c..909ab73 100644 --- a/src/server/consciousness_engine.rs +++ b/src/server/consciousness_engine.rs @@ -4,7 +4,7 @@ //! //! ## N+1 (subconscious) //! The subconscious pass runs immediately after every response. It takes the -//! last exchange (user message + primary's response) and sends it to a Bifrost +//! last exchange (user message + primary's response) and sends it to a provider //! model (defaulting to `glm-5.1`, configurable) with a "subconscious mode" //! system prompt. subconscious has full tool access — Read, Write, Edit, Glob, Grep, //! ListDir, and Memory — so she can read ledgers, check commitments, and write @@ -27,7 +27,7 @@ //! immediately after the primary's turn. use crate::backend::BackendEvent; -use crate::bridge::bifrost::{ChatCompletionRequest, Message, ToolDefinition, ToolFunction}; +use crate::bridge::openai_compatible::{ChatCompletionRequest, Message, ToolDefinition, ToolFunction}; use crate::bridge::model_router::TokenCounter; use crate::bridge::LlmProvider; use crate::bridge::ProviderRegistry; @@ -307,7 +307,7 @@ impl ConsciousnessEngine { } if let Err(e) = self .sessions - .add_message(conv_id, bifrost_to_conversation(m)) + .add_message(conv_id, wire_to_conversation(m)) { tracing::warn!("subconscious session persist failed: {}", e); } @@ -881,7 +881,7 @@ Resolve entries: `[YYYY-MM-DD HH:MM] RESOLVED — note`"#; Vec::new() }; - // Build Bifrost messages: system prompt (always current) + history + new exchange. + // Build wire messages: system prompt (always current) + history + new exchange. let mut messages: Vec = Vec::new(); messages.push(Message::text("system", system_prompt.to_string())); @@ -901,7 +901,7 @@ Resolve entries: `[YYYY-MM-DD HH:MM] RESOLVED — note`"#; persisted.messages = prior_messages; messages.extend( persisted - .to_bifrost_messages() + .to_wire_messages() .into_iter() .filter(|m| m.role != "system"), ); @@ -954,7 +954,7 @@ Resolve entries: `[YYYY-MM-DD HH:MM] RESOLVED — note`"#; ); // Chunked-replay streaming for live TUI visibility — mirrors the - // primary's pattern at `src/server/turn.rs:445-471`. Bifrost + // primary's pattern at `src/server/turn.rs:445-471`. The provider // returns the full response in one shot; we slice it into small // pieces and emit them with a small inter-chunk delay so the // subconscious appears to be typing in real time. @@ -987,7 +987,7 @@ Resolve entries: `[YYYY-MM-DD HH:MM] RESOLVED — note`"#; } for event in &strain { - if let crate::bridge::bifrost::InferenceStrain::Transient { + if let crate::bridge::openai_compatible::InferenceStrain::Transient { attempt, status, model, @@ -1045,11 +1045,11 @@ Resolve entries: `[YYYY-MM-DD HH:MM] RESOLVED — note`"#; } // Add assistant message with tool calls (OpenAI tool-use schema) - let calls: Vec = response + let calls: Vec = response .tool_calls .iter() .map(|tc| { - crate::bridge::bifrost::MessageToolCall::function( + crate::bridge::openai_compatible::MessageToolCall::function( tc.id.clone(), tc.name.clone(), tc.arguments.to_string(), @@ -1256,11 +1256,11 @@ Resolve entries: `[YYYY-MM-DD HH:MM] RESOLVED — note`"#; } } -/// Convert a Bifrost API message into the internal `ConversationMessage` +/// Convert an OpenAI wire message into the internal `ConversationMessage` /// form the session store and compaction engine operate on. The subconscious -/// runs her tool loop in Bifrost `Message`s; this is the bridge back to her +/// runs her tool loop in wire `Message`s; this is the bridge back to her /// persistent session. -fn bifrost_to_conversation(msg: &Message) -> ConversationMessage { +fn wire_to_conversation(msg: &Message) -> ConversationMessage { let role = match msg.role.as_str() { "system" => MessageRole::System, "user" => MessageRole::User, diff --git a/src/server/subagent.rs b/src/server/subagent.rs index d0832fe..9839f14 100644 --- a/src/server/subagent.rs +++ b/src/server/subagent.rs @@ -2,7 +2,7 @@ use async_trait::async_trait; use std::sync::Arc; use std::time::Duration; -use crate::bridge::bifrost::{ChatCompletionRequest, Message as BifrostMessage}; +use crate::bridge::openai_compatible::{ChatCompletionRequest, Message as WireMessage}; use crate::core::tools::defs::{SubagentParams, SubagentRunner, ToolContext}; use crate::server::SouveraineServer; @@ -98,11 +98,11 @@ impl SubagentRunner for ServerSubagentRunner { // Build tool definitions let core_tools = crate::core::tools::tool_definitions().await; - let bifrost_tools: Vec = core_tools + let wire_tools: Vec = core_tools .iter() - .map(|t| crate::bridge::bifrost::ToolDefinition { + .map(|t| crate::bridge::openai_compatible::ToolDefinition { tool_type: "function".to_string(), - function: crate::bridge::bifrost::ToolFunction { + function: crate::bridge::openai_compatible::ToolFunction { name: t.name.clone(), description: t.description.clone(), parameters: t.input_schema.clone(), @@ -126,8 +126,8 @@ impl SubagentRunner for ServerSubagentRunner { // messages into a separate field are left with an empty messages // array otherwise, and reject the request. let mut messages = vec![ - BifrostMessage::text("system", system_prompt), - BifrostMessage::text("user", params.prompt.clone()), + WireMessage::text("system", system_prompt), + WireMessage::text("user", params.prompt.clone()), ]; let mut final_content = String::new(); @@ -145,7 +145,7 @@ impl SubagentRunner for ServerSubagentRunner { let progress = tool_round as f32 / max_tool_rounds as f32; if !warned_1 && progress >= warning_1_threshold { warned_1 = true; - messages.push(BifrostMessage::text( + messages.push(WireMessage::text( "system", format!( "[subagent awareness] I've used {} of {} tool rounds. \ @@ -159,7 +159,7 @@ impl SubagentRunner for ServerSubagentRunner { // Warning 2: nearing the limit, this is the last stretch if !warned_2 && progress >= warning_2_threshold { warned_2 = true; - messages.push(BifrostMessage::text( + messages.push(WireMessage::text( "system", format!( "[subagent awareness] I'm at {} of {} tool rounds. \ @@ -181,7 +181,7 @@ impl SubagentRunner for ServerSubagentRunner { .count(); request_messages.insert( system_prefix, - BifrostMessage::text("system", principal.model_system_block()), + WireMessage::text("system", principal.model_system_block()), ); let req = ChatCompletionRequest { @@ -190,7 +190,7 @@ impl SubagentRunner for ServerSubagentRunner { stream: Some(false), max_tokens: None, temperature, - tools: Some(bifrost_tools.clone()), + tools: Some(wire_tools.clone()), }; let response = llm.chat_completion(req).await.map_err(|e| { @@ -207,11 +207,11 @@ impl SubagentRunner for ServerSubagentRunner { tool_round += 1; // Add assistant tool-call message (OpenAI tool-use schema, not stringified blob) - let calls: Vec = response + let calls: Vec = response .tool_calls .iter() .map(|tc| { - crate::bridge::bifrost::MessageToolCall::function( + crate::bridge::openai_compatible::MessageToolCall::function( tc.id.clone(), tc.name.clone(), tc.arguments.to_string(), @@ -219,7 +219,7 @@ impl SubagentRunner for ServerSubagentRunner { }) .collect(); messages.push( - BifrostMessage::assistant_tool_calls(response.content.clone(), calls) + WireMessage::assistant_tool_calls(response.content.clone(), calls) .with_thinking( response.reasoning.clone(), response.reasoning_signature.clone(), @@ -239,7 +239,7 @@ impl SubagentRunner for ServerSubagentRunner { result.output }; - messages.push(BifrostMessage::tool_result(&tc.id, &tc.name, output)); + messages.push(WireMessage::tool_result(&tc.id, &tc.name, output)); } // Brief pause between tool rounds to let rate limits cool diff --git a/src/server/turn.rs b/src/server/turn.rs index 22d6c0b..7acfd31 100644 --- a/src/server/turn.rs +++ b/src/server/turn.rs @@ -4,8 +4,8 @@ use std::time::{Duration, Instant}; use tokio::sync::mpsc; use tokio_util::sync::CancellationToken; -use crate::bridge::bifrost::{ - ChatCompletionRequest, CompletionResult, ContentPart, ImageUrlSource, Message as BifrostMessage, +use crate::bridge::openai_compatible::{ + ChatCompletionRequest, CompletionResult, ContentPart, ImageUrlSource, Message as WireMessage, }; use crate::bridge::model_router::TokenCounter; use crate::core::compact::CompactionEngine; @@ -20,13 +20,13 @@ use crate::server::energy::write_energy_balance; use crate::server::subagent::ServerSubagentRunner; /// Mirror of `ConsciousnessEngine::calculate_pressure` for the in-loop -/// BifrostMessage shape, so we can recompute pressure as tool results +/// WireMessage shape, so we can recompute pressure as tool results /// accumulate inside a single turn. `context_limit` comes from the /// agent's `llm_config.context_window` (Constitution V.3 — per-model /// physics, no hardcoded 128K). -fn bifrost_pressure( +fn wire_pressure( counter: &TokenCounter, - messages: &[BifrostMessage], + messages: &[WireMessage], context_limit: usize, ) -> (usize, f32) { let tokens: usize = messages @@ -113,7 +113,7 @@ pub(crate) async fn run_turn( let agent = server.agents.get(&agent_id).await?; let supports_images = agent.llm_config.supports_images; - // Snapshot history for the Bifrost call, then drop the dashmap ref before + // Snapshot history for the provider call, then drop the dashmap ref before // any await — `Ref` is not Send across awaits. // // One projection, shared with every other replay of stored history @@ -183,17 +183,17 @@ pub(crate) async fn run_turn( ..tool_ctx }; - // Build bifrost-format tool definitions from the core tool set. The + // Build wire-format tool definitions from the core tool set. The // subconscious-only tools (halt, intrusive) are filtered OUT here — // the primary must never see them in her tool list. Subconscious's // own loop whitelists them in via SUBCONSCIOUS_SAFE_TOOLS. let core_tools = crate::core::tools::tool_definitions().await; - let bifrost_tools: Vec = core_tools + let wire_tools: Vec = core_tools .iter() .filter(|t| !crate::core::tools::SUBCONSCIOUS_ONLY_TOOLS.contains(&t.name.as_str())) - .map(|t| crate::bridge::bifrost::ToolDefinition { + .map(|t| crate::bridge::openai_compatible::ToolDefinition { tool_type: "function".to_string(), - function: crate::bridge::bifrost::ToolFunction { + function: crate::bridge::openai_compatible::ToolFunction { name: t.name.clone(), description: t.description.clone(), parameters: t.input_schema.clone(), @@ -245,7 +245,7 @@ pub(crate) async fn run_turn( loop { // Cancellation is a signal, not enforcement — we check it on round - // boundaries (between Bifrost calls, after tools have completed) so + // boundaries (between provider calls, after tools have completed) so // partial work is preserved. No hard-kill mid-tool. if cancel.is_cancelled() { interrupted = true; @@ -272,7 +272,7 @@ pub(crate) async fn run_turn( for text in drained { let stamp = chrono::Local::now().format("%H:%M"); let note = format!("[user interjected at {} — {}]", stamp, text.trim()); - messages.push(BifrostMessage::text("system", note)); + messages.push(WireMessage::text("system", note)); } // Self-awareness pulse: a beat of noticing the time pass, in her @@ -280,11 +280,11 @@ pub(crate) async fn run_turn( // call so it lands in her context naturally. if pulse_enabled && last_pulse.elapsed() >= pulse_interval { let elapsed_total = turn_start.elapsed(); - messages.push(BifrostMessage::text("system", pulse_text(elapsed_total))); + messages.push(WireMessage::text("system", pulse_text(elapsed_total))); last_pulse = Instant::now(); } - let (tokens_used, pressure) = bifrost_pressure(&counter, &messages, context_limit); + let (tokens_used, pressure) = wire_pressure(&counter, &messages, context_limit); tracing::info!( turn_round = tool_round, agent = %agent_id, @@ -317,7 +317,7 @@ pub(crate) async fn run_turn( .count(); request_messages.insert( system_prefix, - BifrostMessage::text("system", principal.model_system_block()), + WireMessage::text("system", principal.model_system_block()), ); let req = ChatCompletionRequest { @@ -327,7 +327,7 @@ pub(crate) async fn run_turn( max_tokens, temperature, tools: if max_rounds > 0 { - Some(bifrost_tools.clone()) + Some(wire_tools.clone()) } else { None }, @@ -381,7 +381,7 @@ pub(crate) async fn run_turn( } for event in &strain { - if let crate::bridge::bifrost::InferenceStrain::Transient { + if let crate::bridge::openai_compatible::InferenceStrain::Transient { attempt, status, model, @@ -446,8 +446,8 @@ pub(crate) async fn run_turn( let _ = tx.send(Ok(BackendEvent::Token(s.clone()))).await; tokio::time::sleep(tokio::time::Duration::from_millis(10)).await; } - messages.push(BifrostMessage::text("assistant", response.content.clone())); - messages.push(BifrostMessage::text( + messages.push(WireMessage::text("assistant", response.content.clone())); + messages.push(WireMessage::text( "system", "My output just hit its ceiling — I was cut off mid-flow, not \ finished. If I was in the middle of something, I can continue \ @@ -486,13 +486,13 @@ pub(crate) async fn run_turn( dispatcher.emit_segment(&s); tokio::time::sleep(tokio::time::Duration::from_millis(10)).await; } - messages.push(BifrostMessage::text("assistant", response.content.clone())); + messages.push(WireMessage::text("assistant", response.content.clone())); carried_replies.push(response.content.clone()); } for text in &interjected { let stamp = chrono::Local::now().format("%H:%M"); let note = format!("[interjected at {} — {}]", stamp, text.trim()); - messages.push(BifrostMessage::text("user", note)); + messages.push(WireMessage::text("user", note)); } // Continue the loop — agent sees the interjection as a // user message and will respond in the next LLM round. @@ -558,11 +558,11 @@ pub(crate) async fn run_turn( // Add the assistant's tool-call message in proper OpenAI tool-use schema // (not a stringified JSON blob in content — that's what broke turn 2). - let calls: Vec = response + let calls: Vec = response .tool_calls .iter() .map(|tc| { - crate::bridge::bifrost::MessageToolCall::function( + crate::bridge::openai_compatible::MessageToolCall::function( tc.id.clone(), tc.name.clone(), tc.arguments.to_string(), @@ -570,7 +570,7 @@ pub(crate) async fn run_turn( }) .collect(); messages.push( - BifrostMessage::assistant_tool_calls(response.content.clone(), calls).with_thinking( + WireMessage::assistant_tool_calls(response.content.clone(), calls).with_thinking( response.reasoning.clone(), response.reasoning_signature.clone(), ), @@ -739,7 +739,7 @@ pub(crate) async fn run_turn( } // Bind tool result to its call by id (OpenAI tool-use schema). - messages.push(BifrostMessage::tool_result(&tc.id, &tc.name, output)); + messages.push(WireMessage::tool_result(&tc.id, &tc.name, output)); } // Now that every tool result is bound, the images can follow. @@ -757,7 +757,7 @@ pub(crate) async fn run_turn( } else { format!("The {} images I just read:", round_images.len()) }; - messages.push(BifrostMessage::multimodal_user(label, parts)); + messages.push(WireMessage::multimodal_user(label, parts)); } // ── Mid-turn peek ──────────────────────────────────────────── @@ -854,7 +854,7 @@ pub(crate) async fn run_turn( // commentary from outside. This is what she // experiences as "the migraine." let felt = migraine_text(&halt.severity, &halt.reason); - messages.push(BifrostMessage::text("system", &felt)); + messages.push(WireMessage::text("system", &felt)); if halt.severity == "advisory" { // Said slow down, not stop. She carries the diff --git a/src/tui/state.rs b/src/tui/state.rs index 001689d..a25e222 100644 --- a/src/tui/state.rs +++ b/src/tui/state.rs @@ -28,7 +28,7 @@ pub trait TuiState { // ========== Connection ========== fn is_connected(&self) -> bool; - fn is_processing(&self) -> bool; // Waiting for Bifrost + fn is_processing(&self) -> bool; // Waiting for the provider fn connection_error(&self) -> Option<&str>; // ========== Screen State ==========