"""DeepClaude 服务,用于协调 DeepSeek 和 Claude API 的调用""" import json import time import tiktoken import asyncio from typing import AsyncGenerator from app.utils.logger import logger from app.clients import DeepSeekClient, ClaudeClient class DeepClaude: """处理 DeepSeek 和 Claude API 的流式输出衔接""" def __init__(self, deepseek_api_key: str, claude_api_key: str, deepseek_api_url: str = "https://api.deepseek.com/v1/chat/completions", claude_api_url: str = "https://api.anthropic.com/v1/messages", claude_provider: str = "anthropic", is_origin_reasoning: bool = True): """初始化 API 客户端 Args: deepseek_api_key: DeepSeek API密钥 claude_api_key: Claude API密钥 """ self.deepseek_client = DeepSeekClient(deepseek_api_key, deepseek_api_url) self.claude_client = ClaudeClient(claude_api_key, claude_api_url, claude_provider) self.is_origin_reasoning = is_origin_reasoning async def chat_completions_with_stream( self, messages: list, model_arg: tuple[float, float, float, float], deepseek_model: str = "deepseek-reasoner", claude_model: str = "claude-3-5-sonnet-20241022" ) -> AsyncGenerator[bytes, None]: """处理完整的流式输出过程 Args: messages: 初始消息列表 model_arg: 模型参数 deepseek_model: DeepSeek 模型名称 claude_model: Claude 模型名称 Yields: 字节流数据,格式如下: { "id": "chatcmpl-xxx", "object": "chat.completion.chunk", "created": timestamp, "model": model_name, "choices": [{ "index": 0, "delta": { "role": "assistant", "reasoning_content": reasoning_content, "content": content } }] } """ # 生成唯一的会话ID和时间戳 chat_id = f"chatcmpl-{hex(int(time.time() * 1000))[2:]}" created_time = int(time.time()) # 创建队列,用于收集输出数据 output_queue = asyncio.Queue() # 队列,用于传递 DeepSeek 推理内容给 Claude claude_queue = asyncio.Queue() # 用于存储 DeepSeek 的推理累积内容 reasoning_content = [] async def process_deepseek(): logger.info(f"开始处理 DeepSeek 流,使用模型:{deepseek_model}, 提供商: {self.deepseek_client.provider}") try: async for content_type, content in self.deepseek_client.stream_chat(messages, deepseek_model, self.is_origin_reasoning): if content_type == "reasoning": reasoning_content.append(content) response = { "id": chat_id, "object": "chat.completion.chunk", "created": created_time, "model": deepseek_model, "choices": [{ "index": 0, "delta": { "role": "assistant", "reasoning_content": content, "content": "" } }] } await output_queue.put(f"data: {json.dumps(response)}\n\n".encode('utf-8')) elif content_type == "content": # 当收到 content 类型时,将完整的推理内容发送到 claude_queue,并结束 DeepSeek 流处理 logger.info(f"DeepSeek 推理完成,收集到的推理内容长度:{len(''.join(reasoning_content))}") await claude_queue.put("".join(reasoning_content)) break except Exception as e: logger.error(f"处理 DeepSeek 流时发生错误: {e}") await claude_queue.put("") # 用 None 标记 DeepSeek 任务结束 logger.info("DeepSeek 任务处理完成,标记结束") await output_queue.put(None) async def process_claude(): try: logger.info("等待获取 DeepSeek 的推理内容...") reasoning = await claude_queue.get() logger.debug(f"获取到推理内容,内容长度:{len(reasoning) if reasoning else 0}") if not reasoning: logger.warning("未能获取到有效的推理内容,将使用默认提示继续") reasoning = "获取推理内容失败" # 构造 Claude 的输入消息 claude_messages = messages.copy() combined_content = f""" Here's my another model's reasoning process:\n{reasoning}\n\n Based on this reasoning, provide your response directly to me:""" # 改造最后一个消息对象,判断消息对象是 role = user,然后在这个对象的 content 后追加新的 String last_message = claude_messages[-1] if last_message.get("role", "") == "user": original_content = last_message["content"] fixed_content = f"Here's my original input:\n{original_content}\n\n{combined_content}" last_message["content"] = fixed_content # 处理可能 messages 内存在 role = system 的情况,如果有,则去掉当前这一条的消息对象 claude_messages = [message for message in claude_messages if message.get("role", "") != "system"] logger.info(f"开始处理 Claude 流,使用模型: {claude_model}, 提供商: {self.claude_client.provider}") async for content_type, content in self.claude_client.stream_chat( messages=claude_messages, model_arg=model_arg, model=claude_model, ): if content_type == "answer": response = { "id": chat_id, "object": "chat.completion.chunk", "created": created_time, "model": claude_model, "choices": [{ "index": 0, "delta": { "role": "assistant", "content": content } }] } await output_queue.put(f"data: {json.dumps(response)}\n\n".encode('utf-8')) except Exception as e: logger.error(f"处理 Claude 流时发生错误: {e}") # 用 None 标记 Claude 任务结束 logger.info("Claude 任务处理完成,标记结束") await output_queue.put(None) # 创建并发任务 deepseek_task = asyncio.create_task(process_deepseek()) claude_task = asyncio.create_task(process_claude()) # 等待两个任务完成,通过计数判断 finished_tasks = 0 while finished_tasks < 2: item = await output_queue.get() if item is None: finished_tasks += 1 else: yield item # 发送结束标记 yield b'data: [DONE]\n\n' async def chat_completions_without_stream( self, messages: list, model_arg: tuple[float, float, float, float], deepseek_model: str = "deepseek-reasoner", claude_model: str = "claude-3-5-sonnet-20241022" ) -> dict: """处理非流式输出过程 Args: messages: 初始消息列表 model_arg: 模型参数 deepseek_model: DeepSeek 模型名称 claude_model: Claude 模型名称 Returns: dict: OpenAI 格式的完整响应 """ chat_id = f"chatcmpl-{hex(int(time.time() * 1000))[2:]}" created_time = int(time.time()) reasoning_content = [] # 1. 获取 DeepSeek 的推理内容(仍然使用流式) try: async for content_type, content in self.deepseek_client.stream_chat(messages, deepseek_model, self.is_origin_reasoning): if content_type == "reasoning": reasoning_content.append(content) elif content_type == "content": break except Exception as e: logger.error(f"获取 DeepSeek 推理内容时发生错误: {e}") reasoning_content = ["获取推理内容失败"] # 2. 构造 Claude 的输入消息 reasoning = "".join(reasoning_content) claude_messages = messages.copy() combined_content = f""" Here's my another model's reasoning process:\n{reasoning}\n\n Based on this reasoning, provide your response directly to me:""" # 改造最后一个消息对象,判断消息对象是 role = user,然后在这个对象的 content 后追加新的 String last_message = claude_messages[-1] if last_message.get("role", "") == "user": original_content = last_message["content"] fixed_content = f"Here's my original input:\n{original_content}\n\n{combined_content}" last_message["content"] = fixed_content # 处理可能 messages 内存在 role = system 的情况 claude_messages = [message for message in claude_messages if message.get("role", "") != "system"] # 拼接所有 content 为一个字符串,计算 token token_content = "\n".join([message.get("content", "") for message in claude_messages]) encoding = tiktoken.encoding_for_model("gpt-4o") input_tokens = encoding.encode(token_content) logger.debug(f"输入 Tokens: {len(input_tokens)}") logger.debug("claude messages: " + str(claude_messages)) # 3. 获取 Claude 的非流式响应 try: answer = "" async for content_type, content in self.claude_client.stream_chat( messages=claude_messages, model_arg=model_arg, model=claude_model, stream=False ): if content_type == "answer": answer += content output_tokens = encoding.encode(answer) logger.debug(f"输出 Tokens: {len(output_tokens)}") # 4. 构造 OpenAI 格式的响应 return { "id": chat_id, "object": "chat.completion", "created": created_time, "model": claude_model, "choices": [{ "index": 0, "message": { "role": "assistant", "content": answer, "reasoning_content": reasoning }, "finish_reason": "stop" }], "usage": { "prompt_tokens": len(input_tokens), "completion_tokens": len(output_tokens), "total_tokens": len(input_tokens + output_tokens) } } except Exception as e: logger.error(f"获取 Claude 响应时发生错误: {e}") raise e