Skip to content

Commit 3cefcf5

Browse files
committed
feat: add ModelScope quick setup, dynamic summary trigger, and remove max_tokens
- Add 魔搭快捷配置 flow: one-click setup with 12 preset models via API key - Replace hardcoded summary trigger (170k) with dynamic calculation based on model context window size (90% threshold) - Remove deprecated max_tokens parameter in favor of max_completion_tokens - Fix edit_current_model not persisting updated config to disk - Improve error message display with full exception details on retry failure - Enhance /summary JSON parsing with bracket-matching fallback for LLM output - Add new model context window sizes (deepseek-v3.2, deepseek-r1-0528, etc.) - Add psutil and pytest-timeout dependencies - Update tests to reflect config flow changes
1 parent b0cb92e commit 3cefcf5

10 files changed

Lines changed: 290 additions & 96 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ https://github.com/ScarletMercy/chcode/blob/main/assets/test.mp4
3434
- Compatible with **all OpenAI-compatible APIs** (OpenAI, DeepSeek, Qwen, GLM, Claude via proxy, etc.)
3535
- First-run wizard with **env auto-detection** (scans `OPENAI_API_KEY`, `DEEPSEEK_API_KEY`, `ZHIPU_API_KEY`, etc.)
3636
- Create / edit / switch models at runtime
37-
- Per-model hyperparameter tuning (temperature, top_p, top_k, max_tokens, stop_sequences, etc.)
37+
- Per-model hyperparameter tuning (temperature, top_p, top_k, max_completion_tokens, stop_sequences, etc.)
3838
- Automatic **retry with exponential backoff** (3/10/30/60s) and fallback model switching on persistent failure
3939

4040
### Session & History

chcode/agent_setup.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -136,13 +136,13 @@ async def model_retry_with_backoff(
136136
if fallback:
137137
console.print(f"[yellow]主模型重试{retry_count}次失败,切换到备用模型...[/yellow]")
138138
raise ModelSwitchError("切换到备用模型")
139-
console.print(f"[red]请求失败,无备用模型可用,放弃请求[/red]")
139+
console.print(f"[red]请求失败,无备用模型可用,放弃请求\n {e}[/red]")
140140
raise
141141

142142
delay_idx = min(retry_count - 1, len(RETRY_DELAYS) - 1)
143143
delay = RETRY_DELAYS[delay_idx]
144144

145-
console.print(f"[yellow]请求失败 ({retry_count}/{max_retries}), {delay}秒后重试... ({str(e)[:50]})[/yellow]")
145+
console.print(f"[yellow]请求失败 ({retry_count}/{max_retries}), {delay}秒后重试...\n {e}[/yellow]")
146146

147147
await asyncio.sleep(delay)
148148

@@ -266,13 +266,18 @@ def build_agent(
266266
_summarization_model = EnhancedChatOpenAI(**cfg)
267267

268268
# 加载 fallback 模型配置
269-
from chcode.config import load_model_json
269+
from chcode.config import load_model_json, get_context_window_size
270270

271271
data = load_model_json()
272272
fallback = data.get("fallback", {})
273273
if fallback:
274274
set_fallback_models(list(fallback.values()))
275275

276+
# 摘要触发阈值 = 上下文窗口的 90%
277+
model_name = cfg.get("model", "")
278+
ctx_window = get_context_window_size(model_name)
279+
summary_trigger = int(ctx_window * 0.9)
280+
276281
agent = create_agent(
277282
model,
278283
_get_all_tools() + (mcp_tools or []),
@@ -295,7 +300,7 @@ def build_agent(
295300
),
296301
SummarizationMiddleware(
297302
model=_summarization_model,
298-
trigger=("tokens", 170_000),
303+
trigger=("tokens", summary_trigger),
299304
keep=("messages", 20),
300305
),
301306
_hitl_middleware,

chcode/chat.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -434,7 +434,6 @@ def _tab_toggle_mode(event):
434434
from chcode.agent_setup import update_hitl_config
435435

436436
update_hitl_config(self.yolo)
437-
mode = "Yolo" if self.yolo else "Common"
438437
event.app.renderer._last_rendered_width = 0 # 强制刷新 toolbar
439438

440439
_last_width = 0
@@ -738,9 +737,30 @@ async def _cmd_compress(self, _arg: str) -> None:
738737
import re
739738

740739
content = raw_resp.content.strip()
740+
# 去除 markdown 代码块包裹
741741
if content.startswith("```"):
742742
content = re.sub(r"^```(?:json)?\s*\n?", "", content)
743743
content = re.sub(r"\n?```\s*$", "", content)
744+
# 提取包含 "summary" 的 JSON 对象(模型可能在 JSON 前输出思考内容)
745+
json_match = re.search(r'\{[^{}]*"summary"[^{}]*\}', content)
746+
if json_match:
747+
content = json_match.group()
748+
else:
749+
# 可能 summary 值中包含嵌套对象,用逐字符括号匹配兜底
750+
depth = 0
751+
start = -1
752+
for i, ch in enumerate(content):
753+
if ch == '{':
754+
if depth == 0:
755+
start = i
756+
depth += 1
757+
elif ch == '}':
758+
depth -= 1
759+
if depth == 0 and start >= 0:
760+
candidate = content[start:i+1]
761+
if '"summary"' in candidate:
762+
content = candidate
763+
break
744764
data = json.loads(content)
745765
ai_content = data.get("summary", "")
746766
if not ai_content:

chcode/config.py

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ async def first_run_configure() -> dict | None:
122122

123123
if detected:
124124
choices = [f"{d['name']} (检测到 {d['env_var']})" for d in detected]
125+
choices.append("魔搭快捷配置...")
125126
choices.append("手动配置...")
126127
choices.append("退出")
127128

@@ -135,6 +136,9 @@ async def first_run_configure() -> dict | None:
135136
if "手动" in result:
136137
return await configure_new_model()
137138

139+
if "魔搭" in result:
140+
return await _configure_modelscope_with_test()
141+
138142
idx = choices.index(result)
139143
chosen = detected[idx]
140144

@@ -169,19 +173,26 @@ async def first_run_configure() -> dict | None:
169173
return config
170174
else:
171175
console.print("[yellow]未检测到环境变量中的 API Key[/yellow]")
172-
choices = ["手动配置...", "退出"]
176+
choices = ["魔搭快捷配置...", "手动配置...", "退出"]
173177
result = await select("选择:", choices)
174178
if result is None or "退出" in result:
175179
console.print("[dim]提示: 在环境变量中设置 API Key 后重新运行,例如:[/dim]")
176180
console.print("[dim] set BIGMODEL_API_KEY=your_key[/dim]")
177181
console.print("[dim]或执行 chcode config new 手动配置[/dim]")
178182
return None
183+
if "魔搭" in result:
184+
return await _configure_modelscope_with_test()
179185
return await configure_new_model()
180186

181187

182188
async def configure_new_model() -> dict | None:
183189
"""新建模型配置(交互式表单)"""
184190
ensure_config_dir()
191+
result = await select("配置方式:", ["魔搭快捷配置...", "手动配置..."])
192+
if result is None:
193+
return None
194+
if "魔搭" in result:
195+
return await _configure_modelscope_with_test()
185196
config = await model_config_form()
186197
if config is None:
187198
return None
@@ -222,6 +233,56 @@ async def configure_new_model() -> dict | None:
222233
return config
223234

224235

236+
async def _configure_modelscope_with_test() -> dict | None:
237+
"""魔搭快捷配置:收集 API Key → 测试连接 → 保存 12 个预定义模型。"""
238+
from chcode.prompts import configure_modelscope
239+
240+
ms_config = await configure_modelscope()
241+
if ms_config is None:
242+
return None
243+
244+
default = ms_config["default"]
245+
246+
# 测试连接
247+
console.print("[yellow]测试连接中...[/yellow]")
248+
try:
249+
from chcode.utils.enhanced_chat_openai import EnhancedChatOpenAI
250+
251+
model_inst = EnhancedChatOpenAI(**default)
252+
await asyncio.to_thread(model_inst.invoke, "你好")
253+
except Exception as e:
254+
import traceback
255+
256+
err_msg = str(e)
257+
if "null value for 'choices'" not in err_msg:
258+
console.print(f"[red]连接测试失败: {err_msg}[/red]")
259+
console.print(f"[dim]{traceback.format_exc()}[/dim]")
260+
return None
261+
262+
# 合并到已有配置,保留非魔搭的已有模型
263+
data = load_model_json()
264+
old_default = data.get("default")
265+
existing_fallback = data.get("fallback", {})
266+
267+
if not old_default:
268+
# 首次配置 — 魔搭直接作为完整配置
269+
save_model_json(ms_config)
270+
else:
271+
# 已有配置 — 旧的 default 移入 fallback,魔搭作为新 default,合并 fallback
272+
if old_default["model"] not in ms_config["fallback"]:
273+
existing_fallback[old_default["model"]] = old_default
274+
existing_fallback.update(ms_config["fallback"])
275+
data["default"] = ms_config["default"]
276+
data["fallback"] = existing_fallback
277+
save_model_json(data)
278+
fallback_names = ", ".join(ms_config["fallback"].keys())
279+
console.print(f"[green]配置完成: {default['model']} (默认)[/green]")
280+
console.print(f"[dim]备用模型 ({len(ms_config['fallback'])} 个): {fallback_names}[/dim]")
281+
282+
await configure_tavily()
283+
return default
284+
285+
225286
async def edit_current_model() -> dict | None:
226287
"""编辑当前默认模型"""
227288
data = load_model_json()
@@ -249,6 +310,7 @@ async def edit_current_model() -> dict | None:
249310
console.print(f"[red]连接测试失败: {err_msg}[/red]")
250311
console.print(f"[dim]{traceback.format_exc()}[/dim]")
251312
return None
313+
data["default"] = config
252314
save_model_json(data)
253315
console.print(f"[green]模型配置已更新: {config['model']}[/green]")
254316
return config
@@ -357,11 +419,15 @@ def save_tavily_api_key(api_key: str) -> None:
357419
"gpt-4o-mini": 128000,
358420
"claude-sonnet-4-20250514": 200000,
359421
"deepseek-chat": 65536,
422+
"deepseek-v3.2": 128000,
423+
"deepseek-r1-0528": 65536,
360424
"glm-5.1": 200000,
361425
"glm-5": 200000,
362426
"glm-4.7": 200000,
363427
"minimax-m2": 204800,
364-
"kimi-k2": 262144,
428+
"minimax-m2.5": 200000,
429+
"kimi-k2": 256000,
430+
"mimo-v2-flash": 256000,
365431
"qwen3.5-plus": 1000000,
366432
"qwen3.6-plus": 1000000,
367433
"qwen": 256000,

0 commit comments

Comments
 (0)