front: wait-keepalive on the direct streaming path (queue/model-load)

Clients hitting the public API directly (e.g. township via nginx) disconnected
during long waits: the direct proxy() path acquired the front queue slot silently
and awaited the engine's first byte (model load) with no output. The broker path
already kept alive; the direct path now does too.

For a STREAMING inference request, commit to a 200 text/event-stream up front and
emit keepalive while acquiring the queue slot and during the engine's model load /
not-ready retries, then relay the real stream (token-counting for the Tasks page),
ending cleanly if the engine dies mid-flight.

Configurable mode (models.wait_status_mode, global default 'invisible'; per-model
override via the models.json entry):
  * invisible — empty-content SSE chunk + x_queue_info (holds the connection; no
                content pollution)
  * visible   — short visible status text (appears in the content)
  * silent    — nothing (legacy path)
When thinking is enabled the keepalive goes on the reasoning channel instead
(no pollution), unless mode is silent.
Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RdMufYvtTbtGDWsiZVoXce
parent 223b46f7
...@@ -106,6 +106,16 @@ class ModelsConfig: ...@@ -106,6 +106,16 @@ class ModelsConfig:
# Global default on; override per-model via the models.json entry's # Global default on; override per-model via the models.json entry's
# "load_status_updates" (set false to deactivate for that model). # "load_status_updates" (set false to deactivate for that model).
load_status_updates: bool = True load_status_updates: bool = True
# Keepalive sent on the DIRECT streaming API path while a request waits for a
# front queue slot or the engine's model load, so the client doesn't time out:
# "invisible" (default) — empty-content SSE chunk + x_queue_info metadata
# (holds the connection; no message-content pollution)
# "visible" — short visible status text (appears in the content)
# "silent" — send nothing (legacy behaviour)
# When the request has thinking enabled the keepalive is sent on the reasoning
# channel instead (no pollution), unless the mode is "silent". Global default;
# override per-model via the models.json entry's "wait_status_mode".
wait_status_mode: str = "invisible"
# Degenerate tool-call loop guard: when the incoming history shows the same # Degenerate tool-call loop guard: when the incoming history shows the same
# tool invoked with the same arguments repeatedly (and each attempt failed or # tool invoked with the same arguments repeatedly (and each attempt failed or
# the call spilled as un-parsed markup), inject a one-shot system reminder # the call spilled as un-parsed markup), inject a one-shot system reminder
...@@ -651,6 +661,8 @@ class ConfigManager: ...@@ -651,6 +661,8 @@ class ConfigManager:
"gguf_cache_dir": self.config.models.gguf_cache_dir, "gguf_cache_dir": self.config.models.gguf_cache_dir,
"max_model_instances": self.config.models.max_model_instances, "max_model_instances": self.config.models.max_model_instances,
"max_model_instances_overrides": self.config.models.max_model_instances_overrides, "max_model_instances_overrides": self.config.models.max_model_instances_overrides,
"load_status_updates": self.config.models.load_status_updates,
"wait_status_mode": self.config.models.wait_status_mode,
}, },
"offload": { "offload": {
"directory": self.config.offload.directory, "directory": self.config.offload.directory,
......
This diff is collapsed.
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