KSADASF/gemini-rproxy
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-exp"},
236 {"id": "gemini-2.0-pro-exp"},
237 {"id": "gemini-2.0-pro-exp-01-28"}
238]
239
240@app.route('/')
241def index():
242 main_content = "Moonfanz Reminiproxy v2.3.5 2025-01-14"
243 html_template = """
244<!DOCTYPE html>
245<html>
246<head>
247<meta charset="utf-8">
248<script>
249function copyToClipboard(text) {
250 var textarea = document.createElement("textarea");
251 textarea.textContent = text;
252 textarea.style.position = "fixed";
253 document.body.appendChild(textarea);
254 textarea.select();
255 try {
256 return document.execCommand("copy");
257 } catch (ex) {
258 console.warn("Copy to clipboard failed.", ex);
259 return false;
260 } finally {
261 document.body.removeChild(textarea);
262 }
263}
264function copyLink(event) {
265 event.preventDefault();
266 const url = new URL(window.location.href);
267 const link = url.protocol + '//' + url.host + '/hf/v1';
268 copyToClipboard(link);
269 alert('链接已复制: ' + link);
270}
271</script>
272</head>
273<body>
274{{ main_content }}<br/><br/>完全开源、免费且禁止商用<br/><br/>点击复制反向代理: <a href="v1" onclick="copyLink(event)">点击我复制地址(不要自己写地址)</a><br/>聊天来源选择"自定义(兼容 OpenAI)"<br/>将复制的网址填入到自定义端点<br/>将设置password填入自定义API秘钥<br/><br/><br/>
275</body>
276</html>
277 """
278 return render_template_string(html_template, main_content=main_content)
279
280def is_within_rate_limit(api_key):
281 now = datetime.now()
282 if api_key not in request_counts:
283 request_counts[api_key] = deque()
284
285 while request_counts[api_key] and request_counts[api_key][0] < now - timedelta(seconds=LIMIT_WINDOW):
286 request_counts[api_key].popleft()
287
288 if len(request_counts[api_key]) >= MAX_REQUESTS:
289 earliest_request_time = request_counts[api_key][0]
290 wait_time = (earliest_request_time + timedelta(seconds=LIMIT_WINDOW)) - now
291 return False, wait_time.total_seconds()
292 else:
293 return True, 0
294
295def increment_request_count(api_key):
296 now = datetime.now()
297 if api_key not in request_counts:
298 request_counts[api_key] = deque()
299 request_counts[api_key].append(now)
300
301def handle_api_error(error, attempt, current_api_key):
302 if attempt > MAX_RETRIES:
303 logger.error(f"{MAX_RETRIES} 次尝试后仍然失败,请修改预设或输入")
304 return 0, jsonify({
305 'error': {
306 'message': f"{MAX_RETRIES} 次尝试后仍然失败,请修改预设或输入",
307 'type': 'max_retries_exceeded'
308 }
309 })
310
311 if isinstance(error, requests.exceptions.HTTPError):
312 status_code = error.response.status_code
313
314 if status_code == 400:
315
316 try:
317 error_data = error.response.json()
318 if 'error' in error_data:
319 if error_data['error'].get('code') == "invalid_argument":
320 logger.error(f"{current_api_key[:8]} ... {current_api_key[-3:]} → 无效,可能已过期或被删除")
321 key_manager.blacklist_key(current_api_key)
322 switch_api_key()
323 return 0, None
324 error_message = error_data['error'].get('message', 'Bad Request')
325 error_type = error_data['error'].get('type', 'invalid_request_error')
326 logger.warning(f"400 错误请求: {error_message}")
327 return 2, jsonify({'error': {'message': error_message, 'type': error_type}})
328 except ValueError:
329 logger.warning("400 错误请求:响应不是有效的JSON格式")
330 return 2, jsonify({'error': {'message': '', 'type': 'invalid_request_error'}})
331
332 elif status_code == 429:
333 logger.warning(
334 f"{current_api_key[:8]} ... {current_api_key[-3:]} → 429 官方资源耗尽 → 立即重试..."
335 )
336 key_manager.blacklist_key(current_api_key)
337 switch_api_key()
338 return 0, None
339
340 elif status_code == 403:
341 logger.error(
342 f"{current_api_key[:8]} ... {current_api_key[-3:]} → 403 权限被拒绝,该 API KEY 可能已经被官方封禁"
343 )
344 key_manager.blacklist_key(current_api_key)
345 switch_api_key()
346 return 0, None
347
348 elif status_code == 500:
349 logger.warning(
350 f"{current_api_key[:8]} ... {current_api_key[-3:]} → 500 服务器内部错误 → 立即重试..."
351 )
352 switch_api_key()
353 return 0, None
354
355 elif status_code == 503:
356 logger.warning(
357 f"{current_api_key[:8]} ... {current_api_key[-3:]} → 503 服务不可用 → 立即重试..."
358 )
359 switch_api_key()
360 return 0, None
361
362 else:
363 logger.warning(
364 f"{current_api_key[:8]} ... {current_api_key[-3:]} → {status_code} 未知错误/模型不可用 → 不重试..."
365 )
366 switch_api_key()
367 return 2, None
368
369 elif isinstance(error, requests.exceptions.ConnectionError):
370 delay = min(RETRY_DELAY * (2 ** attempt), MAX_RETRY_DELAY)
371 logger.warning(f"连接错误 → 立即重试...")
372 time.sleep(delay)
373 return 0, None
374
375 elif isinstance(error, requests.exceptions.Timeout):
376 delay = min(RETRY_DELAY * (2 ** attempt), MAX_RETRY_DELAY)
377 logger.warning(f"请求超时 → 立即重试...")
378 time.sleep(delay)
379 return 0, None
380
381 else:
382 logger.error(f"发生未知错误: {error}")
383 return 0, jsonify({
384 'error': {
385 'message': f"发生未知错误: {error}",
386 'type': 'unknown_error'
387 }
388 })
389
390@app.route('/hf/v1/chat/completions', methods=['POST'])
391def chat_completions():
392 is_authenticated, auth_error, status_code = func.authenticate_request(request)
393 if not is_authenticated:
394 return auth_error if auth_error else jsonify({'error': '未授权'}), status_code if status_code else 401
395
396 request_data = request.get_json()
397 messages = request_data.get('messages', [])
398 model = request_data.get('model', 'gemini-2.0-flash-exp')
399 temperature = request_data.get('temperature', 1)
400 max_tokens = request_data.get('max_tokens', 8192)
401 show_thoughts = request_data.get('show_thoughts', False)
402 stream = request_data.get('stream', False)
403 use_system_prompt = request_data.get('use_system_prompt', False)
404 hint = "流式" if stream else "非流"
405 logger.info(f"\n{model} [{hint}] → {current_api_key[:8]}...{current_api_key[-3:]}")
406 is_thinking = 'thinking' in model
407 api_version = 'v1alpha' if is_thinking else 'v1beta'
408 response_type = 'streamGenerateContent' if stream else 'generateContent'
409 is_SSE = '&alt=sse' if stream else ''
410
411 contents, system_instruction, error_response = func.process_messages_for_gemini(messages, use_system_prompt)
412
413 if error_response:
414 logger.error(f"处理输入消息时出错↙\n {error_response}")
415 return jsonify(error_response), 400
416
417 def do_request(current_api_key, attempt):
418 isok, time_remaining = is_within_rate_limit(current_api_key)
419 if not isok:
420 logger.warning(f"暂时超过限额,该API key将在 {time_remaining} 秒后启用...")
421 switch_api_key()
422 return 0, None
423
424 increment_request_count(current_api_key)
425
426
427 url = f"https://generativelanguage.googleapis.com/{api_version}/models/{model}:{response_type}?key={current_api_key}{is_SSE}"
428 headers = {
429 "Content-Type": "application/json",
430 }
431
432 data = {
433 "contents": contents,
434 "generationConfig": {
435 "temperature": temperature,
436 "maxOutputTokens": max_tokens,
437 },
438 "safetySettings": safety_settings_g2 if 'gemini-2.0-flash-exp' in model else safety_settings,
439 }
440 if system_instruction:
441 data["system_instruction"] = system_instruction
442
443 try:
444 response = requests.post(url, headers=headers, json=data, stream=True)
445 response.raise_for_status()
446
447 if stream:
448 return 1, response
449 else:
450 return 1, ResponseWrapper(response.json())
451 except requests.exceptions.RequestException as e:
452 return handle_api_error(e, attempt, current_api_key)
453
454 def generate_stream(response):
455 logger.info(f"流式开始 →")
456 buffer = b""
457 try:
458 for line in response.iter_lines():
459 if not line:
460 continue
461 try:
462 if line.startswith(b'data: '):
463 line = line[6:]
464
465 buffer += line
466
467 try:
468 data = json.loads(buffer.decode('utf-8'))
469 buffer = b""
470 if 'candidates' in data and data['candidates']:
471 candidate = data['candidates'][0]
472 if 'content' in candidate:
473 content = candidate['content']
474 if 'parts' in content and content['parts']:
475 parts = content['parts']
476 if is_thinking and not show_thoughts:
477 parts = [part for part in parts if not part.get('thought')]
478 if parts:
479 text = parts[0].get('text', '')
480 finish_reason = candidate.get('finishReason')
481
482 if text:
483 data = {
484 'choices': [{
485 'delta': {
486 'content': text
487 },
488 'finish_reason': finish_reason,
489 'index': 0
490 }],
491 'object': 'chat.completion.chunk'
492 }
493 yield f"data: {json.dumps(data)}\n\n"
494
495 if candidate.get("finishReason") and candidate.get("finishReason") != "STOP":
496 error_message = {
497 "error": {
498 "code": "content_filter",
499 "message": f"模型的响应因违反内容政策而被标记:{candidate.get('finishReason')}",
500 "status": candidate.get("finishReason"),
501 "details": []
502 }
503 }
504 logger.warning(f"模型的响应因违反内容政策而被标记: {candidate.get('finishReason')}")
505 yield f"data: {json.dumps(error_message)}\n\n"
506 break
507
508 if 'safetyRatings' in candidate:
509 for rating in candidate['safetyRatings']:
510 if rating['probability'] == 'HIGH':
511 error_message = {
512 "error": {
513 "code": "content_filter",
514 "message": f"模型的响应因高概率被标记为 {rating['category']}",
515 "status": "SAFETY_RATING_HIGH",
516 "details": [rating]
517 }
518 }
519 logger.warning(f"模型的响应因高概率被标记为 {rating['category']}")
520 yield f"data: {json.dumps(error_message)}\n\n"
521 break
522 else:
523 continue
524 break
525
526 except json.JSONDecodeError:
527 logger.debug(f"JSON解析错误, 当前缓冲区内容: {buffer}")
528 continue
529
530 except Exception as e:
531 logger.error(f"流式处理期间发生错误: {e}, 原始数据行↙\n{line}")
532 yield f"data: {json.dumps({'error': str(e)})}\n\n"
533
534 else:
535 yield f"data: {json.dumps({'choices': [{'delta': {}, 'finish_reason': 'stop', 'index': 0}]})}\n\n"
536 logger.info(f"流式结束 ←")
537 logger.info(f"200!")
538 except Exception as e:
539 logger.error(f"流式处理错误↙\n{e}")
540 yield f"data: {json.dumps({'error': str(e)})}\n\n"
541
542 attempt = 0
543 success = 0
544 response = None
545 for attempt in range(1, MAX_RETRIES + 1):
546 logger.info(f"第 {attempt}/{MAX_RETRIES} 次尝试 ...")
547 success, response = do_request(current_api_key, attempt)
548
549 if success == 0:
550 continue
551 elif success == 1 and response is None:
552 continue
553 elif success == 1 and stream:
554 return Response(
555 stream_with_context(generate_stream(response)),
556 mimetype='text/event-stream'
557 )
558 elif success == 1 and isinstance(response, ResponseWrapper):
559 try:
560 text_content = response.text
561 prompt_tokens = response.prompt_token_count
562 completion_tokens = response.candidates_token_count
563 total_tokens = response.total_token_count
564 finish_reason = response.finish_reason
565
566 if text_content == '':
567 error_message = None
568 if response._data and 'error' in response._data:
569 error_message = response._data['error'].get('message')
570 if error_message:
571 logger.error(f"生成内容失败,API 返回错误: {error_message}")
572 else:
573 logger.error(f"生成内容失败: text_content 为空")
574 continue
575
576 if is_thinking and show_thoughts:
577 text_content = response.thoughts + '\n' + text_content
578
579 except AttributeError as e:
580 logger.error(f"处理响应失败,缺少必要的属性: {e}")
581 logger.error(f"原始响应: {response._data}")
582 continue
583
584 except Exception as e:
585 logger.error(f"处理响应失败: {e}")
586 continue
587
588 response_data = {
589 'id': 'chatcmpl-xxxxxxxxxxxx',
590 'object': 'chat.completion',
591 'created': int(datetime.now().timestamp()),
592 'model': model,
593 'choices': [{
594 'index': 0,
595 'message': {
596 'role': 'assistant',
597 'content': text_content
598 },
599 'finish_reason': finish_reason
600 }],
601 'usage': {
602 'prompt_tokens': prompt_tokens,
603 'completion_tokens': completion_tokens,
604 'total_tokens': total_tokens
605 }
606 }
607 logger.info(f"200!")
608 return jsonify(response_data)
609 elif success == 1 and isinstance(response, tuple):
610 return response[1], response[0]
611 elif success == 2:
612 logger.error(f"{model} 可能暂时不可用,请更换模型或未来一段时间再试")
613 response = {
614 'error': {
615 'message': f'{model} 可能暂时不可用,请更换模型或未来一段时间再试',
616 'type': 'internal_server_error'
617 }
618 }
619 return jsonify(response), 503
620 else:
621 logger.error(f"{MAX_RETRIES} 次尝试均失败,请重试或等待官方恢复")
622 response = {
623 'error': {
624 'message': f'{MAX_RETRIES} 次尝试均失败,请重试或等待官方恢复',
625 'type': 'internal_server_error'
626 }
627 }
628 return jsonify(response), 500 if response is not None else 503
629
630@app.route('/hf/v1/models', methods=['GET'])
631def list_models():
632 response = {"object": "list", "data": GEMINI_MODELS}
633 return jsonify(response)
634
635@app.route('/hf/v1/embeddings', methods=['POST'])
636def embeddings():
637 data = request.get_json()
638 model_input = data.get("input")
639 model = data.get("model", "text-embedding-004")
640 if not model_input:
641 return jsonify({"error": "没有提供输入"}), 400
642
643 if isinstance(model_input, str):
644 model_input = [model_input]
645
646 gemini_request = {
647 "model": f"models/{model}",
648 "content": {
649 "parts": [{"text": text} for text in model_input]
650 }
651 }
652
653 gemini_url = f"https://generativelanguage.googleapis.com/v1beta/models/{model}:embedContent?key={current_api_key}"
654 headers = {"Content-Type": "application/json"}
655 try:
656 gemini_response = requests.post(gemini_url, json=gemini_request, headers=headers)
657 gemini_response.raise_for_status()
658
659 response_json = gemini_response.json()
660 embeddings_data = []
661 if 'embedding' in response_json:
662 embeddings_data.append({
663 "object": "embedding",
664 "embedding": response_json['embedding']['values'],
665 "index": 0,
666 })
667 elif 'embeddings' in response_json:
668 for i, embedding in enumerate(response_json['embeddings']):
669 embeddings_data.append({
670 "object": "embedding",
671 "embedding": embedding['values'],
672 "index": i,
673 })
674
675 client_response = {
676 "object": "list",
677 "data": embeddings_data,
678 "model": model,
679 "usage": {
680 "prompt_tokens": 0,
681 "total_tokens": 0,
682 },
683 }
684 switch_api_key()
685 return jsonify(client_response)
686
687 except requests.exceptions.RequestException as e:
688 print(f"请求Embeddings失败↙\: {e}")
689 return jsonify({"error": str(e)}), 500
690
691def keep_alive():
692 try:
693 response = requests.get("http://127.0.0.1:7860/", timeout=10)
694 response.raise_for_status()
695 print(f"Keep alive ping successful: {response.status_code} at {time.ctime()}")
696 except requests.exceptions.RequestException as e:
697 print(f"Keep alive ping failed: {e} at {time.ctime()}")
698
699if __name__ == '__main__':
700 scheduler = BackgroundScheduler()
701
702 scheduler.add_job(keep_alive, 'interval', hours=12)
703 scheduler.start()
704 logger.info(f"Reminiproxy v2.3.5 启动")
705 logger.info(f"最大尝试次数/MaxRetries: {MAX_RETRIES}")
706 logger.info(f"最大请求次数/MaxRequests: {MAX_REQUESTS}")
707 logger.info(f"请求限额窗口/LimitWindow: {LIMIT_WINDOW} 秒")
708
709 app.run(debug=True, host='0.0.0.0', port=int(os.environ.get('PORT', 7860)))