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
/// 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.

View file

@ -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.
//!

View file

@ -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,
};

View file

@ -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))
}
}

View file

@ -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<String, ModelConfig>,
current_usage: Arc<RwLock<TokenUsage>>,
token_counter: TokenCounter,
bifrost_client: Option<BifrostClient>,
bifrost_models: Vec<String>,
provider_client: Option<OpenAiCompatibleClient>,
discovered_models: Vec<String>,
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<String, ModelConfig>,
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<Vec<String>> {
if let Some(client) = &self.bifrost_client {
/// Fetch the provider's advertised model list
pub async fn fetch_provider_models(&mut self) -> anyhow::Result<Vec<String>> {
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<String> {
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<TaskType>,
pub from_bifrost: bool,
pub from_discovery: bool,
}
#[cfg(test)]

View file

@ -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.<id>]` 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<StreamChoice>,
@ -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<String>) -> Self {
self.id = id.into();
self
}
pub fn with_fallbacks(mut self, fallbacks: Vec<String>) -> 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<Vec<String>> {
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<CompletionResult> {
fn parse_completion_response(&self, body_text: &str) -> Result<CompletionResult> {
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<Vec<String>> {
BifrostClient::list_models(self).await
OpenAiCompatibleClient::list_models(self).await
}
async fn chat_completion_with_strain(
&self,
request: ChatCompletionRequest,
) -> 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> {
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]

View file

@ -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 {

View file

@ -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;

View file

@ -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<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.
let mut request = request;
request.model = catalog::resolve(&request.model, &self.default_model);

View file

@ -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 {

View file

@ -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 {

View file

@ -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<CompactionPlan>;
}
/// 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<dyn LlmProvider>,
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,

View file

@ -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<crate::bridge::bifrost::MessageToolCall> = response
let calls: Vec<crate::bridge::openai_compatible::MessageToolCall> = 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(),

View file

@ -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<crate::bridge::bifrost::Message> {
pub fn to_wire_messages(&self) -> Vec<crate::bridge::openai_compatible::Message> {
replay_messages(&self.messages, ImagePolicy::Degrade)
}
}
@ -388,8 +388,8 @@ const UNFINISHED_CALL: &str =
pub fn replay_messages(
messages: &[ConversationMessage],
images: ImagePolicy,
) -> Vec<crate::bridge::bifrost::Message> {
use crate::bridge::bifrost::{ContentPart, ImageUrlSource, Message, MessageToolCall};
) -> Vec<crate::bridge::openai_compatible::Message> {
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.

View file

@ -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,

View file

@ -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<Message> = 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<crate::bridge::bifrost::MessageToolCall> = response
let calls: Vec<crate::bridge::openai_compatible::MessageToolCall> = 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,

View file

@ -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<crate::bridge::bifrost::ToolDefinition> = core_tools
let wire_tools: Vec<crate::bridge::openai_compatible::ToolDefinition> = 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<crate::bridge::bifrost::MessageToolCall> = response
let calls: Vec<crate::bridge::openai_compatible::MessageToolCall> = 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

View file

@ -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<crate::bridge::bifrost::ToolDefinition> = core_tools
let wire_tools: Vec<crate::bridge::openai_compatible::ToolDefinition> = 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<crate::bridge::bifrost::MessageToolCall> = response
let calls: Vec<crate::bridge::openai_compatible::MessageToolCall> = 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

View file

@ -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 ==========