Skip to content

Commit 60fd305

Browse files
committed
add mock LM for tests, add test for semaphore
1 parent 9054099 commit 60fd305

2 files changed

Lines changed: 115 additions & 0 deletions

File tree

‎rlm/clients/mock_lm.py‎

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
"""
2+
Mock LM client for testing without a real model.
3+
4+
Use this to test LMHandler, REPL integrations, or any code that needs a BaseLM
5+
without calling an external API.
6+
"""
7+
8+
from typing import Any, Callable
9+
10+
from rlm.clients.base_lm import BaseLM
11+
from rlm.core.types import ModelUsageSummary, UsageSummary
12+
13+
14+
class MockLM(BaseLM):
15+
"""
16+
In-memory mock LM that implements BaseLM for testing.
17+
18+
- completion() / acompletion(): return a simple echo-style response by default,
19+
or use the optional response list/callable if provided.
20+
- get_usage_summary() / get_last_usage(): return fixed usage for tests.
21+
"""
22+
23+
def __init__(
24+
self,
25+
model_name: str = "mock-model",
26+
responses: list[str] | None = None,
27+
response_fn: Callable[[str | dict[str, Any]], str] | None = None,
28+
**kwargs: Any,
29+
):
30+
super().__init__(model_name=model_name, **kwargs)
31+
self._responses = list(responses) if responses is not None else None
32+
self._response_fn = response_fn
33+
self._call_count = 0
34+
self._default_usage = ModelUsageSummary(
35+
total_calls=0, total_input_tokens=0, total_output_tokens=0
36+
)
37+
38+
def completion(self, prompt: str | dict[str, Any]) -> str:
39+
self._call_count += 1
40+
if self._responses is not None:
41+
if not self._responses:
42+
raise IndexError("MockLM: no more responses in list")
43+
return self._responses.pop(0)
44+
if self._response_fn is not None:
45+
return self._response_fn(prompt)
46+
prompt_str = prompt if isinstance(prompt, str) else str(prompt)[:80]
47+
return f"Mock response to: {prompt_str}"
48+
49+
async def acompletion(self, prompt: str | dict[str, Any]) -> str:
50+
return self.completion(prompt)
51+
52+
def get_usage_summary(self) -> UsageSummary:
53+
return UsageSummary(
54+
model_usage_summaries={
55+
self.model_name: ModelUsageSummary(
56+
total_calls=self._call_count,
57+
total_input_tokens=self._call_count * 10,
58+
total_output_tokens=self._call_count * 10,
59+
)
60+
}
61+
)
62+
63+
def get_last_usage(self) -> ModelUsageSummary:
64+
return ModelUsageSummary(
65+
total_calls=1,
66+
total_input_tokens=10,
67+
total_output_tokens=10,
68+
)

‎tests/test_lm_handler.py‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
"""Tests for LMHandler using MockLM (no real LM required)."""
2+
3+
import pytest
4+
5+
from rlm.clients.mock_lm import MockLM
6+
from rlm.core.comms_utils import LMRequest, send_lm_request, send_lm_request_batched
7+
from rlm.core.lm_handler import LMHandler
8+
9+
10+
def test_lm_handler_single_request():
11+
"""Single prompt request returns success and echo-style content."""
12+
mock = MockLM(responses=["hello back"])
13+
with LMHandler(client=mock) as handler:
14+
request = LMRequest(prompt="hello")
15+
response = send_lm_request(handler.address, request)
16+
assert response.success
17+
assert response.chat_completion is not None
18+
assert response.chat_completion.response == "hello back"
19+
20+
21+
def test_lm_handler_batched_request():
22+
"""Batched prompts return one response per prompt in order."""
23+
responses = [f"r{i}" for i in range(5)]
24+
mock = MockLM(responses=responses)
25+
with LMHandler(client=mock, batch_max_concurrent=3) as handler:
26+
prompts = [f"prompt-{i}" for i in range(5)]
27+
result = send_lm_request_batched(handler.address, prompts)
28+
assert len(result) == 5
29+
for i, resp in enumerate(result):
30+
assert resp.success, resp.error
31+
assert resp.chat_completion is not None
32+
assert resp.chat_completion.response == f"r{i}"
33+
34+
35+
def test_lm_handler_batched_many_prompts_semaphore_cap():
36+
"""Many prompts complete successfully with semaphore limiting concurrency."""
37+
# 50 prompts, max 4 concurrent: should still all complete
38+
count = 50
39+
responses = [f"resp-{i}" for i in range(count)]
40+
mock = MockLM(responses=responses)
41+
with LMHandler(client=mock, batch_max_concurrent=4) as handler:
42+
prompts = [f"p-{i}" for i in range(count)]
43+
result = send_lm_request_batched(handler.address, prompts)
44+
assert len(result) == count
45+
for i, resp in enumerate(result):
46+
assert resp.success, (i, resp.error)
47+
assert resp.chat_completion.response == f"resp-{i}"

0 commit comments

Comments
 (0)