SRHarness / src /sr_harness /api /openrouter_api.py
system's picture
system HF Staff
Deploy SRHarness v1.0.8 (f138545a336c)
8956871 verified
Raw History Blame Contribute Delete
7.1 kB
# Copyright (c) 2024-present, Yumeow. Licensed under the MIT License.
import httpx
from openai import DefaultHttpxClient, OpenAI
from dotenv import load_dotenv
from typing import Any
from .base_api import BaseAPI, ProgressCallback, ProviderSampleResult
from .utils import RemoteModelCatalog
from ..core import APICallSample, ToolCall
@BaseAPI.register("openrouter")
class OpenRouterAPI(BaseAPI):
"""LLM provider adapter for OpenRouter, supporting live model discovery and streaming tool calls."""
def __init__(self, model: str = "qwen/qwen3.6-plus", **kwargs: Any) -> None:
"""Initialize the OpenRouter API adapter.
Args:
model: OpenRouter model slug (defaults to ``"qwen/qwen3.6-plus"``).
**kwargs: Additional arguments forwarded to :class:`BaseAPI`.
"""
super().__init__(model=model, **kwargs)
self._model_catalog = RemoteModelCatalog(
lambda url: httpx.Client(proxy=self.proxy_url(url), trust_env=False)
)
def format_response(
self, message: dict[str, Any], tool_calls: list[ToolCall], content: str | None = None
) -> APICallSample:
"""Convert an OpenRouter completion message into a normalized :class:`~sr_harness.core.APICallSample`.
Args:
message: Raw OpenRouter assistant message dictionary.
tool_calls: Normalized tool calls extracted from the message.
content: Optional explicit text content override.
Returns:
APICallSample: Normalized assistant response sample.
"""
return self._build_response(message, tool_calls, content)
@property
def model_list(self) -> list[str]:
"""Fetch and return all text-capable model identifiers from the live OpenRouter catalog.
Returns:
list[str]: Available OpenRouter model slugs.
"""
endpoint = self.get_env("OPENROUTER_ENDPOINT", "https://openrouter.ai/api/v1")
def supports_text_output(record: dict[str, Any]) -> bool:
architecture = record.get("architecture")
if not isinstance(architecture, dict):
return True
output_modalities = architecture.get("output_modalities")
return not isinstance(output_modalities, list) or "text" in output_modalities
return self._model_catalog.get(
f"{endpoint.rstrip('/')}/models",
description="OpenRouter model catalog",
api_key=self.get_env("OPENROUTER_API_KEY"),
include=supports_text_output,
)
def _request_sample(
self,
messages: list[dict[str, Any]],
*,
sample_index: int,
max_tokens=None,
temperature=1.0,
top_p=1.0,
callback: ProgressCallback | None = None,
) -> ProviderSampleResult:
load_dotenv()
api_key = self.get_env("OPENROUTER_API_KEY", required=True)
endpoint = self.get_env("OPENROUTER_ENDPOINT", "https://openrouter.ai/api/v1")
client = OpenAI(
timeout=300.0,
max_retries=0,
base_url=endpoint,
api_key=api_key,
http_client=DefaultHttpxClient(
proxy=self.proxy_url(endpoint),
trust_env=False,
),
)
payload = {
"model": self.model,
"messages": [dict(message) for message in messages],
"top_p": top_p,
**({"max_tokens": max_tokens} if max_tokens is not None else {}),
"temperature": temperature,
}
if self.model == "openai/gpt-oss-120b":
# Keep benchmark inference reproducible across OpenRouter calls. The
# default router otherwise mixes providers with different weight
# quantizations (for example bf16 and fp8) in one experiment.
payload["extra_body"] = {
"provider": {
"order": ["AkashML"],
"allow_fallbacks": False,
}
}
if self.tool_box is None:
pass
elif not self.tool_box.native_tool_calls:
description = (
"Use the following tools when a tool call is needed. "
"Return tool calls in the specified format.\n\n"
f"{self.tool_box.format_tools()}"
)
if payload["messages"][0]["role"] in {"system", "developer"}:
first = payload["messages"][0]
payload["messages"][0] = {
**first,
"content": f"{first['content']}\n\n{description}",
}
else:
payload["messages"].insert(0, {"role": "system", "content": description})
else:
tools = self.tool_box.format_tools()
if not isinstance(tools, list):
raise TypeError("Native tool parser must return JSON tool descriptions")
payload["tools"] = tools
payload["tool_choice"] = "auto"
completion, raw_response, message, content = self.openai_chat_completion(
client,
payload,
sample=sample_index,
callback=callback,
)
choices = raw_response.get("choices") or [{}]
finish_reason = raw_response.get("finish_reason") or choices[0].get("finish_reason")
token_usage = self.openai_token_usage(completion)
usage = getattr(completion, "usage", None)
money_usage = {"total": getattr(usage, "cost", 0)} if usage is not None else {}
if finish_reason == "length":
partial_message = dict(message)
partial_message.pop("tool_calls", None)
budget = max_tokens if max_tokens is not None else "provider default"
error = (
f"OpenRouterAPI({self.model}) exhausted max_tokens={budget}; "
"the response was truncated before the model completed its turn. "
"Increase the Agent's maximum output length and retry. "
f"模型已耗尽 max_tokens={budget},响应在本轮完成前被截断;"
"请增大 Agent 的最大输出长度后重试。"
)
partial_response = self.format_response(partial_message, [], content)
partial_response.finish_reason = "length"
return ProviderSampleResult(
response=partial_response,
token_usage=token_usage,
money_usage=money_usage,
raw_response=raw_response,
error=error,
)
tool_calls = self.parse_tool_calls(message, content)
if not (tool_calls or content.strip()):
raise ValueError(
f"OpenRouterAPI({self.model}) returned empty content and no usable tool calls."
)
return ProviderSampleResult(
response=self.format_response(message, tool_calls, content),
token_usage=token_usage,
money_usage=money_usage,
raw_response=raw_response,
)