mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-30 16:19:21 +00:00
chore: resolve conflicts
This commit is contained in:
@@ -8,6 +8,9 @@ use crate::llm::SessionManager;
|
||||
use crate::settings::Settings;
|
||||
use crate::workspace::EmbeddingProvider;
|
||||
|
||||
/// Default maximum number of cached embeddings.
|
||||
pub const DEFAULT_EMBEDDING_CACHE_SIZE: usize = 10_000;
|
||||
|
||||
/// Embeddings provider configuration.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EmbeddingsConfig {
|
||||
@@ -26,6 +29,12 @@ pub struct EmbeddingsConfig {
|
||||
/// Custom base URL for OpenAI-compatible embedding providers.
|
||||
/// When set, overrides the default `https://api.openai.com`.
|
||||
pub openai_base_url: Option<String>,
|
||||
/// Maximum entries in the embedding LRU cache (default 10,000).
|
||||
///
|
||||
/// Approximate raw embedding payload: `cache_size × dimension × 4 bytes`.
|
||||
/// 10,000 × 1536 floats ≈ 58 MB (payload only; actual memory is higher
|
||||
/// due to HashMap buckets, per-entry Vec/timestamp overhead).
|
||||
pub cache_size: usize,
|
||||
}
|
||||
|
||||
impl Default for EmbeddingsConfig {
|
||||
@@ -40,6 +49,7 @@ impl Default for EmbeddingsConfig {
|
||||
ollama_base_url: "http://localhost:11434".to_string(),
|
||||
dimension,
|
||||
openai_base_url: None,
|
||||
cache_size: DEFAULT_EMBEDDING_CACHE_SIZE,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,7 +57,7 @@ impl Default for EmbeddingsConfig {
|
||||
/// Infer the embedding dimension from a well-known model name.
|
||||
///
|
||||
/// Falls back to 1536 (OpenAI text-embedding-3-small default) for unknown models.
|
||||
fn default_dimension_for_model(model: &str) -> usize {
|
||||
pub(crate) fn default_dimension_for_model(model: &str) -> usize {
|
||||
match model {
|
||||
"text-embedding-3-small" => 1536,
|
||||
"text-embedding-3-large" => 3072,
|
||||
@@ -80,6 +90,15 @@ impl EmbeddingsConfig {
|
||||
|
||||
let openai_base_url = optional_env("EMBEDDING_BASE_URL")?;
|
||||
|
||||
let cache_size = parse_optional_env("EMBEDDING_CACHE_SIZE", DEFAULT_EMBEDDING_CACHE_SIZE)?;
|
||||
|
||||
if cache_size == 0 {
|
||||
return Err(ConfigError::InvalidValue {
|
||||
key: "EMBEDDING_CACHE_SIZE".to_string(),
|
||||
message: "must be at least 1".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
enabled,
|
||||
provider,
|
||||
@@ -88,6 +107,7 @@ impl EmbeddingsConfig {
|
||||
ollama_base_url,
|
||||
dimension,
|
||||
openai_base_url,
|
||||
cache_size,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -183,13 +203,13 @@ mod tests {
|
||||
std::env::remove_var("EMBEDDING_MODEL");
|
||||
std::env::remove_var("OPENAI_API_KEY");
|
||||
std::env::remove_var("EMBEDDING_BASE_URL");
|
||||
std::env::remove_var("EMBEDDING_CACHE_SIZE");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn embeddings_disabled_not_overridden_by_openai_key() {
|
||||
let _guard = ENV_MUTEX.lock().expect("env mutex poisoned");
|
||||
|
||||
clear_embedding_env();
|
||||
// SAFETY: Under ENV_MUTEX, no concurrent env access.
|
||||
unsafe {
|
||||
@@ -240,7 +260,6 @@ mod tests {
|
||||
#[test]
|
||||
fn embeddings_env_override_takes_precedence() {
|
||||
let _guard = ENV_MUTEX.lock().expect("env mutex poisoned");
|
||||
|
||||
clear_embedding_env();
|
||||
// SAFETY: Under ENV_MUTEX.
|
||||
unsafe {
|
||||
@@ -281,10 +300,8 @@ mod tests {
|
||||
let config = EmbeddingsConfig::resolve(&settings).expect("resolve should succeed");
|
||||
assert_eq!(
|
||||
config.openai_base_url.as_deref(),
|
||||
Some("https://custom.example.com"),
|
||||
"EMBEDDING_BASE_URL env var should be parsed into openai_base_url"
|
||||
Some("https://custom.example.com")
|
||||
);
|
||||
|
||||
// SAFETY: Under ENV_MUTEX.
|
||||
unsafe {
|
||||
std::env::remove_var("EMBEDDING_BASE_URL");
|
||||
@@ -303,4 +320,24 @@ mod tests {
|
||||
"openai_base_url should be None when EMBEDDING_BASE_URL is not set"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cache_size_zero_rejected() {
|
||||
let _guard = ENV_MUTEX.lock().expect("env mutex poisoned");
|
||||
clear_embedding_env();
|
||||
// SAFETY: Under ENV_MUTEX.
|
||||
unsafe {
|
||||
std::env::set_var("EMBEDDING_CACHE_SIZE", "0");
|
||||
}
|
||||
|
||||
let settings = Settings::default();
|
||||
let result = EmbeddingsConfig::resolve(&settings);
|
||||
assert!(result.is_err(), "cache_size=0 should be rejected");
|
||||
let err = result.unwrap_err().to_string();
|
||||
assert!(err.contains("at least 1"), "should mention minimum: {err}");
|
||||
// SAFETY: Under ENV_MUTEX.
|
||||
unsafe {
|
||||
std::env::remove_var("EMBEDDING_CACHE_SIZE");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -109,7 +109,7 @@ impl LlmConfig {
|
||||
// Always resolve NEAR AI config (used for embeddings even when not the primary backend)
|
||||
let nearai_api_key = optional_env("NEARAI_API_KEY")?.map(SecretString::from);
|
||||
let nearai = NearAiConfig {
|
||||
model: Self::resolve_model("NEARAI_MODEL", settings, "zai-org/GLM-latest")?,
|
||||
model: Self::resolve_model("NEARAI_MODEL", settings, crate::llm::DEFAULT_MODEL)?,
|
||||
cheap_model: optional_env("NEARAI_CHEAP_MODEL")?,
|
||||
base_url: optional_env("NEARAI_BASE_URL")?.unwrap_or_else(|| {
|
||||
if nearai_api_key.is_some() {
|
||||
|
||||
+2
-2
@@ -9,7 +9,7 @@ mod agent;
|
||||
mod builder;
|
||||
mod channels;
|
||||
mod database;
|
||||
mod embeddings;
|
||||
pub(crate) mod embeddings;
|
||||
mod heartbeat;
|
||||
pub(crate) mod helpers;
|
||||
mod hygiene;
|
||||
@@ -38,7 +38,7 @@ pub use self::channels::{
|
||||
ChannelsConfig, CliConfig, DEFAULT_GATEWAY_PORT, GatewayConfig, HttpConfig, SignalConfig,
|
||||
};
|
||||
pub use self::database::{DatabaseBackend, DatabaseConfig, SslMode, default_libsql_path};
|
||||
pub use self::embeddings::EmbeddingsConfig;
|
||||
pub use self::embeddings::{DEFAULT_EMBEDDING_CACHE_SIZE, EmbeddingsConfig};
|
||||
pub use self::heartbeat::HeartbeatConfig;
|
||||
pub use self::hygiene::HygieneConfig;
|
||||
pub use self::llm::default_session_path;
|
||||
|
||||
+29
-35
@@ -7,7 +7,7 @@ use secrecy::SecretString;
|
||||
pub struct RelayConfig {
|
||||
/// Base URL of the channel-relay service (e.g., `http://localhost:3001`).
|
||||
pub url: String,
|
||||
/// API key for authenticated channel-relay endpoints.
|
||||
/// Bearer token for authenticated channel-relay endpoints (`sk-agent-*`).
|
||||
pub api_key: SecretString,
|
||||
/// Override for the OAuth callback URL (e.g., a tunnel URL).
|
||||
pub callback_url: Option<String>,
|
||||
@@ -15,12 +15,8 @@ pub struct RelayConfig {
|
||||
pub instance_id: Option<String>,
|
||||
/// HTTP request timeout in seconds (default: 30).
|
||||
pub request_timeout_secs: u64,
|
||||
/// SSE stream long-poll timeout in seconds (default: 86400 = 24 h).
|
||||
pub stream_timeout_secs: u64,
|
||||
/// Initial exponential backoff in milliseconds (default: 1000).
|
||||
pub backoff_initial_ms: u64,
|
||||
/// Maximum exponential backoff in milliseconds (default: 60000).
|
||||
pub backoff_max_ms: u64,
|
||||
/// Path for the webhook callback endpoint (default: `/relay/events`).
|
||||
pub webhook_path: String,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for RelayConfig {
|
||||
@@ -31,9 +27,7 @@ impl std::fmt::Debug for RelayConfig {
|
||||
.field("callback_url", &self.callback_url)
|
||||
.field("instance_id", &self.instance_id)
|
||||
.field("request_timeout_secs", &self.request_timeout_secs)
|
||||
.field("stream_timeout_secs", &self.stream_timeout_secs)
|
||||
.field("backoff_initial_ms", &self.backoff_initial_ms)
|
||||
.field("backoff_max_ms", &self.backoff_max_ms)
|
||||
.field("webhook_path", &self.webhook_path)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
@@ -41,8 +35,10 @@ impl std::fmt::Debug for RelayConfig {
|
||||
impl RelayConfig {
|
||||
/// Load relay config from environment variables.
|
||||
///
|
||||
/// Returns `None` if either `CHANNEL_RELAY_URL` or `CHANNEL_RELAY_API_KEY`
|
||||
/// is not set, making the relay integration opt-in.
|
||||
/// Returns `None` if either of the required env vars (`CHANNEL_RELAY_URL`,
|
||||
/// `CHANNEL_RELAY_API_KEY`) is not set, making the relay integration opt-in.
|
||||
/// The signing secret is fetched from channel-relay at activation time via
|
||||
/// the authenticated `/relay/signing-secret` endpoint — no env var required.
|
||||
pub fn from_env() -> Option<Self> {
|
||||
Self::from_env_reader(|key| std::env::var(key).ok())
|
||||
}
|
||||
@@ -55,9 +51,7 @@ impl RelayConfig {
|
||||
callback_url: None,
|
||||
instance_id: None,
|
||||
request_timeout_secs: 30,
|
||||
stream_timeout_secs: 86400,
|
||||
backoff_initial_ms: 1000,
|
||||
backoff_max_ms: 60000,
|
||||
webhook_path: "/relay/events".into(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,15 +67,7 @@ impl RelayConfig {
|
||||
request_timeout_secs: env("RELAY_REQUEST_TIMEOUT_SECS")
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(30),
|
||||
stream_timeout_secs: env("RELAY_STREAM_TIMEOUT_SECS")
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(86400),
|
||||
backoff_initial_ms: env("RELAY_BACKOFF_INITIAL_MS")
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(1000),
|
||||
backoff_max_ms: env("RELAY_BACKOFF_MAX_MS")
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(60000),
|
||||
webhook_path: env("RELAY_WEBHOOK_PATH").unwrap_or_else(|| "/relay/events".into()),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -97,7 +83,21 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_env_reader_loads_defaults() {
|
||||
fn from_env_reader_requires_only_url_and_api_key() {
|
||||
// Signing secret is fetched at activation time — only URL + API key needed.
|
||||
let config = RelayConfig::from_env_reader(|key| match key {
|
||||
"CHANNEL_RELAY_URL" => Some("http://localhost:3001".into()),
|
||||
"CHANNEL_RELAY_API_KEY" => Some("test-key".into()),
|
||||
_ => None,
|
||||
});
|
||||
assert!(
|
||||
config.is_some(),
|
||||
"relay config should load with just URL + API key"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_env_reader_loads_all_required() {
|
||||
let config = RelayConfig::from_env_reader(|key| match key {
|
||||
"CHANNEL_RELAY_URL" => Some("http://localhost:3001".into()),
|
||||
"CHANNEL_RELAY_API_KEY" => Some("test-key".into()),
|
||||
@@ -107,9 +107,7 @@ mod tests {
|
||||
|
||||
assert_eq!(config.url, "http://localhost:3001");
|
||||
assert_eq!(config.request_timeout_secs, 30);
|
||||
assert_eq!(config.stream_timeout_secs, 86400);
|
||||
assert_eq!(config.backoff_initial_ms, 1000);
|
||||
assert_eq!(config.backoff_max_ms, 60000);
|
||||
assert_eq!(config.webhook_path, "/relay/events");
|
||||
assert!(config.callback_url.is_none());
|
||||
assert!(config.instance_id.is_none());
|
||||
}
|
||||
@@ -122,9 +120,7 @@ mod tests {
|
||||
"IRONCLAW_OAUTH_CALLBACK_URL" => Some("https://tunnel.example.com".into()),
|
||||
"IRONCLAW_INSTANCE_ID" => Some("my-instance".into()),
|
||||
"RELAY_REQUEST_TIMEOUT_SECS" => Some("60".into()),
|
||||
"RELAY_STREAM_TIMEOUT_SECS" => Some("43200".into()),
|
||||
"RELAY_BACKOFF_INITIAL_MS" => Some("2000".into()),
|
||||
"RELAY_BACKOFF_MAX_MS" => Some("120000".into()),
|
||||
"RELAY_WEBHOOK_PATH" => Some("/custom/events".into()),
|
||||
_ => None,
|
||||
})
|
||||
.expect("config should be Some");
|
||||
@@ -135,9 +131,7 @@ mod tests {
|
||||
);
|
||||
assert_eq!(config.instance_id.as_deref(), Some("my-instance"));
|
||||
assert_eq!(config.request_timeout_secs, 60);
|
||||
assert_eq!(config.stream_timeout_secs, 43200);
|
||||
assert_eq!(config.backoff_initial_ms, 2000);
|
||||
assert_eq!(config.backoff_max_ms, 120000);
|
||||
assert_eq!(config.webhook_path, "/custom/events");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -148,7 +142,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn debug_redacts_api_key() {
|
||||
fn debug_redacts_secrets() {
|
||||
let config = RelayConfig::from_values("http://localhost:3001", "super-secret");
|
||||
let debug = format!("{:?}", config);
|
||||
assert!(debug.contains("[REDACTED]"));
|
||||
|
||||
Reference in New Issue
Block a user