From d2ffd9c6d013947a69bc0018b22b9a4b5b4c843b Mon Sep 17 00:00:00 2001 From: yumoqing Date: Wed, 20 May 2026 15:12:57 +0800 Subject: [PATCH] refactor: introduce llm_api_map table and remove uapiset intermediate layer - New llm_api_map table: extract ability-specific fields (apiname, query_apiname, query_period, ppid) from llm table to support one-model-multi-ability without redundancy - Remove uapiset from llmage JOIN chain: upapp.apisetid now directly joins uapi.apisetid - Updated BufferedLLMs.get_llm() to JOIN llm_api_map for query_apiname/query_period/ppid fields - Updated llmcheck.dspy and list_paging_catelog_llms.dspy to remove uapiset references - Added migration script to generate llm_api_map INSERTs from existing llm data --- README.md | 2 +- json/llm_api_map.json | 17 +++ llmage/utils.py | 26 ++-- scripts/migrate_llm_api_map.py | 165 ++++++++++++++++++++++++++ wwwroot/list_paging_catelog_llms.dspy | 3 +- wwwroot/llmcheck.dspy | 40 ++----- 6 files changed, 209 insertions(+), 44 deletions(-) create mode 100644 json/llm_api_map.json create mode 100644 scripts/migrate_llm_api_map.py diff --git a/README.md b/README.md index 217bd45..cba1574 100644 --- a/README.md +++ b/README.md @@ -350,7 +350,7 @@ llmage ## 开发注意事项 1. **llm.stream 字段**:控制推理模式 — `'async'` 为异步任务、`False` 为同步、`True` 为流式 -2. **llm 表关联链**:llm → upapp → uapiset → uapi + uapiio,新增模型需在 uapi 模块中先配置好 API 定义 +2. **llm 表关联链**:llm → upapp → uapi + uapiio,新增模型需在 uapi 模块中先配置好 API 定义。模型能力字段(apiname, query_apiname, query_period, ppid)已拆分到 llm_api_map 表。 3. **input_fields**:模型的输入字段定义存储在 uapiio 表中,BufferedLLMs 加载时自动关联 4. **计费开关**:目前联机不调账(代码中已注释),所有 amount/cost 为 0,由后台任务统一处理 5. **异步任务 query_apiname**:支持多个 API 名称逗号分隔,逐个轮询直到状态变为 SUCCEEDED/FAILED diff --git a/json/llm_api_map.json b/json/llm_api_map.json new file mode 100644 index 0000000..a608dba --- /dev/null +++ b/json/llm_api_map.json @@ -0,0 +1,17 @@ +{ + "summary": [{"name": "llm_api_map", "title": "模型能力映射表", "primary": ["id"], "catelog": "relation"}], + "fields": [ + {"name": "id", "title": "主键", "type": "str", "length": 21, "nullable": "no"}, + {"name": "llmid", "title": "模型ID", "type": "str", "length": 21, "nullable": "no"}, + {"name": "llmcatelogid", "title": "类目ID", "type": "str", "length": 32, "nullable": "no"}, + {"name": "apiname", "title": "API接口名", "type": "str", "length": 100, "nullable": "no"}, + {"name": "query_apiname", "title": "结果查询API名", "type": "str", "length": 100}, + {"name": "query_period", "title": "查询间隔(秒)", "type": "int"}, + {"name": "ppid", "title": "定价项目ID", "type": "str", "length": 21} + ], + "indexes": [ + {"name": "idx_llm_api_llm", "idxtype": "index", "idxfields": ["llmid"]}, + {"name": "idx_llm_api_catelog", "idxtype": "unique", "idxfields": ["llmid", "llmcatelogid"]} + ], + "codes": [] +} diff --git a/llmage/utils.py b/llmage/utils.py index fc082b8..c10245b 100644 --- a/llmage/utils.py +++ b/llmage/utils.py @@ -235,19 +235,19 @@ class BufferedLLMs: async with get_sor_context(env, 'llmage') as sor: sql = """select x.*, z.input_fields - from ( - select a.*, e.ioid, e.stream, e.callbackurl, f.input_fields as inputfields - from llm a, upapp c, uapiset d, uapi e, uapiio f - where a.upappid = c.id - and c.apisetid = d.id - and e.apisetid = d.id - and e.ioid = f.id - and a.apiname = e.name - and a.expired_date > ${today}$ - and a.enabled_date <= ${today}$ - ) x left join uapiio z on x.ioid = z.id - where x.id = ${llmid}$ - """ +from ( +select a.*, e.ioid, e.stream, e.callbackurl, f.input_fields as inputfields, + m.query_apiname, m.query_period, m.ppid +from llm a +join llm_api_map m on a.id = m.llmid +join upapp c on a.upappid = c.id +join uapi e on c.apisetid = e.apisetid and m.apiname = e.name +join uapiio f on e.ioid = f.id +where a.expired_date > ${today}$ + and a.enabled_date <= ${today}$ +) x left join uapiio z on x.ioid = z.id +where x.id = ${llmid}$ +""" ns = {'llmid': llmid, 'today': today} recs = await sor.sqlExe(sql, ns.copy()) if len(recs) > 0: diff --git a/scripts/migrate_llm_api_map.py b/scripts/migrate_llm_api_map.py new file mode 100644 index 0000000..e02ef44 --- /dev/null +++ b/scripts/migrate_llm_api_map.py @@ -0,0 +1,165 @@ +""" +Migration script: Generate llm_api_map records from existing llm table data. + +This script reads existing llm records and produces SQL INSERT statements +for the new llm_api_map table. It does NOT directly modify the database. + +Usage: + python migrate_llm_api_map.py [--db-config CONFIG_PATH] [--output OUTPUT_FILE] + +The script outputs INSERT SQL statements that can be reviewed and executed manually. +""" +import sys +import json +import argparse +from appPublic.uniqueID import getID + + +def generate_migration_sql(llm_records, catalog_rel_records=None): + """ + Generate INSERT statements for llm_api_map from existing llm data. + + For each llm record: + - If llm_catalog_rel exists: create one llm_api_map per (llmid, llmcatelogid) + - If no catalog_rel: create one llm_api_map with the llm's default catalog + """ + inserts = [] + + # Build catalog_rel lookup: llmid -> [llmcatelogid, ...] + catelog_map = {} + if catalog_rel_records: + for rel in catalog_rel_records: + llmid = rel.get('llmid') + catelogid = rel.get('llmcatelogid') + if llmid and catelogid: + catelog_map.setdefault(llmid, []).append(catelogid) + + for llm in llm_records: + llmid = llm.get('id') + if not llmid: + continue + + apiname = llm.get('apiname', '') + query_apiname = llm.get('query_apiname', '') + query_period = llm.get('query_period', '') + ppid = llm.get('ppid', '') + upappid = llm.get('upappid', '') + + # Get catalog IDs for this llm + catelog_ids = catelog_map.get(llmid) + if not catelog_ids: + # Fallback: use a default or skip + # In practice, every llm should have at least one catalog_rel entry + # If not, we can try to infer from the model type + catelog_ids = [llm.get('llmcatelogid', '')] + if not catelog_ids[0]: + print(f"WARNING: llm {llmid} has no catalog_rel entry, skipping", + file=sys.stderr) + continue + + for catelogid in catelog_ids: + map_id = getID() + + # Build VALUES + values = { + 'id': f"'{map_id}'", + 'llmid': f"'{llmid}'", + 'llmcatelogid': f"'{catelogid}'", + 'apiname': f"'{apiname}'", + } + + if query_apiname: + values['query_apiname'] = f"'{query_apiname}'" + else: + values['query_apiname'] = 'NULL' + + if query_period is not None and query_period != '': + values['query_period'] = str(int(query_period)) + else: + values['query_period'] = 'NULL' + + if ppid: + values['ppid'] = f"'{ppid}'" + else: + values['ppid'] = 'NULL' + + cols = ', '.join(values.keys()) + vals = ', '.join(values.values()) + sql = f"INSERT INTO llm_api_map ({cols}) VALUES ({vals});" + inserts.append(sql) + + return inserts + + +def main(): + parser = argparse.ArgumentParser( + description='Generate llm_api_map migration SQL from existing llm data') + parser.add_argument('--input', '-i', + help='Input JSON file with llm records (for offline mode)') + parser.add_argument('--catalog-rel', '-c', + help='Input JSON file with llm_catalog_rel records') + parser.add_argument('--output', '-o', default='-', + help='Output file for SQL statements (default: stdout)') + parser.add_argument('--dry-run', action='store_true', + help='Only show count of generated statements') + args = parser.parse_args() + + # Load llm records from JSON input (offline mode) + # In production, this would connect to the database + if args.input: + with open(args.input, 'r', encoding='utf-8') as f: + llm_records = json.load(f) + else: + print("No --input provided. Use --input to provide llm records JSON.", + file=sys.stderr) + print("Example: python migrate_llm_api_map.py -i llm_dump.json", + file=sys.stderr) + sys.exit(1) + + catalog_rel_records = None + if args.catalog_rel: + with open(args.catalog_rel, 'r', encoding='utf-8') as f: + catalog_rel_records = json.load(f) + + inserts = generate_migration_sql(llm_records, catalog_rel_records) + + if args.dry_run: + print(f"Would generate {len(inserts)} INSERT statements for llm_api_map") + return + + # Output + header_lines = [ + "-- Migration: Create llm_api_map records from existing llm data", + "-- Generated by migrate_llm_api_map.py", + "-- Review these statements before executing!", + "", + "-- Step 1: Create the llm_api_map table (if not exists)", + """CREATE TABLE llm_api_map ( + id VARCHAR(21) NOT NULL PRIMARY KEY, + llmid VARCHAR(21) NOT NULL, + llmcatelogid VARCHAR(32) NOT NULL, + apiname VARCHAR(100) NOT NULL, + query_apiname VARCHAR(100), + query_period INT, + ppid VARCHAR(21) +);""", + "", + "CREATE INDEX idx_llm_api_llm ON llm_api_map (llmid);", + "CREATE UNIQUE INDEX idx_llm_api_catelog ON llm_api_map (llmid, llmcatelogid);", + "", + "-- Step 2: Insert data", + "" + ] + + output_text = '\n'.join(header_lines) + '\n'.join(inserts) + '\n' + + if args.output == '-': + print(output_text) + else: + with open(args.output, 'w', encoding='utf-8') as f: + f.write(output_text) + print(f"Generated {len(inserts)} INSERT statements -> {args.output}") + + +if __name__ == '__main__': + main() diff --git a/wwwroot/list_paging_catelog_llms.dspy b/wwwroot/list_paging_catelog_llms.dspy index 8c3f106..96e1c6f 100644 --- a/wwwroot/list_paging_catelog_llms.dspy +++ b/wwwroot/list_paging_catelog_llms.dspy @@ -17,8 +17,7 @@ from llm a join llm_catalog_rel rel on a.id = rel.llmid join llmcatelog b on rel.llmcatelogid = b.id join upapp c on a.upappid = c.id -join uapiset d on c.apisetid = d.id -join uapi e on e.apisetid = d.id and a.apiname = e.name +join uapi e on c.apisetid = e.apisetid and a.apiname = e.name ) x left join historyformat y on x.hfid = y.id left join uapiio z on x.ioid = z.id where rel.llmcatelogid = ${llmcatelogid}$ diff --git a/wwwroot/llmcheck.dspy b/wwwroot/llmcheck.dspy index d25e872..771902a 100644 --- a/wwwroot/llmcheck.dspy +++ b/wwwroot/llmcheck.dspy @@ -10,22 +10,10 @@ async with get_sor_context(request._run_ns, 'llmage') as sor: else: msgs.append(f'llm not found: {llmid=}, {today=}') return msgs - sql = """select a.* from llm a, upapp b -where a.id=${llmid}$ + sql = """select a.* from llm a, upapp b +where a.id=${llmid}$ and a.upappid = b.id - and a.enabled_date <= ${today}$ - and a.expired_date > ${today}$""" - recs = await sor.sqlExe(sql, ns.copy()) - if recs: - msgs.append('llm join upapp read ok') - else: - msgs.append('llm join upapp not found: {llmid=}, {today=}') - return msgs - sql = """select a.* from llm a, upapp b, uapiset c -where a.id=${llmid}$ - and a.upappid = b.id - and b.apisetid = c.id - and a.enabled_date <= ${today}$ + and a.enabled_date <= ${today}$ and a.expired_date > ${today}$""" recs = await sor.sqlExe(sql, ns.copy()) if recs: @@ -35,12 +23,10 @@ where a.id=${llmid}$ return msgs sql = """select a.*, e.ioid, e.stream -from llm a, upapp c, uapiset d, uapi e -where a.upappid = c.id - and c.apisetid = d.id - and e.apisetid = d.id - and a.apiname = e.name - and a.id=${llmid}$ +from llm a +join upapp c on a.upappid = c.id +join uapi e on c.apisetid = e.apisetid and a.apiname = e.name +where a.id=${llmid}$ and a.expired_date > ${today}$ and a.enabled_date <= ${today}$""" recs = await sor.sqlExe(sql, ns.copy()) @@ -51,13 +37,11 @@ where a.upappid = c.id return msgs sql = """select a.*, e.ioid, e.stream -from llm a, upapp c, uapiset d, uapi e, uapiio b -where a.upappid = c.id - and c.apisetid = d.id - and e.apisetid = d.id - and a.apiname = e.name - and e.ioid = b.id - and a.id=${llmid}$ +from llm a +join upapp c on a.upappid = c.id +join uapi e on c.apisetid = e.apisetid and a.apiname = e.name +join uapiio b on e.ioid = b.id +where a.id=${llmid}$ and a.expired_date > ${today}$ and a.enabled_date <= ${today}$""" recs = await sor.sqlExe(sql, ns.copy())