forked from retoor/devplacepy
Add personal notes, DeepSearch history, backup offload, and gateway auth throttling
Also streamline the top navigation: drop the Tools dropdown, make Quizzes and Battles icon-only entries, and remove the Workspace, Containers and Editor entry points from the project detail page.
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
from devplacepy.services.openai_gateway import auth_throttle
|
||||
|
||||
|
||||
def test_is_throttled_false_below_threshold():
|
||||
auth_throttle.clear()
|
||||
try:
|
||||
for _ in range(9):
|
||||
auth_throttle.record_failure("1.1.1.1", 60)
|
||||
assert auth_throttle.is_throttled("1.1.1.1", 10, 60) is False
|
||||
finally:
|
||||
auth_throttle.clear()
|
||||
|
||||
|
||||
def test_is_throttled_true_at_threshold():
|
||||
auth_throttle.clear()
|
||||
try:
|
||||
for _ in range(10):
|
||||
auth_throttle.record_failure("2.2.2.2", 60)
|
||||
assert auth_throttle.is_throttled("2.2.2.2", 10, 60) is True
|
||||
finally:
|
||||
auth_throttle.clear()
|
||||
|
||||
|
||||
def test_is_throttled_scoped_per_ip():
|
||||
auth_throttle.clear()
|
||||
try:
|
||||
for _ in range(10):
|
||||
auth_throttle.record_failure("3.3.3.3", 60)
|
||||
assert auth_throttle.is_throttled("3.3.3.3", 10, 60) is True
|
||||
assert auth_throttle.is_throttled("4.4.4.4", 10, 60) is False
|
||||
finally:
|
||||
auth_throttle.clear()
|
||||
|
||||
|
||||
def test_record_failure_returns_running_count():
|
||||
auth_throttle.clear()
|
||||
try:
|
||||
assert auth_throttle.record_failure("5.5.5.5", 60) == 1
|
||||
assert auth_throttle.record_failure("5.5.5.5", 60) == 2
|
||||
assert auth_throttle.record_failure("5.5.5.5", 60) == 3
|
||||
finally:
|
||||
auth_throttle.clear()
|
||||
|
||||
|
||||
def test_throttle_resets_after_window_passes(monkeypatch):
|
||||
auth_throttle.clear()
|
||||
try:
|
||||
now = [1000.0]
|
||||
monkeypatch.setattr(auth_throttle.time, "time", lambda: now[0])
|
||||
for _ in range(10):
|
||||
auth_throttle.record_failure("6.6.6.6", 60)
|
||||
assert auth_throttle.is_throttled("6.6.6.6", 10, 60) is True
|
||||
now[0] += 61
|
||||
assert auth_throttle.is_throttled("6.6.6.6", 10, 60) is False
|
||||
finally:
|
||||
auth_throttle.clear()
|
||||
|
||||
|
||||
def test_reset_clears_one_ip():
|
||||
auth_throttle.clear()
|
||||
try:
|
||||
for _ in range(10):
|
||||
auth_throttle.record_failure("7.7.7.7", 60)
|
||||
auth_throttle.reset("7.7.7.7")
|
||||
assert auth_throttle.is_throttled("7.7.7.7", 10, 60) is False
|
||||
finally:
|
||||
auth_throttle.clear()
|
||||
@@ -7,6 +7,7 @@ from devplacepy.database import get_table, set_setting
|
||||
from devplacepy.utils import generate_uid
|
||||
import devplacepy.services.openai_gateway.gateway as gwmod
|
||||
from devplacepy.services.openai_gateway import GatewayService
|
||||
from devplacepy.services.openai_gateway import auth_throttle
|
||||
class FakeResp_openai_gateway:
|
||||
def __init__(
|
||||
self,
|
||||
@@ -257,6 +258,7 @@ def test_authorize_static_access_key(local_db):
|
||||
finally:
|
||||
set_setting("gateway_access_key", "")
|
||||
set_setting("gateway_require_auth", "1")
|
||||
auth_throttle.clear()
|
||||
|
||||
|
||||
def test_authorize_admin_and_user_toggles(local_db):
|
||||
@@ -282,6 +284,7 @@ def test_authorize_admin_and_user_toggles(local_db):
|
||||
set_setting("gateway_allow_users", "1")
|
||||
set_setting("gateway_require_auth", "1")
|
||||
set_setting("gateway_access_key", "")
|
||||
auth_throttle.clear()
|
||||
|
||||
|
||||
def test_authorize_require_auth_off_is_open(local_db):
|
||||
@@ -293,6 +296,124 @@ def test_authorize_require_auth_off_is_open(local_db):
|
||||
set_setting("gateway_require_auth", "1")
|
||||
|
||||
|
||||
def test_authorize_throttles_repeated_failed_auth_from_one_ip(local_db):
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
|
||||
set_setting("gateway_require_auth", "1")
|
||||
set_setting("gateway_access_key", "")
|
||||
set_setting("gateway_auth_throttle_max_failures", "3")
|
||||
set_setting("gateway_auth_throttle_window_seconds", "60")
|
||||
auth_throttle.clear()
|
||||
try:
|
||||
svc = GatewayService()
|
||||
ip = "203.0.113.10"
|
||||
for _ in range(3):
|
||||
assert (
|
||||
svc.authorize(_make_request_openai_gateway(headers={"X-Real-IP": ip}))
|
||||
is False
|
||||
)
|
||||
with pytest.raises(HTTPException) as excinfo:
|
||||
svc.authorize(_make_request_openai_gateway(headers={"X-Real-IP": ip}))
|
||||
assert excinfo.value.status_code == 429
|
||||
assert "Retry-After" in excinfo.value.headers
|
||||
finally:
|
||||
set_setting("gateway_auth_throttle_max_failures", "10")
|
||||
set_setting("gateway_auth_throttle_window_seconds", "60")
|
||||
set_setting("gateway_require_auth", "1")
|
||||
auth_throttle.clear()
|
||||
|
||||
|
||||
def test_authorize_throttle_does_not_affect_other_ips(local_db):
|
||||
set_setting("gateway_require_auth", "1")
|
||||
set_setting("gateway_access_key", "")
|
||||
set_setting("gateway_auth_throttle_max_failures", "3")
|
||||
set_setting("gateway_auth_throttle_window_seconds", "60")
|
||||
auth_throttle.clear()
|
||||
try:
|
||||
svc = GatewayService()
|
||||
throttled_ip = "203.0.113.20"
|
||||
other_ip = "203.0.113.21"
|
||||
for _ in range(3):
|
||||
svc.authorize(_make_request_openai_gateway(headers={"X-Real-IP": throttled_ip}))
|
||||
assert auth_throttle.is_throttled(throttled_ip, 3, 60) is True
|
||||
assert (
|
||||
svc.authorize(_make_request_openai_gateway(headers={"X-Real-IP": other_ip}))
|
||||
is False
|
||||
)
|
||||
assert auth_throttle.is_throttled(other_ip, 3, 60) is False
|
||||
finally:
|
||||
set_setting("gateway_auth_throttle_max_failures", "10")
|
||||
set_setting("gateway_auth_throttle_window_seconds", "60")
|
||||
set_setting("gateway_require_auth", "1")
|
||||
auth_throttle.clear()
|
||||
|
||||
|
||||
def test_authorize_valid_credentials_succeed_from_throttled_ip(local_db):
|
||||
set_setting("gateway_require_auth", "1")
|
||||
set_setting("gateway_access_key", "")
|
||||
set_setting("gateway_allow_users", "1")
|
||||
set_setting("gateway_auth_throttle_max_failures", "3")
|
||||
set_setting("gateway_auth_throttle_window_seconds", "60")
|
||||
auth_throttle.clear()
|
||||
try:
|
||||
svc = GatewayService()
|
||||
ip = "203.0.113.30"
|
||||
_, member_key = _make_admin_openai_gateway(role="Member")
|
||||
for _ in range(3):
|
||||
svc.authorize(_make_request_openai_gateway(headers={"X-Real-IP": ip}))
|
||||
assert auth_throttle.is_throttled(ip, 3, 60) is True
|
||||
assert (
|
||||
svc.authorize(
|
||||
_make_request_openai_gateway(
|
||||
headers={"X-Real-IP": ip, "X-API-KEY": member_key}
|
||||
)
|
||||
)
|
||||
is True
|
||||
)
|
||||
set_setting("gateway_access_key", "topsecret")
|
||||
assert (
|
||||
svc.authorize(
|
||||
_make_request_openai_gateway(
|
||||
headers={"X-Real-IP": ip, "X-API-KEY": "topsecret"}
|
||||
)
|
||||
)
|
||||
is True
|
||||
)
|
||||
finally:
|
||||
set_setting("gateway_access_key", "")
|
||||
set_setting("gateway_auth_throttle_max_failures", "10")
|
||||
set_setting("gateway_auth_throttle_window_seconds", "60")
|
||||
set_setting("gateway_require_auth", "1")
|
||||
auth_throttle.clear()
|
||||
|
||||
|
||||
def test_authorize_throttle_resets_after_window(local_db, monkeypatch):
|
||||
set_setting("gateway_require_auth", "1")
|
||||
set_setting("gateway_access_key", "")
|
||||
set_setting("gateway_auth_throttle_max_failures", "3")
|
||||
set_setting("gateway_auth_throttle_window_seconds", "60")
|
||||
auth_throttle.clear()
|
||||
try:
|
||||
now = [2000.0]
|
||||
monkeypatch.setattr(auth_throttle.time, "time", lambda: now[0])
|
||||
svc = GatewayService()
|
||||
ip = "203.0.113.40"
|
||||
for _ in range(3):
|
||||
svc.authorize(_make_request_openai_gateway(headers={"X-Real-IP": ip}))
|
||||
assert auth_throttle.is_throttled(ip, 3, 60) is True
|
||||
now[0] += 61
|
||||
assert (
|
||||
svc.authorize(_make_request_openai_gateway(headers={"X-Real-IP": ip}))
|
||||
is False
|
||||
)
|
||||
finally:
|
||||
set_setting("gateway_auth_throttle_max_failures", "10")
|
||||
set_setting("gateway_auth_throttle_window_seconds", "60")
|
||||
set_setting("gateway_require_auth", "1")
|
||||
auth_throttle.clear()
|
||||
|
||||
|
||||
class FakeEmbedClient_openai_gateway:
|
||||
def __init__(self, *a, **k):
|
||||
self.calls = []
|
||||
@@ -381,6 +502,36 @@ def test_embeddings_disabled_returns_503(local_db, monkeypatch):
|
||||
assert resp.status_code == 503
|
||||
|
||||
|
||||
def test_embeddings_missing_key_returns_clean_error_not_unauthenticated_call(
|
||||
local_db, monkeypatch
|
||||
):
|
||||
monkeypatch.setattr(gwmod.httpx, "AsyncClient", FakeEmbedClient_openai_gateway)
|
||||
monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)
|
||||
monkeypatch.delenv("DEEPSEEK_API_KEY", raising=False)
|
||||
set_setting("gateway_api_key", "")
|
||||
set_setting("gateway_vision_key", "")
|
||||
set_setting("gateway_embed_key", "")
|
||||
try:
|
||||
svc = GatewayService()
|
||||
cfg = svc.effective_config()
|
||||
assert cfg["gateway_embed_key"] == ""
|
||||
cfg["gateway_embed_enabled"] = True
|
||||
rt = svc.runtime()
|
||||
resp = run_async(
|
||||
rt.handle_embeddings(
|
||||
{"input": "hello"}, cfg, ("guest", "no_key_probe"), "test", "default"
|
||||
)
|
||||
)
|
||||
assert resp.status_code == 503
|
||||
payload = json.loads(bytes(resp.body).decode())
|
||||
assert payload["error"]["type"] == "embeddings_not_configured"
|
||||
assert rt._client is None
|
||||
finally:
|
||||
set_setting("gateway_api_key", "")
|
||||
set_setting("gateway_vision_key", "")
|
||||
set_setting("gateway_embed_key", "")
|
||||
|
||||
|
||||
class FakeImageClient_openai_gateway:
|
||||
def __init__(self, *a, **k):
|
||||
self.calls = []
|
||||
@@ -1942,3 +2093,121 @@ def test_hostile_upstream_model_header_is_ignored_end_to_end(local_db, monkeypat
|
||||
)
|
||||
)
|
||||
assert resp.headers["X-Gateway-Model"] == "deepseek-chat"
|
||||
|
||||
|
||||
def test_bypass_preamble_skips_the_operator_preamble_when_allowed(local_db, monkeypatch):
|
||||
monkeypatch.setattr(gwmod.httpx, "AsyncClient", FakeClient_openai_gateway)
|
||||
svc = GatewayService()
|
||||
cfg = svc.effective_config()
|
||||
cfg["gateway_system_preamble"] = "OPERATOR-PREAMBLE-TEXT"
|
||||
rt = svc.runtime()
|
||||
run_async(
|
||||
rt.handle_chat(
|
||||
{
|
||||
"messages": [
|
||||
{"role": "system", "content": "Reply with strict JSON only."},
|
||||
{"role": "user", "content": "hi"},
|
||||
],
|
||||
"bypass_preamble": True,
|
||||
},
|
||||
cfg,
|
||||
("internal", "devii"),
|
||||
"test",
|
||||
"default",
|
||||
bypass_allowed=True,
|
||||
)
|
||||
)
|
||||
sent = rt._client.calls[-1][1]
|
||||
system_message = sent["messages"][0]
|
||||
assert system_message["role"] == "system"
|
||||
assert "OPERATOR-PREAMBLE-TEXT" not in system_message["content"]
|
||||
assert "Current date" in system_message["content"]
|
||||
assert "Reply with strict JSON only." in system_message["content"]
|
||||
assert "bypass_preamble" not in sent
|
||||
|
||||
|
||||
def test_bypass_preamble_ignored_without_internal_credentials(local_db, monkeypatch):
|
||||
monkeypatch.setattr(gwmod.httpx, "AsyncClient", FakeClient_openai_gateway)
|
||||
svc = GatewayService()
|
||||
cfg = svc.effective_config()
|
||||
cfg["gateway_system_preamble"] = "OPERATOR-PREAMBLE-TEXT"
|
||||
rt = svc.runtime()
|
||||
run_async(
|
||||
rt.handle_chat(
|
||||
{"messages": [{"role": "user", "content": "hi"}], "bypass_preamble": True},
|
||||
cfg,
|
||||
("user", "someuser"),
|
||||
"test",
|
||||
"default",
|
||||
bypass_allowed=False,
|
||||
)
|
||||
)
|
||||
sent = rt._client.calls[-1][1]
|
||||
system_message = sent["messages"][0]
|
||||
assert "OPERATOR-PREAMBLE-TEXT" in system_message["content"]
|
||||
assert "bypass_preamble" not in sent
|
||||
|
||||
|
||||
def test_bypass_preamble_keeps_the_clients_own_system_content(local_db, monkeypatch):
|
||||
monkeypatch.setattr(gwmod.httpx, "AsyncClient", FakeClient_openai_gateway)
|
||||
svc = GatewayService()
|
||||
cfg = svc.effective_config()
|
||||
cfg["gateway_system_preamble"] = "OPERATOR-PREAMBLE-TEXT"
|
||||
rt = svc.runtime()
|
||||
run_async(
|
||||
rt.handle_chat(
|
||||
{
|
||||
"messages": [
|
||||
{"role": "system", "content": "Grade strictly. Reply JSON only."},
|
||||
{"role": "user", "content": "hi"},
|
||||
],
|
||||
"bypass_preamble": True,
|
||||
},
|
||||
cfg,
|
||||
("internal", "devii"),
|
||||
"test",
|
||||
"default",
|
||||
bypass_allowed=True,
|
||||
)
|
||||
)
|
||||
sent = rt._client.calls[-1][1]
|
||||
system_message = sent["messages"][0]
|
||||
assert "OPERATOR-PREAMBLE-TEXT" not in system_message["content"]
|
||||
assert "Grade strictly. Reply JSON only." in system_message["content"]
|
||||
|
||||
|
||||
def test_internal_bypass_allowed_requires_the_exact_internal_key(local_db):
|
||||
set_setting("gateway_internal_key", "test-internal-secret")
|
||||
try:
|
||||
svc = GatewayService()
|
||||
cfg = svc.effective_config()
|
||||
assert (
|
||||
svc.internal_bypass_allowed(
|
||||
_make_request_openai_gateway(
|
||||
headers={"X-Gateway-Internal-Key": "test-internal-secret"}
|
||||
),
|
||||
cfg,
|
||||
)
|
||||
is True
|
||||
)
|
||||
assert (
|
||||
svc.internal_bypass_allowed(
|
||||
_make_request_openai_gateway(
|
||||
headers={"X-Gateway-Internal-Key": "wrong-secret"}
|
||||
),
|
||||
cfg,
|
||||
)
|
||||
is False
|
||||
)
|
||||
assert (
|
||||
svc.internal_bypass_allowed(
|
||||
_make_request_openai_gateway(
|
||||
headers={"Authorization": "Bearer test-internal-secret"}
|
||||
),
|
||||
cfg,
|
||||
)
|
||||
is False
|
||||
)
|
||||
assert svc.internal_bypass_allowed(_make_request_openai_gateway(), cfg) is False
|
||||
finally:
|
||||
set_setting("gateway_internal_key", "")
|
||||
|
||||
@@ -267,6 +267,32 @@ def test_embed_route_and_kind_isolation(local_db):
|
||||
_cleanup(["utemb"], ["ut-embed"])
|
||||
|
||||
|
||||
def test_embed_overlay_blank_provider_key_preserves_top_level_key(local_db):
|
||||
r.provider_store.set(
|
||||
r.ProviderIn(
|
||||
name="utemblank",
|
||||
base_url="https://emb.example/v1/chat/completions",
|
||||
api_key="",
|
||||
)
|
||||
)
|
||||
r.model_store.set(
|
||||
r.ModelRouteIn(
|
||||
source_model="ut-embed-blank",
|
||||
provider="utemblank",
|
||||
target_model="vendor/embed",
|
||||
kind="embed",
|
||||
)
|
||||
)
|
||||
try:
|
||||
base_cfg = {"gateway_embed_key": "top-level-fallback-key"}
|
||||
overlay = r.embed_overlay("ut-embed-blank", base_cfg)
|
||||
assert "gateway_embed_key" not in overlay
|
||||
merged = {**base_cfg, **overlay}
|
||||
assert merged["gateway_embed_key"] == "top-level-fallback-key"
|
||||
finally:
|
||||
_cleanup(["utemblank"], ["ut-embed-blank"])
|
||||
|
||||
|
||||
def test_inactive_route_ignored(local_db):
|
||||
r.model_store.set(
|
||||
r.ModelRouteIn(
|
||||
|
||||
Reference in New Issue
Block a user