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.base import BalanceProvider
|
||||||
from app.providers.deepseek import DeepSeekProvider
|
from app.providers.deepseek import DeepSeekProvider
|
||||||
|
from app.providers.openrouter import OpenRouterProvider
|
||||||
|
|
||||||
PROVIDERS: dict[str, type[BalanceProvider]] = {
|
PROVIDERS: dict[str, type[BalanceProvider]] = {
|
||||||
"deepseek": DeepSeekProvider,
|
"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.expr import ExprError
|
||||||
from app.providers import get_provider, list_providers
|
from app.providers import get_provider, list_providers
|
||||||
from app.providers.deepseek import DeepSeekProvider
|
from app.providers.deepseek import DeepSeekProvider
|
||||||
|
from app.providers.openrouter import OpenRouterProvider
|
||||||
|
|
||||||
DEEPSEEK_RESPONSE = {
|
DEEPSEEK_RESPONSE = {
|
||||||
"is_available": True,
|
"is_available": True,
|
||||||
@@ -36,10 +37,31 @@ class TestDeepSeek:
|
|||||||
DeepSeekProvider().extract_balance({"is_available": False})
|
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:
|
class TestRegistry:
|
||||||
def test_get_provider(self):
|
def test_get_provider(self):
|
||||||
prov = get_provider("deepseek")
|
assert isinstance(get_provider("deepseek"), DeepSeekProvider)
|
||||||
assert isinstance(prov, DeepSeekProvider)
|
assert isinstance(get_provider("openrouter"), OpenRouterProvider)
|
||||||
|
|
||||||
def test_unknown_provider_none(self):
|
def test_unknown_provider_none(self):
|
||||||
assert get_provider("nope") is None
|
assert get_provider("nope") is None
|
||||||
@@ -47,6 +69,8 @@ class TestRegistry:
|
|||||||
def test_list_providers(self):
|
def test_list_providers(self):
|
||||||
provs = list_providers()
|
provs = list_providers()
|
||||||
ids = [p["id"] for p in provs]
|
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")
|
dp = next(p for p in provs if p["id"] == "deepseek")
|
||||||
assert dp["name"] == "DeepSeek" and dp["currency"] == "CNY" and dp["icon"] == "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