chcode 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- chcode/__init__.py +0 -0
- chcode/__main__.py +5 -0
- chcode/agent_setup.py +395 -0
- chcode/agents/__init__.py +0 -0
- chcode/agents/definitions.py +158 -0
- chcode/agents/loader.py +104 -0
- chcode/agents/runner.py +159 -0
- chcode/chat.py +1630 -0
- chcode/cli.py +142 -0
- chcode/config.py +571 -0
- chcode/display.py +325 -0
- chcode/prompts.py +640 -0
- chcode/session.py +149 -0
- chcode/skill_manager.py +165 -0
- chcode/utils/__init__.py +3 -0
- chcode/utils/enhanced_chat_openai.py +368 -0
- chcode/utils/git_checker.py +38 -0
- chcode/utils/git_manager.py +261 -0
- chcode/utils/modelscope_ratelimit.py +65 -0
- chcode/utils/multimodal.py +268 -0
- chcode/utils/shell/__init__.py +17 -0
- chcode/utils/shell/output.py +63 -0
- chcode/utils/shell/provider.py +128 -0
- chcode/utils/shell/result.py +14 -0
- chcode/utils/shell/semantics.py +55 -0
- chcode/utils/shell/session.py +159 -0
- chcode/utils/skill_loader.py +565 -0
- chcode/utils/text_utils.py +14 -0
- chcode/utils/tool_result_pipeline.py +244 -0
- chcode/utils/tools.py +1724 -0
- chcode/vision_config.py +371 -0
- chcode-0.1.0.dist-info/METADATA +275 -0
- chcode-0.1.0.dist-info/RECORD +36 -0
- chcode-0.1.0.dist-info/WHEEL +4 -0
- chcode-0.1.0.dist-info/entry_points.txt +2 -0
- chcode-0.1.0.dist-info/licenses/LICENSE +21 -0
chcode/prompts.py
ADDED
|
@@ -0,0 +1,640 @@
|
|
|
1
|
+
"""
|
|
2
|
+
统一交互层 — 所有用户交互都通过此模块
|
|
3
|
+
|
|
4
|
+
用 questionary 实现下拉列表、确认框、文本输入等。
|
|
5
|
+
在 async 上下文中用 asyncio.to_thread 包装同步的 questionary 调用。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import os
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
import questionary
|
|
15
|
+
from rich.console import Console
|
|
16
|
+
|
|
17
|
+
console = Console()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
async def select(
|
|
21
|
+
message: str,
|
|
22
|
+
choices: list[str],
|
|
23
|
+
default: str | None = None,
|
|
24
|
+
) -> str | None:
|
|
25
|
+
"""下拉单选"""
|
|
26
|
+
|
|
27
|
+
def _ask():
|
|
28
|
+
return questionary.select(
|
|
29
|
+
message=message,
|
|
30
|
+
choices=choices,
|
|
31
|
+
default=default,
|
|
32
|
+
).ask()
|
|
33
|
+
|
|
34
|
+
return await asyncio.to_thread(_ask)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
async def confirm(message: str, default: bool = True) -> bool:
|
|
38
|
+
"""确认框"""
|
|
39
|
+
|
|
40
|
+
def _ask():
|
|
41
|
+
return questionary.confirm(
|
|
42
|
+
message=message,
|
|
43
|
+
default=default,
|
|
44
|
+
).ask()
|
|
45
|
+
|
|
46
|
+
return await asyncio.to_thread(_ask)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
async def checkbox(message: str, choices: list[str]) -> list[str]:
|
|
50
|
+
"""多选框"""
|
|
51
|
+
|
|
52
|
+
def _ask():
|
|
53
|
+
return questionary.checkbox(message=message, choices=choices).ask()
|
|
54
|
+
|
|
55
|
+
return await asyncio.to_thread(_ask) or []
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
async def text(message: str, default: str = "") -> str:
|
|
59
|
+
"""文本输入"""
|
|
60
|
+
|
|
61
|
+
def _ask():
|
|
62
|
+
return questionary.text(
|
|
63
|
+
message=message,
|
|
64
|
+
default=default,
|
|
65
|
+
).ask()
|
|
66
|
+
|
|
67
|
+
return await asyncio.to_thread(_ask)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
async def password(message: str) -> str:
|
|
71
|
+
"""密码输入(隐藏回显)"""
|
|
72
|
+
|
|
73
|
+
def _ask():
|
|
74
|
+
return questionary.password(
|
|
75
|
+
message=message,
|
|
76
|
+
).ask()
|
|
77
|
+
|
|
78
|
+
return await asyncio.to_thread(_ask)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
async def select_or_custom(
|
|
82
|
+
message: str,
|
|
83
|
+
preset_choices: list[str],
|
|
84
|
+
custom_label: str = "自定义输入...",
|
|
85
|
+
custom_prompt: str = "请输入: ",
|
|
86
|
+
default: str | None = None,
|
|
87
|
+
) -> str:
|
|
88
|
+
"""下拉选择 + 自定义输入。末尾有「自定义输入...」选项。"""
|
|
89
|
+
choices = list(preset_choices) + [custom_label]
|
|
90
|
+
result = await select(message, choices, default=default)
|
|
91
|
+
if result == custom_label:
|
|
92
|
+
return await text(custom_prompt)
|
|
93
|
+
return result
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# ─── 模型配置表单专用 ──────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
BASE_URL_PRESETS = [
|
|
99
|
+
"https://api.openai.com/v1",
|
|
100
|
+
"https://api-inference.modelscope.cn/v1",
|
|
101
|
+
"https://open.bigmodel.cn/api/paas/v4",
|
|
102
|
+
"https://api.deepseek.com/v1",
|
|
103
|
+
"https://dashscope.aliyuncs.com/compatible-mode/v1",
|
|
104
|
+
"https://api.longcat.chat/openai/v1",
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
MODELSCOPE_BASE_URL = "https://api-inference.modelscope.cn/v1"
|
|
108
|
+
|
|
109
|
+
MODELSCOPE_PRESETS = [
|
|
110
|
+
{
|
|
111
|
+
"model": "ZhipuAI/GLM-5",
|
|
112
|
+
"base_url": MODELSCOPE_BASE_URL,
|
|
113
|
+
"temperature": 1.0,
|
|
114
|
+
"top_p": 0.95,
|
|
115
|
+
"stream_usage": True,
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"model": "Qwen/Qwen3-235B-A22B-Thinking-2507",
|
|
119
|
+
"base_url": MODELSCOPE_BASE_URL,
|
|
120
|
+
"temperature": 0.6,
|
|
121
|
+
"top_p": 0.95,
|
|
122
|
+
"stream_usage": True,
|
|
123
|
+
"extra_body": {"top_k": 20},
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
"model": "Qwen/Qwen3-235B-A22B-Instruct-2507",
|
|
127
|
+
"base_url": MODELSCOPE_BASE_URL,
|
|
128
|
+
"temperature": 0.7,
|
|
129
|
+
"top_p": 0.8,
|
|
130
|
+
"stream_usage": True,
|
|
131
|
+
"extra_body": {"top_k": 20},
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
"model": "Qwen/Qwen3.5-397B-A17B",
|
|
135
|
+
"base_url": MODELSCOPE_BASE_URL,
|
|
136
|
+
"temperature": 0.6,
|
|
137
|
+
"top_p": 0.95,
|
|
138
|
+
"stream_usage": True,
|
|
139
|
+
"extra_body": {"top_k": 20, "repetition_penalty": 1.0},
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
"model": "deepseek-ai/DeepSeek-V3.2",
|
|
143
|
+
"base_url": MODELSCOPE_BASE_URL,
|
|
144
|
+
"temperature": 1.0,
|
|
145
|
+
"top_p": 0.95,
|
|
146
|
+
"stream_usage": True,
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
"model": "MiniMax/MiniMax-M2.5",
|
|
150
|
+
"base_url": MODELSCOPE_BASE_URL,
|
|
151
|
+
"temperature": 1.0,
|
|
152
|
+
"top_p": 0.95,
|
|
153
|
+
"stream_usage": True,
|
|
154
|
+
"extra_body": {"top_k": 40},
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
"model": "moonshotai/Kimi-K2.5",
|
|
158
|
+
"base_url": MODELSCOPE_BASE_URL,
|
|
159
|
+
"temperature": 1.0,
|
|
160
|
+
"top_p": 0.95,
|
|
161
|
+
"stream_usage": True,
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
"model": "ZhipuAI/GLM-5.1",
|
|
165
|
+
"base_url": MODELSCOPE_BASE_URL,
|
|
166
|
+
"temperature": 1.0,
|
|
167
|
+
"top_p": 0.95,
|
|
168
|
+
"stream_usage": True,
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
"model": "Qwen/Qwen3-Coder-480B-A35B-Instruct",
|
|
172
|
+
"base_url": MODELSCOPE_BASE_URL,
|
|
173
|
+
"temperature": 0.7,
|
|
174
|
+
"top_p": 0.8,
|
|
175
|
+
"stream_usage": True,
|
|
176
|
+
"extra_body": {"top_k": 20, "repetition_penalty": 1.05},
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
"model": "XiaomiMiMo/MiMo-V2-Flash",
|
|
180
|
+
"base_url": MODELSCOPE_BASE_URL,
|
|
181
|
+
"temperature": 0.3,
|
|
182
|
+
"top_p": 0.95,
|
|
183
|
+
"stream_usage": True,
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
"model": "deepseek-ai/DeepSeek-R1-0528",
|
|
187
|
+
"base_url": MODELSCOPE_BASE_URL,
|
|
188
|
+
"temperature": 0.6,
|
|
189
|
+
"top_p": 0.95,
|
|
190
|
+
"stream_usage": True,
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
"model": "Qwen/Qwen3-Next-80B-A3B-Thinking",
|
|
194
|
+
"base_url": MODELSCOPE_BASE_URL,
|
|
195
|
+
"temperature": 0.6,
|
|
196
|
+
"top_p": 0.95,
|
|
197
|
+
"stream_usage": True,
|
|
198
|
+
"extra_body": {"top_k": 20},
|
|
199
|
+
},
|
|
200
|
+
# {
|
|
201
|
+
# "model": "MiniMax/MiniMax-M2.7",
|
|
202
|
+
# "base_url": MODELSCOPE_BASE_URL,
|
|
203
|
+
# "temperature": 1.0,
|
|
204
|
+
# "top_p": 0.95,
|
|
205
|
+
# "stream_usage": True,
|
|
206
|
+
# "extra_body": {"top_k": 40},
|
|
207
|
+
# },
|
|
208
|
+
{
|
|
209
|
+
"model": "deepseek-ai/DeepSeek-V4-Pro",
|
|
210
|
+
"base_url": MODELSCOPE_BASE_URL,
|
|
211
|
+
"temperature": 1.0,
|
|
212
|
+
"top_p": 1.0,
|
|
213
|
+
"stream_usage": True,
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
"model": "deepseek-ai/DeepSeek-V4-Flash",
|
|
217
|
+
"base_url": MODELSCOPE_BASE_URL,
|
|
218
|
+
"temperature": 1.0,
|
|
219
|
+
"top_p": 1.0,
|
|
220
|
+
"stream_usage": True,
|
|
221
|
+
},
|
|
222
|
+
]
|
|
223
|
+
|
|
224
|
+
API_KEY_ENV_VARS = [
|
|
225
|
+
("BIGMODEL_API_KEY", "智谱 GLM"),
|
|
226
|
+
("ModelScopeToken", "ModelScope"),
|
|
227
|
+
("OPENAI_API_KEY", "OpenAI"),
|
|
228
|
+
("DEEPSEEK_API_KEY", "DeepSeek"),
|
|
229
|
+
("DASHSCOPE_API_KEY", "通义千问"),
|
|
230
|
+
("ANTHROPIC_API_KEY", "Anthropic Claude"),
|
|
231
|
+
("LONGCAT_API_KEY", "LongCat"),
|
|
232
|
+
]
|
|
233
|
+
|
|
234
|
+
TEMPERATURE_PRESETS = ["0", "0.3", "0.5", "0.6", "0.7", "1.0", "1.5", "2.0"]
|
|
235
|
+
TOP_P_PRESETS = ["0.5", "0.7", "0.9", "0.95", "1.0"]
|
|
236
|
+
TOP_K_PRESETS = ["1", "5", "10", "20", "40", "50"]
|
|
237
|
+
MAX_COMPLETION_TOKENS_PRESETS = ["32768", "65536", "122880", "204800"]
|
|
238
|
+
FREQ_PENALTY_PRESETS = ["0", "0.2", "0.5", "1.0", "1.5", "2.0"]
|
|
239
|
+
PRESENCE_PENALTY_PRESETS = ["0", "0.2", "0.5", "1.0", "1.5", "2.0"]
|
|
240
|
+
|
|
241
|
+
SKIP_LABEL = "跳过 (不设置)"
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class _SkipSentinel:
|
|
245
|
+
"""哨兵对象,区分「跳过此字段」和「用户取消整个表单」。"""
|
|
246
|
+
|
|
247
|
+
_instance = None
|
|
248
|
+
|
|
249
|
+
def __new__(cls):
|
|
250
|
+
if cls._instance is None:
|
|
251
|
+
cls._instance = super().__new__(cls)
|
|
252
|
+
return cls._instance
|
|
253
|
+
|
|
254
|
+
def __repr__(self):
|
|
255
|
+
return "SKIP"
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
_SKIP = _SkipSentinel()
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
async def _ask_hyperparam(
|
|
262
|
+
message: str,
|
|
263
|
+
preset_choices: list[str],
|
|
264
|
+
existing_value: str | None = None,
|
|
265
|
+
custom_prompt: str = "请输入: ",
|
|
266
|
+
) -> Any:
|
|
267
|
+
"""单个超参输入,支持「跳过」。返回值 / _SKIP / None(取消)。"""
|
|
268
|
+
choices = [SKIP_LABEL] + list(preset_choices) + ["自定义输入..."]
|
|
269
|
+
|
|
270
|
+
default = None
|
|
271
|
+
if existing_value is not None and existing_value in preset_choices:
|
|
272
|
+
default = existing_value
|
|
273
|
+
|
|
274
|
+
result = await select(message, choices, default=default)
|
|
275
|
+
if result is None:
|
|
276
|
+
return None
|
|
277
|
+
if result == SKIP_LABEL:
|
|
278
|
+
return _SKIP
|
|
279
|
+
if result == "自定义输入...":
|
|
280
|
+
raw = await text(custom_prompt)
|
|
281
|
+
if raw is None or raw.strip() == "":
|
|
282
|
+
return _SKIP
|
|
283
|
+
return raw.strip()
|
|
284
|
+
return result
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
async def model_config_form(
|
|
288
|
+
existing_config: dict | None = None,
|
|
289
|
+
) -> dict | None:
|
|
290
|
+
"""
|
|
291
|
+
模型配置表单 — 全部用下拉列表 + 文本输入
|
|
292
|
+
|
|
293
|
+
Args:
|
|
294
|
+
existing_config: 现有配置(编辑模式)
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
配置字典,用户取消返回 None
|
|
298
|
+
"""
|
|
299
|
+
import os
|
|
300
|
+
|
|
301
|
+
cfg = dict(existing_config) if existing_config else {}
|
|
302
|
+
|
|
303
|
+
# ─── 必填字段 ───
|
|
304
|
+
is_editing = bool(cfg)
|
|
305
|
+
KEEP_LABEL = "保持当前值"
|
|
306
|
+
|
|
307
|
+
# ── 模型名称 ──
|
|
308
|
+
model_name = cfg.get("model", "")
|
|
309
|
+
if not model_name:
|
|
310
|
+
model_name = await text("输入模型名称: ")
|
|
311
|
+
if not model_name or not model_name.strip():
|
|
312
|
+
return None
|
|
313
|
+
model_name = model_name.strip()
|
|
314
|
+
|
|
315
|
+
# ── Base URL ──
|
|
316
|
+
base_url = cfg.get("base_url", "")
|
|
317
|
+
if is_editing and base_url:
|
|
318
|
+
_keep_url = f"{KEEP_LABEL} ({base_url})"
|
|
319
|
+
_url_choices = [_keep_url] + list(BASE_URL_PRESETS) + ["自定义输入..."]
|
|
320
|
+
result = await select("选择 API Base URL:", _url_choices, default=_keep_url)
|
|
321
|
+
if result is None:
|
|
322
|
+
return None
|
|
323
|
+
if result == _keep_url:
|
|
324
|
+
pass # base_url unchanged
|
|
325
|
+
elif result == "自定义输入...":
|
|
326
|
+
base_url = await text("输入 Base URL: ")
|
|
327
|
+
if not base_url or not base_url.strip():
|
|
328
|
+
return None
|
|
329
|
+
else:
|
|
330
|
+
base_url = result
|
|
331
|
+
else:
|
|
332
|
+
_url_choices = ["魔搭 (ModelScope)"] + list(BASE_URL_PRESETS) + ["自定义输入..."]
|
|
333
|
+
result = await select("选择 API Base URL:", _url_choices)
|
|
334
|
+
if result is None:
|
|
335
|
+
return None
|
|
336
|
+
if result == "魔搭 (ModelScope)":
|
|
337
|
+
base_url = MODELSCOPE_BASE_URL
|
|
338
|
+
elif result == "自定义输入...":
|
|
339
|
+
base_url = await text("输入 Base URL: ")
|
|
340
|
+
if not base_url or not base_url.strip():
|
|
341
|
+
return None
|
|
342
|
+
else:
|
|
343
|
+
base_url = result
|
|
344
|
+
|
|
345
|
+
# API Key — 先展示环境变量快捷选择
|
|
346
|
+
existing_api_key = cfg.get("api_key", "")
|
|
347
|
+
|
|
348
|
+
env_choices = [
|
|
349
|
+
f"{var} ({desc})" for var, desc in API_KEY_ENV_VARS if os.getenv(var)
|
|
350
|
+
]
|
|
351
|
+
|
|
352
|
+
if is_editing:
|
|
353
|
+
_masked = (
|
|
354
|
+
existing_api_key[:6] + "****" + existing_api_key[-4:]
|
|
355
|
+
if len(existing_api_key) > 10
|
|
356
|
+
else "****"
|
|
357
|
+
)
|
|
358
|
+
env_choices.insert(0, f"保持当前 Key ({_masked})")
|
|
359
|
+
|
|
360
|
+
env_choices.append("手动输入 API Key...")
|
|
361
|
+
if env_choices:
|
|
362
|
+
result = await select("选择 API Key 来源:", env_choices)
|
|
363
|
+
if result is None:
|
|
364
|
+
return None
|
|
365
|
+
if result.startswith("保持当前 Key"):
|
|
366
|
+
api_key = existing_api_key
|
|
367
|
+
elif result == "手动输入 API Key...":
|
|
368
|
+
api_key = await password("输入 API Key: ")
|
|
369
|
+
else:
|
|
370
|
+
var_name = result.split(" (")[0]
|
|
371
|
+
api_key = os.getenv(var_name, "")
|
|
372
|
+
else:
|
|
373
|
+
api_key = await password("输入 API Key: ")
|
|
374
|
+
|
|
375
|
+
if not api_key:
|
|
376
|
+
console.print("[red]API Key 不能为空[/red]")
|
|
377
|
+
return None
|
|
378
|
+
|
|
379
|
+
config: dict[str, Any] = {
|
|
380
|
+
"model": model_name,
|
|
381
|
+
"base_url": base_url,
|
|
382
|
+
"api_key": api_key,
|
|
383
|
+
"stream_usage": True,
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
# 编辑旧配置时清理已移除的 max_tokens 字段
|
|
387
|
+
cfg.pop("max_tokens", None)
|
|
388
|
+
|
|
389
|
+
# ─── 超参(可选) ───
|
|
390
|
+
want_hyperparams = await confirm("配置超参数?", default=False)
|
|
391
|
+
if want_hyperparams:
|
|
392
|
+
# temperature
|
|
393
|
+
t_val = str(cfg["temperature"]) if "temperature" in cfg else None
|
|
394
|
+
result = await _ask_hyperparam(
|
|
395
|
+
"Temperature:",
|
|
396
|
+
TEMPERATURE_PRESETS,
|
|
397
|
+
existing_value=t_val,
|
|
398
|
+
custom_prompt="输入 temperature: ",
|
|
399
|
+
)
|
|
400
|
+
if result is None:
|
|
401
|
+
return None
|
|
402
|
+
if result is not _SKIP:
|
|
403
|
+
config["temperature"] = float(result)
|
|
404
|
+
else:
|
|
405
|
+
config.pop("temperature", None)
|
|
406
|
+
|
|
407
|
+
# top_p
|
|
408
|
+
tp_val = str(cfg["top_p"]) if "top_p" in cfg else None
|
|
409
|
+
result = await _ask_hyperparam(
|
|
410
|
+
"Top P:",
|
|
411
|
+
TOP_P_PRESETS,
|
|
412
|
+
existing_value=tp_val,
|
|
413
|
+
custom_prompt="输入 top_p: ",
|
|
414
|
+
)
|
|
415
|
+
if result is None:
|
|
416
|
+
return None
|
|
417
|
+
if result is not _SKIP:
|
|
418
|
+
config["top_p"] = float(result)
|
|
419
|
+
else:
|
|
420
|
+
config.pop("top_p", None)
|
|
421
|
+
|
|
422
|
+
# top_k → extra_body
|
|
423
|
+
existing_extra = cfg.get("extra_body", {})
|
|
424
|
+
tk_val = (
|
|
425
|
+
str(existing_extra["top_k"])
|
|
426
|
+
if isinstance(existing_extra, dict) and "top_k" in existing_extra
|
|
427
|
+
else None
|
|
428
|
+
)
|
|
429
|
+
result = await _ask_hyperparam(
|
|
430
|
+
"Top K:",
|
|
431
|
+
TOP_K_PRESETS,
|
|
432
|
+
existing_value=tk_val,
|
|
433
|
+
custom_prompt="输入 top_k: ",
|
|
434
|
+
)
|
|
435
|
+
if result is None:
|
|
436
|
+
return None
|
|
437
|
+
if result is not _SKIP:
|
|
438
|
+
# 合并到已有的 extra_body(可能已有 max_completion_tokens)
|
|
439
|
+
_eb = dict(existing_extra) if isinstance(existing_extra, dict) else {}
|
|
440
|
+
_eb["top_k"] = int(result)
|
|
441
|
+
config["extra_body"] = _eb
|
|
442
|
+
else:
|
|
443
|
+
# 跳过 top_k,但仍保留 extra_body 中的其他字段(如 max_completion_tokens)
|
|
444
|
+
if isinstance(existing_extra, dict):
|
|
445
|
+
_eb = {k: v for k, v in existing_extra.items() if k != "top_k"}
|
|
446
|
+
if _eb:
|
|
447
|
+
config["extra_body"] = _eb
|
|
448
|
+
|
|
449
|
+
# max_completion_tokens → extra_body
|
|
450
|
+
_eb = config.get("extra_body", {})
|
|
451
|
+
mct_val = (
|
|
452
|
+
str(_eb["max_completion_tokens"])
|
|
453
|
+
if isinstance(_eb, dict) and "max_completion_tokens" in _eb
|
|
454
|
+
else None
|
|
455
|
+
)
|
|
456
|
+
result = await _ask_hyperparam(
|
|
457
|
+
"Max Completion Tokens:",
|
|
458
|
+
MAX_COMPLETION_TOKENS_PRESETS,
|
|
459
|
+
existing_value=mct_val,
|
|
460
|
+
custom_prompt="输入 max_completion_tokens: ",
|
|
461
|
+
)
|
|
462
|
+
if result is None:
|
|
463
|
+
return None
|
|
464
|
+
if result is not _SKIP:
|
|
465
|
+
_eb = dict(_eb) if isinstance(_eb, dict) else {}
|
|
466
|
+
_eb["max_completion_tokens"] = int(result)
|
|
467
|
+
config["extra_body"] = _eb
|
|
468
|
+
else:
|
|
469
|
+
if isinstance(_eb, dict):
|
|
470
|
+
_eb = {k: v for k, v in _eb.items() if k != "max_completion_tokens"}
|
|
471
|
+
if _eb:
|
|
472
|
+
config["extra_body"] = _eb
|
|
473
|
+
else:
|
|
474
|
+
config.pop("extra_body", None)
|
|
475
|
+
|
|
476
|
+
# stop_sequences
|
|
477
|
+
ss_val = None
|
|
478
|
+
if "stop_sequences" in cfg:
|
|
479
|
+
v = cfg["stop_sequences"]
|
|
480
|
+
ss_val = ", ".join(str(x) for x in v) if isinstance(v, list) else str(v)
|
|
481
|
+
result = await _ask_hyperparam(
|
|
482
|
+
"Stop Sequences:",
|
|
483
|
+
[], # 无预设,只有自定义
|
|
484
|
+
existing_value=ss_val,
|
|
485
|
+
custom_prompt="输入停止序列 (逗号分隔): ",
|
|
486
|
+
)
|
|
487
|
+
if result is None:
|
|
488
|
+
return None
|
|
489
|
+
if result is not _SKIP:
|
|
490
|
+
config["stop_sequences"] = [
|
|
491
|
+
s.strip() for s in str(result).split(",") if s.strip()
|
|
492
|
+
]
|
|
493
|
+
else:
|
|
494
|
+
config.pop("stop_sequences", None)
|
|
495
|
+
|
|
496
|
+
# frequency_penalty
|
|
497
|
+
fp_val = str(cfg["frequency_penalty"]) if "frequency_penalty" in cfg else None
|
|
498
|
+
result = await _ask_hyperparam(
|
|
499
|
+
"Frequency Penalty:",
|
|
500
|
+
FREQ_PENALTY_PRESETS,
|
|
501
|
+
existing_value=fp_val,
|
|
502
|
+
custom_prompt="输入 frequency_penalty: ",
|
|
503
|
+
)
|
|
504
|
+
if result is None:
|
|
505
|
+
return None
|
|
506
|
+
if result is not _SKIP:
|
|
507
|
+
config["frequency_penalty"] = float(result)
|
|
508
|
+
else:
|
|
509
|
+
config.pop("frequency_penalty", None)
|
|
510
|
+
|
|
511
|
+
# presence_penalty
|
|
512
|
+
pp_val = str(cfg["presence_penalty"]) if "presence_penalty" in cfg else None
|
|
513
|
+
result = await _ask_hyperparam(
|
|
514
|
+
"Presence Penalty:",
|
|
515
|
+
PRESENCE_PENALTY_PRESETS,
|
|
516
|
+
existing_value=pp_val,
|
|
517
|
+
custom_prompt="输入 presence_penalty: ",
|
|
518
|
+
)
|
|
519
|
+
if result is None:
|
|
520
|
+
return None
|
|
521
|
+
if result is not _SKIP:
|
|
522
|
+
config["presence_penalty"] = float(result)
|
|
523
|
+
else:
|
|
524
|
+
config.pop("presence_penalty", None)
|
|
525
|
+
|
|
526
|
+
# max_retries - 固定为 4(失败 5 次后自动切换备用模型),不可配置
|
|
527
|
+
config["max_retries"] = 4
|
|
528
|
+
|
|
529
|
+
return config
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
async def configure_modelscope() -> dict | None:
|
|
533
|
+
"""魔搭快捷配置 — 只需 API Key,自动生成 12 个预定义模型。"""
|
|
534
|
+
# 收集 API Key
|
|
535
|
+
env_choices = [
|
|
536
|
+
f"{var} ({desc})"
|
|
537
|
+
for var, desc in API_KEY_ENV_VARS
|
|
538
|
+
if var == "ModelScopeToken" and os.getenv(var)
|
|
539
|
+
]
|
|
540
|
+
if env_choices:
|
|
541
|
+
result = await select(
|
|
542
|
+
"检测到 ModelScope Token,是否使用?", env_choices + ["手动输入..."]
|
|
543
|
+
)
|
|
544
|
+
if result is None:
|
|
545
|
+
return None
|
|
546
|
+
if result == "手动输入...":
|
|
547
|
+
api_key = await password("输入 ModelScope API Key: ")
|
|
548
|
+
else:
|
|
549
|
+
# 提取 env var 名
|
|
550
|
+
var_name = result.split(" (")[0]
|
|
551
|
+
api_key = os.getenv(var_name, "")
|
|
552
|
+
else:
|
|
553
|
+
api_key = await password("输入 ModelScope API Key: ")
|
|
554
|
+
|
|
555
|
+
if not api_key or not api_key.strip():
|
|
556
|
+
return None
|
|
557
|
+
api_key = api_key.strip()
|
|
558
|
+
|
|
559
|
+
# 用预设模型 + api_key 构建配置
|
|
560
|
+
default_cfg = dict(MODELSCOPE_PRESETS[0])
|
|
561
|
+
default_cfg["api_key"] = api_key
|
|
562
|
+
|
|
563
|
+
fallback = {}
|
|
564
|
+
for preset in MODELSCOPE_PRESETS[1:]:
|
|
565
|
+
cfg = dict(preset)
|
|
566
|
+
cfg["api_key"] = api_key
|
|
567
|
+
fallback[cfg["model"]] = cfg
|
|
568
|
+
|
|
569
|
+
return {"default": default_cfg, "fallback": fallback}
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
LONGCAT_BASE_URL = "https://api.longcat.chat/openai/v1"
|
|
573
|
+
|
|
574
|
+
LONGCAT_PRESETS = [
|
|
575
|
+
{
|
|
576
|
+
"model": "LongCat-2.0-Preview",
|
|
577
|
+
"base_url": LONGCAT_BASE_URL,
|
|
578
|
+
"temperature": 1.0,
|
|
579
|
+
"top_p": 0.95,
|
|
580
|
+
"stream_usage": True,
|
|
581
|
+
},
|
|
582
|
+
{
|
|
583
|
+
"model": "LongCat-Flash-Chat",
|
|
584
|
+
"base_url": LONGCAT_BASE_URL,
|
|
585
|
+
"temperature": 1.0,
|
|
586
|
+
"top_p": 0.95,
|
|
587
|
+
"stream_usage": True,
|
|
588
|
+
},
|
|
589
|
+
{
|
|
590
|
+
"model": "LongCat-Flash-Thinking",
|
|
591
|
+
"base_url": LONGCAT_BASE_URL,
|
|
592
|
+
"temperature": 0.6,
|
|
593
|
+
"top_p": 0.95,
|
|
594
|
+
"stream_usage": True,
|
|
595
|
+
},
|
|
596
|
+
{
|
|
597
|
+
"model": "LongCat-Flash-Lite",
|
|
598
|
+
"base_url": LONGCAT_BASE_URL,
|
|
599
|
+
"temperature": 1.0,
|
|
600
|
+
"top_p": 0.95,
|
|
601
|
+
"stream_usage": True,
|
|
602
|
+
},
|
|
603
|
+
]
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
async def configure_longcat() -> dict | None:
|
|
607
|
+
"""LongCat 快捷配置 — 只需 API Key,自动生成 4 个预定义模型。"""
|
|
608
|
+
env_choices = [
|
|
609
|
+
f"{var} ({desc})"
|
|
610
|
+
for var, desc in API_KEY_ENV_VARS
|
|
611
|
+
if var == "LONGCAT_API_KEY" and os.getenv(var)
|
|
612
|
+
]
|
|
613
|
+
if env_choices:
|
|
614
|
+
result = await select(
|
|
615
|
+
"检测到 LongCat API Key,是否使用?", env_choices + ["手动输入..."]
|
|
616
|
+
)
|
|
617
|
+
if result is None:
|
|
618
|
+
return None
|
|
619
|
+
if result == "手动输入...":
|
|
620
|
+
api_key = await password("输入 LongCat API Key: ")
|
|
621
|
+
else:
|
|
622
|
+
var_name = result.split(" (")[0]
|
|
623
|
+
api_key = os.getenv(var_name, "")
|
|
624
|
+
else:
|
|
625
|
+
api_key = await password("输入 LongCat API Key: ")
|
|
626
|
+
|
|
627
|
+
if not api_key or not api_key.strip():
|
|
628
|
+
return None
|
|
629
|
+
api_key = api_key.strip()
|
|
630
|
+
|
|
631
|
+
default_cfg = dict(LONGCAT_PRESETS[0])
|
|
632
|
+
default_cfg["api_key"] = api_key
|
|
633
|
+
|
|
634
|
+
fallback = {}
|
|
635
|
+
for preset in LONGCAT_PRESETS[1:]:
|
|
636
|
+
cfg = dict(preset)
|
|
637
|
+
cfg["api_key"] = api_key
|
|
638
|
+
fallback[cfg["model"]] = cfg
|
|
639
|
+
|
|
640
|
+
return {"default": default_cfg, "fallback": fallback}
|