api: the orchestrator image now serves its own admin GUI (v0.2.83)

Bug 3 from the Digesta report: no published image was both a GPU-less
RunPod orchestrator and an admin GUI.

The cause was a split nobody had noticed: admin_router is the admin API
(POST /login, /admin/api/*), while the GUI PAGES — GET /admin, /login,
/admin/models … — live in frontproxy/ui_pages.py and were registered only
on the front app. An install running `uvicorn codai.api.app:app` as a pure
orchestrator therefore shipped the entire admin API with no way to reach
it: GET /admin answered 404 and GET /login 405, because only POST /login
existed. Reproduced both exactly before changing anything.

codai/api/app.py now registers those pages itself, against the same config
dir the admin API writes, so one image is orchestrator + GUI. Guarded: a
missing templates or config dir logs and leaves the API untouched, the
same contract the front has.

Verified: /admin 404->302 /login, GET /login 405->200, /admin/models and
/admin/runpod redirect to login, and under X-Forwarded-Prefix=/coderai the
redirect goes to /coderai/login. /healthz and /v1/models unaffected.

Bug 4 (the light app 500ing under X-Forwarded-Prefix) did NOT reproduce:
_ForwardedPrefixMiddleware is installed in this app and /coderai/healthz,
/coderai/v1/models and the GUI all answer correctly with the header set,
stripped or un-stripped. The reported 500 was most likely /admin itself
(404 before this fix) surfacing through their proxy.
Co-Authored-By: 's avatarClaude Opus 5 <noreply@anthropic.com>
parent fd6d7873
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
# Canonical product version for CoderAI — single source of truth. Both the API # Canonical product version for CoderAI — single source of truth. Both the API
# metadata and the admin web UI read from here. # metadata and the admin web UI read from here.
__version__ = "0.2.82" __version__ = "0.2.83"
__tagline__ = "Complete orchestration, distribution and escalation of remotizable advanced inference." __tagline__ = "Complete orchestration, distribution and escalation of remotizable advanced inference."
# Configure the CUDA caching allocator BEFORE torch is imported anywhere. # Configure the CUDA caching allocator BEFORE torch is imported anywhere.
......
...@@ -731,6 +731,33 @@ from codai.api.model_test import router as model_test_router ...@@ -731,6 +731,33 @@ from codai.api.model_test import router as model_test_router
app.include_router(model_test_router, tags=["Models"]) app.include_router(model_test_router, tags=["Models"])
app.include_router(admin_router, tags=["Admin"]) app.include_router(admin_router, tags=["Admin"])
# The admin router is the admin *API* (POST /login, /admin/api/*). The GUI PAGES
# — GET /admin, /login, /admin/models … — live in frontproxy/ui_pages.py and were
# only ever registered on the front app. An install that runs this app directly
# (a GPU-less RunPod orchestrator: `uvicorn codai.api.app:app`, no front, no
# engines) therefore served the whole admin API with no way to reach it:
# GET /admin answered 404 and GET /login 405, because only POST /login existed.
# Registering them here gives one image that is both the orchestrator and its GUI.
# Guarded: a missing templates dir or config dir must not take the API down with
# it, which is the same contract the front has.
def _register_local_ui_pages():
try:
from codai.frontproxy.ui_pages import register_ui_pages
# The same config dir every other entry point uses, so the GUI validates
# sessions against the one auth.json the admin API writes.
from codai.platform_paths import legacy_style_config_dir
from codai.admin import routes as _ar
cfg_dir = getattr(getattr(_ar, "config_manager", None), "config_dir", None) \
or legacy_style_config_dir()
if register_ui_pages(app, cfg_dir):
print(f"[api] admin GUI served locally from {cfg_dir}", flush=True)
except Exception as exc:
print(f"[api] admin GUI not served locally ({exc}); the API is unaffected",
flush=True)
_register_local_ui_pages()
@app.exception_handler(401) @app.exception_handler(401)
async def unauthorized_redirect(request: Request, exc: HTTPException): async def unauthorized_redirect(request: Request, exc: HTTPException):
......
...@@ -150,3 +150,51 @@ def test_the_ocr_image_bakes_a_surya_venv(): ...@@ -150,3 +150,51 @@ def test_the_ocr_image_bakes_a_surya_venv():
assert "pillow<11" in body, "without the cap the resolver may pull pillow 12 in" assert "pillow<11" in body, "without the cap the resolver may pull pillow 12 in"
# The check must prove the pillow pin held, not just that surya imports. # The check must prove the pillow pin held, not just that surya imports.
assert "pillow" in chk.read_text().lower() assert "pillow" in chk.read_text().lower()
# ------------------------------------- bug 3: one image = orchestrator + GUI
def test_the_light_app_serves_the_admin_gui_pages():
"""admin_router is the admin API (POST /login, /admin/api/*). The GUI pages
were only on the front app, so a bare `uvicorn codai.api.app:app`
orchestrator answered 404 for /admin and 405 for GET /login."""
import os
os.environ.setdefault("CODERAI_SKIP_HEAVY", "1")
from fastapi.testclient import TestClient
from codai.api.app import app
c = TestClient(app, raise_server_exceptions=False)
r = c.get("/admin", follow_redirects=False)
assert r.status_code in (200, 302, 303), f"/admin -> {r.status_code}"
r = c.get("/login", follow_redirects=False)
assert r.status_code == 200, f"GET /login -> {r.status_code} (405 = API only)"
for page in ("/admin/models", "/admin/runpod", "/admin/settings"):
assert c.get(page, follow_redirects=False).status_code in (200, 302, 303), page
def test_the_gui_redirect_carries_the_forwarded_prefix():
"""Served under /coderai/, the login redirect must point inside the prefix or
the browser leaves the deployment."""
import os
os.environ.setdefault("CODERAI_SKIP_HEAVY", "1")
from fastapi.testclient import TestClient
from codai.api.app import app
c = TestClient(app, raise_server_exceptions=False)
r = c.get("/admin", headers={"X-Forwarded-Prefix": "/coderai"},
follow_redirects=False)
assert r.status_code in (302, 303)
assert r.headers.get("location", "").startswith("/coderai/"), r.headers.get("location")
def test_the_api_survives_a_gui_registration_failure():
"""A missing templates or config dir must not take the orchestrator down."""
src = (ROOT / "codai/api/app.py").read_text()
blk = src.split("def _register_local_ui_pages")[1].split("_register_local_ui_pages()")[0]
assert "except Exception" in blk
assert "the API is unaffected" in blk
def test_the_forwarded_prefix_middleware_is_installed():
src = (ROOT / "codai/api/app.py").read_text()
assert "class _ForwardedPrefixMiddleware" in src
assert "app.add_middleware(_ForwardedPrefixMiddleware)" in src
assert "x-forwarded-prefix" in src
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment