Skip to content
Merged
Changes from all commits
Commits
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
56 changes: 47 additions & 9 deletions crates/goose-providers/src/openai.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> = 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
Expand All @@ -496,6 +488,22 @@ impl OpenAiProvider {
}
}

fn parse_model_ids(json: &serde_json::Value) -> Result<Vec<String>, 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<String> = models
.iter()
.filter_map(|m| m.get("id").and_then(|v| v.as_str()).map(str::to_string))
.collect();
Comment on lines +499 to +502

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject top-level model arrays without ids

When a newly supported top-level array response is syntactically an array but its entries do not contain string id fields (for example [{}] or a provider schema change), this filter_map drops every item and returns Ok([]). For Together/custom OpenAI providers using dynamic_models, fetch_supported_models then accepts the empty API result and does not fall back to the static model list because fallback only happens on EndpointNotFound, so a malformed response is exposed as an empty supported-models list instead of a request error. Please fail when no model ids can be parsed from a non-empty models array.

Useful? React with 👍 / 👎.

Comment on lines +499 to +502

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Filter non-chat Together models from supported models

When Together's dynamic_models path starts using this top-level array parser, every returned id is exposed as a supported model even though Together's /models schema includes non-chat types such as image, embedding, moderation, and rerank. fetch_supported_models is used directly for the supported-model list, and this provider only sends selected models to chat/responses endpoints, so those non-LLM entries can be offered to users and then fail at runtime. Please filter the array to chat-compatible types before collecting ids for top-level Together-style responses.

Useful? React with 👍 / 👎.

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<usize> {
let data = json.get("data")?.as_array()?;
Expand Down Expand Up @@ -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");
Expand Down
Loading