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:
Henry Park
2026-03-12 22:49:25 -07:00
committed by GitHub
21 changed files with 156 additions and 29 deletions
@@ -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
+1
View File
@@ -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
+1
View File
@@ -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/
+1 -1
View File
@@ -121,7 +121,7 @@ dependencies = [
[[package]]
name = "discord-channel"
version = "0.1.0"
version = "0.2.0"
dependencies = [
"ed25519-dalek",
"hex",
+1
View File
@@ -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);
+1 -1
View File
@@ -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"
}
},
+1 -1
View File
@@ -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 -2
View File
@@ -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"
}
},
+1 -1
View File
@@ -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 -2
View File
@@ -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"
}
},
+1 -1
View File
@@ -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"
}
},
+1 -1
View File
@@ -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"
}
},
+1 -1
View File
@@ -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"
}
},
+1 -1
View File
@@ -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"
}
},
+1 -1
View File
@@ -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"
}
},
+1 -1
View File
@@ -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"
}
},
+2 -2
View File
@@ -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": {
+2 -2
View File
@@ -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": {
+1 -1
View File
@@ -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"
}
},
+63 -7
View File
@@ -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"
);
}
}
+67 -1
View File
@@ -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");
}
}