chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:32:57 +08:00
commit cd420f9332
4811 changed files with 884702 additions and 0 deletions
@@ -0,0 +1 @@
logs/
@@ -0,0 +1,346 @@
#!/usr/bin/env bash
set -euo pipefail
# Generate model-catalog.json by researching each unique base model using Claude Code CLI.
# Usage: ./scripts/generate-model-catalog.sh [options]
#
# Options:
# --dry-run Print models that would be researched without running Claude
# --filter <pattern> Only research models matching this ERE pattern (e.g. "gpt-4o|claude")
# --max <n> Maximum number of models to research (useful for testing)
# --stale-days <n> Re-research models older than N days (default: 7)
# --force Re-research all models regardless of resolvedAt timestamp
# --skip-hidden Skip models already marked as hidden/deprecated (saves time)
# --concurrency <n> Number of models to research in parallel (default: 5)
#
# The script:
# 1. Extracts all modelNames from defaultPrices.ts
# 2. Groups dated variants to their base model
# 3. Runs research-model.sh for each base model (in parallel)
# 4. Writes results incrementally to model-catalog.json
#
# Logs are written to scripts/logs/ for debugging failures.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PACKAGE_DIR="$(dirname "$SCRIPT_DIR")"
DEFAULTS_FILE="$PACKAGE_DIR/src/defaultPrices.ts"
CATALOG_FILE="$PACKAGE_DIR/src/model-catalog.json"
RESEARCH_SCRIPT="$SCRIPT_DIR/research-model.sh"
LOG_DIR="$SCRIPT_DIR/logs"
mkdir -p "$LOG_DIR"
DRY_RUN=false
FILTER=""
MAX_MODELS=0
STALE_DAYS=7
FORCE=false
SKIP_HIDDEN=false
CONCURRENCY=5
while [[ $# -gt 0 ]]; do
case "$1" in
--dry-run) DRY_RUN=true; shift ;;
--filter) FILTER="$2"; shift 2 ;;
--max) MAX_MODELS="$2"; shift 2 ;;
--stale-days) STALE_DAYS="$2"; shift 2 ;;
--force) FORCE=true; shift ;;
--skip-hidden) SKIP_HIDDEN=true; shift ;;
--concurrency) CONCURRENCY="$2"; shift 2 ;;
*) echo "Unknown option: $1" >&2; exit 1 ;;
esac
done
# Extract all model names from defaultPrices.ts
ALL_MODELS=$(grep -o '"modelName": "[^"]*"' "$DEFAULTS_FILE" | sed 's/"modelName": "//;s/"//' | sort -u)
# Skip embedding, legacy completion, and fine-tuned models
SKIP_PATTERNS="^text-embedding|^textembedding|^text-ada|^text-babbage|^text-curie|^text-davinci|^text-bison|^text-unicorn|^code-bison|^code-gecko|^codechat-bison|^chat-bison|^babbage-002|^davinci-002|^ft:|^gemini-live"
FILTERED_MODELS=$(echo "$ALL_MODELS" | grep -vE "$SKIP_PATTERNS")
if [[ -n "$FILTER" ]]; then
FILTERED_MODELS=$(echo "$FILTERED_MODELS" | grep -E "$FILTER" || true)
fi
# Group dated variants to base models
declare -A BASE_TO_VARIANTS
declare -A MODEL_TO_BASE
for model in $FILTERED_MODELS; do
base=$(echo "$model" | sed -E 's/-[0-9]{4}-?[0-9]{2}-?[0-9]{2}$//')
base_no_latest=$(echo "$base" | sed -E 's/-latest$//')
if [[ ${#base_no_latest} -lt ${#base} ]]; then
base="$base_no_latest"
fi
MODEL_TO_BASE["$model"]="$base"
if [[ -n "${BASE_TO_VARIANTS[$base]:-}" ]]; then
BASE_TO_VARIANTS["$base"]="${BASE_TO_VARIANTS[$base]} $model"
else
BASE_TO_VARIANTS["$base"]="$model"
fi
done
BASE_MODELS=$(printf '%s\n' "${!BASE_TO_VARIANTS[@]}" | sort -u)
TOTAL=$(echo "$BASE_MODELS" | wc -l | tr -d ' ')
if [[ "$MAX_MODELS" -gt 0 ]]; then
BASE_MODELS=$(echo "$BASE_MODELS" | head -n "$MAX_MODELS")
TOTAL=$(echo "$BASE_MODELS" | wc -l | tr -d ' ')
fi
echo "Found $TOTAL unique base models (concurrency: $CONCURRENCY)"
if $DRY_RUN; then
echo ""
echo "Base models and their variants:"
for base in $BASE_MODELS; do
echo " $base${BASE_TO_VARIANTS[$base]}"
done
exit 0
fi
# Load existing catalog
if [[ -f "$CATALOG_FILE" ]]; then
EXISTING_CATALOG=$(cat "$CATALOG_FILE")
else
EXISTING_CATALOG="{}"
fi
# Lock file for thread-safe catalog writes
LOCK_FILE="$LOG_DIR/.catalog.lock"
RESULTS_DIR="$LOG_DIR/results"
mkdir -p "$RESULTS_DIR"
ERRORS=0
FAILED_MODELS=""
SKIPPED=0
RESEARCHED=0
CHANGED=0
# --- Determine which models need research ---
MODELS_TO_RESEARCH=""
COUNT=0
for base in $BASE_MODELS; do
COUNT=$((COUNT + 1))
SKIP_REASON=$(echo "$EXISTING_CATALOG" | node -e "
const data = JSON.parse(require('fs').readFileSync('/dev/stdin','utf-8'));
const entry = data['$base'];
if (!entry) { process.stdout.write('missing'); return; }
if ($FORCE) { process.stdout.write('force'); return; }
if ($SKIP_HIDDEN && entry.isHidden) { process.stdout.write('hidden'); return; }
const resolvedAt = entry.resolvedAt ? new Date(entry.resolvedAt) : null;
if (!resolvedAt) { process.stdout.write('no_timestamp'); return; }
const staleMs = $STALE_DAYS * 24 * 60 * 60 * 1000;
if (Date.now() - resolvedAt.getTime() > staleMs) { process.stdout.write('stale'); return; }
process.stdout.write('fresh');
" 2>/dev/null || echo "missing")
case "$SKIP_REASON" in
fresh)
RESOLVED_DATE=$(echo "$EXISTING_CATALOG" | node -e "const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf-8'));const r=d['$base']?.resolvedAt;console.log(r?r.split('T')[0]:'?')" 2>/dev/null)
echo "[$COUNT/$TOTAL] Skipping $base (resolved $RESOLVED_DATE)"
SKIPPED=$((SKIPPED + 1))
;;
hidden)
echo "[$COUNT/$TOTAL] Skipping $base (hidden/deprecated)"
SKIPPED=$((SKIPPED + 1))
;;
*)
MODELS_TO_RESEARCH="$MODELS_TO_RESEARCH $base"
;;
esac
done
RESEARCH_COUNT=$(echo "$MODELS_TO_RESEARCH" | wc -w | tr -d ' ')
echo ""
echo "Researching $RESEARCH_COUNT models, skipped $SKIPPED"
echo ""
if [[ "$RESEARCH_COUNT" -eq 0 ]]; then
echo "Nothing to do."
exit 0
fi
# --- Research function (called per model, may run in parallel) ---
research_model() {
local base="$1"
local idx="$2"
local total="$3"
local model_log="$LOG_DIR/$base.log"
local result_file="$RESULTS_DIR/$base.json"
echo "[$idx/$total] Researching $base..."
local raw
raw=$("$RESEARCH_SCRIPT" "$base" 3 2>&1) || {
echo "$raw" > "$model_log"
echo " ERROR: Failed to research $base (after retries). Log: $model_log" >&2
echo '{"error":true}' > "$result_file"
return 1
}
echo "$raw" > "$model_log"
local entry
entry=$(echo "$raw" | node -e "
try {
const d = JSON.parse(require('fs').readFileSync('/dev/stdin','utf-8'));
let text = (typeof d.result === 'string' ? d.result : JSON.stringify(d)).trim();
text = text.replace(/^\`\`\`(?:json)?\s*/i, '').replace(/\s*\`\`\`\s*$/, '').trim();
const jsonMatch = text.match(/\{[\s\S]*\}/);
if (jsonMatch) text = jsonMatch[0];
const r = JSON.parse(text);
if (!r.provider) throw new Error('missing provider field');
process.stdout.write(JSON.stringify({
provider: r.provider,
description: r.description || '',
contextWindow: r.contextWindow || null,
maxOutputTokens: r.maxOutputTokens || null,
capabilities: r.capabilities || [],
releaseDate: r.releaseDate || null,
isHidden: r.isHidden === true,
supportsStructuredOutput: r.supportsStructuredOutput === true,
supportsParallelToolCalls: r.supportsParallelToolCalls === true,
supportsStreamingToolCalls: r.supportsStreamingToolCalls === true,
deprecationDate: r.deprecationDate || null,
knowledgeCutoff: r.knowledgeCutoff || null,
resolvedAt: new Date().toISOString()
}));
} catch(e) {
process.stderr.write(e.message);
process.exit(1);
}
" 2>"$LOG_DIR/$base.parse-error") || {
local parse_err
parse_err=$(cat "$LOG_DIR/$base.parse-error" 2>/dev/null)
echo " ERROR: Failed to parse response for $base: $parse_err" >&2
echo " Raw response saved to: $model_log" >&2
echo '{"error":true}' > "$result_file"
return 1
}
echo "$entry" > "$result_file"
echo " OK: $(echo "$entry" | node -e "const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf-8'));console.log(d.provider + ' / ' + (d.contextWindow||'?') + ' ctx / ' + d.capabilities.length + ' caps')" 2>/dev/null)"
}
export -f research_model
export RESEARCH_SCRIPT LOG_DIR RESULTS_DIR
# --- Run research in parallel ---
IDX=0
PIDS=()
MODEL_LIST=($MODELS_TO_RESEARCH)
for base in "${MODEL_LIST[@]}"; do
IDX=$((IDX + 1))
research_model "$base" "$IDX" "$RESEARCH_COUNT" &
PIDS+=($!)
# Throttle concurrency
if [[ ${#PIDS[@]} -ge $CONCURRENCY ]]; then
wait "${PIDS[0]}" 2>/dev/null || true
PIDS=("${PIDS[@]:1}")
fi
done
# Wait for remaining
for pid in "${PIDS[@]}"; do
wait "$pid" 2>/dev/null || true
done
echo ""
echo "Research complete. Merging results..."
# --- Merge results into catalog ---
CATALOG="$EXISTING_CATALOG"
for base in "${MODEL_LIST[@]}"; do
RESULT_FILE="$RESULTS_DIR/$base.json"
if [[ ! -f "$RESULT_FILE" ]]; then
ERRORS=$((ERRORS + 1))
FAILED_MODELS="$FAILED_MODELS $base"
continue
fi
ENTRY=$(cat "$RESULT_FILE")
# Check for error marker
if echo "$ENTRY" | node -e "const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf-8'));process.exit(d.error?0:1)" 2>/dev/null; then
ERRORS=$((ERRORS + 1))
FAILED_MODELS="$FAILED_MODELS $base"
continue
fi
RESEARCHED=$((RESEARCHED + 1))
# Diff detection: compare with existing entry
OLD_ENTRY=$(echo "$EXISTING_CATALOG" | node -e "
const d = JSON.parse(require('fs').readFileSync('/dev/stdin','utf-8'));
const e = d['$base'];
if (e) { delete e.resolvedAt; process.stdout.write(JSON.stringify(e)); }
else process.stdout.write('null');
" 2>/dev/null)
NEW_FOR_DIFF=$(echo "$ENTRY" | node -e "
const d = JSON.parse(require('fs').readFileSync('/dev/stdin','utf-8'));
delete d.resolvedAt;
process.stdout.write(JSON.stringify(d));
" 2>/dev/null)
if [[ "$OLD_ENTRY" != "null" && "$OLD_ENTRY" != "$NEW_FOR_DIFF" ]]; then
CHANGED=$((CHANGED + 1))
# Log what changed
node -e "
const old = JSON.parse('$OLD_ENTRY');
const cur = JSON.parse('$NEW_FOR_DIFF');
const changes = [];
for (const k of new Set([...Object.keys(old), ...Object.keys(cur)])) {
const o = JSON.stringify(old[k]); const n = JSON.stringify(cur[k]);
if (o !== n) changes.push(k + ': ' + o + ' → ' + n);
}
if (changes.length) console.log(' CHANGED: ' + changes.join(', '));
" 2>/dev/null || true
fi
# Apply to all variants of this base model
for variant in ${BASE_TO_VARIANTS[$base]}; do
CATALOG=$(echo "$CATALOG" | node -e "
const catalog = JSON.parse(require('fs').readFileSync('/dev/stdin','utf-8'));
catalog['$variant'] = $ENTRY;
process.stdout.write(JSON.stringify(catalog));
")
done
done
# Write final catalog
echo "$CATALOG" | node -e "
const data = JSON.parse(require('fs').readFileSync('/dev/stdin','utf-8'));
const sorted = Object.keys(data).sort().reduce((acc, k) => { acc[k] = data[k]; return acc; }, {});
process.stdout.write(JSON.stringify(sorted, null, 2) + '\n');
" > "$CATALOG_FILE"
# Cleanup results
rm -rf "$RESULTS_DIR"
FINAL_COUNT=$(node -e "console.log(Object.keys(JSON.parse(require('fs').readFileSync('$CATALOG_FILE','utf-8'))).length)")
echo ""
echo "Done! $FINAL_COUNT entries in catalog"
echo " Researched: $RESEARCHED | Changed: $CHANGED | Skipped: $SKIPPED | Errors: $ERRORS"
if [[ "$ERRORS" -gt 0 ]]; then
echo ""
echo "Failed models:$FAILED_MODELS"
RETRY_PATTERN=$(echo "$FAILED_MODELS" | tr ' ' '\n' | grep -v '^$' | sed 's/\./\\./g; s/^/^/; s/$/$/' | paste -sd '|' -)
echo "Retry with: $0 --filter \"$RETRY_PATTERN\""
fi
@@ -0,0 +1,98 @@
#!/usr/bin/env node
// Cross-platform generation script for the llm-pricing package.
// Generates TypeScript modules from JSON data files:
// 1. defaultPrices.ts ← default-model-prices.json (synced from Langfuse)
// 2. modelCatalog.ts ← model-catalog.json (our maintained catalog metadata)
//
// Usage: node scripts/generate.mjs
//
// To update the source JSON files:
// - Pricing: pnpm run sync-prices (fetches from Langfuse, requires curl)
// - Catalog: pnpm run generate-catalog (uses Claude CLI to research models)
import { readFileSync, writeFileSync, existsSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
const srcDir = join(__dirname, "..", "src");
// --- 1. Generate defaultPrices.ts from default-model-prices.json ---
const pricesJsonPath = join(srcDir, "default-model-prices.json");
if (existsSync(pricesJsonPath)) {
const raw = JSON.parse(readFileSync(pricesJsonPath, "utf-8"));
const stripped = raw.map((e) => ({
modelName: e.modelName.trim(),
matchPattern: e.matchPattern,
startDate: e.createdAt,
pricingTiers: e.pricingTiers.map((t) => ({
name: t.name,
isDefault: t.isDefault,
priority: t.priority,
conditions: t.conditions.map((c) => ({
usageDetailPattern: c.usageDetailPattern,
operator: c.operator,
value: c.value,
})),
prices: t.prices,
})),
}));
let out = 'import type { DefaultModelDefinition } from "./types.js";\n\n';
out += "// Auto-generated from default-model-prices.json — do not edit manually.\n";
out +=
"// Run `pnpm run sync-prices` to update the JSON, then `pnpm run generate` to regenerate.\n";
out += "// Source: https://github.com/langfuse/langfuse\n\n";
out += "export const defaultModelPrices: DefaultModelDefinition[] = ";
out += JSON.stringify(stripped, null, 2) + ";\n";
writeFileSync(join(srcDir, "defaultPrices.ts"), out);
console.log(`Generated defaultPrices.ts (${stripped.length} models)`);
} else {
console.log("Skipping defaultPrices.ts — default-model-prices.json not found");
}
// --- 2. Generate modelCatalog.ts from model-catalog.json ---
const catalogJsonPath = join(srcDir, "model-catalog.json");
if (existsSync(catalogJsonPath)) {
const data = JSON.parse(readFileSync(catalogJsonPath, "utf-8"));
// Backfill missing fields for old entries
for (const key of Object.keys(data)) {
if (data[key].releaseDate === undefined) data[key].releaseDate = null;
if (data[key].isHidden === undefined) data[key].isHidden = false;
if (data[key].supportsStructuredOutput === undefined)
data[key].supportsStructuredOutput = false;
if (data[key].supportsParallelToolCalls === undefined)
data[key].supportsParallelToolCalls = false;
if (data[key].supportsStreamingToolCalls === undefined)
data[key].supportsStreamingToolCalls = false;
if (data[key].deprecationDate === undefined) data[key].deprecationDate = null;
if (data[key].knowledgeCutoff === undefined) data[key].knowledgeCutoff = null;
if (data[key].resolvedAt === undefined) data[key].resolvedAt = new Date().toISOString();
{
// Always recompute base model name (don't trust existing values)
// Strip trailing date (-YYYYMMDD or -YYYY-MM-DD) and -latest suffix
// Keep original naming (dots, etc.) — don't normalize
let base = key.replace(/-\d{4}-?\d{2}-?\d{2}$/, "").replace(/-latest$/, "");
data[key].baseModelName = base !== key ? base : null;
}
}
let out = 'import type { ModelCatalogEntry } from "./types.js";\n\n';
out += "// Auto-generated from model-catalog.json — do not edit manually.\n";
out +=
"// Run `pnpm run generate-catalog` to update the JSON, then `pnpm run generate` to regenerate.\n\n";
out += "export const modelCatalog: Record<string, ModelCatalogEntry> = ";
out += JSON.stringify(data, null, 2) + ";\n";
writeFileSync(join(srcDir, "modelCatalog.ts"), out);
console.log(`Generated modelCatalog.ts (${Object.keys(data).length} entries)`);
} else {
console.log("Skipping modelCatalog.ts — model-catalog.json not found");
}
@@ -0,0 +1,64 @@
#!/usr/bin/env bash
set -euo pipefail
# Research a single LLM model using Claude Code CLI and output structured JSON.
# Usage: ./scripts/research-model.sh <model-name>
#
# Example:
# ./scripts/research-model.sh gpt-4o
# → {"provider":"openai","description":"...","contextWindow":128000,...}
if [[ $# -lt 1 ]]; then
echo "Usage: $0 <model-name>" >&2
exit 1
fi
MODEL_NAME="$1"
MAX_RETRIES="${2:-3}"
PROMPT="Research the LLM model '${MODEL_NAME}' and return ONLY a valid JSON object (no markdown, no explanation, no code fences) with these exact fields:
{
\"provider\": \"<provider-id>\",
\"description\": \"<1-2 sentence description of the model>\",
\"contextWindow\": <max input context window in tokens, or null if unknown>,
\"maxOutputTokens\": <max output tokens, or null if unknown>,
\"capabilities\": [<list of capability strings>],
\"releaseDate\": \"<YYYY-MM-DD or null if unknown>\",
\"isHidden\": <true if deprecated, false otherwise>,
\"supportsStructuredOutput\": <true/false>,
\"supportsParallelToolCalls\": <true/false>,
\"supportsStreamingToolCalls\": <true/false>,
\"deprecationDate\": \"<YYYY-MM-DD or null if no known sunset date>\",
\"knowledgeCutoff\": \"<YYYY-MM-DD or null if unknown>\"
}
Rules:
- provider must be one of: \"openai\", \"anthropic\", \"google\", \"meta\", \"mistral\", \"cohere\", \"ai21\", \"amazon\", \"xai\", \"deepseek\", \"qwen\", \"perplexity\" or the correct provider lowercase id
- description should be concise and factual (what the model is good at, its position in the provider's lineup)
- contextWindow is the maximum input context in tokens (e.g. 128000 for GPT-4o, 200000 for Claude Sonnet 4)
- maxOutputTokens is the maximum output the model can generate in a single response
- capabilities should be drawn from: \"vision\", \"tool_use\", \"streaming\", \"json_mode\", \"extended_thinking\", \"code_execution\", \"image_generation\", \"audio_input\", \"audio_output\", \"embedding\", \"fine_tunable\"
- Only include capabilities you are confident the model supports
- releaseDate is when the model was first publicly available (API launch date), in YYYY-MM-DD format. Use null if unknown. If the model is a dated variant (e.g. gpt-4o-2024-08-06), the date in the name IS the release date.
- isHidden should be true if the model is deprecated, discontinued, no longer available via API, or superseded by a newer version. Examples: gpt-3.5-turbo, claude-1.x, claude-2.x, text-davinci-003, gpt-4-0314 are hidden. Current/active models like gpt-4o, claude-sonnet-4-6, gemini-2.5-flash are NOT hidden.
- supportsStructuredOutput: true if the model reliably follows JSON schemas / structured output mode (e.g. OpenAI's response_format, Anthropic's tool_use for structured output). false for older models that don't support it well.
- supportsParallelToolCalls: true if the model can call multiple tools in a single assistant turn. Most modern models support this.
- supportsStreamingToolCalls: true if the model supports streaming partial tool call arguments as they're generated.
- deprecationDate: the date the provider has announced the model will be sunset/removed from their API, in YYYY-MM-DD format. Use null if no deprecation date has been announced. Only use dates that have been officially published by the provider.
- knowledgeCutoff: the date when the model's training data ends, in YYYY-MM-DD format. Use null if unknown. This is different from releaseDate — it's when the training data was cut off, not when the model launched.
- Output ONLY the JSON object, nothing else"
for attempt in $(seq 1 "$MAX_RETRIES"); do
RESULT=$(claude -p "$PROMPT" --model opus --output-format json --permission-mode bypassPermissions --tools WebSearch,WebFetch 2>/dev/null) && {
echo "$RESULT"
exit 0
}
if [[ "$attempt" -lt "$MAX_RETRIES" ]]; then
echo " Retry $attempt/$MAX_RETRIES for $MODEL_NAME..." >&2
sleep 2
fi
done
echo " Failed after $MAX_RETRIES attempts for $MODEL_NAME" >&2
exit 1
@@ -0,0 +1,46 @@
#!/usr/bin/env bash
set -euo pipefail
# Sync default model prices from Langfuse's repository and generate the TS module.
# Usage: ./scripts/sync-model-prices.sh [--check]
# --check: Exit 1 if prices are outdated (for CI)
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PACKAGE_DIR="$(dirname "$SCRIPT_DIR")"
JSON_TARGET="$PACKAGE_DIR/src/default-model-prices.json"
SOURCE_URL="https://raw.githubusercontent.com/langfuse/langfuse/main/worker/src/constants/default-model-prices.json"
CHECK_MODE=false
if [[ "${1:-}" == "--check" ]]; then
CHECK_MODE=true
fi
echo "Fetching latest model prices from Langfuse..."
TMPFILE=$(mktemp)
trap 'rm -f "$TMPFILE"' EXIT
if ! curl -fsSL "$SOURCE_URL" -o "$TMPFILE"; then
echo "ERROR: Failed to fetch from $SOURCE_URL"
exit 1
fi
# Validate it's valid JSON with at least some models
MODEL_COUNT=$(node -e "console.log(JSON.parse(require('fs').readFileSync('$TMPFILE','utf-8')).length)" 2>/dev/null || echo "0")
if [[ "$MODEL_COUNT" -lt 10 ]]; then
echo "ERROR: Downloaded file has only $MODEL_COUNT models (expected 100+). Aborting."
exit 1
fi
if $CHECK_MODE; then
if diff -q "$JSON_TARGET" "$TMPFILE" > /dev/null 2>&1; then
echo "Model prices are up to date ($MODEL_COUNT models)"
exit 0
else
echo "Model prices are OUTDATED. Run 'pnpm run sync-prices' in @internal/llm-model-catalog to update."
exit 1
fi
fi
cp "$TMPFILE" "$JSON_TARGET"
echo "Updated default-model-prices.json ($MODEL_COUNT models)"
echo "Run 'pnpm run generate' to regenerate defaultPrices.ts"