import { isOptionalApiRouteMissing } from "./apiErrorUtils"; import { isRecord, serverRequest } from "./serverConnection"; import type { EnterpriseVideoPricingConfig, EnterpriseVideoPricingRule } from "../utils/enterpriseVideoPolicy"; export interface PublicModelPrice { id?: number | string; modelKey: string; displayName?: string; category?: string; pricingType?: string; inputPriceMills: number | null; outputPriceMills: number | null; flatPriceMills: number | null; currency: string; enabled: boolean; createdAt?: string; updatedAt?: string; } export interface PublicPricingPayload { modelPrices: PublicModelPrice[]; enterpriseVideoPricing: EnterpriseVideoPricingConfig | null; } function readString( record: Record, keys: string[], ): string | undefined { for (const key of keys) { const value = record[key]; if (typeof value === "string" && value.trim()) return value.trim(); } return undefined; } function readNumber( record: Record, keys: string[], ): number | null { for (const key of keys) { const value = record[key]; const parsed = typeof value === "number" ? value : typeof value === "string" ? Number(value) : NaN; if (Number.isFinite(parsed)) return parsed; } return null; } function readBoolean( record: Record, keys: string[], fallback: boolean, ): boolean { for (const key of keys) { const value = record[key]; if (typeof value === "boolean") return value; if (typeof value === "number") return value !== 0; if (typeof value === "string") { const normalized = value.trim().toLowerCase(); if (["1", "true", "yes", "enabled"].includes(normalized)) return true; if (["0", "false", "no", "disabled"].includes(normalized)) return false; } } return fallback; } function readStringArray(record: Record, keys: string[]): string[] { for (const key of keys) { const value = record[key]; if (!Array.isArray(value)) continue; return value .map((item) => (typeof item === "string" ? item.trim() : "")) .filter(Boolean); } return []; } function normalizeRateMap(raw: unknown): Record | null { if (!isRecord(raw)) return null; const result: Record = {}; for (const [key, value] of Object.entries(raw)) { const parsed = typeof value === "number" ? value : typeof value === "string" ? Number(value) : NaN; if (Number.isFinite(parsed) && parsed >= 0) result[key] = parsed; } return Object.keys(result).length ? result : null; } function normalizeEnterpriseVideoPricingRule(raw: unknown): EnterpriseVideoPricingRule | null { if (!isRecord(raw)) return null; const id = readString(raw, ["id", "key", "name"]); const modelIncludes = readStringArray(raw, ["modelIncludes", "model_includes", "modelPatterns", "model_patterns"]); const rates = normalizeRateMap(raw.rates); if (!id || modelIncludes.length === 0 || !rates) return null; const when = isRecord(raw.when) ? { ...(typeof raw.when.muted === "boolean" ? { muted: raw.when.muted } : {}), ...(typeof raw.when.hasReferenceVideo === "boolean" ? { hasReferenceVideo: raw.when.hasReferenceVideo } : {}), } : undefined; return { id, modelIncludes, ...(when && Object.keys(when).length ? { when } : {}), rates, }; } export function normalizePublicModelPrice( raw: unknown, ): PublicModelPrice | null { if (!isRecord(raw)) return null; const modelKey = readString(raw, ["modelKey", "model_key", "key", "model"]); if (!modelKey) return null; const displayName = readString(raw, ["displayName", "display_name", "name"]); const category = readString(raw, ["category", "type"]); const pricingType = readString(raw, ["pricingType", "pricing_type"]); const currency = readString(raw, ["currency"]) || "CNY"; const createdAt = readString(raw, ["createdAt", "created_at"]); const updatedAt = readString(raw, ["updatedAt", "updated_at"]); const idValue = raw.id; return { id: typeof idValue === "number" || typeof idValue === "string" ? idValue : undefined, modelKey, displayName, category, pricingType, inputPriceMills: readNumber(raw, ["inputPriceMills", "input_price_mills"]), outputPriceMills: readNumber(raw, [ "outputPriceMills", "output_price_mills", ]), flatPriceMills: readNumber(raw, ["flatPriceMills", "flat_price_mills"]), currency, enabled: readBoolean(raw, ["enabled", "is_enabled"], true), createdAt, updatedAt, }; } export function normalizePublicModelPrices( payload: unknown, ): PublicModelPrice[] { const rawPrices = Array.isArray(payload) ? payload : isRecord(payload) && Array.isArray(payload.prices) ? payload.prices : isRecord(payload) && Array.isArray(payload.modelPrices) ? payload.modelPrices : isRecord(payload) && Array.isArray(payload.model_prices) ? payload.model_prices : isRecord(payload) && Array.isArray(payload.models) ? payload.models : []; return rawPrices .map((item) => normalizePublicModelPrice(item)) .filter((item): item is PublicModelPrice => Boolean(item)); } export function normalizeEnterpriseVideoPricingConfig(raw: unknown): EnterpriseVideoPricingConfig | null { if (!isRecord(raw)) return null; const rules = Array.isArray(raw.rules) ? raw.rules .map((item) => normalizeEnterpriseVideoPricingRule(item)) .filter((item): item is EnterpriseVideoPricingRule => Boolean(item)) : []; if (rules.length === 0) return null; const creditsPerCny = readNumber(raw, ["creditsPerCny", "credits_per_cny"]); const defaultResolution = readString(raw, ["defaultResolution", "default_resolution"]); const billingUnit = readString(raw, ["billingUnit", "billing_unit"]); const currency = readString(raw, ["currency"]); const resolutions = readStringArray(raw, ["resolutions", "supportedResolutions", "supported_resolutions"]); return { ...(currency ? { currency } : {}), ...(creditsPerCny !== null ? { creditsPerCny } : {}), ...(billingUnit ? { billingUnit } : {}), ...(defaultResolution ? { defaultResolution } : {}), ...(resolutions.length ? { resolutions } : {}), rules, }; } export function normalizePublicPricingPayload(payload: unknown): PublicPricingPayload { const enterpriseVideoPricingRaw = isRecord(payload) && (payload.enterpriseVideoPricing ?? payload.enterprise_video_pricing); return { modelPrices: normalizePublicModelPrices(payload), enterpriseVideoPricing: normalizeEnterpriseVideoPricingConfig(enterpriseVideoPricingRaw), }; } let cachedPricing: PublicPricingPayload | null = null; let pricesRouteMissing = false; export const publicPricingClient = { async getPricing(): Promise { if (cachedPricing) return cachedPricing; if (pricesRouteMissing) return { modelPrices: [], enterpriseVideoPricing: null }; try { const payload = await serverRequest("prices", { fallbackMessage: "Model prices request failed", }); cachedPricing = normalizePublicPricingPayload(payload); return cachedPricing; } catch (error) { if (isOptionalApiRouteMissing(error)) { pricesRouteMissing = true; return { modelPrices: [], enterpriseVideoPricing: null }; } throw error; } }, async getPrices(): Promise { const pricing = await publicPricingClient.getPricing(); return pricing.modelPrices; }, };