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:
parent
f32f00c6e3
commit
76e12045a4
19 changed files with 169 additions and 160 deletions
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
//!
|
//!
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)]
|
||||||
|
|
|
||||||
|
|
@ -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]
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 ==========
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue