Add personal notes, DeepSearch history, backup offload, and gateway auth throttling
DevPlace CI / test (push) Failing after 28m20s
DevPlace CI / test (push) Failing after 28m20s
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:
@@ -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", "")
|
||||
|
||||
Reference in New Issue
Block a user