pipeline-llm/scripts/migrate_legacy_llm.py

219 lines
9.5 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.

#!/usr/bin/env python3
"""pipeline-llm 初始化迁移:旧 `llm` 表 → 模型治理新表(幂等,可重跑)。
背景2026-09-04产线平台模型调用统一收敛到模型治理模块推理 API
旧 llm 表停用。本脚本把存量模型迁到新表并给每个机构配好容错策略,
切换后立即可用(「初始化设置一个可用的模型」)。
迁移规则:
1. 旧行按 (provider, api_base) 归并为供应商llm_vendor端点目录一条
2. 同供应商下按 api_key 明文归并为账号llm_accountAES 重加密)
—— RC4(password_key) 解密旧 key → AES(password_key) 加密入新表
3. 每旧行一个模型llm_modelname 保持原名,存量引用不漂移;
capabilities 'text''t2t',其余照能力类型标准)
4. 给 '0' + users 表全部机构配组织容错策略:主模型 = 默认 t2t
(优先 qwen3.8-max否则首个 t2t备链 = 其余 t2t 模型
5. 定价初始 0未定价模型调用不冻结额度接入定价后再配
用法(应用根目录):
py3/bin/python pkgs/pipeline-llm/scripts/migrate_legacy_llm.py [--dry-run]
"""
import asyncio
import json
import os
import sys
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
# 应用根:本脚本位于 <app>/pkgs/pipeline-llm/scripts/,上溯三级
APP_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(SCRIPT_DIR)))
sys.path.insert(0, os.path.join(APP_ROOT, 'py3', 'lib', 'python3.10', 'site-packages'))
sys.path.insert(0, APP_ROOT)
from sqlor.dbpools import DBPools
from appPublic.jsonConfig import getConfig
from appPublic.folderUtils import ProgramPath
from appPublic.uniqueID import getID
CAP_MAP = {'text': 't2t', '': 't2t'} # 旧 capabilities → 能力类型标准
def _decrypt_legacy(enc, password_key):
"""旧表 api_key 是 RC4(password_key) 加密。解不出原样返回。"""
if not enc:
return ''
try:
from appPublic.rc4 import unpassword
return unpassword(enc, password_key)
except Exception:
return enc
def _encrypt_new(plain, password_key):
"""新表 api_key 用 AES 单层加密(与模块 gateway.encrypt_api_key 一致)。"""
if not plain:
return ''
from appPublic.aes import aes_encode_b64
return aes_encode_b64(password_key, plain)
async def _get_or_create_vendor(sor, provider, base_url, dry):
name = (provider or '').strip() or '未知供应商'
recs = await sor.sqlExe(
"SELECT id, endpoints FROM llm_vendor WHERE name=${n}$ AND status='active' LIMIT 1",
{"n": name})
await sor.sqlExe("COMMIT", {})
if recs:
vid = getattr(recs[0], 'id', '')
# 端点目录缺此 base_url 则补
eps = []
try:
eps = json.loads(getattr(recs[0], 'endpoints', '') or '[]') or []
except Exception:
eps = []
if not any((e.get('base_url') or '') == base_url for e in eps):
eps.append({'base_url': base_url, 'region': 'domestic', 'timeout': 60})
if not dry:
await sor.sqlExe(
"UPDATE llm_vendor SET endpoints=${e}$ WHERE id=${i}$",
{"e": json.dumps(eps, ensure_ascii=False), "i": vid})
await sor.sqlExe("COMMIT", {})
idx = next(i for i, e in enumerate(eps) if (e.get('base_url') or '') == base_url)
return vid, idx
vid = getID()
eps = [{'base_url': base_url, 'region': 'domestic', 'timeout': 60}]
if not dry:
await sor.C('llm_vendor', {
'id': vid, 'name': name,
'endpoints': json.dumps(eps, ensure_ascii=False),
'description': '由旧 llm 表迁移生成', 'status': 'active', 'org_id': '0',
})
await sor.sqlExe("COMMIT", {})
return vid, 0
async def _get_or_create_account(sor, vendor_id, ep_idx, key_plain, password_key, dry):
"""同供应商同 key 复用账号(余额/用量延续)。按解密后明文比对。"""
recs = await sor.sqlExe(
"SELECT id, api_key FROM llm_account WHERE vendor_id=${v}$", {"v": vendor_id})
await sor.sqlExe("COMMIT", {})
for r in (recs or []):
try:
from appPublic.aes import aes_decode_b64
if aes_decode_b64(password_key, getattr(r, 'api_key', '') or '') == key_plain:
return getattr(r, 'id', '')
except Exception:
continue
aid = getID()
if not dry:
await sor.C('llm_account', {
'id': aid, 'vendor_id': vendor_id,
'name': '迁移账号-%s' % (key_plain[:6] if key_plain else 'nokey'),
'api_key': _encrypt_new(key_plain, password_key),
'endpoint_ids': json.dumps([ep_idx]),
'balance': 0, 'total_recharge': 0, 'status': 'active', 'org_id': '0',
})
await sor.sqlExe("COMMIT", {})
return aid
async def main(dry=False):
config = getConfig(APP_ROOT, NS={'workdir': APP_ROOT, 'ProgramPath': ProgramPath()})
DBPools(config.databases)
password_key = config.password_key or 'QRIVSRHrthhwyjy176556332'
async with DBPools().sqlorContext('pipeline') as sor:
recs = await sor.sqlExe(
"SELECT id, name, provider, model_id, api_base, api_key, capabilities, org_id "
"FROM llm WHERE status='active' ORDER BY name", {})
await sor.sqlExe("COMMIT", {})
if not recs:
print("旧 llm 表无 active 模型,无需迁移")
return
print("旧表 active 模型 %d 个,开始迁移(%s..." % (len(recs), 'dry-run' if dry else '实写'))
created = {'vendor': 0, 'account': 0, 'model': 0, 'policy': 0}
t2t_model_ids = [] # (id, name)
for r in recs:
name = getattr(r, 'name', '') or ''
api_base = (getattr(r, 'api_base', '') or '').rstrip('/')
cap = CAP_MAP.get((getattr(r, 'capabilities', '') or '').strip().lower(),
(getattr(r, 'capabilities', '') or '').strip().lower() or 't2t')
key_plain = _decrypt_legacy(getattr(r, 'api_key', '') or '', password_key)
if key_plain and key_plain == getattr(r, 'api_key', ''):
print("%s: api_key 解密结果与密文相同(可能已是明文或解密失败),原样迁移" % name)
vid, ep_idx = await _get_or_create_vendor(sor, getattr(r, 'provider', ''), api_base, dry)
await _get_or_create_account(sor, vid, ep_idx, key_plain, password_key, dry)
exist = await sor.sqlExe("SELECT id FROM llm_model WHERE name=${n}$", {"n": name})
await sor.sqlExe("COMMIT", {})
if exist:
mid = getattr(exist[0], 'id', '')
print(" = 模型已存在跳过: %s" % name)
else:
mid = getID()
if not dry:
await sor.C('llm_model', {
'id': mid, 'vendor_id': vid,
'name': name,
'vendor_model_id': getattr(r, 'model_id', '') or name,
'capability': cap, 'sync_mode': 'sync',
'profile_id': '', 'query_profile_ids': '',
'ppid': '',
'default_params': '', 'status': 'active',
'description': '由旧 llm 表迁移生成', 'org_id': '0',
})
await sor.sqlExe("COMMIT", {})
created['model'] += 1
print(" + 模型: %s (cap=%s vendor=%s)" % (name, cap, getattr(r, 'provider', '')))
if cap == 't2t':
t2t_model_ids.append((mid, name))
if not t2t_model_ids:
print("⚠ 无 t2t 模型,跳过策略配置")
return
# 默认主模型:优先 max 档(能力最强),否则名字序第一个
primary = None
for mid, name in t2t_model_ids:
if 'max' in name.lower():
primary = (mid, name)
break
primary = primary or t2t_model_ids[0]
backups = [mid for mid, name in t2t_model_ids if mid != primary[0]]
print("策略:主模型=%s,备链=%d" % (primary[1], len(backups)))
# 给系统级 + 全部机构配策略(策略即开关:配了才走治理)
orgs = ['0']
urecs = await sor.sqlExe("SELECT DISTINCT orgid FROM users WHERE orgid IS NOT NULL", {})
await sor.sqlExe("COMMIT", {})
for u in (urecs or []):
o = getattr(u, 'orgid', '') or ''
if o and o not in orgs:
orgs.append(o)
for org in orgs:
prec = await sor.sqlExe(
"SELECT id FROM llm_org_policy WHERE org_id=${o}$", {"o": org})
await sor.sqlExe("COMMIT", {})
if prec:
print(" = 策略已存在跳过: org=%s" % org)
continue
if not dry:
await sor.C('llm_org_policy', {
'id': getID(), 'org_id': org,
'primary_model_id': primary[0],
'backup_model_ids': json.dumps(backups, ensure_ascii=False),
'endpoint_pref': 'any', 'status': 'active',
})
await sor.sqlExe("COMMIT", {})
created['policy'] += 1
print(" + 策略: org=%s" % org)
print("\n迁移完成:%s" % json.dumps(created, ensure_ascii=False))
print("注意governance_enabled 有进程内缓存,部署迁移后须重启服务生效。")
if __name__ == '__main__':
dry = '--dry-run' in sys.argv
asyncio.run(main(dry))