Compare commits

..

No commits in common. "main" and "feat/dataviz-llmage" have entirely different histories.

99 changed files with 785 additions and 6324 deletions

10
.gitignore vendored
View File

@ -1,10 +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/

View File

@ -278,74 +278,6 @@ tasks = await get_today_asynctask_list(userid)
await query_task_status(request, luid, onetime=False) 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所有已登录用户仅返回当前登录用户自己的记录。
--- ---
## 前端页面 ## 前端页面

View File

@ -1,801 +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_url` | 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`(非 `image_url`),传入图片 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_url` | 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`。后台定时任务会定期执行计费流程。

View File

@ -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定价 |

View File

@ -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

View File

@ -1,127 +0,0 @@
模型管理: モデル管理
模型名称: モデル名
模型编码: モデルコード
模型类型: モデルタイプ
模型提供商: モデルプロバイダー
模型版本: モデルバージョン
模型状态: モデル状態
API接口: APIインターフェース
API密钥: APIキー
API地址: APIエンドポイント
最大Token: 最大トークン
输入价格: 入力価格
输出价格: 出力価格
折扣: 割引
启用: 有効化
停用: 無効化
已启用: 有効
已停用: 無効
新增模型: モデル追加
编辑模型: モデル編集
删除模型: モデル削除
测试模型: モデルテスト
模型分组: モデルグループ
分组名称: グループ名
分组描述: グループ説明
新增分组: グループ追加
编辑分组: グループ編集
删除分组: グループ削除
使用统计: 使用統計
调用次数: 呼び出し回数
成功次数: 成功回数
失败次数: 失敗回数
Token用量: トークン使用量
输入Token: 入力トークン
输出Token: 出力トークン
总Token: 合計トークン
费用统计: コスト統計
总费用: 合計コスト
本月费用: 今月コスト
今日费用: 今日コスト
按模型统计: モデル別統計
按用户统计: ユーザー別統計
按日期统计: 日付別統計
趋势图: トレンドチャート
日: 日
周: 週
月: 月
年: 年
用户管理: ユーザー管理
用户名称: ユーザー名
用户Token配额: ユーザートークンクォータ
已使用: 使用済み
剩余配额: 残りクォータ
配额重置: クォータリセット
模型映射: モデルマッピング
映射名称: マッピング名
源模型: ソースモデル
目标模型: ターゲットモデル
映射状态: マッピング状態
新增映射: マッピング追加
编辑映射: マッピング編集
删除映射: マッピング削除
密钥管理: キー管理
密钥名称: キー名
密钥值: キー値
密钥状态: キー状態
新增密钥: キー追加
编辑密钥: キー編集
删除密钥: キー削除
日志: ログ
请求日志: リクエストログ
错误日志: エラーログ
请求时间: リクエスト時間
响应时间: レスポンス時間
耗时: 所要時間
状态码: ステータスコード
错误信息: エラーメッセージ
请求参数: リクエストパラメータ
响应内容: レスポンス内容
供应商: ベンダー
所属机构: 所属組織
定价项目: 価格設定項目
定价属于: 価格設定帰属
供应商折扣: ベンダー割引
描述: 説明
规格明细: 仕様詳細
项目名称: 項目名
模型: モデル
API: API
定价: 価格設定
时序: 時系列
开始日期: 開始日
结束日期: 終了日
生效日期: 有効開始日
失效日期: 有効終了日
定价数据: 価格設定データ
定价项目时序: 価格設定項目時系列
测试: テスト
定价测试: 価格設定テスト
新增: 新規追加
保存: 保存
取消: キャンセル
确认: 確認
删除: 削除
编辑: 編集
查看: 表示
导出: エクスポート
打印: 印刷
刷新: 更新
返回: 戻る
提交: 送信
重置: リセット
Conform: 確認
Discard: 破棄
Submit: 送信
Reset: リセット
Cancel: キャンセル
搜索: 検索
操作: 操作
类型: タイプ
状态: ステータス
名称: 名前
编码: コード
备注: 備考
创建时间: 作成日時
更新时间: 更新日時
全部: 全部

View File

@ -1,127 +0,0 @@
模型管理: 모델 관리
模型名称: 모델 이름
模型编码: 모델 코드
模型类型: 모델 유형
模型提供商: 모델 제공자
模型版本: 모델 버전
模型状态: 모델 상태
API接口: API 인터페이스
API密钥: API 키
API地址: API 엔드포인트
最大Token: 최대 토큰
输入价格: 입력 가격
输出价格: 출력 가격
折扣: 할인
启用: 활성화
停用: 비활성화
已启用: 활성화됨
已停用: 비활성화됨
新增模型: 모델 추가
编辑模型: 모델 편집
删除模型: 모델 삭제
测试模型: 모델 테스트
模型分组: 모델 그룹
分组名称: 그룹 이름
分组描述: 그룹 설명
新增分组: 그룹 추가
编辑分组: 그룹 편집
删除分组: 그룹 삭제
使用统计: 사용 통계
调用次数: 호출 횟수
成功次数: 성공 횟수
失败次数: 실패 횟수
Token用量: 토큰 사용량
输入Token: 입력 토큰
输出Token: 출력 토큰
总Token: 총 토큰
费用统计: 비용 통계
总费用: 총 비용
本月费用: 이번 달 비용
今日费用: 오늘 비용
按模型统计: 모델별 통계
按用户统计: 사용자별 통계
按日期统计: 날짜별 통계
趋势图: 추세 차트
日: 일
周: 주
月: 월
年: 년
用户管理: 사용자 관리
用户名称: 사용자 이름
用户Token配额: 사용자 토큰 쿼터
已使用: 사용됨
剩余配额: 잔여 쿼터
配额重置: 쿼터 초기화
模型映射: 모델 매핑
映射名称: 매핑 이름
源模型: 소스 모델
目标模型: 대상 모델
映射状态: 매핑 상태
新增映射: 매핑 추가
编辑映射: 매핑 편집
删除映射: 매핑 삭제
密钥管理: 키 관리
密钥名称: 키 이름
密钥值: 키 값
密钥状态: 키 상태
新增密钥: 키 추가
编辑密钥: 키 편집
删除密钥: 키 삭제
日志: 로그
请求日志: 요청 로그
错误日志: 오류 로그
请求时间: 요청 시간
响应时间: 응답 시간
耗时: 소요 시간
状态码: 상태 코드
错误信息: 오류 메시지
请求参数: 요청 파라미터
响应内容: 응답 내용
供应商: 공급업체
所属机构: 소속 기관
定价项目: 가격 항목
定价属于: 가격 귀속
供应商折扣: 공급업체 할인
描述: 설명
规格明细: 규격 상세
项目名称: 항목 이름
模型: 모델
API: API
定价: 가격
时序: 시계열
开始日期: 시작 날짜
结束日期: 종료 날짜
生效日期: 시작일
失效日期: 만료일
定价数据: 가격 데이터
定价项目时序: 가격 항목 시계열
测试: 테스트
定价测试: 가격 테스트
新增: 추가
保存: 저장
取消: 취소
确认: 확인
删除: 삭제
编辑: 편집
查看: 조회
导出: 내보내기
打印: 인쇄
刷新: 새로고침
返回: 뒤로
提交: 제출
重置: 초기화
Conform: 확인
Discard: 폐기
Submit: 제출
Reset: 초기화
Cancel: 취소
搜索: 검색
操作: 작업
类型: 유형
状态: 상태
名称: 이름
编码: 코드
备注: 비고
创建时间: 생성 시간
更新时间: 업데이트 시간
全部: 전체

View File

@ -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映射已存在
说明: 说明
账户余额不够: 账户余额不够
选择分类: 选择分类
重试: 重试
重试次数: 重试次数
重试记账: 重试记账
金额: 金额
错误: 错误
间隔(秒): 间隔(秒)

View File

@ -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": "是"}
]
}

View File

@ -4,59 +4,21 @@
"params": { "params": {
"sortby":"model", "sortby":"model",
"logined_userorgid": "ownerid", "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": { "browserfields": {
"exclouded": ["id", "ownerid"], "exclouded": ["id", "ownerid"],
"alters": { "alters": {
"ppid":{ "ppid":{
"dataurl":"{{entire_url('/pricing/get_all_pricing_programs.dspy')}}", "dataurl":"{{entire_url('/pricing/get_all_pricing_programs.dspy')}}",
"textField": "name", "textField": "name",
"valueField": "id" "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": { "toolbar": {
"tools":[ "tools":[
{ {
"name":"launch_check", "name":"test",
"label":"上线检查", "label":"体验",
"selected_row":true
},
{
"name":"publish",
"label":"上架",
"selected_row":true
},
{
"name":"unpublish",
"label":"下架",
"selected_row":true "selected_row":true
} }
] ]
@ -64,66 +26,25 @@
"binds":[ "binds":[
{ {
"wid":"self", "wid":"self",
"event":"launch_check", "event":"test",
"actiontype":"urlwidget", "actiontype":"urlwidget",
"target":"PopupWindow", "target":"PopupWindow",
"popup_options":{ "popup_options":{
"title":"上线检查", "title":"model Test",
"cwidth":25, "cwidth":22,
"cheight":20 "height":"75%"
}, },
"options":{ "options":{
"url":"{{entire_url('./llm_launch_check.ui')}}", "url":"{{entire_url('./llm_dialog.ui')}}",
"params":{ "params":{
"id":"${id}" "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": [ "editexclouded": [
"id", "ownerid" "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":[ "subtables":[
{ {
"field":"llmid", "field":"llmid",

View File

@ -5,18 +5,6 @@
"browserfields": { "browserfields": {
"exclouded": ["id", "llmid"], "exclouded": ["id", "llmid"],
"alters": { "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"] "editexclouded": ["id", "llmid"]

View File

@ -9,7 +9,6 @@
}, },
"editexclouded": ["id"], "editexclouded": ["id"],
"editable": { "editable": {
"get_data_url": "{{entire_url(get_llmusage.dspy')}}?pagerows=50",
"new_data_url": "{{entire_url('../api/llmusage_create.dspy')}}", "new_data_url": "{{entire_url('../api/llmusage_create.dspy')}}",
"update_data_url": "{{entire_url('../api/llmusage_update.dspy')}}", "update_data_url": "{{entire_url('../api/llmusage_update.dspy')}}",
"delete_data_url": "{{entire_url('../api/llmusage_delete.dspy')}}" "delete_data_url": "{{entire_url('../api/llmusage_delete.dspy')}}"

View File

@ -3,34 +3,17 @@
"title": "记账失败记录", "title": "记账失败记录",
"params": { "params": {
"sortby": "failed_time desc", "sortby": "failed_time desc",
"toolbar": { "browserfields": {
"tools": [ "exclouded": ["id"],
{ "alters": {
"name": "show_reason", "handled": {
"label": "原因", "uitype": "code",
"selected_row": true "data": [
} {"value": "0", "text": "未处理"},
] {"value": "1", "text": "已处理"}
}, ]
"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"],
"alters": {}
}, },
"editexclouded": ["id", "llmusageid", "failed_time"], "editexclouded": ["id", "llmusageid", "failed_time"],
"editable": { "editable": {

View File

@ -41,7 +41,7 @@ async def checkCustomerBalance(llmid, userid, userorgid, catelogid=None):
debug(f'checkCustomerBalance(): llmid is None') debug(f'checkCustomerBalance(): llmid is None')
return False return False
env = ServerEnv() env = ServerEnv()
llm = await get_llmage_llm(llmid) llm = await get_llm(llmid)
if llm.ownerid == userorgid: if llm.ownerid == userorgid:
debug(f'self orgid user') debug(f'self orgid user')
return True return True
@ -64,34 +64,37 @@ async def checkCustomerBalance(llmid, userid, userorgid, catelogid=None):
async def llm_accounting(llmusage): async def llm_accounting(llmusage):
env = ServerEnv() env = ServerEnv()
llmid = llmusage.llmid 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
async with get_sor_context(env, 'llmage') as sor: 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) biz_date = await env.get_business_date(sor)
timestamp = env.timestampstr() timestamp = env.timestampstr()
orderid = getID() orderid = getID()
@ -160,7 +163,7 @@ async def get_accounting_llmusages(luid=None):
dt = datetime.fromtimestamp(t) dt = datetime.fromtimestamp(t)
tsstr = dt.strftime('%Y-%m-%d %H:%M:%S.') + f'{dt.microsecond // 1000:03d}' tsstr = dt.strftime('%Y-%m-%d %H:%M:%S.') + f'{dt.microsecond // 1000:03d}'
async with get_sor_context(env, 'llmage') as sor: 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 from llmusage a, llm b, llm_api_map c
where a.llmid = b.id where a.llmid = b.id
and a.llmid = c.llmid and a.llmid = c.llmid
@ -332,7 +335,7 @@ async def backend_accounting():
tpac = await get_user_tpac(lu.userid) tpac = await get_user_tpac(lu.userid)
if tpac: if tpac:
debug(f'{lu.id=},{lu.userid=}, {tpac=}, go 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: else:
debug(f'{lu.id=},{lu.userid=}, {tpac=}, go local') debug(f'{lu.id=},{lu.userid=}, {tpac=}, go local')
await llm_accounting(lu) await llm_accounting(lu)

View File

@ -75,8 +75,7 @@ async def async_uapi_request(request, llm,
estr = erase_apikey(e) estr = erase_apikey(e)
ed = {"error": f"ERROR:{estr}", "status": "FAILED"} ed = {"error": f"ERROR:{estr}", "status": "FAILED"}
exception(f'{ed}') exception(f'{ed}')
estr = json.dumps(ed, ensure_ascii=False) yield f'{ed}\n'
yield f'{estr}\n'
return return
if isinstance(b, bytes): if isinstance(b, bytes):
b = b.decode('utf-8') b = b.decode('utf-8')
@ -142,15 +141,9 @@ async def get_llm_llmusage(luid):
return return
if llmusage.status == 'FAILED': if llmusage.status == 'FAILED':
return return
# Use JOIN to get query_apiname/query_period from llm_api_map llms = await sor.R('llm', {'id': llmusage.llmid})
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})
if len(llms) == 0: 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}') exception(f'{e}')
raise e raise e
llm = llms[0] llm = llms[0]

View File

@ -9,8 +9,6 @@ from .utils import (
llm_query_orders, llm_query_orders,
read_webpath, read_webpath,
llm_query_price, llm_query_price,
get_user_tpac,
get_tpac_balance,
get_llm_by_model, get_llm_by_model,
get_llms_by_catelog, get_llms_by_catelog,
get_llms_sort_by_provider, get_llms_sort_by_provider,
@ -18,9 +16,6 @@ from .utils import (
get_llms_by_catelog_to_customer, get_llms_by_catelog_to_customer,
get_llmproviders, get_llmproviders,
get_llm, get_llm,
get_llmage_llm,
get_llm_catelogs,
invalidate_uapi_cache,
) )
from .llmclient import ( from .llmclient import (
@ -45,14 +40,6 @@ from .asyncinference import (
get_today_asynctask_list get_today_asynctask_list
) )
def _on_hot_reload(data=None):
"""Event handler for hot_reload — wraps invalidate_uapi_cache to accept dispatcher's data arg."""
from appPublic.log import debug
debug(f'[llmage] on_hot_reload called, invalidating uapi cache (data={data})')
invalidate_uapi_cache()
def load_llmage(): def load_llmage():
env = ServerEnv() env = ServerEnv()
env.llm_query_orders = llm_query_orders env.llm_query_orders = llm_query_orders
@ -65,12 +52,7 @@ def load_llmage():
env.get_asynctask_status = get_asynctask_status env.get_asynctask_status = get_asynctask_status
env.query_task_status = query_task_status env.query_task_status = query_task_status
env.get_llm = get_llm 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.inference = inference
env.get_user_tpac = get_user_tpac
env.get_tpac_balance = get_tpac_balance
env.inference_generator = inference_generator env.inference_generator = inference_generator
env.get_llms_by_catelog = get_llms_by_catelog env.get_llms_by_catelog = get_llms_by_catelog
env.get_llmcatelogs = get_llmcatelogs env.get_llmcatelogs = get_llmcatelogs
@ -83,9 +65,6 @@ def load_llmage():
env.backup_accounted_llmusage = backup_accounted_llmusage env.backup_accounted_llmusage = backup_accounted_llmusage
env.get_failed_accounting_records = get_failed_accounting_records env.get_failed_accounting_records = get_failed_accounting_records
env.get_llmage_stats = get_llmage_stats env.get_llmage_stats = get_llmage_stats
# Bind hot_reload event — module-level function, ref safe (module keeps it alive)
if hasattr(env, 'event_dispatcher'):
env.event_dispatcher.bind('hot_reload', _on_hot_reload)
rf = RegisterFunction() rf = RegisterFunction()
rf.register('jimeng_auth_headers', jimeng_auth_headers) rf.register('jimeng_auth_headers', jimeng_auth_headers)

View File

@ -116,9 +116,8 @@ async def _inference_generator(request, callerid, callerorgid,
if not params_kw.transno: if not params_kw.transno:
params_kw.transno = getID() params_kw.transno = getID()
llmid = params_kw.llmid llmid = params_kw.llmid
catelogid = params_kw.get('llmcatelogid', None)
f = None f = None
llm = await get_llm(llmid, catelogid) llm = await get_llm(llmid)
if llm is None: if llm is None:
errmsg = f'{{"status": "FAILED", "error":"llmid:{llmid}没找到模型"}}\n' errmsg = f'{{"status": "FAILED", "error":"llmid:{llmid}没找到模型"}}\n'
exception(errmsg) exception(errmsg)

View File

@ -1,68 +1,21 @@
import json import json
import time
import asyncio import asyncio
import aiofiles import aiofiles
from random import randint from random import randint
from functools import partial from functools import partial
from traceback import format_exc from traceback import format_exc
import time
from sqlor.dbpools import DBPools, get_sor_context from sqlor.dbpools import DBPools, get_sor_context
from appPublic.log import debug, exception, error, critical from appPublic.log import debug, exception, error, critical
from appPublic.uniqueID import getID from appPublic.uniqueID import getID
from appPublic.dictObject import DictObject from appPublic.dictObject import DictObject
from appPublic.timeUtils import curDateString, timestampstr from appPublic.timeUtils import curDateString, timestampstr
from uapi.appapi import UAPI, sor_get_callerid, sor_get_uapi, get_uapi from uapi.appapi import UAPI, sor_get_callerid, sor_get_uapi
from ahserver.serverenv import get_serverenv, ServerEnv from ahserver.serverenv import get_serverenv, ServerEnv
from ahserver.filestorage import FileStorage from ahserver.filestorage import FileStorage
from appPublic.jsonConfig import getConfig from appPublic.jsonConfig import getConfig
from appPublic.streamhttpclient import StreamHttpClient 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()
async def update_llmusage(ns): async def update_llmusage(ns):
env = ServerEnv() env = ServerEnv()
async with get_sor_context(env, 'llmage') as sor: async with get_sor_context(env, 'llmage') as sor:
@ -93,14 +46,13 @@ async def get_tpac_balance(tpac, userid):
exception(f'{url=}, {userid=}, error:{e}') exception(f'{url=}, {userid=}, error:{e}')
return None 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 url = tpac.tpac_accounting_url
hc = StreamHttpClient() hc = StreamHttpClient()
d = { d = {
'userid': userid, 'userid': userid,
'llmid': llmid, 'llmid': llmid,
'amount': amount, 'amount': amount,
'model': model,
'usage': usage 'usage': usage
} }
status = 'failed' status = 'failed'
@ -202,7 +154,6 @@ async def get_llmproviders():
sql = """select a.providerid, a.iconid, b.orgname sql = """select a.providerid, a.iconid, b.orgname
from llm a, organization b from llm a, organization b
where a.providerid = b.id where a.providerid = b.id
and a.status = 'published'
group by a.providerid, a.iconid, b.orgname""" group by a.providerid, a.iconid, b.orgname"""
return await sor.sqlExe(sql, {}) return await sor.sqlExe(sql, {})
return [] return []
@ -214,36 +165,14 @@ async def get_llms_sort_by_provider():
sql = """select a.*, b.orgname from llm a, organization b sql = """select a.*, b.orgname from llm a, organization b
where a.enabled_date <= ${today}$ where a.enabled_date <= ${today}$
and a.expired_date > ${today}$ and a.expired_date > ${today}$
and a.status = 'published'
and a.providerid = b.id and a.providerid = b.id
order by a.providerid, a.id order by a.providerid, a.id
""" """
recs = await sor.sqlExe(sql, {'today': today}) 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 = [] d = []
x = None x = None
oldpid = '-111' oldpid = '-111'
for l in recs: for l in recs:
# 获取定价展示文本
pricing_list = []
for ppid in pp_map.get(l.id, []):
try:
pd = await env.get_pricing_display(ppid)
if pd:
pricing_list.append(pd.get('display_text', ''))
except:
pass
l.pricing_display = pricing_list
if l.providerid != oldpid: if l.providerid != oldpid:
x = { x = {
'id': l.providerid, 'id': l.providerid,
@ -257,44 +186,6 @@ where a.enabled_date <= ${today}$
return d return d
return [] 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"
recs = await sor.sqlExe(sql, ns)
if llmid:
return recs[0] if recs else None
return recs
async def get_llmcatelogs(): async def get_llmcatelogs():
db = DBPools() db = DBPools()
dbname = get_serverenv('get_module_dbname')('llmage') dbname = get_serverenv('get_module_dbname')('llmage')
@ -320,7 +211,6 @@ m.ppid
join llm_api_map m on a.id = m.llmid join llm_api_map m on a.id = m.llmid
join llmcatelog b on m.llmcatelogid = b.id join llmcatelog b on m.llmcatelogid = b.id
where a.enabled_date <= ${today}$ where a.enabled_date <= ${today}$
and a.status = 'published'
and m.ppid is not null and m.ppid is not null
and a.expired_date > ${today}$ and a.expired_date > ${today}$
""" """
@ -360,7 +250,6 @@ async def get_llms_by_catelog(catelogid=None, orderby='providerid'):
join llm_api_map m on a.id = m.llmid join llm_api_map m on a.id = m.llmid
join llmcatelog b on m.llmcatelogid = b.id join llmcatelog b on m.llmcatelogid = b.id
where a.enabled_date <= ${today}$ where a.enabled_date <= ${today}$
and a.status = 'published'
and a.expired_date > ${today}$""" and a.expired_date > ${today}$"""
params = {'today': today, 'sort': orderby} params = {'today': today, 'sort': orderby}
if catelogid: if catelogid:
@ -370,30 +259,10 @@ async def get_llms_by_catelog(catelogid=None, orderby='providerid'):
sql += " order by m.llmcatelogid, a.id" sql += " order by m.llmcatelogid, a.id"
recs = await sor.sqlExe(sql, params) 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 = [] d = []
cid = '' cid = ''
x = None x = None
for r in recs: for r in recs:
pricing_list = []
for ppid in pp_map.get(r.id, []):
try:
pd = await env.get_pricing_display(ppid)
if pd:
pricing_list.append(pd.get('display_text', ''))
except:
pass
r.pricing_display = pricing_list
if cid != r.catelog_id: if cid != r.catelog_id:
x = { x = {
'catelogid': r.catelog_id, 'catelogid': r.catelog_id,
@ -407,58 +276,59 @@ async def get_llms_by_catelog(catelogid=None, orderby='providerid'):
return d return d
return [] 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): async def get_llm(llmid, catelogid=None):
"""Get LLM with full uapi info for vendor API calls. today = curDateString()
Refactored to use get_llmage_llm() + cached uapi/uapiio lookups env = ServerEnv()
instead of a 6-table JOIN. async with get_sor_context(env, 'llmage') as sor:
sql = """select a.id,
Returns DictObject with merged fields: a.name,
From get_llmage_llm: id, name, model, providerid, description, a.model,
iconid, upappid, ownerid, min_balance, status, llmcatelogid, a.providerid,
apiname, query_apiname, query_period, ppid, isdefaultcatelog, a.description,
catelogname a.iconid,
From uapi (cached): ioid, stream, callbackurl a.upappid,
From uapiio (cached): input_fields a.ownerid,
""" a.min_balance,
# Step 1: Get base info from get_llmage_llm (3-table JOIN: llm + llm_api_map + llmcatelog) m.llmcatelogid,
llm = await get_llmage_llm(llmid, catelogid) m.apiname,
if not llm: m.query_apiname,
debug(f'{llmid=} not found via get_llmage_llm') m.query_period,
return None m.ppid,
e.ioid,
# Step 2: Get uapi info (cached, keyed by upappid:apiname) e.stream,
uapi = await _get_uapi_cached(llm.upappid, llm.apiname) e.callbackurl,
if not uapi: f.input_fields,
debug(f'uapi not found: upappid={llm.upappid}, apiname={llm.apiname}') lc.name as catelogname
return None from llm a
,llm_api_map m
# Step 3: Get uapiio info (cached, keyed by ioid) ,llmcatelog lc
uapiio = await _get_uapiio_cached(uapi.ioid) ,upapp c
,uapi e
# Merge uapi fields into llm result ,uapiio f
llm.ioid = uapi.ioid where a.id = m.llmid
llm.stream = uapi.stream and a.upappid = c.id
llm.callbackurl = uapi.callbackurl and c.id = e.upappid
llm.input_fields = uapiio.input_fields if uapiio else '{}' and m.apiname = e.name
and e.ioid = f.id
return llm 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): async def write_llmusage(llmusage):
@ -468,7 +338,7 @@ async def write_llmusage(llmusage):
async def llm_query_price(llmid, config_data): async def llm_query_price(llmid, config_data):
env = ServerEnv() env = ServerEnv()
llm = await get_llmage_llm(llmid) llm = await get_llm(llmid)
if llm.ppid is None: if llm.ppid is None:
e = Exception(f'{llm=} ppid is None') e = Exception(f'{llm=} ppid is None')
exception(f'{e}') exception(f'{e}')

View File

@ -72,16 +72,7 @@
"title": "最低余额", "title": "最低余额",
"type": "float", "type": "float",
"length": 20, "length": 20,
"default": 10, "default": 10
"dec": 2
},
{
"name": "status",
"title": "上架状态",
"type": "str",
"length": 16,
"nullable": "no",
"default": "unpublished"
} }
], ],
"codes": [ "codes": [
@ -108,13 +99,6 @@
"table": "organization", "table": "organization",
"valuefield": "id", "valuefield": "id",
"textfield": "orgname" "textfield": "orgname"
},
{
"field": "status",
"table": "appcodes_kv",
"valuefield": "k",
"textfield": "v",
"cond": "parentid='llm_status'"
} }
] ]
} }

BIN
models/llm.xlsx Normal file

Binary file not shown.

View File

@ -4,99 +4,81 @@
{ {
"name": "llm_api_map", "name": "llm_api_map",
"title": "模型API映射表", "title": "模型API映射表",
"primary": [ "primary": "id",
"id"
],
"catelog": "relation" "catelog": "relation"
} }
], ],
"fields": [ "fields": [
{ {
"name": "id", "name": "id",
"type": "str", "type": "varchar(32)",
"not_null": true, "not_null": true,
"title": "主键ID", "title": "主键ID"
"length": 32
}, },
{ {
"name": "llmid", "name": "llmid",
"type": "str", "type": "varchar(32)",
"not_null": true, "not_null": true,
"title": "模型ID", "title": "模型ID"
"length": 32
}, },
{ {
"name": "llmcatelogid", "name": "llmcatelogid",
"type": "str", "type": "varchar(32)",
"not_null": true, "not_null": true,
"title": "模型分类ID", "title": "模型分类ID"
"length": 32
}, },
{ {
"name": "apiname", "name": "apiname",
"type": "str", "type": "varchar(100)",
"not_null": true, "not_null": true,
"title": "接口名称", "title": "接口名称"
"length": 100
}, },
{ {
"name": "query_apiname", "name": "query_apiname",
"type": "str", "type": "varchar(100)",
"title": "任务结果查询接口名称", "title": "任务结果查询接口名称"
"length": 100
}, },
{ {
"name": "query_period", "name": "query_period",
"type": "long", "type": "bigint",
"default": 30, "default": 30,
"title": "任务查询间隔(秒)" "title": "任务查询间隔(秒)"
}, },
{ {
"name": "ppid", "name": "ppid",
"type": "str", "type": "varchar(32)",
"title": "定价ID", "title": "定价ID"
"length": 32
}, },
{ {
"name": "isdefaultcatelog", "name": "isdefaultcatelog",
"type": "str", "type": "varchar(1)",
"not_null": true, "not_null": true,
"title": "缺省分类", "title": "缺省分类"
"length": 1
} }
], ],
"indexes": [ "indexes": [
{ {
"name": "idx_api_map_llmid", "name": "idx_api_map_llmid",
"type": "normal", "type": "normal",
"idxfields": [ "idxfields": ["llmid"],
"llmid"
],
"idxtype": "index" "idxtype": "index"
}, },
{ {
"name": "idx_api_map_catelog", "name": "idx_api_map_catelog",
"type": "normal", "type": "normal",
"idxfields": [ "idxfields": ["llmcatelogid"],
"llmcatelogid"
],
"idxtype": "index" "idxtype": "index"
}, },
{ {
"name": "idx_api_map_apiname", "name": "idx_api_map_apiname",
"type": "normal", "type": "normal",
"idxfields": [ "idxfields": ["apiname"],
"apiname"
],
"idxtype": "index" "idxtype": "index"
}, },
{ {
"name": "uk_llmid_apiname", "name": "uk_llmid_apiname",
"type": "unique", "type": "unique",
"idxfields": [ "idxfields": ["llmid", "apiname"],
"llmid",
"apiname"
],
"idxtype": "unique" "idxtype": "unique"
} }
], ],
@ -118,13 +100,6 @@
"table": "pricing_program", "table": "pricing_program",
"valuefield": "id", "valuefield": "id",
"textfield": "name" "textfield": "name"
},
{
"field": "isdefaultcatelog",
"table": "appcodes_kv",
"valuefield": "k",
"textfield": "v",
"cond": "parentid='isdefaultcatelog_flg'"
} }
] ]
} }

View File

@ -58,15 +58,13 @@
"name": "responsed_seconds", "name": "responsed_seconds",
"title": "响应时间", "title": "响应时间",
"type": "float", "type": "float",
"length": 18, "length": 18
"dec": 2
}, },
{ {
"name": "finish_seconds", "name": "finish_seconds",
"title": "结束时间", "title": "结束时间",
"type": "float", "type": "float",
"length": 18, "length": 18
"dec": 2
}, },
{ {
"name": "status", "name": "status",
@ -84,15 +82,13 @@
"name": "amount", "name": "amount",
"title": "交易金额", "title": "交易金额",
"type": "float", "type": "float",
"length": 18, "length": 18
"dec": 2
}, },
{ {
"name": "cost", "name": "cost",
"title": "交易成本", "title": "交易成本",
"type": "float", "type": "float",
"length": 18, "length": 18
"dec": 2
}, },
{ {
"name": "userorgid", "name": "userorgid",
@ -135,30 +131,6 @@
"accounting_status", "accounting_status",
"use_date" "use_date"
] ]
},
{
"name": "idx_llmusage_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'"
} }
] ]
} }

BIN
models/llmusage.xlsx Normal file

Binary file not shown.

View File

@ -121,32 +121,5 @@
"idxtype": "index", "idxtype": "index",
"idxfields": ["failed_time"] "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"
}
] ]
} }

View File

@ -136,27 +136,6 @@
"name": "idx_lh_backup_time", "name": "idx_lh_backup_time",
"idxtype": "index", "idxtype": "index",
"idxfields": ["backup_time"] "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'"
} }
] ]
} }

View File

@ -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';

View File

@ -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';

View File

@ -1,245 +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}/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_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",
# 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()

View File

@ -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()

View File

@ -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条目

View File

@ -96,27 +96,6 @@ for p in "${LLMUSAGE_PATHS[@]}"; do
done done
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 "============================================" echo "============================================"
echo " 权限配置完成,共设置 ${COUNT} 条权限" echo " 权限配置完成,共设置 ${COUNT} 条权限"

View File

@ -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';

View File

@ -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`);

View File

@ -1,101 +1,76 @@
#!/usr/bin/env python3
import json
import os
result = {'success': False, 'rows': [], 'total': 0, 'page': 1, 'page_size': 50} result = {'success': False, 'rows': [], 'total': 0, 'page': 1, 'page_size': 50}
try: try:
llmage_db = get_module_dbname('llmage') dbname = get_module_dbname('llmage')
sage_db = get_module_dbname('sage') user_orgid = await get_userorgid()
db = DBPools()
filters = {} # Extract filter parameters from params_kw
if params_kw.get('userorgid'): filters = {}
filters['userorgid'] = params_kw.get('userorgid') if params_kw.get('userorgid'):
if params_kw.get('llmid'): filters['userorgid'] = params_kw.get('userorgid')
filters['llmid'] = params_kw.get('llmid') if params_kw.get('llmid'):
if params_kw.get('handled') is not None and params_kw.get('handled') != '': filters['llmid'] = params_kw.get('llmid')
filters['handled'] = params_kw.get('handled') if params_kw.get('handled') is not None:
if params_kw.get('start_date'): filters['handled'] = params_kw.get('handled')
filters['start_date'] = params_kw.get('start_date') if params_kw.get('start_date'):
if params_kw.get('end_date'): filters['start_date'] = params_kw.get('start_date')
filters['end_date'] = params_kw.get('end_date') if params_kw.get('end_date'):
if params_kw.get('filter_userid'): filters['end_date'] = params_kw.get('end_date')
filters['filter_userid'] = params_kw.get('filter_userid')
if params_kw.get('filter_llmid'):
filters['filter_llmid'] = params_kw.get('filter_llmid')
page = int(params_kw.get('page', 1)) page = int(params_kw.get('page', 1))
page_size = int(params_kw.get('page_size', 50)) page_size = int(params_kw.get('page_size', 50))
async with db.sqlorContext(llmage_db) as sor: async with DBPools().sqlorContext(dbname) as sor:
conditions = [] # Build dynamic SQL
ns = {} conditions = []
ns = {}
if filters.get('userorgid'): # Default: show unhandled records
conditions.append("f.userorgid=${userorgid}$") if 'handled' not in filters:
ns['userorgid'] = filters['userorgid'] conditions.append("handled='0'")
if filters.get('llmid'):
conditions.append("f.llmid=${llmid}$")
ns['llmid'] = filters['llmid']
if filters.get('handled') is not None:
conditions.append("f.handled=${handled}$")
ns['handled'] = filters['handled']
if filters.get('start_date'):
conditions.append("f.use_date>=${start_date}$")
ns['start_date'] = filters['start_date']
if filters.get('end_date'):
conditions.append("f.use_date<=${end_date}$")
ns['end_date'] = filters['end_date']
if filters.get('filter_userid'):
conditions.append("(u.username LIKE ${filter_userid}$ OR u.name LIKE ${filter_userid}$)")
ns['filter_userid'] = '%' + filters['filter_userid'] + '%'
if filters.get('filter_llmid'):
conditions.append("(f.llmid LIKE ${filter_llmid}$ OR l.name LIKE ${filter_llmid}$)")
ns['filter_llmid'] = '%' + filters['filter_llmid'] + '%'
where = "" if filters.get('userorgid'):
if conditions: conditions.append("userorgid=${userorgid}$")
where = "WHERE " + " AND ".join(conditions) 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']
# 跨库JOIN获取名称 where = ""
sql = f""" if conditions:
SELECT f.*, where = "where " + " and ".join(conditions)
u.username as userid_text,
o.orgname as userorgid_text,
l.name as llmid_text
FROM llmusage_accounting_failed f
LEFT JOIN {sage_db}.users u ON f.userid = u.id
LEFT JOIN {sage_db}.organization o ON f.userorgid = o.id
LEFT JOIN {llmage_db}.llm l ON f.llmid = l.id
{where}
ORDER BY f.failed_time DESC
"""
count_sql = f""" # Count total
SELECT count(*) as cnt count_sql = f"select count(*) as cnt from llmusage_accounting_failed {where}"
FROM llmusage_accounting_failed f count_recs = await sor.sqlExe(count_sql, ns)
LEFT JOIN {sage_db}.users u ON f.userid = u.id total = count_recs[0].cnt if count_recs else 0
LEFT JOIN {sage_db}.organization o ON f.userorgid = o.id
LEFT JOIN {llmage_db}.llm l ON f.llmid = l.id
{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 with pagination
query_sql = sql + f" LIMIT {page_size} OFFSET {offset}" offset = (page - 1) * page_size
recs = await sor.sqlExe(query_sql, ns) 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)
rows = [] result['rows'] = [dict(r) for r in (recs or [])]
for r in (recs or []): result['total'] = total
d = dict(r) result['page'] = page
rows.append(d) result['page_size'] = page_size
result['success'] = True
result['rows'] = rows
result['total'] = total
result['page'] = page
result['page_size'] = page_size
result['success'] = True
except Exception as e: except Exception as e:
result['error'] = str(e) result['error'] = str(e)
debug(f'failed_accounting_list error: {format_exc()}')
return json.dumps(result, ensure_ascii=False, default=str) return json.dumps(result, ensure_ascii=False, default=str)

View File

@ -1,14 +0,0 @@
#!/usr/bin/env python3
import json
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)

View File

@ -1,14 +0,0 @@
#!/usr/bin/env python3
import json
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)

View File

@ -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)

View File

@ -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)

View File

@ -1,14 +0,0 @@
#!/usr/bin/env python3
import json
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)

View File

@ -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)

View File

@ -1,13 +0,0 @@
result = [{'providerid': '', 'providerid_text': '全部'}]
try:
async with get_sor_context(request._run_ns, 'rbac') as sor:
orgs = await sor.sqlExe(
"select id as providerid, orgname as providerid_text from organization order by orgname",
{}
)
return json.dumps([{'providerid': '', 'providerid_text': '全部'}] + list(orgs), ensure_ascii=False)
except Exception as e:
debug(f'get_search_providerid error: {e}')
return json.dumps(result, ensure_ascii=False)

View File

@ -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)

View File

@ -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)

View File

@ -1,21 +0,0 @@
#!/usr/bin/env python3
import json
from appPublic.uniqueID import getID
result = {'success': False, 'message': ''}
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)
data['id'] = getID()
await sor.C('llm', data)
result['success'] = True
result['message'] = '创建成功'
except Exception as e:
result['message'] = str(e)
return json.dumps(result, ensure_ascii=False, default=str)

View File

@ -1,23 +0,0 @@
#!/usr/bin/env python3
import json
result = {'success': False, 'message': ''}
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['success'] = True
result['message'] = '删除成功'
except Exception as e:
result['message'] = str(e)
return json.dumps(result, ensure_ascii=False, default=str)

View File

@ -1,88 +0,0 @@
llmid = params_kw.get('llmid', '')
action = params_kw.get('action', 'check')
if not llmid:
return json.dumps({'error': '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 '❌ 模型记录不存在'
llm = recs[0]
# 检查 API 映射
maps = await sor.sqlExe(
"select * from llm_api_map where llmid=${llmid}$",
{'llmid': llmid})
if not maps:
return '❌ 无 API 映射配置'
# 检查 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 '❌ uapi 配置不完整,无法调用'
uapi = uapi_recs[0]
# 检查 ioid
io_recs = await sor.sqlExe(
"select * from uapiio where id=${ioid}$", {'ioid': uapi.ioid})
if not io_recs:
return '❌ IO 定义不存在'
return f'✅ 推理配置验证通过\n模型: {llm.name}\nAPI: {uapi.api_name}\nIO: {uapi.ioid}\nStream: {uapi.stream}'
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 '❌ 无 API 映射'
ppids = [m.ppid for m in maps if m.ppid]
if not ppids:
return '❌ 无定价项目(ppid)'
ppid = ppids[0]
# 检查 pricing_program
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 not pregs:
return f'❌ 定价项目不存在 (ppid={ppid})'
pp = pregs[0]
# 检查 pricing_program_timing
try:
timings = await psor.sqlExe(
"select * from pricing_program_timing where ppid=${ppid}$",
{'ppid': ppid})
if timings:
return f'✅ 计费配置验证通过\n定价项目: {pp.name}\n定价数据: {len(timings)}条记录\n测试用量: {json.dumps(usages)}'
else:
return f'⚠️ 定价项目存在但无定价数据\n定价项目: {pp.name}'
except Exception as e:
return f'⚠️ 定价数据查询失败: {e}'
return '无效的操作'

View File

@ -1,24 +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 '下架成功'
except Exception as e:
result['message'] = str(e)
return {
"widgettype": "Text",
"options": {
"otext": result['message'],
"i18n": True
}
}

View File

@ -1,23 +0,0 @@
#!/usr/bin/env python3
import json
result = {'success': False, 'message': ''}
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.pop('id', None)
if not record_id:
result['message'] = '缺少id'
else:
await sor.U('llm', data, {'id': record_id})
result['success'] = True
result['message'] = '更新成功'
except Exception as e:
result['message'] = str(e)
return json.dumps(result, ensure_ascii=False, default=str)

View File

@ -1,3 +1,7 @@
#!/usr/bin/env python3
import json
from datetime import datetime
result = {'success': False, 'message': ''} result = {'success': False, 'message': ''}
try: try:
@ -7,12 +11,14 @@ try:
result['message'] = '缺少llmusageid参数' result['message'] = '缺少llmusageid参数'
else: else:
async with DBPools().sqlorContext(dbname) as sor: async with DBPools().sqlorContext(dbname) as sor:
# 1. 重置 llmusage 记账状态为 created让后台循环重新处理
await sor.U('llmusage', { await sor.U('llmusage', {
'id': luid, 'id': luid,
'accounting_status': 'created' '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(""" await sor.execute("""
UPDATE llmusage_accounting_failed UPDATE llmusage_accounting_failed
SET handled = '1', SET handled = '1',
@ -27,4 +33,4 @@ try:
except Exception as e: except Exception as e:
result['message'] = str(e) result['message'] = str(e)
return json.dumps(result, ensure_ascii=False) return json.dumps(result, ensure_ascii=False, default=str)

View File

@ -1,28 +0,0 @@
record_id = params_kw.get('id', '')
reason = '未找到记录'
if record_id:
dbname = get_module_dbname('llmage')
async with DBPools().sqlorContext(dbname) as sor:
rows = await sor.R('llmusage_accounting_failed', {'id': record_id})
if rows:
reason = rows[0].get('failed_reason', '') or '(空)'
return {
"widgettype": "VScrollPanel",
"options": {
"width": "100%",
"height": "100%",
"css": "card",
"padding": "12px"
},
"subwidgets": [
{
"widgettype": "Text",
"options": {
"text": reason,
"i18n": False
}
}
]
}

View File

@ -1,16 +1,17 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import json import json
result = [] result = {'success': False, 'data': []}
try: try:
dbname = get_module_dbname('llmage') dbname = get_module_dbname('llmage')
async with DBPools().sqlorContext(dbname) as sor: async with DBPools().sqlorContext(dbname) as sor:
rows = await sor.sqlExe("select name, path from uapi order by name", {}) 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: except Exception as e:
pass result['error'] = str(e)
return json.dumps(result, ensure_ascii=False, default=str) return json.dumps(result, ensure_ascii=False, default=str)

View File

@ -1 +0,0 @@
../docs/API.md

View File

@ -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%"
}
}
]
}
]
}

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -1,42 +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:
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:
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 not pregs:
text = "❌ 定价数据: 依赖定价项目未通过"
else:
# 检查 pricing_program_timing 表
try:
timings = await psor.sqlExe(
"select count(*) as cnt from pricing_program_timing where ppid=${ppid}$", {'ppid': ppid})
cnt = timings[0].cnt if timings else 0
if cnt > 0:
text = f"✅ 定价数据(pricing_program_timing): {cnt}条记录"
else:
text = "❌ 定价数据: pricing_program_timing 无记录"
except Exception as e:
text = f"❌ 定价数据: {e}"
except Exception as e:
text = f"❌ 定价数据: {e}"
return json.dumps({
"widgettype": "Text",
"options": {"text": text, "i18n": False}
}, ensure_ascii=False)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -3,170 +3,134 @@
"options": { "options": {
"width": "100%", "width": "100%",
"height": "100%", "height": "100%",
"padding": "8px", "padding": "16px",
"gap": "8px" "spacing": 12
}, },
"subwidgets": [ "subwidgets": [
{ {
"widgettype": "InlineForm", "widgettype": "Title2",
"id": "filter_form",
"options": { "options": {
"css": "card", "text": "记账失败记录",
"padding": "8px", "halign": "left"
"submit_label": "查询", }
"submit_css": "primary", },
"fields": [ {
{ "widgettype": "HBox",
"name": "start_date", "options": {
"label": "开始日期", "width": "100%",
"uitype": "date", "spacing": 12,
"cwidth": 10 "alignItems": "flex-end"
},
{
"name": "end_date",
"label": "结束日期",
"uitype": "date",
"cwidth": 10
},
{
"name": "handled",
"label": "处理状态",
"uitype": "code",
"cwidth": 8,
"data": [
{"value": "", "text": "全部"},
{"value": "0", "text": "未处理"},
{"value": "1", "text": "已处理"}
]
}
]
}, },
"binds": [ "subwidgets": [
{ {
"wid": "self", "widgettype": "VBox",
"event": "submit", "options": {"spacing": 4},
"actiontype": "script", "subwidgets": [
"target": "failed_table", {"widgettype": "Text", "options": {"text": "开始日期", "fontSize": "12px"}},
"script": "var tbl = bricks.getWidgetById('failed_table', bricks.app.root); if(tbl) await tbl.render(params);" {"widgettype": "UiDate", "id": "start_date", "options": {"width": "150px"}}
]
},
{
"widgettype": "VBox",
"options": {"spacing": 4},
"subwidgets": [
{"widgettype": "Text", "options": {"text": "结束日期", "fontSize": "12px"}},
{"widgettype": "UiDate", "id": "end_date", "options": {"width": "150px"}}
]
},
{
"widgettype": "VBox",
"options": {"spacing": 4},
"subwidgets": [
{"widgettype": "Text", "options": {"text": "处理状态", "fontSize": "12px"}},
{
"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"}},
{
"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"}},
{
"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); });"
}]
}
]
} }
] ]
}, },
{ {
"widgettype": "Tabular", "widgettype": "DataViewer",
"id": "failed_table", "id": "failed_table",
"options": { "options": {
"width": "100%", "url": "{{entire_url('/llmage/api/failed_accounting_list.dspy')}}",
"height": "100%", "title": "失败记录列表",
"css": "card", "pageSize": 20,
"toolbar": { "fields": [
"tools": [ {"name": "id", "title": "ID", "hidden": true},
{ {"name": "llmusageid", "title": "使用记录ID", "width": "120px"},
"name": "show_reason", {"name": "llmid", "title": "模型ID", "width": "120px"},
"label": "原因", {"name": "userid", "title": "用户ID", "width": "120px"},
"selected_row": true {"name": "userorgid", "title": "机构ID", "width": "120px"},
}, {"name": "use_date", "title": "使用日期", "width": "110px"},
{ {"name": "use_time", "title": "使用时间", "width": "160px"},
"name": "retry_accounting", {"name": "amount", "title": "金额", "width": "80px"},
"label": "重试记账", {"name": "cost", "title": "成本", "width": "80px"},
"selected_row": true {"name": "failed_reason", "title": "失败原因", "width": "30%"},
} {"name": "failed_time", "title": "失败时间", "width": "160px"},
] {"name": "retry_count", "title": "重试次数", "width": "80px"},
}, {"name": "handled", "title": "状态", "width": "80px",
"data_url": "{{entire_url('/llmage/api/failed_accounting_list.dspy')}}", "formatter": "function(v){return v==='1'?'已处理':'未处理';}"}
"data_method": "GET", ]
"page_rows": 20, }
"row_options": {
"browserfields": {
"exclouded": ["id", "failed_reason"],
"alters": {
"handled": {
"uitype": "code",
"data": [
{"value": "0", "text": "未处理"},
{"value": "1", "text": "已处理"}
]
},
"userid": {
"uitype": "code",
"valueField": "userid",
"textField": "userid_text",
"params": {
"dbname": "sage",
"table": "users",
"tblvalue": "userid",
"tbltext": "username",
"valueField": "userid",
"textField": "userid_text"
},
"dataurl": "{{entire_url('/appbase/get_code.dspy')}}"
},
"userorgid": {
"uitype": "code",
"valueField": "userorgid",
"textField": "userorgid_text",
"params": {
"dbname": "sage",
"table": "organization",
"tblvalue": "id",
"tbltext": "orgname",
"valueField": "userorgid",
"textField": "userorgid_text"
},
"dataurl": "{{entire_url('/appbase/get_code.dspy')}}"
},
"llmid": {
"uitype": "code",
"valueField": "llmid",
"textField": "llmid_text",
"params": {
"dbname": "llmage",
"table": "llm",
"tblvalue": "id",
"tbltext": "name",
"valueField": "llmid",
"textField": "llmid_text"
},
"dataurl": "{{entire_url('/appbase/get_code.dspy')}}"
}
}
},
"fields": [
{"name": "llmusageid", "title": "使用记录ID", "type": "str", "length": 32, "cwidth": 12, "uitype": "str", "label": "使用记录ID"},
{"name": "llmid", "title": "模型", "type": "str", "length": 32, "cwidth": 12, "uitype": "str", "label": "模型"},
{"name": "userid", "title": "用户", "type": "str", "length": 32, "cwidth": 10, "uitype": "str", "label": "用户"},
{"name": "userorgid", "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", "title": "状态", "type": "str", "length": 1, "cwidth": 6, "uitype": "code", "label": "状态"}
]
}
},
"binds": [
{
"wid": "self",
"event": "show_reason",
"actiontype": "urlwidget",
"target": "PopupWindow",
"popup_options": {
"title": "失败原因",
"cwidth": 30,
"cheight": 20
},
"options": {
"url": "{{entire_url('/llmage/api/show_failed_reason.dspy')}}?id=${id}$"
}
},
{
"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; } var resp = await fetch('{{entire_url('/llmage/api/retry_accounting.dspy')}}?id=' + row.llmusageid); var d = await resp.json(); if(d.success) { alert(d.message); await dv.render({}); } else { alert('失败: ' + d.message); }"
}
]
} }
] ]
} }

View File

@ -1,23 +1,9 @@
userid = await get_user() userid = await get_user()
llmcatelogid = params_kw.get('llmcatelogid', 't2t')
tasks = await get_today_asynctask_list(userid) tasks = await get_today_asynctask_list(userid)
async with get_sor_context(request._run_ns, 'llmage') as sor: for t in tasks:
for t in tasks: bin = await read_webpath(t.ioinfo)
bin = await read_webpath(t.ioinfo) t.ioinfo = json.loads(bin.decode('utf-8'))
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]
return { return {
'status': 'ok', 'status': 'ok',

View File

@ -1,36 +1,32 @@
lt = params_kw.llmcatelogid or 't2v'
debug(f'{lt=}') lt = '文生视频'
try: if params_kw.type in ['文生视频', '参考生视频', '图生视频']:
async with get_sor_context(request._run_ns, 'llmage') as sor: lt = params_kw.type
sql = '''select distinct a.*, e.input_fields from llm a async with get_sor_context(request._run_ns, 'llmage') as sor:
join llm_api_map m on a.id = m.llmid sql = '''select distinct a.*, e.input_fields from llm a
join llmcatelog b on m.llmcatelogid = b.id join llm_api_map m on a.id = m.llmid
join uapi d on d.upappid = a.upappid and m.apiname = d.name join llmcatelog b on m.llmcatelogid = b.id
join uapiio e on d.ioid = e.id join upapp c on a.upappid = c.id
where (b.id=${lt}$ OR b.name=${lt}$) join uapi d on c.apisetid = d.apisetid and a.apiname = d.name
and a.enabled_date <= ${biz_date}$ join uapiio e on d.ioid = e.id
and ${biz_date}$ < a.expired_date where b.name=${lt}$
and a.status = 'published' and a.enabled_date <= ${biz_date}$
and m.ppid is not NULL''' and ${biz_date}$ < a.expired_date
biz_date = await get_business_date(sor) and ppid is not NULL'''
recs = await sor.sqlExe(sql, { biz_date = await get_business_date(sor)
'biz_date': biz_date, recs = await sor.sqlExe(sql, {
'lt': lt 'biz_date': biz_date,
}) 'lt': lt
for r in recs: })
r.input_fields = json.loads(r.input_fields) for r in recs:
return { r.input_fields = json.loads(r.input_fields)
'status': 'ok',
'data': recs
}
return { return {
'status': 'error', 'status': 'ok',
'data':{ 'data': recs
'message': 'server error'
}
} }
except Exception as e: return {
debug(f'{lt=},{e},{format_exc()}') 'status': 'error',
'data':{
'message': 'server error'
}
}

View File

@ -17,7 +17,9 @@
{ {
"widgettype": "Title2", "widgettype": "Title2",
"options": { "options": {
"text": "LLM 模型管理" "text": "LLM 模型管理",
"color": "#F1F5F9",
"fontWeight": "700"
} }
}, },
{ {
@ -26,176 +28,215 @@
{ {
"widgettype": "Text", "widgettype": "Text",
"options": { "options": {
"text": "模型类型、模型配置与记账失败记录", "text": "模型配置、目录分类与调用监控",
"cfontsize": 1.2 "fontSize": "14px",
"color": "#64748B"
} }
} }
] ]
}, },
{ {
"widgettype": "VBox", "widgettype": "ResponsableBox",
"options": { "options": {
"css": "filler", "gap": "16px",
"spacing": 16 "minWidth": "200px",
"marginBottom": "24px"
}, },
"subwidgets": [ "subwidgets": [
{ {
"widgettype": "ResponsableBox", "widgettype": "urlwidget",
"options": { "options": {
"gap": "16px", "url": "{{entire_url('/llmage/stat_total_models.ui')}}"
"minWidth": "250px" }
},
{
"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": [ "subwidgets": [
{ {
"widgettype": "VBox", "widgettype": "Svg",
"options": { "options": {
"css": "card", "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>",
"cwidth": 23, "width": "36px",
"padding": "16px", "height": "36px",
"cursor": "pointer", "marginBottom": "16px"
"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
}
}
]
}, },
{ {
"widgettype": "VBox", "widgettype": "Title4",
"options": { "options": {
"css": "card", "text": "模型类型管理",
"cwidth": 23, "color": "#F1F5F9",
"padding": "16px", "fontWeight": "600",
"cursor": "pointer", "marginBottom": "8px"
"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
}
}
]
}, },
{ {
"widgettype": "VBox", "widgettype": "Text",
"options": { "options": {
"css": "card", "text": "管理模型的分类目录和类型定义",
"cwidth": 23, "fontSize": "14px",
"padding": "16px", "color": "#94A3B8"
"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
}
}
]
} }
] ]
}, },
{ {
"widgettype": "VScrollPanel", "widgettype": "VBox",
"id": "llmage_content",
"options": { "options": {
"css": "filler", "bgcolor": "#1E293B",
"width": "100%", "padding": "24px",
"height": "100%" "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"
}
} }
] ]
} }

View File

@ -3,7 +3,7 @@ db = DBPools()
async with db.sqlorContext(dbname) as sor: async with db.sqlorContext(dbname) as sor:
sql = """select distinct a.* from llm a sql = """select distinct a.* from llm a
join llm_api_map m on a.id = m.llmid join llm_api_map m on a.id = m.llmid
where m.llmcatelogid = ${llmcatelogid}$ and a.id != ${llmid}$ and a.status = 'published'""" where m.llmcatelogid = ${llmcatelogid}$ and a.id != ${llmid}$"""
ns = params_kw.copy() ns = params_kw.copy()
recs = await sor.sqlExe(sql, ns) recs = await sor.sqlExe(sql, ns)
for r in recs.get('rows', []): for r in recs.get('rows', []):

View File

@ -6,17 +6,6 @@ page = int(params_kw.get('page', 1))
dbname = get_module_dbname('llmage') dbname = get_module_dbname('llmage')
db = DBPools() db = DBPools()
async with db.sqlorContext(dbname) as sor: async with db.sqlorContext(dbname) as sor:
# If llmcatelogid not provided, derive it from llmid via llm_api_map
llmcatelogid = params_kw.get('llmcatelogid')
if not llmcatelogid:
llmid = params_kw.get('llmid')
if llmid:
recs = await sor.sqlExe("select llmcatelogid from llm_api_map where llmid=${llmid}$ limit 1", {'llmid': llmid})
if recs:
llmcatelogid = recs[0].llmcatelogid
if not llmcatelogid:
return {}
params_kw.llmcatelogid = llmcatelogid
sql = """select x.*, sql = """select x.*,
z.input_fields, z.input_fields,
y.system_message, y.system_message,
@ -28,23 +17,18 @@ from llm a
join llm_api_map m on a.id = m.llmid join llm_api_map m on a.id = m.llmid
join llmcatelog b on m.llmcatelogid = b.id join llmcatelog b on m.llmcatelogid = b.id
join upapp c on a.upappid = c.id join upapp c on a.upappid = c.id
join uapi e on c.id = e.upappid and m.apiname = e.name join uapi e on c.apisetid = e.apisetid and a.apiname = e.name
where a.status = 'published'
and m.llmcatelogid = ${llmcatelogid}$
) x left join historyformat y on x.hfid = y.id ) x left join historyformat y on x.hfid = y.id
left join uapiio z on x.ioid = z.id left join uapiio z on x.ioid = z.id
where 1=1 where m.llmcatelogid = ${llmcatelogid}$
and x.id != ${llmid}$
""" """
llmid = params_kw.get('llmid')
if llmid:
sql += " and x.id != ${llmid}$"
ns = params_kw.copy() ns = params_kw.copy()
ns.page = page ns.page = page
ns.pagerows = pagerows ns.pagerows = pagerows
recs = await sor.sqlPaging(sql, ns) recs = await sor.sqlPaging(sql, ns)
for r in recs.get('rows', []): for r in recs.get('rows', []):
r.llmid = r.id r.llmid = r.id
r.llmcatelogid = llmcatelogid
r.modelname = r.name r.modelname = r.name
r.description = ''.join(''.join(r.description.split('\n')).split('\r')) r.description = ''.join(''.join(r.description.split('\n')).split('\r'))
r.response_mode = r.stream r.response_mode = r.stream

View File

@ -53,22 +53,23 @@
"name": "llmcatelogid", "name": "llmcatelogid",
"label": "选择分类", "label": "选择分类",
"uitype": "code", "uitype": "code",
"dataurl": "{{entire_url('./api/get_catelogs.dspy')}}", "dataurl": "{{entire_url('./api/llm_api_map_options.dspy')}}",
"data_field": "catelogs",
"placeholder": "请选择分类" "placeholder": "请选择分类"
}, },
{ {
"name": "apiname", "name": "apiname",
"label": "API接口", "label": "API接口",
"uitype": "code", "uitype": "code",
"dataurl": "{{entire_url('./api/get_search_apiname.dspy')}}", "dataurl": "{{entire_url('./api/llm_api_map_options.dspy')}}",
"data_field": "apis",
"placeholder": "请选择API接口" "placeholder": "请选择API接口"
}, },
{ {
"name": "query_apiname", "name": "query_apiname",
"label": "查询API", "label": "查询API",
"uitype": "code", "uitype": "str",
"dataurl": "{{entire_url('./api/get_search_apiname.dspy?allow_empty=1')}}", "placeholder": "异步查询API名多个用逗号分隔"
"placeholder": "不指定或选择查询API"
}, },
{ {
"name": "query_period", "name": "query_period",
@ -80,7 +81,8 @@
"name": "ppid", "name": "ppid",
"label": "计费项目", "label": "计费项目",
"uitype": "code", "uitype": "code",
"dataurl": "{{entire_url('./api/get_ppids.dspy')}}", "dataurl": "{{entire_url('./api/llm_api_map_options.dspy')}}",
"data_field": "ppids",
"placeholder": "请选择计费项目" "placeholder": "请选择计费项目"
} }
], ],
@ -98,11 +100,7 @@
"event": "click", "event": "click",
"actiontype": "urlwidget", "actiontype": "urlwidget",
"target": "PopupWindow", "target": "PopupWindow",
"popup_options": { "popup_options": {"archor": "cc", "width": "30%", "height": "20%"},
"archor": "cc",
"width": "30%",
"height": "20%"
},
"options": { "options": {
"url": "{{entire_url('./api/llm_api_map_create.dspy')}}", "url": "{{entire_url('./api/llm_api_map_create.dspy')}}",
"params": { "params": {
@ -126,8 +124,7 @@
"options": { "options": {
"width": "calc(100% - 40px)", "width": "calc(100% - 40px)",
"margin": "0 20px", "margin": "0 20px",
"spacing": 12, "spacing": 12
"cheight": 30
}, },
"subwidgets": [ "subwidgets": [
{ {
@ -149,46 +146,19 @@
"page_rows": 20, "page_rows": 20,
"row_options": { "row_options": {
"fields": [ "fields": [
{ {"name": "llm_name", "title": "模型名称", "width": 180},
"name": "llm_name", {"name": "catelog_name", "title": "分类", "width": 120},
"title": "模型名称", {"name": "apiname", "title": "API接口", "width": 150},
"width": 180 {"name": "query_apiname", "title": "查询API", "width": 180},
}, {"name": "query_period", "title": "间隔(秒)", "width": 80},
{ {"name": "ppid_name", "title": "计费项目", "width": 150},
"name": "catelog_name",
"title": "分类",
"width": 120
},
{
"name": "apiname",
"title": "API接口",
"width": 150
},
{
"name": "query_apiname",
"title": "查询API",
"width": 180
},
{
"name": "query_period",
"title": "间隔(秒)",
"width": 80
},
{
"name": "ppid_name",
"title": "计费项目",
"width": 150
},
{ {
"name": "actions", "name": "actions",
"title": "操作", "title": "操作",
"width": 100, "width": 100,
"uitype": "button", "uitype": "button",
"data": [ "data": [
{ {"text": "删除", "event": "delete_map"}
"text": "删除",
"event": "delete_map"
}
] ]
} }
] ]
@ -200,11 +170,7 @@
"event": "delete_map", "event": "delete_map",
"actiontype": "urlwidget", "actiontype": "urlwidget",
"target": "PopupWindow", "target": "PopupWindow",
"popup_options": { "popup_options": {"archor": "cc", "width": "30%", "height": "20%"},
"archor": "cc",
"width": "30%",
"height": "20%"
},
"options": { "options": {
"url": "{{entire_url('./api/llm_api_map_delete.dspy')}}", "url": "{{entire_url('./api/llm_api_map_delete.dspy')}}",
"params": { "params": {

View File

@ -3,82 +3,10 @@
{% set userorgid = get_userorgid() %} {% set userorgid = get_userorgid() %}
{% if params_kw.id %} {% if params_kw.id %}
{% if checkCustomerBalance(params_kw.id, userid, userorgid) %} {% if checkCustomerBalance(params_kw.id, userid, userorgid) %}
{% set catelogs = get_llm_catelogs(params_kw.id) %} {% set llm = get_llm(params_kw.id) %}
{% set ns = namespace(active_catelogid=params_kw.get('catelogid', '')) %} {% set oops=debug(json.dumps(llm, ensure_ascii=Fasle)) %}
{% if not ns.active_catelogid %}
{% for c in catelogs %}{% if c.isdefault and not ns.active_catelogid %}{% set ns.active_catelogid = c.catelogid %}{% endif %}{% endfor %}
{% endif %}
{% if not ns.active_catelogid and catelogs %}{% set ns.active_catelogid = catelogs[0].catelogid %}{% endif %}
{% set llm = get_llm(params_kw.id, ns.active_catelogid) %}
{% set kdbs = get_user_kdbs(request) %} {% set kdbs = get_user_kdbs(request) %}
{% if llm %} {% if llm %}
{% if len(catelogs) > 1 %}
{
"widgettype":"VBox",
"options":{
"width":"100%",
"height":"100%"
},
"subwidgets":[
{
"widgettype":"HBox",
"options":{
"cheight":3,
"css":"card",
"padding":"8px"
},
"subwidgets":[
{% for c in catelogs %}
{
"widgettype":"Button",
"options":{
"label":"{{c.catelogname}}",
"actiontype":"link",
{% if c.catelogid == ns.active_catelogid %}
"css":"primary",
{% endif %}
"url":"{{entire_url('/llmage/llm_dialog.ui')}}?id={{params_kw.id}}&catelogid={{c.catelogid}}"
}
}{% if not loop.last %},{% endif %}
{% endfor %}
]
},
{
"widgettype":"LlmIO",
"options":{
"width":"100%",
"height":"100%",
"title":"{{llm.name}}",
{% if len(kdbs) > 0 %}
"enabled_kdb": true,
"kdb_setting":{},
"get_kdb_url": "{{entire_url('/rag/get_my_kdbs.dspy')}}",
{% endif %}
"list_models_url":"{{entire_url('list_paging_catelog_llms.dspy')}}?llmcatelogid={{ns.active_catelogid}}",
"estimate_url":"{{entire_url('model_estimate.dspy')}}",
"input_fields":{{llm.input_fields}},
"models":[
{
"llmid":"{{llm.id}}",
"llmcatelogid":"{{ns.active_catelogid}}",
"response_mode": "{{llm.stream}}",
"icon":"{{entire_url('/appbase/show_icon.dspy')}}?id={{llm.iconid}}",
"url":"{{entire_url('/llmage/llminference.dspy')}}",
{% if llm.stream == 'stream' %}
"stream": true,
{% endif %}
{% if llm.stream =='async' %}
"query_url": "{{entire_url('/llmage/tasks')}}",
{% endif %}
"model":"{{llm.model}}",
"modelname":"{{llm.name}}"
}
]
}
}
]
}
{% else %}
{ {
"widgettype":"LlmIO", "widgettype":"LlmIO",
"options":{ "options":{
@ -90,15 +18,12 @@
"kdb_setting":{}, "kdb_setting":{},
"get_kdb_url": "{{entire_url('/rag/get_my_kdbs.dspy')}}", "get_kdb_url": "{{entire_url('/rag/get_my_kdbs.dspy')}}",
{% endif %} {% endif %}
"list_models_url":"{{entire_url('list_paging_catelog_llms.dspy')}}{% if ns.active_catelogid %}?llmcatelogid={{ns.active_catelogid}}{% endif %}", "list_models_url":"{{entire_url('list_paging_catelog_llms.dspy')}}",
"estimate_url":"{{entire_url('model_estimate.dspy')}}", "estimate_url":"{{entire_url('model_estimate.dspy')}}",
"input_fields":{{llm.input_fields}}, "input_fields":{{llm.input_fields}},
"models":[ "models":[
{ {
"llmid":"{{llm.id}}", "llmid":"{{llm.id}}",
{% if ns.active_catelogid %}
"llmcatelogid":"{{ns.active_catelogid}}",
{% endif %}
"response_mode": "{{llm.stream}}", "response_mode": "{{llm.stream}}",
"icon":"{{entire_url('/appbase/show_icon.dspy')}}?id={{llm.iconid}}", "icon":"{{entire_url('/appbase/show_icon.dspy')}}?id={{llm.iconid}}",
"url":"{{entire_url('/llmage/llminference.dspy')}}", "url":"{{entire_url('/llmage/llminference.dspy')}}",
@ -114,7 +39,6 @@
] ]
} }
} }
{% endif %}
{% else %} {% else %}
{ {
"widgettype":"Text", "widgettype":"Text",

View File

@ -1,156 +0,0 @@
{% if params_kw.id %}
{% set llmid = params_kw.id %}
{
"widgettype": "VBox",
"options": {
"width": "100%",
"height": "100%",
"spacing": 10
},
"subwidgets": [
{
"widgettype": "Title",
"options": {
"text": "模型上线检查",
"level": 2
}
},
{
"widgettype": "VScrollPanel",
"id": "check_scroll",
"options": {
"css": "filler",
"width": "100%"
},
"subwidgets": [
{
"widgettype": "VBox",
"id": "checks_list",
"options": {
"spacing": 5
},
"subwidgets": [
{
"widgettype": "urlwidget",
"options": {
"url": "/llmage/check_model_record.dspy?llmid={{llmid}}"
}
},
{
"widgettype": "urlwidget",
"options": {
"url": "/llmage/check_date_status.dspy?llmid={{llmid}}"
}
},
{
"widgettype": "urlwidget",
"options": {
"url": "/llmage/check_upapp.dspy?llmid={{llmid}}"
}
},
{
"widgettype": "urlwidget",
"options": {
"url": "/llmage/check_uapi.dspy?llmid={{llmid}}"
}
},
{
"widgettype": "urlwidget",
"options": {
"url": "/llmage/check_uapiio.dspy?llmid={{llmid}}"
}
},
{
"widgettype": "urlwidget",
"options": {
"url": "/llmage/check_llm_api_map.dspy?llmid={{llmid}}"
}
},
{
"widgettype": "urlwidget",
"options": {
"url": "/llmage/check_pricing_program.dspy?llmid={{llmid}}"
}
},
{
"widgettype": "urlwidget",
"options": {
"url": "/llmage/check_pricing_data.dspy?llmid={{llmid}}"
}
}
]
},
{
"widgettype": "Text",
"id": "test_result",
"options": {
"text": "",
"i18n": false
}
},
{
"widgettype": "Text",
"id": "charge_result",
"options": {
"text": "",
"i18n": false
}
}
]
},
{
"widgettype": "HBox",
"options": {
"spacing": 10,
"alignItems": "center"
},
"subwidgets": [
{
"widgettype": "Button",
"id": "test_btn",
"options": {
"label": "体验一次",
"i18n": false
}
},
{
"widgettype": "Button",
"id": "charge_btn",
"options": {
"label": "检查计费",
"i18n": false
}
}
]
}
],
"binds": [
{
"wid": "test_btn",
"event": "click",
"actiontype": "urldata",
"target": "test_result",
"options": {
"url": "/llmage/api/llm_launch_check_api.dspy?llmid={{llmid}}&action=inference"
}
},
{
"wid": "charge_btn",
"event": "click",
"actiontype": "urldata",
"target": "charge_result",
"options": {
"url": "/llmage/api/llm_launch_check_api.dspy?llmid={{llmid}}&action=check_charging&usages={\"prompt_tokens\":1000,\"completion_tokens\":500}"
}
}
]
}
{% else %}
{
"widgettype": "Text",
"options": {
"text": "缺少模型ID参数",
"i18n": true
}
}
{% endif %}

View File

@ -2,7 +2,7 @@ llmid = params_kw.llmid
today= params_kw.today today= params_kw.today
msgs = [] msgs = []
async with get_sor_context(request._run_ns, 'llmage') as sor: async with get_sor_context(request._run_ns, 'llmage') as sor:
sql = "select * from llm where id=${llmid}$ and enabled_date <= ${today}$ and expired_date > ${today}$ and status = 'published'" sql = "select * from llm where id=${llmid}$ and enabled_date <= ${today}$ and expired_date > ${today}$"
ns = {'llmid': llmid, 'today': today} ns = {'llmid': llmid, 'today': today}
recs = await sor.sqlExe(sql, ns.copy()) recs = await sor.sqlExe(sql, ns.copy())
if recs: if recs:
@ -24,9 +24,8 @@ where a.id=${llmid}$
sql = """select a.*, e.ioid, e.stream sql = """select a.*, e.ioid, e.stream
from llm a from llm a
join llm_api_map m on a.id = m.llmid
join upapp c on a.upappid = c.id join upapp c on a.upappid = c.id
join uapi e on c.id = e.upappid and m.apiname = e.name join uapi e on c.apisetid = e.apisetid and a.apiname = e.name
where a.id=${llmid}$ where a.id=${llmid}$
and a.expired_date > ${today}$ and a.expired_date > ${today}$
and a.enabled_date <= ${today}$""" and a.enabled_date <= ${today}$"""
@ -39,9 +38,8 @@ where a.id=${llmid}$
sql = """select a.*, e.ioid, e.stream sql = """select a.*, e.ioid, e.stream
from llm a from llm a
join llm_api_map m on a.id = m.llmid
join upapp c on a.upappid = c.id join upapp c on a.upappid = c.id
join uapi e on c.id = e.upappid and m.apiname = e.name join uapi e on c.apisetid = e.apisetid and a.apiname = e.name
join uapiio b on e.ioid = b.id join uapiio b on e.ioid = b.id
where a.id=${llmid}$ where a.id=${llmid}$
and a.expired_date > ${today}$ and a.expired_date > ${today}$

View File

@ -1,4 +1,4 @@
debug_params('params_kw', params_kw) debug(f'{params_kw=}')
ns = params_kw.copy() ns = params_kw.copy()
if not ns.page: if not ns.page:
ns.page = 1 ns.page = 1

View File

@ -1,4 +1,4 @@
debug_params('params_kw', params_kw) debug(f'{params_kw=}')
if params_kw.off_peak: if params_kw.off_peak:
off_peak = params_kw.off_peak off_peak = params_kw.off_peak
if off_peak in [True, "Y" "y", 1, "1"]: if off_peak in [True, "Y" "y", 1, "1"]:
@ -11,8 +11,6 @@ userorgid = await get_userorgid()
if userid is None: if userid is None:
return UiError(title='llm inference', message='Please login first') return UiError(title='llm inference', message='Please login first')
f = await checkCustomerBalance(params_kw.llmid, userid, userorgid) f = await checkCustomerBalance(params_kw.llmid, userid, userorgid)
if not f:
return UiError(title='llm inference', message='余额不足或模型未配置定价')
kdbids = params_kw.kdbids kdbids = params_kw.kdbids
if kdbids: if kdbids:
data = { data = {
@ -27,7 +25,7 @@ if kdbids:
ret = await rfexe('fusedsearch', request, params) ret = await rfexe('fusedsearch', request, params)
data.update(ret) data.update(ret)
params_kw.prompt = await tmpl_engine.renders(tmpl, data) params_kw.prompt = await tmpl_engine.renders(tmpl, data)
debug_params('rag', {'query': params.get('query',''), 'prompt_len': len(str(params_kw.prompt))}) debug(f'{params=}rag return {data}, {params_kw.prompt=}')
env = DictObject(**globals()) env = DictObject(**globals())
return await inference(request, env=env) return await inference(request, env=env)

View File

@ -1,161 +0,0 @@
ns = params_kw.copy()
debug(f'get_llmusage_accounting_failed.dspy:{ns=}')
if not ns.get('page'):
ns['page'] = 1
if not ns.get('sort'):
ns['sort'] = 'failed_time desc'
# InlineForm filter conditions
extra_conds = []
if ns.get('handled') and ns['handled'] != '':
extra_conds.append("handled = ${filter_handled}$")
ns['filter_handled'] = ns['handled']
if ns.get('filter_llmid') and ns['filter_llmid'] != '':
extra_conds.append("llmid like ${filter_llmid}$")
ns['filter_llmid'] = f"%{ns['filter_llmid']}%"
if ns.get('filter_userid') and ns['filter_userid'] != '':
extra_conds.append("userid like ${filter_userid}$")
ns['filter_userid'] = f"%{ns['filter_userid']}%"
if ns.get('use_date') and ns['use_date'] != '':
extra_conds.append("use_date = ${filter_use_date}$")
ns['filter_use_date'] = ns['use_date']
if ns.get('failed_reason') and ns['failed_reason'] != '':
extra_conds.append("failed_reason like ${filter_failed_reason}$")
ns['filter_failed_reason'] = f"%{ns['failed_reason']}%"
extra_filterstr = ''
if extra_conds:
extra_filterstr = ' and ' + ' and '.join(extra_conds)
sql = f'''select * from llmusage_accounting_failed where 1=1 {extra_filterstr} [[filterstr]]'''
filterjson = params_kw.get('data_filter')
if filterjson and isinstance(filterjson, str):
try:
filterjson = json.loads(filterjson)
except (json.JSONDecodeError, TypeError):
filterjson = None
fields_str=r'''[
{
"name": "id",
"title": "id",
"type": "str",
"length": 32
},
{
"name": "llmusageid",
"title": "使用记录id",
"type": "str",
"length": 32
},
{
"name": "llmid",
"title": "模型id",
"type": "str",
"length": 32
},
{
"name": "userid",
"title": "用户id",
"type": "str",
"length": 32
},
{
"name": "userorgid",
"title": "用户机构id",
"type": "str",
"length": 32
},
{
"name": "use_date",
"title": "使用日期",
"type": "date"
},
{
"name": "use_time",
"title": "使用时间",
"type": "timestamp"
},
{
"name": "amount",
"title": "交易金额",
"type": "double",
"length": 18,
"dec": 5
},
{
"name": "cost",
"title": "交易成本",
"type": "double",
"length": 18,
"dec": 5
},
{
"name": "failed_reason",
"title": "失败原因",
"type": "text"
},
{
"name": "failed_time",
"title": "失败时间",
"type": "timestamp"
},
{
"name": "retry_count",
"title": "重试次数",
"type": "int"
},
{
"name": "handled",
"title": "是否已处理",
"type": "str",
"length": 1,
"default": "0"
},
{
"name": "handled_time",
"title": "处理时间",
"type": "timestamp"
},
{
"name": "handled_note",
"title": "处理备注",
"type": "text"
}
]'''
ori_fields = json.loads(fields_str)
if not filterjson:
fields = [ f['name'] for f in ori_fields ]
filterjson = default_filterjson(fields, ns)
filterdic = ns.copy()
filterdic['filterstr'] = ''
filterdic['userorgid'] = '${userorgid}$'
filterdic['userid'] = '${userid}$'
if filterjson:
dbf = DBFilter(filterjson)
conds = dbf.gen(ns)
if conds:
ns.update(dbf.consts)
conds = f' and {conds}'
filterdic['filterstr'] = conds
ac = ArgsConvert('[[', ']]')
vars = ac.findAllVariables(sql)
NameSpace = {v:'${' + v + '}$' for v in vars if v != 'filterstr' }
filterdic.update(NameSpace)
sql = ac.convert(sql, filterdic)
debug(f'{sql=}')
db = DBPools()
dbname = get_module_dbname('llmage')
async with db.sqlorContext(dbname) as sor:
r = await sor.sqlPaging(sql, ns)
return r
return {
"total":0,
"rows":[]
}

View File

@ -1,307 +0,0 @@
{
"widgettype": "VBox",
"options": {
"height": "100%",
"width": "100%",
"padding": "8px",
"gap": "8px"
},
"subwidgets": [
{
"widgettype": "InlineForm",
"id": "filter_form",
"options": {
"css": "card",
"padding": "8px",
"submit_label": "查询",
"submit_css": "primary",
"fields": [
{
"name": "handled",
"label": "处理状态",
"uitype": "code",
"cwidth": 10,
"codes": [
{"value": "", "text": "全部"},
{"value": "0", "text": "未处理"},
{"value": "1", "text": "已处理"}
]
},
{
"name": "filter_llmid",
"label": "模型",
"uitype": "str",
"placeholder": "模型ID或名称",
"cwidth": 12
},
{
"name": "filter_userid",
"label": "用户",
"uitype": "str",
"placeholder": "用户ID",
"cwidth": 12
},
{
"name": "failed_reason",
"label": "失败原因",
"uitype": "str",
"placeholder": "关键词",
"cwidth": 15
}
]
},
"binds": [
{
"wid": "self",
"event": "submit",
"actiontype": "method",
"target": "llmusage_accounting_failed_tbl",
"method": "render"
}
]
},
{
"widgettype": "HBox",
"options": {
"css": "card", "padding": "4px 8px", "cheight": 3
},
"subwidgets": [
{
"widgettype": "Button",
"id": "btn_recover_usages",
"options": {
"label": "从IO文件恢复Usages",
"css": "primary"
},
"binds": [
{
"wid": "self",
"event": "click",
"actiontype": "urldata",
"target": "msg_area",
"options": {
"url": "{{entire_url('./recover_usages.dspy')}}"
},
"mode": "replace"
},
{
"wid": "self",
"event": "click",
"actiontype": "method",
"target": "llmusage_accounting_failed_tbl",
"method": "render"
}
]
},
{
"widgettype": "VBox",
"id": "msg_area",
"options": {
"width": "100%", "css": "filler"
}
}
]
},
{
"id": "llmusage_accounting_failed_tbl",
"widgettype": "Tabular",
"options": {
"width": "100%",
"height": "100%",
"title": "记账失败记录",
"css": "card",
"editable": {
"new_data_url": "{{entire_url('add_llmusage_accounting_failed.dspy')}}",
"delete_data_url": "{{entire_url('delete_llmusage_accounting_failed.dspy')}}",
"update_data_url": "{{entire_url('update_llmusage_accounting_failed.dspy')}}"
},
"data_url": "{{entire_url('./get_llmusage_accounting_failed.dspy')}}",
"data_method": "GET",
"data_params": {{json.dumps(params_kw, indent=4, ensure_ascii=False)}},
"row_options": {
"browserfields": {
"exclouded": ["id"],
"alters": {
"handled": {
"uitype": "code",
"data": [
{"value": "0", "text": "未处理"},
{"value": "1", "text": "已处理"}
]
}
}
},
"editexclouded": ["id", "llmusageid", "failed_time"],
"fields": [
{
"name": "id",
"title": "id",
"type": "str",
"length": 32,
"cwidth": 18,
"uitype": "str",
"datatype": "str",
"label": "id"
},
{
"name": "llmusageid",
"title": "使用记录id",
"type": "str",
"length": 32,
"cwidth": 18,
"uitype": "str",
"datatype": "str",
"label": "使用记录id"
},
{
"name": "llmid",
"title": "模型",
"type": "str",
"length": 32,
"cwidth": 18,
"uitype": "code",
"valueField": "llmid",
"textField": "llmid_text",
"params": {
"dbname": "llmage",
"table": "llm",
"tblvalue": "id",
"tbltext": "name",
"valueField": "llmid",
"textField": "llmid_text"
},
"dataurl": "{{entire_url('/appbase/get_code.dspy')}}",
"datatype": "str",
"label": "模型"
},
{
"name": "userid",
"title": "用户",
"type": "str",
"length": 32,
"cwidth": 18,
"uitype": "code",
"valueField": "userid",
"textField": "userid_text",
"params": {
"dbname": "sage",
"table": "users",
"tblvalue": "userid",
"tbltext": "username",
"valueField": "userid",
"textField": "userid_text"
},
"dataurl": "{{entire_url('/appbase/get_code.dspy')}}",
"datatype": "str",
"label": "用户"
},
{
"name": "userorgid",
"title": "机构",
"type": "str",
"length": 32,
"cwidth": 18,
"uitype": "code",
"valueField": "userorgid",
"textField": "userorgid_text",
"params": {
"dbname": "sage",
"table": "organization",
"tblvalue": "id",
"tbltext": "orgname",
"valueField": "userorgid",
"textField": "userorgid_text"
},
"dataurl": "{{entire_url('/appbase/get_code.dspy')}}",
"datatype": "str",
"label": "机构"
},
{
"name": "use_time",
"title": "使用时间",
"type": "timestamp",
"length": 0,
"uitype": "str",
"datatype": "timestamp",
"label": "使用时间"
},
{
"name": "amount",
"title": "交易金额",
"type": "double",
"length": 18,
"dec": 5,
"cwidth": 18,
"uitype": "float",
"datatype": "double",
"label": "交易金额"
},
{
"name": "failed_reason",
"title": "失败原因",
"type": "text",
"length": 0,
"uitype": "text",
"datatype": "text",
"label": "失败原因"
},
{
"name": "failed_time",
"title": "失败时间",
"type": "timestamp",
"length": 0,
"uitype": "str",
"datatype": "timestamp",
"label": "失败时间"
},
{
"name": "retry_count",
"title": "重试次数",
"type": "int",
"length": 0,
"uitype": "int",
"datatype": "int",
"label": "重试次数"
},
{
"name": "handled",
"title": "是否已处理",
"type": "str",
"length": 1,
"default": "0",
"cwidth": 4,
"uitype": "code",
"datatype": "str",
"label": "是否已处理",
"data": [
{"value": "0", "text": "未处理"},
{"value": "1", "text": "已处理"}
]
},
{
"name": "handled_time",
"title": "处理时间",
"type": "timestamp",
"length": 0,
"uitype": "str",
"datatype": "timestamp",
"label": "处理时间"
},
{
"name": "handled_note",
"title": "处理备注",
"type": "text",
"length": 0,
"uitype": "text",
"datatype": "text",
"label": "处理备注"
}
]
},
"page_rows": 160,
"cache_limit": 5
},
"binds": []
}
]
}

View File

@ -1,136 +0,0 @@
ns = params_kw.copy()
limit = int(ns.get('limit') or 200)
single_id = ns.get('id') or None
from ahserver.filestorage import FileStorage
import os
db = DBPools()
dbname = get_module_dbname('llmage')
recovered = 0
failed = 0
skipped = 0
errors = []
try:
async with db.sqlorContext(dbname) as sor:
if single_id:
sql = """select a.id, a.llmid, a.ioinfo, a.status, b.model
from llmusage a
left join llm b on a.llmid = b.id
where a.id = ${id}$"""
params = {'id': single_id}
else:
sql = """select a.id, a.llmid, a.ioinfo, a.status, b.model
from llmusage a
left join llm b on a.llmid = b.id
where a.usages is null
and a.status = 'SUCCEEDED'
order by a.use_date desc"""
params = {'page': 1, 'rows': limit}
recs = await sor.sqlExe(sql, params)
if isinstance(recs, dict):
rows = recs.get('rows', [])
else:
rows = recs if recs else []
if not rows:
return {
"widgettype": "Message",
"options": {
"title": "恢复Usages",
"cwidth": 20,
"cheight": 5,
"timeout": 5,
"message": "没有找到需要恢复的记录"
}
}
fs = FileStorage()
for r in rows:
rid = r.id if hasattr(r, 'id') else r.get('id', '')
model = r.model if hasattr(r, 'model') else r.get('model', '')
ioinfo = r.ioinfo if hasattr(r, 'ioinfo') else r.get('ioinfo', None)
if not ioinfo:
skipped += 1
continue
try:
# ioinfo 可能是 JSON 内容,也可能是文件路径
io_data = None
if ioinfo.startswith('{') or ioinfo.startswith('"'):
# 直接是 JSON 内容
io_data = json.loads(ioinfo)
else:
# 文件路径
real_path = fs.realPath(ioinfo)
if not os.path.isfile(real_path):
errors.append(f'{rid}: 文件不存在')
failed += 1
continue
with open(real_path, 'r', encoding='utf-8') as f:
io_data = json.load(f)
outputs = io_data.get('output', [])
if not outputs:
errors.append(f'{rid}: output为空')
failed += 1
continue
# 从最后一条output开始倒序找usage
usage = None
for out in reversed(outputs):
if isinstance(out, dict) and out.get('usage'):
usage = out['usage']
break
if not usage:
errors.append(f'{rid}: output中未找到usage')
failed += 1
continue
usages_str = json.dumps(usage, ensure_ascii=False)
await sor.U('llmusage', {
'id': rid,
'usages': usages_str
})
recovered += 1
except Exception as e:
debug(f'recover_usages error for {rid}: {e}')
errors.append(f'{rid}: {e}')
failed += 1
except Exception as e:
exception(f'recover_usages error: {e}')
return {
"widgettype": "Error",
"options": {
"title": "恢复Usages失败",
"cwidth": 20,
"cheight": 5,
"timeout": 5,
"message": str(e)
}
}
total = recovered + failed + skipped
msg = f"处理 {total} 条: 恢复成功 {recovered}, 失败 {failed}, 跳过 {skipped}"
if errors:
msg += f"\n失败详情(前5条): {'; '.join(errors[:5])}"
return {
"widgettype": "Message",
"options": {
"title": "恢复Usages完成",
"cwidth": 30,
"cheight": 6,
"timeout": 8,
"message": msg
}
}

View File

@ -1,5 +1,5 @@
debug_params('model_estimate', params_kw) debug(f'model_estimate.dspy:{params_kw=}')
db = DBPools() db = DBPools()
dbname = get_module_dbname('llmage') dbname = get_module_dbname('llmage')
async with db.sqlorContext(dbname) as sor: async with db.sqlorContext(dbname) as sor:

View File

@ -1,128 +0,0 @@
/* Model Plaza — 模型广场 */
/* Hide provider view initially */
#plaza_view_provider {
display: none;
}
.plaza-header {
padding: 8px 16px 4px 16px;
}
/* View switcher buttons */
.plaza-view-switcher {
gap: 8px;
padding: 8px 0 4px 0;
}
.plaza-view-btn {
border-radius: 6px !important;
transition: all 0.2s ease;
}
.plaza-view-btn.plaza-view-active {
border: 2px solid var(--sage-brand, #6366f1) !important;
}
/* Left sidebar */
.plaza-sidebar {
border-right: 1px solid var(--sage-border, #e2e8f0);
padding: 4px;
}
.plaza-nav-btn {
margin-bottom: 2px;
text-align: left;
transition: background-color 0.15s ease;
}
.plaza-nav-btn:hover {
background-color: var(--sage-hover, rgba(99, 102, 241, 0.08));
}
/* Dark mode overrides for sidebar */
[data-theme="dark"] .plaza-sidebar {
border-right: 1px solid #334155;
background-color: #1E293B;
}
[data-theme="dark"] .plaza-nav-btn {
background-color: transparent;
color: #CBD5E1;
border: none;
}
[data-theme="dark"] .plaza-nav-btn:hover {
background-color: #334155;
color: #F1F5F9;
}
/* Card hover effects for model cards */
.plaza-card {
transition: transform 0.2s ease, box-shadow 0.2s ease;
cursor: pointer;
border-radius: 8px !important;
}
.plaza-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
}
/* Category section headers */
.plaza-section-title {
padding: 12px 0 4px 4px;
position: relative;
}
.plaza-section-title::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 4px;
height: 70%;
border-radius: 2px;
background: var(--sage-brand, #6366f1);
}
/* Smooth card grid */
.plaza-grid {
gap: 12px !important;
padding: 4px 8px;
}
/* Model icon area */
.plaza-card .model-icon-row {
gap: 8px;
}
/* Description text */
.plaza-card .model-desc {
line-height: 1.5;
opacity: 0.85;
}
/* Pricing display area */
.pricing-box {
margin-top: 8px;
padding-top: 8px;
border-top: 1px solid var(--sage-border, #e2e8f0);
gap: 6px;
}
.pricing-text {
font-size: 0.85em;
line-height: 1.4;
opacity: 0.75;
white-space: pre-wrap;
}
[data-theme="dark"] .pricing-box {
border-top-color: #475569;
}
[data-theme="dark"] .pricing-text {
color: #94A3B8;
}

View File

@ -1,122 +0,0 @@
{% set catelogs = get_llmcatelogs() %}
{% set providers = get_llms_sort_by_provider() %}
{
"widgettype": "VBox",
"options": {
"css": "filler",
"width": "100%"
},
"subwidgets": [
{
"widgettype": "VBox",
"options": {
"css": "plaza-header",
"width": "100%"
},
"subwidgets": [
{
"widgettype": "Title2",
"options": {
"otext": "模型广场",
"i18n": true,
"halign": "left"
}
},
{
"widgettype": "Text",
"options": {
"otext": "探索和使用各类AI模型",
"i18n": true,
"halign": "left",
"wrap": true
}
},
{
"widgettype": "HBox",
"options": {
"css": "plaza-view-switcher",
"cheight": 3
},
"subwidgets": [
{
"widgettype": "Button",
"id": "btn_by_catelog",
"options": {
"label": "按分类",
"css": "plaza-view-btn plaza-view-active",
"cwidth": 15
},
"binds": [
{
"wid": "self",
"event": "click",
"actiontype": "script",
"target": "self",
"script": "document.getElementById('plaza_view_provider').style.display='none'; document.getElementById('plaza_view_catelog').style.display='flex'; var a=bricks.getWidgetById('btn_by_catelog',bricks.app); var b=bricks.getWidgetById('btn_by_provider',bricks.app); if(a)a.dom_element.classList.add('plaza-view-active'); if(b)b.dom_element.classList.remove('plaza-view-active');"
}
]
},
{
"widgettype": "Button",
"id": "btn_by_provider",
"options": {
"label": "按供应商",
"css": "plaza-view-btn",
"cwidth": 15
},
"binds": [
{
"wid": "self",
"event": "click",
"actiontype": "script",
"target": "self",
"script": "document.getElementById('plaza_view_catelog').style.display='none'; document.getElementById('plaza_view_provider').style.display='flex'; var a=bricks.getWidgetById('btn_by_catelog',bricks.app); var b=bricks.getWidgetById('btn_by_provider',bricks.app); if(a)a.dom_element.classList.remove('plaza-view-active'); if(b)b.dom_element.classList.add('plaza-view-active');"
}
]
}
]
}
]
},
{
"widgettype": "VBox",
"id": "plaza_view_catelog",
"options": {
"css": "filler",
"width": "100%",
"height": "100%"
},
"subwidgets": [
{
"widgettype": "urlwidget",
"options": {
"css": "filler",
"width": "100%",
"height": "100%",
"url": "{{entire_url('show_llms.ui')}}"
}
}
]
},
{
"widgettype": "VBox",
"id": "plaza_view_provider",
"options": {
"css": "filler",
"width": "100%",
"height": "100%"
},
"subwidgets": [
{
"widgettype": "urlwidget",
"options": {
"css": "filler",
"width": "100%",
"height": "100%",
"url": "{{entire_url('show_llms_by_providers.ui')}}"
}
}
]
}
]
}

View File

@ -1,84 +1,106 @@
{% set catelogs = get_llmcatelogs() %} {% set userorgid = get_userorgid() %}
{ {
"widgettype":"HBox", "widgettype":"VScrollPanel",
"options":{ "options":{
"css":"filler",
"width":"100%", "width":"100%",
"height":"100%" "height":"100%"
}, },
"subwidgets":[ "subwidgets":[
{% for cate in get_llms_by_catelog() %}
{ {
"widgettype":"VScrollPanel", "widgettype": "VBox",
"options":{ "options":{
"cwidth":18, "width":"100%"
"height":"100%",
"css":"plaza-sidebar"
}, },
"subwidgets":[ "subwidgets":[
{ {
"widgettype":"Button", "widgettype":"Title3",
"options":{ "options":{
"label":"全部", "wrap":true,
"css":"plaza-nav-btn", "halign": "left",
"width":"100%" "i18n": true,
}, "otext":"{{cate.catelogname}}"
"binds":[ }
{ },
"wid":"self",
"event":"click",
"actiontype":"urlwidget",
"target":"app.plaza_cards_panel",
"mode":"replace",
"options":{
"url":"{{entire_url('show_llms_cards.ui')}}"
}
}
]
}{% for cat in catelogs %},
{ {
"widgettype":"Button", "widgettype":"DynamicColumn",
"options":{
"label":"{{cat.name}}",
"css":"plaza-nav-btn",
"width":"100%"
},
"binds":[
{
"wid":"self",
"event":"click",
"actiontype":"urlwidget",
"target":"app.plaza_cards_panel",
"mode":"replace",
"options":{
"url":"{{entire_url('show_llms_cards.ui')}}",
"params":{
"catelogid":"{{cat.id}}"
}
}
}
]
}{% endfor %}
]
},
{
"widgettype":"VBox",
"id":"plaza_cards_panel",
"options":{
"css":"filler",
"cwidth":82,
"height":"100%"
},
"subwidgets":[
{
"widgettype":"urlwidget",
"options":{ "options":{
"css":"filler", "css":"filler",
"width":"100%", "width":"100%"
"height":"100%", },
"url":"{{entire_url('show_llms_cards.ui')}}" "subwidgets":[
} {% for llm in cate.llms %}
{
"widgettype":"VScrollPanel",
"options":{
"css":"card",
"bgcolor": "#def0f0",
"cwidth":20,
"cheight":12
},
"subwidgets":[
{
"widgettype":"HBox",
"options":{
"cheight":2
},
"subwidgets":[
{
"widgettype":"Svg",
"options":{
"rate":1.5,
"url":"{{entire_url('/appbase/show_icon.dspy')}}?id={{llm.iconid}}"
}
},
{
"widgettype":"Title6",
"options":{
"text":"{{llm.name}}"
}
}
]
},
{
"widgettype":"Text",
"options":{
"text":{{json.dumps(llm.description, ensure_ascii=False)}},
"wrap":true,
"halign":"left"
}
}
],
"binds":[
{
"wid":"self",
"event":"click",
"actiontype":"urlwidget",
"target":"PopupWindow",
"popup_options":{
"title":"{{llm.name}}",
{% if int(params_kw._is_mobile) %}
"width": "100%",
"height": "100%"
{% else %}
"width": "40%",
"height":"85%"
{% endif %}
},
"options":{
"params":{
"id":"{{llm.id}}"
},
"url":"{{entire_url('./llm_dialog.ui')}}"
}
}
]
}
{% if not loop.last %}, {% endif %}
{% endfor %}
]
} }
] ]
} }
{% if not loop.last %}, {% endif %}
{% endfor %}
] ]
} }

View File

@ -1,84 +1,105 @@
{% set providers = get_llms_sort_by_provider() %} {% set userorgid = get_userorgid() %}
{ {
"widgettype":"HBox", "widgettype":"VScrollPanel",
"options":{ "options":{
"css":"filler",
"width":"100%", "width":"100%",
"height":"100%" "height":"100%"
}, },
"subwidgets":[ "subwidgets":[
{% for p in get_llms_sort_by_provider() %}
{ {
"widgettype":"VScrollPanel", "widgettype": "VBox",
"options":{ "options":{
"cwidth":18, "width":"100%"
"height":"100%",
"css":"plaza-sidebar"
}, },
"subwidgets":[ "subwidgets":[
{ {
"widgettype":"Button", "widgettype":"Title3",
"options":{ "options":{
"label":"全部", "wrap":true,
"css":"plaza-nav-btn", "halign": "left",
"width":"100%" "text":"{{p.orgname}}"
}, }
"binds":[ },
{
"wid":"self",
"event":"click",
"actiontype":"urlwidget",
"target":"app.plaza_provider_panel",
"mode":"replace",
"options":{
"url":"{{entire_url('show_llms_cards_by_provider.ui')}}"
}
}
]
}{% for p in providers %},
{ {
"widgettype":"Button", "widgettype":"DynamicColumn",
"options":{
"label":"{{p.orgname}}",
"css":"plaza-nav-btn",
"width":"100%"
},
"binds":[
{
"wid":"self",
"event":"click",
"actiontype":"urlwidget",
"target":"app.plaza_provider_panel",
"mode":"replace",
"options":{
"url":"{{entire_url('show_llms_cards_by_provider.ui')}}",
"params":{
"providerid":"{{p.id}}"
}
}
}
]
}{% endfor %}
]
},
{
"widgettype":"VBox",
"id":"plaza_provider_panel",
"options":{
"css":"filler",
"cwidth":82,
"height":"100%"
},
"subwidgets":[
{
"widgettype":"urlwidget",
"options":{ "options":{
"css":"filler", "css":"filler",
"width":"100%", "width":"100%"
"height":"100%", },
"url":"{{entire_url('show_llms_cards_by_provider.ui')}}" "subwidgets":[
} {% for llm in p.llms %}
{
"widgettype":"VScrollPanel",
"options":{
"css":"card",
"bgcolor": "#def0f0",
"cwidth":20,
"cheight":12
},
"subwidgets":[
{
"widgettype":"HBox",
"options":{
"cheight":2
},
"subwidgets":[
{
"widgettype":"Svg",
"options":{
"rate":1.5,
"url":"{{entire_url('/appbase/show_icon.dspy')}}?id={{llm.iconid}}"
}
},
{
"widgettype":"Title6",
"options":{
"text":"{{llm.name}}"
}
}
]
},
{
"widgettype":"Text",
"options":{
"text":{{json.dumps(llm.description, ensure_ascii=False)}},
"wrap":true,
"halign":"left"
}
}
],
"binds":[
{
"wid":"self",
"event":"click",
"actiontype":"urlwidget",
"target":"PopupWindow",
"popup_options":{
"title":"{{llm.name}}",
{% if int(params_kw._is_mobile) %}
"width": "100%",
"height": "100%"
{% else %}
"width": "40%",
"height":"85%"
{% endif %}
},
"options":{
"params":{
"id":"{{llm.id}}"
},
"url":"{{entire_url('./llm_dialog.ui')}}"
}
}
]
}
{% if not loop.last %}, {% endif %}
{% endfor %}
]
} }
] ]
} }
{% if not loop.last %}, {% endif %}
{% endfor %}
] ]
} }

View File

@ -1,112 +0,0 @@
{% set catelogid = params_kw.get('catelogid', None) %}
{% set data = get_llms_by_catelog(catelogid=catelogid) %}
{% set ns = namespace(first=true) %}
{
"widgettype":"VScrollPanel",
"options":{
"css":"filler",
"width":"100%",
"height":"100%"
},
"subwidgets":[
{
"widgettype":"DynamicColumn",
"options":{
"css":"plaza-grid",
"width":"100%",
"col_cwidth":25,
"col_cgap":1
},
"subwidgets":[
{% for cate in data %}
{% for llm in cate.llms %}
{% if not ns.first %},{% endif %}
{
"widgettype":"VScrollPanel",
"options":{
"css":"card plaza-card",
"cwidth":25,
"cheight":16
},
"subwidgets":[
{
"widgettype":"HBox",
"options":{
"cheight":2
},
"subwidgets":[
{
"widgettype":"Svg",
"options":{
"rate":1.5,
"url":"{{entire_url('/appbase/show_icon.dspy')}}?id={{llm.iconid}}"
}
},
{
"widgettype":"Title6",
"options":{
"text":"{{llm.name}}"
}
}
]
},
{
"widgettype":"Text",
"options":{
"text":{{json.dumps(llm.description, ensure_ascii=False)}},
"wrap":true,
"halign":"left"
}
},
{
"widgettype":"Filler",
"options":{
"css":"pricing-box"
},
"subwidgets":[
{% for pricing_text in llm.pricing_display %}
{
"widgettype":"Text",
"options":{
"text":{{json.dumps(pricing_text, ensure_ascii=False)}},
"wrap":true,
"halign":"left",
"css":"pricing-text"
}
}{% if not loop.last %},{% endif %}
{% endfor %}
]
}
],
"binds":[
{
"wid":"self",
"event":"click",
"actiontype":"urlwidget",
"target":"PopupWindow",
"popup_options":{
"title":"{{llm.name}}",
{% if int(params_kw._is_mobile) %}
"width": "100%",
"height": "100%"
{% else %}
"width": "40%",
"height":"85%"
{% endif %}
},
"options":{
"params":{
"id":"{{llm.id}}"
},
"url":"{{entire_url('./llm_dialog.ui')}}"
}
}
]
}
{% set ns.first = false %}
{% endfor %}
{% endfor %}
]
}
]
}

View File

@ -1,112 +0,0 @@
{% set providerid = params_kw.get('providerid', '') %}
{% set data = get_llms_sort_by_provider() %}
{% set ns = namespace(first=true) %}
{
"widgettype":"VScrollPanel",
"options":{
"css":"filler",
"width":"100%",
"height":"100%"
},
"subwidgets":[
{
"widgettype":"DynamicColumn",
"options":{
"css":"plaza-grid",
"width":"100%"
},
"subwidgets":[
{% for p in data %}
{% if not providerid or p.id|string == providerid|string %}
{% for llm in p.llms %}
{% if not ns.first %},{% endif %}
{
"widgettype":"VScrollPanel",
"options":{
"css":"card plaza-card",
"cwidth":25,
"cheight":16
},
"subwidgets":[
{
"widgettype":"HBox",
"options":{
"cheight":2
},
"subwidgets":[
{
"widgettype":"Svg",
"options":{
"rate":1.5,
"url":"{{entire_url('/appbase/show_icon.dspy')}}?id={{llm.iconid}}"
}
},
{
"widgettype":"Title6",
"options":{
"text":"{{llm.name}}"
}
}
]
},
{
"widgettype":"Text",
"options":{
"text":{{json.dumps(llm.description or '', ensure_ascii=False)}},
"wrap":true,
"halign":"left"
}
},
{
"widgettype":"Filler",
"options":{
"css":"pricing-box"
},
"subwidgets":[
{% for pricing_text in llm.pricing_display %}
{
"widgettype":"Text",
"options":{
"text":{{json.dumps(pricing_text, ensure_ascii=False)}},
"wrap":true,
"halign":"left",
"css":"pricing-text"
}
}{% if not loop.last %},{% endif %}
{% endfor %}
]
}
],
"binds":[
{
"wid":"self",
"event":"click",
"actiontype":"urlwidget",
"target":"PopupWindow",
"popup_options":{
"title":"{{llm.name}}",
{% if int(params_kw._is_mobile) %}
"width": "100%",
"height": "100%"
{% else %}
"width": "40%",
"height":"85%"
{% endif %}
},
"options":{
"params":{
"id":"{{llm.id}}"
},
"url":"{{entire_url('./llm_dialog.ui')}}"
}
}
]
}
{% set ns.first = false %}
{% endfor %}
{% endif %}
{% endfor %}
]
}
]
}

View File

@ -10,6 +10,7 @@
"widgettype":"VScrollPanel", "widgettype":"VScrollPanel",
"options":{ "options":{
"css":"card", "css":"card",
"bgcolor": "#def0f0",
"cwidth":20, "cwidth":20,
"cheight":12 "cheight":12
}, },

View File

@ -2,8 +2,10 @@
{ {
"widgettype": "VBox", "widgettype": "VBox",
"options": { "options": {
"bgcolor": "#1E293B",
"padding": "20px", "padding": "20px",
"borderRadius": "12px", "borderRadius": "12px",
"border": "1px solid #334155",
"flex": "1", "flex": "1",
"minHeight": "110px" "minHeight": "110px"
}, },
@ -34,6 +36,7 @@
"text": "{{stats.catelog_count}}", "text": "{{stats.catelog_count}}",
"fontSize": "32px", "fontSize": "32px",
"fontWeight": "700", "fontWeight": "700",
"color": "#F1F5F9",
"lineHeight": "1.1" "lineHeight": "1.1"
} }
}, },
@ -42,6 +45,7 @@
"options": { "options": {
"text": "模型分类", "text": "模型分类",
"fontSize": "14px", "fontSize": "14px",
"color": "#94A3B8",
"marginTop": "4px" "marginTop": "4px"
} }
} }

View File

@ -2,8 +2,10 @@
{ {
"widgettype": "VBox", "widgettype": "VBox",
"options": { "options": {
"bgcolor": "#1E293B",
"padding": "20px", "padding": "20px",
"borderRadius": "12px", "borderRadius": "12px",
"border": "1px solid #334155",
"flex": "1", "flex": "1",
"minHeight": "110px" "minHeight": "110px"
}, },
@ -34,6 +36,7 @@
"text": "¥{{'%.2f' % stats.today_amount}}", "text": "¥{{'%.2f' % stats.today_amount}}",
"fontSize": "32px", "fontSize": "32px",
"fontWeight": "700", "fontWeight": "700",
"color": "#F1F5F9",
"lineHeight": "1.1" "lineHeight": "1.1"
} }
}, },
@ -42,6 +45,7 @@
"options": { "options": {
"text": "今日消费", "text": "今日消费",
"fontSize": "14px", "fontSize": "14px",
"color": "#94A3B8",
"marginTop": "4px" "marginTop": "4px"
} }
} }

View File

@ -2,8 +2,10 @@
{ {
"widgettype": "VBox", "widgettype": "VBox",
"options": { "options": {
"bgcolor": "#1E293B",
"padding": "20px", "padding": "20px",
"borderRadius": "12px", "borderRadius": "12px",
"border": "1px solid #334155",
"flex": "1", "flex": "1",
"minHeight": "110px" "minHeight": "110px"
}, },
@ -34,6 +36,7 @@
"text": "{{stats.today_usage_count}}", "text": "{{stats.today_usage_count}}",
"fontSize": "32px", "fontSize": "32px",
"fontWeight": "700", "fontWeight": "700",
"color": "#F1F5F9",
"lineHeight": "1.1" "lineHeight": "1.1"
} }
}, },
@ -42,6 +45,7 @@
"options": { "options": {
"text": "今日调用", "text": "今日调用",
"fontSize": "14px", "fontSize": "14px",
"color": "#94A3B8",
"marginTop": "4px" "marginTop": "4px"
} }
} }

View File

@ -2,8 +2,10 @@
{ {
"widgettype": "VBox", "widgettype": "VBox",
"options": { "options": {
"bgcolor": "#1E293B",
"padding": "20px", "padding": "20px",
"borderRadius": "12px", "borderRadius": "12px",
"border": "1px solid #334155",
"flex": "1", "flex": "1",
"minHeight": "110px" "minHeight": "110px"
}, },
@ -34,6 +36,7 @@
"text": "{{stats.total_models}}", "text": "{{stats.total_models}}",
"fontSize": "32px", "fontSize": "32px",
"fontWeight": "700", "fontWeight": "700",
"color": "#F1F5F9",
"lineHeight": "1.1" "lineHeight": "1.1"
} }
}, },
@ -42,6 +45,7 @@
"options": { "options": {
"text": "可用模型数", "text": "可用模型数",
"fontSize": "14px", "fontSize": "14px",
"color": "#94A3B8",
"marginTop": "4px" "marginTop": "4px"
} }
} }

View File

@ -1,5 +1,5 @@
debug_params('params_kw', params_kw) debug(f'{params_kw=}')
lctype='t2t' lctype='文生文'
if params_kw.off_peak: if params_kw.off_peak:
off_peak = params_kw.off_peak off_peak = params_kw.off_peak
if off_peak in [True, "Y" "y", 1, "1"]: if off_peak in [True, "Y" "y", 1, "1"]:
@ -20,9 +20,8 @@ async with get_sor_context(env, 'llmage') as sor:
sql = """select distinct a.* from llm a sql = """select distinct a.* from llm a
join llm_api_map m on a.id = m.llmid join llm_api_map m on a.id = m.llmid
join llmcatelog b on m.llmcatelogid = b.id join llmcatelog b on m.llmcatelogid = b.id
where (b.id = ${lctype}$ OR b.name = ${lctype}$) where b.name = ${lctype}$
and a.model=${model}$ and a.model=${model}$"""
and a.status = 'published'"""
recs = await sor.sqlExe(sql, { recs = await sor.sqlExe(sql, {
'lctype': lctype, 'lctype': lctype,
'model': params_kw.model or 'qwen3-max' 'model': params_kw.model or 'qwen3-max'

View File

@ -1,74 +0,0 @@
# OpenAI-compatible Text-to-Speech API
# POST /v1/audio/speech
# Required params: model, catelogid, prompt (text to synthesize)
# Optional params: speaker (voice_id), speed, emotion
#
# Example request:
# {
# "model": "speech-2.6-turbo",
# "catelogid": "tts",
# "prompt": "你好,欢迎使用语音合成服务",
# "speaker": "female-tianmei",
# "speed": 1.0,
# "emotion": "happy"
# }
#
# Response (stream, hex audio chunks):
# {
# "status": "SUCCEEDED",
# "audio": "base64_encoded_audio_data"
# }
userid = await get_user()
userorgid = await get_userorgid()
if userid is None:
debug('need login')
return openai_403()
# Validate required parameters
if not params_kw.model:
d = return_error('Missing required parameter: model')
return json_response(d, status=400)
if not params_kw.catelogid:
d = return_error('Missing required parameter: catelogid')
return json_response(d, status=400)
if not params_kw.prompt:
d = return_error('Missing required parameter: prompt (text to synthesize)')
return json_response(d, status=400)
lctype = params_kw.catelogid
env = request._run_ns
async with get_sor_context(env, 'llmage') as sor:
# Look up llm by model name and catalog type through llm_api_map
sql = """select distinct a.* from llm a
join llm_api_map m on a.id = m.llmid
join llmcatelog b on m.llmcatelogid = b.id
where (b.id = ${lctype}$ OR b.name = ${lctype}$)
and a.model=${model}$
and a.status = 'published'"""
recs = await sor.sqlExe(sql, {
'lctype': lctype,
'model': params_kw.model
})
if len(recs) == 0:
debug(f'{params_kw.model=} not found for catalog {lctype}')
return openai_400()
params_kw.llmid = recs[0].id
debug(f'{params_kw.llmid=}')
# Check balance
f = await checkCustomerBalance(params_kw.llmid, userid, userorgid)
if not f:
debug(f'{userid=} balance not enough')
return openai_429()
# Generate task ID and attach to params
if not params_kw.transno:
params_kw.transno = getID()
# Call inference (TTS can be stream or sync depending on model)
return await inference(request, env=env)

View File

@ -1,71 +0,0 @@
# OpenAI-compatible Audio Transcription API (ASR)
# POST /v1/audio/transcriptions
# Required params: model, catelogid, audio_file (audio URL or base64)
# Optional params: language
#
# Example request:
# {
# "model": "qwen3-asr-flash",
# "catelogid": "asr",
# "audio_file": "https://example.com/audio.wav"
# }
#
# Response:
# {
# "text": "识别出的文本内容",
# "usage": { "duration_seconds": 5.2 }
# }
userid = await get_user()
userorgid = await get_userorgid()
if userid is None:
debug('need login')
return openai_403()
# Validate required parameters
if not params_kw.model:
d = return_error('Missing required parameter: model')
return json_response(d, status=400)
if not params_kw.catelogid:
d = return_error('Missing required parameter: catelogid')
return json_response(d, status=400)
if not params_kw.audio_file:
d = return_error('Missing required parameter: audio_file')
return json_response(d, status=400)
lctype = params_kw.catelogid
env = request._run_ns
async with get_sor_context(env, 'llmage') as sor:
# Look up llm by model name and catalog type through llm_api_map
sql = """select distinct a.* from llm a
join llm_api_map m on a.id = m.llmid
join llmcatelog b on m.llmcatelogid = b.id
where (b.id = ${lctype}$ OR b.name = ${lctype}$)
and a.model=${model}$
and a.status = 'published'"""
recs = await sor.sqlExe(sql, {
'lctype': lctype,
'model': params_kw.model
})
if len(recs) == 0:
debug(f'{params_kw.model=} not found for catalog {lctype}')
return openai_400()
params_kw.llmid = recs[0].id
debug(f'{params_kw.llmid=}')
# Check balance
f = await checkCustomerBalance(params_kw.llmid, userid, userorgid)
if not f:
debug(f'{userid=} balance not enough')
return openai_429()
# Generate task ID and attach to params
if not params_kw.transno:
params_kw.transno = getID()
# Call inference (ASR is synchronous)
return await inference(request, env=env)

View File

@ -9,8 +9,8 @@ async def gen():
async for l in f(): async for l in f():
yield l yield l
debug_params('params_kw', params_kw) debug(f'{params_kw=}')
catelogid = params_kw.catelogid or 't2t' lctype='文生文'
if params_kw.off_peak: if params_kw.off_peak:
off_peak = params_kw.off_peak off_peak = params_kw.off_peak
if off_peak in [True, "Y" "y", 1, "1"]: if off_peak in [True, "Y" "y", 1, "1"]:
@ -25,7 +25,7 @@ if userid is None:
return openai_403() return openai_403()
if not params_kw.prompt and not params_kw.messages: if not params_kw.prompt and not params_kw.messages:
debug(f'missing prompt and messages, model={params_kw.model}') debug(f'not params_kw.prompt and not params_kw.messages,{params_kw=}')
d = return_error('Missing need data(prompt or messages)') d = return_error('Missing need data(prompt or messages)')
return json_response(d, status=400) return json_response(d, status=400)
env = request._run_ns env = request._run_ns
@ -33,11 +33,10 @@ async with get_sor_context(env, 'llmage') as sor:
sql = """select distinct a.* from llm a sql = """select distinct a.* from llm a
join llm_api_map m on a.id = m.llmid join llm_api_map m on a.id = m.llmid
join llmcatelog b on m.llmcatelogid = b.id join llmcatelog b on m.llmcatelogid = b.id
where (b.id = ${catelogid}$ OR b.name = ${catelogid}$) where b.name = ${lctype}$
and a.model=${model}$ and a.model=${model}$"""
and a.status = 'published'"""
recs = await sor.sqlExe(sql, { recs = await sor.sqlExe(sql, {
'catelogid': catelogid, 'lctype': lctype,
'model': params_kw.model or 'qwen3-max' 'model': params_kw.model or 'qwen3-max'
}) })
if len(recs) == 0: if len(recs) == 0:

View File

@ -1,80 +0,0 @@
# OpenAI-compatible Image Generation API
# POST /v1/image/generations
# Required params: model, catelogid
# Optional params: prompt, image_url, n, size, style, quality, etc.
#
# Example request:
# {
# "model": "jimeng-4.0",
# "catelogid": "t2i",
# "prompt": "A beautiful sunset over the ocean",
# "size": "1024x1024",
# "n": 1
# }
#
# Response format depends on the upstream model (sync returns image data, async returns task info)
import json
import time
from functools import partial
from appPublic.log import debug
from appPublic.dictObject import DictObject
from appPublic.uniqueID import getID
from appPublic.timeUtils import curDateString, timestampstr
from sqlor.dbpools import get_sor_context
debug_params('params_kw', params_kw)
userid = await get_user()
userorgid = await get_userorgid()
if userid is None:
debug('need login')
return openai_403()
# Validate required parameters
if not params_kw.model:
d = return_error('Missing required parameter: model')
return json_response(d, status=400)
if not params_kw.catelogid:
d = return_error('Missing required parameter: catelogid')
return json_response(d, status=400)
if not params_kw.prompt:
d = return_error('Missing required parameter: prompt')
return json_response(d, status=400)
lctype = params_kw.catelogid
env = request._run_ns
async with get_sor_context(env, 'llmage') as sor:
# Look up llm by model name and catalog type through llm_api_map
sql = """select distinct a.* from llm a
join llm_api_map m on a.id = m.llmid
join llmcatelog b on m.llmcatelogid = b.id
where (b.id = ${lctype}$ OR b.name = ${lctype}$)
and a.model=${model}$
and a.status = 'published'"""
recs = await sor.sqlExe(sql, {
'lctype': lctype,
'model': params_kw.model
})
if len(recs) == 0:
debug(f'{params_kw.model=} not found for catalog {lctype}')
return openai_400()
params_kw.llmid = recs[0].id
debug(f'{params_kw.llmid=}')
# Check balance
f = await checkCustomerBalance(params_kw.llmid, userid, userorgid)
if not f:
debug(f'{userid=} balance not enough')
return openai_429()
# Generate task ID and attach to params
if not params_kw.transno:
params_kw.transno = getID()
# Call inference (image generation can be sync or async depending on model config)
return await inference(request, env=env)

View File

@ -1,23 +0,0 @@
# GET /v1/models/catelog
# List published models by catalog, optionally exclude one model
# Params: catelogid (required), exclude_id (optional)
catelogid = params_kw.catelogid
if not catelogid:
return json.dumps({'error': 'catelogid is required'})
exclude_id = params_kw.exclude_id or ''
dbname = get_module_dbname('llmage')
db = DBPools()
async with db.sqlorContext(dbname) as sor:
sql = """select distinct a.* from llm a
join llm_api_map m on a.id = m.llmid
where m.llmcatelogid = ${catelogid}$ and a.status = 'published'
"""
ns = {'catelogid': catelogid}
if exclude_id:
sql += " and a.id != ${exclude_id}$"
ns['exclude_id'] = exclude_id
recs = await sor.sqlExe(sql, ns)
for r in recs.get('rows', []):
r.description = json.dumps(r.description)
return recs
return []

View File

@ -6,9 +6,9 @@ def get_time_in_seconds(datestr):
timestamp = dt_obj.timestamp() timestamp = dt_obj.timestamp()
return timestamp return timestamp
catelogid = params_kw.catelogid lctype=params_kw.lctype
orderby = params_kw.orderby or 'model' orderby=params_kw.orderby or 'model'
rets = await get_llms_by_catelog_to_customer(catelogid=catelogid, orderby=orderby) rets = await get_llms_by_catelog_to_customer(catelogid=lctype, orderby=orderby)
ret = { ret = {
"object": "list", "object": "list",
"data": [] "data": []

View File

@ -1,80 +0,0 @@
# OpenAI-compatible Music Generation API
# POST /v1/music/generations
# Required params: model, catelogid, prompt, lyrics
# Optional params: output_format, audio_setting
#
# Example request:
# {
# "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"
# }
#
# Response (sync for MiniMax):
# {
# "id": "luid_xxx",
# "object": "music.generation",
# "model": "music-2.6",
# "status": "SUCCEEDED",
# "audio": "https://...",
# "created": 1234567890
# }
userid = await get_user()
userorgid = await get_userorgid()
if userid is None:
debug('need login')
return openai_403()
# Validate required parameters
if not params_kw.model:
d = return_error('Missing required parameter: model')
return json_response(d, status=400)
if not params_kw.catelogid:
d = return_error('Missing required parameter: catelogid')
return json_response(d, status=400)
if not params_kw.prompt:
d = return_error('Missing required parameter: prompt')
return json_response(d, status=400)
if not params_kw.lyrics:
d = return_error('Missing required parameter: lyrics')
return json_response(d, status=400)
lctype = params_kw.catelogid
env = request._run_ns
async with get_sor_context(env, 'llmage') as sor:
# Look up llm by model name and catalog type through llm_api_map
sql = """select distinct a.* from llm a
join llm_api_map m on a.id = m.llmid
join llmcatelog b on m.llmcatelogid = b.id
where (b.id = ${lctype}$ OR b.name = ${lctype}$)
and a.model=${model}$
and a.status = 'published'"""
recs = await sor.sqlExe(sql, {
'lctype': lctype,
'model': params_kw.model
})
if len(recs) == 0:
debug(f'{params_kw.model=} not found for catalog {lctype}')
return openai_400()
params_kw.llmid = recs[0].id
debug(f'{params_kw.llmid=}')
# Check balance
f = await checkCustomerBalance(params_kw.llmid, userid, userorgid)
if not f:
debug(f'{userid=} balance not enough')
return openai_429()
# Generate task ID and attach to params
if not params_kw.transno:
params_kw.transno = getID()
# Call inference (music generation via MiniMax is synchronous)
return await inference(request, env=env)

View File

@ -1,34 +0,0 @@
# GET /llmage/v1/pricing
# Get model pricing display information
# Required params: model (model name, e.g. qwen3.7-max)
# Optional params: catelogid (default: t2t)
#
# Example: /llmage/v1/pricing?model=qwen3.7-max
# Returns: { "status": "ok", "data": { "display_text": "...", ... } }
model = params_kw.model
if not model:
return json.dumps({"status": "error", "message": "model parameter required"}, ensure_ascii=False)
catelogid = params_kw.catelogid or 't2t'
env = request._run_ns
try:
async with get_sor_context(env, 'llmage') as sor:
sql = """select m.ppid from llm a
join llm_api_map m on a.id = m.llmid
where a.model = ${model}$
and a.status = 'published'
and m.ppid is not null
and m.isdefaultcatelog = '1'
"""
recs = await sor.sqlExe(sql, {'model': model})
if len(recs) == 0:
return json.dumps({"status": "error", "message": f"model '{model}' not found or has no pricing"}, ensure_ascii=False)
ppid = recs[0].ppid
result = await env.get_pricing_display(ppid)
return json.dumps({"status": "ok", "data": result}, ensure_ascii=False, default=str)
except Exception as e:
exception(f'get pricing for {model} failed: {e}\n{format_exc()}')
return json.dumps({"status": "error", "message": str(e)}, ensure_ascii=False)

View File

@ -1,88 +0,0 @@
# OpenAI-compatible Video Generation API
# POST /v1/video/generations
# Required params: model, catelogid
# Optional params: prompt, image_url, duration, resolution, n, etc.
#
# Example request:
# {
# "model": "keling-2.1",
# "catelogid": "t2v",
# "prompt": "A beautiful sunset over the ocean",
# "duration": "5s",
# "resolution": "1080p"
# }
#
# Response (async task):
# {
# "id": "vid_xxx",
# "object": "video.generation",
# "model": "keling-2.1",
# "status": "submitted",
# "taskid": "task_xxx",
# "created": 1234567890
# }
import json
import time
from functools import partial
from appPublic.log import debug
from appPublic.dictObject import DictObject
from appPublic.uniqueID import getID
from appPublic.timeUtils import curDateString, timestampstr
from sqlor.dbpools import get_sor_context
debug_params('params_kw', params_kw)
userid = await get_user()
userorgid = await get_userorgid()
if userid is None:
debug('need login')
return openai_403()
# Validate required parameters
if not params_kw.model:
d = return_error('Missing required parameter: model')
return json_response(d, status=400)
if not params_kw.catelogid:
d = return_error('Missing required parameter: catelogid')
return json_response(d, status=400)
if not params_kw.prompt:
d = return_error('Missing required parameter: prompt')
return json_response(d, status=400)
lctype = params_kw.catelogid
env = request._run_ns
async with get_sor_context(env, 'llmage') as sor:
# Look up llm by model name and catalog type through llm_api_map
sql = """select distinct a.* from llm a
join llm_api_map m on a.id = m.llmid
join llmcatelog b on m.llmcatelogid = b.id
where (b.id = ${lctype}$ OR b.name = ${lctype}$)
and a.model=${model}$
and a.status = 'published'"""
recs = await sor.sqlExe(sql, {
'lctype': lctype,
'model': params_kw.model
})
if len(recs) == 0:
debug(f'{params_kw.model=} not found for catalog {lctype}')
return openai_400()
params_kw.llmid = recs[0].id
debug(f'{params_kw.llmid=}')
# Check balance
f = await checkCustomerBalance(params_kw.llmid, userid, userorgid)
if not f:
debug(f'{userid=} balance not enough')
return openai_429()
# Generate task ID and attach to params
if not params_kw.transno:
params_kw.transno = getID()
# Call inference (video/image generation is typically async via callback)
return await inference(request, env=env)

View File

@ -1,4 +1,4 @@
debug_params('params_kw', params_kw) debug(f'{params_kw=}')
if params_kw.off_peak: if params_kw.off_peak:
off_peak = params_kw.off_peak off_peak = params_kw.off_peak
if off_peak in [True, "Y" "y", 1, "1"]: if off_peak in [True, "Y" "y", 1, "1"]: