dfbfguetrth commited on
Commit
e240a37
·
verified ·
1 Parent(s): 55dac1c

Upload 6 files

Browse files
Files changed (6) hide show
  1. Dockerfile +10 -0
  2. README.md +4 -5
  3. app.py +707 -0
  4. func.py +112 -0
  5. gitattributes +35 -0
  6. requirements.txt +6 -0
Dockerfile ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.9-slim
2
+
3
+ WORKDIR /app
4
+
5
+ COPY requirements.txt .
6
+ RUN pip install --no-cache-dir -r requirements.txt
7
+
8
+ COPY . .
9
+
10
+ CMD ["python", "app.py"]
README.md CHANGED
@@ -1,11 +1,10 @@
1
  ---
2
- title: '123123'
3
- emoji: 🐨
4
- colorFrom: green
5
- colorTo: purple
6
  sdk: docker
7
  pinned: false
8
- license: other
9
  ---
10
 
11
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
+ title: Gemini Rproxy
3
+ emoji: 🚀
4
+ colorFrom: gray
5
+ colorTo: blue
6
  sdk: docker
7
  pinned: false
 
8
  ---
9
 
10
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
app.py ADDED
@@ -0,0 +1,707 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, request, jsonify, Response, stream_with_context, render_template_string
2
+ import json
3
+ import os
4
+ import re
5
+ import logging
6
+ import func
7
+ from datetime import datetime, timedelta
8
+ from apscheduler.schedulers.background import BackgroundScheduler
9
+ import time
10
+ import requests
11
+ from collections import deque
12
+ import random
13
+ from dataclasses import dataclass
14
+ from typing import Optional, Dict, Any
15
+
16
+ app = Flask(__name__)
17
+
18
+ os.environ['TZ'] = 'Asia/Shanghai'
19
+
20
+ app = Flask(__name__)
21
+
22
+ app.secret_key = os.urandom(24)
23
+
24
+ formatter = logging.Formatter('%(message)s')
25
+ logger = logging.getLogger(__name__)
26
+ logger.setLevel(logging.INFO)
27
+ handler = logging.StreamHandler()
28
+ handler.setFormatter(formatter)
29
+ logger.addHandler(handler)
30
+
31
+ MAX_RETRIES = int(os.environ.get('MaxRetries', '3').strip() or '3')
32
+ MAX_REQUESTS = int(os.environ.get('MaxRequests', '2').strip() or '2')
33
+ LIMIT_WINDOW = int(os.environ.get('LimitWindow', '60').strip() or '60')
34
+
35
+ RETRY_DELAY = 1
36
+ MAX_RETRY_DELAY = 16
37
+
38
+ request_counts = {}
39
+
40
+ api_key_blacklist = set()
41
+ api_key_blacklist_duration = 60
42
+
43
+
44
+ safety_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
+ ]
66
+ safety_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
89
+ class GeneratedText:
90
+ text: str
91
+ finish_reason: Optional[str] = None
92
+
93
+
94
+ class 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
+
175
+ class 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
+
205
+ key_manager = APIKeyManager()
206
+ key_manager.show_all_keys()
207
+ current_api_key = key_manager.get_available_key()
208
+
209
+ def 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
+
218
+ logger.info(f"当前 API key: {current_api_key[:8]}...{current_api_key[-3:]}")
219
+
220
+ GEMINI_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('/')
239
+ def 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>
247
+ function 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
+ }
262
+ function 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
+
278
+ def 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
+
293
+ def 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
+
299
+ def 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'])
389
+ def 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'])
629
+ def list_models():
630
+ response = {"object": "list", "data": GEMINI_MODELS}
631
+ return jsonify(response)
632
+
633
+ @app.route('/hf/v1/embeddings', methods=['POST'])
634
+ def 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
+
689
+ # def 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
+
697
+ if __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=7860)
func.py ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import jsonify
2
+ import logging
3
+ import os
4
+ logger = logging.getLogger(__name__)
5
+
6
+ request_counts = {}
7
+
8
+ password = os.environ['ProxyPassword']
9
+
10
+ def authenticate_request(request):
11
+ auth_header = request.headers.get('Authorization')
12
+
13
+ if not auth_header:
14
+ return False, jsonify({'error': '缺少Authorization请求头'}), 401
15
+
16
+ try:
17
+ auth_type, pass_word = auth_header.split(' ', 1)
18
+ except ValueError:
19
+ return False, jsonify({'error': 'Authorization请求头格式错误'}), 401
20
+
21
+ if auth_type.lower() != 'bearer':
22
+ return False, jsonify({'error': 'Authorization类型必须为Bearer'}), 401
23
+
24
+ if pass_word != password:
25
+ return False, jsonify({'error': '未授权'}), 401
26
+
27
+ return True, None, None
28
+
29
+ def process_messages_for_gemini(messages, use_system_prompt=False):
30
+ gemini_history = []
31
+ errors = []
32
+ system_instruction_text = ""
33
+ is_system_phase = use_system_prompt
34
+ for i, message in enumerate(messages):
35
+ role = message.get('role')
36
+ content = message.get('content')
37
+
38
+ if isinstance(content, str):
39
+ if is_system_phase and role == 'system':
40
+ if system_instruction_text:
41
+ system_instruction_text += "\n" + content
42
+ else:
43
+ system_instruction_text = content
44
+ else:
45
+ is_system_phase = False
46
+
47
+ if role in ['user', 'system']:
48
+ role_to_use = 'user'
49
+ elif role == 'assistant':
50
+ role_to_use = 'model'
51
+ else:
52
+ errors.append(f"Invalid role: {role}")
53
+ continue
54
+
55
+ if gemini_history and gemini_history[-1]['role'] == role_to_use:
56
+ gemini_history[-1]['parts'].append({"text": content})
57
+ else:
58
+ gemini_history.append({"role": role_to_use, "parts": [{"text": content}]})
59
+
60
+ elif isinstance(content, list):
61
+ parts = []
62
+ for item in content:
63
+ if item.get('type') == 'text':
64
+ parts.append({"text": item.get('text')})
65
+ elif item.get('type') == 'image_url':
66
+ image_data = item.get('image_url', {}).get('url', '')
67
+ if image_data.startswith('data:image/'):
68
+ try:
69
+ mime_type, base64_data = image_data.split(';')[0].split(':')[1], image_data.split(',')[1]
70
+ parts.append({
71
+ "inline_data": {
72
+ "mime_type": mime_type,
73
+ "data": base64_data
74
+ }
75
+ })
76
+ except (IndexError, ValueError):
77
+ errors.append(f"Invalid data URI for image: {image_data}")
78
+ else:
79
+ errors.append(f"Invalid image URL format for item: {item}")
80
+ elif item.get('type') == 'file_url':
81
+ file_data = item.get('file_url', {}).get('url', '')
82
+ if file_data.startswith('data:'):
83
+ try:
84
+ mime_type, base64_data = file_data.split(';')[0].split(':')[1], file_data.split(',')[1]
85
+ parts.append({
86
+ "inline_data": {
87
+ "mime_type": mime_type,
88
+ "data": base64_data
89
+ }
90
+ })
91
+ except (IndexError, ValueError):
92
+ errors.append(f"Invalid data URI for file: {file_data}")
93
+ else:
94
+ errors.append(f"Invalid file URL format for item: {item}")
95
+
96
+ if parts:
97
+ if role in ['user', 'system']:
98
+ role_to_use = 'user'
99
+ elif role == 'assistant':
100
+ role_to_use = 'model'
101
+ else:
102
+ errors.append(f"Invalid role: {role}")
103
+ continue
104
+ if gemini_history and gemini_history[-1]['role'] == role_to_use:
105
+ gemini_history[-1]['parts'].extend(parts)
106
+ else:
107
+ gemini_history.append({"role": role_to_use, "parts": parts})
108
+
109
+ if errors:
110
+ return gemini_history, {"parts": [{"text": system_instruction_text}]}, (jsonify({'error': errors}), 400)
111
+ else:
112
+ return gemini_history, {"parts": [{"text": system_instruction_text}]}, None
gitattributes ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ Flask==2.0.3
2
+ Flask-CORS==3.0.10
3
+ requests==2.32.3
4
+ Werkzeug==3.1.3
5
+ pillow==10.3.0
6
+ APScheduler==3.11.0