fix: model load/unload 401 (front auth probe); township refs honor run-page res/steps

1. Model load/unload (and every is_admin-gated front action) returned 401: is_admin
   probed prim.url + /admin/api/status on the engine, but that route is front-only
   (removed from the engine in def78c18), so it 404'd → never 200 → Unauthorized. Add
   an engine-side admin-gated /admin/api/whoami and point is_admin at it.

2. Township reference generation (characters/environments pages — _run_regen_job and
   _run_create_profile_job) hardcoded 768x512/512x512 + 28 steps, ignoring the Run
   page. Add _ref_gen_res_steps(args) (honors keyframe_size/keyframe_steps, re-read
   from the saved config so edits apply without restart) and use it at the reference
   generators; generate_character/generate_environment now forward `steps` (the
   server already accepts it). Keyframes/match videos already honored the config.
Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RdMufYvtTbtGDWsiZVoXce
parent 1f586669
......@@ -16,7 +16,7 @@
# Canonical product version for CoderAI — single source of truth. Both the API
# metadata and the admin web UI read from here.
__version__ = "0.1.15"
__version__ = "0.1.16"
# Configure the CUDA caching allocator BEFORE torch is imported anywhere.
# expandable_segments lets the allocator return freed pages to the driver even
......
......@@ -341,6 +341,17 @@ async def change_password(
return RedirectResponse(url=_url(request, "/admin"), status_code=302)
@router.get("/admin/api/whoami", summary="Validate the current admin session")
async def api_whoami(username: str = Depends(require_admin)):
"""Tiny admin-gated probe: 200 + username when the caller's session is a valid
admin session; 401/403 otherwise. The front calls this on the primary engine
(which owns sessions) to authorize front-handled admin actions — model
load/unload, engine management, etc. NOT a dead UI handler: do not remove (the
front-owned /admin/api/status was removed in def78c1, which silently broke this
auth path). Cheap and process-local."""
return {"ok": True, "user": username}
@router.get("/admin/api/models", summary="List configured models")
async def api_list_models(username: str = Depends(require_admin)):
"""List all configured models with details."""
......
......@@ -830,13 +830,18 @@ class FrontProxy:
async def is_admin(self, request: Request) -> bool:
"""Authorize a front-handled admin action by validating the caller's session
against the primary engine (which owns sessions). 200 → authorized."""
against the primary engine (which owns sessions). 200 → authorized.
Probes the engine's /admin/api/whoami (admin-gated). NOTE: do not point this
at /admin/api/status — that route is front-only (removed from the engine in
def78c1), so probing it on the engine 404s and silently fails every
front-handled admin action (model load/unload, engine mgmt)."""
prim = self.registry.primary()
if prim is None:
return False
try:
headers = self._filter_headers(request.headers, _DROP_REQ)
r = await self._short.get(prim.url + "/admin/api/status", headers=headers)
r = await self._short.get(prim.url + "/admin/api/whoami", headers=headers)
return r.status_code == 200
except Exception:
return False
......
......@@ -813,20 +813,28 @@ class CoderAIClient:
return []
def generate_character(self, name: str, prompt: str, description: str,
model: str, n: int = 4, size: str = "512x512") -> dict:
return self._post("/v1/characters/generate", {
model: str, n: int = 4, size: str = "512x512",
steps: int = None) -> dict:
body = {
"name": name, "prompt": prompt, "description": description,
"model": model, "n": n,
"width": int(size.split("x")[0]), "height": int(size.split("x")[1]),
})
}
if steps:
body["steps"] = int(steps)
return self._post("/v1/characters/generate", body)
def generate_environment(self, name: str, prompt: str, description: str,
model: str, n: int = 3, size: str = "768x512") -> dict:
return self._post("/v1/environments/generate", {
model: str, n: int = 3, size: str = "768x512",
steps: int = None) -> dict:
body = {
"name": name, "prompt": prompt, "description": description,
"model": model, "n": n,
"width": int(size.split("x")[0]), "height": int(size.split("x")[1]),
})
}
if steps:
body["steps"] = int(steps)
return self._post("/v1/environments/generate", body)
def generate_image(self, prompt: str, model: str,
character_profiles: list = None,
......@@ -2219,6 +2227,26 @@ def save_config(path: str, args) -> dict:
return data
def _ref_gen_res_steps(args):
"""(size, steps) for reference / keyframe image generation, honouring the
resolution + steps configured on the Run page (keyframe_size / keyframe_steps).
Re-reads the saved config file when available so Run-page edits apply without a
restart; falls back to the launch args, then sane defaults. This is what makes
the characters/environments/matches reference generators use the same resolution
and steps as keyframes instead of hardcoded 512/768 + 28 steps."""
size = getattr(args, "keyframe_size", None) or "832x480"
steps = getattr(args, "keyframe_steps", None) or 28
try:
cf = getattr(args, "config", None)
if cf and os.path.isfile(cf):
c = load_config(cf)
size = c.get("keyframe_size") or size
steps = c.get("keyframe_steps") or steps
except Exception:
pass
return str(size), int(steps)
def load_config(path: str) -> dict:
"""Load generation options from a saved JSON config file.
......@@ -5001,10 +5029,9 @@ def launch_web_ui(default_args):
pass
# Build a generation prompt from the saved profile.
prompt = (meta.get("prompt") or meta.get("description") or name).strip()
if kind == "environment":
size = "768x512"
else:
size = "512x512"
# Honour the Run-page resolution + steps (keyframe_size/keyframe_steps)
# instead of a hardcoded size/28 steps.
size, _ref_steps = _ref_gen_res_steps(default_args)
client = CoderAIClient(default_args.base_url,
getattr(default_args, "api_key", None))
......@@ -5033,7 +5060,7 @@ def launch_web_ui(default_args):
prompt=prompt, model=model,
character_profiles=char_p, environment_profiles=env_p,
character_strength=0.7,
size=size, steps=28, seed=random.randint(0, 2**31),
size=size, steps=_ref_steps, seed=random.randint(0, 2**31),
)
except Exception as e:
_web_log(f" ✗ regen image {k+1}/{count} for {name} failed: {e}")
......@@ -5103,7 +5130,8 @@ def launch_web_ui(default_args):
prompt = f"{prompt}, {_ref_look}" if prompt and prompt != name else _ref_look
if not description:
description = "Fight referee / official."
size = "768x512" if kind == "environment" else "512x512"
# Honour the Run-page resolution + steps (keyframe_size/keyframe_steps).
size, _ref_steps = _ref_gen_res_steps(default_args)
client = CoderAIClient(default_args.base_url,
getattr(default_args, "api_key", None))
_prog(8, "selecting image model…")
......@@ -5119,11 +5147,11 @@ def launch_web_ui(default_args):
if kind == "character":
client.generate_character(name=name, prompt=prompt,
description=description, model=model,
n=count, size=size)
n=count, size=size, steps=_ref_steps)
else:
client.generate_environment(name=name, prompt=prompt,
description=description, model=model,
n=count, size=size)
n=count, size=size, steps=_ref_steps)
except Exception as e:
_fail(f"generation failed: {e}")
return
......
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