Files
model-gateway/service/pricing_client.go
T

111 lines
4.6 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
}
// 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 为 shop 计费词汇
// audio/video,空=无媒体引用走默认价;DetectMediaType/异步快照已直接为该词汇,不再二次转换)。
// per_char 模型把输出字数映射到 completionTokensTokenMapping),随该字段传给 shop /calc 计价。
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": 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
}