- Add modular DB layer (adapters, migrations, repos, helpers) - Replace localDb/usageDb/requestDetailsDb monoliths with repos - Add Tailscale tunnel integration & status check API - Add /api/cli-tools/all-statuses aggregated endpoint - Add settingsStore (Zustand) and mitm/dbReader - Add DB unit tests (benchmark, concurrent, migration, vs-lowdb)
108 lines
3.5 KiB
JavaScript
108 lines
3.5 KiB
JavaScript
import { getAdapter } from "../driver.js";
|
|
import { parseJson, stringifyJson } from "../helpers/jsonCol.js";
|
|
import { makeKv } from "../helpers/kvStore.js";
|
|
|
|
const pricingKv = makeKv("pricing");
|
|
const CACHE_TTL_MS = 5000;
|
|
|
|
let cache = { value: null, expiresAt: 0 };
|
|
|
|
function invalidate() {
|
|
cache = { value: null, expiresAt: 0 };
|
|
}
|
|
|
|
async function getUserPricing() {
|
|
return await pricingKv.getAll();
|
|
}
|
|
|
|
export async function getPricing() {
|
|
const now = Date.now();
|
|
if (cache.value && cache.expiresAt > now) return cache.value;
|
|
|
|
const userPricing = await getUserPricing();
|
|
const { PROVIDER_PRICING } = await import("@/shared/constants/pricing.js");
|
|
const merged = {};
|
|
|
|
for (const [provider, models] of Object.entries(PROVIDER_PRICING)) {
|
|
merged[provider] = { ...models };
|
|
if (userPricing[provider]) {
|
|
for (const [model, pricing] of Object.entries(userPricing[provider])) {
|
|
merged[provider][model] = merged[provider][model]
|
|
? { ...merged[provider][model], ...pricing }
|
|
: pricing;
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const [provider, models] of Object.entries(userPricing)) {
|
|
if (!merged[provider]) {
|
|
merged[provider] = { ...models };
|
|
} else {
|
|
for (const [model, pricing] of Object.entries(models)) {
|
|
if (!merged[provider][model]) merged[provider][model] = pricing;
|
|
}
|
|
}
|
|
}
|
|
|
|
cache = { value: merged, expiresAt: now + CACHE_TTL_MS };
|
|
return merged;
|
|
}
|
|
|
|
export async function getPricingForModel(provider, model) {
|
|
if (!model) return null;
|
|
const userPricing = await getUserPricing();
|
|
if (provider && userPricing[provider]?.[model]) return userPricing[provider][model];
|
|
const { getPricingForModel: resolveConst } = await import("@/shared/constants/pricing.js");
|
|
return resolveConst(provider, model);
|
|
}
|
|
|
|
// Atomic merge inside transaction (per-provider read-modify-write)
|
|
export async function updatePricing(pricingData) {
|
|
const db = await getAdapter();
|
|
db.transaction(() => {
|
|
for (const [provider, models] of Object.entries(pricingData)) {
|
|
const row = db.get(`SELECT value FROM kv WHERE scope = 'pricing' AND key = ?`, [provider]);
|
|
const current = row ? (parseJson(row.value, {}) || {}) : {};
|
|
const merged = { ...current };
|
|
for (const [model, pricing] of Object.entries(models)) {
|
|
merged[model] = pricing;
|
|
}
|
|
db.run(
|
|
`INSERT INTO kv(scope, key, value) VALUES('pricing', ?, ?) ON CONFLICT(scope, key) DO UPDATE SET value = excluded.value`,
|
|
[provider, stringifyJson(merged)]
|
|
);
|
|
}
|
|
});
|
|
invalidate();
|
|
return await getUserPricing();
|
|
}
|
|
|
|
export async function resetPricing(provider, model) {
|
|
if (!provider) return await getUserPricing();
|
|
const db = await getAdapter();
|
|
db.transaction(() => {
|
|
if (!model) {
|
|
db.run(`DELETE FROM kv WHERE scope = 'pricing' AND key = ?`, [provider]);
|
|
return;
|
|
}
|
|
const row = db.get(`SELECT value FROM kv WHERE scope = 'pricing' AND key = ?`, [provider]);
|
|
const current = row ? (parseJson(row.value, {}) || {}) : {};
|
|
delete current[model];
|
|
if (Object.keys(current).length === 0) {
|
|
db.run(`DELETE FROM kv WHERE scope = 'pricing' AND key = ?`, [provider]);
|
|
} else {
|
|
db.run(
|
|
`INSERT INTO kv(scope, key, value) VALUES('pricing', ?, ?) ON CONFLICT(scope, key) DO UPDATE SET value = excluded.value`,
|
|
[provider, stringifyJson(current)]
|
|
);
|
|
}
|
|
});
|
|
invalidate();
|
|
return await getUserPricing();
|
|
}
|
|
|
|
export async function resetAllPricing() {
|
|
await pricingKv.clear();
|
|
invalidate();
|
|
return {};
|
|
}
|