cstr commited on
Commit
9dba8e1
·
verified ·
1 Parent(s): e0621f5

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +564 -150
app.py CHANGED
@@ -1,94 +1,142 @@
1
  import os
2
- import gradio as gr
3
- import requests
4
  import json
5
  import base64
6
- from PIL import Image
7
- import io
8
- import logging
9
- import PyPDF2
10
- import markdown
11
 
12
  # Configure logging
13
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
14
  logger = logging.getLogger(__name__)
15
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  # API key
17
  OPENROUTER_API_KEY = os.environ.get("OPENROUTER_API_KEY", "")
18
 
19
- # Model list with context sizes - organized by category
20
  MODELS = [
21
  # Vision Models
22
- {"category": "Vision", "models": [
 
 
 
 
 
 
23
  ("Meta: Llama 3.2 11B Vision Instruct", "meta-llama/llama-3.2-11b-vision-instruct:free", 131072),
24
- ("Qwen2.5 VL 72B Instruct", "qwen/qwen2.5-vl-72b-instruct:free", 131072),
25
- ("Qwen2.5 VL 32B Instruct", "qwen/qwen2.5-vl-32b-instruct:free", 8192),
26
- ("Qwen2.5 VL 7B Instruct", "qwen/qwen-2.5-vl-7b-instruct:free", 64000),
27
- ("Qwen2.5 VL 3B Instruct", "qwen/qwen2.5-vl-3b-instruct:free", 64000),
 
28
  ]},
29
 
30
- # Gemini Models
31
- {"category": "Gemini", "models": [
32
- ("Gemini Pro 2.0 Experimental", "google/gemini-2.0-pro-exp-02-05:free", 2000000),
33
- ("Gemini Pro 2.5 Experimental", "google/gemini-2.5-pro-exp-03-25:free", 1000000),
34
- ("Gemini 2.0 Flash Thinking Experimental", "google/gemini-2.0-flash-thinking-exp:free", 1048576),
35
- ("Gemini Flash 2.0 Experimental", "google/gemini-2.0-flash-exp:free", 1048576),
36
- ("Gemini Flash 1.5 8B Experimental", "google/gemini-flash-1.5-8b-exp", 1000000),
37
- ("LearnLM 1.5 Pro Experimental", "google/learnlm-1.5-pro-experimental:free", 40960),
38
  ]},
39
 
40
- # Llama Models
41
- {"category": "Llama", "models": [
42
- ("Llama 3.3 70B Instruct", "meta-llama/llama-3.3-70b-instruct:free", 8000),
43
- ("Llama 3.2 3B Instruct", "meta-llama/llama-3.2-3b-instruct:free", 20000),
44
- ("Llama 3.2 1B Instruct", "meta-llama/llama-3.2-1b-instruct:free", 131072),
45
- ("Llama 3.1 8B Instruct", "meta-llama/llama-3.1-8b-instruct:free", 131072),
46
- ("Llama 3 8B Instruct", "meta-llama/llama-3-8b-instruct:free", 8192),
47
- ("Llama 3.1 Nemotron 70B Instruct", "nvidia/llama-3.1-nemotron-70b-instruct:free", 131072),
48
  ]},
49
 
50
- # DeepSeek Models
51
- {"category": "DeepSeek", "models": [
52
- ("DeepSeek R1 Zero", "deepseek/deepseek-r1-zero:free", 163840),
53
- ("DeepSeek R1", "deepseek/deepseek-r1:free", 163840),
54
- ("DeepSeek V3 Base", "deepseek/deepseek-v3-base:free", 131072),
55
- ("DeepSeek V3 0324", "deepseek/deepseek-v3-0324:free", 131072),
56
- ("DeepSeek V3", "deepseek/deepseek-chat:free", 131072),
57
- ("DeepSeek R1 Distill Qwen 14B", "deepseek/deepseek-r1-distill-qwen-14b:free", 64000),
58
- ("DeepSeek R1 Distill Qwen 32B", "deepseek/deepseek-r1-distill-qwen-32b:free", 16000),
59
- ("DeepSeek R1 Distill Llama 70B", "deepseek/deepseek-r1-distill-llama-70b:free", 8192),
60
  ]},
61
 
62
- # Other Popular Models
63
- {"category": "Other Popular Models", "models": [
64
- ("Mistral Nemo", "mistralai/mistral-nemo:free", 128000),
65
- ("Mistral Small 3.1 24B", "mistralai/mistral-small-3.1-24b-instruct:free", 96000),
66
- ("Gemma 3 27B", "google/gemma-3-27b-it:free", 96000),
67
- ("Gemma 3 12B", "google/gemma-3-12b-it:free", 131072),
68
- ("Gemma 3 4B", "google/gemma-3-4b-it:free", 131072),
69
- ("DeepHermes 3 Llama 3 8B Preview", "nousresearch/deephermes-3-llama-3-8b-preview:free", 131072),
70
- ("Qwen2.5 72B Instruct", "qwen/qwen-2.5-72b-instruct:free", 32768),
71
  ]},
72
 
73
- # Smaller Models (<50B params)
74
- {"category": "Smaller Models", "models": [
75
- ("Gemma 3 1B", "google/gemma-3-1b-it:free", 32768),
76
- ("Gemma 2 9B", "google/gemma-2-9b-it:free", 8192),
77
- ("Mistral 7B Instruct", "mistralai/mistral-7b-instruct:free", 8192),
78
- ("Qwen 2 7B Instruct", "qwen/qwen-2-7b-instruct:free", 8192),
79
- ("Phi-3 Mini 128K Instruct", "microsoft/phi-3-mini-128k-instruct:free", 8192),
80
- ("Phi-3 Medium 128K Instruct", "microsoft/phi-3-medium-128k-instruct:free", 8192),
81
- ("OpenChat 3.5 7B", "openchat/openchat-7b:free", 8192),
82
- ("Zephyr 7B", "huggingfaceh4/zephyr-7b-beta:free", 4096),
83
- ("MythoMax 13B", "gryphe/mythomax-l2-13b:free", 4096),
84
  ]},
85
  ]
86
 
87
  # Flatten model list for easy searching
88
  ALL_MODELS = []
89
  for category in MODELS:
90
- for model in category["models"]:
91
- ALL_MODELS.append(model)
 
 
 
 
 
92
 
93
  def format_to_message_dict(history):
94
  """Convert history to proper message format"""
@@ -103,44 +151,72 @@ def format_to_message_dict(history):
103
  return messages
104
 
105
  def encode_image_to_base64(image_path):
106
- """Encode an image file to base64 string"""
107
  try:
108
  if isinstance(image_path, str): # File path as string
109
  with open(image_path, "rb") as image_file:
110
  encoded_string = base64.b64encode(image_file.read()).decode('utf-8')
111
  file_extension = image_path.split('.')[-1].lower()
112
  mime_type = f"image/{file_extension}"
113
- if file_extension == "jpg" or file_extension == "jpeg":
114
  mime_type = "image/jpeg"
 
 
 
 
 
 
115
  return f"data:{mime_type};base64,{encoded_string}"
116
- else: # Pillow Image or file-like object
117
- buffered = io.BytesIO()
118
- image_path.save(buffered, format="PNG")
 
 
 
 
 
 
 
 
119
  encoded_string = base64.b64encode(buffered.getvalue()).decode('utf-8')
120
  return f"data:image/png;base64,{encoded_string}"
 
 
 
121
  except Exception as e:
122
  logger.error(f"Error encoding image: {str(e)}")
123
  return None
124
 
125
  def extract_text_from_file(file_path):
126
- """Extract text from various file types"""
127
  try:
128
  file_extension = file_path.split('.')[-1].lower()
129
 
130
  if file_extension == 'pdf':
131
- text = ""
132
- with open(file_path, 'rb') as file:
133
- pdf_reader = PyPDF2.PdfReader(file)
134
- for page_num in range(len(pdf_reader.pages)):
135
- page = pdf_reader.pages[page_num]
136
- text += page.extract_text() + "\n\n"
137
- return text
 
 
 
 
 
 
 
138
 
139
  elif file_extension == 'md':
140
- with open(file_path, 'r', encoding='utf-8') as file:
141
- md_text = file.read()
142
- # You can convert markdown to plain text if needed
143
- return md_text
 
 
 
 
144
 
145
  elif file_extension == 'txt':
146
  with open(file_path, 'r', encoding='utf-8') as file:
@@ -184,7 +260,7 @@ def prepare_message_with_media(text, images=None, documents=None):
184
  return text
185
 
186
  # If we have images, create a multimodal content array
187
- content = [{"type": "text", "text": text}]
188
 
189
  # Add images if any
190
  if images:
@@ -203,10 +279,14 @@ def prepare_message_with_media(text, images=None, documents=None):
203
 
204
  def ask_ai(message, chatbot, model_choice, temperature, max_tokens, top_p, frequency_penalty,
205
  presence_penalty, images, documents, reasoning_effort):
206
- """Enhanced AI query function with comprehensive options"""
207
  if not message.strip() and not images and not documents:
208
  return chatbot, ""
209
 
 
 
 
 
210
  # Get model ID and context size
211
  model_id = None
212
  context_size = 0
@@ -239,11 +319,18 @@ def ask_ai(message, chatbot, model_choice, temperature, max_tokens, top_p, frequ
239
  "messages": messages,
240
  "temperature": temperature,
241
  "max_tokens": max_tokens,
242
- "top_p": top_p,
243
- "frequency_penalty": frequency_penalty,
244
- "presence_penalty": presence_penalty
245
  }
246
 
 
 
 
 
 
 
 
 
 
 
247
  # Add reasoning if selected
248
  if reasoning_effort != "none":
249
  payload["reasoning"] = {
@@ -266,7 +353,7 @@ def ask_ai(message, chatbot, model_choice, temperature, max_tokens, top_p, frequ
266
  logger.info(f"Response status: {response.status_code}")
267
 
268
  response_text = response.text
269
- logger.info(f"Response body: {response_text}")
270
 
271
  if response.status_code == 200:
272
  result = response.json()
@@ -288,6 +375,29 @@ def ask_ai(message, chatbot, model_choice, temperature, max_tokens, top_p, frequ
288
  def clear_chat():
289
  return [], "", [], [], 0.7, 1000, 0.8, 0.0, 0.0, "none"
290
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
291
  def filter_models(search_term):
292
  """Filter models based on search term"""
293
  if not search_term:
@@ -316,6 +426,19 @@ def update_context_display(model_name):
316
  return f"{context_formatted} tokens"
317
  return "Unknown"
318
 
 
 
 
 
 
 
 
 
 
 
 
 
 
319
  # Create enhanced interface
320
  with gr.Blocks(css="""
321
  .context-size {
@@ -335,9 +458,9 @@ with gr.Blocks(css="""
335
  }
336
  """) as demo:
337
  gr.Markdown("""
338
- # Enhanced AI Chat
339
 
340
- Chat with various AI models from OpenRouter with support for images and documents.
341
  """)
342
 
343
  with gr.Row():
@@ -365,7 +488,7 @@ with gr.Blocks(css="""
365
 
366
  with gr.Row():
367
  # Image upload
368
- with gr.Accordion("Upload Images (for vision models)", open=False):
369
  images = gr.Gallery(
370
  label="Uploaded Images",
371
  show_label=True,
@@ -424,6 +547,15 @@ with gr.Blocks(css="""
424
  [model[0] for model in MODELS[0]["models"]],
425
  label="Models in Category"
426
  )
 
 
 
 
 
 
 
 
 
427
 
428
  with gr.Accordion("Generation Parameters", open=False):
429
  with gr.Group(elem_classes="parameter-grid"):
@@ -472,81 +604,363 @@ with gr.Blocks(css="""
472
  value="none",
473
  label="Reasoning Effort"
474
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
475
 
476
- # Connect model search to dropdown filter
477
- model_search.change(
478
- fn=filter_models,
479
- inputs=[model_search],
480
- outputs=[model_choice]
481
- )
 
 
482
 
483
- # Update context display when model changes
484
- model_choice.change(
485
- fn=update_context_display,
486
- inputs=[model_choice],
487
- outputs=[context_display]
488
- )
489
 
490
- # Update model list when category changes
491
- def update_category_models(category):
492
- for cat in MODELS:
493
- if cat["category"] == category:
494
- return gr.Radio.update(choices=[model[0] for model in cat["models"]], value=cat["models"][0][0])
495
- return gr.Radio.update(choices=[], value=None)
496
-
497
- model_categories.change(
498
- fn=update_category_models,
499
- inputs=[model_categories],
500
- outputs=[category_models]
501
- )
502
 
503
- # Update main model choice when category model is selected
504
- category_models.change(
505
- fn=lambda x: x,
506
- inputs=[category_models],
507
- outputs=[model_choice]
508
- )
 
 
509
 
510
- # Process uploaded images
511
- def process_uploaded_images(files):
512
- return [file.name for file in files]
513
 
514
- image_upload_btn.upload(
515
- fn=process_uploaded_images,
516
- inputs=[image_upload_btn],
517
- outputs=[images]
518
- )
519
 
520
- # Set up events
521
- submit_btn.click(
522
- fn=ask_ai,
523
- inputs=[
524
- message, chatbot, model_choice, temperature, max_tokens,
525
- top_p, frequency_penalty, presence_penalty, images,
526
- documents, reasoning_effort
527
- ],
528
- outputs=[chatbot, message]
529
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
530
 
531
- message.submit(
532
- fn=ask_ai,
533
- inputs=[
534
- message, chatbot, model_choice, temperature, max_tokens,
535
- top_p, frequency_penalty, presence_penalty, images,
536
- documents, reasoning_effort
537
- ],
538
- outputs=[chatbot, message]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
539
  )
540
 
541
- clear_btn.click(
542
- fn=clear_chat,
543
- inputs=[],
544
- outputs=[
545
- chatbot, message, images, documents, temperature,
546
- max_tokens, top_p, frequency_penalty, presence_penalty, reasoning_effort
547
- ]
 
 
 
 
 
 
 
 
 
 
 
 
548
  )
549
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
550
  # Launch directly with Gradio's built-in server
551
  if __name__ == "__main__":
552
  demo.launch(server_name="0.0.0.0", server_port=7860)
 
1
  import os
2
+ import logging
 
3
  import json
4
  import base64
5
+ from io import BytesIO
 
 
 
 
6
 
7
  # Configure logging
8
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
9
  logger = logging.getLogger(__name__)
10
 
11
+ # Graceful imports with fallbacks
12
+ try:
13
+ import gradio as gr
14
+ except ImportError:
15
+ logger.error("Gradio not found. Please install with 'pip install gradio'")
16
+ raise
17
+
18
+ try:
19
+ import requests
20
+ except ImportError:
21
+ logger.error("Requests not found. Please install with 'pip install requests'")
22
+ raise
23
+
24
+ # Optional libraries with fallbacks
25
+ try:
26
+ from PIL import Image
27
+ PIL_AVAILABLE = True
28
+ except ImportError:
29
+ logger.warning("PIL not found. Image processing functionality will be limited.")
30
+ PIL_AVAILABLE = False
31
+
32
+ # PDF processing
33
+ PDF_AVAILABLE = False
34
+ try:
35
+ import PyPDF2
36
+ PDF_AVAILABLE = True
37
+ except ImportError:
38
+ logger.warning("PyPDF2 not found. Attempting to use pdfminer.six as fallback...")
39
+ try:
40
+ from pdfminer.high_level import extract_text as pdf_extract_text
41
+ PDF_AVAILABLE = True
42
+
43
+ # Create a wrapper to mimic PyPDF2 functionality
44
+ def extract_text_from_pdf(file_path):
45
+ return pdf_extract_text(file_path)
46
+ except ImportError:
47
+ logger.warning("No PDF processing libraries found. PDF support will be disabled.")
48
+
49
+ # Markdown processing
50
+ MD_AVAILABLE = False
51
+ try:
52
+ import markdown
53
+ MD_AVAILABLE = True
54
+ except ImportError:
55
+ logger.warning("Markdown not found. Attempting to use markdownify as fallback...")
56
+ try:
57
+ from markdownify import markdownify as md
58
+ MD_AVAILABLE = True
59
+
60
+ # Create a wrapper for markdown
61
+ def convert_markdown(text):
62
+ return md(text)
63
+ except ImportError:
64
+ logger.warning("No Markdown processing libraries found. Markdown support will be limited.")
65
+
66
  # API key
67
  OPENROUTER_API_KEY = os.environ.get("OPENROUTER_API_KEY", "")
68
 
69
+ # Model list with context sizes - organized by capability
70
  MODELS = [
71
  # Vision Models
72
+ {"category": "Vision Models", "models": [
73
+ ("Google: Gemini Pro 2.0 Experimental", "google/gemini-2.0-pro-exp-02-05:free", 2000000),
74
+ ("Google: Gemini Pro 2.5 Experimental", "google/gemini-2.5-pro-exp-03-25:free", 1000000),
75
+ ("Google: Gemini 2.0 Flash Thinking Experimental", "google/gemini-2.0-flash-thinking-exp:free", 1048576),
76
+ ("Google: Gemini Flash 2.0 Experimental", "google/gemini-2.0-flash-exp:free", 1048576),
77
+ ("Google: Gemini Flash 1.5 8B Experimental", "google/gemini-flash-1.5-8b-exp", 1000000),
78
+ ("Google: Gemini 2.0 Flash Thinking Experimental", "google/gemini-2.0-flash-thinking-exp-1219:free", 40000),
79
  ("Meta: Llama 3.2 11B Vision Instruct", "meta-llama/llama-3.2-11b-vision-instruct:free", 131072),
80
+ ("Qwen: Qwen2.5 VL 72B Instruct", "qwen/qwen2.5-vl-72b-instruct:free", 131072),
81
+ ("Qwen: Qwen2.5 VL 32B Instruct", "qwen/qwen2.5-vl-32b-instruct:free", 8192),
82
+ ("Qwen: Qwen2.5 VL 7B Instruct", "qwen/qwen-2.5-vl-7b-instruct:free", 64000),
83
+ ("Qwen: Qwen2.5 VL 3B Instruct", "qwen/qwen2.5-vl-3b-instruct:free", 64000),
84
+ ("Bytedance: UI-TARS 72B", "bytedance-research/ui-tars-72b:free", 32768),
85
  ]},
86
 
87
+ # Largest Context Models
88
+ {"category": "Largest Context (500K+)", "models": [
89
+ ("Google: Gemini Pro 2.0 Experimental", "google/gemini-2.0-pro-exp-02-05:free", 2000000),
90
+ ("Google: Gemini 2.0 Flash Thinking Experimental", "google/gemini-2.0-flash-thinking-exp:free", 1048576),
91
+ ("Google: Gemini Flash 2.0 Experimental", "google/gemini-2.0-flash-exp:free", 1048576),
92
+ ("Google: Gemini Pro 2.5 Experimental", "google/gemini-2.5-pro-exp-03-25:free", 1000000),
93
+ ("Google: Gemini Flash 1.5 8B Experimental", "google/gemini-flash-1.5-8b-exp", 1000000),
 
94
  ]},
95
 
96
+ # High-performance Models
97
+ {"category": "High Performance", "models": [
98
+ ("Google: Gemini Pro 2.0 Experimental", "google/gemini-2.0-pro-exp-02-05:free", 2000000),
99
+ ("Google: Gemini Pro 2.5 Experimental", "google/gemini-2.5-pro-exp-03-25:free", 1000000),
100
+ ("Google: Gemma 3 27B", "google/gemma-3-27b-it:free", 96000),
101
+ ("Mistral: Mistral Small 3.1 24B", "mistralai/mistral-small-3.1-24b-instruct:free", 96000),
102
+ ("Qwen: Qwen2.5 VL 72B Instruct", "qwen/qwen2.5-vl-72b-instruct:free", 131072),
 
103
  ]},
104
 
105
+ # Mid-size Models
106
+ {"category": "Mid-size Models", "models": [
107
+ ("Google: Gemma 3 12B", "google/gemma-3-12b-it:free", 131072),
108
+ ("Google: Gemma 3 4B", "google/gemma-3-4b-it:free", 131072),
109
+ ("Google: LearnLM 1.5 Pro Experimental", "google/learnlm-1.5-pro-experimental:free", 40960),
110
+ ("Meta: Llama 3.1 8B Instruct", "meta-llama/llama-3.1-8b-instruct:free", 131072),
 
 
 
 
111
  ]},
112
 
113
+ # Smaller Models
114
+ {"category": "Smaller Models", "models": [
115
+ ("Google: Gemma 3 1B", "google/gemma-3-1b-it:free", 32768),
116
+ ("Qwen: Qwen2.5 VL 3B Instruct", "qwen/qwen2.5-vl-3b-instruct:free", 64000),
117
+ ("AllenAI: Molmo 7B D", "allenai/molmo-7b-d:free", 4096),
 
 
 
 
118
  ]},
119
 
120
+ # Sorting Options
121
+ {"category": "Sort By", "models": [
122
+ ("Context: High to Low", "sort_context_desc", 0),
123
+ ("Context: Low to High", "sort_context_asc", 0),
124
+ ("Newest", "sort_newest", 0),
125
+ ("Throughput: High to Low", "sort_throughput", 0),
126
+ ("Latency: Low to High", "sort_latency", 0),
 
 
 
 
127
  ]},
128
  ]
129
 
130
  # Flatten model list for easy searching
131
  ALL_MODELS = []
132
  for category in MODELS:
133
+ if category["category"] != "Sort By": # Skip the sorting options
134
+ for model in category["models"]:
135
+ if model not in ALL_MODELS:
136
+ ALL_MODELS.append(model)
137
+
138
+ # Sort models by context size (descending) by default
139
+ ALL_MODELS.sort(key=lambda x: x[2], reverse=True)
140
 
141
  def format_to_message_dict(history):
142
  """Convert history to proper message format"""
 
151
  return messages
152
 
153
  def encode_image_to_base64(image_path):
154
+ """Encode an image file to base64 string with fallback methods"""
155
  try:
156
  if isinstance(image_path, str): # File path as string
157
  with open(image_path, "rb") as image_file:
158
  encoded_string = base64.b64encode(image_file.read()).decode('utf-8')
159
  file_extension = image_path.split('.')[-1].lower()
160
  mime_type = f"image/{file_extension}"
161
+ if file_extension in ["jpg", "jpeg"]:
162
  mime_type = "image/jpeg"
163
+ elif file_extension == "png":
164
+ mime_type = "image/png"
165
+ elif file_extension in ["webp", "gif"]:
166
+ mime_type = f"image/{file_extension}"
167
+ else:
168
+ mime_type = "image/jpeg" # Default fallback
169
  return f"data:{mime_type};base64,{encoded_string}"
170
+ elif PIL_AVAILABLE: # Pillow Image object
171
+ buffered = BytesIO()
172
+ # Handle if it's a PIL Image or file-like object
173
+ try:
174
+ image_path.save(buffered, format="PNG")
175
+ except AttributeError:
176
+ if hasattr(image_path, 'read'):
177
+ # It's a file-like object but not a PIL Image
178
+ buffered.write(image_path.read())
179
+ else:
180
+ raise
181
  encoded_string = base64.b64encode(buffered.getvalue()).decode('utf-8')
182
  return f"data:image/png;base64,{encoded_string}"
183
+ else:
184
+ logger.error("Cannot process image: PIL not available and input is not a file path")
185
+ return None
186
  except Exception as e:
187
  logger.error(f"Error encoding image: {str(e)}")
188
  return None
189
 
190
  def extract_text_from_file(file_path):
191
+ """Extract text from various file types with fallbacks"""
192
  try:
193
  file_extension = file_path.split('.')[-1].lower()
194
 
195
  if file_extension == 'pdf':
196
+ if PDF_AVAILABLE:
197
+ if 'PyPDF2' in globals():
198
+ text = ""
199
+ with open(file_path, 'rb') as file:
200
+ pdf_reader = PyPDF2.PdfReader(file)
201
+ for page_num in range(len(pdf_reader.pages)):
202
+ page = pdf_reader.pages[page_num]
203
+ text += page.extract_text() + "\n\n"
204
+ return text
205
+ else:
206
+ # Use pdfminer fallback
207
+ return extract_text_from_pdf(file_path)
208
+ else:
209
+ return "PDF support not available. Please install PyPDF2 or pdfminer.six."
210
 
211
  elif file_extension == 'md':
212
+ if MD_AVAILABLE:
213
+ with open(file_path, 'r', encoding='utf-8') as file:
214
+ md_text = file.read()
215
+ return md_text
216
+ else:
217
+ # Simple fallback - just read the file
218
+ with open(file_path, 'r', encoding='utf-8') as file:
219
+ return file.read()
220
 
221
  elif file_extension == 'txt':
222
  with open(file_path, 'r', encoding='utf-8') as file:
 
260
  return text
261
 
262
  # If we have images, create a multimodal content array
263
+ content = [{"type": "text", "text": text or "Please analyze these images:"}]
264
 
265
  # Add images if any
266
  if images:
 
279
 
280
  def ask_ai(message, chatbot, model_choice, temperature, max_tokens, top_p, frequency_penalty,
281
  presence_penalty, images, documents, reasoning_effort):
282
+ """Enhanced AI query function with comprehensive options and fallbacks"""
283
  if not message.strip() and not images and not documents:
284
  return chatbot, ""
285
 
286
+ # Check if this is a sorting option
287
+ if model_choice.startswith("Sort By"):
288
+ return chatbot + [[message, "Please select a model to chat with first."]], ""
289
+
290
  # Get model ID and context size
291
  model_id = None
292
  context_size = 0
 
319
  "messages": messages,
320
  "temperature": temperature,
321
  "max_tokens": max_tokens,
 
 
 
322
  }
323
 
324
+ # Add optional parameters if they have non-default values
325
+ if top_p < 1.0:
326
+ payload["top_p"] = top_p
327
+
328
+ if frequency_penalty != 0:
329
+ payload["frequency_penalty"] = frequency_penalty
330
+
331
+ if presence_penalty != 0:
332
+ payload["presence_penalty"] = presence_penalty
333
+
334
  # Add reasoning if selected
335
  if reasoning_effort != "none":
336
  payload["reasoning"] = {
 
353
  logger.info(f"Response status: {response.status_code}")
354
 
355
  response_text = response.text
356
+ logger.debug(f"Response body: {response_text}")
357
 
358
  if response.status_code == 200:
359
  result = response.json()
 
375
  def clear_chat():
376
  return [], "", [], [], 0.7, 1000, 0.8, 0.0, 0.0, "none"
377
 
378
+ def apply_sort(sort_option):
379
+ """Apply sorting option to models list"""
380
+ if sort_option == "sort_context_desc":
381
+ # Sort by context size (high to low)
382
+ sorted_models = sorted(ALL_MODELS, key=lambda x: x[2], reverse=True)
383
+ elif sort_option == "sort_context_asc":
384
+ # Sort by context size (low to high)
385
+ sorted_models = sorted(ALL_MODELS, key=lambda x: x[2])
386
+ elif sort_option == "sort_newest":
387
+ # This would need a proper timestamp, using a rough approximation
388
+ # Models with "Experimental" in the name come first as they're likely newer
389
+ sorted_models = sorted(ALL_MODELS, key=lambda x: "Experimental" not in x[0])
390
+ elif sort_option == "sort_throughput" or sort_option == "sort_latency":
391
+ # These would need actual performance metrics
392
+ # For now, use model size as a rough proxy (smaller models generally have higher throughput and lower latency)
393
+ # Rough heuristic: models with smaller numbers in their names might be smaller
394
+ sorted_models = sorted(ALL_MODELS, key=lambda x: sum(int(s) for s in x[0] if s.isdigit()))
395
+ else:
396
+ # Default to context size sorting
397
+ sorted_models = sorted(ALL_MODELS, key=lambda x: x[2], reverse=True)
398
+
399
+ return sorted_models
400
+
401
  def filter_models(search_term):
402
  """Filter models based on search term"""
403
  if not search_term:
 
426
  return f"{context_formatted} tokens"
427
  return "Unknown"
428
 
429
+ def update_models_from_sort(sort_option):
430
+ """Update models list based on sorting option"""
431
+ for category in MODELS:
432
+ if category["category"] == "Sort By":
433
+ for option in category["models"]:
434
+ if option[0] == sort_option:
435
+ sort_key = option[1]
436
+ sorted_models = apply_sort(sort_key)
437
+ return gr.Dropdown.update(choices=[model[0] for model in sorted_models], value=sorted_models[0][0])
438
+
439
+ # Default sorting if option not found
440
+ return gr.Dropdown.update(choices=[model[0] for model in ALL_MODELS], value=ALL_MODELS[0][0])
441
+
442
  # Create enhanced interface
443
  with gr.Blocks(css="""
444
  .context-size {
 
458
  }
459
  """) as demo:
460
  gr.Markdown("""
461
+ # Vision AI Chat
462
 
463
+ Chat with various AI vision models from OpenRouter with support for images and documents.
464
  """)
465
 
466
  with gr.Row():
 
488
 
489
  with gr.Row():
490
  # Image upload
491
+ with gr.Accordion("Upload Images", open=False):
492
  images = gr.Gallery(
493
  label="Uploaded Images",
494
  show_label=True,
 
547
  [model[0] for model in MODELS[0]["models"]],
548
  label="Models in Category"
549
  )
550
+
551
+ # Sort options
552
+ with gr.Accordion("Sort Models", open=False):
553
+ sort_options = gr.Radio(
554
+ ["Context: High to Low", "Context: Low to High", "Newest",
555
+ "Throughput: High to Low", "Latency: Low to High"],
556
+ label="Sort By",
557
+ value="Context: High to Low"
558
+ )
559
 
560
  with gr.Accordion("Generation Parameters", open=False):
561
  with gr.Group(elem_classes="parameter-grid"):
 
604
  value="none",
605
  label="Reasoning Effort"
606
  )
607
+
608
+ with gr.Accordion("Advanced Options", open=False):
609
+ with gr.Row():
610
+ with gr.Column():
611
+ repetition_penalty = gr.Slider(
612
+ minimum=0.1,
613
+ maximum=2.0,
614
+ value=1.0,
615
+ step=0.1,
616
+ label="Repetition Penalty"
617
+ )
618
+
619
+ top_k = gr.Slider(
620
+ minimum=1,
621
+ maximum=100,
622
+ value=40,
623
+ step=1,
624
+ label="Top K"
625
+ )
626
+
627
+ min_p = gr.Slider(
628
+ minimum=0.0,
629
+ maximum=1.0,
630
+ value=0.1,
631
+ step=0.05,
632
+ label="Min P"
633
+ )
634
+
635
+ with gr.Column():
636
+ seed = gr.Number(
637
+ value=0,
638
+ label="Seed (0 for random)",
639
+ precision=0
640
+ )
641
+
642
+ top_a = gr.Slider(
643
+ minimum=0.0,
644
+ maximum=1.0,
645
+ value=0.0,
646
+ step=0.05,
647
+ label="Top A"
648
+ )
649
+
650
+ stream_output = gr.Checkbox(
651
+ label="Stream Output",
652
+ value=False
653
+ )
654
+
655
+ with gr.Row():
656
+ response_format = gr.Radio(
657
+ ["default", "json_object"],
658
+ value="default",
659
+ label="Response Format"
660
+ )
661
+
662
+ gr.Markdown("""
663
+ * **json_object**: Forces the model to respond with valid JSON only.
664
+ * Only available on certain models - check model support on OpenRouter.
665
+ """)
666
+
667
+ # Custom instructing options
668
+ with gr.Accordion("Custom Instructions", open=False):
669
+ system_message = gr.Textbox(
670
+ placeholder="Enter a system message to guide the model's behavior...",
671
+ label="System Message",
672
+ lines=3
673
+ )
674
+
675
+ transforms = gr.CheckboxGroup(
676
+ ["prompt_optimize", "prompt_distill", "prompt_compress"],
677
+ label="Prompt Transforms (OpenRouter specific)"
678
+ )
679
+
680
+ gr.Markdown("""
681
+ * **prompt_optimize**: Improve prompt for better responses.
682
+ * **prompt_distill**: Compress prompt to use fewer tokens without changing meaning.
683
+ * **prompt_compress**: Aggressively compress prompt to fit larger contexts.
684
+ """)
685
+
686
+ # Connect model search to dropdown filter
687
+ model_search.change(
688
+ fn=filter_models,
689
+ inputs=[model_search],
690
+ outputs=[model_choice]
691
+ )
692
+
693
+ # Update context display when model changes
694
+ model_choice.change(
695
+ fn=update_context_display,
696
+ inputs=[model_choice],
697
+ outputs=[context_display]
698
+ )
699
+
700
+ # Update model list when category changes
701
+ def update_category_models(category):
702
+ for cat in MODELS:
703
+ if cat["category"] == category:
704
+ return gr.Radio.update(choices=[model[0] for model in cat["models"]], value=cat["models"][0][0])
705
+ return gr.Radio.update(choices=[], value=None)
706
+
707
+ model_categories.change(
708
+ fn=update_category_models,
709
+ inputs=[model_categories],
710
+ outputs=[category_models]
711
+ )
712
+
713
+ # Update main model choice when category model is selected
714
+ category_models.change(
715
+ fn=lambda x: x,
716
+ inputs=[category_models],
717
+ outputs=[model_choice]
718
+ )
719
+
720
+ # Process uploaded images
721
+ def process_uploaded_images(files):
722
+ return [file.name for file in files]
723
+
724
+ image_upload_btn.upload(
725
+ fn=process_uploaded_images,
726
+ inputs=[image_upload_btn],
727
+ outputs=[images]
728
+ )
729
+
730
+ # Enhanced AI query function with all advanced parameters
731
+ def ask_ai(message, chatbot, model_choice, temperature, max_tokens, top_p,
732
+ frequency_penalty, presence_penalty, repetition_penalty, top_k,
733
+ min_p, seed, top_a, stream_output, response_format,
734
+ images, documents, reasoning_effort, system_message, transforms):
735
+ """Comprehensive AI query function with all parameters"""
736
+ if not message.strip() and not images and not documents:
737
+ return chatbot, ""
738
 
739
+ # Get model ID and context size
740
+ model_id = None
741
+ context_size = 0
742
+ for name, model_id_value, ctx_size in ALL_MODELS:
743
+ if name == model_choice:
744
+ model_id = model_id_value
745
+ context_size = ctx_size
746
+ break
747
 
748
+ if model_id is None:
749
+ logger.error(f"Model not found: {model_choice}")
750
+ return chatbot + [[message, "Error: Model not found"]], ""
 
 
 
751
 
752
+ # Create messages from chatbot history
753
+ messages = format_to_message_dict(chatbot)
 
 
 
 
 
 
 
 
 
 
754
 
755
+ # Add system message if provided
756
+ if system_message and system_message.strip():
757
+ # Insert at the beginning to override any existing system message
758
+ for i, msg in enumerate(messages):
759
+ if msg.get("role") == "system":
760
+ messages.pop(i)
761
+ break
762
+ messages.insert(0, {"role": "system", "content": system_message.strip()})
763
 
764
+ # Prepare message with images and documents if any
765
+ content = prepare_message_with_media(message, images, documents)
 
766
 
767
+ # Add current message
768
+ messages.append({"role": "user", "content": content})
 
 
 
769
 
770
+ # Call API
771
+ try:
772
+ logger.info(f"Sending request to model: {model_id}")
773
+
774
+ # Build the comprehensive payload with all parameters
775
+ payload = {
776
+ "model": model_id,
777
+ "messages": messages,
778
+ "temperature": temperature,
779
+ "max_tokens": max_tokens,
780
+ "top_p": top_p,
781
+ "frequency_penalty": frequency_penalty,
782
+ "presence_penalty": presence_penalty,
783
+ "repetition_penalty": repetition_penalty if repetition_penalty != 1.0 else None,
784
+ "top_k": top_k,
785
+ "min_p": min_p if min_p > 0 else None,
786
+ "seed": seed if seed > 0 else None,
787
+ "top_a": top_a if top_a > 0 else None,
788
+ "stream": stream_output
789
+ }
790
+
791
+ # Add response format if not default
792
+ if response_format == "json_object":
793
+ payload["response_format"] = {"type": "json_object"}
794
+
795
+ # Add reasoning if selected
796
+ if reasoning_effort != "none":
797
+ payload["reasoning"] = {
798
+ "effort": reasoning_effort
799
+ }
800
+
801
+ # Add transforms if selected
802
+ if transforms:
803
+ payload["transforms"] = transforms
804
+
805
+ # Remove None values
806
+ payload = {k: v for k, v in payload.items() if v is not None}
807
+
808
+ logger.info(f"Request payload: {json.dumps(payload, default=str)}")
809
+
810
+ response = requests.post(
811
+ "https://openrouter.ai/api/v1/chat/completions",
812
+ headers={
813
+ "Content-Type": "application/json",
814
+ "Authorization": f"Bearer {OPENROUTER_API_KEY}",
815
+ "HTTP-Referer": "https://huggingface.co/spaces"
816
+ },
817
+ json=payload,
818
+ timeout=180, # Longer timeout for document processing and streaming
819
+ stream=stream_output
820
+ )
821
+
822
+ logger.info(f"Response status: {response.status_code}")
823
+
824
+ if stream_output and response.status_code == 200:
825
+ # Handle streaming response
826
+ chatbot = chatbot + [[message, ""]]
827
+
828
+ for line in response.iter_lines():
829
+ if line:
830
+ line = line.decode('utf-8')
831
+ if line.startswith('data: '):
832
+ data = line[6:]
833
+ if data.strip() == '[DONE]':
834
+ break
835
+ try:
836
+ chunk = json.loads(data)
837
+ if "choices" in chunk and len(chunk["choices"]) > 0:
838
+ delta = chunk["choices"][0].get("delta", {})
839
+ if "content" in delta and delta["content"]:
840
+ chatbot[-1][1] += delta["content"]
841
+ yield chatbot, ""
842
+ except json.JSONDecodeError:
843
+ continue
844
+ return chatbot, ""
845
+
846
+ elif response.status_code == 200:
847
+ # Handle normal response
848
+ result = response.json()
849
+ ai_response = result.get("choices", [{}])[0].get("message", {}).get("content", "")
850
+ chatbot = chatbot + [[message, ai_response]]
851
+
852
+ # Log token usage if available
853
+ if "usage" in result:
854
+ logger.info(f"Token usage: {result['usage']}")
855
+ else:
856
+ response_text = response.text
857
+ logger.info(f"Error response body: {response_text}")
858
+ error_message = f"Error: Status code {response.status_code}\n\nResponse: {response_text}"
859
+ chatbot = chatbot + [[message, error_message]]
860
+ except Exception as e:
861
+ logger.error(f"Exception during API call: {str(e)}")
862
+ chatbot = chatbot + [[message, f"Error: {str(e)}"]]
863
 
864
+ return chatbot, ""
865
+
866
+ # Function to clear chat and reset parameters
867
+ def clear_chat():
868
+ return [], "", [], [], 0.7, 1000, 0.8, 0.0, 0.0, 1.0, 40, 0.1, 0, 0.0, False, "default", "none", "", []
869
+
870
+ # Set up events for the submit button
871
+ submit_btn.click(
872
+ fn=ask_ai,
873
+ inputs=[
874
+ message, chatbot, model_choice, temperature, max_tokens,
875
+ top_p, frequency_penalty, presence_penalty, repetition_penalty,
876
+ top_k, min_p, seed, top_a, stream_output, response_format,
877
+ images, documents, reasoning_effort, system_message, transforms
878
+ ],
879
+ outputs=[chatbot, message]
880
+ )
881
+
882
+ # Set up events for message submission (pressing Enter)
883
+ message.submit(
884
+ fn=ask_ai,
885
+ inputs=[
886
+ message, chatbot, model_choice, temperature, max_tokens,
887
+ top_p, frequency_penalty, presence_penalty, repetition_penalty,
888
+ top_k, min_p, seed, top_a, stream_output, response_format,
889
+ images, documents, reasoning_effort, system_message, transforms
890
+ ],
891
+ outputs=[chatbot, message]
892
+ )
893
+
894
+ # Set up events for the clear button
895
+ clear_btn.click(
896
+ fn=clear_chat,
897
+ inputs=[],
898
+ outputs=[
899
+ chatbot, message, images, documents, temperature,
900
+ max_tokens, top_p, frequency_penalty, presence_penalty,
901
+ repetition_penalty, top_k, min_p, seed, top_a, stream_output,
902
+ response_format, reasoning_effort, system_message, transforms
903
+ ]
904
+ )
905
+
906
+ # Add a model information section
907
+ with gr.Accordion("About Selected Model", open=False):
908
+ model_info_display = gr.HTML(
909
+ value="<p>Select a model to see details</p>"
910
  )
911
 
912
+ # Update model info when model changes
913
+ def update_model_info(model_name):
914
+ model_info = get_model_info(model_name)
915
+ if model_info:
916
+ name, model_id, context_size = model_info
917
+ return f"""
918
+ <div class="model-info">
919
+ <h3>{name}</h3>
920
+ <p><strong>Model ID:</strong> {model_id}</p>
921
+ <p><strong>Context Size:</strong> {context_size:,} tokens</p>
922
+ <p><strong>Provider:</strong> {model_id.split('/')[0]}</p>
923
+ </div>
924
+ """
925
+ return "<p>Model information not available</p>"
926
+
927
+ model_choice.change(
928
+ fn=update_model_info,
929
+ inputs=[model_choice],
930
+ outputs=[model_info_display]
931
  )
932
 
933
+ # Add usage instructions
934
+ with gr.Accordion("Usage Instructions", open=False):
935
+ gr.Markdown("""
936
+ ## Basic Usage
937
+ 1. Type your message in the input box
938
+ 2. Select a model from the dropdown
939
+ 3. Click "Send" or press Enter
940
+
941
+ ## Working with Files
942
+ - **Images**: Upload images to use with vision-capable models like Llama 3.2 Vision
943
+ - **Documents**: Upload PDF, Markdown, or text files to analyze their content
944
+
945
+ ## Advanced Parameters
946
+ - **Temperature**: Controls randomness (higher = more creative, lower = more deterministic)
947
+ - **Max Tokens**: Maximum length of the response
948
+ - **Top P**: Nucleus sampling threshold (higher = consider more tokens)
949
+ - **Reasoning Effort**: Some models can show their reasoning process
950
+
951
+ ## Tips
952
+ - For code generation, use models like Qwen Coder
953
+ - For visual tasks, choose vision-capable models
954
+ - For long context, check the context window size next to the model name
955
+ """)
956
+
957
+ # Add a footer with version info
958
+ footer_md = gr.Markdown("""
959
+ ---
960
+ ### OpenRouter AI Chat Interface v1.0
961
+ Built with ❤️ using Gradio and OpenRouter API | Context sizes shown next to model names
962
+ """)
963
+
964
  # Launch directly with Gradio's built-in server
965
  if __name__ == "__main__":
966
  demo.launch(server_name="0.0.0.0", server_port=7860)