Astrobot314/Gemini-reproxy
0
1from flask import Flask, request, jsonify, Response, stream_with_context, render_template_string
2from google.generativeai.types import BlockedPromptException, StopCandidateException, generation_types
3from google.api_core.exceptions import InvalidArgument, ResourceExhausted, Aborted, InternalServerError, ServiceUnavailable, PermissionDenied
4import google.generativeai as genai
5import json
6import os
7import re
8import logging
9import func
10from datetime import datetime, timedelta
11from apscheduler.schedulers.background import BackgroundScheduler
12import time
13import requests
14from collections import deque
15import random
16
17os.environ['TZ'] = 'Asia/Shanghai'
18
19app = Flask(__name__)
20
21app.secret_key = os.urandom(24)
22
23formatter = logging.Formatter('%(message)s')
24logger = logging.getLogger(__name__)
25logger.setLevel(logging.INFO)
26handler = logging.StreamHandler()
27handler.setFormatter(formatter)
28logger.addHandler(handler)
29
30MAX_RETRIES = int(os.environ.get('MaxRetries', 3))
31MAX_REQUESTS = int(os.environ.get('MaxRequests', 2))
32LIMIT_WINDOW = int(os.environ.get('LimitWindow', 60))
33RETRY_DELAY = 1
34MAX_RETRY_DELAY = 16
35
36request_counts = {}
37
38api_key_blacklist = set()
39api_key_blacklist_duration = 60
40
41# 核心优势
42safety_settings = [
43 {
44 "category": "HARM_CATEGORY_HARASSMENT",
45 "threshold": "BLOCK_NONE"
46 },
47 {
48 "category": "HARM_CATEGORY_HATE_SPEECH",
49 "threshold": "BLOCK_NONE"
50 },
51 {
52 "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
53 "threshold": "BLOCK_NONE"
54 },
55 {
56 "category": "HARM_CATEGORY_DANGEROUS_CONTENT",
57 "threshold": "BLOCK_NONE"
58 }
59]
60
61class APIKeyManager:
62 def __init__(self):
63 self.api_keys = re.findall(r"AIzaSy[a-zA-Z0-9_-]{33}", os.environ.get('KeyArray'))
64 self.current_index = random.randint(0, len(self.api_keys) - 1)
65
66 def get_available_key(self):
67 num_keys = len(self.api_keys)
68 for _ in range(num_keys):
69 if self.current_index >= num_keys:
70 self.current_index = 0
71 current_key = self.api_keys[self.current_index]
72 self.current_index += 1
73
74 if current_key not in api_key_blacklist:
75 return current_key
76
77 logger.error("所有API key都已耗尽或被暂时禁用,请重新配置或稍后重试")
78 return None
79
80 def show_all_keys(self):
81 logger.info(f"当前可用API key个数: {len(self.api_keys)} ")
82 for i, api_key in enumerate(self.api_keys):
83 logger.info(f"API Key{i}: {api_key[:11]}...{api_key[-3:]}")
84
85 def blacklist_key(self, key):
86 logger.warning(f"{key[:11]} → 暂时禁用 {api_key_blacklist_duration} 秒")
87 api_key_blacklist.add(key)
88
89 scheduler.add_job(lambda: api_key_blacklist.discard(key), 'date', run_date=datetime.now() + timedelta(seconds=api_key_blacklist_duration))
90
91key_manager = APIKeyManager()
92key_manager.show_all_keys()
93current_api_key = key_manager.get_available_key()
94
95def switch_api_key():
96 global current_api_key
97 key = key_manager.get_available_key()
98 if key:
99 current_api_key = key
100 logger.info(f"API key 替换为 → {current_api_key[:11]}...{current_api_key[-3:]}")
101 else:
102 logger.error("API key 替换失败,所有API key都已耗尽或被暂时禁用,请重新配置或稍后重试")
103
104logger.info(f"当前 API key: {current_api_key[:11]}...{current_api_key[-3:]}")
105
106GEMINI_MODELS = [
107 {"id": "gemini-1.5-flash-8b-latest"},
108 {"id": "gemini-1.5-flash-8b-exp-0924"},
109 {"id": "gemini-1.5-flash-latest"},
110 {"id": "gemini-1.5-flash-exp-0827"},
111 {"id": "gemini-1.5-pro-latest"},
112 {"id": "gemini-1.5-pro-exp-0827"},
113 {"id": "learnlm-1.5-pro-experimental"},
114 {"id": "gemini-exp-1114"},
115 {"id": "gemini-exp-1121"},
116 {"id": "gemini-exp-1206"},
117 {"id": "gemini-2.0-flash-exp"},
118 {"id": "gemini-2.0-flash-thinking-exp-1219"},
119 {"id": "gemini-2.0-pro-exp"}
120]
121
122@app.route('/')
123def index():
124 main_content = "Moonfanz Reminiproxy v2.2.0 2025-01-11"
125 html_template = """
126<!DOCTYPE html>
127<html>
128<head>
129<meta charset="utf-8">
130<script>
131function copyToClipboard(text) {
132 var textarea = document.createElement("textarea");
133 textarea.textContent = text;
134 textarea.style.position = "fixed";
135 document.body.appendChild(textarea);
136 textarea.select();
137 try {
138 return document.execCommand("copy");
139 } catch (ex) {
140 console.warn("Copy to clipboard failed.", ex);
141 return false;
142 } finally {
143 document.body.removeChild(textarea);
144 }
145}
146function copyLink(event) {
147 event.preventDefault();
148 const url = new URL(window.location.href);
149 const link = url.protocol + '//' + url.host + '/hf/v1';
150 copyToClipboard(link);
151 alert('链接已复制: ' + link);
152}
153</script>
154</head>
155<body>
156{{ main_content }}<br/><br/>完全开源、免费且禁止商用<br/><br/>点击复制反向代理: <a href="v1" onclick="copyLink(event)">Copy Link</a><br/>聊天来源选择"自定义(兼容 OpenAI)"<br/>将复制的网址填入到自定义端点<br/>将设置password填入自定义API秘钥<br/><br/><br/>
157</body>
158</html>
159 """
160 return render_template_string(html_template, main_content=main_content)
161
162def is_within_rate_limit(api_key):
163 now = datetime.now()
164 if api_key not in request_counts:
165 request_counts[api_key] = deque()
166
167 while request_counts[api_key] and request_counts[api_key][0] < now - timedelta(seconds=LIMIT_WINDOW):
168 request_counts[api_key].popleft()
169
170 if len(request_counts[api_key]) >= MAX_REQUESTS:
171 earliest_request_time = request_counts[api_key][0]
172 wait_time = (earliest_request_time + timedelta(seconds=LIMIT_WINDOW)) - now
173 return False, wait_time.total_seconds()
174 else:
175 return True, 0
176
177def increment_request_count(api_key):
178 now = datetime.now()
179 if api_key not in request_counts:
180 request_counts[api_key] = deque()
181 request_counts[api_key].append(now)
182
183def handle_api_error(error, attempt):
184 if attempt > MAX_RETRIES:
185 logger.error(f"{MAX_RETRIES} 次尝试后仍然失败,请修改预设或输入")
186 return 0, jsonify({
187 'error': {
188 'message': f"{MAX_RETRIES} 次尝试后仍然失败,请修改预设或输入",
189 'type': 'max_retries_exceeded'
190 }
191 })
192
193 if isinstance(error, InvalidArgument):
194 logger.error(f"{current_api_key[:11]} → 无效,可能已过期或被删除")
195 key_manager.blacklist_key(current_api_key)
196 switch_api_key()
197 return 0, None
198
199 elif isinstance(error, ResourceExhausted):
200 delay = min(RETRY_DELAY * (2 ** attempt), MAX_RETRY_DELAY)
201 logger.warning(f"{current_api_key[:11]} → 429 官方资源耗尽 → {delay} 秒后重试...")
202 key_manager.blacklist_key(current_api_key)
203 switch_api_key()
204 time.sleep(delay)
205 return 0, None
206
207 elif isinstance(error, Aborted):
208 delay = min(RETRY_DELAY * (2 ** attempt), MAX_RETRY_DELAY)
209 logger.warning(f"{current_api_key[:11]} → 操作被中止 → {delay} 秒后重试...")
210 switch_api_key()
211 time.sleep(delay)
212 return 0, None
213
214 elif isinstance(error, InternalServerError):
215 delay = min(RETRY_DELAY * (2 ** attempt), MAX_RETRY_DELAY)
216 logger.warning(f"{current_api_key[:11]} → 500 服务器内部错误 → {delay} 秒后重试...")
217 switch_api_key()
218 time.sleep(delay)
219 return 0, None
220
221 elif isinstance(error, ServiceUnavailable):
222 delay = min(RETRY_DELAY * (2 ** attempt), MAX_RETRY_DELAY)
223 logger.warning(f"{current_api_key[:11]} → 503 服务不可用 → {delay} 秒后重试...")
224 switch_api_key()
225 time.sleep(delay)
226 return 0, None
227
228 elif isinstance(error, PermissionDenied):
229 logger.error(f"{current_api_key[:11]} → 403 权限被拒绝,该 API KEY 可能已经被官方封禁")
230 key_manager.blacklist_key(current_api_key)
231 switch_api_key()
232 return 0, None
233
234 elif isinstance(error, StopCandidateException):
235 logger.warning(f"AI输出内容被Gemini官方阻挡,代理没有得到有效回复")
236 switch_api_key()
237 return 0, None
238
239 elif isinstance(error, generation_types.BlockedPromptException):
240 try:
241 full_reason_str = str(error.args[0])
242 logger.error(f"{full_reason_str}")
243 if "block_reason:" in full_reason_str:
244 start_index = full_reason_str.find("block_reason:") + len("block_reason:")
245 block_reason_str = full_reason_str[start_index:].strip()
246
247 if block_reason_str == "SAFETY":
248 logger.warning(f"用户输入因安全原因被阻止")
249 return 1, None
250 elif block_reason_str == "BLOCKLIST":
251 logger.warning(f"用户输入因包含阻止列表中的术语而被阻止")
252 return 1, None
253 elif block_reason_str == "PROHIBITED_CONTENT":
254 logger.warning(f"用户输入因包含禁止内容而被阻止")
255 return 1, None
256 elif block_reason_str == "OTHER":
257 logger.warning(f"用户输入因未知原因被阻止")
258 return 1, None
259 else:
260 logger.warning(f"用户输入被阻止,原因未知: {block_reason_str}")
261 return 1, None
262 else:
263 logger.warning(f"用户输入被阻止,原因未知: {full_reason_str}")
264 return 1, None
265
266 except (IndexError, AttributeError) as e:
267 logger.error(f"获取提示原因失败↙\n{e}")
268 logger.error(f"提示被阻止↙\n{error}")
269 return 2, None
270
271 else:
272 logger.error(f"该模型还未发布,暂时不可用,请更换模型或未来一段时间再试")
273 logger.error(f"证明↙\n{error}")
274 return 2, None
275
276@app.route('/hf/v1/chat/completions', methods=['POST'])
277def chat_completions():
278 is_authenticated, auth_error, status_code = func.authenticate_request(request)
279 if not is_authenticated:
280 return auth_error if auth_error else jsonify({'error': '未授权'}), status_code if status_code else 401
281
282 request_data = request.get_json()
283 messages = request_data.get('messages', [])
284 model = request_data.get('model', 'gemini-2.0-flash-exp')
285 temperature = request_data.get('temperature', 1)
286 max_tokens = request_data.get('max_tokens', 8192)
287 stream = request_data.get('stream', False)
288 hint = "流式" if stream else "非流"
289 logger.info(f"\n{model} [{hint}] → {current_api_key[:11]}...")
290
291 gemini_history, user_message, system_instruction, error_response = func.process_messages_for_gemini(messages)
292 # r_g = json.dumps(gemini_history, indent=4, ensure_ascii=False).replace('\\n', '\n')
293 # r_u = json.dumps(user_message, indent=4, ensure_ascii=False).replace('\\n', '\n')
294 # r_s = json.dumps(system_instruction, indent=4, ensure_ascii=False).replace('\\n', '\n')
295 # logger.info(f"历史对话: {r_g}")
296 # logger.info(f"用户消息: {r_u}")
297 # logger.info(f"系统指令: {r_s}")
298 if error_response:
299 logger.error(f"处理输入消息时出错↙\n {error_response}")
300 return jsonify(error_response), 400
301
302 def do_request(current_api_key, attempt):
303 isok, time = is_within_rate_limit(current_api_key)
304 if not isok:
305 logger.warning(f"{current_api_key[:11]} → 暂时超过限额,该API key将在 {time} 秒后启用...")
306 switch_api_key()
307 return 0, None
308
309 increment_request_count(current_api_key)
310
311 genai.configure(api_key=current_api_key)
312
313 generation_config = {
314 "temperature": temperature,
315 "max_output_tokens": max_tokens
316 }
317
318 gen_model = genai.GenerativeModel(
319 model_name=model,
320 generation_config=generation_config,
321 safety_settings=safety_settings,
322 system_instruction=system_instruction
323 )
324
325 try:
326 if gemini_history:
327 chat_session = gen_model.start_chat(history=gemini_history)
328 response = chat_session.send_message(user_message, stream=stream)
329 else:
330 response = gen_model.generate_content(user_message, stream=stream)
331 return 1, response
332 except Exception as e:
333 return handle_api_error(e, attempt)
334
335 def generate(response):
336 try:
337 logger.info(f"流式开始...")
338 for chunk in response:
339 if chunk.text:
340 data = {
341 'choices': [
342 {
343 'delta': {
344 'content': chunk.text
345 },
346 'finish_reason': None,
347 'index': 0
348 }
349 ],
350 'object': 'chat.completion.chunk'
351 }
352 yield f"data: {json.dumps(data)}\n\n"
353
354 data = {
355 'choices': [
356 {
357 'delta': {},
358 'finish_reason': 'stop',
359 'index': 0
360 }
361 ],
362 'object': 'chat.completion.chunk'
363 }
364 logger.info(f"流式结束")
365 yield f"data: {json.dumps(data)}\n\n"
366 logger.info(f"200!")
367
368 except Exception:
369 logger.error(f"流式输出中途被截断,请关闭流式输出或修改你的输入")
370 logger.info(f"流式结束")
371 error_data = {
372 'error': {
373 'message': '流式输出时截断,请关闭流式输出或修改你的输入',
374 'type': 'internal_server_error'
375 }
376 }
377 yield f"data: {json.dumps(error_data)}\n\n"
378 data = {
379 'choices': [
380 {
381 'delta': {},
382 'finish_reason': 'stop',
383 'index': 0
384 }
385 ],
386 'object': 'chat.completion.chunk'
387 }
388
389 yield f"data: {json.dumps(data)}\n\n"
390
391 attempt = 0
392 success = 0
393 response = None
394
395 for attempt in range(1, MAX_RETRIES + 1):
396 logger.info(f"第 {attempt}/{MAX_RETRIES} 次尝试 ...")
397 success, response = do_request(current_api_key, attempt)
398
399 if success == 1:
400 break
401 elif success == 2:
402
403 logger.error(f"{model} 很可能暂时不可用,请更换模型或未来一段时间再试")
404 response = {
405 'error': {
406 'message': f'{model} 很可能暂时不可用,请更换模型或未来一段时间再试',
407 'type': 'internal_server_error'
408 }
409 }
410 return jsonify(response), 503
411
412 else:
413 logger.error(f"{MAX_RETRIES} 次尝试均失败,请调整配置,或等待官方恢复,或向Moonfanz反馈")
414 response = {
415 'error': {
416 'message': f'{MAX_RETRIES} 次尝试均失败,请调整配置或向Moonfanz反馈',
417 'type': 'internal_server_error'
418 }
419 }
420 return jsonify(response), 500 if response is not None else 503
421
422 if stream:
423 return Response(stream_with_context(generate(response)), mimetype='text/event-stream')
424 else:
425 try:
426 text_content = response.text
427 except (AttributeError, IndexError, TypeError, ValueError) as e:
428 if "response.candidates" in str(e) or "response.text" in str(e):
429 logger.error(f"用户输入被AI安全过滤器阻止")
430 return jsonify({
431 'error': {
432 'message': '用户输入被AI安全过滤器阻止',
433 'type': 'prompt_blocked_error',
434 'details': str(e)
435 }
436 }), 400
437 else:
438 return jsonify({
439 'error': {
440 'message': 'AI响应处理失败',
441 'type': 'response_processing_error'
442 }
443 }), 500
444
445 response_data = {
446 'id': 'chatcmpl-xxxxxxxxxxxx',
447 'object': 'chat.completion',
448 'created': int(datetime.now().timestamp()),
449 'model': model,
450 'choices': [{
451 'index': 0,
452 'message': {
453 'role': 'assistant',
454 'content': text_content
455 },
456 'finish_reason': 'stop'
457 }],
458 'usage': {
459 'prompt_tokens': 0,
460 'completion_tokens': 0,
461 'total_tokens': 0
462 }
463 }
464 logger.info(f"200!")
465 return jsonify(response_data)
466
467@app.route('/hf/v1/models', methods=['GET'])
468def list_models():
469 response = {"object": "list", "data": GEMINI_MODELS}
470 return jsonify(response)
471
472def keep_alive():
473 try:
474 response = requests.get("http://127.0.0.1:7860/", timeout=10)
475 response.raise_for_status()
476 print(f"Keep alive ping successful: {response.status_code} at {time.ctime()}")
477 except requests.exceptions.RequestException as e:
478 print(f"Keep alive ping failed: {e} at {time.ctime()}")
479
480if __name__ == '__main__':
481 scheduler = BackgroundScheduler()
482
483 scheduler.add_job(keep_alive, 'interval', hours=12)
484 scheduler.start()
485 logger.info(f"Reminiproxy v2.2.0 启动")
486 logger.info(f"最大尝试次数/MaxRetries: {MAX_RETRIES}")
487 logger.info(f"最大请求次数/MaxRequests: {MAX_REQUESTS}")
488 logger.info(f"请求限额窗口/LimitWindow: {LIMIT_WINDOW} 秒")
489
490 app.run(debug=True, host='0.0.0.0', port=int(os.environ.get('PORT', 7860)))