index.ts 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. import { ModelConfig, ProviderConfig } from "@/app/store";
  2. import { createLogger } from "@/app/utils/log";
  3. import { getAuthKey } from "../common/auth";
  4. import { API_PREFIX, AnthropicPath, ApiPath } from "@/app/constant";
  5. import { getApiPath } from "@/app/utils/path";
  6. import { trimEnd } from "@/app/utils/string";
  7. import { Anthropic } from "./types";
  8. import { ChatOptions, LLMModel, LLMUsage, RequestMessage } from "../types";
  9. import { omit } from "@/app/utils/object";
  10. import {
  11. EventStreamContentType,
  12. fetchEventSource,
  13. } from "@fortaine/fetch-event-source";
  14. import { prettyObject } from "@/app/utils/format";
  15. import Locale from "@/app/locales";
  16. import { AnthropicConfig } from "./config";
  17. export function createAnthropicClient(
  18. providerConfigs: ProviderConfig,
  19. modelConfig: ModelConfig,
  20. ) {
  21. const anthropicConfig = { ...providerConfigs.anthropic };
  22. const logger = createLogger("[Anthropic]");
  23. const anthropicModelConfig = { ...modelConfig.anthropic };
  24. return {
  25. headers() {
  26. return {
  27. "Content-Type": "application/json",
  28. "x-api-key": getAuthKey(anthropicConfig.apiKey),
  29. "anthropic-version": anthropicConfig.version,
  30. };
  31. },
  32. path(path: AnthropicPath): string {
  33. let baseUrl: string = anthropicConfig.endpoint;
  34. // if endpoint is empty, use default endpoint
  35. if (baseUrl.trim().length === 0) {
  36. baseUrl = getApiPath(ApiPath.Anthropic);
  37. }
  38. if (!baseUrl.startsWith("http") && !baseUrl.startsWith(API_PREFIX)) {
  39. baseUrl = "https://" + baseUrl;
  40. }
  41. baseUrl = trimEnd(baseUrl, "/");
  42. return `${baseUrl}/${path}`;
  43. },
  44. extractMessage(res: Anthropic.ChatResponse) {
  45. return res.completion;
  46. },
  47. beforeRequest(options: ChatOptions, stream = false) {
  48. const ClaudeMapper: Record<RequestMessage["role"], string> = {
  49. assistant: "Assistant",
  50. user: "Human",
  51. system: "Human",
  52. };
  53. const prompt = options.messages
  54. .map((v) => ({
  55. role: ClaudeMapper[v.role] ?? "Human",
  56. content: v.content,
  57. }))
  58. .map((v) => `\n\n${v.role}: ${v.content}`)
  59. .join("");
  60. if (options.shouldSummarize) {
  61. anthropicModelConfig.model = anthropicModelConfig.summarizeModel;
  62. }
  63. const requestBody: Anthropic.ChatRequest = {
  64. prompt,
  65. stream,
  66. ...omit(anthropicModelConfig, "summarizeModel"),
  67. };
  68. const path = this.path(AnthropicPath.Chat);
  69. logger.log("path = ", path, requestBody);
  70. const controller = new AbortController();
  71. options.onController?.(controller);
  72. const payload = {
  73. method: "POST",
  74. body: JSON.stringify(requestBody),
  75. signal: controller.signal,
  76. headers: this.headers(),
  77. mode: "no-cors" as RequestMode,
  78. };
  79. return {
  80. path,
  81. payload,
  82. controller,
  83. };
  84. },
  85. async chat(options: ChatOptions) {
  86. try {
  87. const { path, payload, controller } = this.beforeRequest(
  88. options,
  89. false,
  90. );
  91. controller.signal.onabort = () => options.onFinish("");
  92. const res = await fetch(path, payload);
  93. const resJson = await res.json();
  94. const message = this.extractMessage(resJson);
  95. options.onFinish(message);
  96. } catch (e) {
  97. logger.error("failed to chat", e);
  98. options.onError?.(e as Error);
  99. }
  100. },
  101. async chatStream(options: ChatOptions) {
  102. try {
  103. const { path, payload, controller } = this.beforeRequest(options, true);
  104. const context = {
  105. text: "",
  106. finished: false,
  107. };
  108. const finish = () => {
  109. if (!context.finished) {
  110. options.onFinish(context.text);
  111. context.finished = true;
  112. }
  113. };
  114. controller.signal.onabort = finish;
  115. logger.log(payload);
  116. fetchEventSource(path, {
  117. ...payload,
  118. async onopen(res) {
  119. const contentType = res.headers.get("content-type");
  120. logger.log("response content type: ", contentType);
  121. if (contentType?.startsWith("text/plain")) {
  122. context.text = await res.clone().text();
  123. return finish();
  124. }
  125. if (
  126. !res.ok ||
  127. !res.headers
  128. .get("content-type")
  129. ?.startsWith(EventStreamContentType) ||
  130. res.status !== 200
  131. ) {
  132. const responseTexts = [context.text];
  133. let extraInfo = await res.clone().text();
  134. try {
  135. const resJson = await res.clone().json();
  136. extraInfo = prettyObject(resJson);
  137. } catch {}
  138. if (res.status === 401) {
  139. responseTexts.push(Locale.Error.Unauthorized);
  140. }
  141. if (extraInfo) {
  142. responseTexts.push(extraInfo);
  143. }
  144. context.text = responseTexts.join("\n\n");
  145. return finish();
  146. }
  147. },
  148. onmessage(msg) {
  149. if (msg.data === "[DONE]" || context.finished) {
  150. return finish();
  151. }
  152. const chunk = msg.data;
  153. try {
  154. const chunkJson = JSON.parse(
  155. chunk,
  156. ) as Anthropic.ChatStreamResponse;
  157. const delta = chunkJson.completion;
  158. if (delta) {
  159. context.text += delta;
  160. options.onUpdate?.(context.text, delta);
  161. }
  162. } catch (e) {
  163. logger.error("[Request] parse error", chunk, msg);
  164. }
  165. },
  166. onclose() {
  167. finish();
  168. },
  169. onerror(e) {
  170. options.onError?.(e);
  171. },
  172. openWhenHidden: true,
  173. });
  174. } catch (e) {
  175. logger.error("failed to chat", e);
  176. options.onError?.(e as Error);
  177. }
  178. },
  179. async usage() {
  180. return {
  181. used: 0,
  182. total: 0,
  183. } as LLMUsage;
  184. },
  185. async models(): Promise<LLMModel[]> {
  186. const customModels = anthropicConfig.customModels
  187. .split(",")
  188. .map((v) => v.trim())
  189. .filter((v) => !!v)
  190. .map((v) => ({
  191. name: v,
  192. available: true,
  193. }));
  194. return [...AnthropicConfig.provider.models.slice(), ...customModels];
  195. },
  196. };
  197. }