feat(provider): 新增 OpenRouter 平台适配器 - GET /api/v1/credits,Bearer 认证 - 余额 = data.total_credits - data.total_usage(剩余可用) - 重启后自动同步为平台记录
This commit is contained in:
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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")
|
||||
+27
-3
@@ -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"
|
||||
Reference in New Issue
Block a user