From e47f685b89f457c14e100dcef069dff5ca60da2a Mon Sep 17 00:00:00 2001 From: nesquena-hermes Date: Mon, 15 Jun 2026 13:16:34 -0700 Subject: [PATCH] Release PA (v0.51.440): advanced per-model options + auxiliary model routing (#3097) (#4264) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --------- Co-authored-by: nesquena-hermes Co-authored-by: ai-ag2026 --- CHANGELOG.md | 6 +- api/config.py | 116 +++- api/routes.py | 8 +- api/streaming.py | 5 + static/i18n.js | 678 ++++++++++++++++++++++++ static/index.html | 5 +- static/panels.js | 196 ++++++- static/style.css | 43 ++ tests/test_auxiliary_models_settings.py | 379 ++++++++++++- 9 files changed, 1412 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 658ef4d85..1eb9d5082 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/api/config.py b/api/config.py index 2e648ce34..f539b5215 100644 --- a/api/config.py +++ b/api/config.py @@ -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 diff --git a/api/routes.py b/api/routes.py index 8b6009498..3e6e65f00 100644 --- a/api/routes.py +++ b/api/routes.py @@ -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) diff --git a/api/streaming.py b/api/streaming.py index 4220888f9..d06c17faa 100644 --- a/api/streaming.py +++ b/api/streaming.py @@ -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()` diff --git a/static/i18n.js b/static/i18n.js index 07e8bf62c..bcbbe61c1 100644 --- a/static/i18n.js +++ b/static/i18n.js @@ -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', diff --git a/static/index.html b/static/index.html index 4bca04e28..d3501dd50 100644 --- a/static/index.html +++ b/static/index.html @@ -1056,7 +1056,10 @@
- +
+ + +
Used for new conversations. Existing conversations keep their selected model.
diff --git a/static/panels.js b/static/panels.js index d4d6010f6..bcfdf0d51 100644 --- a/static/panels.js +++ b/static/panels.js @@ -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=`
+ +
+
+ +
+
+
+ + +
+
`; + 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 ``; +} + +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+ + ``+ + _auxAdvancedInputHtml('auxAdvancedApiKey',t('settings_aux_advanced_api_key')||'API key override','',apiKeyHint,'text','autocomplete="one-time-code" inputmode="text" readonly onfocus="this.removeAttribute("readonly")"',';-webkit-text-security:disc')+ + ``; + } + 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)+'
'+esc(slot.desc)+'
'; + label.innerHTML=esc(t(slot.nameKey)||slot.key)+'
'+esc(t(slot.descKey)||'')+'
'; 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; } } diff --git a/static/style.css b/static/style.css index f22d32f95..69e86c5b0 100644 --- a/static/style.css +++ b/static/style.css @@ -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;} diff --git a/tests/test_auxiliary_models_settings.py b/tests/test_auxiliary_models_settings.py index 5ec6684e0..25fd253eb 100644 --- a/tests/test_auxiliary_models_settings.py +++ b/tests/test_auxiliary_models_settings.py @@ -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