Update app.py
Browse files
app.py
CHANGED
@@ -1,94 +1,142 @@
|
|
1 |
import os
|
2 |
-
import
|
3 |
-
import requests
|
4 |
import json
|
5 |
import base64
|
6 |
-
from
|
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
|
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 |
-
#
|
31 |
-
{"category": "
|
32 |
-
("Gemini Pro 2.0 Experimental", "google/gemini-2.0-pro-exp-02-05:free", 2000000),
|
33 |
-
("Gemini
|
34 |
-
("Gemini 2.0
|
35 |
-
("Gemini
|
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 |
-
#
|
41 |
-
{"category": "
|
42 |
-
("
|
43 |
-
("
|
44 |
-
("
|
45 |
-
("
|
46 |
-
("
|
47 |
-
("Llama 3.1 Nemotron 70B Instruct", "nvidia/llama-3.1-nemotron-70b-instruct:free", 131072),
|
48 |
]},
|
49 |
|
50 |
-
#
|
51 |
-
{"category": "
|
52 |
-
("
|
53 |
-
("
|
54 |
-
("
|
55 |
-
("
|
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 |
-
#
|
63 |
-
{"category": "
|
64 |
-
("
|
65 |
-
("
|
66 |
-
("
|
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 |
-
#
|
74 |
-
{"category": "
|
75 |
-
("
|
76 |
-
("
|
77 |
-
("
|
78 |
-
("
|
79 |
-
("
|
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 |
-
|
91 |
-
|
|
|
|
|
|
|
|
|
|
|
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
|
114 |
mime_type = "image/jpeg"
|
|
|
|
|
|
|
|
|
|
|
|
|
115 |
return f"data:{mime_type};base64,{encoded_string}"
|
116 |
-
|
117 |
-
buffered =
|
118 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
132 |
-
|
133 |
-
|
134 |
-
|
135 |
-
|
136 |
-
|
137 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
138 |
|
139 |
elif file_extension == 'md':
|
140 |
-
|
141 |
-
|
142 |
-
|
143 |
-
|
|
|
|
|
|
|
|
|
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.
|
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 |
-
#
|
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
|
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 |
-
#
|
477 |
-
|
478 |
-
|
479 |
-
|
480 |
-
|
481 |
-
|
|
|
|
|
482 |
|
483 |
-
|
484 |
-
|
485 |
-
|
486 |
-
inputs=[model_choice],
|
487 |
-
outputs=[context_display]
|
488 |
-
)
|
489 |
|
490 |
-
#
|
491 |
-
|
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 |
-
#
|
504 |
-
|
505 |
-
|
506 |
-
|
507 |
-
|
508 |
-
|
|
|
|
|
509 |
|
510 |
-
#
|
511 |
-
|
512 |
-
return [file.name for file in files]
|
513 |
|
514 |
-
|
515 |
-
|
516 |
-
inputs=[image_upload_btn],
|
517 |
-
outputs=[images]
|
518 |
-
)
|
519 |
|
520 |
-
#
|
521 |
-
|
522 |
-
|
523 |
-
|
524 |
-
|
525 |
-
|
526 |
-
|
527 |
-
|
528 |
-
|
529 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
530 |
|
531 |
-
|
532 |
-
|
533 |
-
|
534 |
-
|
535 |
-
|
536 |
-
|
537 |
-
|
538 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
539 |
)
|
540 |
|
541 |
-
|
542 |
-
|
543 |
-
|
544 |
-
|
545 |
-
|
546 |
-
|
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)
|