Nigel123321 commited on
Commit
db1228a
·
verified ·
1 Parent(s): eff685e

Upload 5 files

Browse files
Files changed (5) hide show
  1. Dockerfile +10 -0
  2. README.md +4 -6
  3. app.py +709 -0
  4. func.py +148 -0
  5. requirements.txt +8 -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,12 +1,10 @@
1
  ---
2
- title: TiktokRefuner
3
- emoji: 😻
4
- colorFrom: green
5
- colorTo: gray
6
  sdk: docker
7
  pinned: false
8
- license: openrail
9
- short_description: TiktokRefuner_for tech otachyus
10
  ---
11
 
12
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
+ title: TiktokRefurner
3
+ emoji: 🔥
4
+ colorFrom: red
5
+ colorTo: purple
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,709 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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-exp"},
236
+ {"id": "gemini-2.0-pro-exp"},
237
+ {"id": "gemini-2.0-pro-exp-01-28"}
238
+ ]
239
+
240
+ @app.route('/')
241
+ def 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>
249
+ function 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
+ }
264
+ function 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
+
280
+ def 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
+
295
+ def 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
+
301
+ def 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'])
391
+ def 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'])
631
+ def list_models():
632
+ response = {"object": "list", "data": GEMINI_MODELS}
633
+ return jsonify(response)
634
+
635
+ @app.route('/hf/v1/embeddings', methods=['POST'])
636
+ def 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
+
691
+ def 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
+
699
+ if __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)))
func.py ADDED
@@ -0,0 +1,148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import jsonify
2
+ import logging
3
+ import os
4
+
5
+ formatter = logging.Formatter('%(message)s')
6
+ logger = logging.getLogger(__name__)
7
+ logger.setLevel(logging.INFO)
8
+ handler = logging.StreamHandler()
9
+ handler.setFormatter(formatter)
10
+ logger.addHandler(handler)
11
+
12
+ request_counts = {}
13
+
14
+ password = os.environ['password']
15
+
16
+ def authenticate_request(request):
17
+ auth_header = request.headers.get('Authorization')
18
+
19
+ if not auth_header:
20
+ return False, jsonify({'error': '缺少Authorization请求头'}), 401
21
+
22
+ try:
23
+ auth_type, pass_word = auth_header.split(' ', 1)
24
+ except ValueError:
25
+ return False, jsonify({'error': 'Authorization请求头格式错误'}), 401
26
+
27
+ if auth_type.lower() != 'bearer':
28
+ return False, jsonify({'error': 'Authorization类型必须为Bearer'}), 401
29
+
30
+ if pass_word != password:
31
+ return False, jsonify({'error': '未授权'}), 401
32
+
33
+ return True, None, None
34
+
35
+ def process_messages_for_gemini(messages, use_system_prompt=False):
36
+ gemini_history = []
37
+ errors = []
38
+ system_instruction_text = ""
39
+ is_system_phase = use_system_prompt
40
+
41
+
42
+ for i, message in enumerate(messages):
43
+
44
+ role = message.get('role')
45
+ content = message.get('content')
46
+
47
+ if isinstance(content, str):
48
+
49
+ if is_system_phase and role == 'system':
50
+
51
+ if system_instruction_text:
52
+ system_instruction_text += "\n" + content
53
+ else:
54
+ system_instruction_text = content
55
+
56
+ else:
57
+ is_system_phase = False
58
+
59
+
60
+ if role in ['user', 'system']:
61
+ role_to_use = 'user'
62
+ elif role == 'assistant':
63
+ role_to_use = 'model'
64
+ else:
65
+ errors.append(f"Invalid role: {role}")
66
+ logger.error(f"Invalid role: {role}")
67
+ continue
68
+
69
+ if gemini_history and gemini_history[-1]['role'] == role_to_use:
70
+ gemini_history[-1]['parts'].append({"text": content})
71
+
72
+ else:
73
+ gemini_history.append({"role": role_to_use, "parts": [{"text": content}]})
74
+
75
+
76
+ elif isinstance(content, list):
77
+
78
+ parts = []
79
+ for item in content:
80
+
81
+ if item.get('type') == 'text':
82
+ parts.append({"text": item.get('text')})
83
+
84
+ elif item.get('type') == 'image_url':
85
+ image_data = item.get('image_url', {}).get('url', '')
86
+
87
+ if image_data.startswith('data:image/'):
88
+ try:
89
+ mime_type, base64_data = image_data.split(';')[0].split(':')[1], image_data.split(',')[1]
90
+ parts.append({
91
+ "inline_data": {
92
+ "mime_type": mime_type,
93
+ "data": base64_data
94
+ }
95
+ })
96
+
97
+ except (IndexError, ValueError) as e:
98
+ error_message = f"Invalid data URI for image: {image_data}. Error: {e}"
99
+ errors.append(error_message)
100
+ logger.error(error_message)
101
+ else:
102
+ error_message = f"Invalid image URL format for item: {item}. URL should start with 'data:image/' for inline images."
103
+ errors.append(error_message)
104
+ logger.error(error_message)
105
+ elif item.get('type') == 'file_url':
106
+ file_data = item.get('file_url', {}).get('url', '')
107
+
108
+ if file_data.startswith('data:'):
109
+ try:
110
+ mime_type, base64_data = file_data.split(';')[0].split(':')[1], file_data.split(',')[1]
111
+ parts.append({
112
+ "inline_data": {
113
+ "mime_type": mime_type,
114
+ "data": base64_data
115
+ }
116
+ })
117
+
118
+ except (IndexError, ValueError) as e:
119
+ error_message = f"Invalid data URI for file: {file_data}. Error: {e}"
120
+ errors.append(error_message)
121
+ logger.error(error_message)
122
+ else:
123
+ error_message = f"Invalid file URL format for item: {item}. URL should start with 'data:' for inline files."
124
+ errors.append(error_message)
125
+ logger.error(error_message)
126
+
127
+ if parts:
128
+ if role in ['user', 'system']:
129
+ role_to_use = 'user'
130
+ elif role == 'assistant':
131
+ role_to_use = 'model'
132
+ else:
133
+ errors.append(f"Invalid role: {role}")
134
+ logger.error(f"Invalid role: {role}")
135
+ continue
136
+ if gemini_history and gemini_history[-1]['role'] == role_to_use:
137
+ gemini_history[-1]['parts'].extend(parts)
138
+
139
+ else:
140
+ gemini_history.append({"role": role_to_use, "parts": parts})
141
+
142
+
143
+ if errors:
144
+ logger.warning(f"Errors encountered during message processing: {errors}")
145
+ return gemini_history, {"parts": [{"text": system_instruction_text}]}, (jsonify({'error': errors}), 400)
146
+ else:
147
+
148
+ return gemini_history, {"parts": [{"text": system_instruction_text}]}, None
requirements.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ Flask==3.1.0
2
+ Flask-CORS==5.0.0
3
+ requests==2.32.3
4
+ Werkzeug==3.1.3
5
+ google==3.0.0
6
+ google-generativeai==0.8.3
7
+ pillow==10.3.0
8
+ APScheduler==3.11.0