drriver/gemini-rproxy2
0
1from flask import Flask, request, jsonify, Response, stream_with_context, render_template_string
2import json
3import os
4import re
5import logging
6import func
7from datetime import datetime, timedelta
8from apscheduler.schedulers.background import BackgroundScheduler
9import time
10import requests
11from collections import deque
12import random
13from dataclasses import dataclass
14from typing import Optional, Dict, Any
15
16app = Flask(__name__)
17
18os.environ['TZ'] = 'Asia/Shanghai'
19
20app = Flask(__name__)
21
22app.secret_key = os.urandom(24)
23
24formatter = logging.Formatter('%(message)s')
25logger = logging.getLogger(__name__)
26logger.setLevel(logging.INFO)
27handler = logging.StreamHandler()
28handler.setFormatter(formatter)
29logger.addHandler(handler)
30
31MAX_RETRIES = int(os.environ.get('MaxRetries', '3').strip() or '3')
32MAX_REQUESTS = int(os.environ.get('MaxRequests', '2').strip() or '2')
33LIMIT_WINDOW = int(os.environ.get('LimitWindow', '60').strip() or '60')
34
35RETRY_DELAY = 1
36MAX_RETRY_DELAY = 16
37
38request_counts = {}
39
40api_key_blacklist = set()
41api_key_blacklist_duration = 60
42
43
44safety_settings = [
45 {
46 "category": "HARM_CATEGORY_HARASSMENT",
47 "threshold": "BLOCK_NONE"
48 },
49 {
50 "category": "HARM_CATEGORY_HATE_SPEECH",
51 "threshold": "BLOCK_NONE"
52 },
53 {
54 "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
55 "threshold": "BLOCK_NONE"
56 },
57 {
58 "category": "HARM_CATEGORY_DANGEROUS_CONTENT",
59 "threshold": "BLOCK_NONE"
60 },
61 {
62 "category": 'HARM_CATEGORY_CIVIC_INTEGRITY',
63 "threshold": 'BLOCK_NONE'
64 }
65]
66safety_settings_g2 = [
67 {
68 "category": "HARM_CATEGORY_HARASSMENT",
69 "threshold": "OFF"
70 },
71 {
72 "category": "HARM_CATEGORY_HATE_SPEECH",
73 "threshold": "OFF"
74 },
75 {
76 "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
77 "threshold": "OFF"
78 },
79 {
80 "category": "HARM_CATEGORY_DANGEROUS_CONTENT",
81 "threshold": "OFF"
82 },
83 {
84 "category": 'HARM_CATEGORY_CIVIC_INTEGRITY',
85 "threshold": 'OFF'
86 }
87]
88@dataclass
89class GeneratedText:
90 text: str
91 finish_reason: Optional[str] = None
92
93
94class ResponseWrapper:
95 def __init__(self, data: Dict[Any, Any]):
96 self._data = data
97 self._text = self._extract_text()
98 self._finish_reason = self._extract_finish_reason()
99 self._prompt_token_count = self._extract_prompt_token_count()
100 self._candidates_token_count = self._extract_candidates_token_count()
101 self._total_token_count = self._extract_total_token_count()
102 self._thoughts = self._extract_thoughts()
103 self._json_dumps = json.dumps(self._data, indent=4, ensure_ascii=False)
104
105 def _extract_thoughts(self) -> Optional[str]:
106 try:
107 for part in self._data['candidates'][0]['content']['parts']:
108 if 'thought' in part:
109 return part['text']
110 return ""
111 except (KeyError, IndexError):
112 return ""
113
114 def _extract_text(self) -> str:
115 try:
116 for part in self._data['candidates'][0]['content']['parts']:
117 if 'thought' not in part:
118 return part['text']
119 return ""
120 except (KeyError, IndexError):
121 return ""
122
123 def _extract_finish_reason(self) -> Optional[str]:
124 try:
125 return self._data['candidates'][0].get('finishReason')
126 except (KeyError, IndexError):
127 return None
128
129 def _extract_prompt_token_count(self) -> Optional[int]:
130 try:
131 return self._data['usageMetadata'].get('promptTokenCount')
132 except (KeyError):
133 return None
134
135 def _extract_candidates_token_count(self) -> Optional[int]:
136 try:
137 return self._data['usageMetadata'].get('candidatesTokenCount')
138 except (KeyError):
139 return None
140
141 def _extract_total_token_count(self) -> Optional[int]:
142 try:
143 return self._data['usageMetadata'].get('totalTokenCount')
144 except (KeyError):
145 return None
146
147 @property
148 def text(self) -> str:
149 return self._text
150
151 @property
152 def finish_reason(self) -> Optional[str]:
153 return self._finish_reason
154
155 @property
156 def prompt_token_count(self) -> Optional[int]:
157 return self._prompt_token_count
158
159 @property
160 def candidates_token_count(self) -> Optional[int]:
161 return self._candidates_token_count
162
163 @property
164 def total_token_count(self) -> Optional[int]:
165 return self._total_token_count
166
167 @property
168 def thoughts(self) -> Optional[str]:
169 return self._thoughts
170
171 @property
172 def json_dumps(self) -> str:
173 return self._json_dumps
174
175class APIKeyManager:
176 def __init__(self):
177 self.api_keys = re.findall(r"AIzaSy[a-zA-Z0-9_-]{33}", os.environ.get('KeyArray'))
178 self.current_index = random.randint(0, len(self.api_keys) - 1)
179
180 def get_available_key(self):
181 num_keys = len(self.api_keys)
182 for _ in range(num_keys):
183 if self.current_index >= num_keys:
184 self.current_index = 0
185 current_key = self.api_keys[self.current_index]
186 self.current_index += 1
187
188 if current_key not in api_key_blacklist:
189 return current_key
190
191 logger.error("所有API key都已耗尽或被暂时禁用,请重新配置或稍后重试")
192 return None
193
194 def show_all_keys(self):
195 logger.info(f"当前可用API key个数: {len(self.api_keys)} ")
196 for i, api_key in enumerate(self.api_keys):
197 logger.info(f"API Key{i}: {api_key[:8]}...{api_key[-3:]}")
198
199 def blacklist_key(self, key):
200 logger.warning(f"{key[:8]} → 暂时禁用 {api_key_blacklist_duration} 秒")
201 api_key_blacklist.add(key)
202
203 scheduler.add_job(lambda: api_key_blacklist.discard(key), 'date', run_date=datetime.now() + timedelta(seconds=api_key_blacklist_duration))
204
205key_manager = APIKeyManager()
206key_manager.show_all_keys()
207current_api_key = key_manager.get_available_key()
208
209def switch_api_key():
210 global current_api_key
211 key = key_manager.get_available_key()
212 if key:
213 current_api_key = key
214 logger.info(f"API key 替换为 → {current_api_key[:8]}...{current_api_key[-3:]}")
215 else:
216 logger.error("API key 替换失败,所有API key都已耗尽或被暂时禁用,请重新配置或稍后重试")
217
218logger.info(f"当前 API key: {current_api_key[:8]}...{current_api_key[-3:]}")
219
220GEMINI_MODELS = [
221 {"id": "text-embedding-004"},
222 {"id": "gemini-1.5-flash-8b-latest"},
223 {"id": "gemini-1.5-flash-8b-exp-0924"},
224 {"id": "gemini-1.5-flash-latest"},
225 {"id": "gemini-1.5-flash-exp-0827"},
226 {"id": "gemini-1.5-pro-latest"},
227 {"id": "gemini-1.5-pro-exp-0827"},
228 {"id": "learnlm-1.5-pro-experimental"},
229 {"id": "gemini-exp-1114"},
230 {"id": "gemini-exp-1121"},
231 {"id": "gemini-exp-1206"},
232 {"id": "gemini-2.0-flash-exp"},
233 {"id": "gemini-2.0-flash-thinking-exp-1219"},
234 {"id": "gemini-2.0-flash-thinking-exp-01-21"},
235 {"id": "gemini-2.0-pro-exp"}
236]
237
238@app.route('/')
239def index():
240 main_content = "Moonfanz Reminiproxy v2.3.5 2025-01-14"
241 html_template = """
242<!DOCTYPE html>
243<html>
244<head>
245<meta charset="utf-8">
246<script>
247function copyToClipboard(text) {
248 var textarea = document.createElement("textarea");
249 textarea.textContent = text;
250 textarea.style.position = "fixed";
251 document.body.appendChild(textarea);
252 textarea.select();
253 try {
254 return document.execCommand("copy");
255 } catch (ex) {
256 console.warn("Copy to clipboard failed.", ex);
257 return false;
258 } finally {
259 document.body.removeChild(textarea);
260 }
261}
262function copyLink(event) {
263 event.preventDefault();
264 const url = new URL(window.location.href);
265 const link = url.protocol + '//' + url.host + '/hf/v1';
266 copyToClipboard(link);
267 alert('链接已复制: ' + link);
268}
269</script>
270</head>
271<body>
272{{ main_content }}<br/><br/>完全开源、免费且禁止商用<br/><br/>点击复制反向代理: <a href="v1" onclick="copyLink(event)">点击我复制地址(不要自己写地址)</a><br/>聊天来源选择"自定义(兼容 OpenAI)"<br/>将复制的网址填入到自定义端点<br/>将设置password填入自定义API秘钥<br/><br/><br/>
273</body>
274</html>
275 """
276 return render_template_string(html_template, main_content=main_content)
277
278def is_within_rate_limit(api_key):
279 now = datetime.now()
280 if api_key not in request_counts:
281 request_counts[api_key] = deque()
282
283 while request_counts[api_key] and request_counts[api_key][0] < now - timedelta(seconds=LIMIT_WINDOW):
284 request_counts[api_key].popleft()
285
286 if len(request_counts[api_key]) >= MAX_REQUESTS:
287 earliest_request_time = request_counts[api_key][0]
288 wait_time = (earliest_request_time + timedelta(seconds=LIMIT_WINDOW)) - now
289 return False, wait_time.total_seconds()
290 else:
291 return True, 0
292
293def increment_request_count(api_key):
294 now = datetime.now()
295 if api_key not in request_counts:
296 request_counts[api_key] = deque()
297 request_counts[api_key].append(now)
298
299def handle_api_error(error, attempt, current_api_key):
300 if attempt > MAX_RETRIES:
301 logger.error(f"{MAX_RETRIES} 次尝试后仍然失败,请修改预设或输入")
302 return 0, jsonify({
303 'error': {
304 'message': f"{MAX_RETRIES} 次尝试后仍然失败,请修改预设或输入",
305 'type': 'max_retries_exceeded'
306 }
307 })
308
309 if isinstance(error, requests.exceptions.HTTPError):
310 status_code = error.response.status_code
311
312 if status_code == 400:
313
314 try:
315 error_data = error.response.json()
316 if 'error' in error_data:
317 if error_data['error'].get('code') == "invalid_argument":
318 logger.error(f"{current_api_key[:8]} ... {current_api_key[-3:]} → 无效,可能已过期或被删除")
319 key_manager.blacklist_key(current_api_key)
320 switch_api_key()
321 return 0, None
322 error_message = error_data['error'].get('message', 'Bad Request')
323 error_type = error_data['error'].get('type', 'invalid_request_error')
324 logger.warning(f"400 错误请求: {error_message}")
325 return 2, jsonify({'error': {'message': error_message, 'type': error_type}})
326 except ValueError:
327 logger.warning("400 错误请求:响应不是有效的JSON格式")
328 return 2, jsonify({'error': {'message': '', 'type': 'invalid_request_error'}})
329
330 elif status_code == 429:
331 logger.warning(
332 f"{current_api_key[:8]} ... {current_api_key[-3:]} → 429 官方资源耗尽 → 立即重试..."
333 )
334 key_manager.blacklist_key(current_api_key)
335 switch_api_key()
336 return 0, None
337
338 elif status_code == 403:
339 logger.error(
340 f"{current_api_key[:8]} ... {current_api_key[-3:]} → 403 权限被拒绝,该 API KEY 可能已经被官方封禁"
341 )
342 key_manager.blacklist_key(current_api_key)
343 switch_api_key()
344 return 0, None
345
346 elif status_code == 500:
347 logger.warning(
348 f"{current_api_key[:8]} ... {current_api_key[-3:]} → 500 服务器内部错误 → 立即重试..."
349 )
350 switch_api_key()
351 return 0, None
352
353 elif status_code == 503:
354 logger.warning(
355 f"{current_api_key[:8]} ... {current_api_key[-3:]} → 503 服务不可用 → 立即重试..."
356 )
357 switch_api_key()
358 return 0, None
359
360 else:
361 logger.warning(
362 f"{current_api_key[:8]} ... {current_api_key[-3:]} → {status_code} 未知错误/模型不可用 → 不重试..."
363 )
364 switch_api_key()
365 return 2, None
366
367 elif isinstance(error, requests.exceptions.ConnectionError):
368 delay = min(RETRY_DELAY * (2 ** attempt), MAX_RETRY_DELAY)
369 logger.warning(f"连接错误 → 立即重试...")
370 time.sleep(delay)
371 return 0, None
372
373 elif isinstance(error, requests.exceptions.Timeout):
374 delay = min(RETRY_DELAY * (2 ** attempt), MAX_RETRY_DELAY)
375 logger.warning(f"请求超时 → 立即重试...")
376 time.sleep(delay)
377 return 0, None
378
379 else:
380 logger.error(f"发生未知错误: {error}")
381 return 0, jsonify({
382 'error': {
383 'message': f"发生未知错误: {error}",
384 'type': 'unknown_error'
385 }
386 })
387
388@app.route('/hf/v1/chat/completions', methods=['POST'])
389def chat_completions():
390 is_authenticated, auth_error, status_code = func.authenticate_request(request)
391 if not is_authenticated:
392 return auth_error if auth_error else jsonify({'error': '未授权'}), status_code if status_code else 401
393
394 request_data = request.get_json()
395 messages = request_data.get('messages', [])
396 model = request_data.get('model', 'gemini-2.0-flash-exp')
397 temperature = request_data.get('temperature', 1)
398 max_tokens = request_data.get('max_tokens', 8192)
399 show_thoughts = request_data.get('show_thoughts', False)
400 stream = request_data.get('stream', False)
401 use_system_prompt = request_data.get('use_system_prompt', False)
402 hint = "流式" if stream else "非流"
403 logger.info(f"\n{model} [{hint}] → {current_api_key[:8]}...{current_api_key[-3:]}")
404 is_thinking = 'thinking' in model
405 api_version = 'v1alpha' if is_thinking else 'v1beta'
406 response_type = 'streamGenerateContent' if stream else 'generateContent'
407 is_SSE = '&alt=sse' if stream else ''
408
409 contents, system_instruction, error_response = func.process_messages_for_gemini(messages, use_system_prompt)
410
411 if error_response:
412 logger.error(f"处理输入消息时出错↙\n {error_response}")
413 return jsonify(error_response), 400
414
415 def do_request(current_api_key, attempt):
416 isok, time_remaining = is_within_rate_limit(current_api_key)
417 if not isok:
418 logger.warning(f"暂时超过限额,该API key将在 {time_remaining} 秒后启用...")
419 switch_api_key()
420 return 0, None
421
422 increment_request_count(current_api_key)
423
424
425 url = f"https://generativelanguage.googleapis.com/{api_version}/models/{model}:{response_type}?key={current_api_key}{is_SSE}"
426 headers = {
427 "Content-Type": "application/json",
428 }
429
430 data = {
431 "contents": contents,
432 "generationConfig": {
433 "temperature": temperature,
434 "maxOutputTokens": max_tokens,
435 },
436 "safetySettings": safety_settings_g2 if 'gemini-2.0-flash-exp' in model else safety_settings,
437 }
438 if system_instruction:
439 data["system_instruction"] = system_instruction
440
441 try:
442 response = requests.post(url, headers=headers, json=data, stream=True)
443 response.raise_for_status()
444
445 if stream:
446 return 1, response
447 else:
448 return 1, ResponseWrapper(response.json())
449 except requests.exceptions.RequestException as e:
450 return handle_api_error(e, attempt, current_api_key)
451
452 def generate_stream(response):
453 logger.info(f"流式开始 →")
454 buffer = b""
455 try:
456 for line in response.iter_lines():
457 if not line:
458 continue
459 try:
460 if line.startswith(b'data: '):
461 line = line[6:]
462
463 buffer += line
464
465 try:
466 data = json.loads(buffer.decode('utf-8'))
467 buffer = b""
468 if 'candidates' in data and data['candidates']:
469 candidate = data['candidates'][0]
470 if 'content' in candidate:
471 content = candidate['content']
472 if 'parts' in content and content['parts']:
473 parts = content['parts']
474 if is_thinking and not show_thoughts:
475 parts = [part for part in parts if not part.get('thought')]
476 if parts:
477 text = parts[0].get('text', '')
478 finish_reason = candidate.get('finishReason')
479
480 if text:
481 data = {
482 'choices': [{
483 'delta': {
484 'content': text
485 },
486 'finish_reason': finish_reason,
487 'index': 0
488 }],
489 'object': 'chat.completion.chunk'
490 }
491 yield f"data: {json.dumps(data)}\n\n"
492
493 if candidate.get("finishReason") and candidate.get("finishReason") != "STOP":
494 error_message = {
495 "error": {
496 "code": "content_filter",
497 "message": f"模型的响应因违反内容政策而被标记:{candidate.get('finishReason')}",
498 "status": candidate.get("finishReason"),
499 "details": []
500 }
501 }
502 logger.warning(f"模型的响应因违反内容政策而被标记: {candidate.get('finishReason')}")
503 yield f"data: {json.dumps(error_message)}\n\n"
504 break
505
506 if 'safetyRatings' in candidate:
507 for rating in candidate['safetyRatings']:
508 if rating['probability'] == 'HIGH':
509 error_message = {
510 "error": {
511 "code": "content_filter",
512 "message": f"模型的响应因高概率被标记为 {rating['category']}",
513 "status": "SAFETY_RATING_HIGH",
514 "details": [rating]
515 }
516 }
517 logger.warning(f"模型的响应因高概率被标记为 {rating['category']}")
518 yield f"data: {json.dumps(error_message)}\n\n"
519 break
520 else:
521 continue
522 break
523
524 except json.JSONDecodeError:
525 logger.debug(f"JSON解析错误, 当前缓冲区内容: {buffer}")
526 continue
527
528 except Exception as e:
529 logger.error(f"流式处理期间发生错误: {e}, 原始数据行↙\n{line}")
530 yield f"data: {json.dumps({'error': str(e)})}\n\n"
531
532 else:
533 yield f"data: {json.dumps({'choices': [{'delta': {}, 'finish_reason': 'stop', 'index': 0}]})}\n\n"
534 logger.info(f"流式结束 ←")
535 logger.info(f"200!")
536 except Exception as e:
537 logger.error(f"流式处理错误↙\n{e}")
538 yield f"data: {json.dumps({'error': str(e)})}\n\n"
539
540 attempt = 0
541 success = 0
542 response = None
543 for attempt in range(1, MAX_RETRIES + 1):
544 logger.info(f"第 {attempt}/{MAX_RETRIES} 次尝试 ...")
545 success, response = do_request(current_api_key, attempt)
546
547 if success == 0:
548 continue
549 elif success == 1 and response is None:
550 continue
551 elif success == 1 and stream:
552 return Response(
553 stream_with_context(generate_stream(response)),
554 mimetype='text/event-stream'
555 )
556 elif success == 1 and isinstance(response, ResponseWrapper):
557 try:
558 text_content = response.text
559 prompt_tokens = response.prompt_token_count
560 completion_tokens = response.candidates_token_count
561 total_tokens = response.total_token_count
562 finish_reason = response.finish_reason
563
564 if text_content == '':
565 error_message = None
566 if response._data and 'error' in response._data:
567 error_message = response._data['error'].get('message')
568 if error_message:
569 logger.error(f"生成内容失败,API 返回错误: {error_message}")
570 else:
571 logger.error(f"生成内容失败: text_content 为空")
572 continue
573
574 if is_thinking and show_thoughts:
575 text_content = response.thoughts + '\n' + text_content
576
577 except AttributeError as e:
578 logger.error(f"处理响应失败,缺少必要的属性: {e}")
579 logger.error(f"原始响应: {response._data}")
580 continue
581
582 except Exception as e:
583 logger.error(f"处理响应失败: {e}")
584 continue
585
586 response_data = {
587 'id': 'chatcmpl-xxxxxxxxxxxx',
588 'object': 'chat.completion',
589 'created': int(datetime.now().timestamp()),
590 'model': model,
591 'choices': [{
592 'index': 0,
593 'message': {
594 'role': 'assistant',
595 'content': text_content
596 },
597 'finish_reason': finish_reason
598 }],
599 'usage': {
600 'prompt_tokens': prompt_tokens,
601 'completion_tokens': completion_tokens,
602 'total_tokens': total_tokens
603 }
604 }
605 logger.info(f"200!")
606 return jsonify(response_data)
607 elif success == 1 and isinstance(response, tuple):
608 return response[1], response[0]
609 elif success == 2:
610 logger.error(f"{model} 可能暂时不可用,请更换模型或未来一段时间再试")
611 response = {
612 'error': {
613 'message': f'{model} 可能暂时不可用,请更换模型或未来一段时间再试',
614 'type': 'internal_server_error'
615 }
616 }
617 return jsonify(response), 503
618 else:
619 logger.error(f"{MAX_RETRIES} 次尝试均失败,请重试或等待官方恢复")
620 response = {
621 'error': {
622 'message': f'{MAX_RETRIES} 次尝试均失败,请重试或等待官方恢复',
623 'type': 'internal_server_error'
624 }
625 }
626 return jsonify(response), 500 if response is not None else 503
627
628@app.route('/hf/v1/models', methods=['GET'])
629def list_models():
630 response = {"object": "list", "data": GEMINI_MODELS}
631 return jsonify(response)
632
633@app.route('/hf/v1/embeddings', methods=['POST'])
634def embeddings():
635 data = request.get_json()
636 model_input = data.get("input")
637 model = data.get("model", "text-embedding-004")
638 if not model_input:
639 return jsonify({"error": "没有提供输入"}), 400
640
641 if isinstance(model_input, str):
642 model_input = [model_input]
643
644 gemini_request = {
645 "model": f"models/{model}",
646 "content": {
647 "parts": [{"text": text} for text in model_input]
648 }
649 }
650
651 gemini_url = f"https://generativelanguage.googleapis.com/v1beta/models/{model}:embedContent?key={current_api_key}"
652 headers = {"Content-Type": "application/json"}
653 try:
654 gemini_response = requests.post(gemini_url, json=gemini_request, headers=headers)
655 gemini_response.raise_for_status()
656
657 response_json = gemini_response.json()
658 embeddings_data = []
659 if 'embedding' in response_json:
660 embeddings_data.append({
661 "object": "embedding",
662 "embedding": response_json['embedding']['values'],
663 "index": 0,
664 })
665 elif 'embeddings' in response_json:
666 for i, embedding in enumerate(response_json['embeddings']):
667 embeddings_data.append({
668 "object": "embedding",
669 "embedding": embedding['values'],
670 "index": i,
671 })
672
673 client_response = {
674 "object": "list",
675 "data": embeddings_data,
676 "model": model,
677 "usage": {
678 "prompt_tokens": 0,
679 "total_tokens": 0,
680 },
681 }
682 switch_api_key()
683 return jsonify(client_response)
684
685 except requests.exceptions.RequestException as e:
686 print(f"请求Embeddings失败↙\: {e}")
687 return jsonify({"error": str(e)}), 500
688
689def keep_alive():
690 try:
691 response = requests.get("http://127.0.0.1:7860/", timeout=10)
692 response.raise_for_status()
693 print(f"Keep alive ping successful: {response.status_code} at {time.ctime()}")
694 except requests.exceptions.RequestException as e:
695 print(f"Keep alive ping failed: {e} at {time.ctime()}")
696
697if __name__ == '__main__':
698 scheduler = BackgroundScheduler()
699
700 scheduler.add_job(keep_alive, 'interval', hours=12)
701 scheduler.start()
702 logger.info(f"Reminiproxy v2.3.5 启动")
703 logger.info(f"最大尝试次数/MaxRetries: {MAX_RETRIES}")
704 logger.info(f"最大请求次数/MaxRequests: {MAX_REQUESTS}")
705 logger.info(f"请求限额窗口/LimitWindow: {LIMIT_WINDOW} 秒")
706
707 app.run(debug=True, host='0.0.0.0', port=int(os.environ.get('PORT', 7860)))