fix(providers): fall back to configured models when models endpoint fetch fails (#7530)

Signed-off-by: Pete Gonzalez <4673363+octogonz@users.noreply.github.com>
Signed-off-by: Douwe Osinga <douwe@squareup.com>
Co-authored-by: Pete Gonzalez <octogonz@users.noreply.github.com>
Co-authored-by: Douwe Osinga <douwe@squareup.com>
This commit is contained in:
Pete Gonzalez
2026-03-26 12:49:37 -07:00
committed by GitHub
parent 4a02379731
commit 27e197e467
4 changed files with 139 additions and 43 deletions
+59 -20
View File
@@ -55,6 +55,7 @@ pub struct AnthropicProvider {
model: ModelConfig,
supports_streaming: bool,
name: String,
custom_models: Option<Vec<String>>,
}
impl AnthropicProvider {
@@ -80,6 +81,7 @@ impl AnthropicProvider {
model,
supports_streaming: true,
name: ANTHROPIC_PROVIDER_NAME.to_string(),
custom_models: None,
})
}
@@ -119,11 +121,18 @@ impl AnthropicProvider {
));
}
let custom_models = if !config.models.is_empty() {
Some(config.models.iter().map(|m| m.name.clone()).collect())
} else {
None
};
Ok(Self {
api_client,
model,
supports_streaming,
name: config.name.clone(),
custom_models,
})
}
@@ -139,6 +148,42 @@ impl AnthropicProvider {
headers
}
async fn fetch_models_from_api(&self) -> Result<Vec<String>, ProviderError> {
let response = self.api_client.request(None, "v1/models").api_get().await?;
if response.status == StatusCode::NOT_FOUND {
let msg = response
.payload
.as_ref()
.and_then(|p| p.get("error").and_then(|e| e.get("message")))
.and_then(|m| m.as_str())
.unwrap_or("models endpoint not found")
.to_string();
return Err(ProviderError::EndpointNotFound(msg));
}
if response.status != StatusCode::OK {
return Err(map_http_error_to_provider_error(
response.status,
response.payload,
));
}
let json = response.payload.unwrap_or_default();
let arr = json.get("data").and_then(|v| v.as_array()).ok_or_else(|| {
ProviderError::RequestFailed(
"Missing 'data' array in Anthropic models response".to_string(),
)
})?;
let mut models: Vec<String> = arr
.iter()
.filter_map(|m| m.get("id").and_then(|v| v.as_str()).map(str::to_string))
.collect();
models.sort();
Ok(models)
}
}
impl ProviderDef for AnthropicProvider {
@@ -194,28 +239,22 @@ impl Provider for AnthropicProvider {
}
async fn fetch_supported_models(&self) -> Result<Vec<String>, ProviderError> {
let response = self.api_client.request(None, "v1/models").api_get().await?;
if response.status != StatusCode::OK {
return Err(map_http_error_to_provider_error(
response.status,
response.payload,
));
if let Some(custom_models) = &self.custom_models {
match self.fetch_models_from_api().await {
Ok(models) => return Ok(models),
Err(e) if e.is_endpoint_not_found() => {
tracing::debug!(
"Models endpoint not implemented for provider '{}' ({}), using predefined list",
self.name,
e
);
return Ok(custom_models.clone());
}
Err(e) => return Err(e),
}
}
let json = response.payload.unwrap_or_default();
let arr = json.get("data").and_then(|v| v.as_array()).ok_or_else(|| {
ProviderError::RequestFailed(
"Missing 'data' array in Anthropic models response".to_string(),
)
})?;
let mut models: Vec<String> = arr
.iter()
.filter_map(|m| m.get("id").and_then(|v| v.as_str()).map(str::to_string))
.collect();
models.sort();
Ok(models)
self.fetch_models_from_api().await
}
async fn stream(
+8
View File
@@ -34,6 +34,9 @@ pub enum ProviderError {
#[error("Unsupported operation: {0}")]
NotImplemented(String),
#[error("Endpoint not found (404): {0}")]
EndpointNotFound(String),
#[error("Credits exhausted: {details}")]
CreditsExhausted {
details: String,
@@ -53,9 +56,14 @@ impl ProviderError {
ProviderError::ExecutionError(_) => "execution",
ProviderError::UsageError(_) => "usage",
ProviderError::NotImplemented(_) => "not_implemented",
ProviderError::EndpointNotFound(_) => "endpoint_not_found",
ProviderError::CreditsExhausted { .. } => "credits_exhausted",
}
}
pub fn is_endpoint_not_found(&self) -> bool {
matches!(self, ProviderError::EndpointNotFound(_))
}
}
fn is_network_error(err: &reqwest::Error) -> bool {
+59 -23
View File
@@ -65,6 +65,7 @@ pub struct OpenAiProvider {
custom_headers: Option<HashMap<String, String>>,
supports_streaming: bool,
name: String,
custom_models: Option<Vec<String>>,
skip_canonical_filtering: bool,
}
@@ -127,6 +128,7 @@ impl OpenAiProvider {
custom_headers,
supports_streaming: true,
name: OPEN_AI_PROVIDER_NAME.to_string(),
custom_models: None,
skip_canonical_filtering: false,
})
}
@@ -142,6 +144,7 @@ impl OpenAiProvider {
custom_headers: None,
supports_streaming: true,
name: OPEN_AI_PROVIDER_NAME.to_string(),
custom_models: None,
skip_canonical_filtering: false,
}
}
@@ -212,6 +215,12 @@ impl OpenAiProvider {
api_client = api_client.with_headers(header_map)?;
}
let custom_models = if !config.models.is_empty() {
Some(config.models.iter().map(|m| m.name.clone()).collect())
} else {
None
};
Ok(Self {
api_client,
base_path,
@@ -221,6 +230,7 @@ impl OpenAiProvider {
custom_headers: config.headers,
supports_streaming: config.supports_streaming.unwrap_or(true),
name: config.name.clone(),
custom_models,
skip_canonical_filtering: config.skip_canonical_filtering,
})
}
@@ -314,6 +324,40 @@ impl OpenAiProvider {
fallback.to_string()
}
}
async fn fetch_models_from_api(&self) -> Result<Vec<String>, ProviderError> {
let models_path =
Self::map_base_path(&self.base_path, "models", OPEN_AI_DEFAULT_MODELS_PATH);
let response = self
.api_client
.request(None, &models_path)
.response_get()
.await?;
if response.status() == StatusCode::NOT_FOUND {
let body = response.text().await.unwrap_or_default();
return Err(ProviderError::EndpointNotFound(body));
}
let json = handle_response_openai_compat(response).await?;
if let Some(err_obj) = json.get("error") {
let msg = err_obj
.get("message")
.and_then(|v| v.as_str())
.unwrap_or("unknown error");
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)
}
}
impl ProviderDef for OpenAiProvider {
@@ -384,31 +428,22 @@ impl Provider for OpenAiProvider {
}
async fn fetch_supported_models(&self) -> Result<Vec<String>, ProviderError> {
let models_path =
Self::map_base_path(&self.base_path, "models", OPEN_AI_DEFAULT_MODELS_PATH);
let response = self
.api_client
.request(None, &models_path)
.response_get()
.await?;
let json = handle_response_openai_compat(response).await?;
if let Some(err_obj) = json.get("error") {
let msg = err_obj
.get("message")
.and_then(|v| v.as_str())
.unwrap_or("unknown error");
return Err(ProviderError::Authentication(msg.to_string()));
if let Some(custom_models) = &self.custom_models {
match self.fetch_models_from_api().await {
Ok(models) => return Ok(models),
Err(e) if e.is_endpoint_not_found() => {
tracing::debug!(
"Models endpoint not implemented for provider '{}' ({}), using predefined list",
self.name,
e
);
return Ok(custom_models.clone());
}
Err(e) => return Err(e),
}
}
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)
self.fetch_models_from_api().await
}
fn supports_embeddings(&self) -> bool {
@@ -635,6 +670,7 @@ mod tests {
custom_headers: None,
supports_streaming: true,
name: name.to_string(),
custom_models: None,
skip_canonical_filtering: false,
}
}
@@ -299,6 +299,18 @@ mod tests {
"ServerError"
; "500 server error"
)]
#[test_case(
StatusCode::NOT_FOUND,
None,
"RequestFailed"
; "404 not found"
)]
#[test_case(
StatusCode::NOT_FOUND,
Some(json!({"error": {"message": "model not available"}})),
"RequestFailed"
; "404 with error payload"
)]
fn http_status_maps_to_expected_error(
status: StatusCode,
payload: Option<Value>,
@@ -312,6 +324,7 @@ mod tests {
"Authentication" => "auth",
"ContextLengthExceeded" => "context_length",
"ServerError" => "server",
"RequestFailed" => "request",
other => panic!("Unknown variant: {other}"),
};
assert_eq!(