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:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user