Skip to content

Commit c4e0d31

Browse files
fix: resolve type errors in OpenRouter error response handling
1 parent 67dfea1 commit c4e0d31

File tree

2 files changed

+51
-12
lines changed

2 files changed

+51
-12
lines changed

pydantic_ai_slim/pydantic_ai/models/_openai_compat.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from collections.abc import AsyncIterable, AsyncIterator, Callable, Mapping
1010
from dataclasses import dataclass, field, replace
1111
from datetime import datetime
12-
from typing import Any, Literal, overload
12+
from typing import Any, Literal, cast, overload
1313

1414
from pydantic import ValidationError
1515
from typing_extensions import assert_never
@@ -27,7 +27,7 @@
2727
ToolCallPart,
2828
)
2929

30-
from .. import UnexpectedModelBehavior, _utils, usage
30+
from .. import ModelHTTPError, UnexpectedModelBehavior, _utils, usage
3131
from .._output import OutputObjectDefinition
3232
from .._thinking_part import split_content_into_text_and_thinking
3333
from .._utils import guard_tool_call_id as _guard_tool_call_id, now_utc as _now_utc, number_to_datetime
@@ -277,6 +277,18 @@ def process_response(
277277
if not isinstance(response, chat.ChatCompletion):
278278
raise UnexpectedModelBehavior('Invalid response from OpenAI chat completions endpoint, expected JSON data')
279279

280+
if hasattr(response, 'error'):
281+
error_attr = getattr(response, 'error', None)
282+
if error_attr and isinstance(error_attr, dict):
283+
error_dict = cast(dict[str, Any], error_attr)
284+
error_code = error_dict.get('code')
285+
status_code = error_code if isinstance(error_code, int) else 500
286+
raise ModelHTTPError(
287+
status_code=status_code,
288+
model_name=getattr(model, 'model_name', 'unknown'),
289+
body={'error': error_dict},
290+
)
291+
280292
if response.created:
281293
timestamp = number_to_datetime(response.created)
282294
else:

tests/models/test_openrouter.py

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import json
22
import os
33
from typing import Any, Literal, cast
4-
from unittest.mock import patch
4+
from unittest.mock import AsyncMock, MagicMock, patch
55

66
import pydantic_core
77
import pytest
88
from inline_snapshot import snapshot
99

10-
from pydantic_ai import Agent
10+
from pydantic_ai import Agent, ModelHTTPError
1111
from pydantic_ai.messages import (
1212
ImageUrl,
1313
ModelMessage,
@@ -19,6 +19,8 @@
1919
UserPromptPart,
2020
)
2121
from pydantic_ai.models import ModelRequestParameters
22+
from pydantic_ai.models.openrouter import OpenRouterModel
23+
from pydantic_ai.providers.openrouter import OpenRouterProvider
2224
from pydantic_ai.settings import ModelSettings
2325
from pydantic_ai.tools import ToolDefinition
2426

@@ -31,6 +33,7 @@
3133

3234
with try_import() as imports_successful:
3335
from openai.types.chat import (
36+
ChatCompletion,
3437
ChatCompletionChunk,
3538
ChatCompletionMessage,
3639
)
@@ -46,9 +49,6 @@
4649
)
4750
from openai.types.completion_usage import CompletionUsage
4851

49-
from pydantic_ai.models.openrouter import OpenRouterModel
50-
from pydantic_ai.providers.openrouter import OpenRouterProvider
51-
5252
def create_openrouter_model(model_name: str, mock_client: Any) -> OpenRouterModel:
5353
"""Helper to create OpenRouterModel with mock client using provider pattern."""
5454
provider = OpenRouterProvider(openai_client=mock_client)
@@ -93,8 +93,6 @@ def chunk(choices: list[ChunkChoice]) -> ChatCompletionChunk:
9393
def test_openrouter_model_init():
9494
c = completion_message(ChatCompletionMessage(content='test', role='assistant'))
9595
mock_client = MockOpenAI.create_mock(c)
96-
from pydantic_ai.providers.openrouter import OpenRouterProvider
97-
9896
provider = OpenRouterProvider(openai_client=mock_client)
9997
model = OpenRouterModel('google/gemini-2.5-flash-lite', provider=provider)
10098
assert model.model_name == 'google/gemini-2.5-flash-lite'
@@ -418,9 +416,6 @@ async def test_openrouter_with_reasoning_settings(allow_model_requests: None):
418416

419417
async def test_openrouter_model_custom_base_url(allow_model_requests: None):
420418
"""Test OpenRouterModel with provider."""
421-
# Test with provider using default base URL
422-
from pydantic_ai.providers.openrouter import OpenRouterProvider
423-
424419
provider = OpenRouterProvider(api_key='test-key')
425420
model = OpenRouterModel('openai/gpt-4o', provider=provider)
426421
assert model.model_name == 'openai/gpt-4o'
@@ -573,3 +568,35 @@ async def test_openrouter_user_prompt_mixed_content(allow_model_requests: None):
573568

574569
assert result['role'] == 'user'
575570
assert result['content'] == 'Hello, here is an image: and some more text.'
571+
572+
573+
async def test_openrouter_error_response_with_error_key(allow_model_requests: None):
574+
"""Test that OpenRouter error responses with 'error' key are properly handled.
575+
576+
Regression test for issue #2323 where OpenRouter returns HTTP 200 with an error
577+
object in the body (e.g., from upstream provider failures like Chutes).
578+
"""
579+
with patch('pydantic_ai.models.openrouter.OpenRouterProvider') as mock_provider_class:
580+
mock_provider = MagicMock()
581+
mock_provider.client = AsyncMock()
582+
mock_provider.model_profile = MagicMock(return_value=MagicMock())
583+
mock_provider_class.return_value = mock_provider
584+
585+
model = OpenRouterModel('deepseek/deepseek-chat-v3-0324:free')
586+
587+
error_response = MagicMock(spec=ChatCompletion)
588+
error_response.id = None
589+
error_response.choices = None
590+
error_response.created = None
591+
error_response.model = None
592+
error_response.object = None
593+
error_response.error = {'message': 'Internal Server Error', 'code': 500}
594+
595+
with patch('pydantic_ai.models.openrouter.completions_create', new_callable=AsyncMock) as mock_create:
596+
mock_create.return_value = error_response
597+
598+
messages: list[ModelMessage] = [ModelRequest(parts=[UserPromptPart(content='test')])]
599+
model_params = cast(ModelRequestParameters, {})
600+
601+
with pytest.raises(ModelHTTPError, match='status_code: 500.*Internal Server Error'):
602+
await model.request(messages, None, model_params)

0 commit comments

Comments
 (0)