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 @@ ...@@ -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.1.15" __version__ = "0.1.16"
# Configure the CUDA caching allocator BEFORE torch is imported anywhere. # Configure the CUDA caching allocator BEFORE torch is imported anywhere.
# expandable_segments lets the allocator return freed pages to the driver even # expandable_segments lets the allocator return freed pages to the driver even
......
...@@ -341,6 +341,17 @@ async def change_password( ...@@ -341,6 +341,17 @@ async def change_password(
return RedirectResponse(url=_url(request, "/admin"), status_code=302) 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") @router.get("/admin/api/models", summary="List configured models")
async def api_list_models(username: str = Depends(require_admin)): async def api_list_models(username: str = Depends(require_admin)):
"""List all configured models with details.""" """List all configured models with details."""
......
...@@ -830,13 +830,18 @@ class FrontProxy: ...@@ -830,13 +830,18 @@ class FrontProxy:
async def is_admin(self, request: Request) -> bool: async def is_admin(self, request: Request) -> bool:
"""Authorize a front-handled admin action by validating the caller's session """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() prim = self.registry.primary()
if prim is None: if prim is None:
return False return False
try: try:
headers = self._filter_headers(request.headers, _DROP_REQ) 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 return r.status_code == 200
except Exception: except Exception:
return False return False
......
...@@ -813,20 +813,28 @@ class CoderAIClient: ...@@ -813,20 +813,28 @@ class CoderAIClient:
return [] return []
def generate_character(self, name: str, prompt: str, description: str, def generate_character(self, name: str, prompt: str, description: str,
model: str, n: int = 4, size: str = "512x512") -> dict: model: str, n: int = 4, size: str = "512x512",
return self._post("/v1/characters/generate", { steps: int = None) -> dict:
body = {
"name": name, "prompt": prompt, "description": description, "name": name, "prompt": prompt, "description": description,
"model": model, "n": n, "model": model, "n": n,
"width": int(size.split("x")[0]), "height": int(size.split("x")[1]), "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, def generate_environment(self, name: str, prompt: str, description: str,
model: str, n: int = 3, size: str = "768x512") -> dict: model: str, n: int = 3, size: str = "768x512",
return self._post("/v1/environments/generate", { steps: int = None) -> dict:
body = {
"name": name, "prompt": prompt, "description": description, "name": name, "prompt": prompt, "description": description,
"model": model, "n": n, "model": model, "n": n,
"width": int(size.split("x")[0]), "height": int(size.split("x")[1]), "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, def generate_image(self, prompt: str, model: str,
character_profiles: list = None, character_profiles: list = None,
...@@ -2219,6 +2227,26 @@ def save_config(path: str, args) -> dict: ...@@ -2219,6 +2227,26 @@ def save_config(path: str, args) -> dict:
return data 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: def load_config(path: str) -> dict:
"""Load generation options from a saved JSON config file. """Load generation options from a saved JSON config file.
...@@ -5001,10 +5029,9 @@ def launch_web_ui(default_args): ...@@ -5001,10 +5029,9 @@ def launch_web_ui(default_args):
pass pass
# Build a generation prompt from the saved profile. # Build a generation prompt from the saved profile.
prompt = (meta.get("prompt") or meta.get("description") or name).strip() prompt = (meta.get("prompt") or meta.get("description") or name).strip()
if kind == "environment": # Honour the Run-page resolution + steps (keyframe_size/keyframe_steps)
size = "768x512" # instead of a hardcoded size/28 steps.
else: size, _ref_steps = _ref_gen_res_steps(default_args)
size = "512x512"
client = CoderAIClient(default_args.base_url, client = CoderAIClient(default_args.base_url,
getattr(default_args, "api_key", None)) getattr(default_args, "api_key", None))
...@@ -5033,7 +5060,7 @@ def launch_web_ui(default_args): ...@@ -5033,7 +5060,7 @@ def launch_web_ui(default_args):
prompt=prompt, model=model, prompt=prompt, model=model,
character_profiles=char_p, environment_profiles=env_p, character_profiles=char_p, environment_profiles=env_p,
character_strength=0.7, 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: except Exception as e:
_web_log(f" ✗ regen image {k+1}/{count} for {name} failed: {e}") _web_log(f" ✗ regen image {k+1}/{count} for {name} failed: {e}")
...@@ -5103,7 +5130,8 @@ def launch_web_ui(default_args): ...@@ -5103,7 +5130,8 @@ def launch_web_ui(default_args):
prompt = f"{prompt}, {_ref_look}" if prompt and prompt != name else _ref_look prompt = f"{prompt}, {_ref_look}" if prompt and prompt != name else _ref_look
if not description: if not description:
description = "Fight referee / official." 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, client = CoderAIClient(default_args.base_url,
getattr(default_args, "api_key", None)) getattr(default_args, "api_key", None))
_prog(8, "selecting image model…") _prog(8, "selecting image model…")
...@@ -5119,11 +5147,11 @@ def launch_web_ui(default_args): ...@@ -5119,11 +5147,11 @@ def launch_web_ui(default_args):
if kind == "character": if kind == "character":
client.generate_character(name=name, prompt=prompt, client.generate_character(name=name, prompt=prompt,
description=description, model=model, description=description, model=model,
n=count, size=size) n=count, size=size, steps=_ref_steps)
else: else:
client.generate_environment(name=name, prompt=prompt, client.generate_environment(name=name, prompt=prompt,
description=description, model=model, description=description, model=model,
n=count, size=size) n=count, size=size, steps=_ref_steps)
except Exception as e: except Exception as e:
_fail(f"generation failed: {e}") _fail(f"generation failed: {e}")
return 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