Release PA (v0.51.440): advanced per-model options + auxiliary model routing (#3097) (#4264)

* stage-3097: advanced model options (re-push fixes both no-ops), rebased onto master

* Release PA: advanced per-model options + auxiliary model routing (#3097)

ai-ag2026's #3097, re-pushed to fix both prior silent no-ops:
- base_url no longer clobbered (advanced applied after resolver write)
- extra_body now reaches the agent via request_overrides (+ cache-key); main
  modal hides unsupported timing fields (aux-only). Functionally verified live
  (gear→modal→save→persists). Codex SAFE + Opus SHIP + suite green.
Note: also adds 3 aux task slots (kanban_decomposer/profile_describer/triage_specifier).

Co-authored-by: ai-ag2026 <ai-ag2026@users.noreply.github.com>

---------

Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
Co-authored-by: ai-ag2026 <ai-ag2026@users.noreply.github.com>
This commit is contained in:
nesquena-hermes
2026-06-15 13:16:34 -07:00
committed by GitHub
parent 2b68fe049d
commit e47f685b89
9 changed files with 1412 additions and 24 deletions
+5 -1
View File
@@ -3,7 +3,11 @@
## [Unreleased]
## [v0.51.439] — 2026-06-15 — Release OZ (Insights dashboard polish)
## [v0.51.440] — 2026-06-15 — Release PA (advanced per-model options + auxiliary model routing)
### Added
- **Advanced model options and auxiliary model routing in Settings → Preferences.** A gear button next to the main model selector opens a "Main model options" dialog (Base URL override, Extra body JSON merged into the request, API key override — all the fields that actually take effect). An "Auxiliary Models" section lets you route side-tasks (vision, compression, web-extract, session-search, approval, MCP, title-generation, skills-hub, curator, kanban-decomposer, profile-describer, triage-specifier) to specific models/providers, each with its own advanced options (including per-task timeout / download-timeout / max-concurrency). API keys are write-only (never echoed back), `extra_body` must be a valid JSON object (malformed input is rejected with a 400), and the main model's `extra_body` is now passed through to the agent at request time via `request_overrides`. (#3097)
### Changed
+113 -3
View File
@@ -3056,7 +3056,78 @@ def set_reasoning_effort(
)
def set_hermes_default_model(model_id: str) -> dict:
def _public_advanced_model_options(model_cfg: dict) -> dict:
"""Return write-only-safe advanced options from a model config block."""
if not isinstance(model_cfg, dict):
model_cfg = {}
return {
"base_url": str(model_cfg.get("base_url") or "").strip(),
"timeout": model_cfg.get("timeout", ""),
"download_timeout": model_cfg.get("download_timeout", ""),
"max_concurrency": model_cfg.get("max_concurrency", ""),
"extra_body": model_cfg.get("extra_body") if isinstance(model_cfg.get("extra_body"), dict) else {},
"api_key_set": bool(str(model_cfg.get("api_key") or "").strip()),
}
def _main_model_request_overrides(config_data: dict) -> dict:
"""Return supported runtime request overrides for the main chat model."""
if not isinstance(config_data, dict):
return {}
model_cfg = config_data.get("model", {})
if not isinstance(model_cfg, dict):
return {}
overrides = {}
extra_body = model_cfg.get("extra_body")
if isinstance(extra_body, dict) and extra_body:
overrides["extra_body"] = copy.deepcopy(extra_body)
return overrides
def _apply_advanced_model_options(model_cfg: dict, advanced: dict | None) -> None:
"""Apply supported advanced model options to a config block in-place."""
if advanced is None:
return
if not isinstance(advanced, dict):
raise ValueError("advanced model options must be an object")
if "base_url" in advanced:
base_url = str(advanced.get("base_url") or "").strip().rstrip("/")
if base_url:
model_cfg["base_url"] = base_url
else:
model_cfg.pop("base_url", None)
for field in ("timeout", "download_timeout", "max_concurrency"):
if field in advanced:
coerced = _coerce_optional_positive_int(advanced.get(field), field)
if coerced == "":
model_cfg.pop(field, None)
elif coerced is not None:
model_cfg[field] = coerced
if "extra_body" in advanced:
extra_body = advanced.get("extra_body")
if isinstance(extra_body, str):
text = extra_body.strip()
try:
extra_body = json.loads(text) if text else {}
except json.JSONDecodeError as exc:
raise ValueError("extra_body must be valid JSON") from exc
if extra_body in (None, ""):
model_cfg.pop("extra_body", None)
elif isinstance(extra_body, dict):
if extra_body:
model_cfg["extra_body"] = extra_body
else:
model_cfg.pop("extra_body", None)
else:
raise ValueError("extra_body must be a JSON object")
if advanced.get("api_key_clear"):
model_cfg.pop("api_key", None)
api_key = str(advanced.get("api_key") or "").strip()
if api_key:
model_cfg["api_key"] = api_key
def set_hermes_default_model(model_id: str, advanced: dict | None = None) -> dict:
"""Persist the Hermes default model in config.yaml and reload runtime config."""
selected_model = str(model_id or "").strip()
if not selected_model:
@@ -3107,6 +3178,8 @@ def set_hermes_default_model(model_id: str) -> dict:
elif not persisted_provider.startswith("custom:"):
model_cfg.pop("base_url", None)
_apply_advanced_model_options(model_cfg, advanced)
config_data["model"] = model_cfg
_save_yaml_config_file(config_path, config_data)
# Reload outside the lock — reload_config() acquires _cfg_lock itself.
@@ -3133,6 +3206,9 @@ AUX_TASK_SLOTS: tuple[str, ...] = (
"mcp",
"title_generation",
"curator",
"kanban_decomposer",
"profile_describer",
"triage_specifier",
)
@@ -3169,18 +3245,46 @@ def get_auxiliary_models() -> dict:
"provider": str(entry.get("provider") or "auto").strip(),
"model": str(entry.get("model") or "").strip(),
"base_url": str(entry.get("base_url") or "").strip(),
"timeout": entry.get("timeout", ""),
"download_timeout": entry.get("download_timeout", ""),
"max_concurrency": entry.get("max_concurrency", ""),
"extra_body": entry.get("extra_body") if isinstance(entry.get("extra_body"), dict) else {},
"api_key_set": bool(str(entry.get("api_key") or "").strip()),
})
return {
"tasks": tasks,
"main": {"provider": main_provider, "model": main_model},
"main": {
"provider": main_provider,
"model": main_model,
**_public_advanced_model_options(model_cfg),
},
}
def set_auxiliary_model(task: str, provider: str, model: str) -> dict:
def _coerce_optional_positive_int(value, field: str):
if value is None:
return None
if isinstance(value, str):
value = value.strip()
if value == "":
return ""
try:
number = int(value)
except (TypeError, ValueError) as exc:
raise ValueError(f"{field} must be a positive integer") from exc
if number < 1:
raise ValueError(f"{field} must be a positive integer")
return number
def set_auxiliary_model(task: str, provider: str, model: str, advanced: dict | None = None) -> dict:
"""Persist an auxiliary model assignment in config.yaml.
Special case: task='__reset__' clears all auxiliary slots.
``advanced`` may update per-slot fields surfaced behind the WebUI gear menu.
Sensitive api_key values are write-only: get_auxiliary_models() only reports
whether one is set.
"""
if task != "__reset__" and task not in AUX_TASK_SLOTS:
raise ValueError(
@@ -3220,6 +3324,12 @@ def set_auxiliary_model(task: str, provider: str, model: str) -> dict:
slot_cfg["base_url"] = str(resolved_base_url).strip().rstrip("/")
except Exception:
pass
if advanced is not None:
try:
_apply_advanced_model_options(slot_cfg, advanced)
except ValueError as exc:
msg = str(exc).replace("advanced model options", "advanced auxiliary options")
raise ValueError(msg) from exc
aux_cfg[task] = slot_cfg
config_data["auxiliary"] = aux_cfg
+5 -3
View File
@@ -8207,7 +8207,8 @@ def handle_post(handler, parsed) -> bool:
if parsed.path == "/api/default-model":
try:
return j(handler, set_hermes_default_model(body.get("model")))
advanced = body.get("advanced") if isinstance(body, dict) else None
return j(handler, set_hermes_default_model(body.get("model"), advanced=advanced))
except ValueError as e:
return bad(handler, str(e))
except RuntimeError as e:
@@ -8219,15 +8220,16 @@ def handle_post(handler, parsed) -> bool:
task = str(body.get("task") or "").strip()
provider = str(body.get("provider") or "auto").strip()
model = str(body.get("model") or "").strip()
advanced = body.get("advanced") if isinstance(body, dict) else None
if scope == "auxiliary":
from api.config import set_auxiliary_model
try:
return j(handler, set_auxiliary_model(task, provider, model))
return j(handler, set_auxiliary_model(task, provider, model, advanced=advanced))
except Exception as exc:
return bad(handler, str(exc), status=400)
if scope == "main":
try:
return j(handler, set_hermes_default_model(model))
return j(handler, set_hermes_default_model(model, advanced=advanced))
except ValueError as exc:
return bad(handler, str(exc), status=400)
return bad(handler, f"unknown scope: {scope}", status=400)
+5
View File
@@ -38,6 +38,7 @@ from api.config import (
load_settings,
parse_reasoning_effort,
coerce_reasoning_effort_for_model,
_main_model_request_overrides,
)
from api.helpers import redact_session_data, _redact_text
from api.compression_anchor import is_context_compression_marker, visible_messages_for_anchor
@@ -6189,6 +6190,7 @@ def _run_agent_streaming(
_prefill_context = _load_webui_prefill_context(_cfg)
_prefill_messages = _prefill_messages_with_webui_context(_prefill_context, _cfg)
_prefill_messages = _normalize_prefill_messages_before_user_turn(_prefill_messages)
_main_request_overrides = _main_model_request_overrides(_cfg)
put('context_status', {
'session_id': session_id,
'prefill': _public_prefill_context_status(_prefill_context),
@@ -6370,6 +6372,8 @@ def _run_agent_streaming(
_agent_kwargs['max_iterations'] = _max_iterations_cfg
if 'max_tokens' in _agent_params and _max_tokens_cfg is not None:
_agent_kwargs['max_tokens'] = _max_tokens_cfg
if 'request_overrides' in _agent_params and _main_request_overrides:
_agent_kwargs['request_overrides'] = _main_request_overrides
# Params added in newer hermes-agent — skip if not supported
if 'api_mode' in _agent_params:
_agent_kwargs['api_mode'] = _rt.get('api_mode')
@@ -6411,6 +6415,7 @@ def _run_agent_streaming(
_fallback_resolved or {},
sorted(_toolsets) if _toolsets else [],
_reasoning_config or {},
_main_request_overrides or {},
_public_prefill_context_status(_prefill_context),
# #1897: profile_home is part of the agent's identity because
# AIAgent caches `_cached_system_prompt` from `load_soul_md()`
+678
View File
@@ -609,6 +609,11 @@ const LOCALES = {
settings_save_btn: 'Save Settings',
settings_label_model: 'Default Model',
settings_desc_model: 'Used for new conversations. Existing conversations keep their selected model.',
settings_main_advanced_button_aria: 'Advanced options for main model',
settings_main_advanced_title: 'Main model options',
settings_main_advanced_subtitle: 'Advanced config for the default chat model.',
settings_main_advanced_saved: 'Main model options saved',
settings_main_advanced_save_failed: 'Failed to save main model options',
settings_label_auxiliary_models: 'Auxiliary Models',
settings_desc_auxiliary_models: 'Side-task routing for vision, compression, title generation, etc. "Auto" uses your main chat model.',
settings_btn_reset_aux_models: 'Reset all to auto',
@@ -625,6 +630,53 @@ const LOCALES = {
settings_aux_save_failed: 'Failed to save auxiliary model',
settings_aux_saved: 'Auxiliary models updated',
settings_aux_no_changes: 'No changes to apply',
settings_aux_task_vision: 'Vision',
settings_aux_task_vision_desc: 'image/screenshot analysis',
settings_aux_task_compression: 'Compression',
settings_aux_task_compression_desc: 'context summarization',
settings_aux_task_web_extract: 'Web extract',
settings_aux_task_web_extract_desc: 'web page summarization',
settings_aux_task_session_search: 'Session search',
settings_aux_task_session_search_desc: 'past-conversation recall',
settings_aux_task_approval: 'Approval',
settings_aux_task_approval_desc: 'smart command approval',
settings_aux_task_mcp: 'MCP',
settings_aux_task_mcp_desc: 'MCP tool reasoning',
settings_aux_task_title_generation: 'Title generation',
settings_aux_task_title_generation_desc: 'session titles',
settings_aux_task_skills_hub: 'Skills hub',
settings_aux_task_skills_hub_desc: 'skills search/install',
settings_aux_task_curator: 'Curator',
settings_aux_task_curator_desc: 'skill-usage review pass',
settings_aux_task_kanban_decomposer: 'Kanban decomposer',
settings_aux_task_kanban_decomposer_desc: 'task decomposition',
settings_aux_task_profile_describer: 'Profile describer',
settings_aux_task_profile_describer_desc: 'profile summaries',
settings_aux_task_triage_specifier: 'Triage specifier',
settings_aux_task_triage_specifier_desc: 'issue/task triage specs',
settings_aux_advanced_button_title: 'Advanced options',
settings_aux_advanced_button_aria: 'Advanced options for {task}',
settings_aux_advanced_title: '{task} options',
settings_aux_advanced_subtitle: 'Advanced config for auxiliary.',
settings_aux_advanced_save: 'Save options',
settings_aux_advanced_base_url: 'Base URL',
settings_aux_advanced_base_url_desc: 'Optional provider endpoint override.',
settings_aux_advanced_timeout: 'Timeout seconds',
settings_aux_advanced_timeout_desc: 'Request timeout for this auxiliary task. Blank uses Hermes default.',
settings_aux_advanced_download_timeout: 'Download timeout seconds',
settings_aux_advanced_download_timeout_desc: 'Only relevant for tasks that download media/content, e.g. vision. Blank uses default.',
settings_aux_advanced_max_concurrency: 'Max concurrency',
settings_aux_advanced_max_concurrency_desc: 'Optional per-task concurrency limit. Blank uses default.',
settings_aux_advanced_extra_body: 'Extra body JSON',
settings_aux_advanced_extra_body_desc: 'Optional JSON object merged into the model request body.',
settings_aux_advanced_api_key: 'API key override',
settings_aux_advanced_api_key_set_hint: 'API key is set. Leave blank to keep it, or use clear to remove it.',
settings_aux_advanced_api_key_empty_hint: 'Leave blank to use provider/default credentials.',
settings_aux_advanced_api_key_clear: 'Clear existing API key override',
settings_aux_advanced_extra_body_invalid_json: 'Extra body must be valid JSON',
settings_aux_advanced_extra_body_object_required: 'Extra body must be a JSON object',
settings_aux_advanced_saved: 'Auxiliary options saved',
settings_aux_advanced_save_failed: 'Failed to save auxiliary options',
settings_label_send_key: 'Send Key',
settings_label_theme: 'Theme',
settings_label_skin: 'Skin',
@@ -2035,6 +2087,11 @@ const LOCALES = {
settings_save_btn: 'Salva Impostazioni',
settings_label_model: 'Modello Predefinito',
settings_desc_model: 'Usato per le nuove conversazioni. Le conversazioni esistenti mantengono il modello selezionato.',
settings_main_advanced_button_aria: 'Advanced options for main model',
settings_main_advanced_title: 'Main model options',
settings_main_advanced_subtitle: 'Advanced config for the default chat model.',
settings_main_advanced_saved: 'Main model options saved',
settings_main_advanced_save_failed: 'Failed to save main model options',
settings_label_auxiliary_models: 'Modelli Ausiliari',
settings_desc_auxiliary_models: 'Routing per attività secondarie come visione, compressione, generazione titoli, ecc. "Auto" utilizza il modello chat principale.',
settings_btn_reset_aux_models: 'Ripristina tutto ad auto',
@@ -2051,6 +2108,53 @@ const LOCALES = {
settings_aux_save_failed: 'Salvataggio del modello ausiliario non riuscito',
settings_aux_saved: 'Modelli ausiliari aggiornati',
settings_aux_no_changes: 'Nessuna modifica da applicare',
settings_aux_task_vision: 'Vision',
settings_aux_task_vision_desc: 'image/screenshot analysis',
settings_aux_task_compression: 'Compression',
settings_aux_task_compression_desc: 'context summarization',
settings_aux_task_web_extract: 'Web extract',
settings_aux_task_web_extract_desc: 'web page summarization',
settings_aux_task_session_search: 'Session search',
settings_aux_task_session_search_desc: 'past-conversation recall',
settings_aux_task_approval: 'Approval',
settings_aux_task_approval_desc: 'smart command approval',
settings_aux_task_mcp: 'MCP',
settings_aux_task_mcp_desc: 'MCP tool reasoning',
settings_aux_task_title_generation: 'Title generation',
settings_aux_task_title_generation_desc: 'session titles',
settings_aux_task_skills_hub: 'Skills hub',
settings_aux_task_skills_hub_desc: 'skills search/install',
settings_aux_task_curator: 'Curator',
settings_aux_task_curator_desc: 'skill-usage review pass',
settings_aux_task_kanban_decomposer: 'Kanban decomposer',
settings_aux_task_kanban_decomposer_desc: 'task decomposition',
settings_aux_task_profile_describer: 'Profile describer',
settings_aux_task_profile_describer_desc: 'profile summaries',
settings_aux_task_triage_specifier: 'Triage specifier',
settings_aux_task_triage_specifier_desc: 'issue/task triage specs',
settings_aux_advanced_button_title: 'Advanced options',
settings_aux_advanced_button_aria: 'Advanced options for {task}',
settings_aux_advanced_title: '{task} options',
settings_aux_advanced_subtitle: 'Advanced config for auxiliary.',
settings_aux_advanced_save: 'Save options',
settings_aux_advanced_base_url: 'Base URL',
settings_aux_advanced_base_url_desc: 'Optional provider endpoint override.',
settings_aux_advanced_timeout: 'Timeout seconds',
settings_aux_advanced_timeout_desc: 'Request timeout for this auxiliary task. Blank uses Hermes default.',
settings_aux_advanced_download_timeout: 'Download timeout seconds',
settings_aux_advanced_download_timeout_desc: 'Only relevant for tasks that download media/content, e.g. vision. Blank uses default.',
settings_aux_advanced_max_concurrency: 'Max concurrency',
settings_aux_advanced_max_concurrency_desc: 'Optional per-task concurrency limit. Blank uses default.',
settings_aux_advanced_extra_body: 'Extra body JSON',
settings_aux_advanced_extra_body_desc: 'Optional JSON object merged into the model request body.',
settings_aux_advanced_api_key: 'API key override',
settings_aux_advanced_api_key_set_hint: 'API key is set. Leave blank to keep it, or use clear to remove it.',
settings_aux_advanced_api_key_empty_hint: 'Leave blank to use provider/default credentials.',
settings_aux_advanced_api_key_clear: 'Clear existing API key override',
settings_aux_advanced_extra_body_invalid_json: 'Extra body must be valid JSON',
settings_aux_advanced_extra_body_object_required: 'Extra body must be a JSON object',
settings_aux_advanced_saved: 'Auxiliary options saved',
settings_aux_advanced_save_failed: 'Failed to save auxiliary options',
settings_label_send_key: 'Tasto Invio',
settings_label_theme: 'Tema',
settings_label_skin: 'Skin',
@@ -3452,6 +3556,11 @@ const LOCALES = {
settings_save_btn: '設定を保存',
settings_label_model: 'デフォルトモデル',
settings_desc_model: '新しい会話で使用されます。既存の会話は選択済みモデルを保持します。',
settings_main_advanced_button_aria: 'Advanced options for main model',
settings_main_advanced_title: 'Main model options',
settings_main_advanced_subtitle: 'Advanced config for the default chat model.',
settings_main_advanced_saved: 'Main model options saved',
settings_main_advanced_save_failed: 'Failed to save main model options',
settings_label_auxiliary_models: '補助モデル',
settings_desc_auxiliary_models: 'ビジョン、圧縮、タイトル生成などの補助タスクのルーティング。「自動」はメインチャットモデルを使用します。',
settings_btn_reset_aux_models: 'すべて自動にリセット',
@@ -3468,6 +3577,53 @@ const LOCALES = {
settings_aux_save_failed: '補助モデルの保存に失敗しました',
settings_aux_saved: '補助モデルを更新しました',
settings_aux_no_changes: '適用する変更はありません',
settings_aux_task_vision: 'Vision',
settings_aux_task_vision_desc: 'image/screenshot analysis',
settings_aux_task_compression: 'Compression',
settings_aux_task_compression_desc: 'context summarization',
settings_aux_task_web_extract: 'Web extract',
settings_aux_task_web_extract_desc: 'web page summarization',
settings_aux_task_session_search: 'Session search',
settings_aux_task_session_search_desc: 'past-conversation recall',
settings_aux_task_approval: 'Approval',
settings_aux_task_approval_desc: 'smart command approval',
settings_aux_task_mcp: 'MCP',
settings_aux_task_mcp_desc: 'MCP tool reasoning',
settings_aux_task_title_generation: 'Title generation',
settings_aux_task_title_generation_desc: 'session titles',
settings_aux_task_skills_hub: 'Skills hub',
settings_aux_task_skills_hub_desc: 'skills search/install',
settings_aux_task_curator: 'Curator',
settings_aux_task_curator_desc: 'skill-usage review pass',
settings_aux_task_kanban_decomposer: 'Kanban decomposer',
settings_aux_task_kanban_decomposer_desc: 'task decomposition',
settings_aux_task_profile_describer: 'Profile describer',
settings_aux_task_profile_describer_desc: 'profile summaries',
settings_aux_task_triage_specifier: 'Triage specifier',
settings_aux_task_triage_specifier_desc: 'issue/task triage specs',
settings_aux_advanced_button_title: 'Advanced options',
settings_aux_advanced_button_aria: 'Advanced options for {task}',
settings_aux_advanced_title: '{task} options',
settings_aux_advanced_subtitle: 'Advanced config for auxiliary.',
settings_aux_advanced_save: 'Save options',
settings_aux_advanced_base_url: 'Base URL',
settings_aux_advanced_base_url_desc: 'Optional provider endpoint override.',
settings_aux_advanced_timeout: 'Timeout seconds',
settings_aux_advanced_timeout_desc: 'Request timeout for this auxiliary task. Blank uses Hermes default.',
settings_aux_advanced_download_timeout: 'Download timeout seconds',
settings_aux_advanced_download_timeout_desc: 'Only relevant for tasks that download media/content, e.g. vision. Blank uses default.',
settings_aux_advanced_max_concurrency: 'Max concurrency',
settings_aux_advanced_max_concurrency_desc: 'Optional per-task concurrency limit. Blank uses default.',
settings_aux_advanced_extra_body: 'Extra body JSON',
settings_aux_advanced_extra_body_desc: 'Optional JSON object merged into the model request body.',
settings_aux_advanced_api_key: 'API key override',
settings_aux_advanced_api_key_set_hint: 'API key is set. Leave blank to keep it, or use clear to remove it.',
settings_aux_advanced_api_key_empty_hint: 'Leave blank to use provider/default credentials.',
settings_aux_advanced_api_key_clear: 'Clear existing API key override',
settings_aux_advanced_extra_body_invalid_json: 'Extra body must be valid JSON',
settings_aux_advanced_extra_body_object_required: 'Extra body must be a JSON object',
settings_aux_advanced_saved: 'Auxiliary options saved',
settings_aux_advanced_save_failed: 'Failed to save auxiliary options',
settings_label_send_key: '送信キー',
settings_label_theme: 'テーマ',
settings_label_skin: 'スキン',
@@ -4661,6 +4817,11 @@ const LOCALES = {
settings_save_btn: 'Сохранить настройки',
settings_label_model: 'Модель по умолчанию',
settings_desc_model: 'Используется для новых бесед. Существующие беседы сохраняют выбранную модель.',
settings_main_advanced_button_aria: 'Advanced options for main model',
settings_main_advanced_title: 'Main model options',
settings_main_advanced_subtitle: 'Advanced config for the default chat model.',
settings_main_advanced_saved: 'Main model options saved',
settings_main_advanced_save_failed: 'Failed to save main model options',
settings_label_auxiliary_models: 'Вспомогательные модели',
settings_desc_auxiliary_models: 'Маршрутизация побочных задач: зрение, сжатие, генерация заголовков и т.д. «Авто» использует основную модель чата.',
settings_btn_reset_aux_models: 'Сбросить всё на авто',
@@ -4677,6 +4838,53 @@ const LOCALES = {
settings_aux_save_failed: 'Не удалось сохранить вспомогательную модель',
settings_aux_saved: 'Вспомогательные модели обновлены',
settings_aux_no_changes: 'Нет изменений для применения',
settings_aux_task_vision: 'Vision',
settings_aux_task_vision_desc: 'image/screenshot analysis',
settings_aux_task_compression: 'Compression',
settings_aux_task_compression_desc: 'context summarization',
settings_aux_task_web_extract: 'Web extract',
settings_aux_task_web_extract_desc: 'web page summarization',
settings_aux_task_session_search: 'Session search',
settings_aux_task_session_search_desc: 'past-conversation recall',
settings_aux_task_approval: 'Approval',
settings_aux_task_approval_desc: 'smart command approval',
settings_aux_task_mcp: 'MCP',
settings_aux_task_mcp_desc: 'MCP tool reasoning',
settings_aux_task_title_generation: 'Title generation',
settings_aux_task_title_generation_desc: 'session titles',
settings_aux_task_skills_hub: 'Skills hub',
settings_aux_task_skills_hub_desc: 'skills search/install',
settings_aux_task_curator: 'Curator',
settings_aux_task_curator_desc: 'skill-usage review pass',
settings_aux_task_kanban_decomposer: 'Kanban decomposer',
settings_aux_task_kanban_decomposer_desc: 'task decomposition',
settings_aux_task_profile_describer: 'Profile describer',
settings_aux_task_profile_describer_desc: 'profile summaries',
settings_aux_task_triage_specifier: 'Triage specifier',
settings_aux_task_triage_specifier_desc: 'issue/task triage specs',
settings_aux_advanced_button_title: 'Advanced options',
settings_aux_advanced_button_aria: 'Advanced options for {task}',
settings_aux_advanced_title: '{task} options',
settings_aux_advanced_subtitle: 'Advanced config for auxiliary.',
settings_aux_advanced_save: 'Save options',
settings_aux_advanced_base_url: 'Base URL',
settings_aux_advanced_base_url_desc: 'Optional provider endpoint override.',
settings_aux_advanced_timeout: 'Timeout seconds',
settings_aux_advanced_timeout_desc: 'Request timeout for this auxiliary task. Blank uses Hermes default.',
settings_aux_advanced_download_timeout: 'Download timeout seconds',
settings_aux_advanced_download_timeout_desc: 'Only relevant for tasks that download media/content, e.g. vision. Blank uses default.',
settings_aux_advanced_max_concurrency: 'Max concurrency',
settings_aux_advanced_max_concurrency_desc: 'Optional per-task concurrency limit. Blank uses default.',
settings_aux_advanced_extra_body: 'Extra body JSON',
settings_aux_advanced_extra_body_desc: 'Optional JSON object merged into the model request body.',
settings_aux_advanced_api_key: 'API key override',
settings_aux_advanced_api_key_set_hint: 'API key is set. Leave blank to keep it, or use clear to remove it.',
settings_aux_advanced_api_key_empty_hint: 'Leave blank to use provider/default credentials.',
settings_aux_advanced_api_key_clear: 'Clear existing API key override',
settings_aux_advanced_extra_body_invalid_json: 'Extra body must be valid JSON',
settings_aux_advanced_extra_body_object_required: 'Extra body must be a JSON object',
settings_aux_advanced_saved: 'Auxiliary options saved',
settings_aux_advanced_save_failed: 'Failed to save auxiliary options',
settings_label_send_key: 'Клавиша отправки',
settings_label_theme: 'Тема',
settings_label_language: 'Язык',
@@ -5995,6 +6203,11 @@ const LOCALES = {
settings_save_btn: 'Guardar configuración',
settings_label_model: 'Modelo predeterminado',
settings_desc_model: 'Se usa para conversaciones nuevas. Las conversaciones existentes conservan su modelo seleccionado.',
settings_main_advanced_button_aria: 'Advanced options for main model',
settings_main_advanced_title: 'Main model options',
settings_main_advanced_subtitle: 'Advanced config for the default chat model.',
settings_main_advanced_saved: 'Main model options saved',
settings_main_advanced_save_failed: 'Failed to save main model options',
settings_label_auxiliary_models: 'Modelos auxiliares',
settings_desc_auxiliary_models: 'Enrutamiento para tareas secundarias como visión, compresión, generación de títulos, etc. «Auto» usa el modelo de chat principal.',
settings_btn_reset_aux_models: 'Restablecer todo a auto',
@@ -6011,6 +6224,53 @@ const LOCALES = {
settings_aux_save_failed: 'Error al guardar el modelo auxiliar',
settings_aux_saved: 'Modelos auxiliares actualizados',
settings_aux_no_changes: 'Sin cambios para aplicar',
settings_aux_task_vision: 'Vision',
settings_aux_task_vision_desc: 'image/screenshot analysis',
settings_aux_task_compression: 'Compression',
settings_aux_task_compression_desc: 'context summarization',
settings_aux_task_web_extract: 'Web extract',
settings_aux_task_web_extract_desc: 'web page summarization',
settings_aux_task_session_search: 'Session search',
settings_aux_task_session_search_desc: 'past-conversation recall',
settings_aux_task_approval: 'Approval',
settings_aux_task_approval_desc: 'smart command approval',
settings_aux_task_mcp: 'MCP',
settings_aux_task_mcp_desc: 'MCP tool reasoning',
settings_aux_task_title_generation: 'Title generation',
settings_aux_task_title_generation_desc: 'session titles',
settings_aux_task_skills_hub: 'Skills hub',
settings_aux_task_skills_hub_desc: 'skills search/install',
settings_aux_task_curator: 'Curator',
settings_aux_task_curator_desc: 'skill-usage review pass',
settings_aux_task_kanban_decomposer: 'Kanban decomposer',
settings_aux_task_kanban_decomposer_desc: 'task decomposition',
settings_aux_task_profile_describer: 'Profile describer',
settings_aux_task_profile_describer_desc: 'profile summaries',
settings_aux_task_triage_specifier: 'Triage specifier',
settings_aux_task_triage_specifier_desc: 'issue/task triage specs',
settings_aux_advanced_button_title: 'Advanced options',
settings_aux_advanced_button_aria: 'Advanced options for {task}',
settings_aux_advanced_title: '{task} options',
settings_aux_advanced_subtitle: 'Advanced config for auxiliary.',
settings_aux_advanced_save: 'Save options',
settings_aux_advanced_base_url: 'Base URL',
settings_aux_advanced_base_url_desc: 'Optional provider endpoint override.',
settings_aux_advanced_timeout: 'Timeout seconds',
settings_aux_advanced_timeout_desc: 'Request timeout for this auxiliary task. Blank uses Hermes default.',
settings_aux_advanced_download_timeout: 'Download timeout seconds',
settings_aux_advanced_download_timeout_desc: 'Only relevant for tasks that download media/content, e.g. vision. Blank uses default.',
settings_aux_advanced_max_concurrency: 'Max concurrency',
settings_aux_advanced_max_concurrency_desc: 'Optional per-task concurrency limit. Blank uses default.',
settings_aux_advanced_extra_body: 'Extra body JSON',
settings_aux_advanced_extra_body_desc: 'Optional JSON object merged into the model request body.',
settings_aux_advanced_api_key: 'API key override',
settings_aux_advanced_api_key_set_hint: 'API key is set. Leave blank to keep it, or use clear to remove it.',
settings_aux_advanced_api_key_empty_hint: 'Leave blank to use provider/default credentials.',
settings_aux_advanced_api_key_clear: 'Clear existing API key override',
settings_aux_advanced_extra_body_invalid_json: 'Extra body must be valid JSON',
settings_aux_advanced_extra_body_object_required: 'Extra body must be a JSON object',
settings_aux_advanced_saved: 'Auxiliary options saved',
settings_aux_advanced_save_failed: 'Failed to save auxiliary options',
settings_label_send_key: 'Tecla de envío',
settings_label_theme: 'Tema',
settings_label_skin: 'Piel',
@@ -7333,6 +7593,11 @@ const LOCALES = {
settings_save_btn: 'Einstellungen speichern',
settings_label_model: 'Standard-Modell',
settings_desc_model: 'Wird für neue Chats verwendet. Bestehende Chats behalten ihr ausgewähltes Modell.',
settings_main_advanced_button_aria: 'Advanced options for main model',
settings_main_advanced_title: 'Main model options',
settings_main_advanced_subtitle: 'Advanced config for the default chat model.',
settings_main_advanced_saved: 'Main model options saved',
settings_main_advanced_save_failed: 'Failed to save main model options',
settings_label_auxiliary_models: 'Hilfsmodelle',
settings_desc_auxiliary_models: 'Routing für Nebenaufgaben wie Vision, Komprimierung, Titelgenerierung usw. „Auto" verwendet das Haupt-Chat-Modell.',
settings_btn_reset_aux_models: 'Alle auf auto zurücksetzen',
@@ -7349,6 +7614,53 @@ const LOCALES = {
settings_aux_save_failed: 'Hilfsmodell konnte nicht gespeichert werden',
settings_aux_saved: 'Hilfsmodelle aktualisiert',
settings_aux_no_changes: 'Keine Änderungen anzuwenden',
settings_aux_task_vision: 'Vision',
settings_aux_task_vision_desc: 'Bild-/Screenshot-Analyse',
settings_aux_task_compression: 'Komprimierung',
settings_aux_task_compression_desc: 'Kontext-Zusammenfassung',
settings_aux_task_web_extract: 'Web-Extraktion',
settings_aux_task_web_extract_desc: 'Webseiten-Zusammenfassung',
settings_aux_task_session_search: 'Sitzungssuche',
settings_aux_task_session_search_desc: 'Erinnerung an frühere Gespräche',
settings_aux_task_approval: 'Freigabe',
settings_aux_task_approval_desc: 'intelligente Befehlsfreigabe',
settings_aux_task_mcp: 'MCP',
settings_aux_task_mcp_desc: 'MCP-Tool-Reasoning',
settings_aux_task_title_generation: 'Titelgenerierung',
settings_aux_task_title_generation_desc: 'Sitzungstitel',
settings_aux_task_skills_hub: 'Skills Hub',
settings_aux_task_skills_hub_desc: 'Skill-Suche/-Installation',
settings_aux_task_curator: 'Kurator',
settings_aux_task_curator_desc: 'Skill-Nutzungsprüfung',
settings_aux_task_kanban_decomposer: 'Kanban-Zerleger',
settings_aux_task_kanban_decomposer_desc: 'Aufgabenzerlegung',
settings_aux_task_profile_describer: 'Profilbeschreiber',
settings_aux_task_profile_describer_desc: 'Profil-Zusammenfassungen',
settings_aux_task_triage_specifier: 'Triage-Spezifizierer',
settings_aux_task_triage_specifier_desc: 'Issue-/Aufgaben-Triage-Spezifikationen',
settings_aux_advanced_button_title: 'Erweiterte Optionen',
settings_aux_advanced_button_aria: 'Erweiterte Optionen für {task}',
settings_aux_advanced_title: '{task}-Optionen',
settings_aux_advanced_subtitle: 'Erweiterte Konfiguration für diese Hilfsaufgabe.',
settings_aux_advanced_save: 'Optionen speichern',
settings_aux_advanced_base_url: 'Basis-URL',
settings_aux_advanced_base_url_desc: 'Optionaler Provider-Endpunkt-Override.',
settings_aux_advanced_timeout: 'Timeout in Sekunden',
settings_aux_advanced_timeout_desc: 'Request-Timeout für diese Hilfsaufgabe. Leer nutzt den Hermes-Standard.',
settings_aux_advanced_download_timeout: 'Download-Timeout in Sekunden',
settings_aux_advanced_download_timeout_desc: 'Nur relevant für Aufgaben, die Medien/Inhalte herunterladen, z. B. Vision. Leer nutzt den Standard.',
settings_aux_advanced_max_concurrency: 'Maximale Parallelität',
settings_aux_advanced_max_concurrency_desc: 'Optionale Parallelitätsgrenze pro Aufgabe. Leer nutzt den Standard.',
settings_aux_advanced_extra_body: 'Extra-Body JSON',
settings_aux_advanced_extra_body_desc: 'Optionales JSON-Objekt, das in den Model-Request-Body gemischt wird.',
settings_aux_advanced_api_key: 'API-Key-Override',
settings_aux_advanced_api_key_set_hint: 'API-Key ist gesetzt. Leer lassen, um ihn zu behalten, oder Löschen aktivieren.',
settings_aux_advanced_api_key_empty_hint: 'Leer lassen, um Provider-/Standard-Zugangsdaten zu verwenden.',
settings_aux_advanced_api_key_clear: 'Vorhandenen API-Key-Override löschen',
settings_aux_advanced_extra_body_invalid_json: 'Extra-Body muss gültiges JSON sein',
settings_aux_advanced_extra_body_object_required: 'Extra-Body muss ein JSON-Objekt sein',
settings_aux_advanced_saved: 'Hilfsoptionen gespeichert',
settings_aux_advanced_save_failed: 'Hilfsoptionen konnten nicht gespeichert werden',
settings_label_send_key: 'Sende-Taste',
settings_label_theme: 'Theme',
settings_label_skin: 'Skin',
@@ -8724,6 +9036,11 @@ const LOCALES = {
settings_save_btn: '保存设置',
settings_label_model: '默认模型',
settings_desc_model: '用于新对话。现有对话保持各自选定的模型。',
settings_main_advanced_button_aria: 'Advanced options for main model',
settings_main_advanced_title: 'Main model options',
settings_main_advanced_subtitle: 'Advanced config for the default chat model.',
settings_main_advanced_saved: 'Main model options saved',
settings_main_advanced_save_failed: 'Failed to save main model options',
settings_label_auxiliary_models: '辅助模型',
settings_desc_auxiliary_models: '视觉分析、上下文压缩、标题生成等辅助任务的路由。"自动"表示使用主聊天模型。',
settings_btn_reset_aux_models: '全部重置为自动',
@@ -8740,6 +9057,53 @@ const LOCALES = {
settings_aux_save_failed: '辅助模型保存失败',
settings_aux_saved: '辅助模型已更新',
settings_aux_no_changes: '没有需要应用的更改',
settings_aux_task_vision: 'Vision',
settings_aux_task_vision_desc: 'image/screenshot analysis',
settings_aux_task_compression: 'Compression',
settings_aux_task_compression_desc: 'context summarization',
settings_aux_task_web_extract: 'Web extract',
settings_aux_task_web_extract_desc: 'web page summarization',
settings_aux_task_session_search: 'Session search',
settings_aux_task_session_search_desc: 'past-conversation recall',
settings_aux_task_approval: 'Approval',
settings_aux_task_approval_desc: 'smart command approval',
settings_aux_task_mcp: 'MCP',
settings_aux_task_mcp_desc: 'MCP tool reasoning',
settings_aux_task_title_generation: 'Title generation',
settings_aux_task_title_generation_desc: 'session titles',
settings_aux_task_skills_hub: 'Skills hub',
settings_aux_task_skills_hub_desc: 'skills search/install',
settings_aux_task_curator: 'Curator',
settings_aux_task_curator_desc: 'skill-usage review pass',
settings_aux_task_kanban_decomposer: 'Kanban decomposer',
settings_aux_task_kanban_decomposer_desc: 'task decomposition',
settings_aux_task_profile_describer: 'Profile describer',
settings_aux_task_profile_describer_desc: 'profile summaries',
settings_aux_task_triage_specifier: 'Triage specifier',
settings_aux_task_triage_specifier_desc: 'issue/task triage specs',
settings_aux_advanced_button_title: 'Advanced options',
settings_aux_advanced_button_aria: 'Advanced options for {task}',
settings_aux_advanced_title: '{task} options',
settings_aux_advanced_subtitle: 'Advanced config for auxiliary.',
settings_aux_advanced_save: 'Save options',
settings_aux_advanced_base_url: 'Base URL',
settings_aux_advanced_base_url_desc: 'Optional provider endpoint override.',
settings_aux_advanced_timeout: 'Timeout seconds',
settings_aux_advanced_timeout_desc: 'Request timeout for this auxiliary task. Blank uses Hermes default.',
settings_aux_advanced_download_timeout: 'Download timeout seconds',
settings_aux_advanced_download_timeout_desc: 'Only relevant for tasks that download media/content, e.g. vision. Blank uses default.',
settings_aux_advanced_max_concurrency: 'Max concurrency',
settings_aux_advanced_max_concurrency_desc: 'Optional per-task concurrency limit. Blank uses default.',
settings_aux_advanced_extra_body: 'Extra body JSON',
settings_aux_advanced_extra_body_desc: 'Optional JSON object merged into the model request body.',
settings_aux_advanced_api_key: 'API key override',
settings_aux_advanced_api_key_set_hint: 'API key is set. Leave blank to keep it, or use clear to remove it.',
settings_aux_advanced_api_key_empty_hint: 'Leave blank to use provider/default credentials.',
settings_aux_advanced_api_key_clear: 'Clear existing API key override',
settings_aux_advanced_extra_body_invalid_json: 'Extra body must be valid JSON',
settings_aux_advanced_extra_body_object_required: 'Extra body must be a JSON object',
settings_aux_advanced_saved: 'Auxiliary options saved',
settings_aux_advanced_save_failed: 'Failed to save auxiliary options',
settings_label_send_key: '发送快捷键',
settings_label_theme: '主题',
settings_label_skin: '皮肤',
@@ -10286,6 +10650,11 @@ const LOCALES = {
settings_save_btn: '儲存設定',
settings_label_model: '預設模型',
settings_desc_model: '用於新對話。現有對話會保留各自選定的模型。',
settings_main_advanced_button_aria: 'Advanced options for main model',
settings_main_advanced_title: 'Main model options',
settings_main_advanced_subtitle: 'Advanced config for the default chat model.',
settings_main_advanced_saved: 'Main model options saved',
settings_main_advanced_save_failed: 'Failed to save main model options',
settings_label_auxiliary_models: '輔助模型',
settings_desc_auxiliary_models: '視覺分析、脈絡壓縮、標題產生等輔助任務的路由。「自動」表示使用主要聊天模型。',
settings_btn_reset_aux_models: '全部重置為自動',
@@ -10302,8 +10671,57 @@ const LOCALES = {
settings_aux_save_failed: '輔助模型儲存失敗',
settings_aux_saved: '輔助模型已更新',
settings_aux_no_changes: '沒有需要應用的變更',
settings_aux_task_vision: 'Vision',
settings_aux_task_vision_desc: 'image/screenshot analysis',
settings_aux_task_compression: 'Compression',
settings_aux_task_compression_desc: 'context summarization',
settings_aux_task_web_extract: 'Web extract',
settings_aux_task_web_extract_desc: 'web page summarization',
settings_aux_task_session_search: 'Session search',
settings_aux_task_session_search_desc: 'past-conversation recall',
settings_aux_task_approval: 'Approval',
settings_aux_task_approval_desc: 'smart command approval',
settings_aux_task_mcp: 'MCP',
settings_aux_task_mcp_desc: 'MCP tool reasoning',
settings_aux_task_title_generation: 'Title generation',
settings_aux_task_title_generation_desc: 'session titles',
settings_aux_task_skills_hub: 'Skills hub',
settings_aux_task_skills_hub_desc: 'skills search/install',
settings_aux_task_curator: 'Curator',
settings_aux_task_curator_desc: 'skill-usage review pass',
settings_aux_task_kanban_decomposer: 'Kanban decomposer',
settings_aux_task_kanban_decomposer_desc: 'task decomposition',
settings_aux_task_profile_describer: 'Profile describer',
settings_aux_task_profile_describer_desc: 'profile summaries',
settings_aux_task_triage_specifier: 'Triage specifier',
settings_aux_task_triage_specifier_desc: 'issue/task triage specs',
settings_aux_advanced_button_title: 'Advanced options',
settings_aux_advanced_button_aria: 'Advanced options for {task}',
settings_aux_advanced_title: '{task} options',
settings_aux_advanced_subtitle: 'Advanced config for auxiliary.',
settings_aux_advanced_save: 'Save options',
settings_aux_advanced_base_url: 'Base URL',
settings_aux_advanced_base_url_desc: 'Optional provider endpoint override.',
settings_aux_advanced_timeout: 'Timeout seconds',
settings_aux_advanced_timeout_desc: 'Request timeout for this auxiliary task. Blank uses Hermes default.',
settings_aux_advanced_download_timeout: 'Download timeout seconds',
settings_aux_advanced_download_timeout_desc: 'Only relevant for tasks that download media/content, e.g. vision. Blank uses default.',
settings_aux_advanced_max_concurrency: 'Max concurrency',
settings_aux_advanced_max_concurrency_desc: 'Optional per-task concurrency limit. Blank uses default.',
settings_aux_advanced_extra_body: 'Extra body JSON',
settings_aux_advanced_extra_body_desc: 'Optional JSON object merged into the model request body.',
settings_aux_advanced_api_key: 'API key override',
settings_aux_advanced_api_key_set_hint: 'API key is set. Leave blank to keep it, or use clear to remove it.',
settings_aux_advanced_api_key_empty_hint: 'Leave blank to use provider/default credentials.',
settings_aux_advanced_api_key_clear: 'Clear existing API key override',
settings_aux_advanced_extra_body_invalid_json: 'Extra body must be valid JSON',
settings_aux_advanced_extra_body_object_required: 'Extra body must be a JSON object',
settings_aux_advanced_saved: 'Auxiliary options saved',
settings_aux_advanced_save_failed: 'Failed to save auxiliary options',
settings_label_send_key: '送出按鍵',
settings_label_theme: '主題',
settings_label_skin: '佈景',
settings_label_font_size: '字型大小',
font_size_small: '小',
@@ -11593,6 +12011,11 @@ const LOCALES = {
settings_save_btn: 'Salvar Configurações',
settings_label_model: 'Modelo Padrão',
settings_desc_model: 'Usado para novas conversas. Conversas existentes mantêm o modelo selecionado.',
settings_main_advanced_button_aria: 'Advanced options for main model',
settings_main_advanced_title: 'Main model options',
settings_main_advanced_subtitle: 'Advanced config for the default chat model.',
settings_main_advanced_saved: 'Main model options saved',
settings_main_advanced_save_failed: 'Failed to save main model options',
settings_label_auxiliary_models: 'Modelos auxiliares',
settings_desc_auxiliary_models: 'Roteamento para tarefas secundárias como visão, compressão, geração de títulos, etc. "Auto" usa o modelo de chat principal.',
settings_btn_reset_aux_models: 'Restaurar tudo para auto',
@@ -11609,6 +12032,53 @@ const LOCALES = {
settings_aux_save_failed: 'Falha ao salvar o modelo auxiliar',
settings_aux_saved: 'Modelos auxiliares atualizados',
settings_aux_no_changes: 'Nenhuma alteração para aplicar',
settings_aux_task_vision: 'Vision',
settings_aux_task_vision_desc: 'image/screenshot analysis',
settings_aux_task_compression: 'Compression',
settings_aux_task_compression_desc: 'context summarization',
settings_aux_task_web_extract: 'Web extract',
settings_aux_task_web_extract_desc: 'web page summarization',
settings_aux_task_session_search: 'Session search',
settings_aux_task_session_search_desc: 'past-conversation recall',
settings_aux_task_approval: 'Approval',
settings_aux_task_approval_desc: 'smart command approval',
settings_aux_task_mcp: 'MCP',
settings_aux_task_mcp_desc: 'MCP tool reasoning',
settings_aux_task_title_generation: 'Title generation',
settings_aux_task_title_generation_desc: 'session titles',
settings_aux_task_skills_hub: 'Skills hub',
settings_aux_task_skills_hub_desc: 'skills search/install',
settings_aux_task_curator: 'Curator',
settings_aux_task_curator_desc: 'skill-usage review pass',
settings_aux_task_kanban_decomposer: 'Kanban decomposer',
settings_aux_task_kanban_decomposer_desc: 'task decomposition',
settings_aux_task_profile_describer: 'Profile describer',
settings_aux_task_profile_describer_desc: 'profile summaries',
settings_aux_task_triage_specifier: 'Triage specifier',
settings_aux_task_triage_specifier_desc: 'issue/task triage specs',
settings_aux_advanced_button_title: 'Advanced options',
settings_aux_advanced_button_aria: 'Advanced options for {task}',
settings_aux_advanced_title: '{task} options',
settings_aux_advanced_subtitle: 'Advanced config for auxiliary.',
settings_aux_advanced_save: 'Save options',
settings_aux_advanced_base_url: 'Base URL',
settings_aux_advanced_base_url_desc: 'Optional provider endpoint override.',
settings_aux_advanced_timeout: 'Timeout seconds',
settings_aux_advanced_timeout_desc: 'Request timeout for this auxiliary task. Blank uses Hermes default.',
settings_aux_advanced_download_timeout: 'Download timeout seconds',
settings_aux_advanced_download_timeout_desc: 'Only relevant for tasks that download media/content, e.g. vision. Blank uses default.',
settings_aux_advanced_max_concurrency: 'Max concurrency',
settings_aux_advanced_max_concurrency_desc: 'Optional per-task concurrency limit. Blank uses default.',
settings_aux_advanced_extra_body: 'Extra body JSON',
settings_aux_advanced_extra_body_desc: 'Optional JSON object merged into the model request body.',
settings_aux_advanced_api_key: 'API key override',
settings_aux_advanced_api_key_set_hint: 'API key is set. Leave blank to keep it, or use clear to remove it.',
settings_aux_advanced_api_key_empty_hint: 'Leave blank to use provider/default credentials.',
settings_aux_advanced_api_key_clear: 'Clear existing API key override',
settings_aux_advanced_extra_body_invalid_json: 'Extra body must be valid JSON',
settings_aux_advanced_extra_body_object_required: 'Extra body must be a JSON object',
settings_aux_advanced_saved: 'Auxiliary options saved',
settings_aux_advanced_save_failed: 'Failed to save auxiliary options',
settings_label_send_key: 'Tecla de Envio',
settings_label_theme: 'Tema',
settings_label_skin: 'Skin',
@@ -12911,6 +13381,11 @@ const LOCALES = {
settings_save_btn: '설정 저장',
settings_label_model: '기본 모델',
settings_desc_model: '새 대화에 사용됩니다. 기존 대화는 선택된 모델을 유지합니다.',
settings_main_advanced_button_aria: 'Advanced options for main model',
settings_main_advanced_title: 'Main model options',
settings_main_advanced_subtitle: 'Advanced config for the default chat model.',
settings_main_advanced_saved: 'Main model options saved',
settings_main_advanced_save_failed: 'Failed to save main model options',
settings_label_auxiliary_models: '보조 모델',
settings_desc_auxiliary_models: '비전, 압축, 제목 생성 등 보조 작업 라우팅. "자동"은 기본 채팅 모델을 사용합니다.',
settings_btn_reset_aux_models: '모두 자동으로 재설정',
@@ -12927,6 +13402,53 @@ const LOCALES = {
settings_aux_save_failed: '보조 모델 저장 실패',
settings_aux_saved: '보조 모델 업데이트됨',
settings_aux_no_changes: '적용할 변경 사항 없음',
settings_aux_task_vision: 'Vision',
settings_aux_task_vision_desc: 'image/screenshot analysis',
settings_aux_task_compression: 'Compression',
settings_aux_task_compression_desc: 'context summarization',
settings_aux_task_web_extract: 'Web extract',
settings_aux_task_web_extract_desc: 'web page summarization',
settings_aux_task_session_search: 'Session search',
settings_aux_task_session_search_desc: 'past-conversation recall',
settings_aux_task_approval: 'Approval',
settings_aux_task_approval_desc: 'smart command approval',
settings_aux_task_mcp: 'MCP',
settings_aux_task_mcp_desc: 'MCP tool reasoning',
settings_aux_task_title_generation: 'Title generation',
settings_aux_task_title_generation_desc: 'session titles',
settings_aux_task_skills_hub: 'Skills hub',
settings_aux_task_skills_hub_desc: 'skills search/install',
settings_aux_task_curator: 'Curator',
settings_aux_task_curator_desc: 'skill-usage review pass',
settings_aux_task_kanban_decomposer: 'Kanban decomposer',
settings_aux_task_kanban_decomposer_desc: 'task decomposition',
settings_aux_task_profile_describer: 'Profile describer',
settings_aux_task_profile_describer_desc: 'profile summaries',
settings_aux_task_triage_specifier: 'Triage specifier',
settings_aux_task_triage_specifier_desc: 'issue/task triage specs',
settings_aux_advanced_button_title: 'Advanced options',
settings_aux_advanced_button_aria: 'Advanced options for {task}',
settings_aux_advanced_title: '{task} options',
settings_aux_advanced_subtitle: 'Advanced config for auxiliary.',
settings_aux_advanced_save: 'Save options',
settings_aux_advanced_base_url: 'Base URL',
settings_aux_advanced_base_url_desc: 'Optional provider endpoint override.',
settings_aux_advanced_timeout: 'Timeout seconds',
settings_aux_advanced_timeout_desc: 'Request timeout for this auxiliary task. Blank uses Hermes default.',
settings_aux_advanced_download_timeout: 'Download timeout seconds',
settings_aux_advanced_download_timeout_desc: 'Only relevant for tasks that download media/content, e.g. vision. Blank uses default.',
settings_aux_advanced_max_concurrency: 'Max concurrency',
settings_aux_advanced_max_concurrency_desc: 'Optional per-task concurrency limit. Blank uses default.',
settings_aux_advanced_extra_body: 'Extra body JSON',
settings_aux_advanced_extra_body_desc: 'Optional JSON object merged into the model request body.',
settings_aux_advanced_api_key: 'API key override',
settings_aux_advanced_api_key_set_hint: 'API key is set. Leave blank to keep it, or use clear to remove it.',
settings_aux_advanced_api_key_empty_hint: 'Leave blank to use provider/default credentials.',
settings_aux_advanced_api_key_clear: 'Clear existing API key override',
settings_aux_advanced_extra_body_invalid_json: 'Extra body must be valid JSON',
settings_aux_advanced_extra_body_object_required: 'Extra body must be a JSON object',
settings_aux_advanced_saved: 'Auxiliary options saved',
settings_aux_advanced_save_failed: 'Failed to save auxiliary options',
settings_label_send_key: '전송 키',
settings_label_theme: '테마',
settings_label_skin: '스킨',
@@ -14246,6 +14768,11 @@ const LOCALES = {
settings_save_btn: 'Enregistrer les paramètres',
settings_label_model: 'Modèle par défaut',
settings_desc_model: 'Utilisé pour les nouvelles conversations. Les conversations existantes conservent leur modèle sélectionné.',
settings_main_advanced_button_aria: 'Advanced options for main model',
settings_main_advanced_title: 'Main model options',
settings_main_advanced_subtitle: 'Advanced config for the default chat model.',
settings_main_advanced_saved: 'Main model options saved',
settings_main_advanced_save_failed: 'Failed to save main model options',
settings_label_auxiliary_models: 'Modèles auxiliaires',
settings_desc_auxiliary_models: 'Routage des tâches secondaires : vision, compression, génération de titres, etc. « Auto » utilise le modèle de chat principal.',
settings_btn_reset_aux_models: 'Tout réinitialiser à auto',
@@ -14262,6 +14789,53 @@ const LOCALES = {
settings_aux_save_failed: 'Échec de la sauvegarde du modèle auxiliaire',
settings_aux_saved: 'Modèles auxiliaires mis à jour',
settings_aux_no_changes: 'Aucune modification à appliquer',
settings_aux_task_vision: 'Vision',
settings_aux_task_vision_desc: 'image/screenshot analysis',
settings_aux_task_compression: 'Compression',
settings_aux_task_compression_desc: 'context summarization',
settings_aux_task_web_extract: 'Web extract',
settings_aux_task_web_extract_desc: 'web page summarization',
settings_aux_task_session_search: 'Session search',
settings_aux_task_session_search_desc: 'past-conversation recall',
settings_aux_task_approval: 'Approval',
settings_aux_task_approval_desc: 'smart command approval',
settings_aux_task_mcp: 'MCP',
settings_aux_task_mcp_desc: 'MCP tool reasoning',
settings_aux_task_title_generation: 'Title generation',
settings_aux_task_title_generation_desc: 'session titles',
settings_aux_task_skills_hub: 'Skills hub',
settings_aux_task_skills_hub_desc: 'skills search/install',
settings_aux_task_curator: 'Curator',
settings_aux_task_curator_desc: 'skill-usage review pass',
settings_aux_task_kanban_decomposer: 'Kanban decomposer',
settings_aux_task_kanban_decomposer_desc: 'task decomposition',
settings_aux_task_profile_describer: 'Profile describer',
settings_aux_task_profile_describer_desc: 'profile summaries',
settings_aux_task_triage_specifier: 'Triage specifier',
settings_aux_task_triage_specifier_desc: 'issue/task triage specs',
settings_aux_advanced_button_title: 'Advanced options',
settings_aux_advanced_button_aria: 'Advanced options for {task}',
settings_aux_advanced_title: '{task} options',
settings_aux_advanced_subtitle: 'Advanced config for auxiliary.',
settings_aux_advanced_save: 'Save options',
settings_aux_advanced_base_url: 'Base URL',
settings_aux_advanced_base_url_desc: 'Optional provider endpoint override.',
settings_aux_advanced_timeout: 'Timeout seconds',
settings_aux_advanced_timeout_desc: 'Request timeout for this auxiliary task. Blank uses Hermes default.',
settings_aux_advanced_download_timeout: 'Download timeout seconds',
settings_aux_advanced_download_timeout_desc: 'Only relevant for tasks that download media/content, e.g. vision. Blank uses default.',
settings_aux_advanced_max_concurrency: 'Max concurrency',
settings_aux_advanced_max_concurrency_desc: 'Optional per-task concurrency limit. Blank uses default.',
settings_aux_advanced_extra_body: 'Extra body JSON',
settings_aux_advanced_extra_body_desc: 'Optional JSON object merged into the model request body.',
settings_aux_advanced_api_key: 'API key override',
settings_aux_advanced_api_key_set_hint: 'API key is set. Leave blank to keep it, or use clear to remove it.',
settings_aux_advanced_api_key_empty_hint: 'Leave blank to use provider/default credentials.',
settings_aux_advanced_api_key_clear: 'Clear existing API key override',
settings_aux_advanced_extra_body_invalid_json: 'Extra body must be valid JSON',
settings_aux_advanced_extra_body_object_required: 'Extra body must be a JSON object',
settings_aux_advanced_saved: 'Auxiliary options saved',
settings_aux_advanced_save_failed: 'Failed to save auxiliary options',
settings_label_send_key: 'Envoyer la clé',
settings_label_theme: 'Thème',
settings_label_skin: 'Peau',
@@ -15690,6 +16264,53 @@ const LOCALES = {
settings_aux_model_custom: 'Özel model…',
settings_aux_model_custom_prompt: 'Model kimliğini girin:',
settings_aux_no_changes: 'Uygulanacak değişiklik yok',
settings_aux_task_vision: 'Vision',
settings_aux_task_vision_desc: 'image/screenshot analysis',
settings_aux_task_compression: 'Compression',
settings_aux_task_compression_desc: 'context summarization',
settings_aux_task_web_extract: 'Web extract',
settings_aux_task_web_extract_desc: 'web page summarization',
settings_aux_task_session_search: 'Session search',
settings_aux_task_session_search_desc: 'past-conversation recall',
settings_aux_task_approval: 'Approval',
settings_aux_task_approval_desc: 'smart command approval',
settings_aux_task_mcp: 'MCP',
settings_aux_task_mcp_desc: 'MCP tool reasoning',
settings_aux_task_title_generation: 'Title generation',
settings_aux_task_title_generation_desc: 'session titles',
settings_aux_task_skills_hub: 'Skills hub',
settings_aux_task_skills_hub_desc: 'skills search/install',
settings_aux_task_curator: 'Curator',
settings_aux_task_curator_desc: 'skill-usage review pass',
settings_aux_task_kanban_decomposer: 'Kanban decomposer',
settings_aux_task_kanban_decomposer_desc: 'task decomposition',
settings_aux_task_profile_describer: 'Profile describer',
settings_aux_task_profile_describer_desc: 'profile summaries',
settings_aux_task_triage_specifier: 'Triage specifier',
settings_aux_task_triage_specifier_desc: 'issue/task triage specs',
settings_aux_advanced_button_title: 'Advanced options',
settings_aux_advanced_button_aria: 'Advanced options for {task}',
settings_aux_advanced_title: '{task} options',
settings_aux_advanced_subtitle: 'Advanced config for auxiliary.',
settings_aux_advanced_save: 'Save options',
settings_aux_advanced_base_url: 'Base URL',
settings_aux_advanced_base_url_desc: 'Optional provider endpoint override.',
settings_aux_advanced_timeout: 'Timeout seconds',
settings_aux_advanced_timeout_desc: 'Request timeout for this auxiliary task. Blank uses Hermes default.',
settings_aux_advanced_download_timeout: 'Download timeout seconds',
settings_aux_advanced_download_timeout_desc: 'Only relevant for tasks that download media/content, e.g. vision. Blank uses default.',
settings_aux_advanced_max_concurrency: 'Max concurrency',
settings_aux_advanced_max_concurrency_desc: 'Optional per-task concurrency limit. Blank uses default.',
settings_aux_advanced_extra_body: 'Extra body JSON',
settings_aux_advanced_extra_body_desc: 'Optional JSON object merged into the model request body.',
settings_aux_advanced_api_key: 'API key override',
settings_aux_advanced_api_key_set_hint: 'API key is set. Leave blank to keep it, or use clear to remove it.',
settings_aux_advanced_api_key_empty_hint: 'Leave blank to use provider/default credentials.',
settings_aux_advanced_api_key_clear: 'Clear existing API key override',
settings_aux_advanced_extra_body_invalid_json: 'Extra body must be valid JSON',
settings_aux_advanced_extra_body_object_required: 'Extra body must be a JSON object',
settings_aux_advanced_saved: 'Auxiliary options saved',
settings_aux_advanced_save_failed: 'Failed to save auxiliary options',
settings_aux_saved: 'Yardımcı modeller güncellendi',
settings_aux_save_failed: 'Yardımcı model kaydedilemedi',
settings_aux_reset_confirm_title: 'Yardımcı modeller sıfırlansın mı?',
@@ -15698,6 +16319,11 @@ const LOCALES = {
settings_btn_apply_aux_models: 'Değişiklikleri uygula',
settings_btn_reset_aux_models: 'Tümünü otomatiğe sıfırla',
settings_desc_model: 'Yeni konuşmalar için kullanılır. Mevcut konuşmalar seçilen modellerini korur.',
settings_main_advanced_button_aria: 'Advanced options for main model',
settings_main_advanced_title: 'Main model options',
settings_main_advanced_subtitle: 'Advanced config for the default chat model.',
settings_main_advanced_saved: 'Main model options saved',
settings_main_advanced_save_failed: 'Failed to save main model options',
settings_label_send_key: 'Anahtar Gönder',
settings_label_theme: 'Tema',
settings_label_skin: 'Deri',
@@ -17128,6 +17754,58 @@ const LOCALES = {
settings_aux_save_failed: 'Nie udało się zapisać modelu pomocniczego',
settings_aux_saved: 'Zaktualizowano modele pomocnicze',
settings_aux_no_changes: 'Brak zmian do zastosowania',
settings_aux_task_vision: 'Vision',
settings_aux_task_vision_desc: 'image/screenshot analysis',
settings_aux_task_compression: 'Compression',
settings_aux_task_compression_desc: 'context summarization',
settings_aux_task_web_extract: 'Web extract',
settings_aux_task_web_extract_desc: 'web page summarization',
settings_aux_task_session_search: 'Session search',
settings_aux_task_session_search_desc: 'past-conversation recall',
settings_aux_task_approval: 'Approval',
settings_aux_task_approval_desc: 'smart command approval',
settings_aux_task_mcp: 'MCP',
settings_aux_task_mcp_desc: 'MCP tool reasoning',
settings_aux_task_title_generation: 'Title generation',
settings_aux_task_title_generation_desc: 'session titles',
settings_aux_task_skills_hub: 'Skills hub',
settings_aux_task_skills_hub_desc: 'skills search/install',
settings_aux_task_curator: 'Curator',
settings_aux_task_curator_desc: 'skill-usage review pass',
settings_aux_task_kanban_decomposer: 'Kanban decomposer',
settings_aux_task_kanban_decomposer_desc: 'task decomposition',
settings_aux_task_profile_describer: 'Profile describer',
settings_aux_task_profile_describer_desc: 'profile summaries',
settings_aux_task_triage_specifier: 'Triage specifier',
settings_aux_task_triage_specifier_desc: 'issue/task triage specs',
settings_main_advanced_button_aria: 'Advanced options for main model',
settings_main_advanced_title: 'Main model options',
settings_main_advanced_subtitle: 'Advanced config for the default chat model.',
settings_main_advanced_saved: 'Main model options saved',
settings_main_advanced_save_failed: 'Failed to save main model options',
settings_aux_advanced_button_title: 'Advanced options',
settings_aux_advanced_button_aria: 'Advanced options for {task}',
settings_aux_advanced_title: '{task} options',
settings_aux_advanced_subtitle: 'Advanced config for auxiliary.',
settings_aux_advanced_save: 'Save options',
settings_aux_advanced_base_url: 'Base URL',
settings_aux_advanced_base_url_desc: 'Optional provider endpoint override.',
settings_aux_advanced_timeout: 'Timeout seconds',
settings_aux_advanced_timeout_desc: 'Request timeout for this auxiliary task. Blank uses Hermes default.',
settings_aux_advanced_download_timeout: 'Download timeout seconds',
settings_aux_advanced_download_timeout_desc: 'Only relevant for tasks that download media/content, e.g. vision. Blank uses default.',
settings_aux_advanced_max_concurrency: 'Max concurrency',
settings_aux_advanced_max_concurrency_desc: 'Optional per-task concurrency limit. Blank uses default.',
settings_aux_advanced_extra_body: 'Extra body JSON',
settings_aux_advanced_extra_body_desc: 'Optional JSON object merged into the model request body.',
settings_aux_advanced_api_key: 'API key override',
settings_aux_advanced_api_key_set_hint: 'API key is set. Leave blank to keep it, or use clear to remove it.',
settings_aux_advanced_api_key_empty_hint: 'Leave blank to use provider/default credentials.',
settings_aux_advanced_api_key_clear: 'Clear existing API key override',
settings_aux_advanced_extra_body_invalid_json: 'Extra body must be valid JSON',
settings_aux_advanced_extra_body_object_required: 'Extra body must be a JSON object',
settings_aux_advanced_saved: 'Auxiliary options saved',
settings_aux_advanced_save_failed: 'Failed to save auxiliary options',
settings_label_send_key: 'Klawisz wysyłania',
settings_label_theme: 'Motyw',
settings_label_skin: 'Skórka',
+4 -1
View File
@@ -1056,7 +1056,10 @@
</div>
<div class="settings-field">
<label for="settingsModel" data-i18n="settings_label_model">Default Model</label>
<select id="settingsModel" style="width:100%;padding:8px;background:var(--code-bg);color:var(--text);border:1px solid var(--border2);border-radius:6px"></select>
<div class="model-advanced-row">
<select id="settingsModel" style="width:100%;padding:8px;background:var(--code-bg);color:var(--text);border:1px solid var(--border2);border-radius:6px"></select>
<button type="button" id="mainAdvancedBtn" class="model-advanced-btn" title="Advanced options" aria-label="Advanced options for main model"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg></button>
</div>
<div style="font-size:11px;color:var(--muted);margin-top:4px" data-i18n="settings_desc_model">Used for new conversations. Existing conversations keep their selected model.</div>
</div>
<div class="settings-field">
+183 -13
View File
@@ -6706,6 +6706,7 @@ async function loadSettingsPanel(){
modelSel.addEventListener('change',_markSettingsDirty,{once:false});
}
// Auxiliary models — load task assignments and provider/model options
_bindMainAdvancedOptionsButton();
_loadAuxiliaryModels();
// Send key preference
const sendKeySel=$('settingsSendKey');
@@ -8009,19 +8010,23 @@ async function checkUpdatesNow(){
// Canonical auxiliary task slots with display names.
// Keep in sync with hermes_cli/main.py _AUX_TASKS and hermes_cli/web_server.py _AUX_TASK_SLOTS.
const _AUX_TASK_SLOTS=[
{key:'vision',name:'Vision',desc:'image/screenshot analysis'},
{key:'compression',name:'Compression',desc:'context summarization'},
{key:'web_extract',name:'Web extract',desc:'web page summarization'},
{key:'session_search',name:'Session search',desc:'past-conversation recall'},
{key:'approval',name:'Approval',desc:'smart command approval'},
{key:'mcp',name:'MCP',desc:'MCP tool reasoning'},
{key:'title_generation',name:'Title generation',desc:'session titles'},
{key:'skills_hub',name:'Skills hub',desc:'skills search/install'},
{key:'curator',name:'Curator',desc:'skill-usage review pass'},
{key:'vision',nameKey:'settings_aux_task_vision',descKey:'settings_aux_task_vision_desc'},
{key:'compression',nameKey:'settings_aux_task_compression',descKey:'settings_aux_task_compression_desc'},
{key:'web_extract',nameKey:'settings_aux_task_web_extract',descKey:'settings_aux_task_web_extract_desc'},
{key:'session_search',nameKey:'settings_aux_task_session_search',descKey:'settings_aux_task_session_search_desc'},
{key:'approval',nameKey:'settings_aux_task_approval',descKey:'settings_aux_task_approval_desc'},
{key:'mcp',nameKey:'settings_aux_task_mcp',descKey:'settings_aux_task_mcp_desc'},
{key:'title_generation',nameKey:'settings_aux_task_title_generation',descKey:'settings_aux_task_title_generation_desc'},
{key:'skills_hub',nameKey:'settings_aux_task_skills_hub',descKey:'settings_aux_task_skills_hub_desc'},
{key:'curator',nameKey:'settings_aux_task_curator',descKey:'settings_aux_task_curator_desc'},
{key:'kanban_decomposer',nameKey:'settings_aux_task_kanban_decomposer',descKey:'settings_aux_task_kanban_decomposer_desc'},
{key:'profile_describer',nameKey:'settings_aux_task_profile_describer',descKey:'settings_aux_task_profile_describer_desc'},
{key:'triage_specifier',nameKey:'settings_aux_task_triage_specifier',descKey:'settings_aux_task_triage_specifier_desc'},
];
let _auxProviders=[]; // cached provider list from /api/model/options
let _auxProviders=[]; // cached provider list from /api/models
let _auxOriginalConfig=null; // snapshot of initial config for dirty detection
let _mainAdvancedConfig=null; // current advanced config for the default chat model
function _auxSelectStyle(){
return 'width:100%;padding:6px 8px;background:var(--code-bg);color:var(--text);border:1px solid var(--border2);border-radius:6px;font-size:12px;box-sizing:border-box';
@@ -8109,6 +8114,154 @@ function _markAuxDirty(){
_markSettingsDirty();
}
function _auxAdvancedValue(cfg,key){
const v=cfg&&Object.prototype.hasOwnProperty.call(cfg,key)?cfg[key]:'';
return v===null||v===undefined?'':String(v);
}
function _ensureAuxAdvancedModal(){
let overlay=$('auxAdvancedOverlay');
if(overlay) return overlay;
overlay=document.createElement('div');
overlay.id='auxAdvancedOverlay';
overlay.style.cssText='position:fixed;inset:0;z-index:9999;background:rgba(5,7,15,.68);backdrop-filter:blur(4px);display:none;align-items:center;justify-content:center;padding:20px';
const neutralBtn='font-size:12px;padding:7px 12px;border-radius:8px;border:1px solid var(--border);background:var(--surface);color:var(--text);cursor:pointer;font-weight:600';
const primaryBtn='font-size:12px;padding:7px 12px;border-radius:8px;border:1px solid var(--accent);background:var(--accent);color:#1a1a1a;cursor:pointer;font-weight:700';
overlay.innerHTML=`<div role="dialog" aria-modal="true" aria-labelledby="auxAdvancedTitle" style="width:min(620px,calc(100vw - 32px));max-height:calc(100vh - 48px);overflow:auto;background:var(--surface);color:var(--text);border:1px solid var(--border);border-radius:14px;box-shadow:0 18px 60px rgba(0,0,0,.45);padding:16px">
<style>#auxAdvancedOverlay input:-webkit-autofill,#auxAdvancedOverlay textarea:-webkit-autofill{box-shadow:0 0 0 1000px var(--code-bg) inset!important;-webkit-box-shadow:0 0 0 1000px var(--code-bg) inset!important;-webkit-text-fill-color:var(--text)!important;caret-color:var(--text)!important}</style>
<div style="display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:12px">
<div><div id="auxAdvancedTitle" style="font-weight:700;font-size:16px"></div><div id="auxAdvancedSubtitle" style="font-size:11px;color:var(--muted);margin-top:2px"></div></div>
<button type="button" id="auxAdvancedClose" aria-label="${esc(t('terminal_close')||'Close')}" style="width:28px;height:28px;display:inline-flex;align-items:center;justify-content:center;border-radius:8px;border:1px solid var(--border);background:var(--input-bg);color:var(--text);cursor:pointer;font-size:18px;line-height:1">×</button>
</div>
<div id="auxAdvancedBody" style="display:grid;gap:10px"></div>
<div style="display:flex;justify-content:flex-end;gap:8px;margin-top:14px">
<button type="button" id="auxAdvancedCancel" style="${neutralBtn}">${esc(t('cancel')||'Cancel')}</button>
<button type="button" id="auxAdvancedSave" style="${primaryBtn}">${esc(t('settings_aux_advanced_save')||'Save options')}</button>
</div>
</div>`;
document.body.appendChild(overlay);
const close=()=>{overlay.style.display='none';overlay.dataset.task='';};
$('auxAdvancedClose')?.addEventListener('click',close);
$('auxAdvancedCancel')?.addEventListener('click',close);
overlay.addEventListener('click',ev=>{if(ev.target===overlay) close();});
return overlay;
}
function _auxAdvancedInputHtml(id,label,value,desc,type='text',extraAttrs='',extraStyle=''){
const fieldName=id==='auxAdvancedApiKey'?'aux-manual-override-value':('aux-field-'+id.replace(/^auxAdvanced/,'').toLowerCase());
const autocompleteAttr=/\bautocomplete=/.test(extraAttrs)?'':'autocomplete="off"';
const inputAttrs=`id="${id}" name="${fieldName}" type="${type}" value="${esc(value)}" ${autocompleteAttr} autocapitalize="off" autocorrect="off" spellcheck="false" data-lpignore="true" data-1p-ignore="true" ${extraAttrs}`;
return `<label style="display:grid;gap:4px;font-size:12px;color:var(--text)"><span style="font-weight:600">${esc(label)}</span><input ${inputAttrs} style="width:100%;box-sizing:border-box;padding:7px 8px;background:var(--code-bg);color:var(--text);border:1px solid var(--border2);border-radius:6px;font-size:12px${extraStyle}"><span style="font-size:10px;color:var(--muted);line-height:1.35">${esc(desc)}</span></label>`;
}
function _openAuxAdvancedOptions(taskKey,cfg){
const isMain=taskKey==='__main__';
const slot=isMain?{key:taskKey,nameKey:'settings_label_model',descKey:'settings_desc_model'}:(_AUX_TASK_SLOTS.find(s=>s.key===taskKey)||{key:taskKey,nameKey:'',descKey:''});
const overlay=_ensureAuxAdvancedModal();
overlay.dataset.task=taskKey;
const title=$('auxAdvancedTitle'),sub=$('auxAdvancedSubtitle'),body=$('auxAdvancedBody');
const slotName=t(slot.nameKey)||slot.key;
if(title) title.textContent=isMain?(t('settings_main_advanced_title')||'Main model options'):((t('settings_aux_advanced_title')||'{task} options').replace('{task}',slotName));
if(sub) sub.textContent=isMain?(t('settings_main_advanced_subtitle')||'Advanced config for the default chat model.'):(t('settings_aux_advanced_subtitle')||'Advanced config for auxiliary.');
const extraBody=cfg&&cfg.extra_body&&typeof cfg.extra_body==='object'&&Object.keys(cfg.extra_body).length?JSON.stringify(cfg.extra_body,null,2):'';
const apiKeyHint=cfg&&cfg.api_key_set?(t('settings_aux_advanced_api_key_set_hint')||'API key is set. Leave blank to keep it, or use clear to remove it.'):(t('settings_aux_advanced_api_key_empty_hint')||'Leave blank to use provider/default credentials.');
if(body){
const timingFields=isMain?'':(
_auxAdvancedInputHtml('auxAdvancedTimeout',t('settings_aux_advanced_timeout')||'Timeout seconds',_auxAdvancedValue(cfg,'timeout'),t('settings_aux_advanced_timeout_desc')||'Request timeout for this auxiliary task. Blank uses Hermes default.','number','inputmode="numeric" min="1" step="1"')+
_auxAdvancedInputHtml('auxAdvancedDownloadTimeout',t('settings_aux_advanced_download_timeout')||'Download timeout seconds',_auxAdvancedValue(cfg,'download_timeout'),t('settings_aux_advanced_download_timeout_desc')||'Only relevant for tasks that download media/content, e.g. vision. Blank uses default.','number','inputmode="numeric" min="1" step="1"')+
_auxAdvancedInputHtml('auxAdvancedMaxConcurrency',t('settings_aux_advanced_max_concurrency')||'Max concurrency',_auxAdvancedValue(cfg,'max_concurrency'),t('settings_aux_advanced_max_concurrency_desc')||'Optional per-task concurrency limit. Blank uses default.','number','inputmode="numeric" min="1" step="1"'));
body.innerHTML=
_auxAdvancedInputHtml('auxAdvancedBaseUrl',t('settings_aux_advanced_base_url')||'Base URL',_auxAdvancedValue(cfg,'base_url'),t('settings_aux_advanced_base_url_desc')||'Optional provider endpoint override.','text','inputmode="url"')+
timingFields+
`<label style="display:grid;gap:4px;font-size:12px;color:var(--text)"><span style="font-weight:600">${esc(t('settings_aux_advanced_extra_body')||'Extra body JSON')}</span><textarea id="auxAdvancedExtraBody" rows="6" style="width:100%;box-sizing:border-box;padding:7px 8px;background:var(--code-bg);color:var(--text);border:1px solid var(--border2);border-radius:6px;font-size:12px;font-family:var(--mono,monospace)">${esc(extraBody)}</textarea><span style="font-size:10px;color:var(--muted);line-height:1.35">${esc(t('settings_aux_advanced_extra_body_desc')||'Optional JSON object merged into the model request body.')}</span></label>`+
_auxAdvancedInputHtml('auxAdvancedApiKey',t('settings_aux_advanced_api_key')||'API key override','',apiKeyHint,'text','autocomplete="one-time-code" inputmode="text" readonly onfocus="this.removeAttribute(&quot;readonly&quot;)"',';-webkit-text-security:disc')+
`<label style="display:${cfg&&cfg.api_key_set?'flex':'none'};align-items:center;gap:8px;font-size:12px;color:var(--text)"><input id="auxAdvancedApiKeyClear" type="checkbox" style="width:15px;height:15px;accent-color:var(--accent)"><span>${esc(t('settings_aux_advanced_api_key_clear')||'Clear existing API key override')}</span></label>`;
}
const save=$('auxAdvancedSave');
if(save){
save.onclick=async()=>{
let extra={};
const extraText=($('auxAdvancedExtraBody')?.value||'').trim();
if(extraText){
try{extra=JSON.parse(extraText);}catch(e){if(typeof showToast==='function') showToast(t('settings_aux_advanced_extra_body_invalid_json')||'Extra body must be valid JSON');return;}
if(!extra||Array.isArray(extra)||typeof extra!=='object'){if(typeof showToast==='function') showToast(t('settings_aux_advanced_extra_body_object_required')||'Extra body must be a JSON object');return;}
}
const provSel=isMain?null:$('aux-prov-'+taskKey),modelSel=isMain?$('settingsModel'):$('aux-model-'+taskKey);
const provider=isMain?((cfg&&cfg.provider)||''):(provSel?provSel.value:((cfg&&cfg.provider)||'auto'));
const model=modelSel&&modelSel.value!=='__custom__'?(modelSel.value||''):((cfg&&cfg.model)||'');
const advanced={
base_url:$('auxAdvancedBaseUrl')?.value||'',
extra_body:extra,
api_key:$('auxAdvancedApiKey')?.value||'',
api_key_clear:!!($('auxAdvancedApiKeyClear')&&$('auxAdvancedApiKeyClear').checked),
};
if(!isMain){
advanced.timeout=$('auxAdvancedTimeout')?.value||'';
advanced.download_timeout=$('auxAdvancedDownloadTimeout')?.value||'';
advanced.max_concurrency=$('auxAdvancedMaxConcurrency')?.value||'';
}
try{
await api('/api/model/set',{method:'POST',body:JSON.stringify({scope:isMain?'main':'auxiliary',task:isMain?'':taskKey,provider,model,advanced})});
if(typeof showToast==='function') showToast(isMain?(t('settings_main_advanced_saved')||'Main model options saved'):(t('settings_aux_advanced_saved')||'Auxiliary options saved'));
overlay.style.display='none';
_loadAuxiliaryModels();
}catch(e){
if(typeof showToast==='function') showToast(isMain?(t('settings_main_advanced_save_failed')||'Failed to save main model options'):(t('settings_aux_advanced_save_failed')||'Failed to save auxiliary options'));
}
};
}
overlay.style.display='flex';
setTimeout(()=>$('auxAdvancedBaseUrl')?.focus(),0);
}
function _bindMainAdvancedOptionsButton(){
const modelSel=$('settingsModel');
let btn=$('mainAdvancedBtn');
if(modelSel){
const parent=modelSel.parentElement;
let row=parent&&parent.classList&&parent.classList.contains('model-advanced-row')?parent:null;
if(!row){
row=document.createElement('div');
row.className='model-advanced-row';
parent.insertBefore(row,modelSel);
row.appendChild(modelSel);
}
if(!btn){
btn=document.createElement('button');
btn.type='button';
btn.id='mainAdvancedBtn';
}
if(btn.parentElement!==row) row.appendChild(btn);
row.style.cssText='display:grid;grid-template-columns:minmax(0,1fr) 34px;gap:8px;align-items:center';
modelSel.style.width='100%';
modelSel.style.minWidth='0';
modelSel.style.boxSizing='border-box';
}
if(!btn) return;
btn.classList.add('model-advanced-btn');
if(!btn.querySelector('svg')&&typeof li==='function') btn.innerHTML=li('settings',15);
btn.style.position='';
btn.style.right='';
btn.style.top='';
btn.style.transform='';
btn.style.width='32px';
btn.style.height='32px';
btn.style.display='flex';
btn.style.alignItems='center';
btn.style.justifyContent='center';
btn.style.flex='0 0 32px';
btn.style.boxSizing='border-box';
const title=t('settings_aux_advanced_button_title')||'Advanced options';
btn.title=title;
btn.setAttribute('aria-label',t('settings_main_advanced_button_aria')||'Advanced options for main model');
btn.disabled=_mainAdvancedConfig===null;
btn.style.opacity='';
btn.style.cursor='';
if(btn._bound) return;
btn._bound=true;
btn.addEventListener('click',()=>{if(_mainAdvancedConfig!==null)_openAuxAdvancedOptions('__main__',_mainAdvancedConfig||{});});
}
async function _loadAuxiliaryModels(){
const container=$('auxModelsContainer');
if(!container) return;
@@ -8128,6 +8281,12 @@ async function _loadAuxiliaryModels(){
name:g.provider,
models:g.models.map(m=>m.id),
}));
if(auxData&&Object.prototype.hasOwnProperty.call(auxData,'main')){
_mainAdvancedConfig=auxData.main||{};
}else{
_mainAdvancedConfig=null;
}
_bindMainAdvancedOptionsButton();
const tasks=(auxData&&auxData.tasks)||[];
// Build a quick lookup: taskKey → {provider, model}
const taskMap={};
@@ -8138,12 +8297,12 @@ async function _loadAuxiliaryModels(){
for(const slot of _AUX_TASK_SLOTS){
const cfg=taskMap[slot.key]||{provider:'auto',model:''};
const row=document.createElement('div');
row.style.cssText='display:grid;grid-template-columns:120px 1fr 1fr;gap:8px;align-items:center;margin-bottom:8px';
row.style.cssText='display:grid;grid-template-columns:120px 1fr 1fr 34px;gap:8px;align-items:center;margin-bottom:8px';
// Task name + description
const label=document.createElement('div');
label.style.cssText='font-size:12px;font-weight:500;color:var(--text);line-height:1.3';
label.innerHTML=esc(slot.name)+'<div style="font-size:10px;color:var(--muted);font-weight:400">'+esc(slot.desc)+'</div>';
label.innerHTML=esc(t(slot.nameKey)||slot.key)+'<div style="font-size:10px;color:var(--muted);font-weight:400">'+esc(t(slot.descKey)||'')+'</div>';
row.appendChild(label);
// Provider select
@@ -8162,6 +8321,17 @@ async function _loadAuxiliaryModels(){
modelSel.addEventListener('change',()=>_onAuxModelChange(slot.key));
row.appendChild(modelSel);
const advancedBtn=document.createElement('button');
advancedBtn.type='button';
advancedBtn.className='aux-advanced-btn model-advanced-btn';
const advTitle=t('settings_aux_advanced_button_title')||'Advanced options';
const slotName=t(slot.nameKey)||slot.key;
advancedBtn.title=advTitle;
advancedBtn.setAttribute('aria-label',(t('settings_aux_advanced_button_aria')||'Advanced options for {task}').replace('{task}',slotName));
advancedBtn.innerHTML=typeof li==='function'?li('settings',15):'⚙';
advancedBtn.addEventListener('click',()=>_openAuxAdvancedOptions(slot.key,cfg));
row.appendChild(advancedBtn);
container.appendChild(row);
}
// Hide apply button (no changes yet)
@@ -8211,7 +8381,7 @@ async function _applyAuxModels(){
saved++;
}catch(e){
console.warn('[settings] failed to save aux task',slot.key,e);
if(typeof showToast==='function') showToast(t('settings_aux_save_failed')||'Failed to save auxiliary model for '+slot.name);
if(typeof showToast==='function') showToast(t('settings_aux_save_failed')||'Failed to save auxiliary model');
return;
}
}
+43
View File
@@ -4055,6 +4055,49 @@ main.main > #mainPlugin{display:none;}
#mainSettings .settings-field > div[style*="color:var(--muted)"],
#mainSettings .settings-field > div[style*="color: var(--muted)"]{font-size:12px;color:var(--muted);line-height:1.5;}
.model-advanced-row{
display:grid;
grid-template-columns:minmax(0,1fr) 34px;
gap:8px;
align-items:center;
}
.model-advanced-row #settingsModel{
width:100%!important;
min-width:0;
box-sizing:border-box;
}
.model-advanced-btn{
width:32px;
height:32px;
display:flex;
align-items:center;
justify-content:center;
border-radius:8px;
border:1px solid var(--border);
background:var(--input-bg);
color:var(--muted);
cursor:pointer;
line-height:1;
padding:0;
flex:0 0 32px;
box-sizing:border-box;
}
.model-advanced-btn svg{display:block;flex-shrink:0;}
.model-advanced-btn:hover:not(:disabled),
.model-advanced-btn:focus-visible:not(:disabled){
color:var(--accent);
border-color:var(--accent);
background:var(--accent-bg);
}
.model-advanced-btn:focus-visible{
outline:2px solid var(--focus-ring);
outline-offset:2px;
}
.model-advanced-btn:disabled{
opacity:.7;
cursor:not-allowed;
}
/* Sidebar tab visibility — hide nav-tab and matching rail-btn when toggled off by user */
.nav-tab-hidden{display:none!important;}
+376 -3
View File
@@ -10,6 +10,7 @@ ROOT = Path(__file__).parent.parent
PANELS_JS = (ROOT / "static" / "panels.js").read_text(encoding="utf-8")
INDEX_HTML = (ROOT / "static" / "index.html").read_text(encoding="utf-8")
I18N_JS = (ROOT / "static" / "i18n.js").read_text(encoding="utf-8")
STREAMING_PY = (ROOT / "api" / "streaming.py").read_text(encoding="utf-8")
class TestAuxiliaryModelsHTML:
@@ -62,17 +63,107 @@ class TestAuxiliaryModelsJS:
)
def test_aux_task_slots_defined(self):
"""_AUX_TASK_SLOTS must list the 9 canonical task slots."""
"""_AUX_TASK_SLOTS must list the 12 canonical task slots."""
assert "_AUX_TASK_SLOTS" in PANELS_JS, (
"Missing _AUX_TASK_SLOTS constant in panels.js"
)
# Verify all 9 tasks are present
# Verify all 12 tasks are present
for key in ("vision", "compression", "web_extract", "session_search",
"approval", "mcp", "title_generation", "skills_hub", "curator"):
"approval", "mcp", "title_generation", "skills_hub", "curator",
"kanban_decomposer", "profile_describer", "triage_specifier"):
assert f"key:'{key}'" in PANELS_JS, (
f"Missing auxiliary task slot '{key}' in _AUX_TASK_SLOTS"
)
def test_advanced_options_button_and_modal_wiring(self):
"""Each auxiliary row and the main model should expose gear-driven advanced config editing."""
for marker in (
"aux-advanced-btn",
"model-advanced-row",
"model-advanced-btn",
"mainAdvancedBtn",
"_bindMainAdvancedOptionsButton",
"document.createElement('button')",
"row.appendChild(btn)",
"_openAuxAdvancedOptions",
"_mainAdvancedConfig=null",
"btn.disabled=_mainAdvancedConfig===null",
"if(_mainAdvancedConfig!==null)",
"Object.prototype.hasOwnProperty.call(auxData,'main')",
"_mainAdvancedConfig=null;",
"auxAdvancedOverlay",
"auxAdvancedBaseUrl",
"auxAdvancedTimeout",
"auxAdvancedDownloadTimeout",
"auxAdvancedMaxConcurrency",
"auxAdvancedExtraBody",
"auxAdvancedApiKey",
"api_key_clear",
"Object.keys(cfg.extra_body).length",
):
assert marker in PANELS_JS
def test_main_advanced_modal_hides_unsupported_timing_fields_but_keeps_request_body(self):
"""Main-model modal should not advertise timing knobs that the chat agent cannot apply."""
open_idx = PANELS_JS.find("function _openAuxAdvancedOptions")
assert open_idx >= 0
modal_body = PANELS_JS[open_idx:open_idx + 3200]
assert "const timingFields=isMain?'':(" in modal_body
assert "auxAdvancedExtraBody" in modal_body
assert "auxAdvancedBaseUrl" in modal_body
def test_main_advanced_save_omits_unsupported_timing_keys(self):
"""Saving main-model options must not send blank timing keys that backend treats as clears."""
save_idx = PANELS_JS.find("const advanced={")
assert save_idx >= 0
save_body = PANELS_JS[save_idx:save_idx + 900]
object_literal = save_body[:save_body.find("};") + 2]
assert "timeout:" not in object_literal
assert "download_timeout:" not in object_literal
assert "max_concurrency:" not in object_literal
assert "if(!isMain){" in save_body
assert "advanced.timeout=$('auxAdvancedTimeout')?.value||''" in save_body
assert "advanced.download_timeout=$('auxAdvancedDownloadTimeout')?.value||''" in save_body
assert "advanced.max_concurrency=$('auxAdvancedMaxConcurrency')?.value||''" in save_body
def test_main_extra_body_flows_to_agent_request_overrides(self):
"""Persisted main extra_body must be passed to AIAgent, not only shown in Settings."""
assert "_main_model_request_overrides" in STREAMING_PY
assert "'request_overrides' in _agent_params" in STREAMING_PY
assert "_agent_kwargs['request_overrides'] = _main_request_overrides" in STREAMING_PY
assert "_main_request_overrides or {}" in STREAMING_PY
def test_advanced_modal_uses_defined_theme_tokens_and_inline_button_styles(self):
"""The modal is appended outside #mainSettings, so scoped button CSS must not be required."""
modal_idx = PANELS_JS.find("function _ensureAuxAdvancedModal")
assert modal_idx >= 0
modal_body = PANELS_JS[modal_idx:modal_idx + 2400]
assert "var(--panel)" not in modal_body, "--panel is not a defined WebUI theme token"
assert "class=\"settings-btn\"" not in modal_body, "settings-btn is scoped under #mainSettings"
assert "background:var(--surface)" in modal_body
assert "background:var(--input-bg)" in modal_body
assert ":-webkit-autofill" in PANELS_JS
assert "settings_aux_advanced_title" in PANELS_JS
assert "settings_aux_advanced_button_aria" in PANELS_JS
def test_advanced_modal_inputs_disable_browser_autofill(self):
"""Advanced modal fields must not be mistaken for browser login/password fields."""
input_helper_idx = PANELS_JS.find("function _auxAdvancedInputHtml")
assert input_helper_idx >= 0
input_helper = PANELS_JS[input_helper_idx:input_helper_idx + 1200]
assert 'autocomplete="off"' in input_helper
assert 'data-lpignore="true"' in input_helper
assert 'data-1p-ignore="true"' in input_helper
assert "aux-manual-override-value" in input_helper
assert "aux-${id}" not in input_helper
assert "autocompleteAttr" in input_helper
api_key_idx = PANELS_JS.find("_auxAdvancedInputHtml('auxAdvancedApiKey'")
assert api_key_idx >= 0
api_key_call = PANELS_JS[api_key_idx:api_key_idx + 450]
assert "'password'" not in api_key_call
assert 'autocomplete="one-time-code"' in api_key_call
assert "-webkit-text-security:disc" in api_key_call
def test_calls_model_auxiliary_api(self):
"""_loadAuxiliaryModels must call /api/model/auxiliary."""
assert "/api/model/auxiliary" in PANELS_JS, (
@@ -149,6 +240,58 @@ class TestAuxiliaryModelsI18n:
"settings_aux_save_failed",
"settings_aux_saved",
"settings_aux_no_changes",
"settings_aux_advanced_button_title",
"settings_aux_advanced_button_aria",
"settings_aux_advanced_title",
"settings_aux_advanced_subtitle",
"settings_aux_advanced_save",
"settings_aux_advanced_base_url",
"settings_aux_advanced_base_url_desc",
"settings_aux_advanced_timeout",
"settings_aux_advanced_timeout_desc",
"settings_aux_advanced_download_timeout",
"settings_aux_advanced_download_timeout_desc",
"settings_aux_advanced_max_concurrency",
"settings_aux_advanced_max_concurrency_desc",
"settings_aux_advanced_extra_body",
"settings_aux_advanced_extra_body_desc",
"settings_aux_advanced_api_key",
"settings_aux_advanced_api_key_set_hint",
"settings_aux_advanced_api_key_empty_hint",
"settings_aux_advanced_api_key_clear",
"settings_aux_advanced_extra_body_invalid_json",
"settings_aux_advanced_extra_body_object_required",
"settings_aux_advanced_saved",
"settings_aux_advanced_save_failed",
"settings_main_advanced_button_aria",
"settings_main_advanced_title",
"settings_main_advanced_subtitle",
"settings_main_advanced_saved",
"settings_main_advanced_save_failed",
"settings_aux_task_vision",
"settings_aux_task_vision_desc",
"settings_aux_task_compression",
"settings_aux_task_compression_desc",
"settings_aux_task_web_extract",
"settings_aux_task_web_extract_desc",
"settings_aux_task_session_search",
"settings_aux_task_session_search_desc",
"settings_aux_task_approval",
"settings_aux_task_approval_desc",
"settings_aux_task_mcp",
"settings_aux_task_mcp_desc",
"settings_aux_task_title_generation",
"settings_aux_task_title_generation_desc",
"settings_aux_task_skills_hub",
"settings_aux_task_skills_hub_desc",
"settings_aux_task_curator",
"settings_aux_task_curator_desc",
"settings_aux_task_kanban_decomposer",
"settings_aux_task_kanban_decomposer_desc",
"settings_aux_task_profile_describer",
"settings_aux_task_profile_describer_desc",
"settings_aux_task_triage_specifier",
"settings_aux_task_triage_specifier_desc",
]
def test_all_i18n_keys_present(self):
@@ -191,6 +334,71 @@ class TestAuxiliaryModelsBackend:
"Missing get_auxiliary_models() in api/config.py"
)
def test_backend_aux_task_slots_include_agent_defaults(self):
"""Backend allow-list must include newer Hermes auxiliary slots."""
for key in ("kanban_decomposer", "profile_describer", "triage_specifier"):
assert f'"{key}"' in self.CONFIG_PY
def test_backend_surfaces_advanced_fields_without_api_key_value(self, monkeypatch):
"""Advanced fields should be visible, but API keys remain write-only."""
from api import config
monkeypatch.setattr(config, "reload_config", lambda: None)
monkeypatch.setattr(config, "cfg", {
"model": {"provider": "openai", "default": "gpt-5.5"},
"auxiliary": {
"vision": {
"provider": "openai",
"model": "gpt-5.5",
"base_url": "https://example.invalid/v1",
"timeout": 42,
"download_timeout": 7,
"max_concurrency": 2,
"extra_body": {"reasoning_effort": "none"},
"api_key": "DUMMY_KEY_DO_NOT_RETURN",
}
},
})
data = config.get_auxiliary_models()
vision = next(t for t in data["tasks"] if t["task"] == "vision")
assert vision["base_url"] == "https://example.invalid/v1"
assert vision["timeout"] == 42
assert vision["download_timeout"] == 7
assert vision["max_concurrency"] == 2
assert vision["extra_body"] == {"reasoning_effort": "none"}
assert vision["api_key_set"] is True
assert "api_key" not in vision
def test_backend_surfaces_main_advanced_fields_without_api_key_value(self, monkeypatch):
"""Main model advanced fields should be visible, but API keys remain write-only."""
from api import config
monkeypatch.setattr(config, "reload_config", lambda: None)
monkeypatch.setattr(config, "cfg", {
"model": {
"provider": "openai",
"default": "gpt-5.5",
"base_url": "https://example.invalid/v1",
"timeout": 42,
"download_timeout": 7,
"max_concurrency": 2,
"extra_body": {"reasoning_effort": "none"},
"api_key": "DUMMY_KEY_DO_NOT_RETURN",
},
"auxiliary": {},
})
data = config.get_auxiliary_models()
main = data["main"]
assert main["base_url"] == "https://example.invalid/v1"
assert main["timeout"] == 42
assert main["download_timeout"] == 7
assert main["max_concurrency"] == 2
assert main["extra_body"] == {"reasoning_effort": "none"}
assert main["api_key_set"] is True
assert "api_key" not in main
def test_set_auxiliary_model_function_exists(self):
"""set_auxiliary_model() must exist in api/config.py."""
assert "def set_auxiliary_model" in self.CONFIG_PY, (
@@ -234,6 +442,171 @@ class TestAuxiliaryModelsBackend:
assert "arbitrary_key" not in config_path.read_text(encoding="utf-8")
def test_set_hermes_default_model_persists_advanced_options(self, monkeypatch, tmp_path):
"""Main-model gear-modal payload should persist supported model options."""
from api import config
config_path = tmp_path / "config.yaml"
config_path.write_text("model:\n provider: openai\n default: gpt-5.5\n", encoding="utf-8")
monkeypatch.setattr(config, "_get_config_path", lambda: config_path)
monkeypatch.setattr(config, "reload_config", lambda: None)
monkeypatch.setattr(config, "invalidate_models_cache", lambda: None)
monkeypatch.setattr(config, "resolve_model_provider", lambda model: (model, "openai", None))
result = config.set_hermes_default_model(
"gpt-5.5",
advanced={
"base_url": "https://example.invalid/v1/",
"timeout": "45",
"download_timeout": "9",
"max_concurrency": "2",
"extra_body": {"reasoning_effort": "none"},
"api_key": "DUMMY_KEY_DO_NOT_PRINT",
},
)
assert result["ok"] is True
text = config_path.read_text(encoding="utf-8")
assert "https://example.invalid/v1" in text
assert "timeout: 45" in text
assert "download_timeout: 9" in text
assert "max_concurrency: 2" in text
assert "reasoning_effort: none" in text
assert "DUMMY_KEY_DO_NOT_PRINT" in text
def test_set_auxiliary_model_persists_advanced_options(self, monkeypatch, tmp_path):
"""Gear-modal payload should persist supported per-slot options."""
from api import config
config_path = tmp_path / "config.yaml"
config_path.write_text("auxiliary:\n vision:\n provider: auto\n model: ''\n", encoding="utf-8")
monkeypatch.setattr(config, "_get_config_path", lambda: config_path)
monkeypatch.setattr(config, "reload_config", lambda: None)
result = config.set_auxiliary_model(
"vision",
"openai",
"gpt-5.5",
advanced={
"base_url": "https://example.invalid/v1/",
"timeout": "45",
"download_timeout": "9",
"max_concurrency": "2",
"extra_body": {"reasoning_effort": "none"},
"api_key": "DUMMY_KEY_DO_NOT_PRINT",
},
)
assert result["ok"] is True
text = config_path.read_text(encoding="utf-8")
assert "https://example.invalid/v1" in text
assert "timeout: 45" in text
assert "download_timeout: 9" in text
assert "max_concurrency: 2" in text
assert "reasoning_effort: none" in text
assert "DUMMY_KEY_DO_NOT_PRINT" in text
def test_set_auxiliary_model_explicit_advanced_base_url_wins_over_custom_resolution(self, monkeypatch, tmp_path):
"""Custom-provider auto-resolution must not clobber an explicit gear base_url."""
from api import config
config_path = tmp_path / "config.yaml"
config_path.write_text("auxiliary:\n vision:\n provider: auto\n model: ''\n", encoding="utf-8")
monkeypatch.setattr(config, "_get_config_path", lambda: config_path)
monkeypatch.setattr(config, "reload_config", lambda: None)
monkeypatch.setattr(
config,
"resolve_model_provider",
lambda model: (model, "custom:demo", "https://resolved.invalid/v1"),
)
result = config.set_auxiliary_model(
"vision",
"custom:demo",
"demo/model",
advanced={"base_url": "https://manual.invalid/v1/"},
)
assert result["ok"] is True
text = config_path.read_text(encoding="utf-8")
assert "https://manual.invalid/v1" in text
assert "https://resolved.invalid/v1" not in text
def test_main_extra_body_becomes_runtime_request_overrides(self):
"""The main-model extra_body option is live only if it reaches request_overrides."""
from api import config
cfg = {
"model": {
"provider": "openai",
"default": "gpt-5.5",
"extra_body": {"reasoning_effort": "none"},
}
}
overrides = config._main_model_request_overrides(cfg)
assert overrides == {"extra_body": {"reasoning_effort": "none"}}
assert overrides["extra_body"] is not cfg["model"]["extra_body"]
def test_set_hermes_default_model_clear_api_key_removes_key(self, monkeypatch, tmp_path):
"""Clearing a write-only API key override should remove the key, not persist api_key: ''."""
from api import config
config_path = tmp_path / "config.yaml"
config_path.write_text(
"model:\n provider: openai\n default: gpt-5.5\n api_key: old-secret\n",
encoding="utf-8",
)
monkeypatch.setattr(config, "_get_config_path", lambda: config_path)
monkeypatch.setattr(config, "reload_config", lambda: None)
monkeypatch.setattr(config, "invalidate_models_cache", lambda: None)
monkeypatch.setattr(config, "resolve_model_provider", lambda model: (model, "openai", None))
result = config.set_hermes_default_model(
"gpt-5.5",
advanced={"api_key_clear": True, "api_key": ""},
)
assert result["ok"] is True
text = config_path.read_text(encoding="utf-8")
assert "old-secret" not in text
assert "api_key" not in text
def test_set_auxiliary_model_clears_empty_extra_body(self, monkeypatch, tmp_path):
"""Blank extra_body from the modal should remove config noise instead of writing {}."""
from api import config
config_path = tmp_path / "config.yaml"
config_path.write_text(
"auxiliary:\n vision:\n provider: openai\n model: gpt-5.5\n extra_body:\n reasoning_effort: none\n",
encoding="utf-8",
)
monkeypatch.setattr(config, "_get_config_path", lambda: config_path)
monkeypatch.setattr(config, "reload_config", lambda: None)
result = config.set_auxiliary_model("vision", "openai", "gpt-5.5", advanced={"extra_body": {}})
assert result["ok"] is True
assert "extra_body" not in config_path.read_text(encoding="utf-8")
def test_set_auxiliary_model_validates_extra_body_object(self, monkeypatch, tmp_path):
"""extra_body must stay an object, not arbitrary JSON."""
from api import config
config_path = tmp_path / "config.yaml"
config_path.write_text("auxiliary: {}\n", encoding="utf-8")
monkeypatch.setattr(config, "_get_config_path", lambda: config_path)
try:
config.set_auxiliary_model("vision", "openai", "gpt-5.5", advanced={"extra_body": ["bad"]})
except ValueError as exc:
assert "extra_body must be a JSON object" in str(exc)
else:
raise AssertionError("set_auxiliary_model accepted non-object extra_body")
def test_model_set_route_returns_400_for_unknown_auxiliary_task(self, monkeypatch):
"""The route should surface invalid auxiliary task names as a client error."""
from types import SimpleNamespace