fix(aibox): sync compressor and snapshots on client swap via set_client (M1)
run_full_subagent replaced sub_chat.client by direct assignment after construction, leaving ConversationCompressor (bound to the original client in DeepSeekChat.__init__) and the api_key/name snapshots pointing at the default model — compression requests silently kept using the default endpoint/model while the dialogue itself used the requested one. DeepSeekChat gains set_client(): swaps self.client and refreshes api_key, name, and compressor.llm_client in one place. run_full_subagent now routes model overrides through it. Tests: new TestM1SetClient (2 cases: full reference refresh on a real DeepSeekChat instance, and run_full_subagent calling set_client with the new client); full suite 295 passed.
This commit is contained in:
1 parent
77262f8c3a
commit
5f3ca9a246
3 files changed
+59
-4
No files matched your search
+4
-3
@@ -92,7 +92,7 @@ aibox/
|
||||
│ ├── prompt_tools.py # B 批:UpdateIdentity/UpdateSoul/SaveMemoryAndEndConversation
|
||||
│ ├── proxy_tools.py # B 批:SetProxy/GetProxy
|
||||
│ └── misc_tools.py # B 批:AskUserTool
|
||||
├── tests/ # 单元测试套件(293 项全过)
|
||||
├── tests/ # 单元测试套件(295 项全过)
|
||||
│ ├── conftest.py # sys.path 配置 + websockets 桩
|
||||
│ ├── test_basic_tools.py # A 批工具 43 项
|
||||
│ ├── test_b_tools.py # B 批工具 58 项
|
||||
@@ -885,12 +885,13 @@ elif command == "/mycommand":
|
||||
## 📄 更新日志
|
||||
|
||||
### v3.4
|
||||
- ✅ **多模型档案加固(H1/H2/H3/M2 四项修复)**:
|
||||
- ✅ **多模型档案加固(H1/H2/H3/M1/M2 五项修复)**:
|
||||
- **H1(凭据隔离)**:档案端点环境变量取不到时直接报错,**绝不回退读 `~/.cert/deepseek.key`**(该文件为 DeepSeek 专属凭据,跨厂商复用属于泄漏);key 文件回退仅对无档案的旧 `api` 扁平节生效
|
||||
- **H2(schema 校验前移)**:档案缺 `api_url`/`model` 或类型错误时,解析期即抛出指明档案名与缺失字段的 `ValueError`——不再把档案名当模型名发出、不再延迟到 `requests InvalidURL` 或 API 400
|
||||
- **H3(--model 服务模式生效)**:`--model` 升级为进程级覆盖,WebSocket 服务模式(主对话/每个会话实例/定时任务实例)与 `--once` 均生效,此前服务模式静默忽略
|
||||
- **M2(防模型名幻觉)**:`create_subagent` 工具描述与 JSON Schema 动态携带 `models` 节可用档案名列表(默认档案标注),LLM 无从编造名称;`models` 为空时描述保持原样(零破坏)
|
||||
- ✅ **测试**:293 项全过(H1 隔离 4 项 + H2 校验 6 项 + H3 传导 6 项 + M2 提示 5 项新增)
|
||||
- **M1(换模型同步派生引用)**:新增 `DeepSeekChat.set_client()` 统一替换 LLM 客户端——同步刷新 `compressor.llm_client` 与 `api_key`/`name` 快照;`create_subagent` 子代理换模型经此入口,压缩链不再残留默认模型
|
||||
- ✅ **测试**:295 项全过(H1 隔离 4 项 + H2 校验 6 项 + H3 传导 6 项 + M2 提示 5 项 + M1 替换 2 项新增)
|
||||
|
||||
### v3.3
|
||||
- ✅ **安全审计与修复**(两轮审计报告见 [docs/reports/aibox_audit_20260817.md](../docs/reports/aibox_audit_20260817.md)):
|
||||
|
||||
+15
-1
@@ -651,8 +651,10 @@ def run_full_subagent(role, task, config, logger=None, model=None,
|
||||
)
|
||||
|
||||
# 覆盖模型(如果指定了不同模型)
|
||||
# M1:经 set_client 统一替换——同步刷新 compressor.llm_client 与
|
||||
# api_key/name 快照,避免压缩链仍用默认模型发请求
|
||||
if model:
|
||||
sub_chat.client = create_llm_client(config, api_key=None, logger=logger, model=model)
|
||||
sub_chat.set_client(create_llm_client(config, api_key=None, logger=logger, model=model))
|
||||
|
||||
# 轮次限制:防止工具调用无限循环
|
||||
# 注意:超限时不置 conversation_active=False——那会触发 think_and_respond
|
||||
@@ -944,6 +946,18 @@ class DeepSeekChat:
|
||||
self.tool_registry = ToolRegistry(self.logger)
|
||||
self._register_tools(exit_callback, command_executor)
|
||||
|
||||
def set_client(self, client) -> None:
|
||||
"""M1:统一替换 LLM 客户端(换模型/换档案),同步刷新派生引用。
|
||||
|
||||
直接赋值 self.client 会让 ConversationCompressor 等持有旧 client 的
|
||||
组件继续用默认模型发请求(压缩链与实际对话模型不一致)。本方法
|
||||
一并更新 compressor.llm_client 与 api_key/name 快照。
|
||||
"""
|
||||
self.client = client
|
||||
self.api_key = client.api_key
|
||||
self.name = client.name
|
||||
self.compressor.llm_client = client
|
||||
|
||||
def cancel(self):
|
||||
"""中断当前正在进行的AI思考/工具执行。线程安全,可从任意线程调用。"""
|
||||
self.logger.info("收到中断信号,正在取消当前操作...")
|
||||
|
||||
@@ -439,6 +439,46 @@ class TestM2SubAgentModelHint:
|
||||
assert "可用模型档案" not in tool.get_description()
|
||||
|
||||
|
||||
# ==================== M1:set_client 统一替换 LLM 客户端 ====================
|
||||
|
||||
class TestM1SetClient:
|
||||
"""M1:run_full_subagent 换模型后 compressor 仍持旧 client(压缩链用默认模型)。
|
||||
|
||||
验证 DeepSeekChat.set_client 统一替换:client/api_key/name/compressor.llm_client
|
||||
四处同步刷新;run_full_subagent 经 set_client 而非直接赋值。
|
||||
"""
|
||||
|
||||
def test_set_client_refreshes_all_references(self):
|
||||
"""set_client → client/api_key/name/compressor.llm_client 同步为新 client"""
|
||||
from test_once_mode import _make_ai, FakeLLMClient
|
||||
ai = _make_ai()
|
||||
assert ai.compressor.llm_client is ai.client # 前置:初始一致
|
||||
new_client = FakeLLMClient()
|
||||
new_client.api_key = "sk-new"
|
||||
new_client.name = "NewVendor"
|
||||
ai.set_client(new_client)
|
||||
assert ai.client is new_client
|
||||
assert ai.api_key == "sk-new"
|
||||
assert ai.name == "NewVendor"
|
||||
assert ai.compressor.llm_client is new_client
|
||||
|
||||
def test_run_full_subagent_uses_set_client(self):
|
||||
"""run_full_subagent(model=...) → 经 set_client 替换(compressor 不再持旧 client)"""
|
||||
from aibox import run_full_subagent
|
||||
fake_sub_chat = mock.MagicMock()
|
||||
fake_sub_chat.think_and_respond = mock.MagicMock(
|
||||
return_value="任务完成报告")
|
||||
fake_sub_chat.stats = {}
|
||||
fake_sub_chat.messages = []
|
||||
fake_new_client = mock.MagicMock()
|
||||
with mock.patch("aibox.DeepSeekChat", return_value=fake_sub_chat), \
|
||||
mock.patch("aibox.create_llm_client", return_value=fake_new_client), \
|
||||
mock.patch("aibox.SubprocessCommandExecutor"):
|
||||
run_full_subagent("测试角色", "测试任务", None,
|
||||
model="kimi", logger=None)
|
||||
fake_sub_chat.set_client.assert_called_once_with(fake_new_client)
|
||||
|
||||
|
||||
# ==================== DeepSeekChat 档案级 reasoning 读取 ====================
|
||||
|
||||
class TestChatReasoningFlag:
|
||||
|
||||
Reference in new issue
Block a user