Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
8c094ae
Merge pull request #830 from nearai/staging-promote/3a2989d0-22888378864
henrypark133 Mar 10, 2026
a677b20
chore: promote staging to main (2026-03-10 15:19 UTC) (#865)
ironclaw-ci[bot] Mar 11, 2026
6116c88
merge: resolve main into staging-promote (ChannelSecretUpdater import)
henrypark133 Mar 11, 2026
7a9396f
Merge pull request #904 from nearai/staging-promote/3a841b30-22928320566
henrypark133 Mar 11, 2026
6aae1f8
Merge pull request #907 from nearai/staging-promote/b0214fef-22930316561
henrypark133 Mar 11, 2026
6a1301b
feat(i18n): Add internationalization support with Chinese and English…
ironclaw-ci[bot] Mar 11, 2026
7e8c0fb
chore: release v0.18.0 (#885)
github-actions[bot] Mar 11, 2026
edca67e
Merge pull request #912 from nearai/staging-promote/55b5a462-22934480277
henrypark133 Mar 11, 2026
8391415
chore: update WASM artifact SHA256 checksums [skip ci] (#954)
github-actions[bot] Mar 11, 2026
ffbc0cd
Merge pull request #962 from nearai/staging-promote/d313f44a-22974575035
henrypark133 Mar 11, 2026
696d6a0
Merge pull request #957 from nearai/staging-promote/34550add-22970193833
henrypark133 Mar 11, 2026
99dadcb
Merge pull request #925 from nearai/staging-promote/8f513428-22941325130
henrypark133 Mar 11, 2026
d7024f5
Merge pull request #917 from nearai/staging-promote/369741fc-22935740447
henrypark133 Mar 11, 2026
8c2131d
feat(embeddings): add EMBEDDING_BASE_URL for OpenAI-compatible embedd…
smkrv Mar 12, 2026
8abb045
chore: resolve merge conflicts for main → staging sync
henrypark133 Mar 13, 2026
0bedd70
Merge branch 'staging' into merge/main-into-staging-resolved
henrypark133 Mar 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion registry/channels/discord.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
},
Expand Down
2 changes: 1 addition & 1 deletion registry/channels/slack.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
},
Expand Down
2 changes: 1 addition & 1 deletion registry/channels/telegram.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
},
"artifacts": {
"wasm32-wasip2": {
"url": "https://github.com/nearai/ironclaw/releases/latest/download/telegram-0.2.3-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"
}
Comment on lines 19 to 23
},
Expand Down
2 changes: 1 addition & 1 deletion registry/channels/whatsapp.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
},
Expand Down
4 changes: 2 additions & 2 deletions registry/tools/github.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand All @@ -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"
}
Comment on lines 20 to 24
},
Expand Down
2 changes: 1 addition & 1 deletion registry/tools/gmail.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
},
Expand Down
2 changes: 1 addition & 1 deletion registry/tools/google-calendar.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
},
Expand Down
2 changes: 1 addition & 1 deletion registry/tools/google-docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
},
Expand Down
2 changes: 1 addition & 1 deletion registry/tools/google-drive.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
},
Expand Down
2 changes: 1 addition & 1 deletion registry/tools/google-sheets.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
},
Expand Down
2 changes: 1 addition & 1 deletion registry/tools/google-slides.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
},
Expand Down
4 changes: 2 additions & 2 deletions registry/tools/slack.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Comment on lines 18 to 22
},
"auth_summary": {
Expand Down
4 changes: 2 additions & 2 deletions registry/tools/telegram.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Comment on lines 19 to 23
},
"auth_summary": {
Expand Down
2 changes: 1 addition & 1 deletion registry/tools/web-search.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
},
Expand Down
70 changes: 63 additions & 7 deletions src/config/embeddings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -36,6 +39,7 @@ impl Default for EmbeddingsConfig {
model,
ollama_base_url: "http://localhost:11434".to_string(),
dimension,
openai_base_url: None,
}
}
}
Expand Down Expand Up @@ -74,13 +78,16 @@ 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,
openai_api_key,
model,
ollama_base_url,
dimension,
openai_base_url,
})
}

Expand Down Expand Up @@ -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,
);
Comment on lines +146 to +151
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
Expand All @@ -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");
}
}

Expand Down Expand Up @@ -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"
);
}
}
68 changes: 67 additions & 1 deletion src/workspace/embeddings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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(),
}
}

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

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

Expand All @@ -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
Comment on lines +138 to +139
);
format!("https://{url}")
} else {
url.to_string()
};
Comment on lines +128 to +144

while url.ends_with('/') {
url.pop();
}

self.base_url = url;
self
}
}

#[derive(Debug, Serialize)]
Expand Down Expand Up @@ -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)
Comment on lines +212 to +216
.header("Authorization", format!("Bearer {}", self.api_key))
.json(&request)
.send()
Expand Down Expand Up @@ -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");
}
}
Loading