ZyphrZero commited on
Commit
0a86b9b
·
1 Parent(s): 130b143

✅ fix(tool): 支持 Claude Code Router 兼容 Claude Code

Browse files
.env.example CHANGED
@@ -5,16 +5,12 @@
5
  # API 认证配置
6
  # =============================================================================
7
 
8
- # 客户端认证密钥(OpenAI 和 Anthropic 共用)
9
  # 客户端调用时需要使用此密钥进行认证
10
  AUTH_TOKEN=sk-your-api-key
11
  # 是否跳过api key验证
12
  SKIP_AUTH_TOKEN=false
13
 
14
- # Anthropic API 客户端认证密钥(可选)
15
- # 如果未设置,将使用 AUTH_TOKEN 的值
16
- # ANTHROPIC_API_KEY=sk-your-api-key
17
-
18
  # 备用认证令牌(匿名模式失败时使用)
19
  BACKUP_TOKEN=eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjMxNmJjYjQ4LWZmMmYtNGExNS04NTNkLWYyYTI5YjY3ZmYwZiIsImVtYWlsIjoiR3Vlc3QtMTc1NTg0ODU4ODc4OEBndWVzdC5jb20ifQ.PktllDySS3trlyuFpTeIZf-7hl8Qu1qYF3BxjgIul0BrNux2nX9hVzIjthLXKMWAf9V0qM8Vm_iyDqkjPGsaiQ
20
 
@@ -38,6 +34,9 @@ THINKING_MODEL=GLM-4.5-Thinking
38
  # 搜索模式模型名称
39
  SEARCH_MODEL=GLM-4.5-Search
40
 
 
 
 
41
  # =============================================================================
42
  # 服务器配置
43
  # =============================================================================
 
5
  # API 认证配置
6
  # =============================================================================
7
 
8
+ # 客户端认证密钥
9
  # 客户端调用时需要使用此密钥进行认证
10
  AUTH_TOKEN=sk-your-api-key
11
  # 是否跳过api key验证
12
  SKIP_AUTH_TOKEN=false
13
 
 
 
 
 
14
  # 备用认证令牌(匿名模式失败时使用)
15
  BACKUP_TOKEN=eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjMxNmJjYjQ4LWZmMmYtNGExNS04NTNkLWYyYTI5YjY3ZmYwZiIsImVtYWlsIjoiR3Vlc3QtMTc1NTg0ODU4ODc4OEBndWVzdC5jb20ifQ.PktllDySS3trlyuFpTeIZf-7hl8Qu1qYF3BxjgIul0BrNux2nX9hVzIjthLXKMWAf9V0qM8Vm_iyDqkjPGsaiQ
16
 
 
34
  # 搜索模式模型名称
35
  SEARCH_MODEL=GLM-4.5-Search
36
 
37
+ # Air 模型名称
38
+ AIR_MODEL=GLM-4.5-Air
39
+
40
  # =============================================================================
41
  # 服务器配置
42
  # =============================================================================
README.md CHANGED
@@ -1,24 +1,24 @@
1
- # Z.AI OpenAI & Anthropic API 代理服务
2
 
3
  ![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)
4
  ![Python: 3.8+](https://img.shields.io/badge/python-3.8+-green.svg)
5
  ![FastAPI](https://img.shields.io/badge/framework-FastAPI-009688.svg)
6
- ![Version: 1.1.0](https://img.shields.io/badge/version-1.1.0-brightgreen.svg)
7
 
8
- Z.AI 提供 OpenAI Anthropic API 兼容接口的轻量级代理服务,支持 GLM-4.5 系列模型的完整功能。
9
 
10
  ## ✨ 核心特性
11
 
12
  - 🔌 **完全兼容 OpenAI API** - 无缝集成现有应用
13
- - 🎭 **兼容 Anthropic API** - 支持 Claude CLI 客户端直接接入
14
  - 🚀 **高性能流式响应** - Server-Sent Events (SSE) 支持
15
- - 🛠️ **Function Call 支持** - 完整的工具调用功能
16
  - 🧠 **思考模式支持** - 智能处理模型推理过程
17
  - 🔍 **搜索模型集成** - GLM-4.5-Search 网络搜索能力
18
  - 🐳 **Docker 部署** - 一键容器化部署
19
  - 🛡️ **会话隔离** - 匿名模式保护隐私
20
- - 🔧 **高度可配置** - 环境变量灵活配置
21
- - 📊 **多模型架构** - 灵活的上游模型映射机制
22
 
23
  ## 🚀 快速开始
24
 
@@ -69,28 +69,6 @@ response = client.chat.completions.create(
69
  print(response.choices[0].message.content)
70
  ```
71
 
72
- #### Anthropic API 客户端
73
-
74
- ```python
75
- import anthropic
76
-
77
- # 初始化客户端
78
- client = anthropic.Anthropic(
79
- base_url="http://localhost:8080/v1",
80
- api_key="your-anthropic-token" # 替换为你的 ANTHROPIC_API_KEY
81
- )
82
-
83
- # 普通对话
84
- message = client.messages.create(
85
- model="GLM-4.5",
86
- max_tokens=1024,
87
- messages=[
88
- {"role": "user", "content": "你好,介绍一下 Python"}
89
- ]
90
- )
91
-
92
- print(message.content[0].text)
93
- ```
94
 
95
  ### Docker 部署
96
 
@@ -163,8 +141,7 @@ for chunk in response:
163
 
164
  | 变量名 | 默认值 | 说明 |
165
  |--------|--------|------|
166
- | `AUTH_TOKEN` | `sk-your-api-key` | 客户端认证密钥(OpenAI 和 Anthropic 共用) |
167
- | `ANTHROPIC_API_KEY` | `sk-your-api-key` | Anthropic API 认证密钥(默认使用 AUTH_TOKEN) |
168
  | `API_ENDPOINT` | `https://chat.z.ai/api/chat/completions` | 上游 API 地址 |
169
  | `LISTEN_PORT` | `8080` | 服务监听端口 |
170
  | `PRIMARY_MODEL` | `GLM-4.5` | 主要模型名称 |
@@ -244,8 +221,66 @@ if response.choices[0].message.tool_calls:
244
  **Q: 如何获取 AUTH_TOKEN?**
245
  A: `AUTH_TOKEN` 为自己自定义的api key,在环境变量中配置,需要保证客户端与服务端一致。
246
 
247
- **Q: ANTHROPIC_API_KEY 如何配置?**
248
- A: 默认使用 `AUTH_TOKEN` 的值,两个 API 使用相同的认证密钥。如需分开配置,可单独设置 `ANTHROPIC_API_KEY` 环境变量。
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
249
 
250
  **Q: 匿名模式是什么?**
251
  A: 匿名模式使用临时 token,避免对话历史共享,保护隐私。
@@ -256,8 +291,8 @@ A: 通过智能提示注入实现,将工具定义转换为系统提示。
256
  **Q: 支持哪些 OpenAI 功能?**
257
  A: 支持聊天完成、模型列表、流式响应、工具调用等核心功能。
258
 
259
- **Q: 支持 Anthropic API 的哪些功能?**
260
- A: 支持 messages 创建、流式响应、系统提示等核心功能。
261
 
262
  **Q: 如何选择合适的模型?**
263
  A:
@@ -274,34 +309,36 @@ A: 通过环境变量配置,推荐使用 `.env` 文件。
274
  ```
275
  ┌──────────────┐ ┌─────────────────────────┐ ┌─────────────────┐
276
  │ OpenAI │ │ │ │ │
277
- │ Client │────▶│ FastAPI Router │────▶│ Z.AI API │
278
  └──────────────┘ │ │ │ │
279
  ┌──────────────┐ │ ┌─────────────────────┐ │ │ ┌─────────────┐ │
280
- Anthropic │────▶│ OpenAI Endpoint │ │ │ │0727-360B-API│ │
281
- Client │ │ └─────────────────────┘ │ │ └─────────────┘ │
282
  └──────────────┘ │ ┌─────────────────────┐ │ │ ┌─────────────┐ │
283
- │ │ Anthropic Endpoint │ │────▶│ │0727-106B-API│ │
284
  │ └─────────────────────┘ │ │ └─────────────┘ │
285
  │ ┌─────────────────────┐ │ │ │
286
- │ │ Models Endpoint │ │ └─────────────────┘
287
  │ └─────────────────────┘ │
288
  └─────────────────────────┘
289
- Proxy Server
290
  ```
291
 
292
  ### 核心组件
293
 
294
  - **FastAPI** - 高性能 Web 框架,支持异步处理
295
- - **Pydantic** - 数据验证和序列化,确保 API 兼容性
296
  - **Uvicorn** - ASGI 服务器,提供高性能服务
297
- - **Requests** - HTTP 客户端,与上游 API 通信
 
298
 
299
  ### 架构特点
300
 
301
  - **模块化设计** - 清晰的目录结构,易于维护和扩展
302
- - **多协议支持** - 同时支持 OpenAI Anthropic API 协议
303
- - **动态路由** - 根据请求模型自动选择上游服务
304
- - **流式处理** - 完整支持 SSE 流式响应
 
305
  - **类型安全** - 基于 Pydantic 的严格类型检查
306
 
307
  ### 项目结构
@@ -309,27 +346,27 @@ A: 通过环境变量配置,推荐使用 `.env` 文件。
309
  ```
310
  z.ai2api_python/
311
  ├── app/
312
- │ ├── api/
313
- │ │ ├── __init__.py
314
- │ │ ├── openai.py # OpenAI API 路由
315
- │ │ └── anthropic.py # Anthropic API 路由
316
  │ ├── core/
317
  │ │ ├── __init__.py
318
  │ │ ├── config.py # 配置管理
 
319
  │ │ └── response_handlers.py # 响应处理器
320
  │ ├── models/
321
  │ │ ├── __init__.py
322
- │ │ └── schemas.py # 数据模型定义
323
  │ ├── utils/
324
  │ │ ├── __init__.py
325
- │ │ ├── helpers.py # 工具函数
326
- │ │ ├── tools.py # Function Call 处理
327
- │ │ └── sse_parser.py # SSE 解析器
328
  │ └── __init__.py
329
- ├── tests/ # 测试文件
330
- ├── deploy/ # 部署配置
331
- ├── main.py # 应用入口
332
- ├── requirements.txt # 依赖列表
 
 
 
333
  └── README.md # 项目文档
334
  ```
335
 
 
1
+ # Z.AI OpenAI API 代理服务
2
 
3
  ![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)
4
  ![Python: 3.8+](https://img.shields.io/badge/python-3.8+-green.svg)
5
  ![FastAPI](https://img.shields.io/badge/framework-FastAPI-009688.svg)
6
+ ![Version: 1.2.0](https://img.shields.io/badge/version-1.2.0-brightgreen.svg)
7
 
8
+ 轻量级 OpenAI API 兼容代理服务,通过 Claude Code Router 接入 Z.AI,支持 GLM-4.5 系列模型的完整功能。
9
 
10
  ## ✨ 核心特性
11
 
12
  - 🔌 **完全兼容 OpenAI API** - 无缝集成现有应用
13
+ - 🤖 **Claude Code 支持** - 通过 Claude Code Router 工具接入 Claude Code
14
  - 🚀 **高性能流式响应** - Server-Sent Events (SSE) 支持
15
+ - 🛠️ **增强工具调用** - 改进的 Function Call 实现
16
  - 🧠 **思考模式支持** - 智能处理模型推理过程
17
  - 🔍 **搜索模型集成** - GLM-4.5-Search 网络搜索能力
18
  - 🐳 **Docker 部署** - 一键容器化部署
19
  - 🛡️ **会话隔离** - 匿名模式保护隐私
20
+ - 🔧 **灵活配置** - 环境变量灵活配置
21
+ - 📊 **多模型映射** - 智能上游模型路由
22
 
23
  ## 🚀 快速开始
24
 
 
69
  print(response.choices[0].message.content)
70
  ```
71
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
 
73
  ### Docker 部署
74
 
 
141
 
142
  | 变量名 | 默认值 | 说明 |
143
  |--------|--------|------|
144
+ | `AUTH_TOKEN` | `sk-your-api-key` | 客户端认证密钥 |
 
145
  | `API_ENDPOINT` | `https://chat.z.ai/api/chat/completions` | 上游 API 地址 |
146
  | `LISTEN_PORT` | `8080` | 服务监听端口 |
147
  | `PRIMARY_MODEL` | `GLM-4.5` | 主要模型名称 |
 
221
  **Q: 如何获取 AUTH_TOKEN?**
222
  A: `AUTH_TOKEN` 为自己自定义的api key,在环境变量中配置,需要保证客户端与服务端一致。
223
 
224
+ **Q: 如何通过 Claude Code 使用本服务?**
225
+
226
+ A: 复制 [zai.js 文件](https://gist.githubusercontent.com/musistudio/b35402d6f9c95c64269c7666b8405348/raw/f108d66fa050f308387938f149a2b14a295d29e9/gistfile1.txt) 放在`.claude-code-router\\plugins`目录下,配置 Claude Code Router 指向本服务地址,使用 `AUTH_TOKEN` 进行认证。
227
+
228
+ 示例配置:
229
+ ```json
230
+ {
231
+ "LOG": false,
232
+ "LOG_LEVEL": "debug",
233
+ "CLAUDE_PATH": "",
234
+ "HOST": "127.0.0.1",
235
+ "PORT": 3456,
236
+ "APIKEY": "",
237
+ "API_TIMEOUT_MS": "600000",
238
+ "PROXY_URL": "",
239
+ "transformers": [
240
+ {
241
+ "name": "zai",
242
+ "path": "C:\\Users\\Administrator\\.claude-code-router\\plugins\\zai.js",
243
+ "options": {}
244
+ }
245
+ ],
246
+ "Providers": [
247
+ {
248
+ "name": "GLM",
249
+ "api_base_url": "http://127.0.0.1:8080/v1/chat/completions",
250
+ "api_key": "sk-your-api-key",
251
+ "models": [
252
+ "GLM-4.5",
253
+ "GLM-4.5-Air"
254
+ ],
255
+ "transformers": {
256
+ "use": [
257
+ "zai"
258
+ ]
259
+ }
260
+ }
261
+ ],
262
+ "StatusLine": {
263
+ "enabled": false,
264
+ "currentStyle": "default",
265
+ "default": {
266
+ "modules": []
267
+ },
268
+ "powerline": {
269
+ "modules": []
270
+ }
271
+ },
272
+ "Router": {
273
+ "default": "GLM,GLM-4.5",
274
+ "background": "GLM,GLM-4.5",
275
+ "think": "GLM,GLM-4.5",
276
+ "longContext": "GLM,GLM-4.5",
277
+ "longContextThreshold": 60000,
278
+ "webSearch": "GLM,GLM-4.5",
279
+ "image": "GLM,GLM-4.5"
280
+ },
281
+ "CUSTOM_ROUTER_PATH": ""
282
+ }
283
+ ```
284
 
285
  **Q: 匿名模式是什么?**
286
  A: 匿名模式使用临时 token,避免对话历史共享,保护隐私。
 
291
  **Q: 支持哪些 OpenAI 功能?**
292
  A: 支持聊天完成、模型列表、流式响应、工具调用等核心功能。
293
 
294
+ **Q: Function Call 如何优化?**
295
+ A: 改进了工具调用的请求响应结构,支持更复杂的工具链调用和并行执行。
296
 
297
  **Q: 如何选择合适的模型?**
298
  A:
 
309
  ```
310
  ┌──────────────┐ ┌─────────────────────────┐ ┌─────────────────┐
311
  │ OpenAI │ │ │ │ │
312
+ │ Client │────▶│ FastAPI Server │────▶│ Z.AI API │
313
  └──────────────┘ │ │ │ │
314
  ┌──────────────┐ │ ┌─────────────────────┐ │ │ ┌─────────────┐ │
315
+ Claude Code │ │/v1/chat/completions│ │ │ │0727-360B-API│ │
316
+ Router │────▶│ └─────────────────────┘ │ │ └─────────────┘ │
317
  └──────────────┘ │ ┌─────────────────────┐ │ │ ┌─────────────┐ │
318
+ │ │ /v1/models │ │────▶│ │0727-106B-API│ │
319
  │ └─────────────────────┘ │ │ └─────────────┘ │
320
  │ ┌─────────────────────┐ │ │ │
321
+ │ │ Enhanced Tools │ │ └─────────────────┘
322
  │ └─────────────────────┘ │
323
  └─────────────────────────┘
324
+ OpenAI Compatible API
325
  ```
326
 
327
  ### 核心组件
328
 
329
  - **FastAPI** - 高性能 Web 框架,支持异步处理
330
+ - **Pydantic** - 数据验证和序列化,确保 API 兼容性
331
  - **Uvicorn** - ASGI 服务器,提供高性能服务
332
+ - **httpx** - 现代 HTTP 客户端,支持异步请求
333
+ - **SSE Parser** - 流式响应处理,优化实时交互
334
 
335
  ### 架构特点
336
 
337
  - **模块化设计** - 清晰的目录结构,易于维护和扩展
338
+ - **标准 OpenAI 协议** - 完全兼容 OpenAI API v1 规范
339
+ - **智能模型路由** - 根据模型特性自动选择最优上游
340
+ - **增强工具调用** - 改进的 Function Call 处理机制
341
+ - **流式处理** - 优化的 SSE 流式响应实现
342
  - **类型安全** - 基于 Pydantic 的严格类型检查
343
 
344
  ### 项目结构
 
346
  ```
347
  z.ai2api_python/
348
  ├── app/
 
 
 
 
349
  │ ├── core/
350
  │ │ ├── __init__.py
351
  │ │ ├── config.py # 配置管理
352
+ │ │ ├── openai.py # OpenAI API 实现
353
  │ │ └── response_handlers.py # 响应处理器
354
  │ ├── models/
355
  │ │ ├── __init__.py
356
+ │ │ └── schemas.py # Pydantic 模型定义
357
  │ ├── utils/
358
  │ │ ├── __init__.py
359
+ │ │ ├── helpers.py # 辅助函数
360
+ │ │ ├── tools.py # 增强工具调用处理
361
+ │ │ └── sse_parser.py # SSE 流式解析器
362
  │ └── __init__.py
363
+ ├── tests/ # 单元测试
364
+ ├── test_tool_call.py # 工具调用测试
365
+ │ └── test_function_call.py # Function Call 测试
366
+ ├── deploy/ # Docker 部署配置
367
+ ├── main.py # FastAPI 应用入口
368
+ ├── requirements.txt # Python 依赖
369
+ ├── .env.example # 环境变量示例
370
  └── README.md # 项目文档
371
  ```
372
 
app/__init__.py CHANGED
@@ -2,6 +2,6 @@
2
  Application package initialization
3
  """
4
 
5
- from app import api, core, models, utils
6
 
7
- __all__ = ["api", "core", "models", "utils"]
 
2
  Application package initialization
3
  """
4
 
5
+ from app import core, models, utils
6
 
7
+ __all__ = ["core", "models", "utils"]
app/api/__init__.py DELETED
@@ -1,7 +0,0 @@
1
- """
2
- API module initialization
3
- """
4
-
5
- from app.api import openai, anthropic
6
-
7
- __all__ = ["openai", "anthropic"]
 
 
 
 
 
 
 
 
app/api/anthropic.py DELETED
@@ -1,276 +0,0 @@
1
- """
2
- Anthropic API compatibility endpoints
3
- """
4
-
5
- import json
6
- import time
7
- import uuid
8
- from typing import Generator
9
- import requests
10
- from fastapi import APIRouter, Header, HTTPException
11
- from fastapi.responses import StreamingResponse
12
-
13
- from app.core.config import settings
14
- from app.models.schemas import (
15
- AnthropicRequest, Message, UpstreamRequest, ModelItem,
16
- ContentBlock
17
- )
18
- from app.utils.helpers import debug_log, generate_request_ids, get_auth_token, get_browser_headers, transform_thinking_content
19
-
20
- router = APIRouter()
21
-
22
-
23
- def stream_anthropic_generator(upstream_response: requests.Response, request_id: str, requested_model: str) -> Generator[str, None, None]:
24
- """生成 Anthropic 兼容的流式响应事件"""
25
- usage = {"input_tokens": 0, "output_tokens": 0}
26
-
27
- start_event = {
28
- "type": "message_start",
29
- "message": {
30
- "id": request_id,
31
- "type": "message",
32
- "role": "assistant",
33
- "content": [],
34
- "model": requested_model,
35
- "stop_reason": None,
36
- "stop_sequence": None,
37
- "usage": usage
38
- }
39
- }
40
- yield f"event: {start_event['type']}\ndata: {json.dumps(start_event['message'])}\n\n"
41
-
42
- # 发送 content_block_start 事件
43
- content_start_data = {
44
- "type": "content_block_start",
45
- "index": 0,
46
- "content_block": {
47
- "type": "text",
48
- "text": ""
49
- }
50
- }
51
- yield f"event: content_block_start\ndata: {json.dumps(content_start_data)}\n\n"
52
-
53
- # 处理上游响应
54
- for line in upstream_response.iter_lines():
55
- if not line.startswith(b"data:"): continue
56
- data_str = line[5:].strip()
57
- if not data_str: continue
58
- try:
59
- data = json.loads(data_str.decode('utf-8'))
60
- delta_content = data.get("data", {}).get("delta_content", "")
61
- phase = data.get("data", {}).get("phase", "")
62
-
63
- # 处理内容增量
64
- if delta_content:
65
- out_content = transform_thinking_content(delta_content) if phase == "thinking" else delta_content
66
- if out_content:
67
- usage["output_tokens"] += len(out_content) // 4 # 简单估算
68
- delta_data = {
69
- "type": "content_block_delta",
70
- "index": 0,
71
- "delta": {
72
- "type": "text_delta",
73
- "text": out_content
74
- }
75
- }
76
- yield f"event: content_block_delta\ndata: {json.dumps(delta_data)}\n\n"
77
-
78
- # 处理结束
79
- if data.get("data", {}).get("done", False) or phase == "done":
80
- # 发送 content_block_stop
81
- content_stop_data = {
82
- "type": "content_block_stop",
83
- "index": 0
84
- }
85
- yield f"event: content_block_stop\ndata: {json.dumps(content_stop_data)}\n\n"
86
-
87
- # 发送 message_delta
88
- message_delta_data = {
89
- "type": "message_delta",
90
- "delta": {
91
- "stop_reason": "end_turn",
92
- "stop_sequence": None,
93
- "usage": {
94
- "input_tokens": usage["input_tokens"],
95
- "output_tokens": usage["output_tokens"]
96
- }
97
- }
98
- }
99
- yield f"event: message_delta\ndata: {json.dumps(message_delta_data)}\n\n"
100
-
101
- # 发送 message_stop
102
- yield f"event: message_stop\ndata: {json.dumps({'type': 'message_stop'})}\n\n"
103
- break
104
-
105
- except json.JSONDecodeError:
106
- continue
107
-
108
-
109
- @router.post("/v1/messages")
110
- async def handle_anthropic_message(
111
- req: AnthropicRequest,
112
- x_api_key: str = Header(None, alias="x-api-key"),
113
- authorization: str = Header(None, alias="authorization")
114
- ):
115
- """Handle Anthropic message requests"""
116
- debug_log("收到 Anthropic message 请求")
117
-
118
- # 验证 API key (skip if SKIP_AUTH_TOKEN is enabled)
119
- if not settings.SKIP_AUTH_TOKEN:
120
- api_key = None
121
- if x_api_key:
122
- api_key = x_api_key
123
- elif authorization and authorization.startswith("Bearer "):
124
- api_key = authorization[7:]
125
-
126
- if not api_key or api_key != settings.ANTHROPIC_API_KEY:
127
- debug_log(f"无效的 API key: {api_key}")
128
- raise HTTPException(status_code=401, detail="Invalid API key")
129
-
130
- debug_log(f"API key 验证通过")
131
- else:
132
- debug_log("SKIP_AUTH_TOKEN已启用,跳过API key验证")
133
- debug_log(f"请求解析成功 - 模型: {req.model}, 流式: {req.stream}, 消息数: {len(req.messages)}")
134
-
135
- # 确定上游模型和功能
136
- upstream_model = "GLM-4.5"
137
- if req.model == settings.THINKING_MODEL:
138
- upstream_model = "GLM-4.5-Thinking"
139
- elif req.model == settings.SEARCH_MODEL:
140
- upstream_model = "GLM-4.5-Search"
141
-
142
- debug_log(f"收到请求 (模型: {req.model}) -> 代理到上游 (模型: {upstream_model})")
143
-
144
- # 生成 ID
145
- chat_id, msg_id = generate_request_ids()
146
-
147
- # 转换消息格式
148
- openai_messages = []
149
- if req.system:
150
- # 处理两种格式的 system 内容
151
- if isinstance(req.system, str):
152
- # 字符串格式
153
- system_content = req.system
154
- else:
155
- # 对象数组格式
156
- system_content = ""
157
- for block in req.system:
158
- if block.type == "text":
159
- system_content += block.text
160
-
161
- openai_messages.append({"role": "system", "content": system_content})
162
-
163
- for msg in req.messages:
164
- # 处理两种格式的内容
165
- if isinstance(msg.content, str):
166
- # 字符串格式
167
- text_content = msg.content
168
- else:
169
- # 对象数组格式
170
- text_content = ""
171
- for block in msg.content:
172
- if block.type == "text":
173
- text_content += block.text
174
-
175
- openai_messages.append({
176
- "role": msg.role,
177
- "content": text_content
178
- })
179
-
180
- # 构建上游请求
181
- upstream_messages = []
182
- for msg in openai_messages:
183
- content = msg.get("content", "")
184
- if content is None:
185
- content = ""
186
- upstream_messages.append(Message(
187
- role=msg["role"],
188
- content=content
189
- ))
190
-
191
- upstream_req = UpstreamRequest(
192
- stream=True, # 总是使用上游的流式
193
- chat_id=chat_id,
194
- id=msg_id,
195
- model="0727-360B-API", # 实际的上游模型 ID
196
- messages=upstream_messages,
197
- params={},
198
- features={"enable_thinking": True},
199
- background_tasks={
200
- "title_generation": False,
201
- "tags_generation": False,
202
- },
203
- mcp_servers=[],
204
- model_item=ModelItem(
205
- id="0727-360B-API",
206
- name="GLM-4.5",
207
- owned_by="openai"
208
- ),
209
- tool_servers=[],
210
- variables={
211
- "{{USER_NAME}}": "User",
212
- "{{USER_LOCATION}}": "Unknown",
213
- "{{CURRENT_DATETIME}}": time.strftime("%Y-%m-%d %H:%M:%S"),
214
- }
215
- )
216
-
217
- # 获取认证 token
218
- auth_token = get_auth_token()
219
-
220
- try:
221
- # 调用上游 API
222
- headers = get_browser_headers(chat_id)
223
- headers["Authorization"] = f"Bearer {auth_token}"
224
-
225
- response = requests.post(
226
- settings.API_ENDPOINT,
227
- json=upstream_req.model_dump(exclude_none=True),
228
- headers=headers,
229
- timeout=60.0,
230
- stream=True
231
- )
232
- response.raise_for_status()
233
- except requests.HTTPError as e:
234
- debug_log(f"上游 API 返回错误状态: {e.response.status_code}, 响应: {e.response.text}")
235
- raise HTTPException(status_code=502, detail="Upstream API error")
236
- except requests.RequestException as e:
237
- debug_log(f"请求上游 API 失败: {e}")
238
- raise HTTPException(status_code=502, detail=f"Failed to call upstream API: {e}")
239
-
240
- request_id = f"msg_{uuid.uuid4().hex}"
241
-
242
- if req.stream:
243
- # 流式响应
244
- return StreamingResponse(
245
- stream_anthropic_generator(response, request_id, req.model),
246
- media_type="text/event-stream",
247
- headers={"Cache-Control": "no-cache", "Connection": "keep-alive"}
248
- )
249
- else:
250
- # 非流式响应
251
- full_content = ""
252
- for line in response.iter_lines():
253
- if not line.startswith(b"data:"): continue
254
- data_str = line[5:].strip()
255
- if not data_str: continue
256
- try:
257
- data = json.loads(data_str.decode('utf-8'))
258
- delta_content = data.get("data", {}).get("delta_content", "")
259
- phase = data.get("data", {}).get("phase", "")
260
- if delta_content:
261
- out_content = transform_thinking_content(delta_content) if phase == "thinking" else delta_content
262
- if out_content: full_content += out_content
263
- if data.get("data", {}).get("done", False) or phase == "done":
264
- break
265
- except json.JSONDecodeError:
266
- continue
267
-
268
- return {
269
- "id": request_id,
270
- "type": "message",
271
- "role": "assistant",
272
- "model": req.model,
273
- "content": [{"type": "text", "text": full_content}],
274
- "stop_reason": "end_turn",
275
- "usage": {"input_tokens": 0, "output_tokens": len(full_content) // 4}
276
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/core/__init__.py CHANGED
@@ -2,6 +2,6 @@
2
  Core module initialization
3
  """
4
 
5
- from app.core import config, response_handlers
6
 
7
- __all__ = ["config", "response_handlers"]
 
2
  Core module initialization
3
  """
4
 
5
+ from app.core import config, response_handlers, openai
6
 
7
+ __all__ = ["config", "response_handlers", "openai"]
app/core/config.py CHANGED
@@ -13,7 +13,6 @@ class Settings(BaseSettings):
13
  # API Configuration
14
  API_ENDPOINT: str = os.getenv("API_ENDPOINT", "https://chat.z.ai/api/chat/completions")
15
  AUTH_TOKEN: str = os.getenv("AUTH_TOKEN", "sk-your-api-key")
16
- ANTHROPIC_API_KEY: str = os.getenv("ANTHROPIC_API_KEY", AUTH_TOKEN)
17
  BACKUP_TOKEN: str = os.getenv("BACKUP_TOKEN", "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjMxNmJjYjQ4LWZmMmYtNGExNS04NTNkLWYyYTI5YjY3ZmYwZiIsImVtYWlsIjoiR3Vlc3QtMTc1NTg0ODU4ODc4OEBndWVzdC5jb20ifQ.PktllDySS3trlyuFpTeIZf-7hl8Qu1qYF3BxjgIul0BrNux2nX9hVzIjthLXKMWAf9V0qM8Vm_iyDqkjPGsaiQ")
18
 
19
  # Model Configuration
 
13
  # API Configuration
14
  API_ENDPOINT: str = os.getenv("API_ENDPOINT", "https://chat.z.ai/api/chat/completions")
15
  AUTH_TOKEN: str = os.getenv("AUTH_TOKEN", "sk-your-api-key")
 
16
  BACKUP_TOKEN: str = os.getenv("BACKUP_TOKEN", "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjMxNmJjYjQ4LWZmMmYtNGExNS04NTNkLWYyYTI5YjY3ZmYwZiIsImVtYWlsIjoiR3Vlc3QtMTc1NTg0ODU4ODc4OEBndWVzdC5jb20ifQ.PktllDySS3trlyuFpTeIZf-7hl8Qu1qYF3BxjgIul0BrNux2nX9hVzIjthLXKMWAf9V0qM8Vm_iyDqkjPGsaiQ")
17
 
18
  # Model Configuration
app/{api → core}/openai.py RENAMED
@@ -14,7 +14,7 @@ from app.models.schemas import (
14
  ModelsResponse, Model
15
  )
16
  from app.utils.helpers import debug_log, generate_request_ids, get_auth_token
17
- from app.utils.tools import process_messages_with_tools
18
  from app.core.response_handlers import StreamResponseHandler, NonStreamResponseHandler
19
 
20
  router = APIRouter()
@@ -89,10 +89,7 @@ async def chat_completions(
89
  # Convert back to Message objects
90
  upstream_messages: List[Message] = []
91
  for msg in processed_messages:
92
- content = msg.get("content")
93
- # Ensure content is not None for Message model
94
- if content is None:
95
- content = ""
96
 
97
  upstream_messages.append(Message(
98
  role=msg["role"],
 
14
  ModelsResponse, Model
15
  )
16
  from app.utils.helpers import debug_log, generate_request_ids, get_auth_token
17
+ from app.utils.tools import process_messages_with_tools, content_to_string
18
  from app.core.response_handlers import StreamResponseHandler, NonStreamResponseHandler
19
 
20
  router = APIRouter()
 
89
  # Convert back to Message objects
90
  upstream_messages: List[Message] = []
91
  for msg in processed_messages:
92
+ content = content_to_string(msg.get("content"))
 
 
 
93
 
94
  upstream_messages.append(Message(
95
  role=msg["role"],
app/core/response_handlers.py CHANGED
@@ -205,26 +205,28 @@ class StreamResponseHandler(ResponseHandler):
205
 
206
  def _send_end_chunk(self) -> Generator[str, None, None]:
207
  """Send end chunk and DONE signal"""
 
 
208
  if self.has_tools:
209
  # Try to extract tool calls from buffered content
210
  self.tool_calls = extract_tool_invocations(self.buffered_content)
211
 
212
  if self.tool_calls:
213
- # Send tool calls
214
- tool_calls_list = []
215
  for i, tc in enumerate(self.tool_calls):
216
- tool_calls_list.append({
217
  "index": i,
218
  "id": tc.get("id"),
219
  "type": tc.get("type", "function"),
220
  "function": tc.get("function", {}),
221
- })
 
 
 
 
 
 
222
 
223
- out_chunk = create_openai_response_chunk(
224
- model=settings.PRIMARY_MODEL,
225
- delta=Delta(tool_calls=tool_calls_list)
226
- )
227
- yield f"data: {out_chunk.model_dump_json()}\n\n"
228
  finish_reason = "tool_calls"
229
  else:
230
  # Send regular content
@@ -235,9 +237,6 @@ class StreamResponseHandler(ResponseHandler):
235
  delta=Delta(content=trimmed_content)
236
  )
237
  yield f"data: {content_chunk.model_dump_json()}\n\n"
238
- finish_reason = "stop"
239
- else:
240
- finish_reason = "stop"
241
 
242
  # Send final chunk
243
  end_chunk = create_openai_response_chunk(
@@ -305,9 +304,12 @@ class NonStreamResponseHandler(ResponseHandler):
305
  # Content must be null when tool_calls are present (OpenAI spec)
306
  message_content = None
307
  finish_reason = "tool_calls"
 
308
  else:
309
  # Remove tool JSON from content
310
  message_content = remove_tool_json_content(final_content)
 
 
311
 
312
  # Build response
313
  response_data = OpenAIResponse(
 
205
 
206
  def _send_end_chunk(self) -> Generator[str, None, None]:
207
  """Send end chunk and DONE signal"""
208
+ finish_reason = "stop"
209
+
210
  if self.has_tools:
211
  # Try to extract tool calls from buffered content
212
  self.tool_calls = extract_tool_invocations(self.buffered_content)
213
 
214
  if self.tool_calls:
215
+ # Send tool calls with proper format
 
216
  for i, tc in enumerate(self.tool_calls):
217
+ tool_call_delta = {
218
  "index": i,
219
  "id": tc.get("id"),
220
  "type": tc.get("type", "function"),
221
  "function": tc.get("function", {}),
222
+ }
223
+
224
+ out_chunk = create_openai_response_chunk(
225
+ model=settings.PRIMARY_MODEL,
226
+ delta=Delta(tool_calls=[tool_call_delta])
227
+ )
228
+ yield f"data: {out_chunk.model_dump_json()}\n\n"
229
 
 
 
 
 
 
230
  finish_reason = "tool_calls"
231
  else:
232
  # Send regular content
 
237
  delta=Delta(content=trimmed_content)
238
  )
239
  yield f"data: {content_chunk.model_dump_json()}\n\n"
 
 
 
240
 
241
  # Send final chunk
242
  end_chunk = create_openai_response_chunk(
 
304
  # Content must be null when tool_calls are present (OpenAI spec)
305
  message_content = None
306
  finish_reason = "tool_calls"
307
+ debug_log(f"提取到工具调用: {json.dumps(tool_calls, ensure_ascii=False)}")
308
  else:
309
  # Remove tool JSON from content
310
  message_content = remove_tool_json_content(final_content)
311
+ if not message_content:
312
+ message_content = final_content # 保留原内容如果清理后为空
313
 
314
  # Build response
315
  response_data = OpenAIResponse(
app/models/schemas.py CHANGED
@@ -6,10 +6,16 @@ from typing import Dict, List, Optional, Any, Union, Literal
6
  from pydantic import BaseModel
7
 
8
 
 
 
 
 
 
 
9
  class Message(BaseModel):
10
  """Chat message model"""
11
  role: str
12
- content: Optional[str] = None
13
  reasoning_content: Optional[str] = None
14
  tool_calls: Optional[List[Dict[str, Any]]] = None
15
 
@@ -125,21 +131,3 @@ class ModelsResponse(BaseModel):
125
  data: List[Model]
126
 
127
 
128
- # Anthropic API Models
129
- class ContentBlock(BaseModel):
130
- type: str
131
- text: str
132
-
133
-
134
- class AnthropicMessage(BaseModel):
135
- role: Literal["user", "assistant"]
136
- content: Union[str, List[ContentBlock]]
137
-
138
-
139
- class AnthropicRequest(BaseModel):
140
- model: str
141
- messages: List[AnthropicMessage]
142
- system: Optional[Union[str, List[ContentBlock]]] = None
143
- max_tokens: int = 1024
144
- stream: bool = False
145
- temperature: Optional[float] = None
 
6
  from pydantic import BaseModel
7
 
8
 
9
+ class ContentPart(BaseModel):
10
+ """Content part model for OpenAI's new content format"""
11
+ type: str
12
+ text: Optional[str] = None
13
+
14
+
15
  class Message(BaseModel):
16
  """Chat message model"""
17
  role: str
18
+ content: Optional[Union[str, List[ContentPart]]] = None
19
  reasoning_content: Optional[str] = None
20
  tool_calls: Optional[List[Dict[str, Any]]] = None
21
 
 
131
  data: List[Model]
132
 
133
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/utils/sse_parser.py CHANGED
@@ -6,16 +6,13 @@ import json
6
  from typing import Dict, Any, Generator, Optional, Type
7
  import requests
8
 
9
- from app.core.config import settings
10
- from app.models.schemas import UpstreamData
11
-
12
 
13
  class SSEParser:
14
  """Server-Sent Events parser for streaming responses"""
15
-
16
  def __init__(self, response: requests.Response, debug_mode: bool = False):
17
  """Initialize SSE parser
18
-
19
  Args:
20
  response: requests.Response object with stream=True
21
  debug_mode: Enable debug logging
@@ -24,7 +21,7 @@ class SSEParser:
24
  self.debug_mode = debug_mode
25
  self.buffer = ""
26
  self.line_count = 0
27
-
28
  def debug_log(self, format_str: str, *args) -> None:
29
  """Log debug message if debug mode is enabled"""
30
  if self.debug_mode:
@@ -32,112 +29,99 @@ class SSEParser:
32
  print(f"[SSE_PARSER] {format_str % args}")
33
  else:
34
  print(f"[SSE_PARSER] {format_str}")
35
-
36
  def iter_events(self) -> Generator[Dict[str, Any], None, None]:
37
  """Iterate over SSE events
38
-
39
  Yields:
40
  dict: Parsed SSE event data
41
  """
42
  self.debug_log("开始解析 SSE 流")
43
-
44
  for line in self.response.iter_lines():
45
  self.line_count += 1
46
-
47
  # Skip empty lines
48
  if not line:
49
  continue
50
-
51
  # Decode bytes
52
  if isinstance(line, bytes):
53
  try:
54
- line = line.decode('utf-8')
55
  except UnicodeDecodeError:
56
  self.debug_log(f"第{self.line_count}行解码失败,跳过")
57
  continue
58
-
59
  # Skip comment lines
60
- if line.startswith(':'):
61
  continue
62
-
63
  # Parse field-value pairs
64
- if ':' in line:
65
- field, value = line.split(':', 1)
66
  field = field.strip()
67
  value = value.lstrip()
68
-
69
- if field == 'data':
70
  self.debug_log(f"收到数据 (第{self.line_count}行): {value}")
71
-
72
  # Try to parse JSON
73
  try:
74
  data = json.loads(value)
75
- yield {
76
- 'type': 'data',
77
- 'data': data,
78
- 'raw': value
79
- }
80
  except json.JSONDecodeError:
81
- yield {
82
- 'type': 'data',
83
- 'data': value,
84
- 'raw': value,
85
- 'is_json': False
86
- }
87
-
88
- elif field == 'event':
89
- yield {'type': 'event', 'event': value}
90
-
91
- elif field == 'id':
92
- yield {'type': 'id', 'id': value}
93
-
94
- elif field == 'retry':
95
  try:
96
  retry = int(value)
97
- yield {'type': 'retry', 'retry': retry}
98
  except ValueError:
99
  self.debug_log(f"无效的 retry 值: {value}")
100
-
101
  def iter_data_only(self) -> Generator[Dict[str, Any], None, None]:
102
  """Iterate only over data events"""
103
  for event in self.iter_events():
104
- if event['type'] == 'data':
105
  yield event
106
-
107
  def iter_json_data(self, model_class: Optional[Type] = None) -> Generator[Dict[str, Any], None, None]:
108
  """Iterate only over JSON data events with optional validation
109
-
110
  Args:
111
  model_class: Optional Pydantic model class for validation
112
-
113
  Yields:
114
  dict: JSON data events
115
  """
116
  for event in self.iter_events():
117
- if event['type'] == 'data' and event.get('is_json', True):
118
  try:
119
  if model_class:
120
- data = model_class.model_validate_json(event['raw'])
121
- yield {
122
- 'type': 'data',
123
- 'data': data,
124
- 'raw': event['raw']
125
- }
126
  else:
127
  yield event
128
  except Exception as e:
129
  self.debug_log(f"数据验证失败: {e}")
130
  continue
131
-
132
  def close(self) -> None:
133
  """Close the response connection"""
134
- if hasattr(self.response, 'close'):
135
  self.response.close()
136
-
137
  def __enter__(self):
138
  """Context manager entry"""
139
  return self
140
-
141
  def __exit__(self, exc_type, exc_val, exc_tb) -> None:
142
  """Context manager exit"""
143
- self.close()
 
6
  from typing import Dict, Any, Generator, Optional, Type
7
  import requests
8
 
 
 
 
9
 
10
  class SSEParser:
11
  """Server-Sent Events parser for streaming responses"""
12
+
13
  def __init__(self, response: requests.Response, debug_mode: bool = False):
14
  """Initialize SSE parser
15
+
16
  Args:
17
  response: requests.Response object with stream=True
18
  debug_mode: Enable debug logging
 
21
  self.debug_mode = debug_mode
22
  self.buffer = ""
23
  self.line_count = 0
24
+
25
  def debug_log(self, format_str: str, *args) -> None:
26
  """Log debug message if debug mode is enabled"""
27
  if self.debug_mode:
 
29
  print(f"[SSE_PARSER] {format_str % args}")
30
  else:
31
  print(f"[SSE_PARSER] {format_str}")
32
+
33
  def iter_events(self) -> Generator[Dict[str, Any], None, None]:
34
  """Iterate over SSE events
35
+
36
  Yields:
37
  dict: Parsed SSE event data
38
  """
39
  self.debug_log("开始解析 SSE 流")
40
+
41
  for line in self.response.iter_lines():
42
  self.line_count += 1
43
+
44
  # Skip empty lines
45
  if not line:
46
  continue
47
+
48
  # Decode bytes
49
  if isinstance(line, bytes):
50
  try:
51
+ line = line.decode("utf-8")
52
  except UnicodeDecodeError:
53
  self.debug_log(f"第{self.line_count}行解码失败,跳过")
54
  continue
55
+
56
  # Skip comment lines
57
+ if line.startswith(":"):
58
  continue
59
+
60
  # Parse field-value pairs
61
+ if ":" in line:
62
+ field, value = line.split(":", 1)
63
  field = field.strip()
64
  value = value.lstrip()
65
+
66
+ if field == "data":
67
  self.debug_log(f"收到数据 (第{self.line_count}行): {value}")
68
+
69
  # Try to parse JSON
70
  try:
71
  data = json.loads(value)
72
+ yield {"type": "data", "data": data, "raw": value}
 
 
 
 
73
  except json.JSONDecodeError:
74
+ yield {"type": "data", "data": value, "raw": value, "is_json": False}
75
+
76
+ elif field == "event":
77
+ yield {"type": "event", "event": value}
78
+
79
+ elif field == "id":
80
+ yield {"type": "id", "id": value}
81
+
82
+ elif field == "retry":
 
 
 
 
 
83
  try:
84
  retry = int(value)
85
+ yield {"type": "retry", "retry": retry}
86
  except ValueError:
87
  self.debug_log(f"无效的 retry 值: {value}")
88
+
89
  def iter_data_only(self) -> Generator[Dict[str, Any], None, None]:
90
  """Iterate only over data events"""
91
  for event in self.iter_events():
92
+ if event["type"] == "data":
93
  yield event
94
+
95
  def iter_json_data(self, model_class: Optional[Type] = None) -> Generator[Dict[str, Any], None, None]:
96
  """Iterate only over JSON data events with optional validation
97
+
98
  Args:
99
  model_class: Optional Pydantic model class for validation
100
+
101
  Yields:
102
  dict: JSON data events
103
  """
104
  for event in self.iter_events():
105
+ if event["type"] == "data" and event.get("is_json", True):
106
  try:
107
  if model_class:
108
+ data = model_class.model_validate_json(event["raw"])
109
+ yield {"type": "data", "data": data, "raw": event["raw"]}
 
 
 
 
110
  else:
111
  yield event
112
  except Exception as e:
113
  self.debug_log(f"数据验证失败: {e}")
114
  continue
115
+
116
  def close(self) -> None:
117
  """Close the response connection"""
118
+ if hasattr(self.response, "close"):
119
  self.response.close()
120
+
121
  def __enter__(self):
122
  """Context manager entry"""
123
  return self
124
+
125
  def __exit__(self, exc_type, exc_val, exc_tb) -> None:
126
  """Context manager exit"""
127
+ self.close()
app/utils/tools.py CHANGED
@@ -10,28 +10,43 @@ from typing import Dict, List, Optional, Any
10
  from app.core.config import settings
11
 
12
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  def generate_tool_prompt(tools: List[Dict[str, Any]]) -> str:
14
  """Generate tool injection prompt with enhanced formatting"""
15
  if not tools:
16
  return ""
17
-
18
  tool_definitions = []
19
  for tool in tools:
20
  if tool.get("type") != "function":
21
  continue
22
-
23
  function_spec = tool.get("function", {}) or {}
24
  function_name = function_spec.get("name", "unknown")
25
  function_description = function_spec.get("description", "")
26
  parameters = function_spec.get("parameters", {}) or {}
27
-
28
  # Create structured tool definition
29
  tool_info = [f"## {function_name}", f"**Purpose**: {function_description}"]
30
-
31
  # Add parameter details
32
  parameter_properties = parameters.get("properties", {}) or {}
33
  required_parameters = set(parameters.get("required", []) or [])
34
-
35
  if parameter_properties:
36
  tool_info.append("**Parameters**:")
37
  for param_name, param_details in parameter_properties.items():
@@ -39,111 +54,103 @@ def generate_tool_prompt(tools: List[Dict[str, Any]]) -> str:
39
  param_desc = (param_details or {}).get("description", "")
40
  requirement_flag = "**Required**" if param_name in required_parameters else "*Optional*"
41
  tool_info.append(f"- `{param_name}` ({param_type}) - {requirement_flag}: {param_desc}")
42
-
43
  tool_definitions.append("\n".join(tool_info))
44
-
45
  if not tool_definitions:
46
  return ""
47
-
48
  # Build comprehensive tool prompt
49
  prompt_template = (
50
- "\n\n# AVAILABLE FUNCTIONS\n" +
51
- "\n\n---\n".join(tool_definitions) +
52
- "\n\n# USAGE INSTRUCTIONS\n"
53
  "When you need to execute a function, respond ONLY with a JSON object containing tool_calls:\n"
54
  "```json\n"
55
  "{\n"
56
  ' "tool_calls": [\n'
57
  " {\n"
58
- ' "id": "call_" + unique_id,\n'
59
  ' "type": "function",\n'
60
  ' "function": {\n'
61
  ' "name": "function_name",\n'
62
- ' "arguments": {\n'
63
- ' "param1": "value1"\n'
64
- ' }\n'
65
  " }\n"
66
  " }\n"
67
  " ]\n"
68
  "}\n"
69
  "```\n"
70
- "Important: No explanatory text before or after the JSON.\n"
71
  )
72
-
73
  return prompt_template
74
 
75
 
76
  def process_messages_with_tools(
77
- messages: List[Dict[str, Any]],
78
- tools: Optional[List[Dict[str, Any]]] = None,
79
- tool_choice: Optional[Any] = None
80
  ) -> List[Dict[str, Any]]:
81
  """Process messages and inject tool prompts"""
82
  processed: List[Dict[str, Any]] = []
83
-
84
  if tools and settings.TOOL_SUPPORT and (tool_choice != "none"):
85
  tools_prompt = generate_tool_prompt(tools)
86
  has_system = any(m.get("role") == "system" for m in messages)
87
-
88
  if has_system:
89
  for m in messages:
90
  if m.get("role") == "system":
91
  mm = dict(m)
92
- content = mm.get("content", "")
93
- if content is None:
94
- content = ""
95
  mm["content"] = content + tools_prompt
96
  processed.append(mm)
97
  else:
98
  processed.append(m)
99
  else:
100
  processed = [{"role": "system", "content": "你是一个有用的助手。" + tools_prompt}] + messages
101
-
102
  # Add tool choice hints
103
  if tool_choice in ("required", "auto"):
104
  if processed and processed[-1].get("role") == "user":
105
  last = dict(processed[-1])
106
- content = last.get("content", "")
107
- if content is None:
108
- content = ""
109
  last["content"] = content + "\n\n请根据需要使用提供的工具函数。"
110
  processed[-1] = last
111
  elif isinstance(tool_choice, dict) and tool_choice.get("type") == "function":
112
  fname = (tool_choice.get("function") or {}).get("name")
113
  if fname and processed and processed[-1].get("role") == "user":
114
  last = dict(processed[-1])
115
- content = last.get("content", "")
116
- if content is None:
117
- content = ""
118
  last["content"] = content + f"\n\n请使用 {fname} 函数来处理这个请求。"
119
  processed[-1] = last
120
  else:
121
  processed = list(messages)
122
-
123
  # Handle tool/function messages
124
  final_msgs: List[Dict[str, Any]] = []
125
  for m in processed:
126
  role = m.get("role")
127
  if role in ("tool", "function"):
128
  tool_name = m.get("name", "unknown")
129
- tool_content = m.get("content", "")
130
  if isinstance(tool_content, dict):
131
  tool_content = json.dumps(tool_content, ensure_ascii=False)
132
- elif tool_content is None:
133
- tool_content = ""
134
-
135
  # 确保内容不为空且不包含 None
136
  content = f"工具 {tool_name} 返回结果:\n```json\n{tool_content}\n```"
137
  if not content.strip():
138
  content = f"工具 {tool_name} 执行完成"
139
-
140
- final_msgs.append({
141
- "role": "assistant",
142
- "content": content,
143
- })
 
 
144
  else:
145
- final_msgs.append(m)
146
-
 
 
 
 
147
  return final_msgs
148
 
149
 
@@ -157,10 +164,10 @@ def extract_tool_invocations(text: str) -> Optional[List[Dict[str, Any]]]:
157
  """Extract tool invocations from response text"""
158
  if not text:
159
  return None
160
-
161
  # Limit scan size for performance
162
- scannable_text = text[:settings.SCAN_LIMIT]
163
-
164
  # Attempt 1: Extract from JSON code blocks
165
  json_blocks = TOOL_CALL_FENCE_PATTERN.findall(scannable_text)
166
  for json_block in json_blocks:
@@ -168,10 +175,20 @@ def extract_tool_invocations(text: str) -> Optional[List[Dict[str, Any]]]:
168
  parsed_data = json.loads(json_block)
169
  tool_calls = parsed_data.get("tool_calls")
170
  if tool_calls and isinstance(tool_calls, list):
 
 
 
 
 
 
 
 
 
 
171
  return tool_calls
172
  except (json.JSONDecodeError, AttributeError):
173
  continue
174
-
175
  # Attempt 2: Extract inline JSON objects
176
  inline_match = TOOL_CALL_INLINE_PATTERN.search(scannable_text)
177
  if inline_match:
@@ -180,10 +197,20 @@ def extract_tool_invocations(text: str) -> Optional[List[Dict[str, Any]]]:
180
  parsed_data = json.loads(inline_json)
181
  tool_calls = parsed_data.get("tool_calls")
182
  if tool_calls and isinstance(tool_calls, list):
 
 
 
 
 
 
 
 
 
 
183
  return tool_calls
184
  except (json.JSONDecodeError, AttributeError):
185
  pass
186
-
187
  # Attempt 3: Parse natural language function calls
188
  natural_lang_match = FUNCTION_CALL_PATTERN.search(scannable_text)
189
  if natural_lang_match:
@@ -192,22 +219,22 @@ def extract_tool_invocations(text: str) -> Optional[List[Dict[str, Any]]]:
192
  try:
193
  # Validate JSON format
194
  json.loads(arguments_str)
195
- return [{
196
- "id": f"invoke_{int(time.time() * 1000000)}",
197
- "type": "function",
198
- "function": {
199
- "name": function_name,
200
- "arguments": arguments_str
201
  }
202
- }]
203
  except json.JSONDecodeError:
204
  return None
205
-
206
  return None
207
 
208
 
209
  def remove_tool_json_content(text: str) -> str:
210
  """Remove tool JSON content from response text"""
 
211
  def remove_tool_call_block(match: re.Match) -> str:
212
  json_content = match.group(1)
213
  try:
@@ -217,9 +244,9 @@ def remove_tool_json_content(text: str) -> str:
217
  except (json.JSONDecodeError, AttributeError):
218
  pass
219
  return match.group(0)
220
-
221
  # Remove fenced tool JSON blocks
222
  cleaned_text = TOOL_CALL_FENCE_PATTERN.sub(remove_tool_call_block, text)
223
  # Remove inline tool JSON
224
  cleaned_text = TOOL_CALL_INLINE_PATTERN.sub("", cleaned_text)
225
- return cleaned_text.strip()
 
10
  from app.core.config import settings
11
 
12
 
13
+ def content_to_string(content: Any) -> str:
14
+ """Convert content from various formats to string (following app.py pattern)"""
15
+ if isinstance(content, str):
16
+ return content
17
+ if isinstance(content, list):
18
+ parts = []
19
+ for p in content:
20
+ if isinstance(p, dict) and p.get("type") == "text":
21
+ parts.append(p.get("text", ""))
22
+ elif isinstance(p, str):
23
+ parts.append(p)
24
+ return " ".join(parts)
25
+ return ""
26
+
27
+
28
  def generate_tool_prompt(tools: List[Dict[str, Any]]) -> str:
29
  """Generate tool injection prompt with enhanced formatting"""
30
  if not tools:
31
  return ""
32
+
33
  tool_definitions = []
34
  for tool in tools:
35
  if tool.get("type") != "function":
36
  continue
37
+
38
  function_spec = tool.get("function", {}) or {}
39
  function_name = function_spec.get("name", "unknown")
40
  function_description = function_spec.get("description", "")
41
  parameters = function_spec.get("parameters", {}) or {}
42
+
43
  # Create structured tool definition
44
  tool_info = [f"## {function_name}", f"**Purpose**: {function_description}"]
45
+
46
  # Add parameter details
47
  parameter_properties = parameters.get("properties", {}) or {}
48
  required_parameters = set(parameters.get("required", []) or [])
49
+
50
  if parameter_properties:
51
  tool_info.append("**Parameters**:")
52
  for param_name, param_details in parameter_properties.items():
 
54
  param_desc = (param_details or {}).get("description", "")
55
  requirement_flag = "**Required**" if param_name in required_parameters else "*Optional*"
56
  tool_info.append(f"- `{param_name}` ({param_type}) - {requirement_flag}: {param_desc}")
57
+
58
  tool_definitions.append("\n".join(tool_info))
59
+
60
  if not tool_definitions:
61
  return ""
62
+
63
  # Build comprehensive tool prompt
64
  prompt_template = (
65
+ "\n\n# AVAILABLE FUNCTIONS\n" + "\n\n---\n".join(tool_definitions) + "\n\n# USAGE INSTRUCTIONS\n"
 
 
66
  "When you need to execute a function, respond ONLY with a JSON object containing tool_calls:\n"
67
  "```json\n"
68
  "{\n"
69
  ' "tool_calls": [\n'
70
  " {\n"
71
+ ' "id": "call_xxx",\n'
72
  ' "type": "function",\n'
73
  ' "function": {\n'
74
  ' "name": "function_name",\n'
75
+ ' "arguments": "{\\"param1\\": \\"value1\\"}"\n'
 
 
76
  " }\n"
77
  " }\n"
78
  " ]\n"
79
  "}\n"
80
  "```\n"
81
+ "Important: No explanatory text before or after the JSON. The 'arguments' field must be a JSON string, not an object.\n"
82
  )
83
+
84
  return prompt_template
85
 
86
 
87
  def process_messages_with_tools(
88
+ messages: List[Dict[str, Any]], tools: Optional[List[Dict[str, Any]]] = None, tool_choice: Optional[Any] = None
 
 
89
  ) -> List[Dict[str, Any]]:
90
  """Process messages and inject tool prompts"""
91
  processed: List[Dict[str, Any]] = []
92
+
93
  if tools and settings.TOOL_SUPPORT and (tool_choice != "none"):
94
  tools_prompt = generate_tool_prompt(tools)
95
  has_system = any(m.get("role") == "system" for m in messages)
96
+
97
  if has_system:
98
  for m in messages:
99
  if m.get("role") == "system":
100
  mm = dict(m)
101
+ content = content_to_string(mm.get("content", ""))
 
 
102
  mm["content"] = content + tools_prompt
103
  processed.append(mm)
104
  else:
105
  processed.append(m)
106
  else:
107
  processed = [{"role": "system", "content": "你是一个有用的助手。" + tools_prompt}] + messages
108
+
109
  # Add tool choice hints
110
  if tool_choice in ("required", "auto"):
111
  if processed and processed[-1].get("role") == "user":
112
  last = dict(processed[-1])
113
+ content = content_to_string(last.get("content", ""))
 
 
114
  last["content"] = content + "\n\n请根据需要使用提供的工具函数。"
115
  processed[-1] = last
116
  elif isinstance(tool_choice, dict) and tool_choice.get("type") == "function":
117
  fname = (tool_choice.get("function") or {}).get("name")
118
  if fname and processed and processed[-1].get("role") == "user":
119
  last = dict(processed[-1])
120
+ content = content_to_string(last.get("content", ""))
 
 
121
  last["content"] = content + f"\n\n请使用 {fname} 函数来处理这个请求。"
122
  processed[-1] = last
123
  else:
124
  processed = list(messages)
125
+
126
  # Handle tool/function messages
127
  final_msgs: List[Dict[str, Any]] = []
128
  for m in processed:
129
  role = m.get("role")
130
  if role in ("tool", "function"):
131
  tool_name = m.get("name", "unknown")
132
+ tool_content = content_to_string(m.get("content", ""))
133
  if isinstance(tool_content, dict):
134
  tool_content = json.dumps(tool_content, ensure_ascii=False)
135
+
 
 
136
  # 确保内容不为空且不包含 None
137
  content = f"工具 {tool_name} 返回结果:\n```json\n{tool_content}\n```"
138
  if not content.strip():
139
  content = f"工具 {tool_name} 执行完成"
140
+
141
+ final_msgs.append(
142
+ {
143
+ "role": "assistant",
144
+ "content": content,
145
+ }
146
+ )
147
  else:
148
+ # For regular messages, ensure content is string format
149
+ final_msg = dict(m)
150
+ content = content_to_string(final_msg.get("content", ""))
151
+ final_msg["content"] = content
152
+ final_msgs.append(final_msg)
153
+
154
  return final_msgs
155
 
156
 
 
164
  """Extract tool invocations from response text"""
165
  if not text:
166
  return None
167
+
168
  # Limit scan size for performance
169
+ scannable_text = text[: settings.SCAN_LIMIT]
170
+
171
  # Attempt 1: Extract from JSON code blocks
172
  json_blocks = TOOL_CALL_FENCE_PATTERN.findall(scannable_text)
173
  for json_block in json_blocks:
 
175
  parsed_data = json.loads(json_block)
176
  tool_calls = parsed_data.get("tool_calls")
177
  if tool_calls and isinstance(tool_calls, list):
178
+ # Ensure arguments field is a string
179
+ for tc in tool_calls:
180
+ if "function" in tc:
181
+ func = tc["function"]
182
+ if "arguments" in func:
183
+ if isinstance(func["arguments"], dict):
184
+ # Convert dict to JSON string
185
+ func["arguments"] = json.dumps(func["arguments"], ensure_ascii=False)
186
+ elif not isinstance(func["arguments"], str):
187
+ func["arguments"] = json.dumps(func["arguments"], ensure_ascii=False)
188
  return tool_calls
189
  except (json.JSONDecodeError, AttributeError):
190
  continue
191
+
192
  # Attempt 2: Extract inline JSON objects
193
  inline_match = TOOL_CALL_INLINE_PATTERN.search(scannable_text)
194
  if inline_match:
 
197
  parsed_data = json.loads(inline_json)
198
  tool_calls = parsed_data.get("tool_calls")
199
  if tool_calls and isinstance(tool_calls, list):
200
+ # Ensure arguments field is a string
201
+ for tc in tool_calls:
202
+ if "function" in tc:
203
+ func = tc["function"]
204
+ if "arguments" in func:
205
+ if isinstance(func["arguments"], dict):
206
+ # Convert dict to JSON string
207
+ func["arguments"] = json.dumps(func["arguments"], ensure_ascii=False)
208
+ elif not isinstance(func["arguments"], str):
209
+ func["arguments"] = json.dumps(func["arguments"], ensure_ascii=False)
210
  return tool_calls
211
  except (json.JSONDecodeError, AttributeError):
212
  pass
213
+
214
  # Attempt 3: Parse natural language function calls
215
  natural_lang_match = FUNCTION_CALL_PATTERN.search(scannable_text)
216
  if natural_lang_match:
 
219
  try:
220
  # Validate JSON format
221
  json.loads(arguments_str)
222
+ return [
223
+ {
224
+ "id": f"call_{int(time.time() * 1000000)}",
225
+ "type": "function",
226
+ "function": {"name": function_name, "arguments": arguments_str},
 
227
  }
228
+ ]
229
  except json.JSONDecodeError:
230
  return None
231
+
232
  return None
233
 
234
 
235
  def remove_tool_json_content(text: str) -> str:
236
  """Remove tool JSON content from response text"""
237
+
238
  def remove_tool_call_block(match: re.Match) -> str:
239
  json_content = match.group(1)
240
  try:
 
244
  except (json.JSONDecodeError, AttributeError):
245
  pass
246
  return match.group(0)
247
+
248
  # Remove fenced tool JSON blocks
249
  cleaned_text = TOOL_CALL_FENCE_PATTERN.sub(remove_tool_call_block, text)
250
  # Remove inline tool JSON
251
  cleaned_text = TOOL_CALL_INLINE_PATTERN.sub("", cleaned_text)
252
+ return cleaned_text.strip()
main.py CHANGED
@@ -6,13 +6,13 @@ from fastapi import FastAPI, Request, Response
6
  from fastapi.middleware.cors import CORSMiddleware
7
 
8
  from app.core.config import settings
9
- from app.api import openai, anthropic
10
 
11
  # Create FastAPI app
12
  app = FastAPI(
13
  title="OpenAI Compatible API Server",
14
  description="An OpenAI-compatible API server for Z.AI chat service",
15
- version="1.0.0"
16
  )
17
 
18
  # Add CORS middleware
@@ -26,7 +26,6 @@ app.add_middleware(
26
 
27
  # Include API routers
28
  app.include_router(openai.router)
29
- app.include_router(anthropic.router)
30
 
31
 
32
  @app.options("/")
@@ -43,4 +42,5 @@ async def root():
43
 
44
  if __name__ == "__main__":
45
  import uvicorn
46
- uvicorn.run("main:app", host="0.0.0.0", port=settings.LISTEN_PORT, reload=True)
 
 
6
  from fastapi.middleware.cors import CORSMiddleware
7
 
8
  from app.core.config import settings
9
+ from app.core import openai
10
 
11
  # Create FastAPI app
12
  app = FastAPI(
13
  title="OpenAI Compatible API Server",
14
  description="An OpenAI-compatible API server for Z.AI chat service",
15
+ version="1.0.0",
16
  )
17
 
18
  # Add CORS middleware
 
26
 
27
  # Include API routers
28
  app.include_router(openai.router)
 
29
 
30
 
31
  @app.options("/")
 
42
 
43
  if __name__ == "__main__":
44
  import uvicorn
45
+
46
+ uvicorn.run("main:app", host="0.0.0.0", port=settings.LISTEN_PORT, reload=True)
tests/test_anthropic.py DELETED
@@ -1,79 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
-
3
- import json
4
- import requests
5
-
6
- # 服务器配置
7
- BASE_URL = "http://localhost:8080/v1/messages"
8
- API_KEY = "sk-your-api-key"
9
-
10
- test_data = {
11
- "model": "GLM-4.5",
12
- "messages": [{"role": "user", "content": "你好,这是一个测试"}],
13
- "system": [
14
- {
15
- "type": "text",
16
- "text": "You are Claude Code, Anthropic's official CLI for Claude.",
17
- "cache_control": {"type": "ephemeral"},
18
- }
19
- ],
20
- "max_tokens": 1024,
21
- "stream": False,
22
- }
23
-
24
-
25
- def test_non_stream():
26
- """测试非流式请求"""
27
- print("=== 测试非流式请求 ===")
28
-
29
- try:
30
- response = requests.post(BASE_URL, headers={"x-api-key": API_KEY}, json=test_data, timeout=30.0)
31
-
32
- print(f"状态码: {response.status_code}")
33
-
34
- if response.status_code == 200:
35
- result = response.json()
36
- print("响应成功!")
37
- print(f"ID: {result.get('id')}")
38
- print(f"模型: {result.get('model')}")
39
- if result.get("content"):
40
- print(f"内容: {result['content'][0]['text']}")
41
- else:
42
- print("错误响应:")
43
- print(response.text)
44
-
45
- except Exception as e:
46
- print(f"请求失败: {e}")
47
-
48
-
49
- def test_stream():
50
- """测试流式请求"""
51
- print("\n=== 测试流式请求 ===")
52
-
53
- stream_data = test_data.copy()
54
- stream_data["stream"] = True
55
-
56
- try:
57
- response = requests.post(BASE_URL, headers={"x-api-key": API_KEY}, json=stream_data, stream=True, timeout=30.0)
58
-
59
- print(f"状态码: {response.status_code}")
60
-
61
- if response.status_code == 200:
62
- print("流式响应内容:")
63
- for line in response.iter_lines():
64
- if line:
65
- print(f" {line.decode('utf-8')}")
66
- else:
67
- print("错误响应:")
68
- print(response.text)
69
-
70
- except Exception as e:
71
- print(f"请求失败: {e}")
72
-
73
-
74
- if __name__ == "__main__":
75
- try:
76
- test_non_stream()
77
- test_stream()
78
- except KeyboardInterrupt:
79
- print("\n测试已取消")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/test_system_field.py DELETED
@@ -1,68 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- 测试 Anthropic API system 字段数组类型支持
4
- """
5
- import json
6
- import requests
7
-
8
- # 测试数据
9
- test_cases = [
10
- {
11
- "name": "字符串类型 system",
12
- "data": {
13
- "model": "GLM-4.5",
14
- "messages": [{"role": "user", "content": "你好"}],
15
- "system": "你是一个有帮助的助手",
16
- "max_tokens": 100
17
- }
18
- },
19
- {
20
- "name": "数组类型 system",
21
- "data": {
22
- "model": "GLM-4.5",
23
- "messages": [{"role": "user", "content": "你好"}],
24
- "system": [
25
- {
26
- "type": "text",
27
- "text": "你是一个有帮助的助手",
28
- "cache_control": {"type": "ephemeral"}
29
- }
30
- ],
31
- "max_tokens": 100
32
- }
33
- }
34
- ]
35
-
36
- def test_system_field():
37
- """测试 system 字段的不同格式"""
38
- print("=== 测试 system 字段支持 ===\n")
39
-
40
- for test_case in test_cases:
41
- print(f"测试: {test_case['name']}")
42
-
43
- try:
44
- response = requests.post(
45
- "http://localhost:8080/v1/messages",
46
- headers={"x-api-key": "sk-your-api-key"},
47
- json=test_case["data"],
48
- timeout=10
49
- )
50
-
51
- if response.status_code == 200:
52
- result = response.json()
53
- print("✅ 成功")
54
- print(f" 消息ID: {result.get('id')}")
55
- print(f" 内容预览: {result['content'][0]['text'][:50]}...")
56
- else:
57
- print(f"❌ 失败 - 状态码: {response.status_code}")
58
- print(f" 错误: {response.text}")
59
-
60
- except Exception as e:
61
- print(f"❌ 异常: {e}")
62
-
63
- print()
64
-
65
- if __name__ == "__main__":
66
- print("请确保服务器正在运行在 http://localhost:8080")
67
- input("按 Enter 开始测试...")
68
- test_system_field()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/test_tool_call.py ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ 测试工具调用功能
5
+ """
6
+
7
+ import json
8
+ import requests
9
+
10
+ # 配置
11
+ BASE_URL = "http://localhost:8080"
12
+ API_KEY = "your-api-key" # 替换为实际的 API key
13
+
14
+ def test_tool_call():
15
+ """测试工具调用功能"""
16
+
17
+ # 定义一个简单的工具
18
+ tools = [
19
+ {
20
+ "type": "function",
21
+ "function": {
22
+ "name": "get_weather",
23
+ "description": "获取指定城市的天气信息",
24
+ "parameters": {
25
+ "type": "object",
26
+ "properties": {
27
+ "location": {
28
+ "type": "string",
29
+ "description": "城市名称,例如:北京、上海"
30
+ },
31
+ "unit": {
32
+ "type": "string",
33
+ "description": "温度单位",
34
+ "enum": ["celsius", "fahrenheit"]
35
+ }
36
+ },
37
+ "required": ["location"]
38
+ }
39
+ }
40
+ }
41
+ ]
42
+
43
+ # 构建请求
44
+ request_data = {
45
+ "model": "GLM-4.5",
46
+ "messages": [
47
+ {
48
+ "role": "user",
49
+ "content": "北京的天气怎么样?"
50
+ }
51
+ ],
52
+ "tools": tools,
53
+ "tool_choice": "auto",
54
+ "stream": False
55
+ }
56
+
57
+ headers = {
58
+ "Content-Type": "application/json",
59
+ "Authorization": f"Bearer {API_KEY}"
60
+ }
61
+
62
+ print("=" * 60)
63
+ print("测试工具调用 (非流式)")
64
+ print("=" * 60)
65
+
66
+ # 发送请求
67
+ response = requests.post(
68
+ f"{BASE_URL}/v1/chat/completions",
69
+ json=request_data,
70
+ headers=headers
71
+ )
72
+
73
+ print(f"状态码: {response.status_code}")
74
+
75
+ if response.status_code == 200:
76
+ result = response.json()
77
+ print("\n响应内容:")
78
+ print(json.dumps(result, ensure_ascii=False, indent=2))
79
+
80
+ # 检查是否有工具调用
81
+ if result.get("choices"):
82
+ choice = result["choices"][0]
83
+ if choice.get("message", {}).get("tool_calls"):
84
+ print("\n✅ 检测到工具调用!")
85
+ for tc in choice["message"]["tool_calls"]:
86
+ print(f" - 函数: {tc.get('function', {}).get('name')}")
87
+ print(f" 参数: {tc.get('function', {}).get('arguments')}")
88
+ else:
89
+ print("\n⚠️ 未检测到工具调用")
90
+ if choice.get("message", {}).get("content"):
91
+ print(f"内容: {choice['message']['content'][:200]}")
92
+ else:
93
+ print(f"\n错误响应: {response.text}")
94
+
95
+ # 测试流式响应
96
+ print("\n" + "=" * 60)
97
+ print("测试工具调用 (流式)")
98
+ print("=" * 60)
99
+
100
+ request_data["stream"] = True
101
+
102
+ response = requests.post(
103
+ f"{BASE_URL}/v1/chat/completions",
104
+ json=request_data,
105
+ headers=headers,
106
+ stream=True
107
+ )
108
+
109
+ print(f"状态码: {response.status_code}")
110
+
111
+ if response.status_code == 200:
112
+ print("\n流式响应:")
113
+ tool_calls_detected = False
114
+
115
+ for line in response.iter_lines():
116
+ if line:
117
+ line_str = line.decode('utf-8')
118
+ if line_str.startswith("data: "):
119
+ data = line_str[6:]
120
+ if data == "[DONE]":
121
+ print("流结束")
122
+ break
123
+
124
+ try:
125
+ chunk = json.loads(data)
126
+ if chunk.get("choices"):
127
+ delta = chunk["choices"][0].get("delta", {})
128
+ if delta.get("tool_calls"):
129
+ tool_calls_detected = True
130
+ print(f"检测到工具调用: {json.dumps(delta['tool_calls'], ensure_ascii=False)}")
131
+ elif delta.get("content"):
132
+ print(f"内容: {delta['content']}", end="")
133
+ except json.JSONDecodeError:
134
+ pass
135
+
136
+ if tool_calls_detected:
137
+ print("\n\n✅ 流式响应中检测到工具调用!")
138
+ else:
139
+ print("\n\n⚠️ 流式响应中未检测到工具调用")
140
+ else:
141
+ print(f"\n错误响应: {response.text}")
142
+
143
+
144
+ if __name__ == "__main__":
145
+ test_tool_call()