2026-06-10 14:12:55 +08:00
|
|
|
import { isOptionalApiRouteMissing } from "./apiErrorUtils";
|
|
|
|
|
import { isRecord, serverRequest } from "./serverConnection";
|
2026-06-10 14:27:42 +08:00
|
|
|
import type { EnterpriseVideoPricingConfig, EnterpriseVideoPricingRule } from "../utils/enterpriseVideoPolicy";
|
2026-06-10 14:12:55 +08:00
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-10 14:27:42 +08:00
|
|
|
export interface PublicPricingPayload {
|
|
|
|
|
modelPrices: PublicModelPrice[];
|
|
|
|
|
enterpriseVideoPricing: EnterpriseVideoPricingConfig | null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-10 14:12:55 +08:00
|
|
|
function readString(
|
|
|
|
|
record: Record<string, unknown>,
|
|
|
|
|
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<string, unknown>,
|
|
|
|
|
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<string, unknown>,
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-10 14:27:42 +08:00
|
|
|
function readStringArray(record: Record<string, unknown>, 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<string, number> | null {
|
|
|
|
|
if (!isRecord(raw)) return null;
|
|
|
|
|
const result: Record<string, number> = {};
|
|
|
|
|
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,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-10 14:12:55 +08:00
|
|
|
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
|
2026-06-10 14:27:42 +08:00
|
|
|
: 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
|
|
|
|
|
: [];
|
2026-06-10 14:12:55 +08:00
|
|
|
|
|
|
|
|
return rawPrices
|
|
|
|
|
.map((item) => normalizePublicModelPrice(item))
|
|
|
|
|
.filter((item): item is PublicModelPrice => Boolean(item));
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-10 14:27:42 +08:00
|
|
|
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;
|
2026-06-10 14:12:55 +08:00
|
|
|
let pricesRouteMissing = false;
|
|
|
|
|
|
|
|
|
|
export const publicPricingClient = {
|
2026-06-10 14:27:42 +08:00
|
|
|
async getPricing(): Promise<PublicPricingPayload> {
|
|
|
|
|
if (cachedPricing) return cachedPricing;
|
|
|
|
|
if (pricesRouteMissing) return { modelPrices: [], enterpriseVideoPricing: null };
|
2026-06-10 14:12:55 +08:00
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const payload = await serverRequest<unknown>("prices", {
|
|
|
|
|
fallbackMessage: "Model prices request failed",
|
|
|
|
|
});
|
2026-06-10 14:27:42 +08:00
|
|
|
cachedPricing = normalizePublicPricingPayload(payload);
|
|
|
|
|
return cachedPricing;
|
2026-06-10 14:12:55 +08:00
|
|
|
} catch (error) {
|
|
|
|
|
if (isOptionalApiRouteMissing(error)) {
|
|
|
|
|
pricesRouteMissing = true;
|
2026-06-10 14:27:42 +08:00
|
|
|
return { modelPrices: [], enterpriseVideoPricing: null };
|
2026-06-10 14:12:55 +08:00
|
|
|
}
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
},
|
2026-06-10 14:27:42 +08:00
|
|
|
|
|
|
|
|
async getPrices(): Promise<PublicModelPrice[]> {
|
|
|
|
|
const pricing = await publicPricingClient.getPricing();
|
|
|
|
|
return pricing.modelPrices;
|
|
|
|
|
},
|
2026-06-10 14:12:55 +08:00
|
|
|
};
|