mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-26 23:50:17 +00:00
Merge pull request #1102 from nearai/staging-promote/1e00b1fe-23036363919
chore: promote staging to staging-promote/3c619b62-23035039465 (2026-03-13 04:35 UTC)
This commit is contained in:
@@ -31,10 +31,12 @@ jobs:
|
||||
github.event_name == 'workflow_dispatch'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout base branch
|
||||
- name: Checkout workflow source
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.event_name == 'workflow_dispatch' && 'main' || github.event.pull_request.base.ref }}
|
||||
# For chained promotion PRs, the script lives on the trusted PR head,
|
||||
# not necessarily on the older promotion branch used as the PR base.
|
||||
ref: ${{ github.event_name == 'workflow_dispatch' && 'main' || github.event.pull_request.head.sha }}
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ WORKDIR /app
|
||||
|
||||
# Copy manifests first for layer caching
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
COPY crates/ crates/
|
||||
|
||||
# Copy source, build script, tests, and supporting directories
|
||||
COPY build.rs build.rs
|
||||
|
||||
@@ -20,6 +20,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
WORKDIR /app
|
||||
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
COPY crates/ crates/
|
||||
COPY build.rs build.rs
|
||||
COPY src/ src/
|
||||
COPY tests/ tests/
|
||||
|
||||
Generated
+1
-1
@@ -121,7 +121,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "discord-channel"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"ed25519-dalek",
|
||||
"hex",
|
||||
|
||||
@@ -642,6 +642,7 @@ fn poll_channel_mentions(channel_id: &str, bot_id: &str) {
|
||||
},
|
||||
thread_id: None,
|
||||
metadata_json,
|
||||
attachments: vec![],
|
||||
});
|
||||
|
||||
remember_processed_id(&mut recent_ids, &msg.id);
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
},
|
||||
"artifacts": {
|
||||
"wasm32-wasip2": {
|
||||
"url": "https://github.com/nearai/ironclaw/releases/latest/download/discord-0.2.0-wasm32-wasip2.tar.gz",
|
||||
"url": "https://github.com/nearai/ironclaw/releases/download/v0.18.0/discord-0.2.0-wasm32-wasip2.tar.gz",
|
||||
"sha256": "efa1b9019fa33e243f8db1e1fcc732731d45836336bdd26ca19b6fe227ca8b69"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
},
|
||||
"artifacts": {
|
||||
"wasm32-wasip2": {
|
||||
"url": "https://github.com/nearai/ironclaw/releases/latest/download/slack-0.2.1-wasm32-wasip2.tar.gz",
|
||||
"url": "https://github.com/nearai/ironclaw/releases/download/v0.18.0/slack-0.2.1-wasm32-wasip2.tar.gz",
|
||||
"sha256": "d4667e35126986509d862bc3a0088777305d8f41c75de83c1e223b42312ede48"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "telegram",
|
||||
"display_name": "Telegram Channel",
|
||||
"kind": "channel",
|
||||
"version": "0.2.2",
|
||||
"version": "0.2.3",
|
||||
"wit_version": "0.3.0",
|
||||
"description": "Talk to your agent through a Telegram bot",
|
||||
"keywords": [
|
||||
@@ -18,7 +18,7 @@
|
||||
},
|
||||
"artifacts": {
|
||||
"wasm32-wasip2": {
|
||||
"url": "https://github.com/nearai/ironclaw/releases/latest/download/telegram-0.2.2-wasm32-wasip2.tar.gz",
|
||||
"url": "https://github.com/nearai/ironclaw/releases/download/v0.18.0/telegram-0.2.3-wasm32-wasip2.tar.gz",
|
||||
"sha256": "b9a83d5a2d1285ce0ec116b354336a1f245f893291ccb01dffbcaccf89d72aed"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
},
|
||||
"artifacts": {
|
||||
"wasm32-wasip2": {
|
||||
"url": "https://github.com/nearai/ironclaw/releases/latest/download/whatsapp-0.2.0-wasm32-wasip2.tar.gz",
|
||||
"url": "https://github.com/nearai/ironclaw/releases/download/v0.18.0/whatsapp-0.2.0-wasm32-wasip2.tar.gz",
|
||||
"sha256": "feb9194719d9bed796b070ab4dc30348dbfb5d3dec56f9f21e02d14137abab01"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "github",
|
||||
"display_name": "GitHub",
|
||||
"kind": "tool",
|
||||
"version": "0.2.1",
|
||||
"version": "0.2.0",
|
||||
"wit_version": "0.3.0",
|
||||
"description": "GitHub integration for issues, PRs, repos, and code search",
|
||||
"keywords": [
|
||||
@@ -19,7 +19,7 @@
|
||||
},
|
||||
"artifacts": {
|
||||
"wasm32-wasip2": {
|
||||
"url": "https://github.com/nearai/ironclaw/releases/latest/download/github-0.2.0-wasm32-wasip2.tar.gz",
|
||||
"url": "https://github.com/nearai/ironclaw/releases/download/v0.18.0/github-0.2.0-wasm32-wasip2.tar.gz",
|
||||
"sha256": "da9fac56b6f20197a415489bbaec9fefb085a5cf6324cab79ea48a47eb19c13b"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
},
|
||||
"artifacts": {
|
||||
"wasm32-wasip2": {
|
||||
"url": "https://github.com/nearai/ironclaw/releases/latest/download/gmail-0.2.0-wasm32-wasip2.tar.gz",
|
||||
"url": "https://github.com/nearai/ironclaw/releases/download/v0.18.0/gmail-0.2.0-wasm32-wasip2.tar.gz",
|
||||
"sha256": "ee9574e02e92bc1d481f1310eb88afd99ee52bf6971074ab33bd76bf99b34b1d"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
},
|
||||
"artifacts": {
|
||||
"wasm32-wasip2": {
|
||||
"url": "https://github.com/nearai/ironclaw/releases/latest/download/google-calendar-0.2.0-wasm32-wasip2.tar.gz",
|
||||
"url": "https://github.com/nearai/ironclaw/releases/download/v0.18.0/google-calendar-0.2.0-wasm32-wasip2.tar.gz",
|
||||
"sha256": "2fa47150ea222e787c122182ad6f4dfa2ffaf5fe490d05e8de887a76445f8d2d"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
},
|
||||
"artifacts": {
|
||||
"wasm32-wasip2": {
|
||||
"url": "https://github.com/nearai/ironclaw/releases/latest/download/google-docs-0.2.0-wasm32-wasip2.tar.gz",
|
||||
"url": "https://github.com/nearai/ironclaw/releases/download/v0.18.0/google-docs-0.2.0-wasm32-wasip2.tar.gz",
|
||||
"sha256": "40e134a1c1564f832ca861c3396895d4e33ec67b99313fc1f97baf8d971423a9"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
},
|
||||
"artifacts": {
|
||||
"wasm32-wasip2": {
|
||||
"url": "https://github.com/nearai/ironclaw/releases/latest/download/google-drive-0.2.0-wasm32-wasip2.tar.gz",
|
||||
"url": "https://github.com/nearai/ironclaw/releases/download/v0.18.0/google-drive-0.2.0-wasm32-wasip2.tar.gz",
|
||||
"sha256": "002a341a1d58125563a7c69561b26fbc2629b04ea723cade744102bdc0fbb71f"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
},
|
||||
"artifacts": {
|
||||
"wasm32-wasip2": {
|
||||
"url": "https://github.com/nearai/ironclaw/releases/latest/download/google-sheets-0.2.0-wasm32-wasip2.tar.gz",
|
||||
"url": "https://github.com/nearai/ironclaw/releases/download/v0.18.0/google-sheets-0.2.0-wasm32-wasip2.tar.gz",
|
||||
"sha256": "8aa2c9d52f033edea3a6c2311b0ec694ccb6d0a54ef07e94d72bf8be1ce8009a"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
},
|
||||
"artifacts": {
|
||||
"wasm32-wasip2": {
|
||||
"url": "https://github.com/nearai/ironclaw/releases/latest/download/google-slides-0.2.0-wasm32-wasip2.tar.gz",
|
||||
"url": "https://github.com/nearai/ironclaw/releases/download/v0.18.0/google-slides-0.2.0-wasm32-wasip2.tar.gz",
|
||||
"sha256": "e931a97d4fd0b0b938e464dc7c7f2be6ea6b4d1508f5ea3cd931d44db23f05f5"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
},
|
||||
"artifacts": {
|
||||
"wasm32-wasip2": {
|
||||
"url": "https://github.com/nearai/ironclaw/releases/latest/download/slack-0.2.0-wasm32-wasip2.tar.gz",
|
||||
"sha256": "8af3f884240de8413d272845fad2164a347d7d2a502a0d148aa38425b93f62ed"
|
||||
"url": "https://github.com/nearai/ironclaw/releases/download/v0.18.0/slack-0.2.1-wasm32-wasip2.tar.gz",
|
||||
"sha256": "d4667e35126986509d862bc3a0088777305d8f41c75de83c1e223b42312ede48"
|
||||
}
|
||||
},
|
||||
"auth_summary": {
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
},
|
||||
"artifacts": {
|
||||
"wasm32-wasip2": {
|
||||
"url": "https://github.com/nearai/ironclaw/releases/latest/download/telegram-0.2.0-wasm32-wasip2.tar.gz",
|
||||
"sha256": "2c66245913854be4294021fc6bb479e43f7d65830c5cec25cf6c60a71d1af468"
|
||||
"url": "https://github.com/nearai/ironclaw/releases/download/v0.18.0/telegram-0.2.2-wasm32-wasip2.tar.gz",
|
||||
"sha256": "b9a83d5a2d1285ce0ec116b354336a1f245f893291ccb01dffbcaccf89d72aed"
|
||||
}
|
||||
},
|
||||
"auth_summary": {
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
},
|
||||
"artifacts": {
|
||||
"wasm32-wasip2": {
|
||||
"url": "https://github.com/nearai/ironclaw/releases/latest/download/web-search-0.2.0-wasm32-wasip2.tar.gz",
|
||||
"url": "https://github.com/nearai/ironclaw/releases/download/v0.18.0/web-search-0.2.0-wasm32-wasip2.tar.gz",
|
||||
"sha256": "56834573c54ea2a33cea1eb0f04bbdf59f1ef8d8702995cf431b0921302eeccc"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -23,6 +23,9 @@ pub struct EmbeddingsConfig {
|
||||
pub ollama_base_url: String,
|
||||
/// Embedding vector dimension. Inferred from the model name when not set explicitly.
|
||||
pub dimension: usize,
|
||||
/// Custom base URL for OpenAI-compatible embedding providers.
|
||||
/// When set, overrides the default `https://api.openai.com`.
|
||||
pub openai_base_url: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for EmbeddingsConfig {
|
||||
@@ -36,6 +39,7 @@ impl Default for EmbeddingsConfig {
|
||||
model,
|
||||
ollama_base_url: "http://localhost:11434".to_string(),
|
||||
dimension,
|
||||
openai_base_url: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -74,6 +78,8 @@ impl EmbeddingsConfig {
|
||||
|
||||
let enabled = parse_bool_env("EMBEDDING_ENABLED", settings.embeddings.enabled)?;
|
||||
|
||||
let openai_base_url = optional_env("EMBEDDING_BASE_URL")?;
|
||||
|
||||
Ok(Self {
|
||||
enabled,
|
||||
provider,
|
||||
@@ -81,6 +87,7 @@ impl EmbeddingsConfig {
|
||||
model,
|
||||
ollama_base_url,
|
||||
dimension,
|
||||
openai_base_url,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -130,16 +137,27 @@ impl EmbeddingsConfig {
|
||||
}
|
||||
_ => {
|
||||
if let Some(api_key) = self.openai_api_key() {
|
||||
tracing::debug!(
|
||||
"Embeddings enabled via OpenAI (model: {}, dim: {})",
|
||||
self.model,
|
||||
self.dimension,
|
||||
);
|
||||
Some(Arc::new(crate::workspace::OpenAiEmbeddings::with_model(
|
||||
let mut provider = crate::workspace::OpenAiEmbeddings::with_model(
|
||||
api_key,
|
||||
&self.model,
|
||||
self.dimension,
|
||||
)))
|
||||
);
|
||||
if let Some(ref base_url) = self.openai_base_url {
|
||||
tracing::debug!(
|
||||
"Embeddings enabled via OpenAI (model: {}, base_url: {}, dim: {})",
|
||||
self.model,
|
||||
base_url,
|
||||
self.dimension,
|
||||
);
|
||||
provider = provider.with_base_url(base_url);
|
||||
} else {
|
||||
tracing::debug!(
|
||||
"Embeddings enabled via OpenAI (model: {}, dim: {})",
|
||||
self.model,
|
||||
self.dimension,
|
||||
);
|
||||
}
|
||||
Some(Arc::new(provider))
|
||||
} else {
|
||||
tracing::warn!("Embeddings configured but OPENAI_API_KEY not set");
|
||||
None
|
||||
@@ -164,6 +182,7 @@ mod tests {
|
||||
std::env::remove_var("EMBEDDING_PROVIDER");
|
||||
std::env::remove_var("EMBEDDING_MODEL");
|
||||
std::env::remove_var("OPENAI_API_KEY");
|
||||
std::env::remove_var("EMBEDDING_BASE_URL");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,4 +266,41 @@ mod tests {
|
||||
std::env::remove_var("EMBEDDING_ENABLED");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn embedding_base_url_parsed_from_env() {
|
||||
let _guard = ENV_MUTEX.lock().expect("env mutex poisoned");
|
||||
clear_embedding_env();
|
||||
|
||||
// SAFETY: Under ENV_MUTEX, no concurrent env access.
|
||||
unsafe {
|
||||
std::env::set_var("EMBEDDING_BASE_URL", "https://custom.example.com");
|
||||
}
|
||||
|
||||
let settings = Settings::default();
|
||||
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"
|
||||
);
|
||||
|
||||
// SAFETY: Under ENV_MUTEX.
|
||||
unsafe {
|
||||
std::env::remove_var("EMBEDDING_BASE_URL");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn embedding_base_url_defaults_to_none() {
|
||||
let _guard = ENV_MUTEX.lock().expect("env mutex poisoned");
|
||||
clear_embedding_env();
|
||||
|
||||
let settings = Settings::default();
|
||||
let config = EmbeddingsConfig::resolve(&settings).expect("resolve should succeed");
|
||||
assert!(
|
||||
config.openai_base_url.is_none(),
|
||||
"openai_base_url should be None when EMBEDDING_BASE_URL is not set"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,12 +60,18 @@ pub trait EmbeddingProvider: Send + Sync {
|
||||
}
|
||||
}
|
||||
|
||||
/// Default base URL for the OpenAI API.
|
||||
const OPENAI_API_BASE_URL: &str = "https://api.openai.com";
|
||||
|
||||
/// OpenAI embedding provider using text-embedding-ada-002 or text-embedding-3-small.
|
||||
///
|
||||
/// Supports any OpenAI-compatible embedding endpoint via [`with_base_url`](Self::with_base_url).
|
||||
pub struct OpenAiEmbeddings {
|
||||
client: reqwest::Client,
|
||||
api_key: String,
|
||||
model: String,
|
||||
dimension: usize,
|
||||
base_url: String,
|
||||
}
|
||||
|
||||
impl OpenAiEmbeddings {
|
||||
@@ -78,6 +84,7 @@ impl OpenAiEmbeddings {
|
||||
api_key: api_key.into(),
|
||||
model: "text-embedding-3-small".to_string(),
|
||||
dimension: 1536,
|
||||
base_url: OPENAI_API_BASE_URL.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,6 +95,7 @@ impl OpenAiEmbeddings {
|
||||
api_key: api_key.into(),
|
||||
model: "text-embedding-ada-002".to_string(),
|
||||
dimension: 1536,
|
||||
base_url: OPENAI_API_BASE_URL.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,6 +106,7 @@ impl OpenAiEmbeddings {
|
||||
api_key: api_key.into(),
|
||||
model: "text-embedding-3-large".to_string(),
|
||||
dimension: 3072,
|
||||
base_url: OPENAI_API_BASE_URL.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,8 +121,35 @@ impl OpenAiEmbeddings {
|
||||
api_key: api_key.into(),
|
||||
model: model.into(),
|
||||
dimension,
|
||||
base_url: OPENAI_API_BASE_URL.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set a custom base URL for OpenAI-compatible embedding providers.
|
||||
///
|
||||
/// The URL must use `http://` or `https://` scheme. If no scheme is present,
|
||||
/// `https://` is prepended automatically. Trailing slashes are stripped.
|
||||
pub fn with_base_url(mut self, base_url: &str) -> Self {
|
||||
let url = base_url.trim();
|
||||
|
||||
// Auto-prepend https:// if no scheme is present.
|
||||
let mut url = if !url.starts_with("http://") && !url.starts_with("https://") {
|
||||
tracing::debug!(
|
||||
"No scheme in embedding base URL '{}', prepending https://",
|
||||
url
|
||||
);
|
||||
format!("https://{url}")
|
||||
} else {
|
||||
url.to_string()
|
||||
};
|
||||
|
||||
while url.ends_with('/') {
|
||||
url.pop();
|
||||
}
|
||||
|
||||
self.base_url = url;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
@@ -173,9 +209,11 @@ impl EmbeddingProvider for OpenAiEmbeddings {
|
||||
input: texts,
|
||||
};
|
||||
|
||||
let url = format!("{}/v1/embeddings", self.base_url);
|
||||
|
||||
let response = self
|
||||
.client
|
||||
.post("https://api.openai.com/v1/embeddings")
|
||||
.post(&url)
|
||||
.header("Authorization", format!("Bearer {}", self.api_key))
|
||||
.json(&request)
|
||||
.send()
|
||||
@@ -575,9 +613,37 @@ mod tests {
|
||||
let provider = OpenAiEmbeddings::new("test-key");
|
||||
assert_eq!(provider.dimension(), 1536);
|
||||
assert_eq!(provider.model_name(), "text-embedding-3-small");
|
||||
assert_eq!(provider.base_url, OPENAI_API_BASE_URL);
|
||||
|
||||
let provider = OpenAiEmbeddings::large("test-key");
|
||||
assert_eq!(provider.dimension(), 3072);
|
||||
assert_eq!(provider.model_name(), "text-embedding-3-large");
|
||||
assert_eq!(provider.base_url, OPENAI_API_BASE_URL);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_openai_with_base_url_valid() {
|
||||
let provider =
|
||||
OpenAiEmbeddings::new("test-key").with_base_url("https://custom.example.com");
|
||||
assert_eq!(provider.base_url, "https://custom.example.com");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_openai_with_base_url_strips_trailing_slashes() {
|
||||
let provider =
|
||||
OpenAiEmbeddings::new("test-key").with_base_url("https://custom.example.com///");
|
||||
assert_eq!(provider.base_url, "https://custom.example.com");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_openai_with_base_url_http_scheme() {
|
||||
let provider = OpenAiEmbeddings::new("test-key").with_base_url("http://localhost:8080");
|
||||
assert_eq!(provider.base_url, "http://localhost:8080");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_openai_with_base_url_schemeless_prepends_https() {
|
||||
let provider = OpenAiEmbeddings::new("test-key").with_base_url("custom.example.com/v1");
|
||||
assert_eq!(provider.base_url, "https://custom.example.com/v1");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user