142 lines
3.9 KiB
TypeScript
142 lines
3.9 KiB
TypeScript
|
|
import { isOptionalApiRouteMissing } from "./apiErrorUtils";
|
||
|
|
import { isRecord, serverRequest } from "./serverConnection";
|
||
|
|
|
||
|
|
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;
|
||
|
|
}
|
||
|
|
|
||
|
|
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;
|
||
|
|
}
|
||
|
|
|
||
|
|
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.models)
|
||
|
|
? payload.models
|
||
|
|
: [];
|
||
|
|
|
||
|
|
return rawPrices
|
||
|
|
.map((item) => normalizePublicModelPrice(item))
|
||
|
|
.filter((item): item is PublicModelPrice => Boolean(item));
|
||
|
|
}
|
||
|
|
|
||
|
|
let cachedPrices: PublicModelPrice[] | null = null;
|
||
|
|
let pricesRouteMissing = false;
|
||
|
|
|
||
|
|
export const publicPricingClient = {
|
||
|
|
async getPrices(): Promise<PublicModelPrice[]> {
|
||
|
|
if (cachedPrices) return cachedPrices;
|
||
|
|
if (pricesRouteMissing) return [];
|
||
|
|
|
||
|
|
try {
|
||
|
|
const payload = await serverRequest<unknown>("prices", {
|
||
|
|
fallbackMessage: "Model prices request failed",
|
||
|
|
});
|
||
|
|
cachedPrices = normalizePublicModelPrices(payload);
|
||
|
|
return cachedPrices;
|
||
|
|
} catch (error) {
|
||
|
|
if (isOptionalApiRouteMissing(error)) {
|
||
|
|
pricesRouteMissing = true;
|
||
|
|
return [];
|
||
|
|
}
|
||
|
|
throw error;
|
||
|
|
}
|
||
|
|
},
|
||
|
|
};
|