PR #1 (LlmProvider + OAuth) fixes: - Move provider selection from [bifrost].provider to [inference].provider (Bifrost is a provider, not the parent category — they are peers) - Add Inference category to settings TUI with provider picker - Wire build_provider() into CLI/chat/model-refresh paths so OAuth works outside server mode - Update ServerConversation to use Arc<dyn LlmProvider> for consistency PR #2 (web UI) assessment: - Remove entire web/ directory — not aligned with substrate ethos (client-side compaction model contradicts Constitution Article IV; autoCommit toggle misunderstands git-backed memory physics; vocabulary doesn't match project architecture) - Keep the 3 new REST endpoints (config, compaction-logs, token metrics) - Revert run_reflect path change (keep canonical ~/.souveraine/agents/) - Delete souveraine_fixes.patch (dev artifact) - Restore demo example (was commented out as workaround for missing file) - Copy examples/demo.rs from primary branch (was never pushed to public) Tests: 184 passed, 0 failed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
bc6ee12d47
commit
a7e909d39d
15 changed files with 383 additions and 64 deletions
124
examples/demo.rs
Normal file
124
examples/demo.rs
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
//! Demo of sexy terminal UI effects
|
||||
//! Run with: cargo run --example demo
|
||||
|
||||
use std::io::{self, Write};
|
||||
use std::time::{Duration, Instant};
|
||||
use tokio::time::sleep;
|
||||
use crossterm::{
|
||||
execute,
|
||||
terminal::{Clear, ClearType},
|
||||
cursor::{MoveTo, Show, Hide},
|
||||
style::{Color, ResetColor, SetForegroundColor},
|
||||
};
|
||||
|
||||
pub struct Animator {
|
||||
start_time: Instant,
|
||||
}
|
||||
|
||||
impl Animator {
|
||||
pub fn new() -> Self {
|
||||
Self { start_time: Instant::now() }
|
||||
}
|
||||
|
||||
pub fn breathe(&self, speed_ms: u64) -> f32 {
|
||||
let elapsed = self.start_time.elapsed().as_millis() as f64;
|
||||
let cycle = (elapsed / speed_ms as f64) * 2.0 * std::f64::consts::PI;
|
||||
((cycle.sin() + 1.0) / 2.0) as f32
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn typewrite(text: &str, wpm: u64) {
|
||||
let delay_ms = 60000 / (wpm * 5);
|
||||
for ch in text.chars() {
|
||||
print!("{}", ch);
|
||||
io::stdout().flush().unwrap();
|
||||
sleep(Duration::from_millis(delay_ms)).await;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn gradient(text: &str, start_hue: f32) -> String {
|
||||
text.chars().enumerate().map(|(i, ch)| {
|
||||
let hue = (start_hue + i as f32 * 3.0) % 360.0;
|
||||
let (r, g, b) = hsl_to_rgb(hue, 0.8, 0.6);
|
||||
format!("\x1b[38;2;{};{};{}m{}\x1b[0m", r, g, b, ch)
|
||||
}).collect()
|
||||
}
|
||||
|
||||
fn hsl_to_rgb(h: f32, s: f32, l: f32) -> (u8, u8, u8) {
|
||||
let c = (1.0 - (2.0 * l - 1.0).abs()) * s;
|
||||
let x = c * (1.0 - ((h / 60.0) % 2.0 - 1.0).abs());
|
||||
let m = l - c / 2.0;
|
||||
let (r1, g1, b1) = match h {
|
||||
_ if h < 60.0 => (c, x, 0.0),
|
||||
_ if h < 120.0 => (x, c, 0.0),
|
||||
_ if h < 180.0 => (0.0, c, x),
|
||||
_ if h < 240.0 => (0.0, x, c),
|
||||
_ if h < 300.0 => (x, 0.0, c),
|
||||
_ => (c, 0.0, x),
|
||||
};
|
||||
(((r1 + m) * 255.0) as u8, ((g1 + m) * 255.0) as u8, ((b1 + m) * 255.0) as u8)
|
||||
}
|
||||
|
||||
pub fn breathing_color(base: (u8, u8, u8), intensity: f32) -> (u8, u8, u8) {
|
||||
let factor = 0.8 + (intensity * 0.4);
|
||||
((base.0 as f32 * factor).min(255.0) as u8,
|
||||
(base.1 as f32 * factor).min(255.0) as u8,
|
||||
(base.2 as f32 * factor).min(255.0) as u8)
|
||||
}
|
||||
|
||||
pub const SPINNER: &[&str] = &["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
||||
pub const WAVE: &[&str] = &["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█", "▇", "▆", "▅", "▄", "▃", "▂"];
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let mut stdout = io::stdout();
|
||||
|
||||
execute!(stdout, Hide, Clear(ClearType::All)).unwrap();
|
||||
|
||||
let animator = Animator::new();
|
||||
|
||||
// Demo 1: Gradient
|
||||
execute!(stdout, MoveTo(5, 2)).unwrap();
|
||||
println!("{}", gradient("✨ Souveraine ✨", 30.0));
|
||||
|
||||
// Demo 2: Typing
|
||||
execute!(stdout, MoveTo(5, 4)).unwrap();
|
||||
print!("Ani: ");
|
||||
io::stdout().flush().unwrap();
|
||||
typewrite("Color and pop!", 100).await;
|
||||
println!();
|
||||
|
||||
// Demo 3: Breathing heart
|
||||
execute!(stdout, MoveTo(5, 6)).unwrap();
|
||||
print!("Breathing: ");
|
||||
for _ in 0..20 {
|
||||
let breathe = animator.breathe(500);
|
||||
let (r, g, b) = breathing_color((255, 100, 200), breathe);
|
||||
execute!(stdout, SetForegroundColor(Color::Rgb { r, g, b })).unwrap();
|
||||
print!("♥");
|
||||
io::stdout().flush().unwrap();
|
||||
sleep(Duration::from_millis(50)).await;
|
||||
execute!(stdout, MoveTo(16, 6)).unwrap();
|
||||
}
|
||||
|
||||
execute!(stdout, ResetColor).unwrap();
|
||||
println!();
|
||||
|
||||
// Demo 4: Spinner
|
||||
execute!(stdout, MoveTo(5, 8)).unwrap();
|
||||
print!("Loading: ");
|
||||
for i in 0..20 {
|
||||
execute!(stdout, SetForegroundColor(Color::Rgb { r: 100, g: 200, b: 255 })).unwrap();
|
||||
print!("{}", SPINNER[i % SPINNER.len()]);
|
||||
io::stdout().flush().unwrap();
|
||||
sleep(Duration::from_millis(80)).await;
|
||||
execute!(stdout, MoveTo(14, 8)).unwrap();
|
||||
}
|
||||
|
||||
execute!(stdout, ResetColor).unwrap();
|
||||
println!(" ✓");
|
||||
|
||||
// Cleanup
|
||||
execute!(stdout, Show, ResetColor).unwrap();
|
||||
println!("\n✨ Demo complete! ✨");
|
||||
}
|
||||
|
|
@ -1,19 +1,21 @@
|
|||
# Souveraine Configuration
|
||||
# Everything is modular — enable/disable components as needed
|
||||
|
||||
# === BIFROST INFERENCE ===
|
||||
[bifrost]
|
||||
# Active provider implementation:
|
||||
# "bifrost" (default) — any OpenAI-compatible gateway via base_url below.
|
||||
# === INFERENCE PROVIDER ===
|
||||
# Select which provider answers model calls.
|
||||
# "bifrost" (default) — OpenAI-compatible gateway (see [bifrost] section below).
|
||||
# "openai-oauth" — ride the Codex CLI's ChatGPT login (run
|
||||
# `codex login` first); drives
|
||||
# chatgpt.com/backend-api/codex/responses with no
|
||||
# API key. base_url/api_key/virtual_key are ignored
|
||||
# API key. [bifrost] base_url/api_key/virtual_key are ignored
|
||||
# in this mode; set primary_model to a ChatGPT model
|
||||
# (e.g. "gpt-5.5"). Bifrost-namespaced ids are
|
||||
# tolerated and mapped to the provider default.
|
||||
[inference]
|
||||
# provider = "bifrost"
|
||||
|
||||
# === BIFROST PROVIDER ===
|
||||
[bifrost]
|
||||
# Bifrost is the OpenAI-compatible API gateway
|
||||
base_url = "http://127.0.0.1:3360"
|
||||
api_key = "" # Set via `souveraine auth set` or BIFROST_KEY env var
|
||||
|
|
|
|||
|
|
@ -445,3 +445,143 @@ async fn federation_events_stream(server: Arc<SouveraineServer>, mut socket: Web
|
|||
|
||||
tracing::info!("federation: peer bridge disconnected from inbound endpoint");
|
||||
}
|
||||
|
||||
// ─── Phase 3 REST Handlers ────────────────────────────────────────────────
|
||||
|
||||
pub async fn get_config(
|
||||
State(server): State<Arc<SouveraineServer>>,
|
||||
) -> Result<Json<crate::core::config::ConsciousnessConfig>, ApiError> {
|
||||
let config = server.app_config.read().await.clone();
|
||||
Ok(Json(config))
|
||||
}
|
||||
|
||||
pub async fn update_config(
|
||||
State(server): State<Arc<SouveraineServer>>,
|
||||
Json(new_config): Json<crate::core::config::ConsciousnessConfig>,
|
||||
) -> Result<Json<crate::core::config::ConsciousnessConfig>, ApiError> {
|
||||
// 1. Update active configuration in memory
|
||||
{
|
||||
let mut config = server.app_config.write().await;
|
||||
*config = new_config.clone();
|
||||
}
|
||||
|
||||
// 2. Persist updated TOML configuration file to disk
|
||||
let config_path = crate::core::config::ConsciousnessConfig::discover_path()
|
||||
.unwrap_or_else(|| std::path::PathBuf::from("souveraine.toml"));
|
||||
|
||||
if let Err(e) = new_config.save(&config_path) {
|
||||
tracing::warn!("Failed to persist config to disk at {:?}: {}", config_path, e);
|
||||
return Err((StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse {
|
||||
error: "config_save_failed".to_string(),
|
||||
message: e.to_string(),
|
||||
})));
|
||||
} else {
|
||||
tracing::info!("Saved live settings to {:?}", config_path);
|
||||
}
|
||||
|
||||
Ok(Json(new_config))
|
||||
}
|
||||
|
||||
pub async fn get_compaction_logs(
|
||||
State(server): State<Arc<SouveraineServer>>,
|
||||
) -> Result<Json<serde_json::Value>, ApiError> {
|
||||
let base = dirs::home_dir().unwrap_or_default().join(".souveraine");
|
||||
let events_dir = base.join("events");
|
||||
|
||||
let mut logs = Vec::new();
|
||||
|
||||
// Scan date partitioned logs for the past 7 days
|
||||
let today = chrono::Utc::now().date_naive();
|
||||
for i in 0..7 {
|
||||
let date = today - chrono::Duration::days(i);
|
||||
let path = events_dir.join(format!("events-{}.jsonl", date.format("%Y-%m-%d")));
|
||||
if !path.exists() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Ok(content) = std::fs::read_to_string(&path) {
|
||||
for line in content.lines() {
|
||||
if line.trim().is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Ok(val) = serde_json::from_str::<serde_json::Value>(line) {
|
||||
let sensor = val.get("sensor_name").and_then(|s| s.as_str()).unwrap_or("");
|
||||
let ev_type = val.get("event_type").and_then(|t| t.as_str()).unwrap_or("");
|
||||
let content = val.get("payload").and_then(|p| p.get("content").and_then(|c| c.as_str())).unwrap_or("");
|
||||
|
||||
if sensor == "archivist" ||
|
||||
ev_type.contains("compaction") ||
|
||||
ev_type.contains("archive") ||
|
||||
content.contains("compaction") {
|
||||
logs.push(val);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return newest events first
|
||||
logs.reverse();
|
||||
|
||||
Ok(Json(serde_json::json!({ "logs": logs })))
|
||||
}
|
||||
|
||||
pub async fn get_conversation_tokens(
|
||||
State(server): State<Arc<SouveraineServer>>,
|
||||
Path(conversation_id): Path<String>,
|
||||
) -> Result<Json<serde_json::Value>, ApiError> {
|
||||
let session = server.sessions.get(&conversation_id)
|
||||
.ok_or_else(|| (StatusCode::NOT_FOUND, Json(ErrorResponse {
|
||||
error: "conversation_not_found".to_string(),
|
||||
message: format!("Conversation {} not found", conversation_id),
|
||||
})))?;
|
||||
|
||||
let counter = crate::bridge::model_router::TokenCounter::new();
|
||||
|
||||
let mut system_tokens = 0;
|
||||
let mut user_tokens = 0;
|
||||
let mut assistant_tokens = 0;
|
||||
|
||||
for msg in &session.messages {
|
||||
let mut msg_tokens = 0;
|
||||
for block in &msg.blocks {
|
||||
let block_text = match block {
|
||||
crate::core::session::ContentBlock::Text { text } => text.clone(),
|
||||
crate::core::session::ContentBlock::ToolUse { id, name, input } => format!("{id} {name} {input}"),
|
||||
crate::core::session::ContentBlock::ToolResult { tool_use_id, tool_name, output, .. } => {
|
||||
format!("{tool_use_id} {tool_name} {output}")
|
||||
}
|
||||
crate::core::session::ContentBlock::Reasoning { reasoning } => reasoning.clone(),
|
||||
crate::core::session::ContentBlock::Image { media_type, data } => format!("{media_type} {data}"),
|
||||
};
|
||||
msg_tokens += counter.count(&block_text);
|
||||
}
|
||||
|
||||
match msg.role {
|
||||
crate::core::session::MessageRole::System => system_tokens += msg_tokens,
|
||||
crate::core::session::MessageRole::User => user_tokens += msg_tokens,
|
||||
crate::core::session::MessageRole::Assistant => assistant_tokens += msg_tokens,
|
||||
crate::core::session::MessageRole::Tool => user_tokens += msg_tokens, // Tool inputs/outputs consume context space
|
||||
}
|
||||
}
|
||||
|
||||
let total_tokens = system_tokens + user_tokens + assistant_tokens;
|
||||
|
||||
// Load agent to query their configured model's context limit
|
||||
let limit = if let Ok(agent) = server.agents.get(&session.agent_id).await {
|
||||
agent.llm_config.context_window as usize
|
||||
} else {
|
||||
128000
|
||||
};
|
||||
|
||||
Ok(Json(serde_json::json!({
|
||||
"system_tokens": system_tokens,
|
||||
"user_tokens": user_tokens,
|
||||
"assistant_tokens": assistant_tokens,
|
||||
"total_tokens": total_tokens,
|
||||
"context_limit": limit,
|
||||
"percentage": if limit > 0 { (total_tokens as f32 / limit as f32).min(1.0) } else { 0.0 }
|
||||
})))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,9 @@ pub fn create_routes(state: Arc<SouveraineServer>) -> Router {
|
|||
.route("/v1/conversations", get(handlers::list_conversations).post(handlers::create_conversation))
|
||||
.route("/v1/firehose", get(handlers::firehose))
|
||||
.route("/v1/federation/events", get(handlers::federation_events))
|
||||
.route("/v1/config", get(handlers::get_config).post(handlers::update_config))
|
||||
.route("/v1/compaction-logs", get(handlers::get_compaction_logs))
|
||||
.route("/v1/conversations/:id/tokens", get(handlers::get_conversation_tokens))
|
||||
.route("/health", get(health_check));
|
||||
|
||||
// Protected agent routes — require per-agent bearer token.
|
||||
|
|
|
|||
|
|
@ -516,8 +516,9 @@ impl BifrostClient {
|
|||
return Err(primary_err);
|
||||
}
|
||||
warn!(
|
||||
"Primary model {} exhausted, trying {} fallback(s)",
|
||||
"Primary model {} exhausted (error: {}), trying {} fallback(s)",
|
||||
request.model,
|
||||
primary_err,
|
||||
fallbacks.len()
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,13 +19,13 @@ use crate::core::config::ConsciousnessConfig;
|
|||
|
||||
/// Build the active LLM provider from config.
|
||||
///
|
||||
/// `[bifrost] provider` selects the implementation:
|
||||
/// `[inference] provider` selects the implementation:
|
||||
/// - `"bifrost"` (default) — any OpenAI-compatible gateway via `BifrostClient`.
|
||||
/// - `"openai-oauth"` — ride the Codex CLI's ChatGPT login and drive
|
||||
/// `backend-api/codex/responses`.
|
||||
pub fn build_provider(config: &ConsciousnessConfig) -> anyhow::Result<Arc<dyn LlmProvider>> {
|
||||
let bf = &config.bifrost;
|
||||
match bf.provider.as_str() {
|
||||
match config.inference.provider.as_str() {
|
||||
"openai-oauth" | "openai-codex" => {
|
||||
let provider = providers::openai_oauth::OpenAiOAuthProvider::from_codex_login(
|
||||
bf.primary_model.clone(),
|
||||
|
|
|
|||
|
|
@ -28,7 +28,6 @@ pub async fn run_model_command(
|
|||
emit_json: bool,
|
||||
verbose: bool,
|
||||
) -> Result<()> {
|
||||
use crate::bridge::bifrost::BifrostClient;
|
||||
use crate::core::config::ConsciousnessConfig;
|
||||
|
||||
// Load config
|
||||
|
|
@ -37,17 +36,11 @@ pub async fn run_model_command(
|
|||
.unwrap_or_else(|_| PathBuf::from("souveraine.toml"));
|
||||
let config = ConsciousnessConfig::load(&config_path)?;
|
||||
|
||||
// Create Bifrost client
|
||||
let bifrost = BifrostClient::new(
|
||||
&config.bifrost.base_url,
|
||||
&config.bifrost.api_key,
|
||||
&config.bifrost.virtual_key,
|
||||
&config.bifrost.primary_model,
|
||||
config.bifrost.timeout_secs,
|
||||
);
|
||||
// Create the active inference provider
|
||||
let provider = crate::bridge::build_provider(&config)?;
|
||||
|
||||
// Fetch models from Bifrost
|
||||
let bifrost_models = bifrost.list_models().await.unwrap_or_default();
|
||||
// Fetch models from the provider
|
||||
let bifrost_models = provider.list_models().await.unwrap_or_default();
|
||||
|
||||
// Merge with configured models
|
||||
let mut all_models = bifrost_models.clone();
|
||||
|
|
|
|||
|
|
@ -7,6 +7,10 @@ use crate::core::compact::CompactionConfig;
|
|||
/// Top-level config — mirrors souveraine.example.toml structure
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ConsciousnessConfig {
|
||||
/// Active inference provider (bifrost / openai-oauth / ...)
|
||||
#[serde(default)]
|
||||
pub inference: InferenceConfig,
|
||||
|
||||
/// Bifrost inference gateway config
|
||||
#[serde(default)]
|
||||
pub bifrost: BifrostConfig,
|
||||
|
|
@ -158,8 +162,34 @@ impl Default for AuthConfig {
|
|||
}
|
||||
}
|
||||
|
||||
// ── Inference ──
|
||||
|
||||
/// Selects the active LLM provider. Each provider reads its own config
|
||||
/// section ([bifrost], or external auth like the Codex CLI login).
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct InferenceConfig {
|
||||
/// Active provider: "bifrost" (OpenAI-compatible gateway, default)
|
||||
/// or "openai-oauth" (ride the Codex CLI ChatGPT login →
|
||||
/// backend-api/codex/responses). See `bridge::build_provider`.
|
||||
#[serde(default = "default_provider")]
|
||||
pub provider: String,
|
||||
}
|
||||
|
||||
impl Default for InferenceConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
provider: default_provider(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Bifrost ──
|
||||
|
||||
/// Bifrost provider configuration. Read when `[inference] provider = "bifrost"`.
|
||||
/// `primary_model` and `timeout_secs` are shared cross-provider concepts
|
||||
/// that the OAuth provider receives as constructor parameters; they live here
|
||||
/// because Bifrost is the historical default and every call-site already reads
|
||||
/// them from this struct.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct BifrostConfig {
|
||||
/// Bifrost API base URL (e.g. "http://127.0.0.1:3360")
|
||||
|
|
@ -187,12 +217,6 @@ pub struct BifrostConfig {
|
|||
/// Default: 120 (two minutes per attempt, 7 attempts = ~14 min total).
|
||||
#[serde(default = "default_bifrost_timeout")]
|
||||
pub timeout_secs: u64,
|
||||
|
||||
/// Active provider implementation: "bifrost" (OpenAI-compatible gateway,
|
||||
/// default) or "openai-oauth" (ride the Codex CLI ChatGPT login →
|
||||
/// backend-api/codex/responses). See `bridge::build_provider`.
|
||||
#[serde(default = "default_provider")]
|
||||
pub provider: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
|
|
@ -214,7 +238,6 @@ impl Default for BifrostConfig {
|
|||
primary_model: default_primary_model(),
|
||||
models: HashMap::new(),
|
||||
timeout_secs: default_bifrost_timeout(),
|
||||
provider: default_provider(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -603,6 +626,7 @@ pub struct AgentReflectionSettings {
|
|||
impl Default for ConsciousnessConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
inference: InferenceConfig::default(),
|
||||
bifrost: BifrostConfig::default(),
|
||||
models: default_models(),
|
||||
subconscious: SubconsciousConfig::default(),
|
||||
|
|
|
|||
20
src/main.rs
20
src/main.rs
|
|
@ -1285,16 +1285,16 @@ async fn load_config() -> anyhow::Result<ConsciousnessConfig> {
|
|||
warn!("no config found; using defaults (will probe Bifrost for models)");
|
||||
let config = ConsciousnessConfig::default();
|
||||
|
||||
// Try to seed model list from Bifrost
|
||||
info!("discovering models from Bifrost");
|
||||
let bifrost = bridge::BifrostClient::new(
|
||||
&config.bifrost.base_url,
|
||||
&config.bifrost.api_key,
|
||||
&config.bifrost.virtual_key,
|
||||
&config.bifrost.primary_model,
|
||||
config.bifrost.timeout_secs,
|
||||
);
|
||||
match bifrost.list_models().await {
|
||||
// Try to seed model list from the active inference provider
|
||||
info!("discovering models from provider");
|
||||
let provider = match bridge::build_provider(&config) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
warn!("could not build provider: {}", e);
|
||||
return Ok(config);
|
||||
}
|
||||
};
|
||||
match provider.list_models().await {
|
||||
Ok(models) => {
|
||||
info!("{} models available via Bifrost", models.len());
|
||||
for m in models {
|
||||
|
|
|
|||
|
|
@ -1,16 +1,21 @@
|
|||
//! Server Conversation Handler
|
||||
//!
|
||||
//! Simplified conversation flow for server mode:
|
||||
//! - Uses Bifrost directly (no git-backed components)
|
||||
//! - Persists to SQLite
|
||||
//! - Can be enhanced later with full tool-calling
|
||||
//! Simplified conversation flow for server mode — one turn without the full
|
||||
//! tool loop. Used for lightweight server-side interactions (the SSE streaming
|
||||
//! path uses `server::turn::run_turn` for the full tool loop).
|
||||
//!
|
||||
//! Currently not called by any active code path (May 2026); kept as a
|
||||
//! lighter-weight alternative to `run_turn` for future use.
|
||||
|
||||
use crate::bridge::bifrost::{BifrostClient, ChatCompletionRequest, ContentPart, ImageUrlSource, Message as BifrostMessage};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::bridge::bifrost::{ChatCompletionRequest, ContentPart, ImageUrlSource, Message as BifrostMessage};
|
||||
use crate::bridge::LlmProvider;
|
||||
use crate::core::session::{ContentBlock, ConversationMessage, Session};
|
||||
|
||||
pub struct ServerConversation {
|
||||
pub session: Session,
|
||||
bifrost: BifrostClient,
|
||||
provider: Arc<dyn LlmProvider>,
|
||||
model: String,
|
||||
}
|
||||
|
||||
|
|
@ -19,10 +24,10 @@ pub struct ServerTurnResult {
|
|||
}
|
||||
|
||||
impl ServerConversation {
|
||||
pub fn new(agent_name: &str, bifrost: BifrostClient, model: String) -> Self {
|
||||
pub fn new(agent_name: &str, provider: Arc<dyn LlmProvider>, model: String) -> Self {
|
||||
Self {
|
||||
session: Session::new(agent_name),
|
||||
bifrost,
|
||||
provider,
|
||||
model,
|
||||
}
|
||||
}
|
||||
|
|
@ -32,7 +37,7 @@ impl ServerConversation {
|
|||
// Add user message
|
||||
self.session.add_message(ConversationMessage::user_text(user_input));
|
||||
|
||||
// Build messages for Bifrost — flatten text blocks; ignore tool blocks
|
||||
// Build messages — flatten text blocks; ignore tool blocks
|
||||
// until the server tool loop lands.
|
||||
let messages: Vec<BifrostMessage> = self.session.messages.iter().map(|m| {
|
||||
let role = match m.role {
|
||||
|
|
@ -69,7 +74,7 @@ impl ServerConversation {
|
|||
}
|
||||
}).collect();
|
||||
|
||||
// Call Bifrost
|
||||
// Call the active provider
|
||||
let req = ChatCompletionRequest {
|
||||
model: self.model.clone(),
|
||||
messages,
|
||||
|
|
@ -79,7 +84,7 @@ impl ServerConversation {
|
|||
tools: None,
|
||||
};
|
||||
|
||||
let response = self.bifrost.chat_completion(req).await?;
|
||||
let response = self.provider.chat_completion(req).await?;
|
||||
let content = response.content.clone();
|
||||
|
||||
// Store assistant response
|
||||
|
|
|
|||
|
|
@ -109,18 +109,19 @@ impl App {
|
|||
|
||||
match &action {
|
||||
Some(SettingsAction::FetchModels) => {
|
||||
let base_url = view.config.bifrost.base_url.clone();
|
||||
let api_key = view.config.bifrost.api_key.clone();
|
||||
let virtual_key = view.config.bifrost.virtual_key.clone();
|
||||
let primary = view.config.bifrost.primary_model.clone();
|
||||
let timeout = view.config.bifrost.timeout_secs;
|
||||
let config = view.config.clone();
|
||||
let extra: Vec<String> = view.config.models.keys().cloned().collect();
|
||||
let (tx, rx) = tokio::sync::oneshot::channel();
|
||||
view.models_rx = Some(rx);
|
||||
tokio::spawn(async move {
|
||||
let bifrost = crate::bridge::bifrost::BifrostClient::new(&base_url, &api_key, &virtual_key, &primary, timeout);
|
||||
let mut models = bifrost.list_models().await.unwrap_or_default();
|
||||
let models = match crate::bridge::build_provider(&config) {
|
||||
Ok(provider) => {
|
||||
let mut models = provider.list_models().await.unwrap_or_default();
|
||||
for m in extra { if !models.contains(&m) { models.push(m); } }
|
||||
models
|
||||
}
|
||||
Err(_) => extra,
|
||||
};
|
||||
let _ = tx.send(models);
|
||||
});
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ use tokio_util::sync::CancellationToken;
|
|||
use tokio::sync::mpsc;
|
||||
|
||||
use crate::backend::{Backend, BackendEvent};
|
||||
use crate::bridge::bifrost::BifrostClient;
|
||||
|
||||
use crate::core::config::ConsciousnessConfig;
|
||||
|
||||
use super::{
|
||||
|
|
@ -407,14 +407,14 @@ Tab toggles the cockpit pane. `t` (on empty input) toggles tool expansion.";
|
|||
tokio::spawn(async move {
|
||||
let result = match ConsciousnessConfig::load(&cfg_path) {
|
||||
Ok(cfg) => {
|
||||
let bifrost = BifrostClient::new(
|
||||
&cfg.bifrost.base_url,
|
||||
&cfg.bifrost.api_key,
|
||||
&cfg.bifrost.virtual_key,
|
||||
&cfg.bifrost.primary_model,
|
||||
cfg.bifrost.timeout_secs,
|
||||
);
|
||||
let bifrost_models = bifrost.list_models().await.unwrap_or_default();
|
||||
let provider = match crate::bridge::build_provider(&cfg) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
let _ = tx.send(format!("✕ Could not build provider: {}", e));
|
||||
return;
|
||||
}
|
||||
};
|
||||
let bifrost_models = provider.list_models().await.unwrap_or_default();
|
||||
let mut all_models = bifrost_models.clone();
|
||||
for name in cfg.models.keys() {
|
||||
if !all_models.contains(name) {
|
||||
|
|
|
|||
|
|
@ -364,6 +364,7 @@ pub struct ChatState {
|
|||
/// when a non-token event (tool call/result, or a new pass) arrives.
|
||||
pub subconscious_current: String,
|
||||
|
||||
|
||||
/// Current itinerary route-line for the header strip.
|
||||
/// Empty string means no active itinerary.
|
||||
pub itinerary_line: String,
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ pub enum PanelFocus {
|
|||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Category {
|
||||
Agent,
|
||||
Inference,
|
||||
Bifrost,
|
||||
Subconscious,
|
||||
Reflection,
|
||||
|
|
@ -38,6 +39,7 @@ impl Category {
|
|||
pub fn all() -> &'static [Category] {
|
||||
&[
|
||||
Category::Agent,
|
||||
Category::Inference,
|
||||
Category::Bifrost,
|
||||
Category::Subconscious,
|
||||
Category::Reflection,
|
||||
|
|
@ -60,6 +62,7 @@ impl Category {
|
|||
pub fn label(&self) -> &'static str {
|
||||
match self {
|
||||
Category::Agent => "Agent",
|
||||
Category::Inference => "Inference",
|
||||
Category::Bifrost => "Bifrost",
|
||||
Category::Subconscious => "Subconscious",
|
||||
Category::Reflection => "Reflection",
|
||||
|
|
@ -87,6 +90,8 @@ pub enum FieldLoc {
|
|||
// Agent
|
||||
AgSystemPrompt,
|
||||
AgModel,
|
||||
// Inference
|
||||
IfProvider,
|
||||
// Bifrost
|
||||
BfBaseUrl,
|
||||
BfApiKey,
|
||||
|
|
@ -185,6 +190,7 @@ impl FieldLoc {
|
|||
pub fn category(&self) -> Category {
|
||||
match self {
|
||||
FieldLoc::AgSystemPrompt | FieldLoc::AgModel => Category::Agent,
|
||||
FieldLoc::IfProvider => Category::Inference,
|
||||
FieldLoc::BfBaseUrl | FieldLoc::BfApiKey | FieldLoc::BfVirtualKey | FieldLoc::BfPrimaryModel | FieldLoc::BfTimeoutSecs => Category::Bifrost,
|
||||
FieldLoc::ScN1Enabled | FieldLoc::ScN1Trigger | FieldLoc::ScN1Every | FieldLoc::ScN1Secs
|
||||
| FieldLoc::ScInboxEnabled | FieldLoc::ScModel | FieldLoc::ScMaxTokens
|
||||
|
|
@ -213,6 +219,7 @@ impl FieldLoc {
|
|||
match self {
|
||||
FieldLoc::AgSystemPrompt => "system_prompt",
|
||||
FieldLoc::AgModel => "model",
|
||||
FieldLoc::IfProvider => "provider",
|
||||
FieldLoc::BfBaseUrl => "base_url",
|
||||
FieldLoc::BfApiKey => "api_key",
|
||||
FieldLoc::BfVirtualKey => "virtual_key",
|
||||
|
|
@ -309,6 +316,7 @@ impl FieldLoc {
|
|||
match self {
|
||||
FieldLoc::AgSystemPrompt => "platform prompt",
|
||||
FieldLoc::AgModel => "agent model",
|
||||
FieldLoc::IfProvider => "active provider",
|
||||
FieldLoc::BfBaseUrl => "endpoint",
|
||||
FieldLoc::BfApiKey => "API key",
|
||||
FieldLoc::BfVirtualKey => "virtual key",
|
||||
|
|
|
|||
|
|
@ -172,6 +172,16 @@ impl SettingsView {
|
|||
out.push((AgSubconsciousStatus, EditableValue::Bool(agent.has_subconscious)));
|
||||
}
|
||||
}
|
||||
Category::Inference => {
|
||||
let provider = &self.config.inference.provider;
|
||||
let variants = vec!["bifrost".to_string(), "openai-oauth".to_string()];
|
||||
let mut all = variants.clone();
|
||||
if !all.contains(provider) {
|
||||
all.insert(0, provider.clone());
|
||||
}
|
||||
let idx = all.iter().position(|p| p == provider).unwrap_or(0);
|
||||
out.push((IfProvider, EditableValue::EnumVariant { index: idx, variants: all }));
|
||||
}
|
||||
Category::Bifrost => {
|
||||
out.push((BfBaseUrl, EditableValue::Text(self.config.bifrost.base_url.clone())));
|
||||
out.push((BfApiKey, EditableValue::Secret(self.config.bifrost.api_key.clone())));
|
||||
|
|
@ -409,6 +419,13 @@ impl SettingsView {
|
|||
}
|
||||
}
|
||||
}
|
||||
IfProvider => {
|
||||
if let EditableValue::EnumVariant { index, variants } = value {
|
||||
if let Some(p) = variants.get(index) {
|
||||
self.config.inference.provider = p.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
BfBaseUrl => { if let EditableValue::Text(v) = value { self.config.bifrost.base_url = v; } }
|
||||
BfApiKey => { if let EditableValue::Text(v) = value { self.config.bifrost.api_key = v; } }
|
||||
BfVirtualKey => { if let EditableValue::Text(v) = value { self.config.bifrost.virtual_key = v; } }
|
||||
|
|
|
|||
Loading…
Reference in a new issue