2026-05-15 22:34:22 +08:00
|
|
|
//go:build llm_script
|
|
|
|
|
|
|
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"fmt"
|
|
|
|
|
"regexp"
|
|
|
|
|
"strings"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
defaultPerplexityPricingFetchURL = "https://docs.perplexity.ai/docs/agent-api/models.md"
|
|
|
|
|
defaultPerplexityPricingSourceURL = "https://docs.perplexity.ai/docs/agent-api/models"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
var markdownLinkPattern = regexp.MustCompile(`\[(.*?)\]\((https://[^)]+)\)`)
|
|
|
|
|
|
|
|
|
|
func parsePerplexityPricingCatalog(raw string) ([]officialPricingRecord, error) {
|
|
|
|
|
lines := strings.Split(raw, "\n")
|
|
|
|
|
records := make([]officialPricingRecord, 0)
|
|
|
|
|
header := []string(nil)
|
|
|
|
|
modelIndex := -1
|
|
|
|
|
inputIndex := -1
|
|
|
|
|
outputIndex := -1
|
|
|
|
|
docIndex := -1
|
|
|
|
|
for _, line := range lines {
|
|
|
|
|
line = strings.TrimSpace(line)
|
|
|
|
|
if !strings.HasPrefix(line, "|") {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
parts := splitMarkdownTableRow(line)
|
|
|
|
|
if len(parts) == 0 {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if header == nil {
|
|
|
|
|
header = parts
|
|
|
|
|
modelIndex, inputIndex, outputIndex, docIndex = detectPerplexityTableColumns(parts)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if isMarkdownTableSeparator(parts) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if modelIndex < 0 || inputIndex < 0 || outputIndex < 0 || modelIndex >= len(parts) || inputIndex >= len(parts) || outputIndex >= len(parts) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
modelPath := strings.Trim(parts[modelIndex], "`")
|
|
|
|
|
inputCell := parts[inputIndex]
|
|
|
|
|
outputCell := parts[outputIndex]
|
|
|
|
|
inputPrice, ok := firstDollarPrice(inputCell)
|
|
|
|
|
if !ok {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
outputPrice, ok := firstDollarPrice(outputCell)
|
|
|
|
|
if !ok {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
sourceURL := defaultPerplexityPricingSourceURL
|
|
|
|
|
if docIndex >= 0 && docIndex < len(parts) {
|
|
|
|
|
if matches := markdownLinkPattern.FindStringSubmatch(parts[docIndex]); len(matches) == 3 {
|
|
|
|
|
sourceURL = matches[2]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
providerName := providerFromModelPath(modelPath)
|
|
|
|
|
providerNameCn, providerCountry, providerWebsite := providerMetadata(providerName)
|
|
|
|
|
record := officialPricingRecord{
|
|
|
|
|
ModelID: normalizeExternalID("perplexity", modelPath),
|
|
|
|
|
ModelName: modelPath,
|
|
|
|
|
ProviderName: providerName,
|
|
|
|
|
ProviderNameCn: providerNameCn,
|
|
|
|
|
ProviderCountry: providerCountry,
|
|
|
|
|
ProviderWebsite: providerWebsite,
|
|
|
|
|
OperatorName: "Perplexity API",
|
|
|
|
|
OperatorNameCn: "Perplexity API",
|
|
|
|
|
OperatorCountry: "US",
|
|
|
|
|
OperatorWebsite: "https://docs.perplexity.ai",
|
|
|
|
|
OperatorType: "relay",
|
|
|
|
|
Region: "global",
|
|
|
|
|
Currency: "USD",
|
|
|
|
|
InputPrice: inputPrice,
|
|
|
|
|
OutputPrice: outputPrice,
|
|
|
|
|
SourceURL: defaultPerplexityPricingSourceURL,
|
|
|
|
|
ModelSourceURL: sourceURL,
|
|
|
|
|
DateConfidence: "unknown",
|
|
|
|
|
DateSourceKind: "official_pricing",
|
|
|
|
|
Modality: detectModality(modelPath),
|
|
|
|
|
}
|
|
|
|
|
record.IsFree = record.InputPrice == 0 && record.OutputPrice == 0
|
|
|
|
|
records = append(records, record)
|
|
|
|
|
}
|
|
|
|
|
if len(records) == 0 {
|
|
|
|
|
return nil, fmt.Errorf("unexpected perplexity pricing content")
|
|
|
|
|
}
|
|
|
|
|
return records, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func splitMarkdownTableRow(line string) []string {
|
|
|
|
|
trimmed := strings.TrimSpace(line)
|
|
|
|
|
trimmed = strings.TrimPrefix(trimmed, "|")
|
|
|
|
|
trimmed = strings.TrimSuffix(trimmed, "|")
|
|
|
|
|
if trimmed == "" {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
parts := strings.Split(trimmed, "|")
|
|
|
|
|
result := make([]string, 0, len(parts))
|
|
|
|
|
for _, part := range parts {
|
|
|
|
|
result = append(result, strings.TrimSpace(part))
|
|
|
|
|
}
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func detectPerplexityTableColumns(header []string) (int, int, int, int) {
|
|
|
|
|
modelIndex := -1
|
|
|
|
|
inputIndex := -1
|
|
|
|
|
outputIndex := -1
|
|
|
|
|
docIndex := -1
|
|
|
|
|
for i, col := range header {
|
|
|
|
|
lower := strings.ToLower(strings.TrimSpace(col))
|
|
|
|
|
switch {
|
|
|
|
|
case strings.Contains(lower, "model") && modelIndex == -1:
|
|
|
|
|
modelIndex = i
|
2026-05-22 09:18:14 +08:00
|
|
|
case strings.Contains(lower, "input") && (strings.Contains(lower, "price") || strings.Contains(lower, "$") || strings.Contains(lower, "/1m")) && inputIndex == -1:
|
|
|
|
|
inputIndex = i
|
|
|
|
|
case strings.Contains(lower, "output") && (strings.Contains(lower, "price") || strings.Contains(lower, "$") || strings.Contains(lower, "/1m")) && outputIndex == -1:
|
2026-05-15 22:34:22 +08:00
|
|
|
outputIndex = i
|
|
|
|
|
case (strings.Contains(lower, "documentation") || strings.Contains(lower, "docs")) && docIndex == -1:
|
|
|
|
|
docIndex = i
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return modelIndex, inputIndex, outputIndex, docIndex
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func isMarkdownTableSeparator(parts []string) bool {
|
|
|
|
|
if len(parts) == 0 {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
for _, part := range parts {
|
|
|
|
|
trimmed := strings.TrimSpace(part)
|
|
|
|
|
if trimmed == "" {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
for _, ch := range trimmed {
|
|
|
|
|
if ch != '-' && ch != ':' {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return true
|
|
|
|
|
}
|