feat(imports): add real pricing and subscription collectors

Add plan catalog and subscription schema support, seed baselines, and real importers for core domestic subscriptions plus stable official pricing sources.

This commit also hardens the shared fetch layers so the importers can support live collection and database writes instead of relying on manual placeholders alone.
This commit is contained in:
phamnazage-jpg
2026-05-15 22:32:57 +08:00
parent dd58c18fe3
commit 958245537a
91 changed files with 10474 additions and 1 deletions

View File

@@ -0,0 +1,61 @@
-- Phase 2: Token Plan / Coding Plan 基础目录清单
CREATE TABLE IF NOT EXISTS plan_catalog_inventory (
id BIGSERIAL PRIMARY KEY,
provider_id BIGINT REFERENCES model_provider(id) ON DELETE SET NULL,
operator_id BIGINT REFERENCES operator(id) ON DELETE SET NULL,
catalog_code TEXT NOT NULL UNIQUE,
platform_name TEXT NOT NULL,
platform_name_cn TEXT,
platform_type TEXT NOT NULL,
plan_family TEXT NOT NULL,
plan_status TEXT NOT NULL DEFAULT 'confirmed',
source_url TEXT NOT NULL,
source_title TEXT,
source_kind TEXT NOT NULL DEFAULT 'official_doc',
region TEXT NOT NULL DEFAULT 'global',
currency TEXT,
billing_cycle TEXT,
last_checked_at TIMESTAMP NOT NULL,
importer_key TEXT,
notes TEXT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
created_by TEXT DEFAULT 'system',
updated_by TEXT DEFAULT 'system',
CONSTRAINT chk_plan_catalog_platform_type
CHECK (platform_type IN ('official_vendor', 'cloud_operator', 'relay_platform')),
CONSTRAINT chk_plan_catalog_family
CHECK (plan_family IN ('token_plan', 'coding_plan', 'package_plan', 'pay_as_you_go', 'unknown')),
CONSTRAINT chk_plan_catalog_status
CHECK (plan_status IN ('confirmed', 'pending_verification', 'retired')),
CONSTRAINT chk_plan_catalog_source_kind
CHECK (source_kind IN ('official_doc', 'official_pricing', 'official_product_page', 'official_community', 'inferred')),
CONSTRAINT chk_plan_catalog_currency
CHECK (currency IS NULL OR currency IN ('CNY', 'USD', 'EUR'))
);
CREATE INDEX IF NOT EXISTS idx_plan_catalog_provider_id ON plan_catalog_inventory(provider_id);
CREATE INDEX IF NOT EXISTS idx_plan_catalog_operator_id ON plan_catalog_inventory(operator_id);
CREATE INDEX IF NOT EXISTS idx_plan_catalog_family ON plan_catalog_inventory(plan_family);
CREATE INDEX IF NOT EXISTS idx_plan_catalog_platform_type ON plan_catalog_inventory(platform_type);
CREATE INDEX IF NOT EXISTS idx_plan_catalog_status ON plan_catalog_inventory(plan_status);
CREATE INDEX IF NOT EXISTS idx_plan_catalog_last_checked_at ON plan_catalog_inventory(last_checked_at);
COMMENT ON TABLE plan_catalog_inventory IS 'Token Plan / Coding Plan / 套餐包 / 按量计费基础目录清单,用于后续 importer 排期与证据管理';
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_proc WHERE proname = 'update_updated_at_column')
AND NOT EXISTS (
SELECT 1
FROM pg_trigger
WHERE tgname = 'plan_catalog_inventory_updated_at'
) THEN
CREATE TRIGGER plan_catalog_inventory_updated_at
BEFORE UPDATE ON plan_catalog_inventory
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
END IF;
END
$$;

View File

@@ -0,0 +1,35 @@
-- Phase 2: 基础目录增加榜单分组与排名信息
ALTER TABLE plan_catalog_inventory
ADD COLUMN IF NOT EXISTS catalog_segment TEXT NOT NULL DEFAULT 'general',
ADD COLUMN IF NOT EXISTS market_rank INTEGER;
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'chk_plan_catalog_segment'
) THEN
ALTER TABLE plan_catalog_inventory
ADD CONSTRAINT chk_plan_catalog_segment
CHECK (catalog_segment IN ('general', 'vendor_top20', 'relay_top20plus', 'global_reference'));
END IF;
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'chk_plan_catalog_market_rank'
) THEN
ALTER TABLE plan_catalog_inventory
ADD CONSTRAINT chk_plan_catalog_market_rank
CHECK (market_rank IS NULL OR market_rank > 0);
END IF;
END
$$;
CREATE INDEX IF NOT EXISTS idx_plan_catalog_segment ON plan_catalog_inventory(catalog_segment);
CREATE INDEX IF NOT EXISTS idx_plan_catalog_market_rank ON plan_catalog_inventory(market_rank);
COMMENT ON COLUMN plan_catalog_inventory.catalog_segment IS '目录分组general / vendor_top20 / relay_top20plus / global_reference';
COMMENT ON COLUMN plan_catalog_inventory.market_rank IS '榜单排序,数字越小优先级越高';

View File

@@ -0,0 +1,22 @@
-- 补齐 operator.type 字段,避免订阅与目录 importer 在新库中失败
ALTER TABLE operator
ADD COLUMN IF NOT EXISTS type TEXT NOT NULL DEFAULT 'reseller';
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'chk_operator_type'
) THEN
ALTER TABLE operator
ADD CONSTRAINT chk_operator_type
CHECK (type IN ('official', 'cloud', 'relay', 'reseller'));
END IF;
END
$$;
CREATE INDEX IF NOT EXISTS idx_operator_type ON operator(type);
COMMENT ON COLUMN operator.type IS '运营方类型official / cloud / relay / reseller';

View File

@@ -0,0 +1,8 @@
-- Phase 2: 订阅套餐表支持 package_plan
ALTER TABLE subscription_plan
DROP CONSTRAINT IF EXISTS subscription_plan_plan_family_check;
ALTER TABLE subscription_plan
ADD CONSTRAINT subscription_plan_plan_family_check
CHECK (plan_family IN ('token_plan', 'coding_plan', 'package_plan'));

View File

@@ -0,0 +1,41 @@
-- 第一模块:每日关键信号快照
CREATE TABLE IF NOT EXISTS daily_signal_snapshot (
id BIGSERIAL PRIMARY KEY,
signal_date DATE NOT NULL UNIQUE,
status TEXT NOT NULL DEFAULT 'generated',
new_models INTEGER NOT NULL DEFAULT 0,
price_changes INTEGER NOT NULL DEFAULT 0,
official_free INTEGER NOT NULL DEFAULT 0,
aggregator_free INTEGER NOT NULL DEFAULT 0,
unknown_free INTEGER NOT NULL DEFAULT 0,
event_count INTEGER NOT NULL DEFAULT 0,
page_mode TEXT NOT NULL DEFAULT 'standard',
event_type_counts JSONB NOT NULL DEFAULT '{}'::jsonb,
top_events JSONB NOT NULL DEFAULT '[]'::jsonb,
source_audit TEXT,
generated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_daily_signal_snapshot_date ON daily_signal_snapshot(signal_date);
CREATE INDEX IF NOT EXISTS idx_daily_signal_snapshot_status ON daily_signal_snapshot(status);
COMMENT ON TABLE daily_signal_snapshot IS '第一模块产出的每日关键信号快照,用于日报与其他下游形态消费';
COMMENT ON COLUMN daily_signal_snapshot.top_events IS '已筛选的关键事件数组JSONB 序列化 ModelEvent';
COMMENT ON COLUMN daily_signal_snapshot.event_type_counts IS '按事件类型聚合的数量统计';
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_trigger
WHERE tgname = 'daily_signal_snapshot_updated_at'
) THEN
CREATE TRIGGER daily_signal_snapshot_updated_at
BEFORE UPDATE ON daily_signal_snapshot
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
END IF;
END
$$;

View File

@@ -0,0 +1,21 @@
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_models_date_source_kind') THEN
ALTER TABLE models DROP CONSTRAINT chk_models_date_source_kind;
END IF;
ALTER TABLE models
ADD CONSTRAINT chk_models_date_source_kind
CHECK (
date_source_kind IN (
'official_announcement',
'official_product_page',
'official_pricing',
'secondary_authoritative_report',
'catalog_backfill',
'unknown'
)
);
END $$;
COMMENT ON COLUMN models.date_source_kind IS '发布日期证据来源类型official_announcement / official_product_page / official_pricing / secondary_authoritative_report / catalog_backfill / unknown';

View File

@@ -0,0 +1,234 @@
# Token Plan / Coding Plan 基础目录
更新时间2026-05-15Asia/Shanghai
## 目标
这份清单解决两个问题:
1. 先把“哪些平台确实存在 Token Plan / Coding Plan / 套餐包,哪些只有按量计费”整理成统一基线。
2. 再把这份基线落到数据库 `plan_catalog_inventory`,为后续每个平台的 importer 排期、证据追踪和验收提供稳定入口。
注意:这里记录的是**平台级事实**,不是最终的套餐明细落库。真正的套餐条目仍然应进入 `subscription_plan`,按模型按量价格仍然应进入 `region_pricing`
截至 2026-05-15这份基线已经扩展到
- 国内官方模型厂家 Top 20
- 国内中转 / 聚合 / 云厂商平台 20+
- 全球官方模型平台与全球多模型中转平台参考集
- `plan_catalog_inventory` 最终落库 70 条目录记录
- `subscription_plan` 新增一批手工核实套餐 seed用于在真正抓取器到位前先支撑日报对比
## 分类约定
- `token_plan`:按 token 或 credits 统一额度管理的订阅型方案
- `coding_plan`:按 AI 编码场景设计的包月/限额订阅方案
- `package_plan`:华为云这类“按量 + 套餐包并存”的资源包方案
- `pay_as_you_go`:官方当前只提供按量计费,未发现独立 Token Plan / Coding Plan
- `unknown`:官方已确认平台存在,但公开页面暂未给出可稳定结构化的套餐命名
## 国内官方厂家 Top 20
这不是第三方市场报告意义上的绝对“排名”,而是基于 2026-05-14 当天可公开验证的开放平台能力、行业知名度和接入优先级整理出的 Top 20 清单,方便后续 importer 排期:
1. 阿里巴巴 / 通义千问
2. 腾讯 / 混元
3. 百度 / 文心
4. 字节跳动 / 豆包、Seed
5. 智谱 AI
6. 华为 / 盘古
7. DeepSeek
8. Moonshot AI
9. MiniMax
10. 阶跃星辰
11. 百川智能
12. 零一万物
13. 商汤日日新
14. 科大讯飞星火
15. 360 智脑
16. 网易有道子曰
17. 面壁智能 MiniCPM
18. 智源 FlagOpen
19. 昆仑万维天工 / Skywork
20. 无问芯穹
对应 seed`seeds/plan_catalog_inventory_seed_cn_vendors_top20.json`
## 国内中转 / 聚合平台 20+
当前已经纳入目录基线的平台包括:
1. 腾讯云 TokenHub
2. 腾讯云 CloudBase AI+
3. 腾讯云 TI 平台大模型广场
4. 阿里云百炼
5. 魔搭 API-Inference
6. 百度千帆
7. 火山方舟
8. 华为云 MaaS
9. 天翼云模型推理服务
10. 天翼云息壤
11. 联通云 AICP
12. 联通云 AI 应用开发平台
13. 移动云 AI 应用专区
14. 有道智云 MaaS
15. 360 智脑开放平台
16. 硅基流动 SiliconCloud
17. PPIO Model API
18. UCloud UModelVerse
19. 青云 CoresHub
20. 金山云星流平台
21. 以及腾讯云、阿里云、百度千帆各自拆分出的 Token Plan / Coding Plan / 企业版目录项
对应 seed`seeds/plan_catalog_inventory_seed_cn_relays_top20plus.json`
## 全球官方 / 中转参考集
本轮通过 web 搜索补录并进入目录基线的平台包括:
1. Google Gemini API
2. Mistral La Plateforme
3. Cohere Platform
4. OpenRouter
5. Together AI
6. Fireworks AI
7. DeepInfra
8. GroqCloud
9. Replicate
10. Hyperbolic
11. Novita AI
12. Azure OpenAI Service
13. Amazon Bedrock
14. Vertex AI Generative AI
15. Cloudflare Workers AI
16. Baseten
17. Cerebras Inference
18. Perplexity Agent API
19. SambaNova Cloud
20. 京东云 JoyBuilder
对应 seed`seeds/plan_catalog_inventory_seed_web_research.json`
## 云服务中转 / 云厂商平台
| 平台 | 当前结论 | 目录归类 | 后续 importer |
|------|----------|----------|---------------|
| 腾讯云 TokenHub | 已确认 `Token Plan`(个人版、企业版专业、企业版轻享)与 `Coding Plan` 并存 | `token_plan` + `coding_plan` | 已接入 `tencent_catalog` / `import_tencent_subscription.go` |
| 阿里云百炼 | 已确认 `Token Plan团队版``Coding Plan` 并存,且仍保留按量计费 | `token_plan` + `coding_plan` | 已接入 `import_aliyun_subscription.go` |
| 百度千帆 | 已确认 `Coding Plan``Token 福利包` 并存,后者存在首购优惠价 | `coding_plan` + `token_plan` | 已接入 `import_baidu_subscription.go` |
| 火山方舟 | 已从官方开发者社区确认 `Coding Plan` 已上线,且公开披露标准月费与首月活动价 | `coding_plan` | 已接入 `import_bytedance_subscription.go` |
| 天翼云模型推理服务 | 已确认 `Coding Plan` 与活动型 `Token Plan` 并存 | `coding_plan` + `token_plan` | 已接入 `import_ctyun_subscription.go` |
| 华为云 MaaS | 当前明确支持“按 Token 付费 + 套餐包/资源包计费”,不是 `Coding Plan` 命名体系 | `package_plan` | 已接入 `import_huawei_package.go` |
### 证据入口
- 腾讯云 Token Plan 个人版:[cloud.tencent.com/document/product/1823/130060](https://cloud.tencent.com/document/product/1823/130060)
- 腾讯云 Token Plan 企业版专业套餐:[cloud.tencent.com/document/product/1823/130659](https://cloud.tencent.com/document/product/1823/130659)
- 腾讯云 Token Plan 企业版轻享套餐:[cloud.tencent.com/document/product/1823/131173](https://cloud.tencent.com/document/product/1823/131173)
- 腾讯云 Coding Plan 规则页:[cloud.tencent.com/document/product/1823/130103](https://cloud.tencent.com/document/product/1823/130103)
- 阿里云百炼 Token Plan 概述:[help.aliyun.com/zh/model-studio/token-plan-overview](https://help.aliyun.com/zh/model-studio/token-plan-overview)
- 阿里云百炼 Coding Plan 概述:[help.aliyun.com/zh/model-studio/coding-plan-quickstart](https://help.aliyun.com/zh/model-studio/coding-plan-quickstart)
- 百度千帆 Coding Plan[cloud.baidu.com/doc/qianfan/s/imlg0beiu](https://cloud.baidu.com/doc/qianfan/s/imlg0beiu)
- 百度千帆 Token 福利包:[cloud.baidu.com/doc/qianfan/s/Smoghsq3g](https://cloud.baidu.com/doc/qianfan/s/Smoghsq3g)
- 火山方舟 Coding Plan 社区文章:[developer.volcengine.com/articles/7604465649330749490](https://developer.volcengine.com/articles/7604465649330749490)
- 华为云 MaaS 文本生成模型计费说明:[support.huaweicloud.com/price-maas/price-maas-0002.html](https://support.huaweicloud.com/price-maas/price-maas-0002.html)
## 官方模型平台
| 平台 | 当前结论 | 目录归类 | 说明 |
|------|----------|----------|------|
| 智谱 AI | 已确认 `GLM Coding Plan` | `coding_plan` | 已接入 `import_zhipu_coding_plan.go`,当前先落公开活动底价与套餐能力说明 |
| MiniMax | 已确认 `Token Plan` | `token_plan` | 同时保留按量计费 API Key 切换路径 |
| OpenAI | 当前以按量计费为主,未检索到官方 `Token Plan` / `Coding Plan` | `pay_as_you_go` | 继续走现有官方价格 importer 思路 |
| Anthropic | 当前以按量计费为主,未检索到官方 `Token Plan` / `Coding Plan` | `pay_as_you_go` | 只有模型定价、缓存与批处理折扣 |
| DeepSeek | 当前以按量计费为主,未检索到官方 `Token Plan` / `Coding Plan` | `pay_as_you_go` | 支持赠送余额与限时折扣 |
| Moonshot AI | 当前以按量计费为主,未检索到官方 `Token Plan` / `Coding Plan` | `pay_as_you_go` | 官方重点仍是 Token 单价与缓存计费 |
| xAI | 当前以按量计费为主,未检索到官方 `Token Plan` / `Coding Plan` | `pay_as_you_go` | 同时支持工具调用计费和批处理折扣 |
### 证据入口
- 智谱 GLM Coding Plan[docs.bigmodel.cn/cn/coding-plan/overview](https://docs.bigmodel.cn/cn/coding-plan/overview)
- MiniMax Token Plan[platform.minimaxi.com/docs/token-plan/intro](https://platform.minimaxi.com/docs/token-plan/intro)
- OpenAI Pricing[platform.openai.com/docs/pricing](https://platform.openai.com/docs/pricing/)
- Anthropic Pricing[docs.anthropic.com/en/docs/about-claude/pricing](https://docs.anthropic.com/en/docs/about-claude/pricing)
- DeepSeek Pricing[api-docs.deepseek.com/zh-cn/quick_start/pricing](https://api-docs.deepseek.com/zh-cn/quick_start/pricing/)
- Moonshot Pricing[platform.moonshot.cn/docs/pricing/chat](https://platform.moonshot.cn/docs/pricing/chat)
- xAI Pricing[docs.x.ai/developers/pricing](https://docs.x.ai/developers/pricing)
## 数据库落点
本次新增的数据库清单表:
- 表名:`plan_catalog_inventory`
- 作用:保存平台级证据与 importer 排期,而不是最终套餐明细
- 导入脚本:`scripts/import_plan_catalog.go`
- seed 文件:
- `seeds/plan_catalog_inventory_seed.json`
- `seeds/plan_catalog_inventory_seed_cn_vendors_top20.json`
- `seeds/plan_catalog_inventory_seed_cn_relays_top20plus.json`
- `seeds/plan_catalog_inventory_seed_web_research.json`
- 新增字段:
- `catalog_segment``general / vendor_top20 / relay_top20plus / global_reference`
- `market_rank`:榜单顺序
本次还保留了一个手工套餐 seed 导入器,作为极少数暂无稳定公开结构化页面的平台兜底手段:
- 导入脚本:`scripts/import_manual_subscription_seed.go`
- seed 文件:`seeds/subscription_plan_manual_seed.json`
- 当前覆盖无生产链路默认启用的平台MiniMax Token Plan 已切换到真实 importer
建议使用顺序:
1. 先更新 `plan_catalog_inventory`
2. 再根据 `catalog_segment + market_rank + plan_family + importer_key` 排出平台实现顺序
3. 已确认且价格明确的套餐,先通过手工 seed 进入 `subscription_plan`
4. 官方按量价格继续进入 `region_pricing`
## 当前 importer 状态
已完成:
1. `tencent_catalog` / `import_tencent_subscription.go`
2. `import_aliyun_subscription.go`
3. `import_baidu_subscription.go`
4. `import_ctyun_subscription.go`
这批平台现在都已经进入真实抓取或目录级实时校验链路:
1. `import_bytedance_subscription.go`
2. `import_huawei_package.go`
3. `import_zhipu_coding_plan.go`
4. `import_minimax_subscription.go`
5. `import_cucloud_catalog.go`
6. `import_mobile_cloud_catalog.go`
新增已完成:
1. `import_youdao_pricing.go`
2. `import_360_pricing.go`
3. `import_siliconflow_pricing.go`
4. `import_ppio_pricing.go`
5. `import_ucloud_pricing.go`
6. `import_cloudflare_pricing.go`
7. `import_perplexity_pricing.go`
8. `import_vertex_pricing.go`
9. `import_bedrock_pricing.go`
10. `import_azure_openai_pricing.go`
11. `import_minimax_subscription.go`
这些平台统一按 `pay_as_you_go -> region_pricing` 处理,直接抓取官方公开模型价格,不再停留在 `future_official_pricing`
其中 `SiliconFlow` 当前优先尝试官方价格入口;若入口返回站点落地页或临时不可用,则回退到仓库内最近核验的官方快照,避免日跑流水线因前端路由问题中断。
对于暂时没有稳定公开结构化价格页、但官方平台入口已经确认的长尾平台,当前统一归到:
- `import_catalog_seed_verification.go`
这条链路属于目录级官方入口核验,会持续回写 `plan_catalog_inventory.last_checked_at` 和核验备注,确保第一模块的覆盖方式已经定型,不再保留 `future_official_pricing` 占位状态。
下一步建议优先级:
1. `QingCloud / CoresHub`
2. `火山方舟按量模型价格官方页`
3. `华为云 MaaS 按量模型价格页`
4. `移动云更细颗粒度的模型 API 价格`
5. `联通云更细颗粒度的模型 API 价格`

View File

@@ -38,7 +38,7 @@
### 数据与迁移 ### 数据与迁移
- 已执行 `bash scripts/apply_migration.sh` - 已执行 `bash scripts/apply_migration.sh`
- `daily_report``report_runs``subscription_plan``region_pricing` 等关键表存在 - `daily_report``report_runs``subscription_plan``region_pricing``daily_signal_snapshot` 等关键表存在
- 历史数据回填策略已确认,避免上线首日“空库” - 历史数据回填策略已确认,避免上线首日“空库”
### 应用与产物 ### 应用与产物

View File

@@ -0,0 +1,179 @@
//go:build llm_script
package main
import (
"fmt"
"regexp"
"strings"
)
const (
defaultAliyunTokenPlanURL = "https://help.aliyun.com/zh/model-studio/token-plan-overview"
defaultAliyunCodingPlanURL = "https://help.aliyun.com/zh/model-studio/coding-plan-quickstart"
)
func parseAliyunSubscriptionCatalog(tokenRaw string, codingRaw string) ([]subscriptionImportRecord, error) {
publishedAt, known := publishedAtFromText(firstNonEmptyText(tokenRaw, codingRaw))
tokenRecords, err := parseAliyunTokenPlan(tokenRaw, publishedAt)
if err != nil {
return nil, err
}
codingRecords, err := parseAliyunCodingPlan(codingRaw, publishedAt)
if err != nil {
return nil, err
}
records := append(tokenRecords, codingRecords...)
for i := range records {
records[i].PublishedAtKnown = known
}
return records, nil
}
func parseAliyunTokenPlan(raw string, publishedAt string) ([]subscriptionImportRecord, error) {
seatPattern := regexp.MustCompile(`(?s)(标准坐席|高级坐席|尊享坐席)\s+¥([\d,]+)(?:/坐席/月)?\s+([\d,]+)\s*Credits/坐席/月\s+([^\n]+)`)
matches := seatPattern.FindAllStringSubmatch(raw, -1)
if len(matches) != 3 {
return nil, fmt.Errorf("unexpected aliyun token seat count: %d", len(matches))
}
tierCodes := map[string]string{
"标准坐席": "standard-seat",
"高级坐席": "advanced-seat",
"尊享坐席": "premium-seat",
}
tierNames := map[string]string{
"标准坐席": "Standard",
"高级坐席": "Advanced",
"尊享坐席": "Premium",
}
records := make([]subscriptionImportRecord, 0, 4)
for _, match := range matches {
records = append(records, subscriptionImportRecord{
ProviderName: "Alibaba",
ProviderNameCn: "阿里巴巴",
ProviderCountry: "CN",
ProviderWebsite: "https://www.aliyun.com",
OperatorName: "Alibaba Bailian",
OperatorNameCn: "阿里云百炼",
OperatorCountry: "CN",
OperatorWebsite: "https://help.aliyun.com/zh/model-studio/",
OperatorType: "cloud",
PlanFamily: "token_plan",
PlanCode: "aliyun-token-plan-" + tierCodes[match[1]],
PlanName: "Token Plan 团队版 " + match[1],
Tier: tierNames[match[1]],
BillingCycle: "monthly",
Currency: "CNY",
ListPrice: mustParseSubscriptionPrice(match[2]),
PriceUnit: "CNY/month",
QuotaValue: mustParseSubscriptionInt64(match[3]),
QuotaUnit: "credits/month",
PlanScope: "Token Plan 团队版",
SourceURL: defaultAliyunTokenPlanURL,
PublishedAt: publishedAt,
EffectiveDate: effectiveDateFromPublishedAt(publishedAt),
Notes: strings.TrimSpace(match[4]),
})
}
sharedPattern := regexp.MustCompile(`共享用量包\s+¥([\d,]+)(?:/个)?\s+([\d,]+)\s*Credits/个`)
shared := sharedPattern.FindStringSubmatch(raw)
if len(shared) != 3 {
return nil, fmt.Errorf("aliyun shared pack not found")
}
records = append(records, subscriptionImportRecord{
ProviderName: "Alibaba",
ProviderNameCn: "阿里巴巴",
ProviderCountry: "CN",
ProviderWebsite: "https://www.aliyun.com",
OperatorName: "Alibaba Bailian",
OperatorNameCn: "阿里云百炼",
OperatorCountry: "CN",
OperatorWebsite: "https://help.aliyun.com/zh/model-studio/",
OperatorType: "cloud",
PlanFamily: "token_plan",
PlanCode: "aliyun-token-plan-shared-pack",
PlanName: "Token Plan 团队版 共享用量包",
Tier: "SharedPack",
BillingCycle: "monthly",
Currency: "CNY",
ListPrice: mustParseSubscriptionPrice(shared[1]),
PriceUnit: "CNY/pack",
QuotaValue: mustParseSubscriptionInt64(shared[2]),
QuotaUnit: "credits/pack",
PlanScope: "Token Plan 团队版",
SourceURL: defaultAliyunTokenPlanURL,
PublishedAt: publishedAt,
EffectiveDate: effectiveDateFromPublishedAt(publishedAt),
Notes: "跨坐席共享的弹性用量包,有效期 1 个月。",
})
return records, nil
}
func parseAliyunCodingPlan(raw string, publishedAt string) ([]subscriptionImportRecord, error) {
pricePattern := regexp.MustCompile(`价格\s+¥\s*([\d,]+)\s*/月`)
priceMatch := pricePattern.FindStringSubmatch(raw)
if len(priceMatch) != 2 {
return nil, fmt.Errorf("aliyun coding plan price not found")
}
modelsPattern := regexp.MustCompile(`支持的模型\s+\|\s+推荐模型:([^\n]+)`)
modelsMatch := modelsPattern.FindStringSubmatch(raw)
modelScope := []string{}
if len(modelsMatch) == 2 {
for _, item := range strings.Split(modelsMatch[1], "、") {
item = strings.TrimSpace(item)
if item != "" {
modelScope = append(modelScope, item)
}
}
}
limitPattern := regexp.MustCompile(`每月\s*([\d,]+)\s*次请求`)
limitMatch := limitPattern.FindStringSubmatch(raw)
quotaValue := int64(0)
if len(limitMatch) == 2 {
quotaValue = mustParseSubscriptionInt64(limitMatch[1])
}
notes := []string{}
for _, fragment := range []string{
"Lite 套餐自 2026 年 3 月 20 日 00:00:00UTC+08:00起停止新购",
"活动已结束",
} {
if strings.Contains(raw, fragment) {
notes = append(notes, fragment)
}
}
return []subscriptionImportRecord{{
ProviderName: "Alibaba",
ProviderNameCn: "阿里巴巴",
ProviderCountry: "CN",
ProviderWebsite: "https://www.aliyun.com",
OperatorName: "Alibaba Bailian",
OperatorNameCn: "阿里云百炼",
OperatorCountry: "CN",
OperatorWebsite: "https://help.aliyun.com/zh/model-studio/",
OperatorType: "cloud",
PlanFamily: "coding_plan",
PlanCode: "aliyun-coding-plan-pro",
PlanName: "百炼 Coding Plan Pro",
Tier: "Pro",
BillingCycle: "monthly",
Currency: "CNY",
ListPrice: mustParseSubscriptionPrice(priceMatch[1]),
PriceUnit: "CNY/month",
QuotaValue: quotaValue,
QuotaUnit: "requests/month",
PlanScope: "Coding Plan",
ModelScope: modelScope,
SourceURL: defaultAliyunCodingPlanURL,
PublishedAt: publishedAt,
EffectiveDate: effectiveDateFromPublishedAt(publishedAt),
Notes: strings.Join(notes, ""),
}}, nil
}

View File

@@ -0,0 +1,225 @@
//go:build llm_script
package main
import (
"encoding/json"
"fmt"
"net/http"
"regexp"
"strings"
)
const defaultAzureOpenAIPricingURL = "https://prices.azure.com/api/retail/prices?api-version=2023-01-01-preview&currencyCode='USD'&$filter=contains(productName,'OpenAI')"
type azureRetailPriceResponse struct {
Items []azureRetailPriceItem `json:"Items"`
NextPageLink string `json:"NextPageLink"`
}
type azureRetailPriceItem struct {
CurrencyCode string `json:"currencyCode"`
RetailPrice float64 `json:"retailPrice"`
UnitPrice float64 `json:"unitPrice"`
Location string `json:"location"`
MeterName string `json:"meterName"`
ProductName string `json:"productName"`
SkuName string `json:"skuName"`
ServiceName string `json:"serviceName"`
UnitOfMeasure string `json:"unitOfMeasure"`
Type string `json:"type"`
ArmSkuName string `json:"armSkuName"`
ArmRegionName string `json:"armRegionName"`
IsPrimaryMeter bool `json:"isPrimaryMeterRegion"`
}
type azurePricingPair struct {
ModelName string
Region string
Currency string
InputPrice float64
OutputPrice float64
}
var azureKindPattern = regexp.MustCompile(`(?i)\b(inp|inpt|input|out|outp|outpt|output|opt)\b`)
func fetchAzureOpenAIPricingCatalog(url string, fixture string, client *http.Client) (string, error) {
if strings.TrimSpace(fixture) != "" {
return fetchRawPricingPage(url, fixture, client)
}
aggregated := azureRetailPriceResponse{}
seenPages := map[string]struct{}{}
nextURL := url
for strings.TrimSpace(nextURL) != "" {
if _, exists := seenPages[nextURL]; exists {
return "", fmt.Errorf("azure retail pricing pagination loop detected: %s", nextURL)
}
seenPages[nextURL] = struct{}{}
raw, err := fetchRawPricingPage(nextURL, "", client)
if err != nil {
return "", err
}
var page azureRetailPriceResponse
if err := json.Unmarshal([]byte(raw), &page); err != nil {
return "", fmt.Errorf("unmarshal azure retail pricing page: %w", err)
}
aggregated.Items = append(aggregated.Items, page.Items...)
nextURL = page.NextPageLink
}
payload, err := json.Marshal(aggregated)
if err != nil {
return "", fmt.Errorf("marshal azure retail pricing aggregate: %w", err)
}
return string(payload), nil
}
func parseAzureOpenAIPricingCatalog(raw string) ([]officialPricingRecord, error) {
var response azureRetailPriceResponse
if err := json.Unmarshal([]byte(raw), &response); err != nil {
return nil, fmt.Errorf("unmarshal azure retail pricing: %w", err)
}
pairs := make(map[string]*azurePricingPair)
for _, item := range response.Items {
kind, modelName, ok := classifyAzureRetailPrice(item)
if !ok {
continue
}
region := strings.TrimSpace(item.Location)
if region == "" {
region = "global"
}
currency := strings.TrimSpace(item.CurrencyCode)
if currency == "" {
currency = "USD"
}
key := strings.Join([]string{modelName, region, currency}, "|")
pair := pairs[key]
if pair == nil {
pair = &azurePricingPair{
ModelName: modelName,
Region: region,
Currency: currency,
}
pairs[key] = pair
}
price := item.UnitPrice
if strings.EqualFold(strings.TrimSpace(item.UnitOfMeasure), "1K") {
price *= 1000
}
if kind == "input" {
pair.InputPrice = price
} else {
pair.OutputPrice = price
}
}
records := make([]officialPricingRecord, 0, len(pairs))
providerNameCn, providerCountry, providerWebsite := providerMetadata("OpenAI")
for _, pair := range pairs {
if pair.InputPrice == 0 || pair.OutputPrice == 0 {
continue
}
record := officialPricingRecord{
ModelID: normalizeExternalID("azure-openai", pair.ModelName),
ModelName: pair.ModelName,
ProviderName: "OpenAI",
ProviderNameCn: providerNameCn,
ProviderCountry: providerCountry,
ProviderWebsite: providerWebsite,
OperatorName: "Microsoft Azure",
OperatorNameCn: "微软 Azure",
OperatorCountry: "US",
OperatorWebsite: "https://azure.microsoft.com",
OperatorType: "cloud",
Region: pair.Region,
Currency: pair.Currency,
InputPrice: pair.InputPrice,
OutputPrice: pair.OutputPrice,
SourceURL: defaultAzureOpenAIPricingURL,
ModelSourceURL: defaultAzureOpenAIPricingURL,
DateConfidence: "unknown",
DateSourceKind: "official_pricing",
Modality: detectModality(pair.ModelName),
}
record.IsFree = false
records = append(records, record)
}
if len(records) == 0 {
return nil, fmt.Errorf("no azure openai token prices found")
}
return records, nil
}
func classifyAzureRetailPrice(item azureRetailPriceItem) (string, string, bool) {
if item.ServiceName != "Foundry Models" || item.Type != "Consumption" {
return "", "", false
}
productLower := strings.ToLower(item.ProductName)
if !strings.Contains(productLower, "openai") || strings.Contains(productLower, "media") {
return "", "", false
}
name := strings.ToLower(strings.TrimSpace(strings.Join([]string{item.SkuName, item.MeterName, item.ArmSkuName}, " ")))
if !azureKindPattern.MatchString(name) {
return "", "", false
}
for _, blocked := range []string{
"batch",
"cache",
"cchd",
"prty",
" pp ",
"hosting",
"training",
" ft ",
"ft ",
" mdl ",
"grdr",
"file-search",
"code-interpreter",
"session",
"transcribe",
" aud ",
"audio",
" img ",
"image",
"voice",
"rt ",
"realtime",
"tool",
} {
if strings.Contains(name, blocked) {
return "", "", false
}
}
kind := "output"
if strings.Contains(name, "inp") || strings.Contains(name, "input") || strings.Contains(name, "inpt") {
kind = "input"
}
modelName := normalizeAzureModelName(item)
if modelName == "" {
return "", "", false
}
return kind, modelName, true
}
func normalizeAzureModelName(item azureRetailPriceItem) string {
base := strings.ToLower(strings.TrimSpace(item.MeterName))
replacer := strings.NewReplacer("-", " ", ".", ".", "_", " ")
base = replacer.Replace(base)
base = regexp.MustCompile(`(?i)\s+(inp|inpt|input|out|outp|outpt|output|opt)\b.*$`).ReplaceAllString(base, "")
base = strings.TrimSpace(base)
if base == "" {
return ""
}
if regexp.MustCompile(`^\d`).MatchString(base) {
base = "gpt " + base
}
base = regexp.MustCompile(`\s+`).ReplaceAllString(base, " ")
if strings.HasPrefix(base, "gpt ") {
return "GPT-" + strings.TrimSpace(strings.TrimPrefix(base, "gpt "))
}
return strings.ToUpper(base[:1]) + base[1:]
}

View File

@@ -0,0 +1,113 @@
//go:build llm_script
package main
import (
"fmt"
"regexp"
"strings"
)
const (
defaultBaiduCodingPlanURL = "https://cloud.baidu.com/doc/qianfan/s/imlg0beiu"
defaultBaiduTokenPlanURL = "https://cloud.baidu.com/doc/qianfan/s/Smoghsq3g"
)
func parseBaiduSubscriptionCatalog(codingRaw string, tokenRaw string) ([]subscriptionImportRecord, error) {
publishedAt, known := publishedAtFromText(firstNonEmptyText(codingRaw, tokenRaw))
codingRecords, err := parseBaiduCodingPlan(codingRaw, publishedAt)
if err != nil {
return nil, err
}
tokenRecords, err := parseBaiduTokenBenefitPack(tokenRaw, publishedAt)
if err != nil {
return nil, err
}
records := append(codingRecords, tokenRecords...)
for i := range records {
records[i].PublishedAtKnown = known
}
return records, nil
}
func parseBaiduCodingPlan(raw string, publishedAt string) ([]subscriptionImportRecord, error) {
pattern := regexp.MustCompile(`Coding Plan (Lite|Pro)\s+¥\s*([\d,]+)\s*/\s*月\s+每 5 小时:最多约 [\d,]+ 次请求\s+每周:最多约 [\d,]+ 次请求\s+每订阅月:最多约 ([\d,]+) 次请求`)
matches := pattern.FindAllStringSubmatch(raw, -1)
if len(matches) != 2 {
return nil, fmt.Errorf("unexpected baidu coding plan count: %d", len(matches))
}
records := make([]subscriptionImportRecord, 0, len(matches))
for _, match := range matches {
tier := match[1]
records = append(records, subscriptionImportRecord{
ProviderName: "Baidu",
ProviderNameCn: "百度",
ProviderCountry: "CN",
ProviderWebsite: "https://cloud.baidu.com",
OperatorName: "Baidu Qianfan",
OperatorNameCn: "百度千帆",
OperatorCountry: "CN",
OperatorWebsite: "https://cloud.baidu.com/doc/qianfan/index.html",
OperatorType: "cloud",
PlanFamily: "coding_plan",
PlanCode: "baidu-coding-plan-" + strings.ToLower(tier),
PlanName: "千帆 Coding Plan " + tier,
Tier: tier,
BillingCycle: "monthly",
Currency: "CNY",
ListPrice: mustParseSubscriptionPrice(match[2]),
PriceUnit: "CNY/month",
QuotaValue: mustParseSubscriptionInt64(match[3]),
QuotaUnit: "requests/month",
PlanScope: "Coding Plan",
SourceURL: defaultBaiduCodingPlanURL,
PublishedAt: publishedAt,
EffectiveDate: effectiveDateFromPublishedAt(publishedAt),
Notes: "额度按 5 小时、每周、每订阅月三重窗口刷新。",
})
}
return records, nil
}
func parseBaiduTokenBenefitPack(raw string, publishedAt string) ([]subscriptionImportRecord, error) {
pattern := regexp.MustCompile(`(\d{2,3},\d{3})\s+1个月\s+¥(\d+)\s+¥(\d+)`)
matches := pattern.FindAllStringSubmatch(raw, -1)
if len(matches) != 5 {
return nil, fmt.Errorf("unexpected baidu token benefit pack count: %d", len(matches))
}
records := make([]subscriptionImportRecord, 0, len(matches))
for _, match := range matches {
quota := mustParseSubscriptionInt64(match[1])
originalPrice := strings.TrimSpace(match[2])
promoPrice := strings.TrimSpace(match[3])
records = append(records, subscriptionImportRecord{
ProviderName: "Baidu",
ProviderNameCn: "百度",
ProviderCountry: "CN",
ProviderWebsite: "https://cloud.baidu.com",
OperatorName: "Baidu Qianfan",
OperatorNameCn: "百度千帆",
OperatorCountry: "CN",
OperatorWebsite: "https://cloud.baidu.com/doc/qianfan/index.html",
OperatorType: "cloud",
PlanFamily: "token_plan",
PlanCode: fmt.Sprintf("baidu-token-benefit-pack-%d", quota),
PlanName: fmt.Sprintf("千帆 Token 福利包 %d", quota),
Tier: fmt.Sprintf("%d", quota),
BillingCycle: "monthly",
Currency: "CNY",
ListPrice: mustParseSubscriptionPrice(promoPrice),
PriceUnit: "CNY/pack",
QuotaValue: quota,
QuotaUnit: "credits/pack",
PlanScope: "Token 福利包",
SourceURL: defaultBaiduTokenPlanURL,
PublishedAt: publishedAt,
EffectiveDate: effectiveDateFromPublishedAt(publishedAt),
Notes: fmt.Sprintf("首购优惠价 ¥%s原价 ¥%s有效期 1 个月。", promoPrice, originalPrice),
})
}
return records, nil
}

View File

@@ -0,0 +1,323 @@
//go:build llm_script
package main
import (
"fmt"
"regexp"
"strings"
)
const defaultBedrockPricingURL = "https://aws.amazon.com/bedrock/pricing/"
var (
bedrockRegionPattern = regexp.MustCompile(`(?s)<p><b>Regions?:&nbsp;([^<]+)</b></p>`)
bedrockTablePattern = regexp.MustCompile(`(?s)<table[^>]*>(.*?)</table>`)
bedrockRowPattern = regexp.MustCompile(`(?s)<tr>(.*?)</tr>`)
bedrockCellPattern = regexp.MustCompile(`(?s)<t[dh][^>]*>(.*?)</t[dh]>`)
)
func parseBedrockPricingCatalog(raw string) ([]officialPricingRecord, error) {
section := extractBetween(raw, `<h3 id="Model_Pricing"`, `<h2 id="Pricing_examples"`)
if strings.TrimSpace(section) == "" {
section = raw
}
blocks := splitBedrockProviderBlocks(section)
records := make([]officialPricingRecord, 0)
for _, block := range blocks {
records = append(records, parseBedrockProviderBlock(block.providerLabel, block.content)...)
}
if len(records) == 0 {
records = append(records, parseBedrockPricingTextFallback(cleanHTMLText(section))...)
}
if len(records) == 0 {
return nil, fmt.Errorf("no bedrock pricing rows found")
}
return records, nil
}
func parseBedrockProviderBlock(providerLabel string, raw string) []officialPricingRecord {
providerName := normalizeBedrockProvider(providerLabel)
providerNameCn, providerCountry, providerWebsite := providerMetadata(providerName)
regionMatches := bedrockRegionPattern.FindAllStringSubmatchIndex(raw, -1)
tables := bedrockTablePattern.FindAllStringSubmatchIndex(raw, -1)
records := make([]officialPricingRecord, 0)
seenModelRegion := make(map[string]struct{})
for _, tableIndex := range tables {
tableHTML := raw[tableIndex[2]:tableIndex[3]]
if !strings.Contains(tableHTML, "Price per 1M input tokens") || !strings.Contains(tableHTML, "$") {
continue
}
region := "global"
for _, regionIndex := range regionMatches {
if regionIndex[0] < tableIndex[0] {
region = cleanHTMLText(raw[regionIndex[2]:regionIndex[3]])
}
}
rows := parseBedrockTableRows(tableHTML)
for _, row := range rows {
dedupeKey := strings.Join([]string{region, row.ModelName}, "|")
if _, exists := seenModelRegion[dedupeKey]; exists {
continue
}
record := officialPricingRecord{
ModelID: normalizeExternalID("bedrock", providerName, row.ModelName),
ModelName: row.ModelName,
ProviderName: providerName,
ProviderNameCn: providerNameCn,
ProviderCountry: providerCountry,
ProviderWebsite: providerWebsite,
OperatorName: "Amazon Bedrock",
OperatorNameCn: "Amazon Bedrock",
OperatorCountry: "US",
OperatorWebsite: "https://aws.amazon.com/bedrock/",
OperatorType: "cloud",
Region: region,
Currency: "USD",
InputPrice: row.InputPrice,
OutputPrice: row.OutputPrice,
SourceURL: defaultBedrockPricingURL,
ModelSourceURL: defaultBedrockPricingURL,
DateConfidence: "unknown",
DateSourceKind: "official_pricing",
Modality: detectModality(row.ModelName),
}
record.IsFree = false
seenModelRegion[dedupeKey] = struct{}{}
records = append(records, record)
}
}
return records
}
type bedrockProviderBlock struct {
providerLabel string
content string
}
func splitBedrockProviderBlocks(raw string) []bedrockProviderBlock {
marker := `<h2 id="`
indices := make([]int, 0)
for offset := 0; ; {
next := strings.Index(raw[offset:], marker)
if next == -1 {
break
}
indices = append(indices, offset+next)
offset += next + len(marker)
}
blocks := make([]bedrockProviderBlock, 0, len(indices))
for i, start := range indices {
end := len(raw)
if i+1 < len(indices) {
end = indices[i+1]
}
chunk := raw[start:end]
h2End := strings.Index(chunk, "</h2>")
if h2End == -1 {
continue
}
openEnd := strings.Index(chunk, ">")
if openEnd == -1 || openEnd >= h2End {
continue
}
label := cleanHTMLText(chunk[openEnd+1 : h2End])
if strings.TrimSpace(label) == "" {
continue
}
blocks = append(blocks, bedrockProviderBlock{
providerLabel: label,
content: chunk,
})
}
return blocks
}
func extractBetween(raw string, startMarker string, endMarker string) string {
start := strings.Index(raw, startMarker)
if start == -1 {
return ""
}
segment := raw[start:]
if endMarker == "" {
return segment
}
end := strings.Index(segment, endMarker)
if end == -1 {
return segment
}
return segment[:end]
}
type bedrockPriceRow struct {
ModelName string
InputPrice float64
OutputPrice float64
}
func parseBedrockTableRows(tableHTML string) []bedrockPriceRow {
rows := bedrockRowPattern.FindAllStringSubmatch(tableHTML, -1)
parsed := make([]bedrockPriceRow, 0)
for _, row := range rows {
cells := bedrockCellPattern.FindAllStringSubmatch(row[1], -1)
if len(cells) < 3 {
continue
}
values := make([]string, 0, len(cells))
for _, cell := range cells {
values = append(values, cleanHTMLText(cell[1]))
}
if strings.Contains(strings.ToLower(values[0]), "models") {
continue
}
modelName := values[0]
inputCell := values[1]
outputCell := values[2]
if len(values) >= 6 && strings.Contains(strings.ToLower(values[5]), "$") {
outputCell = values[5]
}
inputPrice, ok := firstDollarPrice(inputCell)
if !ok {
continue
}
outputPrice, ok := firstDollarPrice(outputCell)
if !ok {
continue
}
parsed = append(parsed, bedrockPriceRow{
ModelName: modelName,
InputPrice: inputPrice,
OutputPrice: outputPrice,
})
}
return parsed
}
func normalizeBedrockProvider(raw string) string {
switch strings.TrimSpace(raw) {
case "Amazon Nova":
return "Amazon"
case "Anthropic":
return "Anthropic"
case "Cohere":
return "Cohere"
case "DeepSeek":
return "DeepSeek"
case "Meta":
return "Meta"
case "Mistral AI":
return "Mistral AI"
case "Moonshot AI":
return "Moonshot AI"
case "Kimi":
return "Moonshot AI"
case "NVIDIA":
return "NVIDIA"
case "OpenAI OSS Models":
return "OpenAI"
case "Qwen":
return "Qwen"
case "Writer":
return "Writer"
case "Z AI":
return "Zhipu AI"
default:
return strings.TrimSpace(raw)
}
}
var bedrockTextProviderHeaderPattern = regexp.MustCompile(`([A-Za-z][A-Za-z0-9 .&-]+)\s+models\s+Pr(?:i)?ce per 1M input tokens`)
var bedrockTextRowPattern = regexp.MustCompile(`([A-Za-z0-9 .:+-]+?)\s+\$\s*([0-9.]+)\s+\$\s*([0-9.]+)`)
func parseBedrockPricingTextFallback(raw string) []officialPricingRecord {
matches := bedrockTextProviderHeaderPattern.FindAllStringSubmatchIndex(raw, -1)
records := make([]officialPricingRecord, 0)
seen := make(map[string]struct{})
for i, match := range matches {
if len(match) < 4 {
continue
}
start := match[0]
end := len(raw)
if i+1 < len(matches) {
end = matches[i+1][0]
}
block := raw[start:end]
region := normalizeBedrockRegionText(findBedrockTextRegion(raw, start))
providerName := normalizeBedrockProvider(raw[match[2]:match[3]])
providerNameCn, providerCountry, providerWebsite := providerMetadata(providerName)
rows := bedrockTextRowPattern.FindAllStringSubmatch(block, -1)
for _, row := range rows {
if len(row) != 4 {
continue
}
modelName := strings.TrimSpace(row[1])
key := strings.Join([]string{providerName, region, modelName}, "|")
if _, exists := seen[key]; exists {
continue
}
seen[key] = struct{}{}
records = append(records, officialPricingRecord{
ModelID: normalizeExternalID("bedrock", providerName, modelName),
ModelName: modelName,
ProviderName: providerName,
ProviderNameCn: providerNameCn,
ProviderCountry: providerCountry,
ProviderWebsite: providerWebsite,
OperatorName: "Amazon Bedrock",
OperatorNameCn: "Amazon Bedrock",
OperatorCountry: "US",
OperatorWebsite: "https://aws.amazon.com/bedrock/",
OperatorType: "cloud",
Region: region,
Currency: "USD",
InputPrice: mustParseSubscriptionPrice(row[2]),
OutputPrice: mustParseSubscriptionPrice(row[3]),
SourceURL: defaultBedrockPricingURL,
ModelSourceURL: defaultBedrockPricingURL,
DateConfidence: "unknown",
DateSourceKind: "official_pricing",
Modality: detectModality(modelName),
})
}
}
return records
}
func findBedrockTextRegion(raw string, headerStart int) string {
prefixStart := headerStart - 300
if prefixStart < 0 {
prefixStart = 0
}
prefix := raw[prefixStart:headerStart]
lastPlural := strings.LastIndex(prefix, "Regions:")
lastSingular := strings.LastIndex(prefix, "Region:")
lastIndex := lastPlural
marker := "Regions:"
if lastSingular > lastIndex {
lastIndex = lastSingular
marker = "Region:"
}
if lastIndex == -1 {
return ""
}
region := strings.TrimSpace(prefix[lastIndex+len(marker):])
for _, stopMarker := range []string{" Priority ", " Flex ", " Batch ", " models "} {
if stop := strings.Index(region, stopMarker); stop != -1 {
region = strings.TrimSpace(region[:stop])
}
}
return region
}
func normalizeBedrockRegionText(raw string) string {
trimmed := strings.TrimSpace(raw)
if trimmed == "" {
return "global"
}
trimmed = strings.TrimSuffix(trimmed, ",")
return strings.Join(strings.Fields(trimmed), " ")
}

View File

@@ -0,0 +1,119 @@
//go:build llm_script
package main
import (
"fmt"
"regexp"
"strings"
)
const (
defaultBytedanceCodingPlanURL = "https://developer.volcengine.com/articles/7574419773204004906"
defaultBytedanceCodingPlanNotice = "https://developer.volcengine.com/articles/7604465649330749490"
)
func parseBytedanceSubscriptionCatalog(pricingRaw string, noticeRaw string) ([]subscriptionImportRecord, error) {
publishedAt, known := publishedAtFromText(firstNonEmptyText(pricingRaw, noticeRaw))
liteSection := sliceSection(pricingRaw, "Lite 套餐", "Pro 套餐")
proSection := sliceSection(pricingRaw, "Pro 套餐", "")
if strings.TrimSpace(liteSection) == "" || strings.TrimSpace(proSection) == "" {
return nil, fmt.Errorf("unexpected bytedance coding plan sections")
}
promoNote := "新用户首购优惠。"
if strings.Contains(noticeRaw, "每日 10:30") {
promoNote = "新用户首购优惠,每日 10:30 限量开放。"
}
records := make([]subscriptionImportRecord, 0, 4)
for _, tierSection := range []struct {
Tier string
Content string
}{
{Tier: "Lite", Content: liteSection},
{Tier: "Pro", Content: proSection},
} {
tier := tierSection.Tier
section := strings.TrimSpace(tierSection.Content)
lines := strings.Split(section, "\n")
scene := ""
for _, line := range lines {
line = strings.TrimSpace(line)
if line != "" {
scene = line
break
}
}
pricePattern := regexp.MustCompile(`([\d.]+)\s+元\s+([\d.]+)\s+元/月`)
priceMatch := pricePattern.FindStringSubmatch(section)
if len(priceMatch) != 3 {
return nil, fmt.Errorf("missing bytedance %s prices", tier)
}
quotaPattern := regexp.MustCompile(`每月约\s+([\d,]+)\s+次请求`)
quotaMatch := quotaPattern.FindStringSubmatch(section)
if len(quotaMatch) != 2 {
return nil, fmt.Errorf("missing bytedance %s monthly quota", tier)
}
promoPrice := mustParseSubscriptionPrice(priceMatch[1])
standardPrice := mustParseSubscriptionPrice(priceMatch[2])
monthlyQuota := mustParseSubscriptionInt64(quotaMatch[1])
tierCode := strings.ToLower(tier)
records = append(records, subscriptionImportRecord{
ProviderName: "ByteDance",
ProviderNameCn: "字节跳动",
ProviderCountry: "CN",
ProviderWebsite: "https://www.volcengine.com",
OperatorName: "ByteDance Volcano",
OperatorNameCn: "火山引擎",
OperatorCountry: "CN",
OperatorWebsite: "https://developer.volcengine.com",
OperatorType: "cloud",
PlanFamily: "coding_plan",
PlanCode: "bytedance-coding-plan-" + tierCode,
PlanName: "方舟 Coding Plan " + tier,
Tier: tier,
BillingCycle: "monthly",
Currency: "CNY",
ListPrice: standardPrice,
PriceUnit: "CNY/month",
QuotaValue: monthlyQuota,
QuotaUnit: "requests/month",
PlanScope: "方舟 Coding Plan",
SourceURL: defaultBytedanceCodingPlanURL,
PublishedAt: publishedAt,
EffectiveDate: effectiveDateFromPublishedAt(publishedAt),
Notes: scene + ";续费标准价。",
PublishedAtKnown: known,
})
records = append(records, subscriptionImportRecord{
ProviderName: "ByteDance",
ProviderNameCn: "字节跳动",
ProviderCountry: "CN",
ProviderWebsite: "https://www.volcengine.com",
OperatorName: "ByteDance Volcano",
OperatorNameCn: "火山引擎",
OperatorCountry: "CN",
OperatorWebsite: "https://developer.volcengine.com",
OperatorType: "cloud",
PlanFamily: "coding_plan",
PlanCode: "bytedance-coding-plan-" + tierCode + "-first-month",
PlanName: "方舟 Coding Plan " + tier + " 首月活动版",
Tier: tier + " Promo",
BillingCycle: "monthly",
Currency: "CNY",
ListPrice: promoPrice,
PriceUnit: "CNY/month",
QuotaValue: monthlyQuota,
QuotaUnit: "requests/month",
PlanScope: "方舟 Coding Plan",
SourceURL: defaultBytedanceCodingPlanURL,
PublishedAt: publishedAt,
EffectiveDate: effectiveDateFromPublishedAt(publishedAt),
Notes: scene + "" + promoNote,
PublishedAtKnown: known,
})
}
return records, nil
}

View File

@@ -0,0 +1,56 @@
//go:build llm_script
package main
import (
"database/sql"
"fmt"
"time"
_ "github.com/lib/pq"
)
type catalogVerificationRecord struct {
CatalogCode string
SourceURL string
SourceTitle string
PlanStatus string
Notes string
}
type catalogVerificationImportConfig struct {
URL string
Fixture string
DryRun bool
Timeout time.Duration
}
func upsertCatalogVerificationRecords(db *sql.DB, records []catalogVerificationRecord) error {
if len(records) == 0 {
return fmt.Errorf("catalog verification records are empty")
}
for _, record := range records {
result, err := db.Exec(
`UPDATE plan_catalog_inventory
SET source_url = $2,
source_title = $3,
plan_status = $4,
notes = $5,
last_checked_at = CURRENT_TIMESTAMP,
updated_at = CURRENT_TIMESTAMP
WHERE catalog_code = $1`,
record.CatalogCode, record.SourceURL, record.SourceTitle, record.PlanStatus, record.Notes,
)
if err != nil {
return fmt.Errorf("update plan_catalog_inventory %s: %w", record.CatalogCode, err)
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("rows affected for %s: %w", record.CatalogCode, err)
}
if rowsAffected == 0 {
return fmt.Errorf("catalog_code %s not found in plan_catalog_inventory", record.CatalogCode)
}
}
return nil
}

View File

@@ -0,0 +1,224 @@
//go:build llm_script
package main
import (
"fmt"
"regexp"
"strings"
)
const (
defaultCTYunCodingPlanURL = "https://www.ctyun.cn/document/11061839/11092368"
defaultCTYunTokenPlanURL = "https://www.ctyun.cn/act/AI/zhuanxiang"
)
func parseCTYunSubscriptionCatalog(codingRaw string, tokenRaw string) ([]subscriptionImportRecord, error) {
publishedAt, known := publishedAtFromText(firstNonEmptyText(codingRaw, tokenRaw))
codingRecords, err := parseCTYunCodingPlan(codingRaw, publishedAt)
if err != nil {
return nil, err
}
tokenRecords, err := parseCTYunTokenPlan(tokenRaw, publishedAt)
if err != nil {
return nil, err
}
records := append(codingRecords, tokenRecords...)
for i := range records {
records[i].PublishedAtKnown = known
}
return records, nil
}
func parseCTYunCodingPlan(raw string, publishedAt string) ([]subscriptionImportRecord, error) {
if !strings.Contains(raw, "GLM Lite") || !strings.Contains(raw, "GLM Max") {
return nil, fmt.Errorf("ctyun coding plan tiers not found")
}
pricePattern := regexp.MustCompile(`包月价格\s+(\d+)元/月\s+(\d+)元/月\s+(\d+)元/月`)
priceMatch := pricePattern.FindStringSubmatch(raw)
if len(priceMatch) != 4 {
return nil, fmt.Errorf("ctyun coding plan monthly prices not found")
}
limitPattern := regexp.MustCompile(`每月最多约([\d,]+)次prompts`)
limitMatches := limitPattern.FindAllStringSubmatch(raw, -1)
if len(limitMatches) < 3 {
return nil, fmt.Errorf("ctyun coding plan monthly limits not found")
}
modelScope := extractCTYunCodingModels(raw)
records := []subscriptionImportRecord{
{
ProviderName: "Telecom",
ProviderNameCn: "中国电信",
ProviderCountry: "CN",
ProviderWebsite: "https://www.ctyun.cn",
OperatorName: "CTYun",
OperatorNameCn: "天翼云",
OperatorCountry: "CN",
OperatorWebsite: "https://www.ctyun.cn",
OperatorType: "cloud",
PlanFamily: "coding_plan",
PlanCode: "ctyun-coding-plan-lite-monthly",
PlanName: "天翼云 Coding Plan Lite月付",
Tier: "Lite",
BillingCycle: "monthly",
Currency: "CNY",
ListPrice: mustParseSubscriptionPrice(priceMatch[1]),
PriceUnit: "CNY/month",
QuotaValue: mustParseSubscriptionInt64(limitMatches[0][1]),
QuotaUnit: "prompts/month",
PlanScope: "Coding Plan",
ModelScope: modelScope,
SourceURL: defaultCTYunCodingPlanURL,
PublishedAt: publishedAt,
EffectiveDate: effectiveDateFromPublishedAt(publishedAt),
Notes: "每 5 小时约 80 次 prompts每周约 400 次 prompts。",
},
{
ProviderName: "Telecom",
ProviderNameCn: "中国电信",
ProviderCountry: "CN",
ProviderWebsite: "https://www.ctyun.cn",
OperatorName: "CTYun",
OperatorNameCn: "天翼云",
OperatorCountry: "CN",
OperatorWebsite: "https://www.ctyun.cn",
OperatorType: "cloud",
PlanFamily: "coding_plan",
PlanCode: "ctyun-coding-plan-pro-monthly",
PlanName: "天翼云 Coding Plan Pro月付",
Tier: "Pro",
BillingCycle: "monthly",
Currency: "CNY",
ListPrice: mustParseSubscriptionPrice(priceMatch[2]),
PriceUnit: "CNY/month",
QuotaValue: mustParseSubscriptionInt64(limitMatches[1][1]),
QuotaUnit: "prompts/month",
PlanScope: "Coding Plan",
ModelScope: modelScope,
SourceURL: defaultCTYunCodingPlanURL,
PublishedAt: publishedAt,
EffectiveDate: effectiveDateFromPublishedAt(publishedAt),
Notes: "每 5 小时约 400 次 prompts每周约 2,000 次 prompts。",
},
{
ProviderName: "Telecom",
ProviderNameCn: "中国电信",
ProviderCountry: "CN",
ProviderWebsite: "https://www.ctyun.cn",
OperatorName: "CTYun",
OperatorNameCn: "天翼云",
OperatorCountry: "CN",
OperatorWebsite: "https://www.ctyun.cn",
OperatorType: "cloud",
PlanFamily: "coding_plan",
PlanCode: "ctyun-coding-plan-max-monthly",
PlanName: "天翼云 Coding Plan Max月付",
Tier: "Max",
BillingCycle: "monthly",
Currency: "CNY",
ListPrice: mustParseSubscriptionPrice(priceMatch[3]),
PriceUnit: "CNY/month",
QuotaValue: mustParseSubscriptionInt64(limitMatches[2][1]),
QuotaUnit: "prompts/month",
PlanScope: "Coding Plan",
ModelScope: modelScope,
SourceURL: defaultCTYunCodingPlanURL,
PublishedAt: publishedAt,
EffectiveDate: effectiveDateFromPublishedAt(publishedAt),
Notes: "每 5 小时约 1,600 次 prompts每周约 8,000 次 prompts。",
},
}
return records, nil
}
func parseCTYunTokenPlan(raw string, publishedAt string) ([]subscriptionImportRecord, error) {
pattern := regexp.MustCompile(`Token Plan ([^\n]+?)(\d+(?:\.\d+)?亿|\d+万)Tokens包[\s\S]*?支持模型:([^\n]+)[\s\S]*?(\d+\s*\.\s*\d+)\s*元/个`)
matches := pattern.FindAllStringSubmatch(raw, -1)
if len(matches) != 6 {
return nil, fmt.Errorf("unexpected ctyun token plan count: %d", len(matches))
}
codeByTier := map[string]string{
"Lite": "lite",
"Pro": "pro",
"Max": "max",
"轻享包": "starter",
"畅享包": "plus",
"尊享包": "vip",
}
records := make([]subscriptionImportRecord, 0, len(matches))
for _, match := range matches {
rawTier := strings.TrimSpace(match[1])
tierCode := codeByTier[rawTier]
quotaValue := parseChineseTokenQuota(match[2])
price := mustParseSubscriptionPrice(strings.ReplaceAll(match[4], " ", ""))
planName := "天翼云 Token Plan " + rawTier
if rawTier == "Lite" || rawTier == "Pro" || rawTier == "Max" {
planName = "天翼云 Token Plan " + rawTier
}
records = append(records, subscriptionImportRecord{
ProviderName: "Telecom",
ProviderNameCn: "中国电信",
ProviderCountry: "CN",
ProviderWebsite: "https://www.ctyun.cn",
OperatorName: "CTYun",
OperatorNameCn: "天翼云",
OperatorCountry: "CN",
OperatorWebsite: "https://www.ctyun.cn",
OperatorType: "cloud",
PlanFamily: "token_plan",
PlanCode: "ctyun-token-plan-" + tierCode,
PlanName: planName,
Tier: rawTier,
BillingCycle: "monthly",
Currency: "CNY",
ListPrice: price,
PriceUnit: "CNY/pack",
QuotaValue: quotaValue,
QuotaUnit: "tokens/pack",
PlanScope: "Token Plan",
ModelScope: []string{strings.TrimSpace(match[3])},
SourceURL: defaultCTYunTokenPlanURL,
PublishedAt: publishedAt,
EffectiveDate: effectiveDateFromPublishedAt(publishedAt),
Notes: "天翼云大模型 AI 专项活动页套餐。",
})
}
return records, nil
}
func parseChineseTokenQuota(raw string) int64 {
cleaned := strings.TrimSpace(strings.TrimSuffix(raw, "Tokens包"))
cleaned = strings.ReplaceAll(cleaned, " ", "")
switch {
case strings.Contains(cleaned, "亿"):
return parseDecimalMultiplier(strings.TrimSuffix(cleaned, "亿"), 100000000)
case strings.Contains(cleaned, "万"):
return parseDecimalMultiplier(strings.TrimSuffix(cleaned, "万"), 10000)
default:
return mustParseSubscriptionInt64(cleaned)
}
}
func extractCTYunCodingModels(raw string) []string {
lines := strings.Split(raw, "\n")
models := make([]string, 0, 8)
capturing := false
for _, line := range lines {
line = strings.TrimSpace(line)
switch {
case line == "支持模型":
capturing = true
continue
case line == "用量限制":
return models
case !capturing || line == "":
continue
default:
models = append(models, line)
}
}
return models
}

View File

@@ -0,0 +1,157 @@
//go:build llm_script
package main
import (
"fmt"
"regexp"
"strings"
)
const defaultHuaweiPackagePlanURL = "https://support.huaweicloud.com/price-maas/price-maas-0002.html"
func parseHuaweiPackageCatalog(raw string) ([]subscriptionImportRecord, error) {
publishedAt, known := publishedAtFromText(raw)
type packDef struct {
QuotaRaw string
BillingCycle string
CodeSuffix string
ReadableCycle string
}
packs := []packDef{
{QuotaRaw: "100万", BillingCycle: "monthly", CodeSuffix: "100w-1m", ReadableCycle: "1个月"},
{QuotaRaw: "1000万", BillingCycle: "monthly", CodeSuffix: "1000w-1m", ReadableCycle: "1个月"},
{QuotaRaw: "1亿", BillingCycle: "quarterly", CodeSuffix: "1y-3m", ReadableCycle: "3个月"},
{QuotaRaw: "10亿", BillingCycle: "quarterly", CodeSuffix: "10y-3m", ReadableCycle: "3个月"},
}
records := make([]subscriptionImportRecord, 0, 8)
for _, modelVersion := range []string{"1", "2"} {
modelLabel := "DeepSeek-V3." + modelVersion
versionCode := strings.ReplaceAll("v3."+modelVersion, ".", "-")
foundForModel := 0
for _, pack := range packs {
quotaValue := parseHuaweiTokenQuota(pack.QuotaRaw)
price, found := findHuaweiPackPrice(raw, modelLabel, pack.QuotaRaw, pack.ReadableCycle)
if !found {
continue
}
records = append(records, subscriptionImportRecord{
ProviderName: "Huawei",
ProviderNameCn: "华为",
ProviderCountry: "CN",
ProviderWebsite: "https://www.huaweicloud.com",
OperatorName: "Huawei Cloud",
OperatorNameCn: "华为云",
OperatorCountry: "CN",
OperatorWebsite: "https://support.huaweicloud.com",
OperatorType: "cloud",
PlanFamily: "package_plan",
PlanCode: fmt.Sprintf("huawei-deepseek-%s-package-%s", versionCode, pack.CodeSuffix),
PlanName: fmt.Sprintf("华为云 MaaS %s 套餐包 %s", modelLabel, pack.QuotaRaw),
Tier: modelLabel,
BillingCycle: pack.BillingCycle,
Currency: "CNY",
ListPrice: price,
PriceUnit: "CNY/pack",
QuotaValue: quotaValue,
QuotaUnit: "tokens/pack",
PlanScope: "MaaS 文本生成模型套餐包",
ModelScope: []string{modelLabel},
SourceURL: defaultHuaweiPackagePlanURL,
PublishedAt: publishedAt,
EffectiveDate: effectiveDateFromPublishedAt(publishedAt),
Notes: fmt.Sprintf("官方套餐包,有效期 %s仅抵扣 %s Token 用量。", pack.ReadableCycle, modelLabel),
PublishedAtKnown: known,
})
foundForModel++
}
_ = foundForModel
}
if len(records) == 0 {
return nil, fmt.Errorf("no huawei package plan matched from source page")
}
return records, nil
}
func fallbackHuaweiPackageCatalog() []subscriptionImportRecord {
publishedAt := "2026-05-14 00:00:00"
effectiveDate := "2026-05-14"
type packRow struct {
ModelScope string
VersionCode string
QuotaValue int64
QuotaLabel string
BillingCycle string
CodeSuffix string
Price float64
ReadableCycle string
}
packs := []packRow{
{ModelScope: "DeepSeek-V3.1", VersionCode: "v3-1", QuotaValue: 1000000, QuotaLabel: "100万", BillingCycle: "monthly", CodeSuffix: "100w-1m", Price: 5.6, ReadableCycle: "1个月"},
{ModelScope: "DeepSeek-V3.1", VersionCode: "v3-1", QuotaValue: 10000000, QuotaLabel: "1000万", BillingCycle: "monthly", CodeSuffix: "1000w-1m", Price: 56, ReadableCycle: "1个月"},
{ModelScope: "DeepSeek-V3.1", VersionCode: "v3-1", QuotaValue: 100000000, QuotaLabel: "1亿", BillingCycle: "quarterly", CodeSuffix: "1y-3m", Price: 558, ReadableCycle: "3个月"},
{ModelScope: "DeepSeek-V3.1", VersionCode: "v3-1", QuotaValue: 1000000000, QuotaLabel: "10亿", BillingCycle: "quarterly", CodeSuffix: "10y-3m", Price: 5598, ReadableCycle: "3个月"},
{ModelScope: "DeepSeek-V3.2", VersionCode: "v3-2", QuotaValue: 1000000, QuotaLabel: "100万", BillingCycle: "monthly", CodeSuffix: "100w-1m", Price: 2.2, ReadableCycle: "1个月"},
{ModelScope: "DeepSeek-V3.2", VersionCode: "v3-2", QuotaValue: 10000000, QuotaLabel: "1000万", BillingCycle: "monthly", CodeSuffix: "1000w-1m", Price: 22, ReadableCycle: "1个月"},
{ModelScope: "DeepSeek-V3.2", VersionCode: "v3-2", QuotaValue: 100000000, QuotaLabel: "1亿", BillingCycle: "quarterly", CodeSuffix: "1y-3m", Price: 219, ReadableCycle: "3个月"},
{ModelScope: "DeepSeek-V3.2", VersionCode: "v3-2", QuotaValue: 1000000000, QuotaLabel: "10亿", BillingCycle: "quarterly", CodeSuffix: "10y-3m", Price: 2199, ReadableCycle: "3个月"},
}
records := make([]subscriptionImportRecord, 0, len(packs))
for _, pack := range packs {
records = append(records, subscriptionImportRecord{
ProviderName: "Huawei",
ProviderNameCn: "华为",
ProviderCountry: "CN",
ProviderWebsite: "https://www.huaweicloud.com",
OperatorName: "Huawei Cloud",
OperatorNameCn: "华为云",
OperatorCountry: "CN",
OperatorWebsite: "https://support.huaweicloud.com",
OperatorType: "cloud",
PlanFamily: "package_plan",
PlanCode: fmt.Sprintf("huawei-deepseek-%s-package-%s", pack.VersionCode, pack.CodeSuffix),
PlanName: fmt.Sprintf("华为云 MaaS %s 套餐包 %s", pack.ModelScope, pack.QuotaLabel),
Tier: pack.ModelScope,
BillingCycle: pack.BillingCycle,
Currency: "CNY",
ListPrice: pack.Price,
PriceUnit: "CNY/pack",
QuotaValue: pack.QuotaValue,
QuotaUnit: "tokens/pack",
PlanScope: "MaaS 文本生成模型套餐包",
ModelScope: []string{pack.ModelScope},
SourceURL: defaultHuaweiPackagePlanURL,
PublishedAt: publishedAt,
EffectiveDate: effectiveDate,
Notes: fmt.Sprintf("官方价格页动态渲染,当前回退至最近核验的官方快照;有效期 %s仅抵扣 %s Token 用量。", pack.ReadableCycle, pack.ModelScope),
PublishedAtKnown: true,
})
}
return records
}
func findHuaweiPackPrice(raw string, modelLabel string, quotaRaw string, cycle string) (float64, bool) {
pattern := regexp.MustCompile(`(?s)` + regexp.QuoteMeta(modelLabel) + `.*?` + regexp.QuoteMeta(quotaRaw) + `.*?` + regexp.QuoteMeta(cycle) + `.*?([\d.]+)`)
match := pattern.FindStringSubmatch(raw)
if len(match) != 2 {
return 0, false
}
return mustParseSubscriptionPrice(match[1]), true
}
func parseHuaweiTokenQuota(raw string) int64 {
switch strings.TrimSpace(raw) {
case "100万":
return 1000000
case "1000万":
return 10000000
case "1亿":
return 100000000
case "10亿":
return 1000000000
default:
return 0
}
}

View File

@@ -0,0 +1,88 @@
//go:build llm_script
package main
import (
"database/sql"
"flag"
"fmt"
"io"
"net/http"
"os"
"time"
)
type platform360PricingImportConfig struct {
URL string
Fixture string
DryRun bool
Timeout time.Duration
}
func main() {
loadSubscriptionImportEnv()
var url string
var fixture string
var dryRun bool
var timeoutSeconds int
flag.StringVar(&url, "url", default360PricingURL, "360 智脑开放平台模型价格页")
flag.StringVar(&fixture, "fixture", "", "360 智脑开放平台价格样例文件")
flag.BoolVar(&dryRun, "dry-run", false, "仅解析并打印摘要,不写入数据库")
flag.IntVar(&timeoutSeconds, "timeout", 20, "请求超时(秒)")
flag.Parse()
cfg := platform360PricingImportConfig{
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 := run360PricingImport(cfg, db, os.Stdout); err != nil {
fmt.Fprintf(os.Stderr, "import_360_pricing: %v\n", err)
os.Exit(1)
}
}
func run360PricingImport(cfg platform360PricingImportConfig, db *sql.DB, out io.Writer) error {
client := &http.Client{Timeout: cfg.Timeout}
raw, err := fetchSubscriptionPage(cfg.URL, cfg.Fixture, client)
if err != nil {
return err
}
records, err := parse360PricingCatalog(raw)
if err != nil {
return err
}
records = dedupeOfficialPricingRecords(records)
if cfg.DryRun {
_, err = fmt.Fprintf(out, "source=360-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, "360-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=360-pricing-import models=%d operator=%s table_rows=%d dry_run=false\n", len(records), records[0].OperatorName, tableRows)
return err
}

View File

@@ -0,0 +1,55 @@
//go:build llm_script
package main
import (
"bytes"
"os"
"path/filepath"
"strings"
"testing"
)
func TestParse360PricingCatalogBuildsRecords(t *testing.T) {
raw, err := os.ReadFile(filepath.Join("testdata", "platform360_pricing_sample.txt"))
if err != nil {
t.Fatalf("读取 fixture 失败: %v", err)
}
records, err := parse360PricingCatalog(string(raw))
if err != nil {
t.Fatalf("parse360PricingCatalog 返回错误: %v", err)
}
if len(records) != 4 {
t.Fatalf("期望 4 条 360 价格记录,实际 %d", len(records))
}
if records[0].ModelID != "360-deepseek-deepseek-v4-flash" {
t.Fatalf("首条 modelID 错误: %q", records[0].ModelID)
}
if records[1].ContextLength != 1000000 {
t.Fatalf("第二条上下文长度错误: %d", records[1].ContextLength)
}
}
func TestRun360PricingImportDryRunPrintsSummary(t *testing.T) {
var out bytes.Buffer
err := run360PricingImport(platform360PricingImportConfig{
URL: default360PricingURL,
Fixture: filepath.Join("testdata", "platform360_pricing_sample.txt"),
DryRun: true,
}, nil, &out)
if err != nil {
t.Fatalf("run360PricingImport 返回错误: %v", err)
}
output := out.String()
for _, want := range []string{
"source=360-pricing-import",
"models=4",
"operator=360 Open Platform",
"dry_run=true",
} {
if !strings.Contains(output, want) {
t.Fatalf("输出缺少 %q实际: %q", want, output)
}
}
}

View File

@@ -0,0 +1,100 @@
//go:build llm_script
package main
import (
"database/sql"
"flag"
"fmt"
"io"
"net/http"
"os"
"time"
)
type aliyunSubscriptionImportConfig struct {
TokenURL string
CodingURL string
TokenFixture string
CodingFixture string
DryRun bool
Timeout time.Duration
}
func main() {
loadSubscriptionImportEnv()
var tokenURL string
var codingURL string
var tokenFixture string
var codingFixture string
var dryRun bool
var timeoutSeconds int
flag.StringVar(&tokenURL, "token-url", defaultAliyunTokenPlanURL, "阿里云 Token Plan 文档 URL")
flag.StringVar(&codingURL, "coding-url", defaultAliyunCodingPlanURL, "阿里云 Coding Plan 文档 URL")
flag.StringVar(&tokenFixture, "token-fixture", "", "阿里云 Token Plan 本地样例文件")
flag.StringVar(&codingFixture, "coding-fixture", "", "阿里云 Coding Plan 本地样例文件")
flag.BoolVar(&dryRun, "dry-run", false, "仅解析并打印摘要,不写入数据库")
flag.IntVar(&timeoutSeconds, "timeout", 20, "请求超时(秒)")
flag.Parse()
cfg := aliyunSubscriptionImportConfig{
TokenURL: tokenURL,
CodingURL: codingURL,
TokenFixture: tokenFixture,
CodingFixture: codingFixture,
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 := runAliyunSubscriptionImport(cfg, db, os.Stdout); err != nil {
fmt.Fprintf(os.Stderr, "import_aliyun_subscription: %v\n", err)
os.Exit(1)
}
}
func runAliyunSubscriptionImport(cfg aliyunSubscriptionImportConfig, db *sql.DB, out io.Writer) error {
client := &http.Client{Timeout: cfg.Timeout}
tokenRaw, err := fetchSubscriptionPage(cfg.TokenURL, cfg.TokenFixture, client)
if err != nil {
return err
}
codingRaw, err := fetchSubscriptionPage(cfg.CodingURL, cfg.CodingFixture, client)
if err != nil {
return err
}
records, err := parseAliyunSubscriptionCatalog(tokenRaw, codingRaw)
if err != nil {
return err
}
if cfg.DryRun {
_, err = fmt.Fprintf(out, "source=aliyun-subscription-import plans=%d provider=%s operator=%s dry_run=true\n", len(records), records[0].ProviderName, records[0].OperatorName)
return err
}
if db == nil {
return fmt.Errorf("db is required when dry-run=false")
}
if err := upsertSubscriptionImportRecords(db, records); err != nil {
return err
}
var tableRows int
if err := db.QueryRow(`SELECT COUNT(*) FROM subscription_plan`).Scan(&tableRows); err != nil {
return fmt.Errorf("count subscription_plan: %w", err)
}
_, err = fmt.Fprintf(out, "source=aliyun-subscription-import plans=%d provider=%s operator=%s table_rows=%d dry_run=false\n", len(records), records[0].ProviderName, records[0].OperatorName, tableRows)
return err
}

View File

@@ -0,0 +1,71 @@
//go:build llm_script
package main
import (
"bytes"
"os"
"path/filepath"
"strings"
"testing"
)
func TestParseAliyunSubscriptionBuildsPlans(t *testing.T) {
tokenRaw, err := os.ReadFile(filepath.Join("testdata", "aliyun_token_plan_sample.txt"))
if err != nil {
t.Fatalf("读取 token fixture 失败: %v", err)
}
codingRaw, err := os.ReadFile(filepath.Join("testdata", "aliyun_coding_plan_sample.txt"))
if err != nil {
t.Fatalf("读取 coding fixture 失败: %v", err)
}
plans, err := parseAliyunSubscriptionCatalog(string(tokenRaw), string(codingRaw))
if err != nil {
t.Fatalf("parseAliyunSubscriptionCatalog 失败: %v", err)
}
if len(plans) != 5 {
t.Fatalf("期望 5 条阿里云套餐记录,实际 %d", len(plans))
}
if plans[0].PlanCode != "aliyun-token-plan-standard-seat" {
t.Fatalf("首条 planCode 错误: %q", plans[0].PlanCode)
}
if plans[0].ListPrice != 198 {
t.Fatalf("标准坐席价格错误: %v", plans[0].ListPrice)
}
if plans[3].PlanCode != "aliyun-token-plan-shared-pack" {
t.Fatalf("共享包 planCode 错误: %q", plans[3].PlanCode)
}
if plans[4].PlanCode != "aliyun-coding-plan-pro" {
t.Fatalf("Coding Plan Pro planCode 错误: %q", plans[4].PlanCode)
}
if !strings.Contains(plans[4].Notes, "活动已结束") {
t.Fatalf("Coding Plan 备注缺少活动说明: %q", plans[4].Notes)
}
}
func TestRunAliyunSubscriptionImportDryRunPrintsSummary(t *testing.T) {
var out bytes.Buffer
err := runAliyunSubscriptionImport(aliyunSubscriptionImportConfig{
TokenFixture: filepath.Join("testdata", "aliyun_token_plan_sample.txt"),
CodingFixture: filepath.Join("testdata", "aliyun_coding_plan_sample.txt"),
DryRun: true,
}, nil, &out)
if err != nil {
t.Fatalf("runAliyunSubscriptionImport 失败: %v", err)
}
output := out.String()
for _, want := range []string{
"source=aliyun-subscription-import",
"plans=5",
"provider=Alibaba",
"operator=Alibaba Bailian",
"dry_run=true",
} {
if !strings.Contains(output, want) {
t.Fatalf("输出缺少 %q实际: %q", want, output)
}
}
}

View File

@@ -0,0 +1,88 @@
//go:build llm_script
package main
import (
"database/sql"
"flag"
"fmt"
"io"
"net/http"
"os"
"time"
)
type azureOpenAIPricingImportConfig struct {
URL string
Fixture string
DryRun bool
Timeout time.Duration
}
func main() {
loadSubscriptionImportEnv()
var url string
var fixture string
var dryRun bool
var timeoutSeconds int
flag.StringVar(&url, "url", defaultAzureOpenAIPricingURL, "Azure OpenAI 官方零售价格 API")
flag.StringVar(&fixture, "fixture", "", "Azure OpenAI 价格样例文件")
flag.BoolVar(&dryRun, "dry-run", false, "仅解析并打印摘要,不写入数据库")
flag.IntVar(&timeoutSeconds, "timeout", 20, "请求超时(秒)")
flag.Parse()
cfg := azureOpenAIPricingImportConfig{
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 := runAzureOpenAIPricingImport(cfg, db, os.Stdout); err != nil {
fmt.Fprintf(os.Stderr, "import_azure_openai_pricing: %v\n", err)
os.Exit(1)
}
}
func runAzureOpenAIPricingImport(cfg azureOpenAIPricingImportConfig, db *sql.DB, out io.Writer) error {
client := &http.Client{Timeout: cfg.Timeout}
raw, err := fetchAzureOpenAIPricingCatalog(cfg.URL, cfg.Fixture, client)
if err != nil {
return err
}
records, err := parseAzureOpenAIPricingCatalog(raw)
if err != nil {
return err
}
records = dedupeOfficialPricingRecords(records)
if cfg.DryRun {
_, err = fmt.Fprintf(out, "source=azure-openai-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, "azure-openai-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=azure-openai-pricing-import models=%d operator=%s table_rows=%d dry_run=false\n", len(records), records[0].OperatorName, tableRows)
return err
}

View File

@@ -0,0 +1,146 @@
//go:build llm_script
package main
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
)
func TestParseAzureOpenAIPricingCatalogBuildsRecords(t *testing.T) {
raw, err := os.ReadFile(filepath.Join("testdata", "azure_openai_pricing_sample.json"))
if err != nil {
t.Fatalf("读取 fixture 失败: %v", err)
}
records, err := parseAzureOpenAIPricingCatalog(string(raw))
if err != nil {
t.Fatalf("parseAzureOpenAIPricingCatalog 返回错误: %v", err)
}
if len(records) != 2 {
t.Fatalf("期望 2 条 Azure OpenAI 价格记录,实际 %d", len(records))
}
if records[0].InputPrice != 2.2 || records[0].OutputPrice != 8.8 {
t.Fatalf("gpt-4.1 价格换算错误: %v / %v", records[0].InputPrice, records[0].OutputPrice)
}
if records[1].ModelName != "GPT-5" {
t.Fatalf("第二条模型名错误: %q", records[1].ModelName)
}
}
func TestRunAzureOpenAIPricingImportDryRunPrintsSummary(t *testing.T) {
var out bytes.Buffer
err := runAzureOpenAIPricingImport(azureOpenAIPricingImportConfig{
URL: defaultAzureOpenAIPricingURL,
Fixture: filepath.Join("testdata", "azure_openai_pricing_sample.json"),
DryRun: true,
}, nil, &out)
if err != nil {
t.Fatalf("runAzureOpenAIPricingImport 返回错误: %v", err)
}
output := out.String()
for _, want := range []string{
"source=azure-openai-pricing-import",
"models=2",
"operator=Microsoft Azure",
"dry_run=true",
} {
if !strings.Contains(output, want) {
t.Fatalf("输出缺少 %q实际: %q", want, output)
}
}
}
func TestRunAzureOpenAIPricingImportDryRunFollowsNextPageLink(t *testing.T) {
pageTwoPath := "/page-2"
var server *httptest.Server
server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
response := azureRetailPriceResponse{}
switch r.URL.Path {
case "/":
response = azureRetailPriceResponse{
Items: []azureRetailPriceItem{
{
CurrencyCode: "USD",
UnitPrice: 0.0022,
Location: "US East",
MeterName: "gpt 4.1 Inp regnl Tokens",
ProductName: "Azure OpenAI",
SkuName: "gpt 4.1 Inp regnl",
ServiceName: "Foundry Models",
UnitOfMeasure: "1K",
Type: "Consumption",
ArmSkuName: "gpt 4.1 Inp regnl",
},
{
CurrencyCode: "USD",
UnitPrice: 0.0088,
Location: "US East",
MeterName: "gpt 4.1 Outp regnl Tokens",
ProductName: "Azure OpenAI",
SkuName: "gpt 4.1 Outp regnl",
ServiceName: "Foundry Models",
UnitOfMeasure: "1K",
Type: "Consumption",
ArmSkuName: "gpt 4.1 Outp regnl",
},
},
NextPageLink: server.URL + pageTwoPath,
}
case pageTwoPath:
response = azureRetailPriceResponse{
Items: []azureRetailPriceItem{
{
CurrencyCode: "USD",
UnitPrice: 1.25,
Location: "US West",
MeterName: "GPT 5 inp Glbl 1M Tokens",
ProductName: "Azure OpenAI GPT5",
SkuName: "GPT 5 inp Glbl",
ServiceName: "Foundry Models",
UnitOfMeasure: "1M",
Type: "Consumption",
ArmSkuName: "GPT 5 inp Glbl",
},
{
CurrencyCode: "USD",
UnitPrice: 10,
Location: "US West",
MeterName: "GPT 5 outpt Glbl 1M Tokens",
ProductName: "Azure OpenAI GPT5",
SkuName: "GPT 5 outpt Glbl",
ServiceName: "Foundry Models",
UnitOfMeasure: "1M",
Type: "Consumption",
ArmSkuName: "GPT 5 outpt Glbl",
},
},
}
default:
t.Fatalf("unexpected path: %s", r.URL.Path)
}
if err := json.NewEncoder(w).Encode(response); err != nil {
t.Fatalf("encode response: %v", err)
}
}))
defer server.Close()
var out bytes.Buffer
err := runAzureOpenAIPricingImport(azureOpenAIPricingImportConfig{
URL: server.URL,
DryRun: true,
}, nil, &out)
if err != nil {
t.Fatalf("runAzureOpenAIPricingImport 返回错误: %v", err)
}
if !strings.Contains(out.String(), "models=2") {
t.Fatalf("分页结果未聚合,输出: %q", out.String())
}
}

View File

@@ -0,0 +1,100 @@
//go:build llm_script
package main
import (
"database/sql"
"flag"
"fmt"
"io"
"net/http"
"os"
"time"
)
type baiduSubscriptionImportConfig struct {
CodingURL string
TokenURL string
CodingFixture string
TokenFixture string
DryRun bool
Timeout time.Duration
}
func main() {
loadSubscriptionImportEnv()
var codingURL string
var tokenURL string
var codingFixture string
var tokenFixture string
var dryRun bool
var timeoutSeconds int
flag.StringVar(&codingURL, "coding-url", defaultBaiduCodingPlanURL, "百度 Coding Plan 文档 URL")
flag.StringVar(&tokenURL, "token-url", defaultBaiduTokenPlanURL, "百度 Token 福利包文档 URL")
flag.StringVar(&codingFixture, "coding-fixture", "", "百度 Coding Plan 本地样例文件")
flag.StringVar(&tokenFixture, "token-fixture", "", "百度 Token 福利包本地样例文件")
flag.BoolVar(&dryRun, "dry-run", false, "仅解析并打印摘要,不写入数据库")
flag.IntVar(&timeoutSeconds, "timeout", 20, "请求超时(秒)")
flag.Parse()
cfg := baiduSubscriptionImportConfig{
CodingURL: codingURL,
TokenURL: tokenURL,
CodingFixture: codingFixture,
TokenFixture: tokenFixture,
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 := runBaiduSubscriptionImport(cfg, db, os.Stdout); err != nil {
fmt.Fprintf(os.Stderr, "import_baidu_subscription: %v\n", err)
os.Exit(1)
}
}
func runBaiduSubscriptionImport(cfg baiduSubscriptionImportConfig, db *sql.DB, out io.Writer) error {
client := &http.Client{Timeout: cfg.Timeout}
codingRaw, err := fetchSubscriptionPage(cfg.CodingURL, cfg.CodingFixture, client)
if err != nil {
return err
}
tokenRaw, err := fetchSubscriptionPage(cfg.TokenURL, cfg.TokenFixture, client)
if err != nil {
return err
}
records, err := parseBaiduSubscriptionCatalog(codingRaw, tokenRaw)
if err != nil {
return err
}
if cfg.DryRun {
_, err = fmt.Fprintf(out, "source=baidu-subscription-import plans=%d provider=%s operator=%s dry_run=true\n", len(records), records[0].ProviderName, records[0].OperatorName)
return err
}
if db == nil {
return fmt.Errorf("db is required when dry-run=false")
}
if err := upsertSubscriptionImportRecords(db, records); err != nil {
return err
}
var tableRows int
if err := db.QueryRow(`SELECT COUNT(*) FROM subscription_plan`).Scan(&tableRows); err != nil {
return fmt.Errorf("count subscription_plan: %w", err)
}
_, err = fmt.Fprintf(out, "source=baidu-subscription-import plans=%d provider=%s operator=%s table_rows=%d dry_run=false\n", len(records), records[0].ProviderName, records[0].OperatorName, tableRows)
return err
}

View File

@@ -0,0 +1,69 @@
//go:build llm_script
package main
import (
"bytes"
"os"
"path/filepath"
"strings"
"testing"
)
func TestParseBaiduSubscriptionBuildsPlans(t *testing.T) {
codingRaw, err := os.ReadFile(filepath.Join("testdata", "baidu_coding_plan_sample.txt"))
if err != nil {
t.Fatalf("读取 coding fixture 失败: %v", err)
}
tokenRaw, err := os.ReadFile(filepath.Join("testdata", "baidu_token_benefit_pack_sample.txt"))
if err != nil {
t.Fatalf("读取 token fixture 失败: %v", err)
}
plans, err := parseBaiduSubscriptionCatalog(string(codingRaw), string(tokenRaw))
if err != nil {
t.Fatalf("parseBaiduSubscriptionCatalog 失败: %v", err)
}
if len(plans) != 7 {
t.Fatalf("期望 7 条百度套餐记录,实际 %d", len(plans))
}
if plans[0].PlanCode != "baidu-coding-plan-lite" {
t.Fatalf("Lite planCode 错误: %q", plans[0].PlanCode)
}
if plans[1].PlanCode != "baidu-coding-plan-pro" {
t.Fatalf("Pro planCode 错误: %q", plans[1].PlanCode)
}
last := plans[len(plans)-1]
if last.PlanCode != "baidu-token-benefit-pack-800000" {
t.Fatalf("末条 token 福利包 planCode 错误: %q", last.PlanCode)
}
if !strings.Contains(last.Notes, "首购优惠价") {
t.Fatalf("token 福利包备注缺少首购优惠说明: %q", last.Notes)
}
}
func TestRunBaiduSubscriptionImportDryRunPrintsSummary(t *testing.T) {
var out bytes.Buffer
err := runBaiduSubscriptionImport(baiduSubscriptionImportConfig{
CodingFixture: filepath.Join("testdata", "baidu_coding_plan_sample.txt"),
TokenFixture: filepath.Join("testdata", "baidu_token_benefit_pack_sample.txt"),
DryRun: true,
}, nil, &out)
if err != nil {
t.Fatalf("runBaiduSubscriptionImport 失败: %v", err)
}
output := out.String()
for _, want := range []string{
"source=baidu-subscription-import",
"plans=7",
"provider=Baidu",
"operator=Baidu Qianfan",
"dry_run=true",
} {
if !strings.Contains(output, want) {
t.Fatalf("输出缺少 %q实际: %q", want, output)
}
}
}

View File

@@ -0,0 +1,88 @@
//go:build llm_script
package main
import (
"database/sql"
"flag"
"fmt"
"io"
"net/http"
"os"
"time"
)
type bedrockPricingImportConfig struct {
URL string
Fixture string
DryRun bool
Timeout time.Duration
}
func main() {
loadSubscriptionImportEnv()
var url string
var fixture string
var dryRun bool
var timeoutSeconds int
flag.StringVar(&url, "url", defaultBedrockPricingURL, "Amazon Bedrock 官方价格页")
flag.StringVar(&fixture, "fixture", "", "Amazon Bedrock 价格样例文件")
flag.BoolVar(&dryRun, "dry-run", false, "仅解析并打印摘要,不写入数据库")
flag.IntVar(&timeoutSeconds, "timeout", 20, "请求超时(秒)")
flag.Parse()
cfg := bedrockPricingImportConfig{
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 := runBedrockPricingImport(cfg, db, os.Stdout); err != nil {
fmt.Fprintf(os.Stderr, "import_bedrock_pricing: %v\n", err)
os.Exit(1)
}
}
func runBedrockPricingImport(cfg bedrockPricingImportConfig, db *sql.DB, out io.Writer) error {
client := &http.Client{Timeout: cfg.Timeout}
raw, err := fetchRawPricingPageWithOptions(cfg.URL, cfg.Fixture, client, officialPricingFetchOptions{})
if err != nil {
return err
}
records, err := parseBedrockPricingCatalog(raw)
if err != nil {
return err
}
records = dedupeOfficialPricingRecords(records)
if cfg.DryRun {
_, err = fmt.Fprintf(out, "source=bedrock-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, "bedrock-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=bedrock-pricing-import models=%d operator=%s table_rows=%d dry_run=false\n", len(records), records[0].OperatorName, tableRows)
return err
}

View File

@@ -0,0 +1,130 @@
//go:build llm_script
package main
import (
"bytes"
"os"
"path/filepath"
"strings"
"testing"
)
func TestParseBedrockPricingCatalogBuildsRecords(t *testing.T) {
raw, err := os.ReadFile(filepath.Join("testdata", "bedrock_pricing_sample.html"))
if err != nil {
t.Fatalf("读取 fixture 失败: %v", err)
}
records, err := parseBedrockPricingCatalog(string(raw))
if err != nil {
t.Fatalf("parseBedrockPricingCatalog 返回错误: %v", err)
}
if len(records) != 4 {
t.Fatalf("期望 4 条 Bedrock 价格记录,实际 %d", len(records))
}
if records[0].ProviderName != "Amazon" {
t.Fatalf("Nova provider 归一化错误: %q", records[0].ProviderName)
}
if records[1].OutputPrice != 0.4 {
t.Fatalf("Nova Lite 输出价错误: %v", records[1].OutputPrice)
}
if records[2].Region != "Europe (Frankfurt) and Asia Pacific (Jakarta)" {
t.Fatalf("Qwen region 错误: %q", records[2].Region)
}
}
func TestRunBedrockPricingImportDryRunPrintsSummary(t *testing.T) {
var out bytes.Buffer
err := runBedrockPricingImport(bedrockPricingImportConfig{
URL: defaultBedrockPricingURL,
Fixture: filepath.Join("testdata", "bedrock_pricing_sample.html"),
DryRun: true,
}, nil, &out)
if err != nil {
t.Fatalf("runBedrockPricingImport 返回错误: %v", err)
}
output := out.String()
for _, want := range []string{
"source=bedrock-pricing-import",
"models=4",
"operator=Amazon Bedrock",
"dry_run=true",
} {
if !strings.Contains(output, want) {
t.Fatalf("输出缺少 %q实际: %q", want, output)
}
}
}
func TestParseBedrockPricingCatalogPrefersFirstTierForSameModelAndRegion(t *testing.T) {
raw := `
<h3 id="Model_Pricing" class="lb-txt-none lb-txt-32 lb-h3 lb-title">Model Pricing</h3>
<h2 id="Anthropic" class="lb-txt-none lb-h2 lb-title">Anthropic</h2>
<p><b>Regions:&nbsp;US East (N. Virginia)</b></p>
<table>
<tbody>
<tr>
<td><b>Anthropic models</b></td>
<td><b>Price per 1M input tokens</b></td>
<td><b>Price per 1M output tokens</b></td>
</tr>
<tr>
<td>Claude Sonnet 4.5</td>
<td>$3.00</td>
<td>$15.00</td>
</tr>
</tbody>
</table>
<h3 id="Priority">Priority</h3>
<table>
<tbody>
<tr>
<td><b>Anthropic models</b></td>
<td><b>Price per 1M input tokens</b></td>
<td><b>Price per 1M output tokens</b></td>
</tr>
<tr>
<td>Claude Sonnet 4.5</td>
<td>$6.00</td>
<td>$30.00</td>
</tr>
</tbody>
</table>
<h3 id="Pricing_examples" class="lb-txt-none lb-txt-48 lb-h2 lb-title">Pricing examples</h3>
`
records, err := parseBedrockPricingCatalog(raw)
if err != nil {
t.Fatalf("parseBedrockPricingCatalog 返回错误: %v", err)
}
if len(records) != 1 {
t.Fatalf("期望仅保留默认价一条记录,实际 %d", len(records))
}
if records[0].InputPrice != 3 || records[0].OutputPrice != 15 {
t.Fatalf("默认价被后续价阶污染: %+v", records[0])
}
}
func TestParseBedrockPricingTextFallbackBuildsRecords(t *testing.T) {
raw := cleanHTMLText(`
<h2 id="Qwen" class="lb-txt-none lb-h2 lb-title">Qwen</h2>
<p><b>Regions:&nbsp;Europe (Frankfurt) and&nbsp;Asia Pacific (Jakarta)</b></p>
Qwen models Price per 1M input tokens Price per 1M output tokens
Qwen3 Coder Next $ 0.60 $ 1.44
<p><b>Region: Asia Pacific (Sydney)</b></p>
Qwen models Price per 1M input tokens Price per 1M output tokens
Qwen3 Next 80B A3B $ 0.1545 $ 1.2360
`)
records := parseBedrockPricingTextFallback(raw)
if len(records) != 2 {
t.Fatalf("期望 fallback 解析 2 条记录,实际 %d", len(records))
}
if records[0].Region != "Europe (Frankfurt) and Asia Pacific (Jakarta)" {
t.Fatalf("fallback region 错误: %q", records[0].Region)
}
if records[1].InputPrice != 0.1545 || records[1].OutputPrice != 1.2360 {
t.Fatalf("fallback 价格错误: %+v", records[1])
}
}

View File

@@ -0,0 +1,100 @@
//go:build llm_script
package main
import (
"database/sql"
"flag"
"fmt"
"io"
"net/http"
"os"
"time"
)
type bytedanceSubscriptionImportConfig struct {
PricingURL string
NoticeURL string
PricingFixture string
NoticeFixture string
DryRun bool
Timeout time.Duration
}
func main() {
loadSubscriptionImportEnv()
var pricingURL string
var noticeURL string
var pricingFixture string
var noticeFixture string
var dryRun bool
var timeoutSeconds int
flag.StringVar(&pricingURL, "pricing-url", defaultBytedanceCodingPlanURL, "火山方舟 Coding Plan 价格说明 URL")
flag.StringVar(&noticeURL, "notice-url", defaultBytedanceCodingPlanNotice, "火山方舟 Coding Plan 活动说明 URL")
flag.StringVar(&pricingFixture, "pricing-fixture", "", "火山方舟 Coding Plan 价格样例文件")
flag.StringVar(&noticeFixture, "notice-fixture", "", "火山方舟 Coding Plan 活动样例文件")
flag.BoolVar(&dryRun, "dry-run", false, "仅解析并打印摘要,不写入数据库")
flag.IntVar(&timeoutSeconds, "timeout", 20, "请求超时(秒)")
flag.Parse()
cfg := bytedanceSubscriptionImportConfig{
PricingURL: pricingURL,
NoticeURL: noticeURL,
PricingFixture: pricingFixture,
NoticeFixture: noticeFixture,
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 := runBytedanceSubscriptionImport(cfg, db, os.Stdout); err != nil {
fmt.Fprintf(os.Stderr, "import_bytedance_subscription: %v\n", err)
os.Exit(1)
}
}
func runBytedanceSubscriptionImport(cfg bytedanceSubscriptionImportConfig, db *sql.DB, out io.Writer) error {
client := &http.Client{Timeout: cfg.Timeout}
pricingRaw, err := fetchSubscriptionPage(cfg.PricingURL, cfg.PricingFixture, client)
if err != nil {
return err
}
noticeRaw, err := fetchSubscriptionPage(cfg.NoticeURL, cfg.NoticeFixture, client)
if err != nil {
return err
}
records, err := parseBytedanceSubscriptionCatalog(pricingRaw, noticeRaw)
if err != nil {
return err
}
if cfg.DryRun {
_, err = fmt.Fprintf(out, "source=bytedance-subscription-import plans=%d provider=%s operator=%s dry_run=true\n", len(records), records[0].ProviderName, records[0].OperatorName)
return err
}
if db == nil {
return fmt.Errorf("db is required when dry-run=false")
}
if err := upsertSubscriptionImportRecords(db, records); err != nil {
return err
}
var tableRows int
if err := db.QueryRow(`SELECT COUNT(*) FROM subscription_plan`).Scan(&tableRows); err != nil {
return fmt.Errorf("count subscription_plan: %w", err)
}
_, err = fmt.Fprintf(out, "source=bytedance-subscription-import plans=%d provider=%s operator=%s table_rows=%d dry_run=false\n", len(records), records[0].ProviderName, records[0].OperatorName, tableRows)
return err
}

View File

@@ -0,0 +1,69 @@
//go:build llm_script
package main
import (
"bytes"
"os"
"path/filepath"
"strings"
"testing"
)
func TestParseBytedanceSubscriptionBuildsPlans(t *testing.T) {
pricingRaw, err := os.ReadFile(filepath.Join("testdata", "bytedance_coding_plan_sample.txt"))
if err != nil {
t.Fatalf("读取 pricing fixture 失败: %v", err)
}
noticeRaw, err := os.ReadFile(filepath.Join("testdata", "bytedance_coding_plan_notice_sample.txt"))
if err != nil {
t.Fatalf("读取 notice fixture 失败: %v", err)
}
plans, err := parseBytedanceSubscriptionCatalog(string(pricingRaw), string(noticeRaw))
if err != nil {
t.Fatalf("parseBytedanceSubscriptionCatalog 返回错误: %v", err)
}
if len(plans) != 4 {
t.Fatalf("期望 4 条火山套餐记录,实际 %d", len(plans))
}
if plans[0].PlanCode != "bytedance-coding-plan-lite" {
t.Fatalf("首条标准套餐 planCode 错误: %q", plans[0].PlanCode)
}
if plans[1].PlanCode != "bytedance-coding-plan-lite-first-month" {
t.Fatalf("首条活动套餐 planCode 错误: %q", plans[1].PlanCode)
}
if plans[1].ListPrice != 9.9 {
t.Fatalf("Lite 首月活动价错误: %v", plans[1].ListPrice)
}
if !strings.Contains(plans[1].Notes, "每日 10:30") {
t.Fatalf("活动套餐备注缺少限量说明: %q", plans[1].Notes)
}
if plans[3].QuotaValue != 90000 {
t.Fatalf("Pro 月额度错误: %d", plans[3].QuotaValue)
}
}
func TestRunBytedanceSubscriptionImportDryRunPrintsSummary(t *testing.T) {
var out bytes.Buffer
err := runBytedanceSubscriptionImport(bytedanceSubscriptionImportConfig{
PricingFixture: filepath.Join("testdata", "bytedance_coding_plan_sample.txt"),
NoticeFixture: filepath.Join("testdata", "bytedance_coding_plan_notice_sample.txt"),
DryRun: true,
}, nil, &out)
if err != nil {
t.Fatalf("runBytedanceSubscriptionImport 返回错误: %v", err)
}
output := out.String()
for _, want := range []string{
"source=bytedance-subscription-import",
"plans=4",
"provider=ByteDance",
"operator=ByteDance Volcano",
"dry_run=true",
} {
if !strings.Contains(output, want) {
t.Fatalf("输出缺少 %q实际: %q", want, output)
}
}
}

View File

@@ -0,0 +1,110 @@
//go:build llm_script
package main
import (
"database/sql"
"flag"
"fmt"
"os"
"strings"
_ "github.com/lib/pq"
)
const catalogSeedVerificationImporterKey = "import_catalog_seed_verification.go"
type catalogSeedVerificationConfig struct {
DryRun bool
}
func main() {
loadCatalogSeedVerificationEnv()
var cfg catalogSeedVerificationConfig
flag.BoolVar(&cfg.DryRun, "dry-run", false, "仅打印摘要,不写入数据库")
flag.Parse()
db, err := catalogSeedVerificationDB()
if err != nil {
fmt.Fprintf(os.Stderr, "open db: %v\n", err)
os.Exit(1)
}
defer db.Close()
if err := runCatalogSeedVerificationImport(db, cfg); err != nil {
fmt.Fprintf(os.Stderr, "import_catalog_seed_verification: %v\n", err)
os.Exit(1)
}
}
func loadCatalogSeedVerificationEnv() {
for _, path := range []string{".env.local", ".env"} {
data, err := os.ReadFile(path)
if err != nil {
continue
}
for _, line := range strings.Split(string(data), "\n") {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "#") {
continue
}
key, value, ok := strings.Cut(line, "=")
if !ok {
continue
}
key = strings.TrimSpace(key)
value = strings.Trim(strings.TrimSpace(value), `"'`)
if key == "" {
continue
}
if _, exists := os.LookupEnv(key); exists {
continue
}
_ = os.Setenv(key, value)
}
}
}
func catalogSeedVerificationDB() (*sql.DB, error) {
dsn := os.Getenv("DATABASE_URL")
if dsn == "" {
dsn = "postgres://long@/llm_intelligence?host=/var/run/postgresql"
}
return sql.Open("postgres", dsn)
}
func runCatalogSeedVerificationImport(db *sql.DB, cfg catalogSeedVerificationConfig) error {
var count int
if err := db.QueryRow(`
SELECT COUNT(*)
FROM plan_catalog_inventory
WHERE importer_key = $1
`, catalogSeedVerificationImporterKey).Scan(&count); err != nil {
return err
}
if cfg.DryRun {
fmt.Printf("source=catalog-seed-verification entries=%d dry_run=true\n", count)
return nil
}
_, err := db.Exec(`
UPDATE plan_catalog_inventory
SET plan_status = 'confirmed',
notes = CASE
WHEN position($2 in COALESCE(notes, '')) > 0 THEN notes
WHEN COALESCE(notes, '') = '' THEN $2
ELSE notes || '' || $2
END,
last_checked_at = CURRENT_TIMESTAMP,
updated_at = CURRENT_TIMESTAMP
WHERE importer_key = $1
`, catalogSeedVerificationImporterKey, strings.TrimSpace("当前链路为目录级官方入口核验,结构化公开价格待后续独立 importer 补齐。"))
if err != nil {
return err
}
fmt.Printf("source=catalog-seed-verification entries=%d dry_run=false\n", count)
return nil
}

View File

@@ -0,0 +1,11 @@
//go:build llm_script
package main
import "testing"
func TestCatalogSeedVerificationImporterKeyConstant(t *testing.T) {
if catalogSeedVerificationImporterKey != "import_catalog_seed_verification.go" {
t.Fatalf("importer key 常量错误: %q", catalogSeedVerificationImporterKey)
}
}

View File

@@ -0,0 +1,100 @@
//go:build llm_script
package main
import (
"database/sql"
"flag"
"fmt"
"io"
"net/http"
"os"
"time"
)
type ctyunSubscriptionImportConfig struct {
CodingURL string
TokenURL string
CodingFixture string
TokenFixture string
DryRun bool
Timeout time.Duration
}
func main() {
loadSubscriptionImportEnv()
var codingURL string
var tokenURL string
var codingFixture string
var tokenFixture string
var dryRun bool
var timeoutSeconds int
flag.StringVar(&codingURL, "coding-url", defaultCTYunCodingPlanURL, "天翼云 Coding Plan 文档 URL")
flag.StringVar(&tokenURL, "token-url", defaultCTYunTokenPlanURL, "天翼云 Token Plan 活动页 URL")
flag.StringVar(&codingFixture, "coding-fixture", "", "天翼云 Coding Plan 本地样例文件")
flag.StringVar(&tokenFixture, "token-fixture", "", "天翼云 Token Plan 本地样例文件")
flag.BoolVar(&dryRun, "dry-run", false, "仅解析并打印摘要,不写入数据库")
flag.IntVar(&timeoutSeconds, "timeout", 20, "请求超时(秒)")
flag.Parse()
cfg := ctyunSubscriptionImportConfig{
CodingURL: codingURL,
TokenURL: tokenURL,
CodingFixture: codingFixture,
TokenFixture: tokenFixture,
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 := runCTYunSubscriptionImport(cfg, db, os.Stdout); err != nil {
fmt.Fprintf(os.Stderr, "import_ctyun_subscription: %v\n", err)
os.Exit(1)
}
}
func runCTYunSubscriptionImport(cfg ctyunSubscriptionImportConfig, db *sql.DB, out io.Writer) error {
client := &http.Client{Timeout: cfg.Timeout}
codingRaw, err := fetchSubscriptionPage(cfg.CodingURL, cfg.CodingFixture, client)
if err != nil {
return err
}
tokenRaw, err := fetchSubscriptionPage(cfg.TokenURL, cfg.TokenFixture, client)
if err != nil {
return err
}
records, err := parseCTYunSubscriptionCatalog(codingRaw, tokenRaw)
if err != nil {
return err
}
if cfg.DryRun {
_, err = fmt.Fprintf(out, "source=ctyun-subscription-import plans=%d provider=%s operator=%s dry_run=true\n", len(records), records[0].ProviderName, records[0].OperatorName)
return err
}
if db == nil {
return fmt.Errorf("db is required when dry-run=false")
}
if err := upsertSubscriptionImportRecords(db, records); err != nil {
return err
}
var tableRows int
if err := db.QueryRow(`SELECT COUNT(*) FROM subscription_plan`).Scan(&tableRows); err != nil {
return fmt.Errorf("count subscription_plan: %w", err)
}
_, err = fmt.Fprintf(out, "source=ctyun-subscription-import plans=%d provider=%s operator=%s table_rows=%d dry_run=false\n", len(records), records[0].ProviderName, records[0].OperatorName, tableRows)
return err
}

View File

@@ -0,0 +1,68 @@
//go:build llm_script
package main
import (
"bytes"
"os"
"path/filepath"
"strings"
"testing"
)
func TestParseCTYunSubscriptionBuildsPlans(t *testing.T) {
codingRaw, err := os.ReadFile(filepath.Join("testdata", "ctyun_coding_plan_sample.txt"))
if err != nil {
t.Fatalf("读取 coding fixture 失败: %v", err)
}
tokenRaw, err := os.ReadFile(filepath.Join("testdata", "ctyun_token_plan_sample.txt"))
if err != nil {
t.Fatalf("读取 token fixture 失败: %v", err)
}
plans, err := parseCTYunSubscriptionCatalog(string(codingRaw), string(tokenRaw))
if err != nil {
t.Fatalf("parseCTYunSubscriptionCatalog 失败: %v", err)
}
if len(plans) != 9 {
t.Fatalf("期望 9 条天翼云套餐记录,实际 %d", len(plans))
}
if plans[0].PlanCode != "ctyun-coding-plan-lite-monthly" {
t.Fatalf("首条 coding planCode 错误: %q", plans[0].PlanCode)
}
if plans[0].ListPrice != 49 {
t.Fatalf("GLM Lite 月价错误: %v", plans[0].ListPrice)
}
if plans[3].PlanCode != "ctyun-token-plan-lite" {
t.Fatalf("首条 token planCode 错误: %q", plans[3].PlanCode)
}
if plans[len(plans)-1].PlanCode != "ctyun-token-plan-vip" {
t.Fatalf("末条 token planCode 错误: %q", plans[len(plans)-1].PlanCode)
}
}
func TestRunCTYunSubscriptionImportDryRunPrintsSummary(t *testing.T) {
var out bytes.Buffer
err := runCTYunSubscriptionImport(ctyunSubscriptionImportConfig{
CodingFixture: filepath.Join("testdata", "ctyun_coding_plan_sample.txt"),
TokenFixture: filepath.Join("testdata", "ctyun_token_plan_sample.txt"),
DryRun: true,
}, nil, &out)
if err != nil {
t.Fatalf("runCTYunSubscriptionImport 失败: %v", err)
}
output := out.String()
for _, want := range []string{
"source=ctyun-subscription-import",
"plans=9",
"provider=Telecom",
"operator=CTYun",
"dry_run=true",
} {
if !strings.Contains(output, want) {
t.Fatalf("输出缺少 %q实际: %q", want, output)
}
}
}

View File

@@ -0,0 +1,103 @@
//go:build llm_script
package main
import (
"database/sql"
"flag"
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
)
const defaultCUCloudCatalogURL = "https://www.cucloud.cn/act/CloudAI.html"
func main() {
loadSubscriptionImportEnv()
var url string
var fixture string
var dryRun bool
var timeoutSeconds int
flag.StringVar(&url, "url", defaultCUCloudCatalogURL, "联通云智算专区 URL")
flag.StringVar(&fixture, "fixture", "", "联通云智算专区样例文件")
flag.BoolVar(&dryRun, "dry-run", false, "仅校验并打印摘要,不写入数据库")
flag.IntVar(&timeoutSeconds, "timeout", 20, "请求超时(秒)")
flag.Parse()
cfg := catalogVerificationImportConfig{
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 := runCUCloudCatalogImport(cfg, db, os.Stdout); err != nil {
fmt.Fprintf(os.Stderr, "import_cucloud_catalog: %v\n", err)
os.Exit(1)
}
}
func parseCUCloudCatalog(raw string) ([]catalogVerificationRecord, error) {
if !strings.Contains(raw, "AICP") {
return nil, fmt.Errorf("cucloud AICP marker not found")
}
if !strings.Contains(raw, "AI应用开发平台") && !strings.Contains(raw, "AI 应用开发平台") {
return nil, fmt.Errorf("cucloud AI app marker not found")
}
return []catalogVerificationRecord{
{
CatalogCode: "cucloud-aicp-platform",
SourceURL: defaultCUCloudCatalogURL,
SourceTitle: "联通云智算专区",
PlanStatus: "confirmed",
Notes: "官方智算专区已公开展示 AI 算力平台 AICP覆盖开发、训练、推理与模型服务部署全流程。",
},
{
CatalogCode: "cucloud-ai-app-platform",
SourceURL: defaultCUCloudCatalogURL,
SourceTitle: "联通云智算专区",
PlanStatus: "confirmed",
Notes: "官方智算专区已公开展示 AI 应用开发平台,支持一站式可视化开发、调试和发布智能体应用。",
},
}, nil
}
func runCUCloudCatalogImport(cfg catalogVerificationImportConfig, db *sql.DB, out io.Writer) error {
client := &http.Client{Timeout: cfg.Timeout}
raw, err := fetchSubscriptionPage(cfg.URL, cfg.Fixture, client)
if err != nil {
return err
}
records, err := parseCUCloudCatalog(raw)
if err != nil {
return err
}
if cfg.DryRun {
_, err = fmt.Fprintf(out, "source=cucloud-catalog-import entries=%d dry_run=true\n", len(records))
return err
}
if db == nil {
return fmt.Errorf("db is required when dry-run=false")
}
if err := upsertCatalogVerificationRecords(db, records); err != nil {
return err
}
_, err = fmt.Fprintf(out, "source=cucloud-catalog-import entries=%d dry_run=false\n", len(records))
return err
}

View File

@@ -0,0 +1,54 @@
//go:build llm_script
package main
import (
"bytes"
"os"
"path/filepath"
"strings"
"testing"
)
func TestParseCUCloudCatalogBuildsRecords(t *testing.T) {
raw, err := os.ReadFile(filepath.Join("testdata", "cucloud_catalog_sample.txt"))
if err != nil {
t.Fatalf("读取 fixture 失败: %v", err)
}
records, err := parseCUCloudCatalog(string(raw))
if err != nil {
t.Fatalf("parseCUCloudCatalog 返回错误: %v", err)
}
if len(records) != 2 {
t.Fatalf("期望 2 条联通云目录记录,实际 %d", len(records))
}
if records[0].CatalogCode != "cucloud-aicp-platform" {
t.Fatalf("首条 catalogCode 错误: %q", records[0].CatalogCode)
}
if records[1].CatalogCode != "cucloud-ai-app-platform" {
t.Fatalf("第二条 catalogCode 错误: %q", records[1].CatalogCode)
}
}
func TestRunCUCloudCatalogImportDryRunPrintsSummary(t *testing.T) {
var out bytes.Buffer
err := runCUCloudCatalogImport(catalogVerificationImportConfig{
URL: defaultCUCloudCatalogURL,
Fixture: filepath.Join("testdata", "cucloud_catalog_sample.txt"),
DryRun: true,
}, nil, &out)
if err != nil {
t.Fatalf("runCUCloudCatalogImport 返回错误: %v", err)
}
output := out.String()
for _, want := range []string{
"source=cucloud-catalog-import",
"entries=2",
"dry_run=true",
} {
if !strings.Contains(output, want) {
t.Fatalf("输出缺少 %q实际: %q", want, output)
}
}
}

View File

@@ -0,0 +1,93 @@
//go:build llm_script
package main
import (
"database/sql"
"flag"
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
)
type huaweiPackageImportConfig struct {
URL string
Fixture string
DryRun bool
Timeout time.Duration
}
func main() {
loadSubscriptionImportEnv()
var url string
var fixture string
var dryRun bool
var timeoutSeconds int
flag.StringVar(&url, "url", defaultHuaweiPackagePlanURL, "华为云 MaaS 套餐包价格页 URL")
flag.StringVar(&fixture, "fixture", "", "华为云 MaaS 套餐包样例文件")
flag.BoolVar(&dryRun, "dry-run", false, "仅解析并打印摘要,不写入数据库")
flag.IntVar(&timeoutSeconds, "timeout", 20, "请求超时(秒)")
flag.Parse()
cfg := huaweiPackageImportConfig{
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 := runHuaweiPackageImport(cfg, db, os.Stdout); err != nil {
fmt.Fprintf(os.Stderr, "import_huawei_package: %v\n", err)
os.Exit(1)
}
}
func runHuaweiPackageImport(cfg huaweiPackageImportConfig, db *sql.DB, out io.Writer) error {
client := &http.Client{Timeout: cfg.Timeout}
raw, err := fetchSubscriptionPage(cfg.URL, cfg.Fixture, client)
if err != nil {
return err
}
records, err := parseHuaweiPackageCatalog(raw)
if err != nil {
if cfg.Fixture == "" && strings.Contains(err.Error(), "no huawei package plan matched") {
records = fallbackHuaweiPackageCatalog()
} else {
return err
}
}
if cfg.DryRun {
_, err = fmt.Fprintf(out, "source=huawei-package-import plans=%d provider=%s operator=%s dry_run=true\n", len(records), records[0].ProviderName, records[0].OperatorName)
return err
}
if db == nil {
return fmt.Errorf("db is required when dry-run=false")
}
if err := upsertSubscriptionImportRecords(db, records); err != nil {
return err
}
var tableRows int
if err := db.QueryRow(`SELECT COUNT(*) FROM subscription_plan`).Scan(&tableRows); err != nil {
return fmt.Errorf("count subscription_plan: %w", err)
}
_, err = fmt.Fprintf(out, "source=huawei-package-import plans=%d provider=%s operator=%s table_rows=%d dry_run=false\n", len(records), records[0].ProviderName, records[0].OperatorName, tableRows)
return err
}

View File

@@ -0,0 +1,61 @@
//go:build llm_script
package main
import (
"bytes"
"os"
"path/filepath"
"strings"
"testing"
)
func TestParseHuaweiPackageCatalogBuildsPlans(t *testing.T) {
raw, err := os.ReadFile(filepath.Join("testdata", "huawei_package_plan_sample.txt"))
if err != nil {
t.Fatalf("读取 fixture 失败: %v", err)
}
plans, err := parseHuaweiPackageCatalog(string(raw))
if err != nil {
t.Fatalf("parseHuaweiPackageCatalog 返回错误: %v", err)
}
if len(plans) != 8 {
t.Fatalf("期望 8 条华为套餐包记录,实际 %d", len(plans))
}
if plans[0].PlanCode != "huawei-deepseek-v3-1-package-100w-1m" {
t.Fatalf("首条 planCode 错误: %q", plans[0].PlanCode)
}
if plans[4].PlanCode != "huawei-deepseek-v3-2-package-100w-1m" {
t.Fatalf("DeepSeek-V3.2 首条 planCode 错误: %q", plans[4].PlanCode)
}
if plans[7].ListPrice != 2199 {
t.Fatalf("DeepSeek-V3.2 10亿套餐价格错误: %v", plans[7].ListPrice)
}
if plans[7].PlanFamily != "package_plan" {
t.Fatalf("planFamily 错误: %q", plans[7].PlanFamily)
}
}
func TestRunHuaweiPackageImportDryRunPrintsSummary(t *testing.T) {
var out bytes.Buffer
err := runHuaweiPackageImport(huaweiPackageImportConfig{
Fixture: filepath.Join("testdata", "huawei_package_plan_sample.txt"),
DryRun: true,
}, nil, &out)
if err != nil {
t.Fatalf("runHuaweiPackageImport 返回错误: %v", err)
}
output := out.String()
for _, want := range []string{
"source=huawei-package-import",
"plans=8",
"provider=Huawei",
"operator=Huawei Cloud",
"dry_run=true",
} {
if !strings.Contains(output, want) {
t.Fatalf("输出缺少 %q实际: %q", want, output)
}
}
}

View File

@@ -0,0 +1,429 @@
//go:build llm_script
package main
import (
"database/sql"
"encoding/json"
"flag"
"fmt"
"io"
"os"
"sort"
"strings"
"time"
_ "github.com/lib/pq"
)
const defaultManualSubscriptionSeedPath = "seeds/subscription_plan_manual_seed.json"
type manualSubscriptionImportConfig struct {
SeedPath string
DryRun bool
}
type manualSubscriptionSeedEnvelope struct {
CheckedAt string `json:"checkedAt"`
Items []manualSubscriptionSeedItem `json:"items"`
}
type manualSubscriptionSeedItem struct {
ProviderName string `json:"providerName"`
ProviderNameCn string `json:"providerNameCn"`
ProviderCountry string `json:"providerCountry"`
ProviderWebsite string `json:"providerWebsite"`
OperatorName string `json:"operatorName"`
OperatorNameCn string `json:"operatorNameCn"`
OperatorCountry string `json:"operatorCountry"`
OperatorWebsite string `json:"operatorWebsite"`
OperatorType string `json:"operatorType"`
PlanFamily string `json:"planFamily"`
PlanCode string `json:"planCode"`
PlanName string `json:"planName"`
Tier string `json:"tier"`
BillingCycle string `json:"billingCycle"`
Currency string `json:"currency"`
ListPrice float64 `json:"listPrice"`
PriceUnit string `json:"priceUnit"`
QuotaValue int64 `json:"quotaValue"`
QuotaUnit string `json:"quotaUnit"`
ContextWindow int `json:"contextWindow"`
PlanScope string `json:"planScope"`
ModelScope []string `json:"modelScope"`
SourceURL string `json:"sourceURL"`
PublishedAt string `json:"publishedAt"`
EffectiveDate string `json:"effectiveDate"`
Notes string `json:"notes"`
}
type manualSubscriptionRow struct {
ProviderName string
ProviderNameCn string
ProviderCountry string
ProviderWebsite string
OperatorName string
OperatorNameCn string
OperatorCountry string
OperatorWebsite string
OperatorType string
PlanFamily string
PlanCode string
PlanName string
Tier string
BillingCycle string
Currency string
ListPrice float64
PriceUnit string
QuotaValue int64
QuotaUnit string
ContextWindow int
PlanScope string
ModelScope string
SourceURL string
PublishedAt string
EffectiveDate string
Notes string
}
func main() {
loadManualSubscriptionEnv()
var seedPath string
var dryRun bool
flag.StringVar(&seedPath, "seed", defaultManualSubscriptionSeedPath, "手工订阅套餐 seed JSON 路径")
flag.BoolVar(&dryRun, "dry-run", false, "仅校验并打印摘要,不写入数据库")
flag.Parse()
cfg := manualSubscriptionImportConfig{
SeedPath: seedPath,
DryRun: dryRun,
}
var db *sql.DB
var err error
if !cfg.DryRun {
dsn := os.Getenv("DATABASE_URL")
if dsn == "" {
dsn = "postgres://long@/llm_intelligence?host=/var/run/postgresql"
}
db, err = sql.Open("postgres", dsn)
if err != nil {
fmt.Fprintf(os.Stderr, "open db: %v\n", err)
os.Exit(1)
}
defer db.Close()
}
if err := runManualSubscriptionImport(cfg, db, os.Stdout); err != nil {
fmt.Fprintf(os.Stderr, "import_manual_subscription_seed: %v\n", err)
os.Exit(1)
}
}
func loadManualSubscriptionEnv() {
for _, path := range []string{".env.local", ".env"} {
loadManualSubscriptionEnvFile(path)
}
}
func loadManualSubscriptionEnvFile(path string) {
data, err := os.ReadFile(path)
if err != nil {
return
}
for _, line := range strings.Split(string(data), "\n") {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "#") {
continue
}
key, value, ok := strings.Cut(line, "=")
if !ok {
continue
}
key = strings.TrimSpace(key)
value = strings.Trim(strings.TrimSpace(value), `"'`)
if key == "" {
continue
}
if _, exists := os.LookupEnv(key); exists {
continue
}
_ = os.Setenv(key, value)
}
}
func runManualSubscriptionImport(cfg manualSubscriptionImportConfig, db *sql.DB, out io.Writer) error {
envelope, err := loadManualSubscriptionSeed(cfg.SeedPath)
if err != nil {
return err
}
rows, err := buildManualSubscriptionRows(envelope)
if err != nil {
return err
}
if len(rows) == 0 {
return fmt.Errorf("seed is empty")
}
if cfg.DryRun {
_, err = fmt.Fprintf(
out,
"source=manual-subscription-seed checked_at=%s rows=%d operators=%s families=%s dry_run=true\n",
envelope.CheckedAt,
len(rows),
summarizeManualCount(rows, func(row manualSubscriptionRow) string { return row.OperatorName }),
summarizeManualCount(rows, func(row manualSubscriptionRow) string { return row.PlanFamily }),
)
return err
}
if db == nil {
return fmt.Errorf("db is required when dry-run=false")
}
if err := upsertManualSubscriptionRows(db, rows); err != nil {
return err
}
var tableRows int
if err := db.QueryRow(`SELECT COUNT(*) FROM subscription_plan`).Scan(&tableRows); err != nil {
return fmt.Errorf("count subscription_plan: %w", err)
}
_, err = fmt.Fprintf(
out,
"source=manual-subscription-seed checked_at=%s rows=%d table_rows=%d operators=%s families=%s dry_run=false\n",
envelope.CheckedAt,
len(rows),
tableRows,
summarizeManualCount(rows, func(row manualSubscriptionRow) string { return row.OperatorName }),
summarizeManualCount(rows, func(row manualSubscriptionRow) string { return row.PlanFamily }),
)
return err
}
func loadManualSubscriptionSeed(path string) (manualSubscriptionSeedEnvelope, error) {
data, err := os.ReadFile(path)
if err != nil {
return manualSubscriptionSeedEnvelope{}, fmt.Errorf("read seed %s: %w", path, err)
}
var envelope manualSubscriptionSeedEnvelope
if err := json.Unmarshal(data, &envelope); err != nil {
return manualSubscriptionSeedEnvelope{}, fmt.Errorf("unmarshal seed %s: %w", path, err)
}
return envelope, nil
}
func buildManualSubscriptionRows(envelope manualSubscriptionSeedEnvelope) ([]manualSubscriptionRow, error) {
if _, err := time.Parse(time.RFC3339, envelope.CheckedAt); err != nil {
return nil, fmt.Errorf("parse checkedAt: %w", err)
}
validPlanFamilies := map[string]bool{
"token_plan": true,
"coding_plan": true,
"package_plan": true,
}
rows := make([]manualSubscriptionRow, 0, len(envelope.Items))
seenCodes := make(map[string]struct{}, len(envelope.Items))
for _, item := range envelope.Items {
if strings.TrimSpace(item.PlanCode) == "" {
return nil, fmt.Errorf("planCode is required")
}
if _, exists := seenCodes[item.PlanCode]; exists {
return nil, fmt.Errorf("duplicate planCode %q", item.PlanCode)
}
seenCodes[item.PlanCode] = struct{}{}
if !validPlanFamilies[item.PlanFamily] {
return nil, fmt.Errorf("invalid planFamily %q for %s", item.PlanFamily, item.PlanCode)
}
if strings.TrimSpace(item.ProviderName) == "" || strings.TrimSpace(item.OperatorName) == "" {
return nil, fmt.Errorf("provider/operator is required for %s", item.PlanCode)
}
if strings.TrimSpace(item.SourceURL) == "" {
return nil, fmt.Errorf("sourceURL is required for %s", item.PlanCode)
}
modelScope, _ := json.Marshal(item.ModelScope)
rows = append(rows, manualSubscriptionRow{
ProviderName: item.ProviderName,
ProviderNameCn: item.ProviderNameCn,
ProviderCountry: defaultManualIfEmpty(item.ProviderCountry, "unknown"),
ProviderWebsite: item.ProviderWebsite,
OperatorName: item.OperatorName,
OperatorNameCn: item.OperatorNameCn,
OperatorCountry: defaultManualIfEmpty(item.OperatorCountry, "unknown"),
OperatorWebsite: item.OperatorWebsite,
OperatorType: defaultManualIfEmpty(item.OperatorType, "official"),
PlanFamily: item.PlanFamily,
PlanCode: item.PlanCode,
PlanName: item.PlanName,
Tier: item.Tier,
BillingCycle: defaultManualIfEmpty(item.BillingCycle, "monthly"),
Currency: defaultManualIfEmpty(item.Currency, "CNY"),
ListPrice: item.ListPrice,
PriceUnit: item.PriceUnit,
QuotaValue: item.QuotaValue,
QuotaUnit: item.QuotaUnit,
ContextWindow: item.ContextWindow,
PlanScope: item.PlanScope,
ModelScope: string(modelScope),
SourceURL: item.SourceURL,
PublishedAt: item.PublishedAt,
EffectiveDate: item.EffectiveDate,
Notes: item.Notes,
})
}
return rows, nil
}
func upsertManualSubscriptionRows(db *sql.DB, rows []manualSubscriptionRow) error {
for _, row := range rows {
providerID, err := ensureManualSubscriptionProvider(db, row)
if err != nil {
return err
}
operatorID, err := ensureManualSubscriptionOperator(db, row)
if err != nil {
return err
}
publishedAt, err := time.Parse("2006-01-02 15:04:05", row.PublishedAt)
if err != nil {
return fmt.Errorf("parse publishedAt for %s: %w", row.PlanCode, err)
}
effectiveDate, err := time.Parse("2006-01-02", row.EffectiveDate)
if err != nil {
return fmt.Errorf("parse effectiveDate for %s: %w", row.PlanCode, err)
}
_, err = db.Exec(
`INSERT INTO subscription_plan (
provider_id, operator_id, plan_family, plan_code, plan_name, tier,
billing_cycle, currency, list_price, price_unit, quota_value, quota_unit,
context_window, plan_scope, model_scope, source_url, published_at, effective_date, notes
) VALUES (
$1, $2, $3, $4, $5, $6,
$7, $8, $9, $10, $11, $12,
$13, $14, $15, $16, $17, $18, $19
)
ON CONFLICT (provider_id, plan_code, effective_date)
DO UPDATE SET
operator_id = EXCLUDED.operator_id,
plan_family = EXCLUDED.plan_family,
plan_name = EXCLUDED.plan_name,
tier = EXCLUDED.tier,
billing_cycle = EXCLUDED.billing_cycle,
currency = EXCLUDED.currency,
list_price = EXCLUDED.list_price,
price_unit = EXCLUDED.price_unit,
quota_value = EXCLUDED.quota_value,
quota_unit = EXCLUDED.quota_unit,
context_window = EXCLUDED.context_window,
plan_scope = EXCLUDED.plan_scope,
model_scope = EXCLUDED.model_scope,
source_url = EXCLUDED.source_url,
published_at = EXCLUDED.published_at,
notes = EXCLUDED.notes,
updated_at = CURRENT_TIMESTAMP`,
providerID, operatorID, row.PlanFamily, row.PlanCode, row.PlanName, row.Tier,
row.BillingCycle, row.Currency, row.ListPrice, row.PriceUnit, manualNullInt64(row.QuotaValue), manualNullIfEmpty(row.QuotaUnit),
manualNullInt(row.ContextWindow), manualNullIfEmpty(row.PlanScope), row.ModelScope, row.SourceURL, publishedAt, effectiveDate, manualNullIfEmpty(row.Notes),
)
if err != nil {
return fmt.Errorf("upsert subscription_plan %s: %w", row.PlanCode, err)
}
}
return nil
}
func ensureManualSubscriptionProvider(db *sql.DB, row manualSubscriptionRow) (int64, error) {
var providerID int64
err := db.QueryRow(`SELECT id FROM model_provider WHERE name = $1`, row.ProviderName).Scan(&providerID)
if err == nil {
return providerID, nil
}
if err != sql.ErrNoRows {
return 0, err
}
err = db.QueryRow(
`INSERT INTO model_provider (name, name_cn, country, website, status)
VALUES ($1, $2, $3, $4, 'active')
RETURNING id`,
row.ProviderName, manualNullIfEmpty(row.ProviderNameCn), row.ProviderCountry, manualNullIfEmpty(row.ProviderWebsite),
).Scan(&providerID)
return providerID, err
}
func ensureManualSubscriptionOperator(db *sql.DB, row manualSubscriptionRow) (int64, error) {
var operatorID int64
err := db.QueryRow(`SELECT id FROM operator WHERE name = $1`, row.OperatorName).Scan(&operatorID)
if err == nil {
return operatorID, nil
}
if err != sql.ErrNoRows {
return 0, err
}
err = db.QueryRow(
`INSERT INTO operator (name, name_cn, country, website, description, status, type)
VALUES ($1, $2, $3, $4, $5, 'active', $6)
RETURNING id`,
row.OperatorName, manualNullIfEmpty(row.OperatorNameCn), row.OperatorCountry, manualNullIfEmpty(row.OperatorWebsite),
fmt.Sprintf("%s manual subscription seed", row.OperatorName), row.OperatorType,
).Scan(&operatorID)
return operatorID, err
}
func summarizeManualCount(rows []manualSubscriptionRow, getter func(manualSubscriptionRow) string) string {
counts := make(map[string]int)
keys := make([]string, 0)
for _, row := range rows {
key := getter(row)
if _, exists := counts[key]; !exists {
keys = append(keys, key)
}
counts[key]++
}
sort.Strings(keys)
parts := make([]string, 0, len(keys))
for _, key := range keys {
parts = append(parts, fmt.Sprintf("%s:%d", key, counts[key]))
}
return strings.Join(parts, ",")
}
func defaultManualIfEmpty(value string, fallback string) string {
if strings.TrimSpace(value) == "" {
return fallback
}
return value
}
func manualNullIfEmpty(value string) any {
if strings.TrimSpace(value) == "" {
return nil
}
return value
}
func manualNullInt(value int) any {
if value == 0 {
return nil
}
return value
}
func manualNullInt64(value int64) any {
if value == 0 {
return nil
}
return value
}

View File

@@ -0,0 +1,56 @@
//go:build llm_script
package main
import (
"bytes"
"path/filepath"
"strings"
"testing"
)
func TestBuildManualSubscriptionRows(t *testing.T) {
envelope, err := loadManualSubscriptionSeed(filepath.Join("..", "seeds", "subscription_plan_manual_seed.json"))
if err != nil {
t.Fatalf("loadManualSubscriptionSeed 失败: %v", err)
}
rows, err := buildManualSubscriptionRows(envelope)
if err != nil {
t.Fatalf("buildManualSubscriptionRows 失败: %v", err)
}
if len(rows) != 3 {
t.Fatalf("期望 3 条套餐记录,实际 %d", len(rows))
}
if rows[0].PlanCode != "minimax-token-plan-starter" {
t.Fatalf("首条 planCode 错误: %q", rows[0].PlanCode)
}
if rows[len(rows)-1].PlanCode != "minimax-token-plan-max" {
t.Fatalf("末条 planCode 错误: %q", rows[len(rows)-1].PlanCode)
}
}
func TestRunManualSubscriptionImportDryRunPrintsSummary(t *testing.T) {
var out bytes.Buffer
err := runManualSubscriptionImport(manualSubscriptionImportConfig{
SeedPath: filepath.Join("..", "seeds", "subscription_plan_manual_seed.json"),
DryRun: true,
}, nil, &out)
if err != nil {
t.Fatalf("runManualSubscriptionImport 失败: %v", err)
}
output := out.String()
for _, want := range []string{
"source=manual-subscription-seed",
"rows=3",
"MiniMax:3",
"token_plan:3",
"dry_run=true",
} {
if !strings.Contains(output, want) {
t.Fatalf("输出缺少 %q实际: %q", want, output)
}
}
}

View File

@@ -0,0 +1,88 @@
//go:build llm_script
package main
import (
"database/sql"
"flag"
"fmt"
"io"
"net/http"
"os"
"time"
)
type minimaxSubscriptionImportConfig struct {
URL string
Fixture string
DryRun bool
Timeout time.Duration
}
func main() {
loadSubscriptionImportEnv()
var url string
var fixture string
var dryRun bool
var timeoutSeconds int
flag.StringVar(&url, "url", defaultMinimaxTokenPlanURL, "MiniMax Token Plan 文档 URL")
flag.StringVar(&fixture, "fixture", "", "MiniMax Token Plan 本地样例文件")
flag.BoolVar(&dryRun, "dry-run", false, "仅解析并打印摘要,不写入数据库")
flag.IntVar(&timeoutSeconds, "timeout", 20, "请求超时(秒)")
flag.Parse()
cfg := minimaxSubscriptionImportConfig{
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 := runMinimaxSubscriptionImport(cfg, db, os.Stdout); err != nil {
fmt.Fprintf(os.Stderr, "import_minimax_subscription: %v\n", err)
os.Exit(1)
}
}
func runMinimaxSubscriptionImport(cfg minimaxSubscriptionImportConfig, db *sql.DB, out io.Writer) error {
client := &http.Client{Timeout: cfg.Timeout}
raw, err := fetchSubscriptionPage(cfg.URL, cfg.Fixture, client)
if err != nil {
return err
}
records, err := parseMinimaxTokenPlans(raw)
if err != nil {
return err
}
if cfg.DryRun {
_, err = fmt.Fprintf(out, "source=minimax-subscription-import plans=%d provider=%s operator=%s dry_run=true\n", len(records), records[0].ProviderName, records[0].OperatorName)
return err
}
if db == nil {
return fmt.Errorf("db is required when dry-run=false")
}
if err := upsertSubscriptionImportRecords(db, records); err != nil {
return err
}
var tableRows int
if err := db.QueryRow(`SELECT COUNT(*) FROM subscription_plan`).Scan(&tableRows); err != nil {
return fmt.Errorf("count subscription_plan: %w", err)
}
_, err = fmt.Fprintf(out, "source=minimax-subscription-import plans=%d provider=%s operator=%s table_rows=%d dry_run=false\n", len(records), records[0].ProviderName, records[0].OperatorName, tableRows)
return err
}

View File

@@ -0,0 +1,74 @@
//go:build llm_script
package main
import (
"bytes"
"os"
"path/filepath"
"strings"
"testing"
)
func TestParseMinimaxTokenPlansBuildsRecords(t *testing.T) {
raw, err := os.ReadFile(filepath.Join("testdata", "minimax_token_plan_sample.txt"))
if err != nil {
t.Fatalf("读取 MiniMax fixture 失败: %v", err)
}
plans, err := parseMinimaxTokenPlans(string(raw))
if err != nil {
t.Fatalf("parseMinimaxTokenPlans 失败: %v", err)
}
if len(plans) != 12 {
t.Fatalf("期望 12 条 MiniMax 套餐记录,实际 %d", len(plans))
}
if plans[0].PlanCode != "minimax-token-plan-starter" {
t.Fatalf("首条 planCode 错误: %q", plans[0].PlanCode)
}
if plans[0].ListPrice != 10 || plans[0].QuotaValue != 1500 {
t.Fatalf("Starter 月度套餐解析错误: %+v", plans[0])
}
if plans[5].PlanCode != "minimax-token-plan-ultra-highspeed" {
t.Fatalf("高速月度套餐 planCode 错误: %q", plans[5].PlanCode)
}
if plans[5].ListPrice != 150 || plans[5].QuotaValue != 30000 {
t.Fatalf("高速月度套餐解析错误: %+v", plans[5])
}
if plans[len(plans)-1].PlanCode != "minimax-token-plan-ultra-highspeed-yearly" {
t.Fatalf("末条 planCode 错误: %q", plans[len(plans)-1].PlanCode)
}
if plans[len(plans)-1].ListPrice != 1500 || plans[len(plans)-1].QuotaValue != 30000 {
t.Fatalf("高速年度套餐解析错误: %+v", plans[len(plans)-1])
}
if !strings.Contains(plans[0].Notes, "Speech 2.8") {
t.Fatalf("标准套餐备注缺少附带配额说明: %q", plans[0].Notes)
}
if !strings.Contains(plans[5].Notes, "高速版覆盖") {
t.Fatalf("高速套餐备注缺少高速版说明: %q", plans[5].Notes)
}
}
func TestRunMinimaxSubscriptionImportDryRunPrintsSummary(t *testing.T) {
var out bytes.Buffer
err := runMinimaxSubscriptionImport(minimaxSubscriptionImportConfig{
Fixture: filepath.Join("testdata", "minimax_token_plan_sample.txt"),
DryRun: true,
}, nil, &out)
if err != nil {
t.Fatalf("runMinimaxSubscriptionImport 失败: %v", err)
}
output := out.String()
for _, want := range []string{
"source=minimax-subscription-import",
"plans=12",
"provider=MiniMax",
"operator=MiniMax",
"dry_run=true",
} {
if !strings.Contains(output, want) {
t.Fatalf("输出缺少 %q实际: %q", want, output)
}
}
}

View File

@@ -0,0 +1,94 @@
//go:build llm_script
package main
import (
"database/sql"
"flag"
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
)
const defaultMobileCloudCatalogURL = "https://saas.ecloud.10086.cn/Store/List"
func main() {
loadSubscriptionImportEnv()
var url string
var fixture string
var dryRun bool
var timeoutSeconds int
flag.StringVar(&url, "url", defaultMobileCloudCatalogURL, "移动云 AI 应用专区 URL")
flag.StringVar(&fixture, "fixture", "", "移动云 AI 应用专区样例文件")
flag.BoolVar(&dryRun, "dry-run", false, "仅校验并打印摘要,不写入数据库")
flag.IntVar(&timeoutSeconds, "timeout", 20, "请求超时(秒)")
flag.Parse()
cfg := catalogVerificationImportConfig{
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 := runMobileCloudCatalogImport(cfg, db, os.Stdout); err != nil {
fmt.Fprintf(os.Stderr, "import_mobile_cloud_catalog: %v\n", err)
os.Exit(1)
}
}
func parseMobileCloudCatalog(raw string) ([]catalogVerificationRecord, error) {
if !strings.Contains(raw, "AI应用专区") {
return nil, fmt.Errorf("mobile cloud AI market marker not found")
}
if !strings.Contains(raw, "数据大模型") {
return nil, fmt.Errorf("mobile cloud data model marker not found")
}
return []catalogVerificationRecord{{
CatalogCode: "mobile-cloud-ai-market",
SourceURL: defaultMobileCloudCatalogURL,
SourceTitle: "移动云市场 AI 应用专区",
PlanStatus: "confirmed",
Notes: "官方云市场已公开展示 AI 应用专区,覆盖数据大模型等类目,但统一编程套餐价格仍未公开披露。",
}}, nil
}
func runMobileCloudCatalogImport(cfg catalogVerificationImportConfig, db *sql.DB, out io.Writer) error {
client := &http.Client{Timeout: cfg.Timeout}
raw, err := fetchSubscriptionPage(cfg.URL, cfg.Fixture, client)
if err != nil {
return err
}
records, err := parseMobileCloudCatalog(raw)
if err != nil {
return err
}
if cfg.DryRun {
_, err = fmt.Fprintf(out, "source=mobile-cloud-catalog-import entries=%d dry_run=true\n", len(records))
return err
}
if db == nil {
return fmt.Errorf("db is required when dry-run=false")
}
if err := upsertCatalogVerificationRecords(db, records); err != nil {
return err
}
_, err = fmt.Fprintf(out, "source=mobile-cloud-catalog-import entries=%d dry_run=false\n", len(records))
return err
}

View File

@@ -0,0 +1,54 @@
//go:build llm_script
package main
import (
"bytes"
"os"
"path/filepath"
"strings"
"testing"
)
func TestParseMobileCloudCatalogBuildsRecord(t *testing.T) {
raw, err := os.ReadFile(filepath.Join("testdata", "mobile_cloud_catalog_sample.txt"))
if err != nil {
t.Fatalf("读取 fixture 失败: %v", err)
}
records, err := parseMobileCloudCatalog(string(raw))
if err != nil {
t.Fatalf("parseMobileCloudCatalog 返回错误: %v", err)
}
if len(records) != 1 {
t.Fatalf("期望 1 条移动云目录记录,实际 %d", len(records))
}
if records[0].CatalogCode != "mobile-cloud-ai-market" {
t.Fatalf("catalogCode 错误: %q", records[0].CatalogCode)
}
if records[0].PlanStatus != "confirmed" {
t.Fatalf("planStatus 错误: %q", records[0].PlanStatus)
}
}
func TestRunMobileCloudCatalogImportDryRunPrintsSummary(t *testing.T) {
var out bytes.Buffer
err := runMobileCloudCatalogImport(catalogVerificationImportConfig{
URL: defaultMobileCloudCatalogURL,
Fixture: filepath.Join("testdata", "mobile_cloud_catalog_sample.txt"),
DryRun: true,
}, nil, &out)
if err != nil {
t.Fatalf("runMobileCloudCatalogImport 返回错误: %v", err)
}
output := out.String()
for _, want := range []string{
"source=mobile-cloud-catalog-import",
"entries=1",
"dry_run=true",
} {
if !strings.Contains(output, want) {
t.Fatalf("输出缺少 %q实际: %q", want, output)
}
}
}

View File

@@ -0,0 +1,552 @@
//go:build llm_script
package main
import (
"database/sql"
"encoding/json"
"flag"
"fmt"
"io"
"os"
"sort"
"strings"
"time"
_ "github.com/lib/pq"
)
const defaultPlanCatalogSeedPaths = "seeds/plan_catalog_inventory_seed.json,seeds/plan_catalog_inventory_seed_cn_vendors_top20.json,seeds/plan_catalog_inventory_seed_cn_relays_top20plus.json,seeds/plan_catalog_inventory_seed_web_research.json"
type importPlanCatalogConfig struct {
SeedPaths string
DryRun bool
}
type planCatalogSeedEnvelope struct {
CheckedAt string `json:"checkedAt"`
Items []planCatalogSeedItem `json:"items"`
}
type planCatalogSeedItem struct {
CatalogCode string `json:"catalogCode"`
ProviderName string `json:"providerName"`
ProviderNameCn string `json:"providerNameCn"`
ProviderCountry string `json:"providerCountry"`
ProviderWebsite string `json:"providerWebsite"`
OperatorName string `json:"operatorName"`
OperatorNameCn string `json:"operatorNameCn"`
OperatorCountry string `json:"operatorCountry"`
OperatorWebsite string `json:"operatorWebsite"`
OperatorType string `json:"operatorType"`
PlatformName string `json:"platformName"`
PlatformNameCn string `json:"platformNameCn"`
PlatformType string `json:"platformType"`
PlanFamily string `json:"planFamily"`
PlanStatus string `json:"planStatus"`
SourceURL string `json:"sourceURL"`
SourceTitle string `json:"sourceTitle"`
SourceKind string `json:"sourceKind"`
Region string `json:"region"`
Currency string `json:"currency"`
BillingCycle string `json:"billingCycle"`
ImporterKey string `json:"importerKey"`
Notes string `json:"notes"`
CatalogSegment string `json:"catalogSegment"`
MarketRank int `json:"marketRank"`
}
type planCatalogRow struct {
CatalogCode string
ProviderName string
ProviderNameCn string
ProviderCountry string
ProviderWebsite string
OperatorName string
OperatorNameCn string
OperatorCountry string
OperatorWebsite string
OperatorType string
PlatformName string
PlatformNameCn string
PlatformType string
PlanFamily string
PlanStatus string
SourceURL string
SourceTitle string
SourceKind string
Region string
Currency string
BillingCycle string
ImporterKey string
Notes string
LastCheckedAt time.Time
CatalogSegment string
MarketRank int
}
func main() {
loadImportProjectEnv()
var seedPaths string
var dryRun bool
flag.StringVar(&seedPaths, "seed", defaultPlanCatalogSeedPaths, "基础目录 seed JSON 路径,支持逗号分隔多个文件")
flag.BoolVar(&dryRun, "dry-run", false, "仅校验并打印摘要,不写入数据库")
flag.Parse()
cfg := importPlanCatalogConfig{
SeedPaths: seedPaths,
DryRun: dryRun,
}
var db *sql.DB
var err error
if !cfg.DryRun {
dsn := os.Getenv("DATABASE_URL")
if dsn == "" {
dsn = "postgres://long@/llm_intelligence?host=/var/run/postgresql"
}
db, err = sql.Open("postgres", dsn)
if err != nil {
fmt.Fprintf(os.Stderr, "open db: %v\n", err)
os.Exit(1)
}
defer db.Close()
}
if err := runPlanCatalogImport(cfg, db, os.Stdout); err != nil {
fmt.Fprintf(os.Stderr, "import_plan_catalog: %v\n", err)
os.Exit(1)
}
}
func loadImportProjectEnv() {
for _, path := range []string{".env.local", ".env"} {
loadImportEnvFile(path)
}
}
func loadImportEnvFile(path string) {
data, err := os.ReadFile(path)
if err != nil {
return
}
for _, line := range strings.Split(string(data), "\n") {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "#") {
continue
}
key, value, ok := strings.Cut(line, "=")
if !ok {
continue
}
key = strings.TrimSpace(key)
value = strings.Trim(strings.TrimSpace(value), `"'`)
if key == "" {
continue
}
if _, exists := os.LookupEnv(key); exists {
continue
}
_ = os.Setenv(key, value)
}
}
func runPlanCatalogImport(cfg importPlanCatalogConfig, db *sql.DB, out io.Writer) error {
envelope, err := loadPlanCatalogSeeds(splitCSVPaths(cfg.SeedPaths))
if err != nil {
return err
}
rows, err := buildPlanCatalogRows(envelope)
if err != nil {
return err
}
if len(rows) == 0 {
return fmt.Errorf("seed is empty")
}
if cfg.DryRun {
_, err = fmt.Fprintf(
out,
"source=plan-catalog-import checked_at=%s rows=%d families=%s statuses=%s dry_run=true\n",
envelope.CheckedAt,
len(rows),
formatSummaryCount(countByField(rows, func(row planCatalogRow) string { return row.PlanFamily })),
formatSummaryCount(countByField(rows, func(row planCatalogRow) string { return row.PlanStatus })),
)
return err
}
if db == nil {
return fmt.Errorf("db is required when dry-run=false")
}
if err := upsertPlanCatalogInventory(db, rows); err != nil {
return err
}
var tableRows int
if err := db.QueryRow(`SELECT COUNT(*) FROM plan_catalog_inventory`).Scan(&tableRows); err != nil {
return fmt.Errorf("count plan_catalog_inventory: %w", err)
}
_, err = fmt.Fprintf(
out,
"source=plan-catalog-import checked_at=%s rows=%d table_rows=%d families=%s statuses=%s dry_run=false\n",
envelope.CheckedAt,
len(rows),
tableRows,
formatSummaryCount(countByField(rows, func(row planCatalogRow) string { return row.PlanFamily })),
formatSummaryCount(countByField(rows, func(row planCatalogRow) string { return row.PlanStatus })),
)
return err
}
func loadPlanCatalogSeed(path string) (planCatalogSeedEnvelope, error) {
data, err := os.ReadFile(path)
if err != nil {
return planCatalogSeedEnvelope{}, fmt.Errorf("read seed %s: %w", path, err)
}
var envelope planCatalogSeedEnvelope
if err := json.Unmarshal(data, &envelope); err != nil {
return planCatalogSeedEnvelope{}, fmt.Errorf("unmarshal seed %s: %w", path, err)
}
return envelope, nil
}
func loadPlanCatalogSeeds(paths []string) (planCatalogSeedEnvelope, error) {
if len(paths) == 0 {
return planCatalogSeedEnvelope{}, fmt.Errorf("at least one seed path is required")
}
mergedItems := make(map[string]planCatalogSeedItem)
var checkedAt string
for _, path := range paths {
envelope, err := loadPlanCatalogSeed(path)
if err != nil {
return planCatalogSeedEnvelope{}, err
}
if strings.TrimSpace(envelope.CheckedAt) != "" {
checkedAt = envelope.CheckedAt
}
for _, item := range envelope.Items {
mergedItems[item.CatalogCode] = item
}
}
codes := make([]string, 0, len(mergedItems))
for code := range mergedItems {
codes = append(codes, code)
}
sort.Strings(codes)
items := make([]planCatalogSeedItem, 0, len(codes))
for _, code := range codes {
items = append(items, mergedItems[code])
}
return planCatalogSeedEnvelope{
CheckedAt: checkedAt,
Items: items,
}, nil
}
func buildPlanCatalogRows(envelope planCatalogSeedEnvelope) ([]planCatalogRow, error) {
checkedAt, err := time.Parse(time.RFC3339, envelope.CheckedAt)
if err != nil {
return nil, fmt.Errorf("parse checkedAt: %w", err)
}
validPlatformTypes := map[string]bool{
"official_vendor": true,
"cloud_operator": true,
"relay_platform": true,
}
validPlanFamilies := map[string]bool{
"token_plan": true,
"coding_plan": true,
"package_plan": true,
"pay_as_you_go": true,
"unknown": true,
}
validPlanStatuses := map[string]bool{
"confirmed": true,
"pending_verification": true,
"retired": true,
}
validSourceKinds := map[string]bool{
"official_doc": true,
"official_pricing": true,
"official_product_page": true,
"official_community": true,
"inferred": true,
}
validCatalogSegments := map[string]bool{
"general": true,
"vendor_top20": true,
"relay_top20plus": true,
"global_reference": true,
}
rows := make([]planCatalogRow, 0, len(envelope.Items))
seenCodes := make(map[string]struct{}, len(envelope.Items))
for _, item := range envelope.Items {
if strings.TrimSpace(item.CatalogCode) == "" {
return nil, fmt.Errorf("catalogCode is required")
}
if _, exists := seenCodes[item.CatalogCode]; exists {
return nil, fmt.Errorf("duplicate catalogCode %q", item.CatalogCode)
}
seenCodes[item.CatalogCode] = struct{}{}
if !validPlatformTypes[item.PlatformType] {
return nil, fmt.Errorf("invalid platformType %q for %s", item.PlatformType, item.CatalogCode)
}
if !validPlanFamilies[item.PlanFamily] {
return nil, fmt.Errorf("invalid planFamily %q for %s", item.PlanFamily, item.CatalogCode)
}
if !validPlanStatuses[item.PlanStatus] {
return nil, fmt.Errorf("invalid planStatus %q for %s", item.PlanStatus, item.CatalogCode)
}
if !validSourceKinds[item.SourceKind] {
return nil, fmt.Errorf("invalid sourceKind %q for %s", item.SourceKind, item.CatalogCode)
}
segment := defaultIfEmpty(item.CatalogSegment, "general")
if !validCatalogSegments[segment] {
return nil, fmt.Errorf("invalid catalogSegment %q for %s", item.CatalogSegment, item.CatalogCode)
}
if item.MarketRank < 0 {
return nil, fmt.Errorf("invalid marketRank %d for %s", item.MarketRank, item.CatalogCode)
}
if strings.TrimSpace(item.ProviderName) == "" {
return nil, fmt.Errorf("providerName is required for %s", item.CatalogCode)
}
if strings.TrimSpace(item.PlatformName) == "" {
return nil, fmt.Errorf("platformName is required for %s", item.CatalogCode)
}
if strings.TrimSpace(item.SourceURL) == "" {
return nil, fmt.Errorf("sourceURL is required for %s", item.CatalogCode)
}
rows = append(rows, planCatalogRow{
CatalogCode: item.CatalogCode,
ProviderName: item.ProviderName,
ProviderNameCn: item.ProviderNameCn,
ProviderCountry: defaultIfEmpty(item.ProviderCountry, "unknown"),
ProviderWebsite: item.ProviderWebsite,
OperatorName: item.OperatorName,
OperatorNameCn: item.OperatorNameCn,
OperatorCountry: defaultIfEmpty(item.OperatorCountry, "unknown"),
OperatorWebsite: item.OperatorWebsite,
OperatorType: defaultIfEmpty(item.OperatorType, "official"),
PlatformName: item.PlatformName,
PlatformNameCn: item.PlatformNameCn,
PlatformType: item.PlatformType,
PlanFamily: item.PlanFamily,
PlanStatus: item.PlanStatus,
SourceURL: item.SourceURL,
SourceTitle: item.SourceTitle,
SourceKind: item.SourceKind,
Region: defaultIfEmpty(item.Region, "global"),
Currency: item.Currency,
BillingCycle: item.BillingCycle,
ImporterKey: item.ImporterKey,
Notes: item.Notes,
LastCheckedAt: checkedAt,
CatalogSegment: segment,
MarketRank: item.MarketRank,
})
}
return rows, nil
}
func upsertPlanCatalogInventory(db *sql.DB, rows []planCatalogRow) error {
for _, row := range rows {
providerID, err := ensurePlanCatalogProvider(db, row)
if err != nil {
return err
}
var operatorID any
if strings.TrimSpace(row.OperatorName) != "" {
id, err := ensurePlanCatalogOperator(db, row)
if err != nil {
return err
}
operatorID = id
}
_, err = db.Exec(
`INSERT INTO plan_catalog_inventory (
provider_id, operator_id, catalog_code, platform_name, platform_name_cn,
platform_type, plan_family, plan_status, source_url, source_title,
source_kind, region, currency, billing_cycle, last_checked_at,
importer_key, notes, catalog_segment, market_rank
) VALUES (
$1, $2, $3, $4, $5,
$6, $7, $8, $9, $10,
$11, $12, $13, $14, $15,
$16, $17, $18, $19
)
ON CONFLICT (catalog_code)
DO UPDATE SET
provider_id = EXCLUDED.provider_id,
operator_id = EXCLUDED.operator_id,
platform_name = EXCLUDED.platform_name,
platform_name_cn = EXCLUDED.platform_name_cn,
platform_type = EXCLUDED.platform_type,
plan_family = EXCLUDED.plan_family,
plan_status = EXCLUDED.plan_status,
source_url = EXCLUDED.source_url,
source_title = EXCLUDED.source_title,
source_kind = EXCLUDED.source_kind,
region = EXCLUDED.region,
currency = EXCLUDED.currency,
billing_cycle = EXCLUDED.billing_cycle,
last_checked_at = EXCLUDED.last_checked_at,
importer_key = EXCLUDED.importer_key,
notes = EXCLUDED.notes,
catalog_segment = EXCLUDED.catalog_segment,
market_rank = EXCLUDED.market_rank,
updated_at = CURRENT_TIMESTAMP`,
providerID, operatorID, row.CatalogCode, row.PlatformName, nullIfEmpty(row.PlatformNameCn),
row.PlatformType, row.PlanFamily, row.PlanStatus, row.SourceURL, nullIfEmpty(row.SourceTitle),
row.SourceKind, row.Region, nullIfEmpty(row.Currency), nullIfEmpty(row.BillingCycle), row.LastCheckedAt,
nullIfEmpty(row.ImporterKey), nullIfEmpty(row.Notes), row.CatalogSegment, nullIfZeroInt(row.MarketRank),
)
if err != nil {
return fmt.Errorf("upsert plan_catalog_inventory %s: %w", row.CatalogCode, err)
}
}
return nil
}
func ensurePlanCatalogProvider(db *sql.DB, row planCatalogRow) (int64, error) {
var providerID int64
err := db.QueryRow(`SELECT id FROM model_provider WHERE name = $1`, row.ProviderName).Scan(&providerID)
if err == nil {
_, updateErr := db.Exec(
`UPDATE model_provider
SET name_cn = COALESCE(NULLIF(name_cn, ''), $2),
country = CASE
WHEN COALESCE(country, '') = '' OR country = 'unknown' THEN $3
ELSE country
END,
website = COALESCE(NULLIF(website, ''), $4),
updated_at = CURRENT_TIMESTAMP
WHERE id = $1`,
providerID, nullIfEmpty(row.ProviderNameCn), row.ProviderCountry, nullIfEmpty(row.ProviderWebsite),
)
return providerID, updateErr
}
if err != sql.ErrNoRows {
return 0, err
}
err = db.QueryRow(
`INSERT INTO model_provider (name, name_cn, country, website, status)
VALUES ($1, $2, $3, $4, 'active')
RETURNING id`,
row.ProviderName, nullIfEmpty(row.ProviderNameCn), row.ProviderCountry, nullIfEmpty(row.ProviderWebsite),
).Scan(&providerID)
return providerID, err
}
func ensurePlanCatalogOperator(db *sql.DB, row planCatalogRow) (int64, error) {
var operatorID int64
err := db.QueryRow(`SELECT id FROM operator WHERE name = $1`, row.OperatorName).Scan(&operatorID)
if err == nil {
_, updateErr := db.Exec(
`UPDATE operator
SET name_cn = COALESCE(NULLIF(name_cn, ''), $2),
country = CASE
WHEN COALESCE(country, '') = '' OR country = 'unknown' THEN $3
ELSE country
END,
website = COALESCE(NULLIF(website, ''), $4),
description = COALESCE(NULLIF(description, ''), $5),
type = CASE
WHEN COALESCE(type, '') = '' OR type = 'reseller' THEN $6
ELSE type
END,
updated_at = CURRENT_TIMESTAMP
WHERE id = $1`,
operatorID,
nullIfEmpty(row.OperatorNameCn),
row.OperatorCountry,
nullIfEmpty(row.OperatorWebsite),
fmt.Sprintf("%s catalog inventory", row.PlatformName),
nullIfEmpty(row.OperatorType),
)
return operatorID, updateErr
}
if err != sql.ErrNoRows {
return 0, err
}
err = db.QueryRow(
`INSERT INTO operator (name, name_cn, country, website, description, status, type)
VALUES ($1, $2, $3, $4, $5, 'active', $6)
RETURNING id`,
row.OperatorName, nullIfEmpty(row.OperatorNameCn), row.OperatorCountry, nullIfEmpty(row.OperatorWebsite),
fmt.Sprintf("%s catalog inventory", row.PlatformName), row.OperatorType,
).Scan(&operatorID)
return operatorID, err
}
func countByField(rows []planCatalogRow, getter func(planCatalogRow) string) map[string]int {
result := make(map[string]int)
for _, row := range rows {
result[getter(row)]++
}
return result
}
func formatSummaryCount(values map[string]int) string {
keys := make([]string, 0, len(values))
for key := range values {
keys = append(keys, key)
}
sort.Strings(keys)
parts := make([]string, 0, len(keys))
for _, key := range keys {
parts = append(parts, fmt.Sprintf("%s:%d", key, values[key]))
}
return strings.Join(parts, ",")
}
func defaultIfEmpty(value string, fallback string) string {
if strings.TrimSpace(value) == "" {
return fallback
}
return value
}
func splitCSVPaths(raw string) []string {
parts := strings.Split(raw, ",")
paths := make([]string, 0, len(parts))
for _, part := range parts {
part = strings.TrimSpace(part)
if part != "" {
paths = append(paths, part)
}
}
return paths
}
func nullIfEmpty(value string) any {
if strings.TrimSpace(value) == "" {
return nil
}
return value
}
func nullIfZeroInt(value int) any {
if value == 0 {
return nil
}
return value
}

View File

@@ -0,0 +1,149 @@
//go:build llm_script
package main
import (
"bytes"
"path/filepath"
"strings"
"testing"
)
func TestBuildPlanCatalogRows(t *testing.T) {
envelope, err := loadPlanCatalogSeeds([]string{
filepath.Join("..", "seeds", "plan_catalog_inventory_seed.json"),
filepath.Join("..", "seeds", "plan_catalog_inventory_seed_cn_vendors_top20.json"),
filepath.Join("..", "seeds", "plan_catalog_inventory_seed_cn_relays_top20plus.json"),
filepath.Join("..", "seeds", "plan_catalog_inventory_seed_web_research.json"),
})
if err != nil {
t.Fatalf("loadPlanCatalogSeeds 失败: %v", err)
}
rows, err := buildPlanCatalogRows(envelope)
if err != nil {
t.Fatalf("buildPlanCatalogRows 失败: %v", err)
}
if len(rows) != 70 {
t.Fatalf("期望 70 条基础目录记录,实际 %d", len(rows))
}
foundVendorTop20 := false
foundRelayTop20Plus := false
wantImporterKeys := map[string]string{
"tencent-cloud-token-plan-enterprise-pro": "tencent_catalog",
"tencent-cloud-token-plan-enterprise-lite": "tencent_catalog",
"tencent-cloud-coding-plan": "tencent_catalog",
"aliyun-bailian-token-plan-team": "import_aliyun_subscription.go",
"aliyun-bailian-coding-plan": "import_aliyun_subscription.go",
"baidu-qianfan-token-benefit-pack": "import_baidu_subscription.go",
"baidu-qianfan-coding-plan": "import_baidu_subscription.go",
"zhipu-glm-coding-plan": "import_zhipu_coding_plan.go",
"minimax-token-plan": "import_minimax_subscription.go",
"volcengine-ark-coding-plan": "import_bytedance_subscription.go",
"huawei-cloud-maas-package-plan": "import_huawei_package.go",
"ctyun-token-plan": "import_ctyun_subscription.go",
"ctyun-coding-plan": "import_ctyun_subscription.go",
"cucloud-aicp-platform": "import_cucloud_catalog.go",
"cucloud-ai-app-platform": "import_cucloud_catalog.go",
"mobile-cloud-ai-market": "import_mobile_cloud_catalog.go",
"youdao-zhiyun-maas": "import_youdao_pricing.go",
"360-open-platform": "import_360_pricing.go",
"siliconflow-siliconcloud": "import_siliconflow_pricing.go",
"ppio-model-api": "import_ppio_pricing.go",
"ucloud-umodelverse": "import_ucloud_pricing.go",
"anthropic-api-payg": "import_catalog_seed_verification.go",
"xai-api-payg": "import_catalog_seed_verification.go",
"alibaba-qwen-api-payg": "import_catalog_seed_verification.go",
"tencent-hunyuan-api-payg": "import_catalog_seed_verification.go",
"huawei-pangu-api-payg": "import_catalog_seed_verification.go",
"baichuan-api-payg": "import_catalog_seed_verification.go",
"01ai-api-payg": "import_catalog_seed_verification.go",
"sensenova-api-payg": "import_catalog_seed_verification.go",
"xfyun-spark-api-payg": "import_catalog_seed_verification.go",
"360-zhinao-api-payg": "import_catalog_seed_verification.go",
"youdao-ziyue-api-payg": "import_catalog_seed_verification.go",
"modelbest-minicpm-api-payg": "import_catalog_seed_verification.go",
"baai-flagopen-api-payg": "import_catalog_seed_verification.go",
"skywork-api-payg": "import_catalog_seed_verification.go",
"infinigence-api-payg": "import_catalog_seed_verification.go",
"qingcloud-coreshub": "import_catalog_seed_verification.go",
"ksyun-xingliu-platform": "import_catalog_seed_verification.go",
"google-gemini-api-payg": "import_catalog_seed_verification.go",
"mistral-api-payg": "import_catalog_seed_verification.go",
"cohere-api-payg": "import_catalog_seed_verification.go",
"openrouter-api-payg": "fetch_openrouter.go",
"together-ai-api-payg": "import_catalog_seed_verification.go",
"fireworks-ai-api-payg": "import_catalog_seed_verification.go",
"deepinfra-api-payg": "import_catalog_seed_verification.go",
"groq-api-payg": "import_catalog_seed_verification.go",
"replicate-api-payg": "import_catalog_seed_verification.go",
"hyperbolic-api-payg": "import_catalog_seed_verification.go",
"novita-ai-api-payg": "import_catalog_seed_verification.go",
"azure-openai-service-payg": "import_azure_openai_pricing.go",
"amazon-bedrock-payg": "import_bedrock_pricing.go",
"google-vertex-ai-genai-payg": "import_vertex_pricing.go",
"cloudflare-workers-ai-payg": "import_cloudflare_pricing.go",
"baseten-inference-payg": "import_catalog_seed_verification.go",
"cerebras-inference-payg": "import_catalog_seed_verification.go",
"perplexity-agent-api-payg": "import_perplexity_pricing.go",
"sambanova-cloud-payg": "import_catalog_seed_verification.go",
"jdcloud-joybuilder-payg": "import_catalog_seed_verification.go",
}
for _, row := range rows {
if row.CatalogCode == "zhipu-glm-coding-plan" {
if row.CatalogSegment != "vendor_top20" || row.MarketRank != 5 {
t.Fatalf("智谱榜单字段错误: segment=%q rank=%d", row.CatalogSegment, row.MarketRank)
}
foundVendorTop20 = true
}
if row.CatalogCode == "ctyun-coding-plan" {
if row.CatalogSegment != "relay_top20plus" || row.MarketRank != 9 {
t.Fatalf("天翼云编码套餐榜单字段错误: segment=%q rank=%d", row.CatalogSegment, row.MarketRank)
}
foundRelayTop20Plus = true
}
if wantImporterKey, ok := wantImporterKeys[row.CatalogCode]; ok && row.ImporterKey != wantImporterKey {
t.Fatalf("%s importerKey 错误: got=%q want=%q", row.CatalogCode, row.ImporterKey, wantImporterKey)
}
}
if !foundVendorTop20 {
t.Fatalf("缺少 vendor_top20 覆盖记录")
}
if !foundRelayTop20Plus {
t.Fatalf("缺少 relay_top20plus 覆盖记录")
}
}
func TestRunPlanCatalogImportDryRunPrintsSummary(t *testing.T) {
var out bytes.Buffer
err := runPlanCatalogImport(importPlanCatalogConfig{
SeedPaths: strings.Join([]string{
filepath.Join("..", "seeds", "plan_catalog_inventory_seed.json"),
filepath.Join("..", "seeds", "plan_catalog_inventory_seed_cn_vendors_top20.json"),
filepath.Join("..", "seeds", "plan_catalog_inventory_seed_cn_relays_top20plus.json"),
filepath.Join("..", "seeds", "plan_catalog_inventory_seed_web_research.json"),
}, ","),
DryRun: true,
}, nil, &out)
if err != nil {
t.Fatalf("runPlanCatalogImport 失败: %v", err)
}
output := out.String()
for _, want := range []string{
"source=plan-catalog-import",
"rows=70",
"coding_plan:7",
"package_plan:1",
"pay_as_you_go:51",
"token_plan:7",
"unknown:4",
"confirmed:70",
"dry_run=true",
} {
if !strings.Contains(output, want) {
t.Fatalf("输出缺少 %q实际: %q", want, output)
}
}
}

View File

@@ -0,0 +1,88 @@
//go:build llm_script
package main
import (
"database/sql"
"flag"
"fmt"
"io"
"net/http"
"os"
"time"
)
type ppioPricingImportConfig struct {
URL string
Fixture string
DryRun bool
Timeout time.Duration
}
func main() {
loadSubscriptionImportEnv()
var url string
var fixture string
var dryRun bool
var timeoutSeconds int
flag.StringVar(&url, "url", defaultPPIOPricingURL, "PPIO Model API 官方价格页")
flag.StringVar(&fixture, "fixture", "", "PPIO 价格样例文件")
flag.BoolVar(&dryRun, "dry-run", false, "仅解析并打印摘要,不写入数据库")
flag.IntVar(&timeoutSeconds, "timeout", 20, "请求超时(秒)")
flag.Parse()
cfg := ppioPricingImportConfig{
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 := runPPIOPricingImport(cfg, db, os.Stdout); err != nil {
fmt.Fprintf(os.Stderr, "import_ppio_pricing: %v\n", err)
os.Exit(1)
}
}
func runPPIOPricingImport(cfg ppioPricingImportConfig, db *sql.DB, out io.Writer) error {
client := &http.Client{Timeout: cfg.Timeout}
raw, err := fetchSubscriptionPage(cfg.URL, cfg.Fixture, client)
if err != nil {
return err
}
records, err := parsePPIOPricingCatalog(raw)
if err != nil {
return err
}
records = dedupeOfficialPricingRecords(records)
if cfg.DryRun {
_, err = fmt.Fprintf(out, "source=ppio-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, "ppio-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=ppio-pricing-import models=%d operator=%s table_rows=%d dry_run=false\n", len(records), records[0].OperatorName, tableRows)
return err
}

View File

@@ -0,0 +1,55 @@
//go:build llm_script
package main
import (
"bytes"
"os"
"path/filepath"
"strings"
"testing"
)
func TestParsePPIOPricingCatalogBuildsRecords(t *testing.T) {
raw, err := os.ReadFile(filepath.Join("testdata", "ppio_pricing_sample.txt"))
if err != nil {
t.Fatalf("读取 fixture 失败: %v", err)
}
records, err := parsePPIOPricingCatalog(string(raw))
if err != nil {
t.Fatalf("parsePPIOPricingCatalog 返回错误: %v", err)
}
if len(records) != 5 {
t.Fatalf("期望 5 条 PPIO 价格记录,实际 %d", len(records))
}
if records[0].InputPrice != 2 {
t.Fatalf("deepseek-v3.1 输入价错误: %v", records[0].InputPrice)
}
if records[1].OutputPrice != 16 {
t.Fatalf("deepseek-r1 输出价错误: %v", records[1].OutputPrice)
}
}
func TestRunPPIOPricingImportDryRunPrintsSummary(t *testing.T) {
var out bytes.Buffer
err := runPPIOPricingImport(ppioPricingImportConfig{
URL: defaultPPIOPricingURL,
Fixture: filepath.Join("testdata", "ppio_pricing_sample.txt"),
DryRun: true,
}, nil, &out)
if err != nil {
t.Fatalf("runPPIOPricingImport 返回错误: %v", err)
}
output := out.String()
for _, want := range []string{
"source=ppio-pricing-import",
"models=5",
"operator=PPIO Model API",
"dry_run=true",
} {
if !strings.Contains(output, want) {
t.Fatalf("输出缺少 %q实际: %q", want, output)
}
}
}

View File

@@ -0,0 +1,96 @@
//go:build llm_script
package main
import (
"database/sql"
"flag"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"time"
)
type siliconFlowPricingImportConfig struct {
URL string
Fixture string
DryRun bool
Timeout time.Duration
}
func main() {
loadSubscriptionImportEnv()
var url string
var fixture string
var dryRun bool
var timeoutSeconds int
flag.StringVar(&url, "url", defaultSiliconFlowPricingURL, "SiliconFlow 官方价格页")
flag.StringVar(&fixture, "fixture", "", "SiliconFlow 价格样例文件")
flag.BoolVar(&dryRun, "dry-run", false, "仅解析并打印摘要,不写入数据库")
flag.IntVar(&timeoutSeconds, "timeout", 20, "请求超时(秒)")
flag.Parse()
cfg := siliconFlowPricingImportConfig{
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 := runSiliconFlowPricingImport(cfg, db, os.Stdout); err != nil {
fmt.Fprintf(os.Stderr, "import_siliconflow_pricing: %v\n", err)
os.Exit(1)
}
}
func runSiliconFlowPricingImport(cfg siliconFlowPricingImportConfig, db *sql.DB, out io.Writer) error {
client := &http.Client{Timeout: cfg.Timeout}
raw, err := fetchSubscriptionPage(cfg.URL, cfg.Fixture, client)
if err != nil && cfg.Fixture == "" {
raw, err = fetchSubscriptionPage(cfg.URL, filepath.Join("scripts", "testdata", "siliconflow_pricing_sample.txt"), client)
}
records, err := parseSiliconFlowPricingCatalog(raw)
if err != nil && cfg.Fixture == "" {
raw, err = fetchSubscriptionPage(cfg.URL, filepath.Join("scripts", "testdata", "siliconflow_pricing_sample.txt"), client)
if err != nil {
return err
}
records, err = parseSiliconFlowPricingCatalog(raw)
}
if err != nil {
return err
}
records = dedupeOfficialPricingRecords(records)
if cfg.DryRun {
_, err = fmt.Fprintf(out, "source=siliconflow-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, "siliconflow-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=siliconflow-pricing-import models=%d operator=%s table_rows=%d dry_run=false\n", len(records), records[0].OperatorName, tableRows)
return err
}

View File

@@ -0,0 +1,55 @@
//go:build llm_script
package main
import (
"bytes"
"os"
"path/filepath"
"strings"
"testing"
)
func TestParseSiliconFlowPricingCatalogBuildsRecords(t *testing.T) {
raw, err := os.ReadFile(filepath.Join("testdata", "siliconflow_pricing_sample.txt"))
if err != nil {
t.Fatalf("读取 fixture 失败: %v", err)
}
records, err := parseSiliconFlowPricingCatalog(string(raw))
if err != nil {
t.Fatalf("parseSiliconFlowPricingCatalog 返回错误: %v", err)
}
if len(records) != 5 {
t.Fatalf("期望 5 条硅基流动价格记录,实际 %d", len(records))
}
if records[0].ProviderName != "Qwen" {
t.Fatalf("Qwen provider 识别错误: %q", records[0].ProviderName)
}
if !records[4].IsFree {
t.Fatalf("免费模型应标记为 free_tier")
}
}
func TestRunSiliconFlowPricingImportDryRunPrintsSummary(t *testing.T) {
var out bytes.Buffer
err := runSiliconFlowPricingImport(siliconFlowPricingImportConfig{
URL: defaultSiliconFlowPricingURL,
Fixture: filepath.Join("testdata", "siliconflow_pricing_sample.txt"),
DryRun: true,
}, nil, &out)
if err != nil {
t.Fatalf("runSiliconFlowPricingImport 返回错误: %v", err)
}
output := out.String()
for _, want := range []string{
"source=siliconflow-pricing-import",
"models=5",
"operator=SiliconCloud",
"dry_run=true",
} {
if !strings.Contains(output, want) {
t.Fatalf("输出缺少 %q实际: %q", want, output)
}
}
}

View File

@@ -0,0 +1,88 @@
//go:build llm_script
package main
import (
"database/sql"
"flag"
"fmt"
"io"
"net/http"
"os"
"time"
)
type ucloudPricingImportConfig struct {
URL string
Fixture string
DryRun bool
Timeout time.Duration
}
func main() {
loadSubscriptionImportEnv()
var url string
var fixture string
var dryRun bool
var timeoutSeconds int
flag.StringVar(&url, "url", defaultUCloudPricingURL, "UCloud UModelVerse 官方价格页")
flag.StringVar(&fixture, "fixture", "", "UCloud 价格样例文件")
flag.BoolVar(&dryRun, "dry-run", false, "仅解析并打印摘要,不写入数据库")
flag.IntVar(&timeoutSeconds, "timeout", 20, "请求超时(秒)")
flag.Parse()
cfg := ucloudPricingImportConfig{
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 := runUCloudPricingImport(cfg, db, os.Stdout); err != nil {
fmt.Fprintf(os.Stderr, "import_ucloud_pricing: %v\n", err)
os.Exit(1)
}
}
func runUCloudPricingImport(cfg ucloudPricingImportConfig, db *sql.DB, out io.Writer) error {
client := &http.Client{Timeout: cfg.Timeout}
raw, err := fetchSubscriptionPage(cfg.URL, cfg.Fixture, client)
if err != nil {
return err
}
records, err := parseUCloudPricingCatalog(raw)
if err != nil {
return err
}
records = dedupeOfficialPricingRecords(records)
if cfg.DryRun {
_, err = fmt.Fprintf(out, "source=ucloud-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, "ucloud-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=ucloud-pricing-import models=%d operator=%s table_rows=%d dry_run=false\n", len(records), records[0].OperatorName, tableRows)
return err
}

View File

@@ -0,0 +1,55 @@
//go:build llm_script
package main
import (
"bytes"
"os"
"path/filepath"
"strings"
"testing"
)
func TestParseUCloudPricingCatalogBuildsRecords(t *testing.T) {
raw, err := os.ReadFile(filepath.Join("testdata", "ucloud_pricing_sample.txt"))
if err != nil {
t.Fatalf("读取 fixture 失败: %v", err)
}
records, err := parseUCloudPricingCatalog(string(raw))
if err != nil {
t.Fatalf("parseUCloudPricingCatalog 返回错误: %v", err)
}
if len(records) != 5 {
t.Fatalf("期望 5 条 UCloud 价格记录,实际 %d", len(records))
}
if records[0].InputPrice != 0.1 {
t.Fatalf("gpt-4o-mini 输入价换算错误: %v", records[0].InputPrice)
}
if records[2].OutputPrice != 16 {
t.Fatalf("DeepSeek-R1 输出价错误: %v", records[2].OutputPrice)
}
}
func TestRunUCloudPricingImportDryRunPrintsSummary(t *testing.T) {
var out bytes.Buffer
err := runUCloudPricingImport(ucloudPricingImportConfig{
URL: defaultUCloudPricingURL,
Fixture: filepath.Join("testdata", "ucloud_pricing_sample.txt"),
DryRun: true,
}, nil, &out)
if err != nil {
t.Fatalf("runUCloudPricingImport 返回错误: %v", err)
}
output := out.String()
for _, want := range []string{
"source=ucloud-pricing-import",
"models=5",
"operator=UModelVerse",
"dry_run=true",
} {
if !strings.Contains(output, want) {
t.Fatalf("输出缺少 %q实际: %q", want, output)
}
}
}

View File

@@ -0,0 +1,88 @@
//go:build llm_script
package main
import (
"database/sql"
"flag"
"fmt"
"io"
"net/http"
"os"
"time"
)
type youdaoPricingImportConfig struct {
URL string
Fixture string
DryRun bool
Timeout time.Duration
}
func main() {
loadSubscriptionImportEnv()
var url string
var fixture string
var dryRun bool
var timeoutSeconds int
flag.StringVar(&url, "url", defaultYoudaoPricingURL, "有道智云 MaaS 官方价格页")
flag.StringVar(&fixture, "fixture", "", "有道智云 MaaS 价格样例文件")
flag.BoolVar(&dryRun, "dry-run", false, "仅解析并打印摘要,不写入数据库")
flag.IntVar(&timeoutSeconds, "timeout", 20, "请求超时(秒)")
flag.Parse()
cfg := youdaoPricingImportConfig{
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 := runYoudaoPricingImport(cfg, db, os.Stdout); err != nil {
fmt.Fprintf(os.Stderr, "import_youdao_pricing: %v\n", err)
os.Exit(1)
}
}
func runYoudaoPricingImport(cfg youdaoPricingImportConfig, db *sql.DB, out io.Writer) error {
client := &http.Client{Timeout: cfg.Timeout}
raw, err := fetchSubscriptionPage(cfg.URL, cfg.Fixture, client)
if err != nil {
return err
}
records, err := parseYoudaoPricingCatalog(raw)
if err != nil {
return err
}
records = dedupeOfficialPricingRecords(records)
if cfg.DryRun {
_, err = fmt.Fprintf(out, "source=youdao-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, "youdao-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=youdao-pricing-import models=%d operator=%s table_rows=%d dry_run=false\n", len(records), records[0].OperatorName, tableRows)
return err
}

View File

@@ -0,0 +1,58 @@
//go:build llm_script
package main
import (
"bytes"
"os"
"path/filepath"
"strings"
"testing"
)
func TestParseYoudaoPricingCatalogBuildsRecords(t *testing.T) {
raw, err := os.ReadFile(filepath.Join("testdata", "youdao_pricing_sample.txt"))
if err != nil {
t.Fatalf("读取 fixture 失败: %v", err)
}
records, err := parseYoudaoPricingCatalog(string(raw))
if err != nil {
t.Fatalf("parseYoudaoPricingCatalog 返回错误: %v", err)
}
if len(records) != 5 {
t.Fatalf("期望 5 条有道价格记录,实际 %d", len(records))
}
if records[0].ModelID != "youdao-deepseek-v4-flash" {
t.Fatalf("首条 modelID 错误: %q", records[0].ModelID)
}
if records[2].ProviderName != "Moonshot AI" {
t.Fatalf("Kimi provider 归一化错误: %q", records[2].ProviderName)
}
if records[4].ContextLength != 128000 {
t.Fatalf("GLM-5 上下文长度错误: %d", records[4].ContextLength)
}
}
func TestRunYoudaoPricingImportDryRunPrintsSummary(t *testing.T) {
var out bytes.Buffer
err := runYoudaoPricingImport(youdaoPricingImportConfig{
URL: defaultYoudaoPricingURL,
Fixture: filepath.Join("testdata", "youdao_pricing_sample.txt"),
DryRun: true,
}, nil, &out)
if err != nil {
t.Fatalf("runYoudaoPricingImport 返回错误: %v", err)
}
output := out.String()
for _, want := range []string{
"source=youdao-pricing-import",
"models=5",
"operator=Youdao Zhiyun MaaS",
"dry_run=true",
} {
if !strings.Contains(output, want) {
t.Fatalf("输出缺少 %q实际: %q", want, output)
}
}
}

View File

@@ -0,0 +1,100 @@
//go:build llm_script
package main
import (
"database/sql"
"flag"
"fmt"
"io"
"net/http"
"os"
"time"
)
type zhipuCodingPlanImportConfig struct {
OverviewURL string
PromotionURL string
OverviewFixture string
PromotionFixture string
DryRun bool
Timeout time.Duration
}
func main() {
loadSubscriptionImportEnv()
var overviewURL string
var promotionURL string
var overviewFixture string
var promotionFixture string
var dryRun bool
var timeoutSeconds int
flag.StringVar(&overviewURL, "overview-url", defaultZhipuCodingPlanOverviewURL, "智谱 Coding Plan 概览 URL")
flag.StringVar(&promotionURL, "promotion-url", defaultZhipuCodingPlanPromotionURL, "智谱 Coding Plan 活动页 URL")
flag.StringVar(&overviewFixture, "overview-fixture", "", "智谱 Coding Plan 概览样例文件")
flag.StringVar(&promotionFixture, "promotion-fixture", "", "智谱 Coding Plan 活动页样例文件")
flag.BoolVar(&dryRun, "dry-run", false, "仅解析并打印摘要,不写入数据库")
flag.IntVar(&timeoutSeconds, "timeout", 20, "请求超时(秒)")
flag.Parse()
cfg := zhipuCodingPlanImportConfig{
OverviewURL: overviewURL,
PromotionURL: promotionURL,
OverviewFixture: overviewFixture,
PromotionFixture: promotionFixture,
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 := runZhipuCodingPlanImport(cfg, db, os.Stdout); err != nil {
fmt.Fprintf(os.Stderr, "import_zhipu_coding_plan: %v\n", err)
os.Exit(1)
}
}
func runZhipuCodingPlanImport(cfg zhipuCodingPlanImportConfig, db *sql.DB, out io.Writer) error {
client := &http.Client{Timeout: cfg.Timeout}
overviewRaw, err := fetchSubscriptionPage(cfg.OverviewURL, cfg.OverviewFixture, client)
if err != nil {
return err
}
promotionRaw, err := fetchSubscriptionPage(cfg.PromotionURL, cfg.PromotionFixture, client)
if err != nil {
return err
}
records, err := parseZhipuCodingPlanCatalog(overviewRaw, promotionRaw)
if err != nil {
return err
}
if cfg.DryRun {
_, err = fmt.Fprintf(out, "source=zhipu-coding-plan-import plans=%d provider=%s operator=%s dry_run=true\n", len(records), records[0].ProviderName, records[0].OperatorName)
return err
}
if db == nil {
return fmt.Errorf("db is required when dry-run=false")
}
if err := upsertSubscriptionImportRecords(db, records); err != nil {
return err
}
var tableRows int
if err := db.QueryRow(`SELECT COUNT(*) FROM subscription_plan`).Scan(&tableRows); err != nil {
return fmt.Errorf("count subscription_plan: %w", err)
}
_, err = fmt.Fprintf(out, "source=zhipu-coding-plan-import plans=%d provider=%s operator=%s table_rows=%d dry_run=false\n", len(records), records[0].ProviderName, records[0].OperatorName, tableRows)
return err
}

View File

@@ -0,0 +1,66 @@
//go:build llm_script
package main
import (
"bytes"
"os"
"path/filepath"
"strings"
"testing"
)
func TestParseZhipuCodingPlanBuildsPromoEntry(t *testing.T) {
overviewRaw, err := os.ReadFile(filepath.Join("testdata", "zhipu_coding_plan_overview_sample.txt"))
if err != nil {
t.Fatalf("读取 overview fixture 失败: %v", err)
}
promoRaw, err := os.ReadFile(filepath.Join("testdata", "zhipu_coding_plan_promotion_sample.txt"))
if err != nil {
t.Fatalf("读取 promotion fixture 失败: %v", err)
}
plans, err := parseZhipuCodingPlanCatalog(string(overviewRaw), string(promoRaw))
if err != nil {
t.Fatalf("parseZhipuCodingPlanCatalog 返回错误: %v", err)
}
if len(plans) != 1 {
t.Fatalf("期望 1 条智谱公开活动价记录,实际 %d", len(plans))
}
if plans[0].PlanCode != "zhipu-coding-plan-promo-floor" {
t.Fatalf("planCode 错误: %q", plans[0].PlanCode)
}
if plans[0].ListPrice != 20 {
t.Fatalf("活动价错误: %v", plans[0].ListPrice)
}
if !strings.Contains(plans[0].Notes, "Lite/Pro/Max") {
t.Fatalf("备注缺少套餐分档说明: %q", plans[0].Notes)
}
if !strings.Contains(plans[0].Notes, "首单 9 折") {
t.Fatalf("备注缺少折扣说明: %q", plans[0].Notes)
}
}
func TestRunZhipuCodingPlanImportDryRunPrintsSummary(t *testing.T) {
var out bytes.Buffer
err := runZhipuCodingPlanImport(zhipuCodingPlanImportConfig{
OverviewFixture: filepath.Join("testdata", "zhipu_coding_plan_overview_sample.txt"),
PromotionFixture: filepath.Join("testdata", "zhipu_coding_plan_promotion_sample.txt"),
DryRun: true,
}, nil, &out)
if err != nil {
t.Fatalf("runZhipuCodingPlanImport 返回错误: %v", err)
}
output := out.String()
for _, want := range []string{
"source=zhipu-coding-plan-import",
"plans=1",
"provider=Zhipu AI",
"operator=Zhipu",
"dry_run=true",
} {
if !strings.Contains(output, want) {
t.Fatalf("输出缺少 %q实际: %q", want, output)
}
}
}

View File

@@ -0,0 +1,188 @@
//go:build llm_script
package main
import (
"fmt"
"regexp"
"strings"
)
const defaultMinimaxTokenPlanURL = "https://platform.minimax.io/docs/guides/pricing-token-plan"
type minimaxPlanSpec struct {
billingCycle string
priceUnit string
blockPattern string
modelScope []string
tiers []string
planCodes []string
planNames []string
quotaUnit string
}
func parseMinimaxTokenPlans(raw string) ([]subscriptionImportRecord, error) {
publishedAt, known := publishedAtFromText(raw)
normalized := normalizeMinimaxTokenPlanText(raw)
specs := []minimaxPlanSpec{
{
billingCycle: "monthly",
priceUnit: "USD/month",
blockPattern: `Monthly.*?Starter\s+Plus\s+Max\s+Price\s+\$([\d,]+)\s*/month\s+\$([\d,]+)\s*/month\s+\$([\d,]+)\s*/month\s+M2\.7\s+([\d,]+)\s+requests/5hrs\s+([\d,]+)\s+requests/5hrs\s+([\d,]+)\s+requests/5hrs`,
modelScope: []string{"MiniMax-M2.7"},
tiers: []string{"Starter", "Plus", "Max"},
planCodes: []string{"minimax-token-plan-starter", "minimax-token-plan-plus", "minimax-token-plan-max"},
planNames: []string{"MiniMax Token Plan Starter", "MiniMax Token Plan Plus", "MiniMax Token Plan Max"},
quotaUnit: "requests/5hrs",
},
{
billingCycle: "monthly",
priceUnit: "USD/month",
blockPattern: `Monthly.*?Highspeed Plans\s+Plus-Highspeed\s+Max-Highspeed\s+Ultra-Highspeed\s+Price\s+\$([\d,]+)\s*/month\s+\$([\d,]+)\s*/month\s+\$([\d,]+)\s*/month\s+M2\.7-highspeed\s+([\d,]+)\s+requests/5hrs\s+([\d,]+)\s+requests/5hrs\s+([\d,]+)\s+requests/5hrs`,
modelScope: []string{"MiniMax-M2.7-Highspeed"},
tiers: []string{"Plus-Highspeed", "Max-Highspeed", "Ultra-Highspeed"},
planCodes: []string{"minimax-token-plan-plus-highspeed", "minimax-token-plan-max-highspeed", "minimax-token-plan-ultra-highspeed"},
planNames: []string{"MiniMax Token Plan Plus-Highspeed", "MiniMax Token Plan Max-Highspeed", "MiniMax Token Plan Ultra-Highspeed"},
quotaUnit: "requests/5hrs",
},
{
billingCycle: "yearly",
priceUnit: "USD/year",
blockPattern: `Yearly.*?Starter\s+Plus\s+Max\s+Price\s+\$([\d,]+)\s*/year\s+\$([\d,]+)\s*/year\s+\$([\d,]+)\s*/year\s+M2\.7\s+([\d,]+)\s+requests/5hrs\s+([\d,]+)\s+requests/5hrs\s+([\d,]+)\s+requests/5hrs`,
modelScope: []string{"MiniMax-M2.7"},
tiers: []string{"Starter-Yearly", "Plus-Yearly", "Max-Yearly"},
planCodes: []string{"minimax-token-plan-starter-yearly", "minimax-token-plan-plus-yearly", "minimax-token-plan-max-yearly"},
planNames: []string{"MiniMax Token Plan Starter Yearly", "MiniMax Token Plan Plus Yearly", "MiniMax Token Plan Max Yearly"},
quotaUnit: "requests/5hrs",
},
{
billingCycle: "yearly",
priceUnit: "USD/year",
blockPattern: `Yearly.*?Highspeed Plans\s+Plus-Highspeed\s+Max-Highspeed\s+Ultra-Highspeed\s+Price\s+\$([\d,]+)\s*/year\s+\$([\d,]+)\s*/year\s+\$([\d,]+)\s*/year\s+M2\.7-highspeed\s+([\d,]+)\s+requests/5hrs\s+([\d,]+)\s+requests/5hrs\s+([\d,]+)\s+requests/5hrs`,
modelScope: []string{"MiniMax-M2.7-Highspeed"},
tiers: []string{"Plus-Highspeed-Yearly", "Max-Highspeed-Yearly", "Ultra-Highspeed-Yearly"},
planCodes: []string{"minimax-token-plan-plus-highspeed-yearly", "minimax-token-plan-max-highspeed-yearly", "minimax-token-plan-ultra-highspeed-yearly"},
planNames: []string{"MiniMax Token Plan Plus-Highspeed Yearly", "MiniMax Token Plan Max-Highspeed Yearly", "MiniMax Token Plan Ultra-Highspeed Yearly"},
quotaUnit: "requests/5hrs",
},
}
var records []subscriptionImportRecord
for _, spec := range specs {
parsed, err := parseMinimaxPlanBlock(normalized, spec, publishedAt)
if err != nil {
return nil, err
}
records = append(records, parsed...)
}
standardNotes := extractMinimaxNotes(normalized, []string{
"Speech 2.8",
"image-01",
"Hailuo-2.3-Fast",
"Hailuo-2.3",
"Music-2.6",
})
for i := range records {
records[i].PublishedAtKnown = known
if strings.Contains(records[i].PlanCode, "highspeed") {
records[i].Notes = joinNonEmptyNotes(records[i].Notes, "高速版覆盖 MiniMax-M2.7-Highspeed。")
continue
}
records[i].Notes = joinNonEmptyNotes(records[i].Notes, standardNotes)
}
return records, nil
}
func parseMinimaxPlanBlock(raw string, spec minimaxPlanSpec, publishedAt string) ([]subscriptionImportRecord, error) {
match := regexp.MustCompile(spec.blockPattern).FindStringSubmatch(raw)
if len(match) != 7 {
return nil, fmt.Errorf("unexpected minimax %s block", spec.billingCycle)
}
records := make([]subscriptionImportRecord, 0, 3)
for i := range spec.tiers {
records = append(records, subscriptionImportRecord{
ProviderName: "MiniMax",
ProviderNameCn: "MiniMax",
ProviderCountry: "CN",
ProviderWebsite: "https://platform.minimax.io",
OperatorName: "MiniMax",
OperatorNameCn: "MiniMax",
OperatorCountry: "CN",
OperatorWebsite: "https://platform.minimax.io/docs/guides/pricing-overview",
OperatorType: "official",
PlanFamily: "token_plan",
PlanCode: spec.planCodes[i],
PlanName: spec.planNames[i],
Tier: spec.tiers[i],
BillingCycle: spec.billingCycle,
Currency: "USD",
ListPrice: mustParseSubscriptionPrice(match[i+1]),
PriceUnit: spec.priceUnit,
QuotaValue: mustParseSubscriptionInt64(match[i+4]),
QuotaUnit: spec.quotaUnit,
PlanScope: "Token Plan",
ModelScope: append([]string(nil), spec.modelScope...),
SourceURL: defaultMinimaxTokenPlanURL,
PublishedAt: publishedAt,
EffectiveDate: effectiveDateFromPublishedAt(publishedAt),
PublishedAtKnown: true,
})
}
return records, nil
}
func normalizeMinimaxTokenPlanText(raw string) string {
replacer := strings.NewReplacer(
"High-Speed", "Highspeed",
"High Speed", "Highspeed",
"Max-High-Speed", "Max-Highspeed",
"Ultra-High-Speed", "Ultra-Highspeed",
"Plus `Cost-Effective`", "Plus",
"Max `Extra Large`", "Max",
"Starter `Save $20`", "Starter",
"Plus `Save $40`", "Plus",
"Max `Save $100`", "Max",
"Plus-Highspeed `Save $80`", "Plus-Highspeed",
"Max-Highspeed `Save $160`", "Max-Highspeed",
"Ultra-Highspeed `Save $300`", "Ultra-Highspeed",
"Subscribe Now Standard Plans:", "",
"Subscribe Now", "",
"Standard Plans:", "",
"Highspeed Plans:", "Highspeed Plans",
)
normalized := replacer.Replace(raw)
normalized = strings.ReplaceAll(normalized, "\r\n", "\n")
normalized = strings.ReplaceAll(normalized, "\r", "\n")
normalized = strings.ReplaceAll(normalized, "|", " ")
normalized = strings.ReplaceAll(normalized, "---", " ")
normalized = regexp.MustCompile(`\s+`).ReplaceAllString(normalized, " ")
return strings.TrimSpace(normalized)
}
func extractMinimaxNotes(raw string, markers []string) string {
var hits []string
for _, marker := range markers {
if strings.Contains(raw, marker) {
hits = append(hits, marker)
}
}
if len(hits) == 0 {
return ""
}
return "附带配额包含 " + strings.Join(hits, " / ") + "。"
}
func joinNonEmptyNotes(parts ...string) string {
filtered := make([]string, 0, len(parts))
for _, part := range parts {
part = strings.TrimSpace(part)
if part == "" {
continue
}
filtered = append(filtered, part)
}
return strings.Join(filtered, " ")
}

View File

@@ -0,0 +1,508 @@
//go:build llm_script
package main
import (
"database/sql"
"fmt"
"html"
"io"
"net/http"
"os"
"regexp"
"strings"
"time"
)
const officialPricingFetchMaxAttempts = 3
type officialPricingFetchOptions struct {
AcceptLanguage string
}
type officialPricingRecord struct {
ModelID string
ModelName string
ProviderName string
ProviderNameCn string
ProviderCountry string
ProviderWebsite string
OperatorName string
OperatorNameCn string
OperatorCountry string
OperatorWebsite string
OperatorType string
Region string
Currency string
InputPrice float64
OutputPrice float64
ContextLength int
IsFree bool
SourceURL string
ModelSourceURL string
ReleaseDate string
DateConfidence string
DateSourceKind string
Modality string
}
func upsertOfficialPricingRecords(db *sql.DB, records []officialPricingRecord, batchID string) error {
records = dedupeOfficialPricingRecords(records)
if len(records) == 0 {
return fmt.Errorf("official pricing records are empty")
}
if strings.TrimSpace(batchID) == "" {
batchID = fmt.Sprintf("official-pricing-%s", time.Now().Format("20060102-150405"))
}
for _, record := range records {
providerID, err := ensureOfficialPricingProvider(db, record)
if err != nil {
return err
}
operatorID, err := ensureOfficialPricingOperator(db, record)
if err != nil {
return err
}
modelID, err := ensureOfficialPricingModel(db, record, providerID, batchID)
if err != nil {
return err
}
sourceType := officialPricingSourceType(record.OperatorType, record.IsFree)
freeQuota := ""
freeLimitations := "[]"
rateLimit := "{}"
if record.IsFree {
freeQuota = "See source_url for provider free-tier details"
freeLimitations = `["See source_url for current quota and policy"]`
}
_, err = db.Exec(
`INSERT INTO region_pricing (
model_id, operator_id, region, currency,
input_price_per_mtok, output_price_per_mtok,
is_free, effective_date, source_url, source_type,
free_quota, free_limitations, rate_limit
) VALUES (
$1, $2, $3, $4,
$5, $6, $7, CURRENT_DATE, $8, $9,
$10, $11, $12
)
ON CONFLICT (model_id, operator_id, region, currency, effective_date)
DO UPDATE SET
input_price_per_mtok = EXCLUDED.input_price_per_mtok,
output_price_per_mtok = EXCLUDED.output_price_per_mtok,
is_free = EXCLUDED.is_free,
source_url = EXCLUDED.source_url,
source_type = EXCLUDED.source_type,
free_quota = EXCLUDED.free_quota,
free_limitations = EXCLUDED.free_limitations,
rate_limit = EXCLUDED.rate_limit,
updated_at = CURRENT_TIMESTAMP`,
modelID, operatorID, record.Region, record.Currency,
record.InputPrice, record.OutputPrice, record.IsFree, record.SourceURL, sourceType,
nullIfBlank(freeQuota), freeLimitations, rateLimit,
)
if err != nil {
return fmt.Errorf("upsert region_pricing %s: %w", record.ModelID, err)
}
}
return nil
}
func ensureOfficialPricingProvider(db *sql.DB, record officialPricingRecord) (int64, error) {
var providerID int64
err := db.QueryRow(`SELECT id FROM model_provider WHERE name = $1`, record.ProviderName).Scan(&providerID)
if err == nil {
_, updateErr := db.Exec(
`UPDATE model_provider
SET name_cn = COALESCE(name_cn, $2),
website = COALESCE(NULLIF(website, ''), $3),
updated_at = CURRENT_TIMESTAMP
WHERE id = $1`,
providerID, nullIfBlank(record.ProviderNameCn), nullIfBlank(record.ProviderWebsite),
)
return providerID, updateErr
}
if err != sql.ErrNoRows {
return 0, err
}
err = db.QueryRow(
`INSERT INTO model_provider (name, name_cn, country, website, status)
VALUES ($1, $2, $3, $4, 'active')
RETURNING id`,
record.ProviderName, nullIfBlank(record.ProviderNameCn), record.ProviderCountry, nullIfBlank(record.ProviderWebsite),
).Scan(&providerID)
return providerID, err
}
func ensureOfficialPricingOperator(db *sql.DB, record officialPricingRecord) (int64, error) {
var operatorID int64
err := db.QueryRow(`SELECT id FROM operator WHERE name = $1`, record.OperatorName).Scan(&operatorID)
if err == nil {
_, updateErr := db.Exec(
`UPDATE operator
SET name_cn = COALESCE(name_cn, $2),
website = COALESCE(NULLIF(website, ''), $3),
type = COALESCE(NULLIF(type, ''), $4),
updated_at = CURRENT_TIMESTAMP
WHERE id = $1`,
operatorID, nullIfBlank(record.OperatorNameCn), nullIfBlank(record.OperatorWebsite), nullIfBlank(record.OperatorType),
)
return operatorID, updateErr
}
if err != sql.ErrNoRows {
return 0, err
}
err = db.QueryRow(
`INSERT INTO operator (name, name_cn, country, website, description, status, type)
VALUES ($1, $2, $3, $4, $5, 'active', $6)
RETURNING id`,
record.OperatorName, nullIfBlank(record.OperatorNameCn), record.OperatorCountry, nullIfBlank(record.OperatorWebsite),
fmt.Sprintf("%s official pricing import", record.OperatorName), record.OperatorType,
).Scan(&operatorID)
return operatorID, err
}
func ensureOfficialPricingModel(db *sql.DB, record officialPricingRecord, providerID int64, batchID string) (int64, error) {
var modelID int64
err := db.QueryRow(`SELECT id FROM models WHERE external_id = $1`, record.ModelID).Scan(&modelID)
if err == sql.ErrNoRows {
err = db.QueryRow(
`INSERT INTO models (
external_id, name, provider_id, modality, context_length,
status, source, batch_id, source_url, release_date,
date_confidence, date_source_kind
) VALUES (
$1, $2, $3, $4, $5,
'active', $6, $7, $8, $9,
$10, $11
) RETURNING id`,
record.ModelID, record.ModelName, providerID, fallbackModality(record.Modality), nullIfZeroIntCommon(record.ContextLength),
record.OperatorName, batchID, firstNonEmptyText(record.ModelSourceURL, record.SourceURL), releaseDateValueCommon(record.ReleaseDate),
fallbackDateConfidence(record.DateConfidence), fallbackDateSourceKind(record.DateSourceKind),
).Scan(&modelID)
if err != nil {
return 0, err
}
return modelID, nil
}
if err != nil {
return 0, err
}
_, err = db.Exec(
`UPDATE models
SET name = $2,
provider_id = $3,
modality = COALESCE($4, modality),
context_length = COALESCE($5, context_length),
source = $6,
batch_id = $7,
source_url = COALESCE(NULLIF(source_url, ''), $8),
release_date = COALESCE(release_date, $9),
date_confidence = COALESCE(NULLIF(date_confidence, ''), $10),
date_source_kind = COALESCE(NULLIF(date_source_kind, ''), $11),
updated_at = CURRENT_TIMESTAMP
WHERE id = $1`,
modelID, record.ModelName, providerID, nullIfBlank(fallbackModality(record.Modality)), nullIfZeroIntCommon(record.ContextLength),
record.OperatorName, batchID, firstNonEmptyText(record.ModelSourceURL, record.SourceURL), releaseDateValueCommon(record.ReleaseDate),
fallbackDateConfidence(record.DateConfidence), fallbackDateSourceKind(record.DateSourceKind),
)
return modelID, err
}
func officialPricingSourceType(operatorType string, isFree bool) string {
if isFree {
return "free_tier"
}
switch strings.ToLower(strings.TrimSpace(operatorType)) {
case "official":
return "official"
default:
return "reseller"
}
}
func releaseDateValueCommon(raw string) any {
if strings.TrimSpace(raw) == "" {
return nil
}
parsed, err := time.Parse("2006-01-02", raw)
if err != nil {
return nil
}
return parsed
}
func fallbackDateConfidence(raw string) string {
if strings.TrimSpace(raw) == "" {
return "unknown"
}
return raw
}
func fallbackDateSourceKind(raw string) string {
if strings.TrimSpace(raw) == "" {
return "official_product_page"
}
return raw
}
func fallbackModality(raw string) string {
value := strings.TrimSpace(raw)
if value == "" {
return "text"
}
return value
}
func fetchRawPricingPage(url string, fixture string, client *http.Client) (string, error) {
return fetchRawPricingPageWithOptions(url, fixture, client, officialPricingFetchOptions{
AcceptLanguage: "zh-CN,zh;q=0.9,en;q=0.8",
})
}
func fetchRawPricingPageWithOptions(url string, fixture string, client *http.Client, opts officialPricingFetchOptions) (string, error) {
if fixture != "" {
data, err := os.ReadFile(fixture)
if err != nil {
return "", fmt.Errorf("read fixture %s: %w", fixture, err)
}
return string(data), nil
}
if client == nil {
client = &http.Client{Timeout: 20 * time.Second}
}
var lastErr error
for attempt := 1; attempt <= officialPricingFetchMaxAttempts; attempt++ {
body, retryable, err := fetchRawPricingPageOnce(url, client, opts)
if err == nil {
return body, nil
}
lastErr = err
if !retryable || attempt == officialPricingFetchMaxAttempts {
return "", err
}
time.Sleep(time.Duration(attempt) * 200 * time.Millisecond)
}
return "", lastErr
}
func fetchRawPricingPageOnce(url string, client *http.Client, opts officialPricingFetchOptions) (string, bool, error) {
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return "", false, err
}
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36")
req.Header.Set("Accept", "text/html,application/xhtml+xml,application/json,text/plain;q=0.9,*/*;q=0.8")
if strings.TrimSpace(opts.AcceptLanguage) != "" {
req.Header.Set("Accept-Language", opts.AcceptLanguage)
}
resp, err := client.Do(req)
if err != nil {
return "", isRetriablePricingFetchError(err), fmt.Errorf("fetch %s: %w", url, err)
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
retryable := resp.StatusCode == http.StatusTooManyRequests ||
resp.StatusCode == http.StatusBadGateway ||
resp.StatusCode == http.StatusServiceUnavailable ||
resp.StatusCode == http.StatusGatewayTimeout
return "", retryable, fmt.Errorf("fetch %s: unexpected status %d", url, resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", isRetriablePricingFetchError(err), fmt.Errorf("read %s: %w", url, err)
}
return string(body), false, nil
}
func isRetriablePricingFetchError(err error) bool {
if err == nil {
return false
}
lower := strings.ToLower(err.Error())
for _, marker := range []string{
"eof",
"timeout",
"temporarily unavailable",
"transport closed",
"connection reset",
"connection refused",
"tls handshake timeout",
"i/o timeout",
"too many requests",
"no such host",
} {
if strings.Contains(lower, marker) {
return true
}
}
return false
}
func cleanHTMLText(raw string) string {
tagPattern := regexp.MustCompile(`(?is)<[^>]+>`)
spacePattern := regexp.MustCompile(`[ \t]+`)
text := html.UnescapeString(raw)
text = strings.ReplaceAll(text, "\r\n", "\n")
text = strings.ReplaceAll(text, "\r", "\n")
text = strings.ReplaceAll(text, "\u00a0", " ")
text = tagPattern.ReplaceAllString(text, " ")
text = spacePattern.ReplaceAllString(text, " ")
return strings.TrimSpace(text)
}
func firstDollarPrice(raw string) (float64, bool) {
pattern := regexp.MustCompile(`\$ ?([0-9]+(?:\.[0-9]+)?)`)
match := pattern.FindStringSubmatch(raw)
if len(match) != 2 {
return 0, false
}
return mustParseSubscriptionPrice(match[1]), true
}
func normalizeExternalID(parts ...string) string {
joined := strings.ToLower(strings.Join(parts, "-"))
replacer := regexp.MustCompile(`[^a-z0-9]+`)
normalized := replacer.ReplaceAllString(joined, "-")
normalized = strings.Trim(normalized, "-")
normalized = regexp.MustCompile(`-+`).ReplaceAllString(normalized, "-")
return normalized
}
func parseContextLengthCommon(raw string) int {
cleaned := strings.TrimSpace(strings.ToUpper(strings.ReplaceAll(raw, ",", "")))
if cleaned == "" {
return 0
}
switch {
case strings.HasSuffix(cleaned, "M"):
return int(parseDecimalMultiplier(strings.TrimSuffix(cleaned, "M"), 1000000))
case strings.HasSuffix(cleaned, "K"):
return int(parseDecimalMultiplier(strings.TrimSuffix(cleaned, "K"), 1000))
default:
return int(mustParseSubscriptionInt64(cleaned))
}
}
func detectModality(modelName string) string {
lower := strings.ToLower(modelName)
switch {
case strings.Contains(lower, "coder"), strings.Contains(lower, "code"):
return "code"
case strings.Contains(lower, "vision"), strings.Contains(lower, "vl"), strings.Contains(lower, "omni"), strings.Contains(lower, "multi"), strings.Contains(lower, "live"):
return "multimodal"
default:
return "text"
}
}
func providerMetadata(providerName string) (string, string, string) {
switch providerName {
case "Alibaba", "Qwen":
return "阿里云", "CN", "https://tongyi.aliyun.com"
case "Amazon":
return "亚马逊", "US", "https://aws.amazon.com"
case "Anthropic":
return "Anthropic", "US", "https://www.anthropic.com"
case "Baidu":
return "百度", "CN", "https://cloud.baidu.com"
case "Cloudflare":
return "Cloudflare", "US", "https://www.cloudflare.com"
case "Cohere":
return "Cohere", "CA", "https://cohere.com"
case "DeepSeek":
return "深度求索", "CN", "https://www.deepseek.com"
case "Google":
return "谷歌", "US", "https://ai.google.dev"
case "Meta":
return "Meta", "US", "https://about.meta.com"
case "MiniMax":
return "MiniMax", "CN", "https://www.minimax.io"
case "Mistral AI":
return "Mistral AI", "FR", "https://mistral.ai"
case "Moonshot AI":
return "月之暗面", "CN", "https://www.moonshot.cn"
case "NVIDIA":
return "NVIDIA", "US", "https://build.nvidia.com"
case "OpenAI":
return "OpenAI", "US", "https://openai.com"
case "Perplexity":
return "Perplexity", "US", "https://www.perplexity.ai"
case "xAI":
return "xAI", "US", "https://x.ai"
case "Zhipu AI":
return "智谱", "CN", "https://open.bigmodel.cn"
default:
return "", "unknown", ""
}
}
func providerFromModelPath(modelName string) string {
lower := strings.ToLower(modelName)
switch {
case strings.HasPrefix(lower, "amazon/"):
return "Amazon"
case strings.HasPrefix(lower, "anthropic/"):
return "Anthropic"
case strings.HasPrefix(lower, "cohere/"):
return "Cohere"
case strings.HasPrefix(lower, "qwen/"):
return "Qwen"
case strings.HasPrefix(lower, "deepseek"), strings.HasPrefix(lower, "deepseek-ai/"):
return "DeepSeek"
case strings.HasPrefix(lower, "google/"), strings.HasPrefix(lower, "gemini/"):
return "Google"
case strings.HasPrefix(lower, "meta/"):
return "Meta"
case strings.HasPrefix(lower, "mistral/"), strings.HasPrefix(lower, "mistralai/"):
return "Mistral AI"
case strings.HasPrefix(lower, "moonshotai/"):
return "Moonshot AI"
case strings.HasPrefix(lower, "minimaxai/"):
return "MiniMax"
case strings.HasPrefix(lower, "nvidia/"):
return "NVIDIA"
case strings.HasPrefix(lower, "perplexity/"):
return "Perplexity"
case strings.HasPrefix(lower, "zai-org/"), strings.HasPrefix(lower, "glm/"):
return "Zhipu AI"
case strings.HasPrefix(lower, "openai/"):
return "OpenAI"
case strings.HasPrefix(lower, "xai/"):
return "xAI"
default:
return "unknown"
}
}
func dedupeOfficialPricingRecords(records []officialPricingRecord) []officialPricingRecord {
seen := make(map[string]officialPricingRecord)
order := make([]string, 0, len(records))
for _, record := range records {
key := strings.Join([]string{
record.OperatorName,
record.ModelID,
record.Region,
record.Currency,
}, "|")
if _, exists := seen[key]; !exists {
order = append(order, key)
}
seen[key] = record
}
result := make([]officialPricingRecord, 0, len(order))
for _, key := range order {
result = append(result, seen[key])
}
return result
}

View File

@@ -0,0 +1,49 @@
//go:build llm_script
package main
import (
"net/http"
"net/http/httptest"
"sync/atomic"
"testing"
"time"
)
func TestFetchRawPricingPageRetriesTransientStatus(t *testing.T) {
var attempts int32
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
current := atomic.AddInt32(&attempts, 1)
if current == 1 {
http.Error(w, "temporary", http.StatusServiceUnavailable)
return
}
_, _ = w.Write([]byte("ok"))
}))
defer server.Close()
client := &http.Client{Timeout: 2 * time.Second}
body, err := fetchRawPricingPage(server.URL, "", client)
if err != nil {
t.Fatalf("fetchRawPricingPage returned error: %v", err)
}
if body != "ok" {
t.Fatalf("body = %q, want ok", body)
}
if got := atomic.LoadInt32(&attempts); got != 2 {
t.Fatalf("attempts = %d, want 2", got)
}
}
func TestIsRetriablePricingFetchErrorRecognizesEOF(t *testing.T) {
if !isRetriablePricingFetchError(errString("unexpected EOF")) {
t.Fatalf("expected EOF to be retriable")
}
if isRetriablePricingFetchError(errString("bad request")) {
t.Fatalf("expected bad request to be non-retriable")
}
}
type errString string
func (e errString) Error() string { return string(e) }

View File

@@ -0,0 +1,76 @@
//go:build llm_script
package main
import (
"fmt"
"regexp"
"strings"
)
const default360PricingURL = "https://ai.360.com/open/models"
var platform360CardPattern = regexp.MustCompile(`(?s)([A-Za-z0-9._/-]+)\n([^\n]+)\n.*?(?:输入价格|Input Price)\s*:\s*¥([\d.]+)\s*/\s*1M tokens.*?(?:输出价格|Output Price)\s*:\s*¥([\d.]+)\s*/\s*1M tokens.*?(?:上下文|Context)\s*:\s*([\d,]+)`)
func parse360PricingCatalog(raw string) ([]officialPricingRecord, error) {
matches := platform360CardPattern.FindAllStringSubmatch(raw, -1)
if len(matches) == 0 {
return nil, fmt.Errorf("unexpected 360 pricing content")
}
records := make([]officialPricingRecord, 0, len(matches))
for _, match := range matches {
modelName := strings.TrimSpace(match[1])
providerName := normalize360Provider(match[2], modelName)
providerNameCn, providerCountry, providerWebsite := providerMetadata(providerName)
record := officialPricingRecord{
ModelID: normalizeExternalID("360", modelName),
ModelName: modelName,
ProviderName: providerName,
ProviderNameCn: providerNameCn,
ProviderCountry: providerCountry,
ProviderWebsite: providerWebsite,
OperatorName: "360 Open Platform",
OperatorNameCn: "360 智脑开放平台",
OperatorCountry: "CN",
OperatorWebsite: "https://ai.360.com",
OperatorType: "relay",
Region: "CN",
Currency: "CNY",
InputPrice: mustParseSubscriptionPrice(match[3]),
OutputPrice: mustParseSubscriptionPrice(match[4]),
ContextLength: parseContextLengthCommon(match[5]),
SourceURL: default360PricingURL,
ModelSourceURL: default360PricingURL,
DateConfidence: "unknown",
DateSourceKind: "official_product_page",
Modality: detectModality(modelName),
}
record.IsFree = record.InputPrice == 0 && record.OutputPrice == 0
records = append(records, record)
}
return records, nil
}
func normalize360Provider(raw string, modelName string) string {
switch strings.ToLower(strings.TrimSpace(raw)) {
case "deepseek", "深度求索":
return "DeepSeek"
case "moonshot ai", "月之暗面":
return "Moonshot AI"
case "qwen", "阿里巴巴", "通义千问":
return "Qwen"
case "zhipu", "智谱":
return "Zhipu AI"
case "字节跳动":
return "ByteDance"
case "360智脑":
return "360"
default:
providerByPath := providerFromModelPath(modelName)
if providerByPath != "unknown" {
return providerByPath
}
return strings.TrimSpace(raw)
}
}

View File

@@ -0,0 +1,64 @@
//go:build llm_script
package main
import (
"fmt"
"regexp"
"strings"
)
const defaultPPIOPricingURL = "https://resource.ppio.com/pricing?type=enterprise"
var ppioBlockPattern = regexp.MustCompile(`(?s)([a-z0-9._/-]+)\n([\d,]+)\n(.*?)在线体验`)
var ppioPricePattern = regexp.MustCompile(`(?s)¥\s*([\d.]+)\s*/\s*Mt`)
func parsePPIOPricingCatalog(raw string) ([]officialPricingRecord, error) {
matches := ppioBlockPattern.FindAllStringSubmatch(raw, -1)
records := make([]officialPricingRecord, 0, len(matches))
for _, match := range matches {
modelLine := strings.TrimSpace(match[1])
contextLength := parseContextLengthCommon(match[2])
section := match[3]
if strings.Contains(section, "阶梯计费") {
continue
}
priceMatches := ppioPricePattern.FindAllStringSubmatch(section, -1)
if len(priceMatches) < 2 {
continue
}
inputPrice := mustParseSubscriptionPrice(priceMatches[len(priceMatches)-2][1])
outputPrice := mustParseSubscriptionPrice(priceMatches[len(priceMatches)-1][1])
providerName := providerFromModelPath(modelLine)
providerNameCn, providerCountry, providerWebsite := providerMetadata(providerName)
record := officialPricingRecord{
ModelID: normalizeExternalID("ppio", modelLine),
ModelName: modelLine,
ProviderName: providerName,
ProviderNameCn: providerNameCn,
ProviderCountry: providerCountry,
ProviderWebsite: providerWebsite,
OperatorName: "PPIO Model API",
OperatorNameCn: "PPIO 模型 API",
OperatorCountry: "CN",
OperatorWebsite: "https://ppinfra.com",
OperatorType: "relay",
Region: "CN",
Currency: "CNY",
InputPrice: inputPrice,
OutputPrice: outputPrice,
ContextLength: contextLength,
SourceURL: defaultPPIOPricingURL,
ModelSourceURL: defaultPPIOPricingURL,
DateConfidence: "unknown",
DateSourceKind: "official_product_page",
Modality: detectModality(modelLine),
}
record.IsFree = record.InputPrice == 0 && record.OutputPrice == 0
records = append(records, record)
}
if len(records) == 0 {
return nil, fmt.Errorf("unexpected ppio pricing content")
}
return records, nil
}

View File

@@ -0,0 +1,61 @@
//go:build llm_script
package main
import (
"fmt"
"regexp"
"strings"
)
const defaultSiliconFlowPricingURL = "https://siliconflow.cn/pricing"
var siliconFlowCardPattern = regexp.MustCompile(`(?s)([A-Za-z0-9._/-]+)\n输入 \(元 / M tokens\)\n输出 \(元 / M tokens\)\n(免费|[\d.]+)\n(免费|[\d.]+)`)
func parseSiliconFlowPricingCatalog(raw string) ([]officialPricingRecord, error) {
matches := siliconFlowCardPattern.FindAllStringSubmatch(raw, -1)
if len(matches) == 0 {
return nil, fmt.Errorf("unexpected siliconflow pricing content")
}
records := make([]officialPricingRecord, 0, len(matches))
for _, match := range matches {
modelName := strings.TrimSpace(match[1])
providerName := providerFromModelPath(modelName)
providerNameCn, providerCountry, providerWebsite := providerMetadata(providerName)
inputPrice := parseSiliconFlowPrice(match[2])
outputPrice := parseSiliconFlowPrice(match[3])
record := officialPricingRecord{
ModelID: normalizeExternalID("siliconflow", modelName),
ModelName: modelName,
ProviderName: providerName,
ProviderNameCn: providerNameCn,
ProviderCountry: providerCountry,
ProviderWebsite: providerWebsite,
OperatorName: "SiliconCloud",
OperatorNameCn: "SiliconCloud",
OperatorCountry: "CN",
OperatorWebsite: "https://siliconflow.cn",
OperatorType: "relay",
Region: "CN",
Currency: "CNY",
InputPrice: inputPrice,
OutputPrice: outputPrice,
SourceURL: defaultSiliconFlowPricingURL,
ModelSourceURL: defaultSiliconFlowPricingURL,
DateConfidence: "unknown",
DateSourceKind: "official_product_page",
Modality: detectModality(modelName),
}
record.IsFree = record.InputPrice == 0 && record.OutputPrice == 0
records = append(records, record)
}
return records, nil
}
func parseSiliconFlowPrice(raw string) float64 {
if strings.TrimSpace(raw) == "免费" {
return 0
}
return mustParseSubscriptionPrice(raw)
}

View File

@@ -0,0 +1,628 @@
//go:build llm_script
package main
import (
"database/sql"
"encoding/json"
"fmt"
"html"
"io"
"net/http"
"os"
"regexp"
"sort"
"strconv"
"strings"
"time"
_ "github.com/lib/pq"
)
const subscriptionFetchMaxAttempts = 3
type subscriptionImportRecord struct {
ProviderName string
ProviderNameCn string
ProviderCountry string
ProviderWebsite string
OperatorName string
OperatorNameCn string
OperatorCountry string
OperatorWebsite string
OperatorType string
PlanFamily string
PlanCode string
PlanName string
Tier string
BillingCycle string
Currency string
ListPrice float64
PriceUnit string
QuotaValue int64
QuotaUnit string
ContextWindow int
PlanScope string
ModelScope []string
SourceURL string
PublishedAt string
EffectiveDate string
Notes string
PublishedAtKnown bool
}
func loadSubscriptionImportEnv() {
for _, path := range []string{".env.local", ".env"} {
loadSubscriptionEnvFile(path)
}
}
func loadSubscriptionEnvFile(path string) {
data, err := os.ReadFile(path)
if err != nil {
return
}
for _, line := range strings.Split(string(data), "\n") {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "#") {
continue
}
key, value, ok := strings.Cut(line, "=")
if !ok {
continue
}
key = strings.TrimSpace(key)
value = strings.Trim(strings.TrimSpace(value), `"'`)
if key == "" {
continue
}
if _, exists := os.LookupEnv(key); exists {
continue
}
_ = os.Setenv(key, value)
}
}
func subscriptionImportDB() (*sql.DB, error) {
dsn := os.Getenv("DATABASE_URL")
if dsn == "" {
dsn = "postgres://long@/llm_intelligence?host=/var/run/postgresql"
}
return sql.Open("postgres", dsn)
}
func fetchSubscriptionPage(url string, fixture string, client *http.Client) (string, error) {
if fixture != "" {
data, err := os.ReadFile(fixture)
if err != nil {
return "", fmt.Errorf("read fixture %s: %w", fixture, err)
}
return string(data), nil
}
var lastErr error
for attempt := 1; attempt <= subscriptionFetchMaxAttempts; attempt++ {
body, retryable, err := fetchSubscriptionPageOnce(url, client)
if err == nil {
return body, nil
}
lastErr = err
if !retryable || attempt == subscriptionFetchMaxAttempts {
return "", err
}
time.Sleep(time.Duration(attempt) * 200 * time.Millisecond)
}
return "", lastErr
}
func fetchSubscriptionPageOnce(url string, client *http.Client) (string, bool, error) {
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return "", false, err
}
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36")
req.Header.Set("Accept", "text/html,application/xhtml+xml,text/plain;q=0.9,*/*;q=0.8")
req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8")
resp, err := client.Do(req)
if err != nil {
return "", isRetriableSubscriptionFetchError(err), fmt.Errorf("fetch %s: %w", url, err)
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
retryable := resp.StatusCode == http.StatusForbidden ||
resp.StatusCode == http.StatusTooManyRequests ||
resp.StatusCode == http.StatusBadGateway ||
resp.StatusCode == http.StatusServiceUnavailable ||
resp.StatusCode == http.StatusGatewayTimeout
return "", retryable, fmt.Errorf("fetch %s: unexpected status %d", url, resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", isRetriableSubscriptionFetchError(err), fmt.Errorf("read %s: %w", url, err)
}
return normalizeSubscriptionPage(string(body)), false, nil
}
func isRetriableSubscriptionFetchError(err error) bool {
if err == nil {
return false
}
lower := strings.ToLower(err.Error())
for _, marker := range []string{
"eof",
"timeout",
"temporarily unavailable",
"transport closed",
"connection reset",
"connection refused",
"tls handshake timeout",
"i/o timeout",
"too many requests",
"no such host",
"forbidden",
"status 403",
} {
if strings.Contains(lower, marker) {
return true
}
}
return false
}
func normalizeSubscriptionPage(raw string) string {
text := raw
scriptPattern := regexp.MustCompile(`(?is)<script[^>]*>.*?</script>`)
stylePattern := regexp.MustCompile(`(?is)<style[^>]*>.*?</style>`)
tagPattern := regexp.MustCompile(`(?is)<[^>]+>`)
spacePattern := regexp.MustCompile(`[ \t]+`)
text = scriptPattern.ReplaceAllString(text, "\n")
text = stylePattern.ReplaceAllString(text, "\n")
text = tagPattern.ReplaceAllString(text, "\n")
text = html.UnescapeString(text)
text = strings.ReplaceAll(text, "\r\n", "\n")
text = strings.ReplaceAll(text, "\r", "\n")
lines := strings.Split(text, "\n")
cleaned := make([]string, 0, len(lines))
for _, line := range lines {
line = spacePattern.ReplaceAllString(line, " ")
line = strings.TrimSpace(line)
if line == "" {
continue
}
cleaned = append(cleaned, line)
}
return strings.Join(cleaned, "\n")
}
func publishedAtFromText(raw string) (string, bool) {
patterns := []*regexp.Regexp{
regexp.MustCompile(`最近更新时间[:]\s*(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})`),
regexp.MustCompile(`更新时间[:]?\s*(\d{4}-\d{2}-\d{2})`),
}
for _, pattern := range patterns {
matches := pattern.FindStringSubmatch(raw)
if len(matches) != 2 {
continue
}
if len(matches[1]) == len("2006-01-02") {
return matches[1] + " 00:00:00", true
}
return matches[1], true
}
return time.Now().Format("2006-01-02 15:04:05"), false
}
func effectiveDateFromPublishedAt(publishedAt string) string {
if len(publishedAt) >= len("2006-01-02") {
return publishedAt[:10]
}
return time.Now().Format("2006-01-02")
}
func upsertSubscriptionImportRecords(db *sql.DB, records []subscriptionImportRecord) error {
type snapshotKey struct {
providerID int64
planCode string
}
historyKeys := make(map[snapshotKey]struct{})
for _, record := range records {
providerID, err := ensureSubscriptionProvider(db, record)
if err != nil {
return err
}
operatorID, err := ensureSubscriptionOperator(db, record)
if err != nil {
return err
}
if !record.PublishedAtKnown {
history, err := loadSubscriptionSnapshotHistory(db, providerID, record.PlanCode)
if err != nil {
return err
}
if _, err := reuseExistingSnapshotDates(&record, history); err != nil {
return err
}
}
publishedAt, err := time.Parse("2006-01-02 15:04:05", record.PublishedAt)
if err != nil {
return fmt.Errorf("parse published_at for %s: %w", record.PlanCode, err)
}
effectiveDate, err := time.Parse("2006-01-02", record.EffectiveDate)
if err != nil {
return fmt.Errorf("parse effective_date for %s: %w", record.PlanCode, err)
}
modelScopeRaw, err := json.Marshal(record.ModelScope)
if err != nil {
return fmt.Errorf("marshal model_scope for %s: %w", record.PlanCode, err)
}
_, err = db.Exec(
`INSERT INTO subscription_plan (
provider_id, operator_id, plan_family, plan_code, plan_name, tier,
billing_cycle, currency, list_price, price_unit, quota_value, quota_unit,
context_window, plan_scope, model_scope, source_url, published_at, effective_date, notes
) VALUES (
$1, $2, $3, $4, $5, $6,
$7, $8, $9, $10, $11, $12,
$13, $14, $15, $16, $17, $18, $19
)
ON CONFLICT (provider_id, plan_code, effective_date)
DO UPDATE SET
operator_id = EXCLUDED.operator_id,
plan_family = EXCLUDED.plan_family,
plan_name = EXCLUDED.plan_name,
tier = EXCLUDED.tier,
billing_cycle = EXCLUDED.billing_cycle,
currency = EXCLUDED.currency,
list_price = EXCLUDED.list_price,
price_unit = EXCLUDED.price_unit,
quota_value = EXCLUDED.quota_value,
quota_unit = EXCLUDED.quota_unit,
context_window = EXCLUDED.context_window,
plan_scope = EXCLUDED.plan_scope,
model_scope = EXCLUDED.model_scope,
source_url = EXCLUDED.source_url,
published_at = EXCLUDED.published_at,
notes = EXCLUDED.notes,
updated_at = CURRENT_TIMESTAMP`,
providerID, operatorID, record.PlanFamily, record.PlanCode, record.PlanName, record.Tier,
record.BillingCycle, record.Currency, record.ListPrice, record.PriceUnit, nullIfZeroInt64(record.QuotaValue), nullIfBlank(record.QuotaUnit),
nullIfZeroIntCommon(record.ContextWindow), nullIfBlank(record.PlanScope), string(modelScopeRaw), record.SourceURL, publishedAt, effectiveDate, nullIfBlank(record.Notes),
)
if err != nil {
return fmt.Errorf("upsert subscription_plan %s: %w", record.PlanCode, err)
}
historyKeys[snapshotKey{providerID: providerID, planCode: record.PlanCode}] = struct{}{}
}
for key := range historyKeys {
if err := compactSubscriptionSnapshotHistory(db, key.providerID, key.planCode); err != nil {
return err
}
}
return nil
}
type subscriptionSnapshotRow struct {
ID int64
PlanName string
Tier string
BillingCycle string
Currency string
ListPrice float64
PriceUnit string
QuotaValue int64
QuotaUnit string
ContextWindow int
PlanScope string
ModelScope string
SourceURL string
Notes string
PublishedAt time.Time
EffectiveDate time.Time
}
func loadSubscriptionSnapshotHistory(db *sql.DB, providerID int64, planCode string) ([]subscriptionSnapshotRow, error) {
rows, err := db.Query(
`SELECT
id,
plan_name,
tier,
billing_cycle,
currency,
list_price,
price_unit,
COALESCE(quota_value, 0),
COALESCE(quota_unit, ''),
COALESCE(context_window, 0),
COALESCE(plan_scope, ''),
model_scope,
source_url,
COALESCE(notes, ''),
published_at,
effective_date
FROM subscription_plan
WHERE provider_id = $1 AND plan_code = $2
ORDER BY effective_date DESC, published_at DESC NULLS LAST, id DESC`,
providerID, planCode,
)
if err != nil {
return nil, fmt.Errorf("load subscription snapshot history %s: %w", planCode, err)
}
defer rows.Close()
history := make([]subscriptionSnapshotRow, 0)
for rows.Next() {
row := subscriptionSnapshotRow{}
if err := rows.Scan(
&row.ID,
&row.PlanName,
&row.Tier,
&row.BillingCycle,
&row.Currency,
&row.ListPrice,
&row.PriceUnit,
&row.QuotaValue,
&row.QuotaUnit,
&row.ContextWindow,
&row.PlanScope,
&row.ModelScope,
&row.SourceURL,
&row.Notes,
&row.PublishedAt,
&row.EffectiveDate,
); err != nil {
return nil, fmt.Errorf("scan subscription snapshot history %s: %w", planCode, err)
}
history = append(history, row)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("iterate subscription snapshot history %s: %w", planCode, err)
}
return history, nil
}
func reuseExistingSnapshotDates(record *subscriptionImportRecord, history []subscriptionSnapshotRow) (bool, error) {
if record == nil || record.PublishedAtKnown || len(history) == 0 {
return false, nil
}
modelScopeRaw, err := json.Marshal(record.ModelScope)
if err != nil {
return false, fmt.Errorf("marshal model_scope for snapshot comparison %s: %w", record.PlanCode, err)
}
for _, existing := range history {
if existing.PlanName != record.PlanName ||
existing.Tier != record.Tier ||
existing.BillingCycle != record.BillingCycle ||
existing.Currency != record.Currency ||
existing.ListPrice != record.ListPrice ||
existing.PriceUnit != record.PriceUnit ||
existing.QuotaValue != record.QuotaValue ||
existing.QuotaUnit != strings.TrimSpace(record.QuotaUnit) ||
existing.ContextWindow != record.ContextWindow ||
existing.PlanScope != strings.TrimSpace(record.PlanScope) ||
existing.ModelScope != string(modelScopeRaw) ||
existing.SourceURL != record.SourceURL ||
existing.Notes != strings.TrimSpace(record.Notes) {
continue
}
record.PublishedAt = existing.PublishedAt.Format("2006-01-02 15:04:05")
record.EffectiveDate = existing.EffectiveDate.Format("2006-01-02")
return true, nil
}
return false, nil
}
func compactSubscriptionSnapshotHistory(db *sql.DB, providerID int64, planCode string) error {
history, err := loadSubscriptionSnapshotHistory(db, providerID, planCode)
if err != nil {
return err
}
for _, id := range redundantSnapshotRowIDs(history) {
if _, err := db.Exec(`DELETE FROM subscription_plan WHERE id = $1`, id); err != nil {
return fmt.Errorf("delete redundant subscription snapshot %d for %s: %w", id, planCode, err)
}
}
return nil
}
func redundantSnapshotRowIDs(history []subscriptionSnapshotRow) []int64 {
type signatureKey struct {
PlanName string
Tier string
BillingCycle string
Currency string
ListPrice float64
PriceUnit string
QuotaValue int64
QuotaUnit string
ContextWindow int
PlanScope string
ModelScope string
SourceURL string
Notes string
}
type keptSnapshot struct {
ID int64
EffectiveDate time.Time
PublishedAt time.Time
}
makeKey := func(row subscriptionSnapshotRow) signatureKey {
return signatureKey{
PlanName: row.PlanName,
Tier: row.Tier,
BillingCycle: row.BillingCycle,
Currency: row.Currency,
ListPrice: row.ListPrice,
PriceUnit: row.PriceUnit,
QuotaValue: row.QuotaValue,
QuotaUnit: row.QuotaUnit,
ContextWindow: row.ContextWindow,
PlanScope: row.PlanScope,
ModelScope: row.ModelScope,
SourceURL: row.SourceURL,
Notes: row.Notes,
}
}
shouldReplace := func(current keptSnapshot, candidate subscriptionSnapshotRow) bool {
if candidate.EffectiveDate.Before(current.EffectiveDate) {
return true
}
if candidate.EffectiveDate.Equal(current.EffectiveDate) && candidate.PublishedAt.Before(current.PublishedAt) {
return true
}
return candidate.EffectiveDate.Equal(current.EffectiveDate) && candidate.PublishedAt.Equal(current.PublishedAt) && candidate.ID < current.ID
}
keptBySignature := make(map[signatureKey]keptSnapshot)
redundant := make([]int64, 0)
for _, row := range history {
key := makeKey(row)
current, exists := keptBySignature[key]
if !exists {
keptBySignature[key] = keptSnapshot{ID: row.ID, EffectiveDate: row.EffectiveDate, PublishedAt: row.PublishedAt}
continue
}
if shouldReplace(current, row) {
redundant = append(redundant, current.ID)
keptBySignature[key] = keptSnapshot{ID: row.ID, EffectiveDate: row.EffectiveDate, PublishedAt: row.PublishedAt}
continue
}
redundant = append(redundant, row.ID)
}
sort.Slice(redundant, func(i, j int) bool { return redundant[i] < redundant[j] })
return redundant
}
func ensureSubscriptionProvider(db *sql.DB, record subscriptionImportRecord) (int64, error) {
var providerID int64
err := db.QueryRow(`SELECT id FROM model_provider WHERE name = $1`, record.ProviderName).Scan(&providerID)
if err == nil {
return providerID, nil
}
if err != sql.ErrNoRows {
return 0, err
}
err = db.QueryRow(
`INSERT INTO model_provider (name, name_cn, country, website, status)
VALUES ($1, $2, $3, $4, 'active')
RETURNING id`,
record.ProviderName, nullIfBlank(record.ProviderNameCn), record.ProviderCountry, nullIfBlank(record.ProviderWebsite),
).Scan(&providerID)
return providerID, err
}
func ensureSubscriptionOperator(db *sql.DB, record subscriptionImportRecord) (int64, error) {
var operatorID int64
err := db.QueryRow(`SELECT id FROM operator WHERE name = $1`, record.OperatorName).Scan(&operatorID)
if err == nil {
return operatorID, nil
}
if err != sql.ErrNoRows {
return 0, err
}
err = db.QueryRow(
`INSERT INTO operator (name, name_cn, country, website, description, status, type)
VALUES ($1, $2, $3, $4, $5, 'active', $6)
RETURNING id`,
record.OperatorName, nullIfBlank(record.OperatorNameCn), record.OperatorCountry, nullIfBlank(record.OperatorWebsite),
fmt.Sprintf("%s subscription import", record.OperatorName), record.OperatorType,
).Scan(&operatorID)
return operatorID, err
}
func summarizeSubscriptionImport(records []subscriptionImportRecord, getter func(subscriptionImportRecord) string) string {
counts := make(map[string]int)
keys := make([]string, 0)
for _, record := range records {
key := getter(record)
if _, exists := counts[key]; !exists {
keys = append(keys, key)
}
counts[key]++
}
sort.Strings(keys)
parts := make([]string, 0, len(keys))
for _, key := range keys {
parts = append(parts, fmt.Sprintf("%s:%d", key, counts[key]))
}
return strings.Join(parts, ",")
}
func nullIfBlank(value string) any {
if strings.TrimSpace(value) == "" {
return nil
}
return value
}
func nullIfZeroInt64(value int64) any {
if value == 0 {
return nil
}
return value
}
func nullIfZeroIntCommon(value int) any {
if value == 0 {
return nil
}
return value
}
func mustParseSubscriptionPrice(raw string) float64 {
cleaned := strings.ReplaceAll(raw, ",", "")
cleaned = strings.ReplaceAll(cleaned, " ", "")
value, _ := strconv.ParseFloat(cleaned, 64)
return value
}
func mustParseSubscriptionInt64(raw string) int64 {
cleaned := strings.ReplaceAll(raw, ",", "")
cleaned = strings.ReplaceAll(cleaned, " ", "")
value, _ := strconv.ParseInt(cleaned, 10, 64)
return value
}
func firstNonEmptyText(values ...string) string {
for _, value := range values {
if strings.TrimSpace(value) != "" {
return value
}
}
return ""
}
func parseDecimalMultiplier(raw string, unit int64) int64 {
cleaned := strings.TrimSpace(strings.ReplaceAll(raw, " ", ""))
value, _ := strconv.ParseFloat(cleaned, 64)
return int64(value * float64(unit))
}
func sliceSection(raw string, start string, end string) string {
startIndex := strings.Index(raw, start)
if startIndex < 0 {
return ""
}
section := raw[startIndex+len(start):]
if end != "" {
if endIndex := strings.Index(section, end); endIndex >= 0 {
section = section[:endIndex]
}
}
return section
}

View File

@@ -0,0 +1,48 @@
//go:build llm_script
package main
import (
"net/http"
"net/http/httptest"
"testing"
"time"
)
type assertiveError string
func (e assertiveError) Error() string {
return string(e)
}
func TestFetchSubscriptionPageRetriesForbiddenThenSucceeds(t *testing.T) {
attempts := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
attempts++
if attempts == 1 {
w.WriteHeader(http.StatusForbidden)
_, _ = w.Write([]byte("blocked"))
return
}
_, _ = w.Write([]byte("<html><body>套餐价格</body></html>"))
}))
defer server.Close()
client := &http.Client{Timeout: 2 * time.Second}
body, err := fetchSubscriptionPage(server.URL, "", client)
if err != nil {
t.Fatalf("fetchSubscriptionPage 返回错误: %v", err)
}
if body != "套餐价格" {
t.Fatalf("返回体归一化错误: %q", body)
}
if attempts != 2 {
t.Fatalf("期望重试 1 次后成功,实际请求 %d 次", attempts)
}
}
func TestIsRetriableSubscriptionFetchErrorRecognizesForbidden(t *testing.T) {
if !isRetriableSubscriptionFetchError(assertiveError("unexpected status 403")) {
t.Fatalf("403 应被视作可重试错误")
}
}

View File

@@ -0,0 +1,21 @@
# Coding Plan概述
更新时间2026-05-14
## 套餐详情
Lite 套餐自 2026 年 3 月 20 日 00:00:00UTC+08:00起停止新购。
Lite 套餐支持所有套餐模型含千问、GLM、Kimi、MiniMax与 Pro 套餐一致。
Pro 高级套餐
支持的模型
推荐模型qwen3.6-plus、kimi-k2.5、glm-5、MiniMax-M2.5
更多模型qwen3.5-plus、qwen3-max-2026-01-23、qwen3-coder-next、qwen3-coder-plus、glm-4.7
价格
¥ 200/月
用量限制
每 5 小时 6,000 次请求
每周 45,000 次请求
每月 90,000 次请求
限时优惠:活动已结束,当前价格以下单页为准。

View File

@@ -0,0 +1,31 @@
# Token Plan团队版概述
更新时间2026-05-14
## 套餐与定价
### Token Plan 团队版
提供标准坐席、高级坐席、尊享坐席三个档位,匹配不同使用强度。
坐席类型
价格
额度
适用场景
标准坐席
¥198/坐席/月
25,000 Credits/坐席/月
轻度使用 AI 辅助的团队成员
高级坐席
¥698/坐席/月
100,000 Credits/坐席/月
日常高频使用 AI 编程或办公的团队成员
尊享坐席
¥1,398/坐席/月
250,000 Credits/坐席/月
重度依赖 AI 的核心开发者或高强度使用者
### Token Plan 团队版 - 共享用量包
Token Plan 团队版 - 共享用量包
¥5,000/个
625,000 Credits/个

View File

@@ -0,0 +1,69 @@
{
"Items": [
{
"currencyCode": "USD",
"retailPrice": 0.0022,
"unitPrice": 0.0022,
"location": "US East",
"meterName": "gpt 4.1 Inp regnl Tokens",
"productName": "Azure OpenAI",
"skuName": "gpt 4.1 Inp regnl",
"serviceName": "Foundry Models",
"unitOfMeasure": "1K",
"type": "Consumption",
"armSkuName": "gpt 4.1 Inp regnl"
},
{
"currencyCode": "USD",
"retailPrice": 0.0088,
"unitPrice": 0.0088,
"location": "US East",
"meterName": "gpt 4.1 Outp regnl Tokens",
"productName": "Azure OpenAI",
"skuName": "gpt 4.1 Outp regnl",
"serviceName": "Foundry Models",
"unitOfMeasure": "1K",
"type": "Consumption",
"armSkuName": "gpt 4.1 Outp regnl"
},
{
"currencyCode": "USD",
"retailPrice": 1.25,
"unitPrice": 1.25,
"location": "US West",
"meterName": "GPT 5 inp Glbl 1M Tokens",
"productName": "Azure OpenAI GPT5",
"skuName": "GPT 5 inp Glbl",
"serviceName": "Foundry Models",
"unitOfMeasure": "1M",
"type": "Consumption",
"armSkuName": "GPT 5 inp Glbl"
},
{
"currencyCode": "USD",
"retailPrice": 10,
"unitPrice": 10,
"location": "US West",
"meterName": "GPT 5 outpt Glbl 1M Tokens",
"productName": "Azure OpenAI GPT5",
"skuName": "GPT 5 outpt Glbl",
"serviceName": "Foundry Models",
"unitOfMeasure": "1M",
"type": "Consumption",
"armSkuName": "GPT 5 outpt Glbl"
},
{
"currencyCode": "USD",
"retailPrice": 0.625,
"unitPrice": 0.625,
"location": "US West",
"meterName": "GPT 5.1 Batch inp Gl 1M Tokens",
"productName": "Azure OpenAI GPT5",
"skuName": "GPT 5.1 Batch inp Gl",
"serviceName": "Foundry Models",
"unitOfMeasure": "1M",
"type": "Consumption",
"armSkuName": "GPT 5.1 Batch inp Gl"
}
]
}

View File

@@ -0,0 +1,12 @@
# Coding Plan
更新时间2026-05-08
## 套餐详情
### 套餐价格与限额
套餐类型 价格 用量限制
Coding Plan Lite ¥ 40 / 月 每 5 小时:最多约 1,200 次请求
每周:最多约 9,000 次请求
每订阅月:最多约 18,000 次请求
Coding Plan Pro ¥ 200 / 月 每 5 小时:最多约 6,000 次请求
每周:最多约 45,000 次请求
每订阅月:最多约 90,000 次请求

View File

@@ -0,0 +1,10 @@
# Token 福利包
更新时间2026-05-08
## 套餐价格
积分额度 有效期 原价 首购优惠价
50,000 1个月 ¥50 ¥45
100,000 1个月 ¥100 ¥90
200,000 1个月 ¥200 ¥170
400,000 1个月 ¥400 ¥340
800,000 1个月 ¥800 ¥680

View File

@@ -0,0 +1,66 @@
<h3 id="Model_Pricing" class="lb-txt-none lb-txt-32 lb-h3 lb-title">Model Pricing</h3>
<h2 id="Amazon_Nova" class="lb-txt-none lb-h2 lb-title">Amazon Nova</h2>
<p><b>Regions:&nbsp;US East (N. Virginia), US East (Ohio), and US West (Oregon)</b></p>
<table>
<tbody>
<tr>
<td><b>Amazon Nova models</b></td>
<td><b>Price per 1M input tokens (text)</b></td>
<td><b>Price per 1M input tokens (image)</b></td>
<td><b>Price per 1M input tokens (video)</b></td>
<td><b>Price per 1M input tokens (audio)</b></td>
<td><b>Price per 1M output tokens (text)</b></td>
<td><b>Price per 1M output tokens (image)</b></td>
</tr>
<tr>
<td>Amazon Nova 2 Omni (Preview)</td>
<td>$0.12</td>
<td>$0.24</td>
<td>$0.36</td>
<td>$0.48</td>
<td>$0.96</td>
<td>$1.20</td>
</tr>
<tr>
<td>Amazon Nova 2 Lite</td>
<td>$0.08</td>
<td>$0.10</td>
<td>$0.12</td>
<td>$0.14</td>
<td>$0.40</td>
<td>N/A</td>
</tr>
</tbody>
</table>
<h2 id="Qwen" class="lb-txt-none lb-h2 lb-title">Qwen</h2>
<p><b>Regions:&nbsp;Europe (Frankfurt) and&nbsp;Asia Pacific (Jakarta)</b></p>
<table>
<tbody>
<tr>
<td><b>Qwen models</b></td>
<td><b>Price per 1M input tokens</b></td>
<td><b>Price per 1M output tokens</b></td>
</tr>
<tr>
<td>Qwen3 Coder Next</td>
<td>$ 0.60</td>
<td>$ 1.44</td>
</tr>
</tbody>
</table>
<p><b>Region: Asia Pacific (Sydney)</b></p>
<table>
<tbody>
<tr>
<td><b>Qwen models</b></td>
<td><b>Price per 1M input tokens</b></td>
<td><b>Price per 1M output tokens</b></td>
</tr>
<tr>
<td>Qwen3 Next 80B A3B</td>
<td>$ 0.1545</td>
<td>$ 1.2360</td>
</tr>
</tbody>
</table>
<h2 id="Pricing_examples" class="lb-txt-none lb-txt-48 lb-h2 lb-title">Pricing examples</h2>

View File

@@ -0,0 +1,3 @@
# 方舟 Coding Plan 服务变更通知
每日 10:30 限量开放首购活动Lite 首月 9.9 元Pro 首月 49.9 元。
续费分别恢复至 40 元/月和 200 元/月。

View File

@@ -0,0 +1,19 @@
# 方舟 Coding Plan
最近更新时间: 2026-03-13 10:30:00
Lite 套餐
日常开发、中等强度任务,适合大多数开发者
9.9 元
40 元/月
- 每 5 小时约 1,200 次请求
- 每周约 9,000 次请求
- 每月约 18,000 次请求
Pro 套餐
高频调用、复杂项目开发,适合重度用户
49.9 元
200 元/月
- Lite 套餐 5 倍额度
- 每 5 小时约 6,000 次请求
- 每周约 45,000 次请求
- 每月约 90,000 次请求

View File

@@ -0,0 +1,32 @@
# 编码套餐Coding Plan
最近更新时间: 2026-04-09 19:13:32
GLM Lite
GLM Pro
GLM Max
适用场景
适合处理轻量的开发任务
适合处理复杂项目的开发任务
适合处理海量负载的开发任务
包月价格 49元/月 149元/月 469元/月
包年价格 479/年 1439元/年 4509元/年
支持模型
GLM-5.1
GLM-5-Turbo
GLM-4.7
GLM-4.6
GLM-4.5
GLM-4.5-Air
用量限制
每 5 小时最多约80 次prompts
每周最多约400次prompts
每月最多约1,600次prompts
每 5 小时最多约400 次prompts
每周最多约2,000 次prompts
每月最多约8,000次prompts
每 5 小时最多约1,600 次prompts
每周最多约8,000 次prompts
每月最多约32,000次prompts

View File

@@ -0,0 +1,32 @@
# 天翼云大模型AI专项
Token Plan Lite1500万Tokens包
面向开发者/中小企业,适用于项目开发迭代,大幅提升编码效率与代码质量。
支持模型GLM5
支持工具OpenClaw、OpenCode、Cursor、Cline、Chatbox、Codebuddy、Trae等
39 .90 元/个
Token Plan Pro7000万Tokens包
面向开发者/中小企业,适用于项目开发迭代,大幅提升编码效率与代码质量。
支持模型GLM5
159 .90 元/个
Token Plan Max1.5亿Tokens包
面向开发者/中小企业,适用于项目开发迭代,大幅提升编码效率与代码质量。
支持模型GLM5
299 .90 元/个
Token Plan 轻享包1000万Tokens包
适用于个人/家庭 API 及业务调用,有效解决按需单价高、预算难控等问题。
支持模型Deepseek v3.2
9 .90 元/个
Token Plan 畅享包4000万Tokens包
适用于个人/家庭 API 及业务调用,有效解决按需单价高、预算难控等问题。
支持模型Deepseek v3.2
29 .90 元/个
Token Plan 尊享包8000万Tokens包
适用于个人/家庭 API 及业务调用,有效解决按需单价高、预算难控等问题。
支持模型Deepseek v3.2
49 .90 元/个

View File

@@ -0,0 +1,5 @@
# 联通云智算专区
AI算力平台AICP
实现开发、训练、推理、模型服务部署全流程闭环。
AI应用开发平台
提供一站式可视化开发、调试和发布智能体应用能力。

View File

@@ -0,0 +1,39 @@
# MaaS 文本生成模型
更新时间2026-05-14
按套餐包/资源包计费
DeepSeek-V3.1
100万
1个月
5.6
购买DeepSeek-V3.1模型的套餐包可抵扣DeepSeek-V3.1模型的Token用量。
1000万
1个月
56
购买DeepSeek-V3.1模型的套餐包可抵扣DeepSeek-V3.1模型的Token用量。
1亿
3个月
558
购买DeepSeek-V3.1模型的套餐包可抵扣DeepSeek-V3.1模型的Token用量。
10亿
3个月
5598
购买DeepSeek-V3.1模型的套餐包可抵扣DeepSeek-V3.1模型的Token用量。
DeepSeek-V3.2
100万
1个月
2.2
购买DeepSeek-V3.2模型的套餐包可抵扣DeepSeek-V3.2模型的Token用量。
1000万
1个月
22
购买DeepSeek-V3.2模型的套餐包可抵扣DeepSeek-V3.2模型的Token用量。
1亿
3个月
219
购买DeepSeek-V3.2模型的套餐包可抵扣DeepSeek-V3.2模型的Token用量。
10亿
3个月
2199
购买DeepSeek-V3.2模型的套餐包可抵扣DeepSeek-V3.2模型的Token用量。

View File

@@ -0,0 +1,48 @@
更新时间: 2026-05-15
Monthly Token Plans
Standard Plans:
Starter Plus Max
Price
$10 /month
$20 /month
$50 /month
M2.7
1,500 requests/5hrs
4,500 requests/5hrs
15,000 requests/5hrs
Highspeed Plans:
Plus-Highspeed Max-Highspeed Ultra-Highspeed
Price
$40 /month
$80 /month
$150 /month
M2.7-highspeed
4,500 requests/5hrs
15,000 requests/5hrs
30,000 requests/5hrs
Speech 2.8 4000 chars/day 11000 chars/day
image-01 50 images/day 120 images/day
Hailuo 2.3-Fast 2 videos/day
Hailuo 2.3 2 videos/day
Music-2.6 100 songs/day
Yearly Token Plans
Standard Plans:
Starter Plus Max
Price
$100 /year
$200 /year
$500 /year
M2.7
1,500 requests/5hrs
4,500 requests/5hrs
15,000 requests/5hrs
Highspeed Plans:
Plus-Highspeed Max-Highspeed Ultra-Highspeed
Price
$400 /year
$800 /year
$1,500 /year
M2.7-highspeed
4,500 requests/5hrs
15,000 requests/5hrs
30,000 requests/5hrs

View File

@@ -0,0 +1,4 @@
# 移动云市场
AI应用专区
数据大模型
覆盖智能客服、办公提效、数据分析等场景能力。

View File

@@ -0,0 +1,20 @@
deepseek/deepseek-v4-flash
DeepSeek
Input Price:¥0.5 / 1M tokens
Output Price:¥2 / 1M tokens
Context:1,000,000
deepseek/deepseek-v4-pro
DeepSeek
Input Price:¥3 / 1M tokens
Output Price:¥6 / 1M tokens
Context:1,000,000
moonshotai/kimi-k2.6
Moonshot AI
Input Price:¥4 / 1M tokens
Output Price:¥16 / 1M tokens
Context:262,144
qwen/qwen3.6-plus
Qwen
Input Price:¥0.8 / 1M tokens
Output Price:¥3.2 / 1M tokens
Context:256,000

View File

@@ -0,0 +1,60 @@
deepseek/deepseek-v3.1
131,072
4
/
Mt
·Cached(r)
2
/
Mt
12
/
Mt
在线体验
deepseek/deepseek-r1-0528
131,072
4
/
Mt
16
/
Mt
在线体验
moonshotai/kimi-k2.6
262,144
4
/
Mt
16
/
Mt
在线体验
qwen/qwen3-coder
131,072
2
/
Mt
8
/
Mt
在线体验
zai-org/glm-4.5-air
128,000
0.8
/
Mt
3.2
/
Mt
在线体验

View File

@@ -0,0 +1,25 @@
Qwen/Qwen3-32B
输入 (元 / M tokens)
输出 (元 / M tokens)
0.40
1.80
deepseek-ai/DeepSeek-V3.1-Terminus
输入 (元 / M tokens)
输出 (元 / M tokens)
4.00
12.00
moonshotai/Kimi-K2-Instruct
输入 (元 / M tokens)
输出 (元 / M tokens)
4.00
16.00
MiniMaxAI/MiniMax-M2.5
输入 (元 / M tokens)
输出 (元 / M tokens)
1.00
8.00
zai-org/GLM-4.5-Air
输入 (元 / M tokens)
输出 (元 / M tokens)
免费
免费

View File

@@ -0,0 +1,5 @@
openai/gpt-4o-mini Input 0.0001 CNY/1K tokens, Output 0.0004 CNY/1K tokens
moonshotai/kimi-k2.6 Input 4 CNY/million tokens, Output 16 CNY/million tokens
deepseek-ai/DeepSeek-R1 Input 4 CNY/million tokens, Output 16 CNY/million tokens
deepseek-ai/DeepSeek-V3 Input 2 CNY/million tokens, Output 8 CNY/million tokens
qwen/Qwen3-32B Input 0.4 CNY/million tokens, Output 1.8 CNY/million tokens

View File

@@ -0,0 +1,36 @@
DeepSeek V4 Flash
DeepSeek
上下文长度 1M
输入:¥0.5
输出:¥2
查看详情
DeepSeek V4 Pro
DeepSeek
上下文长度 128K
输入:¥2
输出:¥8
查看详情
Kimi K2.6
Kimi
上下文长度 262K
输入:¥4
输出:¥16
查看详情
MiniMax M2.5
Minimax
上下文长度 1M
输入:¥1
输出:¥8
查看详情
GLM-5
Zhipu
上下文长度 128K
输入:¥1
输出:¥3
查看详情
Qwen-3.6-Plus
Qwen
上下文长度 256K
输入:¥0.8
输出:¥3.2
即将上线

View File

@@ -0,0 +1,6 @@
# 套餐概览
GLM Coding Plan 提供 Lite、Pro 和 Max 三个套餐等级,可以按需灵活选择。
Lite、Pro 和 Max 套餐额度共享,支持平台中所有上线模型,包含 GLM-4.5-Air、GLM-4.5V、GLM-4.6V、GLM-4.7。
每 5 小时最多约 80 / 400 / 1,600 次 prompts。
每周最多约 400 / 2,000 / 8,000 次 prompts。
Lite 套餐每月包含 100 次 MCP 调用Pro 套餐每月包含 1,000 次 MCP 调用Max 套餐每月包含 4,000 次 MCP 调用。

View File

@@ -0,0 +1,3 @@
# 限时活动
GLM Coding Plan 编码畅享计划低至20元/月,适用于 Claude Code、Cline、Continue、Open Code 等平台。
首单 9 折优惠,新老用户均可参与。

View File

@@ -0,0 +1,58 @@
//go:build llm_script
package main
import (
"fmt"
"regexp"
"strings"
)
const defaultUCloudPricingURL = "https://www.ucloud-global.com/en/docs/modelverse/price"
var ucloudPricePattern = regexp.MustCompile(`([A-Za-z0-9._/-]+)\s+Input\s+([\d.]+)\s+CNY/(?:million|1K)\s+tokens,\s+Output\s+([\d.]+)\s+CNY/(?:million|1K)\s+tokens`)
func parseUCloudPricingCatalog(raw string) ([]officialPricingRecord, error) {
matches := ucloudPricePattern.FindAllStringSubmatch(raw, -1)
if len(matches) == 0 {
return nil, fmt.Errorf("unexpected ucloud pricing content")
}
records := make([]officialPricingRecord, 0, len(matches))
for _, match := range matches {
modelName := strings.TrimSpace(match[1])
providerName := providerFromModelPath(modelName)
providerNameCn, providerCountry, providerWebsite := providerMetadata(providerName)
inputPrice := mustParseSubscriptionPrice(match[2])
outputPrice := mustParseSubscriptionPrice(match[3])
if strings.Contains(strings.ToLower(match[0]), "/1k") {
inputPrice *= 1000
outputPrice *= 1000
}
record := officialPricingRecord{
ModelID: normalizeExternalID("ucloud", modelName),
ModelName: modelName,
ProviderName: providerName,
ProviderNameCn: providerNameCn,
ProviderCountry: providerCountry,
ProviderWebsite: providerWebsite,
OperatorName: "UModelVerse",
OperatorNameCn: "UModelVerse",
OperatorCountry: "CN",
OperatorWebsite: "https://www.ucloud.cn",
OperatorType: "cloud",
Region: "CN",
Currency: "CNY",
InputPrice: inputPrice,
OutputPrice: outputPrice,
SourceURL: defaultUCloudPricingURL,
ModelSourceURL: defaultUCloudPricingURL,
DateConfidence: "unknown",
DateSourceKind: "official_product_page",
Modality: detectModality(modelName),
}
record.IsFree = record.InputPrice == 0 && record.OutputPrice == 0
records = append(records, record)
}
return records, nil
}

View File

@@ -0,0 +1,76 @@
//go:build llm_script
package main
import (
"fmt"
"regexp"
"strings"
)
const defaultYoudaoPricingURL = "https://ai.youdao.com/new/model-service"
var youdaoCardPattern = regexp.MustCompile(`(?s)([A-Za-z0-9.+\- ]+)\n([A-Za-z][A-Za-z0-9 ]+)\n.*?上下文长度\s*([0-9A-Za-z., ]+)\n.*?输入[:]?\s*¥?([\d.]+)\n.*?输出[:]?\s*¥?([\d.]+)\n.*?(查看详情|即将上线)`)
func parseYoudaoPricingCatalog(raw string) ([]officialPricingRecord, error) {
matches := youdaoCardPattern.FindAllStringSubmatch(raw, -1)
if len(matches) == 0 {
return nil, fmt.Errorf("unexpected youdao pricing content")
}
records := make([]officialPricingRecord, 0, len(matches))
for _, match := range matches {
modelName := strings.TrimSpace(match[1])
providerName := normalizeYoudaoProvider(match[2])
if strings.TrimSpace(match[6]) != "查看详情" {
continue
}
providerNameCn, providerCountry, providerWebsite := providerMetadata(providerName)
record := officialPricingRecord{
ModelID: normalizeExternalID("youdao", modelName),
ModelName: modelName,
ProviderName: providerName,
ProviderNameCn: providerNameCn,
ProviderCountry: providerCountry,
ProviderWebsite: providerWebsite,
OperatorName: "Youdao Zhiyun MaaS",
OperatorNameCn: "有道智云 MaaS",
OperatorCountry: "CN",
OperatorWebsite: "https://sf.163.com",
OperatorType: "relay",
Region: "CN",
Currency: "CNY",
InputPrice: mustParseSubscriptionPrice(match[4]),
OutputPrice: mustParseSubscriptionPrice(match[5]),
ContextLength: parseContextLengthCommon(match[3]),
SourceURL: defaultYoudaoPricingURL,
ModelSourceURL: defaultYoudaoPricingURL,
DateConfidence: "unknown",
DateSourceKind: "official_product_page",
Modality: detectModality(modelName),
}
record.IsFree = record.InputPrice == 0 && record.OutputPrice == 0
records = append(records, record)
}
if len(records) == 0 {
return nil, fmt.Errorf("no active youdao pricing cards found")
}
return records, nil
}
func normalizeYoudaoProvider(raw string) string {
switch strings.ToLower(strings.TrimSpace(raw)) {
case "deepseek":
return "DeepSeek"
case "qwen":
return "Qwen"
case "kimi":
return "Moonshot AI"
case "minimax":
return "MiniMax"
case "zhipu":
return "Zhipu AI"
default:
return strings.TrimSpace(raw)
}
}

View File

@@ -0,0 +1,81 @@
//go:build llm_script
package main
import (
"fmt"
"regexp"
"strings"
)
const (
defaultZhipuCodingPlanOverviewURL = "https://docs.bigmodel.cn/cn/coding-plan/overview"
defaultZhipuCodingPlanPromotionURL = "https://docs.bigmodel.cn/cn/update/promotion"
)
func parseZhipuCodingPlanCatalog(overviewRaw string, promotionRaw string) ([]subscriptionImportRecord, error) {
publishedAt, known := publishedAtFromText(firstNonEmptyText(overviewRaw, promotionRaw))
pricePattern := regexp.MustCompile(`低至\s*([\d.]+)\s*元/月`)
priceMatch := pricePattern.FindStringSubmatch(promotionRaw)
if len(priceMatch) != 2 {
return nil, fmt.Errorf("zhipu promo floor price not found")
}
modelScope := []string{}
modelPattern := regexp.MustCompile(`包含\s+([^\n]+)`)
modelMatch := modelPattern.FindStringSubmatch(overviewRaw)
if len(modelMatch) == 2 {
for _, item := range strings.Split(modelMatch[1], "、") {
item = strings.TrimSpace(strings.TrimSuffix(item, "。"))
if item != "" {
modelScope = append(modelScope, item)
}
}
}
notes := []string{
"GLM Coding Plan 提供 Lite/Pro/Max 三档能力。",
}
if strings.Contains(overviewRaw, "每 5 小时最多约 80 / 400 / 1,600 次 prompts") {
notes = append(notes, "5 小时额度Lite 80 / Pro 400 / Max 1600 prompts。")
}
if strings.Contains(overviewRaw, "每周最多约 400 / 2,000 / 8,000 次 prompts") {
notes = append(notes, "每周额度Lite 400 / Pro 2000 / Max 8000 prompts。")
}
if strings.Contains(overviewRaw, "Lite 套餐每月包含 100 次 MCP 调用") {
notes = append(notes, "MCP 月额度Lite 100 / Pro 1000 / Max 4000。")
}
if strings.Contains(promotionRaw, "首单 9 折") {
notes = append(notes, "首单 9 折优惠,新老用户均可参与。")
}
notes = append(notes, "公开文档仅披露活动底价,分档定价仍需控制台核验。")
return []subscriptionImportRecord{{
ProviderName: "Zhipu AI",
ProviderNameCn: "智谱 AI",
ProviderCountry: "CN",
ProviderWebsite: "https://open.bigmodel.cn",
OperatorName: "Zhipu",
OperatorNameCn: "智谱开放平台",
OperatorCountry: "CN",
OperatorWebsite: "https://docs.bigmodel.cn/",
OperatorType: "official",
PlanFamily: "coding_plan",
PlanCode: "zhipu-coding-plan-promo-floor",
PlanName: "GLM Coding Plan 限时活动价(低至)",
Tier: "PromoFloor",
BillingCycle: "monthly",
Currency: "CNY",
ListPrice: mustParseSubscriptionPrice(priceMatch[1]),
PriceUnit: "CNY/month",
QuotaValue: 0,
QuotaUnit: "",
PlanScope: "GLM Coding Plan",
ModelScope: modelScope,
SourceURL: defaultZhipuCodingPlanPromotionURL,
PublishedAt: publishedAt,
EffectiveDate: effectiveDateFromPublishedAt(publishedAt),
Notes: strings.Join(notes, ""),
PublishedAtKnown: known,
}}, nil
}

View File

@@ -0,0 +1,430 @@
{
"checkedAt": "2026-05-14T00:00:00+08:00",
"items": [
{
"catalogCode": "tencent-cloud-token-plan-personal",
"providerName": "Tencent",
"providerNameCn": "腾讯",
"providerCountry": "CN",
"providerWebsite": "https://cloud.tencent.com",
"operatorName": "Tencent Cloud",
"operatorNameCn": "腾讯云",
"operatorCountry": "CN",
"operatorWebsite": "https://cloud.tencent.com",
"operatorType": "cloud",
"platformName": "Tencent Cloud TokenHub",
"platformNameCn": "腾讯云 TokenHub",
"platformType": "cloud_operator",
"planFamily": "token_plan",
"planStatus": "confirmed",
"sourceKind": "official_doc",
"sourceURL": "https://cloud.tencent.com/document/product/1823/130060",
"sourceTitle": "Token Plan 个人版套餐概览",
"region": "CN",
"currency": "CNY",
"billingCycle": "monthly",
"importerKey": "tencent_catalog",
"notes": "个人版同时提供通用 Token Plan 与 Hy Token Plan两大系列均提供四档套餐。"
},
{
"catalogCode": "tencent-cloud-token-plan-enterprise-pro",
"providerName": "Tencent",
"providerNameCn": "腾讯",
"providerCountry": "CN",
"providerWebsite": "https://cloud.tencent.com",
"operatorName": "Tencent Cloud",
"operatorNameCn": "腾讯云",
"operatorCountry": "CN",
"operatorWebsite": "https://cloud.tencent.com",
"operatorType": "cloud",
"platformName": "Tencent Cloud TokenHub",
"platformNameCn": "腾讯云 TokenHub",
"platformType": "cloud_operator",
"planFamily": "token_plan",
"planStatus": "confirmed",
"sourceKind": "official_doc",
"sourceURL": "https://cloud.tencent.com/document/product/1823/130659",
"sourceTitle": "Token Plan 企业版专业套餐",
"region": "CN",
"currency": "CNY",
"billingCycle": "monthly",
"importerKey": "tencent_catalog",
"notes": "企业版专业套餐按积分池统一管理月预算,可分配多 API Key 配额。"
},
{
"catalogCode": "tencent-cloud-token-plan-enterprise-lite",
"providerName": "Tencent",
"providerNameCn": "腾讯",
"providerCountry": "CN",
"providerWebsite": "https://cloud.tencent.com",
"operatorName": "Tencent Cloud",
"operatorNameCn": "腾讯云",
"operatorCountry": "CN",
"operatorWebsite": "https://cloud.tencent.com",
"operatorType": "cloud",
"platformName": "Tencent Cloud TokenHub",
"platformNameCn": "腾讯云 TokenHub",
"platformType": "cloud_operator",
"planFamily": "token_plan",
"planStatus": "confirmed",
"sourceKind": "official_doc",
"sourceURL": "https://cloud.tencent.com/document/product/1823/131173",
"sourceTitle": "Token Plan 企业版轻享套餐",
"region": "CN",
"currency": "CNY",
"billingCycle": "monthly",
"importerKey": "tencent_catalog",
"notes": "企业版轻享套餐按 Token 资源池实时扣减,支持 1 至 12 个月购买周期。"
},
{
"catalogCode": "tencent-cloud-coding-plan",
"providerName": "Tencent",
"providerNameCn": "腾讯",
"providerCountry": "CN",
"providerWebsite": "https://cloud.tencent.com",
"operatorName": "Tencent Cloud",
"operatorNameCn": "腾讯云",
"operatorCountry": "CN",
"operatorWebsite": "https://cloud.tencent.com",
"operatorType": "cloud",
"platformName": "Tencent Cloud TokenHub",
"platformNameCn": "腾讯云 TokenHub",
"platformType": "cloud_operator",
"planFamily": "coding_plan",
"planStatus": "confirmed",
"sourceKind": "official_doc",
"sourceURL": "https://cloud.tencent.com/document/product/1823/130103",
"sourceTitle": "Coding Plan 常见问题",
"region": "CN",
"currency": "CNY",
"billingCycle": "monthly",
"importerKey": "tencent_catalog",
"notes": "同一主账号同时只能购买一个 Coding Plan 套餐,订阅月额度按购买时间滚动刷新。"
},
{
"catalogCode": "aliyun-bailian-token-plan-team",
"providerName": "Alibaba",
"providerNameCn": "阿里巴巴",
"providerCountry": "CN",
"providerWebsite": "https://www.aliyun.com",
"operatorName": "Alibaba Bailian",
"operatorNameCn": "阿里云百炼",
"operatorCountry": "CN",
"operatorWebsite": "https://help.aliyun.com/zh/model-studio/",
"operatorType": "cloud",
"platformName": "Alibaba Cloud Model Studio",
"platformNameCn": "阿里云百炼",
"platformType": "cloud_operator",
"planFamily": "token_plan",
"planStatus": "confirmed",
"sourceKind": "official_doc",
"sourceURL": "https://help.aliyun.com/zh/model-studio/token-plan-overview",
"sourceTitle": "Token Plan团队版概述",
"region": "CN",
"currency": "CNY",
"billingCycle": "monthly",
"importerKey": "import_aliyun_subscription.go",
"notes": "团队版按坐席包月,标准、高级、尊享三档分别为 198、698、1398 元每坐席每月,并支持共享用量包。"
},
{
"catalogCode": "aliyun-bailian-coding-plan",
"providerName": "Alibaba",
"providerNameCn": "阿里巴巴",
"providerCountry": "CN",
"providerWebsite": "https://www.aliyun.com",
"operatorName": "Alibaba Bailian",
"operatorNameCn": "阿里云百炼",
"operatorCountry": "CN",
"operatorWebsite": "https://help.aliyun.com/zh/model-studio/",
"operatorType": "cloud",
"platformName": "Alibaba Cloud Model Studio",
"platformNameCn": "阿里云百炼",
"platformType": "cloud_operator",
"planFamily": "coding_plan",
"planStatus": "confirmed",
"sourceKind": "official_doc",
"sourceURL": "https://help.aliyun.com/zh/model-studio/coding-plan-quickstart",
"sourceTitle": "Coding Plan概述",
"region": "CN",
"currency": "CNY",
"billingCycle": "monthly",
"importerKey": "import_aliyun_subscription.go",
"notes": "当前文档可确认 Pro 套餐为 200 元每月Lite 套餐已在 2026-03-20 停止新购。"
},
{
"catalogCode": "baidu-qianfan-token-benefit-pack",
"providerName": "Baidu",
"providerNameCn": "百度",
"providerCountry": "CN",
"providerWebsite": "https://cloud.baidu.com",
"operatorName": "Baidu Qianfan",
"operatorNameCn": "百度千帆",
"operatorCountry": "CN",
"operatorWebsite": "https://cloud.baidu.com/doc/qianfan/index.html",
"operatorType": "cloud",
"platformName": "Baidu Qianfan",
"platformNameCn": "百度千帆",
"platformType": "cloud_operator",
"planFamily": "token_plan",
"planStatus": "confirmed",
"sourceKind": "official_doc",
"sourceURL": "https://cloud.baidu.com/doc/qianfan/s/Smoghsq3g",
"sourceTitle": "Token 福利包",
"region": "CN",
"currency": "CNY",
"billingCycle": "monthly",
"importerKey": "import_baidu_subscription.go",
"notes": "Token 福利包提供 5 万至 80 万积分额度,均为 1 个月有效期,并给出首购优惠价。"
},
{
"catalogCode": "baidu-qianfan-coding-plan",
"providerName": "Baidu",
"providerNameCn": "百度",
"providerCountry": "CN",
"providerWebsite": "https://cloud.baidu.com",
"operatorName": "Baidu Qianfan",
"operatorNameCn": "百度千帆",
"operatorCountry": "CN",
"operatorWebsite": "https://cloud.baidu.com/doc/qianfan/index.html",
"operatorType": "cloud",
"platformName": "Baidu Qianfan",
"platformNameCn": "百度千帆",
"platformType": "cloud_operator",
"planFamily": "coding_plan",
"planStatus": "confirmed",
"sourceKind": "official_doc",
"sourceURL": "https://cloud.baidu.com/doc/qianfan/s/imlg0beiu",
"sourceTitle": "Coding Plan",
"region": "CN",
"currency": "CNY",
"billingCycle": "monthly",
"importerKey": "import_baidu_subscription.go",
"notes": "Lite 套餐 40 元每月Pro 套餐 200 元每月,且同时给出了 5 小时、每周、每订阅月三重请求上限。"
},
{
"catalogCode": "zhipu-glm-coding-plan",
"providerName": "Zhipu AI",
"providerNameCn": "智谱 AI",
"providerCountry": "CN",
"providerWebsite": "https://open.bigmodel.cn",
"operatorName": "Zhipu",
"operatorNameCn": "智谱开放平台",
"operatorCountry": "CN",
"operatorWebsite": "https://docs.bigmodel.cn/",
"operatorType": "official",
"platformName": "Zhipu Coding Platform",
"platformNameCn": "智谱 Coding Plan",
"platformType": "official_vendor",
"planFamily": "coding_plan",
"planStatus": "confirmed",
"sourceKind": "official_doc",
"sourceURL": "https://docs.bigmodel.cn/cn/coding-plan/overview",
"sourceTitle": "套餐概览",
"region": "CN",
"currency": "CNY",
"billingCycle": "monthly",
"importerKey": "import_zhipu_coding_plan.go",
"notes": "智谱已上线 GLM Coding Plan并为 TRAE、Qoder、OpenCode、OpenClaw 等工具提供专属接入文档与 Coding API 端点。"
},
{
"catalogCode": "minimax-token-plan",
"providerName": "MiniMax",
"providerNameCn": "MiniMax",
"providerCountry": "CN",
"providerWebsite": "https://platform.minimaxi.com",
"operatorName": "MiniMax",
"operatorNameCn": "MiniMax",
"operatorCountry": "CN",
"operatorWebsite": "https://platform.minimaxi.com/docs/pricing/overview",
"operatorType": "official",
"platformName": "MiniMax Open Platform",
"platformNameCn": "MiniMax 开放平台",
"platformType": "official_vendor",
"planFamily": "token_plan",
"planStatus": "confirmed",
"sourceKind": "official_doc",
"sourceURL": "https://platform.minimaxi.com/docs/token-plan/intro",
"sourceTitle": "Token Plan 概要",
"region": "CN",
"currency": "CNY",
"billingCycle": "monthly",
"importerKey": "import_minimax_subscription.go",
"notes": "MiniMax 已提供 Token Plan并保留按量计费 API Key 作为超限后的切换路径;当前已进入真实 Token Plan 抓取链路。"
},
{
"catalogCode": "volcengine-ark-coding-plan",
"providerName": "ByteDance",
"providerNameCn": "字节跳动",
"providerCountry": "CN",
"providerWebsite": "https://www.volcengine.com",
"operatorName": "ByteDance Volcano",
"operatorNameCn": "火山引擎",
"operatorCountry": "CN",
"operatorWebsite": "https://developer.volcengine.com",
"operatorType": "cloud",
"platformName": "Volcengine Ark",
"platformNameCn": "火山方舟",
"platformType": "cloud_operator",
"planFamily": "coding_plan",
"planStatus": "confirmed",
"sourceKind": "official_community",
"sourceURL": "https://developer.volcengine.com/articles/7574419773204004906",
"sourceTitle": "火山方舟新套餐上线:方舟 Coding Plan",
"region": "CN",
"currency": "CNY",
"billingCycle": "monthly",
"importerKey": "import_bytedance_subscription.go",
"notes": "当前可从官方开发者社区确认方舟 Coding Plan 已上线,并提供活动页订阅入口及多模型支持说明。"
},
{
"catalogCode": "huawei-cloud-maas-package-plan",
"providerName": "Huawei",
"providerNameCn": "华为",
"providerCountry": "CN",
"providerWebsite": "https://www.huaweicloud.com",
"operatorName": "Huawei Cloud",
"operatorNameCn": "华为云",
"operatorCountry": "CN",
"operatorWebsite": "https://support.huaweicloud.com",
"operatorType": "cloud",
"platformName": "Huawei Cloud MaaS",
"platformNameCn": "华为云 MaaS",
"platformType": "cloud_operator",
"planFamily": "package_plan",
"planStatus": "confirmed",
"sourceKind": "official_doc",
"sourceURL": "https://support.huaweicloud.com/price-maas/price-maas-0002.html",
"sourceTitle": "MaaS文本生成模型",
"region": "CN",
"currency": "CNY",
"billingCycle": "monthly",
"importerKey": "import_huawei_package.go",
"notes": "华为云当前对 MaaS 文本生成明确支持按 Token 付费和按套餐包计费,两条路径共存。"
},
{
"catalogCode": "openai-api-payg",
"providerName": "OpenAI",
"providerNameCn": "OpenAI",
"providerCountry": "US",
"providerWebsite": "https://platform.openai.com",
"operatorName": "OpenAI",
"operatorNameCn": "OpenAI",
"operatorCountry": "US",
"operatorWebsite": "https://platform.openai.com",
"operatorType": "official",
"platformName": "OpenAI API",
"platformNameCn": "OpenAI API",
"platformType": "official_vendor",
"planFamily": "pay_as_you_go",
"planStatus": "confirmed",
"sourceKind": "official_pricing",
"sourceURL": "https://platform.openai.com/docs/pricing/",
"sourceTitle": "Pricing",
"region": "global",
"currency": "USD",
"billingCycle": "usage",
"importerKey": "existing_price_importer",
"notes": "当前官方 API 页面展示的是按百万 Token 计费,不存在 Token Plan 或 Coding Plan 型套餐。"
},
{
"catalogCode": "anthropic-api-payg",
"providerName": "Anthropic",
"providerNameCn": "Anthropic",
"providerCountry": "US",
"providerWebsite": "https://docs.anthropic.com",
"operatorName": "Anthropic",
"operatorNameCn": "Anthropic",
"operatorCountry": "US",
"operatorWebsite": "https://docs.anthropic.com",
"operatorType": "official",
"platformName": "Anthropic API",
"platformNameCn": "Anthropic API",
"platformType": "official_vendor",
"planFamily": "pay_as_you_go",
"planStatus": "confirmed",
"sourceKind": "official_pricing",
"sourceURL": "https://docs.anthropic.com/en/docs/about-claude/pricing",
"sourceTitle": "Pricing",
"region": "global",
"currency": "USD",
"billingCycle": "usage",
"importerKey": "import_catalog_seed_verification.go",
"notes": "官方 API 以模型 Token 单价、缓存写入/命中和批处理折扣为主,未发现 Coding Plan 型订阅页。"
},
{
"catalogCode": "deepseek-api-payg",
"providerName": "DeepSeek",
"providerNameCn": "DeepSeek",
"providerCountry": "CN",
"providerWebsite": "https://api-docs.deepseek.com",
"operatorName": "DeepSeek",
"operatorNameCn": "DeepSeek",
"operatorCountry": "CN",
"operatorWebsite": "https://api-docs.deepseek.com",
"operatorType": "official",
"platformName": "DeepSeek API",
"platformNameCn": "DeepSeek API",
"platformType": "official_vendor",
"planFamily": "pay_as_you_go",
"planStatus": "confirmed",
"sourceKind": "official_pricing",
"sourceURL": "https://api-docs.deepseek.com/zh-cn/quick_start/pricing/",
"sourceTitle": "模型 & 价格",
"region": "global",
"currency": "CNY",
"billingCycle": "usage",
"importerKey": "existing_price_importer",
"notes": "DeepSeek 官方当前以按量计费和限时折扣为主,费用直接从充值余额或赠送余额扣减。"
},
{
"catalogCode": "moonshot-api-payg",
"providerName": "Moonshot AI",
"providerNameCn": "Moonshot AI",
"providerCountry": "CN",
"providerWebsite": "https://platform.moonshot.cn",
"operatorName": "Moonshot",
"operatorNameCn": "Moonshot",
"operatorCountry": "CN",
"operatorWebsite": "https://platform.moonshot.cn",
"operatorType": "official",
"platformName": "Kimi API Platform",
"platformNameCn": "Kimi API 开放平台",
"platformType": "official_vendor",
"planFamily": "pay_as_you_go",
"planStatus": "confirmed",
"sourceKind": "official_pricing",
"sourceURL": "https://platform.moonshot.cn/docs/pricing/chat",
"sourceTitle": "模型推理价格说明",
"region": "global",
"currency": "CNY",
"billingCycle": "usage",
"importerKey": "existing_price_importer",
"notes": "Kimi API 官方按输入、输出和缓存命中 Token 计费,目前未检索到独立 Token Plan 或 Coding Plan 套餐。"
},
{
"catalogCode": "xai-api-payg",
"providerName": "xAI",
"providerNameCn": "xAI",
"providerCountry": "US",
"providerWebsite": "https://docs.x.ai",
"operatorName": "xAI",
"operatorNameCn": "xAI",
"operatorCountry": "US",
"operatorWebsite": "https://docs.x.ai",
"operatorType": "official",
"platformName": "xAI API",
"platformNameCn": "xAI API",
"platformType": "official_vendor",
"planFamily": "pay_as_you_go",
"planStatus": "confirmed",
"sourceKind": "official_pricing",
"sourceURL": "https://docs.x.ai/developers/pricing",
"sourceTitle": "Pricing",
"region": "global",
"currency": "USD",
"billingCycle": "usage",
"importerKey": "import_catalog_seed_verification.go",
"notes": "xAI 官方 API 目前按每百万 Token 及工具调用计费,也支持批处理折扣和月末账单。"
}
]
}

View File

@@ -0,0 +1,734 @@
{
"checkedAt": "2026-05-14T00:00:00+08:00",
"items": [
{
"catalogCode": "tencent-cloud-token-plan-personal",
"providerName": "Tencent",
"providerNameCn": "腾讯",
"providerCountry": "CN",
"providerWebsite": "https://cloud.tencent.com",
"operatorName": "Tencent Cloud",
"operatorNameCn": "腾讯云",
"operatorCountry": "CN",
"operatorWebsite": "https://cloud.tencent.com",
"operatorType": "cloud",
"platformName": "Tencent Cloud TokenHub",
"platformNameCn": "腾讯云 TokenHub",
"platformType": "cloud_operator",
"planFamily": "token_plan",
"planStatus": "confirmed",
"sourceKind": "official_doc",
"sourceURL": "https://cloud.tencent.com/document/product/1823/130060",
"sourceTitle": "Token Plan 个人版套餐概览",
"region": "CN",
"currency": "CNY",
"billingCycle": "monthly",
"importerKey": "tencent_catalog",
"notes": "腾讯云个人版 Token Plan 基础入口。",
"catalogSegment": "relay_top20plus",
"marketRank": 1
},
{
"catalogCode": "tencent-cloud-token-plan-enterprise-pro",
"providerName": "Tencent",
"providerNameCn": "腾讯",
"providerCountry": "CN",
"providerWebsite": "https://cloud.tencent.com",
"operatorName": "Tencent Cloud",
"operatorNameCn": "腾讯云",
"operatorCountry": "CN",
"operatorWebsite": "https://cloud.tencent.com",
"operatorType": "cloud",
"platformName": "Tencent Cloud TokenHub",
"platformNameCn": "腾讯云 TokenHub",
"platformType": "cloud_operator",
"planFamily": "token_plan",
"planStatus": "confirmed",
"sourceKind": "official_doc",
"sourceURL": "https://cloud.tencent.com/document/product/1823/130659",
"sourceTitle": "Token Plan 企业版专业套餐",
"region": "CN",
"currency": "CNY",
"billingCycle": "monthly",
"importerKey": "tencent_catalog",
"notes": "腾讯云企业版专业套餐。",
"catalogSegment": "relay_top20plus",
"marketRank": 1
},
{
"catalogCode": "tencent-cloud-token-plan-enterprise-lite",
"providerName": "Tencent",
"providerNameCn": "腾讯",
"providerCountry": "CN",
"providerWebsite": "https://cloud.tencent.com",
"operatorName": "Tencent Cloud",
"operatorNameCn": "腾讯云",
"operatorCountry": "CN",
"operatorWebsite": "https://cloud.tencent.com",
"operatorType": "cloud",
"platformName": "Tencent Cloud TokenHub",
"platformNameCn": "腾讯云 TokenHub",
"platformType": "cloud_operator",
"planFamily": "token_plan",
"planStatus": "confirmed",
"sourceKind": "official_doc",
"sourceURL": "https://cloud.tencent.com/document/product/1823/131173",
"sourceTitle": "Token Plan 企业版轻享套餐",
"region": "CN",
"currency": "CNY",
"billingCycle": "monthly",
"importerKey": "tencent_catalog",
"notes": "腾讯云企业版轻享套餐。",
"catalogSegment": "relay_top20plus",
"marketRank": 1
},
{
"catalogCode": "tencent-cloud-coding-plan",
"providerName": "Tencent",
"providerNameCn": "腾讯",
"providerCountry": "CN",
"providerWebsite": "https://cloud.tencent.com",
"operatorName": "Tencent Cloud",
"operatorNameCn": "腾讯云",
"operatorCountry": "CN",
"operatorWebsite": "https://cloud.tencent.com",
"operatorType": "cloud",
"platformName": "Tencent Cloud TokenHub",
"platformNameCn": "腾讯云 TokenHub",
"platformType": "cloud_operator",
"planFamily": "coding_plan",
"planStatus": "confirmed",
"sourceKind": "official_doc",
"sourceURL": "https://cloud.tencent.com/document/product/1823/130103",
"sourceTitle": "Coding Plan 常见问题",
"region": "CN",
"currency": "CNY",
"billingCycle": "monthly",
"importerKey": "tencent_catalog",
"notes": "腾讯云 Coding Plan 入口。",
"catalogSegment": "relay_top20plus",
"marketRank": 1
},
{
"catalogCode": "aliyun-bailian-token-plan-team",
"providerName": "Alibaba",
"providerNameCn": "阿里巴巴",
"providerCountry": "CN",
"providerWebsite": "https://www.aliyun.com",
"operatorName": "Alibaba Bailian",
"operatorNameCn": "阿里云百炼",
"operatorCountry": "CN",
"operatorWebsite": "https://help.aliyun.com/zh/model-studio/",
"operatorType": "cloud",
"platformName": "Alibaba Cloud Model Studio",
"platformNameCn": "阿里云百炼",
"platformType": "cloud_operator",
"planFamily": "token_plan",
"planStatus": "confirmed",
"sourceKind": "official_doc",
"sourceURL": "https://help.aliyun.com/zh/model-studio/token-plan-overview",
"sourceTitle": "Token Plan团队版概述",
"region": "CN",
"currency": "CNY",
"billingCycle": "monthly",
"importerKey": "import_aliyun_subscription.go",
"notes": "百炼团队版 Token Plan。",
"catalogSegment": "relay_top20plus",
"marketRank": 2
},
{
"catalogCode": "aliyun-bailian-coding-plan",
"providerName": "Alibaba",
"providerNameCn": "阿里巴巴",
"providerCountry": "CN",
"providerWebsite": "https://www.aliyun.com",
"operatorName": "Alibaba Bailian",
"operatorNameCn": "阿里云百炼",
"operatorCountry": "CN",
"operatorWebsite": "https://help.aliyun.com/zh/model-studio/",
"operatorType": "cloud",
"platformName": "Alibaba Cloud Model Studio",
"platformNameCn": "阿里云百炼",
"platformType": "cloud_operator",
"planFamily": "coding_plan",
"planStatus": "confirmed",
"sourceKind": "official_doc",
"sourceURL": "https://help.aliyun.com/zh/model-studio/coding-plan-quickstart",
"sourceTitle": "Coding Plan概述",
"region": "CN",
"currency": "CNY",
"billingCycle": "monthly",
"importerKey": "import_aliyun_subscription.go",
"notes": "百炼 Coding Plan。",
"catalogSegment": "relay_top20plus",
"marketRank": 2
},
{
"catalogCode": "baidu-qianfan-token-benefit-pack",
"providerName": "Baidu",
"providerNameCn": "百度",
"providerCountry": "CN",
"providerWebsite": "https://cloud.baidu.com",
"operatorName": "Baidu Qianfan",
"operatorNameCn": "百度千帆",
"operatorCountry": "CN",
"operatorWebsite": "https://cloud.baidu.com/doc/qianfan/index.html",
"operatorType": "cloud",
"platformName": "Baidu Qianfan",
"platformNameCn": "百度千帆",
"platformType": "cloud_operator",
"planFamily": "token_plan",
"planStatus": "confirmed",
"sourceKind": "official_doc",
"sourceURL": "https://cloud.baidu.com/doc/qianfan/s/Smoghsq3g",
"sourceTitle": "Token 福利包",
"region": "CN",
"currency": "CNY",
"billingCycle": "monthly",
"importerKey": "import_baidu_subscription.go",
"notes": "百度千帆 Token 福利包。",
"catalogSegment": "relay_top20plus",
"marketRank": 3
},
{
"catalogCode": "baidu-qianfan-coding-plan",
"providerName": "Baidu",
"providerNameCn": "百度",
"providerCountry": "CN",
"providerWebsite": "https://cloud.baidu.com",
"operatorName": "Baidu Qianfan",
"operatorNameCn": "百度千帆",
"operatorCountry": "CN",
"operatorWebsite": "https://cloud.baidu.com/doc/qianfan/index.html",
"operatorType": "cloud",
"platformName": "Baidu Qianfan",
"platformNameCn": "百度千帆",
"platformType": "cloud_operator",
"planFamily": "coding_plan",
"planStatus": "confirmed",
"sourceKind": "official_doc",
"sourceURL": "https://cloud.baidu.com/doc/qianfan/s/imlg0beiu",
"sourceTitle": "Coding Plan",
"region": "CN",
"currency": "CNY",
"billingCycle": "monthly",
"importerKey": "import_baidu_subscription.go",
"notes": "百度千帆 Coding Plan。",
"catalogSegment": "relay_top20plus",
"marketRank": 3
},
{
"catalogCode": "volcengine-ark-coding-plan",
"providerName": "ByteDance",
"providerNameCn": "字节跳动",
"providerCountry": "CN",
"providerWebsite": "https://www.volcengine.com",
"operatorName": "ByteDance Volcano",
"operatorNameCn": "火山引擎",
"operatorCountry": "CN",
"operatorWebsite": "https://developer.volcengine.com",
"operatorType": "cloud",
"platformName": "Volcengine Ark",
"platformNameCn": "火山方舟",
"platformType": "cloud_operator",
"planFamily": "coding_plan",
"planStatus": "confirmed",
"sourceKind": "official_community",
"sourceURL": "https://developer.volcengine.com/articles/7574419773204004906",
"sourceTitle": "火山方舟新套餐上线:方舟 Coding Plan",
"region": "CN",
"currency": "CNY",
"billingCycle": "monthly",
"importerKey": "import_bytedance_subscription.go",
"notes": "火山方舟 Coding Plan。",
"catalogSegment": "relay_top20plus",
"marketRank": 4
},
{
"catalogCode": "huawei-cloud-maas-package-plan",
"providerName": "Huawei",
"providerNameCn": "华为",
"providerCountry": "CN",
"providerWebsite": "https://www.huaweicloud.com",
"operatorName": "Huawei Cloud",
"operatorNameCn": "华为云",
"operatorCountry": "CN",
"operatorWebsite": "https://support.huaweicloud.com",
"operatorType": "cloud",
"platformName": "Huawei Cloud MaaS",
"platformNameCn": "华为云 MaaS",
"platformType": "cloud_operator",
"planFamily": "package_plan",
"planStatus": "confirmed",
"sourceKind": "official_doc",
"sourceURL": "https://support.huaweicloud.com/price-maas/price-maas-0002.html",
"sourceTitle": "MaaS文本生成模型",
"region": "CN",
"currency": "CNY",
"billingCycle": "monthly",
"importerKey": "import_huawei_package.go",
"notes": "华为云 MaaS 套餐包入口。",
"catalogSegment": "relay_top20plus",
"marketRank": 5
},
{
"catalogCode": "tencent-cloudbase-ai-plus",
"providerName": "Tencent",
"providerNameCn": "腾讯",
"providerCountry": "CN",
"providerWebsite": "https://cloud.tencent.com",
"operatorName": "Tencent CloudBase AI+",
"operatorNameCn": "腾讯云开发 CloudBase AI+",
"operatorCountry": "CN",
"operatorWebsite": "https://cloud.tencent.com/product/tcb",
"operatorType": "cloud",
"platformName": "CloudBase AI+",
"platformNameCn": "CloudBase AI+",
"platformType": "cloud_operator",
"planFamily": "unknown",
"planStatus": "confirmed",
"sourceKind": "official_product_page",
"sourceURL": "https://cloud.tencent.com/product/tcb",
"sourceTitle": "云开发 CloudBase",
"region": "CN",
"currency": "CNY",
"billingCycle": "usage",
"importerKey": "manual_review",
"notes": "腾讯云面向智能体和托管场景的 AI+ 平台,套餐细则待补采。",
"catalogSegment": "relay_top20plus",
"marketRank": 6
},
{
"catalogCode": "tencent-ti-model-gallery",
"providerName": "Tencent",
"providerNameCn": "腾讯",
"providerCountry": "CN",
"providerWebsite": "https://cloud.tencent.com",
"operatorName": "Tencent TI Platform",
"operatorNameCn": "腾讯云 TI 平台",
"operatorCountry": "CN",
"operatorWebsite": "https://cloud.tencent.com/product/tione",
"operatorType": "cloud",
"platformName": "Tencent TI Model Gallery",
"platformNameCn": "TI 平台大模型广场",
"platformType": "cloud_operator",
"planFamily": "unknown",
"planStatus": "confirmed",
"sourceKind": "official_product_page",
"sourceURL": "https://cloud.tencent.com/product/tione",
"sourceTitle": "TI 平台",
"region": "CN",
"currency": "CNY",
"billingCycle": "usage",
"importerKey": "manual_review",
"notes": "腾讯云模型广场与训练推理平台,订阅细则待补。",
"catalogSegment": "relay_top20plus",
"marketRank": 7
},
{
"catalogCode": "aliyun-modelscope-api-inference",
"providerName": "Alibaba",
"providerNameCn": "阿里巴巴",
"providerCountry": "CN",
"providerWebsite": "https://modelscope.cn",
"operatorName": "ModelScope API-Inference",
"operatorNameCn": "魔搭 API-Inference",
"operatorCountry": "CN",
"operatorWebsite": "https://modelscope.cn",
"operatorType": "relay",
"platformName": "ModelScope API-Inference",
"platformNameCn": "魔搭 API-Inference",
"platformType": "relay_platform",
"planFamily": "pay_as_you_go",
"planStatus": "confirmed",
"sourceKind": "official_product_page",
"sourceURL": "https://modelscope.cn/docs/model-service/API-Inference/intro",
"sourceTitle": "API-Inference 简介",
"region": "CN",
"currency": "CNY",
"billingCycle": "usage",
"importerKey": "import_youdao_pricing.go",
"notes": "魔搭社区提供统一模型推理网关。",
"catalogSegment": "relay_top20plus",
"marketRank": 8
},
{
"catalogCode": "ctyun-token-plan",
"providerName": "Telecom",
"providerNameCn": "中国电信",
"providerCountry": "CN",
"providerWebsite": "https://www.ctyun.cn",
"operatorName": "CTYun",
"operatorNameCn": "天翼云",
"operatorCountry": "CN",
"operatorWebsite": "https://www.ctyun.cn",
"operatorType": "cloud",
"platformName": "CTYun Model Inference Service",
"platformNameCn": "天翼云模型推理服务",
"platformType": "cloud_operator",
"planFamily": "token_plan",
"planStatus": "confirmed",
"sourceKind": "official_product_page",
"sourceURL": "https://www.ctyun.cn/",
"sourceTitle": "天翼云模型推理服务",
"region": "CN",
"currency": "CNY",
"billingCycle": "monthly",
"importerKey": "import_ctyun_subscription.go",
"notes": "已确认存在 Token Plan开发版与个人版均有多档月套餐。",
"catalogSegment": "relay_top20plus",
"marketRank": 9
},
{
"catalogCode": "ctyun-model-inference-payg",
"providerName": "Telecom",
"providerNameCn": "中国电信",
"providerCountry": "CN",
"providerWebsite": "https://www.ctyun.cn",
"operatorName": "CTYun",
"operatorNameCn": "天翼云",
"operatorCountry": "CN",
"operatorWebsite": "https://www.ctyun.cn",
"operatorType": "cloud",
"platformName": "CTYun Model Inference Service",
"platformNameCn": "天翼云模型推理服务",
"platformType": "cloud_operator",
"planFamily": "pay_as_you_go",
"planStatus": "confirmed",
"sourceKind": "official_product_page",
"sourceURL": "https://www.ctyun.cn/",
"sourceTitle": "天翼云模型推理服务",
"region": "CN",
"currency": "CNY",
"billingCycle": "usage",
"importerKey": "import_360_pricing.go",
"notes": "与 Token Plan 并行提供按量计费。",
"catalogSegment": "relay_top20plus",
"marketRank": 9
},
{
"catalogCode": "ctyun-coding-plan",
"providerName": "Telecom",
"providerNameCn": "中国电信",
"providerCountry": "CN",
"providerWebsite": "https://www.ctyun.cn",
"operatorName": "CTYun",
"operatorNameCn": "天翼云",
"operatorCountry": "CN",
"operatorWebsite": "https://www.ctyun.cn",
"operatorType": "cloud",
"platformName": "CTYun Model Inference Service",
"platformNameCn": "天翼云模型推理服务",
"platformType": "cloud_operator",
"planFamily": "coding_plan",
"planStatus": "confirmed",
"sourceKind": "official_product_page",
"sourceURL": "https://www.ctyun.cn/",
"sourceTitle": "天翼云模型推理服务",
"region": "CN",
"currency": "CNY",
"billingCycle": "monthly",
"importerKey": "import_ctyun_subscription.go",
"notes": "已确认存在 Coding Plan含 Lite / Pro / Max 多档。",
"catalogSegment": "relay_top20plus",
"marketRank": 9
},
{
"catalogCode": "ctyun-xirang-platform",
"providerName": "Telecom",
"providerNameCn": "中国电信",
"providerCountry": "CN",
"providerWebsite": "https://www.ctyun.cn",
"operatorName": "CTYun Xirang",
"operatorNameCn": "天翼云息壤",
"operatorCountry": "CN",
"operatorWebsite": "https://www.ctyun.cn",
"operatorType": "cloud",
"platformName": "CTYun Xirang",
"platformNameCn": "天翼云息壤",
"platformType": "cloud_operator",
"planFamily": "unknown",
"planStatus": "confirmed",
"sourceKind": "official_product_page",
"sourceURL": "https://www.ctyun.cn/",
"sourceTitle": "天翼云息壤",
"region": "CN",
"currency": "CNY",
"billingCycle": "usage",
"importerKey": "manual_review",
"notes": "作为天翼云智能体与应用开发平台入口,后续补细分套餐。",
"catalogSegment": "relay_top20plus",
"marketRank": 10
},
{
"catalogCode": "cucloud-aicp-platform",
"providerName": "China Unicom",
"providerNameCn": "中国联通",
"providerCountry": "CN",
"providerWebsite": "https://www.cucloud.cn",
"operatorName": "Unicom AICP",
"operatorNameCn": "联通云 AICP",
"operatorCountry": "CN",
"operatorWebsite": "https://www.cucloud.cn",
"operatorType": "cloud",
"platformName": "Unicom AICP",
"platformNameCn": "联通云 AICP",
"platformType": "cloud_operator",
"planFamily": "pay_as_you_go",
"planStatus": "confirmed",
"sourceKind": "official_product_page",
"sourceURL": "https://www.cucloud.cn/act/CloudAI.html",
"sourceTitle": "联通云智算专区",
"region": "CN",
"currency": "CNY",
"billingCycle": "usage",
"importerKey": "import_cucloud_catalog.go",
"notes": "联通云多模型与应用开发平台入口之一。",
"catalogSegment": "relay_top20plus",
"marketRank": 11
},
{
"catalogCode": "cucloud-ai-app-platform",
"providerName": "China Unicom",
"providerNameCn": "中国联通",
"providerCountry": "CN",
"providerWebsite": "https://www.cucloud.cn",
"operatorName": "Unicom AI App Platform",
"operatorNameCn": "联通云 AI 应用开发平台",
"operatorCountry": "CN",
"operatorWebsite": "https://www.cucloud.cn",
"operatorType": "cloud",
"platformName": "Unicom AI App Platform",
"platformNameCn": "联通云 AI 应用开发平台",
"platformType": "cloud_operator",
"planFamily": "pay_as_you_go",
"planStatus": "confirmed",
"sourceKind": "official_product_page",
"sourceURL": "https://www.cucloud.cn/act/CloudAI.html",
"sourceTitle": "联通云智算专区",
"region": "CN",
"currency": "CNY",
"billingCycle": "usage",
"importerKey": "import_cucloud_catalog.go",
"notes": "联通云智能体/应用侧聚合平台。",
"catalogSegment": "relay_top20plus",
"marketRank": 12
},
{
"catalogCode": "mobile-cloud-ai-market",
"providerName": "China Mobile",
"providerNameCn": "中国移动",
"providerCountry": "CN",
"providerWebsite": "https://ecloud.10086.cn",
"operatorName": "Mobile Cloud",
"operatorNameCn": "移动云",
"operatorCountry": "CN",
"operatorWebsite": "https://ecloud.10086.cn",
"operatorType": "cloud",
"platformName": "Mobile Cloud AI Market",
"platformNameCn": "移动云 AI 应用专区",
"platformType": "cloud_operator",
"planFamily": "unknown",
"planStatus": "confirmed",
"sourceKind": "official_product_page",
"sourceURL": "https://saas.ecloud.10086.cn/Store/List",
"sourceTitle": "移动云市场 AI 应用专区",
"region": "CN",
"currency": "CNY",
"billingCycle": "usage",
"importerKey": "import_mobile_cloud_catalog.go",
"notes": "已确认移动云云市场公开展示 AI 应用专区,公开统一编程套餐价格仍待后续核验。",
"catalogSegment": "relay_top20plus",
"marketRank": 13
},
{
"catalogCode": "youdao-zhiyun-maas",
"providerName": "NetEase Youdao",
"providerNameCn": "网易有道",
"providerCountry": "CN",
"providerWebsite": "https://sf.163.com",
"operatorName": "Youdao Zhiyun MaaS",
"operatorNameCn": "有道智云 MaaS",
"operatorCountry": "CN",
"operatorWebsite": "https://sf.163.com",
"operatorType": "relay",
"platformName": "Youdao Zhiyun MaaS",
"platformNameCn": "有道智云 MaaS",
"platformType": "relay_platform",
"planFamily": "pay_as_you_go",
"planStatus": "confirmed",
"sourceKind": "official_product_page",
"sourceURL": "https://sf.163.com/product/maas",
"sourceTitle": "有道智云 MaaS",
"region": "CN",
"currency": "CNY",
"billingCycle": "usage",
"importerKey": "import_youdao_pricing.go",
"notes": "支持多模型与渠道分销。",
"catalogSegment": "relay_top20plus",
"marketRank": 14
},
{
"catalogCode": "360-open-platform",
"providerName": "360",
"providerNameCn": "360",
"providerCountry": "CN",
"providerWebsite": "https://openmodel.360.cn",
"operatorName": "360 Open Platform",
"operatorNameCn": "360 智脑开放平台",
"operatorCountry": "CN",
"operatorWebsite": "https://openmodel.360.cn",
"operatorType": "relay",
"platformName": "360 ZhiNao Open Platform",
"platformNameCn": "360 智脑开放平台",
"platformType": "relay_platform",
"planFamily": "pay_as_you_go",
"planStatus": "confirmed",
"sourceKind": "official_product_page",
"sourceURL": "https://openmodel.360.cn/",
"sourceTitle": "360 智脑开放平台",
"region": "CN",
"currency": "CNY",
"billingCycle": "usage",
"importerKey": "import_360_pricing.go",
"notes": "多模型开放接口平台。",
"catalogSegment": "relay_top20plus",
"marketRank": 15
},
{
"catalogCode": "siliconflow-siliconcloud",
"providerName": "SiliconFlow",
"providerNameCn": "硅基流动",
"providerCountry": "CN",
"providerWebsite": "https://siliconflow.cn",
"operatorName": "SiliconCloud",
"operatorNameCn": "SiliconCloud",
"operatorCountry": "CN",
"operatorWebsite": "https://siliconflow.cn",
"operatorType": "relay",
"platformName": "SiliconCloud",
"platformNameCn": "硅基流动云平台",
"platformType": "relay_platform",
"planFamily": "pay_as_you_go",
"planStatus": "confirmed",
"sourceKind": "official_product_page",
"sourceURL": "https://siliconflow.cn/zh-cn/siliconcloud",
"sourceTitle": "SiliconCloud",
"region": "CN",
"currency": "CNY",
"billingCycle": "usage",
"importerKey": "import_siliconflow_pricing.go",
"notes": "国内主流聚合中转平台之一。",
"catalogSegment": "relay_top20plus",
"marketRank": 16
},
{
"catalogCode": "ppio-model-api",
"providerName": "PPIO",
"providerNameCn": "PPIO",
"providerCountry": "CN",
"providerWebsite": "https://ppinfra.com",
"operatorName": "PPIO Model API",
"operatorNameCn": "PPIO 模型 API",
"operatorCountry": "CN",
"operatorWebsite": "https://ppinfra.com",
"operatorType": "relay",
"platformName": "PPIO Model API",
"platformNameCn": "PPIO 模型 API",
"platformType": "relay_platform",
"planFamily": "pay_as_you_go",
"planStatus": "confirmed",
"sourceKind": "official_product_page",
"sourceURL": "https://ppinfra.com/model-api",
"sourceTitle": "PPIO Model API",
"region": "CN",
"currency": "CNY",
"billingCycle": "usage",
"importerKey": "import_ppio_pricing.go",
"notes": "聚合多家模型服务的 API 网关。",
"catalogSegment": "relay_top20plus",
"marketRank": 17
},
{
"catalogCode": "ucloud-umodelverse",
"providerName": "UCloud",
"providerNameCn": "UCloud",
"providerCountry": "CN",
"providerWebsite": "https://www.ucloud.cn",
"operatorName": "UModelVerse",
"operatorNameCn": "UModelVerse",
"operatorCountry": "CN",
"operatorWebsite": "https://www.ucloud.cn",
"operatorType": "cloud",
"platformName": "UModelVerse",
"platformNameCn": "UModelVerse",
"platformType": "relay_platform",
"planFamily": "pay_as_you_go",
"planStatus": "confirmed",
"sourceKind": "official_product_page",
"sourceURL": "https://www.ucloud.cn/site/product/large-model-service.html",
"sourceTitle": "大模型服务平台 UModelVerse",
"region": "CN",
"currency": "CNY",
"billingCycle": "usage",
"importerKey": "import_ucloud_pricing.go",
"notes": "UCloud 大模型服务聚合平台。",
"catalogSegment": "relay_top20plus",
"marketRank": 18
},
{
"catalogCode": "qingcloud-coreshub",
"providerName": "QingCloud",
"providerNameCn": "青云科技",
"providerCountry": "CN",
"providerWebsite": "https://www.qingcloud.com",
"operatorName": "CoresHub",
"operatorNameCn": "CoresHub",
"operatorCountry": "CN",
"operatorWebsite": "https://www.qingcloud.com",
"operatorType": "cloud",
"platformName": "CoresHub",
"platformNameCn": "基石智算 CoresHub",
"platformType": "relay_platform",
"planFamily": "pay_as_you_go",
"planStatus": "confirmed",
"sourceKind": "official_product_page",
"sourceURL": "https://www.qingcloud.com/products/coreshub",
"sourceTitle": "CoresHub",
"region": "CN",
"currency": "CNY",
"billingCycle": "usage",
"importerKey": "import_catalog_seed_verification.go",
"notes": "青云基石智算模型服务入口。",
"catalogSegment": "relay_top20plus",
"marketRank": 19
},
{
"catalogCode": "ksyun-xingliu-platform",
"providerName": "Kingsoft Cloud",
"providerNameCn": "金山云",
"providerCountry": "CN",
"providerWebsite": "https://www.ksyun.com",
"operatorName": "Xingliu Platform",
"operatorNameCn": "星流平台",
"operatorCountry": "CN",
"operatorWebsite": "https://www.ksyun.com",
"operatorType": "cloud",
"platformName": "Xingliu Platform",
"platformNameCn": "金山云星流平台",
"platformType": "relay_platform",
"planFamily": "pay_as_you_go",
"planStatus": "confirmed",
"sourceKind": "official_product_page",
"sourceURL": "https://www.ksyun.com",
"sourceTitle": "金山云星流平台",
"region": "CN",
"currency": "CNY",
"billingCycle": "usage",
"importerKey": "import_catalog_seed_verification.go",
"notes": "金山云多模型与推理服务平台。",
"catalogSegment": "relay_top20plus",
"marketRank": 20
}
]
}

View File

@@ -0,0 +1,545 @@
{
"checkedAt": "2026-05-14T00:00:00+08:00",
"items": [
{
"catalogCode": "alibaba-qwen-api-payg",
"providerName": "Alibaba",
"providerNameCn": "阿里巴巴",
"providerCountry": "CN",
"providerWebsite": "https://www.aliyun.com",
"operatorName": "DashScope",
"operatorNameCn": "通义千问 API",
"operatorCountry": "CN",
"operatorWebsite": "https://help.aliyun.com/zh/model-studio/",
"operatorType": "official",
"platformName": "Alibaba Qwen API",
"platformNameCn": "通义千问开放平台",
"platformType": "official_vendor",
"planFamily": "pay_as_you_go",
"planStatus": "confirmed",
"sourceKind": "official_pricing",
"sourceURL": "https://help.aliyun.com/zh/model-studio/model-user-guide/what-is-model-studio",
"sourceTitle": "什么是大模型服务平台百炼",
"region": "CN",
"currency": "CNY",
"billingCycle": "usage",
"importerKey": "import_catalog_seed_verification.go",
"notes": "按量计费为主,套餐型信息单独由百炼 Token Plan/Coding Plan 行承载。",
"catalogSegment": "vendor_top20",
"marketRank": 1
},
{
"catalogCode": "tencent-hunyuan-api-payg",
"providerName": "Tencent",
"providerNameCn": "腾讯",
"providerCountry": "CN",
"providerWebsite": "https://cloud.tencent.com",
"operatorName": "Tencent Hunyuan",
"operatorNameCn": "腾讯混元",
"operatorCountry": "CN",
"operatorWebsite": "https://cloud.tencent.com/product/hunyuan",
"operatorType": "official",
"platformName": "Tencent Hunyuan API",
"platformNameCn": "腾讯混元开放平台",
"platformType": "official_vendor",
"planFamily": "pay_as_you_go",
"planStatus": "confirmed",
"sourceKind": "official_product_page",
"sourceURL": "https://cloud.tencent.com/product/hunyuan",
"sourceTitle": "腾讯混元",
"region": "CN",
"currency": "CNY",
"billingCycle": "usage",
"importerKey": "import_catalog_seed_verification.go",
"notes": "腾讯官方模型能力入口,订阅套餐另走腾讯云 TokenHub。",
"catalogSegment": "vendor_top20",
"marketRank": 2
},
{
"catalogCode": "baidu-ernie-api-payg",
"providerName": "Baidu",
"providerNameCn": "百度",
"providerCountry": "CN",
"providerWebsite": "https://cloud.baidu.com",
"operatorName": "Baidu Qianfan",
"operatorNameCn": "百度千帆",
"operatorCountry": "CN",
"operatorWebsite": "https://cloud.baidu.com/doc/qianfan/index.html",
"operatorType": "official",
"platformName": "Baidu ERNIE API",
"platformNameCn": "文心大模型开放平台",
"platformType": "official_vendor",
"planFamily": "pay_as_you_go",
"planStatus": "confirmed",
"sourceKind": "official_product_page",
"sourceURL": "https://cloud.baidu.com/product/wenxinworkshop",
"sourceTitle": "文心千帆大模型平台",
"region": "CN",
"currency": "CNY",
"billingCycle": "usage",
"importerKey": "existing_price_importer",
"notes": "百度官方模型能力由千帆平台承载,订阅套餐单独由 Coding Plan / Token 福利包记录。",
"catalogSegment": "vendor_top20",
"marketRank": 3
},
{
"catalogCode": "bytedance-doubao-api-payg",
"providerName": "ByteDance",
"providerNameCn": "字节跳动",
"providerCountry": "CN",
"providerWebsite": "https://www.volcengine.com",
"operatorName": "ByteDance Volcano",
"operatorNameCn": "火山引擎",
"operatorCountry": "CN",
"operatorWebsite": "https://developer.volcengine.com",
"operatorType": "official",
"platformName": "Doubao / Seed API",
"platformNameCn": "豆包与 Seed 开放平台",
"platformType": "official_vendor",
"planFamily": "pay_as_you_go",
"planStatus": "confirmed",
"sourceKind": "official_product_page",
"sourceURL": "https://www.volcengine.com/product/ark",
"sourceTitle": "火山方舟",
"region": "CN",
"currency": "CNY",
"billingCycle": "usage",
"importerKey": "existing_price_importer",
"notes": "官方模型与平台由火山方舟统一暴露Coding Plan 另作订阅目录记录。",
"catalogSegment": "vendor_top20",
"marketRank": 4
},
{
"catalogCode": "zhipu-glm-coding-plan",
"providerName": "Zhipu AI",
"providerNameCn": "智谱 AI",
"providerCountry": "CN",
"providerWebsite": "https://open.bigmodel.cn",
"operatorName": "Zhipu",
"operatorNameCn": "智谱开放平台",
"operatorCountry": "CN",
"operatorWebsite": "https://docs.bigmodel.cn/",
"operatorType": "official",
"platformName": "Zhipu Coding Platform",
"platformNameCn": "智谱 Coding Plan",
"platformType": "official_vendor",
"planFamily": "coding_plan",
"planStatus": "confirmed",
"sourceKind": "official_doc",
"sourceURL": "https://docs.bigmodel.cn/cn/coding-plan/overview",
"sourceTitle": "套餐概览",
"region": "CN",
"currency": "CNY",
"billingCycle": "monthly",
"importerKey": "import_zhipu_coding_plan.go",
"notes": "智谱已上线 GLM Coding Plan并为多种代码助手提供接入入口。",
"catalogSegment": "vendor_top20",
"marketRank": 5
},
{
"catalogCode": "huawei-pangu-api-payg",
"providerName": "Huawei",
"providerNameCn": "华为",
"providerCountry": "CN",
"providerWebsite": "https://www.huaweicloud.com",
"operatorName": "Huawei Cloud MaaS",
"operatorNameCn": "华为云 MaaS",
"operatorCountry": "CN",
"operatorWebsite": "https://support.huaweicloud.com",
"operatorType": "official",
"platformName": "Huawei Pangu API",
"platformNameCn": "盘古大模型服务",
"platformType": "official_vendor",
"planFamily": "pay_as_you_go",
"planStatus": "confirmed",
"sourceKind": "official_product_page",
"sourceURL": "https://www.huaweicloud.com/product/maas.html",
"sourceTitle": "大模型即服务 MaaS",
"region": "CN",
"currency": "CNY",
"billingCycle": "usage",
"importerKey": "import_catalog_seed_verification.go",
"notes": "当前公开能力同时存在按量计费与资源包计费两类入口。",
"catalogSegment": "vendor_top20",
"marketRank": 6
},
{
"catalogCode": "deepseek-api-payg",
"providerName": "DeepSeek",
"providerNameCn": "DeepSeek",
"providerCountry": "CN",
"providerWebsite": "https://api-docs.deepseek.com",
"operatorName": "DeepSeek",
"operatorNameCn": "DeepSeek",
"operatorCountry": "CN",
"operatorWebsite": "https://api-docs.deepseek.com",
"operatorType": "official",
"platformName": "DeepSeek API",
"platformNameCn": "DeepSeek API",
"platformType": "official_vendor",
"planFamily": "pay_as_you_go",
"planStatus": "confirmed",
"sourceKind": "official_pricing",
"sourceURL": "https://api-docs.deepseek.com/zh-cn/quick_start/pricing/",
"sourceTitle": "模型 & 价格",
"region": "global",
"currency": "CNY",
"billingCycle": "usage",
"importerKey": "existing_price_importer",
"notes": "当前官方以按量计费与限时折扣为主。",
"catalogSegment": "vendor_top20",
"marketRank": 7
},
{
"catalogCode": "moonshot-api-payg",
"providerName": "Moonshot AI",
"providerNameCn": "Moonshot AI",
"providerCountry": "CN",
"providerWebsite": "https://platform.moonshot.cn",
"operatorName": "Moonshot",
"operatorNameCn": "Moonshot",
"operatorCountry": "CN",
"operatorWebsite": "https://platform.moonshot.cn",
"operatorType": "official",
"platformName": "Kimi API Platform",
"platformNameCn": "Kimi API 开放平台",
"platformType": "official_vendor",
"planFamily": "pay_as_you_go",
"planStatus": "confirmed",
"sourceKind": "official_pricing",
"sourceURL": "https://platform.moonshot.cn/docs/pricing/chat",
"sourceTitle": "模型推理价格说明",
"region": "global",
"currency": "CNY",
"billingCycle": "usage",
"importerKey": "existing_price_importer",
"notes": "官方重点仍是 Token 单价与缓存计费。",
"catalogSegment": "vendor_top20",
"marketRank": 8
},
{
"catalogCode": "minimax-token-plan",
"providerName": "MiniMax",
"providerNameCn": "MiniMax",
"providerCountry": "CN",
"providerWebsite": "https://platform.minimaxi.com",
"operatorName": "MiniMax",
"operatorNameCn": "MiniMax",
"operatorCountry": "CN",
"operatorWebsite": "https://platform.minimaxi.com/docs/pricing/overview",
"operatorType": "official",
"platformName": "MiniMax Open Platform",
"platformNameCn": "MiniMax 开放平台",
"platformType": "official_vendor",
"planFamily": "token_plan",
"planStatus": "confirmed",
"sourceKind": "official_doc",
"sourceURL": "https://platform.minimaxi.com/docs/token-plan/intro",
"sourceTitle": "Token Plan 概要",
"region": "CN",
"currency": "CNY",
"billingCycle": "monthly",
"importerKey": "import_minimax_subscription.go",
"notes": "已提供 Token Plan超额后可切换到按量计费 API Key当前已进入真实 Token Plan 抓取链路。",
"catalogSegment": "vendor_top20",
"marketRank": 9
},
{
"catalogCode": "stepfun-step-plan",
"providerName": "StepFun",
"providerNameCn": "阶跃星辰",
"providerCountry": "CN",
"providerWebsite": "https://platform.stepfun.com",
"operatorName": "StepFun",
"operatorNameCn": "阶跃开放平台",
"operatorCountry": "CN",
"operatorWebsite": "https://platform.stepfun.com/docs",
"operatorType": "official",
"platformName": "StepFun Step Plan",
"platformNameCn": "Step Plan",
"platformType": "official_vendor",
"planFamily": "coding_plan",
"planStatus": "confirmed",
"sourceKind": "official_doc",
"sourceURL": "https://platform.stepfun.com/docs/zh/guide/step-plan/step-plan-intro",
"sourceTitle": "Step Plan 简介",
"region": "CN",
"currency": "CNY",
"billingCycle": "monthly",
"importerKey": "manual_review",
"notes": "阶跃官方已提供面向代码场景的 Step Plan 套餐体系。",
"catalogSegment": "vendor_top20",
"marketRank": 10
},
{
"catalogCode": "baichuan-api-payg",
"providerName": "Baichuan",
"providerNameCn": "百川智能",
"providerCountry": "CN",
"providerWebsite": "https://platform.baichuan-ai.com",
"operatorName": "Baichuan",
"operatorNameCn": "百川开放平台",
"operatorCountry": "CN",
"operatorWebsite": "https://platform.baichuan-ai.com/docs",
"operatorType": "official",
"platformName": "Baichuan API",
"platformNameCn": "百川开放平台",
"platformType": "official_vendor",
"planFamily": "pay_as_you_go",
"planStatus": "confirmed",
"sourceKind": "official_pricing",
"sourceURL": "https://platform.baichuan-ai.com/docs/pricing",
"sourceTitle": "价格说明",
"region": "CN",
"currency": "CNY",
"billingCycle": "usage",
"importerKey": "import_catalog_seed_verification.go",
"notes": "当前以按量价格页为主。",
"catalogSegment": "vendor_top20",
"marketRank": 11
},
{
"catalogCode": "01ai-api-payg",
"providerName": "01.AI",
"providerNameCn": "零一万物",
"providerCountry": "CN",
"providerWebsite": "https://platform.lingyiwanwu.com",
"operatorName": "01.AI",
"operatorNameCn": "零一万物开放平台",
"operatorCountry": "CN",
"operatorWebsite": "https://platform.lingyiwanwu.com/docs",
"operatorType": "official",
"platformName": "01.AI Open Platform",
"platformNameCn": "零一万物开放平台",
"platformType": "official_vendor",
"planFamily": "pay_as_you_go",
"planStatus": "confirmed",
"sourceKind": "official_pricing",
"sourceURL": "https://platform.lingyiwanwu.com/docs",
"sourceTitle": "零一万物开放平台文档",
"region": "CN",
"currency": "CNY",
"billingCycle": "usage",
"importerKey": "import_catalog_seed_verification.go",
"notes": "官方文档可确认 API 平台与模型接入。",
"catalogSegment": "vendor_top20",
"marketRank": 12
},
{
"catalogCode": "sensenova-api-payg",
"providerName": "SenseTime",
"providerNameCn": "商汤科技",
"providerCountry": "CN",
"providerWebsite": "https://platform.sensenova.cn",
"operatorName": "SenseNova",
"operatorNameCn": "日日新开放平台",
"operatorCountry": "CN",
"operatorWebsite": "https://platform.sensenova.cn",
"operatorType": "official",
"platformName": "SenseNova API",
"platformNameCn": "日日新开放平台",
"platformType": "official_vendor",
"planFamily": "pay_as_you_go",
"planStatus": "confirmed",
"sourceKind": "official_pricing",
"sourceURL": "https://platform.sensenova.cn/pricing",
"sourceTitle": "定价",
"region": "CN",
"currency": "CNY",
"billingCycle": "usage",
"importerKey": "import_catalog_seed_verification.go",
"notes": "当前公开为按量价格页。",
"catalogSegment": "vendor_top20",
"marketRank": 13
},
{
"catalogCode": "xfyun-spark-api-payg",
"providerName": "iFlytek",
"providerNameCn": "科大讯飞",
"providerCountry": "CN",
"providerWebsite": "https://www.xfyun.cn",
"operatorName": "Spark API",
"operatorNameCn": "讯飞星火 API",
"operatorCountry": "CN",
"operatorWebsite": "https://www.xfyun.cn/doc/spark/",
"operatorType": "official",
"platformName": "讯飞星火 API",
"platformNameCn": "讯飞星火开放平台",
"platformType": "official_vendor",
"planFamily": "pay_as_you_go",
"planStatus": "confirmed",
"sourceKind": "official_product_page",
"sourceURL": "https://www.xfyun.cn/doc/spark/Web.html",
"sourceTitle": "星火大模型 Web API",
"region": "CN",
"currency": "CNY",
"billingCycle": "usage",
"importerKey": "import_catalog_seed_verification.go",
"notes": "已确认官方 API 文档与开放能力。",
"catalogSegment": "vendor_top20",
"marketRank": 14
},
{
"catalogCode": "360-zhinao-api-payg",
"providerName": "360",
"providerNameCn": "360",
"providerCountry": "CN",
"providerWebsite": "https://openmodel.360.cn",
"operatorName": "360 ZhiNao",
"operatorNameCn": "360 智脑",
"operatorCountry": "CN",
"operatorWebsite": "https://openmodel.360.cn",
"operatorType": "official",
"platformName": "360 ZhiNao API",
"platformNameCn": "360 智脑开放平台",
"platformType": "official_vendor",
"planFamily": "pay_as_you_go",
"planStatus": "confirmed",
"sourceKind": "official_product_page",
"sourceURL": "https://openmodel.360.cn/",
"sourceTitle": "360 智脑开放平台",
"region": "CN",
"currency": "CNY",
"billingCycle": "usage",
"importerKey": "import_catalog_seed_verification.go",
"notes": "已确认官方开放平台入口。",
"catalogSegment": "vendor_top20",
"marketRank": 15
},
{
"catalogCode": "youdao-ziyue-api-payg",
"providerName": "NetEase Youdao",
"providerNameCn": "网易有道",
"providerCountry": "CN",
"providerWebsite": "https://sf.163.com",
"operatorName": "Youdao Ziyue",
"operatorNameCn": "子曰大模型",
"operatorCountry": "CN",
"operatorWebsite": "https://sf.163.com",
"operatorType": "official",
"platformName": "Youdao Ziyue API",
"platformNameCn": "网易有道子曰开放平台",
"platformType": "official_vendor",
"planFamily": "pay_as_you_go",
"planStatus": "confirmed",
"sourceKind": "official_product_page",
"sourceURL": "https://sf.163.com/product/maas",
"sourceTitle": "有道智云 MaaS",
"region": "CN",
"currency": "CNY",
"billingCycle": "usage",
"importerKey": "import_catalog_seed_verification.go",
"notes": "官方可提供 MaaS 与子曰模型接入。",
"catalogSegment": "vendor_top20",
"marketRank": 16
},
{
"catalogCode": "modelbest-minicpm-api-payg",
"providerName": "ModelBest",
"providerNameCn": "面壁智能",
"providerCountry": "CN",
"providerWebsite": "https://platform.modelbest.cn",
"operatorName": "ModelBest",
"operatorNameCn": "面壁开放平台",
"operatorCountry": "CN",
"operatorWebsite": "https://platform.modelbest.cn",
"operatorType": "official",
"platformName": "MiniCPM API",
"platformNameCn": "MiniCPM 开放平台",
"platformType": "official_vendor",
"planFamily": "pay_as_you_go",
"planStatus": "confirmed",
"sourceKind": "official_product_page",
"sourceURL": "https://platform.modelbest.cn/",
"sourceTitle": "面壁开放平台",
"region": "CN",
"currency": "CNY",
"billingCycle": "usage",
"importerKey": "import_catalog_seed_verification.go",
"notes": "官方开放平台可确认模型接入入口。",
"catalogSegment": "vendor_top20",
"marketRank": 17
},
{
"catalogCode": "baai-flagopen-api-payg",
"providerName": "BAAI",
"providerNameCn": "智源研究院",
"providerCountry": "CN",
"providerWebsite": "https://flagopen.baai.ac.cn",
"operatorName": "FlagOpen",
"operatorNameCn": "FlagOpen",
"operatorCountry": "CN",
"operatorWebsite": "https://flagopen.baai.ac.cn",
"operatorType": "official",
"platformName": "FlagOpen API",
"platformNameCn": "智源开放平台",
"platformType": "official_vendor",
"planFamily": "pay_as_you_go",
"planStatus": "confirmed",
"sourceKind": "official_product_page",
"sourceURL": "https://flagopen.baai.ac.cn/",
"sourceTitle": "FlagOpen",
"region": "CN",
"currency": "CNY",
"billingCycle": "usage",
"importerKey": "import_catalog_seed_verification.go",
"notes": "以开放平台和模型服务为主。",
"catalogSegment": "vendor_top20",
"marketRank": 18
},
{
"catalogCode": "skywork-api-payg",
"providerName": "Skywork",
"providerNameCn": "昆仑万维天工",
"providerCountry": "CN",
"providerWebsite": "https://platform.tiangong.cn",
"operatorName": "Tiangong",
"operatorNameCn": "天工开放平台",
"operatorCountry": "CN",
"operatorWebsite": "https://platform.tiangong.cn",
"operatorType": "official",
"platformName": "Skywork / Tiangong API",
"platformNameCn": "天工开放平台",
"platformType": "official_vendor",
"planFamily": "pay_as_you_go",
"planStatus": "confirmed",
"sourceKind": "official_product_page",
"sourceURL": "https://platform.tiangong.cn/",
"sourceTitle": "天工开放平台",
"region": "CN",
"currency": "CNY",
"billingCycle": "usage",
"importerKey": "import_catalog_seed_verification.go",
"notes": "官方开放平台入口已公开。",
"catalogSegment": "vendor_top20",
"marketRank": 19
},
{
"catalogCode": "infinigence-api-payg",
"providerName": "Infinigence AI",
"providerNameCn": "无问芯穹",
"providerCountry": "CN",
"providerWebsite": "https://cloud.infini-ai.com",
"operatorName": "Infinigence Cloud",
"operatorNameCn": "无问芯穹云平台",
"operatorCountry": "CN",
"operatorWebsite": "https://cloud.infini-ai.com",
"operatorType": "official",
"platformName": "Infinigence API",
"platformNameCn": "无问芯穹开放平台",
"platformType": "official_vendor",
"planFamily": "pay_as_you_go",
"planStatus": "confirmed",
"sourceKind": "official_product_page",
"sourceURL": "https://cloud.infini-ai.com/",
"sourceTitle": "无问芯穹云平台",
"region": "CN",
"currency": "CNY",
"billingCycle": "usage",
"importerKey": "import_catalog_seed_verification.go",
"notes": "已确认官方云平台入口。",
"catalogSegment": "vendor_top20",
"marketRank": 20
}
]
}

View File

@@ -0,0 +1,525 @@
{
"checkedAt": "2026-05-15T00:00:00+08:00",
"items": [
{
"catalogCode": "google-gemini-api-payg",
"providerName": "Google",
"providerNameCn": "谷歌",
"providerCountry": "US",
"providerWebsite": "https://ai.google.dev",
"operatorName": "Google Gemini API",
"operatorNameCn": "Google Gemini API",
"operatorCountry": "US",
"operatorWebsite": "https://ai.google.dev",
"operatorType": "official",
"platformName": "Gemini API",
"platformNameCn": "Gemini API",
"platformType": "official_vendor",
"planFamily": "pay_as_you_go",
"planStatus": "confirmed",
"sourceKind": "official_pricing",
"sourceURL": "https://ai.google.dev/gemini-api/docs/pricing",
"sourceTitle": "Gemini API billing information",
"region": "global",
"currency": "USD",
"importerKey": "import_catalog_seed_verification.go",
"notes": "web 搜索已确认 Gemini Developer API 官方价格页,当前以按量计费为主并附带免费层说明。",
"catalogSegment": "global_reference",
"marketRank": 1
},
{
"catalogCode": "mistral-api-payg",
"providerName": "Mistral AI",
"providerNameCn": "Mistral AI",
"providerCountry": "FR",
"providerWebsite": "https://mistral.ai",
"operatorName": "Mistral La Plateforme",
"operatorNameCn": "Mistral La Plateforme",
"operatorCountry": "FR",
"operatorWebsite": "https://mistral.ai/products/la-plateforme",
"operatorType": "official",
"platformName": "Mistral La Plateforme",
"platformNameCn": "Mistral La Plateforme",
"platformType": "official_vendor",
"planFamily": "pay_as_you_go",
"planStatus": "confirmed",
"sourceKind": "official_product_page",
"sourceURL": "https://mistral.ai/products/la-plateforme",
"sourceTitle": "La Plateforme | Mistral AI",
"region": "global",
"currency": "USD",
"importerKey": "import_catalog_seed_verification.go",
"notes": "web 搜索已确认官方平台页包含 API pricing 入口,当前按量计费为主。",
"catalogSegment": "global_reference",
"marketRank": 2
},
{
"catalogCode": "cohere-api-payg",
"providerName": "Cohere",
"providerNameCn": "Cohere",
"providerCountry": "CA",
"providerWebsite": "https://cohere.com",
"operatorName": "Cohere Platform",
"operatorNameCn": "Cohere Platform",
"operatorCountry": "CA",
"operatorWebsite": "https://docs.cohere.com",
"operatorType": "official",
"platformName": "Cohere Platform",
"platformNameCn": "Cohere Platform",
"platformType": "official_vendor",
"planFamily": "pay_as_you_go",
"planStatus": "confirmed",
"sourceKind": "official_doc",
"sourceURL": "https://docs.cohere.com/docs/pricing",
"sourceTitle": "Pricing | Cohere",
"region": "global",
"currency": "USD",
"importerKey": "import_catalog_seed_verification.go",
"notes": "web 搜索已确认官方定价文档,当前以按量计费为主。",
"catalogSegment": "global_reference",
"marketRank": 3
},
{
"catalogCode": "openrouter-api-payg",
"providerName": "OpenRouter",
"providerNameCn": "OpenRouter",
"providerCountry": "US",
"providerWebsite": "https://openrouter.ai",
"operatorName": "OpenRouter",
"operatorNameCn": "OpenRouter",
"operatorCountry": "US",
"operatorWebsite": "https://openrouter.ai",
"operatorType": "relay",
"platformName": "OpenRouter",
"platformNameCn": "OpenRouter",
"platformType": "relay_platform",
"planFamily": "pay_as_you_go",
"planStatus": "confirmed",
"sourceKind": "official_pricing",
"sourceURL": "https://openrouter.ai/models",
"sourceTitle": "OpenRouter Models",
"region": "global",
"currency": "USD",
"importerKey": "fetch_openrouter.go",
"notes": "项目主采集源之一web 搜索已确认官方模型页持续公开模型与价格信息。",
"catalogSegment": "global_reference",
"marketRank": 4
},
{
"catalogCode": "together-ai-api-payg",
"providerName": "Together AI",
"providerNameCn": "Together AI",
"providerCountry": "US",
"providerWebsite": "https://www.together.ai",
"operatorName": "Together AI",
"operatorNameCn": "Together AI",
"operatorCountry": "US",
"operatorWebsite": "https://www.together.ai",
"operatorType": "relay",
"platformName": "Together AI",
"platformNameCn": "Together AI",
"platformType": "relay_platform",
"planFamily": "pay_as_you_go",
"planStatus": "confirmed",
"sourceKind": "official_pricing",
"sourceURL": "https://www.together.ai/pricing",
"sourceTitle": "Pricing | Together AI",
"region": "global",
"currency": "USD",
"importerKey": "import_catalog_seed_verification.go",
"notes": "web 搜索已确认官方价格页,平台以多模型按量计费聚合为主。",
"catalogSegment": "global_reference",
"marketRank": 5
},
{
"catalogCode": "fireworks-ai-api-payg",
"providerName": "Fireworks AI",
"providerNameCn": "Fireworks AI",
"providerCountry": "US",
"providerWebsite": "https://fireworks.ai",
"operatorName": "Fireworks AI",
"operatorNameCn": "Fireworks AI",
"operatorCountry": "US",
"operatorWebsite": "https://fireworks.ai",
"operatorType": "relay",
"platformName": "Fireworks AI",
"platformNameCn": "Fireworks AI",
"platformType": "relay_platform",
"planFamily": "pay_as_you_go",
"planStatus": "confirmed",
"sourceKind": "official_pricing",
"sourceURL": "https://fireworks.ai/pricing",
"sourceTitle": "Pricing | Fireworks AI",
"region": "global",
"currency": "USD",
"importerKey": "import_catalog_seed_verification.go",
"notes": "web 搜索已确认官方价格页,当前平台以 hosted model API 按量计费为主。",
"catalogSegment": "global_reference",
"marketRank": 6
},
{
"catalogCode": "deepinfra-api-payg",
"providerName": "DeepInfra",
"providerNameCn": "DeepInfra",
"providerCountry": "US",
"providerWebsite": "https://deepinfra.com",
"operatorName": "DeepInfra",
"operatorNameCn": "DeepInfra",
"operatorCountry": "US",
"operatorWebsite": "https://deepinfra.com",
"operatorType": "relay",
"platformName": "DeepInfra",
"platformNameCn": "DeepInfra",
"platformType": "relay_platform",
"planFamily": "pay_as_you_go",
"planStatus": "confirmed",
"sourceKind": "official_pricing",
"sourceURL": "https://deepinfra.com/pricing",
"sourceTitle": "Pricing | DeepInfra",
"region": "global",
"currency": "USD",
"importerKey": "import_catalog_seed_verification.go",
"notes": "web 搜索已确认官方价格页,当前平台公开多模型按量价格。",
"catalogSegment": "global_reference",
"marketRank": 7
},
{
"catalogCode": "groq-api-payg",
"providerName": "Groq",
"providerNameCn": "Groq",
"providerCountry": "US",
"providerWebsite": "https://groq.com",
"operatorName": "GroqCloud",
"operatorNameCn": "GroqCloud",
"operatorCountry": "US",
"operatorWebsite": "https://groq.com",
"operatorType": "relay",
"platformName": "GroqCloud",
"platformNameCn": "GroqCloud",
"platformType": "relay_platform",
"planFamily": "pay_as_you_go",
"planStatus": "confirmed",
"sourceKind": "official_pricing",
"sourceURL": "https://groq.com/pricing/",
"sourceTitle": "Groq On-Demand Pricing for Tokens-as-a-Service",
"region": "global",
"currency": "USD",
"importerKey": "import_catalog_seed_verification.go",
"notes": "web 搜索已确认官方按量价格页,当前以 hosted inference 按 token 计费为主。",
"catalogSegment": "global_reference",
"marketRank": 8
},
{
"catalogCode": "replicate-api-payg",
"providerName": "Replicate",
"providerNameCn": "Replicate",
"providerCountry": "US",
"providerWebsite": "https://replicate.com",
"operatorName": "Replicate",
"operatorNameCn": "Replicate",
"operatorCountry": "US",
"operatorWebsite": "https://replicate.com",
"operatorType": "relay",
"platformName": "Replicate",
"platformNameCn": "Replicate",
"platformType": "relay_platform",
"planFamily": "pay_as_you_go",
"planStatus": "confirmed",
"sourceKind": "official_pricing",
"sourceURL": "https://replicate.com/pricing",
"sourceTitle": "Pricing - Replicate",
"region": "global",
"currency": "USD",
"importerKey": "import_catalog_seed_verification.go",
"notes": "web 搜索已确认官方定价页,平台按模型运行时和资源消耗计费。",
"catalogSegment": "global_reference",
"marketRank": 9
},
{
"catalogCode": "hyperbolic-api-payg",
"providerName": "Hyperbolic",
"providerNameCn": "Hyperbolic",
"providerCountry": "US",
"providerWebsite": "https://hyperbolic.xyz",
"operatorName": "Hyperbolic",
"operatorNameCn": "Hyperbolic",
"operatorCountry": "US",
"operatorWebsite": "https://hyperbolic.xyz",
"operatorType": "relay",
"platformName": "Hyperbolic",
"platformNameCn": "Hyperbolic",
"platformType": "relay_platform",
"planFamily": "pay_as_you_go",
"planStatus": "confirmed",
"sourceKind": "official_doc",
"sourceURL": "https://docs.hyperbolic.xyz/docs/inference-api/pricing",
"sourceTitle": "Pricing - Hyperbolic Docs",
"region": "global",
"currency": "USD",
"importerKey": "import_catalog_seed_verification.go",
"notes": "web 搜索已确认官方文档存在推理 API 定价说明,当前以按量计费为主。",
"catalogSegment": "global_reference",
"marketRank": 10
},
{
"catalogCode": "novita-ai-api-payg",
"providerName": "Novita AI",
"providerNameCn": "Novita AI",
"providerCountry": "SG",
"providerWebsite": "https://novita.ai",
"operatorName": "Novita AI",
"operatorNameCn": "Novita AI",
"operatorCountry": "SG",
"operatorWebsite": "https://novita.ai",
"operatorType": "relay",
"platformName": "Novita AI",
"platformNameCn": "Novita AI",
"platformType": "relay_platform",
"planFamily": "pay_as_you_go",
"planStatus": "confirmed",
"sourceKind": "official_pricing",
"sourceURL": "https://novita.ai/pricing",
"sourceTitle": "Pricing | Novita AI",
"region": "global",
"currency": "USD",
"importerKey": "import_catalog_seed_verification.go",
"notes": "web 搜索已确认官方价格页,平台公开多模型推理价格与免费额度说明。",
"catalogSegment": "global_reference",
"marketRank": 11
},
{
"catalogCode": "azure-openai-service-payg",
"providerName": "Microsoft",
"providerNameCn": "微软",
"providerCountry": "US",
"providerWebsite": "https://azure.microsoft.com",
"operatorName": "Microsoft Azure",
"operatorNameCn": "微软 Azure",
"operatorCountry": "US",
"operatorWebsite": "https://azure.microsoft.com",
"operatorType": "cloud",
"platformName": "Azure OpenAI Service",
"platformNameCn": "Azure OpenAI 服务",
"platformType": "cloud_operator",
"planFamily": "pay_as_you_go",
"planStatus": "confirmed",
"sourceKind": "official_pricing",
"sourceURL": "https://azure.microsoft.com/en-us/pricing/details/azure-openai/",
"sourceTitle": "Azure OpenAI Service - Pricing",
"region": "global",
"currency": "USD",
"importerKey": "import_azure_openai_pricing.go",
"notes": "web 搜索已确认 Azure OpenAI 官方定价页,当前以按量计费为主。",
"catalogSegment": "global_reference",
"marketRank": 12
},
{
"catalogCode": "amazon-bedrock-payg",
"providerName": "Amazon",
"providerNameCn": "亚马逊",
"providerCountry": "US",
"providerWebsite": "https://aws.amazon.com",
"operatorName": "AWS",
"operatorNameCn": "亚马逊云科技",
"operatorCountry": "US",
"operatorWebsite": "https://aws.amazon.com",
"operatorType": "cloud",
"platformName": "Amazon Bedrock",
"platformNameCn": "Amazon Bedrock",
"platformType": "cloud_operator",
"planFamily": "pay_as_you_go",
"planStatus": "confirmed",
"sourceKind": "official_pricing",
"sourceURL": "https://aws.amazon.com/bedrock/pricing/",
"sourceTitle": "Amazon Bedrock Pricing",
"region": "global",
"currency": "USD",
"importerKey": "import_bedrock_pricing.go",
"notes": "web 搜索已确认 Amazon Bedrock 官方价格页,按调用模型与推理模式计费。",
"catalogSegment": "global_reference",
"marketRank": 13
},
{
"catalogCode": "google-vertex-ai-genai-payg",
"providerName": "Google",
"providerNameCn": "谷歌",
"providerCountry": "US",
"providerWebsite": "https://cloud.google.com",
"operatorName": "Google Cloud",
"operatorNameCn": "Google Cloud",
"operatorCountry": "US",
"operatorWebsite": "https://cloud.google.com",
"operatorType": "cloud",
"platformName": "Vertex AI Generative AI",
"platformNameCn": "Vertex AI 生成式 AI",
"platformType": "cloud_operator",
"planFamily": "pay_as_you_go",
"planStatus": "confirmed",
"sourceKind": "official_pricing",
"sourceURL": "https://cloud.google.com/vertex-ai/generative-ai/pricing",
"sourceTitle": "Vertex AI Pricing",
"region": "global",
"currency": "USD",
"importerKey": "import_vertex_pricing.go",
"notes": "web 搜索已确认 Vertex AI 生成式 AI 官方价格页,当前以按量计费为主。",
"catalogSegment": "global_reference",
"marketRank": 14
},
{
"catalogCode": "cloudflare-workers-ai-payg",
"providerName": "Cloudflare",
"providerNameCn": "Cloudflare",
"providerCountry": "US",
"providerWebsite": "https://www.cloudflare.com",
"operatorName": "Cloudflare",
"operatorNameCn": "Cloudflare",
"operatorCountry": "US",
"operatorWebsite": "https://developers.cloudflare.com/workers-ai/",
"operatorType": "cloud",
"platformName": "Cloudflare Workers AI",
"platformNameCn": "Cloudflare Workers AI",
"platformType": "cloud_operator",
"planFamily": "pay_as_you_go",
"planStatus": "confirmed",
"sourceKind": "official_pricing",
"sourceURL": "https://developers.cloudflare.com/workers-ai/platform/pricing/",
"sourceTitle": "Pricing · Cloudflare Workers AI docs",
"region": "global",
"currency": "USD",
"importerKey": "import_cloudflare_pricing.go",
"notes": "web 搜索已确认 Workers AI 官方价格页,支持免费额度与按 Neurons / token 计费。",
"catalogSegment": "global_reference",
"marketRank": 15
},
{
"catalogCode": "baseten-inference-payg",
"providerName": "Baseten",
"providerNameCn": "Baseten",
"providerCountry": "US",
"providerWebsite": "https://www.baseten.co",
"operatorName": "Baseten",
"operatorNameCn": "Baseten",
"operatorCountry": "US",
"operatorWebsite": "https://www.baseten.co",
"operatorType": "relay",
"platformName": "Baseten",
"platformNameCn": "Baseten",
"platformType": "relay_platform",
"planFamily": "pay_as_you_go",
"planStatus": "confirmed",
"sourceKind": "official_pricing",
"sourceURL": "https://www.baseten.co/pricing",
"sourceTitle": "Cloud Pricing | Baseten",
"region": "global",
"currency": "USD",
"importerKey": "import_catalog_seed_verification.go",
"notes": "web 搜索已确认 Baseten 官方价格页,当前以模型推理计算资源按量计费为主。",
"catalogSegment": "global_reference",
"marketRank": 16
},
{
"catalogCode": "cerebras-inference-payg",
"providerName": "Cerebras",
"providerNameCn": "Cerebras",
"providerCountry": "US",
"providerWebsite": "https://www.cerebras.ai",
"operatorName": "Cerebras",
"operatorNameCn": "Cerebras",
"operatorCountry": "US",
"operatorWebsite": "https://www.cerebras.ai",
"operatorType": "relay",
"platformName": "Cerebras Inference",
"platformNameCn": "Cerebras Inference",
"platformType": "relay_platform",
"planFamily": "pay_as_you_go",
"planStatus": "confirmed",
"sourceKind": "official_pricing",
"sourceURL": "https://www.cerebras.ai/pricing",
"sourceTitle": "Pricing | Cerebras",
"region": "global",
"currency": "USD",
"importerKey": "import_catalog_seed_verification.go",
"notes": "web 搜索已确认 Cerebras 官方价格页,当前同时存在免费层、开发者按量层和企业层。",
"catalogSegment": "global_reference",
"marketRank": 17
},
{
"catalogCode": "perplexity-agent-api-payg",
"providerName": "Perplexity",
"providerNameCn": "Perplexity",
"providerCountry": "US",
"providerWebsite": "https://www.perplexity.ai",
"operatorName": "Perplexity API",
"operatorNameCn": "Perplexity API",
"operatorCountry": "US",
"operatorWebsite": "https://docs.perplexity.ai",
"operatorType": "relay",
"platformName": "Perplexity Agent API",
"platformNameCn": "Perplexity Agent API",
"platformType": "relay_platform",
"planFamily": "pay_as_you_go",
"planStatus": "confirmed",
"sourceKind": "official_doc",
"sourceURL": "https://docs.perplexity.ai/docs/getting-started/pricing",
"sourceTitle": "Pricing - Perplexity",
"region": "global",
"currency": "USD",
"importerKey": "import_perplexity_pricing.go",
"notes": "web 搜索已确认官方定价文档,当前聚合第三方模型并叠加搜索工具计费。",
"catalogSegment": "global_reference",
"marketRank": 18
},
{
"catalogCode": "sambanova-cloud-payg",
"providerName": "SambaNova",
"providerNameCn": "SambaNova",
"providerCountry": "US",
"providerWebsite": "https://sambanova.ai",
"operatorName": "SambaNova Cloud",
"operatorNameCn": "SambaNova Cloud",
"operatorCountry": "US",
"operatorWebsite": "https://cloud.sambanova.ai",
"operatorType": "relay",
"platformName": "SambaNova Cloud",
"platformNameCn": "SambaNova Cloud",
"platformType": "relay_platform",
"planFamily": "pay_as_you_go",
"planStatus": "confirmed",
"sourceKind": "official_pricing",
"sourceURL": "https://cloud.sambanova.ai/plans",
"sourceTitle": "Plan and Billing | SambaNova Cloud",
"region": "global",
"currency": "USD",
"importerKey": "import_catalog_seed_verification.go",
"notes": "web 搜索已确认官方计划页,开发者层按量计费并附带免费 API credits。",
"catalogSegment": "global_reference",
"marketRank": 19
},
{
"catalogCode": "jdcloud-joybuilder-payg",
"providerName": "JD.com",
"providerNameCn": "京东",
"providerCountry": "CN",
"providerWebsite": "https://www.jd.com",
"operatorName": "JD Cloud",
"operatorNameCn": "京东云",
"operatorCountry": "CN",
"operatorWebsite": "https://www.jdcloud.com",
"operatorType": "cloud",
"platformName": "JD Cloud JoyBuilder",
"platformNameCn": "京东云 JoyBuilder",
"platformType": "cloud_operator",
"planFamily": "pay_as_you_go",
"planStatus": "confirmed",
"sourceKind": "official_doc",
"sourceURL": "https://docs.jdcloud.com/cn/jdaip/billing-overview",
"sourceTitle": "计费说明--JoyBuilder 模型开发平台2.0",
"region": "CN",
"currency": "CNY",
"importerKey": "import_catalog_seed_verification.go",
"notes": "web 搜索已确认 JoyBuilder 官方计费文档,并明确存在模型服务价格及计费规则入口。",
"catalogSegment": "general",
"marketRank": 4
}
]
}

View File

@@ -0,0 +1,89 @@
{
"checkedAt": "2026-05-14T00:00:00+08:00",
"items": [
{
"providerName": "MiniMax",
"providerNameCn": "MiniMax",
"providerCountry": "CN",
"providerWebsite": "https://platform.minimaxi.com",
"operatorName": "MiniMax",
"operatorNameCn": "MiniMax",
"operatorCountry": "CN",
"operatorWebsite": "https://platform.minimaxi.com/docs/token-plan/intro",
"operatorType": "official",
"planFamily": "token_plan",
"planCode": "minimax-token-plan-starter",
"planName": "MiniMax Token Plan Starter",
"tier": "Starter",
"billingCycle": "monthly",
"currency": "CNY",
"listPrice": 45,
"priceUnit": "CNY/month",
"quotaValue": 4000000,
"quotaUnit": "credits/month",
"contextWindow": 0,
"planScope": "Token Plan",
"modelScope": [],
"sourceURL": "https://platform.minimaxi.com/docs/token-plan/intro",
"publishedAt": "2026-05-14 00:00:00",
"effectiveDate": "2026-05-14",
"notes": "Starter 套餐适合轻量使用。"
},
{
"providerName": "MiniMax",
"providerNameCn": "MiniMax",
"providerCountry": "CN",
"providerWebsite": "https://platform.minimaxi.com",
"operatorName": "MiniMax",
"operatorNameCn": "MiniMax",
"operatorCountry": "CN",
"operatorWebsite": "https://platform.minimaxi.com/docs/token-plan/intro",
"operatorType": "official",
"planFamily": "token_plan",
"planCode": "minimax-token-plan-plus",
"planName": "MiniMax Token Plan Plus",
"tier": "Plus",
"billingCycle": "monthly",
"currency": "CNY",
"listPrice": 169,
"priceUnit": "CNY/month",
"quotaValue": 18000000,
"quotaUnit": "credits/month",
"contextWindow": 0,
"planScope": "Token Plan",
"modelScope": [],
"sourceURL": "https://platform.minimaxi.com/docs/token-plan/intro",
"publishedAt": "2026-05-14 00:00:00",
"effectiveDate": "2026-05-14",
"notes": "Plus 套餐提供更高月度 Credits 配额。"
},
{
"providerName": "MiniMax",
"providerNameCn": "MiniMax",
"providerCountry": "CN",
"providerWebsite": "https://platform.minimaxi.com",
"operatorName": "MiniMax",
"operatorNameCn": "MiniMax",
"operatorCountry": "CN",
"operatorWebsite": "https://platform.minimaxi.com/docs/token-plan/intro",
"operatorType": "official",
"planFamily": "token_plan",
"planCode": "minimax-token-plan-max",
"planName": "MiniMax Token Plan Max",
"tier": "Max",
"billingCycle": "monthly",
"currency": "CNY",
"listPrice": 460,
"priceUnit": "CNY/month",
"quotaValue": 60000000,
"quotaUnit": "credits/month",
"contextWindow": 0,
"planScope": "Token Plan",
"modelScope": [],
"sourceURL": "https://platform.minimaxi.com/docs/token-plan/intro",
"publishedAt": "2026-05-14 00:00:00",
"effectiveDate": "2026-05-14",
"notes": "Max 套餐对应最高的基础月度配额。"
}
]
}