{"repo": "byted-x/wechat-bot", "files": {"shared/model_catalog.json": {"f83ed3b909121a7ff609c71569ffc99d7a196e6c": {"t": 1785674226.078465, "n": 7225, "h": "0c4d9a5550f53796", "c": "{\n \"updated_at\": \"2026-03-27\",\n \"providers\": [\n {\n \"id\": \"openai\",\n \"label\": \"OpenAI\",\n \"base_url\": \"https://api.openai.com/v1\",\n \"api_key_url\": \"https://platform.openai.com/api-keys\",\n \"aliases\": [\"openai\", \"gpt\"],\n \"default_model\": \"gpt-5.4-mini\",\n \"models\": [\"gpt-5.4-pro\", \"gpt-5.4\", \"gpt-5.4-mini\", \"gpt-5.4-nano\", \"gpt-5.3-codex\", \"gpt-5\", \"gpt-5-mini\", \"gpt-5-nano\", \"gpt-4.1\", \"gpt-4.1-mini\", \"gpt-4.1-nano\", \"gpt-4o\", \"gpt-4o-mini\"]\n },\n {\n \"id\": \"anthropic\",\n \"label\": \"Anthropic / Claude\",\n \"base_url\": \"https://api.anthropic.com/v1\",\n \"api_key_url\": \"https://platform.claude.com/settings/keys\",\n \"aliases\": [\"anthropic\", \"claude\"],\n \"default_model\": \"claude-sonnet-4-0\",\n \"models\": [\"claude-sonnet-4-6\", \"claude-opus-4-6\", \"claude-haiku-4-5\", \"claude-sonnet-4-5\", \"claude-opus-4-5\", \"claude-sonnet-4-0\", \"claude-opus-4-1\", \"claude-opus-4-0\", \"claude-3-7-sonnet-latest\", \"claude-3-5-haiku-latest\"]\n },\n {\n \"id\": \"google\",\n \"label\": \"Google / Gemini\",\n \"base_url\": \"https://generativelanguage.googleapis.com/v1beta/openai\",\n \"api_key_url\": \"https://aistudio.google.com/apikey\",\n \"aliases\": [\"google\", \"gemini\", \"vertex\"],\n \"default_model\": \"gemini-2.5-flash\",\n \"models\": [\"gemini-3.1-pro-preview\", \"gemini-3-flash-preview\", \"gemini-3.1-flash-lite-preview\", \"gemini-2.5-pro\", \"gemini-2.5-flash\", \"gemini-2.5-flash-lite\"]\n },\n {\n \"id\": \"doubao\",\n \"label\": \"Doubao (豆包)\",\n \"base_url\": \"https://ark.cn-beijing.volces.com/api/v3\",\n \"api_key_url\": \"https://console.volcengine.com/ark\",\n \"aliases\": [\"doubao\", \"豆包\", \"ark\", \"volc\"],\n \"default_model\": \"doubao-seed-1.8\",\n \"models\": [\"doubao-seed-1.8\", \"doubao-seed-2.0-pro\", \"doubao-seed-2.0-lite\", \"doubao-seed-2.0-mini\", \"doubao-seed-code\"]\n },\n {\n \"id\": \"deepseek\",\n \"label\": \"DeepSeek\",\n \"base_url\": \"https://api.deepseek.com/v1\",\n \"api_key_url\": \"https://platform.deepseek.com/api_keys\",\n \"aliases\": [\"deepseek\"],\n \"default_model\": \"deepseek-chat\",\n \"models\": [\"deepseek-chat\", \"deepseek-reasoner\"]\n },\n {\n \"id\": \"qwen\",\n \"label\": \"Qwen (通义千问)\",\n \"base_url\": \"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n \"api_key_url\": \"https://dashscope.console.aliyun.com/apiKey\",\n \"aliases\": [\"qwen\", \"通义\", \"千问\", \"dashscope\", \"bailian\", \"百炼\"],\n \"default_model\": \"qwen3.5-plus\",\n \"models\": [\"qwen3.5-plus\", \"qwen3.5-flash\", \"qwen3-max-2026-01-23\", \"qwen-plus-latest\", \"qwen-turbo-latest\", \"qwen3-coder-next\", \"qwen3-coder-plus\", \"qwen3-coder-flash\"]\n },\n {\n \"id\": \"zhipu\",\n \"label\": \"Zhipu (智谱)\",\n \"base_url\": \"https://open.bigmodel.cn/api/paas/v4\",\n \"api_key_url\": \"https://open.bigmodel.cn/usercenter/apikeys\",\n \"aliases\": [\"zhipu\", \"glm\", \"智谱\"],\n \"default_model\": \"glm-5\",\n \"models\": [\"glm-5\", \"glm-4.7\", \"glm-4.6\", \"glm-4.5-air\"]\n },\n {\n \"id\": \"kimi\",\n \"label\": \"Kimi (Moonshot)\",\n \"base_url\": \"https://api.moonshot.cn/v1\",\n \"api_key_url\": \"https://platform.moonshot.cn/console/api-keys\",\n \"aliases\": [\"moonshot\", \"kimi\"],\n \"default_model\": \"kimi-k2-turbo-preview\",\n \"models\": [\"kimi-for-coding\", \"kimi-k2-turbo-preview\", \"kimi-k2-0905-preview\", \"kimi-k2-thinking-turbo\", \"kimi-thinking-preview\", \"kimi-latest\"]\n },\n {\n \"id\": \"minimax\",\n \"label\": \"MiniMax\",\n \"base_url\": \"https://api.minimax.io/v1\",\n \"api_key_url\": \"https://platform.minimax.io/\",\n \"aliases\": [\"minimax\", \"minimaxi\"],\n \"default_model\": \"MiniMax-M2.5\",\n \"models\": [\"MiniMax-M2.7\", \"MiniMax-M2.7-highspeed\", \"MiniMax-M2.5\", \"MiniMax-M2.5-highspeed\", \"MiniMax-M2.1\", \"MiniMax-M2.1-highspeed\", \"MiniMax-M2\", \"MiniMax-Text-01\"]\n },\n {\n \"id\": \"groq\",\n \"label\": \"Groq\",\n \"base_url\": \"https://api.groq.com/openai/v1\",\n \"api_key_url\": \"https://console.groq.com/keys\",\n \"aliases\": [\"groq\"],\n \"default_model\": \"moonshotai/kimi-k2-instruct-0905\",\n \"models\": [\"moonshotai/kimi-k2-instruct-0905\", \"openai/gpt-oss-120b\", \"openai/gpt-oss-20b\", \"meta-llama/llama-4-scout-17b-16e-instruct\", \"qwen/qwen3-32b\", \"llama-3.3-70b-versatile\"]\n },\n {\n \"id\": \"siliconflow\",\n \"label\": \"SiliconFlow\",\n \"base_url\": \"https://api.siliconflow.cn/v1\",\n \"api_key_url\": \"https://cloud.siliconflow.cn/account/ak\",\n \"aliases\": [\"siliconflow\", \"silicon\", \"硅基\"],\n \"default_model\": \"deepseek-ai/DeepSeek-V3.2\",\n \"models\": [\"deepseek-ai/DeepSeek-V3.2\", \"deepseek-ai/DeepSeek-V3.1\", \"Qwen/Qwen3-Coder-480B-A35B-Instruct\", \"Qwen/Qwen3-Next-80B-A3B-Instruct\", \"zai-org/GLM-4.5\", \"openai/gpt-oss-120b\"]\n },\n {\n \"id\": \"mistral\",\n \"label\": \"Mistral\",\n \"base_url\": \"https://api.mistral.ai/v1\",\n \"api_key_url\": \"https://console.mistral.ai/api-keys/\",\n \"aliases\": [\"mistral\"],\n \"default_model\": \"mistral-large-latest\",\n \"models\": [\"mistral-large-latest\", \"mistral-medium-latest\", \"mistral-small-latest\", \"codestral-latest\", \"devstral-medium-latest\"]\n },\n {\n \"id\": \"perplexity\",\n \"label\": \"Perplexity\",\n \"base_url\": \"https://api.perplexity.ai\",\n \"api_key_url\": \"https://www.perplexity.ai/settings/api\",\n \"aliases\": [\"perplexity\", \"sonar\"],\n \"default_model\": \"sonar-pro\",\n \"models\": [\"sonar\", \"sonar-pro\", \"sonar-reasoning-pro\", \"sonar-deep-research\"]\n },\n {\n \"id\": \"openrouter\",\n \"label\": \"OpenRouter\",\n \"base_url\": \"https://openrouter.ai/api/v1\",\n \"api_key_url\": \"https://openrouter.ai/keys\",\n \"aliases\": [\"openrouter\"],\n \"default_model\": \"openrouter/auto\",\n \"models\": [\"openrouter/auto\", \"anthropic/claude-opus-4.6\", \"anthropic/claude-sonnet-4.6\", \"anthropic/claude-haiku-4.5\", \"google/gemini-3.1-pro-preview\", \"google/gemini-3.1-flash-lite-preview\", \"openai/gpt-5.4\", \"openai/gpt-5.4-pro\", \"deepseek/deepseek-v3.2\", \"minimax/minimax-m2.7\"]\n },\n {\n \"id\": \"together\",\n \"label\": \"Together\",\n \"base_url\": \"https://api.together.xyz/v1\",\n \"api_key_url\": \"https://api.together.xyz/settings/api-keys\",\n \"aliases\": [\"together\"],\n \"default_model\": \"moonshotai/Kimi-K2.5\",\n \"models\": [\"moonshotai/Kimi-K2.5\", \"Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8\", \"deepseek-ai/DeepSeek-V3.1\", \"openai/gpt-oss-120b\", \"meta-llama/Llama-3.3-70B-Instruct-Turbo\"]\n },\n {\n \"id\": \"fireworks\",\n \"label\": \"Fireworks\",\n \"base_url\": \"https://api.fireworks.ai/inference/v1\",\n \"api_key_url\": \"https://fireworks.ai/account/api-keys\",\n \"aliases\": [\"fireworks\"],\n \"default_model\": \"accounts/fireworks/models/deepseek-v3p1\",\n \"models\": [\"accounts/fireworks/models/deepseek-v3p1\", \"accounts/fireworks/models/qwen3-235b-a22b\", \"accounts/fireworks/models/qwen3-coder-480b-a35b-instruct\", \"accounts/fireworks/models/kimi-k2p5\"]\n },\n {\n \"id\": \"ollama\",\n \"label\": \"Ollama\",\n \"base_url\": \"http://127.0.0.1:11434/v1\",\n \"api_key_url\": \"https://ollama.com/\",\n \"aliases\": [\"ollama\"],\n \"default_model\": \"qwen3\",\n \"allow_empty_key\": true,\n \"models\": [\"qwen3\", \"llama3.1\", \"gemma3\", \"mistral\"]\n }\n ]\n}\n"}}, "backend/model_catalog.py": {"f83ed3b909121a7ff609c71569ffc99d7a196e6c": {"t": 1785674230.016874, "n": 26803, "h": "2c667b1aa59e1f58", "c": "\"\"\"Centralized provider and model catalog for shared config consumers.\"\"\"\n\nfrom __future__ import annotations\n\nfrom copy import deepcopy\nfrom typing import Any, Dict, Optional\n\nfrom backend.shared_config import get_model_catalog_path, load_model_catalog\n\n_MODEL_CATALOG_CACHE_SIGNATURE: tuple[int, int] | None = None\n_MODEL_CATALOG_CACHE_PAYLOAD: Dict[str, Any] | None = None\n_PROVIDER_CACHE_SIGNATURE: tuple[int, int] | None = None\n_PROVIDER_CACHE_UPDATED_AT: Any = None\n_PROVIDER_CACHE: list[Dict[str, Any]] | None = None\n_PROVIDER_BY_ID_CACHE: Dict[str, Dict[str, Any]] | None = None\n\n_PROVIDER_ID_ALIASES = {\n \"claude\": \"anthropic\",\n \"bailian\": \"qwen\",\n \"dashscope\": \"qwen\",\n \"moonshot\": \"kimi\",\n}\n\n_EXTRA_PROVIDERS: list[Dict[str, Any]] = [\n {\n \"id\": \"qwen\",\n \"label\": \"Qwen (閫氫箟鍗冮棶)\",\n \"base_url\": \"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n \"api_key_url\": \"https://dashscope.console.aliyun.com/apiKey\",\n \"aliases\": [\"qwen\", \"閫氫箟\", \"鍗冮棶\", \"dashscope\", \"bailian\", \"鐧剧偧\"],\n \"default_model\": \"qwen3.5-plus\",\n \"models\": [\n \"qwen3.5-plus\",\n \"qwen3.5-flash\",\n \"qwen3-max-2026-01-23\",\n \"qwen-plus-latest\",\n \"qwen-turbo-latest\",\n \"qwen3-coder-next\",\n \"qwen3-coder-plus\",\n \"qwen3-coder-flash\",\n \"MiniMax-M2.5\",\n \"glm-5\",\n \"glm-4.7\",\n \"kimi-k2.5\",\n ],\n },\n {\n \"id\": \"anthropic\",\n \"label\": \"Anthropic / Claude\",\n \"base_url\": \"https://api.anthropic.com/v1\",\n \"api_key_url\": \"https://platform.claude.com/settings/keys\",\n \"aliases\": [\"anthropic\", \"claude\"],\n \"default_model\": \"claude-sonnet-4-0\",\n \"models\": [\n \"claude-sonnet-4-6\",\n \"claude-opus-4-6\",\n \"claude-haiku-4-5\",\n \"claude-sonnet-4-5\",\n \"claude-opus-4-5\",\n \"claude-sonnet-4-0\",\n \"claude-opus-4-1\",\n \"claude-opus-4-0\",\n \"claude-3-7-sonnet-latest\",\n \"claude-3-5-haiku-latest\",\n ],\n },\n {\n \"id\": \"google\",\n \"label\": \"Google / Gemini CLI\",\n \"base_url\": \"https://generativelanguage.googleapis.com/v1beta/openai\",\n \"api_key_url\": \"https://aistudio.google.com/apikey\",\n \"aliases\": [\"google\", \"gemini\", \"vertex\"],\n \"default_model\": \"gemini-2.5-flash\",\n \"models\": [\n \"gemini-3.1-pro-preview\",\n \"gemini-3-flash-preview\",\n \"gemini-3.1-flash-lite-preview\",\n \"gemini-2.5-pro\",\n \"gemini-2.5-flash\",\n \"gemini-2.5-flash-lite\",\n ],\n },\n {\n \"id\": \"yuanbao\",\n \"label\": \"Tencent Yuanbao\",\n \"base_url\": \"\",\n \"api_key_url\": \"https://yuanbao.tencent.com/\",\n \"aliases\": [\"yuanbao\", \"腾讯元宝\"],\n \"default_model\": \"yuanbao-web\",\n \"models\": [\"yuanbao-web\"],\n },\n {\n \"id\": \"kimi\",\n \"label\": \"Kimi (Moonshot)\",\n \"base_url\": \"https://api.moonshot.cn/v1\",\n \"api_key_url\": \"https://platform.moonshot.cn/console/api-keys\",\n \"aliases\": [\"moonshot\", \"kimi\"],\n \"default_model\": \"kimi-k2-turbo-preview\",\n \"models\": [\n \"kimi-for-coding\",\n \"kimi-k2-turbo-preview\",\n \"kimi-k2-0905-preview\",\n \"kimi-k2-thinking-turbo\",\n \"kimi-thinking-preview\",\n \"kimi-latest\",\n ],\n },\n {\n \"id\": \"zhipu\",\n \"label\": \"Zhipu (鏅鸿氨)\",\n \"base_url\": \"https://open.bigmodel.cn/api/paas/v4\",\n \"api_key_url\": \"https://open.bigmodel.cn/usercenter/apikeys\",\n \"aliases\": [\"zhipu\", \"glm\", \"鏅鸿氨\"],\n \"default_model\": \"glm-5\",\n \"models\": [\"glm-5\", \"glm-4.7\", \"glm-4.6\", \"glm-4.5-air\"],\n },\n {\n \"id\": \"minimax\",\n \"label\": \"MiniMax\",\n \"base_url\": \"https://api.minimax.io/v1\",\n \"api_key_url\": \"https://platform.minimax.io/\",\n \"aliases\": [\"minimax\", \"minimaxi\"],\n \"default_model\": \"MiniMax-M2.5\",\n \"models\": [\n \"MiniMax-M2.7\",\n \"MiniMax-M2.7-highspeed\",\n \"MiniMax-M2.5\",\n \"MiniMax-M2.5-highspeed\",\n \"MiniMax-M2.1\",\n \"MiniMax-M2.1-highspeed\",\n \"MiniMax-M2\",\n \"MiniMax-Text-01\",\n ],\n },\n]\n\n_AUTH_METHODS_BY_PROVIDER: Dict[str, list[Dict[str, Any]]] = {\n \"openai\": [\n {\n \"id\": \"api_key\",\n \"type\": \"api_key\",\n \"tier\": \"stable\",\n \"supports_local_reuse\": False,\n \"requires_browser_flow\": False,\n \"requires_fields\": [],\n \"requires_extra_fields\": [],\n \"metadata\": {\n \"recommended_base_url\": \"https://api.openai.com/v1\",\n \"recommended_model\": \"gpt-5.4-mini\",\n },\n },\n {\n \"id\": \"codex_local\",\n \"type\": \"local_import\",\n \"provider_id\": \"openai_codex\",\n \"tier\": \"stable\",\n \"supports_local_reuse\": True,\n \"requires_browser_flow\": True,\n \"requires_fields\": [],\n \"requires_extra_fields\": [],\n \"metadata\": {\n \"recommended_base_url\": \"https://api.openai.com/v1\",\n \"recommended_model\": \"gpt-5.4-mini\",\n },\n },\n ],\n \"qwen\": [\n {\n \"id\": \"api_key\",\n \"type\": \"api_key\",\n \"tier\": \"stable\",\n \"supports_local_reuse\": False,\n \"requires_browser_flow\": False,\n \"requires_fields\": [],\n \"requires_extra_fields\": [],\n \"metadata\": {\n \"recommended_base_url\": \"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n \"recommended_model\": \"qwen3.5-plus\",\n \"key_env_hint\": \"DASHSCOPE_API_KEY\",\n },\n },\n {\n \"id\": \"qwen_oauth\",\n \"type\": \"oauth\",\n \"provider_id\": \"qwen_oauth\",\n \"tier\": \"stable\",\n \"supports_local_reuse\": True,\n \"requires_browser_flow\": True,\n \"requires_fields\": [],\n \"requires_extra_fields\": [],\n \"metadata\": {\n \"recommended_base_url\": \"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n \"recommended_model\": \"qwen3-coder-plus\",\n },\n },\n {\n \"id\": \"qwen_local\",\n \"type\": \"local_import\",\n \"provider_id\": \"qwen_oauth\",\n \"tier\": \"stable\",\n \"supports_local_reuse\": True,\n \"requires_browser_flow\": True,\n \"requires_fields\": [],\n \"requires_extra_fields\": [],\n \"metadata\": {\n \"recommended_base_url\": \"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n \"recommended_model\": \"qwen3-coder-plus\",\n },\n },\n {\n \"id\": \"coding_plan_api_key\",\n \"type\": \"api_key\",\n \"tier\": \"stable\",\n \"supports_local_reuse\": False,\n \"requires_browser_flow\": False,\n \"requires_fields\": [],\n \"requires_extra_fields\": [],\n \"metadata\": {\n \"recommended_base_url\": \"https://coding.dashscope.aliyuncs.com/v1\",\n \"recommended_model\": \"qwen3-coder-next\",\n \"key_env_hint\": \"BAILIAN_CODING_PLAN_API_KEY\",\n \"key_prefix_hint\": \"sk-sp-\",\n \"subscription\": True,\n },\n },\n ],\n \"google\": [\n {\n \"id\": \"api_key\",\n \"type\": \"api_key\",\n \"tier\": \"stable\",\n \"supports_local_reuse\": False,\n \"requires_browser_flow\": False,\n \"requires_fields\": [],\n \"requires_extra_fields\": [],\n \"metadata\": {\n \"recommended_base_url\": \"https://generativelanguage.googleapis.com/v1beta/openai\",\n \"recommended_model\": \"gemini-2.5-flash\",\n },\n },\n {\n \"id\": \"google_oauth\",\n \"type\": \"oauth\",\n \"provider_id\": \"google_gemini_cli\",\n \"tier\": \"experimental\",\n \"supports_local_reuse\": True,\n \"requires_browser_flow\": True,\n \"requires_fields\": [\"oauth_project_id\"],\n \"requires_extra_fields\": [],\n \"metadata\": {\n \"recommended_base_url\": \"https://generativelanguage.googleapis.com/v1beta/openai\",\n \"recommended_model\": \"gemini-2.5-flash\",\n },\n },\n {\n \"id\": \"gemini_cli_local\",\n \"type\": \"local_import\",\n \"provider_id\": \"google_gemini_cli\",\n \"tier\": \"experimental\",\n \"supports_local_reuse\": True,\n \"requires_browser_flow\": True,\n \"requires_fields\": [\"oauth_project_id\"],\n \"requires_extra_fields\": [],\n \"metadata\": {\n \"recommended_base_url\": \"https://generativelanguage.googleapis.com/v1beta/openai\",\n \"recommended_model\": \"gemini-2.5-flash\",\n },\n }\n ],\n \"anthropic\": [\n {\n \"id\": \"api_key\",\n \"type\": \"api_key\",\n \"tier\": \"stable\",\n \"supports_local_reuse\": False,\n \"requires_browser_flow\": False,\n \"requires_fields\": [],\n \"requires_extra_fields\": [],\n \"metadata\": {\n \"recommended_base_url\": \"https://api.anthropic.com/v1\",\n \"recommended_model\": \"claude-sonnet-4-0\",\n },\n },\n {\n \"id\": \"claude_code_local\",\n \"type\": \"local_import\",\n \"provider_id\": \"claude_code_local\",\n \"tier\": \"experimental\",\n \"supports_local_reuse\": True,\n \"requires_browser_flow\": True,\n \"requires_fields\": [],\n \"requires_extra_fields\": [],\n \"metadata\": {\n \"recommended_base_url\": \"https://api.anthropic.com/v1\",\n \"recommended_model\": \"claude-sonnet-4-0\",\n },\n },\n {\n \"id\": \"claude_code_oauth\",\n \"type\": \"oauth\",\n \"provider_id\": \"claude_code_local\",\n \"tier\": \"experimental\",\n \"supports_local_reuse\": True,\n \"requires_browser_flow\": True,\n \"requires_fields\": [],\n \"requires_extra_fields\": [],\n \"metadata\": {\n \"recommended_base_url\": \"https://api.anthropic.com/v1\",\n \"recommended_model\": \"claude-sonnet-4-0\",\n },\n },\n {\n \"id\": \"claude_vertex_local\",\n \"type\": \"local_import\",\n \"provider_id\": \"claude_vertex_local\",\n \"tier\": \"experimental\",\n \"supports_local_reuse\": True,\n \"requires_browser_flow\": True,\n \"requires_fields\": [\"oauth_project_id\", \"oauth_location\"],\n \"requires_extra_fields\": [],\n \"metadata\": {\n \"recommended_base_url\": \"https://global-aiplatform.googleapis.com/v1/projects/{project}/locations/global/publishers/anthropic/models\",\n \"recommended_model\": \"claude-sonnet-4-6\",\n },\n },\n ],\n \"kimi\": [\n {\n \"id\": \"api_key\",\n \"type\": \"api_key\",\n \"tier\": \"stable\",\n \"supports_local_reuse\": False,\n \"requires_browser_flow\": False,\n \"requires_fields\": [],\n \"requires_extra_fields\": [],\n \"metadata\": {\n \"recommended_base_url\": \"https://api.moonshot.cn/v1\",\n \"recommended_model\": \"kimi-k2-turbo-preview\",\n },\n },\n {\n \"id\": \"kimi_code_oauth\",\n \"type\": \"oauth\",\n \"provider_id\": \"kimi_code_local\",\n \"tier\": \"experimental\",\n \"supports_local_reuse\": True,\n \"requires_browser_flow\": True,\n \"requires_fields\": [],\n \"requires_extra_fields\": [],\n \"metadata\": {\n \"recommended_base_url\": \"https://api.kimi.com/coding/v1\",\n \"recommended_model\": \"kimi-for-coding\",\n },\n },\n {\n \"id\": \"kimi_code_local\",\n \"type\": \"local_import\",\n \"provider_id\": \"kimi_code_local\",\n \"tier\": \"experimental\",\n \"supports_local_reuse\": True,\n \"requires_browser_flow\": True,\n \"requires_fields\": [],\n \"requires_extra_fields\": [],\n \"metadata\": {\n \"recommended_base_url\": \"https://api.kimi.com/coding/v1\",\n \"recommended_model\": \"kimi-for-coding\",\n },\n },\n {\n \"id\": \"coding_plan_api_key\",\n \"type\": \"api_key\",\n \"tier\": \"stable\",\n \"supports_local_reuse\": False,\n \"requires_browser_flow\": False,\n \"requires_fields\": [],\n \"requires_extra_fields\": [],\n \"metadata\": {\n \"recommended_base_url\": \"https://api.kimi.com/coding/v1\",\n \"recommended_model\": \"kimi-for-coding\",\n \"key_env_hint\": \"KIMI_API_KEY\",\n },\n },\n ],\n \"zhipu\": [\n {\n \"id\": \"api_key\",\n \"type\": \"api_key\",\n \"tier\": \"stable\",\n \"supports_local_reuse\": False,\n \"requires_browser_flow\": False,\n \"requires_fields\": [],\n \"requires_extra_fields\": [],\n \"metadata\": {\n \"recommended_base_url\": \"https://open.bigmodel.cn/api/paas/v4\",\n \"recommended_model\": \"glm-5\",\n },\n },\n {\n \"id\": \"coding_plan_api_key\",\n \"type\": \"api_key\",\n \"tier\": \"stable\",\n \"supports_local_reuse\": False,\n \"requires_browser_flow\": False,\n \"requires_fields\": [],\n \"requires_extra_fields\": [],\n \"metadata\": {\n \"recommended_base_url\": \"https://open.bigmodel.cn/api/coding/paas/v4\",\n \"recommended_model\": \"glm-5\",\n \"subscription\": True,\n },\n },\n ],\n \"minimax\": [\n {\n \"id\": \"api_key\",\n \"type\": \"api_key\",\n \"tier\": \"stable\",\n \"supports_local_reuse\": False,\n \"requires_browser_flow\": False,\n \"requires_fields\": [],\n \"requires_extra_fields\": [],\n \"metadata\": {\n \"recommended_base_url\": \"https://api.minimax.io/v1\",\n \"recommended_model\": \"MiniMax-M2.5\",\n },\n },\n {\n \"id\": \"coding_plan_api_key\",\n \"type\": \"api_key\",\n \"tier\": \"stable\",\n \"supports_local_reuse\": False,\n \"requires_browser_flow\": False,\n \"requires_fields\": [],\n \"requires_extra_fields\": [],\n \"metadata\": {\n \"recommended_base_url\": \"https://api.minimax.io/v1\",\n \"recommended_model\": \"MiniMax-M2.5\",\n \"regional_base_urls\": [\n \"https://api.minimax.io/v1\",\n \"https://api.minimaxi.com/v1\",\n \"https://api.minimax.io/anthropic\",\n \"https://api.minimaxi.com/anthropic\",\n ],\n \"key_env_hint\": \"MINIMAX_API_KEY\",\n \"subscription\": True,\n },\n },\n ],\n \"yuanbao\": [\n {\n \"id\": \"yuanbao_web_session\",\n \"type\": \"web_session\",\n \"provider_id\": \"tencent_yuanbao\",\n \"tier\": \"experimental\",\n \"supports_local_reuse\": False,\n \"requires_browser_flow\": True,\n \"requires_fields\": [],\n \"requires_extra_fields\": [],\n \"runtime_supported\": False,\n }\n ],\n}\n\n\ndef _canonicalize_provider_id(provider_id: Any) -> str:\n normalized = str(provider_id or \"\").strip().lower()\n if not normalized:\n return \"\"\n return _PROVIDER_ID_ALIASES.get(normalized, normalized)\n\n\ndef _normalize_provider(provider: Dict[str, Any]) -> Dict[str, Any]:\n normalized = dict(provider or {})\n canonical_id = _canonicalize_provider_id(normalized.get(\"id\"))\n if not canonical_id:\n return normalized\n raw_id = str(normalized.get(\"id\") or \"\").strip().lower()\n aliases = [str(item).strip() for item in normalized.get(\"aliases\") or [] if str(item).strip()]\n lowered_aliases = {item.lower() for item in aliases}\n if raw_id and raw_id != canonical_id and raw_id not in lowered_aliases:\n aliases.append(raw_id)\n normalized[\"id\"] = canonical_id\n if aliases:\n normalized[\"aliases\"] = aliases\n return normalized\n\n\ndef _build_auth_methods(provider: Dict[str, Any]) -> list[Dict[str, Any]]:\n provider_id = _canonicalize_provider_id(provider.get(\"id\"))\n if provider_id in _AUTH_METHODS_BY_PROVIDER:\n return deepcopy(_AUTH_METHODS_BY_PROVIDER[provider_id])\n return [\n {\n \"id\": \"api_key\",\n \"type\": \"api_key\",\n \"tier\": \"stable\",\n \"supports_local_reuse\": False,\n \"requires_browser_flow\": False,\n \"requires_fields\": [],\n \"requires_extra_fields\": [],\n }\n ]\n\n\ndef _merge_text_list(*groups: Any) -> list[str]:\n merged: list[str] = []\n seen: set[str] = set()\n for group in groups:\n for item in group or []:\n value = str(item or \"\").strip()\n if not value:\n continue\n lowered = value.lower()\n if lowered in seen:\n continue\n seen.add(lowered)\n merged.append(value)\n return merged\n\n\ndef _merge_auth_methods(existing: Any, fallback: Any) -> list[Dict[str, Any]]:\n merged: list[Dict[str, Any]] = []\n index_by_id: dict[str, int] = {}\n for item in existing or []:\n if not isinstance(item, dict):\n continue\n method = deepcopy(item)\n method_id = str(method.get(\"id\") or \"\").strip()\n if not method_id or method_id in index_by_id:\n continue\n index_by_id[method_id] = len(merged)\n merged.append(method)\n for item in fallback or []:\n if not isinstance(item, dict):\n continue\n method = deepcopy(item)\n method_id = str(method.get(\"id\") or \"\").strip()\n if not method_id:\n continue\n if method_id not in index_by_id:\n index_by_id[method_id] = len(merged)\n merged.append(method)\n continue\n current = merged[index_by_id[method_id]]\n for key, value in method.items():\n current_value = current.get(key)\n if current_value in (None, \"\"):\n current[key] = deepcopy(value)\n continue\n if isinstance(current_value, list) and not current_value:\n current[key] = deepcopy(value)\n return merged\n\n\ndef _merge_provider(provider: Dict[str, Any], fallback: Dict[str, Any]) -> Dict[str, Any]:\n merged = deepcopy(provider)\n normalized_fallback = _normalize_provider(fallback)\n merged[\"id\"] = _canonicalize_provider_id(merged.get(\"id\") or normalized_fallback.get(\"id\"))\n for field in (\"label\", \"base_url\", \"api_key_url\", \"default_model\"):\n if not str(merged.get(field) or \"\").strip() and str(normalized_fallback.get(field) or \"\").strip():\n merged[field] = normalized_fallback[field]\n if \"allow_empty_key\" not in merged and \"allow_empty_key\" in normalized_fallback:\n merged[\"allow_empty_key\"] = bool(normalized_fallback.get(\"allow_empty_key\"))\n aliases = _merge_text_list(merged.get(\"aliases\"), normalized_fallback.get(\"aliases\"))\n if aliases:\n merged[\"aliases\"] = aliases\n models = _merge_text_list(merged.get(\"models\"), normalized_fallback.get(\"models\"))\n if models:\n merged[\"models\"] = models\n fallback_auth_methods = normalized_fallback.get(\"auth_methods\") or _build_auth_methods(normalized_fallback)\n merged[\"auth_methods\"] = _merge_auth_methods(merged.get(\"auth_methods\"), fallback_auth_methods)\n return merged\n\n\ndef get_model_catalog_signature() -> tuple[int, int] | None:\n try:\n stat = get_model_catalog_path().stat()\n except OSError:\n return None\n return (int(stat.st_mtime_ns), int(stat.st_size))\n\n\ndef _catalog_payload() -> Dict[str, Any]:\n global _MODEL_CATALOG_CACHE_SIGNATURE, _MODEL_CATALOG_CACHE_PAYLOAD\n\n signature = get_model_catalog_signature()\n if _MODEL_CATALOG_CACHE_PAYLOAD is None or signature != _MODEL_CATALOG_CACHE_SIGNATURE:\n _MODEL_CATALOG_CACHE_PAYLOAD = load_model_catalog()\n _MODEL_CATALOG_CACHE_SIGNATURE = signature\n return _MODEL_CATALOG_CACHE_PAYLOAD or {}\n\n\ndef _build_provider_cache() -> tuple[list[Dict[str, Any]], Dict[str, Dict[str, Any]], Any]:\n payload = _catalog_payload()\n providers = [_normalize_provider(item) for item in payload.get(\"providers\") or [] if isinstance(item, dict)]\n index_by_id = {\n _canonicalize_provider_id(item.get(\"id\")): index\n for index, item in enumerate(providers)\n if _canonicalize_provider_id(item.get(\"id\"))\n }\n for extra in _EXTRA_PROVIDERS:\n extra_id = _canonicalize_provider_id(extra.get(\"id\"))\n normalized_extra = _normalize_provider(extra)\n if extra_id in index_by_id:\n providers[index_by_id[extra_id]] = _merge_provider(providers[index_by_id[extra_id]], normalized_extra)\n continue\n providers.append(_merge_provider(normalized_extra, normalized_extra))\n index_by_id[extra_id] = len(providers) - 1\n for index, provider in enumerate(providers):\n providers[index] = _merge_provider(provider, provider)\n providers_by_id = {\n _canonicalize_provider_id(provider.get(\"id\")): provider\n for provider in providers\n if _canonicalize_provider_id(provider.get(\"id\"))\n }\n return providers, providers_by_id, payload.get(\"updated_at\")\n\n\ndef _providers() -> list[Dict[str, Any]]:\n global _PROVIDER_CACHE_SIGNATURE, _PROVIDER_CACHE_UPDATED_AT, _PROVIDER_CACHE, _PROVIDER_BY_ID_CACHE\n\n signature = get_model_catalog_signature()\n if _PROVIDER_CACHE is None or signature != _PROVIDER_CACHE_SIGNATURE:\n providers, providers_by_id, updated_at = _build_provider_cache()\n _PROVIDER_CACHE = providers\n _PROVIDER_BY_ID_CACHE = providers_by_id\n _PROVIDER_CACHE_UPDATED_AT = updated_at\n _PROVIDER_CACHE_SIGNATURE = signature\n return _PROVIDER_CACHE or []\n\n\ndef _providers_by_id() -> Dict[str, Dict[str, Any]]:\n _providers()\n return _PROVIDER_BY_ID_CACHE or {}\n\n\ndef _catalog_updated_at() -> Any:\n _providers()\n return _PROVIDER_CACHE_UPDATED_AT\n\n\ndef get_model_catalog() -> Dict[str, Any]:\n return {\n \"updated_at\": _catalog_updated_at(),\n \"providers\": deepcopy(_providers()),\n }\n\n\ndef get_provider_by_id(provider_id: Optional[str]) -> Optional[Dict[str, Any]]:\n wanted = _canonicalize_provider_id(provider_id)\n if not wanted:\n return None\n provider = _providers_by_id().get(wanted)\n if provider is None:\n return None\n return deepcopy(provider)\n\n\ndef infer_provider_id(\n *,\n provider_id: Optional[str] = None,\n preset_name: Optional[str] = None,\n base_url: Optional[str] = None,\n model: Optional[str] = None,\n) -> Optional[str]:\n if provider_id:\n provider = get_provider_by_id(provider_id)\n if provider:\n return str(provider.get(\"id\") or \"\")\n\n lower_name = str(preset_name or \"\").strip().lower()\n lower_base_url = str(base_url or \"\").strip().lower()\n lower_model = str(model or \"\").strip().lower()\n\n for provider in _providers():\n provider_id_value = _canonicalize_provider_id(provider.get(\"id\"))\n provider_base_url = str(provider.get(\"base_url\") or \"\").strip().lower()\n if lower_base_url and provider_base_url and lower_base_url.startswith(provider_base_url):\n return provider_id_value\n if provider_id_value == \"qwen\" and lower_base_url:\n if \"dashscope.aliyuncs.com\" in lower_base_url:\n return provider_id_value\n if provider_id_value == \"anthropic\" and lower_base_url:\n if \"aiplatform.googleapis.com\" in lower_base_url and (\n \"/publishers/anthropic/\" in lower_base_url or \"claude\" in lower_model or \"claude\" in lower_name\n ):\n return provider_id_value\n if provider_id_value == \"google\" and lower_base_url:\n if lower_base_url.startswith(\"https://aiplatform.googleapis.com/\") or \"aiplatform.googleapis.com\" in lower_base_url:\n return provider_id_value\n if provider_id_value == \"zhipu\" and lower_base_url:\n if lower_base_url.startswith(\"https://open.bigmodel.cn/\") or \"open.bigmodel.cn\" in lower_base_url:\n return provider_id_value\n if provider_id_value == \"minimax\" and lower_base_url:\n if (\n lower_base_url.startswith(\"https://api.minimax.io/\")\n or \"api.minimax.io\" in lower_base_url\n or lower_base_url.startswith(\"https://api.minimaxi.com/\")\n or \"api.minimaxi.com\" in lower_base_url\n ):\n return provider_id_value\n\n for provider in _providers():\n provider_id_value = _canonicalize_provider_id(provider.get(\"id\"))\n\n aliases = [str(alias).strip().lower() for alias in provider.get(\"aliases\") or [] if alias]\n if lower_name and any(alias in lower_name for alias in aliases):\n return provider_id_value\n if lower_model and any(alias in lower_model for alias in aliases):\n return provider_id_value\n\n models = {str(item).strip().lower() for item in provider.get(\"models\") or [] if item}\n if lower_model and lower_model in models:\n return provider_id_value\n\n return None\n\n\ndef merge_provider_defaults(preset: Dict[str, Any]) -> Dict[str, Any]:\n merged = dict(preset or {})\n provider_id = infer_provider_id(\n provider_id=merged.get(\"provider_id\"),\n preset_name=merged.get(\"name\"),\n base_url=merged.get(\"base_url\"),\n model=merged.get(\"model\"),\n )\n if not provider_id:\n return merged\n\n provider = get_provider_by_id(provider_id)\n if not provider:\n return merged\n\n merged[\"provider_id\"] = provider_id\n merged.setdefault(\"base_url\", provider.get(\"base_url\"))\n merged.setdefault(\"allow_empty_key\", bool(provider.get(\"allow_empty_key\", False)))\n merged.setdefault(\"model\", provider.get(\"default_model\"))\n merged.setdefault(\"auth_methods\", deepcopy(provider.get(\"auth_methods\") or _build_auth_methods(provider)))\n return merged\n"}}, "src/main/shared-config.js": {"f83ed3b909121a7ff609c71569ffc99d7a196e6c": {"t": 1785674230.21401, "n": 35606, "h": "9bcd06c0f065646f", "c": "const fs = require('fs');\nconst { decodeBufferText } = require('./text-codec');\n\nconst PROVIDER_ID_ALIASES = {\n claude: 'anthropic',\n bailian: 'qwen',\n dashscope: 'qwen',\n moonshot: 'kimi',\n};\n\nconst EXTRA_MODEL_PROVIDERS = [\n {\n id: 'qwen',\n label: 'Qwen',\n base_url: 'https://dashscope.aliyuncs.com/compatible-mode/v1',\n api_key_url: 'https://dashscope.console.aliyun.com/apiKey',\n aliases: ['qwen', 'dashscope', 'bailian'],\n default_model: 'qwen3.5-plus',\n models: ['qwen3.5-plus', 'qwen3.5-flash', 'qwen3-max-2026-01-23', 'qwen-plus-latest', 'qwen-turbo-latest', 'qwen3-coder-next', 'qwen3-coder-plus', 'qwen3-coder-flash', 'MiniMax-M2.5', 'glm-5', 'glm-4.7', 'kimi-k2.5'],\n },\n {\n id: 'anthropic',\n label: 'Anthropic / Claude',\n base_url: 'https://api.anthropic.com/v1',\n api_key_url: 'https://platform.claude.com/settings/keys',\n aliases: ['anthropic', 'claude'],\n default_model: 'claude-sonnet-4-0',\n models: ['claude-sonnet-4-6', 'claude-opus-4-6', 'claude-haiku-4-5', 'claude-sonnet-4-5', 'claude-opus-4-5', 'claude-sonnet-4-0', 'claude-opus-4-1', 'claude-opus-4-0', 'claude-3-7-sonnet-latest', 'claude-3-5-haiku-latest'],\n },\n {\n id: 'google',\n label: 'Google / Gemini CLI',\n base_url: 'https://generativelanguage.googleapis.com/v1beta/openai',\n api_key_url: 'https://aistudio.google.com/apikey',\n aliases: ['google', 'gemini', 'vertex'],\n default_model: 'gemini-2.5-flash',\n models: ['gemini-3.1-pro-preview', 'gemini-3-flash-preview', 'gemini-3.1-flash-lite-preview', 'gemini-2.5-pro', 'gemini-2.5-flash', 'gemini-2.5-flash-lite'],\n },\n {\n id: 'yuanbao',\n label: 'Tencent Yuanbao',\n base_url: '',\n api_key_url: 'https://yuanbao.tencent.com/',\n aliases: ['yuanbao', '腾讯元宝'],\n default_model: 'yuanbao-web',\n models: ['yuanbao-web'],\n },\n {\n id: 'kimi',\n label: 'Kimi / Moonshot',\n base_url: 'https://api.moonshot.cn/v1',\n api_key_url: 'https://platform.moonshot.cn/console/api-keys',\n aliases: ['moonshot', 'kimi'],\n default_model: 'kimi-k2-turbo-preview',\n models: ['kimi-for-coding', 'kimi-k2-turbo-preview', 'kimi-k2-0905-preview', 'kimi-k2-thinking-turbo', 'kimi-thinking-preview', 'kimi-latest'],\n },\n {\n id: 'zhipu',\n label: 'Zhipu',\n base_url: 'https://open.bigmodel.cn/api/paas/v4',\n api_key_url: 'https://open.bigmodel.cn/usercenter/apikeys',\n aliases: ['zhipu', 'glm'],\n default_model: 'glm-5',\n models: ['glm-5', 'glm-4.7', 'glm-4.6', 'glm-4.5-air'],\n },\n {\n id: 'minimax',\n label: 'MiniMax',\n base_url: 'https://api.minimax.io/v1',\n api_key_url: 'https://platform.minimax.io/',\n aliases: ['minimax', 'minimaxi'],\n default_model: 'MiniMax-M2.5',\n models: ['MiniMax-M2.7', 'MiniMax-M2.7-highspeed', 'MiniMax-M2.5', 'MiniMax-M2.5-highspeed', 'MiniMax-M2.1', 'MiniMax-M2.1-highspeed', 'MiniMax-M2', 'MiniMax-Text-01'],\n },\n];\n\nconst AUTH_METHODS_BY_PROVIDER = {\n openai: [\n { id: 'api_key', type: 'api_key', tier: 'stable', supports_local_reuse: false, requires_browser_flow: false, requires_fields: [], requires_extra_fields: [], metadata: { recommended_base_url: 'https://api.openai.com/v1', recommended_model: 'gpt-5.4-mini' } },\n { id: 'codex_local', type: 'local_import', provider_id: 'openai_codex', tier: 'stable', supports_local_reuse: true, requires_browser_flow: true, requires_fields: [], requires_extra_fields: [], metadata: { recommended_base_url: 'https://api.openai.com/v1', recommended_model: 'gpt-5.4-mini' } },\n ],\n qwen: [\n { id: 'api_key', type: 'api_key', tier: 'stable', supports_local_reuse: false, requires_browser_flow: false, requires_fields: [], requires_extra_fields: [], metadata: { recommended_base_url: 'https://dashscope.aliyuncs.com/compatible-mode/v1', recommended_model: 'qwen3.5-plus', key_env_hint: 'DASHSCOPE_API_KEY' } },\n { id: 'qwen_oauth', type: 'oauth', provider_id: 'qwen_oauth', tier: 'stable', supports_local_reuse: true, requires_browser_flow: true, requires_fields: [], requires_extra_fields: [], metadata: { recommended_base_url: 'https://dashscope.aliyuncs.com/compatible-mode/v1', recommended_model: 'qwen3-coder-plus' } },\n { id: 'qwen_local', type: 'local_import', provider_id: 'qwen_oauth', tier: 'stable', supports_local_reuse: true, requires_browser_flow: true, requires_fields: [], requires_extra_fields: [], metadata: { recommended_base_url: 'https://dashscope.aliyuncs.com/compatible-mode/v1', recommended_model: 'qwen3-coder-plus' } },\n { id: 'coding_plan_api_key', type: 'api_key', tier: 'stable', supports_local_reuse: false, requires_browser_flow: false, requires_fields: [], requires_extra_fields: [], metadata: { recommended_base_url: 'https://coding.dashscope.aliyuncs.com/v1', recommended_model: 'qwen3-coder-next', key_env_hint: 'BAILIAN_CODING_PLAN_API_KEY', key_prefix_hint: 'sk-sp-', subscription: true } },\n ],\n google: [\n { id: 'api_key', type: 'api_key', tier: 'stable', supports_local_reuse: false, requires_browser_flow: false, requires_fields: [], requires_extra_fields: [], metadata: { recommended_base_url: 'https://generativelanguage.googleapis.com/v1beta/openai', recommended_model: 'gemini-2.5-flash' } },\n { id: 'google_oauth', type: 'oauth', provider_id: 'google_gemini_cli', tier: 'experimental', supports_local_reuse: true, requires_browser_flow: true, requires_fields: ['oauth_project_id'], requires_extra_fields: [], metadata: { recommended_base_url: 'https://generativelanguage.googleapis.com/v1beta/openai', recommended_model: 'gemini-2.5-flash' } },\n { id: 'gemini_cli_local', type: 'local_import', provider_id: 'google_gemini_cli', tier: 'experimental', supports_local_reuse: true, requires_browser_flow: true, requires_fields: ['oauth_project_id'], requires_extra_fields: [], metadata: { recommended_base_url: 'https://generativelanguage.googleapis.com/v1beta/openai', recommended_model: 'gemini-2.5-flash' } },\n ],\n anthropic: [\n { id: 'api_key', type: 'api_key', tier: 'stable', supports_local_reuse: false, requires_browser_flow: false, requires_fields: [], requires_extra_fields: [], metadata: { recommended_base_url: 'https://api.anthropic.com/v1', recommended_model: 'claude-sonnet-4-0' } },\n { id: 'claude_code_local', type: 'local_import', provider_id: 'claude_code_local', tier: 'experimental', supports_local_reuse: true, requires_browser_flow: true, requires_fields: [], requires_extra_fields: [], metadata: { recommended_base_url: 'https://api.anthropic.com/v1', recommended_model: 'claude-sonnet-4-0' } },\n { id: 'claude_code_oauth', type: 'oauth', provider_id: 'claude_code_local', tier: 'experimental', supports_local_reuse: true, requires_browser_flow: true, requires_fields: [], requires_extra_fields: [], metadata: { recommended_base_url: 'https://api.anthropic.com/v1', recommended_model: 'claude-sonnet-4-0' } },\n { id: 'claude_vertex_local', type: 'local_import', provider_id: 'claude_vertex_local', tier: 'experimental', supports_local_reuse: true, requires_browser_flow: true, requires_fields: ['oauth_project_id', 'oauth_location'], requires_extra_fields: [], metadata: { recommended_base_url: 'https://global-aiplatform.googleapis.com/v1/projects/{project}/locations/global/publishers/anthropic/models', recommended_model: 'claude-sonnet-4-6' } },\n ],\n kimi: [\n { id: 'api_key', type: 'api_key', tier: 'stable', supports_local_reuse: false, requires_browser_flow: false, requires_fields: [], requires_extra_fields: [], metadata: { recommended_base_url: 'https://api.moonshot.cn/v1', recommended_model: 'kimi-k2-turbo-preview' } },\n { id: 'kimi_code_oauth', type: 'oauth', provider_id: 'kimi_code_local', tier: 'experimental', supports_local_reuse: true, requires_browser_flow: true, requires_fields: [], requires_extra_fields: [], metadata: { recommended_base_url: 'https://api.kimi.com/coding/v1', recommended_model: 'kimi-for-coding' } },\n { id: 'kimi_code_local', type: 'local_import', provider_id: 'kimi_code_local', tier: 'experimental', supports_local_reuse: true, requires_browser_flow: true, requires_fields: [], requires_extra_fields: [], metadata: { recommended_base_url: 'https://api.kimi.com/coding/v1', recommended_model: 'kimi-for-coding' } },\n { id: 'coding_plan_api_key', type: 'api_key', tier: 'stable', supports_local_reuse: false, requires_browser_flow: false, requires_fields: [], requires_extra_fields: [], metadata: { recommended_base_url: 'https://api.kimi.com/coding/v1', recommended_model: 'kimi-for-coding', key_env_hint: 'KIMI_API_KEY' } },\n ],\n zhipu: [\n { id: 'api_key', type: 'api_key', tier: 'stable', supports_local_reuse: false, requires_browser_flow: false, requires_fields: [], requires_extra_fields: [], metadata: { recommended_base_url: 'https://open.bigmodel.cn/api/paas/v4', recommended_model: 'glm-5' } },\n { id: 'coding_plan_api_key', type: 'api_key', tier: 'stable', supports_local_reuse: false, requires_browser_flow: false, requires_fields: [], requires_extra_fields: [], metadata: { recommended_base_url: 'https://open.bigmodel.cn/api/coding/paas/v4', recommended_model: 'glm-5', subscription: true } },\n ],\n minimax: [\n { id: 'api_key', type: 'api_key', tier: 'stable', supports_local_reuse: false, requires_browser_flow: false, requires_fields: [], requires_extra_fields: [], metadata: { recommended_base_url: 'https://api.minimax.io/v1', recommended_model: 'MiniMax-M2.5' } },\n { id: 'coding_plan_api_key', type: 'api_key', tier: 'stable', supports_local_reuse: false, requires_browser_flow: false, requires_fields: [], requires_extra_fields: [], metadata: { recommended_base_url: 'https://api.minimax.io/v1', recommended_model: 'MiniMax-M2.5', regional_base_urls: ['https://api.minimax.io/v1', 'https://api.minimaxi.com/v1', 'https://api.minimax.io/anthropic', 'https://api.minimaxi.com/anthropic'], key_env_hint: 'MINIMAX_API_KEY', subscription: true } },\n ],\n yuanbao: [\n { id: 'yuanbao_web_session', type: 'web_session', provider_id: 'tencent_yuanbao', tier: 'experimental', supports_local_reuse: false, requires_browser_flow: true, requires_fields: [], requires_extra_fields: [], runtime_supported: false },\n ],\n};\n\nfunction canonicalizeProviderId(providerId) {\n const normalized = String(providerId || '').trim().toLowerCase();\n if (!normalized) {\n return '';\n }\n return PROVIDER_ID_ALIASES[normalized] || normalized;\n}\n\nfunction buildAuthMethods(provider = {}) {\n const providerId = canonicalizeProviderId(provider.id);\n if (AUTH_METHODS_BY_PROVIDER[providerId]) {\n return JSON.parse(JSON.stringify(AUTH_METHODS_BY_PROVIDER[providerId]));\n }\n return [\n { id: 'api_key', type: 'api_key', tier: 'stable', supports_local_reuse: false, requires_browser_flow: false, requires_fields: [], requires_extra_fields: [] },\n ];\n}\n\nfunction mergeTextList(...groups) {\n const merged = [];\n const seen = new Set();\n groups.forEach((group) => {\n (group || []).forEach((item) => {\n const value = String(item || '').trim();\n if (!value) {\n return;\n }\n const lowered = value.toLowerCase();\n if (seen.has(lowered)) {\n return;\n }\n seen.add(lowered);\n merged.push(value);\n });\n });\n return merged;\n}\n\nfunction mergeAuthMethods(existingMethods = [], fallbackMethods = []) {\n const merged = [];\n const indexById = new Map();\n\n (existingMethods || []).forEach((item) => {\n if (!item || typeof item !== 'object') {\n return;\n }\n const method = { ...item };\n const methodId = String(method.id || '').trim();\n if (!methodId || indexById.has(methodId)) {\n return;\n }\n indexById.set(methodId, merged.length);\n merged.push(method);\n });\n\n (fallbackMethods || []).forEach((item) => {\n if (!item || typeof item !== 'object') {\n return;\n }\n const method = { ...item };\n const methodId = String(method.id || '').trim();\n if (!methodId) {\n return;\n }\n if (!indexById.has(methodId)) {\n indexById.set(methodId, merged.length);\n merged.push(method);\n return;\n }\n const current = merged[indexById.get(methodId)];\n Object.entries(method).forEach(([key, value]) => {\n const currentValue = current[key];\n if (currentValue === undefined || currentValue === null || currentValue === '') {\n current[key] = Array.isArray(value) ? [...value] : value;\n return;\n }\n if (Array.isArray(currentValue) && !currentValue.length) {\n current[key] = Array.isArray(value) ? [...value] : value;\n }\n });\n });\n\n return merged;\n}\n\nfunction mergeProviderDetails(provider = {}, fallback = {}) {\n const nextProvider = { ...provider };\n const fallbackProvider = { ...fallback };\n const canonicalId = canonicalizeProviderId(nextProvider.id || fallbackProvider.id);\n const rawId = String(nextProvider.id || fallbackProvider.id || '').trim().toLowerCase();\n ['label', 'base_url', 'api_key_url', 'default_model'].forEach((fieldName) => {\n if (!String(nextProvider[fieldName] || '').trim() && String(fallbackProvider[fieldName] || '').trim()) {\n nextProvider[fieldName] = fallbackProvider[fieldName];\n }\n });\n if (nextProvider.allow_empty_key === undefined && fallbackProvider.allow_empty_key !== undefined) {\n nextProvider.allow_empty_key = !!fallbackProvider.allow_empty_key;\n }\n nextProvider.id = canonicalId || rawId;\n const aliases = mergeTextList(nextProvider.aliases, fallbackProvider.aliases);\n if (aliases.length) {\n nextProvider.aliases = aliases;\n }\n const models = mergeTextList(nextProvider.models, fallbackProvider.models);\n if (models.length) {\n nextProvider.models = models;\n }\n const fallbackAuthMethods = Array.isArray(fallbackProvider.auth_methods) && fallbackProvider.auth_methods.length\n ? fallbackProvider.auth_methods\n : buildAuthMethods({ id: nextProvider.id });\n nextProvider.auth_methods = mergeAuthMethods(nextProvider.auth_methods, fallbackAuthMethods);\n return nextProvider;\n}\n\nfunction enrichModelCatalog(payload = {}) {\n const providers = Array.isArray(payload.providers)\n ? payload.providers.map((provider) => mergeProviderDetails(provider))\n : [];\n const providerIndexById = new Map(\n providers\n .map((provider, index) => [canonicalizeProviderId(provider.id), index])\n .filter(([providerId]) => providerId),\n );\n EXTRA_MODEL_PROVIDERS.forEach((provider) => {\n const providerId = canonicalizeProviderId(provider.id);\n const nextProvider = mergeProviderDetails(provider);\n if (providerIndexById.has(providerId)) {\n const currentIndex = providerIndexById.get(providerId);\n providers[currentIndex] = mergeProviderDetails(providers[currentIndex], nextProvider);\n return;\n }\n providerIndexById.set(providerId, providers.length);\n providers.push(nextProvider);\n });\n providers.forEach((provider, index) => {\n providers[index] = mergeProviderDetails(provider);\n });\n return {\n ...payload,\n providers,\n };\n}\n\nfunction flattenPaths(input, prefix = '', output = {}) {\n if (!input || typeof input !== 'object' || Array.isArray(input)) {\n if (prefix) {\n output[prefix] = input;\n }\n return output;\n }\n const entries = Object.entries(input);\n if (!entries.length && prefix) {\n output[prefix] = input;\n return output;\n }\n for (const [key, value] of entries) {\n const nextPrefix = prefix ? `${prefix}.${key}` : key;\n if (value && typeof value === 'object' && !Array.isArray(value)) {\n flattenPaths(value, nextPrefix, output);\n } else {\n output[nextPrefix] = value;\n }\n }\n return output;\n}\n\nfunction diffConfigPaths(before = {}, after = {}) {\n const left = flattenPaths(before);\n const right = flattenPaths(after);\n const keys = new Set([...Object.keys(left), ...Object.keys(right)]);\n return [...keys].filter((key) => JSON.stringify(left[key]) !== JSON.stringify(right[key])).sort();\n}\n\nfunction buildFallbackReloadPlan(changedPaths = []) {\n const groups = new Map();\n const resolveEffect = (changedPath) => {\n if (changedPath.startsWith('api.') || changedPath.startsWith('agent.')) {\n return ['reinit', 'ai_client', '后端运行后会重建 AI 客户端'];\n }\n if (changedPath.startsWith('logging.')) {\n return ['live', 'logging', '后端运行后会重新应用日志配置'];\n }\n if (changedPath.startsWith('services.')) {\n return ['reinit', 'services', '相关服务会在后端运行后重新读取配置'];\n }\n if (changedPath.startsWith('bot.')) {\n return ['live', 'bot_runtime', '机器人运行后会应用新的运行参数'];\n }\n return ['unknown', 'unknown', '未定义明确生效策略,建议结合状态页确认'];\n };\n\n changedPaths.forEach((changedPath) => {\n const [mode, component, note] = resolveEffect(String(changedPath || ''));\n const key = `${mode}:${component}:${note}`;\n if (!groups.has(key)) {\n groups.set(key, { mode, component, note, paths: [] });\n }\n groups.get(key).paths.push(changedPath);\n });\n\n return Array.from(groups.values()).map((item) => ({\n ...item,\n paths: item.paths.sort(),\n }));\n}\n\nfunction sanitizeApplyError(error) {\n const rawMessage = String(error?.message || error || 'runtime apply failed').trim();\n return rawMessage.length > 180 ? `${rawMessage.slice(0, 177)}...` : rawMessage;\n}\n\nasync function applyRuntimeConfigFeedback({\n backendCheckServer,\n backendRequestJson,\n patch,\n changedPaths,\n fallbackReloadPlan,\n consoleImpl,\n}) {\n if (!changedPaths.length) {\n return {\n apply_mode: 'none',\n reload_plan: [],\n runtime_apply: null,\n default_config_synced: true,\n default_config_sync_message: '共享配置文件 app_config.json 已更新',\n };\n }\n\n let backendAvailable = false;\n try {\n backendAvailable = await backendCheckServer();\n } catch (error) {\n consoleImpl.warn('[SharedConfigService] backend availability check failed:', error);\n }\n\n if (!backendAvailable) {\n return {\n apply_mode: 'stopped',\n reload_plan: fallbackReloadPlan,\n runtime_apply: null,\n default_config_synced: true,\n default_config_sync_message: '共享配置文件 app_config.json 已更新;Python 服务未运行,启动后将读取新配置',\n };\n }\n\n try {\n const result = await backendRequestJson('POST', '/api/config', patch, 30000);\n const backendChangedPaths = Array.isArray(result?.changed_paths) ? result.changed_paths : changedPaths;\n const backendReloadPlan = Array.isArray(result?.reload_plan) && result.reload_plan.length\n ? result.reload_plan\n : fallbackReloadPlan;\n const runtimeApply = result?.runtime_apply || null;\n let applyMode = 'stopped';\n if (runtimeApply) {\n applyMode = runtimeApply.success === false ? 'failed' : 'immediate';\n } else if (!backendChangedPaths.length) {\n applyMode = 'none';\n }\n\n return {\n apply_mode: applyMode,\n changed_paths: backendChangedPaths,\n reload_plan: backendReloadPlan,\n runtime_apply: runtimeApply,\n default_config_synced: result?.default_config_synced !== false,\n default_config_sync_message: result?.default_config_sync_message\n || (runtimeApply ? '后端已确认运行时应用结果' : '配置已保存;机器人未运行时会在下次启动生效'),\n };\n } catch (error) {\n consoleImpl.warn('[SharedConfigService] runtime config apply failed:', error);\n return {\n apply_mode: 'watcher',\n reload_plan: fallbackReloadPlan,\n runtime_apply: {\n success: false,\n message: `配置已保存,但运行时热应用确认失败:${sanitizeApplyError(error)}`,\n },\n default_config_synced: true,\n default_config_sync_message: '共享配置文件 app_config.json 已更新;运行中实例将通过配置监听器尝试感知变更',\n };\n }\n}\n\nfunction inferProviderId(preset = {}) {\n const existing = canonicalizeProviderId(preset.provider_id);\n if (existing) {\n return existing;\n }\n const name = String(preset.name || '').trim().toLowerCase();\n const baseUrl = String(preset.base_url || '').trim().toLowerCase();\n const model = String(preset.model || '').trim().toLowerCase();\n const isMiniMax = name.includes('minimax')\n || model.includes('minimax')\n || baseUrl.includes('minimax.io')\n || baseUrl.includes('minimaxi.com');\n if (name.includes('ollama') || baseUrl.includes('11434')) return 'ollama';\n if (name.includes('openai') || baseUrl.includes('openai.com')) return 'openai';\n if (name.includes('deepseek') || baseUrl.includes('deepseek.com')) return 'deepseek';\n if (name.includes('qwen') || model.includes('qwen') || baseUrl.includes('dashscope')) return 'qwen';\n if (name.includes('zhipu') || model.includes('glm') || baseUrl.includes('open.bigmodel.cn')) return 'zhipu';\n if (isMiniMax) return 'minimax';\n if (baseUrl.includes('aiplatform.googleapis.com') && (baseUrl.includes('/publishers/anthropic/') || name.includes('claude') || model.includes('claude'))) return 'anthropic';\n if (name.includes('claude') || model.includes('claude') || baseUrl.includes('anthropic')) return 'anthropic';\n if (name.includes('gemini') || model.includes('gemini') || baseUrl.includes('generativelanguage') || baseUrl.includes('aiplatform.googleapis.com')) return 'google';\n if (name.includes('kimi') || name.includes('moonshot') || model.includes('kimi') || baseUrl.includes('moonshot.cn') || baseUrl.includes('api.kimi.com')) return 'kimi';\n if (name.includes('yuanbao') || model.includes('yuanbao') || baseUrl.includes('yuanbao.tencent.com')) return 'yuanbao';\n return '';\n}\n\nfunction maskPreset(preset = {}) {\n const nextPreset = { ...preset };\n nextPreset.provider_id = inferProviderId(nextPreset);\n const apiKey = String(nextPreset.api_key || '').trim();\n const allowEmptyKey = !!nextPreset.allow_empty_key;\n if (allowEmptyKey) {\n nextPreset.api_key_configured = false;\n nextPreset.api_key_masked = '';\n } else if (apiKey && !apiKey.startsWith('YOUR_')) {\n nextPreset.api_key_configured = true;\n nextPreset.api_key_masked = apiKey.length > 12 ? `${apiKey.slice(0, 8)}****${apiKey.slice(-4)}` : '****';\n } else {\n nextPreset.api_key_configured = false;\n nextPreset.api_key_masked = '';\n }\n nextPreset.api_key_required = !allowEmptyKey;\n delete nextPreset.api_key;\n return nextPreset;\n}\n\nfunction buildRendererConfigPayload(config = {}) {\n const api = { ...(config.api || {}) };\n api.presets = Array.isArray(api.presets) ? api.presets.map((preset) => maskPreset(preset)) : [];\n\n const bot = { ...(config.bot || {}) };\n delete bot.reply_timeout_fallback_text;\n delete bot.stream_buffer_chars;\n delete bot.stream_chunk_max_chars;\n delete bot.stream_reply;\n\n const agent = { ...(config.agent || {}) };\n agent.langsmith_api_key_configured = !!String(agent.langsmith_api_key || '').trim();\n delete agent.langsmith_api_key;\n delete agent.streaming_enabled;\n\n return {\n api,\n bot,\n logging: { ...(config.logging || {}) },\n agent,\n services: { ...(config.services || {}) },\n };\n}\n\nfunction readJsonFile(filePath, fallback = {}) {\n try {\n return JSON.parse(fs.readFileSync(filePath, 'utf8'));\n } catch (_) {\n return fallback;\n }\n}\n\nfunction atomicWriteJson(filePath, payload) {\n const rendered = `${JSON.stringify(payload, null, 2)}\\n`;\n const tempPath = `${filePath}.tmp`;\n fs.writeFileSync(tempPath, rendered, 'utf8');\n fs.renameSync(tempPath, filePath);\n}\n\nfunction createConfigCli({\n spawn,\n getBackendCommand,\n getSharedConfigPath,\n readJsonFileImpl = readJsonFile,\n fsModule = fs,\n decodeBufferImpl = decodeBufferText,\n}) {\n return {\n async run(commandArgs, options = {}) {\n const {\n stdinPayload = null,\n timeoutMs = 20000,\n } = options;\n const { cmd, args, options: spawnOptions } = getBackendCommand(commandArgs);\n\n return new Promise((resolve, reject) => {\n const child = spawn(cmd, args, {\n ...spawnOptions,\n stdio: ['pipe', 'pipe', 'pipe'],\n });\n const stdoutChunks = [];\n const stderrChunks = [];\n let settled = false;\n\n const finish = (error, value) => {\n if (settled) {\n return;\n }\n settled = true;\n if (timer) {\n clearTimeout(timer);\n }\n if (error) {\n reject(error);\n } else {\n resolve(value);\n }\n };\n\n const timer = setTimeout(() => {\n try {\n child.kill('SIGTERM');\n } catch (_) {}\n finish(new Error(`配置命令执行超时: ${commandArgs.join(' ')}`));\n }, timeoutMs);\n\n child.stdout.on('data', (chunk) => stdoutChunks.push(Buffer.from(chunk)));\n child.stderr.on('data', (chunk) => stderrChunks.push(Buffer.from(chunk)));\n child.on('error', (error) => finish(error));\n child.on('exit', (code) => {\n const stdout = decodeBufferImpl(Buffer.concat(stdoutChunks)).trim();\n const stderr = decodeBufferImpl(Buffer.concat(stderrChunks)).trim();\n if (code !== 0) {\n finish(new Error(stderr || stdout || `配置命令退出失败 (${code})`));\n return;\n }\n const lines = stdout.split(/\\r?\\n/).map((line) => line.trim()).filter(Boolean);\n const jsonLine = [...lines].reverse().find((line) => line.startsWith('{') && line.endsWith('}'));\n if (!jsonLine) {\n finish(new Error(stdout || '配置命令未返回 JSON 结果'));\n return;\n }\n try {\n finish(null, JSON.parse(jsonLine));\n } catch (_) {\n finish(new Error(`配置命令返回了不可解析 JSON: ${jsonLine}`));\n }\n });\n\n if (stdinPayload != null) {\n child.stdin.write(JSON.stringify(stdinPayload));\n }\n child.stdin.end();\n });\n },\n\n async ensureMigrated() {\n const configPath = getSharedConfigPath();\n if (fsModule.existsSync(configPath)) {\n return readJsonFileImpl(configPath, {});\n }\n const result = await this.run(['config', 'migrate', '--output', configPath], { timeoutMs: 30000 });\n return result.config || {};\n },\n\n async validate(patch = {}) {\n await this.ensureMigrated();\n const useStdin = !!patch && Object.keys(patch).length > 0;\n const args = ['config', 'validate', '--base-path', getSharedConfigPath()];\n if (useStdin) {\n args.push('--stdin');\n }\n const result = await this.run(args, {\n stdinPayload: useStdin ? patch : null,\n timeoutMs: 30000,\n });\n if (!result?.success) {\n throw new Error(result?.message || '配置校验失败');\n }\n return result.config || {};\n },\n\n async probe({ patch = null, presetName = '' } = {}) {\n await this.ensureMigrated();\n const useStdin = !!patch && Object.keys(patch).length > 0;\n const args = ['config', 'probe', '--base-path', getSharedConfigPath()];\n if (useStdin) {\n args.push('--stdin');\n }\n if (presetName) {\n args.push('--preset-name', presetName);\n }\n return this.run(args, {\n stdinPayload: useStdin ? patch : null,\n timeoutMs: 15000,\n });\n },\n };\n}\n\nfunction createSharedConfigService({\n ConfigCli,\n getSharedConfigPath,\n getSharedModelCatalogPath,\n ensureDir,\n buildRendererConfigPayloadImpl = buildRendererConfigPayload,\n readJsonFileImpl = readJsonFile,\n atomicWriteJsonImpl = atomicWriteJson,\n diffConfigPathsImpl = diffConfigPaths,\n listWindows,\n backendCheckServer,\n backendRequestJson,\n consoleImpl = console,\n fsModule = fs,\n}) {\n return {\n _cache: null,\n _watching: false,\n _writeQueue: Promise.resolve(),\n _modelCatalogCache: null,\n\n async ensureLoaded() {\n if (this._cache) {\n return this._cache;\n }\n await ConfigCli.ensureMigrated();\n this._cache = readJsonFileImpl(getSharedConfigPath(), {});\n this._ensureWatcher();\n return this._cache;\n },\n\n _ensureWatcher() {\n if (this._watching) {\n return;\n }\n const configPath = getSharedConfigPath();\n fsModule.watchFile(configPath, { interval: 500 }, async (current, previous) => {\n if (current.mtimeMs === previous.mtimeMs) {\n return;\n }\n try {\n this._cache = readJsonFileImpl(configPath, {});\n this.broadcast('external');\n } catch (error) {\n consoleImpl.error('[Config] reload failed:', error);\n }\n });\n this._watching = true;\n },\n\n getModelCatalog() {\n const catalogPath = getSharedModelCatalogPath();\n try {\n const stat = fsModule.statSync(catalogPath);\n if (\n this._modelCatalogCache\n && this._modelCatalogCache.mtimeMs === stat.mtimeMs\n ) {\n return this._modelCatalogCache.payload;\n }\n const payload = enrichModelCatalog(readJsonFileImpl(catalogPath, { providers: [] }));\n this._modelCatalogCache = {\n mtimeMs: stat.mtimeMs,\n payload,\n };\n return payload;\n } catch (_) {\n this._modelCatalogCache = null;\n return enrichModelCatalog({ providers: [] });\n }\n },\n\n buildPayload(config = {}) {\n return {\n success: true,\n ...buildRendererConfigPayloadImpl(config),\n modelCatalog: this.getModelCatalog(),\n configPath: getSharedConfigPath(),\n };\n },\n\n broadcast(source = 'external') {\n const payload = {\n ...this.buildPayload(this._cache || {}),\n source,\n };\n for (const win of listWindows()) {\n if (!win || (typeof win.isDestroyed === 'function' && win.isDestroyed())) {\n continue;\n }\n try {\n win.webContents.send('config:changed', payload);\n } catch (_) {}\n }\n },\n\n async get() {\n const config = await this.ensureLoaded();\n return this.buildPayload(config);\n },\n\n async patch(patch = {}) {\n let response = null;\n const task = this._writeQueue.catch(() => {}).then(async () => {\n const previous = JSON.parse(JSON.stringify(await this.ensureLoaded()));\n const nextConfig = await ConfigCli.validate(patch);\n const changedPaths = diffConfigPathsImpl(previous, nextConfig);\n const fallbackReloadPlan = buildFallbackReloadPlan(changedPaths);\n const configPath = getSharedConfigPath();\n ensureDir(require('path').dirname(configPath));\n atomicWriteJsonImpl(configPath, nextConfig);\n this._cache = nextConfig;\n const runtimeFeedback = await applyRuntimeConfigFeedback({\n backendCheckServer,\n backendRequestJson,\n patch,\n changedPaths,\n fallbackReloadPlan,\n consoleImpl,\n });\n response = {\n ...this.buildPayload(nextConfig),\n changed_paths: changedPaths,\n reload_plan: Array.isArray(runtimeFeedback.reload_plan) ? runtimeFeedback.reload_plan : fallbackReloadPlan,\n runtime_apply: runtimeFeedback.runtime_apply || null,\n apply_mode: runtimeFeedback.apply_mode || 'unknown',\n default_config_synced: runtimeFeedback.default_config_synced !== false,\n default_config_sync_message: runtimeFeedback.default_config_sync_message || '共享配置文件 app_config.json 已更新',\n message: changedPaths.length ? '配置已保存' : '未检测到配置变更',\n save_state: 'saved',\n };\n });\n this._writeQueue = task.then(() => null, () => null);\n await task;\n this.broadcast('main_write');\n return response;\n },\n\n async testConnection(options = {}) {\n const patch = options?.patch && typeof options.patch === 'object' ? options.patch : null;\n const presetName = String(options?.presetName || '').trim();\n if (await backendCheckServer()) {\n try {\n return await backendRequestJson(\n 'POST',\n '/api/test_connection',\n {\n preset_name: presetName || null,\n patch,\n },\n 12000,\n );\n } catch (error) {\n consoleImpl.warn('[SharedConfigService] live connection test failed, fallback to CLI:', error);\n }\n }\n return ConfigCli.probe({ patch, presetName });\n },\n\n async subscribe() {\n await this.ensureLoaded();\n this._ensureWatcher();\n return this.buildPayload(this._cache || {});\n },\n };\n}\n\nmodule.exports = {\n atomicWriteJson,\n buildRendererConfigPayload,\n createConfigCli,\n createSharedConfigService,\n diffConfigPaths,\n enrichModelCatalog,\n inferProviderId,\n maskPreset,\n readJsonFile,\n};\n"}}}}