Skip to content

Commit

Permalink
支持非流式接口返回
Browse files Browse the repository at this point in the history
  • Loading branch information
ErlichLiu committed Feb 9, 2025
1 parent cd8c434 commit 469647f
Show file tree
Hide file tree
Showing 4 changed files with 190 additions and 51 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
<details>
<summary><strong>更新日志:</strong></summary>
<div>
2025-02-08.2: 支持非流式请求,支持 OpenAI 兼容的 models 接口返回。(⚠️ 当前暂未实现正确的 tokens 消耗统计,稍后更新)

2025-02-08.1: 添加 Github Actions,支持 fork 自动同步、支持自动构建 Docker 最新镜像、支持 docker-compose 部署

2025-02-07.2: 修复 Claude temperature 参数可能会超过范围导致的请求失败的 bug
Expand Down
89 changes: 53 additions & 36 deletions app/clients/claude_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,16 @@ async def stream_chat(
self,
messages: list,
model_arg: tuple[float, float, float, float],
model: str
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]: (内容类型, 内容)
Expand All @@ -37,7 +39,6 @@ async def stream_chat(
"""

if self.provider == "openrouter":
# logger.info("使用 OpenRouter API 作为 Claude 3.5 Sonnet 供应商 ")
# 转换模型名称为 OpenRouter 格式
model = "anthropic/claude-3.5-sonnet"

Expand All @@ -51,14 +52,13 @@ async def stream_chat(
data = {
"model": model, # OpenRouter 使用 anthropic/claude-3.5-sonnet 格式
"messages": messages,
"stream": True,
"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":
# logger.info("使用 OneAPI API 作为 Claude 3.5 Sonnet 供应商 ")
headers = {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json"
Expand All @@ -67,59 +67,76 @@ async def stream_chat(
data = {
"model": model,
"messages": messages,
"stream": True,
"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":
# logger.info("使用 Anthropic API 作为 Claude 3.5 Sonnet 供应商 ")
headers = {
"x-api-key": self.api_key,
"anthropic-version": "2023-06-01",
"content-type": "application/json",
"accept": "text/event-stream",
"accept": "text/event-stream" if stream else "application/json",
}

data = {
"model": model,
"messages": messages,
"max_tokens": 8192,
"stream": True,
"stream": stream,
"temperature": 1 if model_arg[0] < 0 or model_arg[0] > 1 else model_arg[0], # Claude仅支持temperature与top_p
"top_p": model_arg[1]
}
else:
raise ValueError(f"不支持的Claude Provider: {self.provider}")

logger.debug(f"开始流式对话{data}")
logger.debug(f"开始对话{data}")

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:] # 去掉 'data: ' 前缀
if json_str.strip() == '[DONE]':
return

try:
data = json.loads(json_str)
if self.provider in ("openrouter", "oneapi"):
# OpenRouter/OneApi 格式
content = data.get('choices', [{}])[0].get('delta', {}).get('content', '')
if content:
yield "answer", content
elif self.provider == "anthropic":
# Anthropic 格式
if data.get('type') == 'content_block_delta':
content = data.get('delta', {}).get('text', '')
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:] # 去掉 'data: ' 前缀
if json_str.strip() == '[DONE]':
return

try:
data = json.loads(json_str)
if self.provider in ("openrouter", "oneapi"):
# OpenRouter/OneApi 格式
content = data.get('choices', [{}])[0].get('delta', {}).get('content', '')
if content:
yield "answer", content
else:
raise ValueError(f"不支持的Claude Provider: {self.provider}")
except json.JSONDecodeError:
continue
elif self.provider == "anthropic":
# 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
82 changes: 81 additions & 1 deletion app/deepclaude/deepclaude.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,4 +161,84 @@ async def process_claude():
yield item

# 发送结束标记
yield b'data: [DONE]\n\n'
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()
claude_messages.append({
"role": "assistant",
"content": f"Here's my reasoning process:\n{reasoning}\n\nBased on this reasoning, I will now provide my response:"
})
# 处理可能 messages 内存在 role = system 的情况
claude_messages = [message for message in claude_messages if message.get("role", "") != "system"]

# 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

# 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": -1, # 由于我们无法准确计算 token,暂时使用 -1
"completion_tokens": -1,
"total_tokens": -1
}
}
except Exception as e:
logger.error(f"获取 Claude 响应时发生错误: {e}")
raise e
68 changes: 54 additions & 14 deletions app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,14 +62,45 @@ async def root():
logger.info("访问了根路径")
return {"message": "Welcome to DeepClaude API"}

@app.get("/v1/models")
async def list_models():
"""
获取可用模型列表
返回格式遵循 OpenAI API 标准
"""
models = [{
"id": "deepclaude",
"object": "model",
"created": 1677610602,
"owned_by": "deepclaude",
"permission": [{
"id": "modelperm-deepclaude",
"object": "model_permission",
"created": 1677610602,
"allow_create_engine": False,
"allow_sampling": True,
"allow_logprobs": True,
"allow_search_indices": False,
"allow_view": True,
"allow_fine_tuning": False,
"organization": "*",
"group": None,
"is_blocking": False
}],
"root": "deepclaude",
"parent": None
}]

return {"object": "list", "data": models}

@app.post("/v1/chat/completions", dependencies=[Depends(verify_api_key)])
async def chat_completions(request: Request):
"""处理聊天完成请求,返回流式响应
"""处理聊天完成请求,支持流式和非流式输出
请求体格式应与 OpenAI API 保持一致,包含:
- messages: 消息列表
- model: 模型名称(可选)
- stream: 是否使用流式输出(必须为 True)
- stream: 是否使用流式输出(可选,默认为 True)
- temperature: 随机性 (可选)
- top_p: top_p (可选)
- presence_penalty: 话题新鲜度(可选)
Expand All @@ -85,17 +116,28 @@ async def chat_completions(request: Request):
model_arg = (
get_and_validate_params(body)
)

# 3. 返回流式响应
return StreamingResponse(
deep_claude.chat_completions_with_stream(
stream = model_arg[4] # 获取 stream 参数

# 3. 根据 stream 参数返回相应的响应
if stream:
return StreamingResponse(
deep_claude.chat_completions_with_stream(
messages=messages,
model_arg=model_arg[:4], # 不传递 stream 参数
deepseek_model=DEEPSEEK_MODEL,
claude_model=CLAUDE_MODEL
),
media_type="text/event-stream"
)
else:
# 非流式输出
response = await deep_claude.chat_completions_without_stream(
messages=messages,
model_arg=model_arg,
model_arg=model_arg[:4], # 不传递 stream 参数
deepseek_model=DEEPSEEK_MODEL,
claude_model=CLAUDE_MODEL
),
media_type="text/event-stream"
)
)
return response

except Exception as e:
logger.error(f"处理请求时发生错误: {e}")
Expand All @@ -109,12 +151,10 @@ def get_and_validate_params(body):
top_p: float = body.get("top_p", 0.9)
presence_penalty: float = body.get("presence_penalty", 0.0)
frequency_penalty: float = body.get("frequency_penalty", 0.0)

if not body.get("stream", False):
raise ValueError("目前仅支持流式输出, stream 必须为 True")
stream: bool = body.get("stream", True)

if "sonnet" in body.get("model", ""): # Only Sonnet 设定 temperature 必须在 0 到 1 之间
if not isinstance(temperature, (float)) or temperature < 0.0 or temperature > 1.0:
raise ValueError("Sonnet 设定 temperature 必须在 0 到 1 之间")

return (temperature, top_p, presence_penalty, frequency_penalty)
return (temperature, top_p, presence_penalty, frequency_penalty, stream)

0 comments on commit 469647f

Please sign in to comment.