openai.ts 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  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. try {
  87. const json = JSON.parse(text);
  88. const delta = json.choices[0].delta.content;
  89. if (delta) {
  90. responseText += delta;
  91. options.onUpdate?.(responseText, delta);
  92. }
  93. } catch (e) {
  94. console.error("[Request] parse error", text, chunk);
  95. }
  96. }
  97. }
  98. } else {
  99. const res = await fetch(chatPath, chatPayload);
  100. clearTimeout(reqestTimeoutId);
  101. const resJson = await res.json();
  102. const message = this.extractMessage(resJson);
  103. options.onFinish(message);
  104. }
  105. } catch (e) {
  106. console.log("[Request] failed to make a chat reqeust", e);
  107. options.onError?.(e as Error);
  108. }
  109. }
  110. async usage() {
  111. const formatDate = (d: Date) =>
  112. `${d.getFullYear()}-${(d.getMonth() + 1).toString().padStart(2, "0")}-${d
  113. .getDate()
  114. .toString()
  115. .padStart(2, "0")}`;
  116. const ONE_DAY = 1 * 24 * 60 * 60 * 1000;
  117. const now = new Date();
  118. const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
  119. const startDate = formatDate(startOfMonth);
  120. const endDate = formatDate(new Date(Date.now() + ONE_DAY));
  121. const [used, subs] = await Promise.all([
  122. fetch(
  123. this.path(
  124. `${this.UsagePath}?start_date=${startDate}&end_date=${endDate}`,
  125. ),
  126. {
  127. method: "GET",
  128. headers: getHeaders(),
  129. },
  130. ),
  131. fetch(this.path(this.SubsPath), {
  132. method: "GET",
  133. headers: getHeaders(),
  134. }),
  135. ]);
  136. if (!used.ok || !subs.ok || used.status === 401) {
  137. throw new Error(Locale.Error.Unauthorized);
  138. }
  139. const response = (await used.json()) as {
  140. total_usage?: number;
  141. error?: {
  142. type: string;
  143. message: string;
  144. };
  145. };
  146. const total = (await subs.json()) as {
  147. hard_limit_usd?: number;
  148. };
  149. if (response.error && response.error.type) {
  150. throw Error(response.error.message);
  151. }
  152. if (response.total_usage) {
  153. response.total_usage = Math.round(response.total_usage) / 100;
  154. }
  155. if (total.hard_limit_usd) {
  156. total.hard_limit_usd = Math.round(total.hard_limit_usd * 100) / 100;
  157. }
  158. return {
  159. used: response.total_usage,
  160. total: total.hard_limit_usd,
  161. } as LLMUsage;
  162. }
  163. }