Files
cowork-local/tests/fakes/fake_provider.py
T

114 lines
4.3 KiB
Python

"""Fake LLM Provider for offline unit, contract, and characterization testing.
Provides deterministic responses, stream simulation, tool-call dispatching,
and fault injection without requiring any external network access or API keys.
"""
from __future__ import annotations
from typing import Any, Callable, Dict, List, Optional
from providers.base import CancelFn, Provider, ProviderError, TextCallback, ToolSpec
class FakeProvider(Provider):
"""Deterministic test double mimicking real LLM Providers (OpenAI, Anthropic, Ollama)."""
name = "fake"
supports_vision = True
def __init__(self, conf: Optional[Dict[str, Any]] = None) -> None:
# Initialize base provider with default configuration if none provided
super().__init__(conf or {"model": "fake-model-v1"})
# History of all message batches sent across all chat calls
self.call_history: List[List[Dict[str, Any]]] = []
# Queue of programmed assistant responses to return sequentially
self.response_queue: List[Dict[str, Any]] = []
# Queue of exceptions to raise on corresponding calls
self.error_queue: List[Exception] = []
# Default text returned when response queue is empty
self.default_text: str = "Fake model response."
# Total number of chat invocations
self.call_count: int = 0
# Recorded tool specs passed into each turn
self.last_tools: Optional[List[ToolSpec]] = None
def queue_response(
self,
content: str = "",
tool_calls: Optional[List[Dict[str, Any]]] = None,
reasoning: Optional[str] = None,
chunks: Optional[List[str]] = None,
) -> FakeProvider:
"""Enqueue a pre-configured response structure for upcoming chat turns."""
self.response_queue.append({
"content": content,
"tool_calls": tool_calls or [],
"reasoning": reasoning,
"chunks": chunks or ([content] if content else []),
})
return self
def queue_error(self, exc: Exception) -> FakeProvider:
"""Enqueue an exception to simulate network/API errors on the next turn."""
self.error_queue.append(exc)
return self
def chat(
self,
messages: List[Dict[str, Any]],
tools: Optional[List[ToolSpec]] = None,
on_text: Optional[TextCallback] = None,
cancel: Optional[CancelFn] = None,
on_reasoning: Optional[TextCallback] = None,
) -> Dict[str, Any]:
"""Simulate single LLM turn with full streaming and tool-call support."""
self.call_count += 1
self.call_history.append([dict(m) for m in messages])
self.last_tools = tools
# 1. Check for injected errors
if self.error_queue:
raise self.error_queue.pop(0)
# 2. Check early cancellation before processing
if cancel and cancel():
raise ProviderError("Execution aborted by user cancel signal before response generation.")
# 3. Retrieve queued response or construct default response
if self.response_queue:
resp_spec = self.response_queue.pop(0)
content = resp_spec.get("content", "")
tool_calls = resp_spec.get("tool_calls", [])
reasoning = resp_spec.get("reasoning")
chunks = resp_spec.get("chunks", [content] if content else [])
else:
content = self.default_text
tool_calls = []
reasoning = None
chunks = [content]
# 4. Stream reasoning chunks if provided
if reasoning and on_reasoning:
on_reasoning(reasoning)
# 5. Stream text chunks, checking cancellation between fragments
for chunk in chunks:
if cancel and cancel():
raise ProviderError("Execution cancelled during text chunk streaming.")
if on_text and chunk:
on_text(chunk)
# 6. Return canonical assistant message payload
assistant_msg: Dict[str, Any] = {
"role": "assistant",
"content": content,
}
if tool_calls:
assistant_msg["tool_calls"] = tool_calls
return assistant_msg
def list_models(self) -> List[str]:
"""Return available mock models for settings and validation tests."""
return ["fake-model-v1", "fake-reasoner-pro", "fake-vision-plus"]