fix: error state, leg grouping, prompt→generating transition

- log_parser: detect model load failure (child + router patterns) → state=error;
  switch to generating at progress=1.00 and at prompt eval summary line
- frontend: add state-error (llama flashes red, CSS variable override);
  group each leg+hoof in <g> so walk animation moves hooves with legs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Vlad Doloman
2026-06-22 22:10:43 +03:00
parent 311819b51c
commit 9dcb5b977a
4 changed files with 49 additions and 21 deletions

View File

@@ -83,6 +83,8 @@ _PROMPT_SUMMARY_RE = re.compile(
)
_RELEASE_RE = re.compile(r'slot\s+release:.*stop processing')
_UNLOAD_RE = re.compile(r'unload_lru:|unload: stopping model instance')
_LOAD_ERROR_RE = re.compile(r'exiting due to model loading error')
_EXIT_ERROR_RE = re.compile(r'operator\(\):.*exited with status 1')
# ── State machine ─────────────────────────────────────────────────────────────
@@ -121,6 +123,8 @@ class LogParser:
return self._set(state="waiting")
if _UNLOAD_RE.search(content):
return self._set(state="unloading")
if _EXIT_ERROR_RE.search(content):
return self._set(state="error")
return None
# ── Child-process lines ──────────────────────────────────────────────────
@@ -130,6 +134,8 @@ class LogParser:
if m := _CMD_STATE_RE.match(content):
return self._handle_cmd_state(m.group(1))
if _LOAD_ERROR_RE.search(content):
return self._set(state="error")
if _WARMUP_RE.search(content):
return self._set(state="warming_up")
if _IDLE_RE.search(content):
@@ -137,14 +143,14 @@ class LogParser:
if _LAUNCH_RE.search(content):
return self._set(state="processing_prompt")
if m := _PROMPT_TIMING_RE.search(content):
progress = float(m.group(2))
done = progress >= 1.0
metrics = Metrics(
prompt_speed=float(m.group(3)),
prompt_tokens=int(m.group(1)),
prompt_progress=float(m.group(2)),
gen_speed=self._state.metrics.gen_speed,
n_decoded=self._state.metrics.n_decoded,
prompt_progress=None if done else progress,
)
return self._set(state="processing_prompt", metrics=metrics)
return self._set(state="generating" if done else "processing_prompt", metrics=metrics)
if m := _GEN_TIMING_RE.search(content):
metrics = Metrics(
prompt_speed=self._state.metrics.prompt_speed,
@@ -154,14 +160,18 @@ class LogParser:
)
return self._set(state="generating", metrics=metrics)
if m := _PROMPT_SUMMARY_RE.search(content):
# End-of-slot summary: update prompt speed without changing state.
# For short prompts this fires after generating starts (no live prompt line).
metrics = Metrics(
prompt_speed=float(m.group(2)),
prompt_tokens=int(m.group(1)),
gen_speed=self._state.metrics.gen_speed,
n_decoded=self._state.metrics.n_decoded,
)
if self._state.state == "processing_prompt":
# Summary fires right after prompt eval finishes, before first token.
# Advance to generating now rather than waiting for the first tg line.
# For short prompts the summary arrives after generating has already
# started, so the guard prevents stepping back.
return self._set(state="generating", metrics=metrics)
return self._set(metrics=metrics)
if _RELEASE_RE.search(content):
return self._set(