feat: 添加请求统计功能 + API使用示例
- 主服务增加请求统计(请求次数、成功/失败、token数) - 按模型和提供商分别统计 - 每日请求计数自动重置 - 首页增加多种API调用示例(curl、Python OpenAI SDK、Python requests、JavaScript fetch)
This commit is contained in:
123
app.py
123
app.py
@@ -9,9 +9,10 @@ import requests
|
|||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime
|
from datetime import datetime, date
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import sys
|
import sys
|
||||||
|
import threading
|
||||||
|
|
||||||
# 添加配置路径
|
# 添加配置路径
|
||||||
sys.path.insert(0, str(Path(__file__).parent))
|
sys.path.insert(0, str(Path(__file__).parent))
|
||||||
@@ -20,6 +21,75 @@ from config.settings import (
|
|||||||
LOG_CONFIG, RETRY_CONFIG
|
LOG_CONFIG, RETRY_CONFIG
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 数据目录和统计文件
|
||||||
|
DATA_DIR = Path(__file__).parent / 'data'
|
||||||
|
DATA_DIR.mkdir(exist_ok=True)
|
||||||
|
STATS_FILE = DATA_DIR / 'stats.json'
|
||||||
|
|
||||||
|
# 统计锁(避免并发写入冲突)
|
||||||
|
stats_lock = threading.Lock()
|
||||||
|
|
||||||
|
def load_stats():
|
||||||
|
"""加载统计数据"""
|
||||||
|
if STATS_FILE.exists():
|
||||||
|
try:
|
||||||
|
return json.loads(STATS_FILE.read_text(encoding='utf-8'))
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return {
|
||||||
|
'total_requests': 0,
|
||||||
|
'total_success': 0,
|
||||||
|
'total_errors': 0,
|
||||||
|
'total_tokens': 0,
|
||||||
|
'requests_today': 0,
|
||||||
|
'requests_by_model': {},
|
||||||
|
'providers': {},
|
||||||
|
'last_updated': None,
|
||||||
|
'date': None # 用于判断是否需要重置每日计数
|
||||||
|
}
|
||||||
|
|
||||||
|
def save_stats(stats):
|
||||||
|
"""保存统计数据"""
|
||||||
|
stats['last_updated'] = datetime.now().isoformat()
|
||||||
|
STATS_FILE.write_text(json.dumps(stats, ensure_ascii=False, indent=2), encoding='utf-8')
|
||||||
|
|
||||||
|
def increment_stats(model, provider_name, success=False, tokens=0, error=None):
|
||||||
|
"""增加统计计数"""
|
||||||
|
with stats_lock:
|
||||||
|
stats = load_stats()
|
||||||
|
today = date.today().isoformat()
|
||||||
|
|
||||||
|
# 如果是新的一天,重置每日计数
|
||||||
|
if stats.get('date') != today:
|
||||||
|
stats['date'] = today
|
||||||
|
stats['requests_today'] = 0
|
||||||
|
|
||||||
|
stats['total_requests'] += 1
|
||||||
|
stats['requests_today'] += 1
|
||||||
|
|
||||||
|
# 模型统计
|
||||||
|
if model not in stats['requests_by_model']:
|
||||||
|
stats['requests_by_model'][model] = {'count': 0, 'success': 0, 'tokens': 0}
|
||||||
|
stats['requests_by_model'][model]['count'] += 1
|
||||||
|
if success:
|
||||||
|
stats['requests_by_model'][model]['success'] += 1
|
||||||
|
stats['requests_by_model'][model]['tokens'] += tokens
|
||||||
|
|
||||||
|
# 提供商统计
|
||||||
|
if provider_name not in stats['providers']:
|
||||||
|
stats['providers'][provider_name] = {'requests': 0, 'success': 0, 'errors': 0, 'tokens': 0}
|
||||||
|
stats['providers'][provider_name]['requests'] += 1
|
||||||
|
if success:
|
||||||
|
stats['providers'][provider_name]['success'] += 1
|
||||||
|
stats['providers'][provider_name]['tokens'] += tokens
|
||||||
|
stats['total_success'] += 1
|
||||||
|
stats['total_tokens'] += tokens
|
||||||
|
else:
|
||||||
|
stats['providers'][provider_name]['errors'] += 1
|
||||||
|
stats['total_errors'] += 1
|
||||||
|
|
||||||
|
save_stats(stats)
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
CORS(app)
|
CORS(app)
|
||||||
|
|
||||||
@@ -244,8 +314,27 @@ def index():
|
|||||||
"endpoints": {
|
"endpoints": {
|
||||||
"chat": "/v1/chat/completions",
|
"chat": "/v1/chat/completions",
|
||||||
"models": "/v1/models",
|
"models": "/v1/models",
|
||||||
|
"embeddings": "/v1/embeddings",
|
||||||
"health": "/health",
|
"health": "/health",
|
||||||
"status": "/status"
|
"status": "/status"
|
||||||
|
},
|
||||||
|
"examples": {
|
||||||
|
"curl": {
|
||||||
|
"description": "curl 命令行",
|
||||||
|
"code": "curl -X POST http://localhost:19007/v1/chat/completions -H 'Content-Type: application/json' -d '{\"model\": \"auto\", \"messages\": [{\"role\": \"user\", \"content\": \"你好\"}]}'"
|
||||||
|
},
|
||||||
|
"python_openai": {
|
||||||
|
"description": "Python OpenAI SDK",
|
||||||
|
"code": "from openai import OpenAI\nclient = OpenAI(base_url='http://localhost:19007/v1', api_key='any')\nresponse = client.chat.completions.create(model='auto', messages=[{'role': 'user', 'content': '你好'}])\nprint(response.choices[0].message.content)"
|
||||||
|
},
|
||||||
|
"python_requests": {
|
||||||
|
"description": "Python requests",
|
||||||
|
"code": "import requests\nresponse = requests.post('http://localhost:19007/v1/chat/completions', json={'model': 'auto', 'messages': [{'role': 'user', 'content': '你好'}]})\nprint(response.json()['choices'][0]['message']['content'])"
|
||||||
|
},
|
||||||
|
"javascript": {
|
||||||
|
"description": "JavaScript fetch",
|
||||||
|
"code": "fetch('http://localhost:19007/v1/chat/completions', {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({model: 'auto', messages: [{role: 'user', content: '你好'}]})}).then(r => r.json()).then(d => console.log(d.choices[0].message.content))"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -292,19 +381,29 @@ def list_models():
|
|||||||
@app.route('/v1/chat/completions', methods=['POST'])
|
@app.route('/v1/chat/completions', methods=['POST'])
|
||||||
def chat_completions():
|
def chat_completions():
|
||||||
"""聊天完成API"""
|
"""聊天完成API"""
|
||||||
|
# 用于统计的变量
|
||||||
|
request_model = None
|
||||||
|
request_provider = None
|
||||||
|
request_success = False
|
||||||
|
request_tokens = 0
|
||||||
|
|
||||||
try:
|
try:
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
|
|
||||||
if not data:
|
if not data:
|
||||||
|
increment_stats('unknown', 'unknown', success=False, error='Invalid request body')
|
||||||
return jsonify({"error": "Invalid request body"}), 400
|
return jsonify({"error": "Invalid request body"}), 400
|
||||||
|
|
||||||
model = data.get('model', 'auto')
|
model = data.get('model', 'auto')
|
||||||
stream = data.get('stream', False)
|
stream = data.get('stream', False)
|
||||||
|
|
||||||
|
request_model = model
|
||||||
|
|
||||||
# 获取提供商
|
# 获取提供商
|
||||||
provider, resolved_model = get_provider_for_model(model)
|
provider, resolved_model = get_provider_for_model(model)
|
||||||
|
|
||||||
if not provider:
|
if not provider:
|
||||||
|
increment_stats(model, 'unknown', success=False, error=f'No provider for model: {model}')
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"error": {
|
"error": {
|
||||||
"message": f"No available provider for model: {model}",
|
"message": f"No available provider for model: {model}",
|
||||||
@@ -312,6 +411,8 @@ def chat_completions():
|
|||||||
}
|
}
|
||||||
}), 400
|
}), 400
|
||||||
|
|
||||||
|
request_provider = provider['name']
|
||||||
|
|
||||||
logger.info(f"Request: model={model} -> provider={provider['name']}, resolved_model={resolved_model}, stream={stream}")
|
logger.info(f"Request: model={model} -> provider={provider['name']}, resolved_model={resolved_model}, stream={stream}")
|
||||||
|
|
||||||
# 重试逻辑
|
# 重试逻辑
|
||||||
@@ -324,9 +425,11 @@ def chat_completions():
|
|||||||
|
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
mark_provider_success(provider['name'])
|
mark_provider_success(provider['name'])
|
||||||
|
request_success = True
|
||||||
|
|
||||||
if stream:
|
if stream:
|
||||||
# 流式响应
|
# 流式响应 - 统计流式请求
|
||||||
|
increment_stats(model, provider['name'], success=True, tokens=0)
|
||||||
return Response(
|
return Response(
|
||||||
stream_with_context(stream_response(response)),
|
stream_with_context(stream_response(response)),
|
||||||
content_type='text/event-stream',
|
content_type='text/event-stream',
|
||||||
@@ -336,8 +439,14 @@ def chat_completions():
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# 非流式响应
|
# 非流式响应 - 提取token统计
|
||||||
return jsonify(response.json())
|
result = response.json()
|
||||||
|
# 尝试提取usage信息
|
||||||
|
usage = result.get('usage', {})
|
||||||
|
request_tokens = usage.get('total_tokens', 0)
|
||||||
|
|
||||||
|
increment_stats(model, provider['name'], success=True, tokens=request_tokens)
|
||||||
|
return jsonify(result)
|
||||||
|
|
||||||
elif response.status_code == 429:
|
elif response.status_code == 429:
|
||||||
# 速率限制,尝试下一个提供商
|
# 速率限制,尝试下一个提供商
|
||||||
@@ -349,12 +458,15 @@ def chat_completions():
|
|||||||
if next_provider and next_provider['name'] not in tried_providers:
|
if next_provider and next_provider['name'] not in tried_providers:
|
||||||
provider = next_provider
|
provider = next_provider
|
||||||
resolved_model = next_model
|
resolved_model = next_model
|
||||||
|
request_provider = provider['name']
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
increment_stats(model, provider['name'], success=False, error='Rate limit')
|
||||||
return jsonify(response.json()), response.status_code
|
return jsonify(response.json()), response.status_code
|
||||||
|
|
||||||
else:
|
else:
|
||||||
last_error = response.json() if response.headers.get('content-type', '').startswith('application/json') else {"error": response.text}
|
last_error = response.json() if response.headers.get('content-type', '').startswith('application/json') else {"error": response.text}
|
||||||
|
increment_stats(model, provider['name'], success=False, error=str(last_error))
|
||||||
return jsonify(last_error), response.status_code
|
return jsonify(last_error), response.status_code
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -367,10 +479,12 @@ def chat_completions():
|
|||||||
if next_provider and next_provider['name'] not in tried_providers:
|
if next_provider and next_provider['name'] not in tried_providers:
|
||||||
provider = next_provider
|
provider = next_provider
|
||||||
resolved_model = next_model
|
resolved_model = next_model
|
||||||
|
request_provider = provider['name']
|
||||||
time.sleep(RETRY_CONFIG['retry_delay'])
|
time.sleep(RETRY_CONFIG['retry_delay'])
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 所有重试都失败
|
# 所有重试都失败
|
||||||
|
increment_stats(model, request_provider or 'unknown', success=False, error=str(last_error))
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"error": {
|
"error": {
|
||||||
"message": f"All providers failed. Last error: {last_error}",
|
"message": f"All providers failed. Last error: {last_error}",
|
||||||
@@ -380,6 +494,7 @@ def chat_completions():
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Unexpected error: {e}")
|
logger.error(f"Unexpected error: {e}")
|
||||||
|
increment_stats(request_model or 'unknown', request_provider or 'unknown', success=False, error=str(e))
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"error": {
|
"error": {
|
||||||
"message": str(e),
|
"message": str(e),
|
||||||
|
|||||||
Reference in New Issue
Block a user