openai.ts 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. import { REQUEST_TIMEOUT_MS } from "@/app/constant";
  2. import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
  3. import { ChatOptions, getHeaders, LLMApi, LLMUsage } from "../api";
  4. import Locale from "../../locales";
  5. export class ChatGPTApi implements LLMApi {
  6. public ChatPath = "v1/chat/completions";
  7. public UsagePath = "dashboard/billing/usage";
  8. public SubsPath = "dashboard/billing/subscription";
  9. path(path: string): string {
  10. const openaiUrl = useAccessStore.getState().openaiUrl;
  11. if (openaiUrl.endsWith("/")) openaiUrl.slice(0, openaiUrl.length - 1);
  12. return [openaiUrl, path].join("/");
  13. }
  14. extractMessage(res: any) {
  15. return res.choices?.at(0)?.message?.content ?? "";
  16. }
  17. async chat(options: ChatOptions) {
  18. const messages = options.messages.map((v) => ({
  19. role: v.role,
  20. content: v.content,
  21. }));
  22. const modelConfig = {
  23. ...useAppConfig.getState().modelConfig,
  24. ...useChatStore.getState().currentSession().mask.modelConfig,
  25. ...{
  26. model: options.config.model,
  27. },
  28. };
  29. const requestPayload = {
  30. messages,
  31. stream: options.config.stream,
  32. model: modelConfig.model,
  33. temperature: modelConfig.temperature,
  34. presence_penalty: modelConfig.presence_penalty,
  35. };
  36. console.log("[Request] openai payload: ", requestPayload);
  37. const shouldStream = !!options.config.stream;
  38. const controller = new AbortController();
  39. options.onController?.(controller);
  40. try {
  41. const chatPath = this.path(this.ChatPath);
  42. const chatPayload = {
  43. method: "POST",
  44. body: JSON.stringify(requestPayload),
  45. signal: controller.signal,
  46. headers: getHeaders(),
  47. };
  48. // make a fetch request
  49. const reqestTimeoutId = setTimeout(
  50. () => controller.abort(),
  51. REQUEST_TIMEOUT_MS,
  52. );
  53. if (shouldStream) {
  54. let responseText = "";
  55. const finish = () => {
  56. options.onFinish(responseText);
  57. };
  58. const res = await fetch(chatPath, chatPayload);
  59. clearTimeout(reqestTimeoutId);
  60. if (res.status === 401) {
  61. responseText += "\n\n" + Locale.Error.Unauthorized;
  62. return finish();
  63. }
  64. if (
  65. !res.ok ||
  66. !res.headers.get("Content-Type")?.includes("stream") ||
  67. !res.body
  68. ) {
  69. return options.onError?.(new Error());
  70. }
  71. const reader = res.body.getReader();
  72. const decoder = new TextDecoder("utf-8");
  73. while (true) {
  74. const { done, value } = await reader.read();
  75. if (done) {
  76. return finish();
  77. }
  78. const chunk = decoder.decode(value);
  79. const lines = chunk.split("data: ");
  80. for (const line of lines) {
  81. const text = line.trim();
  82. if (line.startsWith("[DONE]")) {
  83. return finish();
  84. }
  85. if (text.length === 0) continue;
  86. const json = JSON.parse(text);
  87. const delta = json.choices[0].delta.content;
  88. if (delta) {
  89. responseText += delta;
  90. options.onUpdate?.(responseText, delta);
  91. }
  92. }
  93. }
  94. } else {
  95. const res = await fetch(chatPath, chatPayload);
  96. clearTimeout(reqestTimeoutId);
  97. const resJson = await res.json();
  98. const message = this.extractMessage(resJson);
  99. options.onFinish(message);
  100. }
  101. } catch (e) {
  102. console.log("[Request] failed to make a chat reqeust", e);
  103. options.onError?.(e as Error);
  104. }
  105. }
  106. async usage() {
  107. const formatDate = (d: Date) =>
  108. `${d.getFullYear()}-${(d.getMonth() + 1).toString().padStart(2, "0")}-${d
  109. .getDate()
  110. .toString()
  111. .padStart(2, "0")}`;
  112. const ONE_DAY = 1 * 24 * 60 * 60 * 1000;
  113. const now = new Date();
  114. const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
  115. const startDate = formatDate(startOfMonth);
  116. const endDate = formatDate(new Date(Date.now() + ONE_DAY));
  117. const [used, subs] = await Promise.all([
  118. fetch(
  119. this.path(
  120. `${this.UsagePath}?start_date=${startDate}&end_date=${endDate}`,
  121. ),
  122. {
  123. method: "GET",
  124. headers: getHeaders(),
  125. },
  126. ),
  127. fetch(this.path(this.SubsPath), {
  128. method: "GET",
  129. headers: getHeaders(),
  130. }),
  131. ]);
  132. if (!used.ok || !subs.ok || used.status === 401) {
  133. throw new Error(Locale.Error.Unauthorized);
  134. }
  135. const response = (await used.json()) as {
  136. total_usage?: number;
  137. error?: {
  138. type: string;
  139. message: string;
  140. };
  141. };
  142. const total = (await subs.json()) as {
  143. hard_limit_usd?: number;
  144. };
  145. if (response.error && response.error.type) {
  146. throw Error(response.error.message);
  147. }
  148. if (response.total_usage) {
  149. response.total_usage = Math.round(response.total_usage) / 100;
  150. }
  151. if (total.hard_limit_usd) {
  152. total.hard_limit_usd = Math.round(total.hard_limit_usd * 100) / 100;
  153. }
  154. return {
  155. used: response.total_usage,
  156. total: total.hard_limit_usd,
  157. } as LLMUsage;
  158. }
  159. }