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 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.context import Context
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.log import logger
from models.openai_compatible_bot import OpenAICompatibleBot
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,
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'):
# 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
# Create a temporary mixin class that combines the bot with OpenAI compatibility

View File

@@ -1,12 +1,13 @@
{
"channel_type": "web",
"model": "claude-sonnet-4-5",
"open_ai_api_key": "",
"open_ai_api_base": "https://api.openai.com/v1",
"claude_api_key": "",
"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_base": "https://generativelanguage.googleapis.com",
"zhipu_ai_api_key": "",
"voice_to_text": "openai",
"text_to_voice": "openai",
"voice_reply_voice": false,

View File

@@ -6,8 +6,8 @@ from config import conf
class ZhipuAIImage(object):
def __init__(self):
from zhipuai import ZhipuAI
self.client = ZhipuAI(api_key=conf().get("zhipu_ai_api_key"))
from zai import ZhipuAiClient
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):
try:

View File

@@ -1,9 +1,8 @@
# encoding:utf-8
import time
import json
import openai
import openai.error
from models.bot import Bot
from models.zhipuai.zhipu_ai_session import ZhipuAISession
from models.zhipuai.zhipu_ai_image import ZhipuAIImage
@@ -12,7 +11,7 @@ from bridge.context import ContextType
from bridge.reply import Reply, ReplyType
from common.log import logger
from config import conf, load_config
from zhipuai import ZhipuAI
from zai import ZhipuAiClient
# ZhipuAI对话模型API
@@ -25,7 +24,7 @@ class ZHIPUAIBot(Bot, ZhipuAIImage):
"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)
}
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):
# acquire reply content
@@ -49,17 +48,13 @@ class ZHIPUAIBot(Bot, ZhipuAIImage):
session = self.sessions.session_query(query, session_id)
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")
new_args = None
if model:
new_args = self.args.copy()
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(
"[ZHIPU_AI] new_query={}, session_id={}, reply_cont={}, completion_tokens={}".format(
session.messages,
@@ -90,21 +85,17 @@ class ZHIPUAIBot(Bot, ZhipuAIImage):
reply = Reply(ReplyType.ERROR, "Bot不支持处理{}类型的消息".format(context.type))
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_id: session id
:param args: request arguments
:param retry_count: retry count
:return: {}
"""
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:
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)
# logger.debug("[ZHIPU_AI] response={}".format(response))
# 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:
need_retry = retry_count < 2
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))
result["content"] = "提问太快啦,请休息一下再问我吧"
if need_retry:
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))
result["content"] = "我没有收到你的消息"
if need_retry:
time.sleep(5)
elif isinstance(e, openai.error.APIError):
logger.warn("[ZHIPU_AI] Bad Gateway: {}".format(e))
elif "api" in error_str and ("error" in error_str or "gateway" in error_str):
logger.warn("[ZHIPU_AI] APIError: {}".format(e))
result["content"] = "请再问我一次"
if need_retry:
time.sleep(10)
elif isinstance(e, openai.error.APIConnectionError):
logger.warn("[ZHIPU_AI] APIConnectionError: {}".format(e))
elif "connection" in error_str or "network" in error_str:
logger.warn("[ZHIPU_AI] ConnectionError: {}".format(e))
result["content"] = "我连接不到你的网络"
if need_retry:
time.sleep(5)
@@ -144,6 +138,325 @@ class ZHIPUAIBot(Bot, ZhipuAIImage):
if need_retry:
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:
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
# zhipuai
zhipuai>=2.0.1
zai-sdk
# tongyi qwen new sdk
dashscope