接入 LLM Router 累计额度结算
按 Router used_quota 累计值与首次基线结算泥点 新增原子 checkpoint 事务及 llm_router_consume 钱包流水 同步额度查询校验、前端展示、生成绑定和技术文档
This commit is contained in:
@@ -19,6 +19,8 @@
|
||||
|
||||
## AI 游戏创作与 Agent Runtime
|
||||
|
||||
- [LLM 累计额度结算](./technical/【技术方案】LLM累计额度结算-2026-09-05.md):Router 累计额度、首次基线与原子钱包结算。
|
||||
|
||||
- [AI 游戏创作智能体 App 实施计划](./technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md):当前 DirectProject、受控语义工具、UI workflow、资源和运行时合同。
|
||||
- [DirectProject 客户端 Skill 与 MCP 扩展导入方案](./technical/【技术方案】DirectProject客户端Skill与MCP扩展导入方案-2026-08-31.md):客户端扩展导入、按独立 Skill/MCP 拆分、命名、启用和启动时注入边界。
|
||||
- [AGC 客户端更新检查与下载](./technical/【技术方案】AGC客户端更新检查与下载-2026-08-31.md):启动版本检测、OSS 清单格式和下载约定。
|
||||
|
||||
@@ -7997,7 +7997,7 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
|
||||
|
||||
- 每个 Genarrative 用户在认证成功后都必须幂等准备独立 Router 账号:api-server 使用管理员 Token 创建随机密码普通用户,查询用户 ID,设置用户 `group=taonier`,登录、创建或复用固定标识 `agc_auto_generate` 的无限额度 Token(Token/API Key 使用 `default` 分组;发现旧 Token 为其它分组时先更新为 `default`)并签发 API Key。Router 账号用户名、随机密码、access token(如需)和 API Key 作为一个服务端加密 bundle 保存到 `llm_router_account.credential_ciphertext`,脱敏账号信息和 API Key 核心字段保存到 `llm_router_account`;客户端和普通用户永远不可见 Router Key。管理员 Token 仅存在 api-server 私有配置,不写入数据库或日志;Router 凭据只来源于这条正式账号流程。
|
||||
- 该账号 provisioning 使用持久 saga 状态:远端注册、登录、token 或 Key 签发结果不确定时进入 `unknown` / `reconciliation_required`,禁止重复注册;远端 Key 已确定签发但本地 `llm_router_account` 写入失败时保持 `key_issued`,后续使用确定 key id 重试落库。Router 确定返回 401/403 时撤销当前 Key 并把账号状态置为 `retryable`,复用已保存的账号密码重新签发替代 Key。
|
||||
- AGC 调用固定为客户端 access token -> api-server -> Router。Router 成功返回后再扣泥点,按临时规则每开始 10,000 token 扣 1 点且至少 1 点;ledger id 由 `Idempotency-Key`(缺省请求 ID)哈希得到,Router 失败不扣费。余额足够时全额扣除;余额不足时按当前可消费余额扣光,差额记为赠送并继续返回已成功的 LLM 响应。该计费规则是过渡实现,待产品定价确认后替换,不能视为 Router 真实成本结算。
|
||||
- AGC 调用固定为客户端 access token -> api-server -> Router。计费读取账号 `used_quota`,每 50000 quota 扣 1 泥点,美元数值乘 10、不乘汇率。首次模型调用前以当前累计额度完整建立免追扣基线,之后调用前后同步;扣钱包、写 `llm_router_consume` 流水与推进已结算额度同事务完成。小数和余额不足未支付部分继续累计,失败或重复同步不推进已结算额度,不使用本地 WAL 或余数队列。完整合同见 `docs/technical/【技术方案】LLM累计额度结算-2026-09-05.md`。
|
||||
- AGC 状态面收口:Tauri `check_game_creator_llm_config`、`/llm-status` 与 `/llm-routes` 只返回账号凭据状态、官方路由锁定状态和运行参数;不序列化 Router 地址、模型、协议名或任何密钥/凭据字段,内部固定路由仅留在运行时配置与服务端代理中。
|
||||
|
||||
## 2026-09-01 LLM Router provisioning 环境隔离与测试门禁
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
# LLM 累计额度结算
|
||||
|
||||
## 目标与边界
|
||||
|
||||
使用 New API `GET /api/user/{id}` 的 `used_quota` 累计值结算,不按 token 估价,不依赖单次响应 cost,不建立本地 WAL、余数 Map 或延迟队列。模型调用前同步并检查余额,成功响应后再同步;失败、断流或进程退出留下的消耗由下一次调用前同步补结算。不承诺无后续调用的闲置账号立即结清。
|
||||
|
||||
## 单位与算法
|
||||
|
||||
Router `GET /api/status` 的 `data.quota_per_unit` 已实测为 500000,即每美元 500000 quota。美元数值直接乘 10 转泥点,不使用 USDExchangeRate。每泥点对应 50000 quota,采用整数运算。查询时验证单位不变,异常时拒绝结算,不静默改价。
|
||||
|
||||
```text
|
||||
pending = max(observed_used_quota - settled_quota, 0)
|
||||
charged_points = min(pending / 50000, spendable_points)
|
||||
next_settled_quota = settled_quota + charged_points * 50000
|
||||
```
|
||||
|
||||
`settled_quota` 只推进实际扣费对应的部分。查询失败、钱包被冻结、扣款失败时不推进。重复或乱序快照不回退游标;Router 重置累计值需人工核对,不自动清零本地记录。
|
||||
|
||||
## 持久化与事务
|
||||
|
||||
新增私有表 `llm_router_billing_checkpoint`,主键沿用已认证用户的 `llm_router_account.account_key`,保存 `router_user_id`、`settled_quota` 和更新时间。账号 owner/route 来自现有本地映射,不信任客户端提交的 Router ID;同一映射更换 Router ID 时拒绝结算,需明确迁移。
|
||||
|
||||
首次同步在模型请求之前执行:若没有 checkpoint,以当前累计额度完整建立基线,不扣历史,不抹零。现有账号与首次接入账号均在使用前建立基线。
|
||||
|
||||
只有服务身份可以调用结算 procedure。事务内先检查人工冻结与退款欠款限制(包括尚不足一整点与首次初始化),再读取 checkpoint,计算可扣金额,更新钱包和流水,最后推进 checkpoint;三者同一事务提交。返回的剩余可消费余额扣除了退款占用与本次消费,模型调用前据此拒绝零可用余额。并发、重复快照和响应丢失后重试不能重复扣费。流水 ID 由账号与已结算额度区间构成,而非单次请求 ID。
|
||||
|
||||
新增流水来源 `llm_router_consume`,显示“LLM 调用消耗”,保持免费/会员/永久泥点消耗顺序、退款冻结和消费统计。历史资产来源流水不改写;旧资产生成扣费/退款协议不变。
|
||||
|
||||
## 验收
|
||||
|
||||
- 首次基线、234 余量跨次保留、重复/乱序快照、余额不足、零余额、整数溢出均有定向测试。
|
||||
- Router 查询验证业务 success、用户 ID、非负整数 used_quota、quota_per_unit;错误不打印凭据或原始用户数据。
|
||||
- 模型请求前基线失败关闭;成功模型响应不因后置同步故障变为失败,下一次可重试。
|
||||
- schema 同步 migration、表目录、生成绑定,运行定向 Rust 测试、schema guard、编码检查、diff check。
|
||||
- 本次不部署、不迁移历史钱包流水、不修改外部 OpenAPI。
|
||||
@@ -633,11 +633,11 @@ Responses 的终态载荷既是工具调用的恢复源,也是正文的恢复
|
||||
- 源码:`server-rs/crates/spacetime-module/src/external_api_key_storage.rs`
|
||||
- 说明:本表只承载普通外部 OpenAPI/MCP API Key。明文只在 `/api/profile/api-keys` 创建接口返回一次,服务端保存 `key_hash`、`key_prefix`、作用域和撤销状态;本需求不向该表增加 Router 字段,也不写入 Router 账号数据。v1 默认作用域为 `editor:project`、`editor:canvas`、`editor:image-generate`、`editor:asset`。
|
||||
- 索引:`by_external_api_key_owner_user_id` 用于登录态 API Key 列表;`key_hash` 唯一索引用于外部 API 鉴权。
|
||||
- 2026-08-31 修订:Router 账号状态、API Key 核心字段、账号元数据和加密凭据统一写入 `llm_router_account`;`external_api_key` 保持普通外部 OpenAPI/MCP Key 的原有链路不变。公共 Router、独立数据库的部署必须共享同一版本 provisioning secret,数据库缺行时才能恢复同一远端账号。Responses 请求成功后再按 usage 写入钱包扣费流水,失败请求不扣费。
|
||||
- 2026-08-31 修订:Router 账号状态、API Key 核心字段、账号元数据和加密凭据统一写入 `llm_router_account`;`external_api_key` 保持普通外部 OpenAPI/MCP Key 的原有链路不变。公共 Router、独立数据库的部署必须共享同一版本 provisioning secret,数据库缺行时才能恢复同一远端账号。LLM 计费的当前权威口径是下方 2026-09-05 累计额度结算规则。
|
||||
- 2026-09-01 修订:Router provisioning 允许所有环境使用官方固定 Router 控制面,以便独立开发数据库通过完整 owner `user_id` 的稳定派生凭据和 `agc_auto_generate` Token 复用同一远端账号/Key。由于 New API 的 `username`、`password`、`display_name` 均限制 20 个字符,用户名固定为 `agc_user_` 加 11 位 URL-safe SHA-256 短码,密码为基于完整 owner `user_id` 与 provisioning secret 派生的 20 位 hex;完整 owner `user_id` 通过 New API 用户 `remark` 字段保存,并在本地 `llm_router_account.owner_user_id` 保留权威映射。非官方公网地址仍拒绝,loopback 仅用于本地 fixture。使用共享官方 Router 的非生产环境启动时告警,提醒会触及线上账号与额度。api-server 不再提供任何 fallback Key 路径;没有已完成 provisioning 的账号行时,LLM 请求必须在本地解析阶段失败关闭。
|
||||
- 2026-09-01 修订:每次账号认证、Router Key 准备或 LLM 请求解析既有账号时,api-server 都会在同一 owner 级 provisioning 锁内检查固定套餐 `plan_id=1`。没有 active 订阅、订阅已过期或 `end_time` 距当前 Unix 秒不超过 24 小时时,使用管理员接口新建一条订阅;超过 24 小时则复用现有订阅。订阅检查不进入 Responses 流式 chunk,管理员 Token 缺失时仅保留启动告警并跳过续期检查。
|
||||
- 2026-09-02 修订:LLM Router 成功返回后按“每开始 10,000 token 扣 1 点、至少 1 点”后置结算。余额足够时全额扣除;余额不足时在同一钱包事务内扣除当前可消费余额并记录 `waivedPoints` 差额,响应仍正常返回。该规则为过渡产品策略,待真实定价接入后替换。
|
||||
- 2026-09-02 修订:`/api/llm/responses` 与 `/api/llm/chat/completions` 在解析 Router 凭据和发起上游请求前先读取用户 `wallet_balance`;余额为 `0` 时直接返回 `409 MUD_POINTS_INSUFFICIENT`,不创建、续期或使用 Router 账号。余额读取失败同样失败关闭,返回泥点余额暂不可用。
|
||||
- LLM Router 计费:读取 Router 用户 `used_quota`,以 `50000 quota = 1 泥点` 结算(`500000 quota = 1 USD`,美元数值直接乘 10,不乘汇率)。上游调用前建立首次基线并补结算,成功响应后同步;不足整数部分、扣费失败和余额不足未支付部分留到后续累计同步。扣钱包、写 `llm_router_consume` 流水与推进 checkpoint 在同一事务完成;流水展示“LLM 调用消耗”。历史费用不追扣。详细契约见 `technical/【技术方案】LLM累计额度结算-2026-09-05.md`。
|
||||
- 2026-09-05 修订:`/api/llm/responses` 与 `/api/llm/chat/completions` 仍在 Router provisioning 前用钱包总余额阻止零余额账号创建或续期;解析凭据后、上游调用前再执行累计额度同步,以扣除退款占用后的剩余可消费余额为准。余额为 `0` 时返回 `409 MUD_POINTS_INSUFFICIENT`;余额或额度同步失败时失败关闭。上游已成功时,后置同步失败只记录错误并留待下次调用前补结算,不把成功模型响应改写为失败。
|
||||
- Windows 私有文件准备:AGC 自有 AppData、凭据目录和 `.agent` 运行态继续使用 managed 范围;用户通过原生选择器明确选中的项目根或文件,若 owner/DACL 仅因权限不足无法读取,则由一次性 UAC helper 在严格复核普通文件/目录、非 reparse/symlink、路径类型和目标 TokenUser 后接管并收紧为当前用户私有 DACL。项目放在当前 profile 之外(例如其他磁盘)不再因为路径位置被拒绝;未经过原生选择器或 AGC 项目根入口的内部路径仍不获得任意提权资格。
|
||||
|
||||
### `llm_router_account`
|
||||
@@ -649,6 +649,13 @@ Responses 的终态载荷既是工具调用的恢复源,也是正文的恢复
|
||||
- 作用:记录用户与官方 LLM Router 账号的稳定映射、API Key 核心字段、账号生命周期、远端账号元数据、订阅检查状态、重试 / 对账状态和加密凭据版本。Router 明文 Key 只在 api-server 进程内短暂存在,数据库仅保存加密凭据;账号读取和写入统一通过对应 procedure 与 `spacetime-client` facade 完成。
|
||||
- 索引:`by_llm_router_account_owner_route` 用于按 owner 与路由来源读取账号;`by_llm_router_account_status_next_retry` 用于按状态和下次重试时间扫描待对账账号。
|
||||
|
||||
### `llm_router_billing_checkpoint`
|
||||
|
||||
- Rust 结构体:`LlmRouterBillingCheckpoint`
|
||||
- 源码:`server-rs/crates/spacetime-module/src/runtime/active/profile.rs`
|
||||
- 私有表,主键 `account_key` 关联 `llm_router_account`,保存 `router_user_id`、`settled_quota`、`updated_at`。
|
||||
- `settle_llm_router_quota_and_return` 仅允许服务身份,验证 owner/route/Router ID;首次同步完整记录当前额度而不扣历史,后续仅按实际扣款推进额度,与钱包和流水同事务提交。旧快照不回退;账号更换拒绝自动重建。
|
||||
|
||||
### `agc_model_catalog`
|
||||
|
||||
- 私有单例表,主键 `id=0`,保存 `catalog_json`、`revision`、`updated_at`;不存凭据。
|
||||
|
||||
@@ -226,7 +226,7 @@ AGC loopback Provider Proxy(Bearer=平台 access token)
|
||||
-> POST https://router.genarrative.world/v1/responses(Bearer=<router-api-key>)
|
||||
```
|
||||
|
||||
`/api/llm/responses` 强制覆盖 `model=gpt-5.6-sol`,透传 Responses JSON/SSE 响应;Router 成功返回后按 usage 以临时规则“每开始 10,000 token 扣 1 泥点、至少 1 点”写入钱包流水,ledger id 由端点、客户端幂等键和请求指纹稳定派生,Router 失败不扣费。Router 明确返回 `401/403` 时,服务端将该用途 Key 标记撤销;网络超时或 provisioning 未取得确定响应时不自动重试。客户端不保存 Router Key,也不把它放进 argv、环境变量、manifest、trace、聊天或普通 IPC。AGC 的运行状态接口与 `/llm-status`、`/llm-routes` 只返回登录/账号凭据状态、官方路由锁定状态和运行参数,不返回 Router 地址、模型、协议名称或任何凭据字段。
|
||||
`/api/llm/responses` 透传 Responses JSON/SSE 响应;模型由当前 AGC 模型目录解析。计费读取 Router 子账号的累计 `used_quota`,按 `50000 quota = 1 泥点` 在调用前后同步;首次同步只建立历史基线,扣钱包、写 `llm_router_consume` 流水与推进 checkpoint 在同一事务完成。Router 明确返回 `401/403` 时,服务端将该用途 Key 标记撤销;网络超时或 provisioning 未取得确定响应时不自动重试。客户端不保存 Router Key,也不把它放进 argv、环境变量、manifest、trace、聊天或普通 IPC。AGC 的运行状态接口与 `/llm-status`、`/llm-routes` 只返回登录/账号凭据状态、官方路由锁定状态和运行参数,不返回 Router 地址、模型、协议名称或任何凭据字段。
|
||||
|
||||
Windows 私有路径由同一套正式准备入口复用:AGC 自有 AppData、凭据目录和 `.agent` 运行态使用 managed 范围;原生文件选择器明确选中的项目根/文件使用 user-selected 范围。两类入口在发现 owner/DACL 权限不足时均允许一次性 UAC helper;helper 只接管严格复核后的普通文件/目录,并写入当前 TokenUser owner、禁止继承且仅含当前用户 ACE 的 DACL。reparse/symlink、非普通对象、祖先类型冲突和未经过正式选择/项目根入口的路径仍失败关闭。
|
||||
|
||||
@@ -293,4 +293,4 @@ docs/openapi/genarrative-external-v1.openapi.json
|
||||
- 外部项目接口覆盖当前已有项目管理操作:项目列表、最近项目、创建、读取、重命名、删除和默认画布保存。
|
||||
- 外部素材生成接口覆盖当前已有编辑器素材操作:生图、重绘 / 调整、规范图生成、宣发素材生成、图标素材生成与拆分、UI 设计图生成与拆分、角色动画、视频、音效和背景音乐。
|
||||
- 修改 SpacetimeDB schema 后运行 `npm run spacetime:generate` 与 `npm run check:spacetime-schema`。
|
||||
> 2026-09-03 修订:认证成功后的 Router provisioning 改为异步尽力修复,不再阻塞主站登录;LLM 请求只使用本地已完成的账号密钥。LLM Router 计费按每开始 10,000 token 至少 1 点结算,客户端幂等键同时绑定端点与规范化请求指纹,禁止跨请求复用账本。账号密码派生根仅从部署侧受保护 secret/file 读取。
|
||||
> 2026-09-05 修订:认证成功后的 Router provisioning 为异步尽力修复,不阻塞主站登录;LLM 请求只使用本地已完成的账号密钥。LLM Router 计费改为累计 `used_quota` checkpoint 结算,不再依赖单次响应 usage、客户端幂等键或请求指纹;账号密码派生根仅从部署侧受保护 secret/file 读取。
|
||||
|
||||
@@ -102,6 +102,7 @@ test('builds wallet ledger presentation with stable source fallbacks', () => {
|
||||
'资产操作消耗',
|
||||
);
|
||||
expect(getWalletLedgerSourceLabel('future_source')).toBe('future_source');
|
||||
expect(getWalletLedgerSourceLabel('llm_router_consume')).toBe('LLM 调用消耗');
|
||||
expect(getWalletLedgerSourceLabel('')).toBe('未知来源');
|
||||
expect(formatWalletLedgerDate('not-a-date')).toBe('not-a-date');
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ const PROFILE_WALLET_LEDGER_SOURCE_LABELS = {
|
||||
daily_free_grant: '每日免费发放',
|
||||
daily_free_reset: '每日免费重置',
|
||||
asset_operation_consume: '资产操作消耗',
|
||||
llm_router_consume: 'LLM 调用消耗',
|
||||
asset_operation_refund: '资产操作退回',
|
||||
recharge_refund_recovery: '充值退款追回',
|
||||
redeem_code_reward: '兑换码奖励',
|
||||
|
||||
@@ -77,6 +77,7 @@ export type ProfileWalletLedgerEntry = {
|
||||
| 'daily_free_grant'
|
||||
| 'daily_free_reset'
|
||||
| 'asset_operation_consume'
|
||||
| 'llm_router_consume'
|
||||
| 'asset_operation_refund'
|
||||
| 'recharge_refund_recovery'
|
||||
| 'redeem_code_reward'
|
||||
|
||||
@@ -2707,7 +2707,7 @@ async fn fetch_admin_dashboard_wallet_stats(
|
||||
continue;
|
||||
}
|
||||
match source_type.as_str() {
|
||||
"asset_operation_consume" if amount_delta < 0 => {
|
||||
"asset_operation_consume" | "llm_router_consume" if amount_delta < 0 => {
|
||||
stats
|
||||
.consumed_mud_points
|
||||
.add(day_key, amount_delta.unsigned_abs());
|
||||
@@ -4483,6 +4483,12 @@ fn wallet_ledger_source_type_to_string(value: &Value) -> Option<String> {
|
||||
7 => "redeem_code_reward",
|
||||
8 => "puzzle_author_incentive_claim",
|
||||
9 => "daily_task_reward",
|
||||
10 => "membership_period_grant",
|
||||
11 => "membership_period_reset",
|
||||
12 => "daily_free_grant",
|
||||
13 => "daily_free_reset",
|
||||
14 => "recharge_refund_recovery",
|
||||
15 => "llm_router_consume",
|
||||
_ => return Some(Value::Array(items.to_vec()).to_string()),
|
||||
}
|
||||
.to_string(),
|
||||
@@ -5895,6 +5901,10 @@ mod tests {
|
||||
wallet_ledger_source_type_to_string(&json!("AssetOperationConsume")).as_deref(),
|
||||
Some("asset_operation_consume")
|
||||
);
|
||||
assert_eq!(
|
||||
wallet_ledger_source_type_to_string(&json!([15, []])).as_deref(),
|
||||
Some("llm_router_consume")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -897,7 +897,7 @@ async fn ensure_router_subscription(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn router_user_id_from_account(record: &LlmRouterAccountRecord) -> Option<i64> {
|
||||
pub(crate) fn router_user_id_from_account(record: &LlmRouterAccountRecord) -> Option<i64> {
|
||||
record
|
||||
.router_account_id
|
||||
.as_deref()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -190,6 +190,9 @@ fn format_profile_wallet_ledger_source_type(
|
||||
RuntimeProfileWalletLedgerSourceType::AssetOperationConsume => {
|
||||
PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_OPERATION_CONSUME
|
||||
}
|
||||
RuntimeProfileWalletLedgerSourceType::LlmRouterConsume => {
|
||||
shared_contracts::runtime::PROFILE_WALLET_LEDGER_SOURCE_TYPE_LLM_ROUTER_CONSUME
|
||||
}
|
||||
RuntimeProfileWalletLedgerSourceType::AssetOperationRefund => {
|
||||
PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_OPERATION_REFUND
|
||||
}
|
||||
@@ -2566,6 +2569,12 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn profile_wallet_ledger_source_type_formats_backend_values() {
|
||||
assert_eq!(
|
||||
format_profile_wallet_ledger_source_type(
|
||||
RuntimeProfileWalletLedgerSourceType::LlmRouterConsume
|
||||
),
|
||||
"llm_router_consume"
|
||||
);
|
||||
assert_eq!(
|
||||
format_profile_wallet_ledger_source_type(
|
||||
RuntimeProfileWalletLedgerSourceType::NewUserRegistrationReward
|
||||
|
||||
@@ -2124,21 +2124,6 @@ pub fn validate_runtime_profile_wallet_debit_availability(
|
||||
Ok(spendable_points.saturating_sub(debit_points))
|
||||
}
|
||||
|
||||
/// LLM Router settles after a successful upstream response. It may consume
|
||||
/// only the currently spendable portion of the wallet; any remainder of the
|
||||
/// requested amount is intentionally waived by the product policy.
|
||||
pub fn calculate_runtime_profile_wallet_best_effort_debit(
|
||||
requested_points: i64,
|
||||
wallet_total_points: u64,
|
||||
active_held_points: u64,
|
||||
) -> i64 {
|
||||
requested_points.max(0).min(
|
||||
wallet_total_points
|
||||
.saturating_sub(active_held_points)
|
||||
.min(i64::MAX as u64) as i64,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn build_runtime_profile_recharge_refund_settlement_plan(
|
||||
current_successful_refund_count: u32,
|
||||
current_cumulative_success_refund_cents: u64,
|
||||
@@ -2602,26 +2587,6 @@ fn parse_optional_json_value(
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn llm_router_best_effort_debit_only_consumes_current_spendable_balance() {
|
||||
assert_eq!(
|
||||
calculate_runtime_profile_wallet_best_effort_debit(31, 100, 20),
|
||||
31
|
||||
);
|
||||
assert_eq!(
|
||||
calculate_runtime_profile_wallet_best_effort_debit(31, 11, 0),
|
||||
11
|
||||
);
|
||||
assert_eq!(
|
||||
calculate_runtime_profile_wallet_best_effort_debit(31, 31, 20),
|
||||
11
|
||||
);
|
||||
assert_eq!(
|
||||
calculate_runtime_profile_wallet_best_effort_debit(31, 20, 20),
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn feature_gate_denies_anonymous_when_enabled() {
|
||||
let gate = test_gate("creation-entry:puzzle");
|
||||
|
||||
@@ -1151,6 +1151,7 @@ pub enum RuntimeProfileWalletLedgerSourceType {
|
||||
DailyFreeGrant,
|
||||
DailyFreeReset,
|
||||
RechargeRefundRecovery,
|
||||
LlmRouterConsume,
|
||||
}
|
||||
|
||||
impl RuntimeProfileWalletLedgerSourceType {
|
||||
@@ -1171,6 +1172,7 @@ impl RuntimeProfileWalletLedgerSourceType {
|
||||
Self::DailyFreeGrant => "daily_free_grant",
|
||||
Self::DailyFreeReset => "daily_free_reset",
|
||||
Self::RechargeRefundRecovery => "recharge_refund_recovery",
|
||||
Self::LlmRouterConsume => "llm_router_consume",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
mod agc_models;
|
||||
mod llm_billing;
|
||||
pub use llm_billing::*;
|
||||
mod application;
|
||||
pub use agc_models::*;
|
||||
mod commands;
|
||||
@@ -928,6 +930,10 @@ mod tests {
|
||||
RuntimeProfileWalletLedgerSourceType::RechargeRefundRecovery.as_str(),
|
||||
"recharge_refund_recovery"
|
||||
);
|
||||
assert_eq!(
|
||||
RuntimeProfileWalletLedgerSourceType::LlmRouterConsume.as_str(),
|
||||
"llm_router_consume"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1362,6 +1368,15 @@ mod tests {
|
||||
assert!(
|
||||
validate_runtime_profile_wallet_debit_restrictions(-1, consume, false, true).is_err()
|
||||
);
|
||||
assert!(
|
||||
validate_runtime_profile_wallet_debit_restrictions(
|
||||
-1,
|
||||
RuntimeProfileWalletLedgerSourceType::LlmRouterConsume,
|
||||
true,
|
||||
false,
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
assert!(
|
||||
validate_runtime_profile_wallet_debit_restrictions(
|
||||
-1,
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
/// Router quota per mud point, with USD numeric cost multiplied by ten.
|
||||
pub const LLM_ROUTER_QUOTA_PER_POINT: u64 = 50_000;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct LlmQuotaSettlement {
|
||||
pub charged_points: u64,
|
||||
pub settled_quota: u64,
|
||||
}
|
||||
|
||||
pub fn calculate_llm_quota_settlement(
|
||||
baseline: Option<u64>,
|
||||
observed: u64,
|
||||
spendable_points: u64,
|
||||
) -> LlmQuotaSettlement {
|
||||
let Some(baseline) = baseline else {
|
||||
return LlmQuotaSettlement {
|
||||
charged_points: 0,
|
||||
settled_quota: observed,
|
||||
};
|
||||
};
|
||||
let points = (observed.saturating_sub(baseline) / LLM_ROUTER_QUOTA_PER_POINT)
|
||||
.min(spendable_points)
|
||||
.min(i64::MAX as u64);
|
||||
LlmQuotaSettlement {
|
||||
charged_points: points,
|
||||
settled_quota: baseline + points * LLM_ROUTER_QUOTA_PER_POINT,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn initializes_without_charging_history() {
|
||||
assert_eq!(
|
||||
calculate_llm_quota_settlement(None, 123_456, 100),
|
||||
LlmQuotaSettlement {
|
||||
charged_points: 0,
|
||||
settled_quota: 123_456
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn carries_fraction_and_replays_without_double_charge() {
|
||||
let first = calculate_llm_quota_settlement(Some(0), 61_700, 100);
|
||||
assert_eq!(first.charged_points, 1);
|
||||
assert_eq!(first.settled_quota, 50_000);
|
||||
let next = calculate_llm_quota_settlement(Some(first.settled_quota), 111_700, 99);
|
||||
assert_eq!(next.charged_points, 1);
|
||||
assert_eq!(next.settled_quota, 100_000);
|
||||
assert_eq!(
|
||||
calculate_llm_quota_settlement(Some(next.settled_quota), 111_700, 98).charged_points,
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
calculate_llm_quota_settlement(Some(next.settled_quota), 61_700, 98).settled_quota,
|
||||
100_000
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insufficient_balance_only_advances_paid_quota() {
|
||||
let first = calculate_llm_quota_settlement(Some(1234), 201_234, 1);
|
||||
assert_eq!(first.settled_quota, 51_234);
|
||||
assert_eq!(
|
||||
calculate_llm_quota_settlement(Some(first.settled_quota), 201_234, 0).settled_quota,
|
||||
51_234
|
||||
);
|
||||
assert_eq!(
|
||||
calculate_llm_quota_settlement(Some(first.settled_quota), 201_234, 10).charged_points,
|
||||
3
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extreme_values_do_not_overflow() {
|
||||
let result = calculate_llm_quota_settlement(Some(0), u64::MAX, u64::MAX);
|
||||
assert!(result.settled_quota <= u64::MAX - u64::MAX % LLM_ROUTER_QUOTA_PER_POINT);
|
||||
assert_eq!(
|
||||
calculate_llm_quota_settlement(Some(u64::MAX), 0, u64::MAX).settled_quota,
|
||||
u64::MAX
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ use serde::{Deserialize, Serialize};
|
||||
use tokio::time::sleep;
|
||||
|
||||
mod provider_adapter;
|
||||
pub mod router_billing;
|
||||
|
||||
pub use provider_adapter::{
|
||||
ANTHROPIC_PROVIDER_INSTANCE_ID, ANTHROPIC_PROVIDER_PROTOCOL_ID, AnthropicProviderAdapter,
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
use reqwest::Client;
|
||||
use serde_json::Value;
|
||||
|
||||
/// Reads only accounting fields; never includes upstream bodies in errors.
|
||||
pub async fn read_router_used_quota(
|
||||
client: &Client,
|
||||
origin: &str,
|
||||
admin_token: &str,
|
||||
user_id: i64,
|
||||
) -> Result<u64, String> {
|
||||
if user_id <= 0 {
|
||||
return Err("Router 用户 ID 无效".into());
|
||||
}
|
||||
if admin_token.trim().is_empty() {
|
||||
return Err("Router 额度查询凭据缺失".into());
|
||||
}
|
||||
let status = read_json(client.get(format!("{origin}/api/status"))).await?;
|
||||
validate_router_quota_unit(&status)?;
|
||||
let payload = read_json(
|
||||
client
|
||||
.get(format!("{origin}/api/user/{user_id}"))
|
||||
.bearer_auth(admin_token),
|
||||
)
|
||||
.await?;
|
||||
parse_router_used_quota(&payload, user_id)
|
||||
}
|
||||
|
||||
fn validate_router_quota_unit(payload: &Value) -> Result<(), String> {
|
||||
if payload.get("success").and_then(Value::as_bool) != Some(true)
|
||||
|| payload
|
||||
.pointer("/data/quota_per_unit")
|
||||
.and_then(Value::as_f64)
|
||||
!= Some(500_000.0)
|
||||
{
|
||||
return Err("Router 额度换算单位异常,已停止结算".into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn read_json(request: reqwest::RequestBuilder) -> Result<Value, String> {
|
||||
let response = request
|
||||
.send()
|
||||
.await
|
||||
.map_err(|_| "Router 额度查询连接失败".to_string())?;
|
||||
if !response.status().is_success() {
|
||||
return Err(format!(
|
||||
"Router 额度查询失败:HTTP {}",
|
||||
response.status().as_u16()
|
||||
));
|
||||
}
|
||||
response
|
||||
.json()
|
||||
.await
|
||||
.map_err(|_| "Router 额度查询响应无效".to_string())
|
||||
}
|
||||
|
||||
fn parse_router_used_quota(payload: &Value, user_id: i64) -> Result<u64, String> {
|
||||
if payload.get("success").and_then(Value::as_bool) != Some(true)
|
||||
|| payload.pointer("/data/id").and_then(Value::as_i64) != Some(user_id)
|
||||
{
|
||||
return Err("Router 额度查询失败或账号不匹配".into());
|
||||
}
|
||||
payload
|
||||
.pointer("/data/used_quota")
|
||||
.and_then(Value::as_u64)
|
||||
.ok_or_else(|| "Router 已用额度不是非负整数".into())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
use std::io::{Read, Write};
|
||||
use std::net::TcpListener;
|
||||
use std::thread;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
#[test]
|
||||
fn validates_account_success_and_integer_quota() {
|
||||
assert_eq!(
|
||||
parse_router_used_quota(
|
||||
&json!({"success":true,"data":{"id":21,"used_quota":90271}}),
|
||||
21
|
||||
),
|
||||
Ok(90271)
|
||||
);
|
||||
for payload in [
|
||||
json!({"success":false,"data":{"id":21,"used_quota":0}}),
|
||||
json!({"success":true,"data":{"id":22,"used_quota":0}}),
|
||||
json!({"success":true,"data":{"id":21,"used_quota":-1}}),
|
||||
json!({"success":true,"data":{"id":21,"used_quota":1.5}}),
|
||||
json!({"success":true,"data":{"id":21}}),
|
||||
] {
|
||||
assert!(parse_router_used_quota(&payload, 21).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quota_unit_is_checked_without_using_exchange_rate() {
|
||||
for unit in [json!(500_000), json!(500_000.0)] {
|
||||
assert!(
|
||||
validate_router_quota_unit(&json!({
|
||||
"success": true,
|
||||
"data": {"quota_per_unit": unit, "usd_exchange_rate": 7.2}
|
||||
}))
|
||||
.is_ok()
|
||||
);
|
||||
}
|
||||
for payload in [
|
||||
json!({"success": false, "data": {"quota_per_unit": 500_000}}),
|
||||
json!({"success": true, "data": {"quota_per_unit": 50_000}}),
|
||||
json!({"success": true, "data": {"quota_per_unit": "500000"}}),
|
||||
json!({"success": true, "data": {}}),
|
||||
] {
|
||||
assert!(validate_router_quota_unit(&payload).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
fn loopback_server(responses: Vec<(u16, String)>) -> (String, thread::JoinHandle<Vec<String>>) {
|
||||
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
|
||||
listener.set_nonblocking(true).unwrap();
|
||||
let origin = format!("http://{}", listener.local_addr().unwrap());
|
||||
let handle = thread::spawn(move || {
|
||||
let mut requests = Vec::new();
|
||||
for (status, body) in responses {
|
||||
let deadline = Instant::now() + Duration::from_secs(5);
|
||||
let mut stream = loop {
|
||||
match listener.accept() {
|
||||
Ok((stream, _)) => break stream,
|
||||
Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
|
||||
assert!(Instant::now() < deadline, "missing Router request");
|
||||
thread::sleep(Duration::from_millis(5));
|
||||
}
|
||||
Err(error) => panic!("{error}"),
|
||||
}
|
||||
};
|
||||
stream.set_nonblocking(false).unwrap();
|
||||
stream
|
||||
.set_read_timeout(Some(Duration::from_secs(5)))
|
||||
.unwrap();
|
||||
let mut bytes = Vec::new();
|
||||
while !bytes.ends_with(b"\r\n\r\n") {
|
||||
let mut byte = [0];
|
||||
stream.read_exact(&mut byte).unwrap();
|
||||
bytes.push(byte[0]);
|
||||
}
|
||||
requests.push(String::from_utf8(bytes).unwrap());
|
||||
write!(stream, "HTTP/1.1 {status} Test\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", body.len()).unwrap();
|
||||
}
|
||||
requests
|
||||
});
|
||||
(origin, handle)
|
||||
}
|
||||
|
||||
fn test_client() -> Client {
|
||||
Client::builder()
|
||||
.no_proxy()
|
||||
.redirect(reqwest::redirect::Policy::none())
|
||||
.timeout(Duration::from_secs(3))
|
||||
.build()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn reads_unit_then_authenticated_account_over_http() {
|
||||
let (origin, server) = loopback_server(vec![
|
||||
(
|
||||
200,
|
||||
json!({"success":true,"data":{"quota_per_unit":500000.0}}).to_string(),
|
||||
),
|
||||
(
|
||||
200,
|
||||
json!({"success":true,"data":{"id":21,"used_quota":90271}}).to_string(),
|
||||
),
|
||||
]);
|
||||
let result = read_router_used_quota(&test_client(), &origin, "test-admin", 21).await;
|
||||
let requests = server.join().unwrap();
|
||||
assert_eq!(result, Ok(90271));
|
||||
assert!(requests[0].starts_with("GET /api/status "));
|
||||
assert!(!requests[0].contains("test-admin"));
|
||||
assert!(requests[1].starts_with("GET /api/user/21 "));
|
||||
assert!(requests[1].contains("Bearer test-admin"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn rejects_changed_unit_before_reading_account() {
|
||||
let (origin, server) = loopback_server(vec![(
|
||||
200,
|
||||
json!({"success":true,"data":{"quota_per_unit":100000}}).to_string(),
|
||||
)]);
|
||||
let result = read_router_used_quota(&test_client(), &origin, "test-admin", 21).await;
|
||||
assert_eq!(server.join().unwrap().len(), 1);
|
||||
assert!(result.unwrap_err().contains("换算单位异常"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn http_errors_do_not_disclose_upstream_body_or_credentials() {
|
||||
for status in [401, 403, 500] {
|
||||
let (origin, server) = loopback_server(vec![
|
||||
(
|
||||
200,
|
||||
json!({"success":true,"data":{"quota_per_unit":500000}}).to_string(),
|
||||
),
|
||||
(status, "private-account-body".into()),
|
||||
]);
|
||||
let error = read_router_used_quota(&test_client(), &origin, "test-admin", 21)
|
||||
.await
|
||||
.unwrap_err();
|
||||
server.join().unwrap();
|
||||
assert!(error.contains(&status.to_string()));
|
||||
assert!(!error.contains("private-account-body"));
|
||||
assert!(!error.contains("test-admin"));
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn rejects_invalid_account_or_missing_credential_without_network() {
|
||||
let client = test_client();
|
||||
for (token, user_id) in [("test-admin", 0), ("test-admin", -1), (" ", 21)] {
|
||||
let error = read_router_used_quota(&client, "http://127.0.0.1:1", token, user_id)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(!error.contains("连接失败"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ pub const PROFILE_WALLET_LEDGER_SOURCE_TYPE_INVITE_INVITER_REWARD: &str = "invit
|
||||
pub const PROFILE_WALLET_LEDGER_SOURCE_TYPE_INVITE_INVITEE_REWARD: &str = "invite_invitee_reward";
|
||||
pub const PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_OPERATION_CONSUME: &str =
|
||||
"asset_operation_consume";
|
||||
pub const PROFILE_WALLET_LEDGER_SOURCE_TYPE_LLM_ROUTER_CONSUME: &str = "llm_router_consume";
|
||||
pub const PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_OPERATION_REFUND: &str = "asset_operation_refund";
|
||||
pub const PROFILE_WALLET_LEDGER_SOURCE_TYPE_REDEEM_CODE_REWARD: &str = "redeem_code_reward";
|
||||
pub const PROFILE_WALLET_LEDGER_SOURCE_TYPE_PUZZLE_AUTHOR_INCENTIVE_CLAIM: &str =
|
||||
|
||||
@@ -46,6 +46,9 @@ pub(crate) fn map_runtime_profile_wallet_ledger_source_type_back(
|
||||
crate::module_bindings::RuntimeProfileWalletLedgerSourceType::AssetOperationConsume => {
|
||||
module_runtime::RuntimeProfileWalletLedgerSourceType::AssetOperationConsume
|
||||
}
|
||||
crate::module_bindings::RuntimeProfileWalletLedgerSourceType::LlmRouterConsume => {
|
||||
module_runtime::RuntimeProfileWalletLedgerSourceType::LlmRouterConsume
|
||||
}
|
||||
crate::module_bindings::RuntimeProfileWalletLedgerSourceType::AssetOperationRefund => {
|
||||
module_runtime::RuntimeProfileWalletLedgerSourceType::AssetOperationRefund
|
||||
}
|
||||
|
||||
@@ -3,7 +3,56 @@ use crate::mapper::llm_router_account::{
|
||||
map_llm_router_account_required_result, map_llm_router_account_result,
|
||||
};
|
||||
|
||||
pub struct LlmRouterQuotaSettlementRecord {
|
||||
pub settled_quota: u64,
|
||||
pub charged_points: u64,
|
||||
pub spendable_points: u64,
|
||||
}
|
||||
|
||||
impl SpacetimeClient {
|
||||
pub async fn settle_llm_router_quota(
|
||||
&self,
|
||||
owner_user_id: String,
|
||||
route_origin: String,
|
||||
router_user_id: i64,
|
||||
used_quota: u64,
|
||||
) -> Result<LlmRouterQuotaSettlementRecord, SpacetimeClientError> {
|
||||
let input = crate::module_bindings::LlmRouterQuotaSettlementInput {
|
||||
owner_user_id,
|
||||
route_origin,
|
||||
router_user_id,
|
||||
used_quota,
|
||||
};
|
||||
self.call_after_connect(
|
||||
"settle_llm_router_quota_and_return",
|
||||
move |connection, sender| {
|
||||
connection
|
||||
.procedures()
|
||||
.settle_llm_router_quota_and_return_then(input, move |_, result| {
|
||||
let mapped = result
|
||||
.map_err(SpacetimeClientError::from_sdk_error)
|
||||
.and_then(|result| {
|
||||
if result.ok {
|
||||
Ok(LlmRouterQuotaSettlementRecord {
|
||||
settled_quota: result.settled_quota,
|
||||
charged_points: result.charged_points,
|
||||
spendable_points: result.spendable_points,
|
||||
})
|
||||
} else {
|
||||
Err(SpacetimeClientError::Procedure(
|
||||
result
|
||||
.error_message
|
||||
.unwrap_or_else(|| "LLM 额度结算失败".to_string()),
|
||||
))
|
||||
}
|
||||
});
|
||||
send_once(&sender, mapped);
|
||||
});
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_llm_router_account(
|
||||
&self,
|
||||
owner_user_id: String,
|
||||
|
||||
@@ -522,6 +522,10 @@ pub mod llm_router_account_snapshot_type;
|
||||
pub mod llm_router_account_table;
|
||||
pub mod llm_router_account_type;
|
||||
pub mod llm_router_account_upsert_input_type;
|
||||
pub mod llm_router_billing_checkpoint_table;
|
||||
pub mod llm_router_billing_checkpoint_type;
|
||||
pub mod llm_router_quota_settlement_input_type;
|
||||
pub mod llm_router_quota_settlement_result_type;
|
||||
pub mod mark_editor_showcase_asset_refunded_and_return_procedure;
|
||||
pub mod mark_profile_recharge_order_expiration_checked_procedure;
|
||||
pub mod mark_profile_recharge_order_paid_and_return_procedure;
|
||||
@@ -845,6 +849,7 @@ pub mod save_editor_project_layout_v_2_ack_procedure;
|
||||
pub mod save_editor_project_layout_v_2_and_return_procedure;
|
||||
pub mod seed_analytics_date_dimensions_reducer;
|
||||
pub mod set_editor_showcase_asset_like_for_viewer_and_return_procedure;
|
||||
pub mod settle_llm_router_quota_and_return_procedure;
|
||||
pub mod square_hole_agent_message_row_type;
|
||||
pub mod square_hole_agent_message_table;
|
||||
pub mod square_hole_agent_session_row_type;
|
||||
@@ -1426,6 +1431,10 @@ pub use llm_router_account_snapshot_type::LlmRouterAccountSnapshot;
|
||||
pub use llm_router_account_table::*;
|
||||
pub use llm_router_account_type::LlmRouterAccount;
|
||||
pub use llm_router_account_upsert_input_type::LlmRouterAccountUpsertInput;
|
||||
pub use llm_router_billing_checkpoint_table::*;
|
||||
pub use llm_router_billing_checkpoint_type::LlmRouterBillingCheckpoint;
|
||||
pub use llm_router_quota_settlement_input_type::LlmRouterQuotaSettlementInput;
|
||||
pub use llm_router_quota_settlement_result_type::LlmRouterQuotaSettlementResult;
|
||||
pub use mark_editor_showcase_asset_refunded_and_return_procedure::mark_editor_showcase_asset_refunded_and_return;
|
||||
pub use mark_profile_recharge_order_expiration_checked_procedure::mark_profile_recharge_order_expiration_checked;
|
||||
pub use mark_profile_recharge_order_paid_and_return_procedure::mark_profile_recharge_order_paid_and_return;
|
||||
@@ -1749,6 +1758,7 @@ pub use save_editor_project_layout_v_2_ack_procedure::save_editor_project_layout
|
||||
pub use save_editor_project_layout_v_2_and_return_procedure::save_editor_project_layout_v_2_and_return;
|
||||
pub use seed_analytics_date_dimensions_reducer::seed_analytics_date_dimensions;
|
||||
pub use set_editor_showcase_asset_like_for_viewer_and_return_procedure::set_editor_showcase_asset_like_for_viewer_and_return;
|
||||
pub use settle_llm_router_quota_and_return_procedure::settle_llm_router_quota_and_return;
|
||||
pub use square_hole_agent_message_row_type::SquareHoleAgentMessageRow;
|
||||
pub use square_hole_agent_message_table::*;
|
||||
pub use square_hole_agent_session_row_type::SquareHoleAgentSessionRow;
|
||||
@@ -1993,6 +2003,7 @@ pub struct DbUpdate {
|
||||
jump_hop_runtime_run: __sdk::TableUpdate<JumpHopRuntimeRunRow>,
|
||||
jump_hop_work_profile: __sdk::TableUpdate<JumpHopWorkProfileRow>,
|
||||
llm_router_account: __sdk::TableUpdate<LlmRouterAccount>,
|
||||
llm_router_billing_checkpoint: __sdk::TableUpdate<LlmRouterBillingCheckpoint>,
|
||||
match_3_d_agent_message: __sdk::TableUpdate<Match3DAgentMessageRow>,
|
||||
match_3_d_agent_session: __sdk::TableUpdate<Match3DAgentSessionRow>,
|
||||
match_3_d_runtime_run: __sdk::TableUpdate<Match3DRuntimeRunRow>,
|
||||
@@ -2318,6 +2329,9 @@ impl TryFrom<__ws::v2::TransactionUpdate> for DbUpdate {
|
||||
"llm_router_account" => db_update
|
||||
.llm_router_account
|
||||
.append(llm_router_account_table::parse_table_update(table_update)?),
|
||||
"llm_router_billing_checkpoint" => db_update.llm_router_billing_checkpoint.append(
|
||||
llm_router_billing_checkpoint_table::parse_table_update(table_update)?,
|
||||
),
|
||||
"match_3_d_agent_message" => db_update.match_3_d_agent_message.append(
|
||||
match_3_d_agent_message_table::parse_table_update(table_update)?,
|
||||
),
|
||||
@@ -2950,6 +2964,12 @@ impl __sdk::DbUpdate for DbUpdate {
|
||||
diff.llm_router_account = cache
|
||||
.apply_diff_to_table::<LlmRouterAccount>("llm_router_account", &self.llm_router_account)
|
||||
.with_updates_by_pk(|row| &row.account_key);
|
||||
diff.llm_router_billing_checkpoint = cache
|
||||
.apply_diff_to_table::<LlmRouterBillingCheckpoint>(
|
||||
"llm_router_billing_checkpoint",
|
||||
&self.llm_router_billing_checkpoint,
|
||||
)
|
||||
.with_updates_by_pk(|row| &row.account_key);
|
||||
diff.match_3_d_agent_message = cache
|
||||
.apply_diff_to_table::<Match3DAgentMessageRow>(
|
||||
"match_3_d_agent_message",
|
||||
@@ -3556,6 +3576,9 @@ impl __sdk::DbUpdate for DbUpdate {
|
||||
"llm_router_account" => db_update
|
||||
.llm_router_account
|
||||
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
|
||||
"llm_router_billing_checkpoint" => db_update
|
||||
.llm_router_billing_checkpoint
|
||||
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
|
||||
"match_3_d_agent_message" => db_update
|
||||
.match_3_d_agent_message
|
||||
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
|
||||
@@ -3998,6 +4021,9 @@ impl __sdk::DbUpdate for DbUpdate {
|
||||
"llm_router_account" => db_update
|
||||
.llm_router_account
|
||||
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
|
||||
"llm_router_billing_checkpoint" => db_update
|
||||
.llm_router_billing_checkpoint
|
||||
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
|
||||
"match_3_d_agent_message" => db_update
|
||||
.match_3_d_agent_message
|
||||
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
|
||||
@@ -4306,6 +4332,7 @@ pub struct AppliedDiff<'r> {
|
||||
jump_hop_runtime_run: __sdk::TableAppliedDiff<'r, JumpHopRuntimeRunRow>,
|
||||
jump_hop_work_profile: __sdk::TableAppliedDiff<'r, JumpHopWorkProfileRow>,
|
||||
llm_router_account: __sdk::TableAppliedDiff<'r, LlmRouterAccount>,
|
||||
llm_router_billing_checkpoint: __sdk::TableAppliedDiff<'r, LlmRouterBillingCheckpoint>,
|
||||
match_3_d_agent_message: __sdk::TableAppliedDiff<'r, Match3DAgentMessageRow>,
|
||||
match_3_d_agent_session: __sdk::TableAppliedDiff<'r, Match3DAgentSessionRow>,
|
||||
match_3_d_runtime_run: __sdk::TableAppliedDiff<'r, Match3DRuntimeRunRow>,
|
||||
@@ -4740,6 +4767,11 @@ impl<'r> __sdk::AppliedDiff<'r> for AppliedDiff<'r> {
|
||||
&self.llm_router_account,
|
||||
event,
|
||||
);
|
||||
callbacks.invoke_table_row_callbacks::<LlmRouterBillingCheckpoint>(
|
||||
"llm_router_billing_checkpoint",
|
||||
&self.llm_router_billing_checkpoint,
|
||||
event,
|
||||
);
|
||||
callbacks.invoke_table_row_callbacks::<Match3DAgentMessageRow>(
|
||||
"match_3_d_agent_message",
|
||||
&self.match_3_d_agent_message,
|
||||
@@ -5823,6 +5855,7 @@ impl __sdk::SpacetimeModule for RemoteModule {
|
||||
jump_hop_runtime_run_table::register_table(client_cache);
|
||||
jump_hop_work_profile_table::register_table(client_cache);
|
||||
llm_router_account_table::register_table(client_cache);
|
||||
llm_router_billing_checkpoint_table::register_table(client_cache);
|
||||
match_3_d_agent_message_table::register_table(client_cache);
|
||||
match_3_d_agent_session_table::register_table(client_cache);
|
||||
match_3_d_runtime_run_table::register_table(client_cache);
|
||||
@@ -5968,6 +6001,7 @@ impl __sdk::SpacetimeModule for RemoteModule {
|
||||
"jump_hop_runtime_run",
|
||||
"jump_hop_work_profile",
|
||||
"llm_router_account",
|
||||
"llm_router_billing_checkpoint",
|
||||
"match_3_d_agent_message",
|
||||
"match_3_d_agent_session",
|
||||
"match_3_d_runtime_run",
|
||||
|
||||
+235
@@ -0,0 +1,235 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
#![allow(unused, clippy::all)]
|
||||
use super::llm_router_billing_checkpoint_type::LlmRouterBillingCheckpoint;
|
||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
|
||||
/// Table handle for the table `llm_router_billing_checkpoint`.
|
||||
///
|
||||
/// Obtain a handle from the [`LlmRouterBillingCheckpointTableAccess::llm_router_billing_checkpoint`] method on [`super::RemoteTables`],
|
||||
/// like `ctx.db.llm_router_billing_checkpoint()`.
|
||||
///
|
||||
/// Users are encouraged not to explicitly reference this type,
|
||||
/// but to directly chain method calls,
|
||||
/// like `ctx.db.llm_router_billing_checkpoint().on_insert(...)`.
|
||||
pub struct LlmRouterBillingCheckpointTableHandle<'ctx> {
|
||||
imp: __sdk::TableHandle<LlmRouterBillingCheckpoint>,
|
||||
ctx: std::marker::PhantomData<&'ctx super::RemoteTables>,
|
||||
}
|
||||
|
||||
/// Lifetime-aware accessor marker for the table `llm_router_billing_checkpoint`.
|
||||
pub struct LlmRouterBillingCheckpointTableAccessor;
|
||||
|
||||
impl __sdk::TableAccessor<super::RemoteTables> for LlmRouterBillingCheckpointTableAccessor {
|
||||
type Row = LlmRouterBillingCheckpoint;
|
||||
type Handle<'db> = LlmRouterBillingCheckpointTableHandle<'db>;
|
||||
|
||||
fn get<'db>(db: &'db super::RemoteTables) -> Self::Handle<'db> {
|
||||
db.llm_router_billing_checkpoint()
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
/// Extension trait for access to the table `llm_router_billing_checkpoint`.
|
||||
///
|
||||
/// Implemented for [`super::RemoteTables`].
|
||||
pub trait LlmRouterBillingCheckpointTableAccess {
|
||||
#[allow(non_snake_case)]
|
||||
/// Obtain a [`LlmRouterBillingCheckpointTableHandle`], which mediates access to the table `llm_router_billing_checkpoint`.
|
||||
fn llm_router_billing_checkpoint(&self) -> LlmRouterBillingCheckpointTableHandle<'_>;
|
||||
}
|
||||
|
||||
impl LlmRouterBillingCheckpointTableAccess for super::RemoteTables {
|
||||
fn llm_router_billing_checkpoint(&self) -> LlmRouterBillingCheckpointTableHandle<'_> {
|
||||
LlmRouterBillingCheckpointTableHandle {
|
||||
imp: self
|
||||
.imp
|
||||
.get_table::<LlmRouterBillingCheckpoint>("llm_router_billing_checkpoint"),
|
||||
ctx: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LlmRouterBillingCheckpointInsertCallbackId(__sdk::CallbackId);
|
||||
pub struct LlmRouterBillingCheckpointDeleteCallbackId(__sdk::CallbackId);
|
||||
|
||||
impl<'ctx> __sdk::TableLike for LlmRouterBillingCheckpointTableHandle<'ctx> {
|
||||
type Row = LlmRouterBillingCheckpoint;
|
||||
type EventContext = super::EventContext;
|
||||
|
||||
fn count(&self) -> u64 {
|
||||
self.imp.count()
|
||||
}
|
||||
fn iter(&self) -> impl Iterator<Item = LlmRouterBillingCheckpoint> + '_ {
|
||||
self.imp.iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'ctx> __sdk::Table for LlmRouterBillingCheckpointTableHandle<'ctx> {
|
||||
type Row = LlmRouterBillingCheckpoint;
|
||||
type EventContext = super::EventContext;
|
||||
|
||||
fn count(&self) -> u64 {
|
||||
self.imp.count()
|
||||
}
|
||||
fn iter(&self) -> impl Iterator<Item = LlmRouterBillingCheckpoint> + '_ {
|
||||
self.imp.iter()
|
||||
}
|
||||
|
||||
type InsertCallbackId = LlmRouterBillingCheckpointInsertCallbackId;
|
||||
|
||||
fn on_insert(
|
||||
&self,
|
||||
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
|
||||
) -> LlmRouterBillingCheckpointInsertCallbackId {
|
||||
LlmRouterBillingCheckpointInsertCallbackId(self.imp.on_insert(Box::new(callback)))
|
||||
}
|
||||
|
||||
fn remove_on_insert(&self, callback: LlmRouterBillingCheckpointInsertCallbackId) {
|
||||
self.imp.remove_on_insert(callback.0)
|
||||
}
|
||||
|
||||
type DeleteCallbackId = LlmRouterBillingCheckpointDeleteCallbackId;
|
||||
|
||||
fn on_delete(
|
||||
&self,
|
||||
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
|
||||
) -> LlmRouterBillingCheckpointDeleteCallbackId {
|
||||
LlmRouterBillingCheckpointDeleteCallbackId(self.imp.on_delete(Box::new(callback)))
|
||||
}
|
||||
|
||||
fn remove_on_delete(&self, callback: LlmRouterBillingCheckpointDeleteCallbackId) {
|
||||
self.imp.remove_on_delete(callback.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'ctx> __sdk::WithInsert for LlmRouterBillingCheckpointTableHandle<'ctx> {
|
||||
type InsertCallbackId = LlmRouterBillingCheckpointInsertCallbackId;
|
||||
|
||||
fn on_insert(
|
||||
&self,
|
||||
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
|
||||
) -> LlmRouterBillingCheckpointInsertCallbackId {
|
||||
LlmRouterBillingCheckpointInsertCallbackId(self.imp.on_insert(Box::new(callback)))
|
||||
}
|
||||
|
||||
fn remove_on_insert(&self, callback: LlmRouterBillingCheckpointInsertCallbackId) {
|
||||
self.imp.remove_on_insert(callback.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'ctx> __sdk::WithDelete for LlmRouterBillingCheckpointTableHandle<'ctx> {
|
||||
type DeleteCallbackId = LlmRouterBillingCheckpointDeleteCallbackId;
|
||||
|
||||
fn on_delete(
|
||||
&self,
|
||||
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
|
||||
) -> LlmRouterBillingCheckpointDeleteCallbackId {
|
||||
LlmRouterBillingCheckpointDeleteCallbackId(self.imp.on_delete(Box::new(callback)))
|
||||
}
|
||||
|
||||
fn remove_on_delete(&self, callback: LlmRouterBillingCheckpointDeleteCallbackId) {
|
||||
self.imp.remove_on_delete(callback.0)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LlmRouterBillingCheckpointUpdateCallbackId(__sdk::CallbackId);
|
||||
|
||||
impl<'ctx> __sdk::TableWithPrimaryKey for LlmRouterBillingCheckpointTableHandle<'ctx> {
|
||||
type UpdateCallbackId = LlmRouterBillingCheckpointUpdateCallbackId;
|
||||
|
||||
fn on_update(
|
||||
&self,
|
||||
callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static,
|
||||
) -> LlmRouterBillingCheckpointUpdateCallbackId {
|
||||
LlmRouterBillingCheckpointUpdateCallbackId(self.imp.on_update(Box::new(callback)))
|
||||
}
|
||||
|
||||
fn remove_on_update(&self, callback: LlmRouterBillingCheckpointUpdateCallbackId) {
|
||||
self.imp.remove_on_update(callback.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'ctx> __sdk::WithUpdate for LlmRouterBillingCheckpointTableHandle<'ctx> {
|
||||
type UpdateCallbackId = LlmRouterBillingCheckpointUpdateCallbackId;
|
||||
|
||||
fn on_update(
|
||||
&self,
|
||||
callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static,
|
||||
) -> LlmRouterBillingCheckpointUpdateCallbackId {
|
||||
LlmRouterBillingCheckpointUpdateCallbackId(self.imp.on_update(Box::new(callback)))
|
||||
}
|
||||
|
||||
fn remove_on_update(&self, callback: LlmRouterBillingCheckpointUpdateCallbackId) {
|
||||
self.imp.remove_on_update(callback.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Access to the `account_key` unique index on the table `llm_router_billing_checkpoint`,
|
||||
/// which allows point queries on the field of the same name
|
||||
/// via the [`LlmRouterBillingCheckpointAccountKeyUnique::find`] method.
|
||||
///
|
||||
/// Users are encouraged not to explicitly reference this type,
|
||||
/// but to directly chain method calls,
|
||||
/// like `ctx.db.llm_router_billing_checkpoint().account_key().find(...)`.
|
||||
pub struct LlmRouterBillingCheckpointAccountKeyUnique<'ctx> {
|
||||
imp: __sdk::UniqueConstraintHandle<LlmRouterBillingCheckpoint, String>,
|
||||
phantom: std::marker::PhantomData<&'ctx super::RemoteTables>,
|
||||
}
|
||||
|
||||
impl<'ctx> LlmRouterBillingCheckpointTableHandle<'ctx> {
|
||||
/// Get a handle on the `account_key` unique index on the table `llm_router_billing_checkpoint`.
|
||||
pub fn account_key(&self) -> LlmRouterBillingCheckpointAccountKeyUnique<'ctx> {
|
||||
LlmRouterBillingCheckpointAccountKeyUnique {
|
||||
imp: self.imp.get_unique_constraint::<String>("account_key"),
|
||||
phantom: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'ctx> LlmRouterBillingCheckpointAccountKeyUnique<'ctx> {
|
||||
/// Find the subscribed row whose `account_key` column value is equal to `col_val`,
|
||||
/// if such a row is present in the client cache.
|
||||
pub fn find(&self, col_val: &String) -> Option<LlmRouterBillingCheckpoint> {
|
||||
self.imp.find(col_val)
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub(super) fn register_table(client_cache: &mut __sdk::ClientCache<super::RemoteModule>) {
|
||||
let _table = client_cache
|
||||
.get_or_make_table::<LlmRouterBillingCheckpoint>("llm_router_billing_checkpoint");
|
||||
_table.add_unique_constraint::<String>("account_key", |row| &row.account_key);
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub(super) fn parse_table_update(
|
||||
raw_updates: __ws::v2::TableUpdate,
|
||||
) -> __sdk::Result<__sdk::TableUpdate<LlmRouterBillingCheckpoint>> {
|
||||
__sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| {
|
||||
__sdk::InternalError::failed_parse("TableUpdate<LlmRouterBillingCheckpoint>", "TableUpdate")
|
||||
.with_cause(e)
|
||||
.into()
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
/// Extension trait for query builder access to the table `LlmRouterBillingCheckpoint`.
|
||||
///
|
||||
/// Implemented for [`__sdk::QueryTableAccessor`].
|
||||
pub trait llm_router_billing_checkpointQueryTableAccess {
|
||||
#[allow(non_snake_case)]
|
||||
/// Get a query builder for the table `LlmRouterBillingCheckpoint`.
|
||||
fn llm_router_billing_checkpoint(
|
||||
&self,
|
||||
) -> __sdk::__query_builder::Table<LlmRouterBillingCheckpoint>;
|
||||
}
|
||||
|
||||
impl llm_router_billing_checkpointQueryTableAccess for __sdk::QueryTableAccessor {
|
||||
fn llm_router_billing_checkpoint(
|
||||
&self,
|
||||
) -> __sdk::__query_builder::Table<LlmRouterBillingCheckpoint> {
|
||||
__sdk::__query_builder::Table::new("llm_router_billing_checkpoint")
|
||||
}
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
#![allow(unused, clippy::all)]
|
||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
|
||||
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||
#[sats(crate = __lib)]
|
||||
pub struct LlmRouterBillingCheckpoint {
|
||||
pub account_key: String,
|
||||
pub router_user_id: i64,
|
||||
pub settled_quota: u64,
|
||||
pub updated_at: __sdk::Timestamp,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for LlmRouterBillingCheckpoint {
|
||||
type Module = super::RemoteModule;
|
||||
}
|
||||
|
||||
/// Column accessor struct for the table `LlmRouterBillingCheckpoint`.
|
||||
///
|
||||
/// Provides typed access to columns for query building.
|
||||
pub struct LlmRouterBillingCheckpointCols {
|
||||
pub account_key: __sdk::__query_builder::Col<LlmRouterBillingCheckpoint, String>,
|
||||
pub router_user_id: __sdk::__query_builder::Col<LlmRouterBillingCheckpoint, i64>,
|
||||
pub settled_quota: __sdk::__query_builder::Col<LlmRouterBillingCheckpoint, u64>,
|
||||
pub updated_at: __sdk::__query_builder::Col<LlmRouterBillingCheckpoint, __sdk::Timestamp>,
|
||||
}
|
||||
|
||||
impl __sdk::__query_builder::HasCols for LlmRouterBillingCheckpoint {
|
||||
type Cols = LlmRouterBillingCheckpointCols;
|
||||
fn cols(table_name: &'static str) -> Self::Cols {
|
||||
LlmRouterBillingCheckpointCols {
|
||||
account_key: __sdk::__query_builder::Col::new(table_name, "account_key"),
|
||||
router_user_id: __sdk::__query_builder::Col::new(table_name, "router_user_id"),
|
||||
settled_quota: __sdk::__query_builder::Col::new(table_name, "settled_quota"),
|
||||
updated_at: __sdk::__query_builder::Col::new(table_name, "updated_at"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Indexed column accessor struct for the table `LlmRouterBillingCheckpoint`.
|
||||
///
|
||||
/// Provides typed access to indexed columns for query building.
|
||||
pub struct LlmRouterBillingCheckpointIxCols {
|
||||
pub account_key: __sdk::__query_builder::IxCol<LlmRouterBillingCheckpoint, String>,
|
||||
}
|
||||
|
||||
impl __sdk::__query_builder::HasIxCols for LlmRouterBillingCheckpoint {
|
||||
type IxCols = LlmRouterBillingCheckpointIxCols;
|
||||
fn ix_cols(table_name: &'static str) -> Self::IxCols {
|
||||
LlmRouterBillingCheckpointIxCols {
|
||||
account_key: __sdk::__query_builder::IxCol::new(table_name, "account_key"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl __sdk::__query_builder::CanBeLookupTable for LlmRouterBillingCheckpoint {}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
#![allow(unused, clippy::all)]
|
||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
|
||||
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||
#[sats(crate = __lib)]
|
||||
pub struct LlmRouterQuotaSettlementInput {
|
||||
pub owner_user_id: String,
|
||||
pub route_origin: String,
|
||||
pub router_user_id: i64,
|
||||
pub used_quota: u64,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for LlmRouterQuotaSettlementInput {
|
||||
type Module = super::RemoteModule;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user