Update app.py
Browse files
app.py
CHANGED
|
@@ -64,42 +64,48 @@ MODEL_CONFIGS = {
|
|
| 64 |
"default_max_tokens": 8192,
|
| 65 |
"has_max_tokens_limit": True,
|
| 66 |
"supports_vision": True,
|
| 67 |
-
"supports_files": True
|
|
|
|
| 68 |
},
|
| 69 |
"anthropic/claude-3.5-sonnet": {
|
| 70 |
"min_max_tokens": 1,
|
| 71 |
"default_max_tokens": 8192,
|
| 72 |
"has_max_tokens_limit": False,
|
| 73 |
"supports_vision": True,
|
| 74 |
-
"supports_files": True
|
|
|
|
| 75 |
},
|
| 76 |
"anthropic/claude-3-sonnet": {
|
| 77 |
"min_max_tokens": 1,
|
| 78 |
"default_max_tokens": 4096,
|
| 79 |
"has_max_tokens_limit": False,
|
| 80 |
"supports_vision": True,
|
| 81 |
-
"supports_files": True
|
|
|
|
| 82 |
},
|
| 83 |
"anthropic/claude-3.5-haiku": {
|
| 84 |
"min_max_tokens": 1,
|
| 85 |
"default_max_tokens": 4096,
|
| 86 |
"has_max_tokens_limit": False,
|
| 87 |
"supports_vision": True,
|
| 88 |
-
"supports_files": True
|
|
|
|
| 89 |
},
|
| 90 |
"anthropic/claude-3-haiku": {
|
| 91 |
"min_max_tokens": 1,
|
| 92 |
"default_max_tokens": 4096,
|
| 93 |
"has_max_tokens_limit": False,
|
| 94 |
"supports_vision": True,
|
| 95 |
-
"supports_files": True
|
|
|
|
| 96 |
},
|
| 97 |
"google/gemini-2.5-pro": {
|
| 98 |
"min_max_tokens": 1,
|
| 99 |
"default_max_tokens": 8192,
|
| 100 |
"has_max_tokens_limit": False,
|
| 101 |
"supports_vision": True,
|
| 102 |
-
"supports_files": True
|
|
|
|
| 103 |
}
|
| 104 |
}
|
| 105 |
|
|
@@ -172,38 +178,74 @@ def decode_base64_file(data_url: str) -> tuple[str, str, str]:
|
|
| 172 |
logger.error(f"Failed to parse data URL: {e}")
|
| 173 |
return None, None, None
|
| 174 |
|
| 175 |
-
def
|
| 176 |
"""
|
| 177 |
-
将 base64
|
|
|
|
| 178 |
"""
|
| 179 |
-
# 检查 base64 数据是否已经包含 data URL 前缀
|
| 180 |
-
if base64_data.startswith("data:"):
|
| 181 |
-
return base64_data
|
| 182 |
-
|
| 183 |
-
# 如果没有前缀,添加默认的 JPEG data URL 前缀
|
| 184 |
-
# 但首先尝试检测实际的图片格式
|
| 185 |
try:
|
| 186 |
-
#
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
if decoded_bytes.startswith(b'\xff\xd8\xff'):
|
| 190 |
-
# JPEG
|
| 191 |
-
return f"data:image/jpeg;base64,{base64_data}"
|
| 192 |
-
elif decoded_bytes.startswith(b'\x89PNG\r\n\x1a\n'):
|
| 193 |
-
# PNG
|
| 194 |
-
return f"data:image/png;base64,{base64_data}"
|
| 195 |
-
elif decoded_bytes.startswith(b'GIF87a') or decoded_bytes.startswith(b'GIF89a'):
|
| 196 |
-
# GIF
|
| 197 |
-
return f"data:image/gif;base64,{base64_data}"
|
| 198 |
-
elif decoded_bytes.startswith(b'RIFF') and b'WEBP' in decoded_bytes[:20]:
|
| 199 |
-
# WebP
|
| 200 |
-
return f"data:image/webp;base64,{base64_data}"
|
| 201 |
else:
|
| 202 |
-
|
| 203 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 204 |
except Exception as e:
|
| 205 |
-
logger.
|
| 206 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 207 |
|
| 208 |
def extract_content_from_message(message: Dict[str, Any]) -> tuple[str, List[str], List[Dict[str, str]]]:
|
| 209 |
"""
|
|
@@ -237,9 +279,8 @@ def extract_content_from_message(message: Dict[str, Any]) -> tuple[str, List[str
|
|
| 237 |
try:
|
| 238 |
if ";base64," in url:
|
| 239 |
base64_data = url.split(";base64,")[1]
|
| 240 |
-
#
|
| 241 |
-
|
| 242 |
-
images.append(formatted_image)
|
| 243 |
logger.info(f"Found base64 image, size: {len(base64_data)} chars")
|
| 244 |
else:
|
| 245 |
logger.warning(f"Image URL format not supported: {url[:100]}...")
|
|
@@ -260,8 +301,7 @@ def extract_content_from_message(message: Dict[str, Any]) -> tuple[str, List[str
|
|
| 260 |
|
| 261 |
if file_ext in SUPPORTED_IMAGE_EXTENSIONS and mime_type.startswith("image/"):
|
| 262 |
# 图片文件
|
| 263 |
-
|
| 264 |
-
images.append(formatted_image)
|
| 265 |
logger.info(f"Found image file: {filename}")
|
| 266 |
elif file_ext in SUPPORTED_TEXT_EXTENSIONS or mime_type.startswith("text/"):
|
| 267 |
# 文本文件
|
|
@@ -307,7 +347,7 @@ def format_files_for_prompt(files: List[Dict[str, str]]) -> str:
|
|
| 307 |
|
| 308 |
return "\n".join(file_sections)
|
| 309 |
|
| 310 |
-
def transform_openai_to_replicate(openai_request: Dict[str, Any], model_override: str = None) -> Dict[str, Any]:
|
| 311 |
"""将OpenAI格式的请求转换为Replicate格式"""
|
| 312 |
try:
|
| 313 |
messages = openai_request.get("messages", [])
|
|
@@ -373,6 +413,18 @@ def transform_openai_to_replicate(openai_request: Dict[str, Any], model_override
|
|
| 373 |
if has_files and not model_config.get("supports_files", False):
|
| 374 |
logger.warning(f"Model {model} may not support file processing")
|
| 375 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 376 |
# 构建 Replicate 格式的输入
|
| 377 |
replicate_input = {}
|
| 378 |
|
|
@@ -404,10 +456,10 @@ def transform_openai_to_replicate(openai_request: Dict[str, Any], model_override
|
|
| 404 |
|
| 405 |
replicate_input["prompt"] = prompt
|
| 406 |
|
| 407 |
-
# 处理图片
|
| 408 |
-
if
|
| 409 |
-
replicate_input["image"] =
|
| 410 |
-
logger.info(f"Added
|
| 411 |
|
| 412 |
# 只在有 system_prompt 时才添加
|
| 413 |
if system_prompt:
|
|
@@ -563,12 +615,13 @@ async def root():
|
|
| 563 |
"message": "Replicate API Proxy for LobeChat with Vision and File Support",
|
| 564 |
"status": "running",
|
| 565 |
"replicate_token_configured": bool(REPLICATE_API_TOKEN),
|
| 566 |
-
"version": "1.1.
|
| 567 |
"supported_models": list(MODEL_CONFIGS.keys()),
|
| 568 |
"vision_support": True,
|
| 569 |
"file_support": True,
|
| 570 |
"supported_text_files": list(SUPPORTED_TEXT_EXTENSIONS),
|
| 571 |
-
"supported_image_files": list(SUPPORTED_IMAGE_EXTENSIONS)
|
|
|
|
| 572 |
}
|
| 573 |
|
| 574 |
@app.get("/health")
|
|
@@ -613,7 +666,7 @@ async def chat_completions(request: Request):
|
|
| 613 |
logger.info(f"Message count: {len(body.get('messages', []))}")
|
| 614 |
|
| 615 |
# 转换请求格式
|
| 616 |
-
replicate_data, model = transform_openai_to_replicate(body)
|
| 617 |
|
| 618 |
if body.get("stream", False):
|
| 619 |
# 流式响应
|
|
|
|
| 64 |
"default_max_tokens": 8192,
|
| 65 |
"has_max_tokens_limit": True,
|
| 66 |
"supports_vision": True,
|
| 67 |
+
"supports_files": True,
|
| 68 |
+
"image_format": "url" # Claude 4 Sonnet 需要 URL 格式
|
| 69 |
},
|
| 70 |
"anthropic/claude-3.5-sonnet": {
|
| 71 |
"min_max_tokens": 1,
|
| 72 |
"default_max_tokens": 8192,
|
| 73 |
"has_max_tokens_limit": False,
|
| 74 |
"supports_vision": True,
|
| 75 |
+
"supports_files": True,
|
| 76 |
+
"image_format": "data_url" # Claude 3.5 支持 data URL
|
| 77 |
},
|
| 78 |
"anthropic/claude-3-sonnet": {
|
| 79 |
"min_max_tokens": 1,
|
| 80 |
"default_max_tokens": 4096,
|
| 81 |
"has_max_tokens_limit": False,
|
| 82 |
"supports_vision": True,
|
| 83 |
+
"supports_files": True,
|
| 84 |
+
"image_format": "data_url"
|
| 85 |
},
|
| 86 |
"anthropic/claude-3.5-haiku": {
|
| 87 |
"min_max_tokens": 1,
|
| 88 |
"default_max_tokens": 4096,
|
| 89 |
"has_max_tokens_limit": False,
|
| 90 |
"supports_vision": True,
|
| 91 |
+
"supports_files": True,
|
| 92 |
+
"image_format": "data_url"
|
| 93 |
},
|
| 94 |
"anthropic/claude-3-haiku": {
|
| 95 |
"min_max_tokens": 1,
|
| 96 |
"default_max_tokens": 4096,
|
| 97 |
"has_max_tokens_limit": False,
|
| 98 |
"supports_vision": True,
|
| 99 |
+
"supports_files": True,
|
| 100 |
+
"image_format": "data_url"
|
| 101 |
},
|
| 102 |
"google/gemini-2.5-pro": {
|
| 103 |
"min_max_tokens": 1,
|
| 104 |
"default_max_tokens": 8192,
|
| 105 |
"has_max_tokens_limit": False,
|
| 106 |
"supports_vision": True,
|
| 107 |
+
"supports_files": True,
|
| 108 |
+
"image_format": "data_url"
|
| 109 |
}
|
| 110 |
}
|
| 111 |
|
|
|
|
| 178 |
logger.error(f"Failed to parse data URL: {e}")
|
| 179 |
return None, None, None
|
| 180 |
|
| 181 |
+
async def upload_image_to_temp_service(session: aiohttp.ClientSession, base64_data: str) -> str:
|
| 182 |
"""
|
| 183 |
+
将 base64 图片上传到临时图片托管服务
|
| 184 |
+
这里使用 imgbb 作为示例,你也可以使用其他服务
|
| 185 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 186 |
try:
|
| 187 |
+
# 从 base64 data URL 中提取纯 base64 数据
|
| 188 |
+
if base64_data.startswith("data:"):
|
| 189 |
+
base64_content = base64_data.split(",")[1]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
else:
|
| 191 |
+
base64_content = base64_data
|
| 192 |
+
|
| 193 |
+
# 使用 imgbb API(需要免费注册获取 API key)
|
| 194 |
+
# 这里暂时返回原始 data URL,你需要根据实际情况实现图片上传
|
| 195 |
+
logger.warning("Image upload to external service not implemented, using workaround")
|
| 196 |
+
|
| 197 |
+
# 临时解决方案:对于 Claude 4,我们需要找到另一种方式
|
| 198 |
+
# 可以考虑:
|
| 199 |
+
# 1. 使用临时文件服务(如 imgbb, imgur 等)
|
| 200 |
+
# 2. 使用自己的文件服务器
|
| 201 |
+
# 3. 修改为使用 claude-3.5-sonnet 作为替代
|
| 202 |
+
|
| 203 |
+
return None # 返回 None 表示上传失败
|
| 204 |
+
|
| 205 |
except Exception as e:
|
| 206 |
+
logger.error(f"Failed to upload image: {e}")
|
| 207 |
+
return None
|
| 208 |
+
|
| 209 |
+
def format_image_for_model(base64_data: str, model_config: Dict[str, Any]) -> str:
|
| 210 |
+
"""
|
| 211 |
+
根据模型配置格式化图片数据
|
| 212 |
+
"""
|
| 213 |
+
image_format = model_config.get("image_format", "data_url")
|
| 214 |
+
|
| 215 |
+
if image_format == "data_url":
|
| 216 |
+
# 检查 base64 数据是否已经包含 data URL 前缀
|
| 217 |
+
if base64_data.startswith("data:"):
|
| 218 |
+
return base64_data
|
| 219 |
+
|
| 220 |
+
# 如果没有前缀,添加默认的 JPEG data URL 前缀
|
| 221 |
+
try:
|
| 222 |
+
# 解码 base64 数据的前几个字节来检测格式
|
| 223 |
+
decoded_bytes = base64.b64decode(base64_data[:100])
|
| 224 |
+
|
| 225 |
+
if decoded_bytes.startswith(b'\xff\xd8\xff'):
|
| 226 |
+
# JPEG
|
| 227 |
+
return f"data:image/jpeg;base64,{base64_data}"
|
| 228 |
+
elif decoded_bytes.startswith(b'\x89PNG\r\n\x1a\n'):
|
| 229 |
+
# PNG
|
| 230 |
+
return f"data:image/png;base64,{base64_data}"
|
| 231 |
+
elif decoded_bytes.startswith(b'GIF87a') or decoded_bytes.startswith(b'GIF89a'):
|
| 232 |
+
# GIF
|
| 233 |
+
return f"data:image/gif;base64,{base64_data}"
|
| 234 |
+
elif decoded_bytes.startswith(b'RIFF') and b'WEBP' in decoded_bytes[:20]:
|
| 235 |
+
# WebP
|
| 236 |
+
return f"data:image/webp;base64,{base64_data}"
|
| 237 |
+
else:
|
| 238 |
+
# 默认使用 JPEG
|
| 239 |
+
return f"data:image/jpeg;base64,{base64_data}"
|
| 240 |
+
except Exception as e:
|
| 241 |
+
logger.warning(f"Failed to detect image format: {e}, using JPEG as default")
|
| 242 |
+
return f"data:image/jpeg;base64,{base64_data}"
|
| 243 |
+
|
| 244 |
+
elif image_format == "url":
|
| 245 |
+
# 对于需要 URL 的模型,返回 None 表示需要上传
|
| 246 |
+
return None
|
| 247 |
+
|
| 248 |
+
return base64_data
|
| 249 |
|
| 250 |
def extract_content_from_message(message: Dict[str, Any]) -> tuple[str, List[str], List[Dict[str, str]]]:
|
| 251 |
"""
|
|
|
|
| 279 |
try:
|
| 280 |
if ";base64," in url:
|
| 281 |
base64_data = url.split(";base64,")[1]
|
| 282 |
+
# 先存储原始的 base64 数据,稍后根据模型需求格式化
|
| 283 |
+
images.append(url) # 保存完整的 data URL
|
|
|
|
| 284 |
logger.info(f"Found base64 image, size: {len(base64_data)} chars")
|
| 285 |
else:
|
| 286 |
logger.warning(f"Image URL format not supported: {url[:100]}...")
|
|
|
|
| 301 |
|
| 302 |
if file_ext in SUPPORTED_IMAGE_EXTENSIONS and mime_type.startswith("image/"):
|
| 303 |
# 图片文件
|
| 304 |
+
images.append(file_url) # 保存完整的 data URL
|
|
|
|
| 305 |
logger.info(f"Found image file: {filename}")
|
| 306 |
elif file_ext in SUPPORTED_TEXT_EXTENSIONS or mime_type.startswith("text/"):
|
| 307 |
# 文本文件
|
|
|
|
| 347 |
|
| 348 |
return "\n".join(file_sections)
|
| 349 |
|
| 350 |
+
async def transform_openai_to_replicate(openai_request: Dict[str, Any], model_override: str = None) -> Dict[str, Any]:
|
| 351 |
"""将OpenAI格式的请求转换为Replicate格式"""
|
| 352 |
try:
|
| 353 |
messages = openai_request.get("messages", [])
|
|
|
|
| 413 |
if has_files and not model_config.get("supports_files", False):
|
| 414 |
logger.warning(f"Model {model} may not support file processing")
|
| 415 |
|
| 416 |
+
# 处理图片格式
|
| 417 |
+
formatted_image = None
|
| 418 |
+
if has_images and primary_image:
|
| 419 |
+
if model_config.get("image_format") == "url":
|
| 420 |
+
# Claude 4 需要 URL 格式,暂时降级到 Claude 3.5
|
| 421 |
+
logger.warning(f"Model {model} requires URL format for images, falling back to claude-3.5-sonnet")
|
| 422 |
+
model = "anthropic/claude-3.5-sonnet"
|
| 423 |
+
model_config = MODEL_CONFIGS[model]
|
| 424 |
+
formatted_image = format_image_for_model(primary_image, model_config)
|
| 425 |
+
else:
|
| 426 |
+
formatted_image = format_image_for_model(primary_image, model_config)
|
| 427 |
+
|
| 428 |
# 构建 Replicate 格式的输入
|
| 429 |
replicate_input = {}
|
| 430 |
|
|
|
|
| 456 |
|
| 457 |
replicate_input["prompt"] = prompt
|
| 458 |
|
| 459 |
+
# 处理图片
|
| 460 |
+
if formatted_image:
|
| 461 |
+
replicate_input["image"] = formatted_image
|
| 462 |
+
logger.info(f"Added image to request for model {model}: {formatted_image[:100]}...")
|
| 463 |
|
| 464 |
# 只在有 system_prompt 时才添加
|
| 465 |
if system_prompt:
|
|
|
|
| 615 |
"message": "Replicate API Proxy for LobeChat with Vision and File Support",
|
| 616 |
"status": "running",
|
| 617 |
"replicate_token_configured": bool(REPLICATE_API_TOKEN),
|
| 618 |
+
"version": "1.1.2",
|
| 619 |
"supported_models": list(MODEL_CONFIGS.keys()),
|
| 620 |
"vision_support": True,
|
| 621 |
"file_support": True,
|
| 622 |
"supported_text_files": list(SUPPORTED_TEXT_EXTENSIONS),
|
| 623 |
+
"supported_image_files": list(SUPPORTED_IMAGE_EXTENSIONS),
|
| 624 |
+
"notes": "Claude 4 Sonnet image support temporarily falls back to Claude 3.5 Sonnet"
|
| 625 |
}
|
| 626 |
|
| 627 |
@app.get("/health")
|
|
|
|
| 666 |
logger.info(f"Message count: {len(body.get('messages', []))}")
|
| 667 |
|
| 668 |
# 转换请求格式
|
| 669 |
+
replicate_data, model = await transform_openai_to_replicate(body)
|
| 670 |
|
| 671 |
if body.get("stream", False):
|
| 672 |
# 流式响应
|