Files
llm-proxy/app.py
T
hz4th_coder d387d5ba09 v2.1.1: Auto配置改为模型粒度拖动排序 + 历史缓存复用 + 对话生图
- Auto配置编辑: 粒度从提供商改为具体模型, 原生HTML5拖动排序, 修复已保存模型不显示勾选(字符串/对象混拼bug)
- 历史缓存: 带历史上下文的auto请求按首条消息识别会话, 优先复用上次模型命中上游前缀缓存
  (可配置: prefer_cache_model + cache_ttl_seconds, 系统配置页开关 + /api/admin/routing)
- 对话生图: chat页选 auto-image/Qwen-Image 直接调生图接口并在对话内展示图片
- 修复: Flask模板缓存导致改模板不生效 -> TEMPLATES_AUTO_RELOAD
- 修复: 自动回退/显式有序模型列表路由, 空列表回退提供商能力选择
2026-08-28 12:16:25 +08:00

1971 lines
71 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
大模型API中转系统
v2.1.0 - 能力(Capability)体系 + 模型管理 + 生图/语音/视频端点
兼容OpenAI API格式,支持多上游提供商优先级调度
端口: 16003
前台API: http://localhost:16003/v1/chat/completions
后台管理: http://localhost:16003/admin
"""
from flask import Flask, request, jsonify, Response, stream_with_context, render_template
from flask_cors import CORS
import requests
import json
import time
import random
import hashlib
import logging
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, get_auto_profile,
SERVER_CONFIG, LOG_CONFIG, RETRY_CONFIG, CAPABILITY_DEFS, ROUTING_CONFIG,
load_config, save_config, get_provider, add_provider, update_provider,
delete_provider, update_priority, update_model_alias, delete_model_alias,
add_auto_profile, update_auto_profile, delete_auto_profile, load_routing_config, save_routing_config,
DEFAULT_PROVIDERS, DEFAULT_MODEL_ALIASES, DEFAULT_AUTO_PROFILES
)
app = Flask(__name__, template_folder='templates')
app.config['TEMPLATES_AUTO_RELOAD'] = True # 模板修改即时生效(无需重启)
CORS(app)
VERSION = "2.1.1"
# 数据目录和统计文件
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 / (LOG_CONFIG.get('log_dir', 'logs'))
LOGS_DIR.mkdir(exist_ok=True)
# 统计锁(避免并发写入冲突)
stats_lock = threading.Lock()
chats_lock = threading.Lock()
# 历史上下文→模型缓存(用于 auto 请求优先复用上次模型,命中前缀缓存)
history_model_cache = {}
history_cache_lock = threading.Lock()
def history_fingerprint(data):
"""计算会话指纹:用首条消息作为会话锚点(同一对话各轮次首条消息不变,可跨轮次复用模型)"""
messages = data.get('messages', []) if isinstance(data, dict) else []
if not messages:
return None
try:
s = json.dumps(messages[0], ensure_ascii=False, sort_keys=True)
except Exception:
return None
return hashlib.md5(s.encode('utf-8')).hexdigest()
def has_history(data):
"""请求是否带历史上下文"""
messages = data.get('messages', []) if isinstance(data, dict) else []
return len(messages) >= 2
def get_cached_history_model(fp, capability=None):
"""获取历史前缀对应的上次模型(未过期且可用才返回)"""
if not fp:
return None
with history_cache_lock:
entry = history_model_cache.get(fp)
if not entry:
return None
routing = load_routing_config()
ttl = routing.get('cache_ttl_seconds', 3600)
if time.time() - entry.get('ts', 0) > ttl:
with history_cache_lock:
history_model_cache.pop(fp, None)
return None
provider = find_provider_for_model(entry.get('model', ''), capability)
if not provider:
return None
return {'provider': provider, 'model': entry['model']}
def remember_history_model(fp, provider_name, model):
"""记录某历史前缀使用的模型"""
if not fp:
return
with history_cache_lock:
history_model_cache[fp] = {'provider': provider_name, 'model': model, 'ts': time.time()}
# 提供商状态缓存
provider_status = {}
# 配置缓存时间(秒)
CONFIG_CACHE_TTL = 3
_last_config_load = 0
_cached_providers = []
_cached_aliases = {}
_cached_auto_profiles = {}
# ============ 能力/模型 工具函数 ============
def normalize_models(models, default_caps):
"""规范化模型列表(支持字符串 或 {name,capabilities} 两种格式)"""
result = []
for m in models or []:
if isinstance(m, str):
result.append({'name': m, 'capabilities': list(default_caps)})
elif isinstance(m, dict):
caps = m.get('capabilities') or list(default_caps)
result.append({'name': m.get('name', ''), 'capabilities': caps})
return [m for m in result if m['name']]
def provider_models(provider):
"""获取提供商的有效模型列表(dict"""
default_caps = provider.get('capabilities', ['text'])
return normalize_models(provider.get('models', []), default_caps)
def provider_model_names(provider):
"""提供商支持的模型名列表"""
return [m['name'] for m in provider_models(provider)]
def model_capabilities(provider, model_name):
"""获取某模型在该提供商下的能力"""
for m in provider_models(provider):
if m['name'] == model_name:
return m.get('capabilities') or provider.get('capabilities', ['text'])
return provider.get('capabilities', ['text'])
def model_has_capability(provider, model_name, capability):
"""判断模型是否具备某能力"""
if capability == 'all' or not capability:
return True
caps = model_capabilities(provider, model_name)
return capability in caps
def provider_supports_capability(provider, capability):
"""判断提供商下是否有模型具备某能力"""
if capability == 'all' or not capability:
return True
return any(model_has_capability(provider, m['name'], capability) for m in provider_models(provider))
def provider_capability_models(provider, capability):
"""返回提供商下具备某能力的所有模型名"""
return [m['name'] for m in provider_models(provider) if model_has_capability(provider, m['name'], capability)]
def find_default_model_for_capability(provider, capability):
"""找提供商下具备某能力的最佳模型(优先 default_model"""
default_model = provider.get('default_model', '')
if default_model and default_model in provider_model_names(provider) and model_has_capability(provider, default_model, capability):
return default_model
for m in provider_models(provider):
if model_has_capability(provider, m['name'], capability):
return m['name']
return None
# ============ 统计 ============
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)
# ============ 配置缓存 ============
def refresh_config():
"""动态刷新配置"""
global _last_config_load, _cached_providers, _cached_aliases, _cached_auto_profiles
current_time = time.time()
if current_time - _last_config_load > CONFIG_CACHE_TTL:
_cached_providers = get_providers()
_cached_aliases = get_model_aliases()
_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']] = {
'available': True,
'last_check': None,
'error_count': 0,
'last_error': None,
}
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 is_provider_available(provider_name):
"""判断提供商是否可用(含熔断冷却自动恢复)"""
status = provider_status.get(provider_name)
if not status:
return True
if status.get('available', True):
return True
# 熔断中:检查冷却期是否已过,过了则半开恢复一次
open_at = status.get('circuit_open_at')
if open_at:
cooldown = RETRY_CONFIG.get('cooldown_seconds', 60)
if time.time() - open_at > cooldown:
status['error_count'] = 0
status['available'] = True
status['circuit_open_at'] = None
logger.info(f"Provider {provider_name} circuit breaker recovered (cooldown passed)")
return True
return False
def mark_provider_error(provider_name, error):
"""标记提供商错误(连续3次熔断)"""
if provider_name in provider_status:
status = provider_status[provider_name]
status['error_count'] = status.get('error_count', 0) + 1
status['last_error'] = str(error)
status['last_check'] = datetime.now()
if status['error_count'] >= 3:
status['available'] = False
status['circuit_open_at'] = time.time()
logger.warning(f"Provider {provider_name} marked as unavailable (circuit open)")
def mark_provider_success(provider_name):
"""标记提供商成功(复位熔断)"""
if provider_name in provider_status:
provider_status[provider_name]['error_count'] = 0
provider_status[provider_name]['available'] = True
provider_status[provider_name]['circuit_open_at'] = None
provider_status[provider_name]['last_check'] = datetime.now()
# ============ 提供商/模型路由 ============
def resolve_model_name(model_name):
"""解析模型别名"""
refresh_config()
return _cached_aliases.get(model_name, model_name)
def is_auto_model(model_name):
"""判断是否为auto类模型(auto 或 auto-xxx 配置)"""
return model_name == 'auto' or model_name.startswith('auto-')
def sorted_providers():
"""按优先级排序的启用提供商"""
refresh_config()
return sorted(_cached_providers, key=lambda x: x['priority'])
def find_provider_for_model(model_name, capability=None, exclude=None):
"""精确查找托管某模型的提供商(保持模型名不变,仅用于切换时的同模型替换)"""
exclude = exclude or set()
for provider in sorted_providers():
if not provider['enabled']:
continue
if provider['name'] in exclude:
continue
if not is_provider_available(provider['name']):
continue
if model_name in provider_model_names(provider):
if model_has_capability(provider, model_name, capability):
return provider
return None
def get_auto_provider(profile_name='auto', capability=None, exclude=None):
"""获取auto模式下可用的提供商与模型
优先级:
1. 配置了显式有序 models 列表 → 按列表顺序逐个找能托管该模型的提供商
2. 否则按提供商能力 + 优先级选择(旧逻辑回退)
"""
refresh_config()
exclude = exclude or set()
profile = _cached_auto_profiles.get(profile_name, _cached_auto_profiles.get('auto', {}))
req_cap = profile.get('capability') or capability or 'text'
allowed_providers = profile.get('providers', ['*'])
strategy = profile.get('strategy', 'priority')
ordered_models = profile.get('models') or []
# 方式1:显式有序模型列表(粒度=具体模型,用户可拖动排序)
if ordered_models:
candidates = []
for mname in ordered_models:
provider = find_provider_for_model(mname, req_cap, exclude=exclude)
if provider:
candidates.append((provider, mname))
if candidates:
if strategy == 'random':
return random.choice(candidates)
return candidates[0]
return None, None # 显式列表全不可用时,不悄悄换成别的模型
# 方式2:按提供商能力 + 优先级
candidates = []
for provider in sorted_providers():
if not provider['enabled']:
continue
if provider['name'] in exclude:
continue
if not is_provider_available(provider['name']):
continue
if not ('*' in allowed_providers or provider.get('id') in allowed_providers or provider['name'] in allowed_providers):
continue
chosen_model = find_default_model_for_capability(provider, req_cap)
if chosen_model:
candidates.append((provider, chosen_model))
if not candidates:
return None, None
if strategy == 'random':
return random.choice(candidates)
return candidates[0]
def get_provider_for_model(model_name, capability=None):
"""根据模型名获取提供商与解析后的模型名"""
resolved_model = resolve_model_name(model_name)
if is_auto_model(resolved_model):
return get_auto_provider(resolved_model, capability)
# 精确匹配:提供商托管该模型
provider = find_provider_for_model(resolved_model, capability)
if provider:
return provider, resolved_model
# 兜底:匹配提供商 default_model
for provider in sorted_providers():
if not provider['enabled']:
continue
if not is_provider_available(provider['name']):
continue
if provider.get('default_model') == resolved_model:
if model_has_capability(provider, resolved_model, capability):
return provider, resolved_model
# 最后:模糊匹配(仅当请求模型是提供商某个模型名的子串或反向,避免误路由)
for provider in sorted_providers():
if not provider['enabled']:
continue
if not is_provider_available(provider['name']):
continue
for m in provider_model_names(provider):
if resolved_model.lower() == m.lower():
return provider, m
return None, None
def detect_capability(data):
"""根据请求内容判断所需能力(含图片则 vision,否则 text"""
messages = data.get('messages', []) if isinstance(data, dict) else []
for msg in messages:
content = msg.get('content')
if isinstance(content, list):
for part in content:
if isinstance(part, dict) and part.get('type') in ('image_url', 'image', 'input_image'):
return 'vision'
return 'text'
def is_image_gen_request(model):
"""判断请求是否为图片生成(auto-image 配置或具备 image_gen 能力的模型)"""
resolved = resolve_model_name(model)
if is_auto_model(resolved):
return _cached_auto_profiles.get(resolved, {}).get('capability') == 'image_gen'
return find_provider_for_model(resolved, 'image_gen') is not None
# ============ 上游转发 ============
def build_headers(provider, content_type='application/json', extra=None):
headers = {
"Authorization": f"Bearer {provider['api_key']}",
"Content-Type": content_type,
}
if extra:
headers.update(extra)
return headers
def proxy_chat(provider, model, request_data, stream=False):
"""转发聊天请求到上游"""
url = f"{provider['base_url'].rstrip('/')}/chat/completions"
data = request_data.copy()
data['model'] = model
try:
if stream:
return requests.post(url, headers=build_headers(provider), json=data, stream=True, timeout=provider.get('timeout', 120))
return requests.post(url, headers=build_headers(provider), json=data, timeout=provider.get('timeout', 120))
except requests.exceptions.Timeout:
mark_provider_error(provider['name'], "Timeout")
raise Exception(f"Provider {provider['name']} timeout")
except requests.exceptions.ConnectionError:
mark_provider_error(provider['name'], "Connection error")
raise Exception(f"Provider {provider['name']} connection error")
except Exception as e:
mark_provider_error(provider['name'], str(e))
raise
def proxy_json(provider, path, request_data, timeout=None):
"""通用JSON转发"""
url = f"{provider['base_url'].rstrip('/')}/{path.lstrip('/')}"
timeout = timeout or provider.get('timeout', 120)
try:
return requests.post(url, headers=build_headers(provider), json=request_data, timeout=timeout)
except requests.exceptions.Timeout:
mark_provider_error(provider['name'], "Timeout")
raise Exception(f"Provider {provider['name']} timeout")
except requests.exceptions.ConnectionError:
mark_provider_error(provider['name'], "Connection error")
raise Exception(f"Provider {provider['name']} connection error")
except Exception as e:
mark_provider_error(provider['name'], str(e))
raise
def proxy_raw(provider, path, timeout=None):
"""通用原始体转发(用于 multipart 表单,如语音识别)"""
url = f"{provider['base_url'].rstrip('/')}/{path.lstrip('/')}"
timeout = timeout or provider.get('timeout', 180)
data = request.get_data()
headers = build_headers(provider, content_type=request.headers.get('Content-Type', 'application/octet-stream'))
try:
return requests.post(url, headers=headers, data=data, timeout=timeout)
except requests.exceptions.Timeout:
mark_provider_error(provider['name'], "Timeout")
raise Exception(f"Provider {provider['name']} timeout")
except requests.exceptions.ConnectionError:
mark_provider_error(provider['name'], "Connection error")
raise Exception(f"Provider {provider['name']} connection error")
except Exception as e:
mark_provider_error(provider['name'], str(e))
raise
def stream_response(response):
"""流式响应生成器"""
try:
for line in response.iter_lines():
if line:
yield line + b'\n'
except Exception as e:
logger.error(f"Stream error: {e}")
yield b'data: {"error": "' + str(e).encode() + b'"}\n\n'
# ============ 日志 ============
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler(LOGS_DIR / 'proxy.log', encoding='utf-8'),
logging.StreamHandler()
]
)
logger = logging.getLogger(__name__)
# 初始化配置与提供商状态
refresh_config()
refresh_provider_status()
# ============ 前台 API 路由 ============
@app.route('/')
def index():
"""首页"""
return jsonify({
"name": "LLM Proxy",
"version": VERSION,
"description": "OpenAI-compatible LLM API Proxy with capability routing",
"endpoints": {
"chat": "/v1/chat/completions",
"models": "/v1/models",
"embeddings": "/v1/embeddings",
"images": "/v1/images/generations",
"audio_speech": "/v1/audio/speech",
"audio_transcriptions": "/v1/audio/transcriptions",
"video": "/v1/video/generations",
"health": "/health",
"status": "/status",
"admin": "/admin"
},
"capabilities": CAPABILITY_DEFS,
})
@app.route('/v1/models', methods=['GET'])
def list_models():
"""列出可用模型(含auto配置与能力标签)"""
refresh_config()
models_list = []
added_models = set()
for profile_name, profile in _cached_auto_profiles.items():
if profile_name not in added_models:
models_list.append({
"id": profile_name,
"object": "model",
"created": int(time.time()),
"owned_by": "proxy",
"kind": "auto",
"capability": profile.get('capability', 'text'),
"description": f"[{CAPABILITY_DEFS.get(profile.get('capability', 'text'), profile.get('capability', 'text'))}] {profile.get('description', '')}"
})
added_models.add(profile_name)
for provider in _cached_providers:
if not provider['enabled']:
continue
for m in provider_models(provider):
if m['name'] not in added_models:
models_list.append({
"id": m['name'],
"object": "model",
"created": int(time.time()),
"owned_by": provider['name'],
"kind": "model",
"capabilities": m['capabilities'],
})
added_models.add(m['name'])
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
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
capability = detect_capability(data)
# 历史上下文缓存:auto 请求始终记录会话锚点(首条消息),带历史时优先复用上次模型
history_fp = None
routing_cfg = load_routing_config()
if routing_cfg.get('prefer_cache_model', True) and is_auto_model(model):
history_fp = history_fingerprint(data)
if has_history(data):
cached = get_cached_history_model(history_fp, capability)
if cached:
provider = cached['provider']
resolved_model = cached['model']
request_provider = provider['name']
logger.info(f"History cache hit: model={model} -> provider={provider['name']}, resolved_model={resolved_model}, stream={stream}, capability={capability}")
else:
provider, resolved_model = get_provider_for_model(model, capability)
else:
provider, resolved_model = get_provider_for_model(model, capability)
else:
provider, resolved_model = get_provider_for_model(model, capability)
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} (capability: {capability})", "type": "invalid_request_error"}}), 400
request_provider = provider['name']
if not history_fp or not routing_cfg.get('prefer_cache_model', True):
logger.info(f"Request: model={model} -> provider={provider['name']}, resolved_model={resolved_model}, stream={stream}, capability={capability}")
last_error = None
tried_providers = set()
for attempt in range(RETRY_CONFIG['max_retries']):
try:
response = proxy_chat(provider, resolved_model, data, stream)
if response.status_code == 200:
mark_provider_success(provider['name'])
request_success = True
if stream:
increment_stats(model, provider['name'], success=True, tokens=0)
remember_history_model(history_fp, provider['name'], resolved_model)
return Response(
stream_with_context(stream_response(response)),
content_type='text/event-stream',
headers={'Cache-Control': 'no-cache', 'Connection': 'keep-alive'}
)
else:
result = response.json()
usage = result.get('usage', {})
request_tokens = usage.get('total_tokens', 0)
increment_stats(model, provider['name'], success=True, tokens=request_tokens)
remember_history_model(history_fp, provider['name'], resolved_model)
return jsonify(result)
else:
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.add(provider['name'])
# 切换到下一个能托管同一模型的提供商(保持模型名不变)
next_provider = find_provider_for_model(resolved_model, capability, exclude=tried_providers)
if next_provider:
logger.info(f"Switching to next provider: {next_provider['name']}")
provider = next_provider
request_provider = provider['name']
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
except Exception as e:
last_error = str(e)
logger.error(f"Attempt {attempt + 1} failed: {e}")
tried_providers.add(provider['name'])
# 非auto模型:优先切换到能托管同一模型的其他提供商
next_provider = find_provider_for_model(resolved_model, capability, exclude=tried_providers)
if next_provider:
provider = next_provider
request_provider = provider['name']
time.sleep(RETRY_CONFIG['retry_delay'])
continue
# auto模型:按auto配置切换到下一个候选(模型可随能力重选)
if is_auto_model(model):
next_provider, next_model = get_auto_provider(model, capability, exclude=tried_providers)
if next_provider:
provider = next_provider
resolved_model = next_model
request_provider = provider['name']
time.sleep(RETRY_CONFIG['retry_delay'])
continue
# 没有更多可切换提供商,立即结束(不再重试同一个失败提供商)
break
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
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
@app.route('/v1/embeddings', methods=['POST'])
def embeddings():
"""嵌入API(按模型路由,不再固定第一个提供商)"""
refresh_config()
try:
data = request.get_json()
if not data:
return jsonify({"error": "Invalid request body"}), 400
model = data.get('model')
provider = None
if model:
provider = find_provider_for_model(model)
if not provider:
# 按名字匹配(如 embedding 模型名可能不在托管列表)
for p in sorted_providers():
if p['enabled'] and is_provider_available(p['name']):
provider = p
break
else:
for p in sorted_providers():
if p['enabled'] and is_provider_available(p['name']):
provider = p
break
if not provider:
return jsonify({"error": "No available provider for embeddings"}), 503
response = proxy_json(provider, 'embeddings', data, timeout=60)
return jsonify(response.json()), response.status_code
except Exception as e:
return jsonify({"error": str(e)}), 500
def _generic_capability_endpoint(capability, path, error_msg="No available provider"):
"""通用能力端点路由"""
refresh_config()
try:
data = request.get_json() if request.is_json else {}
if not isinstance(data, dict):
data = {}
model = data.get('model', 'auto')
if is_auto_model(model):
provider, resolved_model = get_auto_provider(model, capability)
else:
resolved_model = resolve_model_name(model)
provider = find_provider_for_model(resolved_model, capability)
if not provider:
# 允许提供商默认模型具备该能力
for p in sorted_providers():
if not p['enabled'] or not is_provider_available(p['name']):
continue
if p.get('default_model') == resolved_model and model_has_capability(p, resolved_model, capability):
provider = p
break
if not provider:
return jsonify({"error": {"message": f"{error_msg} (capability: {capability})", "type": "invalid_request_error"}}), 400
data['model'] = resolved_model if resolved_model else data.get('model')
response = proxy_json(provider, path, data)
return Response(
response.content,
status=response.status_code,
content_type=response.headers.get('Content-Type', 'application/json')
)
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/v1/images/generations', methods=['POST'])
def images_generations():
"""图片生成APIOpenAI兼容)"""
return _generic_capability_endpoint('image_gen', 'images/generations', "No image generation provider available")
@app.route('/v1/audio/speech', methods=['POST'])
def audio_speech():
"""语音合成APITTSOpenAI兼容)"""
return _generic_capability_endpoint('audio_out', 'audio/speech', "No TTS provider available")
@app.route('/v1/audio/transcriptions', methods=['POST'])
def audio_transcriptions():
"""语音识别APIASROpenAI兼容,multipart转发)"""
refresh_config()
try:
model = request.form.get('model', 'auto')
if is_auto_model(model):
provider, resolved_model = get_auto_provider(model, 'audio_in')
else:
resolved_model = resolve_model_name(model)
provider = find_provider_for_model(resolved_model, 'audio_in')
if not provider:
for p in sorted_providers():
if not p['enabled'] or not is_provider_available(p['name']):
continue
if p.get('default_model') == resolved_model and model_has_capability(p, resolved_model, 'audio_in'):
provider = p
break
if not provider:
return jsonify({"error": {"message": "No ASR provider available (capability: audio_in)", "type": "invalid_request_error"}}), 400
response = proxy_raw(provider, 'audio/transcriptions')
return Response(
response.content,
status=response.status_code,
content_type=response.headers.get('Content-Type', 'application/json')
)
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/v1/video/generations', methods=['POST'])
def video_generations():
"""视频生成API"""
return _generic_capability_endpoint('video_gen', 'video/generations', "No video generation provider available")
@app.route('/health', methods=['GET'])
def health():
"""健康检查"""
available_count = sum(1 for name, s in provider_status.items() if is_provider_available(name))
total_count = len(provider_status)
return jsonify({
"status": "healthy" if available_count > 0 else "degraded",
"providers": {"available": available_count, "total": total_count},
"timestamp": datetime.now().isoformat()
})
@app.route('/status', methods=['GET'])
def status():
"""详细状态"""
refresh_config()
providers_detail = []
for provider in _cached_providers:
status_info = provider_status.get(provider['name'], {})
providers_detail.append({
"name": provider['name'],
"priority": provider['priority'],
"enabled": provider['enabled'],
"available": is_provider_available(provider['name']),
"error_count": status_info.get('error_count', 0),
"last_error": status_info.get('last_error'),
"capabilities": provider.get('capabilities', ['text']),
"models": [{'name': m['name'], 'capabilities': m['capabilities']} for m in provider_models(provider)],
})
return jsonify({
"version": VERSION,
"uptime": time.time(),
"providers": providers_detail,
"model_aliases": _cached_aliases,
"auto_profiles": _cached_auto_profiles,
})
# 兼容旧版端点
@app.route('/v1/engines', methods=['GET'])
def list_engines():
return list_models()
@app.route('/v1/engines/<model>/completions', methods=['POST'])
def engine_completions(model):
data = request.get_json()
if data is None:
data = {}
if not isinstance(data, dict):
return jsonify({"error": {"message": "Invalid request body", "type": "invalid_request_error"}}), 400
data['model'] = model
return chat_completions()
# ============ 后台管理页面路由 ============
@app.route('/admin')
def admin_index():
return render_template('index.html')
@app.route('/admin/providers')
def admin_providers_page():
return render_template('providers.html')
@app.route('/admin/models')
def admin_models_page():
return render_template('models.html')
@app.route('/admin/logs')
def admin_logs_page():
return render_template('logs.html')
@app.route('/admin/config')
def admin_config_page():
return render_template('config.html')
@app.route('/admin/chat')
def admin_chat_page():
return render_template('chat.html')
@app.route('/admin/auto-profiles')
def admin_auto_profiles_page():
return render_template('auto-profiles.html')
# ============ 后台管理 API:能力 ============
@app.route('/api/admin/capabilities')
def api_admin_capabilities():
"""获取能力定义"""
return jsonify(CAPABILITY_DEFS)
# ============ 后台管理 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 is_provider_available(p['name']))
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),
'requests_today': stats.get('requests_today', 0),
'providers_count': len(providers),
'available_providers': available_count,
'models_count': sum(len(provider_model_names(p)) for p in providers),
'uptime': time.time(),
})
# ============ 后台管理 API:提供商 ============
def _provider_public(p):
"""提供商对外数据结构(含能力与模型详情)"""
return {
'id': p.get('id', p['name'].lower().replace(' ', '-')),
'name': p['name'],
'priority': p['priority'],
'enabled': p.get('enabled', True),
'available': is_provider_available(p['name']),
'base_url': p['base_url'],
'api_key': p['api_key'],
'capabilities': p.get('capabilities', ['text']),
'models': [{'name': m['name'], 'capabilities': m['capabilities']} for m in provider_models(p)],
'default_model': p.get('default_model', ''),
'timeout': p.get('timeout', 120),
'request_count': 0,
'success_count': 0,
'error_count': 0,
'last_error': None,
'last_check': None,
}
@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'], {})
item = _provider_public(provider)
item.update({
'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'),
})
providers_data.append(item)
return jsonify(providers_data)
@app.route('/api/admin/providers/<provider_id>', 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'], {})
item = _provider_public(provider)
item['status'] = {
'available': is_provider_available(provider['name']),
'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),
}
return jsonify(item)
def _sanitize_provider_input(data):
"""清洗提供商输入"""
new_provider = {}
if 'id' in data:
new_provider['id'] = data['id']
if 'name' in data:
new_provider['name'] = data['name'].strip()
if 'priority' in data:
try:
new_provider['priority'] = int(data['priority'])
except:
pass
if 'base_url' in data:
new_provider['base_url'] = data['base_url'].rstrip('/')
if 'api_key' in data:
new_provider['api_key'] = data['api_key']
if 'timeout' in data:
try:
new_provider['timeout'] = int(data['timeout'])
except:
pass
if 'enabled' in data:
new_provider['enabled'] = bool(data['enabled'])
if 'capabilities' in data:
caps = data['capabilities']
if isinstance(caps, str):
caps = [c.strip() for c in caps.split(',') if c.strip()]
new_provider['capabilities'] = list(caps)
if 'models' in data:
models = data['models']
if isinstance(models, str):
models = [m.strip() for m in models.split(',') if m.strip()]
# 统一为 dict 形式,新模型默认继承提供商能力
default_caps = new_provider.get('capabilities') or data.get('capabilities', ['text'])
norm = []
for m in models:
if isinstance(m, dict):
norm.append({'name': m['name'], 'capabilities': m.get('capabilities', list(default_caps))})
else:
norm.append({'name': m, 'capabilities': list(default_caps)})
new_provider['models'] = norm
if 'default_model' in data and data['default_model']:
new_provider['default_model'] = data['default_model']
return new_provider
@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 = _sanitize_provider_input(data)
new_provider.setdefault('id', data['name'].lower().replace(' ', '-').replace('.', '-'))
new_provider.setdefault('priority', max_priority + 1)
new_provider.setdefault('enabled', True)
new_provider.setdefault('capabilities', ['text'])
if not new_provider.get('default_model'):
names = [m['name'] for m in new_provider.get('models', [])]
new_provider['default_model'] = names[0] if names else ''
new_provider['priority'] = data.get('priority', max_priority + 1)
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': _provider_public(result)})
@app.route('/api/admin/providers/<provider_id>', methods=['PUT'])
def api_admin_update_provider(provider_id):
"""更新提供商"""
data = request.get_json()
if not data:
return jsonify({'error': 'Invalid request body'}), 400
result = update_provider(provider_id, _sanitize_provider_input(data))
if not result:
return jsonify({'error': 'Provider not found'}), 404
return jsonify({'success': True, 'provider': _provider_public(result)})
@app.route('/api/admin/providers/<provider_id>', methods=['DELETE'])
def api_admin_delete_provider(provider_id):
"""删除提供商"""
provider = get_provider(provider_id)
if not provider:
return jsonify({'error': 'Provider not found'}), 404
result = delete_provider(provider_id)
if provider['name'] in provider_status:
del provider_status[provider['name']]
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/<provider_id>/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/<provider_id>/test', methods=['POST'])
def api_admin_test_provider(provider_id):
"""测试提供商连接(优先 /models,失败则用聊天探测,适配 new-api 等无 /models 的服务)"""
provider = get_provider(provider_id)
if not provider:
return jsonify({'success': False, 'error': 'Provider not found'}), 404
# 方式1: GET /models
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)})
except Exception as e:
pass # 继续尝试聊天探测
# 方式2: 能力探测(用 default_model 发一条最小请求,按能力选择探测端点)
try:
default_model = provider.get('default_model') or ''
probe_model = default_model if default_model in provider_model_names(provider) else (provider_model_names(provider) or [''])[0]
if probe_model:
caps = model_capabilities(provider, probe_model)
if 'image_gen' in caps:
# 生图提供商:探测 /images/generations
url = f"{provider['base_url'].rstrip('/')}/images/generations"
headers = {"Authorization": f"Bearer {provider['api_key']}", "Content-Type": "application/json"}
payload = {"model": probe_model, "prompt": "a tiny red dot", "n": 1}
probe_name = 'image generation'
else:
url = f"{provider['base_url'].rstrip('/')}/chat/completions"
headers = {"Authorization": f"Bearer {provider['api_key']}", "Content-Type": "application/json"}
payload = {"model": probe_model, "messages": [{"role": "user", "content": "hi"}], "max_tokens": 1, "stream": False}
probe_name = 'chat'
resp = requests.post(url, headers=headers, json=payload, timeout=20)
if resp.status_code in (200, 201):
provider_status[provider['name']] = {
'available': True,
'last_check': datetime.now().isoformat(),
'error_count': 0,
'last_error': None,
}
return jsonify({'success': True, 'message': f'Connection successful ({probe_name} probe, model={probe_model})', 'models_count': None})
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 {resp.status_code}',
}
return jsonify({'success': False, 'error': f'HTTP {resp.status_code}: {resp.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)})
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': 'Connection failed',
}
return jsonify({'success': False, 'error': 'Connection failed'})
# ============ 后台管理 API:模型管理 ============
@app.route('/api/admin/models')
def api_admin_models():
"""获取所有模型(按提供商分组,含能力)"""
providers = get_providers()
aliases = get_model_aliases()
result = []
for provider in sorted(providers, key=lambda x: x['priority']):
default_model = provider.get('default_model', '')
model_aliases = {}
for alias, target in aliases.items():
if alias == 'auto' or alias.startswith('auto-'):
continue
if target in provider_model_names(provider):
model_aliases.setdefault(target, []).append(alias)
for m in provider_models(provider):
result.append({
'name': m['name'],
'capabilities': m['capabilities'],
'provider_id': provider.get('id'),
'provider_name': provider['name'],
'provider_priority': provider['priority'],
'provider_enabled': provider.get('enabled', True),
'is_default': m['name'] == default_model,
'aliases': model_aliases.get(m['name'], []),
})
return jsonify(result)
@app.route('/api/admin/models', methods=['POST'])
def api_admin_add_model():
"""向提供商添加模型"""
data = request.get_json()
if not data:
return jsonify({'error': 'Invalid request body'}), 400
provider_id = data.get('provider_id')
name = (data.get('name') or '').strip()
if not provider_id or not name:
return jsonify({'error': 'provider_id and name are required'}), 400
provider = get_provider(provider_id)
if not provider:
return jsonify({'error': 'Provider not found'}), 404
caps = data.get('capabilities') or provider.get('capabilities', ['text'])
if isinstance(caps, str):
caps = [c.strip() for c in caps.split(',') if c.strip()]
if name in provider_model_names(provider):
return jsonify({'error': 'Model already exists in this provider'}), 400
models = provider.get('models', [])
models.append({'name': name, 'capabilities': list(caps)})
result = update_provider(provider_id, {'models': models})
return jsonify({'success': True, 'model': {'name': name, 'capabilities': list(caps)}})
@app.route('/api/admin/models/<provider_id>', methods=['PUT'])
def api_admin_update_model(provider_id):
"""更新模型(能力、名称)——模型名放 body,避免路径斜杠问题"""
data = request.get_json()
if not data:
return jsonify({'error': 'Invalid request body'}), 400
old_name = data.get('name') or data.get('old_name')
if not old_name:
return jsonify({'error': 'Model name is required'}), 400
provider = get_provider(provider_id)
if not provider:
return jsonify({'error': 'Provider not found'}), 404
models = provider.get('models', [])
found = False
for m in models:
mname = m['name'] if isinstance(m, dict) else m
if mname == old_name:
found = True
if 'new_name' in data and data['new_name']:
m['name'] = data['new_name']
if 'capabilities' in data:
caps = data['capabilities']
if isinstance(caps, str):
caps = [c.strip() for c in caps.split(',') if c.strip()]
m['capabilities'] = list(caps)
break
if not found:
return jsonify({'error': 'Model not found'}), 404
# 同步 default_model
if provider.get('default_model') == old_name and 'new_name' in data and data['new_name']:
provider['default_model'] = data['new_name']
result = update_provider(provider_id, {'models': models, 'default_model': provider.get('default_model', '')})
return jsonify({'success': True, 'provider': _provider_public(result)})
@app.route('/api/admin/models/<provider_id>', methods=['DELETE'])
def api_admin_delete_model(provider_id):
"""删除模型——模型名放 query,避免路径斜杠问题"""
name = request.args.get('name', '')
if not name:
return jsonify({'error': 'Model name is required (query param name=)'}), 400
provider = get_provider(provider_id)
if not provider:
return jsonify({'error': 'Provider not found'}), 404
models = [m for m in provider.get('models', []) if (m['name'] if isinstance(m, dict) else m) != name]
update_data = {'models': models}
if provider.get('default_model') == name:
names = [m['name'] for m in provider_models({'models': models, 'capabilities': provider.get('capabilities', ['text'])})]
update_data['default_model'] = names[0] if names else ''
update_provider(provider_id, update_data)
return jsonify({'success': True})
@app.route('/api/admin/models/<provider_id>/default', methods=['POST'])
def api_admin_set_default_model(provider_id):
"""设置默认模型"""
data = request.get_json() or {}
name = data.get('name', '')
if not name:
return jsonify({'error': 'Model name is required'}), 400
provider = get_provider(provider_id)
if not provider:
return jsonify({'error': 'Provider not found'}), 404
if name not in provider_model_names(provider):
return jsonify({'error': 'Model not found in provider'}), 404
result = update_provider(provider_id, {'default_model': name})
return jsonify({'success': True, 'default_model': name})
# ============ 后台管理 API:模型别名 ============
@app.route('/api/admin/aliases')
def api_admin_aliases():
"""获取模型别名"""
return jsonify(get_model_aliases())
@app.route('/api/admin/aliases', methods=['POST'])
def api_admin_add_alias():
"""添加/更新模型别名"""
data = request.get_json()
if not data or not data.get('alias') or not data.get('target'):
return jsonify({'error': 'alias and target are required'}), 400
alias = data['alias'].strip()
target = data['target'].strip()
if alias != 'auto' and alias.startswith('auto-'):
return jsonify({'error': 'auto- 前缀保留给Auto配置'}), 400
result = update_model_alias(alias, target)
return jsonify({'success': True, 'aliases': result})
@app.route('/api/admin/aliases/<alias>', methods=['DELETE'])
def api_admin_delete_alias(alias):
"""删除模型别名"""
result = delete_model_alias(alias)
if not result:
return jsonify({'error': 'Cannot delete alias or alias not found'}), 400
return jsonify({'success': True})
# ============ 后台管理 APIAuto配置 ============
def _auto_profile_public(name, profile):
"""Auto配置对外结构(含能力、有序模型列表与可挑选模型详情)"""
providers = get_providers()
allowed_providers = profile.get('providers', ['*'])
capability = profile.get('capability', 'text')
ordered_models = profile.get('models') or []
# 收集所有具备该能力、且在候选提供商范围内的模型(供编辑器挑选)
matched = []
seen = set()
for p in sorted(providers, key=lambda x: x['priority']):
if not ('*' in allowed_providers or p.get('id') in allowed_providers or p['name'] in allowed_providers):
continue
for m in provider_models(p):
if model_has_capability(p, m['name'], capability) and m['name'] not in seen:
seen.add(m['name'])
matched.append({
'name': m['name'],
'provider_id': p.get('id'),
'provider_name': p['name'],
'provider_priority': p['priority'],
'capabilities': m['capabilities'],
})
# 已保存的有序模型排前面,其余可挑选模型按提供商优先级追加
by_name = {x['name']: x for x in matched}
sorted_matched = []
for mname in ordered_models:
if mname in by_name:
sorted_matched.append(by_name[mname])
for x in matched:
if x['name'] not in ordered_models:
sorted_matched.append(x)
return {
'name': name,
'display_name': profile.get('name', name),
'description': profile.get('description', ''),
'capability': capability,
'capability_label': CAPABILITY_DEFS.get(capability, capability),
'strategy': profile.get('strategy', 'priority'),
'providers': allowed_providers,
'models': ordered_models, # 有序的具体模型列表(优先级顺序)
'matched_models': [x['name'] for x in matched],
'matched_models_detail': sorted_matched, # 每个模型的提供商详情(编辑器用)
}
@app.route('/api/admin/auto-profiles')
def api_admin_auto_profiles():
"""获取所有Auto配置"""
profiles = get_auto_profiles()
result = [_auto_profile_public(name, profile) for name, profile in profiles.items()]
return jsonify(result)
@app.route('/api/admin/auto-profiles/<profile_name>', 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
return jsonify(_auto_profile_public(profile_name, profile))
@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
capability = data.get('capability', 'text')
if capability not in CAPABILITY_DEFS:
return jsonify({'error': f'Invalid capability: {capability}. Valid: {list(CAPABILITY_DEFS.keys())}'}), 400
profile_data = {
'name': data.get('display_name', data['name']),
'description': data.get('description', ''),
'capability': capability,
'models': data.get('models', []), # 有序的具体模型列表(优先级顺序)
'providers': data.get('providers', ['*']),
'strategy': data.get('strategy', 'priority'),
}
result = add_auto_profile(profile_name, profile_data)
return jsonify({'success': True, 'profile': _auto_profile_public(profile_name, profile_data)})
@app.route('/api/admin/auto-profiles/<profile_name>', 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 'capability' in data:
if data['capability'] not in CAPABILITY_DEFS:
return jsonify({'error': f'Invalid capability: {data["capability"]}'}), 400
profile_data['capability'] = data['capability']
if 'models' in data:
profile_data['models'] = data['models']
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': _auto_profile_public(profile_name, result)})
@app.route('/api/admin/auto-profiles/<profile_name>', 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})
# ============ 后台管理 API:日志/配置 ============
@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')[-200:]
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'],
'capabilities': p.get('capabilities', ['text']),
'models': [{'name': m['name'], 'capabilities': m['capabilities']} for m in provider_models(p)],
'default_model': p.get('default_model', ''),
'timeout': p.get('timeout', 120),
'enabled': p['enabled'],
} for p in providers],
'model_aliases': aliases,
'auto_profiles': get_auto_profiles(),
'retry_config': RETRY_CONFIG,
'routing_config': load_routing_config(),
'capabilities': CAPABILITY_DEFS,
'server_config': {'port': SERVER_CONFIG['port']}
})
@app.route('/api/admin/routing', methods=['GET'])
def api_admin_routing_get():
"""获取路由缓存配置"""
return jsonify(load_routing_config())
@app.route('/api/admin/routing', methods=['PUT'])
def api_admin_routing_put():
"""更新路由缓存配置"""
data = request.get_json()
if not data:
return jsonify({'error': 'Invalid request body'}), 400
update = {}
if 'prefer_cache_model' in data:
update['prefer_cache_model'] = bool(data['prefer_cache_model'])
if 'cache_ttl_seconds' in data:
try:
update['cache_ttl_seconds'] = max(0, int(data['cache_ttl_seconds']))
except:
pass
if not update:
return jsonify({'error': 'No valid fields'}), 400
result = save_routing_config(update)
return jsonify({'success': True, 'routing_config': result})
# ============ 后台管理 API:对话 ============
def load_chats():
"""加载对话数据"""
if CHATS_FILE.exists():
try:
return json.loads(CHATS_FILE.read_text(encoding='utf-8'))
except:
pass
return {'chats': []}
def save_chats(data):
"""保存对话数据"""
CHATS_FILE.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding='utf-8')
@app.route('/api/admin/chat/models')
def api_admin_chat_models():
"""获取可用模型列表(含auto配置,标注能力)"""
providers = get_providers()
profiles = get_auto_profiles()
models = []
added = set()
for name, profile in profiles.items():
if name not in added:
cap = profile.get('capability', 'text')
models.append({'id': name, 'description': f"[{CAPABILITY_DEFS.get(cap, cap)}] {profile.get('description', 'Auto-select')}"})
added.add(name)
for provider in providers:
if not provider['enabled']:
continue
for m in provider_models(provider):
if m['name'] not in added:
caps = ','.join(CAPABILITY_DEFS.get(c, c) for c in m['capabilities'])
models.append({'id': m['name'], 'description': f"{provider['name']} ({caps})"})
added.add(m['name'])
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/<chat_id>')
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
with chats_lock:
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()
})
save_chats(data)
try:
proxy_url = f"http://localhost:{SERVER_CONFIG['port']}/v1/chat/completions"
# 图片生成模型(auto-image / Qwen-Image)→ 走生图接口
if is_image_gen_request(model):
img_resp = requests.post(f"http://localhost:{SERVER_CONFIG['port']}/v1/images/generations", json={
'model': model,
'prompt': user_message,
'n': 1,
}, timeout=180)
if img_resp.status_code == 200:
img_result = img_resp.json()
img_url = None
img_b64 = None
if img_result.get('data') and len(img_result['data']) > 0:
img_url = img_result['data'][0].get('url')
img_b64 = img_result['data'][0].get('b64_json')
used_model = img_result.get('model', model)
with chats_lock:
data = load_chats()
for c in data['chats']:
if c['id'] == chat_id:
c['messages'].append({
'role': 'assistant',
'content': '🖼️ 图片生成成功',
'image_url': img_url,
'image_b64': img_b64,
'model': used_model,
'time': datetime.now().isoformat()
})
if len(c['messages']) == 2:
c['title'] = user_message[:30] + ('...' if len(user_message) > 30 else '')
c['updated_at'] = datetime.now().isoformat()
break
save_chats(data)
return jsonify({
'success': True,
'chat_id': chat_id,
'response': '🖼️ 图片生成成功',
'model': used_model,
'image_url': img_url,
'image_b64': img_b64,
'title': chat.get('title', '新对话')
})
else:
try:
error_msg = img_resp.json().get('error', {}).get('message', 'Unknown error')
except:
error_msg = f'HTTP {img_resp.status_code}'
return jsonify({'error': error_msg}), img_resp.status_code
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=180)
if response.status_code == 200:
result = response.json()
assistant_message = result['choices'][0]['message']['content']
used_model = result.get('model', model)
with chats_lock:
data = load_chats()
for c in data['chats']:
if c['id'] == chat_id:
c['messages'].append({
'role': 'assistant',
'content': assistant_message,
'model': used_model,
'time': datetime.now().isoformat()
})
if len(c['messages']) == 2:
c['title'] = user_message[:30] + ('...' if len(user_message) > 30 else '')
c['updated_at'] = datetime.now().isoformat()
break
save_chats(data)
return jsonify({
'success': True,
'chat_id': chat_id,
'response': assistant_message,
'model': used_model,
'title': chat.get('title', '新对话')
})
else:
try:
error_msg = response.json().get('error', {}).get('message', 'Unknown error')
except:
error_msg = f'HTTP {response.status_code}'
return jsonify({'error': error_msg}), response.status_code
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/api/admin/chat/<chat_id>', methods=['DELETE'])
def api_admin_delete_chat(chat_id):
"""删除对话"""
with chats_lock:
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/<chat_id>/clear', methods=['POST'])
def api_admin_clear_chat(chat_id):
"""清空对话消息"""
with chats_lock:
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(f"大模型API中转系统 v{VERSION}")
print("=" * 60)
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']):
caps = ','.join(CAPABILITY_DEFS.get(c, c) for c in p.get('capabilities', ['text']))
print(f" [{p['priority']}] {p['name']}: {p['base_url']} ({caps})")
for m in provider_models(p):
mcaps = ','.join(CAPABILITY_DEFS.get(c, c) for c in m['capabilities'])
marker = " [默认]" if m['name'] == p.get('default_model') else ""
print(f" - {m['name']} ({mcaps}){marker}")
print("=" * 60)
app.run(
host=SERVER_CONFIG['host'],
port=SERVER_CONFIG['port'],
debug=SERVER_CONFIG['debug'],
threaded=True
)