Watch
1
0
Fork
You've already forked souveraine
0

fix: review fixes for PRs #1 and #2

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:
Fimeg 2026-05-29 19:47:54 -04:00
commit a7e909d39d
15 changed files with 383 additions and 64 deletions

124
examples/demo.rs Normal file
View 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! ✨");
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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();
for m in extra { if !models.contains(&m) { models.push(m); } }
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;

View file

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

View file

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

View file

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

View file

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