Initial project import
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest import mock
|
||||
|
||||
from services.account_service import AccountService
|
||||
from services.model_service import ModelRoute, ModelUnavailableError
|
||||
from services.protocol import (
|
||||
anthropic_v1_messages,
|
||||
conversation,
|
||||
openai_v1_chat_complete,
|
||||
openai_v1_response,
|
||||
)
|
||||
from services.storage.json_storage import JSONStorageBackend
|
||||
|
||||
|
||||
class TextAccountRoutingTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.temp_dir = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.temp_dir.cleanup)
|
||||
self.service = AccountService(
|
||||
JSONStorageBackend(Path(self.temp_dir.name) / "accounts.json")
|
||||
)
|
||||
self.service.add_account_items(
|
||||
[
|
||||
{"access_token": "free", "type": "free", "status": "正常"},
|
||||
{"access_token": "plus", "type": "Plus", "status": "正常"},
|
||||
{"access_token": "pro", "type": "Pro", "status": "正常"},
|
||||
{"access_token": "pro-disabled", "type": "Pro", "status": "禁用"},
|
||||
]
|
||||
)
|
||||
self.service.refresh_access_token = lambda token, **_kwargs: token
|
||||
|
||||
def test_explicit_model_selects_only_advertising_account_type(self) -> None:
|
||||
route = ModelRoute(account_types=frozenset({"Pro"}), allow_anonymous=False)
|
||||
with mock.patch(
|
||||
"services.model_service.model_catalog_service.route_for_model",
|
||||
return_value=route,
|
||||
):
|
||||
token = self.service.get_text_access_token(model="pro-only")
|
||||
|
||||
self.assertEqual(token, "pro")
|
||||
|
||||
def test_auto_model_keeps_existing_unfiltered_rotation(self) -> None:
|
||||
with mock.patch(
|
||||
"services.model_service.model_catalog_service.route_for_model",
|
||||
side_effect=AssertionError("auto must not load the model catalog"),
|
||||
):
|
||||
tokens = {
|
||||
self.service.get_text_access_token(model="auto"),
|
||||
self.service.get_text_access_token(model="auto"),
|
||||
self.service.get_text_access_token(model="auto"),
|
||||
}
|
||||
|
||||
self.assertEqual(tokens, {"free", "plus", "pro"})
|
||||
|
||||
def test_anonymous_model_uses_anonymous_backend(self) -> None:
|
||||
route = ModelRoute(account_types=frozenset(), allow_anonymous=True)
|
||||
with mock.patch(
|
||||
"services.model_service.model_catalog_service.route_for_model",
|
||||
return_value=route,
|
||||
):
|
||||
token = self.service.get_text_access_token(model="anon-only")
|
||||
|
||||
self.assertEqual(token, "")
|
||||
|
||||
def test_model_without_eligible_account_fails_closed(self) -> None:
|
||||
route = ModelRoute(account_types=frozenset({"Team"}), allow_anonymous=False)
|
||||
with mock.patch(
|
||||
"services.model_service.model_catalog_service.route_for_model",
|
||||
return_value=route,
|
||||
), self.assertRaisesRegex(ModelUnavailableError, "team-only"):
|
||||
self.service.get_text_access_token(model="team-only")
|
||||
|
||||
|
||||
class TextProtocolRoutingTests(unittest.TestCase):
|
||||
def test_text_backend_passes_requested_model_to_account_selector(self) -> None:
|
||||
backend = mock.Mock()
|
||||
with (
|
||||
mock.patch.object(
|
||||
conversation.account_service,
|
||||
"get_text_access_token",
|
||||
return_value="pro",
|
||||
) as selector,
|
||||
mock.patch.object(conversation, "OpenAIBackendAPI", return_value=backend),
|
||||
):
|
||||
result = conversation.text_backend("pro-only")
|
||||
|
||||
self.assertIs(result, backend)
|
||||
selector.assert_called_once_with(model="pro-only")
|
||||
|
||||
def test_chat_completions_passes_requested_model_to_text_backend(self) -> None:
|
||||
body = {
|
||||
"model": "pro-chat",
|
||||
"messages": [{"role": "user", "content": "route chat"}],
|
||||
}
|
||||
with (
|
||||
mock.patch.object(openai_v1_chat_complete, "text_backend", return_value=object()) as backend,
|
||||
mock.patch.object(openai_v1_chat_complete, "collect_text", return_value="ok"),
|
||||
):
|
||||
openai_v1_chat_complete.handle(body)
|
||||
|
||||
backend.assert_called_once_with("pro-chat")
|
||||
|
||||
def test_responses_passes_requested_model_to_text_backend(self) -> None:
|
||||
body = {"model": "pro-response", "input": "route response"}
|
||||
with (
|
||||
mock.patch.object(openai_v1_response, "text_backend", return_value=object()) as backend,
|
||||
mock.patch.object(openai_v1_response, "stream_text_deltas", return_value=iter(["ok"])),
|
||||
):
|
||||
openai_v1_response.handle(body)
|
||||
|
||||
backend.assert_called_once_with("pro-response")
|
||||
|
||||
def test_anthropic_messages_passes_requested_model_to_account_selector(self) -> None:
|
||||
with (
|
||||
mock.patch.object(
|
||||
anthropic_v1_messages.account_service,
|
||||
"get_text_access_token",
|
||||
return_value="pro",
|
||||
) as selector,
|
||||
mock.patch.object(anthropic_v1_messages, "OpenAIBackendAPI"),
|
||||
):
|
||||
request = anthropic_v1_messages.message_request({
|
||||
"model": "pro-anthropic",
|
||||
"messages": [{"role": "user", "content": "route anthropic"}],
|
||||
})
|
||||
|
||||
self.assertEqual(request.model, "pro-anthropic")
|
||||
selector.assert_called_once_with(model="pro-anthropic")
|
||||
|
||||
def test_invalid_token_retry_keeps_requested_model_filter(self) -> None:
|
||||
initial_backend = SimpleNamespace(access_token="bad")
|
||||
request = conversation.ConversationRequest(
|
||||
model="pro-only",
|
||||
messages=[{"role": "user", "content": "hello"}],
|
||||
)
|
||||
|
||||
def fake_events(backend, **_kwargs):
|
||||
if backend.access_token == "bad":
|
||||
raise RuntimeError("token_invalidated")
|
||||
yield {"type": "conversation.delta", "delta": "ok"}
|
||||
|
||||
with (
|
||||
mock.patch.object(conversation, "OpenAIBackendAPI", side_effect=lambda access_token: SimpleNamespace(
|
||||
access_token=access_token,
|
||||
close=lambda: None,
|
||||
)),
|
||||
mock.patch.object(conversation, "conversation_events", side_effect=fake_events),
|
||||
mock.patch.object(
|
||||
conversation.account_service,
|
||||
"refresh_access_token",
|
||||
return_value="bad",
|
||||
),
|
||||
mock.patch.object(conversation.account_service, "remove_invalid_token"),
|
||||
mock.patch.object(
|
||||
conversation.account_service,
|
||||
"get_text_access_token",
|
||||
return_value="pro",
|
||||
) as selector,
|
||||
mock.patch.object(conversation.account_service, "mark_text_used"),
|
||||
):
|
||||
result = list(conversation.stream_text_deltas(initial_backend, request))
|
||||
|
||||
self.assertEqual(result, ["ok"])
|
||||
selector.assert_called_once_with(
|
||||
excluded_tokens={"bad"},
|
||||
model="pro-only",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user