"""Deterministic fake model client for unit tests.""" from __future__ import annotations from collections.abc import Iterable from typing import TypeAlias from pydantic import BaseModel, ValidationError from gcmd_classifier.errors import NonRetryableModelError, StructuredModelResponseError from gcmd_classifier.llm.base import ModelRequest, ModelResponse, StructuredResponseT ScriptedAction: TypeAlias = BaseModel | dict | Exception class FakeModelClient: """Scripted model client that never imports or calls a live provider.""" def __init__(self, actions: Iterable[ScriptedAction]) -> None: self._actions = list(actions) self.requests: list[ModelRequest] = [] def generate_structured( self, request: ModelRequest[StructuredResponseT], ) -> ModelResponse[StructuredResponseT]: """Return the next scripted response or raise the next scripted exception.""" self.requests.append(request) if not self._actions: raise NonRetryableModelError("Fake model script is exhausted.") action = self._actions.pop(0) if isinstance(action, Exception): raise action try: parsed = _parse_action(action, request.response_schema) except ValidationError as exc: raise StructuredModelResponseError( "Fake model response failed schema validation." ) from exc return ModelResponse( parsed=parsed, provider=request.provider, model_name=request.model_name, prompt_version=request.prompt_version, ) @property def remaining_actions(self) -> int: """Number of scripted actions that have not been consumed.""" return len(self._actions) def _parse_action( action: BaseModel | dict, response_schema: type[StructuredResponseT], ) -> StructuredResponseT: if isinstance(action, response_schema): return action return response_schema.model_validate(action)