Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 247f9e2165 | |||
| e96cf6527a |
157
app.py
157
app.py
@@ -9,9 +9,10 @@ import requests
|
||||
import json
|
||||
import time
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from datetime import datetime, date
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import threading
|
||||
|
||||
# 添加配置路径
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
@@ -20,6 +21,75 @@ from config.settings import (
|
||||
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__)
|
||||
CORS(app)
|
||||
|
||||
@@ -107,10 +177,17 @@ def get_provider_for_model(model_name):
|
||||
return None, None
|
||||
|
||||
|
||||
def get_available_provider_for_auto(auto_name='auto'):
|
||||
"""获取auto模式下的可用提供商(支持自定义auto配置)"""
|
||||
def get_available_provider_for_auto(auto_name='auto', exclude_providers=None):
|
||||
"""获取auto模式下的可用提供商(支持自定义auto配置)
|
||||
|
||||
Args:
|
||||
auto_name: auto配置名称
|
||||
exclude_providers: 要排除的提供商名称列表(已尝试过的)
|
||||
"""
|
||||
refresh_config()
|
||||
|
||||
exclude_providers = exclude_providers or []
|
||||
|
||||
# 获取auto配置
|
||||
profile = _cached_auto_profiles.get(auto_name, _cached_auto_profiles.get('auto', {}))
|
||||
|
||||
@@ -124,6 +201,8 @@ def get_available_provider_for_auto(auto_name='auto'):
|
||||
for provider in sorted_providers:
|
||||
if not provider['enabled']:
|
||||
continue
|
||||
if provider['name'] in exclude_providers:
|
||||
continue
|
||||
if not provider_status.get(provider['name'], {}).get('available', True):
|
||||
continue
|
||||
|
||||
@@ -244,8 +323,27 @@ def index():
|
||||
"endpoints": {
|
||||
"chat": "/v1/chat/completions",
|
||||
"models": "/v1/models",
|
||||
"embeddings": "/v1/embeddings",
|
||||
"health": "/health",
|
||||
"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 +390,29 @@ def list_models():
|
||||
@app.route('/v1/chat/completions', methods=['POST'])
|
||||
def chat_completions():
|
||||
"""聊天完成API"""
|
||||
# 用于统计的变量
|
||||
request_model = None
|
||||
request_provider = None
|
||||
request_success = False
|
||||
request_tokens = 0
|
||||
|
||||
try:
|
||||
data = request.get_json()
|
||||
|
||||
if not data:
|
||||
increment_stats('unknown', 'unknown', success=False, error='Invalid request body')
|
||||
return jsonify({"error": "Invalid request body"}), 400
|
||||
|
||||
model = data.get('model', 'auto')
|
||||
stream = data.get('stream', False)
|
||||
|
||||
request_model = model
|
||||
|
||||
# 获取提供商
|
||||
provider, resolved_model = get_provider_for_model(model)
|
||||
|
||||
if not provider:
|
||||
increment_stats(model, 'unknown', success=False, error=f'No provider for model: {model}')
|
||||
return jsonify({
|
||||
"error": {
|
||||
"message": f"No available provider for model: {model}",
|
||||
@@ -312,6 +420,8 @@ def chat_completions():
|
||||
}
|
||||
}), 400
|
||||
|
||||
request_provider = provider['name']
|
||||
|
||||
logger.info(f"Request: model={model} -> provider={provider['name']}, resolved_model={resolved_model}, stream={stream}")
|
||||
|
||||
# 重试逻辑
|
||||
@@ -324,9 +434,11 @@ def chat_completions():
|
||||
|
||||
if response.status_code == 200:
|
||||
mark_provider_success(provider['name'])
|
||||
request_success = True
|
||||
|
||||
if stream:
|
||||
# 流式响应
|
||||
# 流式响应 - 统计流式请求
|
||||
increment_stats(model, provider['name'], success=True, tokens=0)
|
||||
return Response(
|
||||
stream_with_context(stream_response(response)),
|
||||
content_type='text/event-stream',
|
||||
@@ -336,26 +448,36 @@ def chat_completions():
|
||||
}
|
||||
)
|
||||
else:
|
||||
# 非流式响应
|
||||
return jsonify(response.json())
|
||||
# 非流式响应 - 提取token统计
|
||||
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:
|
||||
# 速率限制,尝试下一个提供商
|
||||
mark_provider_error(provider['name'], "Rate limit")
|
||||
else:
|
||||
# 任何非200响应都尝试下一个提供商
|
||||
error_info = response.json() if response.headers.get('content-type', '').startswith('application/json') else {"error": response.text}
|
||||
last_error = error_info
|
||||
logger.warning(f"Provider {provider['name']} returned {response.status_code}: {error_info}")
|
||||
mark_provider_error(provider['name'], f"HTTP {response.status_code}")
|
||||
tried_providers.append(provider['name'])
|
||||
|
||||
# 尝试下一个提供商
|
||||
next_provider, next_model = get_available_provider_for_auto('auto')
|
||||
next_provider, next_model = get_available_provider_for_auto('auto', exclude_providers=tried_providers)
|
||||
if next_provider and next_provider['name'] not in tried_providers:
|
||||
logger.info(f"Switching to next provider: {next_provider['name']}")
|
||||
provider = next_provider
|
||||
resolved_model = next_model
|
||||
request_provider = provider['name']
|
||||
time.sleep(RETRY_CONFIG['retry_delay'])
|
||||
continue
|
||||
|
||||
return jsonify(response.json()), response.status_code
|
||||
|
||||
else:
|
||||
last_error = response.json() if response.headers.get('content-type', '').startswith('application/json') else {"error": response.text}
|
||||
return jsonify(last_error), response.status_code
|
||||
# 所有提供商都尝试过了,返回最后一个错误
|
||||
increment_stats(model, provider['name'], success=False, error=str(last_error))
|
||||
return jsonify(error_info), response.status_code
|
||||
|
||||
except Exception as e:
|
||||
last_error = str(e)
|
||||
@@ -363,14 +485,16 @@ def chat_completions():
|
||||
tried_providers.append(provider['name'])
|
||||
|
||||
# 尝试下一个提供商
|
||||
next_provider, next_model = get_available_provider_for_auto('auto')
|
||||
next_provider, next_model = get_available_provider_for_auto('auto', exclude_providers=tried_providers)
|
||||
if next_provider and next_provider['name'] not in tried_providers:
|
||||
provider = next_provider
|
||||
resolved_model = next_model
|
||||
request_provider = provider['name']
|
||||
time.sleep(RETRY_CONFIG['retry_delay'])
|
||||
continue
|
||||
|
||||
# 所有重试都失败
|
||||
increment_stats(model, request_provider or 'unknown', success=False, error=str(last_error))
|
||||
return jsonify({
|
||||
"error": {
|
||||
"message": f"All providers failed. Last error: {last_error}",
|
||||
@@ -380,6 +504,7 @@ def chat_completions():
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error: {e}")
|
||||
increment_stats(request_model or 'unknown', request_provider or 'unknown', success=False, error=str(e))
|
||||
return jsonify({
|
||||
"error": {
|
||||
"message": str(e),
|
||||
|
||||
Reference in New Issue
Block a user