From 88f32b208b177de7f526d93a945ef9834b2e6fab Mon Sep 17 00:00:00 2001 From: Liang Jiaqing Date: Wed, 1 Apr 2026 23:09:48 +0800 Subject: [PATCH] feat: support NativeToolClient and optimize tool use format for native API --- agentmain.py | 5 ++++- llmcore.py | 28 +++++++++++++++++++--------- mykey_template.py | 10 ++++++---- 3 files changed, 29 insertions(+), 14 deletions(-) diff --git a/agentmain.py b/agentmain.py index 199debf..4ff0d13 100644 --- a/agentmain.py +++ b/agentmain.py @@ -56,7 +56,10 @@ class GeneraticAgent: except: pass for i, s in enumerate(llm_sessions): if isinstance(s, dict) and 'mixin_cfg' in s: - try: llm_sessions[i] = ToolClient(MixinSession(llm_sessions, s['mixin_cfg'])) + try: + mixin = MixinSession(llm_sessions, s['mixin_cfg']) + if isinstance(mixin._sessions[0], (NativeClaudeSession, NativeOAISession)): llm_sessions[i] = NativeToolClient(mixin) + else: llm_sessions[i] = ToolClient(mixin) except Exception as e: print(f'[WARN] Failed to init MixinSession with cfg {s["mixin_cfg"]}: {e}') self.llmclients = llm_sessions self.lock = threading.Lock() diff --git a/llmcore.py b/llmcore.py index 8424a05..e99a3f5 100644 --- a/llmcore.py +++ b/llmcore.py @@ -62,7 +62,7 @@ def trim_messages_history(history, context_win): print(f'[Debug] Current context: {cost} chars, {len(history)} messages.') if cost > context_win * 3: target = context_win * 3 * 0.6 - while len(history) > 4 and cost > target: + while len(history) > 5 and cost > target: history.pop(0) while history and history[0].get('role') != 'user': history.pop(0) if history and history[0].get('role') == 'user': history[0] = _sanitize_leading_user_msg(history[0]) @@ -434,8 +434,16 @@ class BaseSession: self.history.append({"role": "user", "content": [{"type": "text", "text": prompt}]}) trim_messages_history(self.history, self.context_win) messages = self.make_messages(self.history) - for chunk in self.raw_ask(messages, model): - content += chunk; yield chunk + content_blocks = None + gen = self.raw_ask(messages, model) + try: + while True: chunk = next(gen); content += chunk; yield chunk + except StopIteration as e: content_blocks = e.value or [] + print(f"[DEBUG BaseSession.ask] content_blocks: {content_blocks}") + for block in (content_blocks or []): + if block.get('type', '') == 'tool_use': + tu = {'name': block.get('name', ''), 'arguments': block.get('input', {})} + yield f'{json.dumps(tu, ensure_ascii=False)}' if not content.startswith("Error:"): self.history.append({"role": "assistant", "content": [{"type": "text", "text": content}]}) return _ask_gen() if stream else ''.join(list(_ask_gen())) @@ -448,11 +456,13 @@ class ClaudeSession(BaseSession): headers = {"x-api-key": self.api_key, "Content-Type": "application/json", "anthropic-version": "2023-06-01", "anthropic-beta": "prompt-caching-2024-07-31"} payload = {"model": model, "messages": messages, "temperature": temperature, "max_tokens": max_tokens, "stream": True} if self.system: payload["system"] = [{"type": "text", "text": self.system, "cache_control": {"type": "persistent"}}] + content_blocks = [] try: with requests.post(auto_make_url(self.api_base, "messages"), headers=headers, json=payload, stream=True, timeout=(5,30)) as r: r.raise_for_status() - yield from _parse_claude_sse(r.iter_lines()) + content_blocks = yield from _parse_claude_sse(r.iter_lines()) except Exception as e: yield f"Error: {str(e)}" + return content_blocks or [] def make_messages(self, raw_list): msgs = [{"role": m['role'], "content": list(m['content'])} for m in raw_list] c = msgs[-1]["content"] @@ -462,10 +472,10 @@ class ClaudeSession(BaseSession): class LLMSession(BaseSession): def raw_ask(self, messages, model=None, temperature=0.5): if model is None: model = self.default_model - yield from _openai_stream(self.api_base, self.api_key, messages, model, self.api_mode, + return (yield from _openai_stream(self.api_base, self.api_key, messages, model, self.api_mode, temperature=temperature, reasoning_effort=self.reasoning_effort, max_retries=self.max_retries, connect_timeout=self.connect_timeout, - read_timeout=self.read_timeout, proxies=self.proxies) + read_timeout=self.read_timeout, proxies=self.proxies)) def make_messages(self, raw_list): return _msgs_claude2oai(raw_list) class NativeClaudeSession(BaseSession): @@ -620,11 +630,11 @@ class ToolClient: tools_json = json.dumps(tools, ensure_ascii=False, separators=(',', ':')) tool_instruction = f""" ### 交互协议 (必须严格遵守,持续有效) -请按照以下步骤思考并行动,标签之间需要回车换行: +请按照以下步骤思考并行动: 1. **思考**: 在 `` 标签中先进行思考,分析现状和策略。 2. **总结**: 在 `` 中输出*极为简短*的高度概括的单行(<30字)物理快照,包括上次工具调用结果产生的新信息+本次工具调用意图。此内容将进入长期工作记忆,记录关键信息,严禁输出无实际信息增量的描述。 -3. **行动**: 如需调用工具,请在回复正文之后输出一个(或多个)**块**,然后结束,我会稍后给你返回块。 - 格式: ```\n{{"name": "工具名", "arguments": {{参数}}}}\n\n``` +3. **行动**: 如需调用工具,请在回复正文之后输出一个(或多个)**块**,然后结束。 +格式: ```{{"name": "工具名", "arguments": {{参数}}}}``` ### 可用工具库(已挂载,持续有效) {tools_json} diff --git a/mykey_template.py b/mykey_template.py index 193ef66..0f8162c 100644 --- a/mykey_template.py +++ b/mykey_template.py @@ -8,12 +8,13 @@ # ── Mixin (实验性) ─────────────────────────────────────────────────────────────── # key命名含 'mixin' 触发 MixinSession:多key/endpoint自动fallback + 指数退避重试 -# 约束:引用的session须同类型,不支持Native -# mixin_config = {'llm_nos': [1, 2], 'max_retries': 3, 'base_delay': 1.5} # 序号含自身(此处mixin=0) +# 约束:引用的session须同为Native或非Native +# mixin_config = {'llm_nos': ['modela', 'xxxx'], 'max_retries': 5, 'base_delay': 1.5} # name匹配,含自身 # ── OpenAI-compatible (chat/completions or responses API) ────────────────────── # key命名含 'oai' 触发 LLMSession oai_config = { + 'name': 'modela', # 可选 'apikey': 'sk-...', 'apibase': 'http://your-proxy:2001', 'model': 'openai/gpt-5.1', @@ -37,11 +38,11 @@ oai_config2 = { # ── Claude via OpenAI-compatible proxy ───────────────────────────────────────── # key命名含 'claude'(不含'native')触发 ClaudeSession(走OpenAI兼容层) claude_config = { + 'name': 'xxxx', # 可选 'apikey': 'sk-...', 'apibase': 'http://your-proxy:2001', 'model': 'claude-opus', # 'context_win': 12000, - # 'prompt_cache': False, } # ── Claude Native API ─────────────────────────────────────────────────────────── @@ -50,8 +51,9 @@ claude_config = { native_claude_config = { 'apikey': 'sk-ant-...', # Anthropic原生apikey 'apibase': 'https://api.anthropic.com', - 'model': 'claude-opus-4-5', + 'model': 'claude-opus-4-6', # 'context_win': 24000, + # 'no_system_prompt': True # 是否不使用系统提示而是使用用户消息,为了绕过cc MAX检测 } # ── OpenAI-compatible Native API ─────────────────────────────────────────────