Skip to content

Commit f7d96cf

Browse files
committed
Wrap provider model discovery failures in ModelDiscoveryError
1 parent 7850c3c commit f7d96cf

14 files changed

Lines changed: 333 additions & 40 deletions

File tree

‎src/providers/inference/azure/provider.py‎

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44

55
from typing import TYPE_CHECKING, Any
66

7+
from azure.core.exceptions import ClientAuthenticationError
78
from azure.identity.aio import ClientSecretCredential, get_bearer_token_provider
8-
from openai import AsyncAzureOpenAI
9+
from openai import APIError, AsyncAzureOpenAI
910
from pydantic_ai.embeddings import EmbeddingModel
1011
from pydantic_ai.embeddings.openai import OpenAIEmbeddingModel
1112
from pydantic_ai.models import Model
@@ -17,7 +18,7 @@
1718
from models.config import AzureInferenceProviderEntry
1819
from providers.inference.base import InferenceProvider
1920
from providers.inference.catalog import CatalogModel, DiscoveredModel
20-
from providers.inference.errors import ModelTypeNotSupportedError
21+
from providers.inference.errors import ModelDiscoveryError, ModelTypeNotSupportedError
2122
from providers.inference.model_type_inference import (
2223
infer_model_type_from_pai_known_names,
2324
)
@@ -71,8 +72,15 @@ async def _discover_models(self) -> list[DiscoveredModel]:
7172
Returns:
7273
Discovered models with inferred types, before allow-list filtering.
7374
75+
Raises:
76+
ModelDiscoveryError: If the Azure OpenAI models API or Entra ID
77+
authentication fails.
78+
7479
"""
75-
response = await self._get_provider.client.models.list()
80+
try:
81+
response = await self._get_provider.client.models.list()
82+
except (APIError, ClientAuthenticationError) as exc:
83+
raise ModelDiscoveryError(self.provider_id) from exc
7684
return [
7785
DiscoveredModel(
7886
provider_model_id=model.id,

‎src/providers/inference/bedrock/provider.py‎

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import boto3
1010
from botocore.client import BaseClient
1111
from botocore.config import Config
12+
from botocore.exceptions import BotoCoreError, ClientError
1213
from botocore.session import Session
1314
from botocore.tokens import FrozenAuthToken
1415
from pydantic_ai.embeddings import EmbeddingModel
@@ -27,7 +28,7 @@
2728
to_discovered_model,
2829
)
2930
from providers.inference.catalog import CatalogModel, DiscoveredModel
30-
from providers.inference.errors import ModelTypeNotSupportedError
31+
from providers.inference.errors import ModelDiscoveryError, ModelTypeNotSupportedError
3132

3233
if TYPE_CHECKING:
3334
from providers.inference.types import ModelType
@@ -120,10 +121,17 @@ async def _discover_models(self) -> list[DiscoveredModel]:
120121
Returns:
121122
Discovered models before allow-list filtering.
122123
124+
Raises:
125+
ModelDiscoveryError: If Bedrock control-plane listing or AWS
126+
credentials/transport fail.
127+
123128
"""
124-
client = self._create_bedrock_client()
125-
foundations = await asyncio.to_thread(list_foundation_models, client)
126-
profiles = await asyncio.to_thread(list_inference_profiles, client)
129+
try:
130+
client = self._create_bedrock_client()
131+
foundations = await asyncio.to_thread(list_foundation_models, client)
132+
profiles = await asyncio.to_thread(list_inference_profiles, client)
133+
except (ClientError, BotoCoreError) as exc:
134+
raise ModelDiscoveryError(self.provider_id) from exc
127135
profile_ids = first_profile_id_by_foundation_model_id(profiles)
128136

129137
discovered: list[DiscoveredModel] = []

‎src/providers/inference/errors.py‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,3 +70,17 @@ def __init__(self, model_type: ModelType) -> None:
7070
f"No {model_type!r} model is configured or available in the catalog"
7171
)
7272
self.model_type = model_type
73+
74+
75+
class ModelDiscoveryError(Exception):
76+
"""Raised when a provider cannot discover available models."""
77+
78+
def __init__(self, provider_id: ProviderId) -> None:
79+
"""Initialize with the provider whose model discovery failed.
80+
81+
Args:
82+
provider_id: Provider whose model discovery failed.
83+
84+
"""
85+
super().__init__(f"Failed to discover models for provider {provider_id!r}")
86+
self.provider_id = provider_id

‎src/providers/inference/openai/provider.py‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from typing import TYPE_CHECKING, Any
66

7+
from openai import APIError
78
from pydantic_ai.embeddings import EmbeddingModel
89
from pydantic_ai.embeddings.openai import OpenAIEmbeddingModel
910
from pydantic_ai.models import Model
@@ -14,7 +15,7 @@
1415
from models.config import OpenAIInferenceProviderEntry
1516
from providers.inference.base import InferenceProvider
1617
from providers.inference.catalog import CatalogModel, DiscoveredModel
17-
from providers.inference.errors import ModelTypeNotSupportedError
18+
from providers.inference.errors import ModelDiscoveryError, ModelTypeNotSupportedError
1819
from providers.inference.model_type_inference import (
1920
infer_model_type_from_pai_known_names,
2021
)
@@ -48,8 +49,14 @@ async def _discover_models(self) -> list[DiscoveredModel]:
4849
Returns:
4950
Discovered models with inferred types, before allow-list filtering.
5051
52+
Raises:
53+
ModelDiscoveryError: If the OpenAI models API fails.
54+
5155
"""
52-
response = await self._get_provider.client.models.list()
56+
try:
57+
response = await self._get_provider.client.models.list()
58+
except APIError as exc:
59+
raise ModelDiscoveryError(self.provider_id) from exc
5360
return [
5461
DiscoveredModel(
5562
provider_model_id=model.id,

‎src/providers/inference/vertexai/provider.py‎

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
from typing import TYPE_CHECKING, Any
66

7+
from google.auth.exceptions import GoogleAuthError
8+
from google.genai.errors import APIError as GoogleGenAIAPIError
79
from pydantic_ai.embeddings import EmbeddingModel
810
from pydantic_ai.embeddings.google import GoogleEmbeddingModel
911
from pydantic_ai.models import Model
@@ -14,7 +16,7 @@
1416
from models.config import VertexAIInferenceProviderEntry
1517
from providers.inference.base import InferenceProvider
1618
from providers.inference.catalog import CatalogModel, DiscoveredModel
17-
from providers.inference.errors import ModelTypeNotSupportedError
19+
from providers.inference.errors import ModelDiscoveryError, ModelTypeNotSupportedError
1820
from providers.inference.model_type_inference import (
1921
infer_model_type_from_pai_known_names,
2022
)
@@ -51,21 +53,27 @@ async def _discover_models(self) -> list[DiscoveredModel]:
5153
Returns:
5254
Discovered models with inferred types, before allow-list filtering.
5355
56+
Raises:
57+
ModelDiscoveryError: If the Vertex AI models API or Google auth fails.
58+
5459
"""
5560
discovered: list[DiscoveredModel] = []
56-
pager = await self._get_provider.client.aio.models.list()
57-
async for model in pager:
58-
if not model.name:
59-
continue
60-
discovered.append(
61-
DiscoveredModel(
62-
provider_model_id=model.name,
63-
model_type=self._infer_model_type(
64-
model.name,
65-
supported_actions=model.supported_actions,
66-
),
61+
try:
62+
pager = await self._get_provider.client.aio.models.list()
63+
async for model in pager:
64+
if not model.name:
65+
continue
66+
discovered.append(
67+
DiscoveredModel(
68+
provider_model_id=model.name,
69+
model_type=self._infer_model_type(
70+
model.name,
71+
supported_actions=model.supported_actions,
72+
),
73+
)
6774
)
68-
)
75+
except (GoogleGenAIAPIError, GoogleAuthError) as exc:
76+
raise ModelDiscoveryError(self.provider_id) from exc
6977
return discovered
7078

7179
def _create_llm(self, model: CatalogModel) -> Model:

‎src/providers/inference/vllm/provider.py‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from typing import Any
66

7+
from openai import APIError
78
from pydantic_ai.embeddings import EmbeddingModel
89
from pydantic_ai.embeddings.openai import OpenAIEmbeddingModel
910
from pydantic_ai.models import Model
@@ -14,7 +15,7 @@
1415
from models.config import VllmInferenceProviderEntry
1516
from providers.inference.base import InferenceProvider
1617
from providers.inference.catalog import CatalogModel, DiscoveredModel
17-
from providers.inference.errors import ModelTypeNotSupportedError
18+
from providers.inference.errors import ModelDiscoveryError, ModelTypeNotSupportedError
1819
from providers.inference.model_type_inference import (
1920
infer_model_type_from_pai_known_names,
2021
)
@@ -46,8 +47,14 @@ async def _discover_models(self) -> list[DiscoveredModel]:
4647
Returns:
4748
Discovered models with inferred types, before allow-list filtering.
4849
50+
Raises:
51+
ModelDiscoveryError: If the vLLM models API fails.
52+
4953
"""
50-
response = await self._get_provider.client.models.list()
54+
try:
55+
response = await self._get_provider.client.models.list()
56+
except APIError as exc:
57+
raise ModelDiscoveryError(self.provider_id) from exc
5158
return [
5259
DiscoveredModel(
5360
provider_model_id=model.id,

‎src/providers/inference/watsonx/provider.py‎

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import asyncio
66
from typing import TYPE_CHECKING, Any, Optional
77

8+
from openai import APIError
89
from pydantic_ai.embeddings import EmbeddingModel
910
from pydantic_ai.embeddings.openai import OpenAIEmbeddingModel
1011
from pydantic_ai.models import Model
@@ -14,7 +15,7 @@
1415
from models.config import WatsonxInferenceProviderEntry
1516
from providers.inference.base import InferenceProvider
1617
from providers.inference.catalog import CatalogModel, DiscoveredModel
17-
from providers.inference.errors import ModelTypeNotSupportedError
18+
from providers.inference.errors import ModelDiscoveryError, ModelTypeNotSupportedError
1819
from providers.inference.watsonx.pai_provider import WatsonxProvider
1920
from providers.inference.watsonx.specs import FoundationModelSpecsResponse
2021

@@ -49,15 +50,21 @@ async def _discover_models(self) -> list[DiscoveredModel]:
4950
Returns:
5051
Discovered models before allow-list filtering.
5152
53+
Raises:
54+
ModelDiscoveryError: If WatsonX models or foundation-spec APIs fail.
55+
5256
"""
5357
project_id = self._entry.config.project_id
5458
pai = self._get_provider
55-
models_response, functions_by_id = await asyncio.gather(
56-
pai.client.models.list(
57-
extra_query={"project_id": project_id} if project_id else None,
58-
),
59-
self._list_foundation_model_functions(),
60-
)
59+
try:
60+
models_response, functions_by_id = await asyncio.gather(
61+
pai.client.models.list(
62+
extra_query={"project_id": project_id} if project_id else None,
63+
),
64+
self._list_foundation_model_functions(),
65+
)
66+
except APIError as exc:
67+
raise ModelDiscoveryError(self.provider_id) from exc
6168

6269
discovered: list[DiscoveredModel] = []
6370
for model in models_response.data:

‎tests/unit/providers/inference/azure/test_provider.py‎

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44

55
from typing import Any
66

7+
import httpx
78
import pytest
8-
from openai import AsyncAzureOpenAI
9+
from azure.core.exceptions import ClientAuthenticationError
10+
from openai import APIConnectionError, AsyncAzureOpenAI
911
from pydantic import SecretStr
1012
from pydantic_ai.embeddings.openai import OpenAIEmbeddingModel
1113
from pydantic_ai.models.openai import OpenAIResponsesModel
@@ -14,7 +16,7 @@
1416
from models.config import AzureEntraIdConfiguration, AzureInferenceProviderEntry
1517
from providers.inference.azure.provider import AzureInferenceProvider
1618
from providers.inference.catalog import CatalogModel
17-
from providers.inference.errors import ModelTypeNotSupportedError
19+
from providers.inference.errors import ModelDiscoveryError, ModelTypeNotSupportedError
1820

1921
AZURE_ENDPOINT = "https://example.openai.azure.com/"
2022
API_VERSION = "2024-12-01-preview"
@@ -190,6 +192,38 @@ async def test_discover_models_infers_model_types(
190192
pai_provider.client.models.list.assert_awaited_once_with()
191193

192194

195+
@pytest.mark.asyncio
196+
@pytest.mark.parametrize(
197+
"cause",
198+
[
199+
APIConnectionError(
200+
message="connection failed",
201+
request=httpx.Request( # pyright: ignore[reportArgumentType]
202+
"GET", f"{AZURE_ENDPOINT}openai/models"
203+
),
204+
),
205+
ClientAuthenticationError(message="token acquisition failed"),
206+
],
207+
ids=["connection-error", "entra-auth-error"],
208+
)
209+
async def test_discover_models_wraps_foreign_failures(
210+
provider: AzureInferenceProvider,
211+
mocker: Any,
212+
cause: Exception,
213+
) -> None:
214+
"""OpenAI API subclasses and Entra auth failures become ModelDiscoveryError."""
215+
pai_provider = mocker.MagicMock(spec=AzureProvider)
216+
pai_provider.client.models.list = mocker.AsyncMock(side_effect=cause)
217+
provider._provider = pai_provider
218+
219+
with pytest.raises(ModelDiscoveryError) as exc_info:
220+
await provider._discover_models()
221+
222+
assert exc_info.value.provider_id == "azure"
223+
assert str(exc_info.value) == "Failed to discover models for provider 'azure'"
224+
assert exc_info.value.__cause__ is cause
225+
226+
193227
def test_create_llm_uses_openai_responses_model(
194228
provider: AzureInferenceProvider,
195229
) -> None:

‎tests/unit/providers/inference/bedrock/test_provider.py‎

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from typing import Any, Optional
66

77
import pytest
8+
from botocore.exceptions import ClientError, EndpointConnectionError
89
from pydantic_ai.embeddings.bedrock import BedrockEmbeddingModel
910
from pydantic_ai.models.bedrock import BedrockConverseModel
1011
from pydantic_ai.providers.bedrock import BedrockProvider
@@ -15,7 +16,7 @@
1516
_BearerTokenSession,
1617
)
1718
from providers.inference.catalog import CatalogModel
18-
from providers.inference.errors import ModelTypeNotSupportedError
19+
from providers.inference.errors import ModelDiscoveryError, ModelTypeNotSupportedError
1920

2021

2122
def _entry(**config: Any) -> BedrockInferenceProviderEntry:
@@ -259,6 +260,38 @@ async def test_discover_models_remaps_profile_only_and_skips_unusable(
259260
]
260261

261262

263+
@pytest.mark.asyncio
264+
@pytest.mark.parametrize(
265+
"cause",
266+
[
267+
ClientError(
268+
{"Error": {"Code": "AccessDeniedException", "Message": "denied"}},
269+
"ListFoundationModels",
270+
),
271+
EndpointConnectionError(endpoint_url="https://bedrock.example.test"),
272+
],
273+
ids=["client-error", "endpoint-connection-error"],
274+
)
275+
async def test_discover_models_wraps_foreign_failures(
276+
provider: BedrockInferenceProvider,
277+
mocker: Any,
278+
cause: Exception,
279+
) -> None:
280+
"""Bedrock service and transport subclasses become ModelDiscoveryError."""
281+
mocker.patch.object(
282+
provider,
283+
"_create_bedrock_client",
284+
side_effect=cause,
285+
)
286+
287+
with pytest.raises(ModelDiscoveryError) as exc_info:
288+
await provider._discover_models()
289+
290+
assert exc_info.value.provider_id == "bedrock"
291+
assert str(exc_info.value) == "Failed to discover models for provider 'bedrock'"
292+
assert exc_info.value.__cause__ is cause
293+
294+
262295
def test_create_llm_uses_bedrock_converse_model(
263296
provider: BedrockInferenceProvider,
264297
mocker: Any,

0 commit comments

Comments
 (0)