210 lines
6.2 KiB
Go
210 lines
6.2 KiB
Go
//go:build llm_script && !scripts_pkg
|
|
|
|
package main
|
|
|
|
import (
|
|
"database/sql"
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
defaultHuaweiMaaSPricingURL = "https://portal.huaweicloud.com/api/calculator/rest/cbc/portalcalculatornodeservice/v4/api/productInfo?urlPath=maas&language=zh-cn&sign=common"
|
|
defaultHuaweiMaaSPricingSourceURL = "https://support.huaweicloud.com/price-maas/price-maas-0002.html"
|
|
)
|
|
|
|
type huaweiMaaSPricingImportConfig struct {
|
|
URL string
|
|
Fixture string
|
|
DryRun bool
|
|
Timeout time.Duration
|
|
}
|
|
|
|
type huaweiMaaSPricingEnvelope struct {
|
|
Product map[string][]huaweiMaaSPricingRow `json:"product"`
|
|
}
|
|
|
|
type huaweiMaaSPricingRow struct {
|
|
ResourceSpecCode string `json:"resourceSpecCode"`
|
|
ResourceSpecType string `json:"resourceSpecType"`
|
|
ModelName string `json:"Model Name"`
|
|
PlanList []huaweiMaaSPricingPlan `json:"planList"`
|
|
}
|
|
|
|
type huaweiMaaSPricingPlan struct {
|
|
UsageFactor string `json:"usageFactor"`
|
|
Amount float64 `json:"amount"`
|
|
}
|
|
|
|
func main() {
|
|
loadSubscriptionImportEnv()
|
|
|
|
var url string
|
|
var fixture string
|
|
var dryRun bool
|
|
var timeoutSeconds int
|
|
|
|
flag.StringVar(&url, "url", defaultHuaweiMaaSPricingURL, "华为云 MaaS 官方价格 JSON API")
|
|
flag.StringVar(&fixture, "fixture", "", "华为云 MaaS 价格样例文件")
|
|
flag.BoolVar(&dryRun, "dry-run", false, "仅解析并打印摘要,不写入数据库")
|
|
flag.IntVar(&timeoutSeconds, "timeout", 20, "请求超时(秒)")
|
|
flag.Parse()
|
|
|
|
cfg := huaweiMaaSPricingImportConfig{URL: url, Fixture: fixture, DryRun: dryRun, Timeout: time.Duration(timeoutSeconds) * time.Second}
|
|
|
|
var db *sql.DB
|
|
var err error
|
|
if !cfg.DryRun {
|
|
db, err = subscriptionImportDB()
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "open db: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
defer db.Close()
|
|
}
|
|
|
|
if err := runHuaweiMaaSPricingImport(cfg, db, os.Stdout); err != nil {
|
|
fmt.Fprintf(os.Stderr, "import_huawei_maas_pricing: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func runHuaweiMaaSPricingImport(cfg huaweiMaaSPricingImportConfig, db *sql.DB, out io.Writer) error {
|
|
client := &http.Client{Timeout: cfg.Timeout}
|
|
raw, err := fetchRawPricingPage(cfg.URL, cfg.Fixture, client)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
records, err := parseHuaweiMaaSPricingCatalog(raw)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
records = dedupeOfficialPricingRecords(records)
|
|
if cfg.DryRun {
|
|
_, err = fmt.Fprintf(out, "source=huawei-maas-pricing-import models=%d operator=%s dry_run=true\n", len(records), records[0].OperatorName)
|
|
return err
|
|
}
|
|
if db == nil {
|
|
return fmt.Errorf("db is required when dry-run=false")
|
|
}
|
|
if err := upsertOfficialPricingRecords(db, records, "huawei-maas-pricing-import"); err != nil {
|
|
return err
|
|
}
|
|
var tableRows int
|
|
if err := db.QueryRow(`SELECT COUNT(*) FROM region_pricing`).Scan(&tableRows); err != nil {
|
|
return fmt.Errorf("count region_pricing: %w", err)
|
|
}
|
|
_, err = fmt.Fprintf(out, "source=huawei-maas-pricing-import models=%d operator=%s table_rows=%d dry_run=false\n", len(records), records[0].OperatorName, tableRows)
|
|
return err
|
|
}
|
|
|
|
func parseHuaweiMaaSPricingCatalog(raw string) ([]officialPricingRecord, error) {
|
|
var envelope huaweiMaaSPricingEnvelope
|
|
if err := json.Unmarshal([]byte(raw), &envelope); err != nil {
|
|
return nil, fmt.Errorf("parse huawei maas pricing json: %w", err)
|
|
}
|
|
items := envelope.Product["modelarts_modelarts.tokens"]
|
|
if len(items) == 0 {
|
|
return nil, fmt.Errorf("unexpected huawei maas pricing content")
|
|
}
|
|
|
|
type grouped struct {
|
|
providerType string
|
|
modelName string
|
|
inputs []float64
|
|
outputs []float64
|
|
}
|
|
byCode := map[string]*grouped{}
|
|
for _, item := range items {
|
|
entry := byCode[item.ResourceSpecCode]
|
|
if entry == nil {
|
|
entry = &grouped{providerType: item.ResourceSpecType, modelName: firstNonEmptyText(item.ModelName, item.ResourceSpecCode)}
|
|
byCode[item.ResourceSpecCode] = entry
|
|
}
|
|
for _, plan := range item.PlanList {
|
|
switch {
|
|
case strings.HasPrefix(plan.UsageFactor, "input"):
|
|
entry.inputs = append(entry.inputs, plan.Amount)
|
|
case strings.HasPrefix(plan.UsageFactor, "output"):
|
|
entry.outputs = append(entry.outputs, plan.Amount)
|
|
}
|
|
}
|
|
}
|
|
|
|
keys := make([]string, 0, len(byCode))
|
|
for code := range byCode {
|
|
keys = append(keys, code)
|
|
}
|
|
sort.Strings(keys)
|
|
|
|
records := make([]officialPricingRecord, 0, len(keys))
|
|
for _, code := range keys {
|
|
entry := byCode[code]
|
|
if len(entry.inputs) == 0 || len(entry.outputs) == 0 {
|
|
continue
|
|
}
|
|
sort.Float64s(entry.inputs)
|
|
sort.Float64s(entry.outputs)
|
|
providerName := normalizeHuaweiMaaSProvider(entry.providerType, entry.modelName)
|
|
providerNameCn, providerCountry, providerWebsite := providerMetadata(providerName)
|
|
records = append(records, officialPricingRecord{
|
|
ModelID: normalizeExternalID("huawei-maas", entry.modelName),
|
|
ModelName: entry.modelName,
|
|
ProviderName: providerName,
|
|
ProviderNameCn: providerNameCn,
|
|
ProviderCountry: providerCountry,
|
|
ProviderWebsite: providerWebsite,
|
|
OperatorName: "Huawei Cloud MaaS",
|
|
OperatorNameCn: "华为云 MaaS",
|
|
OperatorCountry: "CN",
|
|
OperatorWebsite: "https://www.huaweicloud.com/product/maas.html",
|
|
OperatorType: "official",
|
|
Region: "CN",
|
|
Currency: "CNY",
|
|
InputPrice: entry.inputs[0],
|
|
OutputPrice: entry.outputs[0],
|
|
SourceURL: defaultHuaweiMaaSPricingSourceURL,
|
|
ModelSourceURL: defaultHuaweiMaaSPricingSourceURL,
|
|
DateConfidence: "unknown",
|
|
DateSourceKind: "official_pricing",
|
|
Modality: detectModality(entry.modelName),
|
|
})
|
|
}
|
|
if len(records) == 0 {
|
|
return nil, fmt.Errorf("no huawei maas input/output pricing rows found")
|
|
}
|
|
return records, nil
|
|
}
|
|
|
|
func normalizeHuaweiMaaSProvider(providerType string, modelName string) string {
|
|
switch strings.ToLower(strings.TrimSpace(providerType)) {
|
|
case "deepseek":
|
|
return "DeepSeek"
|
|
case "qwen", "multimodalunderstanding":
|
|
return "Qwen"
|
|
case "glm":
|
|
return "Zhipu AI"
|
|
case "longcat":
|
|
return "LongCat"
|
|
default:
|
|
lower := strings.ToLower(modelName)
|
|
switch {
|
|
case strings.Contains(lower, "deepseek"):
|
|
return "DeepSeek"
|
|
case strings.Contains(lower, "qwen"):
|
|
return "Qwen"
|
|
case strings.Contains(lower, "glm"):
|
|
return "Zhipu AI"
|
|
default:
|
|
return strings.TrimSpace(providerType)
|
|
}
|
|
}
|
|
}
|