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
|
||||
/// 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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
//!
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 ==========
|
||||
|
|
|
|||
Loading…
Reference in a new issue