TaterTotterson commited on
Commit
c9da8d4
·
verified ·
1 Parent(s): baea888

Update standalone Tater bundle to 0.1.12

Browse files
pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
 
5
  [project]
6
  name = "tater_reachy_standalone"
7
- version = "0.1.11"
8
  description = "Full local Tater AI stack and voice satellite for Reachy Mini"
9
  readme = "README.md"
10
  requires-python = ">=3.11"
@@ -61,6 +61,9 @@ dependencies = [
61
  "accelerate",
62
  "llama-cpp-python>=0.3.23",
63
  "mlx-lm; platform_system == 'Darwin' and platform_machine == 'arm64'",
 
 
 
64
  "faster-whisper",
65
  "vosk",
66
  "pykokoro",
 
4
 
5
  [project]
6
  name = "tater_reachy_standalone"
7
+ version = "0.1.12"
8
  description = "Full local Tater AI stack and voice satellite for Reachy Mini"
9
  readme = "README.md"
10
  requires-python = ">=3.11"
 
61
  "accelerate",
62
  "llama-cpp-python>=0.3.23",
63
  "mlx-lm; platform_system == 'Darwin' and platform_machine == 'arm64'",
64
+ "mlx-vlm; platform_system == 'Darwin' and platform_machine == 'arm64'",
65
+ "outlines; platform_system == 'Darwin' and platform_machine == 'arm64'",
66
+ "outlines-core==0.1.26; platform_system == 'Darwin' and platform_machine == 'arm64'",
67
  "faster-whisper",
68
  "vosk",
69
  "pykokoro",
tater_reachy_standalone/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
  """Standalone Tater app for Reachy Mini."""
2
 
3
- __version__ = "0.1.11"
 
1
  """Standalone Tater app for Reachy Mini."""
2
 
3
+ __version__ = "0.1.12"
tater_reachy_standalone/vendor/tater_app/README.md CHANGED
@@ -320,22 +320,26 @@ After Tater is running, open TaterOS and finish the first-run setup:
320
  - enable `Beast Mode` and set per-head model settings for Chat/Astraeus/Thanatos/Minos/Hermes
321
 
322
  Hydra model settings are saved by TaterOS and used at runtime. Base, Spudex, Beast Mode routing, and Vision can each use the selected built-in local providers or OpenAI-compatible providers.
323
- Hugging Face Transformers model files are cached under `agent_lab/models/llm/huggingface` by default; override with `TATER_HF_TRANSFORMERS_MODEL_ROOT`.
324
- llama.cpp GGUF files are cached under `agent_lab/models/llm/llama-cpp` by default; override with `TATER_LLAMA_CPP_MODEL_ROOT`.
325
- MLX LM files are cached under `agent_lab/models/llm/mlx` by default; override with `TATER_MLX_LM_MODEL_ROOT`.
326
- For llama.cpp, the model value can be `owner/repo::filename.gguf`, `owner/repo/path/to/file.gguf`, a bare GGUF repo id such as `owner/repo` to auto-pick a preferred quant, or a local `.gguf` path.
327
- For MLX LM, the model value can be an MLX-compatible Hugging Face repo such as `mlx-community/Llama-3.2-3B-Instruct-4bit`, or a local MLX model folder.
328
- Download local Hugging Face Transformers, llama.cpp, or MLX LM models from the Hugging Face mini-tab first, then choose the downloaded model from the Settings mini-tab dropdown. The downloaded model registry is stored at `agent_lab/models/llm/downloaded_models.json` by default.
329
- The Hugging Face mini-tab can switch between text and vision model browsing. For llama.cpp vision GGUF repos, Tater detects `mmproj*.gguf` projector files and downloads the matching projector beside the selected GGUF when one is available.
330
- The Hugging Face browser and downloader use the token saved in Integration Manager -> Hugging Face for gated/private models and higher Hub rate limits; explicit env overrides like `TATER_HF_MODEL_BROWSER_TOKEN`, `TATER_HF_TRANSFORMERS_TOKEN`, or `TATER_MLX_LM_TOKEN` still take priority.
331
- llama.cpp tries GPU offload by default with `TATER_LLAMA_CPP_N_GPU_LAYERS=auto`; set it to `0` for CPU-only or to a specific layer count. GPU offload depends on installing llama-cpp-python with CUDA, Metal, or another supported acceleration build. On NVIDIA, rerun `sh setup_tater.sh nvidia` to replace a CPU-only llama.cpp install with the CUDA wheel/build path.
332
- MLX LM is intended for Apple Silicon Macs; use llama.cpp GGUF on Raspberry Pi, Linux, or non-Apple-Silicon devices.
333
- Thinking suppression is enabled by default for local providers where possible; set `TATER_HF_TRANSFORMERS_DISABLE_THINKING=false`, `TATER_LLAMA_CPP_DISABLE_THINKING=false`, or `TATER_MLX_LM_DISABLE_THINKING=false` if a model should keep its native reasoning tags.
334
- Local provider context length can be adjusted with a slider in Settings -> Models -> LLM / Vision. When Tater can read cached model metadata, the slider max follows the model's context limit. Transformers uses the value as the prompt truncation limit, llama.cpp uses it as `n_ctx`, and MLX LM uses it as `max_kv_size`.
335
- Vision can stay on an OpenAI-compatible API, automatically try the Base model first, use the Base model only, or load a dedicated local vision model. A loaded Base model is reused for local vision when its provider/model cache is already vision-capable; dedicated vision models are loaded and unloaded independently from Base.
336
-
337
- Access-log note:
338
- - `run_ui.sh` starts Uvicorn with `--no-access-log` to suppress per-request lines.
339
-
340
-
341
- https://github.com/user-attachments/assets/9138f485-ccd6-46e0-9295-f5617c079fea
 
 
 
 
 
320
  - enable `Beast Mode` and set per-head model settings for Chat/Astraeus/Thanatos/Minos/Hermes
321
 
322
  Hydra model settings are saved by TaterOS and used at runtime. Base, Spudex, Beast Mode routing, and Vision can each use the selected built-in local providers or OpenAI-compatible providers.
323
+
324
+ ### Local Models
325
+
326
+ - Download local Hugging Face Transformers, llama.cpp GGUF, or MLX models from the Hugging Face mini-tab first, then select them from Settings.
327
+ - Model caches live under `agent_lab/models/llm/` by default:
328
+ - `huggingface` for Transformers
329
+ - `llama-cpp` for GGUF models and matching `mmproj*.gguf` vision projectors
330
+ - `mlx` for MLX text and vision models
331
+ - The Hugging Face browser uses the token saved in **Integration Manager -> Hugging Face** for private/gated models and better Hub rate limits.
332
+ - llama.cpp uses GPU offload by default when the installed build supports it. Set `TATER_LLAMA_CPP_N_GPU_LAYERS=0` for CPU-only.
333
+ - MLX is intended for Apple Silicon Macs. Use llama.cpp GGUF on Linux, Raspberry Pi, NVIDIA, AMD/ROCm, Jetson, or other non-Apple-Silicon devices.
334
+
335
+ ### Vision
336
+
337
+ - Vision can use an OpenAI-compatible API, the loaded Base model, or a dedicated local vision model.
338
+ - If Base is already loaded and vision-capable, Tater reuses it instead of loading the same model twice.
339
+ - Dedicated vision models are managed separately from Base.
340
+
341
+ ### Advanced Notes
342
+
343
+ - Local context length is configured in **Settings -> Models -> LLM / Vision**.
344
+ - Thinking suppression is enabled by default for local providers when supported.
345
+ - `run_ui.sh` starts Uvicorn with `--no-access-log` to suppress per-request log spam.
tater_reachy_standalone/vendor/tater_app/helpers.py CHANGED
The diff for this file is too large to render. See raw diff
 
tater_reachy_standalone/vendor/tater_app/requirements.txt CHANGED
@@ -50,6 +50,9 @@ transformers
50
  accelerate
51
  llama-cpp-python>=0.3.23
52
  mlx-lm; platform_system == "Darwin" and platform_machine == "arm64"
 
 
 
53
  faster-whisper
54
  vosk
55
  pykokoro
 
50
  accelerate
51
  llama-cpp-python>=0.3.23
52
  mlx-lm; platform_system == "Darwin" and platform_machine == "arm64"
53
+ mlx-vlm; platform_system == "Darwin" and platform_machine == "arm64"
54
+ outlines; platform_system == "Darwin" and platform_machine == "arm64"
55
+ outlines-core==0.1.26; platform_system == "Darwin" and platform_machine == "arm64"
56
  faster-whisper
57
  vosk
58
  pykokoro
tater_reachy_standalone/vendor/tater_app/setup_tater.sh CHANGED
@@ -272,6 +272,33 @@ install_llama_cpp_metal() {
272
  "${venv_python}" -m pip install --upgrade "${LLAMA_CPP_PYTHON_SPEC}" || warn "Default llama-cpp-python fallback failed."
273
  }
274
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
275
  install_macos() {
276
  venv_python="$1"
277
  tmp_req="$(mktemp "${TMPDIR:-/tmp}/tater-requirements-macos.XXXXXX")"
@@ -305,6 +332,9 @@ install_macos() {
305
  if ! "${venv_python}" -m pip install mlx-whisper kokoro; then
306
  warn "Apple-native speech extras failed to install. Tater will still run with Faster Whisper/Kokoro CPU fallbacks."
307
  fi
 
 
 
308
  rm -f "${tmp_req}"
309
  trap - EXIT
310
  }
 
272
  "${venv_python}" -m pip install --upgrade "${LLAMA_CPP_PYTHON_SPEC}" || warn "Default llama-cpp-python fallback failed."
273
  }
274
 
275
+ install_mlx_engine_checkout() {
276
+ if [ "${TATER_SETUP_MLX_ENGINE:-1}" = "0" ]; then
277
+ warn "Skipping MLX engine checkout because TATER_SETUP_MLX_ENGINE=0."
278
+ return
279
+ fi
280
+ if ! command -v git >/dev/null 2>&1; then
281
+ warn "git was not found; skipping optional MLX engine checkout."
282
+ return
283
+ fi
284
+ mkdir -p "${RUNTIME_DIR}"
285
+ engine_dir="${RUNTIME_DIR}/mlx-engine"
286
+ if [ -d "${engine_dir}/mlx_engine" ]; then
287
+ ok "Using existing ${engine_dir}"
288
+ if [ -d "${engine_dir}/.git" ]; then
289
+ info "Updating optional MLX engine checkout"
290
+ git -C "${engine_dir}" pull --ff-only || warn "Could not update ${engine_dir}; using the existing checkout."
291
+ fi
292
+ return
293
+ fi
294
+ if [ -e "${engine_dir}" ]; then
295
+ warn "${engine_dir} exists but does not look like an mlx-engine checkout. Set TATER_MLX_ENGINE_PATH or remove it and rerun setup."
296
+ return
297
+ fi
298
+ info "Cloning optional MLX engine runtime"
299
+ git clone --depth 1 https://github.com/lmstudio-ai/mlx-engine.git "${engine_dir}" || warn "Could not clone mlx-engine; Tater will use mlx-lm/mlx-vlm directly."
300
+ }
301
+
302
  install_macos() {
303
  venv_python="$1"
304
  tmp_req="$(mktemp "${TMPDIR:-/tmp}/tater-requirements-macos.XXXXXX")"
 
332
  if ! "${venv_python}" -m pip install mlx-whisper kokoro; then
333
  warn "Apple-native speech extras failed to install. Tater will still run with Faster Whisper/Kokoro CPU fallbacks."
334
  fi
335
+ if [ "${is_apple_silicon}" = "1" ]; then
336
+ install_mlx_engine_checkout
337
+ fi
338
  rm -f "${tmp_req}"
339
  trap - EXIT
340
  }
tater_reachy_standalone/vendor/tater_app/tateros_app.py CHANGED
@@ -6,6 +6,7 @@ import hmac
6
  import importlib
7
  import json
8
  import logging
 
9
  import os
10
  import queue
11
  import re
@@ -63,12 +64,28 @@ from hydra import (
63
  from emoji_responder import get_emoji_settings as get_core_emoji_settings, save_emoji_settings as save_core_emoji_settings
64
  from notify import notifier_destination_catalog
65
  from helpers import (
 
66
  DEFAULT_HF_TRANSFORMERS_CONTEXT_TOKENS,
 
 
 
 
 
67
  DEFAULT_LLAMA_CPP_CONTEXT_TOKENS,
68
  DEFAULT_LLAMA_CPP_MTP_DRAFT_TOKENS,
69
  DEFAULT_LLAMA_CPP_MTP_ENABLED,
 
 
 
70
  DEFAULT_LLAMA_CPP_VISION_CONTEXT_TOKENS,
 
 
 
71
  HYDRA_HF_TRANSFORMERS_CONTEXT_TOKENS_KEY,
 
 
 
 
72
  HYDRA_LLM_BASE_SERVERS_KEY,
73
  HYDRA_LLM_PROVIDER_HF_TRANSFORMERS,
74
  HYDRA_LLM_PROVIDER_KEY,
@@ -76,10 +93,20 @@ from helpers import (
76
  HYDRA_LLM_PROVIDER_MLX_LM,
77
  HYDRA_LLM_PROVIDER_OPENAI_COMPATIBLE,
78
  HYDRA_LLAMA_CPP_CONTEXT_TOKENS_KEY,
 
79
  HYDRA_LLAMA_CPP_MTP_DRAFT_TOKENS_KEY,
80
  HYDRA_LLAMA_CPP_MTP_ENABLED_KEY,
 
 
 
81
  HYDRA_LLAMA_CPP_VISION_CONTEXT_TOKENS_KEY,
 
 
 
 
82
  HYDRA_MLX_LM_CONTEXT_TOKENS_KEY,
 
 
83
  HfLlmDownloadCancelled,
84
  decrypt_current_redis_snapshot,
85
  download_hf_transformers_llm_model,
@@ -88,11 +115,14 @@ from helpers import (
88
  encrypt_current_redis_snapshot,
89
  ensure_redis_encryption_key,
90
  get_llama_cpp_runtime_diagnostics,
 
 
91
  get_local_llm_loaded_models_snapshot,
92
  get_redis_connection_config,
93
  get_redis_encryption_status,
94
  get_redis_connection_status,
95
  get_llm_call_runtime_summary,
 
96
  get_vision_call_runtime_summary,
97
  get_llm_client_from_env,
98
  preload_hf_transformers_llm_model,
@@ -108,6 +138,10 @@ from helpers import (
108
  set_main_loop,
109
  shutdown_internal_redis,
110
  test_redis_connection_settings,
 
 
 
 
111
  unload_local_llm_models,
112
  )
113
  from runtime_executors import configure_runtime_executors, run_dashboard, run_wake, shutdown_runtime_executors
@@ -533,6 +567,63 @@ def _read_bool_setting(redis_key: str, env_keys: Tuple[str, ...], default: bool)
533
  return _as_bool_flag(raw, default=default)
534
 
535
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
536
  def _read_bounded_int_setting(
537
  redis_key: str,
538
  env_keys: Tuple[str, ...],
@@ -1604,15 +1695,17 @@ def _hf_browser_models_api_url(
1604
  task: str = "text-generation",
1605
  ) -> str:
1606
  library = _hf_browser_provider_library(provider)
1607
- app_filter = _hf_browser_provider_app_filter(provider)
 
1608
  params: Dict[str, Any] = {
1609
  "sort": sort,
1610
  "direction": "-1",
1611
  "limit": str(max(1, min(100, int(limit or 24)))),
1612
  "full": "true",
1613
  "cardData": "true",
1614
- "pipeline_tag": _normalize_hf_browser_task(task),
1615
  }
 
 
1616
  if library:
1617
  params["library"] = library
1618
  if app_filter:
@@ -1802,11 +1895,299 @@ def _hf_browser_provider_search(provider: str, query: str, task: str = "text-gen
1802
  return q
1803
 
1804
 
1805
- def _hf_browser_provider_app_filter(provider: str) -> str:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1806
  provider_token = _normalize_hydra_llm_provider(provider)
 
1807
  if provider_token == HYDRA_LLM_PROVIDER_LLAMA_CPP:
1808
  return "llama.cpp"
1809
  if provider_token == HYDRA_LLM_PROVIDER_MLX_LM:
 
 
1810
  return "mlx-lm"
1811
  return ""
1812
 
@@ -1818,8 +2199,16 @@ def _hf_browser_provider_library(provider: str) -> str:
1818
  return "transformers"
1819
 
1820
 
 
 
 
 
 
 
 
 
1821
  def _hf_browser_text_generation_tokens() -> Tuple[str, ...]:
1822
- return ("text-generation",)
1823
 
1824
 
1825
  def _hf_browser_vision_tokens() -> Tuple[str, ...]:
@@ -1912,6 +2301,8 @@ def _hf_browser_provider_matches(model: Any, provider: str) -> bool:
1912
  return (
1913
  "mlx-lm" in provider_haystack
1914
  or "mlx_lm" in provider_haystack
 
 
1915
  or library == "mlx"
1916
  or "mlx" in tags
1917
  or "mlx" in files
@@ -1922,9 +2313,26 @@ def _hf_browser_provider_matches(model: Any, provider: str) -> bool:
1922
 
1923
  def _hf_browser_file_quant(path: str) -> str:
1924
  name = os.path.basename(str(path or "")).upper()
1925
- for quant in ("Q2_K", "Q3_K_M", "Q4_K_M", "Q4_K_S", "Q5_K_M", "Q5_K_S", "Q6_K", "Q8_0", "F16", "BF16"):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1926
  if quant in name:
1927
- return quant
1928
  return ""
1929
 
1930
 
@@ -1932,7 +2340,7 @@ def _hf_browser_preferred_gguf(files: List[Dict[str, Any]]) -> str:
1932
  ggufs = [row for row in files if bool(row.get("is_gguf")) and "mmproj" not in str(row.get("path") or "").lower()]
1933
  if not ggufs:
1934
  return ""
1935
- preferred = ("Q4_K_M", "Q5_K_M", "Q4_K_S", "Q5_0", "Q4_0", "Q8_0")
1936
 
1937
  def _score(row: Dict[str, Any]) -> Tuple[int, int, str]:
1938
  path = str(row.get("path") or "")
@@ -1985,6 +2393,8 @@ def _hf_browser_model_summary(model: Any, *, provider: str) -> Dict[str, Any]:
1985
  lower_files = " ".join(files).lower()
1986
  lower_library = str(_hf_browser_object_value(model, "library_name") or "").strip().lower()
1987
  provider_haystack = f"{lower_id} {lower_library} {lower_tags} {lower_files}"
 
 
1988
  supports_vision = _hf_browser_is_vision_model(model) or "mmproj" in lower_files
1989
  if provider_token == HYDRA_LLM_PROVIDER_LLAMA_CPP:
1990
  compatible = (
@@ -1996,12 +2406,17 @@ def _hf_browser_model_summary(model: Any, *, provider: str) -> Dict[str, Any]:
1996
  or ".gguf" in lower_files
1997
  )
1998
  elif provider_token == HYDRA_LLM_PROVIDER_MLX_LM:
1999
- compatible = "mlx-lm" in provider_haystack or "mlx_lm" in provider_haystack or "mlx" in provider_haystack
 
 
 
 
 
 
2000
  else:
2001
  compatible = ".gguf" not in lower_files
2002
- card_data = _hf_browser_object_value(model, "cardData", None) or _hf_browser_object_value(model, "card_data", None)
2003
  model_size_label = _hf_browser_model_size_label(model_id, tags, files, model, card_data)
2004
- return {
2005
  "id": model_id,
2006
  "author": str(_hf_browser_object_value(model, "author") or "").strip(),
2007
  "model_size": model_size_label,
@@ -2022,6 +2437,17 @@ def _hf_browser_model_summary(model: Any, *, provider: str) -> Dict[str, Any]:
2022
  "provider": provider_token,
2023
  "provider_label": _hydra_llm_provider_label(provider_token),
2024
  }
 
 
 
 
 
 
 
 
 
 
 
2025
 
2026
 
2027
  def _hf_browser_file_rows(info: Any, fallback_files: List[str]) -> List[Dict[str, Any]]:
@@ -2393,7 +2819,10 @@ def _local_llm_provider_cache_rows(provider: str) -> List[Dict[str, Any]]:
2393
  "downloaded_ts": float(latest_snapshot.stat().st_mtime if latest_snapshot.exists() else time.time()),
2394
  "max_context_tokens": max_context,
2395
  "context_source": context_source,
2396
- "supports_vision": bool(_local_llm_json_files_support_vision(latest_snapshot)),
 
 
 
2397
  }
2398
  )
2399
  return rows
@@ -2435,7 +2864,9 @@ def _normalize_local_llm_model_row(row: Dict[str, Any]) -> Dict[str, Any]:
2435
  supports_vision = bool(row.get("supports_vision"))
2436
  mmproj_filename = str(row.get("mmproj_filename") or "").strip()
2437
  mmproj_path = str(row.get("mmproj_path") or "").strip()
2438
- if provider == HYDRA_LLM_PROVIDER_LLAMA_CPP:
 
 
2439
  path_obj = Path(str(row.get("model_path") or "")).expanduser()
2440
  if path_obj.exists():
2441
  detected_mmproj = _local_llm_mmproj_for_gguf(path_obj)
@@ -2500,6 +2931,36 @@ def _local_llm_models_payload(provider: str = "") -> Dict[str, Any]:
2500
  return {"models": models, "by_provider": by_provider, "updated_ts": time.time()}
2501
 
2502
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2503
  def _record_downloaded_local_llm_model(
2504
  *,
2505
  provider: str,
@@ -3455,16 +3916,52 @@ def _enforce_user_assistant_alternation(loop_messages: List[Dict[str, Any]]) ->
3455
  return merged
3456
 
3457
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3458
  def _load_chat_history_tail(count: int) -> List[Dict[str, Any]]:
3459
  if count <= 0:
3460
  return []
 
 
 
 
 
3461
  raw = redis_client.lrange(CHAT_HISTORY_KEY, -count, -1)
3462
  out: List[Dict[str, Any]] = []
3463
- for line in raw:
3464
  try:
3465
  parsed = json.loads(line)
3466
  if isinstance(parsed, dict):
3467
- out.append(parsed)
 
 
 
 
 
 
3468
  except Exception:
3469
  continue
3470
  return out
@@ -3473,11 +3970,17 @@ def _load_chat_history_tail(count: int) -> List[Dict[str, Any]]:
3473
  def _load_chat_history() -> List[Dict[str, Any]]:
3474
  raw = redis_client.lrange(CHAT_HISTORY_KEY, 0, -1)
3475
  out: List[Dict[str, Any]] = []
3476
- for line in raw:
3477
  try:
3478
  parsed = json.loads(line)
3479
  if isinstance(parsed, dict):
3480
- out.append(parsed)
 
 
 
 
 
 
3481
  except Exception:
3482
  continue
3483
  return out
@@ -4287,8 +4790,20 @@ def _normalize_plugin_response_item(item: Any) -> Any:
4287
  if not isinstance(item, dict):
4288
  return item
4289
 
4290
- media_type = str(item.get("type") or "").strip().lower()
4291
- if media_type not in {"image", "audio", "video", "file"}:
 
 
 
 
 
 
 
 
 
 
 
 
4292
  return item
4293
 
4294
  raw = None
@@ -4296,15 +4811,62 @@ def _normalize_plugin_response_item(item: Any) -> Any:
4296
  raw = bytes(item.get("data"))
4297
  elif isinstance(item.get("bytes"), (bytes, bytearray)):
4298
  raw = bytes(item.get("bytes"))
 
 
 
 
 
 
 
 
 
 
4299
 
4300
  if raw is None:
4301
- return item
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4302
 
4303
  safe = dict(item)
4304
  safe.pop("data", None)
4305
  safe.pop("bytes", None)
4306
- safe["data_b64"] = base64.b64encode(raw).decode("utf-8")
4307
- safe["size"] = len(raw)
 
 
 
 
 
 
 
 
 
 
 
 
 
4308
  return safe
4309
 
4310
 
@@ -5844,6 +6406,13 @@ class LocalLlmModelDeleteRequest(BaseModel):
5844
  model: Optional[str] = None
5845
 
5846
 
 
 
 
 
 
 
 
5847
  class HfLlmWarmupCancelRequest(BaseModel):
5848
  key: Optional[str] = None
5849
  provider: Optional[str] = None
@@ -5928,11 +6497,26 @@ class AppSettingsRequest(BaseModel):
5928
  hydra_llm_api_key: Optional[str] = None
5929
  hydra_llm_provider: Optional[str] = None
5930
  hydra_hf_transformers_context_tokens: Optional[Any] = None
 
 
 
 
 
5931
  hydra_llama_cpp_context_tokens: Optional[Any] = None
5932
  hydra_llama_cpp_vision_context_tokens: Optional[Any] = None
5933
  hydra_llama_cpp_mtp_enabled: Optional[bool] = None
5934
  hydra_llama_cpp_mtp_draft_tokens: Optional[Any] = None
 
 
 
 
5935
  hydra_mlx_lm_context_tokens: Optional[Any] = None
 
 
 
 
 
 
5936
  spudex_llm_provider: Optional[str] = None
5937
  spudex_llm_host: Optional[str] = None
5938
  spudex_llm_model: Optional[str] = None
@@ -9902,6 +10486,11 @@ def runtime_breakdown() -> Dict[str, Any]:
9902
  return {"ok": True, **payload}
9903
 
9904
 
 
 
 
 
 
9905
  @app.post("/api/runtime/local-llm/unload")
9906
  def unload_runtime_local_llm(payload: LocalLlmUnloadRequest) -> Dict[str, Any]:
9907
  try:
@@ -11938,11 +12527,26 @@ def get_settings() -> Dict[str, Any]:
11938
  "hydra_llm_model": "",
11939
  "hydra_llm_api_key": "",
11940
  "hydra_hf_transformers_context_tokens": str(DEFAULT_HF_TRANSFORMERS_CONTEXT_TOKENS),
 
 
 
 
 
11941
  "hydra_llama_cpp_context_tokens": str(DEFAULT_LLAMA_CPP_CONTEXT_TOKENS),
11942
  "hydra_llama_cpp_vision_context_tokens": str(DEFAULT_LLAMA_CPP_VISION_CONTEXT_TOKENS),
11943
  "hydra_llama_cpp_mtp_enabled": bool(DEFAULT_LLAMA_CPP_MTP_ENABLED),
11944
  "hydra_llama_cpp_mtp_draft_tokens": str(DEFAULT_LLAMA_CPP_MTP_DRAFT_TOKENS),
 
 
 
 
11945
  "hydra_mlx_lm_context_tokens": "",
 
 
 
 
 
 
11946
  "hydra_beast_mode_enabled": False,
11947
  "hydra_max_ledger_items": int(DEFAULT_MAX_LEDGER_ITEMS),
11948
  "hydra_astraeus_plan_review_enabled": bool(DEFAULT_ASTRAEUS_PLAN_REVIEW_ENABLED),
@@ -12049,6 +12653,35 @@ def get_settings() -> Dict[str, Any]:
12049
  ("TATER_HF_TRANSFORMERS_MAX_INPUT_TOKENS",),
12050
  DEFAULT_HF_TRANSFORMERS_CONTEXT_TOKENS,
12051
  ),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12052
  "hydra_llama_cpp_context_tokens": _read_local_llm_context_setting(
12053
  HYDRA_LLAMA_CPP_CONTEXT_TOKENS_KEY,
12054
  ("TATER_LLAMA_CPP_N_CTX", "LLM_CONTEXT_SIZE"),
@@ -12071,12 +12704,71 @@ def get_settings() -> Dict[str, Any]:
12071
  minimum=1,
12072
  maximum=16,
12073
  ),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12074
  "hydra_mlx_lm_context_tokens": _read_local_llm_context_setting(
12075
  HYDRA_MLX_LM_CONTEXT_TOKENS_KEY,
12076
  ("TATER_MLX_LM_MAX_KV_SIZE",),
12077
  None,
12078
  minimum=128,
12079
  ),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12080
  "spudex_llm_provider": _normalize_hydra_llm_provider(spudex_settings.get("llm_provider") or ""),
12081
  "spudex_llm_host": str(spudex_settings.get("llm_host") or ""),
12082
  "spudex_llm_model": str(spudex_settings.get("llm_model") or ""),
@@ -12924,6 +13616,57 @@ def get_llama_cpp_diagnostics() -> Dict[str, Any]:
12924
  return get_llama_cpp_runtime_diagnostics()
12925
 
12926
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12927
  @app.get("/api/settings/huggingface/models")
12928
  def get_huggingface_models(
12929
  provider: str = "hf_transformers",
@@ -12938,6 +13681,25 @@ def get_huggingface_models(
12938
  provider_token = HYDRA_LLM_PROVIDER_HF_TRANSFORMERS
12939
  view_token = str(view or "trending").strip().lower().replace("_", "-")
12940
  task_token = _normalize_hf_browser_task(task)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12941
  if view_token in {"new", "recent", "latest"}:
12942
  sort = "lastModified"
12943
  response_view = "new"
@@ -12947,9 +13709,6 @@ def get_huggingface_models(
12947
  else:
12948
  sort = "trendingScore"
12949
  response_view = "trending"
12950
- clean_limit = max(4, min(48, int(limit or 24)))
12951
- search = _hf_browser_provider_search(provider_token, query, task_token)
12952
- integration_status = _hf_browser_integration_status()
12953
  page_url = _hf_browser_cursor_decode(cursor) if str(cursor or "").strip() else _hf_browser_models_api_url(
12954
  provider=provider_token,
12955
  search=search,
@@ -12988,7 +13747,7 @@ def get_huggingface_models(
12988
  "query": search,
12989
  "task": task_token,
12990
  "library": _hf_browser_provider_library(provider_token),
12991
- "app_filter": _hf_browser_provider_app_filter(provider_token),
12992
  "integration": integration_status,
12993
  "limit": clean_limit,
12994
  "has_next": bool(next_cursor),
@@ -13244,6 +14003,60 @@ def update_settings(payload: AppSettingsRequest, response: Response) -> Dict[str
13244
  )
13245
  redis_client.set(redis_key, str(int(parsed)))
13246
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13247
  _save_tater_api_settings_from_updates(updates)
13248
 
13249
  local_model_keys_cache: Optional[set[Tuple[str, str]]] = None
@@ -13308,10 +14121,25 @@ def update_settings(payload: AppSettingsRequest, response: Response) -> Dict[str
13308
  "hydra_llm_provider",
13309
  "hydra_base_servers",
13310
  "hydra_hf_transformers_context_tokens",
 
 
 
 
 
13311
  "hydra_llama_cpp_context_tokens",
13312
  "hydra_llama_cpp_mtp_enabled",
13313
  "hydra_llama_cpp_mtp_draft_tokens",
 
 
 
 
13314
  "hydra_mlx_lm_context_tokens",
 
 
 
 
 
 
13315
  }
13316
  spudex_model_keys = {"spudex_llm_provider", "spudex_llm_host", "spudex_llm_model"}
13317
  vision_model_keys = {
@@ -13402,6 +14230,35 @@ def update_settings(payload: AppSettingsRequest, response: Response) -> Dict[str
13402
  "hydra_hf_transformers_context_tokens",
13403
  HYDRA_HF_TRANSFORMERS_CONTEXT_TOKENS_KEY,
13404
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13405
  _save_local_llm_context_setting(
13406
  "hydra_llama_cpp_context_tokens",
13407
  HYDRA_LLAMA_CPP_CONTEXT_TOKENS_KEY,
@@ -13422,11 +14279,70 @@ def update_settings(payload: AppSettingsRequest, response: Response) -> Dict[str
13422
  min_value=1,
13423
  max_value=16,
13424
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13425
  _save_local_llm_context_setting(
13426
  "hydra_mlx_lm_context_tokens",
13427
  HYDRA_MLX_LM_CONTEXT_TOKENS_KEY,
13428
  min_value=128,
13429
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13430
 
13431
  username = updates.get("username")
13432
  if isinstance(username, str):
@@ -13590,8 +14506,6 @@ def update_settings(payload: AppSettingsRequest, response: Response) -> Dict[str
13590
  updates.get("vision_provider", current_vision.get("provider") or HYDRA_LLM_PROVIDER_OPENAI_COMPATIBLE)
13591
  )
13592
  vision_model = str(updates.get("vision_model", current_vision.get("model") or "")).strip()
13593
- if vision_mode == "dedicated" and vision_provider == HYDRA_LLM_PROVIDER_MLX_LM:
13594
- raise HTTPException(status_code=400, detail="Dedicated local vision currently supports Transformers and llama.cpp GGUF models.")
13595
  if vision_mode == "dedicated" and _is_local_hydra_llm_provider(vision_provider):
13596
  _require_downloaded_local_model(vision_provider, vision_model, "Vision")
13597
  save_shared_vision_settings(
 
6
  import importlib
7
  import json
8
  import logging
9
+ import mimetypes
10
  import os
11
  import queue
12
  import re
 
64
  from emoji_responder import get_emoji_settings as get_core_emoji_settings, save_emoji_settings as save_core_emoji_settings
65
  from notify import notifier_destination_catalog
66
  from helpers import (
67
+ DEFAULT_HF_TRANSFORMERS_ATTN_IMPLEMENTATION,
68
  DEFAULT_HF_TRANSFORMERS_CONTEXT_TOKENS,
69
+ DEFAULT_HF_TRANSFORMERS_DEVICE,
70
+ DEFAULT_HF_TRANSFORMERS_DEVICE_MAP,
71
+ DEFAULT_HF_TRANSFORMERS_DTYPE,
72
+ DEFAULT_HF_TRANSFORMERS_TRUST_REMOTE_CODE,
73
+ DEFAULT_LLAMA_CPP_FLASH_ATTN,
74
  DEFAULT_LLAMA_CPP_CONTEXT_TOKENS,
75
  DEFAULT_LLAMA_CPP_MTP_DRAFT_TOKENS,
76
  DEFAULT_LLAMA_CPP_MTP_ENABLED,
77
+ DEFAULT_LLAMA_CPP_N_BATCH,
78
+ DEFAULT_LLAMA_CPP_N_UBATCH,
79
+ DEFAULT_LLAMA_CPP_OFFLOAD_KQV,
80
  DEFAULT_LLAMA_CPP_VISION_CONTEXT_TOKENS,
81
+ DEFAULT_MLX_LM_LAZY_LOAD,
82
+ DEFAULT_MLX_LM_TRUST_REMOTE_CODE,
83
+ HYDRA_HF_TRANSFORMERS_ATTN_IMPLEMENTATION_KEY,
84
  HYDRA_HF_TRANSFORMERS_CONTEXT_TOKENS_KEY,
85
+ HYDRA_HF_TRANSFORMERS_DEVICE_KEY,
86
+ HYDRA_HF_TRANSFORMERS_DEVICE_MAP_KEY,
87
+ HYDRA_HF_TRANSFORMERS_DTYPE_KEY,
88
+ HYDRA_HF_TRANSFORMERS_TRUST_REMOTE_CODE_KEY,
89
  HYDRA_LLM_BASE_SERVERS_KEY,
90
  HYDRA_LLM_PROVIDER_HF_TRANSFORMERS,
91
  HYDRA_LLM_PROVIDER_KEY,
 
93
  HYDRA_LLM_PROVIDER_MLX_LM,
94
  HYDRA_LLM_PROVIDER_OPENAI_COMPATIBLE,
95
  HYDRA_LLAMA_CPP_CONTEXT_TOKENS_KEY,
96
+ HYDRA_LLAMA_CPP_FLASH_ATTN_KEY,
97
  HYDRA_LLAMA_CPP_MTP_DRAFT_TOKENS_KEY,
98
  HYDRA_LLAMA_CPP_MTP_ENABLED_KEY,
99
+ HYDRA_LLAMA_CPP_N_BATCH_KEY,
100
+ HYDRA_LLAMA_CPP_N_UBATCH_KEY,
101
+ HYDRA_LLAMA_CPP_OFFLOAD_KQV_KEY,
102
  HYDRA_LLAMA_CPP_VISION_CONTEXT_TOKENS_KEY,
103
+ HYDRA_MLX_ENGINE_KV_BITS_KEY,
104
+ HYDRA_MLX_ENGINE_KV_GROUP_SIZE_KEY,
105
+ HYDRA_MLX_ENGINE_PREFILL_STEP_SIZE_KEY,
106
+ HYDRA_MLX_ENGINE_QUANTIZED_KV_START_KEY,
107
  HYDRA_MLX_LM_CONTEXT_TOKENS_KEY,
108
+ HYDRA_MLX_LM_LAZY_LOAD_KEY,
109
+ HYDRA_MLX_LM_TRUST_REMOTE_CODE_KEY,
110
  HfLlmDownloadCancelled,
111
  decrypt_current_redis_snapshot,
112
  download_hf_transformers_llm_model,
 
115
  encrypt_current_redis_snapshot,
116
  ensure_redis_encryption_key,
117
  get_llama_cpp_runtime_diagnostics,
118
+ get_llama_cpp_chat_template_info,
119
+ get_local_llm_chat_template_info,
120
  get_local_llm_loaded_models_snapshot,
121
  get_redis_connection_config,
122
  get_redis_encryption_status,
123
  get_redis_connection_status,
124
  get_llm_call_runtime_summary,
125
+ get_llm_debug_runtime_snapshot,
126
  get_vision_call_runtime_summary,
127
  get_llm_client_from_env,
128
  preload_hf_transformers_llm_model,
 
138
  set_main_loop,
139
  shutdown_internal_redis,
140
  test_redis_connection_settings,
141
+ set_llama_cpp_chat_template_override,
142
+ clear_llama_cpp_chat_template_override,
143
+ set_local_llm_chat_template_override,
144
+ clear_local_llm_chat_template_override,
145
  unload_local_llm_models,
146
  )
147
  from runtime_executors import configure_runtime_executors, run_dashboard, run_wake, shutdown_runtime_executors
 
567
  return _as_bool_flag(raw, default=default)
568
 
569
 
570
+ def _read_text_choice_setting(
571
+ redis_key: str,
572
+ env_keys: Tuple[str, ...],
573
+ default: str,
574
+ *,
575
+ allowed: Tuple[str, ...],
576
+ ) -> str:
577
+ raw = str(redis_client.get(redis_key) or "").strip()
578
+ if not raw:
579
+ for env_key in env_keys:
580
+ raw = str(os.getenv(env_key) or "").strip()
581
+ if raw:
582
+ break
583
+ token = str(raw or default or "").strip().lower()
584
+ aliases = {
585
+ "fp16": "float16",
586
+ "half": "float16",
587
+ "bf16": "bfloat16",
588
+ "fp32": "float32",
589
+ "none": "disabled",
590
+ "off": "disabled",
591
+ "false": "disabled",
592
+ "0": "disabled",
593
+ "default": "default",
594
+ "auto": "auto",
595
+ }
596
+ token = aliases.get(token, token)
597
+ allowed_set = {str(item).strip().lower() for item in allowed}
598
+ default_token = str(default or "").strip().lower()
599
+ return token if token in allowed_set else default_token
600
+
601
+
602
+ def _read_optional_int_setting(
603
+ redis_key: str,
604
+ env_keys: Tuple[str, ...],
605
+ *,
606
+ minimum: int = 0,
607
+ maximum: int = 1_048_576,
608
+ allow_zero: bool = False,
609
+ ) -> str:
610
+ raw = str(redis_client.get(redis_key) or "").strip()
611
+ if not raw:
612
+ for env_key in env_keys:
613
+ raw = str(os.getenv(env_key) or "").strip()
614
+ if raw:
615
+ break
616
+ if not raw:
617
+ return ""
618
+ try:
619
+ value = int(float(raw))
620
+ except Exception:
621
+ return ""
622
+ if value < 0 or (value == 0 and not allow_zero):
623
+ return ""
624
+ return str(max(int(minimum), min(int(maximum), int(value))))
625
+
626
+
627
  def _read_bounded_int_setting(
628
  redis_key: str,
629
  env_keys: Tuple[str, ...],
 
1695
  task: str = "text-generation",
1696
  ) -> str:
1697
  library = _hf_browser_provider_library(provider)
1698
+ app_filter = _hf_browser_provider_app_filter(provider, task)
1699
+ pipeline_tag = _hf_browser_provider_pipeline_filter(provider, task)
1700
  params: Dict[str, Any] = {
1701
  "sort": sort,
1702
  "direction": "-1",
1703
  "limit": str(max(1, min(100, int(limit or 24)))),
1704
  "full": "true",
1705
  "cardData": "true",
 
1706
  }
1707
+ if pipeline_tag:
1708
+ params["pipeline_tag"] = pipeline_tag
1709
  if library:
1710
  params["library"] = library
1711
  if app_filter:
 
1895
  return q
1896
 
1897
 
1898
+ HF_BROWSER_TATER_PICKS: Tuple[Dict[str, Any], ...] = (
1899
+ {
1900
+ "id": "TaterTotterson/gemma-4-26B-A4B-it-GGUF-Tater-NoThink",
1901
+ "provider": HYDRA_LLM_PROVIDER_LLAMA_CPP,
1902
+ "author": "TaterTotterson",
1903
+ "model_size": "26B",
1904
+ "library_name": "llama.cpp",
1905
+ "pipeline_tag": "image-text-to-text",
1906
+ "license": "apache-2.0",
1907
+ "supports_vision": True,
1908
+ "tasks": ("text-generation", "image-text-to-text"),
1909
+ "tags": (
1910
+ "tater",
1911
+ "nothink",
1912
+ "gguf",
1913
+ "llama.cpp",
1914
+ "gemma4",
1915
+ "vision",
1916
+ "UD-Q4_K_M",
1917
+ ),
1918
+ "tater_pick_label": "Tater Pick",
1919
+ "tater_pick_note": "NoThink UD-Q4_K_M with matching mmproj-F16.",
1920
+ "preferred_gguf": "gemma-4-26B-A4B-it-UD-Q4_K_M.gguf",
1921
+ "preferred_mmproj": "mmproj-F16.gguf",
1922
+ },
1923
+ )
1924
+
1925
+
1926
+ def _hf_browser_tater_pick_owner() -> str:
1927
+ return str(os.getenv("TATER_HF_TATER_PICK_OWNER") or "TaterTotterson").strip()
1928
+
1929
+
1930
+ def _hf_browser_tater_pick_tag_tokens() -> Tuple[str, ...]:
1931
+ raw = str(os.getenv("TATER_HF_TATER_PICK_TAGS") or "").strip()
1932
+ if raw:
1933
+ tags = [item.strip().lower() for item in re.split(r"[,;\s]+", raw) if item.strip()]
1934
+ else:
1935
+ tags = ["tater-pick", "tater-recommended", "tater-nothink"]
1936
+ return tuple(dict.fromkeys(tags))
1937
+
1938
+
1939
+ def _hf_browser_card_tags(card_data: Any) -> List[str]:
1940
+ if not card_data:
1941
+ return []
1942
+ if isinstance(card_data, dict):
1943
+ raw = card_data.get("tags") or []
1944
+ else:
1945
+ raw = getattr(card_data, "tags", []) or []
1946
+ if isinstance(raw, str):
1947
+ return [raw]
1948
+ if isinstance(raw, list):
1949
+ return [str(item or "").strip() for item in raw if str(item or "").strip()]
1950
+ return []
1951
+
1952
+
1953
+ def _hf_browser_paths_to_file_rows(paths: List[str]) -> List[Dict[str, Any]]:
1954
+ rows: List[Dict[str, Any]] = []
1955
+ seen: set[str] = set()
1956
+ for raw_path in paths:
1957
+ path = str(raw_path or "").strip()
1958
+ if not path or path in seen:
1959
+ continue
1960
+ seen.add(path)
1961
+ lower = path.lower()
1962
+ rows.append(
1963
+ {
1964
+ "path": path,
1965
+ "size": 0,
1966
+ "is_gguf": lower.endswith(".gguf"),
1967
+ "is_mmproj": lower.endswith(".gguf") and "mmproj" in lower,
1968
+ "is_safetensors": lower.endswith(".safetensors"),
1969
+ "is_config": os.path.basename(lower) in {"config.json", "tokenizer.json", "tokenizer.model"},
1970
+ "quant": _hf_browser_file_quant(path),
1971
+ }
1972
+ )
1973
+ return rows
1974
+
1975
+
1976
+ def _hf_browser_tater_pick_metadata(
1977
+ model_id: Any,
1978
+ *,
1979
+ tags: Optional[List[str]] = None,
1980
+ card_data: Any = None,
1981
+ files: Optional[List[str]] = None,
1982
+ ) -> Dict[str, Any]:
1983
+ repo = str(model_id or "").strip()
1984
+ if not repo:
1985
+ return {}
1986
+
1987
+ static_row = _hf_browser_tater_pick_by_id(repo)
1988
+ if static_row:
1989
+ return static_row
1990
+
1991
+ owner = _hf_browser_tater_pick_owner().lower()
1992
+ if not owner or "/" not in repo or repo.split("/", 1)[0].lower() != owner:
1993
+ return {}
1994
+
1995
+ tag_set = {
1996
+ str(item or "").strip().lower()
1997
+ for item in [*(tags or []), *_hf_browser_card_tags(card_data)]
1998
+ if str(item or "").strip()
1999
+ }
2000
+ pick_tags = set(_hf_browser_tater_pick_tag_tokens())
2001
+ has_pick_tag = bool(tag_set.intersection(pick_tags))
2002
+ has_nothink_marker = any(
2003
+ token in tag_set
2004
+ for token in (
2005
+ "nothink",
2006
+ "no-think",
2007
+ "no_think",
2008
+ "thinking-disabled",
2009
+ "disable-thinking",
2010
+ "non-thinking",
2011
+ )
2012
+ )
2013
+ if not has_pick_tag:
2014
+ return {}
2015
+
2016
+ file_rows = _hf_browser_paths_to_file_rows(files or [])
2017
+ preferred_gguf = _hf_browser_preferred_gguf(file_rows) if file_rows else ""
2018
+ preferred_mmproj = _hf_browser_preferred_mmproj(file_rows, preferred_gguf) if file_rows else ""
2019
+ quant = _hf_browser_file_quant(preferred_gguf)
2020
+ note_bits: List[str] = []
2021
+ if has_nothink_marker:
2022
+ note_bits.append("NoThink")
2023
+ if quant:
2024
+ note_bits.append(quant)
2025
+ if preferred_mmproj:
2026
+ note_bits.append("with matching projector")
2027
+ note = " ".join(note_bits).strip()
2028
+ return {
2029
+ "id": repo,
2030
+ "tater_pick_label": "Tater Pick",
2031
+ "tater_pick_note": note or "Curated Tater model.",
2032
+ "preferred_gguf": preferred_gguf,
2033
+ "preferred_mmproj": preferred_mmproj,
2034
+ }
2035
+
2036
+
2037
+ def _hf_browser_tater_pick_by_id(model_id: Any) -> Dict[str, Any]:
2038
+ needle = str(model_id or "").strip().lower()
2039
+ if not needle:
2040
+ return {}
2041
+ for row in HF_BROWSER_TATER_PICKS:
2042
+ if str(row.get("id") or "").strip().lower() == needle:
2043
+ return dict(row)
2044
+ return {}
2045
+
2046
+
2047
+ def _hf_browser_tater_pick_matches_query(row: Dict[str, Any], query: str) -> bool:
2048
+ q = str(query or "").strip().lower()
2049
+ if not q:
2050
+ return True
2051
+ haystack = " ".join(
2052
+ [
2053
+ str(row.get("id") or ""),
2054
+ str(row.get("provider") or ""),
2055
+ str(row.get("library_name") or ""),
2056
+ str(row.get("model_size") or ""),
2057
+ str(row.get("tater_pick_note") or ""),
2058
+ " ".join(str(item or "") for item in (row.get("tags") or [])),
2059
+ ]
2060
+ ).lower()
2061
+ return all(token in haystack for token in q.split())
2062
+
2063
+
2064
+ def _hf_browser_tater_pick_summary(row: Dict[str, Any]) -> Dict[str, Any]:
2065
+ provider_token = _normalize_hydra_llm_provider(row.get("provider"))
2066
+ tags = [str(item or "").strip() for item in (row.get("tags") or []) if str(item or "").strip()]
2067
+ return {
2068
+ "id": str(row.get("id") or "").strip(),
2069
+ "author": str(row.get("author") or "").strip(),
2070
+ "model_size": str(row.get("model_size") or "").strip(),
2071
+ "downloads": 0,
2072
+ "likes": 0,
2073
+ "last_modified": "",
2074
+ "pipeline_tag": str(row.get("pipeline_tag") or "").strip(),
2075
+ "library_name": str(row.get("library_name") or "").strip(),
2076
+ "tags": tags[:30],
2077
+ "license": str(row.get("license") or "").strip(),
2078
+ "private": False,
2079
+ "gated": "",
2080
+ "compatible": True,
2081
+ "supports_vision": bool(row.get("supports_vision")),
2082
+ "provider": provider_token,
2083
+ "provider_label": _hydra_llm_provider_label(provider_token),
2084
+ "tater_pick": True,
2085
+ "tater_pick_label": str(row.get("tater_pick_label") or "Tater Pick").strip(),
2086
+ "tater_pick_note": str(row.get("tater_pick_note") or "").strip(),
2087
+ "preferred_gguf": str(row.get("preferred_gguf") or "").strip(),
2088
+ "preferred_mmproj": str(row.get("preferred_mmproj") or "").strip(),
2089
+ }
2090
+
2091
+
2092
+ def _hf_browser_provider_for_model(model: Any, fallback: str = HYDRA_LLM_PROVIDER_HF_TRANSFORMERS) -> str:
2093
+ for provider_token in (HYDRA_LLM_PROVIDER_LLAMA_CPP, HYDRA_LLM_PROVIDER_MLX_LM, HYDRA_LLM_PROVIDER_HF_TRANSFORMERS):
2094
+ try:
2095
+ if _hf_browser_provider_matches(model, provider_token):
2096
+ return provider_token
2097
+ except Exception:
2098
+ continue
2099
+ return _normalize_hydra_llm_provider(fallback)
2100
+
2101
+
2102
+ def _hf_browser_tater_pick_models_from_hub(*, query: str, task: str, limit: int, provider: str) -> List[Dict[str, Any]]:
2103
+ owner = _hf_browser_tater_pick_owner()
2104
+ if not owner:
2105
+ return []
2106
+ try:
2107
+ from huggingface_hub import HfApi # type: ignore
2108
+ except Exception:
2109
+ return []
2110
+
2111
+ api = HfApi(token=_hf_browser_token())
2112
+ try:
2113
+ iterator = _hf_hub_call_compat(
2114
+ api.list_models,
2115
+ author=owner,
2116
+ sort="lastModified",
2117
+ direction=-1,
2118
+ limit=max(50, min(200, int(limit or 24) * 4)),
2119
+ full=True,
2120
+ cardData=True,
2121
+ fetch_config=True,
2122
+ )
2123
+ raw_models = list(iterator or [])
2124
+ except Exception as exc:
2125
+ logger.warning("[huggingface-browser] Tater Picks live lookup failed: %s", exc)
2126
+ return []
2127
+
2128
+ task_token = _normalize_hf_browser_task(task)
2129
+ provider_filter = _normalize_hydra_llm_provider(provider)
2130
+ rows: List[Dict[str, Any]] = []
2131
+ seen: set[str] = set()
2132
+ for model in raw_models:
2133
+ model_id = str(_hf_browser_object_value(model, "modelId") or _hf_browser_object_value(model, "id") or "").strip()
2134
+ if not model_id or model_id.lower() in seen:
2135
+ continue
2136
+ seen.add(model_id.lower())
2137
+ tags = [
2138
+ str(item or "").strip()
2139
+ for item in (_hf_browser_object_value(model, "tags", []) or [])
2140
+ if str(item or "").strip()
2141
+ ]
2142
+ siblings = _hf_browser_object_value(model, "siblings", []) or []
2143
+ files = [
2144
+ str(_hf_browser_object_value(item, "rfilename") or _hf_browser_object_value(item, "path") or "").strip()
2145
+ for item in siblings
2146
+ if str(_hf_browser_object_value(item, "rfilename") or _hf_browser_object_value(item, "path") or "").strip()
2147
+ ]
2148
+ card_data = _hf_browser_object_value(model, "cardData", None) or _hf_browser_object_value(model, "card_data", None)
2149
+ if not _hf_browser_tater_pick_metadata(model_id, tags=tags, card_data=card_data, files=files):
2150
+ continue
2151
+ provider_token = _hf_browser_provider_for_model(model, fallback=HYDRA_LLM_PROVIDER_LLAMA_CPP)
2152
+ if provider_token != provider_filter:
2153
+ continue
2154
+ summary = _hf_browser_model_summary(model, provider=provider_token)
2155
+ if task_token == "image-text-to-text" and not bool(summary.get("supports_vision")):
2156
+ continue
2157
+ if not _hf_browser_tater_pick_matches_query(summary, query):
2158
+ continue
2159
+ rows.append(summary)
2160
+ return rows[: max(1, min(100, int(limit or 24)))]
2161
+
2162
+
2163
+ def _hf_browser_tater_pick_models(*, query: str, task: str, limit: int, provider: str) -> List[Dict[str, Any]]:
2164
+ task_token = _normalize_hf_browser_task(task)
2165
+ provider_filter = _normalize_hydra_llm_provider(provider)
2166
+ live_rows = _hf_browser_tater_pick_models_from_hub(query=query, task=task_token, limit=limit, provider=provider_filter)
2167
+ rows: List[Dict[str, Any]] = list(live_rows)
2168
+ seen = {str(row.get("id") or "").strip().lower() for row in rows if str(row.get("id") or "").strip()}
2169
+ for row in HF_BROWSER_TATER_PICKS:
2170
+ if str(row.get("id") or "").strip().lower() in seen:
2171
+ continue
2172
+ if _normalize_hydra_llm_provider(row.get("provider")) != provider_filter:
2173
+ continue
2174
+ tasks = {str(item or "").strip() for item in (row.get("tasks") or []) if str(item or "").strip()}
2175
+ if tasks and task_token not in tasks:
2176
+ continue
2177
+ if not _hf_browser_tater_pick_matches_query(row, query):
2178
+ continue
2179
+ rows.append(_hf_browser_tater_pick_summary(row))
2180
+ return rows[: max(1, min(100, int(limit or 24)))]
2181
+
2182
+
2183
+ def _hf_browser_provider_app_filter(provider: str, task: str = "text-generation") -> str:
2184
  provider_token = _normalize_hydra_llm_provider(provider)
2185
+ task_token = _normalize_hf_browser_task(task)
2186
  if provider_token == HYDRA_LLM_PROVIDER_LLAMA_CPP:
2187
  return "llama.cpp"
2188
  if provider_token == HYDRA_LLM_PROVIDER_MLX_LM:
2189
+ if task_token == "image-text-to-text":
2190
+ return ""
2191
  return "mlx-lm"
2192
  return ""
2193
 
 
2199
  return "transformers"
2200
 
2201
 
2202
+ def _hf_browser_provider_pipeline_filter(provider: str, task: str = "text-generation") -> str:
2203
+ provider_token = _normalize_hydra_llm_provider(provider)
2204
+ task_token = _normalize_hf_browser_task(task)
2205
+ if task_token == "text-generation" and provider_token in {HYDRA_LLM_PROVIDER_LLAMA_CPP, HYDRA_LLM_PROVIDER_MLX_LM}:
2206
+ return ""
2207
+ return task_token
2208
+
2209
+
2210
  def _hf_browser_text_generation_tokens() -> Tuple[str, ...]:
2211
+ return ("text-generation", "text2text-generation", "conversational", "image-text-to-text")
2212
 
2213
 
2214
  def _hf_browser_vision_tokens() -> Tuple[str, ...]:
 
2301
  return (
2302
  "mlx-lm" in provider_haystack
2303
  or "mlx_lm" in provider_haystack
2304
+ or "mlx-vlm" in provider_haystack
2305
+ or "mlx_vlm" in provider_haystack
2306
  or library == "mlx"
2307
  or "mlx" in tags
2308
  or "mlx" in files
 
2313
 
2314
  def _hf_browser_file_quant(path: str) -> str:
2315
  name = os.path.basename(str(path or "")).upper()
2316
+ for quant in (
2317
+ "UD-Q4_K_M",
2318
+ "UD_Q4_K_M",
2319
+ "UD-Q4_K_S",
2320
+ "UD_Q4_K_S",
2321
+ "UD-Q5_K_M",
2322
+ "UD_Q5_K_M",
2323
+ "Q2_K",
2324
+ "Q3_K_M",
2325
+ "Q4_K_M",
2326
+ "Q4_K_S",
2327
+ "Q5_K_M",
2328
+ "Q5_K_S",
2329
+ "Q6_K",
2330
+ "Q8_0",
2331
+ "F16",
2332
+ "BF16",
2333
+ ):
2334
  if quant in name:
2335
+ return quant.replace("_Q", "-Q", 1) if quant.startswith("UD_") else quant
2336
  return ""
2337
 
2338
 
 
2340
  ggufs = [row for row in files if bool(row.get("is_gguf")) and "mmproj" not in str(row.get("path") or "").lower()]
2341
  if not ggufs:
2342
  return ""
2343
+ preferred = ("UD-Q4_K_M", "Q4_K_M", "UD-Q5_K_M", "Q5_K_M", "UD-Q4_K_S", "Q4_K_S", "Q5_0", "Q4_0", "Q8_0")
2344
 
2345
  def _score(row: Dict[str, Any]) -> Tuple[int, int, str]:
2346
  path = str(row.get("path") or "")
 
2393
  lower_files = " ".join(files).lower()
2394
  lower_library = str(_hf_browser_object_value(model, "library_name") or "").strip().lower()
2395
  provider_haystack = f"{lower_id} {lower_library} {lower_tags} {lower_files}"
2396
+ card_data = _hf_browser_object_value(model, "cardData", None) or _hf_browser_object_value(model, "card_data", None)
2397
+ tater_pick = _hf_browser_tater_pick_metadata(model_id, tags=tags, card_data=card_data, files=files)
2398
  supports_vision = _hf_browser_is_vision_model(model) or "mmproj" in lower_files
2399
  if provider_token == HYDRA_LLM_PROVIDER_LLAMA_CPP:
2400
  compatible = (
 
2406
  or ".gguf" in lower_files
2407
  )
2408
  elif provider_token == HYDRA_LLM_PROVIDER_MLX_LM:
2409
+ compatible = (
2410
+ "mlx-lm" in provider_haystack
2411
+ or "mlx_lm" in provider_haystack
2412
+ or "mlx-vlm" in provider_haystack
2413
+ or "mlx_vlm" in provider_haystack
2414
+ or "mlx" in provider_haystack
2415
+ )
2416
  else:
2417
  compatible = ".gguf" not in lower_files
 
2418
  model_size_label = _hf_browser_model_size_label(model_id, tags, files, model, card_data)
2419
+ summary = {
2420
  "id": model_id,
2421
  "author": str(_hf_browser_object_value(model, "author") or "").strip(),
2422
  "model_size": model_size_label,
 
2437
  "provider": provider_token,
2438
  "provider_label": _hydra_llm_provider_label(provider_token),
2439
  }
2440
+ if tater_pick:
2441
+ summary.update(
2442
+ {
2443
+ "tater_pick": True,
2444
+ "tater_pick_label": str(tater_pick.get("tater_pick_label") or "Tater Pick").strip(),
2445
+ "tater_pick_note": str(tater_pick.get("tater_pick_note") or "").strip(),
2446
+ "preferred_gguf": str(tater_pick.get("preferred_gguf") or "").strip(),
2447
+ "preferred_mmproj": str(tater_pick.get("preferred_mmproj") or "").strip(),
2448
+ }
2449
+ )
2450
+ return summary
2451
 
2452
 
2453
  def _hf_browser_file_rows(info: Any, fallback_files: List[str]) -> List[Dict[str, Any]]:
 
2819
  "downloaded_ts": float(latest_snapshot.stat().st_mtime if latest_snapshot.exists() else time.time()),
2820
  "max_context_tokens": max_context,
2821
  "context_source": context_source,
2822
+ "supports_vision": bool(
2823
+ provider_token != HYDRA_LLM_PROVIDER_MLX_LM
2824
+ and _local_llm_json_files_support_vision(latest_snapshot)
2825
+ ),
2826
  }
2827
  )
2828
  return rows
 
2864
  supports_vision = bool(row.get("supports_vision"))
2865
  mmproj_filename = str(row.get("mmproj_filename") or "").strip()
2866
  mmproj_path = str(row.get("mmproj_path") or "").strip()
2867
+ if provider == HYDRA_LLM_PROVIDER_MLX_LM:
2868
+ supports_vision = False
2869
+ elif provider == HYDRA_LLM_PROVIDER_LLAMA_CPP:
2870
  path_obj = Path(str(row.get("model_path") or "")).expanduser()
2871
  if path_obj.exists():
2872
  detected_mmproj = _local_llm_mmproj_for_gguf(path_obj)
 
2931
  return {"models": models, "by_provider": by_provider, "updated_ts": time.time()}
2932
 
2933
 
2934
+ def _local_llm_chat_template_model_info(provider: str, model: str) -> Dict[str, Any]:
2935
+ provider_token = _normalize_hydra_llm_provider(provider)
2936
+ if not _is_local_hydra_llm_provider(provider_token):
2937
+ raise HTTPException(status_code=400, detail="Local LLM provider is required.")
2938
+ model_token = str(model or "").strip()
2939
+ if not model_token:
2940
+ raise HTTPException(status_code=400, detail="Local model is required.")
2941
+ payload = _local_llm_models_payload(provider=provider_token)
2942
+ row = next(
2943
+ (
2944
+ dict(item)
2945
+ for item in payload.get("models", [])
2946
+ if _normalize_hydra_llm_provider(item.get("provider")) == provider_token
2947
+ and str(item.get("model") or "").strip() == model_token
2948
+ ),
2949
+ {},
2950
+ )
2951
+ model_path = str(row.get("model_path") or "").strip()
2952
+ info = get_local_llm_chat_template_info(provider_token, model_token, model_path=model_path)
2953
+ info["installed"] = bool(row)
2954
+ info["provider"] = provider_token
2955
+ info["provider_label"] = _hydra_llm_provider_label(provider_token)
2956
+ info["model_path"] = model_path or str(info.get("model_path") or "")
2957
+ return info
2958
+
2959
+
2960
+ def _llama_cpp_chat_template_model_info(model: str) -> Dict[str, Any]:
2961
+ return _local_llm_chat_template_model_info(HYDRA_LLM_PROVIDER_LLAMA_CPP, model)
2962
+
2963
+
2964
  def _record_downloaded_local_llm_model(
2965
  *,
2966
  provider: str,
 
3916
  return merged
3917
 
3918
 
3919
+ def _compact_chat_history_row(row: Dict[str, Any]) -> Tuple[Dict[str, Any], bool]:
3920
+ if not isinstance(row, dict):
3921
+ return row, False
3922
+ content = row.get("content")
3923
+ if not isinstance(content, dict):
3924
+ return row, False
3925
+ content_type = str(content.get("type") or "").strip().lower()
3926
+ mimetype_value = str(content.get("mimetype") or content.get("mime_type") or "").strip().lower()
3927
+ if content_type not in {"image", "audio", "video", "file"} and not mimetype_value.startswith(("image/", "audio/", "video/")):
3928
+ return row, False
3929
+ has_inline_payload = any(
3930
+ isinstance(content.get(key), (bytes, bytearray)) or (isinstance(content.get(key), str) and content.get(key).strip())
3931
+ for key in ("data", "bytes", "data_b64")
3932
+ )
3933
+ has_materializable_ref = any(str(content.get(key) or "").strip() for key in ("blob_key", "path", "file_path", "artifact_path"))
3934
+ if not has_inline_payload and not has_materializable_ref:
3935
+ return row, False
3936
+ normalized = _normalize_plugin_response_item(content)
3937
+ if not isinstance(normalized, dict) or normalized == content:
3938
+ return row, False
3939
+ compacted = dict(row)
3940
+ compacted["content"] = normalized
3941
+ return compacted, True
3942
+
3943
+
3944
  def _load_chat_history_tail(count: int) -> List[Dict[str, Any]]:
3945
  if count <= 0:
3946
  return []
3947
+ try:
3948
+ list_len = int(redis_client.llen(CHAT_HISTORY_KEY) or 0)
3949
+ except Exception:
3950
+ list_len = 0
3951
+ start_index = max(0, list_len - int(count))
3952
  raw = redis_client.lrange(CHAT_HISTORY_KEY, -count, -1)
3953
  out: List[Dict[str, Any]] = []
3954
+ for offset, line in enumerate(raw):
3955
  try:
3956
  parsed = json.loads(line)
3957
  if isinstance(parsed, dict):
3958
+ compacted, changed = _compact_chat_history_row(parsed)
3959
+ out.append(compacted)
3960
+ if changed and list_len > 0:
3961
+ try:
3962
+ redis_client.lset(CHAT_HISTORY_KEY, start_index + offset, json.dumps(compacted))
3963
+ except Exception:
3964
+ pass
3965
  except Exception:
3966
  continue
3967
  return out
 
3970
  def _load_chat_history() -> List[Dict[str, Any]]:
3971
  raw = redis_client.lrange(CHAT_HISTORY_KEY, 0, -1)
3972
  out: List[Dict[str, Any]] = []
3973
+ for idx, line in enumerate(raw):
3974
  try:
3975
  parsed = json.loads(line)
3976
  if isinstance(parsed, dict):
3977
+ compacted, changed = _compact_chat_history_row(parsed)
3978
+ out.append(compacted)
3979
+ if changed:
3980
+ try:
3981
+ redis_client.lset(CHAT_HISTORY_KEY, idx, json.dumps(compacted))
3982
+ except Exception:
3983
+ pass
3984
  except Exception:
3985
  continue
3986
  return out
 
4790
  if not isinstance(item, dict):
4791
  return item
4792
 
4793
+ raw_type = str(item.get("type") or "").strip().lower()
4794
+ name = str(item.get("name") or item.get("filename") or "attachment").strip() or "attachment"
4795
+ mimetype_value = str(item.get("mimetype") or item.get("mime_type") or "").strip().lower()
4796
+ if not mimetype_value:
4797
+ mimetype_value = str(mimetypes.guess_type(name)[0] or "").strip().lower() or "application/octet-stream"
4798
+
4799
+ inferred_type = _media_type_from_mimetype(mimetype_value)
4800
+ if raw_type in {"image", "audio", "video"}:
4801
+ media_type = raw_type
4802
+ elif raw_type == "file":
4803
+ media_type = inferred_type if inferred_type != "file" else "file"
4804
+ elif inferred_type in {"image", "audio", "video"}:
4805
+ media_type = inferred_type
4806
+ else:
4807
  return item
4808
 
4809
  raw = None
 
4811
  raw = bytes(item.get("data"))
4812
  elif isinstance(item.get("bytes"), (bytes, bytearray)):
4813
  raw = bytes(item.get("bytes"))
4814
+ elif isinstance(item.get("data_b64"), str):
4815
+ try:
4816
+ raw = _decode_attachment_data(item.get("data_b64"))
4817
+ except Exception:
4818
+ raw = None
4819
+ elif isinstance(item.get("data"), str):
4820
+ try:
4821
+ raw = _decode_attachment_data(item.get("data"))
4822
+ except Exception:
4823
+ raw = None
4824
 
4825
  if raw is None:
4826
+ blob_key = str(item.get("blob_key") or "").strip()
4827
+ if blob_key:
4828
+ try:
4829
+ blob = redis_blob_client.get(blob_key)
4830
+ if isinstance(blob, (bytes, bytearray)):
4831
+ raw = bytes(blob)
4832
+ except Exception:
4833
+ raw = None
4834
+
4835
+ if raw is None:
4836
+ path_value = str(item.get("path") or item.get("file_path") or item.get("artifact_path") or "").strip()
4837
+ if path_value:
4838
+ try:
4839
+ path = Path(path_value).expanduser()
4840
+ if path.is_file():
4841
+ raw = path.read_bytes()
4842
+ if name == "attachment":
4843
+ name = path.name or name
4844
+ if mimetype_value == "application/octet-stream":
4845
+ mimetype_value = str(mimetypes.guess_type(name)[0] or mimetype_value).strip().lower()
4846
+ inferred_type = _media_type_from_mimetype(mimetype_value)
4847
+ if raw_type in {"", "file"} and inferred_type != "file":
4848
+ media_type = inferred_type
4849
+ except Exception:
4850
+ raw = None
4851
 
4852
  safe = dict(item)
4853
  safe.pop("data", None)
4854
  safe.pop("bytes", None)
4855
+ safe.pop("data_b64", None)
4856
+ safe["type"] = media_type
4857
+ safe["name"] = name
4858
+ safe["mimetype"] = mimetype_value
4859
+ if raw is not None:
4860
+ file_id = str(uuid.uuid4())
4861
+ _store_file_blob_in_redis(file_id, raw)
4862
+ safe["id"] = file_id
4863
+ safe["size"] = len(raw)
4864
+ safe.pop("blob_key", None)
4865
+ safe.pop("path", None)
4866
+ safe.pop("file_path", None)
4867
+ safe.pop("artifact_path", None)
4868
+ elif str(safe.get("file_id") or "").strip() and not str(safe.get("id") or "").strip():
4869
+ safe["id"] = str(safe.get("file_id") or "").strip()
4870
  return safe
4871
 
4872
 
 
6406
  model: Optional[str] = None
6407
 
6408
 
6409
+ class LlamaCppChatTemplateRequest(BaseModel):
6410
+ provider: Optional[str] = None
6411
+ model: Optional[str] = None
6412
+ template: Optional[str] = None
6413
+ reset: Optional[bool] = None
6414
+
6415
+
6416
  class HfLlmWarmupCancelRequest(BaseModel):
6417
  key: Optional[str] = None
6418
  provider: Optional[str] = None
 
6497
  hydra_llm_api_key: Optional[str] = None
6498
  hydra_llm_provider: Optional[str] = None
6499
  hydra_hf_transformers_context_tokens: Optional[Any] = None
6500
+ hydra_hf_transformers_device: Optional[str] = None
6501
+ hydra_hf_transformers_dtype: Optional[str] = None
6502
+ hydra_hf_transformers_device_map: Optional[str] = None
6503
+ hydra_hf_transformers_attn_implementation: Optional[str] = None
6504
+ hydra_hf_transformers_trust_remote_code: Optional[bool] = None
6505
  hydra_llama_cpp_context_tokens: Optional[Any] = None
6506
  hydra_llama_cpp_vision_context_tokens: Optional[Any] = None
6507
  hydra_llama_cpp_mtp_enabled: Optional[bool] = None
6508
  hydra_llama_cpp_mtp_draft_tokens: Optional[Any] = None
6509
+ hydra_llama_cpp_n_batch: Optional[Any] = None
6510
+ hydra_llama_cpp_n_ubatch: Optional[Any] = None
6511
+ hydra_llama_cpp_flash_attn: Optional[bool] = None
6512
+ hydra_llama_cpp_offload_kqv: Optional[bool] = None
6513
  hydra_mlx_lm_context_tokens: Optional[Any] = None
6514
+ hydra_mlx_lm_trust_remote_code: Optional[bool] = None
6515
+ hydra_mlx_lm_lazy_load: Optional[bool] = None
6516
+ hydra_mlx_engine_prefill_step_size: Optional[Any] = None
6517
+ hydra_mlx_engine_kv_bits: Optional[Any] = None
6518
+ hydra_mlx_engine_kv_group_size: Optional[Any] = None
6519
+ hydra_mlx_engine_quantized_kv_start: Optional[Any] = None
6520
  spudex_llm_provider: Optional[str] = None
6521
  spudex_llm_host: Optional[str] = None
6522
  spudex_llm_model: Optional[str] = None
 
10486
  return {"ok": True, **payload}
10487
 
10488
 
10489
+ @app.get("/api/runtime/llm/debug")
10490
+ def runtime_llm_debug(since_id: int = 0, limit: int = 200) -> Dict[str, Any]:
10491
+ return get_llm_debug_runtime_snapshot(since_id=since_id, limit=limit)
10492
+
10493
+
10494
  @app.post("/api/runtime/local-llm/unload")
10495
  def unload_runtime_local_llm(payload: LocalLlmUnloadRequest) -> Dict[str, Any]:
10496
  try:
 
12527
  "hydra_llm_model": "",
12528
  "hydra_llm_api_key": "",
12529
  "hydra_hf_transformers_context_tokens": str(DEFAULT_HF_TRANSFORMERS_CONTEXT_TOKENS),
12530
+ "hydra_hf_transformers_device": DEFAULT_HF_TRANSFORMERS_DEVICE,
12531
+ "hydra_hf_transformers_dtype": DEFAULT_HF_TRANSFORMERS_DTYPE,
12532
+ "hydra_hf_transformers_device_map": DEFAULT_HF_TRANSFORMERS_DEVICE_MAP,
12533
+ "hydra_hf_transformers_attn_implementation": DEFAULT_HF_TRANSFORMERS_ATTN_IMPLEMENTATION,
12534
+ "hydra_hf_transformers_trust_remote_code": bool(DEFAULT_HF_TRANSFORMERS_TRUST_REMOTE_CODE),
12535
  "hydra_llama_cpp_context_tokens": str(DEFAULT_LLAMA_CPP_CONTEXT_TOKENS),
12536
  "hydra_llama_cpp_vision_context_tokens": str(DEFAULT_LLAMA_CPP_VISION_CONTEXT_TOKENS),
12537
  "hydra_llama_cpp_mtp_enabled": bool(DEFAULT_LLAMA_CPP_MTP_ENABLED),
12538
  "hydra_llama_cpp_mtp_draft_tokens": str(DEFAULT_LLAMA_CPP_MTP_DRAFT_TOKENS),
12539
+ "hydra_llama_cpp_n_batch": str(DEFAULT_LLAMA_CPP_N_BATCH),
12540
+ "hydra_llama_cpp_n_ubatch": "",
12541
+ "hydra_llama_cpp_flash_attn": bool(DEFAULT_LLAMA_CPP_FLASH_ATTN),
12542
+ "hydra_llama_cpp_offload_kqv": bool(DEFAULT_LLAMA_CPP_OFFLOAD_KQV),
12543
  "hydra_mlx_lm_context_tokens": "",
12544
+ "hydra_mlx_lm_trust_remote_code": bool(DEFAULT_MLX_LM_TRUST_REMOTE_CODE),
12545
+ "hydra_mlx_lm_lazy_load": bool(DEFAULT_MLX_LM_LAZY_LOAD),
12546
+ "hydra_mlx_engine_prefill_step_size": "",
12547
+ "hydra_mlx_engine_kv_bits": "",
12548
+ "hydra_mlx_engine_kv_group_size": "",
12549
+ "hydra_mlx_engine_quantized_kv_start": "",
12550
  "hydra_beast_mode_enabled": False,
12551
  "hydra_max_ledger_items": int(DEFAULT_MAX_LEDGER_ITEMS),
12552
  "hydra_astraeus_plan_review_enabled": bool(DEFAULT_ASTRAEUS_PLAN_REVIEW_ENABLED),
 
12653
  ("TATER_HF_TRANSFORMERS_MAX_INPUT_TOKENS",),
12654
  DEFAULT_HF_TRANSFORMERS_CONTEXT_TOKENS,
12655
  ),
12656
+ "hydra_hf_transformers_device": _read_text_choice_setting(
12657
+ HYDRA_HF_TRANSFORMERS_DEVICE_KEY,
12658
+ ("TATER_HF_TRANSFORMERS_DEVICE",),
12659
+ DEFAULT_HF_TRANSFORMERS_DEVICE,
12660
+ allowed=("auto", "cuda", "mps", "cpu"),
12661
+ ),
12662
+ "hydra_hf_transformers_dtype": _read_text_choice_setting(
12663
+ HYDRA_HF_TRANSFORMERS_DTYPE_KEY,
12664
+ ("TATER_HF_TRANSFORMERS_DTYPE",),
12665
+ DEFAULT_HF_TRANSFORMERS_DTYPE,
12666
+ allowed=("auto", "float16", "bfloat16", "float32"),
12667
+ ),
12668
+ "hydra_hf_transformers_device_map": _read_text_choice_setting(
12669
+ HYDRA_HF_TRANSFORMERS_DEVICE_MAP_KEY,
12670
+ ("TATER_HF_TRANSFORMERS_DEVICE_MAP",),
12671
+ DEFAULT_HF_TRANSFORMERS_DEVICE_MAP,
12672
+ allowed=("default", "disabled", "auto", "balanced"),
12673
+ ),
12674
+ "hydra_hf_transformers_attn_implementation": _read_text_choice_setting(
12675
+ HYDRA_HF_TRANSFORMERS_ATTN_IMPLEMENTATION_KEY,
12676
+ ("TATER_HF_TRANSFORMERS_ATTN_IMPLEMENTATION",),
12677
+ DEFAULT_HF_TRANSFORMERS_ATTN_IMPLEMENTATION,
12678
+ allowed=("", "auto", "sdpa", "flash_attention_2", "eager"),
12679
+ ),
12680
+ "hydra_hf_transformers_trust_remote_code": _read_bool_setting(
12681
+ HYDRA_HF_TRANSFORMERS_TRUST_REMOTE_CODE_KEY,
12682
+ ("TATER_HF_TRANSFORMERS_TRUST_REMOTE_CODE",),
12683
+ DEFAULT_HF_TRANSFORMERS_TRUST_REMOTE_CODE,
12684
+ ),
12685
  "hydra_llama_cpp_context_tokens": _read_local_llm_context_setting(
12686
  HYDRA_LLAMA_CPP_CONTEXT_TOKENS_KEY,
12687
  ("TATER_LLAMA_CPP_N_CTX", "LLM_CONTEXT_SIZE"),
 
12704
  minimum=1,
12705
  maximum=16,
12706
  ),
12707
+ "hydra_llama_cpp_n_batch": _read_bounded_int_setting(
12708
+ HYDRA_LLAMA_CPP_N_BATCH_KEY,
12709
+ ("TATER_LLAMA_CPP_N_BATCH",),
12710
+ DEFAULT_LLAMA_CPP_N_BATCH,
12711
+ minimum=32,
12712
+ maximum=8192,
12713
+ ),
12714
+ "hydra_llama_cpp_n_ubatch": _read_bounded_int_setting(
12715
+ HYDRA_LLAMA_CPP_N_UBATCH_KEY,
12716
+ ("TATER_LLAMA_CPP_N_UBATCH",),
12717
+ DEFAULT_LLAMA_CPP_N_UBATCH,
12718
+ minimum=0,
12719
+ maximum=8192,
12720
+ ),
12721
+ "hydra_llama_cpp_flash_attn": _read_bool_setting(
12722
+ HYDRA_LLAMA_CPP_FLASH_ATTN_KEY,
12723
+ ("TATER_LLAMA_CPP_FLASH_ATTN",),
12724
+ DEFAULT_LLAMA_CPP_FLASH_ATTN,
12725
+ ),
12726
+ "hydra_llama_cpp_offload_kqv": _read_bool_setting(
12727
+ HYDRA_LLAMA_CPP_OFFLOAD_KQV_KEY,
12728
+ ("TATER_LLAMA_CPP_OFFLOAD_KQV",),
12729
+ DEFAULT_LLAMA_CPP_OFFLOAD_KQV,
12730
+ ),
12731
  "hydra_mlx_lm_context_tokens": _read_local_llm_context_setting(
12732
  HYDRA_MLX_LM_CONTEXT_TOKENS_KEY,
12733
  ("TATER_MLX_LM_MAX_KV_SIZE",),
12734
  None,
12735
  minimum=128,
12736
  ),
12737
+ "hydra_mlx_lm_trust_remote_code": _read_bool_setting(
12738
+ HYDRA_MLX_LM_TRUST_REMOTE_CODE_KEY,
12739
+ ("TATER_MLX_LM_TRUST_REMOTE_CODE",),
12740
+ DEFAULT_MLX_LM_TRUST_REMOTE_CODE,
12741
+ ),
12742
+ "hydra_mlx_lm_lazy_load": _read_bool_setting(
12743
+ HYDRA_MLX_LM_LAZY_LOAD_KEY,
12744
+ ("TATER_MLX_LM_LAZY",),
12745
+ DEFAULT_MLX_LM_LAZY_LOAD,
12746
+ ),
12747
+ "hydra_mlx_engine_prefill_step_size": _read_optional_int_setting(
12748
+ HYDRA_MLX_ENGINE_PREFILL_STEP_SIZE_KEY,
12749
+ ("TATER_MLX_ENGINE_PREFILL_STEP_SIZE",),
12750
+ minimum=1,
12751
+ maximum=32768,
12752
+ ),
12753
+ "hydra_mlx_engine_kv_bits": _read_text_choice_setting(
12754
+ HYDRA_MLX_ENGINE_KV_BITS_KEY,
12755
+ ("TATER_MLX_ENGINE_KV_BITS",),
12756
+ "",
12757
+ allowed=("", "2", "3", "4", "6", "8"),
12758
+ ),
12759
+ "hydra_mlx_engine_kv_group_size": _read_text_choice_setting(
12760
+ HYDRA_MLX_ENGINE_KV_GROUP_SIZE_KEY,
12761
+ ("TATER_MLX_ENGINE_KV_GROUP_SIZE",),
12762
+ "",
12763
+ allowed=("", "32", "64", "128"),
12764
+ ),
12765
+ "hydra_mlx_engine_quantized_kv_start": _read_optional_int_setting(
12766
+ HYDRA_MLX_ENGINE_QUANTIZED_KV_START_KEY,
12767
+ ("TATER_MLX_ENGINE_QUANTIZED_KV_START",),
12768
+ minimum=0,
12769
+ maximum=1_048_576,
12770
+ allow_zero=True,
12771
+ ),
12772
  "spudex_llm_provider": _normalize_hydra_llm_provider(spudex_settings.get("llm_provider") or ""),
12773
  "spudex_llm_host": str(spudex_settings.get("llm_host") or ""),
12774
  "spudex_llm_model": str(spudex_settings.get("llm_model") or ""),
 
13616
  return get_llama_cpp_runtime_diagnostics()
13617
 
13618
 
13619
+ @app.get("/api/settings/llama-cpp/chat-template")
13620
+ def get_llama_cpp_chat_template(model: str) -> Dict[str, Any]:
13621
+ return _llama_cpp_chat_template_model_info(model)
13622
+
13623
+
13624
+ @app.post("/api/settings/llama-cpp/chat-template")
13625
+ def save_llama_cpp_chat_template(request: LlamaCppChatTemplateRequest) -> Dict[str, Any]:
13626
+ model = str(request.model or "").strip()
13627
+ if not model:
13628
+ raise HTTPException(status_code=400, detail="llama.cpp model is required.")
13629
+ try:
13630
+ if request.reset:
13631
+ clear_llama_cpp_chat_template_override(model)
13632
+ else:
13633
+ set_llama_cpp_chat_template_override(model, request.template or "")
13634
+ except ValueError as exc:
13635
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
13636
+ unload_result = unload_local_llm_models(provider=HYDRA_LLM_PROVIDER_LLAMA_CPP, model=model)
13637
+ info = _llama_cpp_chat_template_model_info(model)
13638
+ info["ok"] = True
13639
+ info["unload"] = unload_result
13640
+ return info
13641
+
13642
+
13643
+ @app.get("/api/settings/local-llm/chat-template")
13644
+ def get_local_llm_chat_template(provider: str = "", model: str = "") -> Dict[str, Any]:
13645
+ return _local_llm_chat_template_model_info(provider, model)
13646
+
13647
+
13648
+ @app.post("/api/settings/local-llm/chat-template")
13649
+ def save_local_llm_chat_template(request: LlamaCppChatTemplateRequest) -> Dict[str, Any]:
13650
+ provider = _normalize_hydra_llm_provider(request.provider)
13651
+ if not _is_local_hydra_llm_provider(provider):
13652
+ raise HTTPException(status_code=400, detail="Local LLM provider is required.")
13653
+ model = str(request.model or "").strip()
13654
+ if not model:
13655
+ raise HTTPException(status_code=400, detail="Local model is required.")
13656
+ try:
13657
+ if request.reset:
13658
+ clear_local_llm_chat_template_override(provider, model)
13659
+ else:
13660
+ set_local_llm_chat_template_override(provider, model, request.template or "")
13661
+ except ValueError as exc:
13662
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
13663
+ unload_result = unload_local_llm_models(provider=provider, model=model)
13664
+ info = _local_llm_chat_template_model_info(provider, model)
13665
+ info["ok"] = True
13666
+ info["unload"] = unload_result
13667
+ return info
13668
+
13669
+
13670
  @app.get("/api/settings/huggingface/models")
13671
  def get_huggingface_models(
13672
  provider: str = "hf_transformers",
 
13681
  provider_token = HYDRA_LLM_PROVIDER_HF_TRANSFORMERS
13682
  view_token = str(view or "trending").strip().lower().replace("_", "-")
13683
  task_token = _normalize_hf_browser_task(task)
13684
+ clean_limit = max(4, min(48, int(limit or 24)))
13685
+ search = _hf_browser_provider_search(provider_token, query, task_token)
13686
+ integration_status = _hf_browser_integration_status()
13687
+ if view_token in {"picks", "tater", "tater-picks", "recommended"}:
13688
+ models = _hf_browser_tater_pick_models(query=search, task=task_token, limit=clean_limit, provider=provider_token)
13689
+ return {
13690
+ "provider": provider_token,
13691
+ "provider_label": _hydra_llm_provider_label(provider_token),
13692
+ "view": "picks",
13693
+ "query": search,
13694
+ "task": task_token,
13695
+ "library": "",
13696
+ "app_filter": "",
13697
+ "integration": integration_status,
13698
+ "limit": clean_limit,
13699
+ "has_next": False,
13700
+ "next_cursor": "",
13701
+ "models": models,
13702
+ }
13703
  if view_token in {"new", "recent", "latest"}:
13704
  sort = "lastModified"
13705
  response_view = "new"
 
13709
  else:
13710
  sort = "trendingScore"
13711
  response_view = "trending"
 
 
 
13712
  page_url = _hf_browser_cursor_decode(cursor) if str(cursor or "").strip() else _hf_browser_models_api_url(
13713
  provider=provider_token,
13714
  search=search,
 
13747
  "query": search,
13748
  "task": task_token,
13749
  "library": _hf_browser_provider_library(provider_token),
13750
+ "app_filter": _hf_browser_provider_app_filter(provider_token, task_token),
13751
  "integration": integration_status,
13752
  "limit": clean_limit,
13753
  "has_next": bool(next_cursor),
 
14003
  )
14004
  redis_client.set(redis_key, str(int(parsed)))
14005
 
14006
+ def _save_text_choice_setting(
14007
+ payload_key: str,
14008
+ redis_key: str,
14009
+ *,
14010
+ allowed: Tuple[str, ...],
14011
+ default: str = "",
14012
+ ) -> None:
14013
+ if payload_key not in updates:
14014
+ return
14015
+ token = str(updates.get(payload_key) or default or "").strip().lower()
14016
+ aliases = {
14017
+ "fp16": "float16",
14018
+ "half": "float16",
14019
+ "bf16": "bfloat16",
14020
+ "fp32": "float32",
14021
+ "none": "disabled",
14022
+ "off": "disabled",
14023
+ "false": "disabled",
14024
+ "0": "disabled",
14025
+ "default": "default",
14026
+ "auto": "auto",
14027
+ }
14028
+ token = aliases.get(token, token)
14029
+ allowed_set = {str(item).strip().lower() for item in allowed}
14030
+ if token not in allowed_set:
14031
+ raise HTTPException(status_code=400, detail=f"{payload_key} has an unsupported value.")
14032
+ redis_client.set(redis_key, token)
14033
+
14034
+ def _save_optional_int_setting(
14035
+ payload_key: str,
14036
+ redis_key: str,
14037
+ *,
14038
+ min_value: int = 0,
14039
+ max_value: int = 1_048_576,
14040
+ allow_zero: bool = False,
14041
+ ) -> None:
14042
+ if payload_key not in updates:
14043
+ return
14044
+ raw = str(updates.get(payload_key) or "").strip()
14045
+ if not raw:
14046
+ redis_client.delete(redis_key)
14047
+ return
14048
+ try:
14049
+ parsed = int(float(raw))
14050
+ except Exception as exc:
14051
+ raise HTTPException(status_code=400, detail=f"{payload_key} must be a whole number.") from exc
14052
+ if parsed < min_value or parsed > max_value or (parsed == 0 and not allow_zero):
14053
+ lower = min_value if allow_zero or min_value > 0 else 1
14054
+ raise HTTPException(
14055
+ status_code=400,
14056
+ detail=f"{payload_key} must be between {lower} and {max_value}.",
14057
+ )
14058
+ redis_client.set(redis_key, str(int(parsed)))
14059
+
14060
  _save_tater_api_settings_from_updates(updates)
14061
 
14062
  local_model_keys_cache: Optional[set[Tuple[str, str]]] = None
 
14121
  "hydra_llm_provider",
14122
  "hydra_base_servers",
14123
  "hydra_hf_transformers_context_tokens",
14124
+ "hydra_hf_transformers_device",
14125
+ "hydra_hf_transformers_dtype",
14126
+ "hydra_hf_transformers_device_map",
14127
+ "hydra_hf_transformers_attn_implementation",
14128
+ "hydra_hf_transformers_trust_remote_code",
14129
  "hydra_llama_cpp_context_tokens",
14130
  "hydra_llama_cpp_mtp_enabled",
14131
  "hydra_llama_cpp_mtp_draft_tokens",
14132
+ "hydra_llama_cpp_n_batch",
14133
+ "hydra_llama_cpp_n_ubatch",
14134
+ "hydra_llama_cpp_flash_attn",
14135
+ "hydra_llama_cpp_offload_kqv",
14136
  "hydra_mlx_lm_context_tokens",
14137
+ "hydra_mlx_lm_trust_remote_code",
14138
+ "hydra_mlx_lm_lazy_load",
14139
+ "hydra_mlx_engine_prefill_step_size",
14140
+ "hydra_mlx_engine_kv_bits",
14141
+ "hydra_mlx_engine_kv_group_size",
14142
+ "hydra_mlx_engine_quantized_kv_start",
14143
  }
14144
  spudex_model_keys = {"spudex_llm_provider", "spudex_llm_host", "spudex_llm_model"}
14145
  vision_model_keys = {
 
14230
  "hydra_hf_transformers_context_tokens",
14231
  HYDRA_HF_TRANSFORMERS_CONTEXT_TOKENS_KEY,
14232
  )
14233
+ _save_text_choice_setting(
14234
+ "hydra_hf_transformers_device",
14235
+ HYDRA_HF_TRANSFORMERS_DEVICE_KEY,
14236
+ allowed=("auto", "cuda", "mps", "cpu"),
14237
+ default=DEFAULT_HF_TRANSFORMERS_DEVICE,
14238
+ )
14239
+ _save_text_choice_setting(
14240
+ "hydra_hf_transformers_dtype",
14241
+ HYDRA_HF_TRANSFORMERS_DTYPE_KEY,
14242
+ allowed=("auto", "float16", "bfloat16", "float32"),
14243
+ default=DEFAULT_HF_TRANSFORMERS_DTYPE,
14244
+ )
14245
+ _save_text_choice_setting(
14246
+ "hydra_hf_transformers_device_map",
14247
+ HYDRA_HF_TRANSFORMERS_DEVICE_MAP_KEY,
14248
+ allowed=("default", "disabled", "auto", "balanced"),
14249
+ default=DEFAULT_HF_TRANSFORMERS_DEVICE_MAP,
14250
+ )
14251
+ _save_text_choice_setting(
14252
+ "hydra_hf_transformers_attn_implementation",
14253
+ HYDRA_HF_TRANSFORMERS_ATTN_IMPLEMENTATION_KEY,
14254
+ allowed=("", "auto", "sdpa", "flash_attention_2", "eager"),
14255
+ default=DEFAULT_HF_TRANSFORMERS_ATTN_IMPLEMENTATION,
14256
+ )
14257
+ _save_bool_setting(
14258
+ "hydra_hf_transformers_trust_remote_code",
14259
+ HYDRA_HF_TRANSFORMERS_TRUST_REMOTE_CODE_KEY,
14260
+ default=DEFAULT_HF_TRANSFORMERS_TRUST_REMOTE_CODE,
14261
+ )
14262
  _save_local_llm_context_setting(
14263
  "hydra_llama_cpp_context_tokens",
14264
  HYDRA_LLAMA_CPP_CONTEXT_TOKENS_KEY,
 
14279
  min_value=1,
14280
  max_value=16,
14281
  )
14282
+ _save_bounded_int_setting(
14283
+ "hydra_llama_cpp_n_batch",
14284
+ HYDRA_LLAMA_CPP_N_BATCH_KEY,
14285
+ default=DEFAULT_LLAMA_CPP_N_BATCH,
14286
+ min_value=32,
14287
+ max_value=8192,
14288
+ )
14289
+ _save_bounded_int_setting(
14290
+ "hydra_llama_cpp_n_ubatch",
14291
+ HYDRA_LLAMA_CPP_N_UBATCH_KEY,
14292
+ default=DEFAULT_LLAMA_CPP_N_UBATCH,
14293
+ min_value=0,
14294
+ max_value=8192,
14295
+ )
14296
+ _save_bool_setting(
14297
+ "hydra_llama_cpp_flash_attn",
14298
+ HYDRA_LLAMA_CPP_FLASH_ATTN_KEY,
14299
+ default=DEFAULT_LLAMA_CPP_FLASH_ATTN,
14300
+ )
14301
+ _save_bool_setting(
14302
+ "hydra_llama_cpp_offload_kqv",
14303
+ HYDRA_LLAMA_CPP_OFFLOAD_KQV_KEY,
14304
+ default=DEFAULT_LLAMA_CPP_OFFLOAD_KQV,
14305
+ )
14306
  _save_local_llm_context_setting(
14307
  "hydra_mlx_lm_context_tokens",
14308
  HYDRA_MLX_LM_CONTEXT_TOKENS_KEY,
14309
  min_value=128,
14310
  )
14311
+ _save_bool_setting(
14312
+ "hydra_mlx_lm_trust_remote_code",
14313
+ HYDRA_MLX_LM_TRUST_REMOTE_CODE_KEY,
14314
+ default=DEFAULT_MLX_LM_TRUST_REMOTE_CODE,
14315
+ )
14316
+ _save_bool_setting(
14317
+ "hydra_mlx_lm_lazy_load",
14318
+ HYDRA_MLX_LM_LAZY_LOAD_KEY,
14319
+ default=DEFAULT_MLX_LM_LAZY_LOAD,
14320
+ )
14321
+ _save_optional_int_setting(
14322
+ "hydra_mlx_engine_prefill_step_size",
14323
+ HYDRA_MLX_ENGINE_PREFILL_STEP_SIZE_KEY,
14324
+ min_value=1,
14325
+ max_value=32768,
14326
+ )
14327
+ _save_text_choice_setting(
14328
+ "hydra_mlx_engine_kv_bits",
14329
+ HYDRA_MLX_ENGINE_KV_BITS_KEY,
14330
+ allowed=("", "2", "3", "4", "6", "8"),
14331
+ default="",
14332
+ )
14333
+ _save_text_choice_setting(
14334
+ "hydra_mlx_engine_kv_group_size",
14335
+ HYDRA_MLX_ENGINE_KV_GROUP_SIZE_KEY,
14336
+ allowed=("", "32", "64", "128"),
14337
+ default="",
14338
+ )
14339
+ _save_optional_int_setting(
14340
+ "hydra_mlx_engine_quantized_kv_start",
14341
+ HYDRA_MLX_ENGINE_QUANTIZED_KV_START_KEY,
14342
+ min_value=0,
14343
+ max_value=1_048_576,
14344
+ allow_zero=True,
14345
+ )
14346
 
14347
  username = updates.get("username")
14348
  if isinstance(username, str):
 
14506
  updates.get("vision_provider", current_vision.get("provider") or HYDRA_LLM_PROVIDER_OPENAI_COMPATIBLE)
14507
  )
14508
  vision_model = str(updates.get("vision_model", current_vision.get("model") or "")).strip()
 
 
14509
  if vision_mode == "dedicated" and _is_local_hydra_llm_provider(vision_provider):
14510
  _require_downloaded_local_model(vision_provider, vision_model, "Vision")
14511
  save_shared_vision_settings(
tater_reachy_standalone/vendor/tater_app/tateros_static/app.js CHANGED
@@ -127,6 +127,12 @@ const state = {
127
  dashboardRefreshStatus: "",
128
  hfLlmWarmupPollTimer: 0,
129
  hfLlmWarmupLastSnapshot: null,
 
 
 
 
 
 
130
  runtimeSettingsSaveHandler: null,
131
  runtimeSettingsOpenHandler: null,
132
  runtimeSettingsCloseHandler: null,
@@ -178,6 +184,14 @@ const state = {
178
 
179
  safeStorageSet("tater_tateros_session_id", state.sessionId);
180
 
 
 
 
 
 
 
 
 
181
  const APP_BASE_PATH = (() => {
182
  const rawPath = String(window.location.pathname || "/").trim();
183
  const normalized = rawPath.replace(/\/+$/, "");
@@ -1016,6 +1030,35 @@ function finishActionProgress(tone = "success", statusText = "Completed.") {
1016
  setActionProgress(100, statusText, tone);
1017
  }
1018
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1019
  function closeActionProgressModal() {
1020
  const modal = document.getElementById("action-progress-modal");
1021
  if (!modal) {
@@ -1058,6 +1101,220 @@ async function runActionWithProgress(meta, actionFn) {
1058
  }
1059
  }
1060
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1061
  async function api(path, options = {}) {
1062
  const requestOptions = options && typeof options === "object" ? options : {};
1063
  const skipRedisRecovery = Boolean(requestOptions._skipRedisRecovery);
@@ -4248,6 +4505,14 @@ function _sanitizeMarkdownHref(rawHref) {
4248
  return "";
4249
  }
4250
 
 
 
 
 
 
 
 
 
4251
  function _renderMarkdownInline(rawText) {
4252
  const source = String(rawText ?? "");
4253
  if (!source) {
@@ -4447,17 +4712,19 @@ function renderChatMessage(message) {
4447
  `;
4448
  } else {
4449
  const contentType = String(content.type || "").toLowerCase();
4450
- const fileId = String(content.id || "").trim();
4451
  const fileName = String(content.name || "attachment").trim() || "attachment";
4452
  const mimetype = String(content.mimetype || "").trim() || "application/octet-stream";
4453
  const fileUrl = _chatFileUrl(fileId, mimetype);
 
 
4454
  const hasDataB64 = typeof content.data_b64 === "string" && content.data_b64.trim().length > 0;
4455
- const hasFileUrl = Boolean(fileUrl);
4456
  if (contentType === "image" && hasDataB64) {
4457
  const imageUrl = `data:${escapeHtml(content.mimetype || "image/png")};base64,${content.data_b64}`;
4458
  bodyHtml = `<img class="chat-media-image" src="${imageUrl}" alt="${escapeHtml(content.name || "image")}" />`;
4459
- } else if (contentType === "image" && hasFileUrl) {
4460
- bodyHtml = `<img class="chat-media-image" src="${fileUrl}" alt="${escapeHtml(fileName)}" />`;
4461
  } else if (contentType === "audio" && hasDataB64) {
4462
  const mimetype = String(content.mimetype || "audio/mpeg");
4463
  const audioUrl = `data:${escapeHtml(mimetype)};base64,${content.data_b64}`;
@@ -4468,12 +4735,12 @@ function renderChatMessage(message) {
4468
  <a class="inline-btn" href="${audioUrl}" download="${escapeHtml(fileName)}">Download Audio</a>
4469
  </div>
4470
  `;
4471
- } else if (contentType === "audio" && hasFileUrl) {
4472
  bodyHtml = `
4473
  <div class="chat-media-wrap">
4474
- <audio controls preload="metadata" src="${fileUrl}"></audio>
4475
  <div class="chat-file-meta">${escapeHtml(fileName)}</div>
4476
- <a class="inline-btn" href="${fileUrl}" download="${escapeHtml(fileName)}">Download Audio</a>
4477
  </div>
4478
  `;
4479
  } else if (contentType === "video" && hasDataB64) {
@@ -4486,12 +4753,12 @@ function renderChatMessage(message) {
4486
  <a class="inline-btn" href="${videoUrl}" download="${escapeHtml(fileName)}">Download Video</a>
4487
  </div>
4488
  `;
4489
- } else if (contentType === "video" && hasFileUrl) {
4490
  bodyHtml = `
4491
  <div class="chat-media-wrap">
4492
- <video controls preload="metadata" src="${fileUrl}" class="chat-media-video"></video>
4493
  <div class="chat-file-meta">${escapeHtml(fileName)}</div>
4494
- <a class="inline-btn" href="${fileUrl}" download="${escapeHtml(fileName)}">Download Video</a>
4495
  </div>
4496
  `;
4497
  } else if (contentType === "file" && hasDataB64) {
@@ -4504,12 +4771,12 @@ function renderChatMessage(message) {
4504
  )}">Download File</a>
4505
  </div>
4506
  `;
4507
- } else if (contentType === "file" && hasFileUrl) {
4508
  const sizeLabel = _formatBytes(content.size);
4509
  bodyHtml = `
4510
  <div class="chat-file-card">
4511
  <div class="chat-file-meta">${escapeHtml(fileName)}${sizeLabel ? ` (${escapeHtml(sizeLabel)})` : ""}</div>
4512
- <a class="inline-btn" href="${fileUrl}" download="${escapeHtml(fileName)}">Download File</a>
4513
  </div>
4514
  `;
4515
  } else {
@@ -8193,6 +8460,9 @@ function bindModelSettingsTabs(root = document) {
8193
  if (load && ["speakerid", "emotionid"].includes(normalized)) {
8194
  void ensureEspHomeRuntimeLoaded({ force: true, panel: normalized });
8195
  }
 
 
 
8196
  };
8197
  buttons.forEach((button) => {
8198
  if (button.dataset.modelsTabBound === "1") {
@@ -15885,6 +16155,16 @@ async function loadSettingsView() {
15885
  .map((option) => `<option value="${escapeHtml(option.value)}"${option.value === selected ? " selected" : ""}>${escapeHtml(option.label)}</option>`)
15886
  .join("");
15887
  };
 
 
 
 
 
 
 
 
 
 
15888
  const renderHydraLocalProviderOptions = (currentValue) => {
15889
  const selected = normalizeHydraBaseProvider(currentValue);
15890
  return hydraProviderOptions
@@ -16537,6 +16817,7 @@ async function loadSettingsView() {
16537
  <button type="button" class="settings-subtab-btn active" data-llm-vision-tab="settings">Settings</button>
16538
  <button type="button" class="settings-subtab-btn" data-llm-vision-tab="huggingface">Hugging Face</button>
16539
  <button type="button" class="settings-subtab-btn" data-llm-vision-tab="manage">Manage</button>
 
16540
  </div>
16541
  <div id="settings-hf-model-browser" class="hf-model-browser" hidden>
16542
  <div class="hf-model-browser-hero">
@@ -16560,7 +16841,8 @@ async function loadSettingsView() {
16560
  <option value="image-text-to-text">Vision</option>
16561
  </select>
16562
  <div class="hf-model-browser-view-switch" role="group" aria-label="Model list">
16563
- <button type="button" class="active" data-hf-model-view="trending">Trending</button>
 
16564
  <button type="button" data-hf-model-view="new">New</button>
16565
  <button type="button" data-hf-model-view="downloads">Most Downloaded</button>
16566
  </div>
@@ -16611,6 +16893,27 @@ async function loadSettingsView() {
16611
  <div id="local-model-manager-status" class="small hf-model-browser-status"></div>
16612
  <div id="local-model-manager-list" class="local-model-manager-list"></div>
16613
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16614
  <div id="settings-hydra-base-fields" class="hydra-model-panel is-active llm-vision-settings-block">
16615
  <div class="hydra-model-panel-title">Base Model</div>
16616
  <div class="small hydra-model-panel-note">Used for regular AI calls. Multiple base servers rotate in round-robin order.</div>
@@ -16636,6 +16939,10 @@ async function loadSettingsView() {
16636
  <select id="set_hydra_llm_model_select"></select>
16637
  <div id="hydra-local-model-status" class="small"></div>
16638
  </label>
 
 
 
 
16639
  <label class="hydra-context-field" data-hydra-provider-field="hf_transformers">Context Length
16640
  <div class="hydra-context-control" data-hydra-context-control="hf_transformers">
16641
  <input id="set_hydra_hf_transformers_context_tokens_range" type="range" min="256" max="262144" step="256" value="${escapeHtml(
@@ -16648,6 +16955,71 @@ async function loadSettingsView() {
16648
  <div id="hydra-hf-context-hint" class="small hydra-context-hint"></div>
16649
  <div id="hydra-hf-context-estimate" class="hydra-context-estimate" data-hydra-context-estimate="hf_transformers"></div>
16650
  </label>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16651
  <label class="hydra-context-field" data-hydra-provider-field="llama_cpp">Context Length
16652
  <div class="hydra-context-control" data-hydra-context-control="llama_cpp">
16653
  <input id="set_hydra_llama_cpp_context_tokens_range" type="range" min="256" max="262144" step="256" value="${escapeHtml(
@@ -16660,6 +17032,46 @@ async function loadSettingsView() {
16660
  <div id="hydra-llama-context-hint" class="small hydra-context-hint"></div>
16661
  <div id="hydra-llama-context-estimate" class="hydra-context-estimate" data-hydra-context-estimate="llama_cpp"></div>
16662
  </label>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16663
  <label class="hydra-context-field" data-hydra-provider-field="llama_cpp">Multi-Token Prediction
16664
  ${renderToggleRow(
16665
  `<input id="set_hydra_llama_cpp_mtp_enabled" class="toggle-input" type="checkbox" ${
@@ -16691,6 +17103,66 @@ async function loadSettingsView() {
16691
  <div id="hydra-mlx-context-hint" class="small hydra-context-hint"></div>
16692
  <div id="hydra-mlx-context-estimate" class="hydra-context-estimate" data-hydra-context-estimate="mlx_lm"></div>
16693
  </label>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16694
  <label id="hydra-base-api-key-wrap" data-hydra-provider-field="openai_compatible" style="grid-column: 1 / -1;">API Key (optional)
16695
  <input id="set_hydra_llm_api_key" type="password" autocomplete="new-password" value="${escapeHtml(
16696
  hydraPrimaryBaseRow.api_key || ""
@@ -17661,6 +18133,8 @@ async function loadSettingsView() {
17661
  if (["speakerid", "emotionid"].includes(activeModelsPanel)) {
17662
  void ensureEspHomeRuntimeLoaded({ force: true, panel: activeModelsPanel });
17663
  }
 
 
17664
  }
17665
  };
17666
 
@@ -17819,6 +18293,8 @@ async function loadSettingsView() {
17819
  const hydraBaseModelSelectEl = document.getElementById("set_hydra_llm_model_select");
17820
  const hydraBaseModelLabelEl = document.getElementById("hydra-base-model-label");
17821
  const hydraLocalModelStatusEl = document.getElementById("hydra-local-model-status");
 
 
17822
  const contextControlConfig = {
17823
  hf_transformers: {
17824
  min: 256,
@@ -18034,6 +18510,42 @@ async function loadSettingsView() {
18034
  llamaCppMtpDraftNumberEl?.addEventListener("input", () => syncLlamaCppMtpDraftControl(llamaCppMtpDraftNumberEl));
18035
  llamaCppMtpDraftNumberEl?.addEventListener("blur", () => syncLlamaCppMtpDraftControl(llamaCppMtpDraftNumberEl));
18036
  syncLlamaCppMtpDraftControl();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18037
  const normalizeHydraBaseRowInput = (row) => ({
18038
  provider: normalizeHydraBaseProvider(row?.provider || ""),
18039
  host: String(row?.host || "").trim(),
@@ -18133,6 +18645,22 @@ async function loadSettingsView() {
18133
  }
18134
  return String(hydraBaseModelEl?.value || "").trim();
18135
  };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18136
  const refreshLocalLlmModels = async ({ selectModel = "", provider = "" } = {}) => {
18137
  try {
18138
  localLlmModelsPayload = await api("/api/settings/local-llm/models", { _timeoutMs: HEALTH_REQUEST_TIMEOUT_MS });
@@ -18195,6 +18723,7 @@ async function loadSettingsView() {
18195
  syncHydraProviderScopedFields(hydraBaseFieldsEl, provider);
18196
  syncHydraModelInputForProvider(hydraBaseModelEl, hydraBaseModelLabelEl, provider);
18197
  syncHydraPrimaryModelControl(provider);
 
18198
  };
18199
  const hydraRouteControls = [];
18200
  const syncHydraRouteControl = (control, preferredModel = "") => {
@@ -18492,6 +19021,17 @@ async function loadSettingsView() {
18492
  syncActiveHydraContextControl();
18493
  renderHydraContextEstimateCards();
18494
  syncVisionLlamaContextControl();
 
 
 
 
 
 
 
 
 
 
 
18495
  });
18496
  syncHydraPrimaryProviderFields();
18497
  if (state.hydraContextEstimateRuntimeListener) {
@@ -18545,11 +19085,18 @@ async function loadSettingsView() {
18545
  const localModelManagerStatusEl = document.getElementById("local-model-manager-status");
18546
  const localModelManagerListEl = document.getElementById("local-model-manager-list");
18547
  const localModelManagerRefreshEl = document.getElementById("local-model-manager-refresh");
 
 
 
 
 
 
 
18548
  const hfModelBrowserViewButtons = Array.from(root.querySelectorAll("[data-hf-model-view]"));
18549
  const hfModelBrowserState = {
18550
  provider: isHydraLocalProvider(hydraPrimaryBaseRow.provider) ? hydraPrimaryBaseRow.provider : "hf_transformers",
18551
  task: "text-generation",
18552
- view: "trending",
18553
  query: "",
18554
  loaded: false,
18555
  loadingSeq: 0,
@@ -18575,6 +19122,9 @@ async function loadSettingsView() {
18575
  const hfModelTaskLabel = (task) => normalizeHfModelTask(task) === "image-text-to-text" ? "vision" : "text";
18576
  const normalizeHfModelView = (value) => {
18577
  const token = String(value || "trending").trim().toLowerCase().replace(/_/g, "-");
 
 
 
18578
  if (token === "new" || token === "recent" || token === "latest") {
18579
  return "new";
18580
  }
@@ -18585,6 +19135,9 @@ async function loadSettingsView() {
18585
  };
18586
  const hfModelViewLabel = (value) => {
18587
  const view = normalizeHfModelView(value);
 
 
 
18588
  if (view === "new") {
18589
  return "new";
18590
  }
@@ -18681,6 +19234,7 @@ async function loadSettingsView() {
18681
  </div>
18682
  ${pathLabel ? `<div class="local-model-path">${escapeHtml(pathLabel)}</div>` : ""}
18683
  <div class="local-model-actions">
 
18684
  <button type="button" class="inline-btn danger" data-local-model-delete data-provider="${escapeHtml(row.provider)}" data-model="${escapeHtml(row.model)}">Delete</button>
18685
  </div>
18686
  </article>
@@ -18801,20 +19355,24 @@ async function loadSettingsView() {
18801
  const library = String(model?.library_name || model?.pipeline_tag || "").trim();
18802
  const compatible = Boolean(model?.compatible);
18803
  const supportsVision = Boolean(model?.supports_vision) || normalizeHfModelTask(model?.task) === "image-text-to-text" || normalizeHfModelTask(hfModelBrowserState.task) === "image-text-to-text";
 
 
 
18804
  const isMlxProvider = provider === "mlx_lm";
18805
  const downloadLabel = isMlxProvider ? "Download Repo" : "Download";
18806
  return `
18807
- <article class="hf-model-card ${compatible ? "" : "is-uncertain"}" role="button" tabindex="0" aria-label="Show files for ${escapeHtml(id)}" data-hf-model-card data-provider="${escapeHtml(provider)}" data-repo-id="${escapeHtml(id)}">
18808
  <div class="hf-model-card-head">
18809
  <div>
18810
  <strong>${escapeHtml(id)}</strong>
18811
  <span>${escapeHtml(library || hfModelProviderLabel(provider))}</span>
18812
  </div>
18813
- <span class="hf-model-provider-pill">${escapeHtml(hfModelProviderLabel(provider))}</span>
18814
  </div>
18815
  <div class="hf-model-card-meta">
18816
  ${modelSize ? `<span class="hf-model-size-pill">${escapeHtml(modelSize)}</span>` : ""}
18817
  ${supportsVision ? `<span class="hf-model-vision-pill" title="Supports vision models">Vision</span>` : ""}
 
18818
  <span>${escapeHtml(downloads)} downloads</span>
18819
  <span>${escapeHtml(likes)} likes</span>
18820
  ${updated ? `<span>${escapeHtml(updated)}</span>` : ""}
@@ -18922,7 +19480,13 @@ async function loadSettingsView() {
18922
  hfModelBrowserState.provider = provider;
18923
  hfModelBrowserState.task = task;
18924
  hfModelBrowserState.query = query;
18925
- setHfModelBrowserStatus(`Loading ${hfModelViewLabel(hfModelBrowserState.view)} ${hfModelProviderLabel(provider)} ${hfModelTaskLabel(task)} models page ${hfModelBrowserState.pageIndex + 1}...`);
 
 
 
 
 
 
18926
  if (hfModelBrowserResultsEl) {
18927
  hfModelBrowserResultsEl.innerHTML = `<div class="hf-model-browser-empty">Loading models...</div>`;
18928
  }
@@ -18949,7 +19513,12 @@ async function loadSettingsView() {
18949
  }
18950
  renderHfModelCards(payload?.models || []);
18951
  renderHfModelBrowserPagination();
18952
- setHfModelBrowserStatus(`Page ${hfModelBrowserState.pageIndex + 1}: ${(payload?.models || []).length} models loaded.`, "success");
 
 
 
 
 
18953
  } catch (error) {
18954
  if (seq !== hfModelBrowserState.loadingSeq) {
18955
  return;
@@ -19096,15 +19665,210 @@ async function loadSettingsView() {
19096
  showToast(`Model delete failed: ${error.message}`, "error", 3600);
19097
  }
19098
  };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19099
  const activateLlmVisionTab = (tab) => {
19100
  const normalized = String(tab || "settings").trim() || "settings";
19101
  const browserActive = normalized === "huggingface";
19102
  const manageActive = normalized === "manage";
 
19103
  llmVisionTabButtons.forEach((button) => {
19104
  button.classList.toggle("active", String(button.dataset.llmVisionTab || "") === normalized);
19105
  });
19106
  root.querySelectorAll(".llm-vision-settings-block").forEach((block) => {
19107
- block.classList.toggle("llm-vision-hidden", browserActive || manageActive);
19108
  });
19109
  if (hfModelBrowserEl) {
19110
  hfModelBrowserEl.hidden = !browserActive;
@@ -19114,6 +19878,13 @@ async function loadSettingsView() {
19114
  localModelManagerEl.hidden = !manageActive;
19115
  localModelManagerEl.classList.toggle("active", manageActive);
19116
  }
 
 
 
 
 
 
 
19117
  if (browserActive) {
19118
  renderHfDownloadSummary(state.hfLlmWarmupLastSnapshot || {});
19119
  scheduleHfLlmWarmupPoll(150);
@@ -19122,6 +19893,12 @@ async function loadSettingsView() {
19122
  renderLocalModelManager();
19123
  void refreshLocalLlmModels();
19124
  }
 
 
 
 
 
 
19125
  if (browserActive && !hfModelBrowserState.loaded) {
19126
  if (hfModelBrowserProviderEl) {
19127
  hfModelBrowserProviderEl.value = isHydraLocalProvider(hydraBaseProviderEl?.value || "")
@@ -19134,6 +19911,23 @@ async function loadSettingsView() {
19134
  llmVisionTabButtons.forEach((button) => {
19135
  button.addEventListener("click", () => activateLlmVisionTab(button.dataset.llmVisionTab || "settings"));
19136
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19137
  hfModelBrowserViewButtons.forEach((button) => {
19138
  button.addEventListener("click", () => {
19139
  hfModelBrowserState.view = normalizeHfModelView(button.dataset.hfModelView || "trending");
@@ -19270,6 +20064,20 @@ async function loadSettingsView() {
19270
  void refreshLocalLlmModels();
19271
  });
19272
  localModelManagerListEl?.addEventListener("click", (event) => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19273
  const button = event.target instanceof Element ? event.target.closest("[data-local-model-delete]") : null;
19274
  if (!button) {
19275
  return;
@@ -20682,10 +21490,25 @@ async function loadSettingsView() {
20682
  const baseModel = getHydraBaseModelValue();
20683
  const baseApiKey = String(document.getElementById("set_hydra_llm_api_key")?.value || "").trim();
20684
  const hfTransformersContextTokens = String(document.getElementById("set_hydra_hf_transformers_context_tokens")?.value || "").trim();
 
 
 
 
 
20685
  const llamaCppContextTokens = String(document.getElementById("set_hydra_llama_cpp_context_tokens")?.value || "").trim();
20686
  const llamaCppMtpEnabled = Boolean(document.getElementById("set_hydra_llama_cpp_mtp_enabled")?.checked);
20687
  const llamaCppMtpDraftTokens = String(document.getElementById("set_hydra_llama_cpp_mtp_draft_tokens")?.value || "3").trim();
 
 
 
 
20688
  const mlxLmContextTokens = String(document.getElementById("set_hydra_mlx_lm_context_tokens")?.value || "").trim();
 
 
 
 
 
 
20689
  const additionalBaseRows = readHydraAdditionalBaseRows();
20690
  const hydraBaseServersPayload = [
20691
  normalizeHydraBaseRowInput({ provider: baseProvider, host: baseHost, port: basePort, model: baseModel, api_key: baseApiKey }),
@@ -20703,10 +21526,25 @@ async function loadSettingsView() {
20703
  hydra_llm_model: baseModel,
20704
  hydra_llm_api_key: baseApiKey,
20705
  hydra_hf_transformers_context_tokens: hfTransformersContextTokens,
 
 
 
 
 
20706
  hydra_llama_cpp_context_tokens: llamaCppContextTokens,
20707
  hydra_llama_cpp_mtp_enabled: llamaCppMtpEnabled,
20708
  hydra_llama_cpp_mtp_draft_tokens: llamaCppMtpDraftTokens,
 
 
 
 
20709
  hydra_mlx_lm_context_tokens: mlxLmContextTokens,
 
 
 
 
 
 
20710
  hydra_base_servers: hydraBaseServersPayload,
20711
  },
20712
  };
@@ -23371,6 +24209,9 @@ async function loadView(viewName) {
23371
  if (state.view !== "spudex") {
23372
  clearSpudexPollTimer();
23373
  }
 
 
 
23374
  document.body.dataset.view = String(viewName || "").trim().toLowerCase();
23375
  setActiveNav(viewName);
23376
  updateHeader();
@@ -23459,6 +24300,7 @@ window.addEventListener("beforeunload", () => {
23459
  }
23460
  clearDashboardRefreshTimer();
23461
  clearSpudexPollTimer();
 
23462
  closeChatEventSource();
23463
  stopAllChatJobPolling();
23464
  stopRuntimeBreakdownPolling();
 
127
  dashboardRefreshStatus: "",
128
  hfLlmWarmupPollTimer: 0,
129
  hfLlmWarmupLastSnapshot: null,
130
+ llmDebugPollTimer: 0,
131
+ llmDebugInFlight: false,
132
+ llmDebugNextId: 0,
133
+ llmDebugEvents: [],
134
+ llmDebugLastSnapshot: null,
135
+ llmDebugAutoScroll: String(safeStorageGet("tater_llm_debug_auto_scroll", "true")).trim().toLowerCase() !== "false",
136
  runtimeSettingsSaveHandler: null,
137
  runtimeSettingsOpenHandler: null,
138
  runtimeSettingsCloseHandler: null,
 
184
 
185
  safeStorageSet("tater_tateros_session_id", state.sessionId);
186
 
187
+ function clearLlmDebugPollTimer() {
188
+ if (state.llmDebugPollTimer) {
189
+ window.clearTimeout(state.llmDebugPollTimer);
190
+ state.llmDebugPollTimer = 0;
191
+ }
192
+ state.llmDebugInFlight = false;
193
+ }
194
+
195
  const APP_BASE_PATH = (() => {
196
  const rawPath = String(window.location.pathname || "/").trim();
197
  const normalized = rawPath.replace(/\/+$/, "");
 
1030
  setActionProgress(100, statusText, tone);
1031
  }
1032
 
1033
+ function normalizeLocalLlmProviderToken(value) {
1034
+ const token = String(value || "").trim().toLowerCase().replace(/[\s.-]+/g, "_");
1035
+ if (["llama", "llamacpp", "llama_cpp", "gguf", "llama_cpp_python"].includes(token)) {
1036
+ return "llama_cpp";
1037
+ }
1038
+ if (["mlx", "mlx_lm", "apple_mlx", "apple_silicon", "mlxlm"].includes(token)) {
1039
+ return "mlx_lm";
1040
+ }
1041
+ if (["hf", "huggingface", "hugging_face", "transformers", "hf_transformers", "local_transformers"].includes(token)) {
1042
+ return "hf_transformers";
1043
+ }
1044
+ return token || "hf_transformers";
1045
+ }
1046
+
1047
+ function isLocalLlmProviderToken(value) {
1048
+ return ["hf_transformers", "llama_cpp", "mlx_lm"].includes(normalizeLocalLlmProviderToken(value));
1049
+ }
1050
+
1051
+ function localLlmProviderLabel(value) {
1052
+ const provider = normalizeLocalLlmProviderToken(value);
1053
+ if (provider === "llama_cpp") {
1054
+ return "llama.cpp";
1055
+ }
1056
+ if (provider === "mlx_lm") {
1057
+ return "MLX";
1058
+ }
1059
+ return "Transformers";
1060
+ }
1061
+
1062
  function closeActionProgressModal() {
1063
  const modal = document.getElementById("action-progress-modal");
1064
  if (!modal) {
 
1101
  }
1102
  }
1103
 
1104
+ function localLlmChatTemplateCapabilityBadges(capabilities = {}) {
1105
+ const items = [
1106
+ ["enable_thinking", "enable_thinking"],
1107
+ ["reasoning_budget", "reasoning budget"],
1108
+ ["no_think_marker", "/no_think"],
1109
+ ["think_tags", "think tags"],
1110
+ ];
1111
+ return items
1112
+ .map(([key, label]) => {
1113
+ const active = Boolean(capabilities?.[key]);
1114
+ return `<span class="llama-template-badge${active ? " active" : ""}">${escapeHtml(label)} ${active ? "found" : "missing"}</span>`;
1115
+ })
1116
+ .join("");
1117
+ }
1118
+
1119
+ function ensureLocalLlmChatTemplateModal() {
1120
+ let modal = document.getElementById("llama-chat-template-modal");
1121
+ if (modal) {
1122
+ return modal;
1123
+ }
1124
+ document.body.insertAdjacentHTML(
1125
+ "beforeend",
1126
+ `
1127
+ <div id="llama-chat-template-modal" class="cerb-modal" aria-hidden="true">
1128
+ <div class="cerb-modal-dialog card llama-chat-template-dialog" role="dialog" aria-modal="true" aria-label="Local Model Chat Template">
1129
+ <div class="card-head runtime-breakdown-modal-head">
1130
+ <span class="runtime-breakdown-modal-badge" aria-hidden="true">CT</span>
1131
+ <div>
1132
+ <h3 id="llama-chat-template-title" class="card-title">Chat Template</h3>
1133
+ <div id="llama-chat-template-model" class="small"></div>
1134
+ </div>
1135
+ <button type="button" class="inline-btn" id="llama-chat-template-close">Close</button>
1136
+ </div>
1137
+ <div class="cerb-modal-body llama-chat-template-body">
1138
+ <div id="llama-chat-template-summary" class="llama-chat-template-summary"></div>
1139
+ <textarea id="llama-chat-template-editor" class="llama-chat-template-editor" spellcheck="false"></textarea>
1140
+ <div class="llama-chat-template-actions">
1141
+ <button type="button" class="inline-btn" id="llama-chat-template-use-embedded">Use Embedded</button>
1142
+ <button type="button" class="inline-btn" id="llama-chat-template-reload">Reload</button>
1143
+ <button type="button" class="inline-btn" id="llama-chat-template-reset">Reset Override</button>
1144
+ <button type="button" class="inline-btn primary" id="llama-chat-template-save">Save Override</button>
1145
+ </div>
1146
+ <div id="llama-chat-template-status" class="small llama-chat-template-status"></div>
1147
+ </div>
1148
+ </div>
1149
+ </div>
1150
+ `
1151
+ );
1152
+ modal = document.getElementById("llama-chat-template-modal");
1153
+ const closeModal = () => closePopupModal(modal);
1154
+ document.getElementById("llama-chat-template-close")?.addEventListener("click", closeModal);
1155
+ modal?.addEventListener("click", (event) => {
1156
+ if (event.target === modal) {
1157
+ closeModal();
1158
+ }
1159
+ });
1160
+ document.addEventListener("keydown", (event) => {
1161
+ if (event.key === "Escape" && modal?.classList.contains("active")) {
1162
+ closeModal();
1163
+ }
1164
+ });
1165
+ return modal;
1166
+ }
1167
+
1168
+ async function openLocalLlmChatTemplateModal(provider, model, options = {}) {
1169
+ const providerToken = normalizeLocalLlmProviderToken(provider);
1170
+ const modelId = String(model || "").trim();
1171
+ if (!isLocalLlmProviderToken(providerToken) || !modelId) {
1172
+ showToast("Select a local model first.", "error", 2600);
1173
+ return null;
1174
+ }
1175
+ const providerLabel = localLlmProviderLabel(providerToken);
1176
+ const modal = ensureLocalLlmChatTemplateModal();
1177
+ const titleEl = document.getElementById("llama-chat-template-title");
1178
+ const modelEl = document.getElementById("llama-chat-template-model");
1179
+ const summaryEl = document.getElementById("llama-chat-template-summary");
1180
+ const editorEl = document.getElementById("llama-chat-template-editor");
1181
+ const statusEl = document.getElementById("llama-chat-template-status");
1182
+ const saveBtn = document.getElementById("llama-chat-template-save");
1183
+ const resetBtn = document.getElementById("llama-chat-template-reset");
1184
+ const reloadBtn = document.getElementById("llama-chat-template-reload");
1185
+ const embeddedBtn = document.getElementById("llama-chat-template-use-embedded");
1186
+ let latestPayload = null;
1187
+
1188
+ const setStatus = (text, tone = "") => {
1189
+ if (!statusEl) {
1190
+ return;
1191
+ }
1192
+ statusEl.textContent = String(text || "").trim();
1193
+ statusEl.classList.toggle("success", tone === "success");
1194
+ statusEl.classList.toggle("error", tone === "error");
1195
+ };
1196
+ const setBusy = (busy) => {
1197
+ [
1198
+ document.getElementById("llama-chat-template-save"),
1199
+ document.getElementById("llama-chat-template-reset"),
1200
+ document.getElementById("llama-chat-template-reload"),
1201
+ document.getElementById("llama-chat-template-use-embedded"),
1202
+ ].forEach((button) => {
1203
+ if (button) {
1204
+ button.disabled = Boolean(busy);
1205
+ }
1206
+ });
1207
+ };
1208
+ const renderPayload = (payload) => {
1209
+ latestPayload = payload && typeof payload === "object" ? payload : {};
1210
+ const effective = String(latestPayload.effective_template || latestPayload.override_template || latestPayload.embedded_template || "");
1211
+ if (titleEl) {
1212
+ titleEl.textContent = latestPayload.override_active ? "Chat Template Override" : "Chat Template";
1213
+ }
1214
+ if (modelEl) {
1215
+ modelEl.textContent = `${providerLabel} • ${modelId}`;
1216
+ }
1217
+ if (editorEl) {
1218
+ editorEl.value = effective;
1219
+ }
1220
+ const templateNames = Array.isArray(latestPayload.template_names) ? latestPayload.template_names : [];
1221
+ const source = String(latestPayload.source || "none").trim();
1222
+ const sourceLabel = source === "override" ? "Override active" : source === "gguf" || source === "embedded" ? "Using embedded model template" : "No embedded template found";
1223
+ const embeddedChars = Number(latestPayload.embedded_template_chars || 0);
1224
+ const overrideChars = Number(latestPayload.override_template_chars || 0);
1225
+ const maxChars = Number(latestPayload.max_chars || 0);
1226
+ if (summaryEl) {
1227
+ summaryEl.innerHTML = `
1228
+ <div class="llama-chat-template-summary-row">
1229
+ <strong>${escapeHtml(sourceLabel)}</strong>
1230
+ <span>${escapeHtml(templateNames.length ? templateNames.join(", ") : "default template")}</span>
1231
+ </div>
1232
+ <div class="llama-chat-template-badges">
1233
+ ${localLlmChatTemplateCapabilityBadges(latestPayload.capabilities || {})}
1234
+ </div>
1235
+ <div class="llama-chat-template-meta">
1236
+ <span>Embedded ${embeddedChars.toLocaleString()} chars</span>
1237
+ <span>Override ${overrideChars.toLocaleString()} chars</span>
1238
+ ${maxChars > 0 ? `<span>Limit ${maxChars.toLocaleString()} chars</span>` : ""}
1239
+ </div>
1240
+ `;
1241
+ }
1242
+ const embeddedButton = document.getElementById("llama-chat-template-use-embedded");
1243
+ if (embeddedButton) {
1244
+ embeddedButton.disabled = embeddedChars <= 0;
1245
+ }
1246
+ setStatus(source === "none" || source === "fallback" ? "No embedded template was found for this model." : "Template loaded.");
1247
+ };
1248
+ const load = async () => {
1249
+ setBusy(true);
1250
+ setStatus("Loading chat template...");
1251
+ try {
1252
+ const payload = await api(`/api/settings/local-llm/chat-template?provider=${encodeURIComponent(providerToken)}&model=${encodeURIComponent(modelId)}`, {
1253
+ _timeoutMs: HEALTH_REQUEST_TIMEOUT_MS,
1254
+ });
1255
+ renderPayload(payload);
1256
+ } catch (error) {
1257
+ setStatus(`Load failed: ${error.message}`, "error");
1258
+ if (summaryEl) {
1259
+ summaryEl.innerHTML = `<div class="hf-model-browser-empty">Template metadata could not be loaded.</div>`;
1260
+ }
1261
+ } finally {
1262
+ setBusy(false);
1263
+ }
1264
+ };
1265
+ const save = async ({ reset = false } = {}) => {
1266
+ setBusy(true);
1267
+ setStatus(reset ? "Resetting override..." : "Saving override...");
1268
+ try {
1269
+ const payload = await api("/api/settings/local-llm/chat-template", {
1270
+ method: "POST",
1271
+ body: JSON.stringify({
1272
+ provider: providerToken,
1273
+ model: modelId,
1274
+ template: reset ? "" : String(editorEl?.value || ""),
1275
+ reset,
1276
+ }),
1277
+ _timeoutMs: HEALTH_REQUEST_TIMEOUT_MS,
1278
+ });
1279
+ renderPayload(payload);
1280
+ setStatus(reset ? "Override reset. The next model call will use the embedded template." : "Override saved. The matching loaded model was unloaded for a clean reload.", "success");
1281
+ if (typeof options.onSaved === "function") {
1282
+ options.onSaved(payload);
1283
+ }
1284
+ } catch (error) {
1285
+ setStatus(`${reset ? "Reset" : "Save"} failed: ${error.message}`, "error");
1286
+ } finally {
1287
+ setBusy(false);
1288
+ }
1289
+ };
1290
+
1291
+ saveBtn?.replaceWith(saveBtn.cloneNode(true));
1292
+ resetBtn?.replaceWith(resetBtn.cloneNode(true));
1293
+ reloadBtn?.replaceWith(reloadBtn.cloneNode(true));
1294
+ embeddedBtn?.replaceWith(embeddedBtn.cloneNode(true));
1295
+ document.getElementById("llama-chat-template-save")?.addEventListener("click", () => {
1296
+ void save();
1297
+ });
1298
+ document.getElementById("llama-chat-template-reset")?.addEventListener("click", () => {
1299
+ if (window.confirm(`Reset chat template override for ${modelId}?`)) {
1300
+ void save({ reset: true });
1301
+ }
1302
+ });
1303
+ document.getElementById("llama-chat-template-reload")?.addEventListener("click", () => {
1304
+ void load();
1305
+ });
1306
+ document.getElementById("llama-chat-template-use-embedded")?.addEventListener("click", () => {
1307
+ if (editorEl && latestPayload) {
1308
+ editorEl.value = String(latestPayload.embedded_template || "");
1309
+ setStatus("Embedded template copied into the editor.");
1310
+ }
1311
+ });
1312
+
1313
+ openPopupModal(modal);
1314
+ await load();
1315
+ return modal;
1316
+ }
1317
+
1318
  async function api(path, options = {}) {
1319
  const requestOptions = options && typeof options === "object" ? options : {};
1320
  const skipRedisRecovery = Boolean(requestOptions._skipRedisRecovery);
 
4505
  return "";
4506
  }
4507
 
4508
+ function _chatDirectMediaUrl(rawUrl) {
4509
+ const safeHref = _sanitizeMarkdownHref(rawUrl);
4510
+ if (!safeHref) {
4511
+ return "";
4512
+ }
4513
+ return safeHref.startsWith("/") ? withBasePath(safeHref) : safeHref;
4514
+ }
4515
+
4516
  function _renderMarkdownInline(rawText) {
4517
  const source = String(rawText ?? "");
4518
  if (!source) {
 
4712
  `;
4713
  } else {
4714
  const contentType = String(content.type || "").toLowerCase();
4715
+ const fileId = String(content.id || content.file_id || "").trim();
4716
  const fileName = String(content.name || "attachment").trim() || "attachment";
4717
  const mimetype = String(content.mimetype || "").trim() || "application/octet-stream";
4718
  const fileUrl = _chatFileUrl(fileId, mimetype);
4719
+ const directMediaUrl = _chatDirectMediaUrl(content.url || content.src || content.href);
4720
+ const mediaUrl = directMediaUrl || fileUrl;
4721
  const hasDataB64 = typeof content.data_b64 === "string" && content.data_b64.trim().length > 0;
4722
+ const hasMediaUrl = Boolean(mediaUrl);
4723
  if (contentType === "image" && hasDataB64) {
4724
  const imageUrl = `data:${escapeHtml(content.mimetype || "image/png")};base64,${content.data_b64}`;
4725
  bodyHtml = `<img class="chat-media-image" src="${imageUrl}" alt="${escapeHtml(content.name || "image")}" />`;
4726
+ } else if (contentType === "image" && hasMediaUrl) {
4727
+ bodyHtml = `<img class="chat-media-image" src="${escapeHtml(mediaUrl)}" alt="${escapeHtml(fileName)}" />`;
4728
  } else if (contentType === "audio" && hasDataB64) {
4729
  const mimetype = String(content.mimetype || "audio/mpeg");
4730
  const audioUrl = `data:${escapeHtml(mimetype)};base64,${content.data_b64}`;
 
4735
  <a class="inline-btn" href="${audioUrl}" download="${escapeHtml(fileName)}">Download Audio</a>
4736
  </div>
4737
  `;
4738
+ } else if (contentType === "audio" && hasMediaUrl) {
4739
  bodyHtml = `
4740
  <div class="chat-media-wrap">
4741
+ <audio controls preload="metadata" src="${escapeHtml(mediaUrl)}"></audio>
4742
  <div class="chat-file-meta">${escapeHtml(fileName)}</div>
4743
+ <a class="inline-btn" href="${escapeHtml(mediaUrl)}" download="${escapeHtml(fileName)}">Download Audio</a>
4744
  </div>
4745
  `;
4746
  } else if (contentType === "video" && hasDataB64) {
 
4753
  <a class="inline-btn" href="${videoUrl}" download="${escapeHtml(fileName)}">Download Video</a>
4754
  </div>
4755
  `;
4756
+ } else if (contentType === "video" && hasMediaUrl) {
4757
  bodyHtml = `
4758
  <div class="chat-media-wrap">
4759
+ <video controls preload="metadata" src="${escapeHtml(mediaUrl)}" class="chat-media-video"></video>
4760
  <div class="chat-file-meta">${escapeHtml(fileName)}</div>
4761
+ <a class="inline-btn" href="${escapeHtml(mediaUrl)}" download="${escapeHtml(fileName)}">Download Video</a>
4762
  </div>
4763
  `;
4764
  } else if (contentType === "file" && hasDataB64) {
 
4771
  )}">Download File</a>
4772
  </div>
4773
  `;
4774
+ } else if (contentType === "file" && hasMediaUrl) {
4775
  const sizeLabel = _formatBytes(content.size);
4776
  bodyHtml = `
4777
  <div class="chat-file-card">
4778
  <div class="chat-file-meta">${escapeHtml(fileName)}${sizeLabel ? ` (${escapeHtml(sizeLabel)})` : ""}</div>
4779
+ <a class="inline-btn" href="${escapeHtml(mediaUrl)}" download="${escapeHtml(fileName)}">Download File</a>
4780
  </div>
4781
  `;
4782
  } else {
 
8460
  if (load && ["speakerid", "emotionid"].includes(normalized)) {
8461
  void ensureEspHomeRuntimeLoaded({ force: true, panel: normalized });
8462
  }
8463
+ if (normalized !== "routing") {
8464
+ clearLlmDebugPollTimer();
8465
+ }
8466
  };
8467
  buttons.forEach((button) => {
8468
  if (button.dataset.modelsTabBound === "1") {
 
16155
  .map((option) => `<option value="${escapeHtml(option.value)}"${option.value === selected ? " selected" : ""}>${escapeHtml(option.label)}</option>`)
16156
  .join("");
16157
  };
16158
+ const renderChoiceOptions = (options, currentValue) => {
16159
+ const selected = String(currentValue ?? "").trim().toLowerCase();
16160
+ return (Array.isArray(options) ? options : [])
16161
+ .map((option) => {
16162
+ const value = String(option?.value ?? "").trim();
16163
+ const label = String(option?.label ?? value).trim();
16164
+ return `<option value="${escapeHtml(value)}"${value.toLowerCase() === selected ? " selected" : ""}>${escapeHtml(label)}</option>`;
16165
+ })
16166
+ .join("");
16167
+ };
16168
  const renderHydraLocalProviderOptions = (currentValue) => {
16169
  const selected = normalizeHydraBaseProvider(currentValue);
16170
  return hydraProviderOptions
 
16817
  <button type="button" class="settings-subtab-btn active" data-llm-vision-tab="settings">Settings</button>
16818
  <button type="button" class="settings-subtab-btn" data-llm-vision-tab="huggingface">Hugging Face</button>
16819
  <button type="button" class="settings-subtab-btn" data-llm-vision-tab="manage">Manage</button>
16820
+ <button type="button" class="settings-subtab-btn" data-llm-vision-tab="debug">Debug</button>
16821
  </div>
16822
  <div id="settings-hf-model-browser" class="hf-model-browser" hidden>
16823
  <div class="hf-model-browser-hero">
 
16841
  <option value="image-text-to-text">Vision</option>
16842
  </select>
16843
  <div class="hf-model-browser-view-switch" role="group" aria-label="Model list">
16844
+ <button type="button" class="active" data-hf-model-view="picks">Tater Picks</button>
16845
+ <button type="button" data-hf-model-view="trending">Trending</button>
16846
  <button type="button" data-hf-model-view="new">New</button>
16847
  <button type="button" data-hf-model-view="downloads">Most Downloaded</button>
16848
  </div>
 
16893
  <div id="local-model-manager-status" class="small hf-model-browser-status"></div>
16894
  <div id="local-model-manager-list" class="local-model-manager-list"></div>
16895
  </div>
16896
+ <div id="settings-llm-debug-console" class="hf-model-browser llm-debug-console-shell" hidden>
16897
+ <div class="hf-model-browser-hero llm-debug-hero">
16898
+ <div class="hf-model-browser-spud" aria-hidden="true">
16899
+ <span class="hf-model-browser-spud-eye left"></span>
16900
+ <span class="hf-model-browser-spud-eye right"></span>
16901
+ <span class="hf-model-browser-spud-spark"></span>
16902
+ </div>
16903
+ <div class="hf-model-browser-title">
16904
+ <strong>Live LLM Console</strong>
16905
+ <span>Runtime events, active calls, prompt timing, output previews, and backend errors.</span>
16906
+ </div>
16907
+ <div class="llm-debug-actions">
16908
+ <button type="button" id="llm-debug-refresh" class="inline-btn">Refresh</button>
16909
+ <button type="button" id="llm-debug-autoscroll" class="inline-btn">Auto-scroll On</button>
16910
+ <button type="button" id="llm-debug-clear" class="inline-btn danger">Clear View</button>
16911
+ </div>
16912
+ </div>
16913
+ <div id="llm-debug-status" class="small hf-model-browser-status">Open Debug to start the live console.</div>
16914
+ <div id="llm-debug-active" class="llm-debug-active"></div>
16915
+ <div id="llm-debug-events" class="llm-debug-events" role="log" aria-live="polite"></div>
16916
+ </div>
16917
  <div id="settings-hydra-base-fields" class="hydra-model-panel is-active llm-vision-settings-block">
16918
  <div class="hydra-model-panel-title">Base Model</div>
16919
  <div class="small hydra-model-panel-note">Used for regular AI calls. Multiple base servers rotate in round-robin order.</div>
 
16939
  <select id="set_hydra_llm_model_select"></select>
16940
  <div id="hydra-local-model-status" class="small"></div>
16941
  </label>
16942
+ <div id="settings-llama-chat-template-tools" class="hydra-chat-template-tools" data-hydra-provider-field="local">
16943
+ <button type="button" id="settings-llama-chat-template-edit" class="inline-btn">Edit Chat Template</button>
16944
+ <span id="settings-llama-chat-template-status" class="small">Per-model template override.</span>
16945
+ </div>
16946
  <label class="hydra-context-field" data-hydra-provider-field="hf_transformers">Context Length
16947
  <div class="hydra-context-control" data-hydra-context-control="hf_transformers">
16948
  <input id="set_hydra_hf_transformers_context_tokens_range" type="range" min="256" max="262144" step="256" value="${escapeHtml(
 
16955
  <div id="hydra-hf-context-hint" class="small hydra-context-hint"></div>
16956
  <div id="hydra-hf-context-estimate" class="hydra-context-estimate" data-hydra-context-estimate="hf_transformers"></div>
16957
  </label>
16958
+ <label class="hydra-context-field" data-hydra-provider-field="hf_transformers">Device
16959
+ <select id="set_hydra_hf_transformers_device">
16960
+ ${renderChoiceOptions(
16961
+ [
16962
+ { value: "auto", label: "Auto" },
16963
+ { value: "cuda", label: "CUDA" },
16964
+ { value: "mps", label: "Apple MPS" },
16965
+ { value: "cpu", label: "CPU" },
16966
+ ],
16967
+ settings.hydra_hf_transformers_device || "auto"
16968
+ )}
16969
+ </select>
16970
+ <div class="small hydra-context-hint">Auto chooses CUDA, then Apple MPS, then CPU.</div>
16971
+ </label>
16972
+ <label class="hydra-context-field" data-hydra-provider-field="hf_transformers">Precision
16973
+ <select id="set_hydra_hf_transformers_dtype">
16974
+ ${renderChoiceOptions(
16975
+ [
16976
+ { value: "auto", label: "Auto" },
16977
+ { value: "float16", label: "Float16" },
16978
+ { value: "bfloat16", label: "BFloat16" },
16979
+ { value: "float32", label: "Float32" },
16980
+ ],
16981
+ settings.hydra_hf_transformers_dtype || "auto"
16982
+ )}
16983
+ </select>
16984
+ <div class="small hydra-context-hint">Auto lets Transformers choose the model dtype.</div>
16985
+ </label>
16986
+ <label class="hydra-context-field" data-hydra-provider-field="hf_transformers">Device Map
16987
+ <select id="set_hydra_hf_transformers_device_map">
16988
+ ${renderChoiceOptions(
16989
+ [
16990
+ { value: "default", label: "Default" },
16991
+ { value: "disabled", label: "Disabled" },
16992
+ { value: "auto", label: "Auto" },
16993
+ { value: "balanced", label: "Balanced" },
16994
+ ],
16995
+ settings.hydra_hf_transformers_device_map || "default"
16996
+ )}
16997
+ </select>
16998
+ <div class="small hydra-context-hint">Default uses automatic mapping on CUDA and direct device load elsewhere.</div>
16999
+ </label>
17000
+ <label class="hydra-context-field" data-hydra-provider-field="hf_transformers">Attention
17001
+ <select id="set_hydra_hf_transformers_attn_implementation">
17002
+ ${renderChoiceOptions(
17003
+ [
17004
+ { value: "auto", label: "Auto" },
17005
+ { value: "sdpa", label: "SDPA" },
17006
+ { value: "flash_attention_2", label: "Flash Attention 2" },
17007
+ { value: "eager", label: "Eager" },
17008
+ ],
17009
+ settings.hydra_hf_transformers_attn_implementation || "auto"
17010
+ )}
17011
+ </select>
17012
+ <div class="small hydra-context-hint">Uses Transformers attention implementation support when available.</div>
17013
+ </label>
17014
+ <label class="hydra-context-field" data-hydra-provider-field="hf_transformers">Trust Remote Code
17015
+ ${renderToggleRow(
17016
+ `<input id="set_hydra_hf_transformers_trust_remote_code" class="toggle-input" type="checkbox" ${
17017
+ settings.hydra_hf_transformers_trust_remote_code ? "checked" : ""
17018
+ } />`,
17019
+ "Enable"
17020
+ )}
17021
+ <div class="small hydra-context-hint">Allows custom model code from the selected repository.</div>
17022
+ </label>
17023
  <label class="hydra-context-field" data-hydra-provider-field="llama_cpp">Context Length
17024
  <div class="hydra-context-control" data-hydra-context-control="llama_cpp">
17025
  <input id="set_hydra_llama_cpp_context_tokens_range" type="range" min="256" max="262144" step="256" value="${escapeHtml(
 
17032
  <div id="hydra-llama-context-hint" class="small hydra-context-hint"></div>
17033
  <div id="hydra-llama-context-estimate" class="hydra-context-estimate" data-hydra-context-estimate="llama_cpp"></div>
17034
  </label>
17035
+ <label class="hydra-context-field" data-hydra-provider-field="llama_cpp">Eval Batch Size
17036
+ <div class="hydra-context-control hydra-llama-perf-control">
17037
+ <input id="set_hydra_llama_cpp_n_batch_range" type="range" min="32" max="8192" step="32" value="${escapeHtml(
17038
+ settings.hydra_llama_cpp_n_batch || "512"
17039
+ )}" />
17040
+ <input id="set_hydra_llama_cpp_n_batch" type="number" min="32" max="8192" step="32" value="${escapeHtml(
17041
+ settings.hydra_llama_cpp_n_batch || "512"
17042
+ )}" />
17043
+ </div>
17044
+ <div class="small hydra-context-hint">Higher values can improve prompt processing when memory allows.</div>
17045
+ </label>
17046
+ <label class="hydra-context-field" data-hydra-provider-field="llama_cpp">Micro-Batch Size
17047
+ <div class="hydra-context-control hydra-llama-perf-control">
17048
+ <input id="set_hydra_llama_cpp_n_ubatch_range" type="range" min="0" max="8192" step="32" value="${escapeHtml(
17049
+ settings.hydra_llama_cpp_n_ubatch || "0"
17050
+ )}" />
17051
+ <input id="set_hydra_llama_cpp_n_ubatch" type="number" min="0" max="8192" step="32" value="${escapeHtml(
17052
+ settings.hydra_llama_cpp_n_ubatch || "0"
17053
+ )}" />
17054
+ </div>
17055
+ <div class="small hydra-context-hint">0 lets llama.cpp use its default, usually matching the eval batch size.</div>
17056
+ </label>
17057
+ <label class="hydra-context-field" data-hydra-provider-field="llama_cpp">Flash Attention
17058
+ ${renderToggleRow(
17059
+ `<input id="set_hydra_llama_cpp_flash_attn" class="toggle-input" type="checkbox" ${
17060
+ settings.hydra_llama_cpp_flash_attn ? "checked" : ""
17061
+ } />`,
17062
+ "Enable"
17063
+ )}
17064
+ <div class="small hydra-context-hint">Optimizes attention computation when supported by the backend and model.</div>
17065
+ </label>
17066
+ <label class="hydra-context-field" data-hydra-provider-field="llama_cpp">GPU KV Offload
17067
+ ${renderToggleRow(
17068
+ `<input id="set_hydra_llama_cpp_offload_kqv" class="toggle-input" type="checkbox" ${
17069
+ settings.hydra_llama_cpp_offload_kqv !== false ? "checked" : ""
17070
+ } />`,
17071
+ "Enable"
17072
+ )}
17073
+ <div class="small hydra-context-hint">Keeps attention/KV work on the GPU when llama.cpp supports it.</div>
17074
+ </label>
17075
  <label class="hydra-context-field" data-hydra-provider-field="llama_cpp">Multi-Token Prediction
17076
  ${renderToggleRow(
17077
  `<input id="set_hydra_llama_cpp_mtp_enabled" class="toggle-input" type="checkbox" ${
 
17103
  <div id="hydra-mlx-context-hint" class="small hydra-context-hint"></div>
17104
  <div id="hydra-mlx-context-estimate" class="hydra-context-estimate" data-hydra-context-estimate="mlx_lm"></div>
17105
  </label>
17106
+ <label class="hydra-context-field" data-hydra-provider-field="mlx_lm">Lazy Load
17107
+ ${renderToggleRow(
17108
+ `<input id="set_hydra_mlx_lm_lazy_load" class="toggle-input" type="checkbox" ${
17109
+ settings.hydra_mlx_lm_lazy_load ? "checked" : ""
17110
+ } />`,
17111
+ "Enable"
17112
+ )}
17113
+ <div class="small hydra-context-hint">Defers some MLX weight materialization during model load.</div>
17114
+ </label>
17115
+ <label class="hydra-context-field" data-hydra-provider-field="mlx_lm">Trust Remote Code
17116
+ ${renderToggleRow(
17117
+ `<input id="set_hydra_mlx_lm_trust_remote_code" class="toggle-input" type="checkbox" ${
17118
+ settings.hydra_mlx_lm_trust_remote_code ? "checked" : ""
17119
+ } />`,
17120
+ "Enable"
17121
+ )}
17122
+ <div class="small hydra-context-hint">Allows custom tokenizer/config code from the selected repository.</div>
17123
+ </label>
17124
+ <label class="hydra-context-field" data-hydra-provider-field="mlx_lm">Prefill Step Size
17125
+ <input id="set_hydra_mlx_engine_prefill_step_size" type="number" min="1" max="32768" step="1" placeholder="Auto" value="${escapeHtml(
17126
+ settings.hydra_mlx_engine_prefill_step_size || ""
17127
+ )}" />
17128
+ <div class="small hydra-context-hint">Blank lets the MLX runtime choose the prompt prefill chunk size.</div>
17129
+ </label>
17130
+ <label class="hydra-context-field" data-hydra-provider-field="mlx_lm">Quantized KV Bits
17131
+ <select id="set_hydra_mlx_engine_kv_bits">
17132
+ ${renderChoiceOptions(
17133
+ [
17134
+ { value: "", label: "Auto" },
17135
+ { value: "2", label: "2-bit" },
17136
+ { value: "3", label: "3-bit" },
17137
+ { value: "4", label: "4-bit" },
17138
+ { value: "6", label: "6-bit" },
17139
+ { value: "8", label: "8-bit" },
17140
+ ],
17141
+ settings.hydra_mlx_engine_kv_bits || ""
17142
+ )}
17143
+ </select>
17144
+ <div class="small hydra-context-hint">Optional KV cache quantization for lower memory use.</div>
17145
+ </label>
17146
+ <label class="hydra-context-field" data-hydra-provider-field="mlx_lm">KV Group Size
17147
+ <select id="set_hydra_mlx_engine_kv_group_size">
17148
+ ${renderChoiceOptions(
17149
+ [
17150
+ { value: "", label: "Auto" },
17151
+ { value: "32", label: "32" },
17152
+ { value: "64", label: "64" },
17153
+ { value: "128", label: "128" },
17154
+ ],
17155
+ settings.hydra_mlx_engine_kv_group_size || ""
17156
+ )}
17157
+ </select>
17158
+ <div class="small hydra-context-hint">Blank uses the MLX runtime default.</div>
17159
+ </label>
17160
+ <label class="hydra-context-field" data-hydra-provider-field="mlx_lm">Quantized KV Start
17161
+ <input id="set_hydra_mlx_engine_quantized_kv_start" type="number" min="0" max="1048576" step="1" placeholder="Auto" value="${escapeHtml(
17162
+ settings.hydra_mlx_engine_quantized_kv_start || ""
17163
+ )}" />
17164
+ <div class="small hydra-context-hint">Token index where quantized KV begins. Blank uses the runtime default.</div>
17165
+ </label>
17166
  <label id="hydra-base-api-key-wrap" data-hydra-provider-field="openai_compatible" style="grid-column: 1 / -1;">API Key (optional)
17167
  <input id="set_hydra_llm_api_key" type="password" autocomplete="new-password" value="${escapeHtml(
17168
  hydraPrimaryBaseRow.api_key || ""
 
18133
  if (["speakerid", "emotionid"].includes(activeModelsPanel)) {
18134
  void ensureEspHomeRuntimeLoaded({ force: true, panel: activeModelsPanel });
18135
  }
18136
+ } else {
18137
+ clearLlmDebugPollTimer();
18138
  }
18139
  };
18140
 
 
18293
  const hydraBaseModelSelectEl = document.getElementById("set_hydra_llm_model_select");
18294
  const hydraBaseModelLabelEl = document.getElementById("hydra-base-model-label");
18295
  const hydraLocalModelStatusEl = document.getElementById("hydra-local-model-status");
18296
+ const llamaChatTemplateEditEl = document.getElementById("settings-llama-chat-template-edit");
18297
+ const llamaChatTemplateStatusEl = document.getElementById("settings-llama-chat-template-status");
18298
  const contextControlConfig = {
18299
  hf_transformers: {
18300
  min: 256,
 
18510
  llamaCppMtpDraftNumberEl?.addEventListener("input", () => syncLlamaCppMtpDraftControl(llamaCppMtpDraftNumberEl));
18511
  llamaCppMtpDraftNumberEl?.addEventListener("blur", () => syncLlamaCppMtpDraftControl(llamaCppMtpDraftNumberEl));
18512
  syncLlamaCppMtpDraftControl();
18513
+ const syncPairedIntegerControl = ({ rangeEl, numberEl, fallback = 0, min = 0, max = 8192, step = 1 }) => {
18514
+ if (!rangeEl || !numberEl) {
18515
+ return;
18516
+ }
18517
+ const applyValue = (sourceEl = null) => {
18518
+ const raw = Number(sourceEl?.value || numberEl.value || rangeEl.value || fallback);
18519
+ let value = Number.isFinite(raw) ? Math.round(raw) : fallback;
18520
+ value = Math.max(min, Math.min(max, value));
18521
+ if (step > 1 && value > min) {
18522
+ value = Math.round(value / step) * step;
18523
+ value = Math.max(min, Math.min(max, value));
18524
+ }
18525
+ rangeEl.value = String(value);
18526
+ numberEl.value = String(value);
18527
+ };
18528
+ rangeEl.addEventListener("input", () => applyValue(rangeEl));
18529
+ numberEl.addEventListener("input", () => applyValue(numberEl));
18530
+ numberEl.addEventListener("blur", () => applyValue(numberEl));
18531
+ applyValue();
18532
+ };
18533
+ syncPairedIntegerControl({
18534
+ rangeEl: document.getElementById("set_hydra_llama_cpp_n_batch_range"),
18535
+ numberEl: document.getElementById("set_hydra_llama_cpp_n_batch"),
18536
+ fallback: 512,
18537
+ min: 32,
18538
+ max: 8192,
18539
+ step: 32,
18540
+ });
18541
+ syncPairedIntegerControl({
18542
+ rangeEl: document.getElementById("set_hydra_llama_cpp_n_ubatch_range"),
18543
+ numberEl: document.getElementById("set_hydra_llama_cpp_n_ubatch"),
18544
+ fallback: 0,
18545
+ min: 0,
18546
+ max: 8192,
18547
+ step: 32,
18548
+ });
18549
  const normalizeHydraBaseRowInput = (row) => ({
18550
  provider: normalizeHydraBaseProvider(row?.provider || ""),
18551
  host: String(row?.host || "").trim(),
 
18645
  }
18646
  return String(hydraBaseModelEl?.value || "").trim();
18647
  };
18648
+ const syncLlamaChatTemplateTools = () => {
18649
+ const provider = normalizeHydraBaseProvider(hydraBaseProviderEl?.value || "");
18650
+ const local = isHydraLocalProvider(provider);
18651
+ const model = local ? getHydraBaseModelValue() : "";
18652
+ if (llamaChatTemplateEditEl) {
18653
+ llamaChatTemplateEditEl.disabled = !model;
18654
+ }
18655
+ if (llamaChatTemplateStatusEl) {
18656
+ const providerLabel = provider === "llama_cpp" ? "GGUF" : provider === "mlx_lm" ? "MLX" : "Transformers";
18657
+ llamaChatTemplateStatusEl.textContent = !local
18658
+ ? ""
18659
+ : model
18660
+ ? `Per-model ${providerLabel} template override.`
18661
+ : `Select a downloaded ${providerLabel} model first.`;
18662
+ }
18663
+ };
18664
  const refreshLocalLlmModels = async ({ selectModel = "", provider = "" } = {}) => {
18665
  try {
18666
  localLlmModelsPayload = await api("/api/settings/local-llm/models", { _timeoutMs: HEALTH_REQUEST_TIMEOUT_MS });
 
18723
  syncHydraProviderScopedFields(hydraBaseFieldsEl, provider);
18724
  syncHydraModelInputForProvider(hydraBaseModelEl, hydraBaseModelLabelEl, provider);
18725
  syncHydraPrimaryModelControl(provider);
18726
+ syncLlamaChatTemplateTools();
18727
  };
18728
  const hydraRouteControls = [];
18729
  const syncHydraRouteControl = (control, preferredModel = "") => {
 
19021
  syncActiveHydraContextControl();
19022
  renderHydraContextEstimateCards();
19023
  syncVisionLlamaContextControl();
19024
+ syncLlamaChatTemplateTools();
19025
+ });
19026
+ llamaChatTemplateEditEl?.addEventListener("click", () => {
19027
+ const provider = normalizeHydraBaseProvider(hydraBaseProviderEl?.value || "");
19028
+ const model = getHydraBaseModelValue();
19029
+ void openLocalLlmChatTemplateModal(provider, model, {
19030
+ onSaved: () => {
19031
+ void refreshLocalLlmModels({ provider, selectModel: model });
19032
+ syncLlamaChatTemplateTools();
19033
+ },
19034
+ });
19035
  });
19036
  syncHydraPrimaryProviderFields();
19037
  if (state.hydraContextEstimateRuntimeListener) {
 
19085
  const localModelManagerStatusEl = document.getElementById("local-model-manager-status");
19086
  const localModelManagerListEl = document.getElementById("local-model-manager-list");
19087
  const localModelManagerRefreshEl = document.getElementById("local-model-manager-refresh");
19088
+ const llmDebugConsoleEl = document.getElementById("settings-llm-debug-console");
19089
+ const llmDebugStatusEl = document.getElementById("llm-debug-status");
19090
+ const llmDebugActiveEl = document.getElementById("llm-debug-active");
19091
+ const llmDebugEventsEl = document.getElementById("llm-debug-events");
19092
+ const llmDebugRefreshEl = document.getElementById("llm-debug-refresh");
19093
+ const llmDebugAutoscrollEl = document.getElementById("llm-debug-autoscroll");
19094
+ const llmDebugClearEl = document.getElementById("llm-debug-clear");
19095
  const hfModelBrowserViewButtons = Array.from(root.querySelectorAll("[data-hf-model-view]"));
19096
  const hfModelBrowserState = {
19097
  provider: isHydraLocalProvider(hydraPrimaryBaseRow.provider) ? hydraPrimaryBaseRow.provider : "hf_transformers",
19098
  task: "text-generation",
19099
+ view: "picks",
19100
  query: "",
19101
  loaded: false,
19102
  loadingSeq: 0,
 
19122
  const hfModelTaskLabel = (task) => normalizeHfModelTask(task) === "image-text-to-text" ? "vision" : "text";
19123
  const normalizeHfModelView = (value) => {
19124
  const token = String(value || "trending").trim().toLowerCase().replace(/_/g, "-");
19125
+ if (["picks", "tater", "tater-picks", "recommended"].includes(token)) {
19126
+ return "picks";
19127
+ }
19128
  if (token === "new" || token === "recent" || token === "latest") {
19129
  return "new";
19130
  }
 
19135
  };
19136
  const hfModelViewLabel = (value) => {
19137
  const view = normalizeHfModelView(value);
19138
+ if (view === "picks") {
19139
+ return "Tater Picks";
19140
+ }
19141
  if (view === "new") {
19142
  return "new";
19143
  }
 
19234
  </div>
19235
  ${pathLabel ? `<div class="local-model-path">${escapeHtml(pathLabel)}</div>` : ""}
19236
  <div class="local-model-actions">
19237
+ <button type="button" class="inline-btn" data-local-model-template data-provider="${escapeHtml(row.provider)}" data-model="${escapeHtml(row.model)}">Chat Template</button>
19238
  <button type="button" class="inline-btn danger" data-local-model-delete data-provider="${escapeHtml(row.provider)}" data-model="${escapeHtml(row.model)}">Delete</button>
19239
  </div>
19240
  </article>
 
19355
  const library = String(model?.library_name || model?.pipeline_tag || "").trim();
19356
  const compatible = Boolean(model?.compatible);
19357
  const supportsVision = Boolean(model?.supports_vision) || normalizeHfModelTask(model?.task) === "image-text-to-text" || normalizeHfModelTask(hfModelBrowserState.task) === "image-text-to-text";
19358
+ const isTaterPick = Boolean(model?.tater_pick);
19359
+ const taterPickLabel = String(model?.tater_pick_label || "Tater Pick").trim();
19360
+ const taterPickNote = String(model?.tater_pick_note || "").trim();
19361
  const isMlxProvider = provider === "mlx_lm";
19362
  const downloadLabel = isMlxProvider ? "Download Repo" : "Download";
19363
  return `
19364
+ <article class="hf-model-card ${compatible ? "" : "is-uncertain"} ${isTaterPick ? "is-tater-pick" : ""}" role="button" tabindex="0" aria-label="Show files for ${escapeHtml(id)}" data-hf-model-card data-provider="${escapeHtml(provider)}" data-repo-id="${escapeHtml(id)}">
19365
  <div class="hf-model-card-head">
19366
  <div>
19367
  <strong>${escapeHtml(id)}</strong>
19368
  <span>${escapeHtml(library || hfModelProviderLabel(provider))}</span>
19369
  </div>
19370
+ <span class="${isTaterPick ? "hf-model-tater-pill" : "hf-model-provider-pill"}">${escapeHtml(isTaterPick ? taterPickLabel : hfModelProviderLabel(provider))}</span>
19371
  </div>
19372
  <div class="hf-model-card-meta">
19373
  ${modelSize ? `<span class="hf-model-size-pill">${escapeHtml(modelSize)}</span>` : ""}
19374
  ${supportsVision ? `<span class="hf-model-vision-pill" title="Supports vision models">Vision</span>` : ""}
19375
+ ${isTaterPick && taterPickNote ? `<span class="hf-model-pick-note">${escapeHtml(taterPickNote)}</span>` : ""}
19376
  <span>${escapeHtml(downloads)} downloads</span>
19377
  <span>${escapeHtml(likes)} likes</span>
19378
  ${updated ? `<span>${escapeHtml(updated)}</span>` : ""}
 
19480
  hfModelBrowserState.provider = provider;
19481
  hfModelBrowserState.task = task;
19482
  hfModelBrowserState.query = query;
19483
+ const viewLabel = hfModelViewLabel(hfModelBrowserState.view);
19484
+ const viewIsPicks = normalizeHfModelView(hfModelBrowserState.view) === "picks";
19485
+ setHfModelBrowserStatus(
19486
+ viewIsPicks
19487
+ ? `Loading ${viewLabel} ${hfModelProviderLabel(provider)} ${hfModelTaskLabel(task)} models...`
19488
+ : `Loading ${viewLabel} ${hfModelProviderLabel(provider)} ${hfModelTaskLabel(task)} models page ${hfModelBrowserState.pageIndex + 1}...`
19489
+ );
19490
  if (hfModelBrowserResultsEl) {
19491
  hfModelBrowserResultsEl.innerHTML = `<div class="hf-model-browser-empty">Loading models...</div>`;
19492
  }
 
19513
  }
19514
  renderHfModelCards(payload?.models || []);
19515
  renderHfModelBrowserPagination();
19516
+ setHfModelBrowserStatus(
19517
+ viewIsPicks
19518
+ ? `${(payload?.models || []).length} ${viewLabel} ${hfModelProviderLabel(provider)} model${(payload?.models || []).length === 1 ? "" : "s"} loaded.`
19519
+ : `Page ${hfModelBrowserState.pageIndex + 1}: ${(payload?.models || []).length} models loaded.`,
19520
+ "success"
19521
+ );
19522
  } catch (error) {
19523
  if (seq !== hfModelBrowserState.loadingSeq) {
19524
  return;
 
19665
  showToast(`Model delete failed: ${error.message}`, "error", 3600);
19666
  }
19667
  };
19668
+ const llmDebugTimeLabel = (value) => {
19669
+ const seconds = Number(value || 0);
19670
+ if (!Number.isFinite(seconds) || seconds <= 0) {
19671
+ return "--:--";
19672
+ }
19673
+ try {
19674
+ return new Date(seconds * 1000).toLocaleTimeString([], { hour: "numeric", minute: "2-digit", second: "2-digit" });
19675
+ } catch {
19676
+ return "--:--";
19677
+ }
19678
+ };
19679
+ const llmDebugDurationLabel = (value) => {
19680
+ const ms = Number(value || 0);
19681
+ if (!Number.isFinite(ms) || ms <= 0) {
19682
+ return "";
19683
+ }
19684
+ if (ms >= 1000) {
19685
+ return `${(ms / 1000).toFixed(ms >= 10000 ? 1 : 2)}s`;
19686
+ }
19687
+ return `${Math.round(ms)}ms`;
19688
+ };
19689
+ const llmDebugTokenLabel = (event) => {
19690
+ const promptTokens = Math.max(0, Number(event?.prompt_tokens || 0));
19691
+ const completionTokens = Math.max(0, Number(event?.completion_tokens || 0));
19692
+ const totalTokens = Math.max(0, Number(event?.total_tokens || 0));
19693
+ const parts = [];
19694
+ if (promptTokens > 0) {
19695
+ parts.push(`in ${Math.round(promptTokens).toLocaleString()}`);
19696
+ }
19697
+ if (completionTokens > 0) {
19698
+ parts.push(`out ${Math.round(completionTokens).toLocaleString()}`);
19699
+ }
19700
+ if (!parts.length && totalTokens > 0) {
19701
+ parts.push(`${Math.round(totalTokens).toLocaleString()} tok`);
19702
+ }
19703
+ return parts.join(" / ");
19704
+ };
19705
+ const normalizeLlmDebugEvent = (row) => ({
19706
+ id: Math.max(0, Number(row?.id || 0)),
19707
+ ts: Number(row?.ts || row?.timestamp || 0),
19708
+ level: String(row?.level || "info").trim().toLowerCase() || "info",
19709
+ phase: String(row?.phase || "event").trim().toLowerCase() || "event",
19710
+ message: String(row?.message || "").trim(),
19711
+ detail: String(row?.detail || "").trim(),
19712
+ output: String(row?.output || "").trim(),
19713
+ provider: String(row?.provider || "").trim(),
19714
+ host: String(row?.host || "").trim(),
19715
+ model: String(row?.model || "").trim(),
19716
+ activity: String(row?.activity || "").trim(),
19717
+ source_label: String(row?.source_label || "").trim(),
19718
+ source: String(row?.source || "").trim(),
19719
+ kind: String(row?.kind || "").trim(),
19720
+ call_id: String(row?.call_id || "").trim(),
19721
+ prompt_tokens: Math.max(0, Number(row?.prompt_tokens || 0)),
19722
+ completion_tokens: Math.max(0, Number(row?.completion_tokens || 0)),
19723
+ total_tokens: Math.max(0, Number(row?.total_tokens || 0)),
19724
+ duration_ms: Math.max(0, Number(row?.duration_ms || 0)),
19725
+ });
19726
+ const renderLlmDebugActiveCalls = (rows = []) => {
19727
+ if (!llmDebugActiveEl) {
19728
+ return;
19729
+ }
19730
+ const calls = Array.isArray(rows) ? rows : [];
19731
+ if (!calls.length) {
19732
+ llmDebugActiveEl.innerHTML = `<div class="llm-debug-active-empty">No active LLM calls.</div>`;
19733
+ return;
19734
+ }
19735
+ llmDebugActiveEl.innerHTML = calls
19736
+ .map((call) => {
19737
+ const model = String(call?.model || "model").trim();
19738
+ const source = String(call?.source_label || call?.source || "unknown").trim();
19739
+ const activity = String(call?.activity || "llm").trim();
19740
+ const age = Math.max(0, Number(call?.age_seconds || 0));
19741
+ const host = String(call?.host || "").trim();
19742
+ const messages = Math.max(0, Number(call?.message_count || 0));
19743
+ return `
19744
+ <div class="llm-debug-active-call">
19745
+ <strong>${escapeHtml(activity)}</strong>
19746
+ <span>${escapeHtml(model)}</span>
19747
+ <small>${escapeHtml(source)}${host ? ` · ${escapeHtml(host)}` : ""} · ${messages} messages · ${Math.round(age)}s</small>
19748
+ </div>
19749
+ `;
19750
+ })
19751
+ .join("");
19752
+ };
19753
+ const renderLlmDebugEvents = () => {
19754
+ if (!llmDebugEventsEl) {
19755
+ return;
19756
+ }
19757
+ const rows = Array.isArray(state.llmDebugEvents) ? state.llmDebugEvents.slice(-220) : [];
19758
+ if (!rows.length) {
19759
+ llmDebugEventsEl.innerHTML = `<div class="llm-debug-empty">No LLM debug events yet.</div>`;
19760
+ return;
19761
+ }
19762
+ const shouldScroll =
19763
+ state.llmDebugAutoScroll &&
19764
+ Math.abs(llmDebugEventsEl.scrollHeight - llmDebugEventsEl.clientHeight - llmDebugEventsEl.scrollTop) < 72;
19765
+ llmDebugEventsEl.innerHTML = rows
19766
+ .map((event) => {
19767
+ const phase = event.phase || event.level || "event";
19768
+ const meta = [
19769
+ event.provider,
19770
+ event.source_label || event.source,
19771
+ event.activity,
19772
+ event.model,
19773
+ llmDebugTokenLabel(event),
19774
+ llmDebugDurationLabel(event.duration_ms),
19775
+ ].filter(Boolean);
19776
+ return `
19777
+ <article class="llm-debug-line ${escapeHtml(event.level)} phase-${escapeHtml(phase)}">
19778
+ <span class="llm-debug-time">${escapeHtml(llmDebugTimeLabel(event.ts))}</span>
19779
+ <span class="llm-debug-phase">${escapeHtml(phase.replace(/_/g, " "))}</span>
19780
+ <div class="llm-debug-body">
19781
+ <strong>${escapeHtml(event.message || phase)}</strong>
19782
+ ${meta.length ? `<small>${escapeHtml(meta.join(" · "))}</small>` : ""}
19783
+ ${event.detail ? `<pre>${escapeHtml(event.detail)}</pre>` : ""}
19784
+ ${event.output ? `<pre class="llm-debug-output">${escapeHtml(event.output)}</pre>` : ""}
19785
+ </div>
19786
+ </article>
19787
+ `;
19788
+ })
19789
+ .join("");
19790
+ if (shouldScroll) {
19791
+ llmDebugEventsEl.scrollTop = llmDebugEventsEl.scrollHeight;
19792
+ }
19793
+ };
19794
+ const setLlmDebugStatus = (message, tone = "") => {
19795
+ if (!llmDebugStatusEl) {
19796
+ return;
19797
+ }
19798
+ llmDebugStatusEl.textContent = String(message || "");
19799
+ llmDebugStatusEl.classList.toggle("error", tone === "error");
19800
+ llmDebugStatusEl.classList.toggle("success", tone === "success");
19801
+ };
19802
+ const renderLlmDebugSnapshot = (payload = {}, { reset = false } = {}) => {
19803
+ state.llmDebugLastSnapshot = payload && typeof payload === "object" ? payload : {};
19804
+ const events = Array.isArray(payload?.events) ? payload.events.map((row) => normalizeLlmDebugEvent(row)).filter((row) => row.id > 0) : [];
19805
+ if (reset) {
19806
+ state.llmDebugEvents = events;
19807
+ } else if (events.length) {
19808
+ const merged = new Map((Array.isArray(state.llmDebugEvents) ? state.llmDebugEvents : []).map((row) => [Number(row.id), row]));
19809
+ events.forEach((event) => merged.set(Number(event.id), event));
19810
+ state.llmDebugEvents = Array.from(merged.values()).sort((a, b) => Number(a.id) - Number(b.id)).slice(-260);
19811
+ }
19812
+ state.llmDebugNextId = Math.max(Number(state.llmDebugNextId || 0), Number(payload?.next_id || 0));
19813
+ renderLlmDebugActiveCalls(Array.isArray(payload?.active_calls) ? payload.active_calls : []);
19814
+ renderLlmDebugEvents();
19815
+ const activeTotal = Number(payload?.summary?.active_total ?? (payload?.active_calls || []).length ?? 0);
19816
+ const eventCount = Array.isArray(state.llmDebugEvents) ? state.llmDebugEvents.length : 0;
19817
+ setLlmDebugStatus(`${Math.max(0, activeTotal)} active LLM call${activeTotal === 1 ? "" : "s"} · ${eventCount} visible event${eventCount === 1 ? "" : "s"}`, "success");
19818
+ };
19819
+ const updateLlmDebugAutoscrollButton = () => {
19820
+ if (llmDebugAutoscrollEl) {
19821
+ llmDebugAutoscrollEl.textContent = state.llmDebugAutoScroll ? "Auto-scroll On" : "Auto-scroll Off";
19822
+ llmDebugAutoscrollEl.classList.toggle("active", Boolean(state.llmDebugAutoScroll));
19823
+ }
19824
+ };
19825
+ const refreshLlmDebugConsole = async ({ reset = false } = {}) => {
19826
+ if (
19827
+ !llmDebugConsoleEl ||
19828
+ !document.body.contains(llmDebugConsoleEl) ||
19829
+ document.body.dataset.view !== "settings" ||
19830
+ state.llmDebugInFlight
19831
+ ) {
19832
+ return;
19833
+ }
19834
+ state.llmDebugInFlight = true;
19835
+ try {
19836
+ const sinceId = reset ? 0 : Math.max(0, Number(state.llmDebugNextId || 0));
19837
+ const params = new URLSearchParams({ since_id: String(sinceId), limit: "220" });
19838
+ const payload = await api(`/api/runtime/llm/debug?${params.toString()}`, { _skipRedisRecovery: true, _timeoutMs: HEALTH_REQUEST_TIMEOUT_MS });
19839
+ renderLlmDebugSnapshot(payload, { reset });
19840
+ } catch (error) {
19841
+ setLlmDebugStatus(`Debug refresh failed: ${error.message}`, "error");
19842
+ } finally {
19843
+ state.llmDebugInFlight = false;
19844
+ }
19845
+ };
19846
+ const scheduleLlmDebugPoll = (delayMs = 1200) => {
19847
+ clearLlmDebugPollTimer();
19848
+ if (
19849
+ !llmDebugConsoleEl ||
19850
+ !document.body.contains(llmDebugConsoleEl) ||
19851
+ document.body.dataset.view !== "settings" ||
19852
+ llmDebugConsoleEl.hidden
19853
+ ) {
19854
+ return;
19855
+ }
19856
+ state.llmDebugPollTimer = window.setTimeout(async () => {
19857
+ state.llmDebugPollTimer = 0;
19858
+ await refreshLlmDebugConsole();
19859
+ scheduleLlmDebugPoll(1200);
19860
+ }, Math.max(250, Number(delayMs || 1200)));
19861
+ };
19862
  const activateLlmVisionTab = (tab) => {
19863
  const normalized = String(tab || "settings").trim() || "settings";
19864
  const browserActive = normalized === "huggingface";
19865
  const manageActive = normalized === "manage";
19866
+ const debugActive = normalized === "debug";
19867
  llmVisionTabButtons.forEach((button) => {
19868
  button.classList.toggle("active", String(button.dataset.llmVisionTab || "") === normalized);
19869
  });
19870
  root.querySelectorAll(".llm-vision-settings-block").forEach((block) => {
19871
+ block.classList.toggle("llm-vision-hidden", browserActive || manageActive || debugActive);
19872
  });
19873
  if (hfModelBrowserEl) {
19874
  hfModelBrowserEl.hidden = !browserActive;
 
19878
  localModelManagerEl.hidden = !manageActive;
19879
  localModelManagerEl.classList.toggle("active", manageActive);
19880
  }
19881
+ if (llmDebugConsoleEl) {
19882
+ llmDebugConsoleEl.hidden = !debugActive;
19883
+ llmDebugConsoleEl.classList.toggle("active", debugActive);
19884
+ }
19885
+ if (!debugActive) {
19886
+ clearLlmDebugPollTimer();
19887
+ }
19888
  if (browserActive) {
19889
  renderHfDownloadSummary(state.hfLlmWarmupLastSnapshot || {});
19890
  scheduleHfLlmWarmupPoll(150);
 
19893
  renderLocalModelManager();
19894
  void refreshLocalLlmModels();
19895
  }
19896
+ if (debugActive) {
19897
+ updateLlmDebugAutoscrollButton();
19898
+ renderLlmDebugSnapshot(state.llmDebugLastSnapshot || {}, { reset: false });
19899
+ void refreshLlmDebugConsole({ reset: state.llmDebugNextId <= 0 });
19900
+ scheduleLlmDebugPoll(800);
19901
+ }
19902
  if (browserActive && !hfModelBrowserState.loaded) {
19903
  if (hfModelBrowserProviderEl) {
19904
  hfModelBrowserProviderEl.value = isHydraLocalProvider(hydraBaseProviderEl?.value || "")
 
19911
  llmVisionTabButtons.forEach((button) => {
19912
  button.addEventListener("click", () => activateLlmVisionTab(button.dataset.llmVisionTab || "settings"));
19913
  });
19914
+ llmDebugRefreshEl?.addEventListener("click", () => {
19915
+ void refreshLlmDebugConsole({ reset: true });
19916
+ scheduleLlmDebugPoll(1000);
19917
+ });
19918
+ llmDebugAutoscrollEl?.addEventListener("click", () => {
19919
+ state.llmDebugAutoScroll = !state.llmDebugAutoScroll;
19920
+ safeStorageSet("tater_llm_debug_auto_scroll", state.llmDebugAutoScroll ? "true" : "false");
19921
+ updateLlmDebugAutoscrollButton();
19922
+ if (state.llmDebugAutoScroll && llmDebugEventsEl) {
19923
+ llmDebugEventsEl.scrollTop = llmDebugEventsEl.scrollHeight;
19924
+ }
19925
+ });
19926
+ llmDebugClearEl?.addEventListener("click", () => {
19927
+ state.llmDebugEvents = [];
19928
+ renderLlmDebugEvents();
19929
+ setLlmDebugStatus("Debug view cleared. New events will continue to appear.");
19930
+ });
19931
  hfModelBrowserViewButtons.forEach((button) => {
19932
  button.addEventListener("click", () => {
19933
  hfModelBrowserState.view = normalizeHfModelView(button.dataset.hfModelView || "trending");
 
20064
  void refreshLocalLlmModels();
20065
  });
20066
  localModelManagerListEl?.addEventListener("click", (event) => {
20067
+ const templateButton = event.target instanceof Element ? event.target.closest("[data-local-model-template]") : null;
20068
+ if (templateButton) {
20069
+ const provider = normalizeHydraBaseProvider(templateButton.getAttribute("data-provider") || "");
20070
+ const model = templateButton.getAttribute("data-model") || "";
20071
+ if (isHydraLocalProvider(provider)) {
20072
+ void openLocalLlmChatTemplateModal(provider, model, {
20073
+ onSaved: () => {
20074
+ void refreshLocalLlmModels({ provider, selectModel: model });
20075
+ syncLlamaChatTemplateTools();
20076
+ },
20077
+ });
20078
+ }
20079
+ return;
20080
+ }
20081
  const button = event.target instanceof Element ? event.target.closest("[data-local-model-delete]") : null;
20082
  if (!button) {
20083
  return;
 
21490
  const baseModel = getHydraBaseModelValue();
21491
  const baseApiKey = String(document.getElementById("set_hydra_llm_api_key")?.value || "").trim();
21492
  const hfTransformersContextTokens = String(document.getElementById("set_hydra_hf_transformers_context_tokens")?.value || "").trim();
21493
+ const hfTransformersDevice = String(document.getElementById("set_hydra_hf_transformers_device")?.value || "auto").trim();
21494
+ const hfTransformersDtype = String(document.getElementById("set_hydra_hf_transformers_dtype")?.value || "auto").trim();
21495
+ const hfTransformersDeviceMap = String(document.getElementById("set_hydra_hf_transformers_device_map")?.value || "default").trim();
21496
+ const hfTransformersAttnImplementation = String(document.getElementById("set_hydra_hf_transformers_attn_implementation")?.value || "auto").trim();
21497
+ const hfTransformersTrustRemoteCode = Boolean(document.getElementById("set_hydra_hf_transformers_trust_remote_code")?.checked);
21498
  const llamaCppContextTokens = String(document.getElementById("set_hydra_llama_cpp_context_tokens")?.value || "").trim();
21499
  const llamaCppMtpEnabled = Boolean(document.getElementById("set_hydra_llama_cpp_mtp_enabled")?.checked);
21500
  const llamaCppMtpDraftTokens = String(document.getElementById("set_hydra_llama_cpp_mtp_draft_tokens")?.value || "3").trim();
21501
+ const llamaCppNBatch = String(document.getElementById("set_hydra_llama_cpp_n_batch")?.value || "512").trim();
21502
+ const llamaCppNUbatch = String(document.getElementById("set_hydra_llama_cpp_n_ubatch")?.value || "0").trim();
21503
+ const llamaCppFlashAttn = Boolean(document.getElementById("set_hydra_llama_cpp_flash_attn")?.checked);
21504
+ const llamaCppOffloadKqv = Boolean(document.getElementById("set_hydra_llama_cpp_offload_kqv")?.checked);
21505
  const mlxLmContextTokens = String(document.getElementById("set_hydra_mlx_lm_context_tokens")?.value || "").trim();
21506
+ const mlxLmTrustRemoteCode = Boolean(document.getElementById("set_hydra_mlx_lm_trust_remote_code")?.checked);
21507
+ const mlxLmLazyLoad = Boolean(document.getElementById("set_hydra_mlx_lm_lazy_load")?.checked);
21508
+ const mlxEnginePrefillStepSize = String(document.getElementById("set_hydra_mlx_engine_prefill_step_size")?.value || "").trim();
21509
+ const mlxEngineKvBits = String(document.getElementById("set_hydra_mlx_engine_kv_bits")?.value || "").trim();
21510
+ const mlxEngineKvGroupSize = String(document.getElementById("set_hydra_mlx_engine_kv_group_size")?.value || "").trim();
21511
+ const mlxEngineQuantizedKvStart = String(document.getElementById("set_hydra_mlx_engine_quantized_kv_start")?.value || "").trim();
21512
  const additionalBaseRows = readHydraAdditionalBaseRows();
21513
  const hydraBaseServersPayload = [
21514
  normalizeHydraBaseRowInput({ provider: baseProvider, host: baseHost, port: basePort, model: baseModel, api_key: baseApiKey }),
 
21526
  hydra_llm_model: baseModel,
21527
  hydra_llm_api_key: baseApiKey,
21528
  hydra_hf_transformers_context_tokens: hfTransformersContextTokens,
21529
+ hydra_hf_transformers_device: hfTransformersDevice,
21530
+ hydra_hf_transformers_dtype: hfTransformersDtype,
21531
+ hydra_hf_transformers_device_map: hfTransformersDeviceMap,
21532
+ hydra_hf_transformers_attn_implementation: hfTransformersAttnImplementation,
21533
+ hydra_hf_transformers_trust_remote_code: hfTransformersTrustRemoteCode,
21534
  hydra_llama_cpp_context_tokens: llamaCppContextTokens,
21535
  hydra_llama_cpp_mtp_enabled: llamaCppMtpEnabled,
21536
  hydra_llama_cpp_mtp_draft_tokens: llamaCppMtpDraftTokens,
21537
+ hydra_llama_cpp_n_batch: llamaCppNBatch,
21538
+ hydra_llama_cpp_n_ubatch: llamaCppNUbatch,
21539
+ hydra_llama_cpp_flash_attn: llamaCppFlashAttn,
21540
+ hydra_llama_cpp_offload_kqv: llamaCppOffloadKqv,
21541
  hydra_mlx_lm_context_tokens: mlxLmContextTokens,
21542
+ hydra_mlx_lm_trust_remote_code: mlxLmTrustRemoteCode,
21543
+ hydra_mlx_lm_lazy_load: mlxLmLazyLoad,
21544
+ hydra_mlx_engine_prefill_step_size: mlxEnginePrefillStepSize,
21545
+ hydra_mlx_engine_kv_bits: mlxEngineKvBits,
21546
+ hydra_mlx_engine_kv_group_size: mlxEngineKvGroupSize,
21547
+ hydra_mlx_engine_quantized_kv_start: mlxEngineQuantizedKvStart,
21548
  hydra_base_servers: hydraBaseServersPayload,
21549
  },
21550
  };
 
24209
  if (state.view !== "spudex") {
24210
  clearSpudexPollTimer();
24211
  }
24212
+ if (state.view !== "settings") {
24213
+ clearLlmDebugPollTimer();
24214
+ }
24215
  document.body.dataset.view = String(viewName || "").trim().toLowerCase();
24216
  setActiveNav(viewName);
24217
  updateHeader();
 
24300
  }
24301
  clearDashboardRefreshTimer();
24302
  clearSpudexPollTimer();
24303
+ clearLlmDebugPollTimer();
24304
  closeChatEventSource();
24305
  stopAllChatJobPolling();
24306
  stopRuntimeBreakdownPolling();
tater_reachy_standalone/vendor/tater_app/tateros_static/styles.css CHANGED
@@ -2291,6 +2291,154 @@ body[data-popup-effect="melt"] .app-shell.sidebar-expanding .sidebar::after {
2291
  min-width: 0;
2292
  }
2293
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2294
  .hf-model-card,
2295
  .local-model-card,
2296
  .hf-model-browser-detail {
@@ -2310,19 +2458,52 @@ body[data-popup-effect="melt"] .app-shell.sidebar-expanding .sidebar::after {
2310
 
2311
  .hf-model-card {
2312
  cursor: pointer;
2313
- transition: border-color 0.16s ease, transform 0.16s ease, background 0.16s ease;
 
2314
  }
2315
 
2316
  .hf-model-card.is-uncertain {
2317
  border-color: rgba(255, 255, 255, 0.075);
2318
  }
2319
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2320
  .hf-model-card:hover,
2321
  .hf-model-card:focus-visible {
2322
  border-color: rgba(255, 151, 83, 0.38);
2323
  background: linear-gradient(180deg, rgba(35, 34, 34, 0.98), rgba(24, 24, 25, 0.97));
2324
  }
2325
 
 
 
 
 
 
 
 
 
 
2326
  .hf-model-card:focus-visible {
2327
  outline: 2px solid rgba(255, 151, 83, 0.42);
2328
  outline-offset: 2px;
@@ -2369,6 +2550,7 @@ body[data-popup-effect="melt"] .app-shell.sidebar-expanding .sidebar::after {
2369
  }
2370
 
2371
  .hf-model-provider-pill,
 
2372
  .hf-model-tags span {
2373
  flex: 0 0 auto;
2374
  border: 1px solid rgba(255, 151, 83, 0.24);
@@ -2383,6 +2565,14 @@ body[data-popup-effect="melt"] .app-shell.sidebar-expanding .sidebar::after {
2383
  padding: 5px 8px;
2384
  }
2385
 
 
 
 
 
 
 
 
 
2386
  .hf-model-card-meta {
2387
  flex-wrap: wrap;
2388
  color: #aaa198;
@@ -2408,6 +2598,11 @@ body[data-popup-effect="melt"] .app-shell.sidebar-expanding .sidebar::after {
2408
  font-weight: 850;
2409
  }
2410
 
 
 
 
 
 
2411
  .hf-model-tags {
2412
  display: flex;
2413
  flex-wrap: wrap;
@@ -2439,6 +2634,19 @@ body[data-popup-effect="melt"] .app-shell.sidebar-expanding .sidebar::after {
2439
  justify-content: flex-end;
2440
  }
2441
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2442
  .local-model-path {
2443
  overflow-wrap: anywhere;
2444
  border: 1px solid rgba(255, 255, 255, 0.06);
@@ -2733,6 +2941,110 @@ body[data-popup-effect="melt"] .app-shell.sidebar-expanding .sidebar::after {
2733
  font-size: 0.68rem;
2734
  }
2735
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2736
  .hf-model-browser-empty {
2737
  grid-column: 1 / -1;
2738
  padding: 12px;
@@ -2770,6 +3082,18 @@ body[data-popup-effect="melt"] .app-shell.sidebar-expanding .sidebar::after {
2770
  grid-template-columns: 1fr;
2771
  }
2772
 
 
 
 
 
 
 
 
 
 
 
 
 
2773
  .hf-download-summary {
2774
  grid-template-columns: auto minmax(0, 1fr);
2775
  }
 
2291
  min-width: 0;
2292
  }
2293
 
2294
+ .llm-debug-console-shell {
2295
+ min-width: 0;
2296
+ }
2297
+
2298
+ .llm-debug-hero {
2299
+ grid-template-columns: auto minmax(0, 1fr) auto;
2300
+ }
2301
+
2302
+ .llm-debug-actions {
2303
+ display: flex;
2304
+ flex-wrap: wrap;
2305
+ justify-content: flex-end;
2306
+ gap: 8px;
2307
+ }
2308
+
2309
+ .llm-debug-actions .inline-btn.active {
2310
+ border-color: rgba(214, 90, 31, 0.52);
2311
+ background: rgba(214, 90, 31, 0.16);
2312
+ color: var(--accent-soft);
2313
+ }
2314
+
2315
+ .llm-debug-active {
2316
+ display: grid;
2317
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
2318
+ gap: 8px;
2319
+ }
2320
+
2321
+ .llm-debug-active-empty,
2322
+ .llm-debug-empty {
2323
+ padding: 12px;
2324
+ border: 1px solid var(--line-soft);
2325
+ border-radius: 8px;
2326
+ background: rgba(255, 255, 255, 0.035);
2327
+ color: var(--text-muted);
2328
+ font-size: 0.82rem;
2329
+ }
2330
+
2331
+ .llm-debug-active-call {
2332
+ display: grid;
2333
+ gap: 3px;
2334
+ min-width: 0;
2335
+ padding: 10px;
2336
+ border: 1px solid rgba(255, 151, 83, 0.22);
2337
+ border-radius: 8px;
2338
+ background:
2339
+ linear-gradient(180deg, rgba(31, 32, 34, 0.98), rgba(20, 21, 23, 0.97)),
2340
+ linear-gradient(135deg, rgba(214, 90, 31, 0.1), rgba(92, 155, 220, 0.06));
2341
+ }
2342
+
2343
+ .llm-debug-active-call strong {
2344
+ color: #f9efe7;
2345
+ font-size: 0.86rem;
2346
+ line-height: 1.2;
2347
+ }
2348
+
2349
+ .llm-debug-active-call span,
2350
+ .llm-debug-active-call small {
2351
+ min-width: 0;
2352
+ overflow-wrap: anywhere;
2353
+ color: #b8afa5;
2354
+ font-size: 0.76rem;
2355
+ line-height: 1.35;
2356
+ }
2357
+
2358
+ .llm-debug-events {
2359
+ max-height: min(58vh, 620px);
2360
+ min-height: 360px;
2361
+ overflow: auto;
2362
+ border: 1px solid rgba(240, 131, 69, 0.2);
2363
+ border-radius: 10px;
2364
+ background:
2365
+ linear-gradient(180deg, rgba(10, 11, 13, 0.96), rgba(8, 9, 11, 0.98)),
2366
+ repeating-linear-gradient(0deg, rgba(255, 255, 255, 0.018) 0, rgba(255, 255, 255, 0.018) 1px, transparent 1px, transparent 24px);
2367
+ box-shadow: var(--shadow-soft);
2368
+ padding: 10px;
2369
+ color: #e5e9e4;
2370
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
2371
+ font-size: 0.78rem;
2372
+ line-height: 1.45;
2373
+ }
2374
+
2375
+ .llm-debug-line {
2376
+ display: grid;
2377
+ grid-template-columns: 88px 92px minmax(0, 1fr);
2378
+ gap: 10px;
2379
+ align-items: start;
2380
+ padding: 7px 0;
2381
+ border-bottom: 1px solid rgba(255, 255, 255, 0.04);
2382
+ }
2383
+
2384
+ .llm-debug-time {
2385
+ color: #8f9aa5;
2386
+ white-space: nowrap;
2387
+ }
2388
+
2389
+ .llm-debug-phase {
2390
+ color: #f0a678;
2391
+ font-weight: 900;
2392
+ text-transform: lowercase;
2393
+ }
2394
+
2395
+ .llm-debug-line.success .llm-debug-phase,
2396
+ .llm-debug-line.phase-output .llm-debug-phase {
2397
+ color: #8ddfb1;
2398
+ }
2399
+
2400
+ .llm-debug-line.warning .llm-debug-phase {
2401
+ color: #ffd08a;
2402
+ }
2403
+
2404
+ .llm-debug-line.error .llm-debug-phase {
2405
+ color: #ff9b8e;
2406
+ }
2407
+
2408
+ .llm-debug-body {
2409
+ display: grid;
2410
+ gap: 3px;
2411
+ min-width: 0;
2412
+ }
2413
+
2414
+ .llm-debug-body strong {
2415
+ color: #f5efe7;
2416
+ font-size: 0.8rem;
2417
+ line-height: 1.35;
2418
+ }
2419
+
2420
+ .llm-debug-body small {
2421
+ color: #aeb8c2;
2422
+ font-size: 0.72rem;
2423
+ overflow-wrap: anywhere;
2424
+ }
2425
+
2426
+ .llm-debug-body pre {
2427
+ margin: 3px 0 0;
2428
+ white-space: pre-wrap;
2429
+ word-break: break-word;
2430
+ color: #ccd5dc;
2431
+ font: inherit;
2432
+ }
2433
+
2434
+ .llm-debug-body .llm-debug-output {
2435
+ padding: 8px;
2436
+ border-left: 2px solid rgba(141, 223, 177, 0.55);
2437
+ border-radius: 6px;
2438
+ background: rgba(141, 223, 177, 0.07);
2439
+ color: #e7f4ec;
2440
+ }
2441
+
2442
  .hf-model-card,
2443
  .local-model-card,
2444
  .hf-model-browser-detail {
 
2458
 
2459
  .hf-model-card {
2460
  cursor: pointer;
2461
+ position: relative;
2462
+ transition: border-color 0.16s ease, transform 0.16s ease, background 0.16s ease, box-shadow 0.16s ease;
2463
  }
2464
 
2465
  .hf-model-card.is-uncertain {
2466
  border-color: rgba(255, 255, 255, 0.075);
2467
  }
2468
 
2469
+ .hf-model-card.is-tater-pick {
2470
+ border-color: rgba(255, 151, 83, 0.74);
2471
+ background:
2472
+ radial-gradient(circle at 8% 0%, rgba(255, 151, 83, 0.18), transparent 34%),
2473
+ linear-gradient(180deg, rgba(37, 33, 29, 0.99), rgba(24, 23, 23, 0.98));
2474
+ box-shadow:
2475
+ 0 0 0 1px rgba(255, 151, 83, 0.28),
2476
+ 0 0 24px rgba(255, 129, 55, 0.24),
2477
+ 0 12px 32px rgba(0, 0, 0, 0.38);
2478
+ }
2479
+
2480
+ .hf-model-card.is-tater-pick::before {
2481
+ content: "";
2482
+ position: absolute;
2483
+ inset: -2px;
2484
+ z-index: -1;
2485
+ border-radius: 10px;
2486
+ background: linear-gradient(135deg, rgba(255, 151, 83, 0.42), rgba(255, 194, 104, 0.08), rgba(255, 151, 83, 0.28));
2487
+ filter: blur(8px);
2488
+ opacity: 0.72;
2489
+ pointer-events: none;
2490
+ }
2491
+
2492
  .hf-model-card:hover,
2493
  .hf-model-card:focus-visible {
2494
  border-color: rgba(255, 151, 83, 0.38);
2495
  background: linear-gradient(180deg, rgba(35, 34, 34, 0.98), rgba(24, 24, 25, 0.97));
2496
  }
2497
 
2498
+ .hf-model-card.is-tater-pick:hover,
2499
+ .hf-model-card.is-tater-pick:focus-visible {
2500
+ border-color: rgba(255, 176, 102, 0.9);
2501
+ box-shadow:
2502
+ 0 0 0 1px rgba(255, 151, 83, 0.34),
2503
+ 0 0 32px rgba(255, 129, 55, 0.34),
2504
+ 0 16px 36px rgba(0, 0, 0, 0.42);
2505
+ }
2506
+
2507
  .hf-model-card:focus-visible {
2508
  outline: 2px solid rgba(255, 151, 83, 0.42);
2509
  outline-offset: 2px;
 
2550
  }
2551
 
2552
  .hf-model-provider-pill,
2553
+ .hf-model-tater-pill,
2554
  .hf-model-tags span {
2555
  flex: 0 0 auto;
2556
  border: 1px solid rgba(255, 151, 83, 0.24);
 
2565
  padding: 5px 8px;
2566
  }
2567
 
2568
+ .hf-model-tater-pill {
2569
+ padding: 5px 9px;
2570
+ border-color: rgba(255, 176, 102, 0.58);
2571
+ background: linear-gradient(180deg, rgba(255, 151, 83, 0.24), rgba(255, 129, 55, 0.14));
2572
+ color: #ffd2a8;
2573
+ box-shadow: 0 0 14px rgba(255, 129, 55, 0.2);
2574
+ }
2575
+
2576
  .hf-model-card-meta {
2577
  flex-wrap: wrap;
2578
  color: #aaa198;
 
2598
  font-weight: 850;
2599
  }
2600
 
2601
+ .hf-model-pick-note {
2602
+ color: #ffc086;
2603
+ font-weight: 800;
2604
+ }
2605
+
2606
  .hf-model-tags {
2607
  display: flex;
2608
  flex-wrap: wrap;
 
2634
  justify-content: flex-end;
2635
  }
2636
 
2637
+ .hydra-chat-template-tools {
2638
+ display: flex;
2639
+ grid-column: 1 / -1;
2640
+ flex-wrap: wrap;
2641
+ align-items: center;
2642
+ gap: 8px;
2643
+ min-width: 0;
2644
+ }
2645
+
2646
+ .hydra-chat-template-tools .small {
2647
+ color: #a99f95;
2648
+ }
2649
+
2650
  .local-model-path {
2651
  overflow-wrap: anywhere;
2652
  border: 1px solid rgba(255, 255, 255, 0.06);
 
2941
  font-size: 0.68rem;
2942
  }
2943
 
2944
+ .llama-chat-template-dialog {
2945
+ width: min(980px, 96vw);
2946
+ border-color: rgba(240, 131, 69, 0.18);
2947
+ background:
2948
+ linear-gradient(180deg, rgba(28, 29, 30, 0.98), rgba(18, 19, 20, 0.985)),
2949
+ radial-gradient(circle at top left, rgba(240, 131, 69, 0.1), transparent 42%);
2950
+ }
2951
+
2952
+ .llama-chat-template-body {
2953
+ display: grid;
2954
+ gap: 12px;
2955
+ }
2956
+
2957
+ .llama-chat-template-summary {
2958
+ display: grid;
2959
+ gap: 8px;
2960
+ padding: 10px;
2961
+ border: 1px solid rgba(255, 151, 83, 0.14);
2962
+ border-radius: 8px;
2963
+ background: rgba(0, 0, 0, 0.18);
2964
+ }
2965
+
2966
+ .llama-chat-template-summary-row {
2967
+ display: grid;
2968
+ gap: 3px;
2969
+ }
2970
+
2971
+ .llama-chat-template-summary-row strong {
2972
+ color: #f5e9dc;
2973
+ font-size: 0.88rem;
2974
+ line-height: 1.25;
2975
+ }
2976
+
2977
+ .llama-chat-template-summary-row span {
2978
+ overflow-wrap: anywhere;
2979
+ color: #a99f95;
2980
+ font-size: 0.74rem;
2981
+ }
2982
+
2983
+ .llama-chat-template-badges,
2984
+ .llama-chat-template-meta,
2985
+ .llama-chat-template-actions {
2986
+ display: flex;
2987
+ flex-wrap: wrap;
2988
+ align-items: center;
2989
+ gap: 7px;
2990
+ }
2991
+
2992
+ .llama-template-badge,
2993
+ .llama-chat-template-meta span {
2994
+ border: 1px solid rgba(255, 255, 255, 0.07);
2995
+ border-radius: 999px;
2996
+ background: rgba(255, 255, 255, 0.035);
2997
+ color: #a99f95;
2998
+ font-size: 0.7rem;
2999
+ font-weight: 750;
3000
+ line-height: 1.2;
3001
+ padding: 4px 8px;
3002
+ }
3003
+
3004
+ .llama-template-badge.active {
3005
+ border-color: rgba(103, 204, 138, 0.28);
3006
+ background: rgba(78, 188, 132, 0.12);
3007
+ color: #a9edc4;
3008
+ }
3009
+
3010
+ .llama-chat-template-editor {
3011
+ width: 100%;
3012
+ min-height: 360px;
3013
+ max-height: min(56vh, 640px);
3014
+ resize: vertical;
3015
+ border: 1px solid rgba(255, 255, 255, 0.08);
3016
+ border-radius: 8px;
3017
+ background: rgba(4, 5, 7, 0.5);
3018
+ color: #efe6dc;
3019
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
3020
+ font-size: 0.78rem;
3021
+ line-height: 1.45;
3022
+ padding: 11px;
3023
+ }
3024
+
3025
+ .llama-chat-template-editor:focus {
3026
+ border-color: rgba(255, 151, 83, 0.42);
3027
+ outline: none;
3028
+ box-shadow: 0 0 0 2px rgba(255, 151, 83, 0.12);
3029
+ }
3030
+
3031
+ .llama-chat-template-actions {
3032
+ justify-content: flex-end;
3033
+ }
3034
+
3035
+ .llama-chat-template-status {
3036
+ min-height: 1.2em;
3037
+ color: #a99f95;
3038
+ }
3039
+
3040
+ .llama-chat-template-status.success {
3041
+ color: #a9edc4;
3042
+ }
3043
+
3044
+ .llama-chat-template-status.error {
3045
+ color: #ffb2a8;
3046
+ }
3047
+
3048
  .hf-model-browser-empty {
3049
  grid-column: 1 / -1;
3050
  padding: 12px;
 
3082
  grid-template-columns: 1fr;
3083
  }
3084
 
3085
+ .llm-debug-actions {
3086
+ justify-content: flex-start;
3087
+ }
3088
+
3089
+ .llm-debug-line {
3090
+ grid-template-columns: 72px minmax(0, 1fr);
3091
+ }
3092
+
3093
+ .llm-debug-body {
3094
+ grid-column: 1 / -1;
3095
+ }
3096
+
3097
  .hf-download-summary {
3098
  grid-template-columns: auto minmax(0, 1fr);
3099
  }
tater_voice_sat/__init__.py CHANGED
@@ -1,4 +1,3 @@
1
  """Tater voice satellite for Reachy."""
2
 
3
- __version__ = "0.1.0"
4
-
 
1
  """Tater voice satellite for Reachy."""
2
 
3
+ __version__ = "0.1.1"
 
tater_voice_sat/sdk_app.py CHANGED
@@ -210,8 +210,8 @@ def _resolve_app_config_path(config_path: str) -> Path:
210
 
211
  runtime_dir = Path.home() / ".tater_voice_sat"
212
  runtime_config = runtime_dir / requested.name
213
- if not runtime_config.exists():
214
- _seed_runtime_assets(runtime_dir)
215
  return runtime_config.resolve()
216
 
217
 
@@ -253,5 +253,28 @@ def _merge_wake_word_entries(*entry_groups: Any) -> list[dict[str, str]]:
253
  return list(merged.values())
254
 
255
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
256
  if __name__ == "__main__":
257
  main()
 
210
 
211
  runtime_dir = Path.home() / ".tater_voice_sat"
212
  runtime_config = runtime_dir / requested.name
213
+ _seed_runtime_assets(runtime_dir)
214
+ _migrate_default_wake_word(runtime_config)
215
  return runtime_config.resolve()
216
 
217
 
 
253
  return list(merged.values())
254
 
255
 
256
+ def _migrate_default_wake_word(config_path: Path) -> None:
257
+ try:
258
+ text = config_path.read_text(encoding="utf-8")
259
+ except OSError:
260
+ return
261
+ old_markers = (
262
+ 'wake_word = "hey tater"',
263
+ 'model = "hey_tater"',
264
+ 'model_config = "./wakewords/hey_tater.json"',
265
+ )
266
+ if not all(marker in text for marker in old_markers):
267
+ return
268
+ updated = text
269
+ updated = updated.replace('wake_word = "hey tater"', 'wake_word = "hey reachy"', 1)
270
+ updated = updated.replace('model = "hey_tater"', 'model = "hey_reachy"', 1)
271
+ updated = updated.replace('model_config = "./wakewords/hey_tater.json"', 'model_config = "./wakewords/hey_reachy.json"', 1)
272
+ updated = updated.replace("threshold = 0.70", "threshold = 0.81", 1)
273
+ if updated == text:
274
+ return
275
+ config_path.write_text(updated, encoding="utf-8")
276
+ logger.info("migrated default wake word to hey_reachy")
277
+
278
+
279
  if __name__ == "__main__":
280
  main()