2023-05-14 15:00:17 +00:00
|
|
|
import { REQUEST_TIMEOUT_MS } from "@/app/constant";
|
|
|
|
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
|
2023-05-14 17:33:46 +00:00
|
|
|
|
|
|
|
import { ChatOptions, getHeaders, LLMApi, LLMUsage } from "../api";
|
|
|
|
import Locale from "../../locales";
|
2023-05-14 15:00:17 +00:00
|
|
|
|
|
|
|
export class ChatGPTApi implements LLMApi {
|
|
|
|
public ChatPath = "v1/chat/completions";
|
2023-05-14 17:33:46 +00:00
|
|
|
public UsagePath = "dashboard/billing/usage";
|
|
|
|
public SubsPath = "dashboard/billing/subscription";
|
2023-05-14 15:00:17 +00:00
|
|
|
|
|
|
|
path(path: string): string {
|
2023-05-15 16:22:11 +00:00
|
|
|
let openaiUrl = useAccessStore.getState().openaiUrl;
|
|
|
|
if (openaiUrl.endsWith("/")) {
|
|
|
|
openaiUrl = openaiUrl.slice(0, openaiUrl.length - 1);
|
|
|
|
}
|
2023-05-14 15:00:17 +00:00
|
|
|
return [openaiUrl, path].join("/");
|
|
|
|
}
|
|
|
|
|
|
|
|
extractMessage(res: any) {
|
|
|
|
return res.choices?.at(0)?.message?.content ?? "";
|
|
|
|
}
|
|
|
|
|
|
|
|
async chat(options: ChatOptions) {
|
|
|
|
const messages = options.messages.map((v) => ({
|
|
|
|
role: v.role,
|
|
|
|
content: v.content,
|
|
|
|
}));
|
|
|
|
|
|
|
|
const modelConfig = {
|
|
|
|
...useAppConfig.getState().modelConfig,
|
|
|
|
...useChatStore.getState().currentSession().mask.modelConfig,
|
|
|
|
...{
|
2023-05-14 17:33:46 +00:00
|
|
|
model: options.config.model,
|
2023-05-14 15:00:17 +00:00
|
|
|
},
|
|
|
|
};
|
|
|
|
|
|
|
|
const requestPayload = {
|
|
|
|
messages,
|
|
|
|
stream: options.config.stream,
|
|
|
|
model: modelConfig.model,
|
|
|
|
temperature: modelConfig.temperature,
|
|
|
|
presence_penalty: modelConfig.presence_penalty,
|
|
|
|
};
|
|
|
|
|
|
|
|
console.log("[Request] openai payload: ", requestPayload);
|
|
|
|
|
|
|
|
const shouldStream = !!options.config.stream;
|
|
|
|
const controller = new AbortController();
|
2023-05-14 17:33:46 +00:00
|
|
|
options.onController?.(controller);
|
2023-05-14 15:00:17 +00:00
|
|
|
|
|
|
|
try {
|
|
|
|
const chatPath = this.path(this.ChatPath);
|
|
|
|
const chatPayload = {
|
|
|
|
method: "POST",
|
|
|
|
body: JSON.stringify(requestPayload),
|
|
|
|
signal: controller.signal,
|
2023-05-14 17:33:46 +00:00
|
|
|
headers: getHeaders(),
|
2023-05-14 15:00:17 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
// make a fetch request
|
|
|
|
const reqestTimeoutId = setTimeout(
|
|
|
|
() => controller.abort(),
|
|
|
|
REQUEST_TIMEOUT_MS,
|
|
|
|
);
|
2023-05-14 17:33:46 +00:00
|
|
|
|
2023-05-14 15:00:17 +00:00
|
|
|
if (shouldStream) {
|
|
|
|
let responseText = "";
|
|
|
|
|
2023-05-14 17:33:46 +00:00
|
|
|
const finish = () => {
|
|
|
|
options.onFinish(responseText);
|
|
|
|
};
|
2023-05-14 15:00:17 +00:00
|
|
|
|
2023-05-14 17:33:46 +00:00
|
|
|
const res = await fetch(chatPath, chatPayload);
|
|
|
|
clearTimeout(reqestTimeoutId);
|
|
|
|
|
|
|
|
if (res.status === 401) {
|
|
|
|
responseText += "\n\n" + Locale.Error.Unauthorized;
|
|
|
|
return finish();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (
|
|
|
|
!res.ok ||
|
|
|
|
!res.headers.get("Content-Type")?.includes("stream") ||
|
|
|
|
!res.body
|
|
|
|
) {
|
|
|
|
return options.onError?.(new Error());
|
|
|
|
}
|
|
|
|
|
|
|
|
const reader = res.body.getReader();
|
|
|
|
const decoder = new TextDecoder("utf-8");
|
|
|
|
|
|
|
|
while (true) {
|
|
|
|
const { done, value } = await reader.read();
|
|
|
|
if (done) {
|
|
|
|
return finish();
|
|
|
|
}
|
|
|
|
|
2023-05-14 18:01:50 +00:00
|
|
|
const chunk = decoder.decode(value, { stream: true });
|
2023-05-14 17:33:46 +00:00
|
|
|
const lines = chunk.split("data: ");
|
|
|
|
|
|
|
|
for (const line of lines) {
|
|
|
|
const text = line.trim();
|
|
|
|
if (line.startsWith("[DONE]")) {
|
|
|
|
return finish();
|
2023-05-14 15:00:17 +00:00
|
|
|
}
|
2023-05-14 17:33:46 +00:00
|
|
|
if (text.length === 0) continue;
|
2023-05-14 17:55:45 +00:00
|
|
|
try {
|
|
|
|
const json = JSON.parse(text);
|
|
|
|
const delta = json.choices[0].delta.content;
|
|
|
|
if (delta) {
|
|
|
|
responseText += delta;
|
|
|
|
options.onUpdate?.(responseText, delta);
|
|
|
|
}
|
|
|
|
} catch (e) {
|
|
|
|
console.error("[Request] parse error", text, chunk);
|
2023-05-14 15:00:17 +00:00
|
|
|
}
|
2023-05-14 17:33:46 +00:00
|
|
|
}
|
|
|
|
}
|
2023-05-14 15:00:17 +00:00
|
|
|
} else {
|
|
|
|
const res = await fetch(chatPath, chatPayload);
|
2023-05-14 17:33:46 +00:00
|
|
|
clearTimeout(reqestTimeoutId);
|
2023-05-14 15:00:17 +00:00
|
|
|
|
|
|
|
const resJson = await res.json();
|
|
|
|
const message = this.extractMessage(resJson);
|
|
|
|
options.onFinish(message);
|
|
|
|
}
|
|
|
|
} catch (e) {
|
|
|
|
console.log("[Request] failed to make a chat reqeust", e);
|
2023-05-14 17:33:46 +00:00
|
|
|
options.onError?.(e as Error);
|
2023-05-14 15:00:17 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
async usage() {
|
2023-05-14 17:33:46 +00:00
|
|
|
const formatDate = (d: Date) =>
|
|
|
|
`${d.getFullYear()}-${(d.getMonth() + 1).toString().padStart(2, "0")}-${d
|
|
|
|
.getDate()
|
|
|
|
.toString()
|
|
|
|
.padStart(2, "0")}`;
|
|
|
|
const ONE_DAY = 1 * 24 * 60 * 60 * 1000;
|
|
|
|
const now = new Date();
|
|
|
|
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
|
|
|
|
const startDate = formatDate(startOfMonth);
|
|
|
|
const endDate = formatDate(new Date(Date.now() + ONE_DAY));
|
|
|
|
|
|
|
|
const [used, subs] = await Promise.all([
|
|
|
|
fetch(
|
|
|
|
this.path(
|
|
|
|
`${this.UsagePath}?start_date=${startDate}&end_date=${endDate}`,
|
|
|
|
),
|
|
|
|
{
|
|
|
|
method: "GET",
|
|
|
|
headers: getHeaders(),
|
|
|
|
},
|
|
|
|
),
|
|
|
|
fetch(this.path(this.SubsPath), {
|
|
|
|
method: "GET",
|
|
|
|
headers: getHeaders(),
|
|
|
|
}),
|
|
|
|
]);
|
|
|
|
|
|
|
|
if (!used.ok || !subs.ok || used.status === 401) {
|
|
|
|
throw new Error(Locale.Error.Unauthorized);
|
|
|
|
}
|
|
|
|
|
|
|
|
const response = (await used.json()) as {
|
|
|
|
total_usage?: number;
|
|
|
|
error?: {
|
|
|
|
type: string;
|
|
|
|
message: string;
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
const total = (await subs.json()) as {
|
|
|
|
hard_limit_usd?: number;
|
|
|
|
};
|
|
|
|
|
|
|
|
if (response.error && response.error.type) {
|
|
|
|
throw Error(response.error.message);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (response.total_usage) {
|
|
|
|
response.total_usage = Math.round(response.total_usage) / 100;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (total.hard_limit_usd) {
|
|
|
|
total.hard_limit_usd = Math.round(total.hard_limit_usd * 100) / 100;
|
|
|
|
}
|
|
|
|
|
2023-05-14 15:00:17 +00:00
|
|
|
return {
|
2023-05-14 17:33:46 +00:00
|
|
|
used: response.total_usage,
|
|
|
|
total: total.hard_limit_usd,
|
2023-05-14 15:00:17 +00:00
|
|
|
} as LLMUsage;
|
|
|
|
}
|
|
|
|
}
|