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:
@@ -55,6 +55,91 @@ def test_chat_route_overlay_and_economy(local_db):
|
||||
_cleanup(["utor"], ["ut-chat"])
|
||||
|
||||
|
||||
def test_resolve_fallback_returns_none_without_a_configured_fallback(local_db):
|
||||
r.model_store.set(r.ModelRouteIn(source_model="fb-primary", target_model="v/a"))
|
||||
try:
|
||||
assert r.resolve_fallback("fb-primary", "chat") is None
|
||||
finally:
|
||||
_cleanup([], ["fb-primary"])
|
||||
|
||||
|
||||
def test_resolve_fallback_returns_none_for_an_unknown_source(local_db):
|
||||
assert r.resolve_fallback("ghost-fb-source", "chat") is None
|
||||
|
||||
|
||||
def test_resolve_fallback_rejects_self_reference(local_db):
|
||||
with pytest.raises(ValidationError):
|
||||
r.ModelRouteIn(
|
||||
source_model="fb-self", target_model="v/a", fallback_model="fb-self"
|
||||
)
|
||||
|
||||
|
||||
def test_resolve_fallback_returns_the_configured_route(local_db):
|
||||
r.model_store.set(
|
||||
r.ModelRouteIn(
|
||||
source_model="fb-backup", target_model="v/backup", kind="chat"
|
||||
)
|
||||
)
|
||||
r.model_store.set(
|
||||
r.ModelRouteIn(
|
||||
source_model="fb-primary",
|
||||
target_model="v/primary",
|
||||
kind="chat",
|
||||
fallback_model="fb-backup",
|
||||
)
|
||||
)
|
||||
try:
|
||||
fallback = r.resolve_fallback("fb-primary", "chat")
|
||||
assert fallback is not None
|
||||
assert fallback.source_model == "fb-backup"
|
||||
assert fallback.target_model == "v/backup"
|
||||
finally:
|
||||
_cleanup([], ["fb-primary", "fb-backup"])
|
||||
|
||||
|
||||
def test_resolve_fallback_ignores_an_inactive_fallback_route(local_db):
|
||||
r.model_store.set(
|
||||
r.ModelRouteIn(
|
||||
source_model="fb-backup-off",
|
||||
target_model="v/backup",
|
||||
kind="chat",
|
||||
is_active=False,
|
||||
)
|
||||
)
|
||||
r.model_store.set(
|
||||
r.ModelRouteIn(
|
||||
source_model="fb-primary-2",
|
||||
target_model="v/primary",
|
||||
kind="chat",
|
||||
fallback_model="fb-backup-off",
|
||||
)
|
||||
)
|
||||
try:
|
||||
assert r.resolve_fallback("fb-primary-2", "chat") is None
|
||||
finally:
|
||||
_cleanup([], ["fb-primary-2", "fb-backup-off"])
|
||||
|
||||
|
||||
def test_resolve_fallback_ignores_a_fallback_of_a_different_kind(local_db):
|
||||
r.model_store.set(
|
||||
r.ModelRouteIn(
|
||||
source_model="fb-backup-embed", target_model="v/e", kind="embed"
|
||||
)
|
||||
)
|
||||
r.model_store.set(
|
||||
r.ModelRouteIn(
|
||||
source_model="fb-primary-3",
|
||||
target_model="v/primary",
|
||||
kind="chat",
|
||||
fallback_model="fb-backup-embed",
|
||||
)
|
||||
)
|
||||
try:
|
||||
assert r.resolve_fallback("fb-primary-3", "chat") is None
|
||||
finally:
|
||||
_cleanup([], ["fb-primary-3", "fb-backup-embed"])
|
||||
|
||||
|
||||
def test_vision_merge_overlay(local_db):
|
||||
r.provider_store.set(
|
||||
r.ProviderIn(
|
||||
|
||||
Reference in New Issue
Block a user