diff --git a/crates/goose-server/src/routes/providers_and_keys.json b/crates/goose-server/src/routes/providers_and_keys.json index 34589cc08..fb344f2d8 100644 --- a/crates/goose-server/src/routes/providers_and_keys.json +++ b/crates/goose-server/src/routes/providers_and_keys.json @@ -3,7 +3,7 @@ "name": "OpenAI", "description": "Use GPT-4 and other OpenAI models", "models": ["gpt-4o", "gpt-4-turbo","o1"], - "required_keys": ["OPENAI_API_KEY", "OPENAI_HOST"] + "required_keys": ["OPENAI_API_KEY", "OPENAI_HOST", "OPENAI_BASE_PATH"] }, "anthropic": { "name": "Anthropic", diff --git a/crates/goose/src/providers/openai.rs b/crates/goose/src/providers/openai.rs index 4c4716669..568f72b98 100644 --- a/crates/goose/src/providers/openai.rs +++ b/crates/goose/src/providers/openai.rs @@ -28,6 +28,7 @@ pub struct OpenAiProvider { #[serde(skip)] client: Client, host: String, + base_path: String, api_key: String, organization: Option, project: Option, @@ -48,6 +49,9 @@ impl OpenAiProvider { let host: String = config .get("OPENAI_HOST") .unwrap_or_else(|_| "https://api.openai.com".to_string()); + let base_path: String = config + .get("OPENAI_BASE_PATH") + .unwrap_or_else(|_| "v1/chat/completions".to_string()); let organization: Option = config.get("OPENAI_ORGANIZATION").ok(); let project: Option = config.get("OPENAI_PROJECT").ok(); let client = Client::builder() @@ -57,6 +61,7 @@ impl OpenAiProvider { Ok(Self { client, host, + base_path, api_key, organization, project, @@ -67,7 +72,7 @@ impl OpenAiProvider { async fn post(&self, payload: Value) -> Result { let base_url = url::Url::parse(&self.host) .map_err(|e| ProviderError::RequestFailed(format!("Invalid base URL: {e}")))?; - let url = base_url.join("v1/chat/completions").map_err(|e| { + let url = base_url.join(&self.base_path).map_err(|e| { ProviderError::RequestFailed(format!("Failed to construct endpoint URL: {e}")) })?; @@ -108,6 +113,7 @@ impl Provider for OpenAiProvider { vec![ ConfigKey::new("OPENAI_API_KEY", true, true, None), ConfigKey::new("OPENAI_HOST", true, false, Some("https://api.openai.com")), + ConfigKey::new("OPENAI_BASE_PATH", true, false, Some("v1/chat/completions")), ConfigKey::new("OPENAI_ORGANIZATION", false, false, None), ConfigKey::new("OPENAI_PROJECT", false, false, None), ], diff --git a/ui/desktop/src/components/settings/api_keys/utils.tsx b/ui/desktop/src/components/settings/api_keys/utils.tsx index 976984665..a22ab00d9 100644 --- a/ui/desktop/src/components/settings/api_keys/utils.tsx +++ b/ui/desktop/src/components/settings/api_keys/utils.tsx @@ -8,6 +8,7 @@ export function isSecretKey(keyName: string): boolean { 'DATABRICKS_HOST', 'OLLAMA_HOST', 'OPENAI_HOST', + 'OPENAI_BASE_PATH', 'AZURE_OPENAI_ENDPOINT', 'AZURE_OPENAI_DEPLOYMENT_NAME', ]; @@ -24,22 +25,22 @@ export async function getActiveProviders(): Promise { const configSettings = await getConfigSettings(); const activeProviders = Object.values(configSettings) - .filter((provider) => { - // 1. Get provider's config_status - const configStatus = provider.config_status ?? {}; + .filter((provider) => { + // 1. Get provider's config_status + const configStatus = provider.config_status ?? {}; - // 2. Collect only the keys *not* in default_key_value - const requiredKeyEntries = Object.entries(configStatus).filter(([k]) => isRequiredKey(k)); + // 2. Collect only the keys *not* in default_key_value + const requiredKeyEntries = Object.entries(configStatus).filter(([k]) => isRequiredKey(k)); - // 3. If there are *no* non-default keys, it is NOT active - if (requiredKeyEntries.length === 0) { - return false; - } + // 3. If there are *no* non-default keys, it is NOT active + if (requiredKeyEntries.length === 0) { + return false; + } - // 4. Otherwise, all non-default keys must be `is_set` - return requiredKeyEntries.every(([_, value]) => value?.is_set); - }) - .map((provider) => provider.name || 'Unknown Provider'); + // 4. Otherwise, all non-default keys must be `is_set` + return requiredKeyEntries.every(([_, value]) => value?.is_set); + }) + .map((provider) => provider.name || 'Unknown Provider'); console.log('[GET ACTIVE PROVIDERS]:', activeProviders); return activeProviders; @@ -93,4 +94,4 @@ export async function getProvidersList(): Promise { models: item.details?.models || [], // Nested models array requiredKeys: item.details?.required_keys || [], // Nested required keys array })); -} \ No newline at end of file +} diff --git a/ui/desktop/src/components/settings/models/hardcoded_stuff.tsx b/ui/desktop/src/components/settings/models/hardcoded_stuff.tsx index b38840153..3fe4066b1 100644 --- a/ui/desktop/src/components/settings/models/hardcoded_stuff.tsx +++ b/ui/desktop/src/components/settings/models/hardcoded_stuff.tsx @@ -65,7 +65,7 @@ export function getDefaultModel(key: string): string | undefined { export const short_list = ['gpt-4o', 'claude-3-5-sonnet-latest']; export const required_keys = { - OpenAI: ['OPENAI_API_KEY', 'OPENAI_HOST'], + OpenAI: ['OPENAI_API_KEY', 'OPENAI_HOST', 'OPENAI_BASE_PATH'], Anthropic: ['ANTHROPIC_API_KEY'], Databricks: ['DATABRICKS_HOST'], Groq: ['GROQ_API_KEY'], @@ -77,6 +77,7 @@ export const required_keys = { export const default_key_value = { OPENAI_HOST: 'https://api.openai.com', + OPENAI_BASE_PATH: 'v1/chat/completions', OLLAMA_HOST: 'localhost', };