app.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668
  1. from flask import Flask, request, jsonify, render_template_string
  2. from datetime import datetime
  3. import json
  4. import os
  5. import requests
  6. app = Flask(__name__)
  7. # Store recent requests in memory
  8. recent_requests = []
  9. MAX_REQUESTS = 50
  10. # OpenAI endpoint configuration
  11. OPENAI_ENDPOINT = os.getenv(
  12. "OPENAI_ENDPOINT", "http://localhost:11434/v1/chat/completions"
  13. )
  14. OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "none")
  15. OPENAI_MODEL = os.getenv("OPENAI_MODEL", "InternVL3_5-14B")
  16. # Video format: 'openai' (data URL), 'vllm' (try vLLM format), 'skip', or 'error'
  17. VIDEO_FORMAT = os.getenv("VIDEO_FORMAT", "openai")
  18. # NEW: Endpoint type detection
  19. ENDPOINT_TYPE = os.getenv("ENDPOINT_TYPE", "auto") # 'openai', 'ollama', or 'auto'
  20. def detect_endpoint_type(endpoint_url):
  21. """Auto-detect if endpoint is OpenAI-compatible or Ollama native"""
  22. if "/v1/chat/completions" in endpoint_url:
  23. return "openai"
  24. elif "/api/generate" in endpoint_url or "/api/chat" in endpoint_url:
  25. return "ollama"
  26. elif "localhost:11434" in endpoint_url or "ollama" in endpoint_url.lower():
  27. return "openai" # Assume OpenAI-compatible for Ollama
  28. else:
  29. return "openai" # Default to OpenAI format
  30. def convert_gemini_to_openai(gemini_request):
  31. """Convert Gemini API format to OpenAI API format"""
  32. try:
  33. contents = gemini_request.get("contents", [])
  34. messages = []
  35. media_info = {"images": [], "videos": []}
  36. for content in contents:
  37. parts = content.get("parts", [])
  38. message_content = []
  39. for part in parts:
  40. # Handle text parts
  41. if "text" in part:
  42. message_content.append({"type": "text", "text": part["text"]})
  43. # Handle inline_data (images/video)
  44. elif "inline_data" in part:
  45. inline = part["inline_data"]
  46. mime_type = inline.get("mime_type", "")
  47. data = inline.get("data", "")
  48. if mime_type.startswith("image/"):
  49. # Images: Universally supported across all runners
  50. media_info["images"].append(mime_type)
  51. print(f"🖼️ Adding image: {mime_type}")
  52. message_content.append(
  53. {
  54. "type": "image_url",
  55. "image_url": {
  56. "url": f"data:{mime_type};base64,{data}",
  57. "detail": "auto",
  58. },
  59. }
  60. )
  61. elif mime_type.startswith("video/"):
  62. # Videos: Format depends on VIDEO_FORMAT setting
  63. if VIDEO_FORMAT == "skip":
  64. media_info["videos"].append(f"skipped ({mime_type})")
  65. print(f"⏭️ Skipping video: {mime_type} (VIDEO_FORMAT=skip)")
  66. message_content.append(
  67. {
  68. "type": "text",
  69. "text": f"[Video content ({mime_type}) was present but skipped]",
  70. }
  71. )
  72. elif VIDEO_FORMAT == "error":
  73. raise ValueError(
  74. f"Video content detected ({mime_type}) but VIDEO_FORMAT=error"
  75. )
  76. else: # 'openai', 'vllm', or any other value
  77. media_info["videos"].append(
  78. f"format: {VIDEO_FORMAT} ({mime_type})"
  79. )
  80. print(
  81. f"📹 Adding video ({VIDEO_FORMAT} format): {mime_type}"
  82. )
  83. message_content.append(
  84. {
  85. "type": "image_url",
  86. "image_url": {
  87. "url": f"data:{mime_type};base64,{data}",
  88. "detail": "auto",
  89. },
  90. }
  91. )
  92. # Add as user message
  93. # If only one content item and it's text, send as string for better compatibility
  94. if len(message_content) == 1 and message_content[0].get("type") == "text":
  95. messages.append({"role": "user", "content": message_content[0]["text"]})
  96. else:
  97. messages.append({"role": "user", "content": message_content})
  98. # Build OpenAI request
  99. openai_request = {"model": OPENAI_MODEL, "messages": messages}
  100. # Add generation config as OpenAI parameters
  101. gen_config = gemini_request.get("generationConfig", {})
  102. if "maxOutputTokens" in gen_config:
  103. openai_request["max_tokens"] = gen_config["maxOutputTokens"]
  104. if "temperature" in gen_config:
  105. openai_request["temperature"] = gen_config["temperature"]
  106. # Log media summary
  107. if media_info["images"] or media_info["videos"]:
  108. print(f"📊 Media summary:")
  109. if media_info["images"]:
  110. print(
  111. f" Images: {len(media_info['images'])} ({', '.join(media_info['images'])})"
  112. )
  113. if media_info["videos"]:
  114. print(f" Videos: {', '.join(media_info['videos'])}")
  115. return openai_request
  116. except Exception as e:
  117. print(f"❌ Error converting request: {e}")
  118. raise
  119. def convert_gemini_to_ollama(gemini_request):
  120. """Convert Gemini API format to Ollama native format"""
  121. try:
  122. contents = gemini_request.get("contents", [])
  123. # Extract text and combine into a single prompt
  124. prompt_parts = []
  125. images = []
  126. for content in contents:
  127. parts = content.get("parts", [])
  128. for part in parts:
  129. if "text" in part:
  130. prompt_parts.append(part["text"])
  131. elif "inline_data" in part:
  132. inline = part["inline_data"]
  133. mime_type = inline.get("mime_type", "")
  134. data = inline.get("data", "")
  135. if mime_type.startswith("image/") or mime_type.startswith("video/"):
  136. # Ollama expects images in a different format
  137. images.append(data) # Just the base64 data
  138. print(f"🖼️ Adding media for Ollama: {mime_type}")
  139. # Build Ollama request
  140. ollama_request = {
  141. "model": OPENAI_MODEL,
  142. "prompt": " ".join(prompt_parts),
  143. "stream": False,
  144. }
  145. # Add images if present
  146. if images:
  147. ollama_request["images"] = images
  148. # Add generation config
  149. gen_config = gemini_request.get("generationConfig", {})
  150. if "temperature" in gen_config:
  151. ollama_request["options"] = {"temperature": gen_config["temperature"]}
  152. return ollama_request
  153. except Exception as e:
  154. print(f"❌ Error converting to Ollama format: {e}")
  155. raise
  156. def convert_openai_to_gemini(openai_response):
  157. """Convert OpenAI API response to Gemini API format"""
  158. try:
  159. # Extract the message content
  160. choices = openai_response.get("choices", [])
  161. if not choices:
  162. print(f"❌ No choices in OpenAI response: {openai_response}")
  163. return {"error": "No response generated"}
  164. message = choices[0].get("message", {})
  165. content = message.get("content", "")
  166. if not content:
  167. print(f"❌ No content in message: {message}")
  168. return {"error": "No response generated"}
  169. # Convert to Gemini format
  170. gemini_response = {
  171. "candidates": [
  172. {
  173. "content": {"parts": [{"text": content}], "role": "model"},
  174. "finishReason": "STOP",
  175. "index": 0,
  176. }
  177. ],
  178. "usageMetadata": {
  179. "promptTokenCount": openai_response.get("usage", {}).get(
  180. "prompt_tokens", 0
  181. ),
  182. "candidatesTokenCount": openai_response.get("usage", {}).get(
  183. "completion_tokens", 0
  184. ),
  185. "totalTokenCount": openai_response.get("usage", {}).get(
  186. "total_tokens", 0
  187. ),
  188. },
  189. }
  190. return gemini_response
  191. except Exception as e:
  192. print(f"❌ Error converting OpenAI response: {e}")
  193. raise
  194. def convert_ollama_to_gemini(ollama_response):
  195. """Convert Ollama native response to Gemini API format"""
  196. try:
  197. # Ollama /api/generate returns: {"response": "text", "done": true, ...}
  198. response_text = ollama_response.get("response", "")
  199. if not response_text:
  200. print(f"❌ No response text in Ollama response: {ollama_response}")
  201. return {"error": "No response generated"}
  202. # Convert to Gemini format
  203. gemini_response = {
  204. "candidates": [
  205. {
  206. "content": {"parts": [{"text": response_text}], "role": "model"},
  207. "finishReason": "STOP",
  208. "index": 0,
  209. }
  210. ],
  211. "usageMetadata": {
  212. "promptTokenCount": ollama_response.get("prompt_eval_count", 0),
  213. "candidatesTokenCount": ollama_response.get("eval_count", 0),
  214. "totalTokenCount": ollama_response.get("prompt_eval_count", 0)
  215. + ollama_response.get("eval_count", 0),
  216. },
  217. }
  218. return gemini_response
  219. except Exception as e:
  220. print(f"❌ Error converting Ollama response: {e}")
  221. raise
  222. HTML_TEMPLATE = """
  223. <!DOCTYPE html>
  224. <html>
  225. <head>
  226. <title>POST Request Monitor</title>
  227. <style>
  228. body { font-family: Arial, sans-serif; margin: 20px; background: #f5f5f5; }
  229. h1 { color: #333; }
  230. .config { background: #e3f2fd; padding: 10px; margin: 10px 0; border-radius: 5px; }
  231. .request {
  232. background: white;
  233. padding: 15px;
  234. margin: 10px 0;
  235. border-radius: 5px;
  236. box-shadow: 0 2px 4px rgba(0,0,0,0.1);
  237. }
  238. .timestamp { color: #666; font-size: 0.9em; }
  239. .method {
  240. display: inline-block;
  241. padding: 3px 8px;
  242. background: #4CAF50;
  243. color: white;
  244. border-radius: 3px;
  245. font-weight: bold;
  246. }
  247. .forwarded {
  248. display: inline-block;
  249. padding: 3px 8px;
  250. background: #2196F3;
  251. color: white;
  252. border-radius: 3px;
  253. font-size: 0.8em;
  254. margin-left: 10px;
  255. }
  256. .error-badge {
  257. display: inline-block;
  258. padding: 3px 8px;
  259. background: #f44336;
  260. color: white;
  261. border-radius: 3px;
  262. font-size: 0.8em;
  263. margin-left: 10px;
  264. }
  265. pre {
  266. background: #f4f4f4;
  267. padding: 10px;
  268. border-radius: 3px;
  269. overflow-x: auto;
  270. max-height: 300px;
  271. overflow-y: auto;
  272. }
  273. .clear-btn {
  274. background: #f44336;
  275. color: white;
  276. border: none;
  277. padding: 10px 20px;
  278. border-radius: 5px;
  279. cursor: pointer;
  280. margin: 10px 0;
  281. }
  282. .clear-btn:hover { background: #d32f2f; }
  283. </style>
  284. <script>
  285. function clearRequests() {
  286. fetch('/clear', { method: 'POST' })
  287. .then(() => location.reload());
  288. }
  289. // Auto-refresh every 3 seconds
  290. setTimeout(() => location.reload(), 3000);
  291. </script>
  292. </head>
  293. <body>
  294. <h1>📬 POST Request Monitor & AI Proxy</h1>
  295. <div class="config">
  296. <strong>Configuration:</strong><br>
  297. Endpoint: <strong>{{ endpoint }}</strong><br>
  298. Type: <strong>{{ endpoint_type }}</strong><br>
  299. Model: <strong>{{ model }}</strong><br>
  300. Video Format: <strong>{{ video_format }}</strong>
  301. </div>
  302. <p>Send POST requests to <strong>http://localhost:5005/webhook</strong></p>
  303. <button class="clear-btn" onclick="clearRequests()">Clear All</button>
  304. <div id="requests">
  305. {% for req in requests %}
  306. <div class="request">
  307. <div>
  308. <span class="method">{{ req.method }}</span>
  309. <span class="timestamp">{{ req.timestamp }}</span>
  310. {% if req.forwarded %}
  311. <span class="forwarded">FORWARDED ({{ req.endpoint_type }})</span>
  312. {% endif %}
  313. {% if req.error %}
  314. <span class="error-badge">ERROR</span>
  315. {% endif %}
  316. </div>
  317. <div><strong>Path:</strong> {{ req.path }}</div>
  318. {% if req.query_params %}
  319. <div><strong>Query Parameters:</strong></div>
  320. <pre>{{ req.query_params }}</pre>
  321. {% endif %}
  322. {% if req.body %}
  323. <div><strong>Incoming Body (Gemini Format):</strong></div>
  324. <pre>{{ req.body }}</pre>
  325. {% endif %}
  326. {% if req.converted_request %}
  327. <div><strong>Converted Request:</strong></div>
  328. <pre>{{ req.converted_request }}</pre>
  329. {% endif %}
  330. {% if req.raw_response %}
  331. <div><strong>Raw Response:</strong></div>
  332. <pre>{{ req.raw_response }}</pre>
  333. {% endif %}
  334. {% if req.response %}
  335. <div><strong>Final Response (Gemini Format):</strong></div>
  336. <pre>{{ req.response }}</pre>
  337. {% endif %}
  338. {% if req.error %}
  339. <div><strong>Error:</strong></div>
  340. <pre style="color: red;">{{ req.error }}</pre>
  341. {% endif %}
  342. </div>
  343. {% endfor %}
  344. </div>
  345. </body>
  346. </html>
  347. """
  348. @app.route("/")
  349. def index():
  350. endpoint_type = (
  351. ENDPOINT_TYPE
  352. if ENDPOINT_TYPE != "auto"
  353. else detect_endpoint_type(OPENAI_ENDPOINT)
  354. )
  355. return render_template_string(
  356. HTML_TEMPLATE,
  357. requests=reversed(recent_requests),
  358. endpoint=OPENAI_ENDPOINT,
  359. endpoint_type=endpoint_type,
  360. model=OPENAI_MODEL,
  361. video_format=VIDEO_FORMAT,
  362. )
  363. @app.route("/webhook", methods=["POST", "PUT", "PATCH"], defaults={"subpath": ""})
  364. @app.route("/webhook/<path:subpath>", methods=["POST", "PUT", "PATCH"])
  365. def webhook(subpath):
  366. """Accept POST/PUT/PATCH requests, forward to AI endpoint, and return response"""
  367. timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
  368. # Get full path with query parameters
  369. full_path = request.full_path if request.query_string else request.path
  370. print(f"\n{'='*60}")
  371. print(f"[{timestamp}] INCOMING {request.method} {full_path}")
  372. print(f"Matched route with subpath: '{subpath}'")
  373. print(f"{'='*60}")
  374. # Detect endpoint type
  375. endpoint_type = (
  376. ENDPOINT_TYPE
  377. if ENDPOINT_TYPE != "auto"
  378. else detect_endpoint_type(OPENAI_ENDPOINT)
  379. )
  380. print(f"Detected endpoint type: {endpoint_type}")
  381. # Get request data
  382. try:
  383. body = request.get_data(as_text=True)
  384. gemini_request = request.get_json() if request.is_json else {}
  385. body_display = json.dumps(gemini_request, indent=2) if gemini_request else body
  386. except Exception as e:
  387. body_display = str(request.get_data())
  388. gemini_request = {}
  389. # Print request details
  390. if request.args:
  391. print("Query Parameters:")
  392. for key, value in request.args.items():
  393. print(f" {key}: {value}")
  394. print(
  395. f"Body preview:\n{body_display[:500]}{'...' if len(body_display) > 500 else ''}"
  396. )
  397. # Store request info for monitoring
  398. req_info = {
  399. "timestamp": timestamp,
  400. "method": request.method,
  401. "path": full_path,
  402. "query_params": dict(request.args),
  403. "body": body_display,
  404. "forwarded": False,
  405. "response": None,
  406. "error": None,
  407. "endpoint_type": endpoint_type,
  408. "converted_request": None,
  409. "raw_response": None,
  410. }
  411. # Try to forward to AI endpoint
  412. try:
  413. if gemini_request:
  414. print(f"\n{'='*60}")
  415. print(f"CONVERTING AND FORWARDING TO {endpoint_type.upper()} ENDPOINT")
  416. print(f"Target: {OPENAI_ENDPOINT}")
  417. print(f"{'='*60}")
  418. # Convert based on endpoint type
  419. if endpoint_type == "ollama":
  420. converted_request = convert_gemini_to_ollama(gemini_request)
  421. else: # openai
  422. converted_request = convert_gemini_to_openai(gemini_request)
  423. # Log the converted request (truncate base64 for readability)
  424. logged_request = json.loads(json.dumps(converted_request))
  425. if endpoint_type == "openai":
  426. for msg in logged_request.get("messages", []):
  427. if isinstance(msg.get("content"), list):
  428. for item in msg["content"]:
  429. if item.get("type") == "image_url":
  430. url = item["image_url"]["url"]
  431. if len(url) > 100:
  432. item["image_url"]["url"] = (
  433. url[:50] + "...[base64 data]..." + url[-20:]
  434. )
  435. elif "images" in logged_request:
  436. # Truncate Ollama images
  437. for i, img in enumerate(logged_request["images"]):
  438. if len(img) > 100:
  439. logged_request["images"][i] = (
  440. img[:50] + "...[base64 data]..." + img[-20:]
  441. )
  442. print(f"Converted request:\n{json.dumps(logged_request, indent=2)}")
  443. # Forward to endpoint
  444. headers = {
  445. "Content-Type": "application/json",
  446. }
  447. if (
  448. OPENAI_API_KEY
  449. and OPENAI_API_KEY != "none"
  450. and endpoint_type == "openai"
  451. ):
  452. headers["Authorization"] = f"Bearer {OPENAI_API_KEY}"
  453. print(f"Sending request to {OPENAI_ENDPOINT}...")
  454. response = requests.post(
  455. OPENAI_ENDPOINT, json=converted_request, headers=headers, timeout=120
  456. )
  457. print(f"\nResponse Status: {response.status_code}")
  458. print(f"Response Headers: {dict(response.headers)}")
  459. if response.status_code == 200:
  460. raw_response = response.json()
  461. print(f"Raw Response:\n{json.dumps(raw_response, indent=2)[:1000]}...")
  462. # Convert back to Gemini format based on endpoint type
  463. if endpoint_type == "ollama":
  464. gemini_response = convert_ollama_to_gemini(raw_response)
  465. else: # openai
  466. gemini_response = convert_openai_to_gemini(raw_response)
  467. print(
  468. f"\nConverted Gemini Response:\n{json.dumps(gemini_response, indent=2)[:1000]}..."
  469. )
  470. req_info["forwarded"] = True
  471. req_info["response"] = json.dumps(gemini_response, indent=2)
  472. req_info["converted_request"] = json.dumps(logged_request, indent=2)
  473. req_info["raw_response"] = json.dumps(raw_response, indent=2)[:2000] + (
  474. "..." if len(json.dumps(raw_response, indent=2)) > 2000 else ""
  475. )
  476. recent_requests.append(req_info)
  477. if len(recent_requests) > MAX_REQUESTS:
  478. recent_requests.pop(0)
  479. print(f"{'='*60}\n")
  480. return jsonify(gemini_response), 200
  481. else:
  482. # Get detailed error
  483. try:
  484. error_data = response.json()
  485. error_msg = json.dumps(error_data, indent=2)
  486. except:
  487. error_msg = response.text
  488. full_error = f"{endpoint_type.upper()} endpoint returned {response.status_code}:\n{error_msg}"
  489. print(f"ERROR: {full_error}")
  490. req_info["error"] = full_error
  491. req_info["forwarded"] = True
  492. req_info["converted_request"] = json.dumps(logged_request, indent=2)
  493. req_info["raw_response"] = error_msg
  494. recent_requests.append(req_info)
  495. if len(recent_requests) > MAX_REQUESTS:
  496. recent_requests.pop(0)
  497. print(f"{'='*60}\n")
  498. return (
  499. jsonify(
  500. {
  501. "error": {
  502. "message": error_msg,
  503. "status": response.status_code,
  504. }
  505. }
  506. ),
  507. response.status_code,
  508. )
  509. else:
  510. # No JSON body, just acknowledge
  511. req_info["error"] = "No JSON body to forward"
  512. recent_requests.append(req_info)
  513. if len(recent_requests) > MAX_REQUESTS:
  514. recent_requests.pop(0)
  515. print(f"{'='*60}\n")
  516. return (
  517. jsonify(
  518. {
  519. "status": "success",
  520. "message": "Request received but not forwarded (no JSON body)",
  521. "timestamp": timestamp,
  522. }
  523. ),
  524. 200,
  525. )
  526. except Exception as e:
  527. error_msg = f"Error processing request: {str(e)}"
  528. print(f"ERROR: {error_msg}")
  529. import traceback
  530. traceback.print_exc()
  531. req_info["error"] = error_msg
  532. recent_requests.append(req_info)
  533. if len(recent_requests) > MAX_REQUESTS:
  534. recent_requests.pop(0)
  535. print(f"{'='*60}\n")
  536. return jsonify({"error": {"message": error_msg}}), 500
  537. @app.route("/clear", methods=["POST"])
  538. def clear():
  539. """Clear all stored requests"""
  540. recent_requests.clear()
  541. return jsonify({"status": "cleared"}), 200
  542. @app.errorhandler(404)
  543. def not_found(e):
  544. """Handle 404 errors with helpful message"""
  545. print(f"\n❌ 404 ERROR: {request.method} {request.path}")
  546. print(f" Query string: {request.query_string.decode()}")
  547. print(f" Full path: {request.full_path}")
  548. print(f" Available routes:")
  549. for rule in app.url_map.iter_rules():
  550. print(f" - {rule.methods} {rule.rule}")
  551. return (
  552. jsonify(
  553. {
  554. "error": "Not Found",
  555. "message": f"The path {request.path} was not found",
  556. "hint": "POST requests should go to /webhook or /webhook/<path>",
  557. }
  558. ),
  559. 404,
  560. )
  561. if __name__ == "__main__":
  562. print("🚀 POST Request Monitor & AI Proxy starting...")
  563. print("📍 Web UI: http://localhost:5005")
  564. print("📮 Webhook endpoint: http://localhost:5005/webhook")
  565. print(
  566. "📮 Example: http://localhost:5005/webhook/models/model:generateContent?key=none"
  567. )
  568. print(f"🔗 Forwarding to: {OPENAI_ENDPOINT}")
  569. print(f"🤖 Model: {OPENAI_MODEL}")
  570. print(f"📹 Video format: {VIDEO_FORMAT}")
  571. endpoint_type = (
  572. ENDPOINT_TYPE
  573. if ENDPOINT_TYPE != "auto"
  574. else detect_endpoint_type(OPENAI_ENDPOINT)
  575. )
  576. print(f"🔧 Endpoint type: {endpoint_type}")
  577. print("\n" + "=" * 60)
  578. print("CONFIGURATION OPTIONS:")
  579. print("Set these environment variables to configure:")
  580. print(" OPENAI_ENDPOINT - Target endpoint URL")
  581. print(" ENDPOINT_TYPE - 'openai', 'ollama', or 'auto' (default)")
  582. print(" OPENAI_MODEL - Model name")
  583. print(" VIDEO_FORMAT - 'openai', 'vllm', 'skip', or 'error'")
  584. print("\nFor Ollama:")
  585. print(" OpenAI-compatible: http://localhost:11434/v1/chat/completions")
  586. print(" Native format: http://localhost:11434/api/generate")
  587. print("=" * 60)
  588. app.run(host="0.0.0.0", port=5000, debug=True)