Source code for modelly.llm.fake
"""A provider that answers from a Python function: for tests, teaching and dry runs.
It never opens a connection. Its replies depend only on the request, so a run with it
is as reproducible as the function it calls, and it counts what it was asked so a test
can check that a replay made no call at all.
"""
from __future__ import annotations
import math
import threading
from collections.abc import Callable, Iterable
from typing import Any
from modelly.llm.budget import CHARS_PER_TOKEN
from modelly.llm.errors import ProviderError
from modelly.llm.providers import Provider
from modelly.llm.request import Request, Response
FAKE_MODEL = "fake"
# Characters of the key the default reply quotes.
ECHO_CHARS = 12
def echo(request: Request) -> str:
"""The default reply: names the call, so two calls never get the same text."""
return f"fake reply {request.key()[:ECHO_CHARS]}"
[docs]
class FakeProvider(Provider):
"""Replies come from ``responder(request) -> str``; tokens are counted from length.
Args:
responder: A module-level function (so a model holding it can be saved).
failures: HTTP statuses to raise, one per call, before answering normally:
``failures=[429, 503]`` fails twice, then replies.
Example:
>>> from modelly.llm import Client
>>> client = Client(FakeProvider(lambda request: "yes"))
>>> client.complete("Agree?").text
'yes'
"""
name = "fake"
default_model = FAKE_MODEL
def __init__(
self,
responder: Callable[[Request], str] = echo,
failures: Iterable[int] = (),
) -> None:
"""Keep the responder and the failures to play back."""
super().__init__(base_url="fake://")
self.responder = responder
self.failures = list(failures)
self.calls = 0
self.requests: list[Request] = []
self._lock = threading.Lock()
[docs]
def complete(self, request: Request) -> Response:
"""Fail with the next queued status, if any; otherwise call the responder."""
with self._lock:
self.calls += 1
self.requests.append(request)
status = self.failures.pop(0) if self.failures else None
if status is not None:
raise ProviderError(f"fake failure {status}", status=status)
text = self.responder(request)
tokens_in = math.ceil(request.prompt_chars() / CHARS_PER_TOKEN)
tokens_out = math.ceil(len(text) / CHARS_PER_TOKEN)
return Response(text, tokens_in, tokens_out)
def __getstate__(self) -> dict[str, Any]:
"""Everything but the lock and the request log."""
state = super().__getstate__()
return {k: v for k, v in state.items() if k not in {"_lock", "requests"}}
def __setstate__(self, state: dict[str, Any]) -> None:
"""Restore the fields, an empty log and a fresh lock."""
super().__setstate__(state)
self.requests = []
self._lock = threading.Lock()