//go:build llm_script
package main
import (
"fmt"
"regexp"
"strings"
)
const defaultBedrockPricingURL = "https://aws.amazon.com/bedrock/pricing/"
var (
bedrockRegionPattern = regexp.MustCompile(`(?s)
Regions?: ([^<]+)
`)
bedrockTablePattern = regexp.MustCompile(`(?s)`)
bedrockRowPattern = regexp.MustCompile(`(?s)(.*?)
`)
bedrockCellPattern = regexp.MustCompile(`(?s)]*>(.*?)`)
)
func parseBedrockPricingCatalog(raw string) ([]officialPricingRecord, error) {
section := extractBetween(raw, `")
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), " ")
}