diff --git a/crates/goose-providers/src/openai.rs b/crates/goose-providers/src/openai.rs index ac6dd4f0f81e..249c683a6668 100644 --- a/crates/goose-providers/src/openai.rs +++ b/crates/goose-providers/src/openai.rs @@ -468,15 +468,7 @@ impl OpenAiProvider { return Err(ProviderError::Authentication(msg.to_string())); } - let data = json.get("data").and_then(|v| v.as_array()).ok_or_else(|| { - ProviderError::UsageError("Missing data field in JSON response".into()) - })?; - let mut models: Vec = data - .iter() - .filter_map(|m| m.get("id").and_then(|v| v.as_str()).map(str::to_string)) - .collect(); - models.sort(); - Ok(models) + parse_model_ids(&json) } /// llama.cpp and Ollama expose the actual allocated context window in the @@ -496,6 +488,22 @@ impl OpenAiProvider { } } +fn parse_model_ids(json: &serde_json::Value) -> Result, ProviderError> { + let models = json + .get("data") + .and_then(|value| value.as_array()) + .or_else(|| json.as_array()) + .ok_or_else(|| { + ProviderError::RequestFailed("Missing models array in JSON response".into()) + })?; + let mut model_ids: Vec = models + .iter() + .filter_map(|m| m.get("id").and_then(|v| v.as_str()).map(str::to_string)) + .collect(); + model_ids.sort(); + Ok(model_ids) +} + /// Extract `meta.n_ctx` for `model_name` from a `/v1/models` response body. fn parse_n_ctx_from_models(json: &serde_json::Value, model_name: &str) -> Option { let data = json.get("data")?.as_array()?; @@ -1152,6 +1160,36 @@ mod tests { assert_eq!(models_path, "openai/v1/models"); } + #[test] + fn parse_model_ids_accepts_openai_response() { + let response = json!({"data": [{"id": "model-b"}, {"id": "model-a"}]}); + + assert_eq!(parse_model_ids(&response).unwrap(), ["model-a", "model-b"]); + } + + #[test] + fn parse_model_ids_accepts_together_response() { + let response = json!([ + {"id": "meta-llama/Llama-3.3-70B-Instruct-Turbo", "type": "chat"}, + {"id": "Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8", "type": "code"} + ]); + + assert_eq!( + parse_model_ids(&response).unwrap(), + [ + "Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8", + "meta-llama/Llama-3.3-70B-Instruct-Turbo" + ] + ); + } + + #[test] + fn parse_model_ids_rejects_unknown_response() { + let response = json!({"models": []}); + + assert!(parse_model_ids(&response).is_err()); + } + #[test] fn unknown_path_falls_back_to_default_models_path() { let models_path = OpenAiProvider::map_base_path("custom/path", "models", "v1/models");