Skip to content

Commit 6224963

Browse files
committed
add possibility to trigger errors
1 parent 3f16332 commit 6224963

1 file changed

Lines changed: 25 additions & 8 deletions

File tree

openhands-sdk/openhands/sdk/testing/test_llm.py

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,15 @@
1717
>>> user_msg = Message(role="user", content=[TextContent(text="Hi")])
1818
>>> response = llm.completion([user_msg])
1919
>>> print(response.message.content[0].text) # "Hello!"
20+
21+
>>> # Scripted errors (like unittest.mock side_effect)
22+
>>> from openhands.sdk.llm.exceptions import LLMContextWindowExceedError
23+
>>> llm = TestLLM.from_responses([
24+
... Message(role="assistant", content=[TextContent(text="OK")]),
25+
... LLMContextWindowExceedError(),
26+
... ])
27+
>>> llm.completion([...]) # returns "OK"
28+
>>> llm.completion([...]) # raises LLMContextWindowExceedError
2029
"""
2130

2231
from __future__ import annotations
@@ -93,7 +102,7 @@ class TestLLM(LLM):
93102
__test__ = False
94103

95104
model: str = Field(default="test-model")
96-
_scripted_responses: deque[Message] = PrivateAttr(default_factory=deque)
105+
_scripted_responses: deque[Message | Exception] = PrivateAttr(default_factory=deque)
97106
_call_count: int = PrivateAttr(default=0)
98107

99108
model_config: ClassVar[ConfigDict] = ConfigDict(
@@ -110,18 +119,19 @@ def __init__(self, **data: Any) -> None:
110119
@classmethod
111120
def from_messages(
112121
cls,
113-
messages: list[Message],
122+
messages: list[Message | Exception],
114123
*,
115124
model: str = "test-model",
116125
usage_id: str = "test-llm",
117126
**kwargs: Any,
118127
) -> TestLLM:
119-
"""Create a TestLLM with scripted responses.
128+
"""Create a TestLLM with scripted responses and/or errors.
120129
121130
Args:
122-
messages: List of Message objects to return in order.
123-
Each call to completion() or responses() will return
124-
the next message from this list.
131+
messages: List of Message or Exception objects to return in order.
132+
Each call to completion() or responses() consumes the next
133+
item: Message objects are returned normally, Exception objects
134+
are raised (like unittest.mock side_effect).
125135
model: Model name (default: "test-model")
126136
usage_id: Usage ID for metrics (default: "test-llm")
127137
**kwargs: Additional LLM configuration options
@@ -132,7 +142,7 @@ def from_messages(
132142
Example:
133143
>>> llm = TestLLM.from_messages([
134144
... Message(role="assistant", content=[TextContent(text="First")]),
135-
... Message(role="assistant", content=[TextContent(text="Second")]),
145+
... LLMContextWindowExceedError("context too long"),
136146
... ])
137147
"""
138148
return cls(
@@ -166,16 +176,23 @@ def completion(
166176
167177
Raises:
168178
TestLLMExhaustedError: When no more scripted responses are available.
179+
Exception: Any scripted exception placed in the response queue.
169180
"""
170181
if not self._scripted_responses:
171182
raise TestLLMExhaustedError(
172183
f"TestLLM: no more scripted responses "
173184
f"(exhausted after {self._call_count} calls)"
174185
)
175186

176-
message = self._scripted_responses.popleft()
187+
item = self._scripted_responses.popleft()
177188
self._call_count += 1
178189

190+
# Raise scripted exceptions (like unittest.mock side_effect)
191+
if isinstance(item, Exception):
192+
raise item
193+
194+
message = item
195+
179196
# Create a minimal ModelResponse for raw_response
180197
raw_response = self._create_model_response(message)
181198

0 commit comments

Comments
 (0)