init
Browse files- Dockerfile +10 -0
- README.md +3 -3
- app.py +273 -0
- chat.txt +146 -0
- func.py +160 -0
- requirements.txt +9 -0
- templates/login.html +20 -0
- templates/manage.html +33 -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,7 +1,7 @@
|
|
1 |
---
|
2 |
-
title:
|
3 |
-
emoji:
|
4 |
-
colorFrom:
|
5 |
colorTo: yellow
|
6 |
sdk: docker
|
7 |
pinned: false
|
|
|
1 |
---
|
2 |
+
title: my_2
|
3 |
+
emoji: 🔥
|
4 |
+
colorFrom: green
|
5 |
colorTo: yellow
|
6 |
sdk: docker
|
7 |
pinned: false
|
app.py
ADDED
@@ -0,0 +1,273 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from flask import Flask, request, jsonify, Response, stream_with_context, render_template, session, redirect, url_for
|
2 |
+
from apscheduler.schedulers.background import BackgroundScheduler
|
3 |
+
import google.generativeai as genai
|
4 |
+
import json
|
5 |
+
from datetime import datetime
|
6 |
+
import os
|
7 |
+
from termcolor import colored
|
8 |
+
import logging
|
9 |
+
import func
|
10 |
+
import requests
|
11 |
+
import time
|
12 |
+
|
13 |
+
os.environ['TZ'] = 'Asia/Shanghai'
|
14 |
+
app = Flask(__name__)
|
15 |
+
if not os.environ.get('TERM'):
|
16 |
+
os.environ['TERM'] = 'xterm'
|
17 |
+
app.secret_key = os.urandom(24)
|
18 |
+
|
19 |
+
ProxyPasswords = os.environ.get('ProxyPasswords').split(',')
|
20 |
+
|
21 |
+
formatter = logging.Formatter('%(message)s')
|
22 |
+
ADMIN_PASSWORD = os.environ.get('AdminPassword')
|
23 |
+
|
24 |
+
logger = logging.getLogger(__name__)
|
25 |
+
logger.setLevel(logging.INFO)
|
26 |
+
|
27 |
+
handler = logging.StreamHandler()
|
28 |
+
handler.setFormatter(formatter)
|
29 |
+
|
30 |
+
logger.addHandler(handler)
|
31 |
+
|
32 |
+
safety_settings = [
|
33 |
+
{
|
34 |
+
"category": "HARM_CATEGORY_HARASSMENT",
|
35 |
+
"threshold": "BLOCK_NONE"
|
36 |
+
},
|
37 |
+
{
|
38 |
+
"category": "HARM_CATEGORY_HATE_SPEECH",
|
39 |
+
"threshold": "BLOCK_NONE"
|
40 |
+
},
|
41 |
+
{
|
42 |
+
"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
|
43 |
+
"threshold": "BLOCK_NONE"
|
44 |
+
},
|
45 |
+
{
|
46 |
+
"category": "HARM_CATEGORY_DANGEROUS_CONTENT",
|
47 |
+
"threshold": "BLOCK_NONE"
|
48 |
+
},
|
49 |
+
]
|
50 |
+
class APIKeyManager:
|
51 |
+
def __init__(self):
|
52 |
+
self.api_keys = os.environ.get('API_KEYS').split(',')
|
53 |
+
self.current_index = 0
|
54 |
+
|
55 |
+
|
56 |
+
def get_available_key(self):
|
57 |
+
if self.current_index >= len(self.api_keys):
|
58 |
+
self.current_index = 0
|
59 |
+
current_key = self.api_keys[self.current_index]
|
60 |
+
self.current_index += 1
|
61 |
+
return current_key
|
62 |
+
|
63 |
+
key_manager = APIKeyManager()
|
64 |
+
current_api_key = key_manager.get_available_key()
|
65 |
+
logger.info(f"Current API key: {current_api_key[:11]}...")
|
66 |
+
genai.configure(api_key=current_api_key)
|
67 |
+
models = genai.list_models()
|
68 |
+
logger.info("Available models:")
|
69 |
+
for model in models:
|
70 |
+
logger.info(f"- {model.name}: {model.supported_generation_methods}")
|
71 |
+
GEMINI_MODELS = [
|
72 |
+
{"id": "gemini-pro"},
|
73 |
+
{"id": "gemini-pro-vision"},
|
74 |
+
{"id": "gemini-1.0-pro"},
|
75 |
+
{"id": "gemini-1.0-pro-vision"},
|
76 |
+
{"id": "gemini-1.5-pro-002"},
|
77 |
+
{"id": "gemini-exp-1114"},
|
78 |
+
{"id": "gemini-exp-1121"},
|
79 |
+
{"id": "gemini-exp-1206"},
|
80 |
+
{"id": "gemini-2.0-flash-exp"},
|
81 |
+
{"id": "gemini-2.0-flash-thinking-exp-1219"},
|
82 |
+
{"id": "gemini-2.0-flash-thinking-exp-01-21"},
|
83 |
+
{"id": "gemini-2.0-pro-exp"},
|
84 |
+
]
|
85 |
+
|
86 |
+
@app.route("/", methods=["GET", "POST"])
|
87 |
+
def login():
|
88 |
+
if request.method == "POST":
|
89 |
+
password = request.form.get("password")
|
90 |
+
if password == ADMIN_PASSWORD:
|
91 |
+
session["logged_in"] = True
|
92 |
+
return redirect(url_for("manage_keys"))
|
93 |
+
else:
|
94 |
+
return render_template("login.html", error="Incorrect password")
|
95 |
+
return render_template("login.html", error=None)
|
96 |
+
|
97 |
+
@app.route("/manage", methods=["GET", "POST"])
|
98 |
+
def manage_keys():
|
99 |
+
if not session.get("logged_in"):
|
100 |
+
return redirect(url_for("login"))
|
101 |
+
|
102 |
+
if request.method == "POST":
|
103 |
+
action = request.form.get("action")
|
104 |
+
if action == "add":
|
105 |
+
new_key = request.form.get("new_key")
|
106 |
+
if new_key:
|
107 |
+
ProxyPasswords.append(new_key)
|
108 |
+
elif action == "delete":
|
109 |
+
key_to_delete = request.form.get("key_to_delete")
|
110 |
+
if key_to_delete in ProxyPasswords:
|
111 |
+
ProxyPasswords.remove(key_to_delete)
|
112 |
+
|
113 |
+
return render_template("manage.html", keys=ProxyPasswords)
|
114 |
+
|
115 |
+
@app.route("/logout")
|
116 |
+
def logout():
|
117 |
+
session.pop("logged_in", None)
|
118 |
+
return redirect(url_for("login"))
|
119 |
+
|
120 |
+
@app.route('/hf/v1/chat/completions', methods=['POST'])
|
121 |
+
def chat_completions():
|
122 |
+
global current_api_key
|
123 |
+
is_authenticated, auth_error, status_code = func.authenticate_request(request)
|
124 |
+
if not is_authenticated:
|
125 |
+
return auth_error if auth_error else jsonify({'error': 'Unauthorized'}), status_code if status_code else 401
|
126 |
+
try:
|
127 |
+
request_data = request.get_json()
|
128 |
+
r_data = func.sanitize_request_data(request_data)
|
129 |
+
r_data = json.dumps(r_data, indent=4, ensure_ascii=False).replace('\\n', '\n')
|
130 |
+
os.system('cls' if os.name == 'nt' else 'clear')
|
131 |
+
logger.info(r_data)
|
132 |
+
messages = request_data.get('messages', [])
|
133 |
+
model = request_data.get('model', 'gemini-exp-1206')
|
134 |
+
temperature = request_data.get('temperature', 1)
|
135 |
+
max_tokens = request_data.get('max_tokens', 8192)
|
136 |
+
stream = request_data.get('stream', False)
|
137 |
+
|
138 |
+
logger.info(colored(f"\n{model} [r] -> {current_api_key[:11]}...", 'yellow'))
|
139 |
+
|
140 |
+
# 将 OpenAI 格式的消息转换为 Gemini 格式
|
141 |
+
gemini_history, user_message, error_response = func.process_messages_for_gemini(messages)
|
142 |
+
|
143 |
+
if error_response:
|
144 |
+
# 处理错误
|
145 |
+
print(error_response)
|
146 |
+
|
147 |
+
genai.configure(api_key=current_api_key)
|
148 |
+
|
149 |
+
generation_config = {
|
150 |
+
"temperature": temperature,
|
151 |
+
"max_output_tokens": max_tokens
|
152 |
+
}
|
153 |
+
|
154 |
+
gen_model = genai.GenerativeModel(
|
155 |
+
model_name=model,
|
156 |
+
generation_config=generation_config,
|
157 |
+
safety_settings=safety_settings
|
158 |
+
)
|
159 |
+
|
160 |
+
|
161 |
+
if stream:
|
162 |
+
# 流式响应
|
163 |
+
if gemini_history:
|
164 |
+
chat_session = gen_model.start_chat(history=gemini_history)
|
165 |
+
response = chat_session.send_message(user_message, stream=True)
|
166 |
+
else:
|
167 |
+
response = gen_model.generate_content(user_message, stream=True)
|
168 |
+
|
169 |
+
def generate():
|
170 |
+
try:
|
171 |
+
for chunk in response:
|
172 |
+
if chunk.text:
|
173 |
+
data = {
|
174 |
+
'choices': [
|
175 |
+
{
|
176 |
+
'delta': {
|
177 |
+
'content': chunk.text
|
178 |
+
},
|
179 |
+
'finish_reason': None,
|
180 |
+
'index': 0
|
181 |
+
}
|
182 |
+
],
|
183 |
+
'object': 'chat.completion.chunk'
|
184 |
+
}
|
185 |
+
|
186 |
+
yield f"data: {json.dumps(data)}\n\n"
|
187 |
+
data = {
|
188 |
+
'choices': [
|
189 |
+
{
|
190 |
+
'delta': {},
|
191 |
+
'finish_reason': 'stop',
|
192 |
+
'index': 0
|
193 |
+
}
|
194 |
+
],
|
195 |
+
'object': 'chat.completion.chunk'
|
196 |
+
}
|
197 |
+
|
198 |
+
yield f"data: {json.dumps(data)}\n\n"
|
199 |
+
except Exception as e:
|
200 |
+
logger.error(f"Error during streaming: {str(e)}")
|
201 |
+
|
202 |
+
data = {
|
203 |
+
'error': {
|
204 |
+
'message': str(e),
|
205 |
+
'type': 'internal_server_error'
|
206 |
+
}
|
207 |
+
}
|
208 |
+
yield f"data: {json.dumps(data)}\n\n"
|
209 |
+
|
210 |
+
return Response(stream_with_context(generate()), mimetype='text/event-stream')
|
211 |
+
else:
|
212 |
+
# 调用 API
|
213 |
+
if gemini_history:
|
214 |
+
chat_session = gen_model.start_chat(history=gemini_history)
|
215 |
+
response = chat_session.send_message(user_message)
|
216 |
+
else:
|
217 |
+
response = gen_model.generate_content(user_message)
|
218 |
+
try:
|
219 |
+
text_content = response.candidates[0].content.parts[0].text
|
220 |
+
|
221 |
+
except (AttributeError, IndexError, TypeError) as e:
|
222 |
+
logger.error(colored(f"Error getting text content: {str(e)}",'red'))
|
223 |
+
|
224 |
+
text_content = "Error: Unable to get text content."
|
225 |
+
|
226 |
+
response_data = {
|
227 |
+
'id': 'chatcmpl-xxxxxxxxxxxx',
|
228 |
+
'object': 'chat.completion',
|
229 |
+
'created': int(datetime.now().timestamp()),
|
230 |
+
'model': model,
|
231 |
+
'choices': [{
|
232 |
+
'index': 0,
|
233 |
+
'message': {
|
234 |
+
'role': 'assistant',
|
235 |
+
'content': text_content
|
236 |
+
},
|
237 |
+
'finish_reason': 'stop'
|
238 |
+
}],
|
239 |
+
'usage':{
|
240 |
+
'prompt_tokens': 0,
|
241 |
+
'completion_tokens': 0,
|
242 |
+
'total_tokens': 0
|
243 |
+
}
|
244 |
+
}
|
245 |
+
logger.info(colored(f"Generation Success", 'green'))
|
246 |
+
return jsonify(response_data)
|
247 |
+
|
248 |
+
except Exception as e:
|
249 |
+
logger.error(f"Error in chat completions: {str(e)}")
|
250 |
+
return jsonify({
|
251 |
+
'error': {
|
252 |
+
'message': str(e),
|
253 |
+
'type': 'invalid_request_error'
|
254 |
+
}
|
255 |
+
}), 500
|
256 |
+
finally:
|
257 |
+
current_api_key = key_manager.get_available_key()
|
258 |
+
logger.info(colored(f"API KEY Switched -> {current_api_key[:11]}...", 'aqua'))
|
259 |
+
|
260 |
+
@app.route('/hf/v1/models', methods=['GET'])
|
261 |
+
def list_models():
|
262 |
+
is_authenticated, auth_error, status_code = func.authenticate_request(request)
|
263 |
+
if not is_authenticated:
|
264 |
+
return auth_error if auth_error else jsonify({'error': 'Unauthorized'}), status_code if status_code else 401
|
265 |
+
response = {"object": "list", "data": GEMINI_MODELS}
|
266 |
+
return jsonify(response)
|
267 |
+
|
268 |
+
|
269 |
+
if __name__ == '__main__':
|
270 |
+
scheduler = BackgroundScheduler()
|
271 |
+
# 设置定时任务,每 12 小时执行一次 keep_alive 函数
|
272 |
+
scheduler.start()
|
273 |
+
app.run(debug=True, host='0.0.0.0', port=int(os.environ.get('PORT', 7860)))
|
chat.txt
ADDED
@@ -0,0 +1,146 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
@app.route('/hf/v1/chat/completions', methods=['POST'])
|
2 |
+
def chat_completions():
|
3 |
+
global current_api_key
|
4 |
+
is_authenticated, auth_error, status_code = authenticate_request(request)
|
5 |
+
if not is_authenticated:
|
6 |
+
return auth_error if auth_error else jsonify({'error': 'Unauthorized'}), status_code if status_code else 401
|
7 |
+
try:
|
8 |
+
|
9 |
+
request_data = request.get_json()
|
10 |
+
|
11 |
+
messages = request_data.get('messages', [])
|
12 |
+
model = request_data.get('model', 'gemini-exp-1206')
|
13 |
+
temperature = request_data.get('temperature', 1)
|
14 |
+
max_tokens = request_data.get('max_tokens', 8192)
|
15 |
+
stream = request_data.get('stream', False)
|
16 |
+
|
17 |
+
logger.info(colored(f"\n{model} [r] -> {current_api_key[:11]}...", 'yellow'))
|
18 |
+
# 将 OpenAI 格式的消息转换为 Gemini 格式
|
19 |
+
gemini_history = []
|
20 |
+
for message in messages:
|
21 |
+
role = message.get('role')
|
22 |
+
content = message.get('content')
|
23 |
+
if role == 'system':
|
24 |
+
gemini_history.append({"role": "user", "parts": [content]})
|
25 |
+
elif role == 'user':
|
26 |
+
gemini_history.append({"role": "user", "parts": [content]})
|
27 |
+
elif role == 'assistant':
|
28 |
+
gemini_history.append({"role": "model", "parts": [content]})
|
29 |
+
|
30 |
+
user_message = gemini_history[-1]['parts'][0] if gemini_history else messages[-1]['content']
|
31 |
+
gemini_history = gemini_history[:-1] if gemini_history else []
|
32 |
+
genai.configure(api_key=current_api_key)
|
33 |
+
# 调用 Gemini API
|
34 |
+
generation_config = {
|
35 |
+
"temperature": temperature,
|
36 |
+
"max_output_tokens": max_tokens
|
37 |
+
}
|
38 |
+
|
39 |
+
gen_model = genai.GenerativeModel(
|
40 |
+
model_name=model,
|
41 |
+
generation_config=generation_config,
|
42 |
+
safety_settings=safety_settings
|
43 |
+
)
|
44 |
+
|
45 |
+
|
46 |
+
if stream:
|
47 |
+
# 流式响应
|
48 |
+
if gemini_history:
|
49 |
+
chat_session = gen_model.start_chat(history=gemini_history)
|
50 |
+
response = chat_session.send_message(user_message, stream=True)
|
51 |
+
else:
|
52 |
+
response = gen_model.generate_content(user_message, stream=True)
|
53 |
+
|
54 |
+
def generate():
|
55 |
+
try:
|
56 |
+
for chunk in response:
|
57 |
+
if chunk.text:
|
58 |
+
data = {
|
59 |
+
'choices': [
|
60 |
+
{
|
61 |
+
'delta': {
|
62 |
+
'content': chunk.text
|
63 |
+
},
|
64 |
+
'finish_reason': None,
|
65 |
+
'index': 0
|
66 |
+
}
|
67 |
+
],
|
68 |
+
'object': 'chat.completion.chunk'
|
69 |
+
}
|
70 |
+
|
71 |
+
yield f"data: {json.dumps(data)}\n\n"
|
72 |
+
data = {
|
73 |
+
'choices': [
|
74 |
+
{
|
75 |
+
'delta': {},
|
76 |
+
'finish_reason': 'stop',
|
77 |
+
'index': 0
|
78 |
+
}
|
79 |
+
],
|
80 |
+
'object': 'chat.completion.chunk'
|
81 |
+
}
|
82 |
+
|
83 |
+
yield f"data: {json.dumps(data)}\n\n"
|
84 |
+
except Exception as e:
|
85 |
+
logger.error(f"Error during streaming: {str(e)}")
|
86 |
+
|
87 |
+
data = {
|
88 |
+
'error': {
|
89 |
+
'message': str(e),
|
90 |
+
'type': 'internal_server_error'
|
91 |
+
}
|
92 |
+
}
|
93 |
+
yield f"data: {json.dumps(data)}\n\n"
|
94 |
+
|
95 |
+
return Response(stream_with_context(generate()), mimetype='text/event-stream')
|
96 |
+
else:
|
97 |
+
|
98 |
+
# 调用 API
|
99 |
+
if gemini_history:
|
100 |
+
chat_session = gen_model.start_chat(history=gemini_history)
|
101 |
+
response = chat_session.send_message(user_message)
|
102 |
+
else:
|
103 |
+
response = gen_model.generate_content(user_message)
|
104 |
+
|
105 |
+
try:
|
106 |
+
text_content = response.candidates[0].content.parts[0].text
|
107 |
+
|
108 |
+
except (AttributeError, IndexError, TypeError) as e:
|
109 |
+
logger.error(colored(f"Error getting text content: {str(e)}",'red'))
|
110 |
+
|
111 |
+
text_content = "Error: Unable to get text content."
|
112 |
+
|
113 |
+
response_data = {
|
114 |
+
'id': 'chatcmpl-xxxxxxxxxxxx',
|
115 |
+
'object': 'chat.completion',
|
116 |
+
'created': int(datetime.now().timestamp()),
|
117 |
+
'model': model,
|
118 |
+
'choices': [{
|
119 |
+
'index': 0,
|
120 |
+
'message': {
|
121 |
+
'role': 'assistant',
|
122 |
+
'content': text_content
|
123 |
+
},
|
124 |
+
'finish_reason': 'stop'
|
125 |
+
}],
|
126 |
+
'usage':{
|
127 |
+
'prompt_tokens': 0,
|
128 |
+
'completion_tokens': 0,
|
129 |
+
'total_tokens': 0
|
130 |
+
}
|
131 |
+
}
|
132 |
+
logger.info(colored(f"Generation Success", 'green'))
|
133 |
+
return jsonify(response_data)
|
134 |
+
|
135 |
+
except Exception as e:
|
136 |
+
logger.error(f"Error in chat completions: {str(e)}")
|
137 |
+
|
138 |
+
return jsonify({
|
139 |
+
'error': {
|
140 |
+
'message': str(e),
|
141 |
+
'type': 'invalid_request_error'
|
142 |
+
}
|
143 |
+
}), 500
|
144 |
+
finally:
|
145 |
+
current_api_key = key_manager.get_available_key()
|
146 |
+
logger.info(colored(f"API KEY Switched -> {current_api_key[:11]}...", 'aqua'))
|
func.py
ADDED
@@ -0,0 +1,160 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from io import BytesIO
|
2 |
+
import os
|
3 |
+
from flask import jsonify
|
4 |
+
import logging
|
5 |
+
import json
|
6 |
+
import re
|
7 |
+
logger = logging.getLogger(__name__)
|
8 |
+
|
9 |
+
ProxyPasswords = os.environ.get('ProxyPasswords').split(',')
|
10 |
+
|
11 |
+
def authenticate_request(request):
|
12 |
+
auth_header = request.headers.get('Authorization')
|
13 |
+
|
14 |
+
if not auth_header:
|
15 |
+
return False, jsonify({'error': 'Authorization header is missing'}), 401
|
16 |
+
|
17 |
+
try:
|
18 |
+
auth_type, api_key = auth_header.split(' ', 1)
|
19 |
+
except ValueError:
|
20 |
+
return False, jsonify({'error': 'Invalid Authorization header format'}), 401
|
21 |
+
|
22 |
+
if auth_type.lower() != 'bearer':
|
23 |
+
return False, jsonify({'error': 'Authorization type must be Bearer'}), 401
|
24 |
+
|
25 |
+
if api_key not in ProxyPasswords:
|
26 |
+
return False, jsonify({'error': 'Unauthorized'}), 401
|
27 |
+
|
28 |
+
return True, None, None
|
29 |
+
|
30 |
+
def sanitize_request_data(request_data):
|
31 |
+
"""
|
32 |
+
从请求数据中删除base64编码的数据。
|
33 |
+
|
34 |
+
Args:
|
35 |
+
request_data: 包含可能存在base64数据的字典。
|
36 |
+
|
37 |
+
Returns:
|
38 |
+
清理后的字典,其中base64数据被替换为"[Base64 Data Omitted]"。
|
39 |
+
"""
|
40 |
+
|
41 |
+
|
42 |
+
def replace_base64(match):
|
43 |
+
# 替换base64数据为提示信息
|
44 |
+
return '"[Base64 Data Omitted]"'
|
45 |
+
|
46 |
+
|
47 |
+
request_data_str = json.dumps(request_data)
|
48 |
+
|
49 |
+
# 使用正则表达式匹配base64数据,并替换为提示信息
|
50 |
+
sanitized_request_data_str = re.sub(
|
51 |
+
r'"(data:[^;]+;base64,)[^"]+"',
|
52 |
+
replace_base64,
|
53 |
+
request_data_str
|
54 |
+
)
|
55 |
+
|
56 |
+
return json.loads(sanitized_request_data_str)
|
57 |
+
|
58 |
+
def process_messages_for_gemini(messages):
|
59 |
+
"""
|
60 |
+
将通用的对话消息格式转换为Gemini API所需的格式
|
61 |
+
这个函数处理消息列表并将其转换为Gemini API兼容的格式。它支持文本、图片和文件内容的处理。
|
62 |
+
参数:
|
63 |
+
messages (list): 包含对话消息的列表。每条消息应该是一个字典,包含'role'和'content'字段。
|
64 |
+
- role: 可以是 'system', 'user' 或 'assistant'
|
65 |
+
- content: 可以是字符串或包含多个内容项的列表
|
66 |
+
返回:
|
67 |
+
tuple: 包含三个元素:
|
68 |
+
- gemini_history (list): 转换后的历史消息列表
|
69 |
+
- user_message (dict): 最新的用户消息
|
70 |
+
- error (tuple or None): 如果有错误,返回错误响应;否则返回None
|
71 |
+
错误处理:
|
72 |
+
- 检查角色是否有效
|
73 |
+
- 验证图片URL格式
|
74 |
+
- 验证文件URL格式
|
75 |
+
示例消息格式:
|
76 |
+
文本消息:
|
77 |
+
{
|
78 |
+
'role': 'user',
|
79 |
+
'content': '你好'
|
80 |
+
}
|
81 |
+
多模态消息:
|
82 |
+
{
|
83 |
+
'role': 'user',
|
84 |
+
'content': [
|
85 |
+
{'type': 'text', 'text': '这是什么图片?'},
|
86 |
+
{'type': 'image_url', 'image_url': {'url': 'data:image/jpeg;base64,...'}}
|
87 |
+
]
|
88 |
+
}
|
89 |
+
"""
|
90 |
+
gemini_history = []
|
91 |
+
errors = []
|
92 |
+
for message in messages:
|
93 |
+
role = message.get('role')
|
94 |
+
content = message.get('content')
|
95 |
+
|
96 |
+
if isinstance(content, str):
|
97 |
+
if role == 'system':
|
98 |
+
gemini_history.append({"role": "user", "parts": [content]})
|
99 |
+
elif role == 'user':
|
100 |
+
gemini_history.append({"role": "user", "parts": [content]})
|
101 |
+
elif role == 'assistant':
|
102 |
+
gemini_history.append({"role": "model", "parts": [content]})
|
103 |
+
else:
|
104 |
+
errors.append(f"Invalid role: {role}")
|
105 |
+
elif isinstance(content, list):
|
106 |
+
parts = []
|
107 |
+
for item in content:
|
108 |
+
if item.get('type') == 'text':
|
109 |
+
parts.append({"text": item.get('text')})
|
110 |
+
elif item.get('type') == 'image_url':
|
111 |
+
image_data = item.get('image_url', {}).get('url', '')
|
112 |
+
if image_data.startswith('data:image/'):
|
113 |
+
|
114 |
+
try:
|
115 |
+
mime_type, base64_data = image_data.split(';')[0].split(':')[1], image_data.split(',')[1]
|
116 |
+
parts.append({
|
117 |
+
"inline_data": {
|
118 |
+
"mime_type": mime_type,
|
119 |
+
"data": base64_data
|
120 |
+
}
|
121 |
+
})
|
122 |
+
except (IndexError, ValueError):
|
123 |
+
errors.append(f"Invalid data URI for image: {image_data}")
|
124 |
+
else:
|
125 |
+
errors.append(f"Invalid image URL format for item: {item}")
|
126 |
+
elif item.get('type') == 'file_url':
|
127 |
+
file_data = item.get('file_url', {}).get('url', '')
|
128 |
+
if file_data.startswith('data:'):
|
129 |
+
|
130 |
+
try:
|
131 |
+
mime_type, base64_data = file_data.split(';')[0].split(':')[1], file_data.split(',')[1]
|
132 |
+
parts.append({
|
133 |
+
"inline_data": {
|
134 |
+
"mime_type": mime_type,
|
135 |
+
"data": base64_data
|
136 |
+
}
|
137 |
+
})
|
138 |
+
except (IndexError, ValueError):
|
139 |
+
errors.append(f"Invalid data URI for file: {file_data}")
|
140 |
+
else:
|
141 |
+
errors.append(f"Invalid file URL format for item: {item}")
|
142 |
+
|
143 |
+
if parts:
|
144 |
+
if role in ['user', 'system']:
|
145 |
+
gemini_history.append({"role": "user", "parts": parts})
|
146 |
+
elif role == 'assistant':
|
147 |
+
gemini_history.append({"role": "model", "parts": parts})
|
148 |
+
else:
|
149 |
+
errors.append(f"Invalid role: {role}")
|
150 |
+
|
151 |
+
if gemini_history:
|
152 |
+
user_message = gemini_history[-1]
|
153 |
+
gemini_history = gemini_history[:-1]
|
154 |
+
else:
|
155 |
+
user_message = {"role": "user", "parts": [""]}
|
156 |
+
|
157 |
+
if errors:
|
158 |
+
return gemini_history, user_message, (jsonify({'error': errors}), 400)
|
159 |
+
else:
|
160 |
+
return gemini_history, user_message, None
|
requirements.txt
ADDED
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
Flask==2.0.3
|
2 |
+
Flask-CORS==3.0.10
|
3 |
+
requests==2.26.0
|
4 |
+
Werkzeug==2.0.3
|
5 |
+
google==3.0.0
|
6 |
+
google-generativeai==0.8.3
|
7 |
+
termcolor==2.5.0
|
8 |
+
pillow==10.4.0
|
9 |
+
apscheduler
|
templates/login.html
ADDED
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html>
|
3 |
+
|
4 |
+
<head>
|
5 |
+
<title>Login</title>
|
6 |
+
</head>
|
7 |
+
|
8 |
+
<body>
|
9 |
+
<h1>Admin Login</h1>
|
10 |
+
{% if error %}
|
11 |
+
<p style="color: red;">{{ error }}</p>
|
12 |
+
{% endif %}
|
13 |
+
<form method="POST">
|
14 |
+
<label for="password">Password:</label>
|
15 |
+
<input type="password" name="password" id="password" required>
|
16 |
+
<button type="submit">Login</button>
|
17 |
+
</form>
|
18 |
+
</body>
|
19 |
+
|
20 |
+
</html>
|
templates/manage.html
ADDED
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html>
|
3 |
+
|
4 |
+
<head>
|
5 |
+
<title>Manage API Keys</title>
|
6 |
+
</head>
|
7 |
+
|
8 |
+
<body>
|
9 |
+
<h1>Manage Hugging Face API Keys</h1>
|
10 |
+
<h2>Current Keys:</h2>
|
11 |
+
<ul>
|
12 |
+
{% for key in keys %}
|
13 |
+
<li>
|
14 |
+
{{ key }}
|
15 |
+
<form method="POST" style="display: inline;">
|
16 |
+
<input type="hidden" name="action" value="delete">
|
17 |
+
<input type="hidden" name="key_to_delete" value="{{ key }}">
|
18 |
+
<button type="submit">Delete</button>
|
19 |
+
</form>
|
20 |
+
</li>
|
21 |
+
{% endfor %}
|
22 |
+
</ul>
|
23 |
+
<h2>Add New Key:</h2>
|
24 |
+
<form method="POST">
|
25 |
+
<input type="hidden" name="action" value="add">
|
26 |
+
<input type="text" name="new_key" placeholder="Enter new API key">
|
27 |
+
<button type="submit">Add</button>
|
28 |
+
</form>
|
29 |
+
<br>
|
30 |
+
<a href="{{ url_for('logout') }}">Logout</a>
|
31 |
+
</body>
|
32 |
+
|
33 |
+
</html>
|