Update app/main.py
Browse files- app/main.py +150 -115
app/main.py
CHANGED
@@ -273,94 +273,66 @@ async def startup_event():
|
|
273 |
print("WARNING: Failed to initialize Vertex AI authentication")
|
274 |
|
275 |
# Conversion functions
|
276 |
-
|
|
|
|
|
|
|
277 |
"""
|
278 |
Convert OpenAI messages to Gemini format.
|
279 |
-
Returns
|
280 |
"""
|
281 |
-
|
282 |
-
|
283 |
-
|
284 |
-
|
285 |
-
for part in message.content:
|
286 |
-
if isinstance(part, dict) and part.get('type') == 'image_url':
|
287 |
-
has_images = True
|
288 |
-
break
|
289 |
-
elif isinstance(part, ContentPartImage):
|
290 |
-
has_images = True
|
291 |
-
break
|
292 |
-
if has_images:
|
293 |
-
break
|
294 |
|
295 |
-
#
|
296 |
-
|
297 |
-
|
|
|
298 |
|
299 |
-
#
|
300 |
-
|
301 |
-
|
302 |
-
|
303 |
-
|
304 |
-
|
305 |
-
elif isinstance(message.content, list) and message.content and isinstance(message.content[0], dict) and 'text' in message.content[0]:
|
306 |
-
content_text = message.content[0]['text']
|
307 |
-
else:
|
308 |
-
# Fallback for unexpected format
|
309 |
-
content_text = str(message.content)
|
310 |
-
|
311 |
-
if message.role == "system":
|
312 |
-
prompt += f"System: {content_text}\n\n"
|
313 |
-
elif message.role == "user":
|
314 |
-
prompt += f"Human: {content_text}\n"
|
315 |
-
elif message.role == "assistant":
|
316 |
-
prompt += f"AI: {content_text}\n"
|
317 |
|
318 |
-
#
|
319 |
-
if
|
320 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
321 |
|
322 |
-
|
323 |
-
|
324 |
-
# If images are present, create a list of content parts
|
325 |
-
gemini_contents = []
|
326 |
-
|
327 |
-
# Process all messages in their original order
|
328 |
-
for message in messages:
|
329 |
|
330 |
-
#
|
331 |
if isinstance(message.content, str):
|
332 |
-
|
333 |
-
|
334 |
-
|
335 |
-
# For list content, process each part
|
336 |
elif isinstance(message.content, list):
|
337 |
-
#
|
338 |
-
text_content = ""
|
339 |
-
|
340 |
for part in message.content:
|
341 |
-
|
342 |
-
|
343 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
344 |
elif isinstance(part, ContentPartText):
|
345 |
-
|
346 |
-
|
347 |
-
# Add the combined text content if any
|
348 |
-
if text_content:
|
349 |
-
prefix = "Human: " if message.role == "user" else "AI: "
|
350 |
-
gemini_contents.append(f"{prefix}{text_content}")
|
351 |
-
|
352 |
-
# Then process image parts
|
353 |
-
for part in message.content:
|
354 |
-
# Handle image parts
|
355 |
-
if isinstance(part, dict) and part.get('type') == 'image_url':
|
356 |
-
image_url = part.get('image_url', {}).get('url', '')
|
357 |
-
if image_url.startswith('data:'):
|
358 |
-
# Extract mime type and base64 data
|
359 |
-
mime_match = re.match(r'data:([^;]+);base64,(.+)', image_url)
|
360 |
-
if mime_match:
|
361 |
-
mime_type, b64_data = mime_match.groups()
|
362 |
-
image_bytes = base64.b64decode(b64_data)
|
363 |
-
gemini_contents.append(types.Part.from_bytes(data=image_bytes, mime_type=mime_type))
|
364 |
elif isinstance(part, ContentPartImage):
|
365 |
image_url = part.image_url.url
|
366 |
if image_url.startswith('data:'):
|
@@ -369,14 +341,29 @@ def create_gemini_prompt(messages: List[OpenAIMessage]) -> Union[str, List[Any]]
|
|
369 |
if mime_match:
|
370 |
mime_type, b64_data = mime_match.groups()
|
371 |
image_bytes = base64.b64decode(b64_data)
|
372 |
-
|
373 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
374 |
|
375 |
-
def create_encrypted_gemini_prompt(messages: List[OpenAIMessage]) ->
|
376 |
"""
|
377 |
Convert OpenAI messages to Gemini format with special encoding for the encrypt model.
|
378 |
This function URL-encodes user messages and adds specific system instructions.
|
379 |
"""
|
|
|
|
|
380 |
# Check if any message contains image content
|
381 |
has_images = False
|
382 |
for message in messages:
|
@@ -395,8 +382,8 @@ def create_encrypted_gemini_prompt(messages: List[OpenAIMessage]) -> Union[str,
|
|
395 |
if has_images:
|
396 |
return create_gemini_prompt(messages)
|
397 |
|
398 |
-
# Define the
|
399 |
-
|
400 |
STRICT OPERATING PROTOCOL:
|
401 |
1. **Critical Output Format:** Your responses MUST ALWAYS be standard **UNENCODED PLAIN TEXT**. Absolutely NO URL encoding or any other form of encoding in your output. This is non-negotiable.
|
402 |
2. **Input Processing (Internal Only):** User messages and conversation history provided to you will be URL-encoded for privacy. Decode these inputs internally before processing.
|
@@ -422,8 +409,8 @@ Ready for your request."""
|
|
422 |
# Create a new list of messages with the pre-messages and encoded content
|
423 |
new_messages = []
|
424 |
|
425 |
-
# Add a system message with
|
426 |
-
new_messages.append(OpenAIMessage(role="system", content=
|
427 |
|
428 |
# Add pre-messages
|
429 |
new_messages.extend(pre_messages)
|
@@ -431,19 +418,7 @@ Ready for your request."""
|
|
431 |
# Process all messages in their original order
|
432 |
for i, message in enumerate(messages):
|
433 |
if message.role == "system":
|
434 |
-
#
|
435 |
-
# if isinstance(message.content, str):
|
436 |
-
# system_content = message.content
|
437 |
-
# elif isinstance(message.content, list) and message.content and isinstance(message.content[0], dict) and 'text' in message.content[0]:
|
438 |
-
# system_content = message.content[0]['text']
|
439 |
-
# else:
|
440 |
-
# system_content = str(message.content)
|
441 |
-
|
442 |
-
# # URL encode the system message content
|
443 |
-
# new_messages.append(OpenAIMessage(
|
444 |
-
# role="system",
|
445 |
-
# content=urllib.parse.quote(system_content)
|
446 |
-
# ))
|
447 |
new_messages.append(message)
|
448 |
|
449 |
elif message.role == "user":
|
@@ -454,12 +429,26 @@ Ready for your request."""
|
|
454 |
content=urllib.parse.quote(message.content)
|
455 |
))
|
456 |
elif isinstance(message.content, list):
|
457 |
-
#
|
458 |
-
|
459 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
460 |
else:
|
461 |
-
# For
|
462 |
-
# Check if this is the last
|
463 |
is_last_assistant = True
|
464 |
for remaining_msg in messages[i+1:]:
|
465 |
if remaining_msg.role != "user":
|
@@ -473,13 +462,30 @@ Ready for your request."""
|
|
473 |
role=message.role,
|
474 |
content=urllib.parse.quote(message.content)
|
475 |
))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
476 |
else:
|
477 |
-
# For non-string content, keep as is
|
478 |
new_messages.append(message)
|
479 |
else:
|
480 |
-
# For other
|
481 |
new_messages.append(message)
|
482 |
|
|
|
483 |
# Now use the standard function to convert to Gemini format
|
484 |
return create_gemini_prompt(new_messages)
|
485 |
|
@@ -826,6 +832,14 @@ async def chat_completions(request: OpenAIRequest, api_key: str = Depends(get_ap
|
|
826 |
prompt = create_encrypted_gemini_prompt(request.messages)
|
827 |
else:
|
828 |
prompt = create_gemini_prompt(request.messages)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
829 |
|
830 |
if request.stream:
|
831 |
# Handle streaming response
|
@@ -838,12 +852,22 @@ async def chat_completions(request: OpenAIRequest, api_key: str = Depends(get_ap
|
|
838 |
# If multiple candidates are requested, we'll generate them sequentially
|
839 |
for candidate_index in range(candidate_count):
|
840 |
# Generate content with streaming
|
841 |
-
# Handle
|
842 |
-
|
843 |
-
|
844 |
-
|
845 |
-
|
846 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
847 |
|
848 |
# Convert and yield each chunk
|
849 |
for response in responses:
|
@@ -873,12 +897,23 @@ async def chat_completions(request: OpenAIRequest, api_key: str = Depends(get_ap
|
|
873 |
# Make sure generation_config has candidate_count set
|
874 |
if "candidate_count" not in generation_config:
|
875 |
generation_config["candidate_count"] = request.n
|
876 |
-
# Handle
|
877 |
-
|
878 |
-
|
879 |
-
|
880 |
-
|
881 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
882 |
|
883 |
|
884 |
openai_response = convert_to_openai_format(response, request.model)
|
|
|
273 |
print("WARNING: Failed to initialize Vertex AI authentication")
|
274 |
|
275 |
# Conversion functions
|
276 |
+
# Define supported roles for Gemini API
|
277 |
+
SUPPORTED_ROLES = ["user", "model"]
|
278 |
+
|
279 |
+
def create_gemini_prompt(messages: List[OpenAIMessage]) -> List[Dict[str, Any]]:
|
280 |
"""
|
281 |
Convert OpenAI messages to Gemini format.
|
282 |
+
Returns a list of message objects with role and parts as required by the Gemini API.
|
283 |
"""
|
284 |
+
print("Converting OpenAI messages to Gemini format...")
|
285 |
+
|
286 |
+
# Create a list to hold the Gemini-formatted messages
|
287 |
+
gemini_messages = []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
288 |
|
289 |
+
# Process all messages in their original order
|
290 |
+
for idx, message in enumerate(messages):
|
291 |
+
# Map OpenAI roles to Gemini roles
|
292 |
+
role = message.role
|
293 |
|
294 |
+
# If role is "system", use "user" as specified
|
295 |
+
if role == "system":
|
296 |
+
role = "user"
|
297 |
+
# If role is "assistant", map to "model"
|
298 |
+
elif role == "assistant":
|
299 |
+
role = "model"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
300 |
|
301 |
+
# Handle unsupported roles as per user's feedback
|
302 |
+
if role not in SUPPORTED_ROLES:
|
303 |
+
if role == "tool":
|
304 |
+
role = "user"
|
305 |
+
else:
|
306 |
+
# If it's the last message, treat it as a user message
|
307 |
+
if idx == len(messages) - 1:
|
308 |
+
role = "user"
|
309 |
+
else:
|
310 |
+
role = "model"
|
311 |
|
312 |
+
# Create parts list for this message
|
313 |
+
parts = []
|
|
|
|
|
|
|
|
|
|
|
314 |
|
315 |
+
# Handle different content types
|
316 |
if isinstance(message.content, str):
|
317 |
+
# Simple string content
|
318 |
+
parts.append({"text": message.content})
|
|
|
|
|
319 |
elif isinstance(message.content, list):
|
320 |
+
# List of content parts (may include text and images)
|
|
|
|
|
321 |
for part in message.content:
|
322 |
+
if isinstance(part, dict):
|
323 |
+
if part.get('type') == 'text':
|
324 |
+
parts.append({"text": part.get('text', '')})
|
325 |
+
elif part.get('type') == 'image_url':
|
326 |
+
image_url = part.get('image_url', {}).get('url', '')
|
327 |
+
if image_url.startswith('data:'):
|
328 |
+
# Extract mime type and base64 data
|
329 |
+
mime_match = re.match(r'data:([^;]+);base64,(.+)', image_url)
|
330 |
+
if mime_match:
|
331 |
+
mime_type, b64_data = mime_match.groups()
|
332 |
+
image_bytes = base64.b64decode(b64_data)
|
333 |
+
parts.append(types.Part.from_bytes(data=image_bytes, mime_type=mime_type))
|
334 |
elif isinstance(part, ContentPartText):
|
335 |
+
parts.append({"text": part.text})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
336 |
elif isinstance(part, ContentPartImage):
|
337 |
image_url = part.image_url.url
|
338 |
if image_url.startswith('data:'):
|
|
|
341 |
if mime_match:
|
342 |
mime_type, b64_data = mime_match.groups()
|
343 |
image_bytes = base64.b64decode(b64_data)
|
344 |
+
parts.append(types.Part.from_bytes(data=image_bytes, mime_type=mime_type))
|
345 |
+
else:
|
346 |
+
# Fallback for unexpected format
|
347 |
+
parts.append({"text": str(message.content)})
|
348 |
+
|
349 |
+
# Add the message with role and parts to our list
|
350 |
+
gemini_messages.append({
|
351 |
+
"role": role,
|
352 |
+
"parts": parts
|
353 |
+
})
|
354 |
+
|
355 |
+
print(f"Converted to {len(gemini_messages)} Gemini messages")
|
356 |
+
return gemini_messages
|
357 |
+
|
358 |
+
# No need for the separate image handling branch as we now handle all content types in one flow
|
359 |
|
360 |
+
def create_encrypted_gemini_prompt(messages: List[OpenAIMessage]) -> List[Dict[str, Any]]:
|
361 |
"""
|
362 |
Convert OpenAI messages to Gemini format with special encoding for the encrypt model.
|
363 |
This function URL-encodes user messages and adds specific system instructions.
|
364 |
"""
|
365 |
+
print("Creating encrypted Gemini prompt...")
|
366 |
+
|
367 |
# Check if any message contains image content
|
368 |
has_images = False
|
369 |
for message in messages:
|
|
|
382 |
if has_images:
|
383 |
return create_gemini_prompt(messages)
|
384 |
|
385 |
+
# Define the instructions to be added
|
386 |
+
instructions = """// AI Assistant Configuration //
|
387 |
STRICT OPERATING PROTOCOL:
|
388 |
1. **Critical Output Format:** Your responses MUST ALWAYS be standard **UNENCODED PLAIN TEXT**. Absolutely NO URL encoding or any other form of encoding in your output. This is non-negotiable.
|
389 |
2. **Input Processing (Internal Only):** User messages and conversation history provided to you will be URL-encoded for privacy. Decode these inputs internally before processing.
|
|
|
409 |
# Create a new list of messages with the pre-messages and encoded content
|
410 |
new_messages = []
|
411 |
|
412 |
+
# Add a system message with instructions at the beginning
|
413 |
+
new_messages.append(OpenAIMessage(role="system", content=instructions))
|
414 |
|
415 |
# Add pre-messages
|
416 |
new_messages.extend(pre_messages)
|
|
|
418 |
# Process all messages in their original order
|
419 |
for i, message in enumerate(messages):
|
420 |
if message.role == "system":
|
421 |
+
# Pass system messages through as is
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
422 |
new_messages.append(message)
|
423 |
|
424 |
elif message.role == "user":
|
|
|
429 |
content=urllib.parse.quote(message.content)
|
430 |
))
|
431 |
elif isinstance(message.content, list):
|
432 |
+
# For list content (like with images), we need to handle each part
|
433 |
+
encoded_parts = []
|
434 |
+
for part in message.content:
|
435 |
+
if isinstance(part, dict) and part.get('type') == 'text':
|
436 |
+
# URL encode text parts
|
437 |
+
encoded_parts.append({
|
438 |
+
'type': 'text',
|
439 |
+
'text': urllib.parse.quote(part.get('text', ''))
|
440 |
+
})
|
441 |
+
else:
|
442 |
+
# Pass through non-text parts (like images)
|
443 |
+
encoded_parts.append(part)
|
444 |
+
|
445 |
+
new_messages.append(OpenAIMessage(
|
446 |
+
role=message.role,
|
447 |
+
content=encoded_parts
|
448 |
+
))
|
449 |
else:
|
450 |
+
# For assistant messages
|
451 |
+
# Check if this is the last assistant message in the conversation
|
452 |
is_last_assistant = True
|
453 |
for remaining_msg in messages[i+1:]:
|
454 |
if remaining_msg.role != "user":
|
|
|
462 |
role=message.role,
|
463 |
content=urllib.parse.quote(message.content)
|
464 |
))
|
465 |
+
elif isinstance(message.content, list):
|
466 |
+
# Handle list content similar to user messages
|
467 |
+
encoded_parts = []
|
468 |
+
for part in message.content:
|
469 |
+
if isinstance(part, dict) and part.get('type') == 'text':
|
470 |
+
encoded_parts.append({
|
471 |
+
'type': 'text',
|
472 |
+
'text': urllib.parse.quote(part.get('text', ''))
|
473 |
+
})
|
474 |
+
else:
|
475 |
+
encoded_parts.append(part)
|
476 |
+
|
477 |
+
new_messages.append(OpenAIMessage(
|
478 |
+
role=message.role,
|
479 |
+
content=encoded_parts
|
480 |
+
))
|
481 |
else:
|
482 |
+
# For non-string/list content, keep as is
|
483 |
new_messages.append(message)
|
484 |
else:
|
485 |
+
# For other assistant messages, keep as is
|
486 |
new_messages.append(message)
|
487 |
|
488 |
+
print(f"Created encrypted prompt with {len(new_messages)} messages")
|
489 |
# Now use the standard function to convert to Gemini format
|
490 |
return create_gemini_prompt(new_messages)
|
491 |
|
|
|
832 |
prompt = create_encrypted_gemini_prompt(request.messages)
|
833 |
else:
|
834 |
prompt = create_gemini_prompt(request.messages)
|
835 |
+
|
836 |
+
# Log the structure of the prompt (without exposing sensitive content)
|
837 |
+
print(f"Prompt structure: {len(prompt)} messages")
|
838 |
+
for i, msg in enumerate(prompt):
|
839 |
+
role = msg.get('role', 'unknown')
|
840 |
+
parts_count = len(msg.get('parts', []))
|
841 |
+
parts_types = [type(p).__name__ for p in msg.get('parts', [])]
|
842 |
+
print(f" Message {i+1}: role={role}, parts={parts_count}, types={parts_types}")
|
843 |
|
844 |
if request.stream:
|
845 |
# Handle streaming response
|
|
|
852 |
# If multiple candidates are requested, we'll generate them sequentially
|
853 |
for candidate_index in range(candidate_count):
|
854 |
# Generate content with streaming
|
855 |
+
# Handle the new message format for streaming
|
856 |
+
print(f"Sending streaming request to Gemini API with {len(prompt)} messages")
|
857 |
+
try:
|
858 |
+
responses = client.models.generate_content_stream(
|
859 |
+
model=gemini_model,
|
860 |
+
contents={"contents": prompt}, # Wrap in contents field as per API docs
|
861 |
+
config=generation_config,
|
862 |
+
)
|
863 |
+
except Exception as e:
|
864 |
+
# If the above format doesn't work, try the direct format
|
865 |
+
print(f"First streaming attempt failed: {e}. Trying direct format...")
|
866 |
+
responses = client.models.generate_content_stream(
|
867 |
+
model=gemini_model,
|
868 |
+
contents=prompt, # Try direct format
|
869 |
+
config=generation_config,
|
870 |
+
)
|
871 |
|
872 |
# Convert and yield each chunk
|
873 |
for response in responses:
|
|
|
897 |
# Make sure generation_config has candidate_count set
|
898 |
if "candidate_count" not in generation_config:
|
899 |
generation_config["candidate_count"] = request.n
|
900 |
+
# Handle the new message format
|
901 |
+
# The Gemini API expects a specific format for contents
|
902 |
+
print(f"Sending request to Gemini API with {len(prompt)} messages")
|
903 |
+
try:
|
904 |
+
response = client.models.generate_content(
|
905 |
+
model=gemini_model,
|
906 |
+
contents={"contents": prompt}, # Wrap in contents field as per API docs
|
907 |
+
config=generation_config,
|
908 |
+
)
|
909 |
+
except Exception as e:
|
910 |
+
# If the above format doesn't work, try the direct format
|
911 |
+
print(f"First attempt failed: {e}. Trying direct format...")
|
912 |
+
response = client.models.generate_content(
|
913 |
+
model=gemini_model,
|
914 |
+
contents=prompt, # Try direct format
|
915 |
+
config=generation_config,
|
916 |
+
)
|
917 |
|
918 |
|
919 |
openai_response = convert_to_openai_format(response, request.model)
|