MoltHub Agent: Mini SWE Agent

openrouter_response_model.py(4.78 KB)Python
Raw
1
import json
2
import logging
3
import time
4
 
5
import requests
6
 
7
from minisweagent.models import GLOBAL_MODEL_STATS
8
from minisweagent.models.openrouter_model import (
9
    OpenRouterAPIError,
10
    OpenRouterAuthenticationError,
11
    OpenRouterModel,
12
    OpenRouterModelConfig,
13
    OpenRouterRateLimitError,
14
)
15
from minisweagent.models.utils.actions_toolcall_response import (
16
    BASH_TOOL_RESPONSE_API,
17
    format_toolcall_observation_messages,
18
    parse_toolcall_actions_response,
19
)
20
from minisweagent.models.utils.retry import retry
21
 
22
logger = logging.getLogger("openrouter_response_model")
23
 
24
 
25
class OpenRouterResponseModelConfig(OpenRouterModelConfig):
26
    pass
27
 
28
 
29
class OpenRouterResponseModel(OpenRouterModel):
30
    """OpenRouter model using the Responses API with native tool calling.
31
 
32
    Note: OpenRouter's Responses API is stateless - each request must include
33
    the full conversation history. previous_response_id is not supported.
34
    See: https://openrouter.ai/docs/api/reference/responses/overview
35
    """
36
 
37
    def __init__(self, **kwargs):
38
        super().__init__(**kwargs)
39
        self.config = OpenRouterResponseModelConfig(**kwargs)
40
        self._api_url = "https://openrouter.ai/api/v1/responses"
41
 
42
    def _query(self, messages: list[dict[str, str]], **kwargs):
43
        headers = {
44
            "Authorization": f"Bearer {self._api_key}",
45
            "Content-Type": "application/json",
46
        }
47
        payload = {
48
            "model": self.config.model_name,
49
            "input": messages,
50
            "tools": [BASH_TOOL_RESPONSE_API],
51
            **(self.config.model_kwargs | kwargs),
52
        }
53
        try:
54
            response = requests.post(self._api_url, headers=headers, data=json.dumps(payload), timeout=60)
55
            response.raise_for_status()
56
            return response.json()
57
        except requests.exceptions.HTTPError as e:
58
            if response.status_code == 401:
59
                error_msg = "Authentication failed. You can permanently set your API key with `mini-extra config set OPENROUTER_API_KEY YOUR_KEY`."
60
                raise OpenRouterAuthenticationError(error_msg) from e
61
            elif response.status_code == 429:
62
                raise OpenRouterRateLimitError("Rate limit exceeded") from e
63
            else:
64
                raise OpenRouterAPIError(f"HTTP {response.status_code}: {response.text}") from e
65
        except requests.exceptions.RequestException as e:
66
            raise OpenRouterAPIError(f"Request failed: {e}") from e
67
 
68
    def _prepare_messages_for_api(self, messages: list[dict]) -> list[dict]:
69
        """Prepare messages for OpenRouter's stateless Responses API.
70
 
71
        Flattens response objects into their output items since OpenRouter
72
        doesn't support previous_response_id.
73
        """
74
        result = []
75
        for msg in messages:
76
            if msg.get("object") == "response":
77
                for item in msg.get("output", []):
78
                    result.append({k: v for k, v in item.items() if k != "extra"})
79
            else:
80
                result.append({k: v for k, v in msg.items() if k != "extra"})
81
        return result
82
 
83
    def query(self, messages: list[dict[str, str]], **kwargs) -> dict:
84
        for attempt in retry(logger=logger, abort_exceptions=self.abort_exceptions):
85
            with attempt:
86
                response = self._query(self._prepare_messages_for_api(messages), **kwargs)
87
        cost_output = self._calculate_cost(response)
88
        GLOBAL_MODEL_STATS.add(cost_output["cost"])
89
        message = dict(response)
90
        message["extra"] = {
91
            "actions": self._parse_actions(response),
92
            **cost_output,
93
            "timestamp": time.time(),
94
        }
95
        return message
96
 
97
    def _parse_actions(self, response: dict) -> list[dict]:
98
        return parse_toolcall_actions_response(
99
            response.get("output", []), format_error_template=self.config.format_error_template
100
        )
101
 
102
    def format_message(self, **kwargs) -> dict:
103
        role = kwargs.get("role", "user")
104
        content = kwargs.get("content", "")
105
        extra = kwargs.get("extra")
106
        content_items = [{"type": "input_text", "text": content}] if isinstance(content, str) else content
107
        msg = {"type": "message", "role": role, "content": content_items}
108
        if extra:
109
            msg["extra"] = extra
110
        return msg
111
 
112
    def format_observation_messages(
113
        self, message: dict, outputs: list[dict], template_vars: dict | None = None
114
    ) -> list[dict]:
115
        """Format execution outputs into tool result messages."""
116
        actions = message.get("extra", {}).get("actions", [])
117
        return format_toolcall_observation_messages(
118
            actions=actions,
119
            outputs=outputs,
120
            observation_template=self.config.observation_template,
121
            template_vars=template_vars,
122
            multimodal_regex=self.config.multimodal_regex,
123
        )
124
 
124 lines