Skip to content

Commit 4af6b38

Browse files
committed
🐛 fix(models): retry malformed OpenRouter responses
1 parent e187bcb commit 4af6b38

2 files changed

Lines changed: 62 additions & 0 deletions

File tree

src/minisweagent/models/openrouter_model.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ def query(self, messages: list[dict[str, str]], **kwargs) -> dict:
9898
for attempt in retry(logger=logger, abort_exceptions=self.abort_exceptions):
9999
with attempt:
100100
response = self._query(self._prepare_messages_for_api(messages), **kwargs)
101+
self._validate_response(response)
101102
cost_output = self._calculate_cost(response)
102103
GLOBAL_MODEL_STATS.add(cost_output["cost"])
103104
try:
@@ -115,6 +116,16 @@ def query(self, messages: list[dict[str, str]], **kwargs) -> dict:
115116
}
116117
return message
117118

119+
def _validate_response(self, response: dict) -> None:
120+
choices = response.get("choices") if isinstance(response, dict) else None
121+
if (
122+
not isinstance(choices, list)
123+
or not choices
124+
or not isinstance(choices[0], dict)
125+
or not isinstance(choices[0].get("message"), dict)
126+
):
127+
raise OpenRouterAPIError(f"Invalid chat completion response: {response}")
128+
118129
def _calculate_cost(self, response) -> dict[str, float]:
119130
usage = response.get("usage", {})
120131
cost = usage.get("cost", 0.0)
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
from unittest.mock import patch
2+
3+
import pytest
4+
from tenacity import Retrying, stop_after_attempt, wait_none
5+
6+
from minisweagent.models.openrouter_model import OpenRouterAPIError, OpenRouterModel
7+
8+
9+
@pytest.mark.parametrize(
10+
("response",), # noqa: PT006
11+
[
12+
({"error": {"message": "Provider returned an error"}},),
13+
({"choices": []},),
14+
({"choices": [{}]},),
15+
({"choices": "invalid"},),
16+
([],),
17+
],
18+
)
19+
def test_openrouter_model_rejects_invalid_chat_completion(response) -> None:
20+
model = OpenRouterModel(model_name="test/model")
21+
22+
with pytest.raises(OpenRouterAPIError, match="Invalid chat completion response"):
23+
model._validate_response(response)
24+
25+
26+
def test_openrouter_model_retries_invalid_chat_completion() -> None:
27+
model = OpenRouterModel(model_name="test/model")
28+
valid_response = {
29+
"choices": [
30+
{
31+
"message": {
32+
"tool_calls": [
33+
{"id": "call_1", "function": {"name": "bash", "arguments": '{"command": "echo ok"}'}}
34+
]
35+
},
36+
"finish_reason": "tool_calls",
37+
}
38+
],
39+
"usage": {"cost": 0.1},
40+
}
41+
42+
with (
43+
patch.object(model, "_query", side_effect=[{"error": {"message": "temporary error"}}, valid_response]) as query,
44+
patch(
45+
"minisweagent.models.openrouter_model.retry",
46+
return_value=Retrying(stop=stop_after_attempt(2), wait=wait_none(), reraise=True),
47+
),
48+
):
49+
assert model.query([{"role": "user", "content": "hello"}])["extra"]["response"] == valid_response
50+
51+
assert query.call_count == 2

0 commit comments

Comments
 (0)