From 7ec16e8defcae73b8c230a6a91cf7a8641acd0d7 Mon Sep 17 00:00:00 2001 From: Horst1993 Date: Wed, 22 Jul 2026 00:59:36 +0800 Subject: [PATCH] fix(agentbox): normalize GMI_MAAS_BASE_URL with trailing /v1 (#5091) Co-authored-by: Cursor --- .../__tests__/builtinCloudProviders.test.ts | 1 + .../settings/panels/builtinCloudProviders.ts | 2 +- src/openhuman/agentbox/env.rs | 13 +++++- src/openhuman/agentbox/env_tests.rs | 41 +++++++++++++++++-- src/openhuman/agentbox/status.rs | 2 +- 5 files changed, 53 insertions(+), 6 deletions(-) diff --git a/app/src/components/settings/panels/__tests__/builtinCloudProviders.test.ts b/app/src/components/settings/panels/__tests__/builtinCloudProviders.test.ts index 6fce51b1d..ec3f7dfe1 100644 --- a/app/src/components/settings/panels/__tests__/builtinCloudProviders.test.ts +++ b/app/src/components/settings/panels/__tests__/builtinCloudProviders.test.ts @@ -13,6 +13,7 @@ describe('builtinCloudProviders', () => { }); it.each([ + ['gmi', 'https://api.gmi-serving.com/v1', 'bearer'], ['groq', 'https://api.groq.com/openai/v1', 'bearer'], ['deepseek', 'https://api.deepseek.com/v1', 'bearer'], ['minimax', 'https://api.minimax.io/v1', 'bearer'], diff --git a/app/src/components/settings/panels/builtinCloudProviders.ts b/app/src/components/settings/panels/builtinCloudProviders.ts index 3fb212cb6..3f6b762db 100644 --- a/app/src/components/settings/panels/builtinCloudProviders.ts +++ b/app/src/components/settings/panels/builtinCloudProviders.ts @@ -64,7 +64,7 @@ export const BUILTIN_CLOUD_PROVIDERS: BuiltinCloudProvider[] = [ endpoint: 'https://api.gmi-serving.com/v1', authStyle: 'bearer', tone: TONE.fuchsia, - keyPlaceholder: 'gmi-...', + keyPlaceholder: 'eyJ....', }, { slug: 'fireworks', diff --git a/src/openhuman/agentbox/env.rs b/src/openhuman/agentbox/env.rs index c4ab78fd7..b9b79a555 100644 --- a/src/openhuman/agentbox/env.rs +++ b/src/openhuman/agentbox/env.rs @@ -79,12 +79,23 @@ where return Err(format!("missing/blank: {}", missing.join(", "))); } Ok(GmiConfig { - base_url: base_url.unwrap(), + base_url: normalize_gmi_maas_base_url(&base_url.unwrap()), api_key: api_key.unwrap(), model: model.unwrap(), }) } +/// Normalize `GMI_MAAS_BASE_URL` to an OpenAI-compatible `/v1` base URL. +/// +/// AgentBox injects a host-only URL (no `/v1`). `OpenAiModel` appends +/// `/chat/completions` but does not add `/v1`, so marketplace inference +/// 404s without this step. Idempotent for already-correct inputs. +pub fn normalize_gmi_maas_base_url(url: &str) -> String { + let trimmed = url.trim().trim_end_matches('/'); + let without_v1 = trimmed.strip_suffix("/v1").unwrap_or(trimmed); + format!("{}/v1", without_v1.trim_end_matches('/')) +} + fn nonblank Option>(get: &F, key: &str) -> Option { get(key) .map(|v| v.trim().to_string()) diff --git a/src/openhuman/agentbox/env_tests.rs b/src/openhuman/agentbox/env_tests.rs index 78d786a2a..e17753967 100644 --- a/src/openhuman/agentbox/env_tests.rs +++ b/src/openhuman/agentbox/env_tests.rs @@ -1,4 +1,7 @@ -use super::env::{agentbox_mode_enabled, collect_gmi_config, GmiConfig, AGENTBOX_MODE_ENV_VAR}; +use super::env::{ + agentbox_mode_enabled, collect_gmi_config, normalize_gmi_maas_base_url, GmiConfig, + AGENTBOX_MODE_ENV_VAR, +}; #[test] fn collect_returns_some_when_all_three_vars_present() { @@ -11,7 +14,7 @@ fn collect_returns_some_when_all_three_vars_present() { assert_eq!( cfg, Ok(GmiConfig { - base_url: "https://api.gmi-serving.com".into(), + base_url: "https://api.gmi-serving.com/v1".into(), api_key: "sk-test".into(), model: "deepseek-ai/DeepSeek-V4-Pro".into(), }) @@ -86,9 +89,41 @@ fn collect_trims_leading_and_trailing_whitespace() { assert_eq!( cfg, Ok(GmiConfig { - base_url: "https://api.gmi-serving.com".into(), + base_url: "https://api.gmi-serving.com/v1".into(), api_key: "sk-test".into(), model: "deepseek-ai/DeepSeek-V4-Pro".into(), }) ); } + +#[test] +fn normalize_gmi_maas_base_url_appends_v1() { + assert_eq!( + normalize_gmi_maas_base_url("https://api.gmi-serving.com"), + "https://api.gmi-serving.com/v1" + ); +} + +#[test] +fn normalize_gmi_maas_base_url_strips_trailing_slash() { + assert_eq!( + normalize_gmi_maas_base_url("https://api.gmi-serving.com/"), + "https://api.gmi-serving.com/v1" + ); +} + +#[test] +fn normalize_gmi_maas_base_url_keeps_existing_v1() { + assert_eq!( + normalize_gmi_maas_base_url("https://api.gmi-serving.com/v1"), + "https://api.gmi-serving.com/v1" + ); +} + +#[test] +fn normalize_gmi_maas_base_url_strips_v1_trailing_slash() { + assert_eq!( + normalize_gmi_maas_base_url("https://api.gmi-serving.com/v1/"), + "https://api.gmi-serving.com/v1" + ); +} diff --git a/src/openhuman/agentbox/status.rs b/src/openhuman/agentbox/status.rs index cd8e2120c..af98410b0 100644 --- a/src/openhuman/agentbox/status.rs +++ b/src/openhuman/agentbox/status.rs @@ -98,7 +98,7 @@ mod tests { assert!(status.provider_configured); let provider = status.provider.expect("provider populated"); assert_eq!(provider.slug, GMI_MAAS_SLUG); - assert_eq!(provider.base_url, "https://api.gmi-serving.com"); + assert_eq!(provider.base_url, "https://api.gmi-serving.com/v1"); assert_eq!(provider.model, "deepseek-ai/DeepSeek-V4-Pro"); // Defense-in-depth: the serialized status must never carry the key.