- 删 CheckTenantBalance/DeductBalance/GetTenantSurplus(admin-go 租户余额退出 model-gateway) - 新增 pricing_client.go:门禁 config/get + calcModelCost 调 shop-user-trade /calc 算费 - sync/stream/async 三处 Cost 改为按用量调 /calc(媒体类型映射 text/audio/video/image) - 删 price.go 本地 PriceConfig 换算体系与 entity/dto 的 price_config 字段 Co-Authored-By: Claude <noreply@anthropic.com>
86 lines
3.0 KiB
Go
86 lines
3.0 KiB
Go
package service
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
|
||
commonHttp "gitea.redpowerfuture.com/red-future/common/http"
|
||
"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"`
|
||
}
|
||
|
||
// 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-gateway(audio/no_video/has_video)→ shop-user-trade(text/audio/video/image)
|
||
func mgMediaTypeToShop(mg string) string {
|
||
switch mg {
|
||
case "audio":
|
||
return "audio"
|
||
case "has_video":
|
||
return "video"
|
||
default: // no_video
|
||
return "text"
|
||
}
|
||
}
|
||
|
||
// modelBillable 调用前门禁:模型须在 shop-user-trade 已配置且启用计价,否则阻塞调用。
|
||
// 替换原 CheckTenantBalance(admin-go 租户余额门禁);未配置→config/get 返回错误,未启用→enabled!=1。
|
||
func modelBillable(ctx context.Context, modelId int64) error {
|
||
var cfg shopPricingConfig
|
||
err := commonHttp.Get(ctx, pricingURL("config/get"), setCtxHeader(ctx), &cfg,
|
||
"subjectType", "model", "subjectId", fmt.Sprintf("%d", modelId))
|
||
if err != nil {
|
||
return fmt.Errorf("模型未配置计价,无法调用: %w", err)
|
||
}
|
||
if cfg.Enabled != 1 {
|
||
return fmt.Errorf("模型未启用计价,无法调用")
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// buildModelUsage 组装算费用量 JSON 对象(ChargeUsage 形状)。mediaType 为 model-gateway 词汇(DetectMediaType/异步快照)。
|
||
func buildModelUsage(prompt, completion, cached int64, mediaType string, durationSec float64) 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"), setCtxHeader(ctx), &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
|
||
}
|