Add admin-unlimited workspaces, AI gateway model fallback, and real streaming/thinking control
Admin-unlimited Dev Workspaces: an admin-owned workspace is now exempt from the max-workspace-count limit, the max-tunnel-count limit, and the whole idle-stop/idle-warn/retention-delete lifecycle. Resolved once in quota.resolve() as Limits.unlimited (owner uid checked against get_admin_uids()), consumed at the three enforcement points (provision.ensure, provision.publish_tunnel, WorkspaceService._advance_lifecycle). Also hardens get_admin_uids()/get_primary_admin_uid() against a partially-schemaed users table (uid/role column guard), which a fresh test/init_db() path could hit. AI gateway per-model automatic fallback: any gateway_models route (chat/embed/image) can now name a fallback_model, picked on /admin/gateway from a select box of other configured public model names of the same kind only (never an internal upstream model id). When a route fails after its own retries are exhausted, the gateway retries once, automatically, against the fallback's own provider/pricing/key, before any bytes reach the client (including for a streaming response). One hop only, no chains or cycles; self-reference and cross-kind fallbacks are rejected at write time. AI gateway real upstream streaming and thinking-default control: stream:true is now forwarded to the upstream and relayed to the client as real SSE chunks (measured TTFT/inter-token latency) instead of a simulated split response, and every chat/vision call explicitly disables model "thinking" by default (admin-overridable via gateway_thinking), with per-dialect handling for DeepSeek, OpenRouter, and Ollama. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TjdKTWgWpW2SMNW8SFqxz5
This commit is contained in:
@@ -109,6 +109,17 @@ def test_workspace_quota_blocks_beyond_limit():
|
||||
set_setting("workspace_max_per_user", "2")
|
||||
|
||||
|
||||
def test_workspace_quota_does_not_apply_to_an_admin_owner(app_server, seeded_db):
|
||||
admin = _seeded_user("alice_test")
|
||||
set_setting("workspace_max_per_user", "1")
|
||||
try:
|
||||
run_async(provision.ensure(_project("p-admin-a"), admin))
|
||||
run_async(provision.ensure(_project("p-admin-b"), admin))
|
||||
assert provision.count_for_owner(admin["uid"]) == 2
|
||||
finally:
|
||||
set_setting("workspace_max_per_user", "2")
|
||||
|
||||
|
||||
def test_tunnel_revives_rather_than_duplicates():
|
||||
instance = _instance()
|
||||
first = tunnels.create(instance, "web", 8080, OWNER)
|
||||
@@ -344,6 +355,21 @@ def test_auto_delete_off_keeps_an_expired_workspace():
|
||||
assert _reload(instance["uid"]) is not None
|
||||
|
||||
|
||||
def test_advance_lifecycle_never_touches_an_admin_owned_workspace(app_server, seeded_db):
|
||||
admin = _seeded_user("alice_test")
|
||||
instance = _instance(
|
||||
status="running", desired_state="running", workspace_owner_uid=admin["uid"]
|
||||
)
|
||||
store.update_instance(
|
||||
instance["uid"], {"last_active_at": _idle_for(60 * 24 * 400)}
|
||||
)
|
||||
_lifecycle([_reload(instance["uid"])])
|
||||
survivor = _reload(instance["uid"])
|
||||
assert survivor is not None
|
||||
assert survivor["desired_state"] == "running"
|
||||
assert not survivor["idle_warned_at"]
|
||||
|
||||
|
||||
def test_opening_a_workspace_publishes_the_editor_tunnel_automatically():
|
||||
project = _project()
|
||||
user = {"uid": OWNER, "username": "owner"}
|
||||
@@ -1018,6 +1044,18 @@ def test_publish_tunnel_enforces_the_tunnel_quota():
|
||||
set_setting("workspace_max_tunnels", "5")
|
||||
|
||||
|
||||
def test_publish_tunnel_quota_does_not_apply_to_an_admin_owner(app_server, seeded_db):
|
||||
admin = _seeded_user("alice_test")
|
||||
set_setting("workspace_max_tunnels", "1")
|
||||
try:
|
||||
instance = _instance(workspace_owner_uid=admin["uid"])
|
||||
provision.publish_tunnel(instance, "web", 3100, admin["uid"])
|
||||
provision.publish_tunnel(instance, "api", 3101, admin["uid"])
|
||||
assert tunnels.count_for_instance(instance["uid"]) == 2
|
||||
finally:
|
||||
set_setting("workspace_max_tunnels", "5")
|
||||
|
||||
|
||||
def test_publish_tunnel_refuses_a_port_outside_the_valid_range():
|
||||
instance = _instance()
|
||||
with pytest.raises(WorkspaceError):
|
||||
|
||||
Reference in New Issue
Block a user