Merge PR #1976 provider response guards

This commit is contained in:
Affaan Mustafa
2026-05-18 01:05:37 -04:00
4 changed files with 181 additions and 122 deletions

View File

@@ -1,7 +1,10 @@
from types import SimpleNamespace
import pytest
from llm.core.types import LLMInput, Message, Role, ToolDefinition
from llm.providers.claude import ClaudeProvider
from llm.providers.constants import EMPTY_FILTERED_RESPONSE_ERROR
from llm.providers.openai import OpenAIProvider
@@ -14,21 +17,20 @@ def _tool() -> ToolDefinition:
class _OpenAICompletions:
def __init__(self) -> None:
def __init__(self, response: SimpleNamespace | None = None) -> None:
self.params = None
self.response = response
def create(self, **params):
self.params = params
return SimpleNamespace(
choices=[SimpleNamespace(message=SimpleNamespace(content="ok", tool_calls=None), finish_reason="stop")],
model=params["model"],
usage=SimpleNamespace(prompt_tokens=1, completion_tokens=1, total_tokens=2),
)
if self.response:
return self.response
return _openai_response(model=params["model"])
class _OpenAIClient:
def __init__(self) -> None:
self.completions = _OpenAICompletions()
def __init__(self, response: SimpleNamespace | None = None) -> None:
self.completions = _OpenAICompletions(response=response)
self.chat = SimpleNamespace(completions=self.completions)
@@ -52,6 +54,16 @@ class _AnthropicClient:
self.api_key = "test"
def _openai_response(**overrides) -> SimpleNamespace:
defaults = {
"choices": [SimpleNamespace(message=SimpleNamespace(content="ok", tool_calls=None), finish_reason="stop")],
"model": "gpt-4o-mini",
"usage": SimpleNamespace(prompt_tokens=1, completion_tokens=1, total_tokens=2),
}
defaults.update(overrides)
return SimpleNamespace(**defaults)
def test_openai_provider_serializes_tools_for_chat_completions():
provider = OpenAIProvider(api_key="test")
client = _OpenAIClient()
@@ -72,6 +84,36 @@ def test_openai_provider_serializes_tools_for_chat_completions():
]
def test_openai_provider_can_be_constructed_without_credentials(monkeypatch):
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
provider = OpenAIProvider()
assert provider.validate_config() is False
def test_openai_provider_rejects_empty_or_filtered_responses():
provider = OpenAIProvider(api_key="test")
for response in [
_openai_response(choices=[]),
_openai_response(choices=[SimpleNamespace(message=None, finish_reason="content_filter")]),
]:
provider.client = _OpenAIClient(response=response)
with pytest.raises(ValueError, match=EMPTY_FILTERED_RESPONSE_ERROR):
provider.generate(LLMInput(messages=[Message(role=Role.USER, content="hi")]))
def test_openai_provider_allows_missing_usage():
provider = OpenAIProvider(api_key="test")
provider.client = _OpenAIClient(response=_openai_response(usage=None))
output = provider.generate(LLMInput(messages=[Message(role=Role.USER, content="hi")]))
assert output.content == "ok"
assert output.usage is None
def test_claude_provider_serializes_tools_for_messages_api():
provider = ClaudeProvider(api_key="test")
client = _AnthropicClient()