| """Claude API 客户端""" |
| import json |
| from typing import AsyncGenerator |
| from app.utils.logger import logger |
| from .base_client import BaseClient |
|
|
|
|
| class ClaudeClient(BaseClient): |
| def __init__(self, api_key: str, api_url: str = "https://api.anthropic.com/v1/messages", provider: str = "anthropic"): |
| """初始化 Claude 客户端 |
| |
| Args: |
| api_key: Claude API密钥 |
| api_url: Claude API地址 |
| is_openrouter: 是否使用 OpenRouter API |
| """ |
| super().__init__(api_key, api_url) |
| self.provider = provider |
|
|
| async def stream_chat( |
| self, |
| messages: list, |
| model_arg: tuple[float, float, float, float], |
| model: str, |
| stream: bool = True |
| ) -> AsyncGenerator[tuple[str, str], None]: |
| """流式或非流式对话 |
| |
| Args: |
| messages: 消息列表 |
| model_arg: 模型参数元组[temperature, top_p, presence_penalty, frequency_penalty] |
| model: 模型名称。如果是 OpenRouter, 会自动转换为 'anthropic/claude-3.5-sonnet' 格式 |
| stream: 是否使用流式输出,默认为 True |
| |
| Yields: |
| tuple[str, str]: (内容类型, 内容) |
| 内容类型: "answer" |
| 内容: 实际的文本内容 |
| """ |
|
|
| if self.provider == "openrouter": |
| |
| model = "anthropic/claude-3.5-sonnet" |
| |
| headers = { |
| "Authorization": f"Bearer {self.api_key}", |
| "Content-Type": "application/json", |
| "HTTP-Referer": "https://github.com/ErlichLiu/DeepClaude", |
| "X-Title": "DeepClaude" |
| } |
|
|
| data = { |
| "model": model, |
| "messages": messages, |
| "stream": stream, |
| "temperature": 1 if model_arg[0] < 0 or model_arg[0] > 1 else model_arg[0], |
| "top_p": model_arg[1], |
| "presence_penalty": model_arg[2], |
| "frequency_penalty": model_arg[3] |
| } |
| elif self.provider == "oneapi": |
| headers = { |
| "Authorization": f"Bearer {self.api_key}", |
| "Content-Type": "application/json" |
| } |
|
|
| data = { |
| "model": model, |
| "messages": messages, |
| "stream": stream, |
| "temperature": 1 if model_arg[0] < 0 or model_arg[0] > 1 else model_arg[0], |
| "top_p": model_arg[1], |
| "presence_penalty": model_arg[2], |
| "frequency_penalty": model_arg[3] |
| } |
| elif self.provider == "anthropic": |
| headers = { |
| "x-api-key": self.api_key, |
| "anthropic-version": "2023-06-01", |
| "content-type": "application/json", |
| "accept": "text/event-stream" if stream else "application/json", |
| } |
|
|
| data = { |
| "model": model, |
| "messages": messages, |
| "max_tokens": 8192, |
| "stream": stream, |
| "temperature": 1 if model_arg[0] < 0 or model_arg[0] > 1 else model_arg[0], |
| "top_p": model_arg[1] |
| } |
| else: |
| raise ValueError(f"不支持的Claude Provider: {self.provider}") |
|
|
| logger.debug(f"开始对话:{data}") |
|
|
| if stream: |
| async for chunk in self._make_request(headers, data): |
| chunk_str = chunk.decode('utf-8') |
| if not chunk_str.strip(): |
| continue |
| |
| for line in chunk_str.split('\n'): |
| if line.startswith('data: '): |
| json_str = line[6:] |
| if json_str.strip() == '[DONE]': |
| return |
| |
| try: |
| data = json.loads(json_str) |
| if self.provider in ("openrouter", "oneapi"): |
| |
| content = data.get('choices', [{}])[0].get('delta', {}).get('content', '') |
| if content: |
| yield "answer", content |
| elif self.provider == "anthropic": |
| |
| if data.get('type') == 'content_block_delta': |
| content = data.get('delta', {}).get('text', '') |
| if content: |
| yield "answer", content |
| else: |
| raise ValueError(f"不支持的Claude Provider: {self.provider}") |
| except json.JSONDecodeError: |
| continue |
| else: |
| |
| async for chunk in self._make_request(headers, data): |
| try: |
| response = json.loads(chunk.decode('utf-8')) |
| if self.provider in ("openrouter", "oneapi"): |
| content = response.get('choices', [{}])[0].get('message', {}).get('content', '') |
| if content: |
| yield "answer", content |
| elif self.provider == "anthropic": |
| content = response.get('content', [{}])[0].get('text', '') |
| if content: |
| yield "answer", content |
| else: |
| raise ValueError(f"不支持的Claude Provider: {self.provider}") |
| except json.JSONDecodeError: |
| continue |
|
|