From 34afe96055afcc3999f9949997c23613c5cd096c Mon Sep 17 00:00:00 2001 From: hubian <908234780@qq.com> Date: Mon, 13 Apr 2026 10:59:00 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20v2.0.0=20-=20=E5=90=88=E5=B9=B6?= =?UTF-8?q?=E5=90=8E=E5=8F=B0=E7=AE=A1=E7=90=86=E5=88=B0=E5=8D=95=E7=AB=AF?= =?UTF-8?q?=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 端口从 19007+19008 合并为 19007 - 前台API: http://localhost:19007/v1/chat/completions - 后台管理: http://localhost:19007/admin - 新增 templates 目录,整合管理页面模板 - 更新所有路由为 /admin 路径 - 后台API统一到 /api/admin 路径下 --- app.py | 883 ++++++++++++++++++++++++++++------- data/config.json | 35 +- run.sh | 3 + templates/auto-profiles.html | 366 +++++++++++++++ templates/chat.html | 416 +++++++++++++++++ templates/config.html | 124 +++++ templates/index.html | 232 +++++++++ templates/logs.html | 99 ++++ templates/models.html | 98 ++++ templates/providers.html | 610 ++++++++++++++++++++++++ test_server.py | 27 ++ 11 files changed, 2728 insertions(+), 165 deletions(-) create mode 100755 run.sh create mode 100644 templates/auto-profiles.html create mode 100644 templates/chat.html create mode 100644 templates/config.html create mode 100644 templates/index.html create mode 100644 templates/logs.html create mode 100644 templates/models.html create mode 100644 templates/providers.html create mode 100644 test_server.py diff --git a/app.py b/app.py index 0396292..96657f0 100644 --- a/app.py +++ b/app.py @@ -1,9 +1,14 @@ """ 大模型API中转系统 +v2.0.0 - 合并后台管理到单端口 兼容OpenAI API格式,支持多上游提供商优先级调度 + +端口: 19007 +前台API: http://localhost:19007/v1/chat/completions +后台管理: http://localhost:19007/admin """ -from flask import Flask, request, jsonify, Response, stream_with_context +from flask import Flask, request, jsonify, Response, stream_with_context, render_template from flask_cors import CORS import requests import json @@ -13,22 +18,44 @@ from datetime import datetime, date from pathlib import Path import sys import threading +import uuid # 添加配置路径 sys.path.insert(0, str(Path(__file__).parent)) from config.settings import ( get_providers, get_model_aliases, get_auto_profiles, SERVER_CONFIG, - LOG_CONFIG, RETRY_CONFIG + LOG_CONFIG, RETRY_CONFIG, + load_config, save_config, get_provider, add_provider, update_provider, + delete_provider, update_priority, update_model_alias, + get_auto_profile, add_auto_profile, update_auto_profile, delete_auto_profile, + DEFAULT_PROVIDERS, DEFAULT_MODEL_ALIASES, DEFAULT_AUTO_PROFILES ) +app = Flask(__name__, template_folder='templates') +CORS(app) + # 数据目录和统计文件 DATA_DIR = Path(__file__).parent / 'data' DATA_DIR.mkdir(exist_ok=True) STATS_FILE = DATA_DIR / 'stats.json' +CHATS_FILE = DATA_DIR / 'chats.json' +LOGS_DIR = Path(__file__).parent / 'logs' +LOGS_DIR.mkdir(exist_ok=True) # 统计锁(避免并发写入冲突) stats_lock = threading.Lock() +# 提供商状态缓存 +provider_status = {} + +# 配置缓存时间(秒) +CONFIG_CACHE_TTL = 5 +_last_config_load = 0 +_cached_providers = [] +_cached_aliases = {} +_cached_auto_profiles = {} + + def load_stats(): """加载统计数据""" if STATS_FILE.exists(): @@ -45,21 +72,22 @@ def load_stats(): 'requests_by_model': {}, 'providers': {}, 'last_updated': None, - 'date': 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 @@ -67,7 +95,6 @@ def increment_stats(model, provider_name, success=False, tokens=0, error=None): 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 @@ -75,7 +102,6 @@ def increment_stats(model, provider_name, success=False, tokens=0, error=None): 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 @@ -90,18 +116,9 @@ def increment_stats(model, provider_name, success=False, tokens=0, error=None): save_stats(stats) -app = Flask(__name__) -CORS(app) - -# 配置缓存时间(秒) -CONFIG_CACHE_TTL = 5 -_last_config_load = 0 -_cached_providers = [] -_cached_aliases = {} -_cached_auto_profiles = {} def refresh_config(): - """动态刷新配置(支持后台管理修改)""" + """动态刷新配置""" global _last_config_load, _cached_providers, _cached_aliases, _cached_auto_profiles, provider_status current_time = time.time() @@ -111,7 +128,6 @@ def refresh_config(): _cached_auto_profiles = get_auto_profiles() _last_config_load = current_time - # 更新提供商状态缓存(新增的提供商) for provider in _cached_providers: if provider['name'] not in provider_status: provider_status[provider['name']] = { @@ -121,39 +137,61 @@ def refresh_config(): 'last_error': None, } -# 配置日志 -log_dir = Path(__file__).parent / LOG_CONFIG['log_dir'] -log_dir.mkdir(exist_ok=True) +def refresh_provider_status(): + """刷新提供商状态""" + providers = get_providers() + for provider in providers: + if provider['name'] not in provider_status: + provider_status[provider['name']] = { + 'available': True, + 'last_check': None, + 'error_count': 0, + 'last_error': None, + 'request_count': 0, + 'success_count': 0, + 'total_tokens': 0, + } + + +def load_chats(): + """加载对话数据""" + if CHATS_FILE.exists(): + return json.loads(CHATS_FILE.read_text(encoding='utf-8')) + return {'chats': []} + + +def save_chats(data): + """保存对话数据""" + CHATS_FILE.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding='utf-8') + + +# 配置日志 logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', handlers=[ - logging.FileHandler(log_dir / 'proxy.log', encoding='utf-8'), + logging.FileHandler(LOGS_DIR / 'proxy.log', encoding='utf-8'), logging.StreamHandler() ] ) logger = logging.getLogger(__name__) -# 提供商状态缓存 -provider_status = {} - # 初始化 refresh_config() +# ============ 提供商选择逻辑 ============ + def get_provider_for_model(model_name): """根据模型名获取提供商""" refresh_config() - # 解析别名 resolved_model = _cached_aliases.get(model_name, model_name) - # auto模式:按优先级选择可用提供商 if resolved_model == 'auto' or resolved_model.startswith('auto-'): return get_available_provider_for_auto(resolved_model) - # 查找支持该模型的提供商 sorted_providers = sorted(_cached_providers, key=lambda x: x['priority']) for provider in sorted_providers: @@ -164,7 +202,6 @@ def get_provider_for_model(model_name): if resolved_model in provider['models']: return provider, resolved_model - # 如果没找到精确匹配,尝试模糊匹配 for provider in sorted_providers: if not provider['enabled']: continue @@ -178,23 +215,13 @@ def get_provider_for_model(model_name): def get_available_provider_for_auto(auto_name='auto', exclude_providers=None): - """获取auto模式下的可用提供商(支持自定义auto配置) - - Args: - auto_name: auto配置名称 - exclude_providers: 要排除的提供商名称列表(已尝试过的) - """ + """获取auto模式下的可用提供商""" refresh_config() exclude_providers = exclude_providers or [] - - # 获取auto配置 profile = _cached_auto_profiles.get(auto_name, _cached_auto_profiles.get('auto', {})) - - # 获取允许的提供商 allowed_providers = profile.get('providers', ['*']) - # 筛选提供商 sorted_providers = sorted(_cached_providers, key=lambda x: x['priority']) candidates = [] @@ -206,32 +233,17 @@ def get_available_provider_for_auto(auto_name='auto', exclude_providers=None): if not provider_status.get(provider['name'], {}).get('available', True): continue - # 检查是否在允许列表中 if '*' in allowed_providers: candidates.append(provider) elif provider.get('id') in allowed_providers or provider['name'] in allowed_providers: candidates.append(provider) - # 根据策略选择 - strategy = profile.get('strategy', 'priority') - if not candidates: - # 如果没有候选,返回第一个尝试(让错误信息传递) if sorted_providers: return sorted_providers[0], sorted_providers[0]['default_model'] return None, None - if strategy == 'priority': - # 按优先级选择第一个 - return candidates[0], candidates[0]['default_model'] - elif strategy == 'random': - # 随机选择 - import random - selected = random.choice(candidates) - return selected, selected['default_model'] - else: - # 默认按优先级 - return candidates[0], candidates[0]['default_model'] + return candidates[0], candidates[0]['default_model'] def mark_provider_error(provider_name, error): @@ -241,10 +253,9 @@ def mark_provider_error(provider_name, error): provider_status[provider_name]['last_error'] = str(error) provider_status[provider_name]['last_check'] = datetime.now() - # 连续错误超过阈值,暂时标记不可用 if provider_status[provider_name]['error_count'] >= 3: provider_status[provider_name]['available'] = False - logger.warning(f"Provider {provider_name} marked as unavailable due to errors") + logger.warning(f"Provider {provider_name} marked as unavailable") def mark_provider_success(provider_name): @@ -264,31 +275,16 @@ def proxy_request(provider, model, request_data, stream=False): "Authorization": f"Bearer {provider['api_key']}" } - # 构建请求数据 data = request_data.copy() data['model'] = model try: if stream: - # 流式请求 - response = requests.post( - url, - headers=headers, - json=data, - stream=True, - timeout=provider.get('timeout', 120) - ) + response = requests.post(url, headers=headers, json=data, stream=True, timeout=provider.get('timeout', 120)) return response else: - # 非流式请求 - response = requests.post( - url, - headers=headers, - json=data, - timeout=provider.get('timeout', 120) - ) + response = requests.post(url, headers=headers, json=data, timeout=provider.get('timeout', 120)) return response - except requests.exceptions.Timeout: mark_provider_error(provider['name'], "Timeout") raise Exception(f"Provider {provider['name']} timeout") @@ -311,39 +307,22 @@ def stream_response(response): yield b'data: {"error": "' + str(e).encode() + b'"}\n\n' -# ============ API 路由 ============ +# ============ 前台 API 路由 ============ @app.route('/') def index(): """首页""" return jsonify({ "name": "LLM Proxy", - "version": "1.0.0", + "version": "2.0.0", "description": "OpenAI-compatible LLM API Proxy", "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))" - } + "status": "/status", + "admin": "/admin" } }) @@ -356,7 +335,6 @@ def list_models(): models_list = [] added_models = set() - # 添加所有auto配置 for profile_name, profile in _cached_auto_profiles.items(): if profile_name not in added_models: models_list.append({ @@ -381,16 +359,12 @@ def list_models(): }) added_models.add(model) - return jsonify({ - "object": "list", - "data": models_list - }) + return jsonify({"object": "list", "data": models_list}) @app.route('/v1/chat/completions', methods=['POST']) def chat_completions(): """聊天完成API""" - # 用于统计的变量 request_model = None request_provider = None request_success = False @@ -405,26 +379,17 @@ def chat_completions(): 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}", - "type": "invalid_request_error" - } - }), 400 + return jsonify({"error": {"message": f"No available provider for model: {model}", "type": "invalid_request_error"}}), 400 request_provider = provider['name'] - logger.info(f"Request: model={model} -> provider={provider['name']}, resolved_model={resolved_model}, stream={stream}") - # 重试逻辑 last_error = None tried_providers = [] @@ -437,35 +402,26 @@ def chat_completions(): 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', - headers={ - 'Cache-Control': 'no-cache', - 'Connection': 'keep-alive', - } + headers={'Cache-Control': 'no-cache', 'Connection': 'keep-alive'} ) else: - # 非流式响应 - 提取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) 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', 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']}") @@ -475,7 +431,6 @@ def chat_completions(): time.sleep(RETRY_CONFIG['retry_delay']) continue - # 所有提供商都尝试过了,返回最后一个错误 increment_stats(model, provider['name'], success=False, error=str(last_error)) return jsonify(error_info), response.status_code @@ -484,7 +439,6 @@ def chat_completions(): logger.error(f"Attempt {attempt + 1} failed: {e}") tried_providers.append(provider['name']) - # 尝试下一个提供商 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 @@ -493,50 +447,31 @@ def chat_completions(): 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}", - "type": "api_error" - } - }), 503 + return jsonify({"error": {"message": f"All providers failed. Last error: {last_error}", "type": "api_error"}}), 503 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), - "type": "internal_error" - } - }), 500 + return jsonify({"error": {"message": str(e), "type": "internal_error"}}), 500 @app.route('/v1/embeddings', methods=['POST']) def embeddings(): - """嵌入API(简单转发)""" + """嵌入API""" refresh_config() try: data = request.get_json() - model = data.get('model', 'text-embedding-ada-002') - # 使用第一个可用提供商 if _cached_providers: provider = _cached_providers[0] - url = f"{provider['base_url'].rstrip('/')}/embeddings" - headers = { - "Content-Type": "application/json", - "Authorization": f"Bearer {provider['api_key']}" - } - + headers = {"Content-Type": "application/json", "Authorization": f"Bearer {provider['api_key']}"} response = requests.post(url, headers=headers, json=data, timeout=60) return jsonify(response.json()), response.status_code else: return jsonify({"error": "No providers available"}), 503 - except Exception as e: return jsonify({"error": str(e)}), 500 @@ -549,10 +484,7 @@ def health(): return jsonify({ "status": "healthy" if available_count > 0 else "degraded", - "providers": { - "available": available_count, - "total": total_count, - }, + "providers": {"available": available_count, "total": total_count}, "timestamp": datetime.now().isoformat() }) @@ -563,7 +495,6 @@ def status(): refresh_config() providers_detail = [] - for provider in _cached_providers: status_info = provider_status.get(provider['name'], {}) providers_detail.append({ @@ -577,46 +508,674 @@ def status(): }) return jsonify({ - "version": "1.0.0", + "version": "2.0.0", "uptime": time.time(), "providers": providers_detail, "model_aliases": _cached_aliases, }) -# 兼容 OpenAI SDK 的其他端点 +# 兼容旧版端点 @app.route('/v1/engines', methods=['GET']) def list_engines(): - """兼容旧版 engines 端点""" return list_models() @app.route('/v1/engines//completions', methods=['POST']) def engine_completions(model): - """兼容旧版 completions 端点""" data = request.get_json() data['model'] = model return chat_completions() +# ============ 后台管理页面路由 ============ + +@app.route('/admin') +def admin_index(): + return render_template('index.html') + + +@app.route('/admin/providers') +def admin_providers(): + return render_template('providers.html') + + +@app.route('/admin/models') +def admin_models(): + return render_template('models.html') + + +@app.route('/admin/logs') +def admin_logs(): + return render_template('logs.html') + + +@app.route('/admin/config') +def admin_config(): + return render_template('config.html') + + +@app.route('/admin/chat') +def admin_chat(): + return render_template('chat.html') + + +@app.route('/admin/auto-profiles') +def admin_auto_profiles(): + return render_template('auto-profiles.html') + + +# ============ 后台管理 API 路由 ============ + +@app.route('/api/admin/stats') +def api_admin_stats(): + """获取统计数据""" + stats = load_stats() + providers = get_providers() + refresh_provider_status() + + available_count = sum(1 for p in providers if provider_status.get(p['name'], {}).get('available', True)) + + return jsonify({ + 'total_requests': stats.get('total_requests', 0), + 'total_success': stats.get('total_success', 0), + 'total_errors': stats.get('total_errors', 0), + 'total_tokens': stats.get('total_tokens', 0), + 'providers_count': len(providers), + 'available_providers': available_count, + 'models_count': len(get_model_aliases()), + 'uptime': time.time(), + }) + + +@app.route('/api/admin/providers') +def api_admin_providers(): + """获取提供商列表""" + providers = get_providers() + refresh_provider_status() + stats = load_stats() + providers_data = [] + + for provider in sorted(providers, key=lambda x: x['priority']): + p_stats = stats.get('providers', {}).get(provider['name'], {}) + p_status = provider_status.get(provider['name'], {}) + + providers_data.append({ + 'id': provider.get('id', provider['name'].lower().replace(' ', '-')), + 'name': provider['name'], + 'priority': provider['priority'], + 'enabled': provider['enabled'], + 'available': p_status.get('available', True), + 'base_url': provider['base_url'], + 'api_key': provider['api_key'], + 'models': provider['models'], + 'default_model': provider['default_model'], + 'timeout': provider.get('timeout', 120), + 'request_count': p_stats.get('requests', 0), + 'success_count': p_stats.get('success', 0), + 'error_count': p_status.get('error_count', 0), + 'last_error': p_status.get('last_error'), + 'last_check': p_status.get('last_check'), + }) + + return jsonify(providers_data) + + +@app.route('/api/admin/providers/', methods=['GET']) +def api_admin_provider_detail(provider_id): + """获取提供商详情""" + provider = get_provider(provider_id) + + if not provider: + return jsonify({'error': 'Provider not found'}), 404 + + stats = load_stats() + p_stats = stats.get('providers', {}).get(provider['name'], {}) + p_status = provider_status.get(provider['name'], {}) + + return jsonify({ + **provider, + 'status': { + 'available': p_status.get('available', True), + 'error_count': p_status.get('error_count', 0), + 'last_error': p_status.get('last_error'), + 'request_count': p_stats.get('requests', 0), + 'success_count': p_stats.get('success', 0), + } + }) + + +@app.route('/api/admin/providers', methods=['POST']) +def api_admin_add_provider(): + """添加新提供商""" + data = request.get_json() + + if not data: + return jsonify({'error': 'Invalid request body'}), 400 + + required = ['name', 'base_url', 'api_key', 'models'] + for field in required: + if not data.get(field): + return jsonify({'error': f'Missing required field: {field}'}), 400 + + providers = get_providers() + max_priority = max([p['priority'] for p in providers]) if providers else 0 + + new_provider = { + 'id': data.get('id') or data['name'].lower().replace(' ', '-').replace('.', '-'), + 'name': data['name'], + 'priority': data.get('priority', max_priority + 1), + 'base_url': data['base_url'].rstrip('/'), + 'api_key': data['api_key'], + 'models': data['models'] if isinstance(data['models'], list) else data['models'].split(','), + 'default_model': data.get('default_model', data['models'][0] if isinstance(data['models'], list) else data['models'].split(',')[0]), + 'timeout': data.get('timeout', 120), + 'enabled': data.get('enabled', True), + } + + result = add_provider(new_provider) + + provider_status[result['name']] = { + 'available': True, + 'last_check': None, + 'error_count': 0, + 'last_error': None, + } + + return jsonify({'success': True, 'provider': result}) + + +@app.route('/api/admin/providers/', methods=['PUT']) +def api_admin_update_provider(provider_id): + """更新提供商""" + data = request.get_json() + + if not data: + return jsonify({'error': 'Invalid request body'}), 400 + + if 'models' in data and isinstance(data['models'], str): + data['models'] = [m.strip() for m in data['models'].split(',') if m.strip()] + + result = update_provider(provider_id, data) + + if not result: + return jsonify({'error': 'Provider not found'}), 404 + + return jsonify({'success': True, 'provider': result}) + + +@app.route('/api/admin/providers/', methods=['DELETE']) +def api_admin_delete_provider(provider_id): + """删除提供商""" + result = delete_provider(provider_id) + + if not result: + return jsonify({'error': 'Provider not found'}), 404 + + providers = get_providers() + for p in providers: + if p.get('id') == provider_id: + if p['name'] in provider_status: + del provider_status[p['name']] + break + + return jsonify({'success': True}) + + +@app.route('/api/admin/providers/priority', methods=['POST']) +def api_admin_update_priority(): + """更新优先级顺序""" + data = request.get_json() + + if not data or 'order' not in data: + return jsonify({'error': 'Missing order field'}), 400 + + provider_ids = data['order'] + result = update_priority(provider_ids) + + return jsonify({'success': True, 'providers': result}) + + +@app.route('/api/admin/providers//toggle', methods=['POST']) +def api_admin_toggle_provider(provider_id): + """切换提供商启用状态""" + provider = get_provider(provider_id) + + if not provider: + return jsonify({'error': 'Provider not found'}), 404 + + new_enabled = not provider.get('enabled', True) + result = update_provider(provider_id, {'enabled': new_enabled}) + + return jsonify({'success': True, 'enabled': new_enabled}) + + +@app.route('/api/admin/providers//test', methods=['POST']) +def api_admin_test_provider(provider_id): + """测试提供商连接""" + provider = get_provider(provider_id) + + if not provider: + return jsonify({'success': False, 'error': 'Provider not found'}), 404 + + try: + url = f"{provider['base_url'].rstrip('/')}/models" + headers = {"Authorization": f"Bearer {provider['api_key']}"} + + response = requests.get(url, headers=headers, timeout=10) + + if response.status_code == 200: + provider_status[provider['name']] = { + 'available': True, + 'last_check': datetime.now().isoformat(), + 'error_count': 0, + 'last_error': None, + } + models_data = [] + try: + resp_json = response.json() + models_data = resp_json.get('data', []) + except: + pass + + return jsonify({'success': True, 'message': 'Connection successful', 'models_count': len(models_data)}) + else: + provider_status[provider['name']] = { + 'available': False, + 'last_check': datetime.now().isoformat(), + 'error_count': provider_status.get(provider['name'], {}).get('error_count', 0) + 1, + 'last_error': f'HTTP {response.status_code}', + } + return jsonify({'success': False, 'error': f'HTTP {response.status_code}: {response.text[:200]}'}) + + except Exception as e: + provider_status[provider['name']] = { + 'available': False, + 'last_check': datetime.now().isoformat(), + 'error_count': provider_status.get(provider['name'], {}).get('error_count', 0) + 1, + 'last_error': str(e), + } + return jsonify({'success': False, 'error': str(e)}) + + +@app.route('/api/admin/models') +def api_admin_models(): + """获取模型列表""" + providers = get_providers() + aliases = get_model_aliases() + + models_list = [] + added = set() + + models_list.append({'alias': 'auto', 'target': 'auto', 'description': '自动选择可用模型'}) + added.add('auto') + + for provider in sorted(providers, key=lambda x: x['priority']): + for model in provider['models']: + if model not in added: + models_list.append({ + 'alias': model, + 'target': model, + 'provider': provider['name'], + 'priority': provider['priority'], + }) + added.add(model) + + for alias, target in aliases.items(): + if alias != 'auto' and alias not in added: + provider_name = None + for p in providers: + if target in p['models']: + provider_name = p['name'] + break + models_list.append({'alias': alias, 'target': target, 'provider': provider_name}) + + return jsonify(models_list) + + +@app.route('/api/admin/logs') +def api_admin_logs(): + """获取日志""" + log_file = LOGS_DIR / 'proxy.log' + + lines = [] + if log_file.exists(): + content = log_file.read_text(encoding='utf-8') + lines = content.strip().split('\n')[-100:] + + return jsonify({'logs': lines, 'total_lines': len(lines)}) + + +@app.route('/api/admin/config') +def api_admin_config(): + """获取配置""" + providers = get_providers() + aliases = get_model_aliases() + + return jsonify({ + 'providers': [{ + 'id': p.get('id', p['name'].lower().replace(' ', '-')), + 'name': p['name'], + 'priority': p['priority'], + 'base_url': p['base_url'], + 'models': p['models'], + 'timeout': p.get('timeout', 120), + 'enabled': p['enabled'], + } for p in providers], + 'model_aliases': aliases, + 'retry_config': RETRY_CONFIG, + 'server_config': {'port': SERVER_CONFIG['port']} + }) + + +# ============ Auto配置管理 ============ + +@app.route('/api/admin/auto-profiles') +def api_admin_auto_profiles(): + """获取所有Auto配置""" + profiles = get_auto_profiles() + providers = get_providers() + + result = [] + for name, profile in profiles.items(): + allowed_providers = profile.get('providers', ['*']) + provider_details = [] + + if '*' in allowed_providers: + provider_details = [{'id': '*', 'name': '所有启用的提供商'}] + else: + for p in providers: + if p.get('id') in allowed_providers or p['name'] in allowed_providers: + provider_details.append({'id': p.get('id'), 'name': p['name'], 'priority': p['priority']}) + + result.append({ + 'name': name, + 'display_name': profile.get('name', name), + 'description': profile.get('description', ''), + 'strategy': profile.get('strategy', 'priority'), + 'providers': allowed_providers, + 'provider_details': provider_details, + }) + + return jsonify(result) + + +@app.route('/api/admin/auto-profiles/', methods=['GET']) +def api_admin_auto_profile_detail(profile_name): + """获取单个Auto配置详情""" + profile = get_auto_profile(profile_name) + + if not profile: + return jsonify({'error': 'Profile not found'}), 404 + + providers = get_providers() + allowed_providers = profile.get('providers', ['*']) + provider_details = [] + + if '*' in allowed_providers: + provider_details = [{'id': '*', 'name': '所有启用的提供商', 'selected': True}] + for p in providers: + provider_details.append({'id': p.get('id'), 'name': p['name'], 'priority': p['priority'], 'selected': True}) + else: + for p in providers: + selected = p.get('id') in allowed_providers or p['name'] in allowed_providers + provider_details.append({'id': p.get('id'), 'name': p['name'], 'priority': p['priority'], 'selected': selected}) + + return jsonify({ + 'name': profile_name, + 'display_name': profile.get('name', profile_name), + 'description': profile.get('description', ''), + 'strategy': profile.get('strategy', 'priority'), + 'providers': allowed_providers, + 'provider_details': provider_details, + }) + + +@app.route('/api/admin/auto-profiles', methods=['POST']) +def api_admin_add_auto_profile(): + """添加新的Auto配置""" + data = request.get_json() + + if not data or not data.get('name'): + return jsonify({'error': 'Missing profile name'}), 400 + + profile_name = data['name'].lower().replace(' ', '-').replace('.', '-') + + if profile_name in get_auto_profiles(): + return jsonify({'error': 'Profile already exists'}), 400 + + profile_data = { + 'name': data.get('display_name', data['name']), + 'description': data.get('description', ''), + 'providers': data.get('providers', ['*']), + 'strategy': data.get('strategy', 'priority'), + } + + result = add_auto_profile(profile_name, profile_data) + + return jsonify({'success': True, 'profile': {profile_name: profile_data}}) + + +@app.route('/api/admin/auto-profiles/', methods=['PUT']) +def api_admin_update_auto_profile(profile_name): + """更新Auto配置""" + data = request.get_json() + + if not data: + return jsonify({'error': 'Invalid request body'}), 400 + + profile_data = {} + if 'display_name' in data: + profile_data['name'] = data['display_name'] + if 'description' in data: + profile_data['description'] = data['description'] + if 'providers' in data: + profile_data['providers'] = data['providers'] + if 'strategy' in data: + profile_data['strategy'] = data['strategy'] + + result = update_auto_profile(profile_name, profile_data) + + if not result: + return jsonify({'error': 'Profile not found'}), 404 + + return jsonify({'success': True, 'profile': result}) + + +@app.route('/api/admin/auto-profiles/', methods=['DELETE']) +def api_admin_delete_auto_profile(profile_name): + """删除Auto配置""" + result = delete_auto_profile(profile_name) + + if not result: + return jsonify({'error': 'Cannot delete default auto profile or profile not found'}), 400 + + return jsonify({'success': True}) + + +# ============ 对话功能 ============ + +@app.route('/api/admin/chat/models') +def api_admin_chat_models(): + """获取可用模型列表""" + providers = get_providers() + profiles = get_auto_profiles() + + models = [] + added = set() + + for name, profile in profiles.items(): + if name not in added: + models.append({'id': name, 'description': profile.get('description', 'Auto-select')}) + added.add(name) + + for provider in providers: + if not provider['enabled']: + continue + for model in provider['models']: + if model not in added: + models.append({'id': model, 'description': provider['name']}) + added.add(model) + + return jsonify(models) + + +@app.route('/api/admin/chat/list') +def api_admin_chat_list(): + """获取对话列表""" + data = load_chats() + + chats = [] + for chat in data.get('chats', []): + chats.append({ + 'id': chat['id'], + 'title': chat.get('title', '新对话'), + 'model': chat.get('model', 'auto'), + 'message_count': len(chat.get('messages', [])), + 'created_at': chat.get('created_at'), + 'updated_at': chat.get('updated_at') + }) + + chats.sort(key=lambda x: x.get('updated_at', ''), reverse=True) + + return jsonify(chats) + + +@app.route('/api/admin/chat/') +def api_admin_chat_detail(chat_id): + """获取对话详情""" + data = load_chats() + + for chat in data.get('chats', []): + if chat['id'] == chat_id: + return jsonify(chat) + + return jsonify({'error': 'Chat not found'}), 404 + + +@app.route('/api/admin/chat/send', methods=['POST']) +def api_admin_chat_send(): + """发送消息""" + req = request.get_json() + + user_message = req.get('message', '') + model = req.get('model', 'auto') + chat_id = req.get('chat_id') + + if not user_message: + return jsonify({'error': 'Message is required'}), 400 + + data = load_chats() + + chat = None + if chat_id: + for c in data['chats']: + if c['id'] == chat_id: + chat = c + break + + if not chat: + chat_id = str(uuid.uuid4())[:8] + chat = { + 'id': chat_id, + 'title': '新对话', + 'model': model, + 'messages': [], + 'created_at': datetime.now().isoformat(), + 'updated_at': datetime.now().isoformat() + } + data['chats'].append(chat) + + chat['messages'].append({ + 'role': 'user', + 'content': user_message, + 'time': datetime.now().isoformat() + }) + + try: + proxy_url = f"http://localhost:{SERVER_CONFIG['port']}/v1/chat/completions" + + messages = [] + for msg in chat['messages'][-20:]: + messages.append({'role': msg['role'], 'content': msg['content']}) + + response = requests.post(proxy_url, json={ + 'model': model, + 'messages': messages, + 'stream': False + }, timeout=120) + + if response.status_code == 200: + result = response.json() + assistant_message = result['choices'][0]['message']['content'] + used_model = result.get('model', model) + + chat['messages'].append({ + 'role': 'assistant', + 'content': assistant_message, + 'model': used_model, + 'time': datetime.now().isoformat() + }) + + if len(chat['messages']) == 2: + chat['title'] = user_message[:30] + ('...' if len(user_message) > 30 else '') + + chat['updated_at'] = datetime.now().isoformat() + save_chats(data) + + return jsonify({ + 'success': True, + 'chat_id': chat_id, + 'response': assistant_message, + 'model': used_model, + 'title': chat['title'] + }) + else: + error_msg = response.json().get('error', {}).get('message', 'Unknown error') + return jsonify({'error': error_msg}), response.status_code + + except Exception as e: + return jsonify({'error': str(e)}), 500 + + +@app.route('/api/admin/chat/', methods=['DELETE']) +def api_admin_delete_chat(chat_id): + """删除对话""" + data = load_chats() + data['chats'] = [c for c in data['chats'] if c['id'] != chat_id] + save_chats(data) + return jsonify({'success': True}) + + +@app.route('/api/admin/chat//clear', methods=['POST']) +def api_admin_clear_chat(chat_id): + """清空对话消息""" + data = load_chats() + + for chat in data['chats']: + if chat['id'] == chat_id: + chat['messages'] = [] + chat['updated_at'] = datetime.now().isoformat() + save_chats(data) + return jsonify({'success': True}) + + return jsonify({'error': 'Chat not found'}), 404 + + if __name__ == '__main__': refresh_config() print("=" * 60) - print("大模型API中转系统") + print("大模型API中转系统 v2.0.0") print("=" * 60) - print(f"访问地址: http://localhost:{SERVER_CONFIG['port']}") - print(f"API端点: http://localhost:{SERVER_CONFIG['port']}/v1/chat/completions") + print(f"API地址: http://localhost:{SERVER_CONFIG['port']}") + print(f"后台管理: http://localhost:{SERVER_CONFIG['port']}/admin") print("=" * 60) print("上游提供商:") for p in sorted(_cached_providers, key=lambda x: x['priority']): print(f" [{p['priority']}] {p['name']}: {p['base_url']}") print(f" 模型: {', '.join(p['models'])}") print("=" * 60) - print("支持的模型别名:") - for alias, target in _cached_aliases.items(): - print(f" {alias} -> {target}") - print("=" * 60) app.run( host=SERVER_CONFIG['host'], diff --git a/data/config.json b/data/config.json index ecb7825..cebf9c0 100644 --- a/data/config.json +++ b/data/config.json @@ -6,8 +6,13 @@ "priority": 1, "base_url": "http://192.168.2.5:1234/v1", "api_key": "sk-lm-fuP5tGU8:Hi7YU87jHyDP6Ay8Tl2j", - "models": ["qwen3.5-4b", "qwen3.5", "qwen"], - "default_model": "qwen3.5-4b", + "models": [ + "qwen/qwen3.5-35b-a3b", + "qwen3.5-4b", + "qwen3.5", + "qwen" + ], + "default_model": "qwen/qwen3.5-35b-a3b", "timeout": 120, "enabled": true }, @@ -17,7 +22,11 @@ "priority": 2, "base_url": "https://api.siliconflow.cn/v1", "api_key": "sk-fhpoexpptvjghpnphtaxbkhjwulzovoqfffbckcfscjmwhcg", - "models": ["Pro/deepseek-ai/DeepSeek-V3.2", "deepseek-v3", "deepseek"], + "models": [ + "Pro/deepseek-ai/DeepSeek-V3.2", + "deepseek-v3", + "deepseek" + ], "default_model": "Pro/deepseek-ai/DeepSeek-V3.2", "timeout": 120, "enabled": true @@ -31,5 +40,25 @@ "deepseek": "Pro/deepseek-ai/DeepSeek-V3.2", "deepseek-v3": "Pro/deepseek-ai/DeepSeek-V3.2", "deepseek-v3.2": "Pro/deepseek-ai/DeepSeek-V3.2" + }, + "auto_profiles": { + "auto-vlm": { + "name": "auto-vlm", + "description": "视觉多模态大模型自动版", + "providers": [ + "local-qwen", + "siliconflow-deepseek" + ], + "strategy": "priority" + }, + "auto": { + "name": "auto", + "description": "auto", + "providers": [ + "local-qwen", + "siliconflow-deepseek" + ], + "strategy": "priority" + } } } \ No newline at end of file diff --git a/run.sh b/run.sh new file mode 100755 index 0000000..3ac301c --- /dev/null +++ b/run.sh @@ -0,0 +1,3 @@ +#!/bin/bash +cd /home/xian/.openclaw/common/projects/llm-proxy +exec python3 app.py diff --git a/templates/auto-profiles.html b/templates/auto-profiles.html new file mode 100644 index 0000000..5ebeb43 --- /dev/null +++ b/templates/auto-profiles.html @@ -0,0 +1,366 @@ + + + + + + Auto配置管理 - LLM Proxy + + + + + + +
+ + +
+ +
+
+

Auto配置管理

+

创建自定义的自动选择模式,指定候选提供商和优先级

+
+ +
+ + +
+ + Auto配置:创建自定义的模型自动选择模式。例如 model="auto-fast" 只选择响应快的提供商,model="auto-cheap" 只选择便宜的提供商。 +
+ + +
+

加载中...

+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/templates/chat.html b/templates/chat.html new file mode 100644 index 0000000..f49f4c3 --- /dev/null +++ b/templates/chat.html @@ -0,0 +1,416 @@ + + + + + + 对话 - LLM Proxy + + + + + + + +
+ + + + +
+ +
+
+

历史对话

+ +
+
+

加载中...

+
+
+ + +
+ +
+
+ + 新对话 +
+
+ +
+
+ + +
+
+ +

开始新对话

+
+
+ + +
+
+ + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/templates/config.html b/templates/config.html new file mode 100644 index 0000000..0674677 --- /dev/null +++ b/templates/config.html @@ -0,0 +1,124 @@ + + + + + + 系统配置 - LLM Proxy + + + + + + +
+ + +
+

系统配置

+ +
+

加载中...

+
+
+
+ + + + \ No newline at end of file diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..7d538b6 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,232 @@ + + + + + + LLM Proxy - 后台管理 + + + + + + +
+ + + + +
+ +
+
+
+
+

总请求数

+

-

+
+
+ +
+
+
+ +
+
+
+

成功率

+

-

+
+
+ +
+
+
+ +
+
+
+

提供商状态

+

-

+
+
+ +
+
+
+ +
+
+
+

支持模型

+

-

+
+
+ +
+
+
+
+ + +
+
+

+ + 提供商状态 +

+
+

加载中...

+
+
+ +
+

+ + 调用流程 +

+
+
+ 1 +
+

客户端请求

+

POST /v1/chat/completions

+
+
+
+ 2 +
+

解析模型名

+

model="auto" → 按优先级选择

+
+
+
+ 3 +
+

转发请求

+

调用高优先级提供商

+
+
+
+ 4 +
+

故障切换

+

失败自动切换备用提供商

+
+
+
+
+
+ + + +
+
+ + + + \ No newline at end of file diff --git a/templates/logs.html b/templates/logs.html new file mode 100644 index 0000000..a3fcbdc --- /dev/null +++ b/templates/logs.html @@ -0,0 +1,99 @@ + + + + + + 日志查看 - LLM Proxy + + + + + + +
+ + +
+
+

日志查看

+ +
+ +
+
+

加载中...

+
+
+
+
+ + + + \ No newline at end of file diff --git a/templates/models.html b/templates/models.html new file mode 100644 index 0000000..730dfb6 --- /dev/null +++ b/templates/models.html @@ -0,0 +1,98 @@ + + + + + + 模型管理 - LLM Proxy + + + + + + +
+ + +
+

模型管理

+ +
+ + + + + + + + + + + + + +
模型别名目标模型提供商优先级说明
加载中...
+
+
+
+ + + + \ No newline at end of file diff --git a/templates/providers.html b/templates/providers.html new file mode 100644 index 0000000..0b605c6 --- /dev/null +++ b/templates/providers.html @@ -0,0 +1,610 @@ + + + + + + 提供商管理 - LLM Proxy + + + + + + +
+ + +
+ +
+
+

提供商管理

+

拖拽卡片调整优先级顺序(auto模式选择顺序)

+
+ +
+ + +
+

加载中...

+
+ + +
+ + Auto模式优先级: 当使用 model="auto" 时,系统会按优先级顺序依次尝试可用的提供商。拖拽上方卡片可调整顺序。 +
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/test_server.py b/test_server.py new file mode 100644 index 0000000..ae7a0a8 --- /dev/null +++ b/test_server.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 +"""简单的测试服务""" + +from flask import Flask, jsonify +from flask_cors import CORS + +app = Flask(__name__) +CORS(app) + +@app.route('/') +def index(): + return jsonify({ + "name": "LLM Proxy", + "status": "running" + }) + +@app.route('/health') +def health(): + return jsonify({"status": "ok"}) + +if __name__ == '__main__': + print("=" * 50) + print("LLM Proxy Test Server") + print("=" * 50) + print("Port: 19007") + print("=" * 50) + app.run(host='0.0.0.0', port=19007) \ No newline at end of file