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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +502 -649
app.py CHANGED
@@ -1,142 +1,141 @@
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,72 +150,54 @@ def format_to_message_dict(history):
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,7 +241,7 @@ def prepare_message_with_media(text, images=None, documents=None):
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:
@@ -277,127 +258,6 @@ def prepare_message_with_media(text, images=None, documents=None):
277
 
278
  return content
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
293
- for name, model_id_value, ctx_size in ALL_MODELS:
294
- if name == model_choice:
295
- model_id = model_id_value
296
- context_size = ctx_size
297
- break
298
-
299
- if model_id is None:
300
- logger.error(f"Model not found: {model_choice}")
301
- return chatbot + [[message, "Error: Model not found"]], ""
302
-
303
- # Create messages from chatbot history
304
- messages = format_to_message_dict(chatbot)
305
-
306
- # Prepare message with images and documents if any
307
- content = prepare_message_with_media(message, images, documents)
308
-
309
- # Add current message
310
- messages.append({"role": "user", "content": content})
311
-
312
- # Call API
313
- try:
314
- logger.info(f"Sending request to model: {model_id}")
315
-
316
- # Build the payload with all parameters
317
- payload = {
318
- "model": model_id,
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"] = {
337
- "effort": reasoning_effort
338
- }
339
-
340
- logger.info(f"Request payload: {json.dumps(payload, default=str)}")
341
-
342
- response = requests.post(
343
- "https://openrouter.ai/api/v1/chat/completions",
344
- headers={
345
- "Content-Type": "application/json",
346
- "Authorization": f"Bearer {OPENROUTER_API_KEY}",
347
- "HTTP-Referer": "https://huggingface.co/spaces"
348
- },
349
- json=payload,
350
- timeout=120 # Longer timeout for document processing
351
- )
352
-
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()
360
- ai_response = result.get("choices", [{}])[0].get("message", {}).get("content", "")
361
- chatbot = chatbot + [[message, ai_response]]
362
-
363
- # Log token usage if available
364
- if "usage" in result:
365
- logger.info(f"Token usage: {result['usage']}")
366
- else:
367
- error_message = f"Error: Status code {response.status_code}\n\nResponse: {response_text}"
368
- chatbot = chatbot + [[message, error_message]]
369
- except Exception as e:
370
- logger.error(f"Exception during API call: {str(e)}")
371
- chatbot = chatbot + [[message, f"Error: {str(e)}"]]
372
-
373
- return chatbot, ""
374
-
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,308 +286,13 @@ def update_context_display(model_name):
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 {
445
- font-size: 0.9em;
446
- color: #666;
447
- margin-left: 10px;
448
- }
449
- footer { display: none !important; }
450
- .model-selection-row {
451
- display: flex;
452
- align-items: center;
453
- }
454
- .parameter-grid {
455
- display: grid;
456
- grid-template-columns: 1fr 1fr;
457
- gap: 10px;
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():
467
- with gr.Column(scale=2):
468
- chatbot = gr.Chatbot(
469
- height=500,
470
- show_copy_button=True,
471
- show_label=False,
472
- avatar_images=(None, "https://upload.wikimedia.org/wikipedia/commons/0/04/ChatGPT_logo.svg")
473
- )
474
-
475
- with gr.Row():
476
- message = gr.Textbox(
477
- placeholder="Type your message here...",
478
- label="Message",
479
- lines=2
480
- )
481
-
482
- with gr.Row():
483
- with gr.Column(scale=3):
484
- submit_btn = gr.Button("Send", variant="primary")
485
-
486
- with gr.Column(scale=1):
487
- clear_btn = gr.Button("Clear Chat", variant="secondary")
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,
495
- columns=4,
496
- height="auto",
497
- object_fit="contain"
498
- )
499
-
500
- image_upload_btn = gr.UploadButton(
501
- label="Upload Images",
502
- file_types=["image"],
503
- file_count="multiple"
504
- )
505
-
506
- # Document upload
507
- with gr.Accordion("Upload Documents (PDF, MD, TXT)", open=False):
508
- documents = gr.File(
509
- label="Uploaded Documents",
510
- file_types=[".pdf", ".md", ".txt"],
511
- file_count="multiple"
512
- )
513
-
514
- with gr.Column(scale=1):
515
- with gr.Group():
516
- gr.Markdown("### Model Selection")
517
-
518
- with gr.Row(elem_classes="model-selection-row"):
519
- model_search = gr.Textbox(
520
- placeholder="Search models...",
521
- label="",
522
- show_label=False
523
- )
524
-
525
- with gr.Row(elem_classes="model-selection-row"):
526
- model_choice = gr.Dropdown(
527
- [model[0] for model in ALL_MODELS],
528
- value=ALL_MODELS[0][0],
529
- label="Model"
530
- )
531
- context_display = gr.Textbox(
532
- value=update_context_display(ALL_MODELS[0][0]),
533
- label="Context",
534
- interactive=False,
535
- elem_classes="context-size"
536
- )
537
-
538
- # Model category selection
539
- with gr.Accordion("Browse by Category", open=False):
540
- model_categories = gr.Radio(
541
- [category["category"] for category in MODELS],
542
- label="Categories",
543
- value=MODELS[0]["category"]
544
- )
545
-
546
- category_models = gr.Radio(
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"):
562
- temperature = gr.Slider(
563
- minimum=0.0,
564
- maximum=2.0,
565
- value=0.7,
566
- step=0.1,
567
- label="Temperature"
568
- )
569
-
570
- max_tokens = gr.Slider(
571
- minimum=100,
572
- maximum=4000,
573
- value=1000,
574
- step=100,
575
- label="Max Tokens"
576
- )
577
-
578
- top_p = gr.Slider(
579
- minimum=0.1,
580
- maximum=1.0,
581
- value=0.8,
582
- step=0.1,
583
- label="Top P"
584
- )
585
-
586
- frequency_penalty = gr.Slider(
587
- minimum=-2.0,
588
- maximum=2.0,
589
- value=0.0,
590
- step=0.1,
591
- label="Frequency Penalty"
592
- )
593
-
594
- presence_penalty = gr.Slider(
595
- minimum=-2.0,
596
- maximum=2.0,
597
- value=0.0,
598
- step=0.1,
599
- label="Presence Penalty"
600
- )
601
-
602
- reasoning_effort = gr.Radio(
603
- ["none", "low", "medium", "high"],
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,
@@ -863,104 +428,392 @@ def ask_ai(message, chatbot, model_choice, temperature, max_tokens, top_p,
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)
 
1
  import os
2
+ import gradio as gr
3
+ import requests
4
  import json
5
  import base64
6
+ import logging
7
+ import io
8
 
9
  # Configure logging
10
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
11
  logger = logging.getLogger(__name__)
12
 
13
+ # Gracefully import libraries with fallbacks
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  try:
15
  from PIL import Image
 
16
  except ImportError:
17
+ logger.warning("PIL not installed. Image processing will be limited.")
18
+ Image = None
19
 
 
 
20
  try:
21
  import PyPDF2
 
22
  except ImportError:
23
+ logger.warning("PyPDF2 not installed. PDF processing will be limited.")
24
+ PyPDF2 = None
 
 
 
 
 
 
 
 
25
 
 
 
26
  try:
27
  import markdown
 
28
  except ImportError:
29
+ logger.warning("Markdown not installed. Markdown processing will be limited.")
30
+ markdown = None
 
 
 
 
 
 
 
 
31
 
32
  # API key
33
  OPENROUTER_API_KEY = os.environ.get("OPENROUTER_API_KEY", "")
34
 
35
+ # Complete model list with context sizes - as per requested list
36
  MODELS = [
37
+ # 1M+ Context Models
38
+ {"category": "1M+ Context", "models": [
39
  ("Google: Gemini Pro 2.0 Experimental", "google/gemini-2.0-pro-exp-02-05:free", 2000000),
40
+ ("Google: Gemini 2.0 Flash Thinking Experimental 01-21", "google/gemini-2.0-flash-thinking-exp:free", 1048576),
 
41
  ("Google: Gemini Flash 2.0 Experimental", "google/gemini-2.0-flash-exp:free", 1048576),
42
+ ("Google: Gemini Pro 2.5 Experimental", "google/gemini-2.5-pro-exp-03-25:free", 1000000),
43
  ("Google: Gemini Flash 1.5 8B Experimental", "google/gemini-flash-1.5-8b-exp", 1000000),
 
 
 
 
 
 
 
44
  ]},
45
 
46
+ # 100K-1M Context Models
47
+ {"category": "100K+ Context", "models": [
48
+ ("DeepSeek: DeepSeek R1 Zero", "deepseek/deepseek-r1-zero:free", 163840),
49
+ ("DeepSeek: R1", "deepseek/deepseek-r1:free", 163840),
50
+ ("DeepSeek: DeepSeek V3 Base", "deepseek/deepseek-v3-base:free", 131072),
51
+ ("DeepSeek: DeepSeek V3 0324", "deepseek/deepseek-chat-v3-0324:free", 131072),
52
+ ("Google: Gemma 3 4B", "google/gemma-3-4b-it:free", 131072),
53
+ ("Google: Gemma 3 12B", "google/gemma-3-12b-it:free", 131072),
54
+ ("Nous: DeepHermes 3 Llama 3 8B Preview", "nousresearch/deephermes-3-llama-3-8b-preview:free", 131072),
55
+ ("Qwen: Qwen2.5 VL 72B Instruct", "qwen/qwen2.5-vl-72b-instruct:free", 131072),
56
+ ("DeepSeek: DeepSeek V3", "deepseek/deepseek-chat:free", 131072),
57
+ ("NVIDIA: Llama 3.1 Nemotron 70B Instruct", "nvidia/llama-3.1-nemotron-70b-instruct:free", 131072),
58
+ ("Meta: Llama 3.2 1B Instruct", "meta-llama/llama-3.2-1b-instruct:free", 131072),
59
+ ("Meta: Llama 3.2 11B Vision Instruct", "meta-llama/llama-3.2-11b-vision-instruct:free", 131072),
60
+ ("Meta: Llama 3.1 8B Instruct", "meta-llama/llama-3.1-8b-instruct:free", 131072),
61
+ ("Mistral: Mistral Nemo", "mistralai/mistral-nemo:free", 128000),
62
  ]},
63
 
64
+ # 64K-100K Context Models
65
+ {"category": "64K-100K Context", "models": [
 
 
 
66
  ("Mistral: Mistral Small 3.1 24B", "mistralai/mistral-small-3.1-24b-instruct:free", 96000),
67
+ ("Google: Gemma 3 27B", "google/gemma-3-27b-it:free", 96000),
68
+ ("Qwen: Qwen2.5 VL 3B Instruct", "qwen/qwen2.5-vl-3b-instruct:free", 64000),
69
+ ("DeepSeek: R1 Distill Qwen 14B", "deepseek/deepseek-r1-distill-qwen-14b:free", 64000),
70
+ ("Qwen: Qwen2.5-VL 7B Instruct", "qwen/qwen-2.5-vl-7b-instruct:free", 64000),
71
  ]},
72
 
73
+ # 32K-64K Context Models
74
+ {"category": "32K-64K Context", "models": [
 
 
75
  ("Google: LearnLM 1.5 Pro Experimental", "google/learnlm-1.5-pro-experimental:free", 40960),
76
+ ("Qwen: QwQ 32B", "qwen/qwq-32b:free", 40000),
77
+ ("Google: Gemini 2.0 Flash Thinking Experimental", "google/gemini-2.0-flash-thinking-exp-1219:free", 40000),
78
+ ("Bytedance: UI-TARS 72B", "bytedance-research/ui-tars-72b:free", 32768),
79
+ ("Qwerky 72b", "featherless/qwerky-72b:free", 32768),
80
+ ("OlympicCoder 7B", "open-r1/olympiccoder-7b:free", 32768),
81
+ ("OlympicCoder 32B", "open-r1/olympiccoder-32b:free", 32768),
82
+ ("Google: Gemma 3 1B", "google/gemma-3-1b-it:free", 32768),
83
+ ("Reka: Flash 3", "rekaai/reka-flash-3:free", 32768),
84
+ ("Dolphin3.0 R1 Mistral 24B", "cognitivecomputations/dolphin3.0-r1-mistral-24b:free", 32768),
85
+ ("Dolphin3.0 Mistral 24B", "cognitivecomputations/dolphin3.0-mistral-24b:free", 32768),
86
+ ("Mistral: Mistral Small 3", "mistralai/mistral-small-24b-instruct-2501:free", 32768),
87
+ ("Qwen2.5 Coder 32B Instruct", "qwen/qwen-2.5-coder-32b-instruct:free", 32768),
88
+ ("Qwen2.5 72B Instruct", "qwen/qwen-2.5-72b-instruct:free", 32768),
89
  ]},
90
 
91
+ # 8K-32K Context Models
92
+ {"category": "8K-32K Context", "models": [
93
+ ("Meta: Llama 3.2 3B Instruct", "meta-llama/llama-3.2-3b-instruct:free", 20000),
94
+ ("Qwen: QwQ 32B Preview", "qwen/qwq-32b-preview:free", 16384),
95
+ ("DeepSeek: R1 Distill Qwen 32B", "deepseek/deepseek-r1-distill-qwen-32b:free", 16000),
96
+ ("Qwen: Qwen2.5 VL 32B Instruct", "qwen/qwen2.5-vl-32b-instruct:free", 8192),
97
+ ("Moonshot AI: Moonlight 16B A3B Instruct", "moonshotai/moonlight-16b-a3b-instruct:free", 8192),
98
+ ("DeepSeek: R1 Distill Llama 70B", "deepseek/deepseek-r1-distill-llama-70b:free", 8192),
99
+ ("Qwen 2 7B Instruct", "qwen/qwen-2-7b-instruct:free", 8192),
100
+ ("Google: Gemma 2 9B", "google/gemma-2-9b-it:free", 8192),
101
+ ("Mistral: Mistral 7B Instruct", "mistralai/mistral-7b-instruct:free", 8192),
102
+ ("Microsoft: Phi-3 Mini 128K Instruct", "microsoft/phi-3-mini-128k-instruct:free", 8192),
103
+ ("Microsoft: Phi-3 Medium 128K Instruct", "microsoft/phi-3-medium-128k-instruct:free", 8192),
104
+ ("Meta: Llama 3 8B Instruct", "meta-llama/llama-3-8b-instruct:free", 8192),
105
+ ("OpenChat 3.5 7B", "openchat/openchat-7b:free", 8192),
106
+ ("Meta: Llama 3.3 70B Instruct", "meta-llama/llama-3.3-70b-instruct:free", 8000),
107
+ ]},
108
+
109
+ # <8K Context Models
110
+ {"category": "4K Context", "models": [
111
  ("AllenAI: Molmo 7B D", "allenai/molmo-7b-d:free", 4096),
112
+ ("Rogue Rose 103B v0.2", "sophosympatheia/rogue-rose-103b-v0.2:free", 4096),
113
+ ("Toppy M 7B", "undi95/toppy-m-7b:free", 4096),
114
+ ("Hugging Face: Zephyr 7B", "huggingfaceh4/zephyr-7b-beta:free", 4096),
115
+ ("MythoMax 13B", "gryphe/mythomax-l2-13b:free", 4096),
116
  ]},
117
 
118
+ # Vision-capable Models
119
+ {"category": "Vision Models", "models": [
120
+ ("Meta: Llama 3.2 11B Vision Instruct", "meta-llama/llama-3.2-11b-vision-instruct:free", 131072),
121
+ ("Qwen: Qwen2.5 VL 72B Instruct", "qwen/qwen2.5-vl-72b-instruct:free", 131072),
122
+ ("Qwen: Qwen2.5 VL 32B Instruct", "qwen/qwen2.5-vl-32b-instruct:free", 8192),
123
+ ("Qwen: Qwen2.5-VL 7B Instruct", "qwen/qwen-2.5-vl-7b-instruct:free", 64000),
124
+ ("Qwen: Qwen2.5 VL 3B Instruct", "qwen/qwen2.5-vl-3b-instruct:free", 64000),
125
+ ("Google: Gemini Pro 2.0 Experimental", "google/gemini-2.0-pro-exp-02-05:free", 2000000),
126
+ ("Google: Gemini Pro 2.5 Experimental", "google/gemini-2.5-pro-exp-03-25:free", 1000000),
127
+ ("Google: Gemini 2.0 Flash Thinking Experimental", "google/gemini-2.0-flash-thinking-exp:free", 1048576),
128
+ ("Google: Gemini Flash 2.0 Experimental", "google/gemini-2.0-flash-exp:free", 1048576),
129
+ ("AllenAI: Molmo 7B D", "allenai/molmo-7b-d:free", 4096),
130
  ]},
131
  ]
132
 
133
  # Flatten model list for easy searching
134
  ALL_MODELS = []
135
  for category in MODELS:
136
+ for model in category["models"]:
137
+ if model not in ALL_MODELS: # Avoid duplicates
138
+ ALL_MODELS.append(model)
 
 
 
 
139
 
140
  def format_to_message_dict(history):
141
  """Convert history to proper message format"""
 
150
  return messages
151
 
152
  def encode_image_to_base64(image_path):
153
+ """Encode an image file to base64 string"""
154
  try:
155
  if isinstance(image_path, str): # File path as string
156
  with open(image_path, "rb") as image_file:
157
  encoded_string = base64.b64encode(image_file.read()).decode('utf-8')
158
  file_extension = image_path.split('.')[-1].lower()
159
  mime_type = f"image/{file_extension}"
160
+ if file_extension == "jpg" or file_extension == "jpeg":
161
  mime_type = "image/jpeg"
162
  elif file_extension == "png":
163
  mime_type = "image/png"
164
+ elif file_extension == "webp":
165
+ mime_type = "image/webp"
 
 
166
  return f"data:{mime_type};base64,{encoded_string}"
167
+ else: # Pillow Image or file-like object
168
+ if Image is not None:
169
+ buffered = io.BytesIO()
 
170
  image_path.save(buffered, format="PNG")
171
+ encoded_string = base64.b64encode(buffered.getvalue()).decode('utf-8')
172
+ return f"data:image/png;base64,{encoded_string}"
173
+ else:
174
+ logger.error("PIL is not installed, cannot process image object")
175
+ return None
 
 
 
 
 
 
176
  except Exception as e:
177
  logger.error(f"Error encoding image: {str(e)}")
178
  return None
179
 
180
  def extract_text_from_file(file_path):
181
+ """Extract text from various file types"""
182
  try:
183
  file_extension = file_path.split('.')[-1].lower()
184
 
185
  if file_extension == 'pdf':
186
+ if PyPDF2 is not None:
187
+ text = ""
188
+ with open(file_path, 'rb') as file:
189
+ pdf_reader = PyPDF2.PdfReader(file)
190
+ for page_num in range(len(pdf_reader.pages)):
191
+ page = pdf_reader.pages[page_num]
192
+ text += page.extract_text() + "\n\n"
193
+ return text
 
 
 
 
194
  else:
195
+ return "PDF processing is not available (PyPDF2 not installed)"
196
 
197
  elif file_extension == 'md':
198
+ with open(file_path, 'r', encoding='utf-8') as file:
199
+ md_text = file.read()
200
+ return md_text
 
 
 
 
 
201
 
202
  elif file_extension == 'txt':
203
  with open(file_path, 'r', encoding='utf-8') as file:
 
241
  return text
242
 
243
  # If we have images, create a multimodal content array
244
+ content = [{"type": "text", "text": text}]
245
 
246
  # Add images if any
247
  if images:
 
258
 
259
  return content
260
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
261
  def filter_models(search_term):
262
  """Filter models based on search term"""
263
  if not search_term:
 
286
  return f"{context_formatted} tokens"
287
  return "Unknown"
288
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
289
  def update_category_models(category):
290
+ """Update models list when category changes"""
291
  for cat in MODELS:
292
  if cat["category"] == category:
293
  return gr.Radio.update(choices=[model[0] for model in cat["models"]], value=cat["models"][0][0])
294
  return gr.Radio.update(choices=[], value=None)
295
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
296
  def ask_ai(message, chatbot, model_choice, temperature, max_tokens, top_p,
297
  frequency_penalty, presence_penalty, repetition_penalty, top_k,
298
  min_p, seed, top_a, stream_output, response_format,
 
428
 
429
  return chatbot, ""
430
 
431
+ def process_uploaded_images(files):
432
+ """Process uploaded image files"""
433
+ return [file.name for file in files]
434
+
435
  def clear_chat():
436
+ """Reset all inputs"""
437
  return [], "", [], [], 0.7, 1000, 0.8, 0.0, 0.0, 1.0, 40, 0.1, 0, 0.0, False, "default", "none", "", []
438
 
439
+ # Create requirements.txt content
440
+ requirements = """
441
+ gradio>=4.44.1
442
+ requests>=2.28.1
443
+ Pillow>=9.0.0
444
+ PyPDF2>=3.0.0
445
+ markdown>=3.4.1
446
+ """
447
+
448
+ # Main application
449
+ def create_app():
450
+ with gr.Blocks(css="""
451
+ .context-size {
452
+ font-size: 0.9em;
453
+ color: #666;
454
+ margin-left: 10px;
455
+ }
456
+ footer { display: none !important; }
457
+ .model-selection-row {
458
+ display: flex;
459
+ align-items: center;
460
+ }
461
+ .parameter-grid {
462
+ display: grid;
463
+ grid-template-columns: 1fr 1fr;
464
+ gap: 10px;
465
+ }
466
+ """) as demo:
467
+ gr.Markdown("""
468
+ # CrispChat
469
+
470
+ Chat with various AI models from OpenRouter with support for images and documents.
471
+ """)
472
+
473
+ with gr.Row():
474
+ with gr.Column(scale=2):
475
+ chatbot = gr.Chatbot(
476
+ height=500,
477
+ show_copy_button=True,
478
+ show_label=False,
479
+ avatar_images=(None, "https://upload.wikimedia.org/wikipedia/commons/0/04/ChatGPT_logo.svg"),
480
+ type="messages" # Fixed: Use messages format instead of tuples
481
+ )
482
+
483
+ with gr.Row():
484
+ message = gr.Textbox(
485
+ placeholder="Type your message here...",
486
+ label="Message",
487
+ lines=2
488
+ )
489
+
490
+ with gr.Row():
491
+ with gr.Column(scale=3):
492
+ submit_btn = gr.Button("Send", variant="primary")
493
+
494
+ with gr.Column(scale=1):
495
+ clear_btn = gr.Button("Clear Chat", variant="secondary")
496
+
497
+ with gr.Row():
498
+ # Image upload
499
+ with gr.Accordion("Upload Images (for vision models)", open=False):
500
+ images = gr.Gallery(
501
+ label="Uploaded Images",
502
+ show_label=True,
503
+ columns=4,
504
+ height="auto",
505
+ object_fit="contain"
506
+ )
507
+
508
+ image_upload_btn = gr.UploadButton(
509
+ label="Upload Images",
510
+ file_types=["image"],
511
+ file_count="multiple"
512
+ )
513
+
514
+ # Document upload
515
+ with gr.Accordion("Upload Documents (PDF, MD, TXT)", open=False):
516
+ documents = gr.File(
517
+ label="Uploaded Documents",
518
+ file_types=[".pdf", ".md", ".txt"],
519
+ file_count="multiple"
520
+ )
521
+
522
+ with gr.Column(scale=1):
523
+ with gr.Group():
524
+ gr.Markdown("### Model Selection")
525
+
526
+ with gr.Row(elem_classes="model-selection-row"):
527
+ model_search = gr.Textbox(
528
+ placeholder="Search models...",
529
+ label="",
530
+ show_label=False
531
+ )
532
+
533
+ with gr.Row(elem_classes="model-selection-row"):
534
+ model_choice = gr.Dropdown(
535
+ [model[0] for model in ALL_MODELS],
536
+ value=ALL_MODELS[0][0],
537
+ label="Model"
538
+ )
539
+ context_display = gr.Textbox(
540
+ value=update_context_display(ALL_MODELS[0][0]),
541
+ label="Context",
542
+ interactive=False,
543
+ elem_classes="context-size"
544
+ )
545
+
546
+ # Model category selection
547
+ with gr.Accordion("Browse by Category", open=False):
548
+ model_categories = gr.Radio(
549
+ [category["category"] for category in MODELS],
550
+ label="Categories",
551
+ value=MODELS[0]["category"]
552
+ )
553
+
554
+ category_models = gr.Radio(
555
+ [model[0] for model in MODELS[0]["models"]],
556
+ label="Models in Category"
557
+ )
558
+
559
+ with gr.Accordion("Generation Parameters", open=False):
560
+ with gr.Group(elem_classes="parameter-grid"):
561
+ temperature = gr.Slider(
562
+ minimum=0.0,
563
+ maximum=2.0,
564
+ value=0.7,
565
+ step=0.1,
566
+ label="Temperature"
567
+ )
568
+
569
+ max_tokens = gr.Slider(
570
+ minimum=100,
571
+ maximum=4000,
572
+ value=1000,
573
+ step=100,
574
+ label="Max Tokens"
575
+ )
576
+
577
+ top_p = gr.Slider(
578
+ minimum=0.1,
579
+ maximum=1.0,
580
+ value=0.8,
581
+ step=0.1,
582
+ label="Top P"
583
+ )
584
+
585
+ frequency_penalty = gr.Slider(
586
+ minimum=-2.0,
587
+ maximum=2.0,
588
+ value=0.0,
589
+ step=0.1,
590
+ label="Frequency Penalty"
591
+ )
592
+
593
+ presence_penalty = gr.Slider(
594
+ minimum=-2.0,
595
+ maximum=2.0,
596
+ value=0.0,
597
+ step=0.1,
598
+ label="Presence Penalty"
599
+ )
600
+
601
+ reasoning_effort = gr.Radio(
602
+ ["none", "low", "medium", "high"],
603
+ value="none",
604
+ label="Reasoning Effort"
605
+ )
606
+
607
+ with gr.Accordion("Advanced Options", open=False):
608
+ with gr.Row():
609
+ with gr.Column():
610
+ repetition_penalty = gr.Slider(
611
+ minimum=0.1,
612
+ maximum=2.0,
613
+ value=1.0,
614
+ step=0.1,
615
+ label="Repetition Penalty"
616
+ )
617
+
618
+ top_k = gr.Slider(
619
+ minimum=1,
620
+ maximum=100,
621
+ value=40,
622
+ step=1,
623
+ label="Top K"
624
+ )
625
+
626
+ min_p = gr.Slider(
627
+ minimum=0.0,
628
+ maximum=1.0,
629
+ value=0.1,
630
+ step=0.05,
631
+ label="Min P"
632
+ )
633
+
634
+ with gr.Column():
635
+ seed = gr.Number(
636
+ value=0,
637
+ label="Seed (0 for random)",
638
+ precision=0
639
+ )
640
+
641
+ top_a = gr.Slider(
642
+ minimum=0.0,
643
+ maximum=1.0,
644
+ value=0.0,
645
+ step=0.05,
646
+ label="Top A"
647
+ )
648
+
649
+ stream_output = gr.Checkbox(
650
+ label="Stream Output",
651
+ value=False
652
+ )
653
+
654
+ with gr.Row():
655
+ response_format = gr.Radio(
656
+ ["default", "json_object"],
657
+ value="default",
658
+ label="Response Format"
659
+ )
660
+
661
+ gr.Markdown("""
662
+ * **json_object**: Forces the model to respond with valid JSON only.
663
+ * Only available on certain models - check model support on OpenRouter.
664
+ """)
665
+
666
+ # Custom instructing options
667
+ with gr.Accordion("Custom Instructions", open=False):
668
+ system_message = gr.Textbox(
669
+ placeholder="Enter a system message to guide the model's behavior...",
670
+ label="System Message",
671
+ lines=3
672
+ )
673
+
674
+ transforms = gr.CheckboxGroup(
675
+ ["prompt_optimize", "prompt_distill", "prompt_compress"],
676
+ label="Prompt Transforms (OpenRouter specific)"
677
+ )
678
+
679
+ gr.Markdown("""
680
+ * **prompt_optimize**: Improve prompt for better responses.
681
+ * **prompt_distill**: Compress prompt to use fewer tokens without changing meaning.
682
+ * **prompt_compress**: Aggressively compress prompt to fit larger contexts.
683
+ """)
684
+
685
+ # Add a model information section
686
+ with gr.Accordion("About Selected Model", open=False):
687
+ model_info_display = gr.HTML(
688
+ value="<p>Select a model to see details</p>"
689
+ )
690
+
691
+ # Add usage instructions
692
+ with gr.Accordion("Usage Instructions", open=False):
693
+ gr.Markdown("""
694
+ ## Basic Usage
695
+ 1. Type your message in the input box
696
+ 2. Select a model from the dropdown
697
+ 3. Click "Send" or press Enter
698
+
699
+ ## Working with Files
700
+ - **Images**: Upload images to use with vision-capable models
701
+ - **Documents**: Upload PDF, Markdown, or text files to analyze their content
702
+
703
+ ## Advanced Parameters
704
+ - **Temperature**: Controls randomness (higher = more creative, lower = more deterministic)
705
+ - **Max Tokens**: Maximum length of the response
706
+ - **Top P**: Nucleus sampling threshold (higher = consider more tokens)
707
+ - **Reasoning Effort**: Some models can show their reasoning process
708
+
709
+ ## Tips
710
+ - For code generation, use models like Qwen Coder
711
+ - For visual tasks, choose vision-capable models
712
+ - For long context, check the context window size next to the model name
713
+ """)
714
+
715
+ # Add a footer with version info
716
+ footer_md = gr.Markdown("""
717
+ ---
718
+ ### OpenRouter AI Chat Interface v1.0
719
+ Built with ❤️ using Gradio and OpenRouter API | Context sizes shown next to model names
720
+ """)
721
+
722
+ # Connect model search to dropdown filter
723
+ model_search.change(
724
+ fn=filter_models,
725
+ inputs=[model_search],
726
+ outputs=[model_choice]
727
+ )
728
+
729
+ # Update context display when model changes
730
+ model_choice.change(
731
+ fn=update_context_display,
732
+ inputs=[model_choice],
733
+ outputs=[context_display]
734
+ )
735
+
736
+ # Update model list when category changes
737
+ model_categories.change(
738
+ fn=update_category_models,
739
+ inputs=[model_categories],
740
+ outputs=[category_models]
741
+ )
742
+
743
+ # Update main model choice when category model is selected
744
+ category_models.change(
745
+ fn=lambda x: x,
746
+ inputs=[category_models],
747
+ outputs=[model_choice]
748
+ )
749
+
750
+ # Process uploaded images
751
+ image_upload_btn.upload(
752
+ fn=process_uploaded_images,
753
+ inputs=[image_upload_btn],
754
+ outputs=[images]
755
+ )
756
+
757
+ # Update model info when model changes
758
+ def update_model_info(model_name):
759
+ model_info = get_model_info(model_name)
760
+ if model_info:
761
+ name, model_id, context_size = model_info
762
+ return f"""
763
+ <div class="model-info">
764
+ <h3>{name}</h3>
765
+ <p><strong>Model ID:</strong> {model_id}</p>
766
+ <p><strong>Context Size:</strong> {context_size:,} tokens</p>
767
+ <p><strong>Provider:</strong> {model_id.split('/')[0]}</p>
768
+ </div>
769
+ """
770
+ return "<p>Model information not available</p>"
771
+
772
+ model_choice.change(
773
+ fn=update_model_info,
774
+ inputs=[model_choice],
775
+ outputs=[model_info_display]
776
+ )
777
+
778
+ # Set up events for the submit button
779
+ submit_btn.click(
780
+ fn=ask_ai,
781
+ inputs=[
782
+ message, chatbot, model_choice, temperature, max_tokens,
783
+ top_p, frequency_penalty, presence_penalty, repetition_penalty,
784
+ top_k, min_p, seed, top_a, stream_output, response_format,
785
+ images, documents, reasoning_effort, system_message, transforms
786
+ ],
787
+ outputs=[chatbot, message]
788
+ )
789
+
790
+ # Set up events for message submission (pressing Enter)
791
+ message.submit(
792
+ fn=ask_ai,
793
+ inputs=[
794
+ message, chatbot, model_choice, temperature, max_tokens,
795
+ top_p, frequency_penalty, presence_penalty, repetition_penalty,
796
+ top_k, min_p, seed, top_a, stream_output, response_format,
797
+ images, documents, reasoning_effort, system_message, transforms
798
+ ],
799
+ outputs=[chatbot, message]
800
+ )
801
+
802
+ # Set up events for the clear button
803
+ clear_btn.click(
804
+ fn=clear_chat,
805
+ inputs=[],
806
+ outputs=[
807
+ chatbot, message, images, documents, temperature,
808
+ max_tokens, top_p, frequency_penalty, presence_penalty,
809
+ repetition_penalty, top_k, min_p, seed, top_a, stream_output,
810
+ response_format, reasoning_effort, system_message, transforms
811
+ ]
812
+ )
813
+
814
+ return demo
815
 
816
+ # Launch the app
817
  if __name__ == "__main__":
818
+ demo = create_app()
819
  demo.launch(server_name="0.0.0.0", server_port=7860)