openai.ts 5.3 KB

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