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
2231from __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