fix: support glm-4.7

This commit is contained in:
zhayujie
2026-02-02 22:43:08 +08:00
parent 50e60e6d05
commit d8298b3eab
5 changed files with 349 additions and 32 deletions

View File

@@ -6,14 +6,14 @@ import os
from typing import Optional, List from typing import Optional, List
from agent.protocol import Agent, LLMModel, LLMRequest from agent.protocol import Agent, LLMModel, LLMRequest
from models.openai_compatible_bot import OpenAICompatibleBot from bridge.agent_event_handler import AgentEventHandler
from bridge.agent_initializer import AgentInitializer
from bridge.bridge import Bridge from bridge.bridge import Bridge
from bridge.context import Context from bridge.context import Context
from bridge.reply import Reply, ReplyType from bridge.reply import Reply, ReplyType
from bridge.agent_event_handler import AgentEventHandler
from bridge.agent_initializer import AgentInitializer
from common import const from common import const
from common.log import logger from common.log import logger
from models.openai_compatible_bot import OpenAICompatibleBot
def add_openai_compatible_support(bot_instance): def add_openai_compatible_support(bot_instance):
@@ -22,9 +22,12 @@ def add_openai_compatible_support(bot_instance):
This allows any bot to gain tool calling capability without modifying its code, This allows any bot to gain tool calling capability without modifying its code,
as long as it uses OpenAI-compatible API format. as long as it uses OpenAI-compatible API format.
Note: Some bots like ZHIPUAIBot have native tool calling support and don't need enhancement.
""" """
if hasattr(bot_instance, 'call_with_tools'): if hasattr(bot_instance, 'call_with_tools'):
# Bot already has tool calling support # Bot already has tool calling support (e.g., ZHIPUAIBot)
logger.info(f"[AgentBridge] {type(bot_instance).__name__} already has native tool calling support")
return bot_instance return bot_instance
# Create a temporary mixin class that combines the bot with OpenAI compatibility # Create a temporary mixin class that combines the bot with OpenAI compatibility

View File

@@ -1,12 +1,13 @@
{ {
"channel_type": "web", "channel_type": "web",
"model": "claude-sonnet-4-5", "model": "claude-sonnet-4-5",
"open_ai_api_key": "",
"open_ai_api_base": "https://api.openai.com/v1",
"claude_api_key": "", "claude_api_key": "",
"claude_api_base": "https://api.anthropic.com/v1", "claude_api_base": "https://api.anthropic.com/v1",
"open_ai_api_key": "",
"open_ai_api_base": "https://api.openai.com/v1",
"gemini_api_key": "", "gemini_api_key": "",
"gemini_api_base": "https://generativelanguage.googleapis.com", "gemini_api_base": "https://generativelanguage.googleapis.com",
"zhipu_ai_api_key": "",
"voice_to_text": "openai", "voice_to_text": "openai",
"text_to_voice": "openai", "text_to_voice": "openai",
"voice_reply_voice": false, "voice_reply_voice": false,

View File

@@ -6,8 +6,8 @@ from config import conf
class ZhipuAIImage(object): class ZhipuAIImage(object):
def __init__(self): def __init__(self):
from zhipuai import ZhipuAI from zai import ZhipuAiClient
self.client = ZhipuAI(api_key=conf().get("zhipu_ai_api_key")) self.client = ZhipuAiClient(api_key=conf().get("zhipu_ai_api_key"))
def create_img(self, query, retry_count=0, api_key=None, api_base=None): def create_img(self, query, retry_count=0, api_key=None, api_base=None):
try: try:

View File

@@ -1,9 +1,8 @@
# encoding:utf-8 # encoding:utf-8
import time import time
import json
import openai
import openai.error
from models.bot import Bot from models.bot import Bot
from models.zhipuai.zhipu_ai_session import ZhipuAISession from models.zhipuai.zhipu_ai_session import ZhipuAISession
from models.zhipuai.zhipu_ai_image import ZhipuAIImage from models.zhipuai.zhipu_ai_image import ZhipuAIImage
@@ -12,7 +11,7 @@ from bridge.context import ContextType
from bridge.reply import Reply, ReplyType from bridge.reply import Reply, ReplyType
from common.log import logger from common.log import logger
from config import conf, load_config from config import conf, load_config
from zhipuai import ZhipuAI from zai import ZhipuAiClient
# ZhipuAI对话模型API # ZhipuAI对话模型API
@@ -25,7 +24,7 @@ class ZHIPUAIBot(Bot, ZhipuAIImage):
"temperature": conf().get("temperature", 0.9), # 值在(0,1)之间(智谱AI 的温度不能取 0 或者 1) "temperature": conf().get("temperature", 0.9), # 值在(0,1)之间(智谱AI 的温度不能取 0 或者 1)
"top_p": conf().get("top_p", 0.7), # 值在(0,1)之间(智谱AI 的 top_p 不能取 0 或者 1) "top_p": conf().get("top_p", 0.7), # 值在(0,1)之间(智谱AI 的 top_p 不能取 0 或者 1)
} }
self.client = ZhipuAI(api_key=conf().get("zhipu_ai_api_key")) self.client = ZhipuAiClient(api_key=conf().get("zhipu_ai_api_key"))
def reply(self, query, context=None): def reply(self, query, context=None):
# acquire reply content # acquire reply content
@@ -49,17 +48,13 @@ class ZHIPUAIBot(Bot, ZhipuAIImage):
session = self.sessions.session_query(query, session_id) session = self.sessions.session_query(query, session_id)
logger.debug("[ZHIPU_AI] session query={}".format(session.messages)) logger.debug("[ZHIPU_AI] session query={}".format(session.messages))
api_key = context.get("openai_api_key") or openai.api_key
model = context.get("gpt_model") model = context.get("gpt_model")
new_args = None new_args = None
if model: if model:
new_args = self.args.copy() new_args = self.args.copy()
new_args["model"] = model new_args["model"] = model
# if context.get('stream'):
# # reply in stream
# return self.reply_text_stream(query, new_query, session_id)
reply_content = self.reply_text(session, api_key, args=new_args) reply_content = self.reply_text(session, args=new_args)
logger.debug( logger.debug(
"[ZHIPU_AI] new_query={}, session_id={}, reply_cont={}, completion_tokens={}".format( "[ZHIPU_AI] new_query={}, session_id={}, reply_cont={}, completion_tokens={}".format(
session.messages, session.messages,
@@ -90,21 +85,17 @@ class ZHIPUAIBot(Bot, ZhipuAIImage):
reply = Reply(ReplyType.ERROR, "Bot不支持处理{}类型的消息".format(context.type)) reply = Reply(ReplyType.ERROR, "Bot不支持处理{}类型的消息".format(context.type))
return reply return reply
def reply_text(self, session: ZhipuAISession, api_key=None, args=None, retry_count=0) -> dict: def reply_text(self, session: ZhipuAISession, args=None, retry_count=0) -> dict:
""" """
call openai's ChatCompletion to get the answer Call ZhipuAI API to get the answer
:param session: a conversation session :param session: a conversation session
:param session_id: session id :param args: request arguments
:param retry_count: retry count :param retry_count: retry count
:return: {} :return: {}
""" """
try: try:
# if conf().get("rate_limit_chatgpt") and not self.tb4chatgpt.get_token():
# raise openai.error.RateLimitError("RateLimitError: rate limit exceeded")
# if api_key == None, the default openai.api_key will be used
if args is None: if args is None:
args = self.args args = self.args
# response = openai.ChatCompletion.create(api_key=api_key, messages=session.messages, **args)
response = self.client.chat.completions.create(messages=session.messages, **args) response = self.client.chat.completions.create(messages=session.messages, **args)
# logger.debug("[ZHIPU_AI] response={}".format(response)) # logger.debug("[ZHIPU_AI] response={}".format(response))
# logger.info("[ZHIPU_AI] reply={}, total_tokens={}".format(response.choices[0]['message']['content'], response["usage"]["total_tokens"])) # logger.info("[ZHIPU_AI] reply={}, total_tokens={}".format(response.choices[0]['message']['content'], response["usage"]["total_tokens"]))
@@ -117,23 +108,26 @@ class ZHIPUAIBot(Bot, ZhipuAIImage):
except Exception as e: except Exception as e:
need_retry = retry_count < 2 need_retry = retry_count < 2
result = {"completion_tokens": 0, "content": "我现在有点累了,等会再来吧"} result = {"completion_tokens": 0, "content": "我现在有点累了,等会再来吧"}
if isinstance(e, openai.error.RateLimitError): error_str = str(e).lower()
# Check error type by error message content
if "rate" in error_str and "limit" in error_str:
logger.warn("[ZHIPU_AI] RateLimitError: {}".format(e)) logger.warn("[ZHIPU_AI] RateLimitError: {}".format(e))
result["content"] = "提问太快啦,请休息一下再问我吧" result["content"] = "提问太快啦,请休息一下再问我吧"
if need_retry: if need_retry:
time.sleep(20) time.sleep(20)
elif isinstance(e, openai.error.Timeout): elif "timeout" in error_str or "timed out" in error_str:
logger.warn("[ZHIPU_AI] Timeout: {}".format(e)) logger.warn("[ZHIPU_AI] Timeout: {}".format(e))
result["content"] = "我没有收到你的消息" result["content"] = "我没有收到你的消息"
if need_retry: if need_retry:
time.sleep(5) time.sleep(5)
elif isinstance(e, openai.error.APIError): elif "api" in error_str and ("error" in error_str or "gateway" in error_str):
logger.warn("[ZHIPU_AI] Bad Gateway: {}".format(e)) logger.warn("[ZHIPU_AI] APIError: {}".format(e))
result["content"] = "请再问我一次" result["content"] = "请再问我一次"
if need_retry: if need_retry:
time.sleep(10) time.sleep(10)
elif isinstance(e, openai.error.APIConnectionError): elif "connection" in error_str or "network" in error_str:
logger.warn("[ZHIPU_AI] APIConnectionError: {}".format(e)) logger.warn("[ZHIPU_AI] ConnectionError: {}".format(e))
result["content"] = "我连接不到你的网络" result["content"] = "我连接不到你的网络"
if need_retry: if need_retry:
time.sleep(5) time.sleep(5)
@@ -144,6 +138,325 @@ class ZHIPUAIBot(Bot, ZhipuAIImage):
if need_retry: if need_retry:
logger.warn("[ZHIPU_AI] 第{}次重试".format(retry_count + 1)) logger.warn("[ZHIPU_AI] 第{}次重试".format(retry_count + 1))
return self.reply_text(session, api_key, args, retry_count + 1) return self.reply_text(session, args, retry_count + 1)
else: else:
return result return result
def call_with_tools(self, messages, tools=None, stream=False, **kwargs):
"""
Call ZhipuAI API with tool support for agent integration
This method handles:
1. Format conversion (Claude format → ZhipuAI format)
2. System prompt injection
3. API calling with ZhipuAI SDK
4. Tool stream support (tool_stream=True for GLM-4.7)
Args:
messages: List of messages (may be in Claude format from agent)
tools: List of tool definitions (may be in Claude format from agent)
stream: Whether to use streaming
**kwargs: Additional parameters (max_tokens, temperature, system, etc.)
Returns:
Formatted response or generator for streaming
"""
try:
# Convert messages from Claude format to ZhipuAI format
messages = self._convert_messages_to_zhipu_format(messages)
# Convert tools from Claude format to ZhipuAI format
if tools:
tools = self._convert_tools_to_zhipu_format(tools)
# Handle system prompt
system_prompt = kwargs.get('system')
if system_prompt:
# Add system message at the beginning if not already present
if not messages or messages[0].get('role') != 'system':
messages = [{"role": "system", "content": system_prompt}] + messages
else:
# Replace existing system message
messages[0] = {"role": "system", "content": system_prompt}
# Build request parameters
request_params = {
"model": kwargs.get("model", self.args.get("model", "glm-4")),
"messages": messages,
"temperature": kwargs.get("temperature", self.args.get("temperature", 0.9)),
"top_p": kwargs.get("top_p", self.args.get("top_p", 0.7)),
"stream": stream
}
# Add max_tokens if specified
if kwargs.get("max_tokens"):
request_params["max_tokens"] = kwargs["max_tokens"]
# Add tools if provided
if tools:
request_params["tools"] = tools
# GLM-4.7 with zai-sdk supports tool_stream for streaming tool calls
if stream:
request_params["tool_stream"] = kwargs.get("tool_stream", True)
# Add thinking parameter for deep thinking mode (GLM-4.7)
thinking = kwargs.get("thinking")
if thinking:
request_params["thinking"] = thinking
elif "glm-4.7" in request_params["model"]:
# Enable thinking by default for GLM-4.7
request_params["thinking"] = {"type": "enabled"}
# Make API call with ZhipuAI SDK
if stream:
return self._handle_stream_response(request_params)
else:
return self._handle_sync_response(request_params)
except Exception as e:
error_msg = str(e)
logger.error(f"[ZHIPU_AI] call_with_tools error: {error_msg}")
if stream:
def error_generator():
yield {
"error": True,
"message": error_msg,
"status_code": 500
}
return error_generator()
else:
return {
"error": True,
"message": error_msg,
"status_code": 500
}
def _handle_sync_response(self, request_params):
"""Handle synchronous ZhipuAI API response"""
try:
response = self.client.chat.completions.create(**request_params)
# Convert ZhipuAI response to OpenAI-compatible format
return {
"id": response.id,
"object": "chat.completion",
"created": response.created,
"model": response.model,
"choices": [{
"index": 0,
"message": {
"role": response.choices[0].message.role,
"content": response.choices[0].message.content,
"tool_calls": self._convert_tool_calls_to_openai_format(
getattr(response.choices[0].message, 'tool_calls', None)
)
},
"finish_reason": response.choices[0].finish_reason
}],
"usage": {
"prompt_tokens": response.usage.prompt_tokens,
"completion_tokens": response.usage.completion_tokens,
"total_tokens": response.usage.total_tokens
}
}
except Exception as e:
logger.error(f"[ZHIPU_AI] sync response error: {e}")
return {
"error": True,
"message": str(e),
"status_code": 500
}
def _handle_stream_response(self, request_params):
"""Handle streaming ZhipuAI API response"""
try:
stream = self.client.chat.completions.create(**request_params)
# Stream chunks to caller, converting to OpenAI format
for chunk in stream:
if not chunk.choices:
continue
delta = chunk.choices[0].delta
# Convert to OpenAI-compatible format
openai_chunk = {
"id": chunk.id,
"object": "chat.completion.chunk",
"created": chunk.created,
"model": chunk.model,
"choices": [{
"index": 0,
"delta": {},
"finish_reason": chunk.choices[0].finish_reason
}]
}
# Add role if present
if hasattr(delta, 'role') and delta.role:
openai_chunk["choices"][0]["delta"]["role"] = delta.role
# Add content if present
if hasattr(delta, 'content') and delta.content:
openai_chunk["choices"][0]["delta"]["content"] = delta.content
# Add reasoning_content if present (GLM-4.7 specific)
if hasattr(delta, 'reasoning_content') and delta.reasoning_content:
# Store reasoning in content or metadata
if "content" not in openai_chunk["choices"][0]["delta"]:
openai_chunk["choices"][0]["delta"]["content"] = ""
# Prepend reasoning to content
openai_chunk["choices"][0]["delta"]["content"] = delta.reasoning_content + openai_chunk["choices"][0]["delta"].get("content", "")
# Add tool_calls if present
if hasattr(delta, 'tool_calls') and delta.tool_calls:
# For streaming, tool_calls need special handling
openai_tool_calls = []
for tc in delta.tool_calls:
tool_call_dict = {
"index": getattr(tc, 'index', 0),
"id": getattr(tc, 'id', None),
"type": "function",
"function": {}
}
# Add function name if present
if hasattr(tc, 'function') and hasattr(tc.function, 'name') and tc.function.name:
tool_call_dict["function"]["name"] = tc.function.name
# Add function arguments if present
if hasattr(tc, 'function') and hasattr(tc.function, 'arguments') and tc.function.arguments:
tool_call_dict["function"]["arguments"] = tc.function.arguments
openai_tool_calls.append(tool_call_dict)
openai_chunk["choices"][0]["delta"]["tool_calls"] = openai_tool_calls
yield openai_chunk
except Exception as e:
logger.error(f"[ZHIPU_AI] stream response error: {e}")
yield {
"error": True,
"message": str(e),
"status_code": 500
}
def _convert_tools_to_zhipu_format(self, tools):
"""
Convert tools from Claude format to ZhipuAI format
Claude format: {name, description, input_schema}
ZhipuAI format: {type: "function", function: {name, description, parameters}}
"""
if not tools:
return None
zhipu_tools = []
for tool in tools:
# Check if already in ZhipuAI/OpenAI format
if 'type' in tool and tool['type'] == 'function':
zhipu_tools.append(tool)
else:
# Convert from Claude format
zhipu_tools.append({
"type": "function",
"function": {
"name": tool.get("name"),
"description": tool.get("description"),
"parameters": tool.get("input_schema", {})
}
})
return zhipu_tools
def _convert_messages_to_zhipu_format(self, messages):
"""
Convert messages from Claude format to ZhipuAI format
Claude uses content blocks with types like 'tool_use', 'tool_result'
ZhipuAI uses 'tool_calls' in assistant messages and 'tool' role for results
"""
if not messages:
return []
zhipu_messages = []
for msg in messages:
role = msg.get("role")
content = msg.get("content")
# Handle string content (already in correct format)
if isinstance(content, str):
zhipu_messages.append(msg)
continue
# Handle list content (Claude format with content blocks)
if isinstance(content, list):
# Check if this is a tool result message (user role with tool_result blocks)
if role == "user" and any(block.get("type") == "tool_result" for block in content):
# Convert each tool_result block to a separate tool message
for block in content:
if block.get("type") == "tool_result":
zhipu_messages.append({
"role": "tool",
"tool_call_id": block.get("tool_use_id"),
"content": block.get("content", "")
})
# Check if this is an assistant message with tool_use blocks
elif role == "assistant":
# Separate text content and tool_use blocks
text_parts = []
tool_calls = []
for block in content:
if block.get("type") == "text":
text_parts.append(block.get("text", ""))
elif block.get("type") == "tool_use":
tool_calls.append({
"id": block.get("id"),
"type": "function",
"function": {
"name": block.get("name"),
"arguments": json.dumps(block.get("input", {}))
}
})
# Build ZhipuAI format assistant message
zhipu_msg = {
"role": "assistant",
"content": " ".join(text_parts) if text_parts else None
}
if tool_calls:
zhipu_msg["tool_calls"] = tool_calls
zhipu_messages.append(zhipu_msg)
else:
# Other list content, keep as is
zhipu_messages.append(msg)
else:
# Other formats, keep as is
zhipu_messages.append(msg)
return zhipu_messages
def _convert_tool_calls_to_openai_format(self, tool_calls):
"""Convert ZhipuAI tool_calls to OpenAI format"""
if not tool_calls:
return None
openai_tool_calls = []
for tool_call in tool_calls:
openai_tool_calls.append({
"id": tool_call.id,
"type": "function",
"function": {
"name": tool_call.function.name,
"arguments": tool_call.function.arguments
}
})
return openai_tool_calls

View File

@@ -33,7 +33,7 @@ broadscope_bailian
google-generativeai google-generativeai
# zhipuai # zhipuai
zhipuai>=2.0.1 zai-sdk
# tongyi qwen new sdk # tongyi qwen new sdk
dashscope dashscope