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:
2026-09-03 08:47:57 +02:00
co-authored by Claude Sonnet 5
parent 8ae3f628c7
commit a693a6f4d8
33 changed files with 1310 additions and 110 deletions
+89
View File
@@ -135,6 +135,95 @@ def test_model_route_off_peak_requires_both_start_and_end(seeded_db):
assert only_end.json()["ok"] is False
def test_model_route_fallback_round_trip(seeded_db):
admin = admin_session(seeded_db)
backup = _unique_gateway("fb-backup")
primary = _unique_gateway("fb-primary")
created_backup = admin.post(
f"{BASE_URL}/admin/gateway/models",
json={"source_model": backup, "target_model": "vendor/backup", "kind": "chat"},
)
assert created_backup.status_code == 200, created_backup.text[:300]
created_primary = admin.post(
f"{BASE_URL}/admin/gateway/models",
json={
"source_model": primary,
"target_model": "vendor/primary",
"kind": "chat",
"fallback_model": backup,
},
)
assert created_primary.status_code == 200, created_primary.text[:300]
assert created_primary.json()["model"]["fallback_model"] == backup
listed = admin.get(f"{BASE_URL}/admin/gateway/models").json()
row = next(m for m in listed["models"] if m["source_model"] == primary)
assert row["fallback_model"] == backup
admin.delete(f"{BASE_URL}/admin/gateway/models/{primary}")
admin.delete(f"{BASE_URL}/admin/gateway/models/{backup}")
def test_model_route_fallback_must_reference_an_existing_route(seeded_db):
admin = admin_session(seeded_db)
source = _unique_gateway("fb-ghost")
response = admin.post(
f"{BASE_URL}/admin/gateway/models",
json={
"source_model": source,
"target_model": "vendor/z",
"kind": "chat",
"fallback_model": _unique_gateway("does-not-exist"),
},
)
assert response.status_code == 400
assert response.json()["ok"] is False
def test_model_route_fallback_must_be_the_same_kind(seeded_db):
admin = admin_session(seeded_db)
embed_route = _unique_gateway("fb-embed")
chat_route = _unique_gateway("fb-chat")
admin.post(
f"{BASE_URL}/admin/gateway/models",
json={"source_model": embed_route, "target_model": "vendor/e", "kind": "embed"},
)
response = admin.post(
f"{BASE_URL}/admin/gateway/models",
json={
"source_model": chat_route,
"target_model": "vendor/c",
"kind": "chat",
"fallback_model": embed_route,
},
)
assert response.status_code == 400
assert response.json()["ok"] is False
admin.delete(f"{BASE_URL}/admin/gateway/models/{embed_route}")
def test_model_route_fallback_rejects_self_reference(seeded_db):
admin = admin_session(seeded_db)
source = _unique_gateway("fb-self")
response = admin.post(
f"{BASE_URL}/admin/gateway/models",
json={
"source_model": source,
"target_model": "vendor/z",
"kind": "chat",
"fallback_model": source,
},
)
assert response.status_code == 400
assert response.json()["ok"] is False
def test_model_route_validation(seeded_db):
admin = admin_session(seeded_db)
missing_target = admin.post(
+38
View File
@@ -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):