diff --git a/promptlens/models/config.py b/promptlens/models/config.py index 7c85798..df24923 100644 --- a/promptlens/models/config.py +++ b/promptlens/models/config.py @@ -2,7 +2,7 @@ from typing import Any, Dict, List, Optional -from pydantic import BaseModel, Field +from pydantic import BaseModel, ConfigDict, Field class ProviderConfig(BaseModel): @@ -114,9 +114,7 @@ class RunConfig(BaseModel): execution: ExecutionConfig = Field(default_factory=ExecutionConfig) output: OutputConfig = Field(default_factory=OutputConfig) - class Config: - """Pydantic config.""" - json_schema_extra = { + model_config = ConfigDict(json_schema_extra={ "example": { "golden_set": "./examples/golden_sets/customer_support.yaml", "models": [ @@ -142,4 +140,4 @@ class Config: "formats": ["html", "json"], }, } - } + }) diff --git a/promptlens/models/test_case.py b/promptlens/models/test_case.py index 931208f..a95bb39 100644 --- a/promptlens/models/test_case.py +++ b/promptlens/models/test_case.py @@ -2,7 +2,7 @@ from typing import Any, Dict, List, Optional -from pydantic import BaseModel, Field +from pydantic import BaseModel, ConfigDict, Field from promptlens.models.tools import ToolDefinition, ExpectedToolCall @@ -50,9 +50,7 @@ class TestCase(BaseModel): description="Whether to actually execute tools (default: False, evaluation only)" ) - class Config: - """Pydantic config.""" - json_schema_extra = { + model_config = ConfigDict(json_schema_extra={ "example": { "id": "cs-001", "query": "How do I reset my password?", @@ -60,7 +58,7 @@ class Config: "category": "account_management", "tags": ["password", "account"], } - } + }) class GoldenSet(BaseModel): @@ -80,9 +78,7 @@ class GoldenSet(BaseModel): test_cases: List[TestCase] metadata: Dict[str, Any] = Field(default_factory=dict) - class Config: - """Pydantic config.""" - json_schema_extra = { + model_config = ConfigDict(json_schema_extra={ "example": { "name": "Customer Support Tests", "description": "Test cases for customer support chatbot", @@ -97,4 +93,4 @@ class Config: } ], } - } + }) diff --git a/promptlens/models/tools.py b/promptlens/models/tools.py index bd7d52b..011447f 100644 --- a/promptlens/models/tools.py +++ b/promptlens/models/tools.py @@ -8,7 +8,7 @@ """ from typing import Any, Dict, List, Optional -from pydantic import BaseModel, Field +from pydantic import BaseModel, ConfigDict, Field class ToolParameter(BaseModel): @@ -24,8 +24,7 @@ class ToolParameter(BaseModel): properties: Optional[Dict[str, "ToolParameter"]] = Field(None, description="For object types, nested properties") items: Optional["ToolParameter"] = Field(None, description="For array types, the item schema") - class Config: - extra = "allow" # Allow additional JSON Schema fields + model_config = ConfigDict(extra="allow") # Allow additional JSON Schema fields class ToolDefinition(BaseModel): diff --git a/promptlens/providers/http.py b/promptlens/providers/http.py index f1828e2..e1d39ac 100644 --- a/promptlens/providers/http.py +++ b/promptlens/providers/http.py @@ -1,5 +1,6 @@ """Generic HTTP provider for local models (Ollama, LM Studio, etc.).""" +import json import logging from datetime import datetime from typing import Any, Dict, List, Optional @@ -119,7 +120,19 @@ async def _make_request() -> ModelResponse: timeout=aiohttp.ClientTimeout(total=self.config.timeout), ) as response: response.raise_for_status() - data = await response.json() + raw_text = await response.text() + + try: + data = json.loads(raw_text) + except json.JSONDecodeError as exc: + logger.error( + "HTTP provider returned non-JSON response from %s: %s", + self.endpoint, + raw_text[:500], + ) + raise ValueError( + "HTTP provider expected JSON response but received non-JSON content" + ) from exc # Extract content (try common response formats) content = self._extract_content(data) diff --git a/tests/test_http_provider_response_parsing.py b/tests/test_http_provider_response_parsing.py index 0949b6e..4ef58a2 100644 --- a/tests/test_http_provider_response_parsing.py +++ b/tests/test_http_provider_response_parsing.py @@ -1,3 +1,5 @@ +import pytest + from promptlens.models.config import ProviderConfig from promptlens.providers.http import HTTPProvider @@ -57,3 +59,39 @@ def test_extract_content_returns_empty_for_unknown_shape() -> None: provider = _provider() assert provider._extract_content({"foo": "bar"}) == "" + + +@pytest.mark.asyncio +async def test_http_provider_returns_clear_error_for_non_json_response() -> None: + provider = _provider() + + class MockResponse: + async def __aenter__(self): + return self + + async def __aexit__(self, exc_type, exc, tb): + return False + + def raise_for_status(self) -> None: + return None + + async def text(self) -> str: + return "not-json" + + class MockSession: + async def __aenter__(self): + return self + + async def __aexit__(self, exc_type, exc, tb): + return False + + def post(self, *args, **kwargs): + return MockResponse() + + from unittest.mock import patch + + with patch("promptlens.providers.http.aiohttp.ClientSession", return_value=MockSession()): + result = await provider.generate("hello") + + assert result.error is not None + assert "expected JSON response" in result.error