"""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"]