From 400b3cfaa269ac115fa11a0cb69613f867131b8e Mon Sep 17 00:00:00 2001 From: rock Date: Tue, 15 Sep 2026 17:35:55 +0900 Subject: [PATCH] fix: double /v1 in embeddings URL + wrong auth header Bug 1: LLM_API_BASE=https://api.riotpiao.com/v1 + code appends /v1/embeddings = https://api.riotpiao.com/v1/v1/embeddings (404). Fix: strip trailing /v1 from base URL in from_env(). Bug 2: embeddings client sent 'apikey' custom header, but gateway expects 'Authorization: Bearer '. Fix: use Authorization Bearer header. Caused: 'expected ident at line 1 column 2' error on /memory/query (gateway returned HTML/text error, client tried to parse as JSON). --- crates/mem-llm/src/embeddings.rs | 35 ++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/crates/mem-llm/src/embeddings.rs b/crates/mem-llm/src/embeddings.rs index fa17741..0329ea6 100644 --- a/crates/mem-llm/src/embeddings.rs +++ b/crates/mem-llm/src/embeddings.rs @@ -64,9 +64,15 @@ impl EmbeddingsClient { /// - `LLM_API_BASE`: Gateway endpoint (default: https://api.riotpiao.com) /// - `LLM_API_KEY`: API key (optional) pub fn from_env() -> Result { - let base_url = env::var("LLM_API_BASE") + let mut base_url = env::var("LLM_API_BASE") .unwrap_or_else(|_| "https://api.riotpiao.com".to_string()); + // Strip trailing /v1 to avoid double /v1/v1/embeddings + base_url = base_url.trim_end_matches('/').to_string(); + if base_url.ends_with("/v1") { + base_url = base_url[..base_url.len() - 3].to_string(); + } + let model = env::var("EMBEDDINGS_MODEL") .unwrap_or_else(|_| "nomic-ai/nomic-embed-text-v2-moe".to_string()); @@ -161,10 +167,9 @@ impl EmbeddingsClient { let url = format!("{}/v1/embeddings", self.base_url); let mut builder = self.http.post(&url); - // Send apikey header even though route currently doesn't require auth - // This future-proofs for when the route's auth plugin gets enabled + // Send as Bearer token (gateway expects Authorization: Bearer ) if !self.api_key.is_empty() { - builder = builder.header("apikey", &self.api_key); + builder = builder.header("Authorization", format!("Bearer {}", &self.api_key)); } let resp = builder.json(&req).send().await?; @@ -215,6 +220,28 @@ mod tests { assert_eq!(EMBEDDINGS_DIM, 768); } + #[test] + fn test_strip_trailing_v1() { + // Simulates LLM_API_BASE=https://api.riotpiao.com/v1 + let mut base = "https://api.riotpiao.com/v1".to_string(); + base = base.trim_end_matches('/').to_string(); + if base.ends_with("/v1") { + base = base[..base.len() - 3].to_string(); + } + assert_eq!(base, "https://api.riotpiao.com"); + assert_eq!(format!("{}/v1/embeddings", base), "https://api.riotpiao.com/v1/embeddings"); + } + + #[test] + fn test_no_strip_when_no_v1() { + let mut base = "https://api.riotpiao.com".to_string(); + base = base.trim_end_matches('/').to_string(); + if base.ends_with("/v1") { + base = base[..base.len() - 3].to_string(); + } + assert_eq!(base, "https://api.riotpiao.com"); + } + #[test] fn test_parse_real_embedding_response() { // Exact format returned by embeddings-predictor service