Watch
1
0
Fork
You've already forked souveraine
0

bridge: generic openai client stops calling itself bifrost

OpenAiCompatibleClient serves every openai-compatible provider, so Gem's
turns to Google logged as "Bifrost client initialized" and "Bifrost
connection failed" without touching the gateway at all. Logs and errors
now name the configured [providers.<id>] and the endpoint. The real
[providers.bifrost], its config section and its key are untouched.
This commit is contained in:
Fimeg 2026-08-18 14:57:47 -04:00
commit 76e12045a4
19 changed files with 169 additions and 160 deletions

View file

@ -362,7 +362,7 @@ pub struct CreateConversationRequest {
/// ///
/// Untagged on purpose: a bare JSON string is exactly the wire it has always /// 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 /// 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 /// shape facing the provider. This is the inward-facing half, written in the
/// substrate's own [`ContentBlock`] vocabulary rather than OpenAI's, because /// substrate's own [`ContentBlock`] vocabulary rather than OpenAI's, because
/// `ContentBlock` is what the session stores, persists, and replays. /// `ContentBlock` is what the session stores, persists, and replays.

View file

@ -2,7 +2,7 @@
//! In-process Backend impl. Same engine as the HTTP server, no socket. //! In-process Backend impl. Same engine as the HTTP server, no socket.
//! //!
//! Constructed once with a `ConsciousnessConfig`; spins up an `AgentInventory` //! 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` //! and `ConsciousnessEngine`. `send` mirrors the server's `stream_messages`
//! handler, but emits `BackendEvent`s directly instead of SSE frames. //! handler, but emits `BackendEvent`s directly instead of SSE frames.
//! //!

View file

@ -4,7 +4,7 @@
//! Speaks the Claude Code (claude.ai OAuth) wire protocol directly against //! Speaks the Claude Code (claude.ai OAuth) wire protocol directly against
//! `api.anthropic.com`, using the OAuth credentials Claude Code already stored //! `api.anthropic.com`, using the OAuth credentials Claude Code already stored
//! at `~/.claude/.credentials.json`. Implements [`LlmProvider`] so it drops //! 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 //! Internally it translates OpenAI chat-completion requests (Souveraine's
//! internal shape) to Anthropic `/v1/messages`, applies the subscription wire //! internal shape) to Anthropic `/v1/messages`, applies the subscription wire
@ -31,7 +31,7 @@ use sha2::{Digest, Sha256};
use tokio::sync::Mutex; use tokio::sync::Mutex;
use tracing::{info, warn}; use tracing::{info, warn};
use super::bifrost::{ use super::openai_compatible::{
ChatCompletionRequest, CompletionResult, InferenceStrain, Message, ParsedToolCall, ChatCompletionRequest, CompletionResult, InferenceStrain, Message, ParsedToolCall,
ToolDefinition, Usage, ToolDefinition, Usage,
}; };
@ -1484,7 +1484,7 @@ fn status_hint(status: u16) -> &'static str {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::bridge::bifrost::{ use crate::bridge::openai_compatible::{
ContentValue, MessageToolCall, MessageToolCallFunction, ToolFunction, ContentValue, MessageToolCall, MessageToolCallFunction, ToolFunction,
}; };

View file

@ -2,17 +2,17 @@
/// Bridge module — LLM inference providers. /// Bridge module — LLM inference providers.
/// ///
/// Connects Souveraine to inference providers behind the [`LlmProvider`] trait. /// 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 /// ChatGPT provider lives in [`providers`]. [`build_provider`] selects the
/// active one from config; [`ProviderRegistry`] resolves a provider per agent. /// active one from config; [`ProviderRegistry`] resolves a provider per agent.
pub mod bifrost; pub mod openai_compatible;
pub mod claude_subscription; pub mod claude_subscription;
pub mod model_router; pub mod model_router;
pub mod oauth; pub mod oauth;
pub mod provider; pub mod provider;
pub mod providers; pub mod providers;
pub use bifrost::BifrostClient; pub use openai_compatible::OpenAiCompatibleClient;
pub use provider::LlmProvider; pub use provider::LlmProvider;
use std::collections::HashMap; use std::collections::HashMap;
@ -60,14 +60,14 @@ pub fn build_provider_from_config(
} }
_ => { _ => {
// "openai-compatible" and any unrecognized type → OpenAI-compatible gateway. // "openai-compatible" and any unrecognized type → OpenAI-compatible gateway.
let client = BifrostClient::new( let client = OpenAiCompatibleClient::new(
name,
&cfg.base_url, &cfg.base_url,
&cfg.api_key, &cfg.api_key,
&cfg.virtual_key, &cfg.virtual_key,
&cfg.primary_model, &cfg.primary_model,
cfg.timeout_secs, cfg.timeout_secs,
)? )?;
.with_id(name);
Ok(Arc::new(client)) Ok(Arc::new(client))
} }
} }

View file

@ -4,7 +4,7 @@ use std::sync::Arc;
use tokio::sync::RwLock; use tokio::sync::RwLock;
use tracing::{debug, info}; use tracing::{debug, info};
use crate::bridge::bifrost::BifrostClient; use crate::bridge::openai_compatible::OpenAiCompatibleClient;
use crate::core::config::{ModelConfig, TaskType}; use crate::core::config::{ModelConfig, TaskType};
/// Context pressure — how full the context window is /// Context pressure — how full the context window is
@ -83,8 +83,8 @@ pub struct ModelRouter {
configs: HashMap<String, ModelConfig>, configs: HashMap<String, ModelConfig>,
current_usage: Arc<RwLock<TokenUsage>>, current_usage: Arc<RwLock<TokenUsage>>,
token_counter: TokenCounter, token_counter: TokenCounter,
bifrost_client: Option<BifrostClient>, provider_client: Option<OpenAiCompatibleClient>,
bifrost_models: Vec<String>, discovered_models: Vec<String>,
selected_model: String, selected_model: String,
} }
@ -105,19 +105,19 @@ impl ModelRouter {
configs, configs,
current_usage: Arc::new(RwLock::new(TokenUsage::default())), current_usage: Arc::new(RwLock::new(TokenUsage::default())),
token_counter: TokenCounter::new(), token_counter: TokenCounter::new(),
bifrost_client: None, provider_client: None,
bifrost_models: Vec::new(), discovered_models: Vec::new(),
selected_model: String::new(), selected_model: String::new(),
} }
} }
/// Create a ModelRouter with Bifrost client for dynamic model discovery /// Create a ModelRouter with a provider client for dynamic model discovery
pub fn with_bifrost( pub fn with_provider_client(
configs: HashMap<String, ModelConfig>, configs: HashMap<String, ModelConfig>,
bifrost_client: BifrostClient, provider_client: OpenAiCompatibleClient,
) -> Self { ) -> Self {
let mut router = Self::new(configs); let mut router = Self::new(configs);
router.bifrost_client = Some(bifrost_client); router.provider_client = Some(provider_client);
router router
} }
@ -210,17 +210,17 @@ impl ModelRouter {
providers providers
} }
/// Fetch models from Bifrost dynamically /// Fetch the provider's advertised model list
pub async fn fetch_bifrost_models(&mut self) -> anyhow::Result<Vec<String>> { pub async fn fetch_provider_models(&mut self) -> anyhow::Result<Vec<String>> {
if let Some(client) = &self.bifrost_client { if let Some(client) = &self.provider_client {
match client.list_models().await { match client.list_models().await {
Ok(models) => { Ok(models) => {
self.bifrost_models = models.clone(); self.discovered_models = models.clone();
info!("🌐 Fetched {} models from Bifrost ", models.len()); info!("🌐 Fetched {} models from the provider", models.len());
Ok(models) Ok(models)
} }
Err(e) => { Err(e) => {
tracing::warn!("Failed to fetch models from Bifrost: {}", e); tracing::warn!("Failed to fetch the provider's model list: {}", e);
Ok(vec![]) 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<String> { pub fn all_models(&self) -> Vec<String> {
let mut models = self.bifrost_models.clone(); let mut models = self.discovered_models.clone();
for name in self.configs.keys() { for name in self.configs.keys() {
if !models.contains(name) { if !models.contains(name) {
models.push(name.clone()); models.push(name.clone());
@ -264,9 +264,9 @@ impl ModelRouter {
} }
} }
/// Check if model is from Bifrost /// Check if the model came from provider discovery
pub fn is_bifrost_model(&self, name: &str) -> bool { pub fn is_discovered_model(&self, name: &str) -> bool {
self.bifrost_models.contains(&name.to_string()) self.discovered_models.contains(&name.to_string())
} }
/// Get model info for display /// Get model info for display
@ -277,7 +277,7 @@ impl ModelRouter {
context_limit: cfg.context_limit, context_limit: cfg.context_limit,
output_limit: cfg.output_limit, output_limit: cfg.output_limit,
preferred_for: cfg.preferred_for.clone(), 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 context_limit: usize,
pub output_limit: usize, pub output_limit: usize,
pub preferred_for: Vec<TaskType>, pub preferred_for: Vec<TaskType>,
pub from_bifrost: bool, pub from_discovery: bool,
} }
#[cfg(test)] #[cfg(test)]

View file

@ -7,16 +7,17 @@ use tracing::{debug, info, warn};
use crate::bridge::provider::LlmProvider; use crate::bridge::provider::LlmProvider;
/// Bifrost Inference Client /// 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 is an OpenAI-compatible API gateway: http://127.0.0.1:3360/v1 /// Bifrost gateway among them. It is not specific to Bifrost, and its logs
/// must not imply a request traversed that service.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct BifrostClient { pub struct OpenAiCompatibleClient {
/// Base URL including its version path (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"). /// or "https://api.z.ai/api/coding/paas/v4").
base_url: String, base_url: String,
/// Provider identity label (logs/catalog). Defaults to "bifrost"; a /// Configured `[providers.<id>]` name — what logs and errors call this
/// differently-branded OpenAI-compatible endpoint (e.g. z.ai) sets its own. /// endpoint, so a failure names the provider the operator configured.
id: String, id: String,
/// Bearer token for auth /// Bearer token for auth
api_key: String, api_key: String,
@ -339,7 +340,7 @@ pub struct Usage {
pub cache_write_tokens: u32, pub cache_write_tokens: u32,
} }
/// Stream chunk from Bifrost (OpenAI SSE format) /// Stream chunk in OpenAI SSE format
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
pub struct StreamChunk { pub struct StreamChunk {
pub choices: Vec<StreamChoice>, pub choices: Vec<StreamChoice>,
@ -441,8 +442,9 @@ pub enum InferenceStrain {
}, },
} }
/// A Bifrost upstream timeout (`504` with `request_timed_out` / `"type":"timeout"`) /// An upstream timeout (`504` with `request_timed_out` / `"type":"timeout"`, as
/// is *deterministic* — the same slow model on the same request will time out /// 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 /// 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 /// multi-minute stall that was never going to succeed. Cap those at a single
/// retry (2 attempts total). Genuine transient blips — `503` overloaded, /// 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 /// `claude_subscription.rs` learned this first and answers it by rotating to
/// another login, so there an *absent* `retry-after` also counts as exhausted. /// 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. /// jittered backoff — only an explicitly long one fails fast.
const BURST_RETRY_CEILING_SECS: u64 = 60; 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() != "/") reqwest::Url::parse(url).is_ok_and(|u| u.path() != "/")
} }
impl BifrostClient { impl OpenAiCompatibleClient {
pub fn new( pub fn new(
id: &str,
base_url: &str, base_url: &str,
api_key: &str, api_key: &str,
virtual_key: &str, virtual_key: &str,
@ -555,31 +558,23 @@ impl BifrostClient {
}; };
info!( info!(
"🌉 Bifrost client initialized — model: {}, endpoint: {}, timeout: {}s", "provider {} ready — model: {}, endpoint: {}, timeout: {}s",
default_model, base_url, timeout_secs id, default_model, base_url, timeout_secs
); );
Ok(Self { Ok(Self {
base_url, base_url,
id: "bifrost".to_string(), id: id.to_string(),
api_key: api_key.to_string(), api_key: api_key.to_string(),
virtual_key: virtual_key.to_string(), virtual_key: virtual_key.to_string(),
client: reqwest::Client::builder() client: reqwest::Client::builder()
.timeout(Duration::from_secs(timeout_secs)) .timeout(Duration::from_secs(timeout_secs))
.build() .build()
.context("building bifrost reqwest client")?, .with_context(|| format!("building reqwest client for provider {id}"))?,
default_model: default_model.to_string(), default_model: default_model.to_string(),
retry_policy: RetryPolicy::default(), 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<String>) -> Self {
self.id = id.into();
self
}
pub fn with_fallbacks(mut self, fallbacks: Vec<String>) -> Self { pub fn with_fallbacks(mut self, fallbacks: Vec<String>) -> Self {
self.retry_policy.fallback_models = fallbacks; self.retry_policy.fallback_models = fallbacks;
self self
@ -603,7 +598,7 @@ impl BifrostClient {
headers headers
} }
/// List available models from Bifrost /// List the models this provider advertises at `/models`.
pub async fn list_models(&self) -> Result<Vec<String>> { pub async fn list_models(&self) -> Result<Vec<String>> {
let url = format!("{}/models", self.base_url); let url = format!("{}/models", self.base_url);
let resp = self let resp = self
@ -612,7 +607,7 @@ impl BifrostClient {
.headers(self.auth_headers()) .headers(self.auth_headers())
.send() .send()
.await .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 body: serde_json::Value = resp.json().await?;
let models = body["data"] let models = body["data"]
@ -719,12 +714,18 @@ impl BifrostClient {
Ok(r) => r, Ok(r) => r,
Err(e) if e.is_timeout() || e.is_connect() => { Err(e) if e.is_timeout() || e.is_connect() => {
if attempt == policy.max_retries { 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); let delay = jittered_delay(attempt, policy);
warn!( warn!(
"Bifrost connection failed (attempt {}), retrying in {:?}: {}", "provider {} connection failed at {} (attempt {}), retrying in {:?}: {}",
attempt, delay, e self.id, url, attempt, delay, e
); );
strain_events.push(InferenceStrain::Transient { strain_events.push(InferenceStrain::Transient {
attempt, attempt,
@ -745,14 +746,14 @@ impl BifrostClient {
let body_text = resp let body_text = resp
.text() .text()
.await .await
.context("Failed to read Bifrost response body")?; .with_context(|| format!("provider {} — unreadable response body", self.id))?;
return Self::parse_completion_response(&body_text); return self.parse_completion_response(&body_text);
} }
let body_text = resp let body_text = resp
.text() .text()
.await .await
.context("Failed to read Bifrost error body")?; .with_context(|| format!("provider {} — unreadable error body", self.id))?;
match classify_status(status, &body_text) { match classify_status(status, &body_text) {
ErrorClass::Transient ErrorClass::Transient
@ -761,7 +762,8 @@ impl BifrostClient {
{ {
let delay = retry_after.unwrap_or_else(|| jittered_delay(attempt, policy)); let delay = retry_after.unwrap_or_else(|| jittered_delay(attempt, policy));
warn!( warn!(
"Bifrost {} on {} (attempt {}), retrying in {:?}", "provider {} returned {} on {} (attempt {}), retrying in {:?}",
self.id,
status.as_u16(), status.as_u16(),
model, model,
attempt, attempt,
@ -783,7 +785,8 @@ impl BifrostClient {
body: body_text[..body_text.len().min(300)].to_string(), body: body_text[..body_text.len().min(300)].to_string(),
}); });
anyhow::bail!( anyhow::bail!(
"Bifrost returned {} after {} attempt(s) on {}{}: {}", "provider {} returned {} after {} attempt(s) on {}{}: {}",
self.id,
status, status,
attempt + 1, attempt + 1,
model, 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<CompletionResult> { fn parse_completion_response(&self, body_text: &str) -> Result<CompletionResult> {
let parsed: ChatCompletionResponse = let parsed: ChatCompletionResponse =
serde_json::from_str(body_text).with_context(|| { serde_json::from_str(body_text).with_context(|| {
let preview = &body_text[..body_text.len().min(200)]; 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 let choice = parsed
.choices .choices
.into_iter() .into_iter()
.next() .next()
.context("Bifrost returned empty choices")?; .with_context(|| format!("provider {} returned empty choices", self.id))?;
let content = choice.message.content.unwrap_or_default(); let content = choice.message.content.unwrap_or_default();
let reasoning = choice.message.reasoning; 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 /// provider seam. The inherent methods do the work; the trait just exposes them
/// behind `dyn LlmProvider` so the engine can hold any provider uniformly. /// behind `dyn LlmProvider` so the engine can hold any provider uniformly.
#[async_trait] #[async_trait]
impl LlmProvider for BifrostClient { impl LlmProvider for OpenAiCompatibleClient {
fn id(&self) -> &str { fn id(&self) -> &str {
&self.id &self.id
} }
@ -853,18 +859,18 @@ impl LlmProvider for BifrostClient {
} }
async fn list_models(&self) -> Result<Vec<String>> { async fn list_models(&self) -> Result<Vec<String>> {
BifrostClient::list_models(self).await OpenAiCompatibleClient::list_models(self).await
} }
async fn chat_completion_with_strain( async fn chat_completion_with_strain(
&self, &self,
request: ChatCompletionRequest, request: ChatCompletionRequest,
) -> Result<(CompletionResult, Vec<InferenceStrain>)> { ) -> Result<(CompletionResult, Vec<InferenceStrain>)> {
BifrostClient::chat_completion_with_strain(self, request).await OpenAiCompatibleClient::chat_completion_with_strain(self, request).await
} }
async fn chat_completion(&self, request: ChatCompletionRequest) -> Result<CompletionResult> { async fn chat_completion(&self, request: ChatCompletionRequest) -> Result<CompletionResult> {
BifrostClient::chat_completion(self, request).await OpenAiCompatibleClient::chat_completion(self, request).await
} }
} }
@ -897,7 +903,7 @@ mod tests {
#[test] #[test]
fn exhausted_window_ignores_absent_headers_and_other_statuses() { 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 // the ordinary jittered backoff — this is where it diverges from
// `claude_subscription::is_exhausted_window` on purpose. // `claude_subscription::is_exhausted_window` on purpose.
assert!(!is_exhausted_window( assert!(!is_exhausted_window(
@ -923,7 +929,8 @@ mod tests {
/// Appending `/v1` there produced a path that hangs on POST. /// Appending `/v1` there produced a path that hangs on POST.
#[test] #[test]
fn google_nested_version_path_is_not_doubled() { fn google_nested_version_path_is_not_doubled() {
let client = BifrostClient::new( let client = OpenAiCompatibleClient::new(
"gemini",
"https://generativelanguage.googleapis.com/v1beta/openai", "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"), ("http://10.10.20.19:8080", "http://10.10.20.19:8080/v1"),
]; ];
for (base, want) in cases { 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}"); assert_eq!(client.base_url, want, "base {base}");
} }
} }
#[test] #[test]
fn test_client_creation() { fn test_client_creation() {
let client = BifrostClient::new( let client = OpenAiCompatibleClient::new(
"bifrost",
"http://127.0.0.1:3360", "http://127.0.0.1:3360",
"sk-bf-test", "sk-bf-test",
"", "",
@ -983,6 +991,7 @@ mod tests {
) )
.unwrap(); .unwrap();
assert!(client.base_url.ends_with("/v1")); assert!(client.base_url.ends_with("/v1"));
assert_eq!(client.id, "bifrost");
} }
#[test] #[test]

View file

@ -14,7 +14,7 @@
use anyhow::Result; use anyhow::Result;
use async_trait::async_trait; use async_trait::async_trait;
use super::bifrost::{ChatCompletionRequest, CompletionResult, InferenceStrain}; use super::openai_compatible::{ChatCompletionRequest, CompletionResult, InferenceStrain};
#[async_trait] #[async_trait]
pub trait LlmProvider: Send + Sync { pub trait LlmProvider: Send + Sync {

View file

@ -1,7 +1,7 @@
//! Concrete [`LlmProvider`](crate::bridge::provider::LlmProvider) implementations. //! Concrete [`LlmProvider`](crate::bridge::provider::LlmProvider) implementations.
//! //!
//! `BifrostClient` (the OpenAI-compatible gateway client) implements the trait //! `OpenAiCompatibleClient` (the OpenAI-compatible gateway client) implements the trait
//! in `bridge::bifrost`; the OAuth-riding ChatGPT provider lives here. //! in `bridge::openai_compatible`; the OAuth-riding ChatGPT provider lives here.
pub mod openai_oauth; pub mod openai_oauth;
pub mod responses; pub mod responses;

View file

@ -15,7 +15,7 @@ use async_trait::async_trait;
use tokio::sync::Mutex; use tokio::sync::Mutex;
use tracing::warn; use tracing::warn;
use crate::bridge::bifrost::{ use crate::bridge::openai_compatible::{
ChatCompletionRequest, CompletionResult, InferenceStrain, RetryPolicy, ChatCompletionRequest, CompletionResult, InferenceStrain, RetryPolicy,
}; };
use crate::bridge::oauth::codex_creds::{self, CodexCredentials}; use crate::bridge::oauth::codex_creds::{self, CodexCredentials};
@ -44,7 +44,7 @@ impl OpenAiOAuthProvider {
.timeout(Duration::from_secs(timeout_secs)) .timeout(Duration::from_secs(timeout_secs))
.build() .build()
.context("building OpenAI OAuth reqwest client")?; .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. // (`openai/…`); pin the provider default to a model this backend serves.
let default_model = catalog::resolve(&default_model, catalog::DEFAULT_MODEL); let default_model = catalog::resolve(&default_model, catalog::DEFAULT_MODEL);
Ok(Self { Ok(Self {
@ -81,7 +81,7 @@ impl LlmProvider for OpenAiOAuthProvider {
&self, &self,
request: ChatCompletionRequest, request: ChatCompletionRequest,
) -> Result<(CompletionResult, Vec<InferenceStrain>)> { ) -> Result<(CompletionResult, Vec<InferenceStrain>)> {
// 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. // model the codex backend actually serves before building the payload.
let mut request = request; let mut request = request;
request.model = catalog::resolve(&request.model, &self.default_model); request.model = catalog::resolve(&request.model, &self.default_model);

View file

@ -9,7 +9,7 @@
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use serde_json::{json, Value}; 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). /// Models that take a `reasoning` block (GPT-5.x / o-series).
fn is_reasoning_model(model: &str) -> bool { fn is_reasoning_model(model: &str) -> bool {

View file

@ -36,7 +36,7 @@ use anyhow::Result;
use chrono::{NaiveDate, Utc}; use chrono::{NaiveDate, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::bridge::bifrost::{ChatCompletionRequest, Message}; use crate::bridge::openai_compatible::{ChatCompletionRequest, Message};
use crate::bridge::LlmProvider; use crate::bridge::LlmProvider;
use crate::bridge::ProviderRegistry; use crate::bridge::ProviderRegistry;
use crate::core::cadence::Cadence; use crate::core::cadence::Cadence;
@ -376,7 +376,7 @@ impl ArchivistEngine {
let (response, strain) = llm.chat_completion_with_strain(request).await?; let (response, strain) = llm.chat_completion_with_strain(request).await?;
for event in &strain { 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); tracing::info!("archivist felt inference strain: {} on {}", status, model);
if *status == 429 { if *status == 429 {

View file

@ -3,7 +3,7 @@ use std::sync::Arc;
use async_trait::async_trait; 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::model_router::TokenCounter;
use crate::bridge::LlmProvider; use crate::bridge::LlmProvider;
use crate::core::session::ConversationMessage; use crate::core::session::ConversationMessage;
@ -51,8 +51,8 @@ pub trait CompactionStrategy: Send + Sync {
) -> anyhow::Result<CompactionPlan>; ) -> anyhow::Result<CompactionPlan>;
} }
/// Helper: call a Bifrost model with system+user prompt, get text response. /// Helper: call a provider model with system+user prompt, get text response.
async fn bifrost_complete( async fn provider_complete(
client: &Arc<dyn LlmProvider>, client: &Arc<dyn LlmProvider>,
model: &str, model: &str,
system: &str, system: &str,
@ -151,7 +151,7 @@ impl CompactionStrategy for SummaryStrategy {
), ),
}; };
let summary = bifrost_complete( let summary = provider_complete(
&self.client, &self.client,
&self.model, &self.model,
SUMMARY_SYSTEM_PROMPT, SUMMARY_SYSTEM_PROMPT,
@ -533,7 +533,7 @@ impl CompactionStrategy for SlidingWindowStrategy {
/// even after the originals are gone. /// even after the originals are gone.
/// ///
/// Uses whichever model the engine provides (subconscious model if subconscious is /// 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 /// available, falls back to plain SlidingWindow (no threads lost is better
/// than no compaction at all). /// than no compaction at all).
pub struct SlidingReflectStrategy { pub struct SlidingReflectStrategy {
@ -620,7 +620,7 @@ impl CompactionStrategy for SlidingReflectStrategy {
), ),
}; };
let reflection = bifrost_complete( let reflection = provider_complete(
&self.client, &self.client,
&self.model, &self.model,
&system_prompt, &system_prompt,

View file

@ -31,7 +31,7 @@ use anyhow::Result;
use chrono::Utc; use chrono::Utc;
use serde::{Deserialize, Serialize}; 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::LlmProvider;
use crate::bridge::ProviderRegistry; use crate::bridge::ProviderRegistry;
use crate::core::cadence::Cadence; use crate::core::cadence::Cadence;
@ -326,7 +326,7 @@ impl ReflectionEngine {
let (response, strain) = llm.chat_completion_with_strain(request).await?; let (response, strain) = llm.chat_completion_with_strain(request).await?;
for event in &strain { for event in &strain {
if let crate::bridge::bifrost::InferenceStrain::Transient { if let crate::bridge::openai_compatible::InferenceStrain::Transient {
status, model, .. status, model, ..
} = event } = event
{ {
@ -354,11 +354,11 @@ impl ReflectionEngine {
}); });
} }
let calls: Vec<crate::bridge::bifrost::MessageToolCall> = response let calls: Vec<crate::bridge::openai_compatible::MessageToolCall> = response
.tool_calls .tool_calls
.iter() .iter()
.map(|tc| { .map(|tc| {
crate::bridge::bifrost::MessageToolCall::function( crate::bridge::openai_compatible::MessageToolCall::function(
tc.id.clone(), tc.id.clone(),
tc.name.clone(), tc.name.clone(),
tc.arguments.to_string(), tc.arguments.to_string(),

View file

@ -319,12 +319,12 @@ impl Session {
Ok(Some(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 /// Delegates to [`replay_messages`] — the one projection of stored history
/// onto the wire. This wrapper degrades images, because the callers that /// onto the wire. This wrapper degrades images, because the callers that
/// can see natively build their own policy from the agent's config. /// can see natively build their own policy from the agent's config.
pub fn to_bifrost_messages(&self) -> Vec<crate::bridge::bifrost::Message> { pub fn to_wire_messages(&self) -> Vec<crate::bridge::openai_compatible::Message> {
replay_messages(&self.messages, ImagePolicy::Degrade) replay_messages(&self.messages, ImagePolicy::Degrade)
} }
} }
@ -388,8 +388,8 @@ const UNFINISHED_CALL: &str =
pub fn replay_messages( pub fn replay_messages(
messages: &[ConversationMessage], messages: &[ConversationMessage],
images: ImagePolicy, images: ImagePolicy,
) -> Vec<crate::bridge::bifrost::Message> { ) -> Vec<crate::bridge::openai_compatible::Message> {
use crate::bridge::bifrost::{ContentPart, ImageUrlSource, Message, MessageToolCall}; use crate::bridge::openai_compatible::{ContentPart, ImageUrlSource, Message, MessageToolCall};
use std::collections::HashSet; use std::collections::HashSet;
let ids = |f: fn(&ContentBlock) -> Option<&str>| -> HashSet<&str> { let ids = |f: fn(&ContentBlock) -> Option<&str>| -> HashSet<&str> {
@ -721,10 +721,10 @@ mod tests {
} }
#[test] #[test]
fn test_bifrost_conversion() { fn test_wire_conversion() {
let mut session = Session::new("ani"); let mut session = Session::new("ani");
session.add_message(ConversationMessage::user_text("hello")); 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.len(), 1);
assert_eq!(msgs[0].role, "user"); assert_eq!(msgs[0].role, "user");
assert_eq!(msgs[0].content.as_text(), "hello"); assert_eq!(msgs[0].content.as_text(), "hello");
@ -763,7 +763,7 @@ mod tests {
timestamp: None, 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 // Every call is declared, and every tool message answers a declared
// call. A half-pair in either direction is rejected by the wire. // call. A half-pair in either direction is rejected by the wire.

View file

@ -59,7 +59,7 @@ pub const SUBCONSCIOUS_ONLY_TOOLS: &[&str] = &["halt", "intrusive"];
// ── Re-export for backward compat ─────────────────────────────── // ── 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)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolDefinition { pub struct ToolDefinition {
pub name: String, pub name: String,

View file

@ -4,7 +4,7 @@
//! //!
//! ## N+1 (subconscious) //! ## N+1 (subconscious)
//! The subconscious pass runs immediately after every response. It takes the //! 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" //! model (defaulting to `glm-5.1`, configurable) with a "subconscious mode"
//! system prompt. subconscious has full tool access — Read, Write, Edit, Glob, Grep, //! system prompt. subconscious has full tool access — Read, Write, Edit, Glob, Grep,
//! ListDir, and Memory — so she can read ledgers, check commitments, and write //! ListDir, and Memory — so she can read ledgers, check commitments, and write
@ -27,7 +27,7 @@
//! immediately after the primary's turn. //! immediately after the primary's turn.
use crate::backend::BackendEvent; 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::model_router::TokenCounter;
use crate::bridge::LlmProvider; use crate::bridge::LlmProvider;
use crate::bridge::ProviderRegistry; use crate::bridge::ProviderRegistry;
@ -307,7 +307,7 @@ impl ConsciousnessEngine {
} }
if let Err(e) = self if let Err(e) = self
.sessions .sessions
.add_message(conv_id, bifrost_to_conversation(m)) .add_message(conv_id, wire_to_conversation(m))
{ {
tracing::warn!("subconscious session persist failed: {}", e); tracing::warn!("subconscious session persist failed: {}", e);
} }
@ -881,7 +881,7 @@ Resolve entries: `[YYYY-MM-DD HH:MM] RESOLVED — note`"#;
Vec::new() 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<Message> = Vec::new(); let mut messages: Vec<Message> = Vec::new();
messages.push(Message::text("system", system_prompt.to_string())); 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; persisted.messages = prior_messages;
messages.extend( messages.extend(
persisted persisted
.to_bifrost_messages() .to_wire_messages()
.into_iter() .into_iter()
.filter(|m| m.role != "system"), .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 // 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 // returns the full response in one shot; we slice it into small
// pieces and emit them with a small inter-chunk delay so the // pieces and emit them with a small inter-chunk delay so the
// subconscious appears to be typing in real time. // 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 { for event in &strain {
if let crate::bridge::bifrost::InferenceStrain::Transient { if let crate::bridge::openai_compatible::InferenceStrain::Transient {
attempt, attempt,
status, status,
model, model,
@ -1045,11 +1045,11 @@ Resolve entries: `[YYYY-MM-DD HH:MM] RESOLVED — note`"#;
} }
// Add assistant message with tool calls (OpenAI tool-use schema) // Add assistant message with tool calls (OpenAI tool-use schema)
let calls: Vec<crate::bridge::bifrost::MessageToolCall> = response let calls: Vec<crate::bridge::openai_compatible::MessageToolCall> = response
.tool_calls .tool_calls
.iter() .iter()
.map(|tc| { .map(|tc| {
crate::bridge::bifrost::MessageToolCall::function( crate::bridge::openai_compatible::MessageToolCall::function(
tc.id.clone(), tc.id.clone(),
tc.name.clone(), tc.name.clone(),
tc.arguments.to_string(), 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 /// 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. /// persistent session.
fn bifrost_to_conversation(msg: &Message) -> ConversationMessage { fn wire_to_conversation(msg: &Message) -> ConversationMessage {
let role = match msg.role.as_str() { let role = match msg.role.as_str() {
"system" => MessageRole::System, "system" => MessageRole::System,
"user" => MessageRole::User, "user" => MessageRole::User,

View file

@ -2,7 +2,7 @@ use async_trait::async_trait;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; 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::core::tools::defs::{SubagentParams, SubagentRunner, ToolContext};
use crate::server::SouveraineServer; use crate::server::SouveraineServer;
@ -98,11 +98,11 @@ impl SubagentRunner for ServerSubagentRunner {
// Build tool definitions // Build tool definitions
let core_tools = crate::core::tools::tool_definitions().await; let core_tools = crate::core::tools::tool_definitions().await;
let bifrost_tools: Vec<crate::bridge::bifrost::ToolDefinition> = core_tools let wire_tools: Vec<crate::bridge::openai_compatible::ToolDefinition> = core_tools
.iter() .iter()
.map(|t| crate::bridge::bifrost::ToolDefinition { .map(|t| crate::bridge::openai_compatible::ToolDefinition {
tool_type: "function".to_string(), tool_type: "function".to_string(),
function: crate::bridge::bifrost::ToolFunction { function: crate::bridge::openai_compatible::ToolFunction {
name: t.name.clone(), name: t.name.clone(),
description: t.description.clone(), description: t.description.clone(),
parameters: t.input_schema.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 // messages into a separate field are left with an empty messages
// array otherwise, and reject the request. // array otherwise, and reject the request.
let mut messages = vec![ let mut messages = vec![
BifrostMessage::text("system", system_prompt), WireMessage::text("system", system_prompt),
BifrostMessage::text("user", params.prompt.clone()), WireMessage::text("user", params.prompt.clone()),
]; ];
let mut final_content = String::new(); 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; let progress = tool_round as f32 / max_tool_rounds as f32;
if !warned_1 && progress >= warning_1_threshold { if !warned_1 && progress >= warning_1_threshold {
warned_1 = true; warned_1 = true;
messages.push(BifrostMessage::text( messages.push(WireMessage::text(
"system", "system",
format!( format!(
"[subagent awareness] I've used {} of {} tool rounds. \ "[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 // Warning 2: nearing the limit, this is the last stretch
if !warned_2 && progress >= warning_2_threshold { if !warned_2 && progress >= warning_2_threshold {
warned_2 = true; warned_2 = true;
messages.push(BifrostMessage::text( messages.push(WireMessage::text(
"system", "system",
format!( format!(
"[subagent awareness] I'm at {} of {} tool rounds. \ "[subagent awareness] I'm at {} of {} tool rounds. \
@ -181,7 +181,7 @@ impl SubagentRunner for ServerSubagentRunner {
.count(); .count();
request_messages.insert( request_messages.insert(
system_prefix, system_prefix,
BifrostMessage::text("system", principal.model_system_block()), WireMessage::text("system", principal.model_system_block()),
); );
let req = ChatCompletionRequest { let req = ChatCompletionRequest {
@ -190,7 +190,7 @@ impl SubagentRunner for ServerSubagentRunner {
stream: Some(false), stream: Some(false),
max_tokens: None, max_tokens: None,
temperature, temperature,
tools: Some(bifrost_tools.clone()), tools: Some(wire_tools.clone()),
}; };
let response = llm.chat_completion(req).await.map_err(|e| { let response = llm.chat_completion(req).await.map_err(|e| {
@ -207,11 +207,11 @@ impl SubagentRunner for ServerSubagentRunner {
tool_round += 1; tool_round += 1;
// Add assistant tool-call message (OpenAI tool-use schema, not stringified blob) // Add assistant tool-call message (OpenAI tool-use schema, not stringified blob)
let calls: Vec<crate::bridge::bifrost::MessageToolCall> = response let calls: Vec<crate::bridge::openai_compatible::MessageToolCall> = response
.tool_calls .tool_calls
.iter() .iter()
.map(|tc| { .map(|tc| {
crate::bridge::bifrost::MessageToolCall::function( crate::bridge::openai_compatible::MessageToolCall::function(
tc.id.clone(), tc.id.clone(),
tc.name.clone(), tc.name.clone(),
tc.arguments.to_string(), tc.arguments.to_string(),
@ -219,7 +219,7 @@ impl SubagentRunner for ServerSubagentRunner {
}) })
.collect(); .collect();
messages.push( messages.push(
BifrostMessage::assistant_tool_calls(response.content.clone(), calls) WireMessage::assistant_tool_calls(response.content.clone(), calls)
.with_thinking( .with_thinking(
response.reasoning.clone(), response.reasoning.clone(),
response.reasoning_signature.clone(), response.reasoning_signature.clone(),
@ -239,7 +239,7 @@ impl SubagentRunner for ServerSubagentRunner {
result.output 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 // Brief pause between tool rounds to let rate limits cool

View file

@ -4,8 +4,8 @@ use std::time::{Duration, Instant};
use tokio::sync::mpsc; use tokio::sync::mpsc;
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
use crate::bridge::bifrost::{ use crate::bridge::openai_compatible::{
ChatCompletionRequest, CompletionResult, ContentPart, ImageUrlSource, Message as BifrostMessage, ChatCompletionRequest, CompletionResult, ContentPart, ImageUrlSource, Message as WireMessage,
}; };
use crate::bridge::model_router::TokenCounter; use crate::bridge::model_router::TokenCounter;
use crate::core::compact::CompactionEngine; use crate::core::compact::CompactionEngine;
@ -20,13 +20,13 @@ use crate::server::energy::write_energy_balance;
use crate::server::subagent::ServerSubagentRunner; use crate::server::subagent::ServerSubagentRunner;
/// Mirror of `ConsciousnessEngine::calculate_pressure` for the in-loop /// 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 /// accumulate inside a single turn. `context_limit` comes from the
/// agent's `llm_config.context_window` (Constitution V.3 — per-model /// agent's `llm_config.context_window` (Constitution V.3 — per-model
/// physics, no hardcoded 128K). /// physics, no hardcoded 128K).
fn bifrost_pressure( fn wire_pressure(
counter: &TokenCounter, counter: &TokenCounter,
messages: &[BifrostMessage], messages: &[WireMessage],
context_limit: usize, context_limit: usize,
) -> (usize, f32) { ) -> (usize, f32) {
let tokens: usize = messages let tokens: usize = messages
@ -113,7 +113,7 @@ pub(crate) async fn run_turn(
let agent = server.agents.get(&agent_id).await?; let agent = server.agents.get(&agent_id).await?;
let supports_images = agent.llm_config.supports_images; 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. // any await — `Ref` is not Send across awaits.
// //
// One projection, shared with every other replay of stored history // One projection, shared with every other replay of stored history
@ -183,17 +183,17 @@ pub(crate) async fn run_turn(
..tool_ctx ..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 — // subconscious-only tools (halt, intrusive) are filtered OUT here —
// the primary must never see them in her tool list. Subconscious's // the primary must never see them in her tool list. Subconscious's
// own loop whitelists them in via SUBCONSCIOUS_SAFE_TOOLS. // own loop whitelists them in via SUBCONSCIOUS_SAFE_TOOLS.
let core_tools = crate::core::tools::tool_definitions().await; let core_tools = crate::core::tools::tool_definitions().await;
let bifrost_tools: Vec<crate::bridge::bifrost::ToolDefinition> = core_tools let wire_tools: Vec<crate::bridge::openai_compatible::ToolDefinition> = core_tools
.iter() .iter()
.filter(|t| !crate::core::tools::SUBCONSCIOUS_ONLY_TOOLS.contains(&t.name.as_str())) .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(), tool_type: "function".to_string(),
function: crate::bridge::bifrost::ToolFunction { function: crate::bridge::openai_compatible::ToolFunction {
name: t.name.clone(), name: t.name.clone(),
description: t.description.clone(), description: t.description.clone(),
parameters: t.input_schema.clone(), parameters: t.input_schema.clone(),
@ -245,7 +245,7 @@ pub(crate) async fn run_turn(
loop { loop {
// Cancellation is a signal, not enforcement — we check it on round // 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. // partial work is preserved. No hard-kill mid-tool.
if cancel.is_cancelled() { if cancel.is_cancelled() {
interrupted = true; interrupted = true;
@ -272,7 +272,7 @@ pub(crate) async fn run_turn(
for text in drained { for text in drained {
let stamp = chrono::Local::now().format("%H:%M"); let stamp = chrono::Local::now().format("%H:%M");
let note = format!("[user interjected at {}{}]", stamp, text.trim()); 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 // 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. // call so it lands in her context naturally.
if pulse_enabled && last_pulse.elapsed() >= pulse_interval { if pulse_enabled && last_pulse.elapsed() >= pulse_interval {
let elapsed_total = turn_start.elapsed(); 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(); 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!( tracing::info!(
turn_round = tool_round, turn_round = tool_round,
agent = %agent_id, agent = %agent_id,
@ -317,7 +317,7 @@ pub(crate) async fn run_turn(
.count(); .count();
request_messages.insert( request_messages.insert(
system_prefix, system_prefix,
BifrostMessage::text("system", principal.model_system_block()), WireMessage::text("system", principal.model_system_block()),
); );
let req = ChatCompletionRequest { let req = ChatCompletionRequest {
@ -327,7 +327,7 @@ pub(crate) async fn run_turn(
max_tokens, max_tokens,
temperature, temperature,
tools: if max_rounds > 0 { tools: if max_rounds > 0 {
Some(bifrost_tools.clone()) Some(wire_tools.clone())
} else { } else {
None None
}, },
@ -381,7 +381,7 @@ pub(crate) async fn run_turn(
} }
for event in &strain { for event in &strain {
if let crate::bridge::bifrost::InferenceStrain::Transient { if let crate::bridge::openai_compatible::InferenceStrain::Transient {
attempt, attempt,
status, status,
model, model,
@ -446,8 +446,8 @@ pub(crate) async fn run_turn(
let _ = tx.send(Ok(BackendEvent::Token(s.clone()))).await; let _ = tx.send(Ok(BackendEvent::Token(s.clone()))).await;
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await; 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()));
messages.push(BifrostMessage::text( messages.push(WireMessage::text(
"system", "system",
"My output just hit its ceiling — I was cut off mid-flow, not \ "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 \ 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); dispatcher.emit_segment(&s);
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await; 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()); carried_replies.push(response.content.clone());
} }
for text in &interjected { for text in &interjected {
let stamp = chrono::Local::now().format("%H:%M"); let stamp = chrono::Local::now().format("%H:%M");
let note = format!("[interjected at {}{}]", stamp, text.trim()); 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 // Continue the loop — agent sees the interjection as a
// user message and will respond in the next LLM round. // 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 // 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). // (not a stringified JSON blob in content — that's what broke turn 2).
let calls: Vec<crate::bridge::bifrost::MessageToolCall> = response let calls: Vec<crate::bridge::openai_compatible::MessageToolCall> = response
.tool_calls .tool_calls
.iter() .iter()
.map(|tc| { .map(|tc| {
crate::bridge::bifrost::MessageToolCall::function( crate::bridge::openai_compatible::MessageToolCall::function(
tc.id.clone(), tc.id.clone(),
tc.name.clone(), tc.name.clone(),
tc.arguments.to_string(), tc.arguments.to_string(),
@ -570,7 +570,7 @@ pub(crate) async fn run_turn(
}) })
.collect(); .collect();
messages.push( 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.clone(),
response.reasoning_signature.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). // 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. // Now that every tool result is bound, the images can follow.
@ -757,7 +757,7 @@ pub(crate) async fn run_turn(
} else { } else {
format!("The {} images I just read:", round_images.len()) 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 ──────────────────────────────────────────── // ── Mid-turn peek ────────────────────────────────────────────
@ -854,7 +854,7 @@ pub(crate) async fn run_turn(
// commentary from outside. This is what she // commentary from outside. This is what she
// experiences as "the migraine." // experiences as "the migraine."
let felt = migraine_text(&halt.severity, &halt.reason); let felt = migraine_text(&halt.severity, &halt.reason);
messages.push(BifrostMessage::text("system", &felt)); messages.push(WireMessage::text("system", &felt));
if halt.severity == "advisory" { if halt.severity == "advisory" {
// Said slow down, not stop. She carries the // Said slow down, not stop. She carries the

View file

@ -28,7 +28,7 @@ pub trait TuiState {
// ========== Connection ========== // ========== Connection ==========
fn is_connected(&self) -> bool; 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>; fn connection_error(&self) -> Option<&str>;
// ========== Screen State ========== // ========== Screen State ==========