Files
model-gateway/service/pricing_client.go
T
19904408334 1ca25abca0 refactor: 模型解析/价格工具拆分至 service/utils,调用方适配(WIP)
- price.go -> service/utils/media_type.go(媒体类型检测)
- model_resolve.go -> service/utils/model_resolve.go(模型解析)
- 删除 service/parse_error_test.go(逻辑迁往 utils 后无对应测试)
- session_sync/session_stream/model_*_service/pricing_client 等调用方适配
- schema_mapping/model_call_dto 补字段路径
2026-09-01 10:18:23 +08:00

121 lines
4.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package service
import (
"context"
"fmt"
commonHttp "gitea.redpowerfuture.com/red-future/common/http"
"gitea.redpowerfuture.com/red-future/common/utils"
"github.com/gogf/gf/v2/frame/g"
)
// ====================== shop-user-trade 计价对接(独立 module,本地 JSON 对齐) ======================
// shopPricingConfig shop-user-trade config/get 响应(仅取启用标记与最低余额门限;费率规则在 shop-user-trade 侧,model-gateway 不消费)
type shopPricingConfig struct {
Enabled int `json:"enabled"`
MinBalance float64 `json:"minBalance" dc:"门禁:调用前可用余额须>=该值(元),0=不校验"`
}
// shopWalletAccount shop-user-trade wallet/account/get 响应
type shopWalletAccount struct {
ID int64 `json:"id"`
UserID int64 `json:"userId"`
Balance float64 `json:"balance" dc:"可用余额(元),负值=欠费"`
Currency string `json:"currency"`
Status int `json:"status" dc:"状态:1启用 0禁用 -1冻结"`
}
// shopCalcFeeRes shop-user-trade /calc 响应
type shopCalcFeeRes struct {
Cost float64 `json:"cost"`
}
// pricingURL 组装 shop-user-trade 计价接口地址(common http.RouteRegister 推导前缀,同 ai-agent billing.go
func pricingURL(sub string) string {
return "shop-user-trade/pricing/controller/" + sub
}
// mgMediaTypeToShop 媒体类型词汇映射:model-gatewayaudio/no_video/has_video)→ shop-user-tradetext/audio/video/image
func mgMediaTypeToShop(mg string) string {
switch mg {
case "audio":
return "audio"
case "has_video":
return "video"
default: // no_video
return "text"
}
}
// walletURL 组装 shop-user-trade 钱包接口地址(accountController → account/controller,与 pricing 同 RouteRegister 推导规则)
func walletURL(sub string) string {
return "shop-user-trade/account/controller/" + sub
}
// modelBillable 调用前门禁:模型须在 shop-user-trade 已配置且启用计价,否则阻塞调用。
// 替换原 CheckTenantBalanceadmin-go 租户余额门禁);未配置→config/get 返回错误,未启用→enabled!=1。
// minBalance>0 时追加最低余额门禁:钱包须存在且可用余额 >= 门限(与 shop-user-trade open_order 同语义,0=不校验)。
func modelBillable(ctx context.Context, modelId int64) error {
var cfg shopPricingConfig
err := commonHttp.Get(ctx, pricingURL("config/get"), utils.HeadersFromCtx(ctx, utils.HeadersOptions{ResolveToken: true}), &cfg,
"subjectType", "model", "subjectId", fmt.Sprintf("%d", modelId))
if err != nil {
return fmt.Errorf("模型未配置计价,无法调用: %w", err)
}
if cfg.Enabled != 1 {
return fmt.Errorf("模型未启用计价,无法调用")
}
if cfg.MinBalance <= 0 {
return nil
}
user, e := utils.GetUserInfo(ctx)
if e != nil || user == nil || user.Id == 0 {
return fmt.Errorf("取不到用户,无法校验最低余额")
}
var acc shopWalletAccount
if e = commonHttp.Get(ctx, walletURL("get"), utils.HeadersFromCtx(ctx, utils.HeadersOptions{ResolveToken: true}), &acc,
"userId", fmt.Sprintf("%d", user.Id)); e != nil {
return fmt.Errorf("获取钱包失败,无法校验最低余额: %w", e)
}
if acc.Status != 1 {
return fmt.Errorf("钱包不可用,无法调用")
}
if acc.Balance < cfg.MinBalance {
return fmt.Errorf("余额不足:可用余额须不低于 %.2f 元才能发起", cfg.MinBalance)
}
return nil
}
// buildModelUsage 组装算费用量 JSON 对象(ChargeUsage 形状)。mediaType 为 model-gateway 词汇(DetectMediaType/异步快照)。
func buildModelUsage(prompt, completion, cached int64, mediaType string, durationSec int64) map[string]any {
if durationSec < 0 {
durationSec = 0
}
return map[string]any{
"promptTokens": prompt,
"completionTokens": completion,
"cachedTokens": cached,
"mediaType": mgMediaTypeToShop(mediaType),
"durationSec": durationSec,
}
}
// calcModelCost 调 shop-user-trade /calc 按用量算费(不建单不扣费)。
// 调用前门禁已保证配置存在;此处失败(配置中途删除/网络抖动)→ 记日志返回 0,不拖垮已完成的模型调用。
func calcModelCost(ctx context.Context, modelId int64, usage map[string]any) float64 {
var res shopCalcFeeRes
err := commonHttp.Post(ctx, pricingURL("calc"), utils.HeadersFromCtx(ctx, utils.HeadersOptions{ResolveToken: true}), &res, &struct {
SubjectType string `json:"subjectType"`
SubjectID string `json:"subjectId"`
Usage map[string]any `json:"usage"`
}{
SubjectType: "model", SubjectID: fmt.Sprintf("%d", modelId), Usage: usage,
})
if err != nil {
g.Log().Errorf(ctx, "[算费] 调用 shop-user-trade 失败 modelId=%d: %v", modelId, err)
return 0
}
return res.Cost
}