|
1 | 1 | import json |
2 | 2 | import os |
3 | 3 | from typing import Any, Literal, cast |
4 | | -from unittest.mock import patch |
| 4 | +from unittest.mock import AsyncMock, MagicMock, patch |
5 | 5 |
|
6 | 6 | import pydantic_core |
7 | 7 | import pytest |
8 | 8 | from inline_snapshot import snapshot |
9 | 9 |
|
10 | | -from pydantic_ai import Agent |
| 10 | +from pydantic_ai import Agent, ModelHTTPError |
11 | 11 | from pydantic_ai.messages import ( |
12 | 12 | ImageUrl, |
13 | 13 | ModelMessage, |
|
19 | 19 | UserPromptPart, |
20 | 20 | ) |
21 | 21 | from pydantic_ai.models import ModelRequestParameters |
| 22 | +from pydantic_ai.models.openrouter import OpenRouterModel |
| 23 | +from pydantic_ai.providers.openrouter import OpenRouterProvider |
22 | 24 | from pydantic_ai.settings import ModelSettings |
23 | 25 | from pydantic_ai.tools import ToolDefinition |
24 | 26 |
|
|
31 | 33 |
|
32 | 34 | with try_import() as imports_successful: |
33 | 35 | from openai.types.chat import ( |
| 36 | + ChatCompletion, |
34 | 37 | ChatCompletionChunk, |
35 | 38 | ChatCompletionMessage, |
36 | 39 | ) |
|
46 | 49 | ) |
47 | 50 | from openai.types.completion_usage import CompletionUsage |
48 | 51 |
|
49 | | - from pydantic_ai.models.openrouter import OpenRouterModel |
50 | | - from pydantic_ai.providers.openrouter import OpenRouterProvider |
51 | | - |
52 | 52 | def create_openrouter_model(model_name: str, mock_client: Any) -> OpenRouterModel: |
53 | 53 | """Helper to create OpenRouterModel with mock client using provider pattern.""" |
54 | 54 | provider = OpenRouterProvider(openai_client=mock_client) |
@@ -93,8 +93,6 @@ def chunk(choices: list[ChunkChoice]) -> ChatCompletionChunk: |
93 | 93 | def test_openrouter_model_init(): |
94 | 94 | c = completion_message(ChatCompletionMessage(content='test', role='assistant')) |
95 | 95 | mock_client = MockOpenAI.create_mock(c) |
96 | | - from pydantic_ai.providers.openrouter import OpenRouterProvider |
97 | | - |
98 | 96 | provider = OpenRouterProvider(openai_client=mock_client) |
99 | 97 | model = OpenRouterModel('google/gemini-2.5-flash-lite', provider=provider) |
100 | 98 | 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): |
418 | 416 |
|
419 | 417 | async def test_openrouter_model_custom_base_url(allow_model_requests: None): |
420 | 418 | """Test OpenRouterModel with provider.""" |
421 | | - # Test with provider using default base URL |
422 | | - from pydantic_ai.providers.openrouter import OpenRouterProvider |
423 | | - |
424 | 419 | provider = OpenRouterProvider(api_key='test-key') |
425 | 420 | model = OpenRouterModel('openai/gpt-4o', provider=provider) |
426 | 421 | assert model.model_name == 'openai/gpt-4o' |
@@ -573,3 +568,35 @@ async def test_openrouter_user_prompt_mixed_content(allow_model_requests: None): |
573 | 568 |
|
574 | 569 | assert result['role'] == 'user' |
575 | 570 | 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