diff --git a/app/providers/__init__.py b/app/providers/__init__.py index 709a001..dc83515 100644 --- a/app/providers/__init__.py +++ b/app/providers/__init__.py @@ -4,9 +4,11 @@ from __future__ import annotations from app.providers.base import BalanceProvider from app.providers.deepseek import DeepSeekProvider +from app.providers.openrouter import OpenRouterProvider PROVIDERS: dict[str, type[BalanceProvider]] = { "deepseek": DeepSeekProvider, + "openrouter": OpenRouterProvider, } diff --git a/app/providers/openrouter.py b/app/providers/openrouter.py new file mode 100644 index 0000000..8ee5db4 --- /dev/null +++ b/app/providers/openrouter.py @@ -0,0 +1,29 @@ +"""OpenRouter 余额。 + +接口:GET https://openrouter.ai/api/v1/credits +响应:{"data": {"total_credits": 100.5, "total_usage": 25.75}} +余额取剩余可用:total_credits - total_usage +""" + +from __future__ import annotations + +from app.expr import evaluate_balance +from app.providers.base import BalanceProvider + + +class OpenRouterProvider(BalanceProvider): + id = "openrouter" + name = "OpenRouter" + currency = "USD" + icon = "OpenRouter" + description = "OpenRouter 余额(GET /api/v1/credits,Bearer 认证,剩余 = total_credits - total_usage)" + + def build_request(self, api_key: str) -> tuple[str, dict, str | None]: + return ( + "https://openrouter.ai/api/v1/credits", + {"Authorization": f"Bearer {api_key}"}, + None, + ) + + def extract_balance(self, data: object) -> float: + return evaluate_balance(data, "data.total_credits - data.total_usage") diff --git a/tests/test_providers.py b/tests/test_providers.py index fc1a45e..427546b 100644 --- a/tests/test_providers.py +++ b/tests/test_providers.py @@ -5,6 +5,7 @@ import pytest from app.expr import ExprError from app.providers import get_provider, list_providers from app.providers.deepseek import DeepSeekProvider +from app.providers.openrouter import OpenRouterProvider DEEPSEEK_RESPONSE = { "is_available": True, @@ -36,10 +37,31 @@ class TestDeepSeek: DeepSeekProvider().extract_balance({"is_available": False}) +class TestOpenRouter: + def test_build_request(self): + prov = OpenRouterProvider() + url, headers, body = prov.build_request("sk-or-1") + assert url == "https://openrouter.ai/api/v1/credits" + assert headers["Authorization"] == "Bearer sk-or-1" + assert body is None + + def test_extract_balance_remaining(self): + resp = {"data": {"total_credits": 100.5, "total_usage": 25.75}} + assert OpenRouterProvider().extract_balance(resp) == 74.75 + + def test_extract_zero_usage(self): + resp = {"data": {"total_credits": 10.0, "total_usage": 0}} + assert OpenRouterProvider().extract_balance(resp) == 10.0 + + def test_extract_missing(self): + with pytest.raises(ExprError): + OpenRouterProvider().extract_balance({"data": {}}) + + class TestRegistry: def test_get_provider(self): - prov = get_provider("deepseek") - assert isinstance(prov, DeepSeekProvider) + assert isinstance(get_provider("deepseek"), DeepSeekProvider) + assert isinstance(get_provider("openrouter"), OpenRouterProvider) def test_unknown_provider_none(self): assert get_provider("nope") is None @@ -47,6 +69,8 @@ class TestRegistry: def test_list_providers(self): provs = list_providers() ids = [p["id"] for p in provs] - assert "deepseek" in ids + assert "deepseek" in ids and "openrouter" in ids dp = next(p for p in provs if p["id"] == "deepseek") assert dp["name"] == "DeepSeek" and dp["currency"] == "CNY" and dp["icon"] == "DeepSeek" + orp = next(p for p in provs if p["id"] == "openrouter") + assert orp["currency"] == "USD" and orp["icon"] == "OpenRouter"