diff --git a/.gitignore b/.gitignore
index c18dd8d..99f8b75 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,8 @@
__pycache__/
+# CRUD definition directories (auto-generated by Sage platform)
+wwwroot/llm/
+wwwroot/llm_api_map/
+wwwroot/llmcatelog_list/
+wwwroot/llmusage/
+wwwroot/llmusage_accounting_failed/
+wwwroot/llmusage_history/
diff --git a/scripts/load_path.py b/scripts/load_path.py
index 2c50749..69320aa 100644
--- a/scripts/load_path.py
+++ b/scripts/load_path.py
@@ -13,17 +13,19 @@ import subprocess
import os
import sys
+
def find_sage_root():
candidates = [
os.path.expanduser("~/repos/sage"),
os.path.expanduser("~/sage"),
- os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))),
+ os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
]
for c in candidates:
if os.path.isdir(os.path.join(c, "py3")) and os.path.isdir(os.path.join(c, "wwwroot")):
return c
return None
+
SAGE_ROOT = find_sage_root()
if not SAGE_ROOT:
print("ERROR: Cannot find Sage root directory")
@@ -38,96 +40,65 @@ MOD = "llmage"
# 权限路径定义 — 每次新增页面或API时同步更新
# ============================================================
-# any — 无需登录(仅静态资源和菜单)
+# any — 无需登录(菜单、静态资源)
PATHS_ANY = [
f"/{MOD}/menu.ui",
- f"/{MOD}/imgs",
- f"/{MOD}/imgs/kdb.svg",
- f"/{MOD}/list_catelog_models.dspy",
+ f"/{MOD}/imgs/%",
]
-# logined — 需要认证的页面和 API
+# logined — 所有已登录用户
PATHS_LOGINED = [
- # Module entry
+ # 模块入口
f"/{MOD}",
+ f"/{MOD}/index.ui",
- # Top-level pages and APIs
- f"/{MOD}/llmcost.dspy",
- f"/{MOD}/llminference.dspy",
+ # 顶层 .ui 页面
f"/{MOD}/llm_dialog.ui",
f"/{MOD}/show_same_catelog_llm.ui",
- f"/{MOD}/model_estimate.dspy",
f"/{MOD}/show_llms.ui",
- f"/{MOD}/llmcheck.dspy",
f"/{MOD}/show_llms_by_providers.ui",
- f"/{MOD}/list_paging_catelog_llms.dspy",
+ f"/{MOD}/failed_accounting.ui",
+ f"/{MOD}/llmcatelog_list.ui",
- # llmusage CRUD directory
- f"/{MOD}/llmusage",
- f"/{MOD}/llmusage/update_llmusage.dspy",
- f"/{MOD}/llmusage/delete_llmusage.dspy",
- f"/{MOD}/llmusage/add_llmusage.dspy",
- f"/{MOD}/llmusage/index.ui",
- f"/{MOD}/llmusage/get_llmusage.dspy",
+ # 顶层 .dspy(非 api/ 目录)
+ f"/{MOD}/%.dspy",
- # llmcatelog CRUD directory
- f"/{MOD}/llmcatelog",
- f"/{MOD}/llmcatelog/add_llmcatelog.dspy",
- f"/{MOD}/llmcatelog/get_llmcatelog.dspy",
- f"/{MOD}/llmcatelog/delete_llmcatelog.dspy",
- f"/{MOD}/llmcatelog/index.ui",
- f"/{MOD}/llmcatelog/update_llmcatelog.dspy",
+ # api/ 目录 — 所有 .dspy 通配
+ f"/{MOD}/api/%",
- # llm CRUD directory
- f"/{MOD}/llm",
- f"/{MOD}/llm/update_llm.dspy",
- f"/{MOD}/llm/delete_llm.dspy",
- f"/{MOD}/llm/index.ui",
- f"/{MOD}/llm/get_llm.dspy",
- f"/{MOD}/llm/add_llm.dspy",
+ # CRUD 子目录 — 通配(每个子目录下的所有文件)
+ f"/{MOD}/llm/%",
+ f"/{MOD}/llmcatelog/%",
+ f"/{MOD}/llmcatelog_list/%",
+ f"/{MOD}/llmusage/%",
+ f"/{MOD}/llmusage_accounting_failed/%",
+ f"/{MOD}/llmusage_history/%",
+ f"/{MOD}/llm_api_map/%",
- # API endpoints
- f"/{MOD}/api/llm_list.dspy",
- f"/{MOD}/api/llm_create.dspy",
- f"/{MOD}/api/llm_update.dspy",
- f"/{MOD}/api/llm_delete.dspy",
- f"/{MOD}/api/get_organizations.dspy",
- f"/{MOD}/api/get_upapps.dspy",
- f"/{MOD}/api/llm_api_map_list.dspy",
- f"/{MOD}/api/llm_api_map_create.dspy",
- f"/{MOD}/api/llm_api_map_delete.dspy",
- f"/{MOD}/api/llm_api_map_options.dspy",
- f"/{MOD}/api/uapi_options.dspy",
- f"/{MOD}/api/failed_accounting_list.dspy",
- f"/{MOD}/api/llmusage_accounting_failed_create.dspy",
- f"/{MOD}/api/llmusage_accounting_failed_update.dspy",
- f"/{MOD}/api/llmusage_accounting_failed_delete.dspy",
- f"/{MOD}/api/llmusage_create.dspy",
- f"/{MOD}/api/llmusage_update.dspy",
- f"/{MOD}/api/llmusage_delete.dspy",
+ # v1 API 目录
+ f"/{MOD}/v1/%",
- # v1 API endpoints
- f"/{MOD}/v1/chat/completions",
- f"/{MOD}/v1/chat/completions/index.dspy",
- f"/{MOD}/v1/models",
- f"/{MOD}/v1/models/index.dspy",
- f"/{MOD}/v1/tasks",
- f"/{MOD}/v1/tasks/index.dspy",
- f"/{MOD}/v1/video/generations",
- f"/{MOD}/v1/video/generations/index.dspy",
- f"/{MOD}/v1/image/generations",
- f"/{MOD}/v1/image/generations/index.dspy",
+ # 其他子目录
+ f"/{MOD}/list_llmcatelogs/%",
+ f"/{MOD}/list_llms/%",
+ f"/{MOD}/openai/%",
+ f"/{MOD}/t2t/%",
+ f"/{MOD}/tasks/%",
+ f"/{MOD}/upload_asset/%",
+ f"/{MOD}/video/%",
]
# ============================================================
# 执行注册
# ============================================================
+
def run_set_perm(role, path):
cmd = [PYTHON, SET_PERM_SCRIPT, role, path]
result = subprocess.run(cmd, capture_output=True, text=True)
return result.returncode == 0
+
def register_role_paths(role, paths):
count = 0
for p in paths:
@@ -136,6 +107,7 @@ def register_role_paths(role, paths):
print(f" {role}: {count}/{len(paths)} paths registered")
return count
+
def main():
print(f"Sage root: {SAGE_ROOT}")
total = 0
@@ -144,5 +116,6 @@ def main():
print(f"\nDone. Total {total} permission entries registered.")
print("NOTE: Restart Sage after permission changes to reload RBAC cache.")
+
if __name__ == "__main__":
main()
diff --git a/wwwroot/index.ui b/wwwroot/index.ui
index a5afa71..93cb53d 100644
--- a/wwwroot/index.ui
+++ b/wwwroot/index.ui
@@ -11,15 +11,13 @@
"options": {
"width": "100%",
"alignItems": "center",
- "padding": "16px 24px",
- "marginBottom": "0"
+ "marginBottom": "24px"
},
"subwidgets": [
{
"widgettype": "Title2",
"options": {
- "text": "LLM 模型管理",
- "fontWeight": "700"
+ "text": "LLM 模型管理"
}
},
{
@@ -28,139 +26,181 @@
{
"widgettype": "Text",
"options": {
- "text": "模型配置、目录分类与调用监控",
- "fontSize": "14px"
+ "text": "模型类型、模型配置与记账失败记录",
+ "cfontsize": 1.2
}
}
]
},
{
- "widgettype": "ResponsableBox",
+ "widgettype": "VScrollPanel",
"options": {
- "gap": "12px",
- "minWidth": "200px",
- "padding": "0 24px 16px 24px"
+ "css": "filler"
},
"subwidgets": [
{
"widgettype": "VBox",
"options": {
- "css": "card",
- "padding": "16px 24px",
- "cursor": "pointer"
+ "spacing": 24
},
- "binds": [
- {
- "wid": "self",
- "event": "click",
- "actiontype": "urlwidget",
- "target": "app.llmage_content",
- "options": {
- "url": "{{entire_url('/llmage/llmcatelog_list.ui')}}"
- },
- "mode": "replace"
- }
- ],
"subwidgets": [
{
- "widgettype": "Title5",
+ "widgettype": "ResponsableBox",
"options": {
- "text": "模型类型管理",
- "fontWeight": "600"
- }
- },
- {
- "widgettype": "Text",
- "options": {
- "text": "管理模型的分类目录和类型定义",
- "fontSize": "12px"
- }
+ "gap": "16px",
+ "minWidth": "250px"
+ },
+ "subwidgets": [
+ {
+ "widgettype": "VBox",
+ "options": {
+ "css": "card",
+ "cheight": 12,
+ "cwidth": 23,
+ "padding": "24px",
+ "cursor": "pointer",
+ "borderRadius": "8px"
+ },
+ "binds": [
+ {
+ "wid": "self",
+ "event": "click",
+ "actiontype": "urlwidget",
+ "target": "app.llmage_content",
+ "options": {
+ "url": "{{entire_url('/llmage/llmcatelog_list.ui')}}"
+ },
+ "mode": "replace"
+ }
+ ],
+ "subwidgets": [
+ {
+ "widgettype": "Svg",
+ "options": {
+ "svg": "",
+ "width": "40px",
+ "height": "40px"
+ }
+ },
+ {
+ "widgettype": "Title4",
+ "options": {
+ "text": "模型类型管理",
+ "marginTop": "12px"
+ }
+ },
+ {
+ "widgettype": "Text",
+ "options": {
+ "text": "管理模型的分类和类型",
+ "cfontsize": 1.2
+ }
+ }
+ ]
+ },
+ {
+ "widgettype": "VBox",
+ "options": {
+ "css": "card",
+ "cheight": 12,
+ "cwidth": 23,
+ "padding": "24px",
+ "cursor": "pointer",
+ "borderRadius": "8px"
+ },
+ "binds": [
+ {
+ "wid": "self",
+ "event": "click",
+ "actiontype": "urlwidget",
+ "target": "app.llmage_content",
+ "options": {
+ "url": "{{entire_url('/llmage/llm')}}"
+ },
+ "mode": "replace"
+ }
+ ],
+ "subwidgets": [
+ {
+ "widgettype": "Svg",
+ "options": {
+ "svg": "",
+ "width": "40px",
+ "height": "40px"
+ }
+ },
+ {
+ "widgettype": "Title4",
+ "options": {
+ "text": "模型管理",
+ "marginTop": "12px"
+ }
+ },
+ {
+ "widgettype": "Text",
+ "options": {
+ "text": "管理 LLM 模型配置",
+ "cfontsize": 1.2
+ }
+ }
+ ]
+ },
+ {
+ "widgettype": "VBox",
+ "options": {
+ "css": "card",
+ "cheight": 12,
+ "cwidth": 23,
+ "padding": "24px",
+ "cursor": "pointer",
+ "borderRadius": "8px"
+ },
+ "binds": [
+ {
+ "wid": "self",
+ "event": "click",
+ "actiontype": "urlwidget",
+ "target": "app.llmage_content",
+ "options": {
+ "url": "{{entire_url('/llmage/failed_accounting.ui')}}"
+ },
+ "mode": "replace"
+ }
+ ],
+ "subwidgets": [
+ {
+ "widgettype": "Svg",
+ "options": {
+ "svg": "",
+ "width": "40px",
+ "height": "40px"
+ }
+ },
+ {
+ "widgettype": "Title4",
+ "options": {
+ "text": "记账失败记录",
+ "marginTop": "12px"
+ }
+ },
+ {
+ "widgettype": "Text",
+ "options": {
+ "text": "查看和检索记账失败的记录",
+ "cfontsize": 1.2
+ }
+ }
+ ]
+ }
+ ]
}
]
},
{
"widgettype": "VBox",
- "options": {
- "css": "card",
- "padding": "16px 24px",
- "cursor": "pointer"
- },
- "binds": [
- {
- "wid": "self",
- "event": "click",
- "actiontype": "urlwidget",
- "target": "app.llmage_content",
- "options": {
- "url": "{{entire_url('/llmage/llm')}}"
- },
- "mode": "replace"
- }
- ],
- "subwidgets": [
- {
- "widgettype": "Title5",
- "options": {
- "text": "模型配置",
- "fontWeight": "600"
- }
- },
- {
- "widgettype": "Text",
- "options": {
- "text": "管理 LLM 模型的API配置与供应商映射",
- "fontSize": "12px"
- }
- }
- ]
- },
- {
- "widgettype": "VBox",
- "options": {
- "css": "card",
- "padding": "16px 24px",
- "cursor": "pointer"
- },
- "binds": [
- {
- "wid": "self",
- "event": "click",
- "actiontype": "urlwidget",
- "target": "app.llmage_content",
- "options": {
- "url": "{{entire_url('/llmage/failed_accounting.ui')}}"
- },
- "mode": "replace"
- }
- ],
- "subwidgets": [
- {
- "widgettype": "Title5",
- "options": {
- "text": "记账失败记录",
- "fontWeight": "600"
- }
- },
- {
- "widgettype": "Text",
- "options": {
- "text": "查看和检索调用计费失败记录",
- "fontSize": "12px"
- }
- }
- ]
+ "id": "llmage_content"
}
]
- },
- {
- "widgettype": "VBox",
- "id": "llmage_content",
- "css": "filler",
- "options": {
- "width": "100%",
- "padding": "0 24px",
- "overflowY": "auto"
- }
}
]
}
diff --git a/wwwroot/llmcost.dspy b/wwwroot/llmcost.dspy
index 87b77aa..45cbdfc 100644
--- a/wwwroot/llmcost.dspy
+++ b/wwwroot/llmcost.dspy
@@ -1,4 +1,4 @@
-debug(f'{params_kw=}')
+debug_params('params_kw', params_kw)
ns = params_kw.copy()
if not ns.page:
ns.page = 1
diff --git a/wwwroot/llminference.dspy b/wwwroot/llminference.dspy
index bd32ebf..fcc6a46 100644
--- a/wwwroot/llminference.dspy
+++ b/wwwroot/llminference.dspy
@@ -1,4 +1,4 @@
-debug(f'{params_kw=}')
+debug_params('params_kw', params_kw)
if params_kw.off_peak:
off_peak = params_kw.off_peak
if off_peak in [True, "Y" "y", 1, "1"]:
@@ -25,7 +25,7 @@ if kdbids:
ret = await rfexe('fusedsearch', request, params)
data.update(ret)
params_kw.prompt = await tmpl_engine.renders(tmpl, data)
- debug(f'{params=}rag return {data}, {params_kw.prompt=}')
+ debug_params('rag', {'query': params.get('query',''), 'prompt_len': len(str(params_kw.prompt))})
env = DictObject(**globals())
return await inference(request, env=env)
diff --git a/wwwroot/model_estimate.dspy b/wwwroot/model_estimate.dspy
index 0c04aed..f6a4917 100644
--- a/wwwroot/model_estimate.dspy
+++ b/wwwroot/model_estimate.dspy
@@ -1,5 +1,5 @@
-debug(f'model_estimate.dspy:{params_kw=}')
+debug_params('model_estimate', params_kw)
db = DBPools()
dbname = get_module_dbname('llmage')
async with db.sqlorContext(dbname) as sor:
diff --git a/wwwroot/t2t/index.dspy b/wwwroot/t2t/index.dspy
index 58ad527..61b7a2a 100644
--- a/wwwroot/t2t/index.dspy
+++ b/wwwroot/t2t/index.dspy
@@ -1,4 +1,4 @@
-debug(f'{params_kw=}')
+debug_params('params_kw', params_kw)
lctype='文生文'
if params_kw.off_peak:
off_peak = params_kw.off_peak
diff --git a/wwwroot/v1/chat/completions/index.dspy b/wwwroot/v1/chat/completions/index.dspy
index 6850c23..46b0ddc 100644
--- a/wwwroot/v1/chat/completions/index.dspy
+++ b/wwwroot/v1/chat/completions/index.dspy
@@ -9,7 +9,7 @@ async def gen():
async for l in f():
yield l
-debug(f'{params_kw=}')
+debug_params('params_kw', params_kw)
lctype='文生文'
if params_kw.off_peak:
off_peak = params_kw.off_peak
@@ -25,7 +25,7 @@ if userid is None:
return openai_403()
if not params_kw.prompt and not params_kw.messages:
- debug(f'not params_kw.prompt and not params_kw.messages,{params_kw=}')
+ debug(f'missing prompt and messages, model={params_kw.model}')
d = return_error('Missing need data(prompt or messages)')
return json_response(d, status=400)
env = request._run_ns
diff --git a/wwwroot/v1/image/generations/index.dspy b/wwwroot/v1/image/generations/index.dspy
index 067c014..a1d68cb 100644
--- a/wwwroot/v1/image/generations/index.dspy
+++ b/wwwroot/v1/image/generations/index.dspy
@@ -23,7 +23,7 @@ from appPublic.uniqueID import getID
from appPublic.timeUtils import curDateString, timestampstr
from sqlor.dbpools import get_sor_context
-debug(f'{params_kw=}')
+debug_params('params_kw', params_kw)
userid = await get_user()
userorgid = await get_userorgid()
diff --git a/wwwroot/v1/video/generations/index.dspy b/wwwroot/v1/video/generations/index.dspy
index 29a94c6..e81ff42 100644
--- a/wwwroot/v1/video/generations/index.dspy
+++ b/wwwroot/v1/video/generations/index.dspy
@@ -31,7 +31,7 @@ from appPublic.uniqueID import getID
from appPublic.timeUtils import curDateString, timestampstr
from sqlor.dbpools import get_sor_context
-debug(f'{params_kw=}')
+debug_params('params_kw', params_kw)
userid = await get_user()
userorgid = await get_userorgid()
diff --git a/wwwroot/vidu_inference.dspy b/wwwroot/vidu_inference.dspy
index cf0f6da..28d0a73 100644
--- a/wwwroot/vidu_inference.dspy
+++ b/wwwroot/vidu_inference.dspy
@@ -1,4 +1,4 @@
-debug(f'{params_kw=}')
+debug_params('params_kw', params_kw)
if params_kw.off_peak:
off_peak = params_kw.off_peak
if off_peak in [True, "Y" "y", 1, "1"]: