nomid2 commited on
Commit
a52668f
·
verified ·
1 Parent(s): c425d75

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +99 -46
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 format_image_for_replicate(base64_data: str) -> str:
176
  """
177
- 将 base64 图片数据格式化为 Replicate 期望的格式
 
178
  """
179
- # 检查 base64 数据是否已经包含 data URL 前缀
180
- if base64_data.startswith("data:"):
181
- return base64_data
182
-
183
- # 如果没有前缀,添加默认的 JPEG data URL 前缀
184
- # 但首先尝试检测实际的图片格式
185
  try:
186
- # 解码 base64 数据的前几个字节来检测格式
187
- decoded_bytes = base64.b64decode(base64_data[:100])
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
- # 默认使用 JPEG
203
- return f"data:image/jpeg;base64,{base64_data}"
 
 
 
 
 
 
 
 
 
 
 
 
204
  except Exception as e:
205
- logger.warning(f"Failed to detect image format: {e}, using JPEG as default")
206
- return f"data:image/jpeg;base64,{base64_data}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- # 格式化为 Replicate 期望的格式
241
- formatted_image = format_image_for_replicate(base64_data)
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
- formatted_image = format_image_for_replicate(file_content)
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
- # 处理图片 - 使用正确的 data URL 格式
408
- if has_images and primary_image:
409
- replicate_input["image"] = primary_image
410
- logger.info(f"Added primary image to request: {primary_image[:100]}...")
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.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
  # 流式响应