diff --git a/app/api/common.ts b/app/api/common.ts index 842eeacaf..b2e8f918f 100644 --- a/app/api/common.ts +++ b/app/api/common.ts @@ -8,6 +8,8 @@ const BASE_URL = process.env.BASE_URL ?? OPENAI_URL; export async function requestOpenai(req: NextRequest) { const apiKey = req.headers.get("token"); const openaiPath = req.headers.get("path"); + const fetchCache = + req.headers.get("fetch-cache") == "enable" ? "default" : "no-store"; console.log("[Proxy] ", openaiPath); @@ -16,6 +18,7 @@ export async function requestOpenai(req: NextRequest) { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}`, }, + cache: fetchCache, method: req.method, body: req.body, }); diff --git a/app/components/settings.tsx b/app/components/settings.tsx index e418d4843..b74414d2c 100644 --- a/app/components/settings.tsx +++ b/app/components/settings.tsx @@ -21,6 +21,7 @@ import { ALL_MODELS, useUpdateStore, useAccessStore, + useUsageStore, ModalConfigValidator, } from "../store"; import { Avatar } from "./chat"; @@ -92,25 +93,33 @@ export function Settings(props: { closeSettings: () => void }) { const remoteId = updateStore.remoteVersion; const hasNewVersion = currentVersion !== remoteId; - function checkUpdate(force = false) { + function checkUpdate(force: boolean = false) { setCheckingUpdate(true); updateStore.getLatestVersion(force).then(() => { setCheckingUpdate(false); }); } - const [usage, setUsage] = useState<{ - used?: number; - subscription?: number; - }>(); + // const [usage, setUsage] = useState<{ + // used?: number; + // subscription?: number; + // }>(); + const { used, subscription, updateUsage, hasUsageData } = useUsageStore(); const [loadingUsage, setLoadingUsage] = useState(false); - function checkUsage() { - setLoadingUsage(true); - requestUsage() - .then((res) => setUsage(res)) - .finally(() => { - setLoadingUsage(false); - }); + function checkUsage(forceUpdate: boolean = false) { + if (!hasUsageData() || forceUpdate) { + setLoadingUsage(true); + requestUsage() + .then((res) => { + if (res) { + const { used, subscription } = res; + updateUsage(used?.toString(), subscription?.toString()); + } + }) + .finally(() => { + setLoadingUsage(false); + }); + } } const accessStore = useAccessStore(); @@ -385,10 +394,7 @@ export function Settings(props: { closeSettings: () => void }) { showUsage ? loadingUsage ? Locale.Settings.Usage.IsChecking - : Locale.Settings.Usage.SubTitle( - usage?.used ?? "[?]", - usage?.subscription ?? "[?]", - ) + : Locale.Settings.Usage.SubTitle(used, subscription) : Locale.Settings.Usage.NoAccess } > @@ -398,7 +404,9 @@ export function Settings(props: { closeSettings: () => void }) { } text={Locale.Settings.Usage.Check} - onClick={checkUsage} + onClick={() => { + checkUsage(true); + }} /> )} diff --git a/app/requests.ts b/app/requests.ts index 86d180f71..cc87030c3 100644 --- a/app/requests.ts +++ b/app/requests.ts @@ -49,15 +49,17 @@ function getHeaders() { } export function requestOpenaiClient(path: string) { - return (body: any, method = "POST") => + return (body: any, method: string = "POST", fetchCache: boolean = true) => fetch("/api/openai?_vercel_no_cache=1", { method, headers: { "Content-Type": "application/json", path, ...getHeaders(), + "fetch-cache": fetchCache ? "enable" : "disable", }, body: body && JSON.stringify(body), + cache: fetchCache ? "default" : "no-store", //https://beta.nextjs.org/docs/data-fetching/fetching#dynamic-data-fetching }); } @@ -75,6 +77,7 @@ export async function requestChat(messages: Message[]) { } export async function requestUsage() { + const useFetchCache = false; const formatDate = (d: Date) => `${d.getFullYear()}-${(d.getMonth() + 1).toString().padStart(2, "0")}-${d .getDate() @@ -89,8 +92,12 @@ export async function requestUsage() { const [used, subs] = await Promise.all([ requestOpenaiClient( `dashboard/billing/usage?start_date=${startDate}&end_date=${endDate}`, - )(null, "GET"), - requestOpenaiClient("dashboard/billing/subscription")(null, "GET"), + )(null, "GET", useFetchCache), + requestOpenaiClient("dashboard/billing/subscription")( + null, + "GET", + useFetchCache, + ), ]); const response = (await used.json()) as { @@ -171,11 +178,11 @@ export async function requestChatStream( const resTimeoutId = setTimeout(() => finish(), TIME_OUT_MS); const content = await reader?.read(); clearTimeout(resTimeoutId); - + if (!content || !content.value) { break; } - + const text = decoder.decode(content.value, { stream: true }); responseText += text; diff --git a/app/store/index.ts b/app/store/index.ts index 3bdb58ca2..814acd926 100644 --- a/app/store/index.ts +++ b/app/store/index.ts @@ -1,3 +1,4 @@ export * from "./app"; export * from "./update"; export * from "./access"; +export * from "./usage"; diff --git a/app/store/usage.ts b/app/store/usage.ts new file mode 100644 index 000000000..90a58bb59 --- /dev/null +++ b/app/store/usage.ts @@ -0,0 +1,41 @@ +import { create } from "zustand"; +import { persist } from "zustand/middleware"; + +export interface UsageStore { + used: string; + subscription: string; + + updateUsage: (used?: string, subscription?: string) => void; + hasUsageData: () => boolean; +} + +const USAGE_KEY = "api-usage"; + +let fetchState = 0; // 0 not fetch, 1 fetching, 2 done + +export const useUsageStore = create()( + persist( + (set, get) => ({ + used: "", + subscription: "", + + updateUsage(used?: string, subscription?: string) { + set((state) => ({ + used: used ?? "[?]", + subscription: subscription ?? "[?]", + })); + }, + hasUsageData() { + const used = get().used; + const sub = get().subscription; + const hasUsed = used != "" && used != "[?]"; + const hasSubscription = sub != "" && sub != "[?]"; + return hasUsed && hasSubscription; + }, + }), + { + name: USAGE_KEY, + version: 1, + }, + ), +);