Compare commits
No commits in common. "main" and "feat/dataviz-llmage" have entirely different histories.
main
...
feat/datav
15
.gitignore
vendored
15
.gitignore
vendored
@ -1,15 +0,0 @@
|
||||
__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_accounting_failed/recover_usages.dspy
|
||||
wwwroot/llmusage_history/
|
||||
build/
|
||||
|
||||
# Generated CRUD files (managed by xls2ddl/build.sh, not tracked in git)
|
||||
wwwroot/llm_metrics/
|
||||
wwwroot/user_llm_policy/
|
||||
.swp
|
||||
68
README.md
68
README.md
@ -278,74 +278,6 @@ tasks = await get_today_asynctask_list(userid)
|
||||
await query_task_status(request, luid, onetime=False)
|
||||
```
|
||||
|
||||
### 历史推理记录查询
|
||||
|
||||
`GET /llmage/api/get_inference_history.dspy`
|
||||
|
||||
跨表(llmusage + llmusage_history)分页查询当前用户的推理历史,按时间倒序返回,默认每页 10 条。自动通过 FileStorage 读取 ioinfo 文件内容,返回实际输入输出。
|
||||
|
||||
**请求参数**:
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| page | int | 否 | 页码,默认 1 |
|
||||
| pagerows | int | 否 | 每页条数,默认 10 |
|
||||
| llmcatelogid | str | 否 | 按模型分类 ID 过滤,仅返回该分类下模型的记录 |
|
||||
|
||||
**返回字段**:
|
||||
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| success | 是否成功 |
|
||||
| total | 两表合计总记录数 |
|
||||
| page | 当前页码 |
|
||||
| page_size | 每页条数(默认 10,可通过 pagerows 参数指定) |
|
||||
| rows | 记录列表 |
|
||||
|
||||
**rows 中每条记录**:
|
||||
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| id | 记录 ID |
|
||||
| llmid | 模型 ID |
|
||||
| use_date | 使用日期 |
|
||||
| use_time | 使用时间(排序依据) |
|
||||
| userid | 用户 ID |
|
||||
| usages | token 用量(JSON 对象) |
|
||||
| status | 调用状态(ok/failed 等) |
|
||||
| ioinfo | 原始 webpath |
|
||||
| io_content | 解析后的输入输出内容,包含 input 和 output;读取失败时为 null |
|
||||
| amount | 费用金额 |
|
||||
| userorgid | 组织 ID |
|
||||
| accounting_status | 记账状态 |
|
||||
|
||||
**返回示例**:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"rows": [
|
||||
{
|
||||
"id": "abc123",
|
||||
"llmid": "model001",
|
||||
"use_date": "2026-06-05",
|
||||
"use_time": "2026-06-05 12:30:00",
|
||||
"userid": "user001",
|
||||
"usages": {"total_tokens": 1000, "prompt_tokens": 800, "completion_tokens": 200},
|
||||
"status": "ok",
|
||||
"io_content": {"input": [...], "output": [...]},
|
||||
"amount": 0.05,
|
||||
"accounting_status": "accounted"
|
||||
}
|
||||
],
|
||||
"total": 156,
|
||||
"page": 1,
|
||||
"page_size": 50
|
||||
}
|
||||
```
|
||||
|
||||
**权限**:logined(所有已登录用户),仅返回当前登录用户自己的记录。
|
||||
|
||||
---
|
||||
|
||||
## 前端页面
|
||||
|
||||
851
docs/API.md
851
docs/API.md
@ -1,851 +0,0 @@
|
||||
# llmage API 文档
|
||||
|
||||
Base Path: `/llmage/v1`
|
||||
|
||||
所有 API 端点需要 Bearer Token 认证(`logined` 权限)。
|
||||
|
||||
---
|
||||
|
||||
## POST /v1/chat/completions
|
||||
|
||||
文本生成接口,兼容 OpenAI 格式。
|
||||
|
||||
### 必填参数
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `model` | string | 模型名称,如 `"qwen3-max"` |
|
||||
| `messages` 或 `prompt` | array / string | 对话消息数组或文本提示 |
|
||||
|
||||
### 可选参数
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `catelogid` | string | 目录类型ID,默认 `"t2t"`,也支持中文名(向后兼容) |
|
||||
| `stream` | boolean | 是否启用流式输出 |
|
||||
| `off_peak` | boolean | 是否使用非高峰时段 |
|
||||
| `transno` | string | 交易流水号(不传则自动生成) |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "qwen3-max",
|
||||
"messages": [
|
||||
{"role": "user", "content": "Hello"}
|
||||
],
|
||||
"stream": false
|
||||
}
|
||||
```
|
||||
|
||||
### 响应格式
|
||||
|
||||
**非流式响应:**
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "luid_xxx",
|
||||
"object": "chat.completion",
|
||||
"model": "qwen3-max",
|
||||
"choices": [{
|
||||
"index": 0,
|
||||
"message": {"role": "assistant", "content": "Hi there!"},
|
||||
"finish_reason": "stop"
|
||||
}],
|
||||
"usage": {"prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15}
|
||||
}
|
||||
```
|
||||
|
||||
**流式响应 (SSE):**
|
||||
|
||||
```
|
||||
data: {"choices": [{"delta": {"content": "Hi"}, "index": 0}]}
|
||||
data: {"choices": [{"delta": {"content": " there!"}, "index": 0}]}
|
||||
data: [DONE]
|
||||
```
|
||||
|
||||
### 错误响应
|
||||
|
||||
| 状态码 | 说明 |
|
||||
|--------|------|
|
||||
| 400 | 缺少必填参数或模型不存在 |
|
||||
| 403 | 未登录 |
|
||||
| 429 | 账户余额不足 |
|
||||
|
||||
---
|
||||
|
||||
## POST /v1/video/generations
|
||||
|
||||
视频生成接口。
|
||||
|
||||
### 必填参数
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `model` | string | 模型名称,如 `"keling-2.1"` |
|
||||
| `catelogid` | string | 目录类型ID,如 `"t2v"` / `"i2v"` / `"r2v"` |
|
||||
| `prompt` | string | 生成提示词 |
|
||||
|
||||
### 可选参数
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `image_file` | string | 图生视频时提供参考图 URL |
|
||||
| `duration` | string | 视频时长,如 `"5s"` |
|
||||
| `resolution` | string | 分辨率,如 `"1080p"` |
|
||||
| `n` | integer | 生成数量 |
|
||||
| `transno` | string | 交易流水号 |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "keling-2.1",
|
||||
"catelogid": "t2v",
|
||||
"prompt": "A beautiful sunset over the ocean",
|
||||
"duration": "5s",
|
||||
"resolution": "1080p"
|
||||
}
|
||||
```
|
||||
|
||||
### 响应格式
|
||||
|
||||
视频生成通常为异步任务,提交后返回任务信息:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "luid_xxx",
|
||||
"object": "video.generation",
|
||||
"model": "keling-2.1",
|
||||
"status": "submitted",
|
||||
"taskid": "task_xxx",
|
||||
"created": 1716912000
|
||||
}
|
||||
```
|
||||
|
||||
通过 `/v1/tasks?taskid=xxx` 查询任务状态。
|
||||
|
||||
### 各模型输入参数明细
|
||||
|
||||
> 以下为各平台/模型的具体输入参数。调用时通过 `model` + `catelogid` 自动路由到对应供应商。
|
||||
|
||||
---
|
||||
|
||||
#### Vidu 平台
|
||||
|
||||
##### T2V - 文生视频
|
||||
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 | 可选值 |
|
||||
|--------|------|------|--------|------|--------|
|
||||
| `model` | string | 是 | `viduq3-pro` | 模型名称 | `viduq3-turbo`, `viduq3-pro` |
|
||||
| `prompt` | string | 是 | - | 提示词 | - |
|
||||
| `off_peak` | string | 否 | `N` | 错峰执行 | `Y`, `N` |
|
||||
| `duration` | integer | 否 | `10` | 视频长度(1-16秒) | 1-16 |
|
||||
| `ratio` | string | 否 | `16:9` | 长宽比 | `16:9`, `9:16`, `4:3`, `3:4`, `1:1` |
|
||||
| `resolution` | string | 否 | `1080p` | 分辨率 | `540p`, `720p`, `1080p` |
|
||||
|
||||
##### I2V - 图生视频
|
||||
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 | 可选值 |
|
||||
|--------|------|------|--------|------|--------|
|
||||
| `model` | string | 是 | `viduq3-pro` | 模型名称 | `viduq3-pro`, `viduq3-turbo` |
|
||||
| `prompt` | string | 是 | - | 提示词 | - |
|
||||
| `image_file` | image | 是 | - | 首帧图片 | - |
|
||||
| `off_peak` | string | 否 | `N` | 错峰执行 | `Y`, `N` |
|
||||
| `duration` | integer | 否 | `10` | 视频长度(1-16秒) | 1-16 |
|
||||
| `ratio` | string | 否 | `16:9` | 长宽比 | `16:9`, `9:16`, `4:3`, `3:4`, `1:1` |
|
||||
| `resolution` | string | 否 | `1080p` | 分辨率 | `540p`, `720p`, `1080p` |
|
||||
|
||||
##### 2I2V - 首尾帧生视频
|
||||
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|
||||
|--------|------|------|--------|------|
|
||||
| `model` | string | 否 | `viduq2` | 模型名称 |
|
||||
| `payload` | string | 是 | `2i2v` | 固定值 |
|
||||
| `off_peak` | boolean | 否 | `false` | 错峰模式 |
|
||||
| `images` | array | 是 | - | 两张图片URL `[首帧, 尾帧]` |
|
||||
| `duration` | integer | 否 | `10` | 视频时长 |
|
||||
| `prompt` | string | 是 | - | 提示词 |
|
||||
| `audio` | boolean | 否 | `true` | 音频直出 |
|
||||
| `seed` | integer | 否 | `12345` | 随机种子 |
|
||||
| `aspect_ratio` | string | 否 | `16:9` | 画面比例 |
|
||||
| `resolution` | string | 否 | `1080p` | 分辨率 |
|
||||
|
||||
##### Ref2V - 参考生视频 v2(主体模式)
|
||||
|
||||
> 使用主体(图片/视频/文字)生成视频,支持 viduq3-turbo/q3/q2-pro/q2/q1/2.0
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| `model` | string | 是 | 模型名称 |
|
||||
| `subjects` | array | 是 | 主体列表(最多7个图片/文字主体,每个主体最多3张图) |
|
||||
| `prompt` | string | 是 | 提示词 |
|
||||
| `audio` | boolean | 否 | 音视频直出 |
|
||||
| `audio_type` | string | 否 | 音频类型 |
|
||||
| `duration` | integer | 否 | 视频时长 |
|
||||
| `seed` | integer | 否 | 随机种子 |
|
||||
| `aspect_ratio` | string | 否 | 画面比例 |
|
||||
| `resolution` | string | 否 | 分辨率 |
|
||||
| `movement_amplitude` | string | 否 | 运动幅度 |
|
||||
| `off_peak` | boolean | 否 | 错峰模式 |
|
||||
| `auto_subjects` | boolean | 否 | 智能主体 |
|
||||
|
||||
##### Ref2V - 参考生视频 v2(非主体模式)
|
||||
|
||||
> 直接上传图片参考生成视频,支持 viduq3-mix/q3-turbo/q3/q2-pro/q2/q1/2.0
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| `model` | string | 是 | 模型名称 |
|
||||
| `images` | array | 是 | 参考图片URL列表(1-7张) |
|
||||
| `videos` | array | 否 | 参考视频URL列表(仅viduq2-pro) |
|
||||
| `prompt` | string | 是 | 提示词 |
|
||||
| `audio` | boolean | 否 | 音视频直出 |
|
||||
| `bgm` | boolean | 否 | 背景音乐 |
|
||||
| `duration` | integer | 否 | 视频时长 |
|
||||
| `seed` | integer | 否 | 随机种子 |
|
||||
| `aspect_ratio` | string | 否 | 画面比例 |
|
||||
| `resolution` | string | 否 | 分辨率 |
|
||||
| `off_peak` | boolean | 否 | 错峰模式 |
|
||||
|
||||
##### Ref2V - 参考生视频 v1
|
||||
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 | 可选值 |
|
||||
|--------|------|------|--------|------|--------|
|
||||
| `model` | string | 是 | `viduq2-pro` | 模型名称 | `viduq2`, `viduq1`, `vidu2.0` |
|
||||
| `prompt` | string | 是 | - | 提示词 | - |
|
||||
| `off_peak` | string | 否 | `N` | 错峰执行 | `Y`, `N` |
|
||||
| `duration` | integer | 否 | `10` | 视频长度 | - |
|
||||
| `ratio` | string | 否 | `16:9` | 长宽比 | `16:9`, `9:16`, `4:3`, `3:4`, `1:1` |
|
||||
| `resolution` | string | 否 | `1080p` | 分辨率 | `540p`, `720p`, `1080p` |
|
||||
|
||||
---
|
||||
|
||||
#### Seedance 平台(火山方舟)
|
||||
|
||||
##### T2V - 文生视频
|
||||
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 | 可选值 |
|
||||
|--------|------|------|--------|------|--------|
|
||||
| `model` | string | 是 | `doubao-seedance-2-0-260128` | 模型名称 | `doubao-seedance-2-0-260128`, `doubao-seedance-2-0-fast-260128` |
|
||||
| `prompt` | string | 是 | - | 提示词 | - |
|
||||
| `resolution` | string | 否 | `720p` | 尺寸 | `480p`, `720p`, `1080p` |
|
||||
| `duration` | integer | 否 | `8` | 视频长度 | - |
|
||||
| `ratio` | string | 否 | `1:1` | 宽高比 | `1:1`, `16:9`, `9:16`, `4:3`, `3:4`, `21:9`, `9:21` |
|
||||
|
||||
##### TI2V - 文图生视频
|
||||
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 | 可选值 |
|
||||
|--------|------|------|--------|------|--------|
|
||||
| `model` | string | 是 | `doubao-seedance-2-0-260128` | 模型名称 | `doubao-seedance-2-0-260128`, `doubao-seedance-2-0-fast-260128` |
|
||||
| `prompt` | string | 是 | - | 提示词 | - |
|
||||
| `image1_file` | image | 是 | - | 首帧图片 | - |
|
||||
| `image2_file` | image | 否 | - | 尾帧图片 | - |
|
||||
| `resolution` | string | 否 | `720p` | 尺寸 | `480p`, `720p`, `1080p` |
|
||||
| `duration` | integer | 否 | `8` | 视频长度 | - |
|
||||
| `ratio` | string | 否 | `1:1` | 宽高比 | `1:1`, `16:9`, `9:16`, `4:3`, `3:4`, `21:9`, `9:21` |
|
||||
|
||||
##### Ref2V - 参考生视频
|
||||
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|
||||
|--------|------|------|--------|------|
|
||||
| `model` | string | 是 | - | 模型名称 |
|
||||
| `prompt` | string | 是 | - | 提示词 |
|
||||
| `image_file` | image | 否 | - | 参考图片(支持数组,多张参考图) |
|
||||
| `video_file` | video | 否 | - | 参考视频(支持数组) |
|
||||
| `audio_file` | audio | 否 | - | 参考音频(支持数组) |
|
||||
| `duration` | integer | 否 | `12` | 视频长度 |
|
||||
| `resolution` | string | 否 | `720p` | 尺寸 |
|
||||
| `ratio` | string | 否 | - | 宽高比 |
|
||||
|
||||
---
|
||||
|
||||
#### 通义万象(DashScope)
|
||||
|
||||
##### T2V - 文生视频
|
||||
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|
||||
|--------|------|------|--------|------|
|
||||
| `model` | string | 是 | - | 模型名称(如 `wan2.6-t2v`) |
|
||||
| `prompt` | string | 是 | - | 提示词 |
|
||||
| `negative_prompt` | string | 否 | - | 反向提示词 |
|
||||
| `audio_file` | audio | 否 | - | 配音文件 |
|
||||
| `size` | string | 否 | `1920*1080` | 视频尺寸 |
|
||||
| `duration` | string | 否 | `15` | 视频时长 |
|
||||
|
||||
**size 可选值:** `832*480`, `480*832`, `624*624`, `1280*720`, `720*1280`, `960*960`, `1088*832`, `832*1088`, `1920*1080`, `1080*1920`, `1440*1440`, `1632*1248`, `1248*1632`
|
||||
|
||||
**duration 可选值:** `5`, `10`, `15`
|
||||
|
||||
##### I2V - 图生视频
|
||||
|
||||
可用模型:`wan2.6-i2v`, `wan2.6-i2v-flash`
|
||||
|
||||
> 输入参数与 T2V 类似,额外需要首帧图片。
|
||||
|
||||
##### 2I2V - 首尾帧生视频
|
||||
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|
||||
|--------|------|------|--------|------|
|
||||
| `model` | string | 是 | - | 模型名称 |
|
||||
| `prompt` | string | 是 | - | 提示词 |
|
||||
| `negative_prompt` | string | 否 | - | 反向提示词 |
|
||||
| `image1_file` | image | 是 | - | 首帧图片 |
|
||||
| `image2_file` | image | 是 | - | 尾帧图片 |
|
||||
| `resolution` | string | 否 | `1080P` | 分辨率 |
|
||||
| `duration` | integer | - | `5` | 固定5秒 |
|
||||
|
||||
##### Ref2V - 角色参考生视频
|
||||
|
||||
> 参考输入视频中的角色形象和音色,搭配提示词生成保持角色一致性的视频。可以输入1-3个人物视频,每个视频一个角色。
|
||||
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|
||||
|--------|------|------|--------|------|
|
||||
| `model` | string | 是 | - | 模型名称(如 `wan2.6-r2v`) |
|
||||
| `prompt` | string | 是 | - | 提示词 |
|
||||
| `video1_file` | video | 是 | - | 角色一视频 |
|
||||
| `video2_file` | video | 否 | - | 角色二视频 |
|
||||
| `video3_file` | video | 否 | - | 角色三视频 |
|
||||
| `size` | string | 否 | `1920*1080` | 视频尺寸 |
|
||||
| `duration` | string | 否 | `10` | 视频时长 |
|
||||
|
||||
**size 可选值:** 同 T2V
|
||||
|
||||
**duration 可选值:** `10`, `15`
|
||||
|
||||
##### IA2V - 图像音频生视频
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| `image_file` | image | 是 | 图像 |
|
||||
| `audio_file` | audio | 是 | 音频 |
|
||||
|
||||
---
|
||||
|
||||
#### 可灵(Kling)
|
||||
|
||||
##### T2V - 文生视频
|
||||
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 | 可选值 |
|
||||
|--------|------|------|--------|------|--------|
|
||||
| `model` | string | 是 | - | 模型名称 | `kling-v2-1-master`, `kling-v2-master`, `kling-v1-6`, `kling-v1` |
|
||||
| `prompt` | string | 是 | - | 提示词 | - |
|
||||
| `negative_prompt` | string | 否 | - | 反向提示词 | - |
|
||||
|
||||
---
|
||||
|
||||
#### 海螺(Hailuo/MiniMax)
|
||||
|
||||
##### TI2V - 图生视频
|
||||
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 | 可选值 |
|
||||
|--------|------|------|--------|------|--------|
|
||||
| `prompt` | string | 是 | - | 提示词 | - |
|
||||
| `image_file` | image | 否 | - | 首帧图片 | - |
|
||||
| `image_file1` | image | 否 | - | 尾帧图片 | - |
|
||||
| `resolution` | string | 否 | `768P` | 尺寸 | `768P`, `1080P` |
|
||||
| `duration` | integer | 否 | `6` | 视频长度 | `6`(6秒), `10`(10秒) |
|
||||
|
||||
---
|
||||
|
||||
#### 快乐马(HappyHorse)
|
||||
|
||||
> 基于通义万象平台(tongyi-wan),输入参数与通义万象对应类型一致。
|
||||
|
||||
##### T2V - 文生视频
|
||||
|
||||
输入参数同通义万象 T2V。可用模型:`happyhorse-1.0-t2v`
|
||||
|
||||
##### I2V - 图生视频
|
||||
|
||||
输入参数同通义万象 I2V。可用模型:`happyhorse-1.0-i2v`
|
||||
|
||||
> **注意:** 图片参数名为 `image_file`,传入图片 URL。
|
||||
|
||||
##### Ref2V - 参考生视频
|
||||
|
||||
输入参数同通义万象 Ref2V,额外支持:
|
||||
|
||||
| 参数名 | 说明 |
|
||||
|--------|------|
|
||||
| `resolution` | 可选 `1080P`(默认), `720P` |
|
||||
| `ratio` | 可选 `16:9`(默认), `9:16`, `3:4`, `4:3` |
|
||||
|
||||
可用模型:`happyhorse-1.0-r2v`(参考图像数量1-9张,支持多角色参考)
|
||||
|
||||
---
|
||||
|
||||
## POST /v1/image/generations
|
||||
|
||||
图像生成接口。
|
||||
|
||||
### 必填参数
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `model` | string | 模型名称,如 `"jimeng-4.0"` |
|
||||
| `catelogid` | string | 目录类型ID,如 `"t2i"` |
|
||||
| `prompt` | string | 生成提示词 |
|
||||
|
||||
### 可选参数
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `image_file` | string | 图生图时提供参考图 URL |
|
||||
| `size` | string | 尺寸,如 `"1024x1024"` |
|
||||
| `n` | integer | 生成数量 |
|
||||
| `style` | string | 风格参数 |
|
||||
| `quality` | string | 质量参数 |
|
||||
| `transno` | string | 交易流水号 |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "jimeng-4.0",
|
||||
"catelogid": "t2i",
|
||||
"prompt": "A beautiful sunset over the ocean",
|
||||
"size": "1024x1024",
|
||||
"n": 1
|
||||
}
|
||||
```
|
||||
|
||||
### 响应格式
|
||||
|
||||
响应格式取决于上游模型配置(同步返回图像数据,异步返回任务信息):
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "luid_xxx",
|
||||
"object": "image.generation",
|
||||
"model": "jimeng-4.0",
|
||||
"status": "submitted",
|
||||
"taskid": "task_xxx",
|
||||
"created": 1716912000
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## POST /v1/music/generations
|
||||
|
||||
音乐生成接口。
|
||||
|
||||
### 必填参数
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `model` | string | 模型名称,如 `"music-2.6"`, `"music-2.5"` |
|
||||
| `catelogid` | string | 目录类型ID,固定为 `"music_gen"` |
|
||||
| `prompt` | string | 音乐风格描述(风格、情绪、场景),如 `"流行音乐, 开心, 适合阳光明媚的下午"` |
|
||||
| `lyrics` | string | 歌词内容,使用 `\n` 分隔每行,可包含结构标签 |
|
||||
|
||||
### 歌词结构标签
|
||||
|
||||
歌词中可包含以下结构标签来优化生成的音乐结构:
|
||||
- `[Intro]` - 前奏
|
||||
- `[Verse]` - 主歌
|
||||
- `[Pre Chorus]` - 预副歌
|
||||
- `[Chorus]` - 副歌
|
||||
- `[Bridge]` - 桥段
|
||||
- `[Outro]` - 尾声
|
||||
- `[Interlude]` - 间奏
|
||||
- `[Hook]` - 记忆点
|
||||
- `[Build Up]` - 情绪铺垫
|
||||
- `[Solo]` - 独奏
|
||||
|
||||
### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "music-2.6",
|
||||
"catelogid": "music_gen",
|
||||
"prompt": "Pop music, happy, suitable for a sunny day",
|
||||
"lyrics": "[Intro]\n\n[Verse]\nWalking down the street\nFeeling the beat\n\n[Chorus]\nDancing in the sun\nHaving so much fun"
|
||||
}
|
||||
```
|
||||
|
||||
### 响应格式
|
||||
|
||||
MiniMax 音乐生成为同步接口,直接返回音频URL:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "luid_xxx",
|
||||
"object": "music.generation",
|
||||
"model": "music-2.6",
|
||||
"status": "SUCCEEDED",
|
||||
"audio": "https://...",
|
||||
"created": 1716912000
|
||||
}
|
||||
```
|
||||
|
||||
### 可用模型
|
||||
|
||||
| 模型名称 | model 参数 | 说明 |
|
||||
|---------|-----------|------|
|
||||
| MiniMax Music 2.6 | `music-2.6` | 最新版本,音质最佳 |
|
||||
| MiniMax Music 2.5 | `music-2.5` | 支持14种段落级结构标签,物理级高保真 |
|
||||
|
||||
### MiniMax Music 2.5 特性
|
||||
|
||||
Music 2.5 在「段落级强控制」与「物理级高保真」两大技术难题上实现突破:
|
||||
- 开放全段落标签控制,精准支持14种结构变体
|
||||
- 长度限制:歌词内容 [1, 3500] 个字符
|
||||
- prompt 长度限制:[10, 300] 个字符
|
||||
|
||||
### MiniMax Music 2.0 特性(已过期)
|
||||
|
||||
Music 2.0 能根据文本描述和歌词直接生成包含人声的完整歌曲:
|
||||
- prompt 长度限制:[10, 300] 个字符
|
||||
- lyrics 长度限制:[10, 3000] 个字符
|
||||
- 状态:已过期(expired_date: 2026-01-01)
|
||||
|
||||
### 错误响应
|
||||
|
||||
| 状态码 | 说明 |
|
||||
|--------|------|
|
||||
| 400 | 缺少必填参数或模型不存在 |
|
||||
| 403 | 未登录 |
|
||||
| 429 | 账户余额不足 |
|
||||
|
||||
---
|
||||
|
||||
## POST /v1/audio/speech
|
||||
|
||||
文本转语音(TTS)接口。
|
||||
|
||||
### 必填参数
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `model` | string | 模型名称,如 `"speech-2.6-turbo"`, `"speech-2.6-hd"` |
|
||||
| `catelogid` | string | 目录类型ID,固定为 `"tts"` |
|
||||
| `prompt` | string | 需要合成的文本内容,最长 10,000 字符 |
|
||||
|
||||
### 可选参数
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `speaker` | string | 说话人/音色ID,如 `"female-tianmei"` |
|
||||
| `speed` | float | 语速,默认 `1.0` |
|
||||
| `emotion` | string | 情感,如 `"happy"`, `"sad"` |
|
||||
| `transno` | string | 交易流水号 |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "speech-2.6-turbo",
|
||||
"catelogid": "tts",
|
||||
"prompt": "你好,欢迎使用语音合成服务",
|
||||
"speaker": "female-tianmei",
|
||||
"speed": 1.0,
|
||||
"emotion": "happy"
|
||||
}
|
||||
```
|
||||
|
||||
### 响应格式
|
||||
|
||||
MiniMax TTS 为流式接口,逐块返回音频数据(hex编码自动转base64):
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "SUCCEEDED",
|
||||
"audio": "base64_encoded_audio_data"
|
||||
}
|
||||
```
|
||||
|
||||
### 可用模型
|
||||
|
||||
| 模型名称 | model 参数 | 说明 |
|
||||
|---------|-----------|------|
|
||||
| MiniMax Speech 2.6 Turbo | `speech-2.6-turbo` | 极速版,更快更优惠,适用于语音聊天和数字人 |
|
||||
| MiniMax Speech 2.6 HD | `speech-2.6-hd` | 高清版,超低延时,更高自然度 |
|
||||
| MiniMax Speech 2.5 HD | `speech-2.5-hd-preview` | Preview版本 |
|
||||
| F5-TTS 本地 | `f5tts` | 本地部署,零样本声音克隆,多语言支持 |
|
||||
|
||||
### 错误响应
|
||||
|
||||
| 状态码 | 说明 |
|
||||
|--------|------|
|
||||
| 400 | 缺少必填参数或模型不存在 |
|
||||
| 403 | 未登录 |
|
||||
| 429 | 账户余额不足 |
|
||||
|
||||
---
|
||||
|
||||
## POST /v1/audio/transcriptions
|
||||
|
||||
语音识别(ASR)接口,将音频转为文本。
|
||||
|
||||
### 必填参数
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `model` | string | 模型名称,如 `"qwen3-asr-flash"`, `"parakeet-tdt-0.6b-v2"` |
|
||||
| `catelogid` | string | 目录类型ID,固定为 `"asr"` |
|
||||
| `audio_file` | string | 音频文件URL |
|
||||
|
||||
### 可选参数
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `language` | string | 语言代码(部分模型支持) |
|
||||
| `transno` | string | 交易流水号 |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "qwen3-asr-flash",
|
||||
"catelogid": "asr",
|
||||
"audio_file": "https://example.com/audio.wav"
|
||||
}
|
||||
```
|
||||
|
||||
### 响应格式
|
||||
|
||||
```json
|
||||
{
|
||||
"text": "识别出的文本内容",
|
||||
"usage": {
|
||||
"duration_seconds": 5.2
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 可用模型
|
||||
|
||||
| 模型名称 | model 参数 | 说明 |
|
||||
|---------|-----------|------|
|
||||
| 通义千问 ASR | `qwen3-asr-flash` | 多语种识别、歌唱识别、情感识别、噪声拒识,0.00026元/秒 |
|
||||
| Nvidia ASR | `parakeet-tdt-0.6b-v2` | 仅支持英文,6亿参数,支持标点/大小写/时间戳 |
|
||||
|
||||
### 通义千问 ASR 核心功能
|
||||
|
||||
- 多语种识别:涵盖普通话及多种方言(粤语、四川话等)
|
||||
- 复杂环境适应:自动语种检测与智能非人声过滤
|
||||
- 歌唱识别:伴随BGM下也能实现整首歌曲转写
|
||||
- 上下文增强:通过配置上下文提高识别准确率
|
||||
- 情感识别:支持惊讶、平静、愉快、悲伤、厌恶、愤怒、恐惧
|
||||
|
||||
### 错误响应
|
||||
|
||||
| 状态码 | 说明 |
|
||||
|--------|------|
|
||||
| 400 | 缺少必填参数或模型不存在 |
|
||||
| 403 | 未登录 |
|
||||
| 429 | 账户余额不足 |
|
||||
|
||||
---
|
||||
|
||||
## GET /v1/tasks
|
||||
|
||||
查询异步任务状态。
|
||||
|
||||
### 必填参数
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `taskid` | string | 任务 ID |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```
|
||||
GET /llmage/v1/tasks?taskid=task_xxx
|
||||
```
|
||||
|
||||
### 响应格式
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"data": {
|
||||
"status": "SUCCEEDED",
|
||||
"output": [...]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
任务状态值: `UNKNOWN` / `SUCCEEDED` / `FAILED`
|
||||
|
||||
---
|
||||
|
||||
## GET /v1/models
|
||||
|
||||
列出可用模型列表。
|
||||
|
||||
### 可选参数
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `catelogid` | string | 按目录类型过滤 |
|
||||
| `orderby` | string | 排序字段 |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```
|
||||
GET /llmage/v1/models
|
||||
```
|
||||
|
||||
### 响应格式
|
||||
|
||||
```json
|
||||
{
|
||||
"object": "list",
|
||||
"data": [
|
||||
{
|
||||
"id": "qwen3-max",
|
||||
"object": "model",
|
||||
"created": 1748044800,
|
||||
"owned_by": "opencomputing.ai"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## GET /v1/pricing
|
||||
|
||||
获取模型定价展示信息。
|
||||
|
||||
### 必填参数
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `model` | string | 模型名称,如 `"qwen3.7-max"` |
|
||||
|
||||
### 可选参数
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `catelogid` | string | 目录类型ID,默认 `"t2t"` |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```
|
||||
GET /llmage/v1/pricing?model=qwen3.7-max
|
||||
```
|
||||
|
||||
### 响应格式
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"data": {
|
||||
"ppid": "pp_xxx",
|
||||
"name": "qwen3.7-max",
|
||||
"pricing_type": "per_use",
|
||||
"display_text": "【通义千问 qwen3.7-max】定价:\n - 输入Token: 12 元/百万 [模型=qwen3.7-max]\n - 输出Token: 48 元/百万 [模型=qwen3.7-max]",
|
||||
"items": [...]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 错误响应
|
||||
|
||||
| 状态 | 说明 |
|
||||
|------|------|
|
||||
| error | 缺少 model 参数 |
|
||||
| error | 模型不存在或无定价配置 |
|
||||
|
||||
---
|
||||
|
||||
## 通用说明
|
||||
|
||||
### catelogid 目录类型ID对照表
|
||||
|
||||
| ID | 中文名 | 说明 |
|
||||
|----|--------|------|
|
||||
| `t2t` | 文生文 | 文本生成(默认) |
|
||||
| `t2i` | 文生图 | 图像生成 |
|
||||
| `t2v` | 文生视频 | 文本生成视频 |
|
||||
| `i2v` | 图生视频 | 图像生成视频 |
|
||||
| `r2v` | 参考生视频 | 参考图像生成视频 |
|
||||
| `tts` | 语音合成 | 文本转语音 |
|
||||
| `asr` | 语音识别 | 语音转文本 |
|
||||
| `vision` | 图理解 | 图像理解 |
|
||||
| `ai_search` | AI搜索 | AI搜索 |
|
||||
| `digital_human` | 数字人 | 数字人 |
|
||||
| `music_gen` | 音乐生成 | 音乐生成 |
|
||||
| `text_cls` | 文本分类 | 文本分类 |
|
||||
| `3d_gen` | 3D生成 | 3D模型生成 |
|
||||
| `video_tool` | 视频工具 | 视频处理工具 |
|
||||
| `translate` | 翻译 | 文本翻译 |
|
||||
|
||||
> 向后兼容:catelogid 参数同时支持新ID(如 `"t2v"`)和旧中文名(如 `"文生视频"`),推荐使用新ID。
|
||||
|
||||
### 参数统一
|
||||
|
||||
所有 v1 接口统一使用 `catelogid` 参数标识目录类型,替代原有的 `lctype` / `llmcatelogid`。
|
||||
|
||||
### 认证
|
||||
|
||||
所有接口需要 Bearer Token 认证,请求头中携带:
|
||||
|
||||
```
|
||||
Authorization: Bearer ***
|
||||
```
|
||||
|
||||
### 余额检查
|
||||
|
||||
每次请求都会自动调用 `checkCustomerBalance()` 进行余额检查:
|
||||
- 如果模型属于用户所在组织(`llm.ownerid == userorgid`),则跳过余额检查
|
||||
- 否则检查 tpac 余额或本地余额
|
||||
- 余额不足时返回 429 状态码
|
||||
|
||||
### 计费
|
||||
|
||||
请求成功后自动创建 `llmusage` 记录,状态为 `created`。后台定时任务会定期执行计费流程。
|
||||
|
||||
# KTV Pipeline API(视频制作)
|
||||
|
||||
Base url: `https://token.opencomputing.cn/llmage/v1`
|
||||
|
||||
供应商: 开元云(北京)科技, 14个原子化GPU端点覆盖KTV全流程。
|
||||
|
||||
## 调用方式
|
||||
|
||||
所有模型通过 `POST /v1/pipeline/submit` 调用,`catelogid` 固定为 `ktv_pipeline`(无需传参)。
|
||||
|
||||
### 通用格式
|
||||
|
||||
```json
|
||||
// 请求
|
||||
{"model": "ky-xxx", ...服务特定参数...}
|
||||
|
||||
// 同步响应
|
||||
{"taskid": "luid_xxx", "taskstatus": "SUCCEEDED", "usage": {...}}
|
||||
|
||||
// 异步提交响应
|
||||
{"taskid": "luid_xxx", "taskstatus": "PENDING"}
|
||||
// 异步结果通过 GET /v1/tasks?taskid=xxx 查询
|
||||
```
|
||||
|
||||
## 模型列表
|
||||
|
||||
| model | 功能 | 模式 | 计费 |
|
||||
|-------|------|------|------|
|
||||
| `ky-asr-transcribe` | 语音转文字+时间戳 | 同步 | 0.01元/秒 |
|
||||
| `ky-demucs-separate` | 人声/伴奏分离 | 同步 | 0.02元/秒 |
|
||||
| `ky-face-detect` | 人脸检测 | 同步 | 0.10元/次 |
|
||||
| `ky-face-recognize` | 人脸识别 | 同步 | 0.10元/次 |
|
||||
| `ky-face-compare` | 人脸比对 | 同步 | 0.10元/次 |
|
||||
| `ky-subtitle-render` | 歌词→ASS字幕渲染 | 同步 | 0.01元/条 |
|
||||
| `ky-merge-video` | 视频+音频+字幕合并 | 同步 | 0.01元/秒 |
|
||||
| `ky-songrate-evaluate` | AI歌曲质量评分 | 同步 | 0.005元/秒 |
|
||||
| `ky-synth-generate` | AI歌声合成 | **异步** | 0.20元/秒,任务状态通过 `GET /v1/tasks?taskid=xxx` 查询 |
|
||||
| `ky-realesrgan-upscale` | 图像/视频超分辨率 | **异步** | 0.50元/张,任务状态通过 `GET /v1/tasks?taskid=xxx` 查询 |
|
||||
| `ky-rvc-convert` | 声音克隆/变声 | 同步 | 0.15元/秒 |
|
||||
| `ky-video-eval-evaluate` | 视频质量评估 | 同步 | 0.05元/秒 |
|
||||
|
||||
## 模型发现
|
||||
|
||||
```bash
|
||||
curl 'https://token.opencomputing.cn/llmage/v1/models?catelogid=ktv_pipeline' \
|
||||
-H 'Authorization: Bearer ***'
|
||||
```
|
||||
|
||||
详细参数说明和 curl 示例见 [dashboard_for_sage 文档](https://token.opencomputing.cn/dashboard_for_sage/api_doc.md)。
|
||||
@ -1,118 +0,0 @@
|
||||
# MiniMax 供应商接入记录
|
||||
|
||||
## 供应商信息
|
||||
|
||||
| 项目 | 值 |
|
||||
|------|-----|
|
||||
| 供应商名称 | MiniMax (上海稀宇科技有限公司) |
|
||||
| 平台网址 | https://platform.minimaxi.com |
|
||||
| API文档 | https://platform.minimaxi.com/docs/api-reference/text-chat-openai |
|
||||
| 定价页面 | https://platform.minimaxi.com/subscribe/token-plan?tab=api-enterprise |
|
||||
| API基础URL | https://api.minimaxi.com/v1 (upapp.baseurl) |
|
||||
| 系统upappid | minimax |
|
||||
| 系统providerid | ww4e_kfX3Lh65Sdys0Vku |
|
||||
| API认证方式 | Bearer Token (Authorization: Bearer *** |
|
||||
|
||||
## 已接入模型 (共11个, 截至2026-06-12)
|
||||
|
||||
### 文本生成 (t2t) — 定价项目: 5jmzupARABxkDFwUraFiQ
|
||||
|
||||
| 模型名称 | model | llm.id | 状态 | httpapi |
|
||||
|----------|-------|--------|------|---------|
|
||||
| **MiniMax M3** | MiniMax-M3 | mm3_MiniMax_M3 | 新增 | minimax_openai t2t |
|
||||
| MiniMax M2.7 | minimax-m2.7 | oiLvLl75qNX9IQkWFm60i | 已有 | t2t |
|
||||
| **MiniMax M2.7 Highspeed** | MiniMax-M2.7-highspeed | mm_m27_highspeed | 新增 | minimax_openai t2t |
|
||||
|
||||
### 视频生成 (i2v) — 定价项目: 0V89eilc_UQ2KiZIRJO8M
|
||||
|
||||
| 模型名称 | model | llm.id | 状态 |
|
||||
|----------|-------|--------|------|
|
||||
| MiniMax Hailuo 2.3 | MiniMax-Hailuo-2.3 | AU1f40HV3tqFjxcVWWpyR | 已有, 补充ppid |
|
||||
| 海螺参考生视频 | S2V-01 | oks-VG9D8p2b0Agvs-LeQ | 已有, 补充ppid |
|
||||
|
||||
### 语音合成 (tts) — 定价项目: mm_tts_pricing (新增)
|
||||
|
||||
| 模型名称 | model | llm.id | 状态 |
|
||||
|----------|-------|--------|------|
|
||||
| speech-2.6-hd | speech-2.6-hd | q6rdMUsGD1z3S3NyZh_A_ | 已有, 补充ppid |
|
||||
| speech-2.6-turbo | speech-2.6-turbo | CEYD4YWRxjCj4k_6bpzIM | 已有, 补充ppid |
|
||||
| speech-2.5-hd-preview | speech-2.5-hd-preview | Si2g0XJ9ym3P5jlrdmcfB | 已有, 补充ppid |
|
||||
|
||||
### 音乐生成 (music_gen) — 定价项目: fQzkUeS6t6NBz_Fu4Fi77
|
||||
|
||||
| 模型名称 | model | llm.id | 状态 |
|
||||
|----------|-------|--------|------|
|
||||
| Music 2.6 | music-2.6 | dleFKyYSSllCl70etn7yU | 已有 |
|
||||
| Music 2.5 | music-2.5 | tTREa9nNy3yIRxywQLjvT | 已有 |
|
||||
| Music 2.0 | music-2.0 | ns7egG9aXi91wjI62yKfu | 已有, 补充ppid |
|
||||
|
||||
## 定价信息
|
||||
|
||||
### 文本模型 (元/百万tokens) — 5jmzupARABxkDFwUraFiQ
|
||||
|
||||
| 模型 | 输入 | 输出 | 缓存 | 备注 |
|
||||
|------|------|------|------|------|
|
||||
| MiniMax-M3 (≤512K) | ¥2.1 | ¥8.4 | ¥0.42 | 永久五折 |
|
||||
| MiniMax-M3 (512K~1M) | ¥4.2 | ¥16.8 | ¥0.84 | 永久五折 |
|
||||
| MiniMax-M2.7 | ¥2.1 | ¥8.4 | - | 五折 |
|
||||
| MiniMax-M2.7-highspeed | ¥4.2 | ¥16.8 | - | - |
|
||||
| MiniMax-M2.5 | ¥2.1 | ¥8.4 | - | - |
|
||||
| MiniMax-M2.5-highspeed | ¥4.2 | ¥16.8 | - | - |
|
||||
| M2-her | ¥2.1 | ¥8.4 | - | - |
|
||||
|
||||
### TTS (元/万字符) — mm_tts_pricing
|
||||
|
||||
| 模型 | 单价 |
|
||||
|------|------|
|
||||
| speech-2.6-hd | ¥3.5 |
|
||||
| speech-2.6-turbo | ¥2.0 |
|
||||
| speech-2.5-hd-preview | ¥3.5 |
|
||||
|
||||
### 视频 (元/次) — 0V89eilc_UQ2KiZIRJO8M
|
||||
|
||||
| 模型 | 分辨率 | 时长 | 单价 |
|
||||
|------|--------|------|------|
|
||||
| Hailuo-2.3 | 768P | 6s | ¥2.00 |
|
||||
| Hailuo-2.3 | 768P | 10s | ¥3.50 |
|
||||
| Hailuo-2.3 | 1080P | 6s | ¥2.00 |
|
||||
| Hailuo-2.3-Fast | 768P | 6s | ¥2.25 |
|
||||
|
||||
### 音乐 (元/次) — fQzkUeS6t6NBz_Fu4Fi77
|
||||
|
||||
| 模型 | 单价 |
|
||||
|------|------|
|
||||
| Music-2.6/2.5/2.0 | ¥1.0 |
|
||||
|
||||
## uapi配置 (uapi模块)
|
||||
|
||||
### minimax t2t (新增, id=mm_minimax_t2t)
|
||||
- path: /chat/completions (upapp.baseurl拼接)
|
||||
- 完整URL: https://api.minimaxi.com/v1/chat/completions
|
||||
- ioid: Is8l4TGkcZcqFSjbbeIK2 (文本会话, 共享)
|
||||
- stream: stream, chunk_match: data:
|
||||
- headers: Bearer {{apikey}}, Content-Type: application/json
|
||||
|
||||
### minimax tm2t (新增, id=mm_minimax_tm2t)
|
||||
- 多模态对话, 支持image_file/video_file/audio_file
|
||||
- ioid: t-ujII59ku45tIPcdXu4O (文本媒体转文本, 共享)
|
||||
- 与ali-qwen的tm2t模板相同(b64media2url处理)
|
||||
|
||||
## SQL文件
|
||||
|
||||
`scripts/minimax_m3_add.sql` — 包含11条SQL语句:
|
||||
1. INSERT httpapi (minimax_openai t2t)
|
||||
2. INSERT llm (MiniMax-M3)
|
||||
3. INSERT llm (MiniMax-M2.7-highspeed)
|
||||
4. INSERT llm_api_map (M3)
|
||||
5. INSERT llm_api_map (M2.7-highspeed)
|
||||
6. UPDATE llm_api_map ppid × 6 (视频/TTS/音乐)
|
||||
7. INSERT pricing_program (mm_tts_pricing)
|
||||
8. INSERT pricing_program_timing (TTS定价)
|
||||
9. UPDATE 5jmzup timing (追加M3定价)
|
||||
10. UPDATE 5jmzup spec (添加M3到模型选项)
|
||||
|
||||
## 变更记录
|
||||
|
||||
| 日期 | 操作 |
|
||||
|------|------|
|
||||
| 2026-06-12 | 新增M3+M2.7-highspeed, 补齐Hailuo/S2V/TTS/Music定价 |
|
||||
127
i18n/en/msg.txt
127
i18n/en/msg.txt
@ -1,127 +0,0 @@
|
||||
模型管理: Model Management
|
||||
模型名称: Model Name
|
||||
模型编码: Model Code
|
||||
模型类型: Model Type
|
||||
模型提供商: Model Provider
|
||||
模型版本: Model Version
|
||||
模型状态: Model Status
|
||||
API接口: API Interface
|
||||
API密钥: API Key
|
||||
API地址: API Endpoint
|
||||
最大Token: Max Tokens
|
||||
输入价格: Input Price
|
||||
输出价格: Output Price
|
||||
折扣: Discount
|
||||
启用: Enable
|
||||
停用: Disable
|
||||
已启用: Enabled
|
||||
已停用: Disabled
|
||||
新增模型: Add Model
|
||||
编辑模型: Edit Model
|
||||
删除模型: Delete Model
|
||||
测试模型: Test Model
|
||||
模型分组: Model Group
|
||||
分组名称: Group Name
|
||||
分组描述: Group Description
|
||||
新增分组: Add Group
|
||||
编辑分组: Edit Group
|
||||
删除分组: Delete Group
|
||||
使用统计: Usage Statistics
|
||||
调用次数: Call Count
|
||||
成功次数: Success Count
|
||||
失败次数: Failure Count
|
||||
Token用量: Token Usage
|
||||
输入Token: Input Tokens
|
||||
输出Token: Output Tokens
|
||||
总Token: Total Tokens
|
||||
费用统计: Cost Statistics
|
||||
总费用: Total Cost
|
||||
本月费用: Monthly Cost
|
||||
今日费用: Daily Cost
|
||||
按模型统计: By Model
|
||||
按用户统计: By User
|
||||
按日期统计: By Date
|
||||
趋势图: Trend Chart
|
||||
日: Day
|
||||
周: Week
|
||||
月: Month
|
||||
年: Year
|
||||
用户管理: User Management
|
||||
用户名称: User Name
|
||||
用户Token配额: User Token Quota
|
||||
已使用: Used
|
||||
剩余配额: Remaining Quota
|
||||
配额重置: Quota Reset
|
||||
模型映射: Model Mapping
|
||||
映射名称: Mapping Name
|
||||
源模型: Source Model
|
||||
目标模型: Target Model
|
||||
映射状态: Mapping Status
|
||||
新增映射: Add Mapping
|
||||
编辑映射: Edit Mapping
|
||||
删除映射: Delete Mapping
|
||||
密钥管理: Key Management
|
||||
密钥名称: Key Name
|
||||
密钥值: Key Value
|
||||
密钥状态: Key Status
|
||||
新增密钥: Add Key
|
||||
编辑密钥: Edit Key
|
||||
删除密钥: Delete Key
|
||||
日志: Log
|
||||
请求日志: Request Log
|
||||
错误日志: Error Log
|
||||
请求时间: Request Time
|
||||
响应时间: Response Time
|
||||
耗时: Duration
|
||||
状态码: Status Code
|
||||
错误信息: Error Message
|
||||
请求参数: Request Parameters
|
||||
响应内容: Response Content
|
||||
供应商: Vendor
|
||||
所属机构: Organization
|
||||
定价项目: Pricing Item
|
||||
定价属于: Pricing Belongs To
|
||||
供应商折扣: Vendor Discount
|
||||
描述: Description
|
||||
规格明细: Specification Details
|
||||
项目名称: Item Name
|
||||
模型: Model
|
||||
API: API
|
||||
定价: Pricing
|
||||
时序: Timeline
|
||||
开始日期: Start Date
|
||||
结束日期: End Date
|
||||
生效日期: Effective Date
|
||||
失效日期: Expiration Date
|
||||
定价数据: Pricing Data
|
||||
定价项目时序: Pricing Item Timeline
|
||||
测试: Test
|
||||
定价测试: Pricing Test
|
||||
新增: Add
|
||||
保存: Save
|
||||
取消: Cancel
|
||||
确认: Confirm
|
||||
删除: Delete
|
||||
编辑: Edit
|
||||
查看: View
|
||||
导出: Export
|
||||
打印: Print
|
||||
刷新: Refresh
|
||||
返回: Back
|
||||
提交: Submit
|
||||
重置: Reset
|
||||
Conform: Confirm
|
||||
Discard: Discard
|
||||
Submit: Submit
|
||||
Reset: Reset
|
||||
Cancel: Cancel
|
||||
搜索: Search
|
||||
操作: Action
|
||||
类型: Type
|
||||
状态: Status
|
||||
名称: Name
|
||||
编码: Code
|
||||
备注: Remark
|
||||
创建时间: Created Time
|
||||
更新时间: Updated Time
|
||||
全部: All
|
||||
127
i18n/jp/msg.txt
127
i18n/jp/msg.txt
@ -1,127 +0,0 @@
|
||||
模型管理: モデル管理
|
||||
模型名称: モデル名
|
||||
模型编码: モデルコード
|
||||
模型类型: モデルタイプ
|
||||
模型提供商: モデルプロバイダー
|
||||
模型版本: モデルバージョン
|
||||
模型状态: モデル状態
|
||||
API接口: APIインターフェース
|
||||
API密钥: APIキー
|
||||
API地址: APIエンドポイント
|
||||
最大Token: 最大トークン
|
||||
输入价格: 入力価格
|
||||
输出价格: 出力価格
|
||||
折扣: 割引
|
||||
启用: 有効化
|
||||
停用: 無効化
|
||||
已启用: 有効
|
||||
已停用: 無効
|
||||
新增模型: モデル追加
|
||||
编辑模型: モデル編集
|
||||
删除模型: モデル削除
|
||||
测试模型: モデルテスト
|
||||
模型分组: モデルグループ
|
||||
分组名称: グループ名
|
||||
分组描述: グループ説明
|
||||
新增分组: グループ追加
|
||||
编辑分组: グループ編集
|
||||
删除分组: グループ削除
|
||||
使用统计: 使用統計
|
||||
调用次数: 呼び出し回数
|
||||
成功次数: 成功回数
|
||||
失败次数: 失敗回数
|
||||
Token用量: トークン使用量
|
||||
输入Token: 入力トークン
|
||||
输出Token: 出力トークン
|
||||
总Token: 合計トークン
|
||||
费用统计: コスト統計
|
||||
总费用: 合計コスト
|
||||
本月费用: 今月コスト
|
||||
今日费用: 今日コスト
|
||||
按模型统计: モデル別統計
|
||||
按用户统计: ユーザー別統計
|
||||
按日期统计: 日付別統計
|
||||
趋势图: トレンドチャート
|
||||
日: 日
|
||||
周: 週
|
||||
月: 月
|
||||
年: 年
|
||||
用户管理: ユーザー管理
|
||||
用户名称: ユーザー名
|
||||
用户Token配额: ユーザートークンクォータ
|
||||
已使用: 使用済み
|
||||
剩余配额: 残りクォータ
|
||||
配额重置: クォータリセット
|
||||
模型映射: モデルマッピング
|
||||
映射名称: マッピング名
|
||||
源模型: ソースモデル
|
||||
目标模型: ターゲットモデル
|
||||
映射状态: マッピング状態
|
||||
新增映射: マッピング追加
|
||||
编辑映射: マッピング編集
|
||||
删除映射: マッピング削除
|
||||
密钥管理: キー管理
|
||||
密钥名称: キー名
|
||||
密钥值: キー値
|
||||
密钥状态: キー状態
|
||||
新增密钥: キー追加
|
||||
编辑密钥: キー編集
|
||||
删除密钥: キー削除
|
||||
日志: ログ
|
||||
请求日志: リクエストログ
|
||||
错误日志: エラーログ
|
||||
请求时间: リクエスト時間
|
||||
响应时间: レスポンス時間
|
||||
耗时: 所要時間
|
||||
状态码: ステータスコード
|
||||
错误信息: エラーメッセージ
|
||||
请求参数: リクエストパラメータ
|
||||
响应内容: レスポンス内容
|
||||
供应商: ベンダー
|
||||
所属机构: 所属組織
|
||||
定价项目: 価格設定項目
|
||||
定价属于: 価格設定帰属
|
||||
供应商折扣: ベンダー割引
|
||||
描述: 説明
|
||||
规格明细: 仕様詳細
|
||||
项目名称: 項目名
|
||||
模型: モデル
|
||||
API: API
|
||||
定价: 価格設定
|
||||
时序: 時系列
|
||||
开始日期: 開始日
|
||||
结束日期: 終了日
|
||||
生效日期: 有効開始日
|
||||
失效日期: 有効終了日
|
||||
定价数据: 価格設定データ
|
||||
定价项目时序: 価格設定項目時系列
|
||||
测试: テスト
|
||||
定价测试: 価格設定テスト
|
||||
新增: 新規追加
|
||||
保存: 保存
|
||||
取消: キャンセル
|
||||
确认: 確認
|
||||
删除: 削除
|
||||
编辑: 編集
|
||||
查看: 表示
|
||||
导出: エクスポート
|
||||
打印: 印刷
|
||||
刷新: 更新
|
||||
返回: 戻る
|
||||
提交: 送信
|
||||
重置: リセット
|
||||
Conform: 確認
|
||||
Discard: 破棄
|
||||
Submit: 送信
|
||||
Reset: リセット
|
||||
Cancel: キャンセル
|
||||
搜索: 検索
|
||||
操作: 操作
|
||||
类型: タイプ
|
||||
状态: ステータス
|
||||
名称: 名前
|
||||
编码: コード
|
||||
备注: 備考
|
||||
创建时间: 作成日時
|
||||
更新时间: 更新日時
|
||||
全部: 全部
|
||||
127
i18n/ko/msg.txt
127
i18n/ko/msg.txt
@ -1,127 +0,0 @@
|
||||
模型管理: 모델 관리
|
||||
模型名称: 모델 이름
|
||||
模型编码: 모델 코드
|
||||
模型类型: 모델 유형
|
||||
模型提供商: 모델 제공자
|
||||
模型版本: 모델 버전
|
||||
模型状态: 모델 상태
|
||||
API接口: API 인터페이스
|
||||
API密钥: API 키
|
||||
API地址: API 엔드포인트
|
||||
最大Token: 최대 토큰
|
||||
输入价格: 입력 가격
|
||||
输出价格: 출력 가격
|
||||
折扣: 할인
|
||||
启用: 활성화
|
||||
停用: 비활성화
|
||||
已启用: 활성화됨
|
||||
已停用: 비활성화됨
|
||||
新增模型: 모델 추가
|
||||
编辑模型: 모델 편집
|
||||
删除模型: 모델 삭제
|
||||
测试模型: 모델 테스트
|
||||
模型分组: 모델 그룹
|
||||
分组名称: 그룹 이름
|
||||
分组描述: 그룹 설명
|
||||
新增分组: 그룹 추가
|
||||
编辑分组: 그룹 편집
|
||||
删除分组: 그룹 삭제
|
||||
使用统计: 사용 통계
|
||||
调用次数: 호출 횟수
|
||||
成功次数: 성공 횟수
|
||||
失败次数: 실패 횟수
|
||||
Token用量: 토큰 사용량
|
||||
输入Token: 입력 토큰
|
||||
输出Token: 출력 토큰
|
||||
总Token: 총 토큰
|
||||
费用统计: 비용 통계
|
||||
总费用: 총 비용
|
||||
本月费用: 이번 달 비용
|
||||
今日费用: 오늘 비용
|
||||
按模型统计: 모델별 통계
|
||||
按用户统计: 사용자별 통계
|
||||
按日期统计: 날짜별 통계
|
||||
趋势图: 추세 차트
|
||||
日: 일
|
||||
周: 주
|
||||
月: 월
|
||||
年: 년
|
||||
用户管理: 사용자 관리
|
||||
用户名称: 사용자 이름
|
||||
用户Token配额: 사용자 토큰 쿼터
|
||||
已使用: 사용됨
|
||||
剩余配额: 잔여 쿼터
|
||||
配额重置: 쿼터 초기화
|
||||
模型映射: 모델 매핑
|
||||
映射名称: 매핑 이름
|
||||
源模型: 소스 모델
|
||||
目标模型: 대상 모델
|
||||
映射状态: 매핑 상태
|
||||
新增映射: 매핑 추가
|
||||
编辑映射: 매핑 편집
|
||||
删除映射: 매핑 삭제
|
||||
密钥管理: 키 관리
|
||||
密钥名称: 키 이름
|
||||
密钥值: 키 값
|
||||
密钥状态: 키 상태
|
||||
新增密钥: 키 추가
|
||||
编辑密钥: 키 편집
|
||||
删除密钥: 키 삭제
|
||||
日志: 로그
|
||||
请求日志: 요청 로그
|
||||
错误日志: 오류 로그
|
||||
请求时间: 요청 시간
|
||||
响应时间: 응답 시간
|
||||
耗时: 소요 시간
|
||||
状态码: 상태 코드
|
||||
错误信息: 오류 메시지
|
||||
请求参数: 요청 파라미터
|
||||
响应内容: 응답 내용
|
||||
供应商: 공급업체
|
||||
所属机构: 소속 기관
|
||||
定价项目: 가격 항목
|
||||
定价属于: 가격 귀속
|
||||
供应商折扣: 공급업체 할인
|
||||
描述: 설명
|
||||
规格明细: 규격 상세
|
||||
项目名称: 항목 이름
|
||||
模型: 모델
|
||||
API: API
|
||||
定价: 가격
|
||||
时序: 시계열
|
||||
开始日期: 시작 날짜
|
||||
结束日期: 종료 날짜
|
||||
生效日期: 시작일
|
||||
失效日期: 만료일
|
||||
定价数据: 가격 데이터
|
||||
定价项目时序: 가격 항목 시계열
|
||||
测试: 테스트
|
||||
定价测试: 가격 테스트
|
||||
新增: 추가
|
||||
保存: 저장
|
||||
取消: 취소
|
||||
确认: 확인
|
||||
删除: 삭제
|
||||
编辑: 편집
|
||||
查看: 조회
|
||||
导出: 내보내기
|
||||
打印: 인쇄
|
||||
刷新: 새로고침
|
||||
返回: 뒤로
|
||||
提交: 제출
|
||||
重置: 초기화
|
||||
Conform: 확인
|
||||
Discard: 폐기
|
||||
Submit: 제출
|
||||
Reset: 초기화
|
||||
Cancel: 취소
|
||||
搜索: 검색
|
||||
操作: 작업
|
||||
类型: 유형
|
||||
状态: 상태
|
||||
名称: 이름
|
||||
编码: 코드
|
||||
备注: 비고
|
||||
创建时间: 생성 시간
|
||||
更新时间: 업데이트 시간
|
||||
全部: 전체
|
||||
140
i18n/zh/msg.txt
140
i18n/zh/msg.txt
@ -1,140 +0,0 @@
|
||||
API接口: API接口
|
||||
Add Error: Add Error
|
||||
Add Success: Add Success
|
||||
Authorization Error: Authorization Error
|
||||
Cancel: Cancel
|
||||
Conform: Conform
|
||||
Delete Error: Delete Error
|
||||
Delete Success: Delete Success
|
||||
Discard: Discard
|
||||
Error: Error
|
||||
ID: ID
|
||||
Invalid: Invalid
|
||||
Messages array cannot be empty: Messages array cannot be empty
|
||||
Missing required parameter\x3A model: Missing required parameter\x3A model
|
||||
Not enrogh balance to use llm: Not enrogh balance to use llm
|
||||
Please login: Please login
|
||||
Record no exist or with wrong ownership: Record no exist or with wrong ownership
|
||||
Reset: Reset
|
||||
Submit: Submit
|
||||
Success: Success
|
||||
Update Error: Update Error
|
||||
Update Success: Update Success
|
||||
You need login to use llm: You need login to use llm
|
||||
failed: failed
|
||||
id: id
|
||||
model parameter required: model parameter required
|
||||
need a config_data: need a config_data
|
||||
need a llmid: need a llmid
|
||||
ok: ok
|
||||
server error: server error
|
||||
上位系统id: 上位系统id
|
||||
上架: 上架
|
||||
上架状态: 上架状态
|
||||
上线检查: 上线检查
|
||||
下架: 下架
|
||||
主键ID: 主键ID
|
||||
交互内容: 交互内容
|
||||
交易号: 交易号
|
||||
交易成本: 交易成本
|
||||
交易金额: 交易金额
|
||||
从IO文件恢复Usages: 从IO文件恢复Usages
|
||||
任务号: 任务号
|
||||
任务查询间隔(秒): 任务查询间隔(秒)
|
||||
任务结果查询接口名称: 任务结果查询接口名称
|
||||
体验一次: 体验一次
|
||||
使用信息: 使用信息
|
||||
使用日期: 使用日期
|
||||
使用时间: 使用时间
|
||||
使用记录ID: 使用记录ID
|
||||
使用记录id: 使用记录id
|
||||
供应商id: 供应商id
|
||||
供应商模型列表: 供应商模型列表
|
||||
全部: 全部
|
||||
分类: 分类
|
||||
删除成功: 删除成功
|
||||
历史数据为只读,不可修改: 历史数据为只读,不可修改
|
||||
历史数据为只读,不可删除: 历史数据为只读,不可删除
|
||||
历史数据为只读,不可新增: 历史数据为只读,不可新增
|
||||
原因: 原因
|
||||
名称: 名称
|
||||
启用日期: 启用日期
|
||||
响应时间: 响应时间
|
||||
图标id: 图标id
|
||||
处理备注: 处理备注
|
||||
处理时间: 处理时间
|
||||
处理状态: 处理状态
|
||||
备份时间: 备份时间
|
||||
大语言模型: 大语言模型
|
||||
失效日期: 失效日期
|
||||
失败原因: 失败原因
|
||||
失败时间: 失败时间
|
||||
定价ID: 定价ID
|
||||
开始日期: 开始日期
|
||||
恢复Usages: 恢复Usages
|
||||
恢复Usages失败: 恢复Usages失败
|
||||
恢复Usages完成: 恢复Usages完成
|
||||
成功: 成功
|
||||
所属机构id: 所属机构id
|
||||
按供应商: 按供应商
|
||||
按分类: 按分类
|
||||
探索和使用各类AI模型: 探索和使用各类AI模型
|
||||
接口名称: 接口名称
|
||||
提示: 提示
|
||||
操作: 操作
|
||||
无效的参数,未找到模型ID: 无效的参数,未找到模型ID
|
||||
无权删除该映射: 无权删除该映射
|
||||
无权操作该模型: 无权操作该模型
|
||||
是否已处理: 是否已处理
|
||||
最低余额: 最低余额
|
||||
机构: 机构
|
||||
查询API: 查询API
|
||||
查询间隔(秒): 查询间隔(秒)
|
||||
检查计费: 检查计费
|
||||
模型: 模型
|
||||
模型API映射表: 模型API映射表
|
||||
模型ID: 模型ID
|
||||
模型id: 模型id
|
||||
模型、分类和API接口为必填项: 模型、分类和API接口为必填项
|
||||
模型使用: 模型使用
|
||||
模型使用历史记录: 模型使用历史记录
|
||||
模型分类ID: 模型分类ID
|
||||
模型列表: 模型列表
|
||||
模型名称: 模型名称
|
||||
模型广场: 模型广场
|
||||
模型机构id: 模型机构id
|
||||
模型用量: 模型用量
|
||||
模型类型: 模型类型
|
||||
模型类型管理: 模型类型管理
|
||||
模型类目: 模型类目
|
||||
没找到模型: 没找到模型
|
||||
没有找到需要恢复的记录: 没有找到需要恢复的记录
|
||||
添加成功: 添加成功
|
||||
添加映射: 添加映射
|
||||
状态: 状态
|
||||
用户: 用户
|
||||
用户id: 用户id
|
||||
用户机构id: 用户机构id
|
||||
类型名: 类型名
|
||||
类型名不能为空: 类型名不能为空
|
||||
类型说明: 类型说明
|
||||
结束日期: 结束日期
|
||||
结束时间: 结束时间
|
||||
缺少ID参数: 缺少ID参数
|
||||
缺省分类: 缺省分类
|
||||
能力映射: 能力映射
|
||||
计费项目: 计费项目
|
||||
记录ID不能为空: 记录ID不能为空
|
||||
记账失败记录: 记账失败记录
|
||||
记账状态: 记账状态
|
||||
识别名: 识别名
|
||||
该模型的此API映射已存在: 该模型的此API映射已存在
|
||||
说明: 说明
|
||||
账户余额不够: 账户余额不够
|
||||
选择分类: 选择分类
|
||||
重试: 重试
|
||||
重试次数: 重试次数
|
||||
重试记账: 重试记账
|
||||
金额: 金额
|
||||
错误: 错误
|
||||
间隔(秒): 间隔(秒)
|
||||
@ -1,47 +0,0 @@
|
||||
{
|
||||
"appcodes": [
|
||||
{
|
||||
"id": "llm_status",
|
||||
"name": "模型上架状态",
|
||||
"hierarchy_flg": "0"
|
||||
},
|
||||
{
|
||||
"id": "llmusage_status",
|
||||
"name": "调用状态",
|
||||
"hierarchy_flg": "0"
|
||||
},
|
||||
{
|
||||
"id": "accounting_status",
|
||||
"name": "记账状态",
|
||||
"hierarchy_flg": "0"
|
||||
},
|
||||
{
|
||||
"id": "handled_flg",
|
||||
"name": "是否已处理",
|
||||
"hierarchy_flg": "0"
|
||||
},
|
||||
{
|
||||
"id": "isdefaultcatelog_flg",
|
||||
"name": "是否缺省分类",
|
||||
"hierarchy_flg": "0"
|
||||
}
|
||||
],
|
||||
"appcodes_kv": [
|
||||
{"id": "llm_status_published", "parentid": "llm_status", "k": "published", "v": "已上架"},
|
||||
{"id": "llm_status_unpublished", "parentid": "llm_status", "k": "unpublished", "v": "已下架"},
|
||||
|
||||
{"id": "llmusage_status_succeeded", "parentid": "llmusage_status", "k": "SUCCEEDED", "v": "成功"},
|
||||
{"id": "llmusage_status_failed", "parentid": "llmusage_status", "k": "FAILED", "v": "失败"},
|
||||
{"id": "llmusage_status_unknown", "parentid": "llmusage_status", "k": "UNKNOWN", "v": "未知"},
|
||||
|
||||
{"id": "accounting_status_created", "parentid": "accounting_status", "k": "created", "v": "待记账"},
|
||||
{"id": "accounting_status_accounted", "parentid": "accounting_status", "k": "accounted", "v": "已记账"},
|
||||
{"id": "accounting_status_failed", "parentid": "accounting_status", "k": "failed", "v": "记账失败"},
|
||||
|
||||
{"id": "handled_flg_0", "parentid": "handled_flg", "k": "0", "v": "未处理"},
|
||||
{"id": "handled_flg_1", "parentid": "handled_flg", "k": "1", "v": "已处理"},
|
||||
|
||||
{"id": "isdefaultcatelog_flg_0", "parentid": "isdefaultcatelog_flg", "k": "0", "v": "否"},
|
||||
{"id": "isdefaultcatelog_flg_1", "parentid": "isdefaultcatelog_flg", "k": "1", "v": "是"}
|
||||
]
|
||||
}
|
||||
101
json/llm.json
101
json/llm.json
@ -4,59 +4,21 @@
|
||||
"params": {
|
||||
"sortby":"model",
|
||||
"logined_userorgid": "ownerid",
|
||||
"data_filter": {
|
||||
"AND": [
|
||||
{"field": "name", "op": "LIKE", "var": "name_input"},
|
||||
{"field": "model", "op": "LIKE", "var": "model_input"},
|
||||
{"field": "providerid", "op": "=", "var": "providerid_input"},
|
||||
{"field": "upappid", "op": "=", "var": "upappid_input"},
|
||||
{"field": "status", "op": "=", "var": "status_input"}
|
||||
]
|
||||
},
|
||||
"filter_labels": {
|
||||
"name_input": "名称",
|
||||
"model_input": "识别名",
|
||||
"providerid_input": "供应商",
|
||||
"upappid_input": "上位系统",
|
||||
"status_input": "上架状态"
|
||||
},
|
||||
"browserfields": {
|
||||
"exclouded": ["id", "ownerid"],
|
||||
"alters": {
|
||||
"ppid":{
|
||||
"dataurl":"{{entire_url('/pricing/get_all_pricing_programs.dspy')}}",
|
||||
"textField": "name",
|
||||
"ppid":{
|
||||
"dataurl":"{{entire_url('/pricing/get_all_pricing_programs.dspy')}}",
|
||||
"textField": "name",
|
||||
"valueField": "id"
|
||||
},
|
||||
"providerid": {
|
||||
"uitype": "code",
|
||||
"dataurl": "{{entire_url('../api/get_search_providerid.dspy')}}",
|
||||
"valueField": "providerid",
|
||||
"textField": "providerid_text"
|
||||
},
|
||||
"upappid": {
|
||||
"uitype": "code",
|
||||
"dataurl": "{{entire_url('../api/get_search_upappid.dspy')}}",
|
||||
"valueField": "upappid",
|
||||
"textField": "upappid_text"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"toolbar": {
|
||||
"tools":[
|
||||
{
|
||||
"name":"launch_check",
|
||||
"label":"上线检查",
|
||||
"selected_row":true
|
||||
},
|
||||
{
|
||||
"name":"publish",
|
||||
"label":"上架",
|
||||
"selected_row":true
|
||||
},
|
||||
{
|
||||
"name":"unpublish",
|
||||
"label":"下架",
|
||||
"name":"test",
|
||||
"label":"体验",
|
||||
"selected_row":true
|
||||
}
|
||||
]
|
||||
@ -64,66 +26,25 @@
|
||||
"binds":[
|
||||
{
|
||||
"wid":"self",
|
||||
"event":"launch_check",
|
||||
"event":"test",
|
||||
"actiontype":"urlwidget",
|
||||
"target":"PopupWindow",
|
||||
"popup_options":{
|
||||
"title":"上线检查",
|
||||
"cwidth":25,
|
||||
"cheight":20
|
||||
"title":"model Test",
|
||||
"cwidth":22,
|
||||
"height":"75%"
|
||||
},
|
||||
"options":{
|
||||
"url":"{{entire_url('./llm_launch_check.ui')}}",
|
||||
"url":"{{entire_url('./llm_dialog.ui')}}",
|
||||
"params":{
|
||||
"id":"${id}"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"wid":"self",
|
||||
"event":"publish",
|
||||
"actiontype":"urlwidget",
|
||||
"target":"PopupWindow",
|
||||
"popup_options":{
|
||||
"title":"上架",
|
||||
"cwidth":20,
|
||||
"cheight":8
|
||||
},
|
||||
"options":{
|
||||
"url":"{{entire_url('../api/llm_status_update.dspy')}}",
|
||||
"params":{
|
||||
"id":"${id}",
|
||||
"action":"published"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"wid":"self",
|
||||
"event":"unpublish",
|
||||
"actiontype":"urlwidget",
|
||||
"target":"PopupWindow",
|
||||
"popup_options":{
|
||||
"title":"下架",
|
||||
"cwidth":20,
|
||||
"cheight":8
|
||||
},
|
||||
"options":{
|
||||
"url":"{{entire_url('../api/llm_status_update.dspy')}}",
|
||||
"params":{
|
||||
"id":"${id}",
|
||||
"action":"unpublished"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"editexclouded": [
|
||||
"id", "ownerid"
|
||||
],
|
||||
"editable": {
|
||||
"new_data_url": "{{entire_url('../api/llm_create.dspy')}}",
|
||||
"update_data_url": "{{entire_url('../api/llm_update.dspy')}}",
|
||||
"delete_data_url": "{{entire_url('../api/llm_delete.dspy')}}"
|
||||
},
|
||||
"subtables":[
|
||||
{
|
||||
"field":"llmid",
|
||||
|
||||
@ -5,18 +5,6 @@
|
||||
"browserfields": {
|
||||
"exclouded": ["id", "llmid"],
|
||||
"alters": {
|
||||
"apiname": {
|
||||
"uitype": "code",
|
||||
"dataurl": "{{entire_url('../api/get_search_apiname.dspy')}}?llmid={{params_kw.llmid}}",
|
||||
"valueField": "apiname",
|
||||
"textField": "apiname_text"
|
||||
},
|
||||
"query_apiname": {
|
||||
"uitype": "code",
|
||||
"dataurl": "{{entire_url('../api/get_search_apiname.dspy')}}?allow_empty=1&llmid={{params_kw.llmid}}",
|
||||
"valueField": "apiname",
|
||||
"textField": "apiname_text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"editexclouded": ["id", "llmid"]
|
||||
|
||||
@ -1,21 +0,0 @@
|
||||
{
|
||||
"tblname": "llm_metrics",
|
||||
"title": "模型使用指标",
|
||||
"params": {
|
||||
"sortby": "use_time",
|
||||
"logined_userorgid": "userorgid",
|
||||
"browserfields": {
|
||||
"exclouded": ["id", "taskid"],
|
||||
"cwidth": {}
|
||||
},
|
||||
"editexclouded": ["id", "amount", "responsed_seconds", "finish_seconds", "status", "taskid"],
|
||||
"data_filter": {
|
||||
"AND": [
|
||||
{"field": "userorgid", "op": "=", "var": "userorgid"},
|
||||
{"field": "use_date", "op": "between", "var": "use_date"}
|
||||
]
|
||||
},
|
||||
"filter_labels": {"use_date": "使用日期"},
|
||||
"filter_title": "查询条件"
|
||||
}
|
||||
}
|
||||
@ -10,9 +10,9 @@
|
||||
"delete_data_url": "{{entire_url('../api/llmcatelog_delete.dspy')}}"
|
||||
},
|
||||
"browserfields": {
|
||||
"exclouded": [],
|
||||
"exclouded": ["id"],
|
||||
"alters": {}
|
||||
},
|
||||
"editexclouded": []
|
||||
"editexclouded": ["id"]
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,70 +3,15 @@
|
||||
"title": "模型使用",
|
||||
"params": {
|
||||
"sortby": "use_time desc",
|
||||
"toolbar": {
|
||||
"tools": [
|
||||
{
|
||||
"name": "show_usages",
|
||||
"label": "使用信息",
|
||||
"selected_row": true,
|
||||
"icon": "{{entire_url('/bricks/imgs/database.svg')}}"
|
||||
},
|
||||
{
|
||||
"name": "show_ioinfo",
|
||||
"label": "交互内容",
|
||||
"selected_row": true,
|
||||
"icon": "{{entire_url('/bricks/imgs/chat-user.svg')}}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"binds": [
|
||||
{
|
||||
"wid": "self",
|
||||
"event": "show_usages",
|
||||
"actiontype": "urlwidget",
|
||||
"target": "PopupWindow",
|
||||
"popup_options": {
|
||||
"title": "使用信息",
|
||||
"width": "70%",
|
||||
"height": "70%"
|
||||
},
|
||||
"options": {
|
||||
"url": "{{entire_url('../llmusage_usages_display.dspy')}}?id=${id}$"
|
||||
}
|
||||
},
|
||||
{
|
||||
"wid": "self",
|
||||
"event": "show_ioinfo",
|
||||
"actiontype": "urlwidget",
|
||||
"target": "PopupWindow",
|
||||
"popup_options": {
|
||||
"title": "交互内容",
|
||||
"width": "70%",
|
||||
"height": "70%"
|
||||
},
|
||||
"options": {
|
||||
"url": "{{entire_url('../llmusage_ioinfo_display.dspy')}}?id=${id}$"
|
||||
}
|
||||
}
|
||||
],
|
||||
"browserfields": {
|
||||
"exclouded": [
|
||||
"id",
|
||||
"usages",
|
||||
"ioinfo"
|
||||
],
|
||||
"exclouded": ["id"],
|
||||
"alters": {}
|
||||
},
|
||||
"editexclouded": [
|
||||
"id",
|
||||
"usages",
|
||||
"ioinfo"
|
||||
],
|
||||
"editexclouded": ["id"],
|
||||
"editable": {
|
||||
"get_data_url": "{{entire_url('../api/llmusage_list.dspy')}}?pagerows=50",
|
||||
"new_data_url": "{{entire_url('../api/llmusage_create.dspy')}}",
|
||||
"update_data_url": "{{entire_url('../api/llmusage_update.dspy')}}",
|
||||
"delete_data_url": "{{entire_url('../api/llmusage_delete.dspy')}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,63 +3,23 @@
|
||||
"title": "记账失败记录",
|
||||
"params": {
|
||||
"sortby": "failed_time desc",
|
||||
"toolbar": {
|
||||
"tools": [
|
||||
{
|
||||
"name": "show_reason",
|
||||
"label": "原因",
|
||||
"selected_row": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"binds": [
|
||||
{
|
||||
"wid": "self",
|
||||
"event": "show_reason",
|
||||
"actiontype": "urlwidget",
|
||||
"target": "PopupWindow",
|
||||
"popup_options": {
|
||||
"title": "失败原因",
|
||||
"cwidth": 30,
|
||||
"cheight": 20
|
||||
},
|
||||
"options": {
|
||||
"url": "{{entire_url('../api/show_failed_reason.dspy')}}?id=${id}$"
|
||||
}
|
||||
}
|
||||
],
|
||||
"browserfields": {
|
||||
"exclouded": [
|
||||
"id",
|
||||
"failed_reason"
|
||||
],
|
||||
"exclouded": ["id"],
|
||||
"alters": {
|
||||
"llmid": {
|
||||
"valueField": "llmid",
|
||||
"textField": "llmid_text",
|
||||
"uitype": "code"
|
||||
},
|
||||
"userid": {
|
||||
"valueField": "userid",
|
||||
"textField": "userid_text",
|
||||
"uitype": "code"
|
||||
},
|
||||
"userorgid": {
|
||||
"valueField": "userorgid",
|
||||
"textField": "userorgid_text",
|
||||
"uitype": "code"
|
||||
"handled": {
|
||||
"uitype": "code",
|
||||
"data": [
|
||||
{"value": "0", "text": "未处理"},
|
||||
{"value": "1", "text": "已处理"}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"editexclouded": [
|
||||
"id",
|
||||
"llmusageid",
|
||||
"failed_time"
|
||||
],
|
||||
"editexclouded": ["id", "llmusageid", "failed_time"],
|
||||
"editable": {
|
||||
"new_data_url": "{{entire_url('../api/llmusage_accounting_failed_create.dspy')}}",
|
||||
"update_data_url": "{{entire_url('../api/llmusage_accounting_failed_update.dspy')}}",
|
||||
"delete_data_url": "{{entire_url('../api/llmusage_accounting_failed_delete.dspy')}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,18 +0,0 @@
|
||||
{
|
||||
"tblname": "user_llm_policy",
|
||||
"title": "用户模型策略",
|
||||
"params": {
|
||||
"sortby": "userid",
|
||||
"logined_userorgid": "orgid",
|
||||
"browserfields": {
|
||||
"exclouded": ["id", "orgid", "created_at", "updated_at"],
|
||||
"cwidth": {}
|
||||
},
|
||||
"editexclouded": ["id", "orgid", "created_at", "updated_at", "created_by"],
|
||||
"data_filter": {
|
||||
"AND": [
|
||||
{"field": "orgid", "op": "=", "var": "orgid"}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2,7 +2,7 @@ import asyncio
|
||||
import json
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
from appPublic.log import exception, debug, info
|
||||
from appPublic.log import exception, debug
|
||||
from appPublic.uniqueID import getID
|
||||
from appPublic.dictObject import DictObject
|
||||
from sqlor.dbpools import get_sor_context
|
||||
@ -21,43 +21,19 @@ async def llm_charging(ppid, llmusage):
|
||||
e = Exception(f'{ppid=}, {usages=}{llmusage.id=} env.buffered_charging() return None')
|
||||
exception(f'{e}')
|
||||
raise e
|
||||
return None
|
||||
amount = 0
|
||||
cost = 0
|
||||
for p in prices:
|
||||
amount += p.amount
|
||||
if p.cost:
|
||||
cost += p.cost
|
||||
discount = await env.get_customer_discount(llmusage.ownerid,
|
||||
llmusage.userorgid)
|
||||
# Get pricing program currency (provider's settlement currency)
|
||||
cost_currency = 'CNY'
|
||||
try:
|
||||
pp = await env.get_ppid_pricing(ppid)
|
||||
if pp and hasattr(pp, 'pp') and hasattr(pp.pp, 'currency'):
|
||||
cost_currency = pp.pp.currency or 'CNY'
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Get user's billing currency
|
||||
user_currency = 'CNY'
|
||||
try:
|
||||
user_currency = await env.get_user_currency(llmusage.userorgid)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Convert cost to user currency at real-time rate
|
||||
cost_in_user_currency = amount
|
||||
if cost_currency != user_currency:
|
||||
rate = await env.get_exchange_rate(cost_currency, user_currency, 'buy_rate')
|
||||
cost_in_user_currency = round(amount * rate, 2)
|
||||
|
||||
# Convert both to base currency (CNY) for reporting
|
||||
amount_base = await env.convert_to_base(cost_in_user_currency, user_currency, 'sell_rate')
|
||||
|
||||
return DictObject(**{
|
||||
'original_amount': amount,
|
||||
'amount': cost_in_user_currency * discount,
|
||||
'cost_currency': cost_currency,
|
||||
'amount_currency': user_currency,
|
||||
'amount_base': amount_base * discount,
|
||||
'cost_base': await env.convert_to_base(amount, cost_currency, 'buy_rate'),
|
||||
'amount': amount * discount,
|
||||
'cost': cost
|
||||
})
|
||||
|
||||
async def checkCustomerBalance(llmid, userid, userorgid, catelogid=None):
|
||||
@ -65,7 +41,7 @@ async def checkCustomerBalance(llmid, userid, userorgid, catelogid=None):
|
||||
debug(f'checkCustomerBalance(): llmid is None')
|
||||
return False
|
||||
env = ServerEnv()
|
||||
llm = await get_llmage_llm(llmid)
|
||||
llm = await get_llm(llmid)
|
||||
if llm.ownerid == userorgid:
|
||||
debug(f'self orgid user')
|
||||
return True
|
||||
@ -81,53 +57,44 @@ async def checkCustomerBalance(llmid, userid, userorgid, catelogid=None):
|
||||
bal = 0 if balance is None else balance
|
||||
if llm.min_balance is None:
|
||||
llm.min_balance = 0.00
|
||||
if not llm.ppid:
|
||||
debug(f'{llm.ppid=} is empty, model unavailable')
|
||||
return False
|
||||
try:
|
||||
await env.get_ppid_pricing(llm.ppid)
|
||||
except Exception as e:
|
||||
debug(f'{llm.ppid=} has no pricing data for today: {e}')
|
||||
return False
|
||||
ret = llm.min_balance < bal
|
||||
debug(f'{llm.ppid=}, {llm.min_balance=}, {bal=}, pricing OK')
|
||||
ret = llm.ppid and llm.min_balance < bal
|
||||
debug(f'{llm.ppid=}, {llm.min_balance=}, {bal=}')
|
||||
return ret
|
||||
|
||||
async def llm_accounting(llmusage):
|
||||
env = ServerEnv()
|
||||
llmid = llmusage.llmid
|
||||
llm = await get_llmage_llm(llmid)
|
||||
if llm is None:
|
||||
async with get_sor_context(env, 'llmage') as sor:
|
||||
ns = {
|
||||
'id': llmusage.id,
|
||||
'accounting_status': 'failed'
|
||||
}
|
||||
await sor.U('llmusage', ns)
|
||||
e = Exception(f'llm not found({llmid})')
|
||||
exception(f'{e}')
|
||||
raise e
|
||||
if llm.ppid is None:
|
||||
async with get_sor_context(env, 'llmage') as sor:
|
||||
ns = {
|
||||
'id': llmusage.id,
|
||||
'accounting_status': 'failed'
|
||||
}
|
||||
await sor.U('llmusage', ns)
|
||||
e = Exception(f'llm ({llmid}) donot has a pricing_program')
|
||||
exception(f'{e}')
|
||||
raise e
|
||||
customerid = llmusage.userorgid
|
||||
userid = llmusage.userid
|
||||
resellerid = llm.ownerid
|
||||
providerid = llm.providerid
|
||||
trans_amount = llmusage.amount
|
||||
trans_cost = llmusage.cost
|
||||
amount_currency = getattr(llmusage, 'amount_currency', 'CNY')
|
||||
cost_currency = getattr(llmusage, 'cost_currency', 'CNY')
|
||||
amount_base = getattr(llmusage, 'amount_base', trans_amount)
|
||||
cost_base = getattr(llmusage, 'cost_base', trans_cost)
|
||||
async with get_sor_context(env, 'llmage') as sor:
|
||||
sql = """select a.*, b.ppid from llm a, llm_api_map b
|
||||
where a.id=${llmid}$
|
||||
and a.id = b.llmid
|
||||
and b.isdefaultcatelog = '1'
|
||||
"""
|
||||
recs = await sor.sqlExe(sql, {'llmid': llmusage.llmid})
|
||||
if len(recs) == 0:
|
||||
ns = {
|
||||
'id': llmusage.id,
|
||||
'accounting_status': 'failed'
|
||||
}
|
||||
await sor.U('llmusage', ns)
|
||||
e = Exception(f'llm not found({llmid})')
|
||||
exception(f'{e}')
|
||||
raise e
|
||||
if recs[0].ppid is None:
|
||||
ns = {
|
||||
'id': llmusage.id,
|
||||
'accounting_status': 'failed'
|
||||
}
|
||||
await sor.U('llmusage', ns)
|
||||
e = Exception(f'llm ({llmid}) donot has a pricing_program')
|
||||
exception(f'{e}')
|
||||
raise e
|
||||
customerid = llmusage.userorgid
|
||||
userid = llmusage.userid
|
||||
resellerid = recs[0].ownerid
|
||||
providerid = recs[0].providerid
|
||||
trans_amount = llmusage.amount
|
||||
trans_cost = llmusage.cost
|
||||
biz_date = await env.get_business_date(sor)
|
||||
timestamp = env.timestampstr()
|
||||
orderid = getID()
|
||||
@ -148,8 +115,7 @@ async def llm_accounting(llmusage):
|
||||
"orderid": orderid,
|
||||
"productid": llmid,
|
||||
"product_cnt": 1,
|
||||
"trans_amount": trans_amount,
|
||||
"currency": amount_currency,
|
||||
"trans_amount": trans_amount
|
||||
}
|
||||
await sor.C('biz_orderdetail', orderdetail)
|
||||
ais = []
|
||||
@ -163,12 +129,8 @@ async def llm_accounting(llmusage):
|
||||
ai0.timestamp = timestamp
|
||||
ai0.productid = llmid
|
||||
ai0.transamt = trans_amount
|
||||
ai0.currency = amount_currency
|
||||
ai0.base_amount = amount_base
|
||||
ai0.variable = {
|
||||
"交易金额": trans_amount,
|
||||
"交易币种": amount_currency,
|
||||
"折本位币": amount_base,
|
||||
"交易手续费": 0
|
||||
}
|
||||
ais.append(ai0)
|
||||
@ -182,8 +144,6 @@ async def llm_accounting(llmusage):
|
||||
ai1.providerid = providerid
|
||||
ai1.productid = llmid
|
||||
ai1.transamt = trans_cost
|
||||
ai1.currency = cost_currency
|
||||
ai1.base_amount = cost_base
|
||||
ai1.variable = {
|
||||
"采购成本": trans_cost
|
||||
}
|
||||
@ -195,13 +155,6 @@ async def llm_accounting(llmusage):
|
||||
'accounting_status': 'accounted'
|
||||
}
|
||||
await sor.U('llmusage', ns)
|
||||
# Finalize Redis balance reservation (non-critical)
|
||||
try:
|
||||
from .balance import finalize_balance
|
||||
await finalize_balance(env, llmusage.id, trans_amount,
|
||||
llmid=llmusage.llmid)
|
||||
except Exception as e:
|
||||
exception(f'finalize_balance failed (non-critical): {e}')
|
||||
|
||||
async def get_accounting_llmusages(luid=None):
|
||||
env = ServerEnv()
|
||||
@ -210,7 +163,7 @@ async def get_accounting_llmusages(luid=None):
|
||||
dt = datetime.fromtimestamp(t)
|
||||
tsstr = dt.strftime('%Y-%m-%d %H:%M:%S.') + f'{dt.microsecond // 1000:03d}'
|
||||
async with get_sor_context(env, 'llmage') as sor:
|
||||
sql = """select a.*, b.model, c.ppid
|
||||
sql = """select a.*, c.ppid
|
||||
from llmusage a, llm b, llm_api_map c
|
||||
where a.llmid = b.id
|
||||
and a.llmid = c.llmid
|
||||
@ -223,6 +176,7 @@ where a.llmid = b.id
|
||||
sql += " and a.id=${luid}$"
|
||||
ns['luid'] = luid
|
||||
recs = await sor.sqlExe(sql, ns)
|
||||
# debug(f'{sql=}, {ns=}, {len(recs)=}')
|
||||
for r in recs:
|
||||
if r.usages is None:
|
||||
try:
|
||||
@ -231,25 +185,24 @@ where a.llmid = b.id
|
||||
continue
|
||||
r.usages = output.get('usage')
|
||||
if r.usages is None:
|
||||
debug(f'{r.usages=} is None, accounting failed')
|
||||
debug(f'{r.usages=} is None, accoiunting failed')
|
||||
await llm_accoung_failed(r.id, reason='usages is None')
|
||||
continue
|
||||
d = None
|
||||
try:
|
||||
debug(f'{r.ppid=}, {r.usages=} {r.id=}')
|
||||
d = await llm_charging(r.ppid, r)
|
||||
|
||||
except Exception as e:
|
||||
exception(f'{r.ppid=}, {r.usages=} llm_charging() failed,{e}')
|
||||
await llm_accoung_failed(r.id, reason=f'llm_charging failed: {e}')
|
||||
continue
|
||||
r.amount = d.amount
|
||||
r.cost = d.cost
|
||||
ns = {
|
||||
'id': r.id,
|
||||
'amount': r.amount,
|
||||
'amount_currency': getattr(d, 'amount_currency', 'CNY'),
|
||||
'amount_base': getattr(d, 'amount_base', r.amount),
|
||||
'cost_currency': getattr(d, 'cost_currency', 'CNY'),
|
||||
'cost_base': getattr(d, 'cost_base', 0),
|
||||
'cost': r.cost,
|
||||
'usage': json.dumps(r.usage, ensure_ascii=False, indent=4)
|
||||
}
|
||||
await sor.U('llmusage', ns)
|
||||
@ -277,6 +230,7 @@ async def llm_accoung_failed(luid, reason=None):
|
||||
'use_date': r.use_date,
|
||||
'use_time': r.use_time,
|
||||
'amount': r.amount,
|
||||
'cost': r.cost,
|
||||
'failed_reason': reason or 'accounting failed',
|
||||
'failed_time': env.timestampstr(),
|
||||
'retry_count': 0,
|
||||
@ -302,8 +256,8 @@ WHERE accounting_status='accounted' AND use_date < ${cutoff_date}$"""
|
||||
|
||||
# Step 1: INSERT INTO history SELECT from main table
|
||||
insert_sql = """INSERT INTO llmusage_history
|
||||
(id, llmid, use_date, use_time, userid, usages, ioinfo, transno, responsed_seconds, finish_seconds, status, taskid, amount, userorgid, ownerid, accounting_status, tenantid, backup_time)
|
||||
SELECT id, llmid, use_date, use_time, userid, usages, ioinfo, transno, responsed_seconds, finish_seconds, status, taskid, amount, userorgid, ownerid, accounting_status, tenantid, ${ts}$
|
||||
(id, llmid, use_date, use_time, userid, usages, ioinfo, transno, responsed_seconds, finish_seconds, status, taskid, amount, cost, userorgid, ownerid, accounting_status, backup_time)
|
||||
SELECT id, llmid, use_date, use_time, userid, usages, ioinfo, transno, responsed_seconds, finish_seconds, status, taskid, amount, cost, userorgid, ownerid, accounting_status, ${ts}$
|
||||
FROM llmusage
|
||||
WHERE accounting_status='accounted' AND use_date < ${cutoff_date}$"""
|
||||
await sor.execute(insert_sql, {'cutoff_date': cutoff_date, 'ts': ts})
|
||||
@ -368,33 +322,23 @@ order by failed_time desc limit {page_size} offset {offset}"""
|
||||
|
||||
async def backend_accounting():
|
||||
env = ServerEnv()
|
||||
info(f"backend accounting started ...")
|
||||
debug(f'backend accounting started ...')
|
||||
last_backup_date = None
|
||||
while True:
|
||||
try:
|
||||
lus = await get_accounting_llmusages()
|
||||
info(f"accounting loop: got {len(lus)} records")
|
||||
except Exception as e:
|
||||
exception(f"get_accounting_llmusages failed: {e}")
|
||||
exception(f'{e}')
|
||||
lus = []
|
||||
for lu in lus:
|
||||
try:
|
||||
tpac = await get_user_tpac(lu.userid)
|
||||
if tpac:
|
||||
debug(f'{lu.id=},{lu.userid=}, {tpac=}, go tpac')
|
||||
await tpac_accounting(tpac, lu.userid, lu.llmid, lu.amount, lu.usages, lu.id, lu.model)
|
||||
await tpac_accounting(tpac, lu.userid, lu.llmid, lu.amount, lu.usages, lu.id)
|
||||
else:
|
||||
debug(f'{lu.id=},{lu.userid=}, {tpac=}, go local')
|
||||
await llm_accounting(lu)
|
||||
# 记账成功,清理对应的失败记录
|
||||
try:
|
||||
async with get_sor_context(env, 'llmage') as sor:
|
||||
await sor.execute(
|
||||
"DELETE FROM llmusage_accounting_failed WHERE llmusageid=${luid}$",
|
||||
{'luid': lu.id}
|
||||
)
|
||||
except Exception as e2:
|
||||
debug(f'清理失败记录异常(不影响记账): {e2}')
|
||||
except Exception as e:
|
||||
exception(f'{e}, {lu.id=}')
|
||||
await llm_accoung_failed(lu.id, reason=str(e))
|
||||
|
||||
@ -13,12 +13,8 @@ from appPublic.base64_to_file import base64_to_file, getFilenameFromBase64
|
||||
from ahserver.serverenv import get_serverenv, ServerEnv
|
||||
from ahserver.filestorage import FileStorage
|
||||
from .accounting import llm_accounting, llm_charging
|
||||
from .balance import refund_balance
|
||||
from .utils import *
|
||||
|
||||
# Global set to keep references to background tasks
|
||||
_background_tasks = set()
|
||||
|
||||
async def get_today_asynctask_list(userid):
|
||||
env = ServerEnv()
|
||||
async with get_sor_context(env, 'llmage') as sor:
|
||||
@ -43,9 +39,7 @@ async def get_asynctask_status(request, taskid):
|
||||
t = timestampAdd(r.use_time, 600)
|
||||
now = time.time()
|
||||
if r.status not in ['UNKNOWN', 'FAILED', 'SUCCEEDED'] and now > t:
|
||||
task = asyncio.create_task(query_task_status(request, r.id))
|
||||
_background_tasks.add(task)
|
||||
task.add_done_callback(_background_tasks.discard)
|
||||
asyncio.create_task(query_task_status(request, r.id))
|
||||
return output
|
||||
return {
|
||||
'taskid': taskid,
|
||||
@ -68,7 +62,7 @@ async def async_uapi_request(request, llm,
|
||||
uapi = env.UpAppApi(request)
|
||||
userid = await env.uapi_data.get_calluserid(llm.upappid, orgid=llm.ownerid)
|
||||
b = None
|
||||
luid = params_kw.get('_luid') or getID()
|
||||
luid = getID()
|
||||
try:
|
||||
start_timestamp = time.time()
|
||||
if llm.callbackurl:
|
||||
@ -78,17 +72,10 @@ async def async_uapi_request(request, llm,
|
||||
try:
|
||||
b = await uapi.call(llm.upappid, llm.apiname, userid, params=params_kw)
|
||||
except Exception as e:
|
||||
# Refund balance reservation on submission failure
|
||||
try:
|
||||
if luid:
|
||||
await refund_balance(ServerEnv(), luid)
|
||||
except Exception:
|
||||
pass
|
||||
estr = erase_apikey(e)
|
||||
ed = {"error": f"ERROR:{estr}", "status": "FAILED"}
|
||||
exception(f'{ed}')
|
||||
estr = json.dumps(ed, ensure_ascii=False)
|
||||
yield f'{estr}\n'
|
||||
yield f'{ed}\n'
|
||||
return
|
||||
if isinstance(b, bytes):
|
||||
b = b.decode('utf-8')
|
||||
@ -114,56 +101,25 @@ async def async_uapi_request(request, llm,
|
||||
llmusage.finish_seconds = finish_seconds
|
||||
llmusage.status = d.status
|
||||
llmusage.userorgid = callerorgid
|
||||
llmusage.tenantid = params_kw.get('tenantid', params_kw.get('tentantid'))
|
||||
llmusage.ownerid = llm.ownerid
|
||||
llmusage.accounting_status = 'created'
|
||||
b = json.dumps(d, ensure_ascii=False)
|
||||
yield b
|
||||
# await write_llmusage(llmusage)
|
||||
await write_llmusage(llmusage)
|
||||
# if llm.callbackurl:
|
||||
# return
|
||||
if d.status == 'FAILED':
|
||||
e = Exception(f'resp={d} FFAILED')
|
||||
return
|
||||
task = asyncio.create_task(query_task_status(request, luid))
|
||||
_background_tasks.add(task)
|
||||
task.add_done_callback(_background_tasks.discard)
|
||||
asyncio.create_task(query_task_status(request, luid))
|
||||
|
||||
except Exception as e:
|
||||
# Refund balance reservation on outer failure
|
||||
try:
|
||||
if luid:
|
||||
await refund_balance(ServerEnv(), luid)
|
||||
except Exception:
|
||||
pass
|
||||
ed = {"error": f"ERROR:{e}", "status": "FAILED"}
|
||||
s = json.dumps(ed, ensure_ascii=False)
|
||||
s = ''.join(s.split('\n'))
|
||||
exception(s)
|
||||
yield f'{s}\n'
|
||||
llmusage = DictObject()
|
||||
llmusage.id = luid
|
||||
llmusage.llmid = llm.id
|
||||
llmusage.use_date = curDateString()
|
||||
llmusage.use_time = timestampstr()
|
||||
llmusage.userid = callerid
|
||||
ioinfo = {
|
||||
"input": params_kw,
|
||||
'output': [ed]
|
||||
}
|
||||
webpath = await write_llmio(llmusage.id, ioinfo)
|
||||
llmusage.ioinfo = webpath
|
||||
llmusage.taskid = d.taskid
|
||||
llmusage.transno = params_kw.transno
|
||||
llmusage.responsed_seconds = responsed_seconds
|
||||
llmusage.finish_seconds = finish_seconds
|
||||
llmusage.status = 'FAILED'
|
||||
llmusage.userorgid = callerorgid
|
||||
llmusage.tenantid = params_kw.get('tenantid', params_kw.get('tentantid'))
|
||||
llmusage.ownerid = llm.ownerid
|
||||
return
|
||||
finally:
|
||||
await write_llmusage(llmusage)
|
||||
|
||||
async def modify_llmusage(ns):
|
||||
env = ServerEnv()
|
||||
@ -185,15 +141,9 @@ async def get_llm_llmusage(luid):
|
||||
return
|
||||
if llmusage.status == 'FAILED':
|
||||
return
|
||||
# Use JOIN to get query_apiname/query_period from llm_api_map
|
||||
sql = """select a.id, a.name, a.model, a.upappid, a.ownerid, a.status,
|
||||
m.apiname, m.query_apiname, m.query_period, m.ppid
|
||||
from llm a
|
||||
join llm_api_map m on a.id = m.llmid
|
||||
where a.id = ${llmid}$ and m.isdefaultcatelog = '1'"""
|
||||
llms = await sor.sqlExe(sql, {'llmid': llmusage.llmid})
|
||||
llms = await sor.R('llm', {'id': llmusage.llmid})
|
||||
if len(llms) == 0:
|
||||
e = Exception(f'{llmusage.llmid=} not found in llm/llm_api_map')
|
||||
e = Exception(f'{llmusage.llmid=} not found in llm')
|
||||
exception(f'{e}')
|
||||
raise e
|
||||
llm = llms[0]
|
||||
@ -208,128 +158,46 @@ async def query_task_status(request, luid, onetime=False):
|
||||
upappid = llm.upappid
|
||||
apinames = llm.query_apiname.split(',')
|
||||
|
||||
try:
|
||||
for apiname in apinames:
|
||||
while True:
|
||||
lastoutout = await get_lastoutput(llmusage.ioinfo)
|
||||
if lastoutout.get('status', '') in ['UNKNOWN', 'FAILED', 'SUCCEEDED']:
|
||||
critical(f"{lastoutout.get('status', '')=}")
|
||||
return
|
||||
ns = {'taskid': taskid}
|
||||
new_output = b = d = None
|
||||
try:
|
||||
b = await uapi.call(upappid, apiname, userid, params=ns)
|
||||
if isinstance(b, bytes):
|
||||
b = b.decode('utf-8')
|
||||
new_output = json.loads(b)
|
||||
except Exception as e:
|
||||
exception(f'{e}, {b=}')
|
||||
new_output = {
|
||||
'status': 'FAILED',
|
||||
'error': f'{b},{e}'
|
||||
}
|
||||
if not new_output.get('status'):
|
||||
e = Exception(f"{new_output=} {upappid=}, {apiname=} has not status field")
|
||||
critical(f'{e}')
|
||||
raise e
|
||||
if lastoutout.get('status', '') != new_output.get('status'):
|
||||
llmusage.status = new_output['status']
|
||||
ns = {
|
||||
'id': llmusage.id,
|
||||
'status': llmusage.status
|
||||
}
|
||||
if 'usage' in new_output.keys():
|
||||
ns['usages'] = json.dumps(new_output['usage'])
|
||||
await append_new_llmoutput(llmusage.ioinfo, new_output)
|
||||
await modify_llmusage(ns)
|
||||
if llmusage.status == 'FAILED':
|
||||
# Async task failed — refund the balance reservation
|
||||
try:
|
||||
await refund_balance(ServerEnv(), luid)
|
||||
except Exception:
|
||||
pass
|
||||
if llmusage.status in ['UNKNOWN', 'FAILED', 'SUCCEEDED']:
|
||||
critical(f'finished .. {llmusage.status=}')
|
||||
return
|
||||
for apiname in apinames:
|
||||
while True:
|
||||
lastoutout = await get_lastoutput(llmusage.ioinfo)
|
||||
if lastoutout['status'] in ['UNKNOWN', 'FAILED', 'SUCCEEDED']:
|
||||
critical(f"{lastoutout['status']=}")
|
||||
return
|
||||
ns = {'taskid': taskid}
|
||||
new_output = b = d = None
|
||||
try:
|
||||
b = await uapi.call(upappid, apiname, userid, params=ns)
|
||||
if isinstance(b, bytes):
|
||||
b = b.decode('utf-8')
|
||||
new_output = json.loads(b)
|
||||
except Exception as e:
|
||||
exception(f'{e}, {b=}')
|
||||
new_output = {
|
||||
'status': 'FAILED',
|
||||
'error': f'{b},{e}'
|
||||
}
|
||||
if not new_output.get('status'):
|
||||
e = Exception(f"{new_output=} {upappid=}, {apiname=} has not status field")
|
||||
critical(f'{e}')
|
||||
raise e
|
||||
if lastoutout['status'] != new_output.get('status'):
|
||||
llmusage.status = new_output['status']
|
||||
ns = {
|
||||
'id': llmusage.id,
|
||||
'status': llmusage.status
|
||||
}
|
||||
if 'usage' in new_output.keys():
|
||||
ns['usages'] = json.dumps(new_output['usage'])
|
||||
await append_new_llmoutput(llmusage.ioinfo, new_output)
|
||||
await modify_llmusage(ns)
|
||||
if llmusage.status in ['UNKNOWN', 'FAILED', 'SUCCEEDED']:
|
||||
critical(f'finished .. {llmusage.status=}')
|
||||
return
|
||||
|
||||
if onetime:
|
||||
critical(f'onetime is true, returned')
|
||||
return
|
||||
await asyncio.sleep(llm.query_period or 30)
|
||||
critical(f'{llm.query_period=} seconds will retry, {new_output["status"]=}')
|
||||
except asyncio.CancelledError:
|
||||
critical(f'query_task_status cancelled for {luid=}')
|
||||
raise
|
||||
except Exception as e:
|
||||
exception(f'query_task_status error for {luid=}: {e}')
|
||||
raise
|
||||
|
||||
|
||||
async def async_uapi_request_product(llm, api_userid, user_id, user_org_id, params_kw, luid):
|
||||
"""Product interface version of async task submission. Returns dict with task info."""
|
||||
env = ServerEnv()
|
||||
from uapi.appapi import UAPI
|
||||
uapi = UAPI(llm.upappid, llm.apiname)
|
||||
b = None
|
||||
try:
|
||||
start_timestamp = time.time()
|
||||
if llm.callbackurl:
|
||||
params_kw.callbackurl = llm.callbackurl
|
||||
|
||||
b = await uapi.call(llm.upappid, llm.apiname, api_userid, params=params_kw)
|
||||
if isinstance(b, bytes):
|
||||
b = b.decode('utf-8')
|
||||
debug(f'async task submitted: {b}')
|
||||
d = DictObject(**json.loads(b))
|
||||
|
||||
responsed_seconds = time.time() - start_timestamp
|
||||
finish_seconds = responsed_seconds
|
||||
|
||||
llmusage = DictObject()
|
||||
llmusage.id = luid
|
||||
llmusage.llmid = llm.id
|
||||
llmusage.use_date = curDateString()
|
||||
llmusage.use_time = timestampstr()
|
||||
llmusage.userid = user_id
|
||||
ioinfo = {"input": dict(params_kw), "output": [d]}
|
||||
webpath = await write_llmio(luid, ioinfo)
|
||||
llmusage.ioinfo = webpath
|
||||
llmusage.taskid = d.taskid
|
||||
llmusage.transno = params_kw.get('transno', luid)
|
||||
llmusage.responsed_seconds = responsed_seconds
|
||||
llmusage.finish_seconds = finish_seconds
|
||||
llmusage.status = d.status
|
||||
llmusage.userorgid = user_org_id
|
||||
llmusage.tenantid = params_kw.get('tenantid', params_kw.get('tentantid'))
|
||||
llmusage.ownerid = llm.ownerid
|
||||
llmusage.accounting_status = 'created'
|
||||
await write_llmusage(llmusage)
|
||||
|
||||
if d.status == 'FAILED':
|
||||
return {
|
||||
'success': False,
|
||||
'message': f'Task submission failed: {d}',
|
||||
'task_id': luid,
|
||||
'status': 'FAILED',
|
||||
}
|
||||
|
||||
# Task submitted successfully — return task info
|
||||
# Background polling is handled by existing query_task_status or callback
|
||||
return {
|
||||
'success': True,
|
||||
'result': {'taskid': d.taskid, 'status': d.status},
|
||||
'usage_data': {},
|
||||
'resource_ref_id': llm.id,
|
||||
'task_id': luid,
|
||||
'external_task_id': d.taskid,
|
||||
'status': d.status,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
exception(f'async_uapi_request_product error: {e}')
|
||||
return {
|
||||
'success': False,
|
||||
'message': str(e),
|
||||
'task_id': luid,
|
||||
'status': 'FAILED',
|
||||
}
|
||||
if onetime:
|
||||
critical(f'onetime is true, returned')
|
||||
return
|
||||
await asyncio.sleep(llm.query_period or 30)
|
||||
critical(f'{llm.query_period=} seconds will retry, {new_output["status"]=}')
|
||||
|
||||
|
||||
@ -1,357 +0,0 @@
|
||||
"""
|
||||
Redis atomic balance reservation for llmage.
|
||||
Prevents concurrent overspend: pre-deduct before inference, settle after accounting.
|
||||
|
||||
Uses a module-level redis.asyncio singleton so the atomic reserve works in
|
||||
EVERY process (web workers + backend_accounting) without needing env.redis
|
||||
to be injected. This fixes the previous fatal issue where env.redis was never
|
||||
set, so reserve always silently fell back to the non-atomic DB check.
|
||||
|
||||
Key patterns:
|
||||
balance:{userorgid} — current pre-deducted balance (int, cents)
|
||||
reserve:{luid} — {userorgid}|{llmid}|{max_cost} (TTL)
|
||||
model:max_cost:{llmid} — historical max actual customer charge (int, cents)
|
||||
|
||||
Reserve TTL: 600s for fast (stream/sync) calls, 3600s for async tasks that
|
||||
may run for minutes before finalize/refund.
|
||||
"""
|
||||
import asyncio
|
||||
from appPublic.log import debug, exception
|
||||
|
||||
# ── Module-level async Redis singleton ───────────────────────
|
||||
_redis = None
|
||||
_redis_lock = asyncio.Lock()
|
||||
|
||||
|
||||
def _redis_url():
|
||||
try:
|
||||
from appPublic.jsonConfig import getConfig
|
||||
config = getConfig()
|
||||
url = getattr(config.website, 'session_redis', None)
|
||||
if url:
|
||||
u = getattr(url, 'url', None)
|
||||
if u:
|
||||
return u
|
||||
except Exception:
|
||||
pass
|
||||
return "redis://127.0.0.1:6379"
|
||||
|
||||
|
||||
async def _get_redis():
|
||||
"""Return a shared redis.asyncio client (lazy singleton)."""
|
||||
global _redis
|
||||
if _redis is not None:
|
||||
return _redis
|
||||
async with _redis_lock:
|
||||
if _redis is None:
|
||||
import redis.asyncio as aioredis
|
||||
_redis = await aioredis.from_url(
|
||||
_redis_url(), decode_responses=True)
|
||||
return _redis
|
||||
|
||||
|
||||
# ── Lua scripts ──────────────────────────────────────────────
|
||||
|
||||
RESERVE_LUA = """
|
||||
local bal_key = KEYS[1]
|
||||
local cost_key = KEYS[2]
|
||||
local reserve_key = KEYS[3]
|
||||
local max_cost = tonumber(ARGV[1])
|
||||
local db_balance = tonumber(ARGV[2]) -- 0 means no DB fallback
|
||||
local reserve_val = ARGV[3]
|
||||
local ttl = tonumber(ARGV[4])
|
||||
|
||||
-- Get or init max_cost: use stored if higher, else set from arg
|
||||
local stored_max = redis.call('GET', cost_key)
|
||||
if stored_max then
|
||||
max_cost = math.max(max_cost, tonumber(stored_max))
|
||||
elseif max_cost > 0 then
|
||||
redis.call('SET', cost_key, max_cost)
|
||||
end
|
||||
if max_cost <= 0 then
|
||||
return {0, 'max_cost is zero'}
|
||||
end
|
||||
|
||||
-- Load or init balance
|
||||
local balance = redis.call('GET', bal_key)
|
||||
if not balance then
|
||||
if db_balance > 0 then
|
||||
balance = db_balance
|
||||
redis.call('SET', bal_key, db_balance)
|
||||
else
|
||||
return {0, 'balance not initialized'}
|
||||
end
|
||||
end
|
||||
balance = tonumber(balance)
|
||||
|
||||
-- Check and deduct
|
||||
if balance < max_cost then
|
||||
return {0, 'insufficient balance'}
|
||||
end
|
||||
redis.call('DECRBY', bal_key, max_cost)
|
||||
redis.call('SETEX', reserve_key, ttl, reserve_val)
|
||||
return {1, max_cost}
|
||||
"""
|
||||
|
||||
FINALIZE_LUA = """
|
||||
local cost_key = KEYS[1]
|
||||
local reserve_key = KEYS[2]
|
||||
local actual_cost = tonumber(ARGV[1])
|
||||
|
||||
-- Atomically pop reserve (GET+DEL is atomic inside a Lua script; GETDEL needs Redis 6.2+)
|
||||
local reserve_val = redis.call('GET', reserve_key)
|
||||
if not reserve_val then
|
||||
return {0, 'no reserve'}
|
||||
end
|
||||
redis.call('DEL', reserve_key)
|
||||
|
||||
-- Parse: userorgid|llmid|max_cost
|
||||
local i = 1
|
||||
local userorgid, llmid, max_cost
|
||||
for part in string.gmatch(reserve_val, '([^|]+)') do
|
||||
if i == 1 then userorgid = part
|
||||
elseif i == 2 then llmid = part
|
||||
elseif i == 3 then max_cost = tonumber(part) end
|
||||
i = i + 1
|
||||
end
|
||||
|
||||
-- Update max_cost if actual exceeds
|
||||
if actual_cost > max_cost then
|
||||
redis.call('SET', cost_key, actual_cost)
|
||||
max_cost = actual_cost
|
||||
end
|
||||
|
||||
-- Adjust balance
|
||||
local bal_key = 'balance:' .. userorgid
|
||||
local diff = max_cost - actual_cost
|
||||
if diff > 0 then
|
||||
redis.call('INCRBY', bal_key, diff)
|
||||
elseif diff < 0 then
|
||||
redis.call('DECRBY', bal_key, -diff)
|
||||
end
|
||||
|
||||
return {1, userorgid, max_cost, actual_cost, diff}
|
||||
"""
|
||||
|
||||
REFUND_LUA = """
|
||||
local reserve_key = KEYS[1]
|
||||
|
||||
local reserve_val = redis.call('GET', reserve_key)
|
||||
if not reserve_val then
|
||||
return {0, 'no reserve'}
|
||||
end
|
||||
redis.call('DEL', reserve_key)
|
||||
|
||||
local i = 1
|
||||
local userorgid, llmid, max_cost
|
||||
for part in string.gmatch(reserve_val, '([^|]+)') do
|
||||
if i == 1 then userorgid = part
|
||||
elseif i == 2 then llmid = part
|
||||
elseif i == 3 then max_cost = tonumber(part) end
|
||||
i = i + 1
|
||||
end
|
||||
|
||||
if max_cost > 0 then
|
||||
redis.call('INCRBY', 'balance:' .. userorgid, max_cost)
|
||||
end
|
||||
return {1, max_cost, userorgid}
|
||||
"""
|
||||
|
||||
|
||||
def _cents(f):
|
||||
return int(round(float(f) * 100))
|
||||
|
||||
|
||||
def _from_cents(c):
|
||||
return round(int(c) / 100, 2)
|
||||
|
||||
|
||||
async def _update_max_cost(llmid, actual_cost):
|
||||
"""Bump model:max_cost if actual exceeds stored value; persist to DB.
|
||||
|
||||
Keeps the pre-deduct baseline growing even when the original reserve was
|
||||
skipped (no_history cold start) — otherwise reserve would never activate.
|
||||
"""
|
||||
try:
|
||||
redis = await _get_redis()
|
||||
cost_key = f'model:max_cost:{llmid}'
|
||||
cost_cents = _cents(actual_cost)
|
||||
stored = await redis.get(cost_key)
|
||||
if stored and int(stored) >= cost_cents:
|
||||
return
|
||||
await redis.set(cost_key, cost_cents)
|
||||
from .utils import update_model_max_cost
|
||||
await update_model_max_cost(llmid, actual_cost)
|
||||
except Exception as e:
|
||||
debug(f'_update_max_cost failed: {e}')
|
||||
|
||||
|
||||
async def reserve_balance(env, llmid, userorgid, luid, ttl=600, userid=None):
|
||||
"""Atomically check balance and deduct max_cost via Redis Lua.
|
||||
|
||||
Policy (centralized so every entry behaves the same):
|
||||
- Self-owned org (llm.ownerid == userorgid): skip reserve.
|
||||
- tpac user (external balance system): skip reserve.
|
||||
- No pricing (ppid empty): skip reserve (availability handled elsewhere).
|
||||
|
||||
Returns:
|
||||
{'ok': True, 'max_cost': X} — reserved X
|
||||
{'ok': True, 'max_cost': 0, 'skip': ...} — reserve skipped by policy
|
||||
{'ok': True, 'max_cost': 0, 'no_history': True} — no max_cost data yet
|
||||
{'ok': False, 'reason': ...} — insufficient balance
|
||||
{'ok': True, 'max_cost': 0, 'no_redis': True} — Redis down, DB fallback
|
||||
"""
|
||||
try:
|
||||
# ── Policy checks (need llm info) ──
|
||||
try:
|
||||
from .utils import get_llmage_llm, get_user_tpac
|
||||
llm = await get_llmage_llm(llmid)
|
||||
if llm and llm.ownerid == userorgid:
|
||||
return {'ok': True, 'max_cost': 0, 'skip': 'self_org'}
|
||||
if not llm or not llm.ppid:
|
||||
return {'ok': True, 'max_cost': 0, 'skip': 'no_ppid'}
|
||||
# tpac user: balance lives in external system, skip redis reserve
|
||||
if userid:
|
||||
try:
|
||||
tpac = await get_user_tpac(userid)
|
||||
if tpac:
|
||||
return {'ok': True, 'max_cost': 0, 'skip': 'tpac'}
|
||||
except Exception as e:
|
||||
debug(f'reserve_balance: tpac check failed: {e}')
|
||||
except Exception as e:
|
||||
debug(f'reserve_balance: policy check failed: {e}')
|
||||
|
||||
redis = await _get_redis()
|
||||
bal_key = f'balance:{userorgid}'
|
||||
cost_key = f'model:max_cost:{llmid}'
|
||||
reserve_key = f'reserve:{luid}'
|
||||
|
||||
# Load max_cost from Redis if cached, else from DB history
|
||||
max_cost = 0
|
||||
stored = await redis.get(cost_key)
|
||||
if stored:
|
||||
max_cost = int(stored)
|
||||
else:
|
||||
try:
|
||||
from .utils import get_model_max_cost
|
||||
mc = await get_model_max_cost(llmid)
|
||||
if mc and mc > 0:
|
||||
max_cost = _cents(mc)
|
||||
except Exception as e:
|
||||
debug(f'reserve_balance: get_model_max_cost failed: {e}')
|
||||
|
||||
if max_cost <= 0:
|
||||
return {'ok': True, 'max_cost': 0, 'no_history': True}
|
||||
|
||||
# Load balance from DB if not cached in Redis
|
||||
db_balance = 0
|
||||
stored_bal = await redis.get(bal_key)
|
||||
if not stored_bal:
|
||||
try:
|
||||
from accounting.getaccount import getCustomerBalance
|
||||
from sqlor.dbpools import get_sor_context
|
||||
async with get_sor_context(env, 'accounting') as sor:
|
||||
bal = await getCustomerBalance(sor, userorgid)
|
||||
if bal is not None:
|
||||
db_balance = _cents(float(bal))
|
||||
except Exception as e:
|
||||
debug(f'reserve_balance: getCustomerBalance failed: {e}')
|
||||
|
||||
reserve_val = f'{userorgid}|{llmid}|{max_cost}'
|
||||
result = await redis.eval(RESERVE_LUA, 3,
|
||||
bal_key, cost_key, reserve_key,
|
||||
str(max_cost), str(db_balance), reserve_val, str(ttl))
|
||||
|
||||
if result[0] == 0:
|
||||
return {'ok': False, 'reason': result[1]}
|
||||
return {'ok': True, 'max_cost': _from_cents(result[1])}
|
||||
|
||||
except Exception as e:
|
||||
# Redis down or unreachable — fall back to DB check, don't block
|
||||
debug(f'reserve_balance: Redis error, falling back to DB: {e}')
|
||||
return {'ok': True, 'max_cost': 0, 'no_redis': True}
|
||||
|
||||
|
||||
async def finalize_balance(env, luid, actual_cost, llmid=None):
|
||||
"""After accounting: adjust balance, update max_cost.
|
||||
|
||||
llmid: needed for cold-start max_cost updates when no reserve existed
|
||||
(reserve skipped due to no_history) — keeps the pre-deduct baseline
|
||||
growing so future calls can reserve.
|
||||
"""
|
||||
try:
|
||||
redis = await _get_redis()
|
||||
reserve_key = f'reserve:{luid}'
|
||||
|
||||
reserve_val = await redis.get(reserve_key)
|
||||
if not reserve_val:
|
||||
debug(f'finalize_balance: no reserve for luid={luid}')
|
||||
if llmid:
|
||||
await _update_max_cost(llmid, actual_cost)
|
||||
return
|
||||
|
||||
# Parse to get llmid (reserve_val: userorgid|llmid|max_cost)
|
||||
parts = reserve_val.split('|')
|
||||
if len(parts) < 3:
|
||||
return
|
||||
llmid = parts[1]
|
||||
cost_key = f'model:max_cost:{llmid}'
|
||||
|
||||
result = await redis.eval(FINALIZE_LUA, 2,
|
||||
cost_key, reserve_key, str(_cents(actual_cost)))
|
||||
|
||||
if result[0] == 0:
|
||||
debug(f'finalize_balance: {result[1]}')
|
||||
return
|
||||
|
||||
max_c = _from_cents(result[2])
|
||||
diff = _from_cents(result[4])
|
||||
debug(f'finalize_balance: luid={luid} max={max_c} actual={actual_cost} diff={diff}')
|
||||
|
||||
# Persist max_cost to DB if actual exceeded previous max
|
||||
if actual_cost > max_c:
|
||||
await _update_max_cost(llmid, actual_cost)
|
||||
|
||||
except Exception as e:
|
||||
exception(f'finalize_balance error: {e}')
|
||||
|
||||
|
||||
async def refund_balance(env, luid):
|
||||
"""Full refund on API failure."""
|
||||
try:
|
||||
redis = await _get_redis()
|
||||
reserve_key = f'reserve:{luid}'
|
||||
result = await redis.eval(REFUND_LUA, 1, reserve_key)
|
||||
if result[0] == 1:
|
||||
debug(f'refund_balance: luid={luid} refund={_from_cents(result[1])}')
|
||||
else:
|
||||
debug(f'refund_balance: luid={luid} skipped ({result[1]})')
|
||||
except Exception as e:
|
||||
exception(f'refund_balance error: {e}')
|
||||
|
||||
|
||||
async def extend_reserve(env, luid, ttl):
|
||||
"""Extend TTL of an existing reserve.
|
||||
|
||||
Used when an entry pre-reserved with the short (600s) TTL but the
|
||||
request is dispatched to async mode, where the task may run longer.
|
||||
"""
|
||||
try:
|
||||
redis = await _get_redis()
|
||||
await redis.expire(f'reserve:{luid}', ttl)
|
||||
except Exception as e:
|
||||
debug(f'extend_reserve failed: {e}')
|
||||
|
||||
|
||||
async def invalidate_balance_cache(userorgid):
|
||||
"""Delete the cached Redis balance so the next reserve reloads from DB.
|
||||
|
||||
Must be called after recharge / recharge reversal, otherwise the
|
||||
pre-deduct baseline stays stale and valid requests get rejected.
|
||||
"""
|
||||
try:
|
||||
redis = await _get_redis()
|
||||
await redis.delete(f'balance:{userorgid}')
|
||||
debug(f'invalidate_balance_cache: balance:{userorgid} deleted')
|
||||
except Exception as e:
|
||||
debug(f'invalidate_balance_cache failed: {e}')
|
||||
135
llmage/init.py
135
llmage/init.py
@ -1,37 +1,21 @@
|
||||
import asyncio
|
||||
from appPublic.registerfunction import RegisterFunction
|
||||
from sqlor.dbpools import DBPools, get_sor_context
|
||||
from sqlor.dbpools import DBPools
|
||||
from ahserver.serverenv import ServerEnv
|
||||
from appPublic.log import debug
|
||||
from appPublic.share_cache import cache_start_listener
|
||||
from .keling import keling_token
|
||||
from .jimeng import jimeng_auth_headers
|
||||
from .utils import (
|
||||
llm_query_orders,
|
||||
read_webpath,
|
||||
llm_query_price,
|
||||
get_user_tpac,
|
||||
get_tpac_balance,
|
||||
get_llm_by_model,
|
||||
get_llms_by_catelog,
|
||||
get_llms_sort_by_provider,
|
||||
get_llmcatelogs,
|
||||
get_llms_by_catelog_to_customer,
|
||||
get_llmproviders,
|
||||
get_llm,
|
||||
get_llmage_llm,
|
||||
get_llm_catelogs,
|
||||
invalidate_uapi_cache,
|
||||
get_llmusage_by_id,
|
||||
read_ioinfo_content,
|
||||
_warm_llmid_cache,
|
||||
get_llmid_cached,
|
||||
invalidate_llmid_cache,
|
||||
get_plaza_models,
|
||||
# Redis balance reservation
|
||||
reserve_balance,
|
||||
finalize_balance,
|
||||
refund_balance,
|
||||
get_llm,
|
||||
)
|
||||
|
||||
from .llmclient import (
|
||||
@ -56,88 +40,6 @@ from .asyncinference import (
|
||||
get_today_asynctask_list
|
||||
)
|
||||
|
||||
from .product_interface import (
|
||||
get_product_display,
|
||||
check_product_availability,
|
||||
check_product_consumable,
|
||||
execute_product_service,
|
||||
execute_product_service_stream,
|
||||
calculate_product_cost,
|
||||
)
|
||||
|
||||
|
||||
async def load_product_category_product(parent_category_id):
|
||||
"""Return llmage catalogs and published models as standardized import data.
|
||||
|
||||
Called by product_management.import_categories_and_products().
|
||||
Only reads source data and returns it in the standard format.
|
||||
Does NOT write to product_management tables — that's product_management's job.
|
||||
|
||||
Returns:
|
||||
{
|
||||
'success': True,
|
||||
'categories': [{'source_id', 'name', 'description', 'product_type', 'product_type_title', 'sort_order'}, ...],
|
||||
'products': [{'source_category_id', 'resource_ref_id', 'product_code', 'product_name', 'product_type', 'brief_intro', 'sort_order'}, ...]
|
||||
}
|
||||
"""
|
||||
env = ServerEnv()
|
||||
|
||||
# Read source data from llmage
|
||||
async with get_sor_context(env, 'llmage') as sor:
|
||||
catelogs = await sor.R('llmcatelog', {})
|
||||
if not catelogs:
|
||||
return {'success': False, 'error': 'llmage中没有产品类别数据'}
|
||||
|
||||
llm_sql = """select a.id, a.name, a.model, a.description, a.status, a.providerid,
|
||||
m.llmcatelogid, lc.name as catelogname
|
||||
from llm a
|
||||
join llm_api_map m on a.id = m.llmid
|
||||
join llmcatelog lc on m.llmcatelogid = lc.id
|
||||
where m.isdefaultcatelog = '1' and a.status = 'published'
|
||||
order by lc.name, a.name"""
|
||||
llms = await sor.sqlExe(llm_sql, {})
|
||||
|
||||
# Build standardized categories
|
||||
categories = []
|
||||
for c in catelogs:
|
||||
categories.append({
|
||||
'source_id': c.id,
|
||||
'name': c.name,
|
||||
'description': getattr(c, 'description', '') or '',
|
||||
'product_type': 'llm_model',
|
||||
'product_type_title': '大模型按量',
|
||||
'sort_order': 0,
|
||||
})
|
||||
|
||||
# Build standardized products
|
||||
products = []
|
||||
for llm in (llms or []):
|
||||
products.append({
|
||||
'source_category_id': llm.llmcatelogid,
|
||||
'resource_ref_id': llm.id,
|
||||
'product_code': llm.model,
|
||||
'product_name': llm.name,
|
||||
'product_type': 'llm_model',
|
||||
'brief_intro': getattr(llm, 'description', '') or '',
|
||||
'sort_order': 0,
|
||||
'providerid': getattr(llm, 'providerid', '') or '',
|
||||
})
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'categories': categories,
|
||||
'products': products,
|
||||
}
|
||||
|
||||
|
||||
def _on_hot_reload(data=None):
|
||||
"""Event handler for hot_reload — invalidate caches."""
|
||||
from appPublic.log import debug
|
||||
debug(f'[llmage] on_hot_reload called, invalidating caches (data={data})')
|
||||
invalidate_uapi_cache()
|
||||
invalidate_llmid_cache()
|
||||
|
||||
|
||||
def load_llmage():
|
||||
env = ServerEnv()
|
||||
env.llm_query_orders = llm_query_orders
|
||||
@ -150,18 +52,9 @@ def load_llmage():
|
||||
env.get_asynctask_status = get_asynctask_status
|
||||
env.query_task_status = query_task_status
|
||||
env.get_llm = get_llm
|
||||
env.get_llmage_llm = get_llmage_llm
|
||||
env.get_llm_catelogs = get_llm_catelogs
|
||||
env.invalidate_uapi_cache = invalidate_uapi_cache
|
||||
env.inference = inference
|
||||
env.get_user_tpac = get_user_tpac
|
||||
env.get_tpac_balance = get_tpac_balance
|
||||
env.inference_generator = inference_generator
|
||||
env.get_llms_by_catelog = get_llms_by_catelog
|
||||
env.get_llmid_cached = get_llmid_cached
|
||||
env.invalidate_llmid_cache = invalidate_llmid_cache
|
||||
# 启动时预热 llmid 缓存
|
||||
asyncio.ensure_future(_warm_llmid_cache(env))
|
||||
env.get_llmcatelogs = get_llmcatelogs
|
||||
env.checkCustomerBalance = checkCustomerBalance
|
||||
env.get_llmproviders = get_llmproviders
|
||||
@ -169,33 +62,9 @@ def load_llmage():
|
||||
env.keling_token = keling_token
|
||||
env.llm_query_price = llm_query_price
|
||||
env.get_llms_by_catelog_to_customer = get_llms_by_catelog_to_customer
|
||||
env.get_plaza_models = get_plaza_models
|
||||
env._reserve_balance = reserve_balance
|
||||
env._finalize_balance = finalize_balance
|
||||
env._refund_balance = refund_balance
|
||||
env.reserve_balance = lambda llmid, userorgid, luid, ttl=600, userid=None: reserve_balance(env, llmid, userorgid, luid, ttl=ttl, userid=userid)
|
||||
env.finalize_balance = lambda luid, actual_cost, llmid=None: finalize_balance(env, luid, actual_cost, llmid=llmid)
|
||||
env.refund_balance = lambda luid: refund_balance(env, luid)
|
||||
env.backup_accounted_llmusage = backup_accounted_llmusage
|
||||
env.read_ioinfo_content = read_ioinfo_content
|
||||
env.get_llmusage_by_id = get_llmusage_by_id
|
||||
env.get_failed_accounting_records = get_failed_accounting_records
|
||||
env.get_llmage_stats = get_llmage_stats
|
||||
# Product module standard interface
|
||||
env.product_interface = {
|
||||
'module_name': 'llmage',
|
||||
'get_product_display': get_product_display,
|
||||
'check_product_availability': check_product_availability,
|
||||
'check_product_consumable': check_product_consumable,
|
||||
'execute_product_service': execute_product_service,
|
||||
'execute_product_service_stream': execute_product_service_stream,
|
||||
'calculate_product_cost': calculate_product_cost,
|
||||
'load_product_category_product': load_product_category_product,
|
||||
}
|
||||
# Bind hot_reload event — module-level function, ref safe (module keeps it alive)
|
||||
cache_start_listener()
|
||||
if hasattr(env, 'event_dispatcher'):
|
||||
env.event_dispatcher.bind('hot_reload', _on_hot_reload)
|
||||
rf = RegisterFunction()
|
||||
rf.register('jimeng_auth_headers', jimeng_auth_headers)
|
||||
|
||||
|
||||
@ -14,7 +14,6 @@ from ahserver.filestorage import FileStorage
|
||||
from .asyncinference import async_uapi_request
|
||||
from .syncinference import sync_uapi_request
|
||||
from .accounting import llm_accounting, llm_charging
|
||||
from .balance import refund_balance, reserve_balance, extend_reserve
|
||||
from .utils import *
|
||||
|
||||
async def uapi_request(request, llm, callerid, callerorgid, params_kw=None):
|
||||
@ -27,15 +26,13 @@ async def uapi_request(request, llm, callerid, callerorgid, params_kw=None):
|
||||
userid = await env.uapi_data.get_calluserid(llm.upappid, orgid=llm.ownerid)
|
||||
outlines = []
|
||||
txt = ''
|
||||
luid = params_kw.get('_luid') or getID()
|
||||
llmusage = None
|
||||
luid = getID()
|
||||
try:
|
||||
start_timestamp = time.time()
|
||||
responsed_seconds = None
|
||||
finish_seconds = None
|
||||
first = True
|
||||
usage = None
|
||||
last_choices = None
|
||||
async for l in uapi.stream_linify(llm.upappid, llm.apiname, userid,
|
||||
params=params_kw):
|
||||
if first:
|
||||
@ -58,10 +55,6 @@ async def uapi_request(request, llm, callerid, callerorgid, params_kw=None):
|
||||
if d.get('reasoning_content'):
|
||||
txt += d.get('reasoning_content')
|
||||
yield_it = True
|
||||
if d.get('choices'):
|
||||
last_choices = d['choices']
|
||||
elif last_choices:
|
||||
d['choices'] = last_choices
|
||||
if d.get('content'):
|
||||
txt = txt + d['content']
|
||||
yield_it = True
|
||||
@ -72,7 +65,6 @@ async def uapi_request(request, llm, callerid, callerorgid, params_kw=None):
|
||||
yield json.dumps(d, ensure_ascii=False) + '\n'
|
||||
if usage is None:
|
||||
error(f'{llm=} response has not usage')
|
||||
|
||||
finish_seconds = time.time() - start_timestamp
|
||||
if responsed_seconds is None:
|
||||
responsed_seconds = finish_seconds
|
||||
@ -95,46 +87,18 @@ async def uapi_request(request, llm, callerid, callerorgid, params_kw=None):
|
||||
llmusage.finish_seconds = finish_seconds
|
||||
llmusage.status = 'SUCCEEDED'
|
||||
llmusage.userorgid = callerorgid
|
||||
llmusage.tenantid = params_kw.get('tenantid', params_kw.get('tentantid'))
|
||||
llmusage.ownerid = llm.ownerid
|
||||
llmusage.accounting_status = 'created'
|
||||
# await write_llmusage(llmusage)
|
||||
await write_llmusage(llmusage)
|
||||
except Exception as e:
|
||||
# Refund balance reservation on failure
|
||||
try:
|
||||
if luid:
|
||||
await refund_balance(ServerEnv(), luid)
|
||||
except:
|
||||
debug(f'refund_balance(ServerEnv(), {luid=}) errir')
|
||||
pass
|
||||
exception(f'{e=},{format_exc()}')
|
||||
estr = erase_apikey(e)
|
||||
ed = {"error": f"ERROR:{estr}", "status": "FAILED" ,"llmusageid": luid}
|
||||
s = json.dumps(ed, ensure_ascii=False)
|
||||
s = ''.join(s.split('\\n'))
|
||||
s = ''.join(s.split('\n'))
|
||||
outlines.append(ed)
|
||||
yield f'{s}\\n'
|
||||
## except happand at call llm server
|
||||
llmusage = DictObject()
|
||||
llmusage.id = luid
|
||||
llmusage.llmid = llm.id
|
||||
llmusage.use_date = curDateString()
|
||||
llmusage.use_time = timestampstr()
|
||||
llmusage.userid = callerid
|
||||
ioinfo = {
|
||||
"input": params_kw,
|
||||
'output': ed
|
||||
}
|
||||
webpath = await write_llmio(llmusage.id, ioinfo)
|
||||
llmusage.ioinfo = webpath
|
||||
llmusage.transno = params_kw.transno
|
||||
llmusage.status = 'FAILED'
|
||||
llmusage.userorgid = callerorgid
|
||||
llmusage.tenantid = params_kw.get('tenantid', params_kw.get('tentantid'))
|
||||
llmusage.ownerid = llm.ownerid
|
||||
finally:
|
||||
# GeneratorExit / client disconnect — flush partial usage
|
||||
await write_llmusage(llmusage)
|
||||
yield f'{s}\n'
|
||||
return
|
||||
|
||||
async def inference_generator(request, *args, params_kw=None, **kw):
|
||||
env = request._run_ns.copy()
|
||||
@ -152,38 +116,15 @@ async def _inference_generator(request, callerid, callerorgid,
|
||||
if not params_kw.transno:
|
||||
params_kw.transno = getID()
|
||||
llmid = params_kw.llmid
|
||||
catelogid = params_kw.get('llmcatelogid', None)
|
||||
f = None
|
||||
llm = await get_llm(llmid, catelogid)
|
||||
llm = await get_llm(llmid)
|
||||
if llm is None:
|
||||
errmsg = f'{{"status": "FAILED", "error":"llmid:{llmid}没找到模型"}}\n'
|
||||
exception(errmsg)
|
||||
yield errmsg
|
||||
return
|
||||
params_kw.model = llm.model
|
||||
# ── Unified balance reserve ────────────────────────────────────────
|
||||
# Covers every entry that did not pre-reserve. v1/chat/completions
|
||||
# reserves at dspy level and passes _luid; everyone else reserves here.
|
||||
# Reusing _luid downstream also fixes the old mismatch where the
|
||||
# reserve key could never equal the llmusage id used by finalize.
|
||||
if params_kw.get('_luid'):
|
||||
if llm.stream == 'async':
|
||||
# Entry reserved with the short TTL; async tasks run longer
|
||||
await extend_reserve(env, params_kw._luid, 3600)
|
||||
else:
|
||||
_luid = getID()
|
||||
_ttl = 3600 if llm.stream == 'async' else 600
|
||||
reserved = await reserve_balance(env, llm.id, callerorgid, _luid,
|
||||
ttl=_ttl, userid=callerid)
|
||||
if not reserved.get('ok'):
|
||||
debug(f'balance reserve rejected: {reserved}')
|
||||
errmsg = json.dumps({'status': 'FAILED',
|
||||
'error': f'余额不足(balance reserve rejected): {reserved.get("reason")}'},
|
||||
ensure_ascii=False) + '\n'
|
||||
yield errmsg
|
||||
return
|
||||
params_kw._luid = _luid
|
||||
params_kw._reserved = reserved
|
||||
if not params_kw.model:
|
||||
params_kw.model = llm.model
|
||||
if llm.stream == 'async':
|
||||
if llm.callbackurl:
|
||||
cb_url = env.entire_url(llm.callbackurl)
|
||||
|
||||
@ -1,515 +0,0 @@
|
||||
"""
|
||||
llmage Product Module Interface Implementation
|
||||
|
||||
Implements the standard resource module interface for product_management.
|
||||
All functions take resource_ref_id (= llm.id) as the primary identifier.
|
||||
The product module stores this mapping in product.resource_ref_id.
|
||||
"""
|
||||
import json
|
||||
import time
|
||||
from appPublic.log import debug, exception
|
||||
from appPublic.uniqueID import getID
|
||||
from appPublic.dictObject import DictObject
|
||||
from appPublic.timeUtils import curDateString, timestampstr
|
||||
from sqlor.dbpools import get_sor_context
|
||||
from ahserver.serverenv import ServerEnv
|
||||
from .utils import (
|
||||
get_llmage_llm,
|
||||
get_llm,
|
||||
get_user_tpac,
|
||||
get_tpac_balance,
|
||||
write_llmio,
|
||||
write_llmusage,
|
||||
)
|
||||
from .accounting import llm_charging
|
||||
from .balance import reserve_balance, refund_balance
|
||||
|
||||
|
||||
async def _resolve_llm(resource_ref_id):
|
||||
"""Resolve resource_ref_id (llm.id) to llm record with full info.
|
||||
Returns (llm_record, error_message).
|
||||
"""
|
||||
llm = await get_llmage_llm(resource_ref_id)
|
||||
if not llm:
|
||||
return None, f'模型 {resource_ref_id} 不存在或配置不完整'
|
||||
return llm, None
|
||||
|
||||
|
||||
async def get_product_display(resource_ref_id):
|
||||
"""获取产品定价展示信息。
|
||||
|
||||
Args:
|
||||
resource_ref_id: 资源模块内部ID(= llm.id)
|
||||
|
||||
Returns:
|
||||
{'success': True, 'pricing_text': str, 'pricing_detail': dict, 'extra_info': dict}
|
||||
"""
|
||||
llm, err = await _resolve_llm(resource_ref_id)
|
||||
if err:
|
||||
return {'success': False, 'message': err}
|
||||
|
||||
env = ServerEnv()
|
||||
pricing_text = ''
|
||||
pricing_detail = {}
|
||||
|
||||
if llm.ppid:
|
||||
try:
|
||||
pd = await env.get_pricing_display(llm.ppid)
|
||||
if pd:
|
||||
pricing_text = pd.get('display_text', '')
|
||||
pricing_detail = pd
|
||||
except Exception as e:
|
||||
debug(f'get_pricing_display failed for ppid={llm.ppid}: {e}')
|
||||
pricing_text = '定价信息暂不可用'
|
||||
|
||||
# Get provider name
|
||||
provider_name = ''
|
||||
if llm.providerid:
|
||||
async with get_sor_context(env, 'rbac') as sor:
|
||||
org_recs = await sor.R('organization', {'id': llm.providerid})
|
||||
if org_recs:
|
||||
provider_name = org_recs[0].orgname
|
||||
|
||||
extra_info = {
|
||||
'provider': provider_name,
|
||||
'llmid': llm.id,
|
||||
'model': llm.model,
|
||||
'catelog': getattr(llm, 'catelogname', ''),
|
||||
'ownerid': llm.ownerid,
|
||||
}
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'pricing_text': pricing_text,
|
||||
'pricing_detail': pricing_detail,
|
||||
'extra_info': extra_info,
|
||||
}
|
||||
|
||||
|
||||
async def check_product_availability(resource_ref_id, user_org_id=None):
|
||||
"""检查产品是否可用(定价方案是否有效)。
|
||||
|
||||
Args:
|
||||
resource_ref_id: 资源模块内部ID(= llm.id)
|
||||
user_org_id: 用户组织ID(自有模型免检)
|
||||
|
||||
Returns:
|
||||
{'available': bool, 'reason': str}
|
||||
"""
|
||||
llm, err = await _resolve_llm(resource_ref_id)
|
||||
if err:
|
||||
return {'available': False, 'reason': err}
|
||||
|
||||
# Self-owned model: always available
|
||||
if user_org_id and llm.ownerid == user_org_id:
|
||||
return {'available': True, 'reason': ''}
|
||||
|
||||
# Check status
|
||||
if llm.status != 'published':
|
||||
return {'available': False, 'reason': '模型已下线'}
|
||||
|
||||
# Check ppid exists
|
||||
if not llm.ppid:
|
||||
return {'available': False, 'reason': '无定价方案(ppid为空)'}
|
||||
|
||||
# Check pricing data for today
|
||||
env = ServerEnv()
|
||||
try:
|
||||
await env.get_ppid_pricing(llm.ppid)
|
||||
except Exception as e:
|
||||
debug(f'check_product_availability: ppid={llm.ppid} no pricing: {e}')
|
||||
return {'available': False, 'reason': '今日无有效定价数据'}
|
||||
|
||||
return {'available': True, 'reason': ''}
|
||||
|
||||
|
||||
async def check_product_consumable(resource_ref_id, user_id, user_org_id):
|
||||
"""消费前综合预检:可用性 + 余额 + 定价。
|
||||
|
||||
Args:
|
||||
resource_ref_id: 资源模块内部ID(= llm.id)
|
||||
user_id: 用户ID
|
||||
user_org_id: 用户组织ID
|
||||
|
||||
Returns:
|
||||
{'consumable': bool, 'reason': str, 'min_balance': float, 'pricing_available': bool}
|
||||
"""
|
||||
llm, err = await _resolve_llm(resource_ref_id)
|
||||
if err:
|
||||
return {'consumable': False, 'reason': err,
|
||||
'min_balance': 0, 'pricing_available': False}
|
||||
|
||||
# Self-owned model: skip balance check
|
||||
if llm.ownerid == user_org_id:
|
||||
return {'consumable': True, 'reason': '',
|
||||
'min_balance': 0, 'pricing_available': True}
|
||||
|
||||
# Check ppid
|
||||
if not llm.ppid:
|
||||
return {'consumable': False, 'reason': '无定价方案',
|
||||
'min_balance': 0, 'pricing_available': False}
|
||||
|
||||
# Check pricing data
|
||||
env = ServerEnv()
|
||||
pricing_available = True
|
||||
try:
|
||||
await env.get_ppid_pricing(llm.ppid)
|
||||
except Exception as e:
|
||||
debug(f'check_product_consumable: ppid={llm.ppid} no pricing: {e}')
|
||||
pricing_available = False
|
||||
return {'consumable': False, 'reason': '今日无有效定价数据',
|
||||
'min_balance': float(llm.min_balance or 0),
|
||||
'pricing_available': False}
|
||||
|
||||
# Check balance
|
||||
min_balance = float(llm.min_balance or 0)
|
||||
balance = 0.0
|
||||
tpac = await get_user_tpac(user_id)
|
||||
if tpac:
|
||||
balance = await get_tpac_balance(tpac, user_id)
|
||||
if balance is None:
|
||||
balance = 0.0
|
||||
else:
|
||||
from accounting.getaccount import getCustomerBalance
|
||||
async with get_sor_context(env, 'accounting') as sor:
|
||||
bal = await getCustomerBalance(sor, user_org_id)
|
||||
balance = float(bal) if bal else 0.0
|
||||
|
||||
if balance < min_balance:
|
||||
return {'consumable': False,
|
||||
'reason': f'余额不足(当前{balance:.2f}, 最低{min_balance:.2f})',
|
||||
'min_balance': min_balance,
|
||||
'pricing_available': pricing_available}
|
||||
|
||||
return {'consumable': True, 'reason': '',
|
||||
'min_balance': min_balance,
|
||||
'pricing_available': pricing_available}
|
||||
|
||||
|
||||
async def execute_product_service(resource_ref_id, user_id, user_org_id, request_data):
|
||||
"""执行大模型API调用(非流式)。
|
||||
|
||||
Args:
|
||||
resource_ref_id: 资源模块内部ID(= llm.id)
|
||||
user_id: 用户ID
|
||||
user_org_id: 用户组织ID
|
||||
request_data: dict, 请求参数(messages, temperature, max_tokens 等)
|
||||
|
||||
Returns:
|
||||
{'success': True, 'result': dict, 'usage_data': dict,
|
||||
'resource_ref_id': str, 'task_id': str, 'status': 'SUCCEEDED'}
|
||||
"""
|
||||
env = ServerEnv()
|
||||
llm, err = await _resolve_llm(resource_ref_id)
|
||||
if err:
|
||||
return {'success': False, 'message': err, 'status': 'FAILED'}
|
||||
|
||||
# Get full llm info (with uapi) for API call
|
||||
full_llm = await get_llm(llm.id)
|
||||
if not full_llm:
|
||||
return {'success': False, 'message': '模型API配置不完整', 'status': 'FAILED'}
|
||||
|
||||
from uapi.appapi import UAPI
|
||||
|
||||
# Get API user
|
||||
userid = await env.uapi_data.get_calluserid(full_llm.upappid, orgid=full_llm.ownerid)
|
||||
|
||||
luid = getID()
|
||||
params_kw = DictObject(**request_data)
|
||||
params_kw.model = full_llm.model
|
||||
if not params_kw.get('transno'):
|
||||
params_kw.transno = luid
|
||||
|
||||
# ── Balance reservation (product path) ─────────────────────────
|
||||
# Same atomic pre-deduct as the direct dspy path; policy checks
|
||||
# (self-org / tpac / no-ppid) live inside reserve_balance.
|
||||
ttl = 3600 if full_llm.stream == 'async' else 600
|
||||
reserved = await reserve_balance(env, full_llm.id, user_org_id, luid,
|
||||
ttl=ttl, userid=user_id)
|
||||
if not reserved.get('ok'):
|
||||
return {'success': False,
|
||||
'message': f'余额不足(balance reserve rejected): {reserved.get("reason")}',
|
||||
'status': 'FAILED', 'task_id': luid}
|
||||
params_kw._luid = luid
|
||||
|
||||
try:
|
||||
if full_llm.stream == 'async':
|
||||
from .asyncinference import async_uapi_request_product
|
||||
result = await async_uapi_request_product(
|
||||
full_llm, userid, user_id, user_org_id, params_kw, luid)
|
||||
elif not full_llm.stream:
|
||||
from .syncinference import sync_uapi_request_product
|
||||
result = await sync_uapi_request_product(
|
||||
full_llm, userid, user_id, user_org_id, params_kw, luid)
|
||||
else:
|
||||
result = await _collect_stream(full_llm, userid, user_id,
|
||||
user_org_id, params_kw, luid)
|
||||
|
||||
# Sub-callers swallow exceptions and return success=False —
|
||||
# refund here too (refund is idempotent via GETDEL)
|
||||
if not result.get('success'):
|
||||
try:
|
||||
await refund_balance(env, luid)
|
||||
except Exception:
|
||||
pass
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
# Refund balance reservation on failure
|
||||
try:
|
||||
await refund_balance(env, luid)
|
||||
except Exception:
|
||||
pass
|
||||
exception(f'execute_product_service error: {e}')
|
||||
return {'success': False, 'message': str(e), 'status': 'FAILED',
|
||||
'task_id': luid}
|
||||
|
||||
|
||||
async def _collect_stream(llm, api_userid, user_id, user_org_id, params_kw, luid):
|
||||
"""Collect streaming response into a single result dict."""
|
||||
env = ServerEnv()
|
||||
from uapi.appapi import UAPI
|
||||
uapi = UAPI(llm.upappid, llm.apiname)
|
||||
|
||||
outlines = []
|
||||
txt = ''
|
||||
usage = None
|
||||
start_timestamp = time.time()
|
||||
responsed_seconds = None
|
||||
|
||||
try:
|
||||
first = True
|
||||
async for l in uapi.stream_linify(llm.upappid, llm.apiname,
|
||||
api_userid, params=params_kw):
|
||||
if first:
|
||||
first = False
|
||||
responsed_seconds = time.time() - start_timestamp
|
||||
if isinstance(l, bytes):
|
||||
l = l.decode('utf-8')
|
||||
if l and l[-1] == '\n':
|
||||
l = l[:-1]
|
||||
l = ''.join(l.split('\n'))
|
||||
if l and l != '[DONE]':
|
||||
try:
|
||||
d = json.loads(l)
|
||||
except:
|
||||
continue
|
||||
if d.get('reasoning_content'):
|
||||
txt += d.get('reasoning_content')
|
||||
if d.get('content'):
|
||||
txt += d['content']
|
||||
if d.get('usage'):
|
||||
usage = d['usage']
|
||||
outlines.append(d)
|
||||
|
||||
finish_seconds = time.time() - start_timestamp
|
||||
if responsed_seconds is None:
|
||||
responsed_seconds = finish_seconds
|
||||
|
||||
# Write llmusage
|
||||
llmusage = DictObject()
|
||||
llmusage.id = luid
|
||||
llmusage.llmid = llm.id
|
||||
llmusage.use_date = curDateString()
|
||||
llmusage.use_time = timestampstr()
|
||||
llmusage.userid = user_id
|
||||
llmusage.usages = json.dumps(usage, ensure_ascii=False, indent=4) if usage else '{}'
|
||||
ioinfo = {"input": dict(params_kw), "output": outlines}
|
||||
webpath = await write_llmio(luid, ioinfo)
|
||||
llmusage.ioinfo = webpath
|
||||
llmusage.transno = params_kw.get('transno', luid)
|
||||
llmusage.responsed_seconds = responsed_seconds
|
||||
llmusage.finish_seconds = finish_seconds
|
||||
llmusage.status = 'SUCCEEDED'
|
||||
llmusage.userorgid = user_org_id
|
||||
llmusage.tenantid = params_kw.get('tenantid', params_kw.get('tentantid'))
|
||||
llmusage.ownerid = llm.ownerid
|
||||
llmusage.accounting_status = 'created'
|
||||
await write_llmusage(llmusage)
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'result': {'content': txt, 'chunks': outlines},
|
||||
'usage_data': usage or {},
|
||||
'resource_ref_id': llm.id,
|
||||
'task_id': luid,
|
||||
'status': 'SUCCEEDED',
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
exception(f'stream collect error: {e}')
|
||||
return {'success': False, 'message': str(e),
|
||||
'task_id': luid, 'status': 'FAILED'}
|
||||
|
||||
|
||||
async def execute_product_service_stream(resource_ref_id, user_id, user_org_id, request_data):
|
||||
"""执行大模型API调用(流式),返回异步生成器。
|
||||
|
||||
Yields: {'chunk': dict, 'usage_data': dict|None, 'done': bool}
|
||||
Final chunk has done=True with complete usage_data.
|
||||
"""
|
||||
llm, err = await _resolve_llm(resource_ref_id)
|
||||
if err:
|
||||
yield {'chunk': None, 'usage_data': None, 'done': True,
|
||||
'error': err}
|
||||
return
|
||||
|
||||
full_llm = await get_llm(llm.id)
|
||||
if not full_llm:
|
||||
yield {'chunk': None, 'usage_data': None, 'done': True,
|
||||
'error': '模型API配置不完整'}
|
||||
return
|
||||
|
||||
env = ServerEnv()
|
||||
from uapi.appapi import UAPI
|
||||
uapi = UAPI(full_llm.upappid, full_llm.apiname)
|
||||
api_userid = await env.uapi_data.get_calluserid(
|
||||
full_llm.upappid, orgid=full_llm.ownerid)
|
||||
|
||||
params_kw = DictObject(**request_data)
|
||||
params_kw.model = full_llm.model
|
||||
luid = getID()
|
||||
if not params_kw.get('transno'):
|
||||
params_kw.transno = luid
|
||||
|
||||
# ── Balance reservation (product stream path) ──────────────────
|
||||
reserved = await reserve_balance(env, full_llm.id, user_org_id, luid,
|
||||
ttl=600, userid=user_id)
|
||||
if not reserved.get('ok'):
|
||||
yield {'chunk': None, 'usage_data': None, 'done': True,
|
||||
'error': f'余额不足(balance reserve rejected): {reserved.get("reason")}',
|
||||
'task_id': luid, 'status': 'FAILED'}
|
||||
return
|
||||
params_kw._luid = luid
|
||||
|
||||
outlines = []
|
||||
txt = ''
|
||||
usage = None
|
||||
start_timestamp = time.time()
|
||||
responsed_seconds = None
|
||||
|
||||
try:
|
||||
first = True
|
||||
async for l in uapi.stream_linify(full_llm.upappid, full_llm.apiname,
|
||||
api_userid, params=params_kw):
|
||||
if first:
|
||||
first = False
|
||||
responsed_seconds = time.time() - start_timestamp
|
||||
if isinstance(l, bytes):
|
||||
l = l.decode('utf-8')
|
||||
if l and l[-1] == '\n':
|
||||
l = l[:-1]
|
||||
l = ''.join(l.split('\n'))
|
||||
if l and l != '[DONE]':
|
||||
try:
|
||||
d = json.loads(l)
|
||||
except:
|
||||
continue
|
||||
if d.get('reasoning_content'):
|
||||
txt += d.get('reasoning_content')
|
||||
if d.get('content'):
|
||||
txt += d['content']
|
||||
if d.get('usage'):
|
||||
usage = d['usage']
|
||||
outlines.append(d)
|
||||
d['llmusageid'] = luid
|
||||
yield {'chunk': d, 'usage_data': None, 'done': False}
|
||||
|
||||
# Stream done — write llmusage
|
||||
finish_seconds = time.time() - start_timestamp
|
||||
if responsed_seconds is None:
|
||||
responsed_seconds = finish_seconds
|
||||
|
||||
llmusage = DictObject()
|
||||
llmusage.id = luid
|
||||
llmusage.llmid = full_llm.id
|
||||
llmusage.use_date = curDateString()
|
||||
llmusage.use_time = timestampstr()
|
||||
llmusage.userid = user_id
|
||||
llmusage.usages = json.dumps(usage, ensure_ascii=False, indent=4) if usage else '{}'
|
||||
ioinfo = {"input": dict(params_kw), "output": outlines}
|
||||
webpath = await write_llmio(luid, ioinfo)
|
||||
llmusage.ioinfo = webpath
|
||||
llmusage.transno = params_kw.get('transno', luid)
|
||||
llmusage.responsed_seconds = responsed_seconds
|
||||
llmusage.finish_seconds = finish_seconds
|
||||
llmusage.status = 'SUCCEEDED'
|
||||
llmusage.userorgid = user_org_id
|
||||
llmusage.tenantid = params_kw.get('tenantid', params_kw.get('tentantid'))
|
||||
llmusage.ownerid = full_llm.ownerid
|
||||
llmusage.accounting_status = 'created'
|
||||
await write_llmusage(llmusage)
|
||||
|
||||
yield {
|
||||
'chunk': None,
|
||||
'usage_data': usage or {},
|
||||
'done': True,
|
||||
'task_id': luid,
|
||||
'status': 'SUCCEEDED',
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
# Refund balance reservation on failure
|
||||
try:
|
||||
await refund_balance(env, luid)
|
||||
except Exception:
|
||||
pass
|
||||
exception(f'stream error: {e}')
|
||||
yield {'chunk': None, 'usage_data': None, 'done': True,
|
||||
'error': str(e), 'task_id': luid, 'status': 'FAILED'}
|
||||
|
||||
|
||||
async def calculate_product_cost(resource_ref_id, usage_data, user_org_id=None):
|
||||
"""计算本次消费的费用。
|
||||
|
||||
Args:
|
||||
resource_ref_id: 资源模块内部ID(= llm.id)
|
||||
usage_data: dict, 用量数据 (prompt_tokens, completion_tokens 等)
|
||||
user_org_id: 用户组织ID(用于折扣)
|
||||
|
||||
Returns:
|
||||
{'success': True, 'amount': float, 'original_amount': float,
|
||||
'cost': float, 'discount': float, 'pricing_program_id': str}
|
||||
"""
|
||||
llm, err = await _resolve_llm(resource_ref_id)
|
||||
if err:
|
||||
return {'success': False, 'message': err}
|
||||
|
||||
if not llm.ppid:
|
||||
return {'success': False, 'message': '无定价方案(ppid为空)'}
|
||||
|
||||
env = ServerEnv()
|
||||
try:
|
||||
prices = await env.buffered_charging(llm.ppid, usage_data)
|
||||
if prices is None:
|
||||
return {'success': False,
|
||||
'message': f'定价计算返回空(ppid={llm.ppid})'}
|
||||
|
||||
amount = 0
|
||||
cost = 0
|
||||
for p in prices:
|
||||
amount += p.amount
|
||||
if p.cost:
|
||||
cost += p.cost
|
||||
|
||||
# Apply customer discount
|
||||
discount = 1.0
|
||||
if user_org_id:
|
||||
try:
|
||||
discount = await env.get_customer_discount(
|
||||
llm.ownerid, user_org_id)
|
||||
except:
|
||||
pass
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'original_amount': round(amount, 6),
|
||||
'amount': round(amount * discount, 6),
|
||||
'cost': round(cost, 6),
|
||||
'discount': discount,
|
||||
'pricing_program_id': llm.ppid,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
exception(f'calculate_product_cost error: {e}')
|
||||
return {'success': False, 'message': f'定价计算失败: {e}'}
|
||||
@ -14,7 +14,6 @@ from appPublic.base64_to_file import base64_to_file, getFilenameFromBase64
|
||||
from ahserver.serverenv import get_serverenv, ServerEnv
|
||||
from ahserver.filestorage import FileStorage
|
||||
from .accounting import llm_accounting, llm_charging
|
||||
from .balance import refund_balance
|
||||
from .utils import *
|
||||
|
||||
async def sync_uapi_request(request, llm, callerid, callerorgid, params_kw=None):
|
||||
@ -29,7 +28,7 @@ async def sync_uapi_request(request, llm, callerid, callerorgid, params_kw=None)
|
||||
outlines = []
|
||||
b = None
|
||||
d = None
|
||||
luid = params_kw.get('_luid') or getID()
|
||||
luid = getID()
|
||||
try:
|
||||
start_timestamp = time.time()
|
||||
responsed_seconds = None
|
||||
@ -61,21 +60,34 @@ async def sync_uapi_request(request, llm, callerid, callerorgid, params_kw=None)
|
||||
llmusage.responsed_seconds = responsed_seconds
|
||||
llmusage.finish_seconds = finish_seconds
|
||||
llmusage.status = 'SUCCEEDED'
|
||||
llmusage.amount = 0.00
|
||||
llmusage.amount = llmusage.cost = 0.00
|
||||
""" 联机不记账
|
||||
if llm.ppid:
|
||||
try:
|
||||
charging = await llm_charging(llm.ppid, llmusage)
|
||||
if charging:
|
||||
llmusage.amount = charging.amount
|
||||
llmusage.cost = charging.cost
|
||||
else:
|
||||
llmusage.amount = llmusage.cost = 0.0
|
||||
except Exception as e:
|
||||
e = Exception(f'{llm.pid} charging error{e}')
|
||||
exception(f'{e}')
|
||||
else:
|
||||
llmusage.amount = 0
|
||||
llmusage.cost = 0
|
||||
"""
|
||||
llmusage.userorgid = callerorgid
|
||||
llmusage.tenantid = params_kw.get('tenantid', params_kw.get('tentantid'))
|
||||
llmusage.ownerid = llm.ownerid
|
||||
llmusage.accounting_status = 'created'
|
||||
b = json.dumps(d, ensure_ascii=False)
|
||||
yield b
|
||||
# await write_llmusage(llmusage)
|
||||
await write_llmusage(llmusage)
|
||||
"""联机不记账
|
||||
if llmusage.amount > 0.0001:
|
||||
await llm_accounting(llmusage)
|
||||
"""
|
||||
except Exception as e:
|
||||
# Refund balance reservation on failure
|
||||
try:
|
||||
if luid:
|
||||
await refund_balance(ServerEnv(), luid)
|
||||
except Exception:
|
||||
pass
|
||||
exception(f'{e=},{format_exc()}, {b=}')
|
||||
estr = erase_apikey(e)
|
||||
ed = {"error": f"ERROR:{estr}", "status": "FAILED" ,"llmusageid": luid}
|
||||
@ -83,88 +95,4 @@ async def sync_uapi_request(request, llm, callerid, callerorgid, params_kw=None)
|
||||
s = ''.join(s.split('\n'))
|
||||
outlines.append(ed)
|
||||
yield f'{s}\n'
|
||||
llmusage = DictObject()
|
||||
llmusage.id = luid
|
||||
llmusage.llmid = llm.id
|
||||
llmusage.use_date = curDateString()
|
||||
llmusage.use_time = timestampstr()
|
||||
llmusage.userid = callerid
|
||||
llmusage.usages = None
|
||||
ioinfo = {
|
||||
"input": params_kw,
|
||||
'output': [ed]
|
||||
}
|
||||
webpath = await write_llmio(llmusage.id, ioinfo)
|
||||
llmusage.ioinfo = webpath
|
||||
llmusage.transno = params_kw.transno
|
||||
llmusage.responsed_seconds = responsed_seconds
|
||||
llmusage.finish_seconds = finish_seconds
|
||||
llmusage.status = 'FAILED'
|
||||
llmusage.amount = 0.00
|
||||
llmusage.userorgid = callerorgid
|
||||
llmusage.tenantid = params_kw.get('tenantid', params_kw.get('tentantid'))
|
||||
llmusage.ownerid = llm.ownerid
|
||||
finally:
|
||||
await write_llmusage(llmusage)
|
||||
|
||||
async def sync_uapi_request_product(llm, api_userid, user_id, user_org_id, params_kw, luid):
|
||||
"""Product interface version: no HTTP request dependency. Returns dict."""
|
||||
env = ServerEnv()
|
||||
from uapi.appapi import UAPI
|
||||
uapi = UAPI(llm.upappid, llm.apiname)
|
||||
b = None
|
||||
d = None
|
||||
try:
|
||||
start_timestamp = time.time()
|
||||
b = await uapi.call(llm.upappid, llm.apiname, api_userid, params=params_kw)
|
||||
if isinstance(b, bytes):
|
||||
b = b.decode('utf-8')
|
||||
d = json.loads(b)
|
||||
status = d.get('status')
|
||||
usage = d.get('usage')
|
||||
if status and status != 'SUCCEEDED':
|
||||
raise Exception(d.get('error', 'Unknown error'))
|
||||
|
||||
responsed_seconds = time.time() - start_timestamp
|
||||
finish_seconds = responsed_seconds
|
||||
|
||||
llmusage = DictObject()
|
||||
llmusage.id = luid
|
||||
llmusage.llmid = llm.id
|
||||
llmusage.use_date = curDateString()
|
||||
llmusage.use_time = timestampstr()
|
||||
llmusage.userid = user_id
|
||||
llmusage.usages = json.dumps(usage, ensure_ascii=False) if usage else '{}'
|
||||
ioinfo = {"input": dict(params_kw), "output": [d]}
|
||||
webpath = await write_llmio(luid, ioinfo)
|
||||
llmusage.ioinfo = webpath
|
||||
llmusage.transno = params_kw.get('transno', luid)
|
||||
llmusage.responsed_seconds = responsed_seconds
|
||||
llmusage.finish_seconds = finish_seconds
|
||||
llmusage.status = 'SUCCEEDED'
|
||||
llmusage.amount = 0.00
|
||||
llmusage.userorgid = user_org_id
|
||||
llmusage.tenantid = params_kw.get('tenantid', params_kw.get('tentantid'))
|
||||
llmusage.ownerid = llm.ownerid
|
||||
llmusage.accounting_status = 'created'
|
||||
await write_llmusage(llmusage)
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'result': d,
|
||||
'usage_data': usage or {},
|
||||
'resource_ref_id': llm.id,
|
||||
'task_id': luid,
|
||||
'status': 'SUCCEEDED',
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
exception(f'sync_uapi_request_product error: {e}')
|
||||
estr = erase_apikey(e)
|
||||
return {
|
||||
'success': False,
|
||||
'message': str(estr),
|
||||
'task_id': luid,
|
||||
'status': 'FAILED',
|
||||
}
|
||||
|
||||
|
||||
437
llmage/utils.py
437
llmage/utils.py
@ -1,118 +1,21 @@
|
||||
import json
|
||||
import time
|
||||
import asyncio
|
||||
import aiofiles
|
||||
from random import randint
|
||||
from functools import partial
|
||||
from traceback import format_exc
|
||||
import time
|
||||
from sqlor.dbpools import DBPools, get_sor_context
|
||||
from appPublic.log import debug, exception, error, critical
|
||||
from appPublic.uniqueID import getID
|
||||
from appPublic.dictObject import DictObject
|
||||
from appPublic.timeUtils import curDateString, timestampstr
|
||||
from uapi.appapi import UAPI, sor_get_callerid, sor_get_uapi, get_uapi
|
||||
from appPublic.share_cache import cache_get, cache_invalidate
|
||||
from uapi.appapi import UAPI, sor_get_callerid, sor_get_uapi
|
||||
from ahserver.serverenv import get_serverenv, ServerEnv
|
||||
from ahserver.filestorage import FileStorage
|
||||
from appPublic.jsonConfig import getConfig
|
||||
from appPublic.streamhttpclient import StreamHttpClient
|
||||
|
||||
# =============================================================
|
||||
# Process-level cache for uapi/uapiio (static config, rarely changes)
|
||||
# =============================================================
|
||||
_UAPI_CACHE_TTL = 300 # 5 minutes
|
||||
_uapi_cache = {} # key: "upappid:apiname" -> {data, ts}
|
||||
_uapiio_cache = {} # key: "ioid" -> {data, ts}
|
||||
|
||||
|
||||
async def _get_uapi_cached(upappid, apiname):
|
||||
"""Get uapi record with process-level cache (uapi config rarely changes)"""
|
||||
global _uapi_cache
|
||||
cache_key = f"{upappid}:{apiname}"
|
||||
cached = _uapi_cache.get(cache_key)
|
||||
if cached and (time.time() - cached['ts']) < _UAPI_CACHE_TTL:
|
||||
return cached['data']
|
||||
uapi_rec = await get_uapi(upappid, apiname)
|
||||
_uapi_cache[cache_key] = {'data': uapi_rec, 'ts': time.time()}
|
||||
return uapi_rec
|
||||
|
||||
|
||||
async def _get_uapiio_cached(ioid):
|
||||
"""Get uapiio record with process-level cache (io config rarely changes)"""
|
||||
global _uapiio_cache
|
||||
if ioid is None:
|
||||
return None
|
||||
cached = _uapiio_cache.get(ioid)
|
||||
if cached and (time.time() - cached['ts']) < _UAPI_CACHE_TTL:
|
||||
return cached['data']
|
||||
env = ServerEnv()
|
||||
uapi_dbname = get_serverenv('get_module_dbname')('uapi')
|
||||
async with DBPools().sqlorContext(uapi_dbname) as sor:
|
||||
recs = await sor.R('uapiio', {'id': ioid})
|
||||
result = recs[0] if recs else None
|
||||
_uapiio_cache[ioid] = {'data': result, 'ts': time.time()}
|
||||
return result
|
||||
|
||||
|
||||
def invalidate_uapi_cache(upappid=None, apiname=None):
|
||||
"""Invalidate uapi/uapiio cache entries. Call when uapi config changes."""
|
||||
global _uapi_cache, _uapiio_cache
|
||||
if upappid and apiname:
|
||||
_uapi_cache.pop(f"{upappid}:{apiname}", None)
|
||||
else:
|
||||
_uapi_cache.clear()
|
||||
_uapiio_cache.clear()
|
||||
|
||||
|
||||
# =============================================================
|
||||
# Process-level cache for llmid lookup (model+catelogid -> llmid)
|
||||
# =============================================================
|
||||
_llmid_cache = {} # key: "model_name:catelogid" -> llmid
|
||||
|
||||
|
||||
async def _warm_llmid_cache(env):
|
||||
"""启动时全量加载 model+catelogid -> llmid 映射"""
|
||||
global _llmid_cache
|
||||
try:
|
||||
async with get_sor_context(env, 'llmage') as sor:
|
||||
sql = """SELECT a.name, b.id as catelogid, m.llmid
|
||||
FROM llm_api_map m
|
||||
JOIN llm a ON a.id = m.llmid AND a.status = 'published'
|
||||
JOIN llmcatelog b ON b.id = m.llmcatelogid"""
|
||||
recs = await sor.sqlExe(sql, {})
|
||||
for r in recs:
|
||||
key = f"{r.name}:{r.catelogid}"
|
||||
_llmid_cache[key] = r.llmid
|
||||
debug(f'[llmage] llmid cache warmed: {len(_llmid_cache)} entries')
|
||||
except Exception as e:
|
||||
exception(f'[llmage] llmid cache warm failed: {e}')
|
||||
_llmid_cache = {}
|
||||
|
||||
|
||||
async def get_llmid_cached(env, model_name, catelogid):
|
||||
"""从缓存获取 llmid,未命中则查 DB 并缓存"""
|
||||
global _llmid_cache
|
||||
key = f"{model_name}:{catelogid}"
|
||||
if key in _llmid_cache:
|
||||
return _llmid_cache[key]
|
||||
# 缓存未命中,查 DB(兼容模型在缓存预热后新增的场景)
|
||||
async with get_sor_context(env, 'llmage') as sor:
|
||||
sql = """SELECT m.llmid
|
||||
FROM llm_api_map m
|
||||
JOIN llm a ON a.id = m.llmid AND a.name = ${model}$ AND a.status = 'published'
|
||||
JOIN llmcatelog b ON b.id = m.llmcatelogid AND (b.id = ${catelogid}$ OR b.name = ${catelogid}$)"""
|
||||
recs = await sor.sqlExe(sql, {'model': model_name, 'catelogid': catelogid})
|
||||
llmid = recs[0].llmid if recs else None
|
||||
if llmid:
|
||||
_llmid_cache[key] = llmid
|
||||
return llmid
|
||||
|
||||
|
||||
def invalidate_llmid_cache():
|
||||
global _llmid_cache
|
||||
_llmid_cache.clear()
|
||||
|
||||
|
||||
async def update_llmusage(ns):
|
||||
env = ServerEnv()
|
||||
async with get_sor_context(env, 'llmage') as sor:
|
||||
@ -143,14 +46,13 @@ async def get_tpac_balance(tpac, userid):
|
||||
exception(f'{url=}, {userid=}, error:{e}')
|
||||
return None
|
||||
|
||||
async def tpac_accounting(tpac, userid, llmid, amount, usage, luid, model):
|
||||
async def tpac_accounting(tpac, userid, llmid, amount, usage, luid):
|
||||
url = tpac.tpac_accounting_url
|
||||
hc = StreamHttpClient()
|
||||
d = {
|
||||
'userid': userid,
|
||||
'llmid': llmid,
|
||||
'amount': amount,
|
||||
'model': model,
|
||||
'usage': usage
|
||||
}
|
||||
status = 'failed'
|
||||
@ -199,32 +101,6 @@ async def write_llmio(luid, io_dic):
|
||||
webpath = await fs.save(name, s, userid='llmio')
|
||||
return webpath
|
||||
|
||||
async def get_llmusage_by_id(usage_id):
|
||||
"""从数据库获取 llmusage 记录"""
|
||||
env = ServerEnv()
|
||||
async with get_sor_context(env, 'llmage') as sor:
|
||||
sql = "select id, llmid, ioinfo, usages from llmusage where id = ${id}$"
|
||||
recs = await sor.sqlExe(sql, {'id': usage_id})
|
||||
if recs and len(recs) > 0:
|
||||
return dict(recs[0])
|
||||
return None
|
||||
|
||||
|
||||
async def read_ioinfo_content(ioinfo_webpath):
|
||||
"""从 FileStorage 读取交互信息文件内容"""
|
||||
if not ioinfo_webpath:
|
||||
return None
|
||||
try:
|
||||
fs = FileStorage()
|
||||
real_path = fs.realPath(ioinfo_webpath)
|
||||
async with aiofiles.open(real_path, 'rb') as f:
|
||||
bin_data = await f.read()
|
||||
return json.loads(bin_data.decode('utf-8'))
|
||||
except Exception as e:
|
||||
debug(f'read_ioinfo_content error: {e}')
|
||||
return None
|
||||
|
||||
|
||||
async def llm_query_orders(userorgid, page, pagerows=80):
|
||||
env = ServerEnv()
|
||||
async with get_sor_context(env, 'llmage') as sor:
|
||||
@ -275,11 +151,10 @@ def erase_apikey(e):
|
||||
async def get_llmproviders():
|
||||
env = ServerEnv()
|
||||
async with get_sor_context(env, 'llmage') as sor:
|
||||
sql = """select a.providerid, b.orgname
|
||||
sql = """select a.providerid, a.iconid, b.orgname
|
||||
from llm a, organization b
|
||||
where a.providerid = b.id
|
||||
and a.status = 'published'
|
||||
group by a.providerid, b.orgname"""
|
||||
group by a.providerid, a.iconid, b.orgname"""
|
||||
return await sor.sqlExe(sql, {})
|
||||
return []
|
||||
|
||||
@ -290,36 +165,14 @@ async def get_llms_sort_by_provider():
|
||||
sql = """select a.*, b.orgname from llm a, organization b
|
||||
where a.enabled_date <= ${today}$
|
||||
and a.expired_date > ${today}$
|
||||
and a.status = 'published'
|
||||
and a.providerid = b.id
|
||||
order by a.providerid, a.name
|
||||
"""
|
||||
order by a.providerid, a.id
|
||||
"""
|
||||
recs = await sor.sqlExe(sql, {'today': today})
|
||||
# 批量查询所有模型的 ppid 映射
|
||||
llm_ids = [r.id for r in recs]
|
||||
pp_map = {} # llmid -> [ppid, ...]
|
||||
if llm_ids:
|
||||
placeholders = ','.join([f"'{lid}'" for lid in llm_ids])
|
||||
pp_sql = f"select distinct llmid, ppid from llm_api_map where llmid in ({placeholders}) and ppid is not null"
|
||||
pp_recs = await sor.sqlExe(pp_sql, {})
|
||||
for pp in pp_recs:
|
||||
pp_map.setdefault(pp.llmid, []).append(pp.ppid)
|
||||
|
||||
d = []
|
||||
x = None
|
||||
oldpid = '-111'
|
||||
for l in recs:
|
||||
# 获取定价展示文本
|
||||
pricing_list = []
|
||||
for ppid in pp_map.get(l.id, []):
|
||||
try:
|
||||
pd = await env.get_pricing_display(ppid, model=l.name)
|
||||
if pd:
|
||||
pricing_list.append(pd.get('display_text', ''))
|
||||
except:
|
||||
pass
|
||||
l.pricing_display = pricing_list
|
||||
|
||||
if l.providerid != oldpid:
|
||||
x = {
|
||||
'id': l.providerid,
|
||||
@ -333,44 +186,6 @@ where a.enabled_date <= ${today}$
|
||||
return d
|
||||
return []
|
||||
|
||||
async def get_llmage_llm(llmid=None, catelogid=None):
|
||||
"""Unified accessor for llm + llm_api_map + llmcatelog.
|
||||
For non-API-call scenarios only (display, listing, querying, accounting).
|
||||
Do NOT use for vendor model API calls — use get_llm() instead.
|
||||
|
||||
- llmid: get specific llm by id (returns single DictObject or None)
|
||||
- catelogid: filter by catalog (returns list)
|
||||
- neither: return all with catalog info (returns list)
|
||||
"""
|
||||
env = ServerEnv()
|
||||
async with get_sor_context(env, 'llmage') as sor:
|
||||
sql = """select a.id, a.name, a.model, a.providerid, a.description,
|
||||
a.iconid, a.upappid, a.ownerid, a.min_balance, a.status,
|
||||
m.llmcatelogid, m.apiname, m.query_apiname, m.query_period, m.ppid, m.isdefaultcatelog,
|
||||
lc.name as catelogname
|
||||
from llm a
|
||||
join llm_api_map m on a.id = m.llmid
|
||||
join llmcatelog lc on m.llmcatelogid = lc.id
|
||||
where 1=1
|
||||
"""
|
||||
ns = {}
|
||||
if llmid:
|
||||
sql += " and a.id = ${llmid}$"
|
||||
ns['llmid'] = llmid
|
||||
if catelogid:
|
||||
sql += " and m.llmcatelogid = ${catelogid}$"
|
||||
ns['catelogid'] = catelogid
|
||||
else:
|
||||
sql += " and m.isdefaultcatelog = '1'"
|
||||
elif catelogid:
|
||||
sql += " and m.llmcatelogid = ${catelogid}$"
|
||||
ns['catelogid'] = catelogid
|
||||
sql += " order by m.llmcatelogid, a.id, a.name"
|
||||
recs = await sor.sqlExe(sql, ns)
|
||||
if llmid:
|
||||
return recs[0] if recs else None
|
||||
return recs
|
||||
|
||||
async def get_llmcatelogs():
|
||||
db = DBPools()
|
||||
dbname = get_serverenv('get_module_dbname')('llmage')
|
||||
@ -380,19 +195,7 @@ async def get_llmcatelogs():
|
||||
|
||||
return []
|
||||
|
||||
async def get_pricing_text(l):
|
||||
try:
|
||||
env = ServerEnv()
|
||||
pd = await env.get_pricing_display(l.ppid, model=l.name)
|
||||
if pd:
|
||||
l.pricing_display = pd.get('display_text', '')
|
||||
except Exception as e:
|
||||
debug(f'{e}')
|
||||
pass
|
||||
|
||||
async def get_llms_by_catelog_to_customer(catelogid=None, orderby='providerid, name'):
|
||||
# icon "{{entire_url('/appbase/show_icon.dspy')}}?id={{llm.iconid}}"
|
||||
# pricing: llm.pricing_display
|
||||
async def get_llms_by_catelog_to_customer(catelogid=None, orderby='providerid'):
|
||||
env = ServerEnv()
|
||||
async with get_sor_context(env, 'llmage') as sor:
|
||||
today = curDateString()
|
||||
@ -403,24 +206,19 @@ m.llmcatelogid as catelog_id,
|
||||
m.apiname,
|
||||
m.query_apiname,
|
||||
m.query_period,
|
||||
m.ppid,
|
||||
o.orgname as provider_name
|
||||
m.ppid
|
||||
from llm a
|
||||
join llm_api_map m on a.id = m.llmid
|
||||
join llmcatelog b on m.llmcatelogid = b.id
|
||||
join organization o
|
||||
where a.enabled_date <= ${today}$
|
||||
and a.status = 'published'
|
||||
and m.ppid is not null
|
||||
and a.expired_date > ${today}$
|
||||
and a.providerid = o.id
|
||||
"""
|
||||
params = {'today': today}
|
||||
sortstr='catelog_id, ' + orderby
|
||||
params = {'today': today, 'sort': sortstr}
|
||||
if catelogid:
|
||||
sql += " and m.llmcatelogid = ${catelogid}$"
|
||||
params['catelogid'] = catelogid
|
||||
|
||||
sql += " order by m.llmcatelogid, a.providerid, a.name"
|
||||
|
||||
debug(f'{sql=}')
|
||||
recs = await sor.sqlExe(sql, params.copy())
|
||||
@ -429,7 +227,6 @@ o.orgname as provider_name
|
||||
cid = ''
|
||||
x = None
|
||||
for r in recs:
|
||||
await get_pricing_text(r)
|
||||
if cid != r.catelog_id:
|
||||
x = {
|
||||
'catelogid': r.catelog_id,
|
||||
@ -448,44 +245,24 @@ async def get_llms_by_catelog(catelogid=None, orderby='providerid'):
|
||||
async with get_sor_context(env, 'llmage') as sor:
|
||||
today = curDateString()
|
||||
# Join with llm_api_map to get catalog relationship
|
||||
sql = """select distinct a.*, b.name as catelogname, m.llmcatelogid as catelog_id
|
||||
sql = """select distinct a.*, b.name as catelogname, m.llmcatelogid as catelog_id
|
||||
from llm a
|
||||
join llm_api_map m on a.id = m.llmid
|
||||
join llmcatelog b on m.llmcatelogid = b.id
|
||||
where a.enabled_date <= ${today}$
|
||||
and a.status = 'published'
|
||||
and a.expired_date > ${today}$"""
|
||||
params = {'today': today, 'sort': orderby}
|
||||
if catelogid:
|
||||
sql += " and m.llmcatelogid = ${catelogid}$"
|
||||
params['catelogid'] = catelogid
|
||||
sql += " order by m.llmcatelogid, a.id, a.name"
|
||||
|
||||
sql += " order by m.llmcatelogid, a.id"
|
||||
|
||||
recs = await sor.sqlExe(sql, params)
|
||||
# 批量查询所有模型的 ppid 映射(避免 N+1 查询)
|
||||
llm_ids = [r.id for r in recs]
|
||||
pp_map = {}
|
||||
if llm_ids:
|
||||
placeholders = ','.join([f"'{lid}'" for lid in llm_ids])
|
||||
pp_sql = f"select distinct llmid, ppid from llm_api_map where llmid in ({placeholders}) and ppid is not null"
|
||||
pp_recs = await sor.sqlExe(pp_sql, {})
|
||||
for pp in pp_recs:
|
||||
pp_map.setdefault(pp.llmid, []).append(pp.ppid)
|
||||
|
||||
d = []
|
||||
cid = ''
|
||||
x = None
|
||||
for r in recs:
|
||||
pricing_list = []
|
||||
for ppid in pp_map.get(r.id, []):
|
||||
try:
|
||||
pd = await env.get_pricing_display(ppid, model=r.name)
|
||||
if pd:
|
||||
pricing_list.append(pd.get('display_text', ''))
|
||||
except:
|
||||
pass
|
||||
r.pricing_display = pricing_list
|
||||
|
||||
if cid != r.catelog_id:
|
||||
x = {
|
||||
'catelogid': r.catelog_id,
|
||||
@ -499,76 +276,69 @@ async def get_llms_by_catelog(catelogid=None, orderby='providerid'):
|
||||
return d
|
||||
return []
|
||||
|
||||
async def get_llm_catelogs(llmid):
|
||||
"""Get all catelog entries for a given llmid from llm_api_map + llmcatelog.
|
||||
Returns list of {catelogid, catelogname, isdefaultcatelog}
|
||||
"""
|
||||
if not llmid:
|
||||
return []
|
||||
llmage_dbname = get_serverenv('get_module_dbname')('llmage')
|
||||
async with DBPools().sqlorContext(llmage_dbname) as sor:
|
||||
sql = """select m.llmcatelogid as catelogid, lc.name as catelogname, m.isdefaultcatelog
|
||||
from llm_api_map m
|
||||
join llmcatelog lc on m.llmcatelogid = lc.id
|
||||
where m.llmid = ${llmid}$
|
||||
order by m.isdefaultcatelog desc"""
|
||||
recs = await sor.sqlExe(sql, {'llmid': llmid})
|
||||
return [dict(catelogid=r.catelogid, catelogname=r.catelogname, isdefault=r.isdefaultcatelog == '1') for r in recs]
|
||||
|
||||
|
||||
async def get_llm(llmid, catelogid=None):
|
||||
"""Get LLM with full uapi info for vendor API calls.
|
||||
Refactored to use get_llmage_llm() + cached uapi/uapiio lookups
|
||||
instead of a 6-table JOIN.
|
||||
|
||||
Returns DictObject with merged fields:
|
||||
From get_llmage_llm: id, name, model, providerid, description,
|
||||
iconid, upappid, ownerid, min_balance, status, llmcatelogid,
|
||||
apiname, query_apiname, query_period, ppid, isdefaultcatelog,
|
||||
catelogname
|
||||
From uapi (cached): ioid, stream, callbackurl
|
||||
From uapiio (cached): input_fields
|
||||
"""
|
||||
# Step 1: Get base info from get_llmage_llm (3-table JOIN: llm + llm_api_map + llmcatelog)
|
||||
llm = await get_llmage_llm(llmid, catelogid)
|
||||
if not llm:
|
||||
debug(f'{llmid=} not found via get_llmage_llm')
|
||||
return None
|
||||
|
||||
# Step 2: Get uapi info (cached, keyed by upappid:apiname)
|
||||
uapi = await _get_uapi_cached(llm.upappid, llm.apiname)
|
||||
if not uapi:
|
||||
debug(f'uapi not found: upappid={llm.upappid}, apiname={llm.apiname}')
|
||||
return None
|
||||
|
||||
# Step 3: Get uapiio info (cached, keyed by ioid)
|
||||
uapiio = await _get_uapiio_cached(uapi.ioid)
|
||||
|
||||
# Merge uapi fields into llm result
|
||||
llm.ioid = uapi.ioid
|
||||
llm.stream = uapi.stream
|
||||
llm.callbackurl = uapi.callbackurl
|
||||
llm.input_fields = uapiio.input_fields if uapiio else '{}'
|
||||
|
||||
return llm
|
||||
today = curDateString()
|
||||
env = ServerEnv()
|
||||
async with get_sor_context(env, 'llmage') as sor:
|
||||
sql = """select a.id,
|
||||
a.name,
|
||||
a.model,
|
||||
a.providerid,
|
||||
a.description,
|
||||
a.iconid,
|
||||
a.upappid,
|
||||
a.ownerid,
|
||||
a.min_balance,
|
||||
m.llmcatelogid,
|
||||
m.apiname,
|
||||
m.query_apiname,
|
||||
m.query_period,
|
||||
m.ppid,
|
||||
e.ioid,
|
||||
e.stream,
|
||||
e.callbackurl,
|
||||
f.input_fields,
|
||||
lc.name as catelogname
|
||||
from llm a
|
||||
,llm_api_map m
|
||||
,llmcatelog lc
|
||||
,upapp c
|
||||
,uapi e
|
||||
,uapiio f
|
||||
where a.id = m.llmid
|
||||
and a.upappid = c.id
|
||||
and c.id = e.upappid
|
||||
and m.apiname = e.name
|
||||
and e.ioid = f.id
|
||||
and a.id = ${llmid}$
|
||||
and a.expired_date > ${today}$
|
||||
and a.enabled_date <= ${today}$
|
||||
"""
|
||||
ns = {'llmid': llmid, 'today': today}
|
||||
if catelogid:
|
||||
sql += ' and m.llmcatelogid = ${catelogid}$ '
|
||||
ns['catelogid'] = catelogid
|
||||
else:
|
||||
sql += " and m.isdefaultcatelog = '1'"
|
||||
recs = await sor.sqlExe(sql, ns.copy())
|
||||
if len(recs) > 0:
|
||||
r = recs[0]
|
||||
return r
|
||||
else:
|
||||
debug(f'{llmid=} not found, {ns=}, {sql=}')
|
||||
return None
|
||||
exception(f'Error: {format_exc()}')
|
||||
return None
|
||||
|
||||
|
||||
async def write_llmusage(llmusage):
|
||||
env = ServerEnv()
|
||||
async with get_sor_context(env, 'llmage') as sor:
|
||||
n = 0
|
||||
part0 = llmusage.id
|
||||
while True:
|
||||
recs = await sor.R('llmusage', {'id': llmusage.id})
|
||||
if len(recs) == 0:
|
||||
break
|
||||
n = n + 1
|
||||
llmusage.id = f'{part0}*{n}'
|
||||
await sor.C('llmusage', llmusage)
|
||||
|
||||
async def llm_query_price(llmid, config_data):
|
||||
env = ServerEnv()
|
||||
llm = await get_llmage_llm(llmid)
|
||||
llm = await get_llm(llmid)
|
||||
if llm.ppid is None:
|
||||
e = Exception(f'{llm=} ppid is None')
|
||||
exception(f'{e}')
|
||||
@ -576,76 +346,3 @@ async def llm_query_price(llmid, config_data):
|
||||
prices = await env.buffered_charging(llm.ppid, config_data)
|
||||
return prices
|
||||
|
||||
import base64 as _base64
|
||||
import mimetypes as _mimetypes
|
||||
import os as _os
|
||||
|
||||
def _file_to_b64(filepath):
|
||||
"""读取文件并返回 base64 字符串"""
|
||||
with open(filepath, 'rb') as fh:
|
||||
return _base64.b64encode(fh.read()).decode('utf-8')
|
||||
|
||||
def _file_mime(filepath):
|
||||
"""猜测文件 MIME 类型"""
|
||||
mime, _ = _mimetypes.guess_type(filepath)
|
||||
return mime or 'application/octet-stream'
|
||||
|
||||
async def get_plaza_models(providerid='', catelogid='', search=''):
|
||||
"""Flat model list for cockpit plaza: filterable by provider/catalog/search, sorted by name."""
|
||||
env = ServerEnv()
|
||||
data = await get_llms_by_catelog_to_customer(
|
||||
catelogid=catelogid if catelogid else None,
|
||||
orderby='a.name'
|
||||
)
|
||||
result = []
|
||||
for cate in data:
|
||||
for llm in cate['llms']:
|
||||
if providerid and llm.providerid != providerid:
|
||||
continue
|
||||
if search:
|
||||
sl = search.lower()
|
||||
n = (llm.name or '').lower()
|
||||
d = (llm.description or '').lower()
|
||||
if sl not in n and sl not in d:
|
||||
continue
|
||||
result.append({
|
||||
'id': llm.id, 'name': llm.name, 'model': llm.model,
|
||||
'description': llm.description or '', 'iconid': llm.iconid,
|
||||
'providerid': llm.providerid,
|
||||
'provider_name': getattr(llm, 'provider_name', getattr(llm, 'orgname', '')),
|
||||
'catelog_id': getattr(llm, 'catelog_id', ''),
|
||||
'catelogname': getattr(llm, 'catelogname', ''),
|
||||
'pricing_display': getattr(llm, 'pricing_display', []),
|
||||
})
|
||||
return result
|
||||
|
||||
ServerEnv().base64 = _base64
|
||||
ServerEnv().mimetypes = _mimetypes
|
||||
ServerEnv().os = _os
|
||||
ServerEnv().file_to_b64 = _file_to_b64
|
||||
ServerEnv().file_mime = _file_mime
|
||||
|
||||
async def get_model_max_cost(llmid):
|
||||
"""Get historical max actual customer charge for a model from llmusage."""
|
||||
env = ServerEnv()
|
||||
async with get_sor_context(env, 'llmage') as sor:
|
||||
sql = """SELECT MAX(amount) as max_cost
|
||||
FROM llmusage
|
||||
WHERE llmid = ${llmid}$
|
||||
AND status = 'SUCCEEDED'
|
||||
AND amount IS NOT NULL
|
||||
AND amount > 0"""
|
||||
recs = await sor.sqlExe(sql, {'llmid': llmid})
|
||||
if recs and recs[0].max_cost:
|
||||
return float(recs[0].max_cost)
|
||||
return 0
|
||||
|
||||
async def update_model_max_cost(llmid, new_max):
|
||||
"""Persist updated max_cost to llm table."""
|
||||
env = ServerEnv()
|
||||
async with get_sor_context(env, 'llmage') as sor:
|
||||
await sor.U('llm', {'id': llmid, 'max_cost': float(new_max)})
|
||||
|
||||
|
||||
# Redis balance reservation (implemented in balance.py)
|
||||
from .balance import reserve_balance, finalize_balance, refund_balance
|
||||
|
||||
219
load_test.py
219
load_test.py
@ -1,219 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""并发压力测试 llmage /v1/chat/completions — 4分钟×3组(50/100/200),TTFB/QPM/500/主机资源"""
|
||||
|
||||
import asyncio, aiohttp, time, json, sys, statistics, subprocess
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List
|
||||
|
||||
URL = "https://token.opencomputing.cn/llmage/v1/chat/completions"
|
||||
TOKEN = "V9J41PngWBUU6gdHWJWDJ"
|
||||
MODEL = "qwen3.6-35b-a3b"
|
||||
DURATION = 240 # 4 minutes
|
||||
CONCURRENCIES = [50, 100, 200]
|
||||
|
||||
# Host monitoring via SSH
|
||||
HOST_SSH = "token@token.opencomputing.cn"
|
||||
HOST_CPU_CMD = "top -bn1 | grep 'Cpu(s)' | awk '{print $2+$4}'"
|
||||
HOST_MEM_CMD = "free -m | awk '/Mem:/{printf \"%.1f\", $3/$2*100}'"
|
||||
HOST_LOAD_CMD = "uptime | awk -F'[a-z]:' '{print $2}' | awk '{print $1,$2,$3}'"
|
||||
|
||||
|
||||
@dataclass
|
||||
class ReqStat:
|
||||
idx: int
|
||||
start_ts: float
|
||||
first_byte_ts: float | None = None
|
||||
end_ts: float | None = None
|
||||
http_status: int = 0
|
||||
|
||||
|
||||
@dataclass
|
||||
class HostSnap:
|
||||
ts: float
|
||||
cpu: float
|
||||
mem: float
|
||||
load: str
|
||||
|
||||
|
||||
async def host_snapshot() -> HostSnap:
|
||||
loop = asyncio.get_running_loop()
|
||||
try:
|
||||
cpu = float((await loop.run_in_executor(
|
||||
None, lambda: subprocess.run(
|
||||
["ssh", "-o", "ConnectTimeout=3", HOST_SSH, HOST_CPU_CMD],
|
||||
capture_output=True, text=True, timeout=5
|
||||
).stdout.strip()
|
||||
)) or 0)
|
||||
except:
|
||||
cpu = 0
|
||||
try:
|
||||
mem = float((await loop.run_in_executor(
|
||||
None, lambda: subprocess.run(
|
||||
["ssh", "-o", "ConnectTimeout=3", HOST_SSH, HOST_MEM_CMD],
|
||||
capture_output=True, text=True, timeout=5
|
||||
).stdout.strip()
|
||||
)) or 0)
|
||||
except:
|
||||
mem = 0
|
||||
try:
|
||||
load = (await loop.run_in_executor(
|
||||
None, lambda: subprocess.run(
|
||||
["ssh", "-o", "ConnectTimeout=3", HOST_SSH, HOST_LOAD_CMD],
|
||||
capture_output=True, text=True, timeout=5
|
||||
).stdout.strip()
|
||||
)) or "N/A"
|
||||
except:
|
||||
load = "N/A"
|
||||
return HostSnap(ts=time.monotonic(), cpu=cpu, mem=mem, load=load)
|
||||
|
||||
|
||||
async def worker(session: aiohttp.ClientSession, idx: int, stats_out: list):
|
||||
payload = {
|
||||
"model": MODEL,
|
||||
"stream": True,
|
||||
"messages": [{"role": "user", "content": f"你是谁? 请用一句话回答,编号{idx}"}],
|
||||
}
|
||||
stat = ReqStat(idx=idx, start_ts=time.monotonic())
|
||||
try:
|
||||
async with session.post(
|
||||
URL, json=payload,
|
||||
headers={"Content-Type": "application/json", "Authorization": f"Bearer {TOKEN}"},
|
||||
timeout=aiohttp.ClientTimeout(total=120),
|
||||
) as resp:
|
||||
stat.http_status = resp.status
|
||||
first = True
|
||||
async for line in resp.content:
|
||||
if first:
|
||||
stat.first_byte_ts = time.monotonic()
|
||||
first = False
|
||||
stat.end_ts = time.monotonic()
|
||||
except Exception:
|
||||
stat.end_ts = time.monotonic()
|
||||
stats_out.append(stat)
|
||||
|
||||
|
||||
async def run_concurrency(concurrency: int):
|
||||
stats: List[ReqStat] = []
|
||||
host_snaps: List[HostSnap] = []
|
||||
idx = 0
|
||||
stop_at = time.monotonic() + DURATION
|
||||
|
||||
connector = aiohttp.TCPConnector(limit=concurrency + 50, force_close=True)
|
||||
async with aiohttp.ClientSession(connector=connector) as session:
|
||||
tasks: list[asyncio.Task] = []
|
||||
last_snap = 0
|
||||
|
||||
while time.monotonic() < stop_at:
|
||||
# Fill to concurrency
|
||||
while len(tasks) < concurrency and time.monotonic() < stop_at:
|
||||
idx += 1
|
||||
tasks.append(asyncio.create_task(worker(session, idx, stats)))
|
||||
|
||||
if not tasks:
|
||||
break
|
||||
|
||||
# Host snapshot every 15s
|
||||
now = time.monotonic()
|
||||
if now - last_snap > 15:
|
||||
host_snaps.append(await host_snapshot())
|
||||
last_snap = now
|
||||
sys.stdout.write(f"\r [{concurrency}] {len(stats)} req | CPU:{host_snaps[-1].cpu:.0f}% MEM:{host_snaps[-1].mem:.0f}% LOAD:{host_snaps[-1].load}")
|
||||
sys.stdout.flush()
|
||||
|
||||
done, tasks = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED, timeout=0.5)
|
||||
tasks = list(tasks)
|
||||
|
||||
# Drain remaining
|
||||
if tasks:
|
||||
await asyncio.wait(tasks)
|
||||
|
||||
# Final snapshot
|
||||
host_snaps.append(await host_snapshot())
|
||||
|
||||
return stats, host_snaps
|
||||
|
||||
|
||||
def analyze(concurrency: int, stats: List[ReqStat], host_snaps: List[HostSnap]):
|
||||
ttfb_list = [s.first_byte_ts - s.start_ts for s in stats if s.first_byte_ts]
|
||||
total_list = [s.end_ts - s.start_ts for s in stats if s.end_ts and s.first_byte_ts]
|
||||
failed = sum(1 for s in stats if s.first_byte_ts is None)
|
||||
status_500 = sum(1 for s in stats if s.http_status >= 500)
|
||||
status_errors = sum(1 for s in stats if s.http_status >= 400 and s.http_status != 200)
|
||||
|
||||
total_req = len(stats)
|
||||
qpm = total_req / (DURATION / 60)
|
||||
|
||||
print(f"\n{'='*65}")
|
||||
print(f" 并发={concurrency} | {DURATION}s | 请求={total_req} | 失败={failed} | 5xx={status_500}")
|
||||
print(f"{'='*65}")
|
||||
if ttfb_list:
|
||||
print(f" TTFB(s): min={min(ttfb_list):.3f} avg={statistics.mean(ttfb_list):.3f} "
|
||||
f"p50={statistics.median(ttfb_list):.3f} p95={_pct(ttfb_list,95):.3f} p99={_pct(ttfb_list,99):.3f}")
|
||||
if total_list:
|
||||
print(f" 完成(s): min={min(total_list):.3f} avg={statistics.mean(total_list):.3f} "
|
||||
f"p50={statistics.median(total_list):.3f} p95={_pct(total_list,95):.3f} p99={_pct(total_list,99):.3f}")
|
||||
print(f" QPM: {qpm:.1f} | QPS: {total_req/DURATION:.1f} | HTTP错误: {status_errors}")
|
||||
|
||||
# Per-minute breakdown
|
||||
for minute in range(int(DURATION / 60)):
|
||||
win_start = minute * 60
|
||||
win_end = (minute + 1) * 60
|
||||
pm = sum(1 for s in stats if s.end_ts and s.first_byte_ts
|
||||
and win_start <= (s.start_ts - stats[0].start_ts) < win_end)
|
||||
print(f" 第{minute+1}分钟完成: {pm}")
|
||||
|
||||
# Host stats
|
||||
if host_snaps:
|
||||
cpus = [s.cpu for s in host_snaps if s.cpu > 0]
|
||||
mems = [s.mem for s in host_snaps if s.mem > 0]
|
||||
print(f" 主机: CPU avg={statistics.mean(cpus):.1f}% max={max(cpus):.1f}% "
|
||||
f"MEM avg={statistics.mean(mems):.1f}% max={max(mems):.1f}% "
|
||||
f"LOAD max={max((s.load for s in host_snaps if s.load!='N/A'), default='N/A')}")
|
||||
|
||||
return {"concurrency": concurrency, "total": total_req, "failed": failed,
|
||||
"status_500": status_500, "status_errors": status_errors,
|
||||
"ttfb_avg": statistics.mean(ttfb_list) if ttfb_list else None,
|
||||
"ttfb_p50": statistics.median(ttfb_list) if ttfb_list else None,
|
||||
"ttfb_p95": _pct(ttfb_list, 95) if ttfb_list else None,
|
||||
"ttfb_p99": _pct(ttfb_list, 99) if ttfb_list else None,
|
||||
"total_avg": statistics.mean(total_list) if total_list else None,
|
||||
"total_p50": statistics.median(total_list) if total_list else None,
|
||||
"qpm": qpm,
|
||||
"host_cpu_avg": statistics.mean(cpus) if cpus else None,
|
||||
"host_cpu_max": max(cpus) if cpus else None,
|
||||
"host_mem_avg": statistics.mean(mems) if mems else None}
|
||||
|
||||
|
||||
def _pct(data, p):
|
||||
return sorted(data)[int(len(data) * p / 100)]
|
||||
|
||||
|
||||
async def main():
|
||||
results = []
|
||||
for c in CONCURRENCIES:
|
||||
print(f"\n>>> 开始 并发={c} [{DURATION}s] ...")
|
||||
stats, snaps = await run_concurrency(c)
|
||||
r = analyze(c, stats, snaps)
|
||||
results.append(r)
|
||||
|
||||
print(f"\n{'='*65}")
|
||||
print(" 汇总对比")
|
||||
print(f"{'='*65}")
|
||||
print(f" {'并发':>5} {'请求':>7} {'失败':>5} {'5xx':>5} "
|
||||
f"{'TTFB_avg':>9} {'TTFB_p50':>9} {'TTFB_p95':>9} {'TTFB_p99':>9} "
|
||||
f"{'完成_avg':>9} {'QPM':>8} {'CPU_avg':>7} {'CPU_max':>7}")
|
||||
for r in results:
|
||||
t_avg = f'{r["ttfb_avg"]:.3f}' if r['ttfb_avg'] else 'N/A'
|
||||
t_p50 = f'{r["ttfb_p50"]:.3f}' if r['ttfb_p50'] else 'N/A'
|
||||
t_p95 = f'{r["ttfb_p95"]:.3f}' if r['ttfb_p95'] else 'N/A'
|
||||
t_p99 = f'{r["ttfb_p99"]:.3f}' if r['ttfb_p99'] else 'N/A'
|
||||
c_avg = f'{r["total_avg"]:.3f}' if r['total_avg'] else 'N/A'
|
||||
cpu = f'{r["host_cpu_avg"]:.0f}%' if r['host_cpu_avg'] else 'N/A'
|
||||
cmax = f'{r["host_cpu_max"]:.0f}%' if r['host_cpu_max'] else 'N/A'
|
||||
print(f" {r['concurrency']:>5} {r['total']:>7} {r['failed']:>5} {r['status_500']:>5} "
|
||||
f"{t_avg:>9} {t_p50:>9} {t_p95:>9} {t_p99:>9} "
|
||||
f"{c_avg:>9} {r['qpm']:>8.0f} {cpu:>7} {cmax:>7}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@ -72,24 +72,15 @@
|
||||
"title": "最低余额",
|
||||
"type": "float",
|
||||
"length": 20,
|
||||
"default": 10,
|
||||
"dec": 2
|
||||
},
|
||||
{
|
||||
"name": "status",
|
||||
"title": "上架状态",
|
||||
"type": "str",
|
||||
"length": 16,
|
||||
"nullable": "no",
|
||||
"default": "unpublished"
|
||||
"default": 10
|
||||
}
|
||||
],
|
||||
"codes": [
|
||||
{
|
||||
"field": "providerid",
|
||||
"table": "suppliers",
|
||||
"table": "organization",
|
||||
"valuefield": "id",
|
||||
"textfield": "supplier_name"
|
||||
"textfield": "orgname"
|
||||
},
|
||||
{
|
||||
"field": "iconid",
|
||||
@ -108,20 +99,6 @@
|
||||
"table": "organization",
|
||||
"valuefield": "id",
|
||||
"textfield": "orgname"
|
||||
},
|
||||
{
|
||||
"field": "status",
|
||||
"table": "appcodes_kv",
|
||||
"valuefield": "k",
|
||||
"textfield": "v",
|
||||
"cond": "parentid='llm_status'"
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
{
|
||||
"name": "idx_llm_name",
|
||||
"idxtype": "unique",
|
||||
"idxfields": ["name"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
BIN
models/llm.xlsx
Normal file
BIN
models/llm.xlsx
Normal file
Binary file not shown.
@ -4,99 +4,81 @@
|
||||
{
|
||||
"name": "llm_api_map",
|
||||
"title": "模型API映射表",
|
||||
"primary": [
|
||||
"id"
|
||||
],
|
||||
"primary": "id",
|
||||
"catelog": "relation"
|
||||
}
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"name": "id",
|
||||
"type": "str",
|
||||
"type": "varchar(32)",
|
||||
"not_null": true,
|
||||
"title": "主键ID",
|
||||
"length": 32
|
||||
"title": "主键ID"
|
||||
},
|
||||
{
|
||||
"name": "llmid",
|
||||
"type": "str",
|
||||
"type": "varchar(32)",
|
||||
"not_null": true,
|
||||
"title": "模型ID",
|
||||
"length": 32
|
||||
"title": "模型ID"
|
||||
},
|
||||
{
|
||||
"name": "llmcatelogid",
|
||||
"type": "str",
|
||||
"type": "varchar(32)",
|
||||
"not_null": true,
|
||||
"title": "模型分类ID",
|
||||
"length": 32
|
||||
"title": "模型分类ID"
|
||||
},
|
||||
{
|
||||
"name": "apiname",
|
||||
"type": "str",
|
||||
"type": "varchar(100)",
|
||||
"not_null": true,
|
||||
"title": "接口名称",
|
||||
"length": 100
|
||||
"title": "接口名称"
|
||||
},
|
||||
{
|
||||
"name": "query_apiname",
|
||||
"type": "str",
|
||||
"title": "任务结果查询接口名称",
|
||||
"length": 100
|
||||
"type": "varchar(100)",
|
||||
"title": "任务结果查询接口名称"
|
||||
},
|
||||
{
|
||||
"name": "query_period",
|
||||
"type": "long",
|
||||
"type": "bigint",
|
||||
"default": 30,
|
||||
"title": "任务查询间隔(秒)"
|
||||
},
|
||||
{
|
||||
"name": "ppid",
|
||||
"type": "str",
|
||||
"title": "定价ID",
|
||||
"length": 32
|
||||
"type": "varchar(32)",
|
||||
"title": "定价ID"
|
||||
},
|
||||
{
|
||||
"name": "isdefaultcatelog",
|
||||
"type": "str",
|
||||
"type": "varchar(1)",
|
||||
"not_null": true,
|
||||
"title": "缺省分类",
|
||||
"length": 1
|
||||
"title": "缺省分类"
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
{
|
||||
"name": "idx_api_map_llmid",
|
||||
"type": "normal",
|
||||
"idxfields": [
|
||||
"llmid"
|
||||
],
|
||||
"idxfields": ["llmid"],
|
||||
"idxtype": "index"
|
||||
},
|
||||
{
|
||||
"name": "idx_api_map_catelog",
|
||||
"type": "normal",
|
||||
"idxfields": [
|
||||
"llmcatelogid"
|
||||
],
|
||||
"idxfields": ["llmcatelogid"],
|
||||
"idxtype": "index"
|
||||
},
|
||||
{
|
||||
"name": "idx_api_map_apiname",
|
||||
"type": "normal",
|
||||
"idxfields": [
|
||||
"apiname"
|
||||
],
|
||||
"idxfields": ["apiname"],
|
||||
"idxtype": "index"
|
||||
},
|
||||
{
|
||||
"name": "uk_llmid_apiname",
|
||||
"type": "unique",
|
||||
"idxfields": [
|
||||
"llmid",
|
||||
"apiname"
|
||||
],
|
||||
"idxfields": ["llmid", "apiname"],
|
||||
"idxtype": "unique"
|
||||
}
|
||||
],
|
||||
@ -118,13 +100,6 @@
|
||||
"table": "pricing_program",
|
||||
"valuefield": "id",
|
||||
"textfield": "name"
|
||||
},
|
||||
{
|
||||
"field": "isdefaultcatelog",
|
||||
"table": "appcodes_kv",
|
||||
"valuefield": "k",
|
||||
"textfield": "v",
|
||||
"cond": "parentid='isdefaultcatelog_flg'"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,36 +0,0 @@
|
||||
{
|
||||
"summary": [
|
||||
{
|
||||
"name": "llm_metrics",
|
||||
"title": "模型使用指标",
|
||||
"primary": ["id"]
|
||||
}
|
||||
],
|
||||
"fields": [
|
||||
{"name": "id", "title": "id", "type": "str", "length": 32},
|
||||
{"name": "userid", "title": "用户id", "type": "str", "length": 32},
|
||||
{"name": "userorgid", "title": "用户机构", "type": "str", "length": 32},
|
||||
{"name": "llmid", "title": "模型", "type": "str", "length": 32},
|
||||
{"name": "use_date", "title": "使用日期", "type": "date"},
|
||||
{"name": "use_time", "title": "使用时间", "type": "timestamp"},
|
||||
{"name": "input_tokens", "title": "输入tokens", "type": "int", "default": "0"},
|
||||
{"name": "output_tokens", "title": "输出tokens", "type": "int", "default": "0"},
|
||||
{"name": "total_tokens", "title": "总tokens", "type": "int", "default": "0"},
|
||||
{"name": "amount", "title": "金额", "type": "float", "length": 18, "dec": 4, "default": "0"},
|
||||
{"name": "responsed_seconds", "title": "响应时长(秒)", "type": "float", "length": 18, "dec": 2, "nullable": true},
|
||||
{"name": "finish_seconds", "title": "完成时长(秒)", "type": "float", "length": 18, "dec": 2, "nullable": true},
|
||||
{"name": "status", "title": "状态", "type": "str", "length": 32, "nullable": true},
|
||||
{"name": "taskid", "title": "任务号", "type": "str", "length": 32, "nullable": true}
|
||||
],
|
||||
"indexes": [
|
||||
{"name": "idx1", "idxtype": "index", "idxfields": ["userid", "use_date"]},
|
||||
{"name": "idx2", "idxtype": "index", "idxfields": ["userorgid", "use_date"]},
|
||||
{"name": "idx3", "idxtype": "index", "idxfields": ["llmid", "use_date"]},
|
||||
{"name": "idx4", "idxtype": "index", "idxfields": ["use_time"]}
|
||||
],
|
||||
"codes": [
|
||||
{"field": "userid", "table": "users", "valuefield": "id", "textfield": "username"},
|
||||
{"field": "llmid", "table": "llm", "valuefield": "id", "textfield": "model"},
|
||||
{"field": "userorgid", "table": "organization", "valuefield": "id", "textfield": "orgname"}
|
||||
]
|
||||
}
|
||||
@ -18,7 +18,7 @@
|
||||
},
|
||||
{
|
||||
"name": "llmid",
|
||||
"title": "model",
|
||||
"title": "模型id",
|
||||
"type": "str",
|
||||
"length": 32
|
||||
},
|
||||
@ -34,7 +34,7 @@
|
||||
},
|
||||
{
|
||||
"name": "userid",
|
||||
"title": "username",
|
||||
"title": "用户id",
|
||||
"type": "str",
|
||||
"length": 32
|
||||
},
|
||||
@ -58,15 +58,13 @@
|
||||
"name": "responsed_seconds",
|
||||
"title": "响应时间",
|
||||
"type": "float",
|
||||
"length": 18,
|
||||
"dec": 2
|
||||
"length": 18
|
||||
},
|
||||
{
|
||||
"name": "finish_seconds",
|
||||
"title": "结束时间",
|
||||
"type": "float",
|
||||
"length": 18,
|
||||
"dec": 2
|
||||
"length": 18
|
||||
},
|
||||
{
|
||||
"name": "status",
|
||||
@ -84,55 +82,23 @@
|
||||
"name": "amount",
|
||||
"title": "交易金额",
|
||||
"type": "float",
|
||||
"length": 18,
|
||||
"dec": 2
|
||||
},
|
||||
{
|
||||
"name": "amount_currency",
|
||||
"title": "计费币种",
|
||||
"type": "str",
|
||||
"length": 3,
|
||||
"default": "CNY"
|
||||
},
|
||||
{
|
||||
"name": "amount_base",
|
||||
"title": "折本位币金额",
|
||||
"type": "float",
|
||||
"length": 20,
|
||||
"dec": 2,
|
||||
"default": 0
|
||||
"length": 18
|
||||
},
|
||||
{
|
||||
"name": "cost",
|
||||
"title": "交易成本",
|
||||
"type": "float",
|
||||
"length": 18,
|
||||
"dec": 2
|
||||
},
|
||||
{
|
||||
"name": "cost_currency",
|
||||
"title": "成本币种",
|
||||
"type": "str",
|
||||
"length": 3,
|
||||
"default": "CNY"
|
||||
},
|
||||
{
|
||||
"name": "cost_base",
|
||||
"title": "折本位币成本",
|
||||
"type": "float",
|
||||
"length": 20,
|
||||
"dec": 2,
|
||||
"default": 0
|
||||
"length": 18
|
||||
},
|
||||
{
|
||||
"name": "userorgid",
|
||||
"title": "user_orgname",
|
||||
"title": "用户机构id",
|
||||
"type": "str",
|
||||
"length": 32
|
||||
},
|
||||
{
|
||||
"name": "ownerid",
|
||||
"title": "owner_orgname",
|
||||
"title": "模型机构id",
|
||||
"type": "str",
|
||||
"length": 32
|
||||
},
|
||||
@ -165,69 +131,6 @@
|
||||
"accounting_status",
|
||||
"use_date"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "idx_llmusage_userid_usetime",
|
||||
"idxtype": "index",
|
||||
"idxfields": [
|
||||
"userid",
|
||||
"use_time"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "idx_llmusage_userorgid_usetime",
|
||||
"idxtype": "index",
|
||||
"idxfields": [
|
||||
"userorgid",
|
||||
"use_time"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "idx_llmusage_usetime",
|
||||
"idxtype": "index",
|
||||
"idxfields": [
|
||||
"use_time"
|
||||
]
|
||||
}
|
||||
],
|
||||
"codes": [
|
||||
{
|
||||
"field": "userid",
|
||||
"table": "users",
|
||||
"valuefield": "id",
|
||||
"textfield": "username"
|
||||
},
|
||||
{
|
||||
"field": "ownerid",
|
||||
"table": "organization",
|
||||
"valuefield": "id",
|
||||
"textfield": "orgname"
|
||||
},
|
||||
{
|
||||
"field": "userorgid",
|
||||
"table": "organization",
|
||||
"valuefield": "id",
|
||||
"textfield": "orgname"
|
||||
},
|
||||
{
|
||||
"field": "llmid",
|
||||
"table": "llm",
|
||||
"valuefield": "id",
|
||||
"textfield": "model"
|
||||
},
|
||||
{
|
||||
"field": "status",
|
||||
"table": "appcodes_kv",
|
||||
"valuefield": "k",
|
||||
"textfield": "v",
|
||||
"cond": "parentid='llmusage_status'"
|
||||
},
|
||||
{
|
||||
"field": "accounting_status",
|
||||
"table": "appcodes_kv",
|
||||
"valuefield": "k",
|
||||
"textfield": "v",
|
||||
"cond": "parentid='accounting_status'"
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
models/llmusage.xlsx
Normal file
BIN
models/llmusage.xlsx
Normal file
Binary file not shown.
@ -121,32 +121,5 @@
|
||||
"idxtype": "index",
|
||||
"idxfields": ["failed_time"]
|
||||
}
|
||||
],
|
||||
"codes": [
|
||||
{
|
||||
"field": "handled",
|
||||
"table": "appcodes_kv",
|
||||
"valuefield": "k",
|
||||
"textfield": "v",
|
||||
"cond": "parentid='handled_flg'"
|
||||
},
|
||||
{
|
||||
"field": "userid",
|
||||
"table": "users",
|
||||
"valuefield": "userid",
|
||||
"textfield": "username"
|
||||
},
|
||||
{
|
||||
"field": "userorgid",
|
||||
"table": "organization",
|
||||
"valuefield": "id",
|
||||
"textfield": "orgname"
|
||||
},
|
||||
{
|
||||
"field": "llmid",
|
||||
"table": "llm",
|
||||
"valuefield": "id",
|
||||
"textfield": "name"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -136,27 +136,6 @@
|
||||
"name": "idx_lh_backup_time",
|
||||
"idxtype": "index",
|
||||
"idxfields": ["backup_time"]
|
||||
},
|
||||
{
|
||||
"name": "idx_lh_userid_usetime",
|
||||
"idxtype": "index",
|
||||
"idxfields": ["userid", "use_time"]
|
||||
}
|
||||
],
|
||||
"codes": [
|
||||
{
|
||||
"field": "status",
|
||||
"table": "appcodes_kv",
|
||||
"valuefield": "k",
|
||||
"textfield": "v",
|
||||
"cond": "parentid='llmusage_status'"
|
||||
},
|
||||
{
|
||||
"field": "accounting_status",
|
||||
"table": "appcodes_kv",
|
||||
"valuefield": "k",
|
||||
"textfield": "v",
|
||||
"cond": "parentid='accounting_status'"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -1,35 +0,0 @@
|
||||
{
|
||||
"summary": [
|
||||
{
|
||||
"name": "user_llm_policy",
|
||||
"title": "用户模型策略",
|
||||
"primary": ["id"]
|
||||
}
|
||||
],
|
||||
"fields": [
|
||||
{"name": "id", "title": "id", "type": "str", "length": 32},
|
||||
{"name": "orgid", "title": "所属机构", "type": "str", "length": 32},
|
||||
{"name": "userid", "title": "用户", "type": "str", "length": 32},
|
||||
{"name": "llmid", "title": "模型", "type": "str", "length": 32, "nullable": true},
|
||||
{"name": "daily_call_limit", "title": "日调用上限", "type": "int", "nullable": true},
|
||||
{"name": "monthly_call_limit", "title": "月调用上限", "type": "int", "nullable": true},
|
||||
{"name": "spending_alert", "title": "花费预警", "type": "float", "length": 18, "dec": 2, "nullable": true},
|
||||
{"name": "spending_ceiling", "title": "费用上限", "type": "float", "length": 18, "dec": 2, "nullable": true},
|
||||
{"name": "max_concurrency", "title": "并发上限", "type": "int", "nullable": true},
|
||||
{"name": "status", "title": "状态", "type": "str", "length": 10, "default": "active"},
|
||||
{"name": "created_at", "title": "创建时间", "type": "timestamp", "nullable": true},
|
||||
{"name": "updated_at", "title": "更新时间", "type": "timestamp", "nullable": true},
|
||||
{"name": "created_by", "title": "创建人", "type": "str", "length": 32, "nullable": true},
|
||||
{"name": "remark", "title": "备注", "type": "str", "length": 500, "nullable": true}
|
||||
],
|
||||
"indexes": [
|
||||
{"name": "idx1", "idxtype": "unique", "idxfields": ["orgid", "userid", "llmid"]},
|
||||
{"name": "idx2", "idxtype": "index", "idxfields": ["orgid"]},
|
||||
{"name": "idx3", "idxtype": "index", "idxfields": ["userid"]}
|
||||
],
|
||||
"codes": [
|
||||
{"field": "userid", "table": "users", "valuefield": "id", "textfield": "username"},
|
||||
{"field": "llmid", "table": "llm", "valuefield": "id", "textfield": "model"},
|
||||
{"field": "orgid", "table": "organization", "valuefield": "id", "textfield": "orgname"}
|
||||
]
|
||||
}
|
||||
@ -1,83 +0,0 @@
|
||||
-- ============================================================
|
||||
-- 修复数字人模型显示错误 — pricing timing缺失
|
||||
-- 问题: get_ppid_pricing 找不到有效timing记录导致"data not found"
|
||||
-- 影响:
|
||||
-- 1. orNSwYIFP0HFv2UnY-9EW (wan2.6-i2v-flash) — 有program无timing
|
||||
-- 2. 0B6aldoAej1PpZ4ydtrEZ (wan2.2-s2v数字人) — program和timing都缺
|
||||
-- 生成时间: 2026-06-13
|
||||
-- 执行用户: sword (bugfix模块) 或 root (mysql直接执行)
|
||||
-- ============================================================
|
||||
|
||||
-- ============================================================
|
||||
-- 1. 修复 wan2.6-i2v-flash: 设置discount + 创建timing记录
|
||||
-- ============================================================
|
||||
|
||||
-- 1a. 修复 discount (当前为null)
|
||||
UPDATE pricing_program
|
||||
SET discount = 1.0
|
||||
WHERE id = 'orNSwYIFP0HFv2UnY-9EW';
|
||||
|
||||
-- 1b. 创建 pricing_program_timing 记录
|
||||
-- 官方定价: 有声720P=0.3元/秒, 1080P=0.5元/秒; 无声720P=0.15元/秒, 1080P=0.25元/秒
|
||||
INSERT INTO pricing_program_timing (id, ppid, name, enabled_date, expired_date, pricing_data)
|
||||
VALUES (
|
||||
'orNSwYIFP0HFv2UnY-t1',
|
||||
'orNSwYIFP0HFv2UnY-9EW',
|
||||
NULL,
|
||||
'2026-06-13',
|
||||
'9999-12-31',
|
||||
'unit_values:\n 秒: 1\nfields:\n price_factors:\n type: string\n role: factor\n label: 计价因子\n unit_prices:\n type: float\n role: factor\n label: 单位定价\n unit:\n type: string\n role: factor\n label: 计价单位\n size:\n type: string\n role: filter\n label: 分辨率\n audio:\n type: string\n role: filter\n label: 音频\npricings:\n- price_factors: duration\n unit_prices: 0.3\n unit: 秒\n filters:\n - size: 720P\n - audio: true\n- price_factors: duration\n unit_prices: 0.5\n unit: 秒\n filters:\n - size: 1080P\n - audio: true\n- price_factors: duration\n unit_prices: 0.15\n unit: 秒\n filters:\n - size: 720P\n - audio: false\n- price_factors: duration\n unit_prices: 0.25\n unit: 秒\n filters:\n - size: 1080P\n - audio: false'
|
||||
);
|
||||
|
||||
-- ============================================================
|
||||
-- 2. 创建 wan2.2-s2v 数字人定价 (program + timing)
|
||||
-- ============================================================
|
||||
|
||||
-- 2a. 创建 pricing_program
|
||||
INSERT INTO pricing_program (id, name, ownerid, providerid, pricing_belong, discount, description, pricing_spec)
|
||||
VALUES (
|
||||
'0B6aldoAej1PpZ4ydtrEZ',
|
||||
'通义万象-数字人 wan2.2-s2v',
|
||||
'0',
|
||||
'6fadgewjraOyvxC_EkHou',
|
||||
'provider',
|
||||
1.0,
|
||||
'万相数字人视频生成定价,按输出视频秒数计费',
|
||||
'fields:\n model:\n type: str\n label: 模型\n options:\n - wan2.2-s2v\n size:\n type: str\n label: 分辨率\n options:\n - 480P\n - 720P\n duration:\n type: factor\n label: 时长(秒)'
|
||||
);
|
||||
|
||||
-- 2b. 创建 pricing_program_timing
|
||||
-- 官方定价: 480P=0.5元/秒, 720P=0.9元/秒
|
||||
INSERT INTO pricing_program_timing (id, ppid, name, enabled_date, expired_date, pricing_data)
|
||||
VALUES (
|
||||
'0B6aldoAej1PpZ4ydtrE-t1',
|
||||
'0B6aldoAej1PpZ4ydtrEZ',
|
||||
NULL,
|
||||
'2026-06-13',
|
||||
'9999-12-31',
|
||||
'unit_values:\n 秒: 1\nfields:\n price_factors:\n type: string\n role: factor\n label: 计价因子\n unit_prices:\n type: float\n role: factor\n label: 单位定价\n unit:\n type: string\n role: factor\n label: 计价单位\n size:\n type: string\n role: filter\n label: 分辨率\npricings:\n- price_factors: duration\n unit_prices: 0.5\n unit: 秒\n filters:\n - size: 480P\n- price_factors: duration\n unit_prices: 0.9\n unit: 秒\n filters:\n - size: 720P'
|
||||
);
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- 验证 (执行后运行以下查询确认)
|
||||
-- ============================================================
|
||||
-- SELECT pp.id, pp.name, pp.discount, COUNT(ppt.id) as timing_count
|
||||
-- FROM pricing_program pp
|
||||
-- LEFT JOIN pricing_program_timing ppt ON pp.id = ppt.ppid
|
||||
-- WHERE pp.id IN ('orNSwYIFP0HFv2UnY-9EW', '0B6aldoAej1PpZ4ydtrEZ')
|
||||
-- GROUP BY pp.id, pp.name, pp.discount;
|
||||
--
|
||||
-- 预期结果:
|
||||
-- | id | name | discount | timing_count |
|
||||
-- |-------------------------|----------------------------|----------|--------------|
|
||||
-- | orNSwYIFP0HFv2UnY-9EW | wan2.6-i2v-flash | 1.0 | 1 |
|
||||
-- | 0B6aldoAej1PpZ4ydtrEZ | 通义万象-数字人 wan2.2-s2v | 1.0 | 1 |
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- 回滚 (如需回滚)
|
||||
-- ============================================================
|
||||
-- DELETE FROM pricing_program_timing WHERE id IN ('orNSwYIFP0HFv2UnY-t1', '0B6aldoAej1PpZ4ydtrE-t1');
|
||||
-- DELETE FROM pricing_program WHERE id = '0B6aldoAej1PpZ4ydtrEZ';
|
||||
-- UPDATE pricing_program SET discount = NULL WHERE id = 'orNSwYIFP0HFv2UnY-9EW';
|
||||
@ -1,26 +0,0 @@
|
||||
-- ============================================================
|
||||
-- 修复 MiniMax-M3 定价重复条目
|
||||
-- 问题:步骤11b的CONCAT重复执行导致M3条目重复
|
||||
-- 解决:删除没有prompt_tokens filter的旧M3条目(前3条)
|
||||
-- ============================================================
|
||||
|
||||
UPDATE `pricing_program_timing`
|
||||
SET `pricing_data` = REPLACE(`pricing_data`,
|
||||
'- price_factors: prompt_tokens
|
||||
unit_prices: 2.1
|
||||
unit: 百万
|
||||
filters:
|
||||
- model: MiniMax-M3
|
||||
- price_factors: completion_tokens
|
||||
unit_prices: 8.4
|
||||
unit: 百万
|
||||
filters:
|
||||
- model: MiniMax-M3
|
||||
- price_factors: cached_tokens
|
||||
unit_prices: 0.42
|
||||
unit: 百万
|
||||
filters:
|
||||
- model: MiniMax-M3
|
||||
|
||||
', '')
|
||||
WHERE `ppid` = '5jmzupARABxkDFwUraFiQ' AND `enabled_date` = '2026-04-12';
|
||||
@ -1,85 +0,0 @@
|
||||
-- ============================================================
|
||||
--
|
||||
-- Kimi K3 (月之暗面 / Moonshot) 完整注册
|
||||
-- 生成时间: 2026-07-20
|
||||
-- 模型: kimi-k3
|
||||
-- 依赖: kimi_k3_uapi.sql (uapi 'kimi_t2t' 已创建)
|
||||
-- ============================================================
|
||||
-- 前置条件:
|
||||
-- modelprovider 已有记录: id='0OkxCHYbZOdD_W_o5N9tN', name='moonshot'
|
||||
-- uapi 已有记录: id='kimi_t2t' (由 kimi_k3_uapi.sql 创建)
|
||||
-- uapi 已有记录: id='oL9cufrcRb7SPfH11Ra62' (tm2t, moonshot 通用多模态)
|
||||
-- uapiio 已有记录: id='Is8l4TGkcZcqFSjbbeIK2' (文本会话)
|
||||
-- uapiio 已有记录: id='t-ujII59ku45tIPcdXu4O' (文本媒体转文本)
|
||||
-- ============================================================
|
||||
|
||||
-- ============================================================
|
||||
-- 1. 新增 llm: kimi-k3 模型注册
|
||||
-- ============================================================
|
||||
INSERT IGNORE INTO `llm` (`id`, `name`, `model`, `description`, `iconid`, `upappid`, `providerid`, `ownerid`, `enabled_date`, `expired_date`, `min_balance`, `status`)
|
||||
VALUES (
|
||||
'kimi-k3-llm',
|
||||
'kimi-k3',
|
||||
'kimi-k3',
|
||||
'月之暗面 Kimi K3,支持多模态(图片/视频)输入,兼容 OpenAI Chat Completions 格式,支持深度思考模式',
|
||||
'moonshot',
|
||||
'upapp_moonshot',
|
||||
'0OkxCHYbZOdD_W_o5N9tN',
|
||||
'0',
|
||||
'2026-07-20',
|
||||
'9999-12-31',
|
||||
10.00,
|
||||
'published'
|
||||
);
|
||||
|
||||
-- ============================================================
|
||||
-- 2. 新增 llm_api_map: t2t (纯文本对话)
|
||||
-- ============================================================
|
||||
INSERT IGNORE INTO `llm_api_map` (`id`, `llmid`, `llmcatelogid`, `apiname`, `query_apiname`, `query_period`, `ppid`, `isdefaultcatelog`)
|
||||
VALUES (
|
||||
'kimi_k3_map_t2t',
|
||||
'kimi-k3-llm',
|
||||
't2t',
|
||||
't2t',
|
||||
NULL,
|
||||
30,
|
||||
NULL,
|
||||
'1'
|
||||
);
|
||||
|
||||
-- ============================================================
|
||||
-- 3. 新增 llm_api_map: tm2t (多模态对话, 文本+图片+视频)
|
||||
-- ============================================================
|
||||
INSERT IGNORE INTO `llm_api_map` (`id`, `llmid`, `llmcatelogid`, `apiname`, `query_apiname`, `query_period`, `ppid`, `isdefaultcatelog`)
|
||||
VALUES (
|
||||
'kimi_k3_map_tm2t',
|
||||
'kimi-k3-llm',
|
||||
'vision',
|
||||
'tm2t',
|
||||
NULL,
|
||||
30,
|
||||
NULL,
|
||||
'1'
|
||||
);
|
||||
|
||||
-- ============================================================
|
||||
-- 验证 (执行后运行确认)
|
||||
-- ============================================================
|
||||
-- SELECT m.id, m.llmid, m.llmcatelogid, m.apiname, m.ppid,
|
||||
-- l.name as model_name, l.model, l.status
|
||||
-- FROM llm_api_map m
|
||||
-- JOIN llm l ON m.llmid = l.id
|
||||
-- WHERE m.llmid = 'kimi-k3-llm';
|
||||
--
|
||||
-- 预期: 2 行 (t2t + tm2t/vision)
|
||||
|
||||
-- SELECT id, name, model, status, providerid, upappid
|
||||
-- FROM llm WHERE id = 'kimi-k3-llm';
|
||||
--
|
||||
-- 预期: 1 行, status='published'
|
||||
|
||||
-- ============================================================
|
||||
-- 回滚
|
||||
-- ============================================================
|
||||
-- DELETE FROM llm_api_map WHERE id IN ('kimi_k3_map_t2t', 'kimi_k3_map_tm2t');
|
||||
-- DELETE FROM llm WHERE id = 'kimi-k3-llm';
|
||||
@ -1,93 +0,0 @@
|
||||
-- ============================================================
|
||||
-- Kimi K3 API 接入 (月之暗面 / Moonshot)
|
||||
-- Base URL: https://api.moonshot.cn/v1
|
||||
-- 兼容 OpenAI 格式, 文档: https://platform.kimi.com/docs/api/chat
|
||||
-- 多模态: image_files/video_files 在 data 模板中用纯 Jinja2 构造 content 数组
|
||||
-- ============================================================
|
||||
|
||||
-- ============================================================
|
||||
-- 0. 前置: modelprovider / upapp (如不存在先创建)
|
||||
-- ============================================================
|
||||
-- INSERT IGNORE INTO modelprovider (id, name) VALUES ('moonshot', '月之暗面');
|
||||
-- INSERT IGNORE INTO upapp (id, name, `key`, baseurl, enabled_date)
|
||||
-- VALUES ('upapp_moonshot', '月之暗面', 'MOONSHOT_API_KEY', 'https://api.moonshot.cn', '2026-07-17');
|
||||
SET @upapp_id = 'upapp_moonshot'; -- 替换为实际的月之暗面 upapp.id
|
||||
|
||||
-- ============================================================
|
||||
-- Chat Completions — POST /v1/chat/completions
|
||||
-- 支持纯文本 + 多模态(图片/视频 base64)
|
||||
-- ioid 复用 Is8l4TGkcZcqFSjbbeIK2 (文本会话)
|
||||
-- ============================================================
|
||||
REPLACE INTO `uapi` (`id`, `name`, `need_auth`, `stream`, `path`, `httpmethod`, `chunk_match`, `headers`, `params`, `data`, `response`, `ioid`, `callbackurl`, `upappid`)
|
||||
VALUES (
|
||||
'kimi_t2t', 't2t', '0', 'stream',
|
||||
'/chat/completions', 'POST', 'data: ',
|
||||
'{"Authorization": "Bearer {{apikey}}", "Content-Type": "application/json"}',
|
||||
NULL,
|
||||
'{
|
||||
{% if stream %}
|
||||
"stream_options":{"include_usage": true},
|
||||
{% endif %}
|
||||
{% if tools %}
|
||||
"tools": {{json.dumps(tools, ensure_ascii=False)}},
|
||||
{% endif %}
|
||||
{% if tool_choice %}
|
||||
"tool_choice": "{{tool_choice}}",
|
||||
{% endif %}
|
||||
{% if messages %}
|
||||
"messages": {{json.dumps(messages, ensure_ascii=False)}},
|
||||
{% else %}
|
||||
"messages": [
|
||||
{% if sys_prompt %}
|
||||
{"role": "system", "content": {{json.dumps(sys_prompt, ensure_ascii=False)}}},
|
||||
{% endif %}
|
||||
{% set _parts = [] %}
|
||||
{% if prompt %}
|
||||
{% set _ = _parts.append({"type": "text", "text": prompt}) %}
|
||||
{% endif %}
|
||||
{% if image_files %}
|
||||
{% for _f in image_files %}
|
||||
{% set _fp = FileStorage().realPath(_f) %}
|
||||
{% if os.path.isfile(_fp) %}
|
||||
{% set _mime = file_mime(_fp) %}
|
||||
{% set _b64 = file_to_b64(_fp) %}
|
||||
{% set _ = _parts.append({"type": "image_url", "image_url": "data:" + _mime + ";base64," + _b64}) %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if video_files %}
|
||||
{% for _f in video_files %}
|
||||
{% set _fp = FileStorage().realPath(_f) %}
|
||||
{% if os.path.isfile(_fp) %}
|
||||
{% set _mime = file_mime(_fp) %}
|
||||
{% set _b64 = file_to_b64(_fp) %}
|
||||
{% set _ = _parts.append({"type": "video_url", "video_url": "data:" + _mime + ";base64," + _b64}) %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{"role": "user", "content": {{json.dumps(_parts, ensure_ascii=False)}}}
|
||||
],
|
||||
{% endif %}
|
||||
{% if stream %}
|
||||
"stream":true,
|
||||
{% endif %}
|
||||
"model": "{{model}}",
|
||||
"reasoning_effort": "max"
|
||||
}',
|
||||
'{
|
||||
"id": "{{id}}", "object": "{{object}}", "created": {{created}},
|
||||
"choices": {{json.dumps(choices, ensure_ascii=False)}}, "model": "{{model}}",
|
||||
{% if object == "chat.completion" %}
|
||||
"reasoning_content": {{json.dumps(choices[0].message.reasoning_content, ensure_ascii=False)}},
|
||||
"content":{{json.dumps(choices[0].message.content, ensure_ascii=False)}},
|
||||
{% elif len(choices)>0 %}
|
||||
"reasoning_content": {{json.dumps(choices[0].delta.reasoning_content, ensure_ascii=False)}},
|
||||
"content":{{json.dumps(choices[0].delta.content, ensure_ascii=False)}},
|
||||
{% endif %}
|
||||
{% if usage %}{% set usage1 = usage.update({"model": model}) %}
|
||||
"finish": "1", "usage":{{json.dumps(usage)}}
|
||||
{% else %}
|
||||
"finish":"0"
|
||||
{% endif %}}',
|
||||
'Is8l4TGkcZcqFSjbbeIK2', NULL, @upapp_id
|
||||
);
|
||||
@ -1,251 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
llmage 模块 RBAC 权限管理脚本
|
||||
|
||||
使用方法:
|
||||
cd ~/repos/sage
|
||||
./py3/bin/python ~/repos/llmage/scripts/load_path.py
|
||||
|
||||
每次代码变更如有新 path 出现,需同步更新此脚本。
|
||||
"""
|
||||
|
||||
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.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")
|
||||
sys.exit(1)
|
||||
|
||||
PYTHON = os.path.join(SAGE_ROOT, "py3", "bin", "python")
|
||||
SET_PERM_SCRIPT = os.path.join(SAGE_ROOT, "set_role_perm.py")
|
||||
|
||||
MOD = "llmage"
|
||||
|
||||
# ============================================================
|
||||
# 权限路径定义 — 每次新增页面或API时同步更新
|
||||
# ============================================================
|
||||
|
||||
# any — 无需登录(菜单、静态资源)
|
||||
PATHS_ANY = [
|
||||
f"/{MOD}/menu.ui",
|
||||
f"/{MOD}/imgs/kdb.svg",
|
||||
]
|
||||
|
||||
# logined — 所有已登录用户
|
||||
PATHS_LOGINED = [
|
||||
# 模块入口
|
||||
f"/{MOD}",
|
||||
f"/{MOD}/index.ui",
|
||||
|
||||
# 顶层 .ui 页面
|
||||
f"/{MOD}/api_doc.ui",
|
||||
f"/{MOD}/api_doc.md",
|
||||
f"/{MOD}/llm_dialog.ui",
|
||||
f"/{MOD}/llm_launch_check.ui",
|
||||
f"/{MOD}/check_model_record.dspy",
|
||||
f"/{MOD}/check_date_status.dspy",
|
||||
f"/{MOD}/check_upapp.dspy",
|
||||
f"/{MOD}/check_uapi.dspy",
|
||||
f"/{MOD}/check_uapiio.dspy",
|
||||
f"/{MOD}/check_llm_api_map.dspy",
|
||||
f"/{MOD}/check_pricing_program.dspy",
|
||||
f"/{MOD}/check_pricing_data.dspy",
|
||||
f"/{MOD}/show_same_catelog_llm.ui",
|
||||
f"/{MOD}/show_llms.ui",
|
||||
f"/{MOD}/show_llms_by_providers.ui",
|
||||
f"/{MOD}/model_plaza.ui",
|
||||
f"/{MOD}/model_pricing.dspy",
|
||||
f"/{MOD}/model_pricing.ui",
|
||||
f"/{MOD}/failed_accounting.ui",
|
||||
f"/{MOD}/llmcatelog_list.ui",
|
||||
|
||||
# 顶层 .dspy(非 api/ 目录)
|
||||
f"/{MOD}/get_accounting_llmusages.dspy",
|
||||
f"/{MOD}/get_asynctask_status.dspy",
|
||||
f"/{MOD}/get_my_asynctasks.dspy",
|
||||
f"/{MOD}/get_type_llms.dspy",
|
||||
f"/{MOD}/grap_task_status.dspy",
|
||||
f"/{MOD}/list_catelog_models.dspy",
|
||||
f"/{MOD}/list_paging_catelog_llms.dspy",
|
||||
f"/{MOD}/llmaccounting.dspy",
|
||||
f"/{MOD}/llmcheck.dspy",
|
||||
f"/{MOD}/llmcost.dspy",
|
||||
f"/{MOD}/llminference.dspy",
|
||||
f"/{MOD}/model_estimate.dspy",
|
||||
f"/{MOD}/query_orders.dspy",
|
||||
f"/{MOD}/query_price.dspy",
|
||||
f"/{MOD}/test_llm_charging.dspy",
|
||||
f"/{MOD}/vidu_callback.dspy",
|
||||
f"/{MOD}/vidu_inference.dspy",
|
||||
|
||||
# api/ 目录
|
||||
f"/{MOD}/api/failed_accounting_list.dspy",
|
||||
f"/{MOD}/api/get_inference_history.dspy",
|
||||
f"/{MOD}/api/get_apis.dspy",
|
||||
f"/{MOD}/api/get_catelogs.dspy",
|
||||
f"/{MOD}/api/get_organizations.dspy",
|
||||
f"/{MOD}/api/get_ppids.dspy",
|
||||
f"/{MOD}/api/get_search_apiname.dspy",
|
||||
f"/{MOD}/api/get_search_providerid.dspy",
|
||||
f"/{MOD}/api/get_search_upappid.dspy",
|
||||
f"/{MOD}/api/get_search_model.dspy",
|
||||
f"/{MOD}/api/get_upapps.dspy",
|
||||
f"/{MOD}/api/llm_launch_check_api.dspy",
|
||||
f"/{MOD}/api/llm_api_map_create.dspy",
|
||||
f"/{MOD}/api/llm_api_map_delete.dspy",
|
||||
f"/{MOD}/api/llm_api_map_list.dspy",
|
||||
f"/{MOD}/api/llm_api_map_options.dspy",
|
||||
f"/{MOD}/api/llm_catelog_options.dspy",
|
||||
f"/{MOD}/api/llm_create.dspy",
|
||||
f"/{MOD}/api/llm_delete.dspy",
|
||||
f"/{MOD}/api/llm_status_update.dspy",
|
||||
f"/{MOD}/api/llm_update.dspy",
|
||||
f"/{MOD}/api/llmcatelog_create.dspy",
|
||||
f"/{MOD}/api/llmcatelog_delete.dspy",
|
||||
f"/{MOD}/api/llmcatelog_list.dspy",
|
||||
f"/{MOD}/api/llmcatelog_update.dspy",
|
||||
f"/{MOD}/api/llmusage_accounting_failed_create.dspy",
|
||||
f"/{MOD}/api/llmusage_accounting_failed_delete.dspy",
|
||||
f"/{MOD}/api/llmusage_accounting_failed_update.dspy",
|
||||
f"/{MOD}/api/llmusage_create.dspy",
|
||||
f"/{MOD}/api/llmusage_delete.dspy",
|
||||
f"/{MOD}/api/llmusage_history_create.dspy",
|
||||
f"/{MOD}/api/llmusage_history_delete.dspy",
|
||||
f"/{MOD}/api/llmusage_history_update.dspy",
|
||||
f"/{MOD}/api/llmusage_update.dspy",
|
||||
f"/{MOD}/api/retry_accounting.dspy",
|
||||
f"/{MOD}/api/uapi_options.dspy",
|
||||
|
||||
# CRUD 子目录 — llm/
|
||||
f"/{MOD}/llm/index.ui",
|
||||
f"/{MOD}/llm/add_llm.dspy",
|
||||
f"/{MOD}/llm/delete_llm.dspy",
|
||||
f"/{MOD}/llm/get_llm.dspy",
|
||||
f"/{MOD}/llm/update_llm.dspy",
|
||||
|
||||
# CRUD 子目录 — llm_api_map/
|
||||
f"/{MOD}/llm_api_map/index.ui",
|
||||
f"/{MOD}/llm_api_map/add_llm_api_map.dspy",
|
||||
f"/{MOD}/llm_api_map/delete_llm_api_map.dspy",
|
||||
f"/{MOD}/llm_api_map/get_llm_api_map.dspy",
|
||||
f"/{MOD}/llm_api_map/update_llm_api_map.dspy",
|
||||
|
||||
# CRUD 子目录 — llmcatelog_list/ (alias for llmcatelog)
|
||||
f"/{MOD}/llmcatelog_list/index.ui",
|
||||
f"/{MOD}/llmcatelog_list/add_llmcatelog.dspy",
|
||||
f"/{MOD}/llmcatelog_list/delete_llmcatelog.dspy",
|
||||
f"/{MOD}/llmcatelog_list/get_llmcatelog.dspy",
|
||||
f"/{MOD}/llmcatelog_list/update_llmcatelog.dspy",
|
||||
|
||||
# CRUD 子目录 — llmusage/
|
||||
f"/{MOD}/llmusage/index.ui",
|
||||
f"/{MOD}/llmusage/add_llmusage.dspy",
|
||||
f"/{MOD}/llmusage/delete_llmusage.dspy",
|
||||
f"/{MOD}/llmusage/get_llmusage.dspy",
|
||||
f"/{MOD}/llmusage/update_llmusage.dspy",
|
||||
f"/{MOD}/llmusage_usages_display.dspy",
|
||||
f"/{MOD}/llmusage_ioinfo_display.dspy",
|
||||
f"/{MOD}/api/llmusage_list.dspy",
|
||||
|
||||
# CRUD 子目录 — llmusage_accounting_failed/
|
||||
f"/{MOD}/llmusage_accounting_failed/index.ui",
|
||||
f"/{MOD}/llmusage_accounting_failed/add_llmusage_accounting_failed.dspy",
|
||||
f"/{MOD}/llmusage_accounting_failed/delete_llmusage_accounting_failed.dspy",
|
||||
f"/{MOD}/llmusage_accounting_failed/get_llmusage_accounting_failed.dspy",
|
||||
f"/{MOD}/llmusage_accounting_failed/recover_usages.dspy",
|
||||
f"/{MOD}/llmusage_accounting_failed/update_llmusage_accounting_failed.dspy",
|
||||
|
||||
# CRUD 子目录 — llmusage_history/
|
||||
f"/{MOD}/llmusage_history/index.ui",
|
||||
f"/{MOD}/llmusage_history/add_llmusage_history.dspy",
|
||||
f"/{MOD}/llmusage_history/delete_llmusage_history.dspy",
|
||||
f"/{MOD}/llmusage_history/get_llmusage_history.dspy",
|
||||
f"/{MOD}/llmusage_history/update_llmusage_history.dspy",
|
||||
|
||||
# v1 API 目录
|
||||
f"/{MOD}/v1/chat/completions/index.dspy",
|
||||
f"/{MOD}/v1/image/generations/index.dspy",
|
||||
f"/{MOD}/v1/models/catelog.dspy",
|
||||
f"/{MOD}/v1/models/index.dspy",
|
||||
f"/{MOD}/v1/tasks/index.dspy",
|
||||
f"/{MOD}/v1/video/generations/index.dspy",
|
||||
f"/{MOD}/v1/music/generations/index.dspy",
|
||||
f"/{MOD}/v1/audio/speech/index.dspy",
|
||||
f"/{MOD}/v1/audio/transcriptions/index.dspy",
|
||||
f"/{MOD}/v1/pricing/index.dspy",
|
||||
|
||||
# 其他子目录
|
||||
f"/{MOD}/list_llmcatelogs/index.dspy",
|
||||
f"/{MOD}/list_llms/index.dspy",
|
||||
f"/{MOD}/openai/index.dspy",
|
||||
f"/{MOD}/t2t/index.dspy",
|
||||
f"/{MOD}/tasks/index.dspy",
|
||||
f"/{MOD}/upload_asset/index.dspy",
|
||||
f"/{MOD}/video/index.dspy",
|
||||
]
|
||||
|
||||
# ============================================================
|
||||
# 客户角色 — v1 API 调用权限
|
||||
# ============================================================
|
||||
|
||||
PATHS_V1_CUSTOMER = [
|
||||
f"/{MOD}/v1/chat/completions/index.dspy",
|
||||
f"/{MOD}/v1/video/generations/index.dspy",
|
||||
f"/{MOD}/v1/image/generations/index.dspy",
|
||||
f"/{MOD}/v1/music/generations/index.dspy",
|
||||
f"/{MOD}/v1/audio/speech/index.dspy",
|
||||
f"/{MOD}/v1/audio/transcriptions/index.dspy",
|
||||
f"/{MOD}/v1/pricing/index.dspy",
|
||||
f"/{MOD}/v1/models/index.dspy",
|
||||
f"/{MOD}/v1/tasks/index.dspy",
|
||||
]
|
||||
|
||||
# ============================================================
|
||||
# 执行注册
|
||||
# ============================================================
|
||||
|
||||
|
||||
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:
|
||||
if run_set_perm(role, p):
|
||||
count += 1
|
||||
print(f" {role}: {count}/{len(paths)} paths registered")
|
||||
return count
|
||||
|
||||
|
||||
def main():
|
||||
print(f"Sage root: {SAGE_ROOT}")
|
||||
total = 0
|
||||
total += register_role_paths("any", PATHS_ANY)
|
||||
total += register_role_paths("logined", PATHS_LOGINED)
|
||||
# 客户角色 — v1 API 调用权限
|
||||
for role in ["customer.admin", "customer.user"]:
|
||||
total += register_role_paths(role, PATHS_V1_CUSTOMER)
|
||||
print(f"\nDone. Total {total} permission entries registered.")
|
||||
print("NOTE: Restart Sage after permission changes to reload RBAC cache.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -1,245 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
llmcatelog ID 迁移脚本
|
||||
将 llmcatelog.id 和 llm_api_map.llmcatelogid 从旧ID迁移为有意义的缩写ID。
|
||||
|
||||
执行顺序:
|
||||
1. 先更新 llm_api_map.llmcatelogid(外键表)
|
||||
2. 再更新 llmcatelog.id(主表)
|
||||
3. 验证迁移结果
|
||||
|
||||
用法:
|
||||
# 预览模式(不执行,只显示将要做的变更)
|
||||
python migrate_llmcatelog_ids.py --dry-run
|
||||
|
||||
# 正式执行
|
||||
python migrate_llmcatelog_ids.py
|
||||
|
||||
# 指定数据库名(默认 llmage)
|
||||
python migrate_llmcatelog_ids.py --dbname my_llmage
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import argparse
|
||||
import sys
|
||||
import os
|
||||
|
||||
# 从脚本位置推断 sage 根目录(脚本在 pkgs/llmage/scripts/ 下)
|
||||
_script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
sage_root = os.path.abspath(os.path.join(_script_dir, '..', '..', '..'))
|
||||
sys.path.insert(0, sage_root)
|
||||
sys.path.insert(0, os.path.join(sage_root, 'py3/lib/python3.10/site-packages'))
|
||||
|
||||
from appPublic.jsonConfig import getConfig
|
||||
|
||||
# 旧ID -> 新ID 映射表
|
||||
ID_MAP = {
|
||||
'text2text': 't2t',
|
||||
'text2image': 't2i',
|
||||
'-i2ET0YkhfVQdHONfk9pX': 't2v',
|
||||
'RdsO6pXgXcUTvUj819-7X': 'i2v',
|
||||
'fHrfsOnAFCz53DAILMO7G': 'r2v',
|
||||
'text2speech': 'tts',
|
||||
'audio2text': 'asr',
|
||||
'image2text': 'vision',
|
||||
'9_P5y-qiQzQASacTVk2Lq': 'ai_search',
|
||||
'czKvk-clQTRLS2KVddSWo': 'digital_human',
|
||||
'HaRXiNCaAACurZsmEqpsU': 'music_gen',
|
||||
'Rqj-QBj1v4560l-FPCrIU': 'text_cls',
|
||||
's6-nhQtEvDKxG_qDPWwT7': '3d_gen',
|
||||
'sRmpG8draTM-tsbO5nMJO': 'video_tool',
|
||||
't7sUuj8BCnsD762PwMUKM': 'translate',
|
||||
}
|
||||
|
||||
|
||||
async def migrate(dry_run=False, dbname='llmage'):
|
||||
from sqlor.dbpools import DBPools
|
||||
from appPublic.log import debug
|
||||
|
||||
config = getConfig(sage_root)
|
||||
db = DBPools(config.databases)
|
||||
|
||||
# 如果传入的 dbname 不在配置中,尝试使用第一个数据库
|
||||
if dbname not in config.databases:
|
||||
available = list(config.databases.keys())
|
||||
print(f"Warning: '{dbname}' not in config.databases, available: {available}")
|
||||
if available:
|
||||
dbname = available[0]
|
||||
print(f"Using '{dbname}' instead")
|
||||
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
print(f"{'='*60}")
|
||||
print(f"llmcatelog ID 迁移脚本")
|
||||
print(f"数据库: {dbname}")
|
||||
print(f"模式: {'预览(DRY-RUN)' if dry_run else '正式执行'}")
|
||||
print(f"{'='*60}\n")
|
||||
|
||||
# ===== 阶段0: 检查当前数据 =====
|
||||
print("[阶段0] 检查当前 llmcatelog 数据...")
|
||||
current = await sor.sqlExe("SELECT id, name FROM llmcatelog ORDER BY name", {})
|
||||
if not current:
|
||||
print(" llmcatelog 表为空,无需迁移。")
|
||||
return
|
||||
|
||||
print(f" 当前共 {len(current)} 条记录:\n")
|
||||
print(f" {'旧ID':<30} {'name':<15} {'新ID':<15} {'状态'}")
|
||||
print(f" {'-'*30} {'-'*15} {'-'*15} {'-'*10}")
|
||||
|
||||
valid_records = []
|
||||
unmapped = []
|
||||
for row in current:
|
||||
old_id = row['id']
|
||||
name = row['name']
|
||||
new_id = ID_MAP.get(old_id)
|
||||
if new_id:
|
||||
# 检查是否已经迁移过(old_id == new_id 的情况不会发生,
|
||||
# 但如果 id 已经是新值则跳过)
|
||||
if old_id == new_id:
|
||||
status = '已迁移'
|
||||
else:
|
||||
status = '待迁移'
|
||||
valid_records.append((old_id, new_id, name))
|
||||
print(f" {old_id:<30} {name:<15} {new_id:<15} {status}")
|
||||
else:
|
||||
status = '无映射!'
|
||||
unmapped.append((old_id, name))
|
||||
print(f" {old_id:<30} {name:<15} {'---':<15} {status}")
|
||||
|
||||
if unmapped:
|
||||
print(f"\n ⚠ 警告: {len(unmapped)} 条记录无映射关系,将跳过:")
|
||||
for uid, uname in unmapped:
|
||||
print(f" - {uid} ({uname})")
|
||||
|
||||
if not valid_records:
|
||||
print("\n 没有需要迁移的记录。")
|
||||
return
|
||||
|
||||
print(f"\n 共 {len(valid_records)} 条记录需要迁移。\n")
|
||||
|
||||
# ===== 阶段1: 检查 llm_api_map 关联 =====
|
||||
print("[阶段1] 检查 llm_api_map 关联...")
|
||||
for old_id, new_id, name in valid_records:
|
||||
maps = await sor.sqlExe(
|
||||
"SELECT COUNT(*) as cnt FROM llm_api_map WHERE llmcatelogid = ${old_id}$",
|
||||
{'old_id': old_id}
|
||||
)
|
||||
cnt = maps[0]['cnt'] if maps else 0
|
||||
print(f" {name}({old_id}): {cnt} 条映射")
|
||||
|
||||
# ===== 阶段2: 检查新ID是否已被占用 =====
|
||||
print(f"\n[阶段2] 检查新ID是否已被占用...")
|
||||
conflict = False
|
||||
for old_id, new_id, name in valid_records:
|
||||
check = await sor.sqlExe(
|
||||
"SELECT id, name FROM llmcatelog WHERE id = ${new_id}$",
|
||||
{'new_id': new_id}
|
||||
)
|
||||
if check:
|
||||
# 如果新ID已存在且就是当前记录(已经迁移过),跳过
|
||||
if check[0]['id'] == old_id:
|
||||
print(f" {new_id}: 已是当前记录,跳过")
|
||||
else:
|
||||
print(f" ✗ 冲突! 新ID '{new_id}' 已被 {check[0]['name']} 使用")
|
||||
conflict = True
|
||||
else:
|
||||
print(f" ✓ {new_id}: 可用")
|
||||
|
||||
if conflict:
|
||||
print("\n ✗ 存在ID冲突,终止迁移!")
|
||||
return
|
||||
|
||||
if dry_run:
|
||||
print(f"\n{'='*60}")
|
||||
print("预览模式结束。以上是将会执行的变更。")
|
||||
print("去掉 --dry-run 参数以正式执行。")
|
||||
print(f"{'='*60}")
|
||||
return
|
||||
|
||||
# ===== 阶段3: 执行迁移 =====
|
||||
print(f"\n[阶段3] 开始执行迁移...")
|
||||
|
||||
# 3a: 先更新 llm_api_map(外键表)
|
||||
print(f"\n --- 3a: 更新 llm_api_map.llmcatelogid ---")
|
||||
for old_id, new_id, name in valid_records:
|
||||
try:
|
||||
await sor.sqlExe(
|
||||
"UPDATE llm_api_map SET llmcatelogid = ${new_id}$ WHERE llmcatelogid = ${old_id}$",
|
||||
{'new_id': new_id, 'old_id': old_id}
|
||||
)
|
||||
maps = await sor.sqlExe(
|
||||
"SELECT COUNT(*) as cnt FROM llm_api_map WHERE llmcatelogid = ${new_id}$",
|
||||
{'new_id': new_id}
|
||||
)
|
||||
cnt = maps[0]['cnt'] if maps else 0
|
||||
print(f" ✓ {name}: {old_id} -> {new_id} (关联 {cnt} 条)")
|
||||
except Exception as e:
|
||||
print(f" ✗ {name}: 更新 llm_api_map 失败: {e}")
|
||||
print(f" 回滚中...")
|
||||
raise
|
||||
|
||||
# 3b: 再更新 llmcatelog(主表)
|
||||
print(f"\n --- 3b: 更新 llmcatelog.id ---")
|
||||
for old_id, new_id, name in valid_records:
|
||||
try:
|
||||
await sor.sqlExe(
|
||||
"UPDATE llmcatelog SET id = ${new_id}$ WHERE id = ${old_id}$",
|
||||
{'new_id': new_id, 'old_id': old_id}
|
||||
)
|
||||
print(f" ✓ {name}: {old_id} -> {new_id}")
|
||||
except Exception as e:
|
||||
print(f" ✗ {name}: 更新 llmcatelog 失败: {e}")
|
||||
raise
|
||||
|
||||
# ===== 阶段4: 验证 =====
|
||||
print(f"\n[阶段4] 验证迁移结果...")
|
||||
|
||||
# 验证 llmcatelog
|
||||
catelogs = await sor.sqlExe("SELECT id, name FROM llmcatelog ORDER BY id", {})
|
||||
print(f"\n llmcatelog ({len(catelogs)} 条):")
|
||||
for row in catelogs:
|
||||
print(f" {row['id']:<20} {row['name']}")
|
||||
|
||||
# 验证关联完整性
|
||||
orphans = await sor.sqlExe("""
|
||||
SELECT m.llmcatelogid, COUNT(*) as cnt
|
||||
FROM llm_api_map m
|
||||
LEFT JOIN llmcatelog c ON m.llmcatelogid = c.id
|
||||
WHERE c.id IS NULL
|
||||
GROUP BY m.llmcatelogid
|
||||
""", {})
|
||||
if orphans:
|
||||
print(f"\n ✗ 发现孤立关联:")
|
||||
for o in orphans:
|
||||
print(f" llmcatelogid={o['llmcatelogid']}: {o['cnt']} 条无对应主记录")
|
||||
else:
|
||||
print(f"\n ✓ 所有 llm_api_map 关联完整,无孤立记录")
|
||||
|
||||
# 验证映射表
|
||||
map_stats = await sor.sqlExe("""
|
||||
SELECT m.llmcatelogid, c.name, COUNT(*) as cnt
|
||||
FROM llm_api_map m
|
||||
JOIN llmcatelog c ON m.llmcatelogid = c.id
|
||||
GROUP BY m.llmcatelogid, c.name
|
||||
ORDER BY m.llmcatelogid
|
||||
""", {})
|
||||
if map_stats:
|
||||
print(f"\n llm_api_map 关联统计:")
|
||||
for row in map_stats:
|
||||
print(f" {row['llmcatelogid']:<20} {row['name']:<15} {row['cnt']} 条映射")
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print("迁移完成!")
|
||||
print(f"{'='*60}")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='llmcatelog ID 迁移脚本')
|
||||
parser.add_argument('--dry-run', action='store_true', help='预览模式,不执行实际变更')
|
||||
parser.add_argument('--dbname', default='llmage', help='数据库名 (默认: llmage)')
|
||||
args = parser.parse_args()
|
||||
|
||||
asyncio.run(migrate(dry_run=args.dry_run, dbname=args.dbname))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@ -1,393 +0,0 @@
|
||||
-- ============================================================
|
||||
-- MiniMax M3 接入 + M2.7-highspeed + 补充全模型定价
|
||||
-- 生成时间: 2026-06-12
|
||||
-- 数据来源: token.opencomputing.cn 实时查询 (bugfix/execute_sql)
|
||||
-- 参考: qwen3.7-max (llm:u1EtkR9xRcmwMvdoCZRC8, ppid:5i1JIpqERgCWqKQ4DCegD)
|
||||
-- 接口: 使用uapi模块, upappid=minimax, baseurl=https://api.minimaxi.com/v1
|
||||
-- ============================================================
|
||||
|
||||
-- ============================================================
|
||||
-- 1. 新增 uapi: minimax t2t (纯文本对话, OpenAI兼容)
|
||||
-- 复用ioid: Is8l4TGkcZcqFSjbbeIK2 (文本会话, 共享)
|
||||
-- ============================================================
|
||||
REPLACE INTO `uapi` (`id`, `name`, `need_auth`, `stream`, `path`, `httpmethod`, `chunk_match`, `headers`, `params`, `data`, `response`, `ioid`, `callbackurl`, `upappid`)
|
||||
VALUES (
|
||||
'mm_minimax_t2t',
|
||||
't2t',
|
||||
'0',
|
||||
'stream',
|
||||
'/chat/completions',
|
||||
'POST',
|
||||
'data: ',
|
||||
'{
|
||||
"Authorization": "Bearer {{apikey}}",
|
||||
"Content-Type": "application/json"
|
||||
}',
|
||||
NULL,
|
||||
'{
|
||||
{% if stream %}
|
||||
"stream_options":{
|
||||
"include_usage": true
|
||||
},
|
||||
{% endif %}
|
||||
{% if tools %}
|
||||
"tools": {{json.dumps(tools, ensure_ascii=False)}},
|
||||
{% endif %}
|
||||
{% if tool_choice %}
|
||||
"tool_choice": "{{tool_choice}}",
|
||||
{% endif %}
|
||||
{% if messages %}
|
||||
"messages": {{json.dumps(messages, ensure_ascii=False)}},
|
||||
{% else %}
|
||||
"messages": [
|
||||
{% if sys_prompt %}
|
||||
{
|
||||
"role": "system",
|
||||
"content": {{json.dumps(sys_prompt, ensure_ascii=False)}}
|
||||
},
|
||||
{% endif %}
|
||||
{
|
||||
"role": "user",
|
||||
"content": {{json.dumps(prompt, ensure_ascii=False)}}
|
||||
}
|
||||
],
|
||||
{% endif %}
|
||||
{% if stream %}
|
||||
"stream":true,
|
||||
{% endif %}
|
||||
"model": "{{model}}"
|
||||
}
|
||||
',
|
||||
'{
|
||||
"id": "{{id}}",
|
||||
"object": "{{object}}",
|
||||
"created": {{created}},
|
||||
"choices": {{json.dumps(choices, ensure_ascii=False)}},
|
||||
"model": "{{model}}",
|
||||
{% if object == "chat.completion" %}
|
||||
"reasoning_content": {{json.dumps(choices[0].message.reasoning_content, ensure_ascii=False)}},
|
||||
"content":{{json.dumps(choices[0].message.content, ensure_ascii=False)}},
|
||||
{% elif len(choices)>0 %}
|
||||
"reasoning_content": {{json.dumps(choices[0].delta.reasoning_content, ensure_ascii=False)}},
|
||||
"content":{{json.dumps(choices[0].delta.content, ensure_ascii=False)}},
|
||||
{% endif %}
|
||||
{% if usage %}
|
||||
{% set usage1 = usage.update({"model": model}) %}
|
||||
"finish": "1",
|
||||
"usage":{{json.dumps(usage)}}
|
||||
{% else %}
|
||||
"finish":"0"
|
||||
{% endif %}
|
||||
}',
|
||||
'Is8l4TGkcZcqFSjbbeIK2',
|
||||
NULL,
|
||||
'minimax'
|
||||
);
|
||||
|
||||
-- ============================================================
|
||||
-- 2. 新增 uapi: minimax tm2t (多模态对话, 支持图片/视频/音频)
|
||||
-- 复用ioid: t-ujII59ku45tIPcdXu4O (文本媒体转文本, 共享)
|
||||
-- ============================================================
|
||||
INSERT IGNORE INTO `uapi` (`id`, `name`, `need_auth`, `stream`, `path`, `httpmethod`, `chunk_match`, `headers`, `params`, `data`, `response`, `ioid`, `callbackurl`, `upappid`)
|
||||
VALUES (
|
||||
'mm_minimax_tm2t',
|
||||
'tm2t',
|
||||
'0',
|
||||
'stream',
|
||||
'/chat/completions',
|
||||
'POST',
|
||||
'data: ',
|
||||
'{
|
||||
"Authorization": "Bearer {{apikey}}",
|
||||
"Content-Type": "application/json"
|
||||
}',
|
||||
NULL,
|
||||
'{
|
||||
"model": "{{model}}",
|
||||
"stream_options":{
|
||||
"include_usage": true
|
||||
},
|
||||
"messages": [
|
||||
{% if sys_prompt %}
|
||||
{
|
||||
"role": "system",
|
||||
"content": {{json.dumps(sys_prompt, ensure_ascii=False)}}
|
||||
},
|
||||
{% endif %}
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{% if image_file %}
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url":"{{b64media2url(request, image_file)}}"
|
||||
},
|
||||
{% endif %}
|
||||
{% if video_file %}
|
||||
{
|
||||
"type": "video_url",
|
||||
"video_url":"{{b64media2url(request, video_file)}}"
|
||||
},
|
||||
{% endif %}
|
||||
{% if audio_file %}
|
||||
{
|
||||
"type": "audio_url",
|
||||
"audio_url":"{{b64media2url(request, audio_file)}}"
|
||||
},
|
||||
{% endif %}
|
||||
{
|
||||
"type": "text",
|
||||
"text": {{json.dumps(prompt, ensure_ascii=False)}}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"stream":true
|
||||
}',
|
||||
'{
|
||||
"model": "{{model}}",
|
||||
{% if object == "chat.completion" %}
|
||||
"reasoning_content": {{json.dumps(choices[0].message.reasoning_content, ensure_ascii=False)}},
|
||||
"content":{{json.dumps(choices[0].message.content, ensure_ascii=False)}},
|
||||
{% elif len(choices)>0 %}
|
||||
"reasoning_content": {{json.dumps(choices[0].delta.reasoning_content, ensure_ascii=False)}},
|
||||
"content":{{json.dumps(choices[0].delta.content, ensure_ascii=False)}},
|
||||
{% endif %}
|
||||
{% if usage %}
|
||||
"finish": "1",
|
||||
"usage": {{json.dumps(usage)}}
|
||||
{% else %}
|
||||
"finish":"0"
|
||||
{% endif %}
|
||||
}',
|
||||
't-ujII59ku45tIPcdXu4O',
|
||||
NULL,
|
||||
'minimax'
|
||||
);
|
||||
|
||||
-- ============================================================
|
||||
-- 3. 新增 llm: MiniMax-M3
|
||||
-- ============================================================
|
||||
INSERT IGNORE INTO `llm` (`id`, `name`, `model`, `description`, `iconid`, `upappid`, `providerid`, `ownerid`, `enabled_date`, `expired_date`, `min_balance`, `status`)
|
||||
VALUES (
|
||||
'mm3_MiniMax_M3',
|
||||
'MiniMax M3',
|
||||
'MiniMax-M3',
|
||||
'MiniMax M3: 编程及Agent SOTA, 1M超长上下文, 多模态, 交错思维链。≤512K永久五折。',
|
||||
'minimax',
|
||||
'minimax',
|
||||
'ww4e_kfX3Lh65Sdys0Vku',
|
||||
'0',
|
||||
'2026-06-12',
|
||||
'9999-12-31',
|
||||
10.00,
|
||||
'published'
|
||||
);
|
||||
|
||||
-- ============================================================
|
||||
-- 4. 新增 llm: MiniMax-M2.7-highspeed
|
||||
-- ============================================================
|
||||
INSERT IGNORE INTO `llm` (`id`, `name`, `model`, `description`, `iconid`, `upappid`, `providerid`, `ownerid`, `enabled_date`, `expired_date`, `min_balance`, `status`)
|
||||
VALUES (
|
||||
'mm_m27_highspeed',
|
||||
'MiniMax M2.7 Highspeed',
|
||||
'MiniMax-M2.7-highspeed',
|
||||
'MiniMax M2.7高速版, 更快速度, 适合低延迟场景。输入¥4.2/百万tokens, 输出¥16.8/百万tokens。',
|
||||
'minimax',
|
||||
'minimax',
|
||||
'ww4e_kfX3Lh65Sdys0Vku',
|
||||
'0',
|
||||
'2026-06-12',
|
||||
'9999-12-31',
|
||||
10.00,
|
||||
'published'
|
||||
);
|
||||
|
||||
-- ============================================================
|
||||
-- 5. 新增 llm_api_map: MiniMax-M3 (t2t)
|
||||
-- apiname='t2t' → 匹配 uapi name='t2t' + upappid='minimax'
|
||||
-- ============================================================
|
||||
INSERT IGNORE INTO `llm_api_map` (`id`, `llmid`, `llmcatelogid`, `apiname`, `query_apiname`, `query_period`, `ppid`, `isdefaultcatelog`)
|
||||
VALUES (
|
||||
'mm3_map_t2t',
|
||||
'mm3_MiniMax_M3',
|
||||
't2t',
|
||||
't2t',
|
||||
NULL,
|
||||
NULL,
|
||||
'5jmzupARABxkDFwUraFiQ',
|
||||
'1'
|
||||
);
|
||||
|
||||
-- ============================================================
|
||||
-- 6. 新增 llm_api_map: MiniMax-M3 (tm2t, 多模态)
|
||||
-- ============================================================
|
||||
INSERT IGNORE INTO `llm_api_map` (`id`, `llmid`, `llmcatelogid`, `apiname`, `query_apiname`, `query_period`, `ppid`, `isdefaultcatelog`)
|
||||
VALUES (
|
||||
'mm3_map_tm2t',
|
||||
'mm3_MiniMax_M3',
|
||||
'tm2t',
|
||||
'tm2t',
|
||||
NULL,
|
||||
NULL,
|
||||
'5jmzupARABxkDFwUraFiQ',
|
||||
'0'
|
||||
);
|
||||
|
||||
-- ============================================================
|
||||
-- 7. 新增 llm_api_map: MiniMax-M2.7-highspeed (t2t)
|
||||
-- ============================================================
|
||||
INSERT IGNORE INTO `llm_api_map` (`id`, `llmid`, `llmcatelogid`, `apiname`, `query_apiname`, `query_period`, `ppid`, `isdefaultcatelog`)
|
||||
VALUES (
|
||||
'mm_m27hs_map_t2t',
|
||||
'mm_m27_highspeed',
|
||||
't2t',
|
||||
't2t',
|
||||
NULL,
|
||||
NULL,
|
||||
'5jmzupARABxkDFwUraFiQ',
|
||||
'1'
|
||||
);
|
||||
|
||||
-- ============================================================
|
||||
-- 8. 补充现有模型 llm_api_map.ppid
|
||||
-- ============================================================
|
||||
|
||||
-- 8a. MiniMax-Hailuo-2.3 (视频i2v) → 0V89
|
||||
UPDATE `llm_api_map` SET `ppid` = '0V89eilc_UQ2KiZIRJO8M'
|
||||
WHERE `llmid` = 'AU1f40HV3tqFjxcVWWpyR' AND (`ppid` IS NULL OR `ppid` = '');
|
||||
|
||||
-- 8b. Minimax海螺参考生视频 S2V-01 (视频i2v) → 0V89
|
||||
UPDATE `llm_api_map` SET `ppid` = '0V89eilc_UQ2KiZIRJO8M'
|
||||
WHERE `llmid` = 'oks-VG9D8p2b0Agvs-LeQ' AND (`ppid` IS NULL OR `ppid` = '');
|
||||
|
||||
-- 8c. music-2.0 (音乐) → fQzk
|
||||
UPDATE `llm_api_map` SET `ppid` = 'fQzkUeS6t6NBz_Fu4Fi77'
|
||||
WHERE `llmid` = 'ns7egG9aXi91wjI62yKfu' AND (`ppid` IS NULL OR `ppid` = '');
|
||||
|
||||
-- 8d. speech-2.6-hd (TTS) → mm_tts_pricing
|
||||
UPDATE `llm_api_map` SET `ppid` = 'mm_tts_pricing'
|
||||
WHERE `llmid` = 'q6rdMUsGD1z3S3NyZh_A_' AND (`ppid` IS NULL OR `ppid` = '');
|
||||
|
||||
-- 8e. speech-2.6-turbo (TTS) → mm_tts_pricing
|
||||
UPDATE `llm_api_map` SET `ppid` = 'mm_tts_pricing'
|
||||
WHERE `llmid` = 'CEYD4YWRxjCj4k_6bpzIM' AND (`ppid` IS NULL OR `ppid` = '');
|
||||
|
||||
-- 8f. speech-2.5-hd-preview (TTS) → mm_tts_pricing
|
||||
UPDATE `llm_api_map` SET `ppid` = 'mm_tts_pricing'
|
||||
WHERE `llmid` = 'Si2g0XJ9ym3P5jlrdmcfB' AND (`ppid` IS NULL OR `ppid` = '');
|
||||
|
||||
-- ============================================================
|
||||
-- 9. 新增 pricing_program: MiniMax TTS定价 (元/万字符)
|
||||
-- ============================================================
|
||||
INSERT IGNORE INTO `pricing_program` (`id`, `name`, `ownerid`, `providerid`, `pricing_belong`, `discount`, `description`, `pricing_spec`)
|
||||
VALUES (
|
||||
'mm_tts_pricing',
|
||||
'MiniMax语音合成定价',
|
||||
'0',
|
||||
'ww4e_kfX3Lh65Sdys0Vku',
|
||||
'provider',
|
||||
1.000,
|
||||
'MiniMax speech系列TTS定价,按万字符计费',
|
||||
'fields:\n model:\n type: str\n label: 模型\n formula:\n type: str\n label: 公式\n'
|
||||
);
|
||||
|
||||
-- ============================================================
|
||||
-- 10. 新增 pricing_program_timing: MiniMax TTS
|
||||
-- ============================================================
|
||||
INSERT IGNORE INTO `pricing_program_timing` (`id`, `ppid`, `name`, `enabled_date`, `expired_date`, `pricing_data`)
|
||||
VALUES (
|
||||
'mm_tts_timing',
|
||||
'mm_tts_pricing',
|
||||
'MiniMax TTS全价',
|
||||
'2026-06-12',
|
||||
'9999-12-31',
|
||||
'unit_values:\n 万字符: 10000\nfields:\n price_factors:\n type: string\n role: factor\n label: 计价因子\n unit_prices:\n type: float\n role: factor\n label: 单位定价\n unit:\n type: string\n role: factor\n label: 计价单位\n model:\n type: string\n role: filter\n label: model\npricings:\n- price_factors: flat\n unit_prices: 3.5\n unit: 万字符\n filters:\n - model: speech-2.6-hd\n- price_factors: flat\n unit_prices: 2.0\n unit: 万字符\n filters:\n - model: speech-2.6-turbo\n- price_factors: flat\n unit_prices: 3.5\n unit: 万字符\n filters:\n - model: speech-2.5-hd-preview\n'
|
||||
);
|
||||
|
||||
-- ============================================================
|
||||
-- 11a. 更新 5jmzup fields: 添加 prompt_tokens 字段定义
|
||||
-- 用于分段定价的 range filter,需要 value_mode: between
|
||||
-- ============================================================
|
||||
UPDATE `pricing_program_timing`
|
||||
SET `pricing_data` = REPLACE(`pricing_data`,
|
||||
' model:\n type: string\n role: filter\n label: model',
|
||||
' model:\n type: string\n role: filter\n label: model\n prompt_tokens:\n type: int\n role: filter\n label: prompt_tokens\n value_mode: between')
|
||||
WHERE `ppid` = '5jmzupARABxkDFwUraFiQ' AND `enabled_date` = '2026-04-12';
|
||||
|
||||
-- ============================================================
|
||||
-- 11b. 更新 5jmzup timing: 添加 MiniMax-M3 分段定价
|
||||
-- M3 ≤512K永久五折: 输入2.1/输出8.4/缓存0.42 元/百万tokens
|
||||
-- M3 512K~1M: 输入4.2/输出16.8/缓存0.84
|
||||
-- 使用 prompt_tokens range filter 区分两个计价段
|
||||
-- ============================================================
|
||||
UPDATE `pricing_program_timing`
|
||||
SET `pricing_data` = CONCAT(`pricing_data`, '
|
||||
- price_factors: prompt_tokens
|
||||
unit_prices: 2.1
|
||||
unit: 百万
|
||||
filters:
|
||||
- model: MiniMax-M3
|
||||
prompt_tokens: 0 =~ 524288
|
||||
value_mode: between
|
||||
- price_factors: completion_tokens
|
||||
unit_prices: 8.4
|
||||
unit: 百万
|
||||
filters:
|
||||
- model: MiniMax-M3
|
||||
prompt_tokens: 0 =~ 524288
|
||||
value_mode: between
|
||||
- price_factors: cached_tokens
|
||||
unit_prices: 0.42
|
||||
unit: 百万
|
||||
filters:
|
||||
- model: MiniMax-M3
|
||||
prompt_tokens: 0 =~ 524288
|
||||
value_mode: between
|
||||
- price_factors: prompt_tokens
|
||||
unit_prices: 4.2
|
||||
unit: 百万
|
||||
filters:
|
||||
- model: MiniMax-M3
|
||||
prompt_tokens: 524288 =~ 1048576
|
||||
value_mode: between
|
||||
- price_factors: completion_tokens
|
||||
unit_prices: 16.8
|
||||
unit: 百万
|
||||
filters:
|
||||
- model: MiniMax-M3
|
||||
prompt_tokens: 524288 =~ 1048576
|
||||
value_mode: between
|
||||
- price_factors: cached_tokens
|
||||
unit_prices: 0.84
|
||||
unit: 百万
|
||||
filters:
|
||||
- model: MiniMax-M3
|
||||
prompt_tokens: 524288 =~ 1048576
|
||||
value_mode: between
|
||||
')
|
||||
WHERE `ppid` = '5jmzupARABxkDFwUraFiQ' AND `enabled_date` = '2026-04-12';
|
||||
|
||||
-- ============================================================
|
||||
-- 12. 更新 pricing_program 5jmzup: 添加M3到模型选项
|
||||
-- ============================================================
|
||||
UPDATE `pricing_program`
|
||||
SET `pricing_spec` = 'fields:\n model:\n type: str\n label: 模型\n options:\n - MiniMax-M3\n - MiniMax-M2.7\n - MiniMax-M2.7-highspeed\n - MiniMax-M2.5\n - MiniMax-M2.5-highspeed\n - M2-her\n formula:\n type: str\n label: 公式\n'
|
||||
WHERE `id` = '5jmzupARABxkDFwUraFiQ';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- ROLLBACK 语句 (如需回滚)
|
||||
-- ============================================================
|
||||
-- DELETE FROM `uapi` WHERE `id` IN ('mm_minimax_t2t', 'mm_minimax_tm2t');
|
||||
-- DELETE FROM `llm` WHERE `id` IN ('mm3_MiniMax_M3', 'mm_m27_highspeed');
|
||||
-- DELETE FROM `llm_api_map` WHERE `id` IN ('mm3_map_t2t', 'mm3_map_tm2t', 'mm_m27hs_map_t2t');
|
||||
-- UPDATE `llm_api_map` SET `ppid` = NULL WHERE `llmid` = 'AU1f40HV3tqFjxcVWWpyR';
|
||||
-- UPDATE `llm_api_map` SET `ppid` = NULL WHERE `llmid` = 'oks-VG9D8p2b0Agvs-LeQ';
|
||||
-- UPDATE `llm_api_map` SET `ppid` = NULL WHERE `llmid` = 'ns7egG9aXi91wjI62yKfu';
|
||||
-- UPDATE `llm_api_map` SET `ppid` = NULL WHERE `llmid` = 'q6rdMUsGD1z3S3NyZh_A_';
|
||||
-- UPDATE `llm_api_map` SET `ppid` = NULL WHERE `llmid` = 'CEYD4YWRxjCj4k_6bpzIM';
|
||||
-- UPDATE `llm_api_map` SET `ppid` = NULL WHERE `llmid` = 'Si2g0XJ9ym3P5jlrdmcfB';
|
||||
-- DELETE FROM `pricing_program` WHERE `id` = 'mm_tts_pricing';
|
||||
-- DELETE FROM `pricing_program_timing` WHERE `id` = 'mm_tts_timing';
|
||||
-- -- 5jmzup的pricing_data CONCAT追加需手动编辑YAML移除M3条目
|
||||
@ -96,27 +96,6 @@ for p in "${LLMUSAGE_PATHS[@]}"; do
|
||||
done
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "============================================"
|
||||
echo " llmage: 客户 v1 API 调用权限"
|
||||
echo "============================================"
|
||||
|
||||
CUSTOMER_ROLES=("customer.admin" "customer.user")
|
||||
|
||||
V1_API_PATHS=(
|
||||
"/llmage/v1/chat/completions/index.dspy"
|
||||
"/llmage/v1/video/generations/index.dspy"
|
||||
"/llmage/v1/image/generations/index.dspy"
|
||||
"/llmage/v1/models/index.dspy"
|
||||
"/llmage/v1/tasks/index.dspy"
|
||||
)
|
||||
|
||||
for p in "${V1_API_PATHS[@]}"; do
|
||||
for role in "${CUSTOMER_ROLES[@]}"; do
|
||||
set_perm "${role}" "${p}"
|
||||
done
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "============================================"
|
||||
echo " 权限配置完成,共设置 ${COUNT} 条权限"
|
||||
|
||||
@ -1,63 +0,0 @@
|
||||
-- ============================================================
|
||||
--
|
||||
-- Wan2.7 文生视频 API接口接入
|
||||
-- 生成时间: 2026-06-12 (重写: 2026-06-13)
|
||||
-- 模型: wan2.7-t2v-2026-04-25 (文生视频)
|
||||
-- 支持: 720P/1080P, 2-15秒, 音频, 多镜头叙事
|
||||
-- ============================================================
|
||||
-- 前置条件:
|
||||
-- llm表已有记录: id='IE8Ws20ZSoyAkOryWqhG_', model='wan2.7-t2v-2026-04-25'
|
||||
-- pricing_program已有记录: id='GFJm2LIQoq2C70fFoY1H3', name='通义万相 wan2.7-t2v'
|
||||
-- uapi 't2v' (id='It-ShFhCGIhS0ds3C2JJ0') 已有,复用万象通用文生视频接口
|
||||
-- ============================================================
|
||||
|
||||
-- ============================================================
|
||||
-- 1. 新增 llm_api_map: wan2.7-t2v → t2v接口 + 定价
|
||||
-- ============================================================
|
||||
INSERT IGNORE INTO `llm_api_map` (`id`, `llmid`, `llmcatelogid`, `apiname`, `query_apiname`, `query_period`, `ppid`, `isdefaultcatelog`)
|
||||
VALUES (
|
||||
'wan27t2v_map_001',
|
||||
'IE8Ws20ZSoyAkOryWqhG_',
|
||||
't2v',
|
||||
't2v',
|
||||
't2vstatus',
|
||||
10,
|
||||
'GFJm2LIQoq2C70fFoY1H3',
|
||||
'1'
|
||||
);
|
||||
|
||||
-- ============================================================
|
||||
-- 2. 新增 pricing_program_timing: wan2.7-t2v 定价
|
||||
-- 官方定价: 720P=0.6元/秒, 1080P=1.0元/秒
|
||||
-- ============================================================
|
||||
INSERT INTO `pricing_program_timing` (`id`, `ppid`, `name`, `enabled_date`, `expired_date`, `pricing_data`)
|
||||
VALUES (
|
||||
'wan27t2v_timing_001',
|
||||
'GFJm2LIQoq2C70fFoY1H3',
|
||||
NULL,
|
||||
'2026-05-20',
|
||||
'9999-12-31',
|
||||
'unit_values:\n 秒: 1\nfields:\n price_factors:\n type: string\n role: factor\n label: 计价因子\n unit_prices:\n type: float\n role: factor\n label: 单位定价\n unit:\n type: string\n role: factor\n label: 计价单位\n SR:\n type: string\n role: filter\n label: SR\npricings:\n- price_factors: duration\n unit_prices: 0.6\n unit: 秒\n filters:\n - SR: 720\n- price_factors: duration\n unit_prices: 1.0\n unit: 秒\n filters:\n - SR: 1080'
|
||||
);
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- 验证 (执行后运行确认)
|
||||
-- ============================================================
|
||||
-- SELECT m.id, m.llmid, m.llmcatelogid, m.apiname, m.query_apiname, m.ppid,
|
||||
-- l.name as model_name, l.model,
|
||||
-- pp.name as pricing_name,
|
||||
-- (SELECT COUNT(*) FROM pricing_program_timing WHERE ppid = m.ppid) as timing_count
|
||||
-- FROM llm_api_map m
|
||||
-- JOIN llm l ON m.llmid = l.id
|
||||
-- JOIN pricing_program pp ON m.ppid = pp.id
|
||||
-- WHERE m.llmid = 'IE8Ws20ZSoyAkOryWqhG_';
|
||||
--
|
||||
-- 预期: timing_count = 1
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- 回滚
|
||||
-- ============================================================
|
||||
-- DELETE FROM llm_api_map WHERE id = 'wan27t2v_map_001';
|
||||
-- DELETE FROM pricing_program_timing WHERE id = 'wan27t2v_timing_001';
|
||||
@ -1,11 +0,0 @@
|
||||
-- llmage: 添加模型上架/下架功能
|
||||
-- 执行此 SQL 后,所有现有模型默认已上架,不影响线上使用
|
||||
|
||||
-- 1. 添加 status 字段
|
||||
ALTER TABLE llm ADD COLUMN `status` VARCHAR(16) NOT NULL DEFAULT 'unpublished' COMMENT '上架状态: published/unpublished' AFTER `min_balance`;
|
||||
|
||||
-- 2. 现有模型全部设为已上架
|
||||
UPDATE llm SET status = 'published';
|
||||
|
||||
-- 3. 添加索引(按状态筛选是高频操作)
|
||||
ALTER TABLE llm ADD INDEX `idx_status` (`status`);
|
||||
@ -1,6 +0,0 @@
|
||||
-- DROP cost column, ADD tenantid column to llmusage
|
||||
ALTER TABLE llmusage DROP COLUMN IF EXISTS cost;
|
||||
ALTER TABLE llmusage ADD COLUMN IF NOT EXISTS tenantid VARCHAR(32) DEFAULT NULL COMMENT '租户orgid,标识客户在哪个分销商入口消费';
|
||||
-- Also update llmusage_history if exists
|
||||
ALTER TABLE llmusage_history DROP COLUMN IF EXISTS cost;
|
||||
ALTER TABLE llmusage_history ADD COLUMN IF NOT EXISTS tenantid VARCHAR(32) DEFAULT NULL;
|
||||
@ -1,132 +1,76 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
import os
|
||||
|
||||
result = {'success': False, 'rows': [], 'total': 0, 'page': 1, 'page_size': 50}
|
||||
|
||||
try:
|
||||
llmage_db = get_module_dbname('llmage')
|
||||
sage_db = get_module_dbname('sage')
|
||||
db = DBPools()
|
||||
dbname = get_module_dbname('llmage')
|
||||
user_orgid = await get_userorgid()
|
||||
|
||||
filters = {}
|
||||
if params_kw.get('userorgid'):
|
||||
filters['userorgid'] = params_kw.get('userorgid')
|
||||
if params_kw.get('llmid'):
|
||||
filters['llmid'] = params_kw.get('llmid')
|
||||
if params_kw.get('handled') is not None and params_kw.get('handled') != '':
|
||||
filters['handled'] = params_kw.get('handled')
|
||||
if params_kw.get('start_date'):
|
||||
filters['start_date'] = params_kw.get('start_date')
|
||||
if params_kw.get('end_date'):
|
||||
filters['end_date'] = params_kw.get('end_date')
|
||||
# Extract filter parameters from params_kw
|
||||
filters = {}
|
||||
if params_kw.get('userorgid'):
|
||||
filters['userorgid'] = params_kw.get('userorgid')
|
||||
if params_kw.get('llmid'):
|
||||
filters['llmid'] = params_kw.get('llmid')
|
||||
if params_kw.get('handled') is not None:
|
||||
filters['handled'] = params_kw.get('handled')
|
||||
if params_kw.get('start_date'):
|
||||
filters['start_date'] = params_kw.get('start_date')
|
||||
if params_kw.get('end_date'):
|
||||
filters['end_date'] = params_kw.get('end_date')
|
||||
|
||||
try:
|
||||
page = int(params_kw.get('page', 1))
|
||||
except (ValueError, TypeError):
|
||||
page = 1
|
||||
if page < 1:
|
||||
page = 1
|
||||
page_size = int(params_kw.get('rows', params_kw.get('page_size', 20)))
|
||||
page = int(params_kw.get('page', 1))
|
||||
page_size = int(params_kw.get('page_size', 50))
|
||||
|
||||
async with db.sqlorContext(llmage_db) as sor:
|
||||
conditions = []
|
||||
ns = {}
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
# Build dynamic SQL
|
||||
conditions = []
|
||||
ns = {}
|
||||
|
||||
if filters.get('userorgid'):
|
||||
conditions.append("userorgid=${userorgid}$")
|
||||
ns['userorgid'] = filters['userorgid']
|
||||
if filters.get('llmid'):
|
||||
conditions.append("llmid=${llmid}$")
|
||||
ns['llmid'] = filters['llmid']
|
||||
if filters.get('handled') is not None:
|
||||
conditions.append("handled=${handled}$")
|
||||
ns['handled'] = filters['handled']
|
||||
if filters.get('start_date'):
|
||||
conditions.append("use_date>=${start_date}$")
|
||||
ns['start_date'] = filters['start_date']
|
||||
if filters.get('end_date'):
|
||||
conditions.append("use_date<=${end_date}$")
|
||||
ns['end_date'] = filters['end_date']
|
||||
# Default: show unhandled records
|
||||
if 'handled' not in filters:
|
||||
conditions.append("handled='0'")
|
||||
|
||||
where = ""
|
||||
if conditions:
|
||||
where = "WHERE " + " AND ".join(conditions)
|
||||
if filters.get('userorgid'):
|
||||
conditions.append("userorgid=${userorgid}$")
|
||||
ns['userorgid'] = filters['userorgid']
|
||||
if filters.get('llmid'):
|
||||
conditions.append("llmid=${llmid}$")
|
||||
ns['llmid'] = filters['llmid']
|
||||
if filters.get('handled') is not None:
|
||||
conditions.append("handled=${handled}$")
|
||||
ns['handled'] = filters['handled']
|
||||
if filters.get('start_date'):
|
||||
conditions.append("use_date>=${start_date}$")
|
||||
ns['start_date'] = filters['start_date']
|
||||
if filters.get('end_date'):
|
||||
conditions.append("use_date<=${end_date}$")
|
||||
ns['end_date'] = filters['end_date']
|
||||
|
||||
# 不查 failed_reason(大 TEXT),只查列表需要的字段
|
||||
sql = f"""
|
||||
SELECT id, llmusageid, llmid, userid, userorgid, use_date, use_time,
|
||||
amount, cost, failed_time, retry_count, handled
|
||||
FROM llmusage_accounting_failed
|
||||
{where}
|
||||
ORDER BY failed_time DESC
|
||||
"""
|
||||
where = ""
|
||||
if conditions:
|
||||
where = "where " + " and ".join(conditions)
|
||||
|
||||
count_sql = f"""
|
||||
SELECT count(*) as cnt
|
||||
FROM llmusage_accounting_failed
|
||||
{where}
|
||||
"""
|
||||
count_recs = await sor.sqlExe(count_sql, ns)
|
||||
total = count_recs[0].cnt if count_recs else 0
|
||||
# Count total
|
||||
count_sql = f"select count(*) as cnt from llmusage_accounting_failed {where}"
|
||||
count_recs = await sor.sqlExe(count_sql, ns)
|
||||
total = count_recs[0].cnt if count_recs else 0
|
||||
|
||||
offset = (page - 1) * page_size
|
||||
query_sql = sql + f" LIMIT {page_size} OFFSET {offset}"
|
||||
recs = await sor.sqlExe(query_sql, ns)
|
||||
# Query with pagination
|
||||
offset = (page - 1) * page_size
|
||||
query_sql = f"""select * from llmusage_accounting_failed {where}
|
||||
order by failed_time desc limit {page_size} offset {offset}"""
|
||||
recs = await sor.sqlExe(query_sql, ns)
|
||||
|
||||
if recs:
|
||||
# 收集所有需要转换的 ID
|
||||
llm_ids = list(set(r.llmid for r in recs if r.llmid))
|
||||
user_ids = list(set(r.userid for r in recs if r.userid))
|
||||
org_ids = list(set(r.userorgid for r in recs if r.userorgid))
|
||||
|
||||
# 批量查询映射(内存中完成)
|
||||
llm_map = {}
|
||||
if llm_ids:
|
||||
llm_recs = await sor.sqlExe(
|
||||
"SELECT id, name FROM llm WHERE id IN ${ids}$",
|
||||
{'ids': tuple(llm_ids)}
|
||||
)
|
||||
if llm_recs:
|
||||
llm_map = {r.id: r.name for r in llm_recs}
|
||||
|
||||
user_map = {}
|
||||
if user_ids:
|
||||
async with db.sqlorContext(sage_db) as sage_sor:
|
||||
user_recs = await sage_sor.sqlExe(
|
||||
"SELECT id, username FROM users WHERE id IN ${ids}$",
|
||||
{'ids': tuple(user_ids)}
|
||||
)
|
||||
if user_recs:
|
||||
user_map = {r.id: r.username for r in user_recs}
|
||||
|
||||
org_map = {}
|
||||
if org_ids:
|
||||
async with db.sqlorContext(sage_db) as sage_sor:
|
||||
org_recs = await sage_sor.sqlExe(
|
||||
"SELECT id, orgname FROM organization WHERE id IN ${ids}$",
|
||||
{'ids': tuple(org_ids)}
|
||||
)
|
||||
if org_recs:
|
||||
org_map = {r.id: r.orgname for r in org_recs}
|
||||
|
||||
# 转换
|
||||
handled_map = {'0': '未处理', '1': '已处理'}
|
||||
rows = []
|
||||
for r in recs:
|
||||
d = dict(r)
|
||||
d['llmid_text'] = llm_map.get(r.llmid, r.llmid)
|
||||
d['userid_text'] = user_map.get(r.userid, r.userid)
|
||||
d['userorgid_text'] = org_map.get(r.userorgid, r.userorgid)
|
||||
d['handled_text'] = handled_map.get(r.handled, r.handled)
|
||||
rows.append(d)
|
||||
|
||||
result['rows'] = rows
|
||||
result['total'] = total
|
||||
result['page'] = page
|
||||
result['page_size'] = page_size
|
||||
result['success'] = True
|
||||
else:
|
||||
result['success'] = True
|
||||
result['rows'] = [dict(r) for r in (recs or [])]
|
||||
result['total'] = total
|
||||
result['page'] = page
|
||||
result['page_size'] = page_size
|
||||
result['success'] = True
|
||||
|
||||
except Exception as e:
|
||||
result['error'] = str(e)
|
||||
debug(f'failed_accounting_list error: {format_exc()}')
|
||||
result['error'] = str(e)
|
||||
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
|
||||
@ -1,13 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
result = []
|
||||
|
||||
try:
|
||||
dbname = get_module_dbname('llmage')
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
rows = await sor.sqlExe("select name, path from uapi order by name", {})
|
||||
result = [{'value': r['name'], 'text': f"{r['name']} ({r['path']})"} for r in (rows or [])]
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
@ -1,13 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
result = []
|
||||
|
||||
try:
|
||||
dbname = get_module_dbname('llmage')
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
rows = await sor.sqlExe("select id, name from llmcatelog order by name", {})
|
||||
result = [{'value': r['id'], 'text': r['name']} for r in (rows or [])]
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
@ -1,93 +0,0 @@
|
||||
result = {'success': False, 'rows': [], 'total': 0, 'page': 1, 'page_size': 10}
|
||||
|
||||
try:
|
||||
dbname = get_module_dbname('llmage')
|
||||
userid = await get_user()
|
||||
|
||||
page = int(params_kw.get('page', 1))
|
||||
page_size = int(params_kw.get('pagerows', 10))
|
||||
llmcatelogid = params_kw.get('llmcatelogid')
|
||||
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
# Build filter conditions
|
||||
conditions = ["userid = ${userid}$"]
|
||||
ns = {'userid': userid}
|
||||
if llmcatelogid:
|
||||
conditions.append("llmid in (select llmid from llm_api_map where llmcatelogid = ${llmcatelogid}$)")
|
||||
ns['llmcatelogid'] = llmcatelogid
|
||||
|
||||
where_clause = " and ".join(conditions)
|
||||
# Count total from both tables (并行两个 count 查询)
|
||||
sql1 = f"select count(*) as cnt from llmusage where {where_clause}"
|
||||
sql2 = f"select count(*) as cnt from llmusage_history where {where_clause}"
|
||||
cnt1_recs = await sor.sqlExe(sql1, ns.copy())
|
||||
cnt2_recs = await sor.sqlExe(sql2, ns.copy())
|
||||
total = (cnt1_recs[0].cnt if cnt1_recs else 0) + (cnt2_recs[0].cnt if cnt2_recs else 0)
|
||||
# 优化点 1: 分别查询两张表, 让各自走 (userid, use_time) 复合索引
|
||||
# 每表取前 offset+page_size 条 (已按 use_time desc 排好)
|
||||
offset = (page - 1) * page_size
|
||||
fetch = offset + page_size
|
||||
select_cols = ("id, llmid, use_date, use_time, userid, usages, ioinfo, "
|
||||
"status, taskid, amount, cost, userorgid, accounting_status")
|
||||
|
||||
q1 = f"select {select_cols} from llmusage where {where_clause} order by use_time desc limit {fetch}"
|
||||
q2 = f"select {select_cols} from llmusage_history where {where_clause} order by use_time desc limit {fetch}"
|
||||
recs1 = await sor.sqlExe(q1, ns)
|
||||
recs2 = await sor.sqlExe(q2, ns)
|
||||
|
||||
# 优化点 2: Python 归并两个已排序序列 (O(n) 比 SQL UNION+sort 快)
|
||||
merged = []
|
||||
i = j = 0
|
||||
rows1 = [dict(r) for r in (recs1 or [])]
|
||||
rows2 = [dict(r) for r in (recs2 or [])]
|
||||
while i < len(rows1) and j < len(rows2):
|
||||
if (rows1[i].get('use_time') or '') >= (rows2[j].get('use_time') or ''):
|
||||
merged.append(rows1[i]); i += 1
|
||||
else:
|
||||
merged.append(rows2[j]); j += 1
|
||||
merged.extend(rows1[i:])
|
||||
merged.extend(rows2[j:])
|
||||
|
||||
# 应用分页
|
||||
page_rows = merged[offset:offset + page_size]
|
||||
|
||||
# 优化点 3: 并发读取 ioinfo 文件 (不再串行 await)
|
||||
import aiofiles
|
||||
from ahserver.filestorage import FileStorage
|
||||
fs = FileStorage()
|
||||
|
||||
async def _load_io(row):
|
||||
webpath = row.get('ioinfo')
|
||||
io_content = None
|
||||
if webpath:
|
||||
try:
|
||||
real_path = fs.realPath(webpath)
|
||||
async with aiofiles.open(real_path, 'rb') as f:
|
||||
bin_data = await f.read()
|
||||
io_content = json.loads(bin_data.decode('utf-8'))
|
||||
except Exception:
|
||||
io_content = None
|
||||
row['io_content'] = io_content
|
||||
if isinstance(row.get('usages'), str):
|
||||
try:
|
||||
row['usages'] = json.loads(row['usages'])
|
||||
except Exception:
|
||||
pass
|
||||
return row
|
||||
|
||||
rows = []
|
||||
for r in page_rows:
|
||||
d = await _load_io(r)
|
||||
rows.append(d)
|
||||
result['rows'] = list(rows)
|
||||
result['total'] = total
|
||||
result['page'] = page
|
||||
result['page_size'] = page_size
|
||||
result['success'] = True
|
||||
|
||||
except Exception as e:
|
||||
exception(f'{e}{format_exc()}')
|
||||
result['error'] = str(e)
|
||||
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
|
||||
@ -1,15 +0,0 @@
|
||||
result = []
|
||||
|
||||
try:
|
||||
async with get_sor_context(request._run_ns, 'rbac') as sor:
|
||||
orgs = await sor.sqlExe(
|
||||
"select id, orgname from organization order by orgname",
|
||||
{}
|
||||
)
|
||||
if orgs:
|
||||
for r in orgs:
|
||||
result.append({'providerid': str(r.id), 'providerid_text': r.orgname or ''})
|
||||
except Exception as e:
|
||||
debug(f'get_organizations error: {e}')
|
||||
|
||||
return json.dumps(result, ensure_ascii=False)
|
||||
@ -1,36 +0,0 @@
|
||||
import json
|
||||
|
||||
providerid = (params_kw or {}).get('providerid', '')
|
||||
catelogid = (params_kw or {}).get('catelogid', '')
|
||||
search = (params_kw or {}).get('search', '').strip()
|
||||
|
||||
data = await request._run_ns.get_llms_by_catelog_to_customer(
|
||||
catelogid=catelogid if catelogid else None,
|
||||
orderby='a.name'
|
||||
)
|
||||
|
||||
result = []
|
||||
for cate in data:
|
||||
for llm in cate['llms']:
|
||||
if providerid and llm.providerid != providerid:
|
||||
continue
|
||||
if search:
|
||||
sl = search.lower()
|
||||
n = (llm.name or '').lower()
|
||||
d = (llm.description or '').lower()
|
||||
if sl not in n and sl not in d:
|
||||
continue
|
||||
result.append({
|
||||
'id': llm.id,
|
||||
'name': llm.name,
|
||||
'model': llm.model,
|
||||
'description': llm.description or '',
|
||||
'iconid': llm.iconid,
|
||||
'providerid': llm.providerid,
|
||||
'provider_name': getattr(llm, 'provider_name', getattr(llm, 'orgname', '')),
|
||||
'catelog_id': getattr(llm, 'catelog_id', ''),
|
||||
'catelogname': getattr(llm, 'catelogname', ''),
|
||||
'pricing_display': getattr(llm, 'pricing_display', []),
|
||||
})
|
||||
|
||||
return json.dumps({'total': len(result), 'rows': result}, ensure_ascii=False)
|
||||
@ -1,13 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
result = []
|
||||
|
||||
try:
|
||||
dbname = get_module_dbname('pricing')
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
rows = await sor.sqlExe("select id, name from pricing_program order by name", {})
|
||||
result = [{'value': r['id'], 'text': r['name']} for r in (rows or [])]
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
@ -1,37 +0,0 @@
|
||||
llmid = params_kw.get('llmid')
|
||||
allow_empty = params_kw.get('allow_empty', '')
|
||||
|
||||
result = []
|
||||
if allow_empty:
|
||||
result = [{'apiname': '', 'apiname_text': '不指定', 'value': '', 'text': '不指定'}]
|
||||
|
||||
try:
|
||||
if not llmid:
|
||||
return json.dumps(result, ensure_ascii=False)
|
||||
|
||||
# Get model's upappid from llmage db
|
||||
dbname = get_module_dbname('llmage')
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
llm_recs = await sor.sqlExe(
|
||||
"select upappid from llm where id = ${llmid}$",
|
||||
{'llmid': llmid}
|
||||
)
|
||||
if not llm_recs or not llm_recs[0].get('upappid'):
|
||||
return json.dumps(result, ensure_ascii=False)
|
||||
upappid = llm_recs[0].upappid
|
||||
|
||||
# Query uapi table from uapi module's db
|
||||
async with get_sor_context(request._run_ns, 'uapi') as sor:
|
||||
apis = await sor.sqlExe(
|
||||
"select name as apiname, name as apiname_text from uapi where upappid = ${upappid}$ order by name",
|
||||
{'upappid': upappid}
|
||||
)
|
||||
# Add value/text keys for form dropdown compatibility
|
||||
for a in apis:
|
||||
a['value'] = a['apiname']
|
||||
a['text'] = a['apiname_text']
|
||||
return json.dumps(result + list(apis), ensure_ascii=False)
|
||||
except Exception as e:
|
||||
debug(f'get_search_apiname error: {e}')
|
||||
|
||||
return json.dumps(result, ensure_ascii=False)
|
||||
@ -1,7 +0,0 @@
|
||||
async with get_sor_context(request._run_ns, 'llmage') as sor:
|
||||
recs = await sor.sqlExe(
|
||||
"SELECT id, name FROM llm ORDER BY name", {})
|
||||
result = [{'value': '', 'text': '全部'}]
|
||||
for r in recs:
|
||||
result.append({'value': r.id, 'text': r.name})
|
||||
return result
|
||||
@ -1,13 +0,0 @@
|
||||
result = [{'providerid': '', 'providerid_text': '全部'}]
|
||||
|
||||
try:
|
||||
async with get_sor_context(request._run_ns, 'rbac') as sor:
|
||||
rows = await sor.sqlExe(
|
||||
"select id as providerid, supplier_name as providerid_text from suppliers where status = '1' order by supplier_name",
|
||||
{}
|
||||
)
|
||||
return json.dumps([{'providerid': '', 'providerid_text': '全部'}] + list(rows), ensure_ascii=False)
|
||||
except Exception as e:
|
||||
debug(f'get_search_providerid error: {e}')
|
||||
|
||||
return json.dumps(result, ensure_ascii=False)
|
||||
@ -1,13 +0,0 @@
|
||||
result = [{'upappid': '', 'upappid_text': '全部'}]
|
||||
|
||||
try:
|
||||
async with get_sor_context(request._run_ns, 'uapi') as sor:
|
||||
apps = await sor.sqlExe(
|
||||
"select id as upappid, name as upappid_text from upapp order by name",
|
||||
{}
|
||||
)
|
||||
return json.dumps([{'upappid': '', 'upappid_text': '全部'}] + list(apps), ensure_ascii=False)
|
||||
except Exception as e:
|
||||
debug(f'get_search_upappid error: {e}')
|
||||
|
||||
return json.dumps(result, ensure_ascii=False)
|
||||
@ -1,16 +0,0 @@
|
||||
result = []
|
||||
|
||||
try:
|
||||
async with get_sor_context(request._run_ns, 'uapi') as sor:
|
||||
user_orgid = await get_userorgid()
|
||||
apps = await sor.sqlExe(
|
||||
"select id, name from upapp order by name",
|
||||
{}
|
||||
)
|
||||
if apps:
|
||||
for r in apps:
|
||||
result.append({'upappid': str(r.id), 'upappid_text': r.name or ''})
|
||||
except Exception as e:
|
||||
debug(f'get_upapps error: {e}')
|
||||
|
||||
return json.dumps(result, ensure_ascii=False)
|
||||
@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
|
||||
result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Invalid', 'type': 'error'}}
|
||||
|
||||
@ -59,4 +60,4 @@ try:
|
||||
except Exception as e:
|
||||
result['options'] = {'title': 'Error', 'message': f'添加失败: {str(e)}', 'type': 'error'}
|
||||
|
||||
return result
|
||||
return json.dumps(result, ensure_ascii=False)
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
|
||||
result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Invalid', 'type': 'error'}}
|
||||
|
||||
@ -27,4 +28,4 @@ try:
|
||||
except Exception as e:
|
||||
result['options'] = {'title': 'Error', 'message': f'删除失败: {str(e)}', 'type': 'error'}
|
||||
|
||||
return result
|
||||
return json.dumps(result, ensure_ascii=False)
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
|
||||
result = {'success': False, 'rows': [], 'total': 0}
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
|
||||
result = {'success': False, 'data': {'llms': [], 'catelogs': [], 'apis': [], 'ppids': []}}
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
|
||||
result = {'success': False, 'data': {'llms': [], 'catelogs': []}}
|
||||
|
||||
|
||||
@ -1,20 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
result = {"widgettype":"Error","options":{"title":"操作失败","message":"Invalid request","cwidth":16,"cheight":9,"timeout":3}}
|
||||
|
||||
try:
|
||||
env = request._run_ns
|
||||
dbname = get_module_dbname('llmage')
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
data = params_kw.copy()
|
||||
data.pop('page', None)
|
||||
data.pop('rows', None)
|
||||
data.pop('data_filter', None)
|
||||
data['id'] = getID()
|
||||
data['ownerid'] = data.get('ownerid') or await env.get_userorgid()
|
||||
await sor.C('llm', data)
|
||||
result = {"widgettype":"Message","options":{"title":"创建成功","message":"ok","cwidth":16,"cheight":9,"timeout":3}}
|
||||
except Exception as e:
|
||||
result["options"] = {"title":"操作失败","message":str(e),"cwidth":16,"cheight":9,"timeout":3}
|
||||
|
||||
return result
|
||||
@ -1,21 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
result = {"widgettype":"Error","options":{"title":"操作失败","message":"Invalid request","cwidth":16,"cheight":9,"timeout":3}}
|
||||
|
||||
try:
|
||||
dbname = get_module_dbname('llmage')
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
data = params_kw.copy()
|
||||
data.pop('page', None)
|
||||
data.pop('rows', None)
|
||||
data.pop('data_filter', None)
|
||||
record_id = data.get('id')
|
||||
if not record_id:
|
||||
result['message'] = '缺少id'
|
||||
else:
|
||||
await sor.D('llm', {'id': record_id})
|
||||
result = {"widgettype":"Message","options":{"title":"删除成功","message":"ok","cwidth":16,"cheight":9,"timeout":3}}
|
||||
except Exception as e:
|
||||
result["options"] = {"title":"操作失败","message":str(e),"cwidth":16,"cheight":9,"timeout":3}
|
||||
|
||||
return result
|
||||
@ -1,86 +0,0 @@
|
||||
llmid = params_kw.get('llmid', '')
|
||||
action = params_kw.get('action', 'check')
|
||||
|
||||
if not llmid:
|
||||
return json.dumps({'widgettype':'Error','options':{'text':'missing llmid'}}, ensure_ascii=False)
|
||||
|
||||
if action == 'inference':
|
||||
# 验证推理配置是否完整
|
||||
async with get_sor_context(request._run_ns, 'llmage') as sor:
|
||||
recs = await sor.sqlExe(
|
||||
"select * from llm where id=${llmid}$", {'llmid': llmid})
|
||||
if not recs:
|
||||
return json.dumps({'widgettype':'Error','options':{'text':'模型记录不存在'}}, ensure_ascii=False)
|
||||
llm = recs[0]
|
||||
|
||||
# 检查 API 映射
|
||||
maps = await sor.sqlExe(
|
||||
"select * from llm_api_map where llmid=${llmid}$",
|
||||
{'llmid': llmid})
|
||||
if not maps:
|
||||
return json.dumps({'widgettype':'Error','options':{'text':'无 API 映射配置'}}, ensure_ascii=False)
|
||||
|
||||
# 检查 upapp 和 uapi
|
||||
uapi_recs = await sor.sqlExe("""
|
||||
select a.*, e.ioid, e.stream, e.name as api_name
|
||||
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.id = e.upappid and m.apiname = e.name
|
||||
where a.id=${llmid}$""", {'llmid': llmid})
|
||||
|
||||
if not uapi_recs:
|
||||
return json.dumps({'widgettype':'Error','options':{'text':'uapi 配置不完整,无法调用'}}, ensure_ascii=False)
|
||||
|
||||
uapi = uapi_recs[0]
|
||||
|
||||
# 检查 ioid
|
||||
io_recs = await sor.sqlExe(
|
||||
"select * from uapiio where id=${ioid}$", {'ioid': uapi.ioid})
|
||||
if not io_recs:
|
||||
return json.dumps({'widgettype':'Error','options':{'text':'IO 定义不存在'}}, ensure_ascii=False)
|
||||
|
||||
return json.dumps({'widgettype':'Message','options':{'text':f'推理配置验证通过\n模型: {llm.name}\nAPI: {uapi.api_name}\nIO: {uapi.ioid}\nStream: {uapi.stream}'}}, ensure_ascii=False)
|
||||
|
||||
elif action == 'check_charging':
|
||||
# 验证计费配置是否完整
|
||||
usages_str = params_kw.get('usages', '{}')
|
||||
|
||||
try:
|
||||
usages = json.loads(usages_str) if isinstance(usages_str, str) else usages_str
|
||||
except:
|
||||
usages = {}
|
||||
|
||||
async with get_sor_context(request._run_ns, 'llmage') as sor:
|
||||
maps = await sor.sqlExe(
|
||||
"select * from llm_api_map where llmid=${llmid}$",
|
||||
{'llmid': llmid})
|
||||
if not maps:
|
||||
return json.dumps({'widgettype':'Error','options':{'text':'无 API 映射'}}, ensure_ascii=False)
|
||||
|
||||
ppids = [m.ppid for m in maps if m.ppid]
|
||||
if not ppids:
|
||||
return json.dumps({'widgettype':'Error','options':{'text':'无定价项目(ppid)'}}, ensure_ascii=False)
|
||||
|
||||
# 实际调用定价计算
|
||||
try:
|
||||
prices = await llm_query_price(llmid, usages)
|
||||
if not prices:
|
||||
return json.dumps({'widgettype':'Error','options':{'text':'定价计算返回空'}}, ensure_ascii=False)
|
||||
|
||||
lines = []
|
||||
total = 0
|
||||
for p in prices:
|
||||
amount = getattr(p, 'amount', 0) or 0
|
||||
name = getattr(p, 'name', '') or getattr(p, 'timing_name', '') or getattr(p, 'item', '') or ''
|
||||
# 显示所有可用字段
|
||||
fields = [f' {k}={getattr(p,k,"")}' for k in dir(p) if not k.startswith('_') and k not in ('jinja_pass_arg',)]
|
||||
total += amount
|
||||
lines.append(f'{name}: ¥{amount}' + ('\n' + '\n'.join(fields) if fields else ''))
|
||||
lines.append(f'─────────────────')
|
||||
lines.append(f'合计: ¥{total}')
|
||||
return json.dumps({'widgettype':'Message','options':{'text':f'计费测试通过\n' + chr(10).join(lines)}}, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
return json.dumps({'widgettype':'Error','options':{'text':f'计费测试失败: {e}'}}, ensure_ascii=False)
|
||||
|
||||
return json.dumps({'widgettype':'Error','options':{'text':'无效的操作'}}, ensure_ascii=False)
|
||||
@ -1,39 +0,0 @@
|
||||
result = {'success': False, 'message': ''}
|
||||
action = params_kw.action
|
||||
try:
|
||||
dbname = get_module_dbname('llmage')
|
||||
record_id = params_kw.get('id')
|
||||
if not record_id:
|
||||
result['message'] = '缺少id'
|
||||
elif action not in ('published', 'unpublished'):
|
||||
result['message'] = '无效的状态值'
|
||||
else:
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
await sor.U('llm', {'id': record_id, 'status': action})
|
||||
result['success'] = True
|
||||
result['message'] = '上架成功' if action == 'published' else '下架成功'
|
||||
if action == 'published':
|
||||
# 同步必须放在事务块外:块内未提交时另一连接读到旧状态
|
||||
env = request._run_ns
|
||||
sync_fn = getattr(env, 'sync_llm_product', None)
|
||||
if sync_fn:
|
||||
try:
|
||||
sync_result = await sync_fn(record_id)
|
||||
except Exception as sync_e:
|
||||
sync_result = {'success': False, 'error': str(sync_e)}
|
||||
if not sync_result:
|
||||
result['message'] += ',已同步为产品'
|
||||
elif sync_result.get('success'):
|
||||
result['message'] += ',已同步为产品'
|
||||
else:
|
||||
result['message'] += ',但产品同步失败: ' + str(sync_result.get('error', '未知错误'))
|
||||
except Exception as e:
|
||||
result['message'] = str(e)
|
||||
|
||||
return {
|
||||
"widgettype": "Text",
|
||||
"options": {
|
||||
"otext": result['message'],
|
||||
"i18n": True
|
||||
}
|
||||
}
|
||||
@ -1,19 +0,0 @@
|
||||
|
||||
ns = params_kw.copy()
|
||||
|
||||
data = params_kw.copy()
|
||||
data.pop('page', None)
|
||||
data.pop('rows', None)
|
||||
data.pop('data_filter', None)
|
||||
|
||||
if not data.get('id'):
|
||||
return {'widgettype':'Error','options':{'title':'Update Error','message':'缺少id'}}
|
||||
|
||||
try:
|
||||
db = DBPools()
|
||||
dbname = get_module_dbname('llmage')
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
await sor.U('llm', data)
|
||||
return {'widgettype':'Message','options':{'title':'Update Success','message':'ok'}}
|
||||
except Exception as e:
|
||||
return {'widgettype':'Error','options':{'title':'Update Error','message':str(e)}}
|
||||
@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
|
||||
result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Invalid', 'type': 'error'}}
|
||||
|
||||
@ -25,4 +26,4 @@ try:
|
||||
except Exception as e:
|
||||
result['options'] = {'title': '错误', 'message': str(e), 'type': 'error'}
|
||||
|
||||
return result
|
||||
return json.dumps(result, ensure_ascii=False)
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
|
||||
result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Invalid', 'type': 'error'}}
|
||||
|
||||
@ -29,4 +30,4 @@ try:
|
||||
except Exception as e:
|
||||
result['options'] = {'title': '错误', 'message': str(e), 'type': 'error'}
|
||||
|
||||
return result
|
||||
return json.dumps(result, ensure_ascii=False)
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
|
||||
result = {'success': False, 'rows': [], 'total': 0}
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
|
||||
result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Invalid', 'type': 'error'}}
|
||||
|
||||
@ -27,4 +28,4 @@ try:
|
||||
except Exception as e:
|
||||
result['options'] = {'title': '错误', 'message': str(e), 'type': 'error'}
|
||||
|
||||
return result
|
||||
return json.dumps(result, ensure_ascii=False)
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
from appPublic.uniqueID import getID
|
||||
|
||||
result = {"widgettype":"Error","options":{"title":"操作失败","message":"Invalid request","cwidth":16,"cheight":9,"timeout":3}}
|
||||
result = {'success': False, 'message': ''}
|
||||
|
||||
try:
|
||||
dbname = get_module_dbname('llmage')
|
||||
@ -8,8 +10,9 @@ try:
|
||||
data = params_kw
|
||||
data['id'] = getID()
|
||||
await sor.C('llmusage_accounting_failed', data)
|
||||
result = {"widgettype":"Message","options":{"title":"创建成功","message":"ok","cwidth":16,"cheight":9,"timeout":3}}
|
||||
result['success'] = True
|
||||
result['message'] = '创建成功'
|
||||
except Exception as e:
|
||||
result["options"] = {"title":"操作失败","message":str(e),"cwidth":16,"cheight":9,"timeout":3}
|
||||
result['message'] = str(e)
|
||||
|
||||
return result
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
|
||||
result = {"widgettype":"Error","options":{"title":"操作失败","message":"Invalid request","cwidth":16,"cheight":9,"timeout":3}}
|
||||
result = {'success': False, 'message': ''}
|
||||
|
||||
try:
|
||||
dbname = get_module_dbname('llmage')
|
||||
@ -10,8 +11,9 @@ try:
|
||||
result['message'] = '缺少id参数'
|
||||
else:
|
||||
await sor.D('llmusage_accounting_failed', {'id': rid})
|
||||
result = {"widgettype":"Message","options":{"title":"删除成功","message":"ok","cwidth":16,"cheight":9,"timeout":3}}
|
||||
result['success'] = True
|
||||
result['message'] = '删除成功'
|
||||
except Exception as e:
|
||||
result["options"] = {"title":"操作失败","message":str(e),"cwidth":16,"cheight":9,"timeout":3}
|
||||
result['message'] = str(e)
|
||||
|
||||
return result
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
result = {"widgettype":"Error","options":{"title":"操作失败","message":"Invalid request","cwidth":16,"cheight":9,"timeout":3}}
|
||||
result = {'success': False, 'message': ''}
|
||||
|
||||
try:
|
||||
dbname = get_module_dbname('llmage')
|
||||
@ -15,8 +17,9 @@ try:
|
||||
if data.get('handled') == '1' and not data.get('handled_time'):
|
||||
data['handled_time'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
await sor.U('llmusage_accounting_failed', data)
|
||||
result = {"widgettype":"Message","options":{"title":"更新成功","message":"ok","cwidth":16,"cheight":9,"timeout":3}}
|
||||
result['success'] = True
|
||||
result['message'] = '更新成功'
|
||||
except Exception as e:
|
||||
result["options"] = {"title":"操作失败","message":str(e),"cwidth":16,"cheight":9,"timeout":3}
|
||||
result['message'] = str(e)
|
||||
|
||||
return result
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
from appPublic.uniqueID import getID
|
||||
|
||||
result = {"widgettype":"Error","options":{"title":"操作失败","message":"Invalid request","cwidth":16,"cheight":9,"timeout":3}}
|
||||
result = {'success': False, 'message': ''}
|
||||
|
||||
try:
|
||||
dbname = get_module_dbname('llmage')
|
||||
@ -8,8 +10,9 @@ try:
|
||||
data = params_kw
|
||||
data['id'] = getID()
|
||||
await sor.C('llmusage', data)
|
||||
result = {"widgettype":"Message","options":{"title":"创建成功","message":"ok","cwidth":16,"cheight":9,"timeout":3}}
|
||||
result['success'] = True
|
||||
result['message'] = '创建成功'
|
||||
except Exception as e:
|
||||
result["options"] = {"title":"操作失败","message":str(e),"cwidth":16,"cheight":9,"timeout":3}
|
||||
result['message'] = str(e)
|
||||
|
||||
return result
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
|
||||
result = {"widgettype":"Error","options":{"title":"操作失败","message":"Invalid request","cwidth":16,"cheight":9,"timeout":3}}
|
||||
result = {'success': False, 'message': ''}
|
||||
|
||||
try:
|
||||
dbname = get_module_dbname('llmage')
|
||||
@ -10,8 +11,9 @@ try:
|
||||
result['message'] = '缺少id参数'
|
||||
else:
|
||||
await sor.D('llmusage', {'id': rid})
|
||||
result = {"widgettype":"Message","options":{"title":"删除成功","message":"ok","cwidth":16,"cheight":9,"timeout":3}}
|
||||
result['success'] = True
|
||||
result['message'] = '删除成功'
|
||||
except Exception as e:
|
||||
result["options"] = {"title":"操作失败","message":str(e),"cwidth":16,"cheight":9,"timeout":3}
|
||||
result['message'] = str(e)
|
||||
|
||||
return result
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
|
||||
@ -1,2 +1,4 @@
|
||||
#!/usr/bin/env python3
|
||||
return {"widgettype":"Error","options":{"title":"操作失败","message":"历史数据为只读,不可新增","cwidth":16,"cheight":9,"timeout":3}}
|
||||
import json
|
||||
result = {'success': False, 'message': '历史数据为只读,不可新增'}
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
|
||||
@ -1,2 +1,4 @@
|
||||
#!/usr/bin/env python3
|
||||
return {"widgettype":"Error","options":{"title":"操作失败","message":"历史数据为只读,不可删除","cwidth":16,"cheight":9,"timeout":3}}
|
||||
import json
|
||||
result = {'success': False, 'message': '历史数据为只读,不可删除'}
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
|
||||
@ -1,2 +1,4 @@
|
||||
#!/usr/bin/env python3
|
||||
return {"widgettype":"Error","options":{"title":"操作失败","message":"历史数据为只读,不可修改","cwidth":16,"cheight":9,"timeout":3}}
|
||||
import json
|
||||
result = {'success': False, 'message': '历史数据为只读,不可修改'}
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
|
||||
@ -1,83 +0,0 @@
|
||||
result = {'success': False, 'rows': [], 'total': 0, 'page': 1, 'page_size': 50}
|
||||
|
||||
try:
|
||||
# 参数解析
|
||||
try:
|
||||
page = int(params_kw.get('page', 1))
|
||||
except (ValueError, TypeError):
|
||||
page = 1
|
||||
if page < 1:
|
||||
page = 1
|
||||
rows_per_page = int(params_kw.get('rows', params_kw.get('pagerows', 50)))
|
||||
offset = (page - 1) * rows_per_page
|
||||
sort_field = params_kw.get('sort', 'use_time desc')
|
||||
|
||||
# 构建 WHERE 条件(只支持常用筛选字段,手动拼接避免框架开销)
|
||||
conditions = ['1=1']
|
||||
ns = {}
|
||||
|
||||
llmid = params_kw.get('llmid')
|
||||
if llmid:
|
||||
conditions.append('llmid=${llmid}$')
|
||||
ns['llmid'] = llmid
|
||||
|
||||
status = params_kw.get('status')
|
||||
if status:
|
||||
conditions.append('status=${status}$')
|
||||
ns['status'] = status
|
||||
|
||||
accounting_status = params_kw.get('accounting_status')
|
||||
if accounting_status:
|
||||
conditions.append('accounting_status=${accounting_status}$')
|
||||
ns['accounting_status'] = accounting_status
|
||||
|
||||
userid = params_kw.get('userid')
|
||||
if userid:
|
||||
conditions.append('userid=${userid}$')
|
||||
ns['userid'] = userid
|
||||
|
||||
userorgid = params_kw.get('userorgid')
|
||||
if userorgid:
|
||||
conditions.append('userorgid=${userorgid}$')
|
||||
ns['userorgid'] = userorgid
|
||||
|
||||
start_date = params_kw.get('start_date')
|
||||
if start_date:
|
||||
conditions.append('use_date>=${start_date}$')
|
||||
ns['start_date'] = start_date
|
||||
|
||||
end_date = params_kw.get('end_date')
|
||||
if end_date:
|
||||
conditions.append('use_date<=${end_date}$')
|
||||
ns['end_date'] = end_date
|
||||
|
||||
where = 'WHERE ' + ' AND '.join(conditions)
|
||||
|
||||
# 列表字段(不含 usages/ioinfo 大 TEXT)
|
||||
select_fields = 'id, llmid, use_date, use_time, userid, transno, responsed_seconds, finish_seconds, status, taskid, amount, cost, userorgid, ownerid, accounting_status'
|
||||
|
||||
db = DBPools()
|
||||
dbname = get_module_dbname('llmage')
|
||||
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
# count
|
||||
count_recs = await sor.sqlExe(f'SELECT count(*) as cnt FROM llmusage {where}', ns)
|
||||
total = count_recs[0].cnt if count_recs else 0
|
||||
|
||||
# data
|
||||
rows = await sor.sqlExe(
|
||||
f'SELECT {select_fields} FROM llmusage {where} ORDER BY {sort_field} LIMIT {rows_per_page} OFFSET {offset}',
|
||||
ns
|
||||
)
|
||||
|
||||
result['success'] = True
|
||||
result['total'] = total
|
||||
result['rows'] = rows if rows else []
|
||||
result['page'] = page
|
||||
result['page_size'] = rows_per_page
|
||||
|
||||
except Exception as e:
|
||||
debug(f'llmusage_list error: {format_exc()}')
|
||||
result['error'] = str(e)
|
||||
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
|
||||
result = {"widgettype":"Error","options":{"title":"操作失败","message":"Invalid request","cwidth":16,"cheight":9,"timeout":3}}
|
||||
result = {'success': False, 'message': ''}
|
||||
|
||||
try:
|
||||
dbname = get_module_dbname('llmage')
|
||||
@ -12,8 +13,9 @@ try:
|
||||
else:
|
||||
data['id'] = rid
|
||||
await sor.U('llmusage', data)
|
||||
result = {"widgettype":"Message","options":{"title":"更新成功","message":"ok","cwidth":16,"cheight":9,"timeout":3}}
|
||||
result['success'] = True
|
||||
result['message'] = '更新成功'
|
||||
except Exception as e:
|
||||
result["options"] = {"title":"操作失败","message":str(e),"cwidth":16,"cheight":9,"timeout":3}
|
||||
result['message'] = str(e)
|
||||
|
||||
return result
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
|
||||
@ -1,8 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import json
|
||||
from llmage.utils import get_llmcatelogs
|
||||
|
||||
data = await get_llmcatelogs()
|
||||
rows = [{'id': r.id, 'name': r.name} for r in (data or [])]
|
||||
return json.dumps({'rows': rows}, ensure_ascii=False)
|
||||
@ -1,8 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import json
|
||||
from llmage.utils import get_llmproviders
|
||||
|
||||
data = await get_llmproviders()
|
||||
rows = [{'id': p.providerid, 'name': p.orgname} for p in (data or [])]
|
||||
return json.dumps({'rows': rows}, ensure_ascii=False)
|
||||
@ -1,3 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
result = {'success': False, 'message': ''}
|
||||
|
||||
try:
|
||||
@ -7,12 +11,14 @@ try:
|
||||
result['message'] = '缺少llmusageid参数'
|
||||
else:
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
# 1. 重置 llmusage 记账状态为 created,让后台循环重新处理
|
||||
await sor.U('llmusage', {
|
||||
'id': luid,
|
||||
'accounting_status': 'created'
|
||||
})
|
||||
|
||||
now = curDateString() + ' ' + timestampstr().split(' ')[1] if ' ' not in curDateString() else curDateString()
|
||||
|
||||
# 2. 更新失败记录:标记已处理,增加重试次数
|
||||
now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
await sor.execute("""
|
||||
UPDATE llmusage_accounting_failed
|
||||
SET handled = '1',
|
||||
@ -21,10 +27,10 @@ try:
|
||||
handled_note = CONCAT(IFNULL(handled_note, ''), '[', ${now}$, '] 触发重试; ')
|
||||
WHERE llmusageid = ${luid}$
|
||||
""", {'luid': luid, 'now': now})
|
||||
|
||||
|
||||
result['success'] = True
|
||||
result['message'] = '已重置为待记账状态,后台循环将重新处理'
|
||||
except Exception as e:
|
||||
result['message'] = str(e)
|
||||
|
||||
return json.dumps(result, ensure_ascii=False)
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
|
||||
@ -1,81 +0,0 @@
|
||||
record_id = params_kw.get('id', '')
|
||||
|
||||
llm_info = {}
|
||||
reason = '未找到记录'
|
||||
if record_id:
|
||||
dbname = get_module_dbname('llmage')
|
||||
sage_db = get_module_dbname('sage')
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
# 查询失败记录 + JOIN llm表获取模型详情
|
||||
sql = f"""
|
||||
SELECT f.*, l.name as llm_name, l.model as llm_model, l.upappid as llm_ppid,
|
||||
u.username as userid_text, o.orgname as userorgid_text
|
||||
FROM llmusage_accounting_failed f
|
||||
LEFT JOIN llm l ON f.llmid = l.id
|
||||
LEFT JOIN {sage_db}.users u ON f.userid = u.id
|
||||
LEFT JOIN {sage_db}.organization o ON f.userorgid = o.id
|
||||
WHERE f.id = ${{record_id}}$
|
||||
"""
|
||||
rows = await sor.sqlExe(sql, {'record_id': record_id})
|
||||
if rows:
|
||||
rec = dict(rows[0])
|
||||
llm_info = {
|
||||
'llmid': rec.get('llmid', ''),
|
||||
'llm_name': rec.get('llm_name', ''),
|
||||
'llm_model': rec.get('llm_model', ''),
|
||||
'llm_ppid': rec.get('llm_ppid', ''),
|
||||
'userid': rec.get('userid', ''),
|
||||
'username': rec.get('userid_text', ''),
|
||||
'orgid': rec.get('userorgid', ''),
|
||||
'orgname': rec.get('userorgid_text', ''),
|
||||
'use_time': str(rec.get('use_time', '')),
|
||||
'amount': str(rec.get('amount', '')),
|
||||
'failed_time': str(rec.get('failed_time', '')),
|
||||
'retry_count': str(rec.get('retry_count', '')),
|
||||
}
|
||||
reason = rec.get('failed_reason', '') or '(空)'
|
||||
|
||||
fields = [
|
||||
{'label': '模型ID (llmID)', 'value': llm_info.get('llmid', '')},
|
||||
{'label': '模型名称', 'value': llm_info.get('llm_name', '')},
|
||||
{'label': '模型标识 (model)', 'value': llm_info.get('llm_model', '')},
|
||||
{'label': '上位系统 (ppid)', 'value': llm_info.get('llm_ppid', '')},
|
||||
{'label': '用户', 'value': f"{llm_info.get('username', '')} ({llm_info.get('userid', '')})"},
|
||||
{'label': '机构', 'value': f"{llm_info.get('orgname', '')} ({llm_info.get('orgid', '')})"},
|
||||
{'label': '使用时间', 'value': llm_info.get('use_time', '')},
|
||||
{'label': '金额', 'value': llm_info.get('amount', '')},
|
||||
{'label': '失败时间', 'value': llm_info.get('failed_time', '')},
|
||||
{'label': '重试次数', 'value': llm_info.get('retry_count', '')},
|
||||
]
|
||||
|
||||
field_widgets = []
|
||||
for f in fields:
|
||||
if f['value']:
|
||||
field_widgets.append({
|
||||
"widgettype": "HBox",
|
||||
"options": {"padding": "4px 0"},
|
||||
"subwidgets": [
|
||||
{"widgettype": "Text", "options": {"text": f['label'] + ":", "cwidth": 12, "i18n": False, "css": "field-label"}},
|
||||
{"widgettype": "Text", "options": {"text": f['value'], "cwidth": 18, "i18n": False}}
|
||||
]
|
||||
})
|
||||
|
||||
field_widgets.append({
|
||||
"widgettype": "Text",
|
||||
"options": {"text": "───── 失败原因 ─────", "cwidth": 30, "i18n": False, "padding": "12px 0 4px 0"}
|
||||
})
|
||||
field_widgets.append({
|
||||
"widgettype": "Text",
|
||||
"options": {"text": reason, "i18n": False}
|
||||
})
|
||||
|
||||
return json.dumps({
|
||||
"widgettype": "VScrollPanel",
|
||||
"options": {
|
||||
"width": "100%",
|
||||
"height": "100%",
|
||||
"css": "card",
|
||||
"padding": "12px"
|
||||
},
|
||||
"subwidgets": field_widgets
|
||||
}, ensure_ascii=False)
|
||||
@ -1,15 +1,17 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
|
||||
result = []
|
||||
result = {'success': False, 'data': []}
|
||||
|
||||
try:
|
||||
dbname = get_module_dbname('llmage')
|
||||
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
rows = await sor.sqlExe("select name, path from uapi order by name", {})
|
||||
result = [{'value': r['name'], 'text': f"{r['name']} ({r['path']})"} for r in (rows or [])]
|
||||
result['data'] = [{'id': r['name'], 'text': f"{r['name']} ({r['path']})"} for r in (rows or [])]
|
||||
result['success'] = True
|
||||
|
||||
except Exception as e:
|
||||
pass
|
||||
result['error'] = str(e)
|
||||
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
|
||||
@ -1 +0,0 @@
|
||||
../docs/API.md
|
||||
@ -1,41 +0,0 @@
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"options": {
|
||||
"width": "100%",
|
||||
"height": "100%",
|
||||
"padding": "0"
|
||||
},
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "HBox",
|
||||
"options": {
|
||||
"width": "100%",
|
||||
"alignItems": "center",
|
||||
"marginBottom": "16px"
|
||||
},
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "Title2",
|
||||
"options": {
|
||||
"text": "大模型 API 文档"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"widgettype": "VScrollPanel",
|
||||
"options": {
|
||||
"css": "filler"
|
||||
},
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "MarkdownViewer",
|
||||
"options": {
|
||||
"md_url": "{{entire_url('/llmage/api_doc.md')}}",
|
||||
"width": "100%"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -1,31 +0,0 @@
|
||||
llmid = params_kw.get('llmid', '')
|
||||
|
||||
if not llmid:
|
||||
return json.dumps({
|
||||
"widgettype": "Text",
|
||||
"options": {"text": "❌ 日期与状态: 缺少llmid参数", "i18n": False}
|
||||
}, ensure_ascii=False)
|
||||
|
||||
async with get_sor_context(request._run_ns, 'llmage') as sor:
|
||||
recs = await sor.sqlExe(
|
||||
"select * from llm where id=${llmid}$", {'llmid': llmid})
|
||||
|
||||
if not recs:
|
||||
return json.dumps({
|
||||
"widgettype": "Text",
|
||||
"options": {"text": "❌ 日期与状态: 模型不存在", "i18n": False}
|
||||
}, ensure_ascii=False)
|
||||
|
||||
llm = recs[0]
|
||||
date_ok = bool(llm.enabled_date and llm.expired_date)
|
||||
status_ok = llm.status == 'published'
|
||||
|
||||
if date_ok and status_ok:
|
||||
text = f"✅ 日期与状态: 启用:{llm.enabled_date} 失效:{llm.expired_date} 状态:{llm.status}"
|
||||
else:
|
||||
text = f"❌ 日期与状态: 启用:{llm.enabled_date} 失效:{llm.expired_date} 状态:{llm.status}"
|
||||
|
||||
return json.dumps({
|
||||
"widgettype": "Text",
|
||||
"options": {"text": text, "i18n": False}
|
||||
}, ensure_ascii=False)
|
||||
@ -1,22 +0,0 @@
|
||||
llmid = params_kw.get('llmid', '')
|
||||
|
||||
if not llmid:
|
||||
return json.dumps({
|
||||
"widgettype": "Text",
|
||||
"options": {"text": "❌ 能力映射(llm_api_map): 缺少llmid参数", "i18n": False}
|
||||
}, ensure_ascii=False)
|
||||
|
||||
async with get_sor_context(request._run_ns, 'llmage') as sor:
|
||||
maps = await sor.sqlExe(
|
||||
"select * from llm_api_map where llmid=${llmid}$", {'llmid': llmid})
|
||||
|
||||
if maps:
|
||||
ppids = [m.ppid for m in maps if m.ppid]
|
||||
text = f"✅ 能力映射(llm_api_map): {len(maps)}条记录, {len(ppids)}个有定价"
|
||||
else:
|
||||
text = "❌ 能力映射(llm_api_map): 无映射记录"
|
||||
|
||||
return json.dumps({
|
||||
"widgettype": "Text",
|
||||
"options": {"text": text, "i18n": False}
|
||||
}, ensure_ascii=False)
|
||||
@ -1,22 +0,0 @@
|
||||
llmid = params_kw.get('llmid', '')
|
||||
|
||||
if not llmid:
|
||||
return json.dumps({
|
||||
"widgettype": "Text",
|
||||
"options": {"text": "❌ 模型记录: 缺少llmid参数", "i18n": False}
|
||||
}, ensure_ascii=False)
|
||||
|
||||
async with get_sor_context(request._run_ns, 'llmage') as sor:
|
||||
recs = await sor.sqlExe(
|
||||
"select * from llm where id=${llmid}$", {'llmid': llmid})
|
||||
|
||||
if recs:
|
||||
llm = recs[0]
|
||||
text = f"✅ 模型记录: {llm.name} ({llm.model})"
|
||||
else:
|
||||
text = f"❌ 模型记录: llm id={llmid} 不存在"
|
||||
|
||||
return json.dumps({
|
||||
"widgettype": "Text",
|
||||
"options": {"text": text, "i18n": False}
|
||||
}, ensure_ascii=False)
|
||||
@ -1,39 +0,0 @@
|
||||
from pricing.pricing import get_pricing_display
|
||||
|
||||
llmid = params_kw.get('llmid', '')
|
||||
|
||||
if not llmid:
|
||||
return json.dumps({
|
||||
"widgettype": "Text",
|
||||
"options": {"text": "❌ 定价数据: 缺少llmid参数", "i18n": False}
|
||||
}, ensure_ascii=False)
|
||||
|
||||
async with get_sor_context(request._run_ns, 'llmage') as sor:
|
||||
maps = await sor.sqlExe(
|
||||
"select * from llm_api_map where llmid=${llmid}$", {'llmid': llmid})
|
||||
ppids = [m.ppid for m in maps if m.ppid] if maps else []
|
||||
|
||||
if not ppids:
|
||||
text = "❌ 定价数据: 无定价项目"
|
||||
else:
|
||||
ppid = ppids[0]
|
||||
try:
|
||||
result = await get_pricing_display(ppid)
|
||||
if not result:
|
||||
text = "❌ 定价数据: 无当前生效的定价记录"
|
||||
elif result.get('display_text'):
|
||||
lines = [f"✅ 定价项目: {result.get('name', ppid)}"]
|
||||
if result.get('pricing_type'):
|
||||
lines.append(f" 类型: {result['pricing_type']}")
|
||||
lines.append("")
|
||||
lines.append(result['display_text'])
|
||||
text = '\n'.join(lines)
|
||||
else:
|
||||
text = f"✅ 定价项目: {result.get('name', ppid)} (无定价明细)"
|
||||
except Exception as e:
|
||||
text = f"❌ 定价数据: {e}"
|
||||
|
||||
return json.dumps({
|
||||
"widgettype": "Text",
|
||||
"options": {"text": text, "i18n": False}
|
||||
}, ensure_ascii=False)
|
||||
@ -1,33 +0,0 @@
|
||||
llmid = params_kw.get('llmid', '')
|
||||
|
||||
if not llmid:
|
||||
return json.dumps({
|
||||
"widgettype": "Text",
|
||||
"options": {"text": "❌ 定价项目(pricing_program): 缺少llmid参数", "i18n": False}
|
||||
}, ensure_ascii=False)
|
||||
|
||||
async with get_sor_context(request._run_ns, 'llmage') as sor:
|
||||
maps = await sor.sqlExe(
|
||||
"select * from llm_api_map where llmid=${llmid}$", {'llmid': llmid})
|
||||
ppids = [m.ppid for m in maps if m.ppid] if maps else []
|
||||
|
||||
if not ppids:
|
||||
text = "❌ 定价项目(pricing_program): llm_api_map中无ppid"
|
||||
else:
|
||||
ppid = ppids[0]
|
||||
async with get_sor_context(request._run_ns, 'pricing') as psor:
|
||||
pregs = await psor.sqlExe(
|
||||
"select * from pricing_program where id=${ppid}$", {'ppid': ppid})
|
||||
if pregs:
|
||||
p = pregs[0]
|
||||
display_name = getattr(p, 'display_text', '') or getattr(p, 'name', '')
|
||||
text = f"✅ 定价项目(pricing_program): {display_name}"
|
||||
if hasattr(p, 'id'):
|
||||
text += f" (id={p.id})"
|
||||
else:
|
||||
text = f"❌ 定价项目(pricing_program): ppid={ppid} 未找到"
|
||||
|
||||
return json.dumps({
|
||||
"widgettype": "Text",
|
||||
"options": {"text": text, "i18n": False}
|
||||
}, ensure_ascii=False)
|
||||
@ -1,30 +0,0 @@
|
||||
llmid = params_kw.get('llmid', '')
|
||||
|
||||
if not llmid:
|
||||
return json.dumps({
|
||||
"widgettype": "Text",
|
||||
"options": {"text": "❌ API映射(uapi): 缺少llmid参数", "i18n": False}
|
||||
}, ensure_ascii=False)
|
||||
|
||||
async with get_sor_context(request._run_ns, 'llmage') as sor:
|
||||
recs = await sor.sqlExe("""
|
||||
select a.*, e.ioid, e.stream
|
||||
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.id = e.upappid and m.apiname = e.name
|
||||
where a.id=${llmid}$""", {'llmid': llmid})
|
||||
|
||||
if recs:
|
||||
text = f"✅ API映射(uapi): ioid={recs[0].ioid}, stream={recs[0].stream}"
|
||||
else:
|
||||
# Get apiname from llm
|
||||
async with get_sor_context(request._run_ns, 'llmage') as sor:
|
||||
llm_recs = await sor.sqlExe("select apiname from llm where id=${llmid}$", {'llmid': llmid})
|
||||
apiname = llm_recs[0].apiname if llm_recs else 'N/A'
|
||||
text = f"❌ API映射(uapi): apiname={apiname} 在upapp中未找到"
|
||||
|
||||
return json.dumps({
|
||||
"widgettype": "Text",
|
||||
"options": {"text": text, "i18n": False}
|
||||
}, ensure_ascii=False)
|
||||
@ -1,33 +0,0 @@
|
||||
llmid = params_kw.get('llmid', '')
|
||||
|
||||
if not llmid:
|
||||
return json.dumps({
|
||||
"widgettype": "Text",
|
||||
"options": {"text": "❌ IO定义(uapiio): 缺少llmid参数", "i18n": False}
|
||||
}, ensure_ascii=False)
|
||||
|
||||
async with get_sor_context(request._run_ns, 'llmage') as sor:
|
||||
# First get ioid from uapi
|
||||
recs = await sor.sqlExe("""
|
||||
select e.ioid
|
||||
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.id = e.upappid and m.apiname = e.name
|
||||
where a.id=${llmid}$""", {'llmid': llmid})
|
||||
|
||||
if not recs:
|
||||
text = "❌ IO定义(uapiio): 依赖 uapi 未通过"
|
||||
else:
|
||||
ioid = recs[0].ioid
|
||||
recs2 = await sor.sqlExe(
|
||||
"select * from uapiio where id=${ioid}$", {'ioid': ioid})
|
||||
if recs2:
|
||||
text = f"✅ IO定义(uapiio): uapiio id={ioid}"
|
||||
else:
|
||||
text = f"❌ IO定义(uapiio): ioid={ioid} 未找到"
|
||||
|
||||
return json.dumps({
|
||||
"widgettype": "Text",
|
||||
"options": {"text": text, "i18n": False}
|
||||
}, ensure_ascii=False)
|
||||
@ -1,27 +0,0 @@
|
||||
llmid = params_kw.get('llmid', '')
|
||||
|
||||
if not llmid:
|
||||
return json.dumps({
|
||||
"widgettype": "Text",
|
||||
"options": {"text": "❌ 上位系统(upapp): 缺少llmid参数", "i18n": False}
|
||||
}, ensure_ascii=False)
|
||||
|
||||
async with get_sor_context(request._run_ns, 'llmage') as sor:
|
||||
recs = await sor.sqlExe(
|
||||
"select a.* from llm a, upapp b where a.id=${llmid}$ and a.upappid=b.id",
|
||||
{'llmid': llmid})
|
||||
|
||||
if recs:
|
||||
llm = recs[0]
|
||||
text = f"✅ 上位系统(upapp): upappid={llm.upappid}"
|
||||
else:
|
||||
# Get llm info to show upappid
|
||||
llm_recs = await sor.sqlExe(
|
||||
"select upappid from llm where id=${llmid}$", {'llmid': llmid})
|
||||
upappid = llm_recs[0].upappid if llm_recs else '未知'
|
||||
text = f"❌ 上位系统(upapp): upappid={upappid} 未找到关联"
|
||||
|
||||
return json.dumps({
|
||||
"widgettype": "Text",
|
||||
"options": {"text": text, "i18n": False}
|
||||
}, ensure_ascii=False)
|
||||
@ -1,188 +1,136 @@
|
||||
{
|
||||
"widgettype": "Tabular",
|
||||
"id": "failed_table",
|
||||
"widgettype": "VBox",
|
||||
"options": {
|
||||
"width": "100%",
|
||||
"height": "100%",
|
||||
"css": "card",
|
||||
"title": "记账失败记录",
|
||||
"toolbar": {
|
||||
"tools": [
|
||||
{
|
||||
"name": "filter",
|
||||
"label": "搜索"
|
||||
},
|
||||
{
|
||||
"name": "show_reason",
|
||||
"label": "原因",
|
||||
"selected_row": true
|
||||
},
|
||||
{
|
||||
"name": "retry_accounting",
|
||||
"label": "重试记账",
|
||||
"selected_row": true
|
||||
}
|
||||
]
|
||||
"padding": "16px",
|
||||
"spacing": 12
|
||||
},
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "Title2",
|
||||
"options": {
|
||||
"text": "记账失败记录",
|
||||
"halign": "left"
|
||||
}
|
||||
},
|
||||
"search_form": {
|
||||
"fields": [
|
||||
{
|
||||
"widgettype": "HBox",
|
||||
"options": {
|
||||
"width": "100%",
|
||||
"spacing": 12,
|
||||
"alignItems": "flex-end"
|
||||
},
|
||||
"subwidgets": [
|
||||
{
|
||||
"name": "start_date",
|
||||
"label": "开始日期",
|
||||
"uitype": "date",
|
||||
"cwidth": 10
|
||||
"widgettype": "VBox",
|
||||
"options": {"spacing": 4},
|
||||
"subwidgets": [
|
||||
{"widgettype": "Text", "options": {"text": "开始日期", "fontSize": "12px"}},
|
||||
{"widgettype": "UiDate", "id": "start_date", "options": {"width": "150px"}}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "end_date",
|
||||
"label": "结束日期",
|
||||
"uitype": "date",
|
||||
"cwidth": 10
|
||||
"widgettype": "VBox",
|
||||
"options": {"spacing": 4},
|
||||
"subwidgets": [
|
||||
{"widgettype": "Text", "options": {"text": "结束日期", "fontSize": "12px"}},
|
||||
{"widgettype": "UiDate", "id": "end_date", "options": {"width": "150px"}}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "handled",
|
||||
"label": "处理状态",
|
||||
"uitype": "code",
|
||||
"cwidth": 8,
|
||||
"data": [
|
||||
"widgettype": "VBox",
|
||||
"options": {"spacing": 4},
|
||||
"subwidgets": [
|
||||
{"widgettype": "Text", "options": {"text": "处理状态", "fontSize": "12px"}},
|
||||
{
|
||||
"value": "",
|
||||
"text": "全部"
|
||||
},
|
||||
"widgettype": "Combobox",
|
||||
"id": "handled_filter",
|
||||
"options": {
|
||||
"width": "120px",
|
||||
"data": [
|
||||
{"value": "", "text": "全部"},
|
||||
{"value": "0", "text": "未处理"},
|
||||
{"value": "1", "text": "已处理"}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"options": {"spacing": 4},
|
||||
"subwidgets": [
|
||||
{"widgettype": "Text", "options": {"text": "", "fontSize": "12px"}},
|
||||
{
|
||||
"value": "0",
|
||||
"text": "未处理"
|
||||
},
|
||||
"widgettype": "Button",
|
||||
"id": "search_btn",
|
||||
"options": {
|
||||
"label": "查询",
|
||||
"bgcolor": "#1976d2",
|
||||
"color": "#ffffff",
|
||||
"width": "80px"
|
||||
},
|
||||
"binds": [{
|
||||
"wid": "self",
|
||||
"event": "click",
|
||||
"actiontype": "script",
|
||||
"target": "failed_table",
|
||||
"script": "var sd = this.root.getElementById('start_date'); var ed = this.root.getElementById('end_date'); var hf = this.root.getElementById('handled_filter'); var params = {handled: hf.value}; if(sd.value) params.start_date = sd.value; if(ed.value) params.end_date = ed.value; this.root.getElementById('failed_table').load(params);"
|
||||
}]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"options": {"spacing": 4},
|
||||
"subwidgets": [
|
||||
{"widgettype": "Text", "options": {"text": "", "fontSize": "12px"}},
|
||||
{
|
||||
"value": "1",
|
||||
"text": "已处理"
|
||||
"widgettype": "Button",
|
||||
"id": "retry_btn",
|
||||
"options": {
|
||||
"label": "重试",
|
||||
"bgcolor": "#4caf50",
|
||||
"color": "#ffffff",
|
||||
"width": "80px"
|
||||
},
|
||||
"binds": [{
|
||||
"wid": "self",
|
||||
"event": "click",
|
||||
"actiontype": "script",
|
||||
"target": "self",
|
||||
"script": "var dv = this.root.getElementById('failed_table'); var row = dv.selected_row || (dv.selected_rows && dv.selected_rows[0]); if(!row || !row.llmusageid) { alert('请先选中一条记录'); return; } var url = bricks.build_url ? bricks.build_url('/llmage/api/retry_accounting.dspy') : '/llmage/api/retry_accounting.dspy'; fetch(url + '?id=' + row.llmusageid).then(function(r){return r.json();}).then(function(d){ if(d.success) { alert(d.message); dv.load({}); } else { alert('失败: ' + d.message); } }).catch(function(e){ alert('请求异常: ' + e); });"
|
||||
}]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"data_url": "{{entire_url('/llmage/api/failed_accounting_list.dspy')}}",
|
||||
"data_method": "GET",
|
||||
"page_rows": 20,
|
||||
"row_options": {
|
||||
"browserfields": {
|
||||
"exclouded": [
|
||||
"id",
|
||||
"failed_reason"
|
||||
],
|
||||
"alters": {}
|
||||
},
|
||||
"fields": [
|
||||
{
|
||||
"name": "llmusageid",
|
||||
"title": "使用记录ID",
|
||||
"type": "str",
|
||||
"length": 32,
|
||||
"cwidth": 12,
|
||||
"uitype": "str",
|
||||
"label": "使用记录ID"
|
||||
},
|
||||
{
|
||||
"name": "llmid_text",
|
||||
"title": "模型",
|
||||
"type": "str",
|
||||
"length": 32,
|
||||
"cwidth": 12,
|
||||
"uitype": "str",
|
||||
"label": "模型"
|
||||
},
|
||||
{
|
||||
"name": "userid_text",
|
||||
"title": "用户",
|
||||
"type": "str",
|
||||
"length": 32,
|
||||
"cwidth": 10,
|
||||
"uitype": "str",
|
||||
"label": "用户"
|
||||
},
|
||||
{
|
||||
"name": "userorgid_text",
|
||||
"title": "机构",
|
||||
"type": "str",
|
||||
"length": 32,
|
||||
"cwidth": 10,
|
||||
"uitype": "str",
|
||||
"label": "机构"
|
||||
},
|
||||
{
|
||||
"name": "use_time",
|
||||
"title": "使用时间",
|
||||
"type": "timestamp",
|
||||
"cwidth": 14,
|
||||
"uitype": "str",
|
||||
"label": "使用时间"
|
||||
},
|
||||
{
|
||||
"name": "amount",
|
||||
"title": "金额",
|
||||
"type": "double",
|
||||
"length": 18,
|
||||
"dec": 5,
|
||||
"cwidth": 8,
|
||||
"uitype": "float",
|
||||
"label": "金额"
|
||||
},
|
||||
{
|
||||
"name": "failed_reason",
|
||||
"title": "失败原因",
|
||||
"type": "text",
|
||||
"cwidth": 20,
|
||||
"uitype": "text",
|
||||
"label": "失败原因"
|
||||
},
|
||||
{
|
||||
"name": "failed_time",
|
||||
"title": "失败时间",
|
||||
"type": "timestamp",
|
||||
"cwidth": 14,
|
||||
"uitype": "str",
|
||||
"label": "失败时间"
|
||||
},
|
||||
{
|
||||
"name": "retry_count",
|
||||
"title": "重试",
|
||||
"type": "int",
|
||||
"cwidth": 4,
|
||||
"uitype": "int",
|
||||
"label": "重试"
|
||||
},
|
||||
{
|
||||
"name": "handled_text",
|
||||
"title": "状态",
|
||||
"type": "str",
|
||||
"length": 1,
|
||||
"cwidth": 6,
|
||||
"uitype": "str",
|
||||
"label": "状态"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"binds": [
|
||||
{
|
||||
"wid": "self",
|
||||
"event": "show_reason",
|
||||
"actiontype": "urlwidget",
|
||||
"target": "PopupWindow",
|
||||
"popup_options": {
|
||||
"title": "失败原因",
|
||||
"cwidth": 30,
|
||||
"cheight": 20
|
||||
},
|
||||
"widgettype": "DataViewer",
|
||||
"id": "failed_table",
|
||||
"options": {
|
||||
"url": "{{entire_url('/llmage/api/show_failed_reason.dspy')}}?id=${id}$"
|
||||
"url": "{{entire_url('/llmage/api/failed_accounting_list.dspy')}}",
|
||||
"title": "失败记录列表",
|
||||
"pageSize": 20,
|
||||
"fields": [
|
||||
{"name": "id", "title": "ID", "hidden": true},
|
||||
{"name": "llmusageid", "title": "使用记录ID", "width": "120px"},
|
||||
{"name": "llmid", "title": "模型ID", "width": "120px"},
|
||||
{"name": "userid", "title": "用户ID", "width": "120px"},
|
||||
{"name": "userorgid", "title": "机构ID", "width": "120px"},
|
||||
{"name": "use_date", "title": "使用日期", "width": "110px"},
|
||||
{"name": "use_time", "title": "使用时间", "width": "160px"},
|
||||
{"name": "amount", "title": "金额", "width": "80px"},
|
||||
{"name": "cost", "title": "成本", "width": "80px"},
|
||||
{"name": "failed_reason", "title": "失败原因", "width": "30%"},
|
||||
{"name": "failed_time", "title": "失败时间", "width": "160px"},
|
||||
{"name": "retry_count", "title": "重试次数", "width": "80px"},
|
||||
{"name": "handled", "title": "状态", "width": "80px",
|
||||
"formatter": "function(v){return v==='1'?'已处理':'未处理';}"}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"wid": "self",
|
||||
"event": "retry_accounting",
|
||||
"actiontype": "script",
|
||||
"target": "self",
|
||||
"script": "var dv = bricks.getWidgetById('failed_table', bricks.app.root); if(!dv || !dv.select_row || !dv.select_row.user_data) { alert('请先选中一条记录'); return; } var row = dv.select_row.user_data; if(!row.llmusageid) { alert('记录缺少llmusageid'); return; } try { var resp = await fetch('{{entire_url('/llmage/api/retry_accounting.dspy')}}?id=' + encodeURIComponent(row.llmusageid), { credentials: 'include' }); var d = await resp.json(); if(d.success) { alert(d.message); await dv.render({}); } else { alert('失败: ' + d.message); } } catch(e) { alert('请求失败: ' + e.message); }"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,23 +1,9 @@
|
||||
userid = await get_user()
|
||||
llmcatelogid = params_kw.get('llmcatelogid', 't2t')
|
||||
tasks = await get_today_asynctask_list(userid)
|
||||
|
||||
async with get_sor_context(request._run_ns, 'llmage') as sor:
|
||||
for t in tasks:
|
||||
bin = await read_webpath(t.ioinfo)
|
||||
t.ioinfo = json.loads(bin.decode('utf-8'))
|
||||
|
||||
# 查询 llmcatelogid
|
||||
catid = None
|
||||
if hasattr(t, 'llmid') and t.llmid:
|
||||
sql = '''select m.llmcatelogid from llm_api_map m where m.llmid = ${llmid}$ limit 1'''
|
||||
recs = await sor.sqlExe(sql, {'llmid': t.llmid})
|
||||
if recs:
|
||||
catid = recs[0].llmcatelogid
|
||||
t.llmcatelogid = catid
|
||||
|
||||
# 按 llmcatelogid 过滤
|
||||
tasks = [t for t in tasks if t.llmcatelogid == llmcatelogid]
|
||||
for t in tasks:
|
||||
bin = await read_webpath(t.ioinfo)
|
||||
t.ioinfo = json.loads(bin.decode('utf-8'))
|
||||
|
||||
return {
|
||||
'status': 'ok',
|
||||
|
||||
@ -1,36 +1,32 @@
|
||||
lt = params_kw.llmcatelogid or 't2v'
|
||||
debug(f'{lt=}')
|
||||
try:
|
||||
async with get_sor_context(request._run_ns, 'llmage') as sor:
|
||||
sql = '''select distinct a.*, e.input_fields from llm a
|
||||
join llm_api_map m on a.id = m.llmid
|
||||
join llmcatelog b on m.llmcatelogid = b.id
|
||||
join uapi d on d.upappid = a.upappid and m.apiname = d.name
|
||||
join uapiio e on d.ioid = e.id
|
||||
where (b.id=${lt}$ OR b.name=${lt}$)
|
||||
and a.enabled_date <= ${biz_date}$
|
||||
and ${biz_date}$ < a.expired_date
|
||||
and a.status = 'published'
|
||||
and m.ppid is not NULL'''
|
||||
biz_date = await get_business_date(sor)
|
||||
recs = await sor.sqlExe(sql, {
|
||||
'biz_date': biz_date,
|
||||
'lt': lt
|
||||
})
|
||||
for r in recs:
|
||||
r.input_fields = json.loads(r.input_fields)
|
||||
return {
|
||||
'status': 'ok',
|
||||
'data': recs
|
||||
}
|
||||
|
||||
lt = '文生视频'
|
||||
if params_kw.type in ['文生视频', '参考生视频', '图生视频']:
|
||||
lt = params_kw.type
|
||||
async with get_sor_context(request._run_ns, 'llmage') as sor:
|
||||
sql = '''select distinct a.*, e.input_fields from llm a
|
||||
join llm_api_map m on a.id = m.llmid
|
||||
join llmcatelog b on m.llmcatelogid = b.id
|
||||
join upapp c on a.upappid = c.id
|
||||
join uapi d on c.apisetid = d.apisetid and a.apiname = d.name
|
||||
join uapiio e on d.ioid = e.id
|
||||
where b.name=${lt}$
|
||||
and a.enabled_date <= ${biz_date}$
|
||||
and ${biz_date}$ < a.expired_date
|
||||
and ppid is not NULL'''
|
||||
biz_date = await get_business_date(sor)
|
||||
recs = await sor.sqlExe(sql, {
|
||||
'biz_date': biz_date,
|
||||
'lt': lt
|
||||
})
|
||||
for r in recs:
|
||||
r.input_fields = json.loads(r.input_fields)
|
||||
return {
|
||||
'status': 'error',
|
||||
'data':{
|
||||
'message': 'server error'
|
||||
}
|
||||
'status': 'ok',
|
||||
'data': recs
|
||||
}
|
||||
except Exception as e:
|
||||
debug(f'{lt=},{e},{format_exc()}')
|
||||
|
||||
|
||||
|
||||
return {
|
||||
'status': 'error',
|
||||
'data':{
|
||||
'message': 'server error'
|
||||
}
|
||||
}
|
||||
|
||||
329
wwwroot/index.ui
329
wwwroot/index.ui
@ -17,7 +17,9 @@
|
||||
{
|
||||
"widgettype": "Title2",
|
||||
"options": {
|
||||
"text": "LLM 模型管理"
|
||||
"text": "LLM 模型管理",
|
||||
"color": "#F1F5F9",
|
||||
"fontWeight": "700"
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -26,176 +28,215 @@
|
||||
{
|
||||
"widgettype": "Text",
|
||||
"options": {
|
||||
"text": "模型类型、模型配置与记账失败记录",
|
||||
"cfontsize": 1.2
|
||||
"text": "模型配置、目录分类与调用监控",
|
||||
"fontSize": "14px",
|
||||
"color": "#64748B"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"widgettype": "ResponsableBox",
|
||||
"options": {
|
||||
"css": "filler",
|
||||
"spacing": 16
|
||||
"gap": "16px",
|
||||
"minWidth": "200px",
|
||||
"marginBottom": "24px"
|
||||
},
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "ResponsableBox",
|
||||
"widgettype": "urlwidget",
|
||||
"options": {
|
||||
"gap": "16px",
|
||||
"minWidth": "250px"
|
||||
"url": "{{entire_url('/llmage/stat_total_models.ui')}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"widgettype": "urlwidget",
|
||||
"options": {
|
||||
"url": "{{entire_url('/llmage/stat_today_calls.ui')}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"widgettype": "urlwidget",
|
||||
"options": {
|
||||
"url": "{{entire_url('/llmage/stat_today_amount.ui')}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"widgettype": "urlwidget",
|
||||
"options": {
|
||||
"url": "{{entire_url('/llmage/stat_catelog_count.ui')}}"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"widgettype": "ResponsableBox",
|
||||
"options": {
|
||||
"gap": "16px",
|
||||
"minWidth": "250px",
|
||||
"marginBottom": "24px"
|
||||
},
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"options": {
|
||||
"bgcolor": "#1E293B",
|
||||
"padding": "24px",
|
||||
"borderRadius": "12px",
|
||||
"border": "1px solid #334155",
|
||||
"cursor": "pointer"
|
||||
},
|
||||
"binds": [
|
||||
{
|
||||
"wid": "self",
|
||||
"event": "click",
|
||||
"actiontype": "urlwidget",
|
||||
"target": "app.llmage_content",
|
||||
"options": {
|
||||
"url": "{{entire_url('/llmage/llmcatelog_list.ui')}}"
|
||||
},
|
||||
"mode": "replace"
|
||||
}
|
||||
],
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"widgettype": "Svg",
|
||||
"options": {
|
||||
"css": "card",
|
||||
"cwidth": 23,
|
||||
"padding": "16px",
|
||||
"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": "<svg width=\"28\" height=\"28\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#3B82F6\" stroke-width=\"2\"><path d=\"M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z\"/></svg>",
|
||||
"width": "28px",
|
||||
"height": "28px"
|
||||
}
|
||||
},
|
||||
{
|
||||
"widgettype": "Title4",
|
||||
"options": {
|
||||
"text": "模型类型管理",
|
||||
"marginTop": "8px"
|
||||
}
|
||||
},
|
||||
{
|
||||
"widgettype": "Text",
|
||||
"options": {
|
||||
"text": "管理模型的分类和类型",
|
||||
"cfontsize": 1.2
|
||||
}
|
||||
}
|
||||
]
|
||||
"svg": "<svg width=\"36\" height=\"36\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#90caf9\" stroke-width=\"1.5\"><path d=\"M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z\"/></svg>",
|
||||
"width": "36px",
|
||||
"height": "36px",
|
||||
"marginBottom": "16px"
|
||||
}
|
||||
},
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"widgettype": "Title4",
|
||||
"options": {
|
||||
"css": "card",
|
||||
"cwidth": 23,
|
||||
"padding": "16px",
|
||||
"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": "<svg width=\"28\" height=\"28\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#22C55E\" stroke-width=\"2\"><path d=\"M9.75 3.104v5.714a2.25 2.25 0 01-.659 1.591L5 14.5M9.75 3.104c-.251.023-.501.05-.75.082m.75-.082a24.301 24.301 0 014.5 0m0 0v5.714c0 .597.237 1.17.659 1.591L19.8 15.3M14.25 3.104c.251.023.501.05.75.082M19.8 15.3l-1.57.393A9.065 9.065 0 0112 15.75c-2.062 0-4.024-.614-5.67-1.757l-1.57-.393m15.04 0L12 21 5.25 13.893\"/></svg>",
|
||||
"width": "28px",
|
||||
"height": "28px"
|
||||
}
|
||||
},
|
||||
{
|
||||
"widgettype": "Title4",
|
||||
"options": {
|
||||
"text": "模型管理",
|
||||
"marginTop": "8px"
|
||||
}
|
||||
},
|
||||
{
|
||||
"widgettype": "Text",
|
||||
"options": {
|
||||
"text": "管理 LLM 模型配置",
|
||||
"cfontsize": 1.2
|
||||
}
|
||||
}
|
||||
]
|
||||
"text": "模型类型管理",
|
||||
"color": "#F1F5F9",
|
||||
"fontWeight": "600",
|
||||
"marginBottom": "8px"
|
||||
}
|
||||
},
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"widgettype": "Text",
|
||||
"options": {
|
||||
"css": "card",
|
||||
"cwidth": 23,
|
||||
"padding": "16px",
|
||||
"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": "<svg width=\"28\" height=\"28\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#EF4444\" stroke-width=\"2\"><path d=\"M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z\"/></svg>",
|
||||
"width": "28px",
|
||||
"height": "28px"
|
||||
}
|
||||
},
|
||||
{
|
||||
"widgettype": "Title4",
|
||||
"options": {
|
||||
"text": "记账失败记录",
|
||||
"marginTop": "8px"
|
||||
}
|
||||
},
|
||||
{
|
||||
"widgettype": "Text",
|
||||
"options": {
|
||||
"text": "查看和检索记账失败的记录",
|
||||
"cfontsize": 1.2
|
||||
}
|
||||
}
|
||||
]
|
||||
"text": "管理模型的分类目录和类型定义",
|
||||
"fontSize": "14px",
|
||||
"color": "#94A3B8"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"widgettype": "VScrollPanel",
|
||||
"id": "llmage_content",
|
||||
"widgettype": "VBox",
|
||||
"options": {
|
||||
"css": "filler",
|
||||
"width": "100%",
|
||||
"height": "100%"
|
||||
}
|
||||
"bgcolor": "#1E293B",
|
||||
"padding": "24px",
|
||||
"borderRadius": "12px",
|
||||
"border": "1px solid #334155",
|
||||
"cursor": "pointer"
|
||||
},
|
||||
"binds": [
|
||||
{
|
||||
"wid": "self",
|
||||
"event": "click",
|
||||
"actiontype": "urlwidget",
|
||||
"target": "app.llmage_content",
|
||||
"options": {
|
||||
"url": "{{entire_url('/llmage/llm')}}"
|
||||
},
|
||||
"mode": "replace"
|
||||
}
|
||||
],
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "Svg",
|
||||
"options": {
|
||||
"svg": "<svg width=\"36\" height=\"36\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#4caf50\" stroke-width=\"1.5\"><path d=\"M9.75 3.104v5.714a2.25 2.25 0 01-.659 1.591L5 14.5M9.75 3.104c-.251.023-.501.05-.75.082m.75-.082a24.301 24.301 0 014.5 0m0 0v5.714c0 .597.237 1.17.659 1.591L19.8 15.3M14.25 3.104c.251.023.501.05.75.082M19.8 15.3l-1.57.393A9.065 9.065 0 0112 15.75c-2.062 0-4.024-.614-5.67-1.757l-1.57-.393m15.04 0L12 21 5.25 13.893\"/></svg>",
|
||||
"width": "36px",
|
||||
"height": "36px",
|
||||
"marginBottom": "16px"
|
||||
}
|
||||
},
|
||||
{
|
||||
"widgettype": "Title4",
|
||||
"options": {
|
||||
"text": "模型配置",
|
||||
"color": "#F1F5F9",
|
||||
"fontWeight": "600",
|
||||
"marginBottom": "8px"
|
||||
}
|
||||
},
|
||||
{
|
||||
"widgettype": "Text",
|
||||
"options": {
|
||||
"text": "管理 LLM 模型的API配置与供应商映射",
|
||||
"fontSize": "14px",
|
||||
"color": "#94A3B8"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"options": {
|
||||
"bgcolor": "#1E293B",
|
||||
"padding": "24px",
|
||||
"borderRadius": "12px",
|
||||
"border": "1px solid #334155",
|
||||
"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": "Svg",
|
||||
"options": {
|
||||
"svg": "<svg width=\"36\" height=\"36\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#EF4444\" stroke-width=\"1.5\"><path d=\"M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z\"/></svg>",
|
||||
"width": "36px",
|
||||
"height": "36px",
|
||||
"marginBottom": "16px"
|
||||
}
|
||||
},
|
||||
{
|
||||
"widgettype": "Title4",
|
||||
"options": {
|
||||
"text": "记账失败记录",
|
||||
"color": "#F1F5F9",
|
||||
"fontWeight": "600",
|
||||
"marginBottom": "8px"
|
||||
}
|
||||
},
|
||||
{
|
||||
"widgettype": "Text",
|
||||
"options": {
|
||||
"text": "查看和检索调用计费失败记录",
|
||||
"fontSize": "14px",
|
||||
"color": "#94A3B8"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"id": "llmage_content",
|
||||
"options": {
|
||||
"width": "100%",
|
||||
"flex": "1",
|
||||
"marginTop": "20px"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user