feat(provider): 新增 OpenRouter 平台适配器 - GET /api/v1/credits,Bearer 认证 - 余额 = data.total_credits - data.total_usage(剩余可用) - 重启后自动同步为平台记录

This commit is contained in:
2026-08-05 07:46:19 +08:00
parent 4cf31c76bb
commit 794332aff6
3 changed files with 58 additions and 3 deletions
+2
View File
@@ -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,
} }
+29
View File
@@ -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/creditsBearer 认证,剩余 = 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
View File
@@ -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"