youbiaokachi commited on
Commit
1a06196
·
verified ·
1 Parent(s): 34e5fa0

Upload 10 files

Browse files
k2think_proxy.py CHANGED
@@ -1,592 +1,56 @@
1
- from fastapi import FastAPI, HTTPException, Request, Response
2
- from fastapi.responses import StreamingResponse, JSONResponse, HTMLResponse
3
- from fastapi.middleware.cors import CORSMiddleware
4
- from pydantic import BaseModel
5
- from typing import List, Dict, Optional, Union, AsyncGenerator
6
- import httpx
7
- import json
8
- import asyncio
9
  import time
10
- import os
11
  import logging
12
- import re
13
  from contextlib import asynccontextmanager
14
- from dotenv import load_dotenv
15
-
16
- # 加载环境变量
17
- load_dotenv()
18
-
19
- # 配置
20
- VALID_API_KEY = os.getenv("VALID_API_KEY")
21
- if not VALID_API_KEY:
22
- raise ValueError("错误:VALID_API_KEY 环境变量未设置。请在 .env 文件中提供一个安全的API密钥。")
23
- K2THINK_API_URL = os.getenv("K2THINK_API_URL", "https://www.k2think.ai/api/chat/completions")
24
- K2THINK_TOKEN = os.getenv("K2THINK_TOKEN")
25
- OUTPUT_THINKING = os.getenv("OUTPUT_THINKING", "true").lower() == "true"
26
- TOOL_SUPPORT = os.getenv("TOOL_SUPPORT", "true").lower() == "true"
27
- SCAN_LIMIT = int(os.getenv("SCAN_LIMIT", "200000"))
28
- SYSTEM_MESSAGE_LENTH = int(os.getenv("SYSTEM_MESSAGE_LENTH", "200000"))
29
-
30
- # 高级配置
31
- REQUEST_TIMEOUT = float(os.getenv("REQUEST_TIMEOUT", "60"))
32
- MAX_KEEPALIVE_CONNECTIONS = int(os.getenv("MAX_KEEPALIVE_CONNECTIONS", "20"))
33
- MAX_CONNECTIONS = int(os.getenv("MAX_CONNECTIONS", "100"))
34
- DEBUG_LOGGING = os.getenv("DEBUG_LOGGING", "false").lower() == "true"
35
- STREAM_DELAY = float(os.getenv("STREAM_DELAY", "0.05"))
36
- STREAM_CHUNK_SIZE = int(os.getenv("STREAM_CHUNK_SIZE", "50"))
37
- MAX_STREAM_TIME = float(os.getenv("MAX_STREAM_TIME", "10.0")) # 最大流式输出时间(秒)
38
- ENABLE_ACCESS_LOG = os.getenv("ENABLE_ACCESS_LOG", "true").lower() == "true"
39
- CORS_ORIGINS = os.getenv("CORS_ORIGINS", "*").split(",") if os.getenv("CORS_ORIGINS", "*") != "*" else ["*"]
40
-
41
- # 设置日志
42
- LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO").upper()
43
- if LOG_LEVEL == "DEBUG":
44
- logging.basicConfig(level=logging.DEBUG)
45
- elif LOG_LEVEL == "WARNING":
46
- logging.basicConfig(level=logging.WARNING)
47
- elif LOG_LEVEL == "ERROR":
48
- logging.basicConfig(level=logging.ERROR)
49
- else:
50
- logging.basicConfig(level=logging.INFO)
51
 
52
  logger = logging.getLogger(__name__)
53
 
54
- # 数据模型
55
- class ContentPart(BaseModel):
56
- """Content part model for OpenAI's new content format"""
57
- type: str
58
- text: Optional[str] = None
59
-
60
- class Message(BaseModel):
61
- role: str
62
- content: Optional[Union[str, List[ContentPart]]] = None
63
- tool_calls: Optional[List[Dict]] = None
64
-
65
- class ChatCompletionRequest(BaseModel):
66
- model: str = "MBZUAI-IFM/K2-Think"
67
- messages: List[Message]
68
- stream: bool = False
69
- temperature: float = 0.7
70
- max_tokens: Optional[int] = None
71
- top_p: Optional[float] = None
72
- frequency_penalty: Optional[float] = None
73
- presence_penalty: Optional[float] = None
74
- stop: Optional[Union[str, List[str]]] = None
75
- tools: Optional[List[Dict]] = None
76
- tool_choice: Optional[Union[str, Dict]] = None
77
-
78
- class ModelInfo(BaseModel):
79
- id: str
80
- object: str = "model"
81
- created: int
82
- owned_by: str
83
- permission: List[Dict] = []
84
- root: str
85
- parent: Optional[str] = None
86
-
87
- class ModelsResponse(BaseModel):
88
- object: str = "list"
89
- data: List[ModelInfo]
90
-
91
- # HTTP客户端工厂函数
92
- def create_http_client() -> httpx.AsyncClient:
93
- """创建HTTP客户端"""
94
- base_kwargs = {
95
- "timeout": httpx.Timeout(timeout=None, connect=10.0),
96
- "limits": httpx.Limits(
97
- max_keepalive_connections=MAX_KEEPALIVE_CONNECTIONS,
98
- max_connections=MAX_CONNECTIONS
99
- ),
100
- "follow_redirects": True
101
- }
102
-
103
- try:
104
- return httpx.AsyncClient(**base_kwargs)
105
- except Exception as e:
106
- logger.error(f"创建客户端失败: {e}")
107
- raise e
108
-
109
  # 全局HTTP客户端管理
110
  @asynccontextmanager
111
  async def lifespan(app: FastAPI):
 
112
  yield
 
113
 
114
  # 创建FastAPI应用
115
- app = FastAPI(title="K2Think API Proxy", lifespan=lifespan)
 
 
 
 
 
116
 
117
  # CORS配置
118
  app.add_middleware(
119
  CORSMiddleware,
120
- allow_origins=CORS_ORIGINS,
121
  allow_credentials=True,
122
  allow_methods=["*"],
123
  allow_headers=["*"],
124
  )
125
 
126
-
127
- def validate_api_key(authorization: str) -> bool:
128
- """验证API密钥"""
129
- if not authorization or not authorization.startswith("Bearer "):
130
- return False
131
- api_key = authorization[7:] # 移除 "Bearer " 前缀
132
- return api_key == VALID_API_KEY
133
-
134
- def generate_session_id() -> str:
135
- """生成会话ID"""
136
- import uuid
137
- return str(uuid.uuid4())
138
-
139
- def generate_chat_id() -> str:
140
- """生成聊天ID"""
141
- import uuid
142
- return str(uuid.uuid4())
143
-
144
- def get_current_datetime_info():
145
- """获取当前时间信息"""
146
- from datetime import datetime
147
- import pytz
148
-
149
- # 设置时区为上海
150
- tz = pytz.timezone('Asia/Shanghai')
151
- now = datetime.now(tz)
152
-
153
- return {
154
- "{{USER_NAME}}": "User",
155
- "{{USER_LOCATION}}": "Unknown",
156
- "{{CURRENT_DATETIME}}": now.strftime("%Y-%m-%d %H:%M:%S"),
157
- "{{CURRENT_DATE}}": now.strftime("%Y-%m-%d"),
158
- "{{CURRENT_TIME}}": now.strftime("%H:%M:%S"),
159
- "{{CURRENT_WEEKDAY}}": now.strftime("%A"),
160
- "{{CURRENT_TIMEZONE}}": "Asia/Shanghai",
161
- "{{USER_LANGUAGE}}": "en-US"
162
- }
163
-
164
- def extract_answer_content(full_content: str) -> str:
165
- """删除第一个<answer>标签和最后一个</answer>标签,保留内容"""
166
- if not full_content:
167
- return full_content
168
- if OUTPUT_THINKING:
169
- # 删除第一个<answer>
170
- answer_start = full_content.find('<answer>')
171
- if answer_start != -1:
172
- full_content = full_content[:answer_start] + full_content[answer_start + 8:]
173
-
174
- # 删除最后一个</answer>
175
- answer_end = full_content.rfind('</answer>')
176
- if answer_end != -1:
177
- full_content = full_content[:answer_end] + full_content[answer_end + 9:]
178
-
179
- return full_content.strip()
180
- else:
181
- # 删除<think>部分(包括标签)
182
- think_start = full_content.find('<think>')
183
- think_end = full_content.find('</think>')
184
- if think_start != -1 and think_end != -1:
185
- full_content = full_content[:think_start] + full_content[think_end + 8:]
186
-
187
- # 删除<answer>标签及其内容之外的部分
188
- answer_start = full_content.find('<answer>')
189
- answer_end = full_content.rfind('</answer>')
190
- if answer_start != -1 and answer_end != -1:
191
- content = full_content[answer_start + 8:answer_end]
192
- return content.strip()
193
-
194
- return full_content.strip()
195
-
196
- def calculate_dynamic_chunk_size(content_length: int) -> int:
197
- """
198
- 动态计算流式输出的chunk大小
199
- 确保总输出时间不超过MAX_STREAM_TIME秒
200
-
201
- Args:
202
- content_length: 待输出内容的总长度
203
-
204
- Returns:
205
- int: 动态计算的chunk大小,最小为50
206
- """
207
- if content_length <= 0:
208
- return STREAM_CHUNK_SIZE
209
-
210
- # 计算需要的总chunk数量以满足时间限制
211
- # 总时间 = chunk数量 * STREAM_DELAY
212
- # chunk数量 = content_length / chunk_size
213
- # 所以:总时间 = (content_length / chunk_size) * STREAM_DELAY
214
- # 解出:chunk_size = (content_length * STREAM_DELAY) / MAX_STREAM_TIME
215
-
216
- calculated_chunk_size = int((content_length * STREAM_DELAY) / MAX_STREAM_TIME)
217
-
218
- # 确保chunk_size不小于最小值50
219
- min_chunk_size = 50
220
- dynamic_chunk_size = max(calculated_chunk_size, min_chunk_size)
221
-
222
- # 如果计算出的chunk_size太大(比如内容很短),使用默认值
223
- if dynamic_chunk_size > content_length:
224
- dynamic_chunk_size = min(STREAM_CHUNK_SIZE, content_length)
225
-
226
- logger.debug(f"动态chunk_size计算: 内容长度={content_length}, 计算值={calculated_chunk_size}, 最终值={dynamic_chunk_size}")
227
-
228
- return dynamic_chunk_size
229
-
230
- def content_to_string(content) -> str:
231
- """Convert content from various formats to string"""
232
- if content is None:
233
- return ""
234
- if isinstance(content, str):
235
- return content
236
- if isinstance(content, list):
237
- parts = []
238
- for p in content:
239
- if hasattr(p, 'text'): # ContentPart object
240
- parts.append(getattr(p, 'text', ''))
241
- elif isinstance(p, dict) and p.get("type") == "text":
242
- parts.append(p.get("text", ""))
243
- elif isinstance(p, str):
244
- parts.append(p)
245
- else:
246
- # 处理其他类型的对象
247
- try:
248
- if hasattr(p, '__dict__'):
249
- # 如果是对象,尝试获取text属性或转换为字符串
250
- parts.append(str(getattr(p, 'text', str(p))))
251
- else:
252
- parts.append(str(p))
253
- except:
254
- continue
255
- return " ".join(parts)
256
- # ��理其他类型
257
- try:
258
- return str(content)
259
- except:
260
- return ""
261
-
262
- def generate_tool_prompt(tools: List[Dict]) -> str:
263
- """Generate concise tool injection prompt"""
264
- if not tools:
265
- return ""
266
-
267
- tool_definitions = []
268
- for tool in tools:
269
- if tool.get("type") != "function":
270
- continue
271
-
272
- function_spec = tool.get("function", {}) or {}
273
- function_name = function_spec.get("name", "unknown")
274
- function_description = function_spec.get("description", "")
275
- parameters = function_spec.get("parameters", {}) or {}
276
-
277
- # Create concise tool definition
278
- tool_info = f"{function_name}: {function_description}"
279
-
280
- # Add simplified parameter info
281
- parameter_properties = parameters.get("properties", {}) or {}
282
- required_parameters = set(parameters.get("required", []) or [])
283
-
284
- if parameter_properties:
285
- param_list = []
286
- for param_name, param_details in parameter_properties.items():
287
- param_desc = (param_details or {}).get("description", "")
288
- is_required = param_name in required_parameters
289
- param_list.append(f"{param_name}{'*' if is_required else ''}: {param_desc}")
290
- tool_info += f" Parameters: {', '.join(param_list)}"
291
-
292
- tool_definitions.append(tool_info)
293
-
294
- if not tool_definitions:
295
- return ""
296
-
297
- # Build concise tool prompt
298
- prompt_template = (
299
- f"\n\nAvailable tools: {'; '.join(tool_definitions)}. "
300
- "To use a tool, respond with JSON: "
301
- '{"tool_calls":[{"id":"call_xxx","type":"function","function":{"name":"tool_name","arguments":"{\\"param\\":\\"value\\"}"}}]}'
302
- )
303
-
304
- return prompt_template
305
-
306
- def process_messages_with_tools(messages: List[Dict], tools: Optional[List[Dict]] = None, tool_choice: Optional[Union[str, Dict]] = None) -> List[Dict]:
307
- """Process messages and inject tool prompts"""
308
- if not tools or not TOOL_SUPPORT or (tool_choice == "none"):
309
- # 如果没有工具或禁用工具,直接返回原消息
310
- return [dict(m) for m in messages]
311
-
312
- tools_prompt = generate_tool_prompt(tools)
313
-
314
- # 限制工具提示长度,避免过长导致上游API拒绝
315
- if len(tools_prompt) > 1000:
316
- logger.warning(f"工具提示过长 ({len(tools_prompt)} 字符),将截断")
317
- tools_prompt = tools_prompt[:1000] + "..."
318
-
319
- processed = []
320
- has_system = any(m.get("role") == "system" for m in messages)
321
-
322
- if has_system:
323
- # 如果已有系统消息,在第一个系统消息中添加工具提示
324
- for m in messages:
325
- if m.get("role") == "system":
326
- mm = dict(m)
327
- content = content_to_string(mm.get("content", ""))
328
- # 确保系统消息不会过长
329
- new_content = content + tools_prompt
330
- if len(new_content) > SYSTEM_MESSAGE_LENTH:
331
- logger.warning(f"系统消息过长 ({len(new_content)} 字符),使用简化版本")
332
- mm["content"] = "你是一个有用的助手。" + tools_prompt
333
- else:
334
- mm["content"] = new_content
335
- processed.append(mm)
336
- # 只在第一个系统消息中添加工具提示
337
- tools_prompt = ""
338
- else:
339
- processed.append(dict(m))
340
- else:
341
- # 如果没有系统消息,需要添加一个,但只有当确实需要工具时
342
- if tools_prompt.strip():
343
- processed = [{"role": "system", "content": "你是一个有用的助手。" + tools_prompt}]
344
- processed.extend([dict(m) for m in messages])
345
- else:
346
- processed = [dict(m) for m in messages]
347
-
348
- # Add simplified tool choice hints
349
- if tool_choice == "required":
350
- if processed and processed[-1].get("role") == "user":
351
- last = processed[-1]
352
- content = content_to_string(last.get("content", ""))
353
- last["content"] = content + "\n请使用工具来处理这个请求。"
354
- elif isinstance(tool_choice, dict) and tool_choice.get("type") == "function":
355
- fname = (tool_choice.get("function") or {}).get("name")
356
- if fname and processed and processed[-1].get("role") == "user":
357
- last = processed[-1]
358
- content = content_to_string(last.get("content", ""))
359
- last["content"] = content + f"\n请使用 {fname} 工具。"
360
-
361
- # Handle tool/function messages
362
- final_msgs = []
363
- for m in processed:
364
- role = m.get("role")
365
- if role in ("tool", "function"):
366
- tool_name = m.get("name", "unknown")
367
- tool_content = content_to_string(m.get("content", ""))
368
- if isinstance(tool_content, dict):
369
- tool_content = json.dumps(tool_content, ensure_ascii=False)
370
-
371
- # 简化工具结果���息
372
- content = f"工具 {tool_name} 结果: {tool_content}"
373
- if not content.strip():
374
- content = f"工具 {tool_name} 执行完成"
375
-
376
- final_msgs.append({
377
- "role": "assistant",
378
- "content": content,
379
- })
380
- else:
381
- # For regular messages, ensure content is string format
382
- final_msg = dict(m)
383
- content = content_to_string(final_msg.get("content", ""))
384
- final_msg["content"] = content
385
- final_msgs.append(final_msg)
386
-
387
- return final_msgs
388
-
389
- # Tool Extraction Patterns
390
- TOOL_CALL_FENCE_PATTERN = re.compile(r"```json\s*(\{.*?\})\s*```", re.DOTALL)
391
- FUNCTION_CALL_PATTERN = re.compile(r"调用函数\s*[::]\s*([\w\-\.]+)\s*(?:参数|arguments)[::]\s*(\{.*?\})", re.DOTALL)
392
-
393
- def extract_tool_invocations(text: str) -> Optional[List[Dict]]:
394
- """Extract tool invocations from response text"""
395
- if not text:
396
- return None
397
-
398
- # Limit scan size for performance
399
- scannable_text = text[:SCAN_LIMIT]
400
-
401
- # Attempt 1: Extract from JSON code blocks
402
- json_blocks = TOOL_CALL_FENCE_PATTERN.findall(scannable_text)
403
- for json_block in json_blocks:
404
- try:
405
- parsed_data = json.loads(json_block)
406
- tool_calls = parsed_data.get("tool_calls")
407
- if tool_calls and isinstance(tool_calls, list):
408
- # Ensure arguments field is a string
409
- for tc in tool_calls:
410
- if "function" in tc:
411
- func = tc["function"]
412
- if "arguments" in func:
413
- if isinstance(func["arguments"], dict):
414
- # Convert dict to JSON string
415
- func["arguments"] = json.dumps(func["arguments"], ensure_ascii=False)
416
- elif not isinstance(func["arguments"], str):
417
- func["arguments"] = json.dumps(func["arguments"], ensure_ascii=False)
418
- return tool_calls
419
- except (json.JSONDecodeError, AttributeError):
420
- continue
421
-
422
- # Attempt 2: Extract inline JSON objects using bracket balance method
423
- i = 0
424
- while i < len(scannable_text):
425
- if scannable_text[i] == '{':
426
- # 尝试找到匹配的右括号
427
- brace_count = 1
428
- j = i + 1
429
- in_string = False
430
- escape_next = False
431
-
432
- while j < len(scannable_text) and brace_count > 0:
433
- if escape_next:
434
- escape_next = False
435
- elif scannable_text[j] == '\\':
436
- escape_next = True
437
- elif scannable_text[j] == '"' and not escape_next:
438
- in_string = not in_string
439
- elif not in_string:
440
- if scannable_text[j] == '{':
441
- brace_count += 1
442
- elif scannable_text[j] == '}':
443
- brace_count -= 1
444
- j += 1
445
-
446
- if brace_count == 0:
447
- # 找到了完整的 JSON 对象
448
- json_str = scannable_text[i:j]
449
- try:
450
- parsed_data = json.loads(json_str)
451
- tool_calls = parsed_data.get("tool_calls")
452
- if tool_calls and isinstance(tool_calls, list):
453
- # Ensure arguments field is a string
454
- for tc in tool_calls:
455
- if "function" in tc:
456
- func = tc["function"]
457
- if "arguments" in func:
458
- if isinstance(func["arguments"], dict):
459
- # Convert dict to JSON string
460
- func["arguments"] = json.dumps(func["arguments"], ensure_ascii=False)
461
- elif not isinstance(func["arguments"], str):
462
- func["arguments"] = json.dumps(func["arguments"], ensure_ascii=False)
463
- return tool_calls
464
- except (json.JSONDecodeError, AttributeError):
465
- pass
466
-
467
- i += 1
468
- else:
469
- i += 1
470
-
471
- # Attempt 3: Parse natural language function calls
472
- natural_lang_match = FUNCTION_CALL_PATTERN.search(scannable_text)
473
- if natural_lang_match:
474
- function_name = natural_lang_match.group(1).strip()
475
- arguments_str = natural_lang_match.group(2).strip()
476
- try:
477
- # Validate JSON format
478
- json.loads(arguments_str)
479
- return [
480
- {
481
- "id": f"call_{int(time.time() * 1000000)}",
482
- "type": "function",
483
- "function": {"name": function_name, "arguments": arguments_str},
484
- }
485
- ]
486
- except json.JSONDecodeError:
487
- return None
488
-
489
- return None
490
-
491
- def remove_tool_json_content(text: str) -> str:
492
- """Remove tool JSON content from response text - using bracket balance method"""
493
-
494
- def remove_tool_call_block(match: re.Match) -> str:
495
- json_content = match.group(1)
496
- try:
497
- parsed_data = json.loads(json_content)
498
- if "tool_calls" in parsed_data:
499
- return ""
500
- except (json.JSONDecodeError, AttributeError):
501
- pass
502
- return match.group(0)
503
-
504
- # Step 1: Remove fenced tool JSON blocks
505
- cleaned_text = TOOL_CALL_FENCE_PATTERN.sub(remove_tool_call_block, text)
506
-
507
- # Step 2: Remove inline tool JSON - 使用基于括号平衡的智能方法
508
- result = []
509
- i = 0
510
- while i < len(cleaned_text):
511
- if cleaned_text[i] == '{':
512
- # 尝试找到匹配的右括号
513
- brace_count = 1
514
- j = i + 1
515
- in_string = False
516
- escape_next = False
517
-
518
- while j < len(cleaned_text) and brace_count > 0:
519
- if escape_next:
520
- escape_next = False
521
- elif cleaned_text[j] == '\\':
522
- escape_next = True
523
- elif cleaned_text[j] == '"' and not escape_next:
524
- in_string = not in_string
525
- elif not in_string:
526
- if cleaned_text[j] == '{':
527
- brace_count += 1
528
- elif cleaned_text[j] == '}':
529
- brace_count -= 1
530
- j += 1
531
-
532
- if brace_count == 0:
533
- # 找到了完整的 JSON 对象
534
- json_str = cleaned_text[i:j]
535
- try:
536
- parsed = json.loads(json_str)
537
- if "tool_calls" in parsed:
538
- # 这是一个工具调用,跳过它
539
- i = j
540
- continue
541
- except:
542
- pass
543
-
544
- # 不是工具调用或无法解析,保留这个字符
545
- result.append(cleaned_text[i])
546
- i += 1
547
- else:
548
- result.append(cleaned_text[i])
549
- i += 1
550
-
551
- return ''.join(result).strip()
552
-
553
- async def make_request(method: str, url: str, headers: dict, json_data: dict = None,
554
- stream: bool = False) -> httpx.Response:
555
- """发送HTTP请求"""
556
- client = None
557
-
558
- try:
559
- client = create_http_client()
560
-
561
- if stream:
562
- # 流式请求返回context manager
563
- return client.stream(method, url, headers=headers, json=json_data, timeout=None)
564
- else:
565
- response = await client.request(method, url, headers=headers, json=json_data, timeout=REQUEST_TIMEOUT)
566
-
567
- # 详细记录非200响应
568
- if response.status_code != 200:
569
- logger.error(f"上游API返回错误状态码: {response.status_code}")
570
- logger.error(f"响应头: {dict(response.headers)}")
571
- try:
572
- error_body = response.text
573
- logger.error(f"错误响应体: {error_body}")
574
- except:
575
- logger.error("无法读取错误响应体")
576
-
577
- response.raise_for_status()
578
- return response
579
-
580
- except httpx.HTTPStatusError as e:
581
- logger.error(f"HTTP状态错误: {e.response.status_code} - {e.response.text}")
582
- if client and not stream:
583
- await client.aclose()
584
- raise e
585
- except Exception as e:
586
- logger.error(f"请求异常: {e}")
587
- if client and not stream:
588
- await client.aclose()
589
- raise e
590
 
591
  @app.get("/")
592
  async def homepage():
@@ -595,8 +59,8 @@ async def homepage():
595
  "status": "success",
596
  "message": "K2Think API Proxy is running",
597
  "service": "K2Think API Gateway",
598
- "model": "MBZUAI-IFM/K2-Think",
599
- "version": "1.0.0",
600
  "endpoints": {
601
  "chat": "/v1/chat/completions",
602
  "models": "/v1/models"
@@ -608,7 +72,12 @@ async def health_check():
608
  """健康检查"""
609
  return JSONResponse(content={
610
  "status": "healthy",
611
- "timestamp": int(time.time())
 
 
 
 
 
612
  })
613
 
614
  @app.get("/favicon.ico")
@@ -617,533 +86,31 @@ async def favicon():
617
  return Response(content="", media_type="image/x-icon")
618
 
619
  @app.get("/v1/models")
620
- async def get_models() -> ModelsResponse:
621
  """获取模型列表"""
622
- model_info = ModelInfo(
623
- id="MBZUAI-IFM/K2-Think",
624
- created=int(time.time()),
625
- owned_by="MBZUAI",
626
- root="mbzuai-k2-think-2508"
627
- )
628
- return ModelsResponse(data=[model_info])
629
-
630
-
631
- async def process_non_stream_response(k2think_payload: dict, headers: dict) -> tuple[str, dict]:
632
- """处理非流式响应"""
633
- try:
634
- response = await make_request(
635
- "POST",
636
- K2THINK_API_URL,
637
- headers,
638
- k2think_payload,
639
- stream=False
640
- )
641
-
642
- # K2Think 非流式请求返回标准JSON格式
643
- result = response.json()
644
-
645
- # 提取内容
646
- full_content = ""
647
- if result.get('choices') and len(result['choices']) > 0:
648
- choice = result['choices'][0]
649
- if choice.get('message') and choice['message'].get('content'):
650
- raw_content = choice['message']['content']
651
- # 提取<answer>标签中的内容,去除标签
652
- full_content = extract_answer_content(raw_content)
653
-
654
- # 提取token信息
655
- token_info = result.get('usage', {
656
- "prompt_tokens": 0,
657
- "completion_tokens": 0,
658
- "total_tokens": 0
659
- })
660
-
661
- await response.aclose()
662
- return full_content, token_info
663
-
664
- except Exception as e:
665
- logger.error(f"处理非流式响应错误: {e}")
666
- raise
667
-
668
- async def process_stream_response(k2think_payload: dict, headers: dict) -> AsyncGenerator[str, None]:
669
- """处理流式响应 - 使用模拟流式输出"""
670
- try:
671
- # 将流式请求转换为非流式请求
672
- k2think_payload_copy = k2think_payload.copy()
673
- k2think_payload_copy["stream"] = False
674
-
675
- # 修改headers为非流式
676
- headers_copy = headers.copy()
677
- headers_copy["accept"] = "application/json"
678
-
679
- # 获取完整响应
680
- full_content, token_info = await process_non_stream_response(k2think_payload_copy, headers_copy)
681
-
682
- if not full_content:
683
- yield "data: [DONE]\n\n"
684
- return
685
-
686
- # 开始流式输出 - 发送开始chunk
687
- start_chunk = {
688
- "id": f"chatcmpl-{int(time.time() * 1000)}",
689
- "object": "chat.completion.chunk",
690
- "created": int(time.time()),
691
- "model": "MBZUAI-IFM/K2-Think",
692
- "choices": [{
693
- "index": 0,
694
- "delta": {
695
- "role": "assistant",
696
- "content": ""
697
- },
698
- "finish_reason": None
699
- }]
700
- }
701
- yield f"data: {json.dumps(start_chunk)}\n\n"
702
-
703
- # 模拟流式输出 - 按字符分块发送,使用动态chunk_size
704
-
705
- chunk_size = calculate_dynamic_chunk_size(len(full_content)) # 动态计算每次发送的字符数
706
-
707
- for i in range(0, len(full_content), chunk_size):
708
- chunk_content = full_content[i:i + chunk_size]
709
-
710
- chunk = {
711
- "id": f"chatcmpl-{int(time.time() * 1000)}",
712
- "object": "chat.completion.chunk",
713
- "created": int(time.time()),
714
- "model": "MBZUAI-IFM/K2-Think",
715
- "choices": [{
716
- "index": 0,
717
- "delta": {
718
- "content": chunk_content
719
- },
720
- "finish_reason": None
721
- }]
722
- }
723
-
724
- yield f"data: {json.dumps(chunk)}\n\n"
725
- # 添加小延迟模拟真实流式效果
726
- await asyncio.sleep(STREAM_DELAY)
727
-
728
- # 发送结束chunk
729
- end_chunk = {
730
- "id": f"chatcmpl-{int(time.time() * 1000)}",
731
- "object": "chat.completion.chunk",
732
- "created": int(time.time()),
733
- "model": "MBZUAI-IFM/K2-Think",
734
- "choices": [{
735
- "index": 0,
736
- "delta": {},
737
- "finish_reason": "stop"
738
- }]
739
- }
740
- yield f"data: {json.dumps(end_chunk)}\n\n"
741
- yield "data: [DONE]\n\n"
742
-
743
- except Exception as e:
744
- logger.error(f"流式请求失败: {e}")
745
- # 发送错误信息
746
- error_chunk = {
747
- "id": f"chatcmpl-{int(time.time() * 1000)}",
748
- "object": "chat.completion.chunk",
749
- "created": int(time.time()),
750
- "model": "MBZUAI-IFM/K2-Think",
751
- "choices": [{
752
- "index": 0,
753
- "delta": {
754
- "content": f"Error: {str(e)}"
755
- },
756
- "finish_reason": "stop"
757
- }]
758
- }
759
- yield f"data: {json.dumps(error_chunk)}\n\n"
760
- yield "data: [DONE]\n\n"
761
-
762
- async def process_stream_response_with_tools(k2think_payload: dict, headers: dict, has_tools: bool = False) -> AsyncGenerator[str, None]:
763
- """处理流式响应 - 支持工具调用,优化性能"""
764
- try:
765
- # 发送开始chunk
766
- start_chunk = {
767
- "id": f"chatcmpl-{int(time.time() * 1000)}",
768
- "object": "chat.completion.chunk",
769
- "created": int(time.time()),
770
- "model": "MBZUAI-IFM/K2-Think",
771
- "choices": [{
772
- "index": 0,
773
- "delta": {
774
- "role": "assistant",
775
- "content": ""
776
- },
777
- "finish_reason": None
778
- }]
779
- }
780
- yield f"data: {json.dumps(start_chunk)}\n\n"
781
-
782
- # 优化的模拟流式输出 - 立即开始获取响应并流式发送
783
- k2think_payload_copy = k2think_payload.copy()
784
- k2think_payload_copy["stream"] = False
785
-
786
- headers_copy = headers.copy()
787
- headers_copy["accept"] = "application/json"
788
-
789
- # 获取完整响应
790
- full_content, token_info = await process_non_stream_response(k2think_payload_copy, headers_copy)
791
-
792
- if not full_content:
793
- yield "data: [DONE]\n\n"
794
- return
795
-
796
- # Handle tool calls for streaming
797
- finish_reason = "stop"
798
- if has_tools:
799
- tool_calls = extract_tool_invocations(full_content)
800
- if tool_calls:
801
- # Send tool calls with proper format
802
- for i, tc in enumerate(tool_calls):
803
- tool_call_delta = {
804
- "index": i,
805
- "id": tc.get("id"),
806
- "type": tc.get("type", "function"),
807
- "function": tc.get("function", {}),
808
- }
809
-
810
- tool_chunk = {
811
- "id": f"chatcmpl-{int(time.time() * 1000)}",
812
- "object": "chat.completion.chunk",
813
- "created": int(time.time()),
814
- "model": "MBZUAI-IFM/K2-Think",
815
- "choices": [{
816
- "index": 0,
817
- "delta": {
818
- "tool_calls": [tool_call_delta]
819
- },
820
- "finish_reason": None
821
- }]
822
- }
823
- yield f"data: {json.dumps(tool_chunk)}\n\n"
824
-
825
- finish_reason = "tool_calls"
826
- else:
827
- # Send regular content with true streaming feel
828
- trimmed_content = remove_tool_json_content(full_content)
829
- if trimmed_content:
830
- # 快速流式输出 - 动态计算块大小
831
- chunk_size = calculate_dynamic_chunk_size(len(trimmed_content)) # 动态计算每次发送的字符数
832
-
833
- for i in range(0, len(trimmed_content), chunk_size):
834
- chunk_content = trimmed_content[i:i + chunk_size]
835
-
836
- chunk = {
837
- "id": f"chatcmpl-{int(time.time() * 1000)}",
838
- "object": "chat.completion.chunk",
839
- "created": int(time.time()),
840
- "model": "MBZUAI-IFM/K2-Think",
841
- "choices": [{
842
- "index": 0,
843
- "delta": {
844
- "content": chunk_content
845
- },
846
- "finish_reason": None
847
- }]
848
- }
849
-
850
- yield f"data: {json.dumps(chunk)}\n\n"
851
- # 添加极小延迟确保块分别发送
852
- await asyncio.sleep(STREAM_DELAY) # 毫秒延迟
853
- else:
854
- # No tools - send regular content with fast streaming
855
- chunk_size = calculate_dynamic_chunk_size(len(full_content)) # 动态计算每次发送的字符数
856
-
857
- for i in range(0, len(full_content), chunk_size):
858
- chunk_content = full_content[i:i + chunk_size]
859
-
860
- chunk = {
861
- "id": f"chatcmpl-{int(time.time() * 1000)}",
862
- "object": "chat.completion.chunk",
863
- "created": int(time.time()),
864
- "model": "MBZUAI-IFM/K2-Think",
865
- "choices": [{
866
- "index": 0,
867
- "delta": {
868
- "content": chunk_content
869
- },
870
- "finish_reason": None
871
- }]
872
- }
873
-
874
- yield f"data: {json.dumps(chunk)}\n\n"
875
- # 添加极小延迟确保块分别发送
876
- await asyncio.sleep(STREAM_DELAY) # 毫秒延迟
877
-
878
- # 发送结束chunk
879
- end_chunk = {
880
- "id": f"chatcmpl-{int(time.time() * 1000)}",
881
- "object": "chat.completion.chunk",
882
- "created": int(time.time()),
883
- "model": "MBZUAI-IFM/K2-Think",
884
- "choices": [{
885
- "index": 0,
886
- "delta": {},
887
- "finish_reason": finish_reason
888
- }]
889
- }
890
- yield f"data: {json.dumps(end_chunk)}\n\n"
891
- yield "data: [DONE]\n\n"
892
-
893
- except Exception as e:
894
- logger.error(f"流式响应处理错误: {e}")
895
- error_chunk = {
896
- "id": f"chatcmpl-{int(time.time() * 1000)}",
897
- "object": "chat.completion.chunk",
898
- "created": int(time.time()),
899
- "model": "MBZUAI-IFM/K2-Think",
900
- "choices": [{
901
- "index": 0,
902
- "delta": {},
903
- "finish_reason": "error"
904
- }]
905
- }
906
- yield f"data: {json.dumps(error_chunk)}\n\n"
907
- yield "data: [DONE]\n\n"
908
 
909
  @app.post("/v1/chat/completions")
910
  async def chat_completions(request: ChatCompletionRequest, auth_request: Request):
911
  """处理聊天补全请求"""
912
- # 验证API密钥
913
- authorization = auth_request.headers.get("Authorization", "")
914
- if not validate_api_key(authorization):
915
- raise HTTPException(
916
- status_code=401,
917
- detail={
918
- "error": {
919
- "message": "Invalid API key provided",
920
- "type": "authentication_error"
921
- }
 
922
  }
923
- )
924
-
925
- try:
926
- # Process messages with tools - 确保内容被正确转换为字符串
927
- raw_messages = []
928
- for msg in request.messages:
929
- try:
930
- content = content_to_string(msg.content)
931
- raw_messages.append({
932
- "role": msg.role,
933
- "content": content,
934
- "tool_calls": msg.tool_calls
935
- })
936
- except Exception as e:
937
- logger.error(f"处理消息时出错: {e}, 消息: {msg}")
938
- # 使用默认值
939
- raw_messages.append({
940
- "role": msg.role,
941
- "content": str(msg.content) if msg.content else "",
942
- "tool_calls": msg.tool_calls
943
- })
944
-
945
- # Check if tools are enabled and present
946
- has_tools = (TOOL_SUPPORT and
947
- request.tools and
948
- len(request.tools) > 0 and
949
- request.tool_choice != "none")
950
-
951
- logger.info(f"🔧 工具调用状态: has_tools={has_tools}, tools_count={len(request.tools) if request.tools else 0}")
952
- logger.info(f"📥 接收到的原始消息数: {len(raw_messages)}")
953
-
954
- # 记录原始消息的角色分布
955
- role_count = {}
956
- for msg in raw_messages:
957
- role = msg.get("role", "unknown")
958
- role_count[role] = role_count.get(role, 0) + 1
959
- logger.info(f"📊 原始消息角色分布: {role_count}")
960
-
961
- if has_tools:
962
- processed_messages = process_messages_with_tools(
963
- raw_messages,
964
- request.tools,
965
- request.tool_choice
966
- )
967
- logger.info(f"🔄 消息处理完成,原始消息数: {len(raw_messages)}, 处理后消息数: {len(processed_messages)}")
968
-
969
- # 记录处理后消息的角色分布
970
- processed_role_count = {}
971
- for msg in processed_messages:
972
- role = msg.get("role", "unknown")
973
- processed_role_count[role] = processed_role_count.get(role, 0) + 1
974
- logger.info(f"📊 处理后消息角色分布: {processed_role_count}")
975
- else:
976
- processed_messages = raw_messages
977
- logger.info("⏭️ 无工具调用,直接使用原始消息")
978
-
979
- # 构建 K2Think 格式的请求体 - 确保所有内容可JSON序列化
980
- k2think_messages = []
981
- for msg in processed_messages:
982
- try:
983
- # 确保消息内容是字符串
984
- content = content_to_string(msg.get("content", ""))
985
- k2think_messages.append({
986
- "role": msg["role"],
987
- "content": content
988
- })
989
- except Exception as e:
990
- logger.error(f"构建K2Think消息时出错: {e}, 消息: {msg}")
991
- # 使用安全的默认值
992
- k2think_messages.append({
993
- "role": msg.get("role", "user"),
994
- "content": str(msg.get("content", ""))
995
- })
996
-
997
- k2think_payload = {
998
- "stream": request.stream,
999
- "model": "MBZUAI-IFM/K2-Think",
1000
- "messages": k2think_messages,
1001
- "params": {},
1002
- "tool_servers": [],
1003
- "features": {
1004
- "image_generation": False,
1005
- "code_interpreter": False,
1006
- "web_search": False
1007
- },
1008
- "variables": get_current_datetime_info(),
1009
- "model_item": {
1010
- "id": "MBZUAI-IFM/K2-Think",
1011
- "object": "model",
1012
- "owned_by": "MBZUAI",
1013
- "root": "mbzuai-k2-think-2508",
1014
- "parent": None,
1015
- "status": "active",
1016
- "connection_type": "external",
1017
- "name": "MBZUAI-IFM/K2-Think"
1018
- },
1019
- "background_tasks": {
1020
- "title_generation": True,
1021
- "tags_generation": True
1022
- },
1023
- "chat_id": generate_chat_id(),
1024
- "id": generate_session_id(),
1025
- "session_id": generate_session_id()
1026
- }
1027
-
1028
- # 验证JSON序列化并记录发送到上游的请求
1029
- try:
1030
- # 测试JSON序列化
1031
- json.dumps(k2think_payload, ensure_ascii=False)
1032
- logger.info(f"✅ K2Think请求体JSON序列化验证通过")
1033
- except Exception as e:
1034
- logger.error(f"❌ K2Think请求体JSON序列化失败: {e}")
1035
- # 尝试修复序列化问题
1036
- try:
1037
- k2think_payload = json.loads(json.dumps(k2think_payload, default=str, ensure_ascii=False))
1038
- logger.info("🔧 使用default=str修复了序列化问题")
1039
- except Exception as fix_error:
1040
- logger.error(f"无法修复序列化问题: {fix_error}")
1041
- raise HTTPException(status_code=500, detail="请求数据序列化失败")
1042
-
1043
- logger.info(f"发送到 K2Think 的消息数量: {len(k2think_payload['messages'])}")
1044
- if DEBUG_LOGGING or logger.level <= logging.DEBUG:
1045
- for i, msg in enumerate(k2think_payload['messages']):
1046
- content_preview = msg['content'][:200] + "..." if len(msg['content']) > 200 else msg['content']
1047
- logger.debug(f"消息 {i+1} ({msg['role']}): {content_preview}")
1048
-
1049
- # 设置请求头
1050
- headers = {
1051
- "accept": "text/event-stream,application/json" if request.stream else "application/json",
1052
- "content-type": "application/json",
1053
- "authorization": f"Bearer {K2THINK_TOKEN}",
1054
- "origin": "https://www.k2think.ai",
1055
- "referer": "https://www.k2think.ai/c/" + k2think_payload["chat_id"],
1056
- "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36 Edg/140.0.0.0"
1057
  }
1058
-
1059
- if request.stream:
1060
- # 流式响应
1061
- return StreamingResponse(
1062
- process_stream_response_with_tools(k2think_payload, headers, has_tools),
1063
- media_type="text/event-stream",
1064
- headers={
1065
- "Cache-Control": "no-cache",
1066
- "Connection": "keep-alive",
1067
- "X-Accel-Buffering": "no"
1068
- }
1069
- )
1070
- else:
1071
- # 非流式响应
1072
- full_content, token_info = await process_non_stream_response(k2think_payload, headers)
1073
-
1074
- # Handle tool calls for non-streaming
1075
- tool_calls = None
1076
- finish_reason = "stop"
1077
- message_content = full_content
1078
-
1079
- if has_tools:
1080
- tool_calls = extract_tool_invocations(full_content)
1081
- if tool_calls:
1082
- # Content must be null when tool_calls are present (OpenAI spec)
1083
- message_content = None
1084
- finish_reason = "tool_calls"
1085
- logger.info(f"提取到工具调用: {json.dumps(tool_calls, ensure_ascii=False)}")
1086
- else:
1087
- # Remove tool JSON from content
1088
- message_content = remove_tool_json_content(full_content)
1089
- if not message_content:
1090
- message_content = full_content # 保留原内容如果清理后为空
1091
-
1092
- openai_response = {
1093
- "id": f"chatcmpl-{int(time.time())}",
1094
- "object": "chat.completion",
1095
- "created": int(time.time()),
1096
- "model": "MBZUAI-IFM/K2-Think",
1097
- "choices": [{
1098
- "index": 0,
1099
- "message": {
1100
- "role": "assistant",
1101
- "content": message_content,
1102
- **({"tool_calls": tool_calls} if tool_calls else {})
1103
- },
1104
- "finish_reason": finish_reason
1105
- }],
1106
- "usage": token_info
1107
- }
1108
-
1109
- return JSONResponse(content=openai_response)
1110
-
1111
- except httpx.HTTPStatusError as e:
1112
- logger.error(f"HTTP错误: {e.response.status_code}")
1113
- raise HTTPException(
1114
- status_code=e.response.status_code,
1115
- detail={
1116
- "error": {
1117
- "message": f"上游服务错误: {e.response.status_code}",
1118
- "type": "upstream_error"
1119
- }
1120
- }
1121
- )
1122
- except httpx.TimeoutException:
1123
- logger.error("请求超时")
1124
- raise HTTPException(
1125
- status_code=504,
1126
- detail={
1127
- "error": {
1128
- "message": "请求超时",
1129
- "type": "timeout_error"
1130
- }
1131
- }
1132
- )
1133
- except Exception as e:
1134
- logger.error(f"API转发错误: {e}")
1135
- raise HTTPException(
1136
- status_code=500,
1137
- detail={
1138
- "error": {
1139
- "message": str(e),
1140
- "type": "api_error"
1141
- }
1142
- }
1143
- )
1144
 
1145
  @app.exception_handler(404)
1146
  async def not_found_handler(request: Request, exc):
 
1147
  return JSONResponse(
1148
  status_code=404,
1149
  content={"error": "Not Found"}
@@ -1151,16 +118,18 @@ async def not_found_handler(request: Request, exc):
1151
 
1152
  if __name__ == "__main__":
1153
  import uvicorn
1154
- host = os.getenv("HOST", "0.0.0.0")
1155
- port = int(os.getenv("PORT", "8001"))
1156
 
1157
  # 配置日志级别
1158
- log_level = "debug" if DEBUG_LOGGING else "info"
 
 
 
 
1159
 
1160
  uvicorn.run(
1161
  app,
1162
- host=host,
1163
- port=port,
1164
- access_log=ENABLE_ACCESS_LOG,
1165
  log_level=log_level
1166
  )
 
1
+ """
2
+ K2Think API 代理服务 - 重构版本
3
+ 提供OpenAI兼容的API接口,代理到K2Think服务
4
+ """
 
 
 
 
5
  import time
 
6
  import logging
 
7
  from contextlib import asynccontextmanager
8
+ from fastapi import FastAPI, Request
9
+ from fastapi.middleware.cors import CORSMiddleware
10
+ from fastapi.responses import JSONResponse, Response
11
+
12
+ from src.config import Config
13
+ from src.constants import APIConstants
14
+ from src.exceptions import K2ThinkProxyError
15
+ from src.models import ChatCompletionRequest
16
+ from src.api_handler import APIHandler
17
+
18
+ # 初始化配置
19
+ try:
20
+ Config.validate()
21
+ Config.setup_logging()
22
+ except Exception as e:
23
+ print(f"配置错误: {e}")
24
+ exit(1)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
 
26
  logger = logging.getLogger(__name__)
27
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  # 全局HTTP客户端管理
29
  @asynccontextmanager
30
  async def lifespan(app: FastAPI):
31
+ logger.info("K2Think API Proxy 启动中...")
32
  yield
33
+ logger.info("K2Think API Proxy 关闭中...")
34
 
35
  # 创建FastAPI应用
36
+ app = FastAPI(
37
+ title="K2Think API Proxy",
38
+ description="OpenAI兼容的K2Think API代理服务",
39
+ version="2.0.0",
40
+ lifespan=lifespan
41
+ )
42
 
43
  # CORS配置
44
  app.add_middleware(
45
  CORSMiddleware,
46
+ allow_origins=Config.CORS_ORIGINS,
47
  allow_credentials=True,
48
  allow_methods=["*"],
49
  allow_headers=["*"],
50
  )
51
 
52
+ # 初始化API处理器
53
+ api_handler = APIHandler(Config)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
 
55
  @app.get("/")
56
  async def homepage():
 
59
  "status": "success",
60
  "message": "K2Think API Proxy is running",
61
  "service": "K2Think API Gateway",
62
+ "model": APIConstants.MODEL_ID,
63
+ "version": "2.0.0",
64
  "endpoints": {
65
  "chat": "/v1/chat/completions",
66
  "models": "/v1/models"
 
72
  """健康检查"""
73
  return JSONResponse(content={
74
  "status": "healthy",
75
+ "timestamp": int(time.time()),
76
+ "config": {
77
+ "tool_support": Config.TOOL_SUPPORT,
78
+ "debug_logging": Config.DEBUG_LOGGING,
79
+ "note": "思考内容输出现在通过模型名控制"
80
+ }
81
  })
82
 
83
  @app.get("/favicon.ico")
 
86
  return Response(content="", media_type="image/x-icon")
87
 
88
  @app.get("/v1/models")
89
+ async def get_models():
90
  """获取模型列表"""
91
+ return await api_handler.get_models()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
 
93
  @app.post("/v1/chat/completions")
94
  async def chat_completions(request: ChatCompletionRequest, auth_request: Request):
95
  """处理聊天补全请求"""
96
+ return await api_handler.chat_completions(request, auth_request)
97
+
98
+ @app.exception_handler(K2ThinkProxyError)
99
+ async def proxy_exception_handler(request: Request, exc: K2ThinkProxyError):
100
+ """处理自定义代理异常"""
101
+ return JSONResponse(
102
+ status_code=exc.status_code,
103
+ content={
104
+ "error": {
105
+ "message": exc.message,
106
+ "type": exc.error_type
107
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  }
109
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
 
111
  @app.exception_handler(404)
112
  async def not_found_handler(request: Request, exc):
113
+ """处理404错误"""
114
  return JSONResponse(
115
  status_code=404,
116
  content={"error": "Not Found"}
 
118
 
119
  if __name__ == "__main__":
120
  import uvicorn
 
 
121
 
122
  # 配置日志级别
123
+ log_level = "debug" if Config.DEBUG_LOGGING else "info"
124
+
125
+ logger.info(f"启动服务器: {Config.HOST}:{Config.PORT}")
126
+ logger.info(f"工具支持: {Config.TOOL_SUPPORT}")
127
+ logger.info("思考内容输出: 通过模型名控制 (MBZUAI-IFM/K2-Think vs MBZUAI-IFM/K2-Think-nothink)")
128
 
129
  uvicorn.run(
130
  app,
131
+ host=Config.HOST,
132
+ port=Config.PORT,
133
+ access_log=Config.ENABLE_ACCESS_LOG,
134
  log_level=log_level
135
  )
requirements.txt CHANGED
@@ -3,4 +3,5 @@ uvicorn[standard]
3
  httpx
4
  pydantic
5
  python-dotenv
6
- pytz
 
 
3
  httpx
4
  pydantic
5
  python-dotenv
6
+ pytz
7
+ requests
src/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ """
2
+ K2Think API Proxy 源代码包
3
+ """
src/api_handler.py ADDED
@@ -0,0 +1,347 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ API处理模块
3
+ 处理主要的API路由逻辑
4
+ """
5
+ import json
6
+ import time
7
+ import logging
8
+ from typing import Dict, List
9
+ from fastapi import HTTPException, Request
10
+ from fastapi.responses import StreamingResponse, JSONResponse
11
+
12
+ from src.config import Config
13
+ from src.constants import (
14
+ APIConstants, ResponseConstants, LogMessages,
15
+ ErrorMessages, HeaderConstants
16
+ )
17
+ from src.exceptions import (
18
+ AuthenticationError, SerializationError,
19
+ K2ThinkProxyError
20
+ )
21
+ from src.models import ChatCompletionRequest, ModelsResponse, ModelInfo
22
+ from src.tool_handler import ToolHandler
23
+ from src.response_processor import ResponseProcessor
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+ class APIHandler:
28
+ """API处理器"""
29
+
30
+ def __init__(self, config: Config):
31
+ self.config = config
32
+ self.tool_handler = ToolHandler(config)
33
+ self.response_processor = ResponseProcessor(config, self.tool_handler)
34
+
35
+ def validate_api_key(self, authorization: str) -> bool:
36
+ """验证API密钥"""
37
+ if not authorization or not authorization.startswith(APIConstants.BEARER_PREFIX):
38
+ return False
39
+ api_key = authorization[APIConstants.BEARER_PREFIX_LENGTH:] # 移除 "Bearer " 前缀
40
+ return api_key == self.config.VALID_API_KEY
41
+
42
+ def should_output_thinking(self, model_name: str) -> bool:
43
+ """根据模型名判断是否应该输出思考内容"""
44
+ return model_name != APIConstants.MODEL_ID_NOTHINK
45
+
46
+ def get_actual_model_id(self, model_name: str) -> str:
47
+ """获取实际的模型ID(将nothink版本映射回原始模型)"""
48
+ if model_name == APIConstants.MODEL_ID_NOTHINK:
49
+ return APIConstants.MODEL_ID
50
+ return model_name
51
+
52
+ async def get_models(self) -> ModelsResponse:
53
+ """获取模型列表"""
54
+ model_info_standard = ModelInfo(
55
+ id=APIConstants.MODEL_ID,
56
+ created=int(time.time()),
57
+ owned_by=APIConstants.MODEL_OWNER,
58
+ root=APIConstants.MODEL_ROOT
59
+ )
60
+ model_info_nothink = ModelInfo(
61
+ id=APIConstants.MODEL_ID_NOTHINK,
62
+ created=int(time.time()),
63
+ owned_by=APIConstants.MODEL_OWNER,
64
+ root=APIConstants.MODEL_ROOT
65
+ )
66
+ return ModelsResponse(data=[model_info_standard, model_info_nothink])
67
+
68
+ async def chat_completions(self, request: ChatCompletionRequest, auth_request: Request):
69
+ """处理聊天补全请求"""
70
+ # 验证API密钥
71
+ authorization = auth_request.headers.get(HeaderConstants.AUTHORIZATION, "")
72
+ if not self.validate_api_key(authorization):
73
+ raise AuthenticationError()
74
+
75
+ # 判断是否应该输出思考内容
76
+ output_thinking = self.should_output_thinking(request.model)
77
+ actual_model_id = self.get_actual_model_id(request.model)
78
+
79
+ try:
80
+ # 处理消息
81
+ raw_messages = self._process_raw_messages(request.messages)
82
+
83
+ # 检查工具是否启用和存在
84
+ has_tools = self._check_tools_enabled(request)
85
+
86
+ self._log_request_info(raw_messages, has_tools, request.tools)
87
+
88
+ # 处理工具相关消息
89
+ processed_messages = self._process_messages_with_tools(
90
+ raw_messages, request, has_tools
91
+ )
92
+
93
+ # 构建K2Think请求
94
+ k2think_payload = self._build_k2think_payload(
95
+ request, processed_messages, actual_model_id
96
+ )
97
+
98
+ # 验证JSON序列化
99
+ self._validate_json_serialization(k2think_payload)
100
+
101
+ # 设置请求头
102
+ headers = self._build_request_headers(request, k2think_payload)
103
+
104
+ # 处理响应
105
+ if request.stream:
106
+ return await self._handle_stream_response(
107
+ k2think_payload, headers, has_tools, output_thinking, request.model
108
+ )
109
+ else:
110
+ return await self._handle_non_stream_response(
111
+ k2think_payload, headers, has_tools, output_thinking, request.model
112
+ )
113
+
114
+ except K2ThinkProxyError:
115
+ # 重新抛出自定义异常
116
+ raise
117
+ except Exception as e:
118
+ logger.error(f"API转发错误: {e}")
119
+ raise HTTPException(
120
+ status_code=APIConstants.HTTP_INTERNAL_ERROR,
121
+ detail={
122
+ "error": {
123
+ "message": str(e),
124
+ "type": ErrorMessages.API_ERROR
125
+ }
126
+ }
127
+ )
128
+
129
+ def _process_raw_messages(self, messages: List) -> List[Dict]:
130
+ """处理原始消息"""
131
+ raw_messages = []
132
+ for msg in messages:
133
+ try:
134
+ raw_messages.append({
135
+ "role": msg.role,
136
+ "content": msg.content, # 保持原始格式,稍后再转换
137
+ "tool_calls": msg.tool_calls
138
+ })
139
+ except Exception as e:
140
+ logger.error(f"处理消息时出错: {e}, 消息: {msg}")
141
+ # 使用默认值
142
+ raw_messages.append({
143
+ "role": msg.role,
144
+ "content": str(msg.content) if msg.content else "",
145
+ "tool_calls": msg.tool_calls
146
+ })
147
+ return raw_messages
148
+
149
+ def _check_tools_enabled(self, request: ChatCompletionRequest) -> bool:
150
+ """检查工具是否启用"""
151
+ return (
152
+ self.config.TOOL_SUPPORT and
153
+ request.tools is not None and
154
+ len(request.tools) > 0 and
155
+ request.tool_choice != "none"
156
+ )
157
+
158
+ def _log_request_info(self, raw_messages: List[Dict], has_tools: bool, tools: List):
159
+ """记录请求信息"""
160
+ logger.info(LogMessages.TOOL_STATUS.format(
161
+ has_tools, len(tools) if tools else 0
162
+ ))
163
+ logger.info(LogMessages.MESSAGE_RECEIVED.format(len(raw_messages)))
164
+
165
+ # 记录原始消息的角色分布
166
+ role_count = {}
167
+ for msg in raw_messages:
168
+ role = msg.get("role", "unknown")
169
+ role_count[role] = role_count.get(role, 0) + 1
170
+ logger.info(LogMessages.ROLE_DISTRIBUTION.format("原始", role_count))
171
+
172
+ def _process_messages_with_tools(
173
+ self,
174
+ raw_messages: List[Dict],
175
+ request: ChatCompletionRequest,
176
+ has_tools: bool
177
+ ) -> List[Dict]:
178
+ """处理工具相关消息"""
179
+ if has_tools:
180
+ processed_messages = self.tool_handler.process_messages_with_tools(
181
+ raw_messages,
182
+ request.tools,
183
+ request.tool_choice
184
+ )
185
+ logger.info(LogMessages.MESSAGE_PROCESSED.format(
186
+ len(raw_messages), len(processed_messages)
187
+ ))
188
+
189
+ # 记录处理后消息的角色分布
190
+ processed_role_count = {}
191
+ for msg in processed_messages:
192
+ role = msg.get("role", "unknown")
193
+ processed_role_count[role] = processed_role_count.get(role, 0) + 1
194
+ logger.info(LogMessages.ROLE_DISTRIBUTION.format("处理后", processed_role_count))
195
+ else:
196
+ processed_messages = raw_messages
197
+ logger.info(LogMessages.NO_TOOLS)
198
+
199
+ return processed_messages
200
+
201
+ def _build_k2think_payload(
202
+ self,
203
+ request: ChatCompletionRequest,
204
+ processed_messages: List[Dict],
205
+ actual_model_id: str = None
206
+ ) -> Dict:
207
+ """构建K2Think请求负载"""
208
+ # 构建K2Think格式的请求体 - 支持多模态内容
209
+ k2think_messages = []
210
+ for msg in processed_messages:
211
+ try:
212
+ # 使用多模态内容转换函数
213
+ content = self.response_processor.content_to_multimodal(msg.get("content", ""))
214
+ k2think_messages.append({
215
+ "role": msg["role"],
216
+ "content": content
217
+ })
218
+ except Exception as e:
219
+ logger.error(f"构建K2Think消息时出错: {e}, 消息: {msg}")
220
+ # 使用安全的默认值
221
+ fallback_content = self.tool_handler._content_to_string(msg.get("content", ""))
222
+ k2think_messages.append({
223
+ "role": msg.get("role", "user"),
224
+ "content": fallback_content
225
+ })
226
+
227
+ # 使用实际的模型ID
228
+ model_id = actual_model_id or APIConstants.MODEL_ID
229
+
230
+ return {
231
+ "stream": request.stream,
232
+ "model": model_id,
233
+ "messages": k2think_messages,
234
+ "params": {},
235
+ "tool_servers": [],
236
+ "features": {
237
+ "image_generation": False,
238
+ "code_interpreter": False,
239
+ "web_search": False
240
+ },
241
+ "variables": self.response_processor.get_current_datetime_info(),
242
+ "model_item": {
243
+ "id": model_id,
244
+ "object": ResponseConstants.MODEL_OBJECT,
245
+ "owned_by": APIConstants.MODEL_OWNER,
246
+ "root": APIConstants.MODEL_ROOT,
247
+ "parent": None,
248
+ "status": "active",
249
+ "connection_type": "external",
250
+ "name": model_id
251
+ },
252
+ "background_tasks": {
253
+ "title_generation": True,
254
+ "tags_generation": True
255
+ },
256
+ "chat_id": self.response_processor.generate_chat_id(),
257
+ "id": self.response_processor.generate_session_id(),
258
+ "session_id": self.response_processor.generate_session_id()
259
+ }
260
+
261
+ def _validate_json_serialization(self, k2think_payload: Dict):
262
+ """验证JSON序列化"""
263
+ try:
264
+ # 测试JSON序列化
265
+ json.dumps(k2think_payload, ensure_ascii=False)
266
+ logger.info(LogMessages.JSON_VALIDATION_SUCCESS)
267
+ except Exception as e:
268
+ logger.error(LogMessages.JSON_VALIDATION_FAILED.format(e))
269
+ # 尝试修复序列化问题
270
+ try:
271
+ k2think_payload = json.loads(json.dumps(k2think_payload, default=str, ensure_ascii=False))
272
+ logger.info(LogMessages.JSON_FIXED)
273
+ except Exception as fix_error:
274
+ logger.error(f"无法修复序列化问题: {fix_error}")
275
+ raise SerializationError()
276
+
277
+ def _build_request_headers(self, request: ChatCompletionRequest, k2think_payload: Dict) -> Dict[str, str]:
278
+ """构建请求头"""
279
+ return {
280
+ HeaderConstants.ACCEPT: (
281
+ HeaderConstants.EVENT_STREAM_JSON if request.stream
282
+ else HeaderConstants.APPLICATION_JSON
283
+ ),
284
+ HeaderConstants.CONTENT_TYPE: HeaderConstants.APPLICATION_JSON,
285
+ HeaderConstants.AUTHORIZATION: f"{APIConstants.BEARER_PREFIX}{self.config.K2THINK_TOKEN}",
286
+ HeaderConstants.ORIGIN: "https://www.k2think.ai",
287
+ HeaderConstants.REFERER: "https://www.k2think.ai/c/" + k2think_payload["chat_id"],
288
+ HeaderConstants.USER_AGENT: HeaderConstants.DEFAULT_USER_AGENT
289
+ }
290
+
291
+ async def _handle_stream_response(
292
+ self,
293
+ k2think_payload: Dict,
294
+ headers: Dict[str, str],
295
+ has_tools: bool,
296
+ output_thinking: bool = True,
297
+ original_model: str = None
298
+ ) -> StreamingResponse:
299
+ """处理流式响应"""
300
+ return StreamingResponse(
301
+ self.response_processor.process_stream_response_with_tools(
302
+ k2think_payload, headers, has_tools, output_thinking, original_model
303
+ ),
304
+ media_type=HeaderConstants.TEXT_EVENT_STREAM,
305
+ headers={
306
+ HeaderConstants.CACHE_CONTROL: HeaderConstants.NO_CACHE,
307
+ HeaderConstants.CONNECTION: HeaderConstants.KEEP_ALIVE,
308
+ HeaderConstants.X_ACCEL_BUFFERING: HeaderConstants.NO_BUFFERING
309
+ }
310
+ )
311
+
312
+ async def _handle_non_stream_response(
313
+ self,
314
+ k2think_payload: Dict,
315
+ headers: Dict[str, str],
316
+ has_tools: bool,
317
+ output_thinking: bool = True,
318
+ original_model: str = None
319
+ ) -> JSONResponse:
320
+ """处理非流式响应"""
321
+ full_content, token_info = await self.response_processor.process_non_stream_response(
322
+ k2think_payload, headers, output_thinking
323
+ )
324
+
325
+ # 处理工具调用
326
+ tool_calls = None
327
+ message_content = full_content
328
+
329
+ if has_tools:
330
+ tool_calls = self.tool_handler.extract_tool_invocations(full_content)
331
+ if tool_calls:
332
+ # 当存在工具调用时,内容必须为null(OpenAI规范)
333
+ message_content = None
334
+ logger.info(LogMessages.TOOL_CALLS_EXTRACTED.format(
335
+ json.dumps(tool_calls, ensure_ascii=False)
336
+ ))
337
+ else:
338
+ # 从内容中移除工具JSON
339
+ message_content = self.tool_handler.remove_tool_json_content(full_content)
340
+ if not message_content:
341
+ message_content = full_content # 保留原内容如果清理后为空
342
+
343
+ openai_response = self.response_processor.create_completion_response(
344
+ message_content, tool_calls, token_info, original_model
345
+ )
346
+
347
+ return JSONResponse(content=openai_response)
src/config.py ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 配置管理模块
3
+ 统一管理所有环境变量和配置项
4
+ """
5
+ import os
6
+ import logging
7
+ from typing import List
8
+ from dotenv import load_dotenv
9
+
10
+ # 加载环境变量
11
+ load_dotenv()
12
+
13
+ class Config:
14
+ """应用配置类"""
15
+
16
+ # API认证配置
17
+ VALID_API_KEY: str = os.getenv("VALID_API_KEY", "")
18
+ K2THINK_TOKEN: str = os.getenv("K2THINK_TOKEN", "")
19
+ K2THINK_API_URL: str = os.getenv("K2THINK_API_URL", "https://www.k2think.ai/api/chat/completions")
20
+
21
+ # 服务器配置
22
+ HOST: str = os.getenv("HOST", "0.0.0.0")
23
+ PORT: int = int(os.getenv("PORT", "8001"))
24
+
25
+ # 功能开关
26
+ TOOL_SUPPORT: bool = os.getenv("TOOL_SUPPORT", "true").lower() == "true"
27
+ DEBUG_LOGGING: bool = os.getenv("DEBUG_LOGGING", "false").lower() == "true"
28
+ ENABLE_ACCESS_LOG: bool = os.getenv("ENABLE_ACCESS_LOG", "true").lower() == "true"
29
+
30
+ # 性能配置
31
+ SCAN_LIMIT: int = int(os.getenv("SCAN_LIMIT", "200000"))
32
+ SYSTEM_MESSAGE_LENGTH: int = int(os.getenv("SYSTEM_MESSAGE_LENTH", "200000"))
33
+ REQUEST_TIMEOUT: float = float(os.getenv("REQUEST_TIMEOUT", "60"))
34
+ MAX_KEEPALIVE_CONNECTIONS: int = int(os.getenv("MAX_KEEPALIVE_CONNECTIONS", "20"))
35
+ MAX_CONNECTIONS: int = int(os.getenv("MAX_CONNECTIONS", "100"))
36
+ STREAM_DELAY: float = float(os.getenv("STREAM_DELAY", "0.05"))
37
+ STREAM_CHUNK_SIZE: int = int(os.getenv("STREAM_CHUNK_SIZE", "50"))
38
+ MAX_STREAM_TIME: float = float(os.getenv("MAX_STREAM_TIME", "10.0"))
39
+
40
+ # 日志配置
41
+ LOG_LEVEL: str = os.getenv("LOG_LEVEL", "INFO").upper()
42
+
43
+ # CORS配置
44
+ CORS_ORIGINS: List[str] = (
45
+ os.getenv("CORS_ORIGINS", "*").split(",")
46
+ if os.getenv("CORS_ORIGINS", "*") != "*"
47
+ else ["*"]
48
+ )
49
+
50
+ @classmethod
51
+ def validate(cls) -> None:
52
+ """验证必需的配置项"""
53
+ if not cls.VALID_API_KEY:
54
+ raise ValueError("错误:VALID_API_KEY 环境变量未设置。请在 .env 文件中提供一个安全的API密钥。")
55
+
56
+ if not cls.K2THINK_TOKEN:
57
+ raise ValueError("错误:K2THINK_TOKEN 环境变量未设置。请在 .env 文件中提供有效的K2Think JWT Token。")
58
+
59
+ # 验证数值范围
60
+ if cls.PORT < 1 or cls.PORT > 65535:
61
+ raise ValueError(f"错误:PORT 值 {cls.PORT} 不在有效范围内 (1-65535)")
62
+
63
+ if cls.REQUEST_TIMEOUT <= 0:
64
+ raise ValueError(f"错误:REQUEST_TIMEOUT 必须大于0,当前值: {cls.REQUEST_TIMEOUT}")
65
+
66
+ if cls.STREAM_DELAY < 0:
67
+ raise ValueError(f"错误:STREAM_DELAY 不能为负数,当前值: {cls.STREAM_DELAY}")
68
+
69
+ @classmethod
70
+ def setup_logging(cls) -> None:
71
+ """设置日志配置"""
72
+ level_map = {
73
+ "DEBUG": logging.DEBUG,
74
+ "INFO": logging.INFO,
75
+ "WARNING": logging.WARNING,
76
+ "ERROR": logging.ERROR
77
+ }
78
+
79
+ log_level = level_map.get(cls.LOG_LEVEL, logging.INFO)
80
+ logging.basicConfig(
81
+ level=log_level,
82
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
83
+ )
src/constants.py ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 常量定义模块
3
+ 统一管理所有魔法数字和硬编码字符串
4
+ """
5
+
6
+ # API相关常量
7
+ class APIConstants:
8
+ MODEL_ID = "MBZUAI-IFM/K2-Think"
9
+ MODEL_ID_NOTHINK = "MBZUAI-IFM/K2-Think-nothink"
10
+ MODEL_OWNER = "MBZUAI"
11
+ MODEL_ROOT = "mbzuai-k2-think-2508"
12
+
13
+ # HTTP状态码
14
+ HTTP_OK = 200
15
+ HTTP_UNAUTHORIZED = 401
16
+ HTTP_NOT_FOUND = 404
17
+ HTTP_INTERNAL_ERROR = 500
18
+ HTTP_GATEWAY_TIMEOUT = 504
19
+
20
+ # 认证相关
21
+ BEARER_PREFIX = "Bearer "
22
+ BEARER_PREFIX_LENGTH = 7
23
+
24
+ # 响应相关常量
25
+ class ResponseConstants:
26
+ CHAT_COMPLETION_OBJECT = "chat.completion"
27
+ CHAT_COMPLETION_CHUNK_OBJECT = "chat.completion.chunk"
28
+ MODEL_OBJECT = "model"
29
+ LIST_OBJECT = "list"
30
+
31
+ # 完成原因
32
+ FINISH_REASON_STOP = "stop"
33
+ FINISH_REASON_TOOL_CALLS = "tool_calls"
34
+ FINISH_REASON_ERROR = "error"
35
+
36
+ # 流式响应标记
37
+ STREAM_DONE_MARKER = "data: [DONE]\n\n"
38
+ STREAM_DATA_PREFIX = "data: "
39
+
40
+ # 工具调用相关常量
41
+ class ToolConstants:
42
+ FUNCTION_TYPE = "function"
43
+ TOOL_TYPE = "function"
44
+
45
+ # 工具调用ID前缀
46
+ CALL_ID_PREFIX = "call_"
47
+
48
+ # 工具提示长度限制
49
+ MAX_TOOL_PROMPT_LENGTH = 1000
50
+ TOOL_PROMPT_TRUNCATE_SUFFIX = "..."
51
+
52
+ # 内容处理相关常量
53
+ class ContentConstants:
54
+ # XML标签
55
+ THINK_START_TAG = "<think>"
56
+ THINK_END_TAG = "</think>"
57
+ ANSWER_START_TAG = "<answer>"
58
+ ANSWER_END_TAG = "</answer>"
59
+
60
+ # 内容类型
61
+ TEXT_TYPE = "text"
62
+ IMAGE_URL_TYPE = "image_url"
63
+
64
+ # 图像占位符
65
+ IMAGE_PLACEHOLDER = "[图像内容]"
66
+
67
+ # 默认值
68
+ DEFAULT_USER_NAME = "User"
69
+ DEFAULT_USER_LOCATION = "Unknown"
70
+ DEFAULT_USER_LANGUAGE = "en-US"
71
+ DEFAULT_TIMEZONE = "Asia/Shanghai"
72
+
73
+ # 错误消息常量
74
+ class ErrorMessages:
75
+ INVALID_API_KEY = "Invalid API key provided"
76
+ AUTHENTICATION_ERROR = "authentication_error"
77
+ UPSTREAM_ERROR = "upstream_error"
78
+ TIMEOUT_ERROR = "timeout_error"
79
+ API_ERROR = "api_error"
80
+
81
+ # 中文错误消息
82
+ REQUEST_TIMEOUT = "请求超时"
83
+ SERIALIZATION_FAILED = "请求数据序列化失败"
84
+ UPSTREAM_SERVICE_ERROR = "上游服务错误"
85
+
86
+ # 日志消息常量
87
+ class LogMessages:
88
+ TOOL_STATUS = "🔧 工具调用状态: has_tools={}, tools_count={}"
89
+ MESSAGE_RECEIVED = "📥 接收到的原始消息数: {}"
90
+ ROLE_DISTRIBUTION = "📊 {}消息角色分布: {}"
91
+ MESSAGE_PROCESSED = "🔄 消息处理完成,原始消息数: {}, 处理后消息数: {}"
92
+ NO_TOOLS = "⏭️ 无工具调用,直接使用原始消息"
93
+ JSON_VALIDATION_SUCCESS = "✅ K2Think请求体JSON序列化验证通过"
94
+ JSON_VALIDATION_FAILED = "❌ K2Think请求体JSON序列化失败: {}"
95
+ JSON_FIXED = "🔧 使用default=str修复了序列化问题"
96
+
97
+ # 动态chunk计算日志
98
+ DYNAMIC_CHUNK_CALC = "动态chunk_size计算: 内容长度={}, 计算值={}, 最终值={}"
99
+
100
+ # 工具相关日志
101
+ TOOL_PROMPT_TOO_LONG = "工具提示过长 ({} 字符),将截断"
102
+ SYSTEM_MESSAGE_TOO_LONG = "系统消息过长 ({} 字符),使用简化版本"
103
+ TOOL_CALLS_EXTRACTED = "提取到工具调用: {}"
104
+
105
+ # HTTP头常量
106
+ class HeaderConstants:
107
+ AUTHORIZATION = "Authorization"
108
+ CONTENT_TYPE = "Content-Type"
109
+ ACCEPT = "Accept"
110
+ ORIGIN = "Origin"
111
+ REFERER = "Referer"
112
+ USER_AGENT = "User-Agent"
113
+ CACHE_CONTROL = "Cache-Control"
114
+ CONNECTION = "Connection"
115
+ X_ACCEL_BUFFERING = "X-Accel-Buffering"
116
+
117
+ # 值
118
+ APPLICATION_JSON = "application/json"
119
+ TEXT_EVENT_STREAM = "text/event-stream"
120
+ EVENT_STREAM_JSON = "text/event-stream,application/json"
121
+ NO_CACHE = "no-cache"
122
+ KEEP_ALIVE = "keep-alive"
123
+ NO_BUFFERING = "no"
124
+
125
+ # User-Agent值
126
+ DEFAULT_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36 Edg/140.0.0.0"
127
+
128
+ # 时间相关常量
129
+ class TimeConstants:
130
+ # 时间格式
131
+ DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S"
132
+ DATE_FORMAT = "%Y-%m-%d"
133
+ TIME_FORMAT = "%H:%M:%S"
134
+ WEEKDAY_FORMAT = "%A"
135
+
136
+ # 微秒转换
137
+ MICROSECONDS_MULTIPLIER = 1000000
138
+
139
+ # 数值常量
140
+ class NumericConstants:
141
+ # chunk大小限制
142
+ MIN_CHUNK_SIZE = 50
143
+
144
+ # 内容预览长度
145
+ CONTENT_PREVIEW_LENGTH = 200
146
+ CONTENT_PREVIEW_SUFFIX = "..."
147
+
148
+ # 默认token使用量
149
+ DEFAULT_PROMPT_TOKENS = 0
150
+ DEFAULT_COMPLETION_TOKENS = 0
151
+ DEFAULT_TOTAL_TOKENS = 0
src/exceptions.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 自定义异常类模块
3
+ 统一管理所有自定义异常
4
+ """
5
+
6
+ class K2ThinkProxyError(Exception):
7
+ """K2Think代理服务基础异常类"""
8
+ def __init__(self, message: str, error_type: str = "api_error", status_code: int = 500):
9
+ self.message = message
10
+ self.error_type = error_type
11
+ self.status_code = status_code
12
+ super().__init__(self.message)
13
+
14
+ class ConfigurationError(K2ThinkProxyError):
15
+ """配置错误异常"""
16
+ def __init__(self, message: str):
17
+ super().__init__(message, "configuration_error", 500)
18
+
19
+ class AuthenticationError(K2ThinkProxyError):
20
+ """认证错误异常"""
21
+ def __init__(self, message: str = "Invalid API key provided"):
22
+ super().__init__(message, "authentication_error", 401)
23
+
24
+ class UpstreamError(K2ThinkProxyError):
25
+ """上游服务错误异常"""
26
+ def __init__(self, message: str, status_code: int = 502):
27
+ super().__init__(message, "upstream_error", status_code)
28
+
29
+ class TimeoutError(K2ThinkProxyError):
30
+ """超时错误异常"""
31
+ def __init__(self, message: str = "请求超时"):
32
+ super().__init__(message, "timeout_error", 504)
33
+
34
+ class SerializationError(K2ThinkProxyError):
35
+ """序列化错误异常"""
36
+ def __init__(self, message: str = "请求数据序列化失败"):
37
+ super().__init__(message, "serialization_error", 400)
38
+
39
+ class ToolProcessingError(K2ThinkProxyError):
40
+ """工具处理错误异常"""
41
+ def __init__(self, message: str):
42
+ super().__init__(message, "tool_processing_error", 400)
43
+
44
+ class ContentProcessingError(K2ThinkProxyError):
45
+ """内容处理错误异常"""
46
+ def __init__(self, message: str):
47
+ super().__init__(message, "content_processing_error", 400)
src/models.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 数据模型定义
3
+ 定义所有API请求和响应的数据模型
4
+ """
5
+ from pydantic import BaseModel
6
+ from typing import List, Dict, Optional, Union
7
+
8
+ class ImageUrl(BaseModel):
9
+ """Image URL model for vision content"""
10
+ url: str
11
+ detail: Optional[str] = "auto"
12
+
13
+ class ContentPart(BaseModel):
14
+ """Content part model for OpenAI's new content format"""
15
+ type: str
16
+ text: Optional[str] = None
17
+ image_url: Optional[ImageUrl] = None
18
+
19
+ class Message(BaseModel):
20
+ role: str
21
+ content: Optional[Union[str, List[ContentPart]]] = None
22
+ tool_calls: Optional[List[Dict]] = None
23
+
24
+ class ChatCompletionRequest(BaseModel):
25
+ model: str = "MBZUAI-IFM/K2-Think"
26
+ messages: List[Message]
27
+ stream: bool = False
28
+ temperature: float = 0.7
29
+ max_tokens: Optional[int] = None
30
+ top_p: Optional[float] = None
31
+ frequency_penalty: Optional[float] = None
32
+ presence_penalty: Optional[float] = None
33
+ stop: Optional[Union[str, List[str]]] = None
34
+ tools: Optional[List[Dict]] = None
35
+ tool_choice: Optional[Union[str, Dict]] = None
36
+
37
+ class ModelInfo(BaseModel):
38
+ id: str
39
+ object: str = "model"
40
+ created: int
41
+ owned_by: str
42
+ permission: List[Dict] = []
43
+ root: str
44
+ parent: Optional[str] = None
45
+
46
+ class ModelsResponse(BaseModel):
47
+ object: str = "list"
48
+ data: List[ModelInfo]
src/response_processor.py ADDED
@@ -0,0 +1,446 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 响应处理模块
3
+ 处理流式和非流式响应的所有逻辑
4
+ """
5
+ import json
6
+ import time
7
+ import asyncio
8
+ import logging
9
+ import uuid
10
+ from datetime import datetime
11
+ from typing import Dict, AsyncGenerator, Tuple, Optional
12
+ import pytz
13
+ import httpx
14
+
15
+ from src.constants import (
16
+ ToolConstants,APIConstants, ResponseConstants, ContentConstants,
17
+ NumericConstants, TimeConstants, HeaderConstants
18
+ )
19
+ from src.exceptions import UpstreamError, TimeoutError as ProxyTimeoutError
20
+ from src.tool_handler import ToolHandler
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+ class ResponseProcessor:
25
+ """响应处理器"""
26
+
27
+ def __init__(self, config, tool_handler: ToolHandler):
28
+ self.config = config
29
+ self.tool_handler = tool_handler
30
+
31
+ def extract_answer_content(self, full_content: str, output_thinking: bool = True) -> str:
32
+ """删除第一个<answer>标签和最后一个</answer>标签,保留内容"""
33
+ if not full_content:
34
+ return full_content
35
+
36
+ # 完全通过模型名控制思考内容输出,默认显示思考内容
37
+ should_output_thinking = output_thinking
38
+
39
+ if should_output_thinking:
40
+ # 删除第一个<answer>
41
+ answer_start = full_content.find(ContentConstants.ANSWER_START_TAG)
42
+ if answer_start != -1:
43
+ full_content = full_content[:answer_start] + full_content[answer_start + len(ContentConstants.ANSWER_START_TAG):]
44
+
45
+ # 删除最后一个</answer>
46
+ answer_end = full_content.rfind(ContentConstants.ANSWER_END_TAG)
47
+ if answer_end != -1:
48
+ full_content = full_content[:answer_end] + full_content[answer_end + len(ContentConstants.ANSWER_END_TAG):]
49
+
50
+ return full_content.strip()
51
+ else:
52
+ # 删除<think>部分(包括标签)
53
+ think_start = full_content.find(ContentConstants.THINK_START_TAG)
54
+ think_end = full_content.find(ContentConstants.THINK_END_TAG)
55
+ if think_start != -1 and think_end != -1:
56
+ full_content = full_content[:think_start] + full_content[think_end + len(ContentConstants.THINK_END_TAG):]
57
+
58
+ # 删除<answer>标签及其内容之外的部分
59
+ answer_start = full_content.find(ContentConstants.ANSWER_START_TAG)
60
+ answer_end = full_content.rfind(ContentConstants.ANSWER_END_TAG)
61
+ if answer_start != -1 and answer_end != -1:
62
+ content = full_content[answer_start + len(ContentConstants.ANSWER_START_TAG):answer_end]
63
+ return content.strip()
64
+
65
+ return full_content.strip()
66
+
67
+ def calculate_dynamic_chunk_size(self, content_length: int) -> int:
68
+ """
69
+ 动态计算流式输出的chunk大小
70
+ 确保总输出时间不超过MAX_STREAM_TIME秒
71
+
72
+ Args:
73
+ content_length: 待输出内容的总长度
74
+
75
+ Returns:
76
+ int: 动态计算的chunk大小,最小为50
77
+ """
78
+ if content_length <= 0:
79
+ return self.config.STREAM_CHUNK_SIZE
80
+
81
+ # 计算需要的总chunk数量以满足时间限制
82
+ # 总时间 = chunk数量 * STREAM_DELAY
83
+ # chunk数量 = content_length / chunk_size
84
+ # 所以:总时间 = (content_length / chunk_size) * STREAM_DELAY
85
+ # 解出:chunk_size = (content_length * STREAM_DELAY) / MAX_STREAM_TIME
86
+
87
+ calculated_chunk_size = int((content_length * self.config.STREAM_DELAY) / self.config.MAX_STREAM_TIME)
88
+
89
+ # 确保chunk_size不小于最小值
90
+ dynamic_chunk_size = max(calculated_chunk_size, NumericConstants.MIN_CHUNK_SIZE)
91
+
92
+ # 如果计算出的chunk_size太大(比如内容很短),使用默认值
93
+ if dynamic_chunk_size > content_length:
94
+ dynamic_chunk_size = min(self.config.STREAM_CHUNK_SIZE, content_length)
95
+
96
+ logger.debug(f"动态chunk_size计算: 内容长度={content_length}, 计算值={calculated_chunk_size}, 最终值={dynamic_chunk_size}")
97
+
98
+ return dynamic_chunk_size
99
+
100
+ def content_to_multimodal(self, content) -> str | list[dict]:
101
+ """将内容转换为多模态格式用于K2Think API"""
102
+ if content is None:
103
+ return ""
104
+ if isinstance(content, str):
105
+ return content
106
+ if isinstance(content, list):
107
+ # 检查是否包含图像内容
108
+ has_image = False
109
+ result_parts = []
110
+
111
+ for p in content:
112
+ if hasattr(p, 'type'): # ContentPart object
113
+ if getattr(p, 'type') == ContentConstants.TEXT_TYPE and getattr(p, 'text', None):
114
+ result_parts.append({
115
+ "type": ContentConstants.TEXT_TYPE,
116
+ "text": getattr(p, 'text')
117
+ })
118
+ elif getattr(p, 'type') == ContentConstants.IMAGE_URL_TYPE and getattr(p, 'image_url', None):
119
+ has_image = True
120
+ image_url_obj = getattr(p, 'image_url')
121
+ if hasattr(image_url_obj, 'url'):
122
+ url = getattr(image_url_obj, 'url')
123
+ else:
124
+ url = image_url_obj.get('url') if isinstance(image_url_obj, dict) else str(image_url_obj)
125
+
126
+ result_parts.append({
127
+ "type": ContentConstants.IMAGE_URL_TYPE,
128
+ "image_url": {
129
+ "url": url
130
+ }
131
+ })
132
+ elif isinstance(p, dict):
133
+ if p.get("type") == ContentConstants.TEXT_TYPE and p.get("text"):
134
+ result_parts.append({
135
+ "type": ContentConstants.TEXT_TYPE,
136
+ "text": p.get("text")
137
+ })
138
+ elif p.get("type") == ContentConstants.IMAGE_URL_TYPE and p.get("image_url"):
139
+ has_image = True
140
+ result_parts.append({
141
+ "type": ContentConstants.IMAGE_URL_TYPE,
142
+ "image_url": p.get("image_url")
143
+ })
144
+ elif isinstance(p, str):
145
+ result_parts.append({
146
+ "type": ContentConstants.TEXT_TYPE,
147
+ "text": p
148
+ })
149
+
150
+ # 如果包含图像,返回多模态格式;否则返回纯文本
151
+ if has_image and result_parts:
152
+ return result_parts
153
+ else:
154
+ # 提取所有文本内容
155
+ text_parts = []
156
+ for part in result_parts:
157
+ if part.get("type") == ContentConstants.TEXT_TYPE:
158
+ text_parts.append(part.get("text", ""))
159
+ return " ".join(text_parts)
160
+
161
+ # 处理其他类型
162
+ try:
163
+ return str(content)
164
+ except:
165
+ return ""
166
+
167
+ def get_current_datetime_info(self) -> Dict[str, str]:
168
+ """获取当前时间信息"""
169
+ # 设置时区为上海
170
+ tz = pytz.timezone(ContentConstants.DEFAULT_TIMEZONE)
171
+ now = datetime.now(tz)
172
+
173
+ return {
174
+ "{{USER_NAME}}": ContentConstants.DEFAULT_USER_NAME,
175
+ "{{USER_LOCATION}}": ContentConstants.DEFAULT_USER_LOCATION,
176
+ "{{CURRENT_DATETIME}}": now.strftime(TimeConstants.DATETIME_FORMAT),
177
+ "{{CURRENT_DATE}}": now.strftime(TimeConstants.DATE_FORMAT),
178
+ "{{CURRENT_TIME}}": now.strftime(TimeConstants.TIME_FORMAT),
179
+ "{{CURRENT_WEEKDAY}}": now.strftime(TimeConstants.WEEKDAY_FORMAT),
180
+ "{{CURRENT_TIMEZONE}}": ContentConstants.DEFAULT_TIMEZONE,
181
+ "{{USER_LANGUAGE}}": ContentConstants.DEFAULT_USER_LANGUAGE
182
+ }
183
+
184
+ def generate_session_id(self) -> str:
185
+ """生成会话ID"""
186
+ return str(uuid.uuid4())
187
+
188
+ def generate_chat_id(self) -> str:
189
+ """生成聊天ID"""
190
+ return str(uuid.uuid4())
191
+
192
+ async def create_http_client(self) -> httpx.AsyncClient:
193
+ """创建HTTP客户端"""
194
+ base_kwargs = {
195
+ "timeout": httpx.Timeout(timeout=None, connect=10.0),
196
+ "limits": httpx.Limits(
197
+ max_keepalive_connections=self.config.MAX_KEEPALIVE_CONNECTIONS,
198
+ max_connections=self.config.MAX_CONNECTIONS
199
+ ),
200
+ "follow_redirects": True
201
+ }
202
+
203
+ try:
204
+ return httpx.AsyncClient(**base_kwargs)
205
+ except Exception as e:
206
+ logger.error(f"创建客户端失败: {e}")
207
+ raise e
208
+
209
+ async def make_request(
210
+ self,
211
+ method: str,
212
+ url: str,
213
+ headers: dict,
214
+ json_data: dict = None,
215
+ stream: bool = False
216
+ ) -> httpx.Response:
217
+ """发送HTTP请求"""
218
+ client = None
219
+
220
+ try:
221
+ client = await self.create_http_client()
222
+
223
+ if stream:
224
+ # 流式请求返回context manager
225
+ return client.stream(method, url, headers=headers, json=json_data, timeout=None)
226
+ else:
227
+ response = await client.request(
228
+ method, url, headers=headers, json=json_data,
229
+ timeout=self.config.REQUEST_TIMEOUT
230
+ )
231
+
232
+ # 详细记录非200响应
233
+ if response.status_code != APIConstants.HTTP_OK:
234
+ logger.error(f"上游API返回错误状态码: {response.status_code}")
235
+ logger.error(f"响应头: {dict(response.headers)}")
236
+ try:
237
+ error_body = response.text
238
+ logger.error(f"错误响应体: {error_body}")
239
+ except:
240
+ logger.error("无法读取错误响应体")
241
+
242
+ response.raise_for_status()
243
+ return response
244
+
245
+ except httpx.HTTPStatusError as e:
246
+ logger.error(f"HTTP状态错误: {e.response.status_code} - {e.response.text}")
247
+ if client and not stream:
248
+ await client.aclose()
249
+ raise UpstreamError(f"上游服务错误: {e.response.status_code}", e.response.status_code)
250
+ except httpx.TimeoutException as e:
251
+ logger.error(f"请求超时: {e}")
252
+ if client and not stream:
253
+ await client.aclose()
254
+ raise ProxyTimeoutError("请求超时")
255
+ except Exception as e:
256
+ logger.error(f"请求异常: {e}")
257
+ if client and not stream:
258
+ await client.aclose()
259
+ raise e
260
+
261
+ async def process_non_stream_response(self, k2think_payload: dict, headers: dict, output_thinking: bool = None) -> Tuple[str, dict]:
262
+ """处理非流式响应"""
263
+ try:
264
+ response = await self.make_request(
265
+ "POST",
266
+ self.config.K2THINK_API_URL,
267
+ headers,
268
+ k2think_payload,
269
+ stream=False
270
+ )
271
+
272
+ # K2Think 非流式请求返回标准JSON格式
273
+ result = response.json()
274
+
275
+ # 提取内容
276
+ full_content = ""
277
+ if result.get('choices') and len(result['choices']) > 0:
278
+ choice = result['choices'][0]
279
+ if choice.get('message') and choice['message'].get('content'):
280
+ raw_content = choice['message']['content']
281
+ # 提取<answer>标签中的内容,去除标签
282
+ full_content = self.extract_answer_content(raw_content, output_thinking)
283
+
284
+ # 提取token信息
285
+ token_info = result.get('usage', {
286
+ "prompt_tokens": NumericConstants.DEFAULT_PROMPT_TOKENS,
287
+ "completion_tokens": NumericConstants.DEFAULT_COMPLETION_TOKENS,
288
+ "total_tokens": NumericConstants.DEFAULT_TOTAL_TOKENS
289
+ })
290
+
291
+ await response.aclose()
292
+ return full_content, token_info
293
+
294
+ except Exception as e:
295
+ logger.error(f"处理非流式响应错误: {e}")
296
+ raise
297
+
298
+ async def process_stream_response_with_tools(
299
+ self,
300
+ k2think_payload: dict,
301
+ headers: dict,
302
+ has_tools: bool = False,
303
+ output_thinking: bool = None,
304
+ original_model: str = None
305
+ ) -> AsyncGenerator[str, None]:
306
+ """处理流式响应 - 支持工具调用,优化性能"""
307
+ try:
308
+ # 发送开始chunk
309
+ start_chunk = self._create_chunk_data(
310
+ delta={"role": "assistant", "content": ""},
311
+ finish_reason=None,
312
+ model=original_model
313
+ )
314
+ yield f"{ResponseConstants.STREAM_DATA_PREFIX}{json.dumps(start_chunk)}\n\n"
315
+
316
+ # 优化的模拟流式输出 - 立即开始获取响应并流式发送
317
+ k2think_payload_copy = k2think_payload.copy()
318
+ k2think_payload_copy["stream"] = False
319
+
320
+ headers_copy = headers.copy()
321
+ headers_copy[HeaderConstants.ACCEPT] = HeaderConstants.APPLICATION_JSON
322
+
323
+ # 获取完整响应
324
+ full_content, token_info = await self.process_non_stream_response(k2think_payload_copy, headers_copy, output_thinking)
325
+
326
+ if not full_content:
327
+ yield ResponseConstants.STREAM_DONE_MARKER
328
+ return
329
+
330
+ # 处理工具调用的流式响应
331
+ finish_reason = ResponseConstants.FINISH_REASON_STOP
332
+ if has_tools:
333
+ tool_calls = self.tool_handler.extract_tool_invocations(full_content)
334
+ if tool_calls:
335
+ # 发送工具调用
336
+ for i, tc in enumerate(tool_calls):
337
+ tool_call_delta = {
338
+ "index": i,
339
+ "id": tc.get("id"),
340
+ "type": tc.get("type", ToolConstants.FUNCTION_TYPE),
341
+ "function": tc.get("function", {}),
342
+ }
343
+
344
+ tool_chunk = self._create_chunk_data(
345
+ delta={"tool_calls": [tool_call_delta]},
346
+ finish_reason=None,
347
+ model=original_model
348
+ )
349
+ yield f"{ResponseConstants.STREAM_DATA_PREFIX}{json.dumps(tool_chunk)}\n\n"
350
+
351
+ finish_reason = ResponseConstants.FINISH_REASON_TOOL_CALLS
352
+ else:
353
+ # 发送常规内容
354
+ trimmed_content = self.tool_handler.remove_tool_json_content(full_content)
355
+ if trimmed_content:
356
+ async for chunk in self._stream_content(trimmed_content, original_model):
357
+ yield chunk
358
+ else:
359
+ # 无工具 - 发送常规内容
360
+ async for chunk in self._stream_content(full_content, original_model):
361
+ yield chunk
362
+
363
+ # 发送结束chunk
364
+ end_chunk = self._create_chunk_data(
365
+ delta={},
366
+ finish_reason=finish_reason,
367
+ model=original_model
368
+ )
369
+ yield f"{ResponseConstants.STREAM_DATA_PREFIX}{json.dumps(end_chunk)}\n\n"
370
+ yield ResponseConstants.STREAM_DONE_MARKER
371
+
372
+ except Exception as e:
373
+ logger.error(f"流式响应处理错误: {e}")
374
+ error_chunk = self._create_chunk_data(
375
+ delta={},
376
+ finish_reason=ResponseConstants.FINISH_REASON_ERROR,
377
+ model=original_model
378
+ )
379
+ yield f"{ResponseConstants.STREAM_DATA_PREFIX}{json.dumps(error_chunk)}\n\n"
380
+ yield ResponseConstants.STREAM_DONE_MARKER
381
+
382
+ async def _stream_content(self, content: str, model: str = None) -> AsyncGenerator[str, None]:
383
+ """流式发送内容"""
384
+ chunk_size = self.calculate_dynamic_chunk_size(len(content))
385
+
386
+ for i in range(0, len(content), chunk_size):
387
+ chunk_content = content[i:i + chunk_size]
388
+
389
+ chunk = self._create_chunk_data(
390
+ delta={"content": chunk_content},
391
+ finish_reason=None,
392
+ model=model
393
+ )
394
+
395
+ yield f"{ResponseConstants.STREAM_DATA_PREFIX}{json.dumps(chunk)}\n\n"
396
+ # 添加延迟模拟真实流式效果
397
+ await asyncio.sleep(self.config.STREAM_DELAY)
398
+
399
+ def _create_chunk_data(self, delta: dict, finish_reason: Optional[str], model: str = None) -> dict:
400
+ """创建流式响应chunk数据"""
401
+ return {
402
+ "id": f"chatcmpl-{int(time.time() * 1000)}",
403
+ "object": ResponseConstants.CHAT_COMPLETION_CHUNK_OBJECT,
404
+ "created": int(time.time()),
405
+ "model": model or APIConstants.MODEL_ID,
406
+ "choices": [{
407
+ "index": 0,
408
+ "delta": delta,
409
+ "finish_reason": finish_reason
410
+ }]
411
+ }
412
+
413
+ def create_completion_response(
414
+ self,
415
+ content: Optional[str],
416
+ tool_calls: Optional[list] = None,
417
+ token_info: Optional[dict] = None,
418
+ model: str = None
419
+ ) -> dict:
420
+ """创建完整的聊天补全响应"""
421
+ finish_reason = ResponseConstants.FINISH_REASON_TOOL_CALLS if tool_calls else ResponseConstants.FINISH_REASON_STOP
422
+
423
+ message = {
424
+ "role": "assistant",
425
+ "content": content,
426
+ }
427
+
428
+ if tool_calls:
429
+ message["tool_calls"] = tool_calls
430
+
431
+ return {
432
+ "id": f"chatcmpl-{int(time.time())}",
433
+ "object": ResponseConstants.CHAT_COMPLETION_OBJECT,
434
+ "created": int(time.time()),
435
+ "model": model or APIConstants.MODEL_ID,
436
+ "choices": [{
437
+ "index": 0,
438
+ "message": message,
439
+ "finish_reason": finish_reason
440
+ }],
441
+ "usage": token_info or {
442
+ "prompt_tokens": NumericConstants.DEFAULT_PROMPT_TOKENS,
443
+ "completion_tokens": NumericConstants.DEFAULT_COMPLETION_TOKENS,
444
+ "total_tokens": NumericConstants.DEFAULT_TOTAL_TOKENS
445
+ }
446
+ }
src/tool_handler.py ADDED
@@ -0,0 +1,368 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 工具处理模块
3
+ 处理工具调用相关的所有逻辑
4
+ """
5
+ import json
6
+ import re
7
+ import time
8
+ import logging
9
+ from typing import List, Dict, Optional, Union
10
+
11
+ from src.constants import (
12
+ ToolConstants, ContentConstants, LogMessages,
13
+ TimeConstants
14
+ )
15
+ from src.exceptions import ToolProcessingError
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+ class ToolHandler:
20
+ """工具调用处理器"""
21
+
22
+ # 工具调用提取模式
23
+ TOOL_CALL_FENCE_PATTERN = re.compile(r"```json\s*(\{.*?\})\s*```", re.DOTALL)
24
+ FUNCTION_CALL_PATTERN = re.compile(
25
+ r"调用函数\s*[::]\s*([\w\-\.]+)\s*(?:参数|arguments)[::]\s*(\{.*?\})",
26
+ re.DOTALL
27
+ )
28
+
29
+ def __init__(self, config):
30
+ self.config = config
31
+ self.scan_limit = config.SCAN_LIMIT
32
+ self.system_message_length = config.SYSTEM_MESSAGE_LENGTH
33
+ self.tool_support = config.TOOL_SUPPORT
34
+
35
+ def generate_tool_prompt(self, tools: List[Dict]) -> str:
36
+ """生成简洁的工具注入提示"""
37
+ if not tools:
38
+ return ""
39
+
40
+ tool_definitions = []
41
+ for tool in tools:
42
+ if tool.get("type") != ToolConstants.FUNCTION_TYPE:
43
+ continue
44
+
45
+ function_spec = tool.get("function", {}) or {}
46
+ function_name = function_spec.get("name", "unknown")
47
+ function_description = function_spec.get("description", "")
48
+ parameters = function_spec.get("parameters", {}) or {}
49
+
50
+ # 创建简洁的工具定义
51
+ tool_info = f"{function_name}: {function_description}"
52
+
53
+ # 添加简化的参数信息
54
+ parameter_properties = parameters.get("properties", {}) or {}
55
+ required_parameters = set(parameters.get("required", []) or [])
56
+
57
+ if parameter_properties:
58
+ param_list = []
59
+ for param_name, param_details in parameter_properties.items():
60
+ param_desc = (param_details or {}).get("description", "")
61
+ is_required = param_name in required_parameters
62
+ param_list.append(f"{param_name}{'*' if is_required else ''}: {param_desc}")
63
+ tool_info += f" Parameters: {', '.join(param_list)}"
64
+
65
+ tool_definitions.append(tool_info)
66
+
67
+ if not tool_definitions:
68
+ return ""
69
+
70
+ # 构建简洁的工具提示
71
+ prompt_template = (
72
+ f"\n\nAvailable tools: {'; '.join(tool_definitions)}. "
73
+ "To use a tool, respond with JSON: "
74
+ '{"tool_calls":[{"id":"call_xxx","type":"function","function":{"name":"tool_name","arguments":"{\\"param\\":\\"value\\"}"}}]}'
75
+ )
76
+
77
+ return prompt_template
78
+
79
+ def process_messages_with_tools(
80
+ self,
81
+ messages: List[Dict],
82
+ tools: Optional[List[Dict]] = None,
83
+ tool_choice: Optional[Union[str, Dict]] = None
84
+ ) -> List[Dict]:
85
+ """处理消息并注入工具提示"""
86
+ if not tools or not self.tool_support or (tool_choice == "none"):
87
+ # 如果没有工具或禁用工具,直接返回原消息
88
+ return [dict(m) for m in messages]
89
+
90
+ tools_prompt = self.generate_tool_prompt(tools)
91
+
92
+ # 限制工具提示长度,避免过长导致上游API拒绝
93
+ if len(tools_prompt) > ToolConstants.MAX_TOOL_PROMPT_LENGTH:
94
+ logger.warning(LogMessages.TOOL_PROMPT_TOO_LONG.format(len(tools_prompt)))
95
+ tools_prompt = tools_prompt[:ToolConstants.MAX_TOOL_PROMPT_LENGTH] + ToolConstants.TOOL_PROMPT_TRUNCATE_SUFFIX
96
+
97
+ processed = []
98
+ has_system = any(m.get("role") == "system" for m in messages)
99
+
100
+ if has_system:
101
+ # 如果已有系统消息,在第一个系统消息中添加工具提示
102
+ for m in messages:
103
+ if m.get("role") == "system":
104
+ mm = dict(m)
105
+ content = self._content_to_string(mm.get("content", ""))
106
+ # 确保系统消息不会过长
107
+ new_content = content + tools_prompt
108
+ if len(new_content) > self.system_message_length:
109
+ logger.warning(LogMessages.SYSTEM_MESSAGE_TOO_LONG.format(len(new_content)))
110
+ mm["content"] = "你是一个有用的助手。" + tools_prompt
111
+ else:
112
+ mm["content"] = new_content
113
+ processed.append(mm)
114
+ # 只在第一个系统消息中添加工具提示
115
+ tools_prompt = ""
116
+ else:
117
+ processed.append(dict(m))
118
+ else:
119
+ # 如果没有系统消息,需要添加一个,但只有当确实需要工具时
120
+ if tools_prompt.strip():
121
+ processed = [{"role": "system", "content": "你���一个有用的助手。" + tools_prompt}]
122
+ processed.extend([dict(m) for m in messages])
123
+ else:
124
+ processed = [dict(m) for m in messages]
125
+
126
+ # 添加简化的工具选择提示
127
+ if tool_choice == "required":
128
+ if processed and processed[-1].get("role") == "user":
129
+ last = processed[-1]
130
+ content = self._content_to_string(last.get("content", ""))
131
+ last["content"] = content + "\n请使用工具来处理这个请求。"
132
+ elif isinstance(tool_choice, dict) and tool_choice.get("type") == ToolConstants.FUNCTION_TYPE:
133
+ fname = (tool_choice.get("function") or {}).get("name")
134
+ if fname and processed and processed[-1].get("role") == "user":
135
+ last = processed[-1]
136
+ content = self._content_to_string(last.get("content", ""))
137
+ last["content"] = content + f"\n请使用 {fname} 工具。"
138
+
139
+ # 处理工具/函数消息
140
+ final_msgs = []
141
+ for m in processed:
142
+ role = m.get("role")
143
+ if role in ("tool", "function"):
144
+ tool_name = m.get("name", "unknown")
145
+ tool_content = self._content_to_string(m.get("content", ""))
146
+ if isinstance(tool_content, dict):
147
+ tool_content = json.dumps(tool_content, ensure_ascii=False)
148
+
149
+ # 简化工具结果消息
150
+ content = f"工具 {tool_name} 结果: {tool_content}"
151
+ if not content.strip():
152
+ content = f"工具 {tool_name} 执行完成"
153
+
154
+ final_msgs.append({
155
+ "role": "assistant",
156
+ "content": content,
157
+ })
158
+ else:
159
+ # 对于常规消息,确保内容是字符串格式
160
+ final_msg = dict(m)
161
+ content = self._content_to_string(final_msg.get("content", ""))
162
+ final_msg["content"] = content
163
+ final_msgs.append(final_msg)
164
+
165
+ return final_msgs
166
+
167
+ def extract_tool_invocations(self, text: str) -> Optional[List[Dict]]:
168
+ """从响应文本中提取工具调用"""
169
+ if not text:
170
+ return None
171
+
172
+ # 限制扫描大小以提高性能
173
+ scannable_text = text[:self.scan_limit]
174
+
175
+ # 尝试1:从JSON代码块中提取
176
+ json_blocks = self.TOOL_CALL_FENCE_PATTERN.findall(scannable_text)
177
+ for json_block in json_blocks:
178
+ try:
179
+ parsed_data = json.loads(json_block)
180
+ tool_calls = parsed_data.get("tool_calls")
181
+ if tool_calls and isinstance(tool_calls, list):
182
+ # 确保arguments字段是字符串
183
+ self._normalize_tool_calls(tool_calls)
184
+ return tool_calls
185
+ except (json.JSONDecodeError, AttributeError):
186
+ continue
187
+
188
+ # 尝试2:使用括号平衡方法提取内联JSON对象
189
+ tool_calls = self._extract_inline_json_tool_calls(scannable_text)
190
+ if tool_calls:
191
+ return tool_calls
192
+
193
+ # 尝试3:解析自然语言函数调用
194
+ natural_lang_match = self.FUNCTION_CALL_PATTERN.search(scannable_text)
195
+ if natural_lang_match:
196
+ function_name = natural_lang_match.group(1).strip()
197
+ arguments_str = natural_lang_match.group(2).strip()
198
+ try:
199
+ # 验证JSON格式
200
+ json.loads(arguments_str)
201
+ return [
202
+ {
203
+ "id": f"{ToolConstants.CALL_ID_PREFIX}{int(time.time() * TimeConstants.MICROSECONDS_MULTIPLIER)}",
204
+ "type": ToolConstants.FUNCTION_TYPE,
205
+ "function": {"name": function_name, "arguments": arguments_str},
206
+ }
207
+ ]
208
+ except json.JSONDecodeError:
209
+ return None
210
+
211
+ return None
212
+
213
+ def remove_tool_json_content(self, text: str) -> str:
214
+ """从响应文本中移除工具JSON内容 - 使用括号平衡方法"""
215
+
216
+ def remove_tool_call_block(match: re.Match) -> str:
217
+ json_content = match.group(1)
218
+ try:
219
+ parsed_data = json.loads(json_content)
220
+ if "tool_calls" in parsed_data:
221
+ return ""
222
+ except (json.JSONDecodeError, AttributeError):
223
+ pass
224
+ return match.group(0)
225
+
226
+ # 步骤1:移除围栏工具JSON块
227
+ cleaned_text = self.TOOL_CALL_FENCE_PATTERN.sub(remove_tool_call_block, text)
228
+
229
+ # 步骤2:移除内联工具JSON - 使用基于括号平衡的智能方法
230
+ result = []
231
+ i = 0
232
+ while i < len(cleaned_text):
233
+ if cleaned_text[i] == '{':
234
+ # 尝试找到匹配的右括号
235
+ brace_count = 1
236
+ j = i + 1
237
+ in_string = False
238
+ escape_next = False
239
+
240
+ while j < len(cleaned_text) and brace_count > 0:
241
+ if escape_next:
242
+ escape_next = False
243
+ elif cleaned_text[j] == '\\':
244
+ escape_next = True
245
+ elif cleaned_text[j] == '"' and not escape_next:
246
+ in_string = not in_string
247
+ elif not in_string:
248
+ if cleaned_text[j] == '{':
249
+ brace_count += 1
250
+ elif cleaned_text[j] == '}':
251
+ brace_count -= 1
252
+ j += 1
253
+
254
+ if brace_count == 0:
255
+ # 找到了完整的JSON对象
256
+ json_str = cleaned_text[i:j]
257
+ try:
258
+ parsed = json.loads(json_str)
259
+ if "tool_calls" in parsed:
260
+ # 这是一个工具调用,跳过它
261
+ i = j
262
+ continue
263
+ except:
264
+ pass
265
+
266
+ # 不是工具调用或无法解析,保留这个字符
267
+ result.append(cleaned_text[i])
268
+ i += 1
269
+ else:
270
+ result.append(cleaned_text[i])
271
+ i += 1
272
+
273
+ return ''.join(result).strip()
274
+
275
+ def _extract_inline_json_tool_calls(self, text: str) -> Optional[List[Dict]]:
276
+ """使用括号平衡方法提取内联JSON工具调用"""
277
+ i = 0
278
+ while i < len(text):
279
+ if text[i] == '{':
280
+ # 尝试找到匹配的右括号
281
+ brace_count = 1
282
+ j = i + 1
283
+ in_string = False
284
+ escape_next = False
285
+
286
+ while j < len(text) and brace_count > 0:
287
+ if escape_next:
288
+ escape_next = False
289
+ elif text[j] == '\\':
290
+ escape_next = True
291
+ elif text[j] == '"' and not escape_next:
292
+ in_string = not in_string
293
+ elif not in_string:
294
+ if text[j] == '{':
295
+ brace_count += 1
296
+ elif text[j] == '}':
297
+ brace_count -= 1
298
+ j += 1
299
+
300
+ if brace_count == 0:
301
+ # 找到了完整的JSON对象
302
+ json_str = text[i:j]
303
+ try:
304
+ parsed_data = json.loads(json_str)
305
+ tool_calls = parsed_data.get("tool_calls")
306
+ if tool_calls and isinstance(tool_calls, list):
307
+ # 确保arguments字段是字符串
308
+ self._normalize_tool_calls(tool_calls)
309
+ return tool_calls
310
+ except (json.JSONDecodeError, AttributeError):
311
+ pass
312
+
313
+ i += 1
314
+ else:
315
+ i += 1
316
+
317
+ return None
318
+
319
+ def _normalize_tool_calls(self, tool_calls: List[Dict]) -> None:
320
+ """标准化工具调用,确保arguments字段是字符串"""
321
+ for tc in tool_calls:
322
+ if "function" in tc:
323
+ func = tc["function"]
324
+ if "arguments" in func:
325
+ if isinstance(func["arguments"], dict):
326
+ # 将字典转换为JSON字符串
327
+ func["arguments"] = json.dumps(func["arguments"], ensure_ascii=False)
328
+ elif not isinstance(func["arguments"], str):
329
+ func["arguments"] = json.dumps(func["arguments"], ensure_ascii=False)
330
+
331
+ def _content_to_string(self, content) -> str:
332
+ """将各种格式的内容转换为字符串"""
333
+ if content is None:
334
+ return ""
335
+ if isinstance(content, str):
336
+ return content
337
+ if isinstance(content, list):
338
+ parts = []
339
+ for p in content:
340
+ if hasattr(p, 'text'): # ContentPart object
341
+ if getattr(p, 'text', None):
342
+ parts.append(getattr(p, 'text', ''))
343
+ elif isinstance(p, dict):
344
+ if p.get("type") == ContentConstants.TEXT_TYPE:
345
+ parts.append(p.get("text", ""))
346
+ elif p.get("type") == ContentConstants.IMAGE_URL_TYPE:
347
+ # 处理图像内容,添加描述性文本
348
+ parts.append(ContentConstants.IMAGE_PLACEHOLDER)
349
+ elif isinstance(p, str):
350
+ parts.append(p)
351
+ else:
352
+ # 处理其他类型的对象
353
+ try:
354
+ if hasattr(p, '__dict__'):
355
+ # 如果是对象,尝试获取text属性或转换为字符串
356
+ text_attr = getattr(p, 'text', None)
357
+ if text_attr:
358
+ parts.append(str(text_attr))
359
+ else:
360
+ parts.append(str(p))
361
+ except:
362
+ continue
363
+ return " ".join(parts)
364
+ # 处理其他类型
365
+ try:
366
+ return str(content)
367
+ except:
368
+ return ""