diff --git a/.env.template b/.env.template index 3411db3d2..da658ae47 100644 --- a/.env.template +++ b/.env.template @@ -78,3 +78,17 @@ BILIBILI_COOKIES= # Default: Empty # Address of the metaprocess server for advanced video processing features (currently music recognition). Leaving this empty will disable them. BILIVID_METAPROCESS_SERVER_ADDRESS= + +# anthropic claude Api Key.(optional) +ANTHROPIC_API_KEY= + +### anthropic claude Api version. (optional) +ANTHROPIC_API_VERSION= + + + +### anthropic claude Api url (optional) +ANTHROPIC_URL= + +### (optional) +WHITE_WEBDEV_ENDPOINTS= diff --git a/README.md b/README.md index a1871a2eb..01c0d5883 100644 --- a/README.md +++ b/README.md @@ -265,6 +265,29 @@ Azure Api 版本,你可以在这里找到:[Azure 文档](https://learn.micro 如果你不想让用户使用历史摘要功能,将此环境变量设置为 1 即可。 +### `ANTHROPIC_API_KEY` (optional) + +anthropic claude Api Key. + +### `ANTHROPIC_API_VERSION` (optional) + +anthropic claude Api version. + +### `ANTHROPIC_URL` (optional) + +anthropic claude Api Url. + +### `DISABLE_FAST_LINK` (可选) + +如果你想禁用从链接解析预制设置,将此环境变量设置为 1 即可。 + +### `WHITE_WEBDEV_ENDPOINTS` (可选) + +如果你想增加允许访问的webdav服务地址,可以使用该选项,格式要求: +- 每一个地址必须是一个完整的 endpoint +> `https://xxxx/xxx` +- 多个地址以`,`相连 + ## 部署 ### 容器部署 (推荐) diff --git a/README_CN.md b/README_CN.md index 464d538da..963fda5e5 100644 --- a/README_CN.md +++ b/README_CN.md @@ -114,6 +114,18 @@ Google Gemini Pro 密钥. Google Gemini Pro Api Url. +### `ANTHROPIC_API_KEY` (optional) + +anthropic claude Api Key. + +### `ANTHROPIC_API_VERSION` (optional) + +anthropic claude Api version. + +### `ANTHROPIC_URL` (optional) + +anthropic claude Api Url. + ### `HIDE_USER_API_KEY` (可选) 如果你不想让用户自行填入 API Key,将此环境变量设置为 1 即可。 @@ -130,6 +142,13 @@ Google Gemini Pro Api Url. 如果你想禁用从链接解析预制设置,将此环境变量设置为 1 即可。 +### `WHITE_WEBDEV_ENDPOINTS` (可选) + +如果你想增加允许访问的webdav服务地址,可以使用该选项,格式要求: +- 每一个地址必须是一个完整的 endpoint +> `https://xxxx/xxx` +- 多个地址以`,`相连 + ### `CUSTOM_MODELS` (可选) > 示例:`+qwen-7b-chat,+glm-6b,-gpt-3.5-turbo,gpt-4-1106-preview=gpt-4-turbo` 表示增加 `qwen-7b-chat` 和 `glm-6b` 到模型列表,而从列表中删除 `gpt-3.5-turbo`,并将 `gpt-4-1106-preview` 模型名字展示为 `gpt-4-turbo`。 diff --git a/app/api/anthropic/[...path]/route.ts b/app/api/anthropic/[...path]/route.ts new file mode 100644 index 000000000..4264893d9 --- /dev/null +++ b/app/api/anthropic/[...path]/route.ts @@ -0,0 +1,189 @@ +import { getServerSideConfig } from "@/app/config/server"; +import { + ANTHROPIC_BASE_URL, + Anthropic, + ApiPath, + DEFAULT_MODELS, + ModelProvider, +} from "@/app/constant"; +import { prettyObject } from "@/app/utils/format"; +import { NextRequest, NextResponse } from "next/server"; +import { auth } from "../../auth"; +import { collectModelTable } from "@/app/utils/model"; + +const ALLOWD_PATH = new Set([Anthropic.ChatPath, Anthropic.ChatPath1]); + +async function handle( + req: NextRequest, + { params }: { params: { path: string[] } }, +) { + console.log("[Anthropic Route] params ", params); + + if (req.method === "OPTIONS") { + return NextResponse.json({ body: "OK" }, { status: 200 }); + } + + const subpath = params.path.join("/"); + + if (!ALLOWD_PATH.has(subpath)) { + console.log("[Anthropic Route] forbidden path ", subpath); + return NextResponse.json( + { + error: true, + msg: "you are not allowed to request " + subpath, + }, + { + status: 403, + }, + ); + } + + const authResult = auth(req, ModelProvider.Claude); + if (authResult.error) { + return NextResponse.json(authResult, { + status: 401, + }); + } + + try { + const response = await request(req); + return response; + } catch (e) { + console.error("[Anthropic] ", e); + return NextResponse.json(prettyObject(e)); + } +} + +export const GET = handle; +export const POST = handle; + +export const runtime = "edge"; +export const preferredRegion = [ + "arn1", + "bom1", + "cdg1", + "cle1", + "cpt1", + "dub1", + "fra1", + "gru1", + "hnd1", + "iad1", + "icn1", + "kix1", + "lhr1", + "pdx1", + "sfo1", + "sin1", + "syd1", +]; + +const serverConfig = getServerSideConfig(); + +async function request(req: NextRequest) { + const controller = new AbortController(); + + let authHeaderName = "x-api-key"; + let authValue = + req.headers.get(authHeaderName) || + req.headers.get("Authorization")?.replaceAll("Bearer ", "").trim() || + serverConfig.anthropicApiKey || + ""; + + let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.Anthropic, ""); + + let baseUrl = + serverConfig.anthropicUrl || serverConfig.baseUrl || ANTHROPIC_BASE_URL; + + if (!baseUrl.startsWith("http")) { + baseUrl = `https://${baseUrl}`; + } + + if (baseUrl.endsWith("/")) { + baseUrl = baseUrl.slice(0, -1); + } + + console.log("[Proxy] ", path); + console.log("[Base Url]", baseUrl); + + const timeoutId = setTimeout( + () => { + controller.abort(); + }, + 10 * 60 * 1000, + ); + + const fetchUrl = `${baseUrl}${path}`; + + const fetchOptions: RequestInit = { + headers: { + "Content-Type": "application/json", + "Cache-Control": "no-store", + [authHeaderName]: authValue, + "anthropic-version": + req.headers.get("anthropic-version") || + serverConfig.anthropicApiVersion || + Anthropic.Vision, + }, + method: req.method, + body: req.body, + redirect: "manual", + // @ts-ignore + duplex: "half", + signal: controller.signal, + }; + + // #1815 try to refuse some request to some models + if (serverConfig.customModels && req.body) { + try { + const modelTable = collectModelTable( + DEFAULT_MODELS, + serverConfig.customModels, + ); + const clonedBody = await req.text(); + fetchOptions.body = clonedBody; + + const jsonBody = JSON.parse(clonedBody) as { model?: string }; + + // not undefined and is false + if (modelTable[jsonBody?.model ?? ""].available === false) { + return NextResponse.json( + { + error: true, + message: `you are not allowed to use ${jsonBody?.model} model`, + }, + { + status: 403, + }, + ); + } + } catch (e) { + console.error(`[Anthropic] filter`, e); + } + } + console.log("[Anthropic request]", fetchOptions.headers, req.method); + try { + const res = await fetch(fetchUrl, fetchOptions); + + console.log( + "[Anthropic response]", + res.status, + " ", + res.headers, + res.url, + ); + // to prevent browser prompt for credentials + const newHeaders = new Headers(res.headers); + newHeaders.delete("www-authenticate"); + // to disable nginx buffering + newHeaders.set("X-Accel-Buffering", "no"); + + return new Response(res.body, { + status: res.status, + statusText: res.statusText, + headers: newHeaders, + }); + } finally { + clearTimeout(timeoutId); + } +} diff --git a/app/api/auth.ts b/app/api/auth.ts index 4a12268a9..21e8c3468 100644 --- a/app/api/auth.ts +++ b/app/api/auth.ts @@ -62,12 +62,31 @@ export function auth(req: NextRequest, modelProvider: ModelProvider) { if (!apiKey) { const serverConfig = getServerSideConfig(); - const systemApiKey = - modelProvider === ModelProvider.GeminiPro - ? serverConfig.googleApiKey - : serverConfig.isAzure - ? serverConfig.azureApiKey - : serverConfig.apiKey; + // const systemApiKey = + // modelProvider === ModelProvider.GeminiPro + // ? serverConfig.googleApiKey + // : serverConfig.isAzure + // ? serverConfig.azureApiKey + // : serverConfig.apiKey; + + let systemApiKey: string | undefined; + + switch (modelProvider) { + case ModelProvider.GeminiPro: + systemApiKey = serverConfig.googleApiKey; + break; + case ModelProvider.Claude: + systemApiKey = serverConfig.anthropicApiKey; + break; + case ModelProvider.GPT: + default: + if (serverConfig.isAzure) { + systemApiKey = serverConfig.azureApiKey; + } else { + systemApiKey = serverConfig.apiKey; + } + } + if (systemApiKey) { console.log("[Auth] use system api key"); req.headers.set("Authorization", `Bearer ${systemApiKey}`); diff --git a/app/api/webdav/[...path]/route.ts b/app/api/webdav/[...path]/route.ts index 75c23ac87..f64a9ef13 100644 --- a/app/api/webdav/[...path]/route.ts +++ b/app/api/webdav/[...path]/route.ts @@ -1,5 +1,14 @@ import { NextRequest, NextResponse } from "next/server"; -import { STORAGE_KEY } from "../../../constant"; +import { STORAGE_KEY, internalWhiteWebDavEndpoints } from "../../../constant"; +import { getServerSideConfig } from "@/app/config/server"; + +const config = getServerSideConfig(); + +const mergedWhiteWebDavEndpoints = [ + ...internalWhiteWebDavEndpoints, + ...config.whiteWebDevEndpoints, +].filter((domain) => Boolean(domain.trim())); + async function handle( req: NextRequest, { params }: { params: { path: string[] } }, @@ -14,7 +23,9 @@ async function handle( let endpoint = requestUrl.searchParams.get("endpoint"); // Validate the endpoint to prevent potential SSRF attacks - if (!endpoint || !endpoint.startsWith("/")) { + if ( + !mergedWhiteWebDavEndpoints.some((white) => endpoint?.startsWith(white)) + ) { return NextResponse.json( { error: true, @@ -25,6 +36,11 @@ async function handle( }, ); } + + if (!endpoint?.endsWith("/")) { + endpoint += "/"; + } + const endpointPath = params.path.join("/"); const targetPath = `${endpoint}/${endpointPath}`; @@ -108,7 +124,7 @@ async function handle( return fetchResult; } -export const POST = handle; +export const PUT = handle; export const GET = handle; export const OPTIONS = handle; diff --git a/app/client/api.ts b/app/client/api.ts index 77759b318..68a8e7dbf 100644 --- a/app/client/api.ts +++ b/app/client/api.ts @@ -9,6 +9,7 @@ import { ChatMessage, ModelType, useAccessStore, useChatStore } from "../store"; import { ChatGPTApi } from "./platforms/openai"; import { FileApi, FileInfo } from "./platforms/utils"; import { GeminiProApi } from "./platforms/google"; +import { ClaudeApi } from "./platforms/anthropic"; export const ROLES = ["system", "user", "assistant"] as const; export type MessageRole = (typeof ROLES)[number]; @@ -152,10 +153,15 @@ export class ClientApi { public file: FileApi; constructor(provider: ModelProvider = ModelProvider.GPT) { - if (provider === ModelProvider.GeminiPro) { - this.llm = new GeminiProApi(); - } else { - this.llm = new ChatGPTApi(); + switch (provider) { + case ModelProvider.GeminiPro: + this.llm = new GeminiProApi(); + break; + case ModelProvider.Claude: + this.llm = new ClaudeApi(); + break; + default: + this.llm = new ChatGPTApi(); } this.file = new FileApi(); } diff --git a/app/client/platforms/anthropic.ts b/app/client/platforms/anthropic.ts new file mode 100644 index 000000000..fe5c61dae --- /dev/null +++ b/app/client/platforms/anthropic.ts @@ -0,0 +1,424 @@ +import { ACCESS_CODE_PREFIX, Anthropic, ApiPath } from "@/app/constant"; +import { + AgentChatOptions, + ChatOptions, + CreateRAGStoreOptions, + LLMApi, + MultimodalContent, + SpeechOptions, + TranscriptionOptions, +} from "../api"; +import { useAccessStore, useAppConfig, useChatStore } from "@/app/store"; +import { getClientConfig } from "@/app/config/client"; +import { DEFAULT_API_HOST } from "@/app/constant"; +import { RequestMessage } from "@/app/typing"; +import { + EventStreamContentType, + fetchEventSource, +} from "@fortaine/fetch-event-source"; + +import Locale from "../../locales"; +import { prettyObject } from "@/app/utils/format"; +import { getMessageTextContent, isVisionModel } from "@/app/utils"; + +export type MultiBlockContent = { + type: "image" | "text"; + source?: { + type: string; + media_type: string; + data: string; + }; + text?: string; +}; + +export type AnthropicMessage = { + role: (typeof ClaudeMapper)[keyof typeof ClaudeMapper]; + content: string | MultiBlockContent[]; +}; + +export interface AnthropicChatRequest { + model: string; // The model that will complete your prompt. + messages: AnthropicMessage[]; // The prompt that you want Claude to complete. + max_tokens: number; // The maximum number of tokens to generate before stopping. + stop_sequences?: string[]; // Sequences that will cause the model to stop generating completion text. + temperature?: number; // Amount of randomness injected into the response. + top_p?: number; // Use nucleus sampling. + top_k?: number; // Only sample from the top K options for each subsequent token. + metadata?: object; // An object describing metadata about the request. + stream?: boolean; // Whether to incrementally stream the response using server-sent events. +} + +export interface ChatRequest { + model: string; // The model that will complete your prompt. + prompt: string; // The prompt that you want Claude to complete. + max_tokens_to_sample: number; // The maximum number of tokens to generate before stopping. + stop_sequences?: string[]; // Sequences that will cause the model to stop generating completion text. + temperature?: number; // Amount of randomness injected into the response. + top_p?: number; // Use nucleus sampling. + top_k?: number; // Only sample from the top K options for each subsequent token. + metadata?: object; // An object describing metadata about the request. + stream?: boolean; // Whether to incrementally stream the response using server-sent events. +} + +export interface ChatResponse { + completion: string; + stop_reason: "stop_sequence" | "max_tokens"; + model: string; +} + +export type ChatStreamResponse = ChatResponse & { + stop?: string; + log_id: string; +}; + +const ClaudeMapper = { + assistant: "assistant", + user: "user", + system: "user", +} as const; + +const keys = ["claude-2, claude-instant-1"]; + +export class ClaudeApi implements LLMApi { + speech(options: SpeechOptions): Promise { + throw new Error("Method not implemented."); + } + transcription(options: TranscriptionOptions): Promise { + throw new Error("Method not implemented."); + } + toolAgentChat(options: AgentChatOptions): Promise { + throw new Error("Method not implemented."); + } + createRAGStore(options: CreateRAGStoreOptions): Promise { + throw new Error("Method not implemented."); + } + extractMessage(res: any) { + console.log("[Response] claude response: ", res); + + return res?.content?.[0]?.text; + } + async chat(options: ChatOptions): Promise { + const visionModel = isVisionModel(options.config.model); + + const accessStore = useAccessStore.getState(); + + const shouldStream = !!options.config.stream; + + const modelConfig = { + ...useAppConfig.getState().modelConfig, + ...useChatStore.getState().currentSession().mask.modelConfig, + ...{ + model: options.config.model, + }, + }; + + const messages = [...options.messages]; + + const keys = ["system", "user"]; + + // roles must alternate between "user" and "assistant" in claude, so add a fake assistant message between two user messages + for (let i = 0; i < messages.length - 1; i++) { + const message = messages[i]; + const nextMessage = messages[i + 1]; + + if (keys.includes(message.role) && keys.includes(nextMessage.role)) { + messages[i] = [ + message, + { + role: "assistant", + content: ";", + }, + ] as any; + } + } + + const prompt = messages + .flat() + .filter((v) => { + if (!v.content) return false; + if (typeof v.content === "string" && !v.content.trim()) return false; + return true; + }) + .map((v) => { + const { role, content } = v; + const insideRole = ClaudeMapper[role] ?? "user"; + + if (!visionModel || typeof content === "string") { + return { + role: insideRole, + content: getMessageTextContent(v), + }; + } + return { + role: insideRole, + content: content + .filter((v) => v.image_url || v.text) + .map(({ type, text, image_url }) => { + if (type === "text") { + return { + type, + text: text!, + }; + } + const { url = "" } = image_url || {}; + const colonIndex = url.indexOf(":"); + const semicolonIndex = url.indexOf(";"); + const comma = url.indexOf(","); + + const mimeType = url.slice(colonIndex + 1, semicolonIndex); + const encodeType = url.slice(semicolonIndex + 1, comma); + const data = url.slice(comma + 1); + + return { + type: "image" as const, + source: { + type: encodeType, + media_type: mimeType, + data, + }, + }; + }), + }; + }); + + const requestBody: AnthropicChatRequest = { + messages: prompt, + stream: shouldStream, + + model: modelConfig.model, + max_tokens: modelConfig.max_tokens, + temperature: modelConfig.temperature, + top_p: modelConfig.top_p, + // top_k: modelConfig.top_k, + top_k: 5, + }; + + const path = this.path(Anthropic.ChatPath); + + const controller = new AbortController(); + options.onController?.(controller); + + const payload = { + method: "POST", + body: JSON.stringify(requestBody), + signal: controller.signal, + headers: { + "Content-Type": "application/json", + Accept: "application/json", + "x-api-key": accessStore.anthropicApiKey, + "anthropic-version": accessStore.anthropicApiVersion, + Authorization: getAuthKey(accessStore.anthropicApiKey), + }, + }; + + if (shouldStream) { + try { + const context = { + text: "", + finished: false, + }; + + const finish = () => { + if (!context.finished) { + options.onFinish(context.text); + context.finished = true; + } + }; + + controller.signal.onabort = finish; + fetchEventSource(path, { + ...payload, + async onopen(res) { + const contentType = res.headers.get("content-type"); + console.log("response content type: ", contentType); + + if (contentType?.startsWith("text/plain")) { + context.text = await res.clone().text(); + return finish(); + } + + if ( + !res.ok || + !res.headers + .get("content-type") + ?.startsWith(EventStreamContentType) || + res.status !== 200 + ) { + const responseTexts = [context.text]; + let extraInfo = await res.clone().text(); + try { + const resJson = await res.clone().json(); + extraInfo = prettyObject(resJson); + } catch {} + + if (res.status === 401) { + responseTexts.push(Locale.Error.Unauthorized); + } + + if (extraInfo) { + responseTexts.push(extraInfo); + } + + context.text = responseTexts.join("\n\n"); + + return finish(); + } + }, + onmessage(msg) { + let chunkJson: + | undefined + | { + type: "content_block_delta" | "content_block_stop"; + delta?: { + type: "text_delta"; + text: string; + }; + index: number; + }; + try { + chunkJson = JSON.parse(msg.data); + } catch (e) { + console.error("[Response] parse error", msg.data); + } + + if (!chunkJson || chunkJson.type === "content_block_stop") { + return finish(); + } + + const { delta } = chunkJson; + if (delta?.text) { + context.text += delta.text; + options.onUpdate?.(context.text, delta.text); + } + }, + onclose() { + finish(); + }, + onerror(e) { + options.onError?.(e); + throw e; + }, + openWhenHidden: true, + }); + } catch (e) { + console.error("failed to chat", e); + options.onError?.(e as Error); + } + } else { + try { + controller.signal.onabort = () => options.onFinish(""); + + const res = await fetch(path, payload); + const resJson = await res.json(); + + const message = this.extractMessage(resJson); + options.onFinish(message); + } catch (e) { + console.error("failed to chat", e); + options.onError?.(e as Error); + } + } + } + async usage() { + return { + used: 0, + total: 0, + }; + } + async models() { + // const provider = { + // id: "anthropic", + // providerName: "Anthropic", + // providerType: "anthropic", + // }; + + return [ + // { + // name: "claude-instant-1.2", + // available: true, + // provider, + // }, + // { + // name: "claude-2.0", + // available: true, + // provider, + // }, + // { + // name: "claude-2.1", + // available: true, + // provider, + // }, + // { + // name: "claude-3-opus-20240229", + // available: true, + // provider, + // }, + // { + // name: "claude-3-sonnet-20240229", + // available: true, + // provider, + // }, + // { + // name: "claude-3-haiku-20240307", + // available: true, + // provider, + // }, + ]; + } + path(path: string): string { + const accessStore = useAccessStore.getState(); + + let baseUrl: string = accessStore.anthropicUrl; + + // if endpoint is empty, use default endpoint + if (baseUrl.trim().length === 0) { + const isApp = !!getClientConfig()?.isApp; + + baseUrl = isApp + ? DEFAULT_API_HOST + "/api/proxy/anthropic" + : ApiPath.Anthropic; + } + + if (!baseUrl.startsWith("http") && !baseUrl.startsWith("/api")) { + baseUrl = "https://" + baseUrl; + } + + baseUrl = trimEnd(baseUrl, "/"); + + return `${baseUrl}/${path}`; + } +} + +function trimEnd(s: string, end = " ") { + if (end.length === 0) return s; + + while (s.endsWith(end)) { + s = s.slice(0, -end.length); + } + + return s; +} + +function bearer(value: string) { + return `Bearer ${value.trim()}`; +} + +function getAuthKey(apiKey = "") { + const accessStore = useAccessStore.getState(); + const isApp = !!getClientConfig()?.isApp; + let authKey = ""; + + if (apiKey) { + // use user's api key first + authKey = bearer(apiKey); + } else if ( + accessStore.enabledAccessControl() && + !isApp && + !!accessStore.accessCode + ) { + // or use access code + authKey = bearer(ACCESS_CODE_PREFIX + accessStore.accessCode); + } + + return authKey; +} diff --git a/app/client/platforms/openai.ts b/app/client/platforms/openai.ts index 0ff9ad706..a1d7d05d3 100644 --- a/app/client/platforms/openai.ts +++ b/app/client/platforms/openai.ts @@ -44,6 +44,20 @@ export interface OpenAIListModelResponse { }>; } +interface RequestPayload { + messages: { + role: "system" | "user" | "assistant"; + content: string | MultimodalContent[]; + }[]; + stream?: boolean; + model: string; + temperature: number; + presence_penalty: number; + frequency_penalty: number; + top_p: number; + max_tokens?: number; +} + export class ChatGPTApi implements LLMApi { private disableListModels = true; @@ -181,7 +195,8 @@ export class ChatGPTApi implements LLMApi { model: options.config.model, }, }; - const requestPayload = { + + const requestPayload: RequestPayload = { messages, stream: options.config.stream, model: modelConfig.model, @@ -189,21 +204,13 @@ export class ChatGPTApi implements LLMApi { presence_penalty: modelConfig.presence_penalty, frequency_penalty: modelConfig.frequency_penalty, top_p: modelConfig.top_p, - max_tokens: modelConfig.model.includes("vision") - ? modelConfig.max_tokens - : null, // max_tokens: Math.max(modelConfig.max_tokens, 1024), // Please do not ask me why not send max_tokens, no reason, this param is just shit, I dont want to explain anymore. }; // add max_tokens to vision model if (visionModel) { - Object.defineProperty(requestPayload, "max_tokens", { - enumerable: true, - configurable: true, - writable: true, - value: modelConfig.max_tokens, - }); + requestPayload["max_tokens"] = Math.max(modelConfig.max_tokens, 4000); } console.log("[Request] openai payload: ", requestPayload); diff --git a/app/components/exporter.tsx b/app/components/exporter.tsx index d64581246..cc70af460 100644 --- a/app/components/exporter.tsx +++ b/app/components/exporter.tsx @@ -40,6 +40,7 @@ import { EXPORT_MESSAGE_CLASS_NAME, ModelProvider } from "../constant"; import { getClientConfig } from "../config/client"; import { ClientApi } from "../client/api"; import { getMessageTextContent } from "../utils"; +import { identifyDefaultClaudeModel } from "../utils/checkers"; const Markdown = dynamic(async () => (await import("./markdown")).Markdown, { loading: () => , @@ -315,6 +316,8 @@ export function PreviewActions(props: { var api: ClientApi; if (config.modelConfig.model.startsWith("gemini")) { api = new ClientApi(ModelProvider.GeminiPro); + } else if (identifyDefaultClaudeModel(config.modelConfig.model)) { + api = new ClientApi(ModelProvider.Claude); } else { api = new ClientApi(ModelProvider.GPT); } diff --git a/app/components/home.tsx b/app/components/home.tsx index 00044a4dc..88f25593b 100644 --- a/app/components/home.tsx +++ b/app/components/home.tsx @@ -29,6 +29,7 @@ import { AuthPage } from "./auth"; import { getClientConfig } from "../config/client"; import { ClientApi } from "../client/api"; import { useAccessStore } from "../store"; +import { identifyDefaultClaudeModel } from "../utils/checkers"; export function Loading(props: { noLogo?: boolean }) { return ( @@ -180,6 +181,8 @@ export function useLoadData() { var api: ClientApi; if (config.modelConfig.model.startsWith("gemini")) { api = new ClientApi(ModelProvider.GeminiPro); + } else if (identifyDefaultClaudeModel(config.modelConfig.model)) { + api = new ClientApi(ModelProvider.Claude); } else { api = new ClientApi(ModelProvider.GPT); } diff --git a/app/components/markdown.tsx b/app/components/markdown.tsx index 7c70fe1a5..1afd7de3b 100644 --- a/app/components/markdown.tsx +++ b/app/components/markdown.tsx @@ -135,10 +135,9 @@ function escapeBrackets(text: string) { } function _MarkDownContent(props: { content: string }) { - const escapedContent = useMemo( - () => escapeBrackets(escapeDollarNumber(props.content)), - [props.content], - ); + const escapedContent = useMemo(() => { + return escapeBrackets(escapeDollarNumber(props.content)); + }, [props.content]); return ( {checkingUpdate ? ( @@ -970,7 +971,7 @@ export function Settings() { - {accessStore.provider === "OpenAI" ? ( + {accessStore.provider === ServiceProvider.OpenAI && ( <> - ) : accessStore.provider === "Azure" ? ( + )} + {accessStore.provider === ServiceProvider.Azure && ( <> - ) : accessStore.provider === "Google" ? ( + )} + {accessStore.provider === ServiceProvider.Google && ( <> - ) : null} + )} + {accessStore.provider === ServiceProvider.Anthropic && ( + <> + + + accessStore.update( + (access) => + (access.anthropicUrl = e.currentTarget.value), + ) + } + > + + + { + accessStore.update( + (access) => + (access.anthropicApiKey = + e.currentTarget.value), + ); + }} + /> + + + + accessStore.update( + (access) => + (access.anthropicApiVersion = + e.currentTarget.value), + ) + } + > + + + )} )} diff --git a/app/config/server.ts b/app/config/server.ts index 36ff0d3cf..e92cccc3e 100644 --- a/app/config/server.ts +++ b/app/config/server.ts @@ -69,6 +69,7 @@ export const getServerSideConfig = () => { const isAzure = !!process.env.AZURE_URL; const isGoogle = !!process.env.GOOGLE_API_KEY; + const isAnthropic = !!process.env.ANTHROPIC_API_KEY; const apiKeyEnvVar = process.env.OPENAI_API_KEY ?? ""; const apiKeys = apiKeyEnvVar.split(",").map((v) => v.trim()); @@ -78,6 +79,10 @@ export const getServerSideConfig = () => { `[Server Config] using ${randomIndex + 1} of ${apiKeys.length} api key`, ); + const whiteWebDevEndpoints = (process.env.WHITE_WEBDEV_ENDPOINTS ?? "").split( + ",", + ); + return { baseUrl: process.env.BASE_URL, apiKey, @@ -92,6 +97,11 @@ export const getServerSideConfig = () => { googleApiKey: process.env.GOOGLE_API_KEY, googleUrl: process.env.GEMINI_BASE_URL ?? process.env.GOOGLE_URL, + isAnthropic, + anthropicApiKey: process.env.ANTHROPIC_API_KEY, + anthropicApiVersion: process.env.ANTHROPIC_API_VERSION, + anthropicUrl: process.env.ANTHROPIC_URL, + gtmId: process.env.GTM_ID, needCode: ACCESS_CODES.size > 0, @@ -107,6 +117,8 @@ export const getServerSideConfig = () => { disableFastLink: !!process.env.DISABLE_FAST_LINK, customModels, + whiteWebDevEndpoints, + isStoreFileToLocal: !!process.env.NEXT_PUBLIC_ENABLE_NODEJS_PLUGIN && !process.env.R2_ACCOUNT_ID && diff --git a/app/constant.ts b/app/constant.ts index e081e2bff..ac89c8007 100644 --- a/app/constant.ts +++ b/app/constant.ts @@ -11,6 +11,8 @@ export const RUNTIME_CONFIG_DOM = "danger-runtime-config"; export const DEFAULT_API_HOST = "https://api.nextchat.dev"; export const OPENAI_BASE_URL = "https://api.openai.com"; export const GOOGLE_BASE_URL = "https://generativelanguage.googleapis.com"; +export const GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/"; +export const ANTHROPIC_BASE_URL = "https://api.anthropic.com"; export enum Path { Home = "/", @@ -26,6 +28,7 @@ export enum ApiPath { Cors = "", OpenAI = "/api/openai", GoogleAI = "/api/google", + Anthropic = "/api/anthropic", } export enum SlotID { @@ -70,13 +73,22 @@ export enum ServiceProvider { OpenAI = "OpenAI", Azure = "Azure", Google = "Google", + Anthropic = "Anthropic", } export enum ModelProvider { GPT = "GPT", GeminiPro = "GeminiPro", + Claude = "Claude", } +export const Anthropic = { + ChatPath: "v1/messages", + ChatPath1: "v1/complete", + ExampleEndpoint: "https://api.anthropic.com", + Vision: "2023-06-01", +}; + export const OpenaiPath = { ChatPath: "v1/chat/completions", SpeechPath: "v1/audio/speech", @@ -99,12 +111,20 @@ export const Google = { }; export const DEFAULT_INPUT_TEMPLATE = `{{input}}`; // input / time / model / lang +// export const DEFAULT_SYSTEM_TEMPLATE = ` +// You are ChatGPT, a large language model trained by {{ServiceProvider}}. +// Knowledge cutoff: {{cutoff}} +// Current model: {{model}} +// Current time: {{time}} +// Latex inline: $x^2$ +// Latex block: $$e=mc^2$$ +// `; export const DEFAULT_SYSTEM_TEMPLATE = ` You are ChatGPT, a large language model trained by {{ServiceProvider}}. Knowledge cutoff: {{cutoff}} Current model: {{model}} Current time: {{time}} -Latex inline: $x^2$ +Latex inline: \\(x^2\\) Latex block: $$e=mc^2$$ `; @@ -113,6 +133,7 @@ export const GEMINI_SUMMARIZE_MODEL = "gemini-pro"; export const KnowledgeCutOffDate: Record = { default: "2021-09", + "gpt-4-turbo": "2023-12", "gpt-4-turbo-preview": "2023-12", "gpt-4-1106-preview": "2023-04", "gpt-4-0125-preview": "2023-12", @@ -175,6 +196,24 @@ export const DEFAULT_MODELS = [ providerType: "openai", }, }, + { + name: "gpt-4-turbo", + available: true, + provider: { + id: "openai", + providerName: "OpenAI", + providerType: "openai", + }, + }, + { + name: "gpt-4-turbo-2024-04-09", + available: true, + provider: { + id: "openai", + providerName: "OpenAI", + providerType: "openai", + }, + }, { name: "gpt-4-turbo-preview", available: true, @@ -283,7 +322,73 @@ export const DEFAULT_MODELS = [ providerType: "google", }, }, + { + name: "claude-instant-1.2", + available: true, + provider: { + id: "anthropic", + providerName: "Anthropic", + providerType: "anthropic", + }, + }, + { + name: "claude-2.0", + available: true, + provider: { + id: "anthropic", + providerName: "Anthropic", + providerType: "anthropic", + }, + }, + { + name: "claude-2.1", + available: true, + provider: { + id: "anthropic", + providerName: "Anthropic", + providerType: "anthropic", + }, + }, + { + name: "claude-3-opus-20240229", + available: true, + provider: { + id: "anthropic", + providerName: "Anthropic", + providerType: "anthropic", + }, + }, + { + name: "claude-3-sonnet-20240229", + available: true, + provider: { + id: "anthropic", + providerName: "Anthropic", + providerType: "anthropic", + }, + }, + { + name: "claude-3-haiku-20240307", + available: true, + provider: { + id: "anthropic", + providerName: "Anthropic", + providerType: "anthropic", + }, + }, ] as const; export const CHAT_PAGE_SIZE = 15; export const MAX_RENDER_MSG_COUNT = 45; + +// some famous webdav endpoints +export const internalWhiteWebDavEndpoints = [ + "https://dav.jianguoyun.com/dav/", + "https://dav.dropdav.com/", + "https://dav.box.com/dav", + "https://nanao.teracloud.jp/dav/", + "https://webdav.4shared.com/", + "https://dav.idrivesync.com", + "https://webdav.yandex.com", + "https://app.koofr.net/dav/Koofr", +]; diff --git a/app/layout.tsx b/app/layout.tsx index 07df4413d..16f0091e8 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -36,6 +36,10 @@ export default function RootLayout({ + diff --git a/app/locales/cn.ts b/app/locales/cn.ts index 543d7bc37..9064df08d 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -321,6 +321,23 @@ const cn = { SubTitle: "选择指定的部分版本", }, }, + Anthropic: { + ApiKey: { + Title: "接口密钥", + SubTitle: "使用自定义 Anthropic Key 绕过密码访问限制", + Placeholder: "Anthropic API Key", + }, + + Endpoint: { + Title: "接口地址", + SubTitle: "样例:", + }, + + ApiVerion: { + Title: "接口版本 (claude api version)", + SubTitle: "选择一个特定的 API 版本输入", + }, + }, Google: { ApiKey: { Title: "API 密钥", diff --git a/app/locales/en.ts b/app/locales/en.ts index 2fe2cc975..325c257f0 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -324,6 +324,24 @@ const en: LocaleType = { SubTitle: "Check your api version from azure console", }, }, + Anthropic: { + ApiKey: { + Title: "Anthropic API Key", + SubTitle: + "Use a custom Anthropic Key to bypass password access restrictions", + Placeholder: "Anthropic API Key", + }, + + Endpoint: { + Title: "Endpoint Address", + SubTitle: "Example:", + }, + + ApiVerion: { + Title: "API Version (claude api version)", + SubTitle: "Select and input a specific API version", + }, + }, CustomModel: { Title: "Custom Models", SubTitle: "Custom model options, seperated by comma", diff --git a/app/locales/pt.ts b/app/locales/pt.ts index 85226ed50..8151b7aa4 100644 --- a/app/locales/pt.ts +++ b/app/locales/pt.ts @@ -316,6 +316,23 @@ const pt: PartialLocaleType = { SubTitle: "Verifique sua versão API do console Azure", }, }, + Anthropic: { + ApiKey: { + Title: "Chave API Anthropic", + SubTitle: "Verifique sua chave API do console Anthropic", + Placeholder: "Chave API Anthropic", + }, + + Endpoint: { + Title: "Endpoint Address", + SubTitle: "Exemplo: ", + }, + + ApiVerion: { + Title: "Versão API (Versão api claude)", + SubTitle: "Verifique sua versão API do console Anthropic", + }, + }, CustomModel: { Title: "Modelos Personalizados", SubTitle: "Opções de modelo personalizado, separados por vírgula", diff --git a/app/locales/sk.ts b/app/locales/sk.ts index 025519e77..a97b7175c 100644 --- a/app/locales/sk.ts +++ b/app/locales/sk.ts @@ -317,6 +317,23 @@ const sk: PartialLocaleType = { SubTitle: "Skontrolujte svoju verziu API v Azure konzole", }, }, + Anthropic: { + ApiKey: { + Title: "API kľúč Anthropic", + SubTitle: "Skontrolujte svoj API kľúč v Anthropic konzole", + Placeholder: "API kľúč Anthropic", + }, + + Endpoint: { + Title: "Adresa koncového bodu", + SubTitle: "Príklad:", + }, + + ApiVerion: { + Title: "Verzia API (claude verzia API)", + SubTitle: "Vyberte špecifickú verziu časti", + }, + }, CustomModel: { Title: "Vlastné modely", SubTitle: "Možnosti vlastného modelu, oddelené čiarkou", diff --git a/app/locales/tw.ts b/app/locales/tw.ts index 1add5d12a..96811ae7e 100644 --- a/app/locales/tw.ts +++ b/app/locales/tw.ts @@ -314,6 +314,23 @@ const tw = { SubTitle: "選擇指定的部分版本", }, }, + Anthropic: { + ApiKey: { + Title: "API 密鑰", + SubTitle: "從 Anthropic AI 獲取您的 API 密鑰", + Placeholder: "Anthropic API Key", + }, + + Endpoint: { + Title: "終端地址", + SubTitle: "示例:", + }, + + ApiVerion: { + Title: "API 版本 (claude api version)", + SubTitle: "選擇一個特定的 API 版本输入", + }, + }, Google: { ApiKey: { Title: "API 密鑰", diff --git a/app/store/access.ts b/app/store/access.ts index c10e4e6c9..9c94951b7 100644 --- a/app/store/access.ts +++ b/app/store/access.ts @@ -36,6 +36,11 @@ const DEFAULT_ACCESS_STATE = { googleApiKey: "", googleApiVersion: "v1", + // anthropic + anthropicApiKey: "", + anthropicApiVersion: "2023-06-01", + anthropicUrl: "", + // server config needCode: true, hideUserApiKey: false, @@ -74,6 +79,10 @@ export const useAccessStore = createPersistStore( return ensure(get(), ["googleApiKey"]); }, + isValidAnthropic() { + return ensure(get(), ["anthropicApiKey"]); + }, + isAuthorized() { this.fetch(); @@ -82,6 +91,7 @@ export const useAccessStore = createPersistStore( this.isValidOpenAI() || this.isValidAzure() || this.isValidGoogle() || + this.isValidAnthropic() || !this.enabledAccessControl() || (this.enabledAccessControl() && ensure(get(), ["accessCode"])) ); diff --git a/app/store/chat.ts b/app/store/chat.ts index 9293aecf4..7fe75032e 100644 --- a/app/store/chat.ts +++ b/app/store/chat.ts @@ -27,6 +27,7 @@ export interface ChatToolMessage { } import { createPersistStore } from "../utils/store"; import { FileInfo } from "../client/platforms/utils"; +import { identifyDefaultClaudeModel } from "../utils/checkers"; export type ChatMessage = RequestMessage & { date: string; @@ -141,6 +142,11 @@ function fillTemplateWith(input: string, modelConfig: ModelConfig) { let output = modelConfig.template ?? DEFAULT_INPUT_TEMPLATE; + // remove duplicate + if (input.startsWith(output)) { + output = ""; + } + // must contains {{input}} const inputVar = "{{input}}"; if (!output.includes(inputVar)) { @@ -469,6 +475,10 @@ export const useChatStore = createPersistStore( } else { if (modelConfig.model.startsWith("gemini")) { api = new ClientApi(ModelProvider.GeminiPro); + } else if (identifyDefaultClaudeModel(modelConfig.model)) { + api = new ClientApi(ModelProvider.Claude); + } else { + api = new ClientApi(ModelProvider.GPT); } // make request api.llm.chat({ @@ -613,7 +623,6 @@ export const useChatStore = createPersistStore( tokenCount += estimateTokenLength(getMessageTextContent(msg)); reversedRecentMessages.push(msg); } - // concat all messages const recentMessages = [ ...systemPrompts, @@ -652,6 +661,8 @@ export const useChatStore = createPersistStore( var api: ClientApi; if (modelConfig.model.startsWith("gemini")) { api = new ClientApi(ModelProvider.GeminiPro); + } else if (identifyDefaultClaudeModel(modelConfig.model)) { + api = new ClientApi(ModelProvider.Claude); } else { api = new ClientApi(ModelProvider.GPT); } diff --git a/app/typing.ts b/app/typing.ts index 25e474abf..b09722ab9 100644 --- a/app/typing.ts +++ b/app/typing.ts @@ -1 +1,9 @@ export type Updater = (updater: (value: T) => void) => void; + +export const ROLES = ["system", "user", "assistant"] as const; +export type MessageRole = (typeof ROLES)[number]; + +export interface RequestMessage { + role: MessageRole; + content: string; +} diff --git a/app/utils.ts b/app/utils.ts index e6042402b..284867a88 100644 --- a/app/utils.ts +++ b/app/utils.ts @@ -3,7 +3,6 @@ import { showToast } from "./components/ui-lib"; import Locale from "./locales"; import { RequestMessage } from "./client/api"; import { DEFAULT_MODELS } from "./constant"; -import { useAccessStore } from "./store"; export function trimTopic(topic: string) { // Fix an issue where double quotes still show in the Indonesian language @@ -292,10 +291,13 @@ export function getMessageImages(message: RequestMessage): string[] { } export function isVisionModel(model: string) { - // Note: This is a better way using the TypeScript feature instead of `&&` or `||` (ts v5.5.0-dev.20240314 I've been using) const visionKeywords = ["vision", "claude-3"]; + const isGpt4Turbo = + model.includes("gpt-4-turbo") && !model.includes("preview"); - return visionKeywords.some((keyword) => model.includes(keyword)); + return ( + visionKeywords.some((keyword) => model.includes(keyword)) || isGpt4Turbo + ); } export function isSupportRAGModel(modelName: string) { diff --git a/app/utils/checkers.ts b/app/utils/checkers.ts new file mode 100644 index 000000000..4496e1039 --- /dev/null +++ b/app/utils/checkers.ts @@ -0,0 +1,21 @@ +import { useAccessStore } from "../store/access"; +import { useAppConfig } from "../store/config"; +import { collectModels } from "./model"; + +export function identifyDefaultClaudeModel(modelName: string) { + const accessStore = useAccessStore.getState(); + const configStore = useAppConfig.getState(); + + const allModals = collectModels( + configStore.models, + [configStore.customModels, accessStore.customModels].join(","), + ); + + const modelMeta = allModals.find((m) => m.name === modelName); + + return ( + modelName.startsWith("claude") && + modelMeta && + modelMeta.provider?.providerType === "anthropic" + ); +} diff --git a/app/utils/model.ts b/app/utils/model.ts index 434850d90..378fc498e 100644 --- a/app/utils/model.ts +++ b/app/utils/model.ts @@ -22,6 +22,12 @@ export function collectModelTable( }; }); + const customProvider = (modelName: string) => ({ + id: modelName, + providerName: "", + providerType: "custom", + }); + // server custom models customModels .split(",") @@ -42,7 +48,7 @@ export function collectModelTable( name, displayName: displayName || name, available, - provider: modelTable[name]?.provider, // Use optional chaining + provider: modelTable[name]?.provider ?? customProvider(name), // Use optional chaining }; } }); diff --git a/app/utils/object.ts b/app/utils/object.ts new file mode 100644 index 000000000..b2588779d --- /dev/null +++ b/app/utils/object.ts @@ -0,0 +1,17 @@ +export function omit( + obj: T, + ...keys: U +): Omit { + const ret: any = { ...obj }; + keys.forEach((key) => delete ret[key]); + return ret; +} + +export function pick( + obj: T, + ...keys: U +): Pick { + const ret: any = {}; + keys.forEach((key) => (ret[key] = obj[key])); + return ret; +} diff --git a/next.config.mjs b/next.config.mjs index c8e7adb83..daaeba468 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -77,6 +77,10 @@ if (mode !== "export") { source: "/api/proxy/openai/:path*", destination: "https://api.openai.com/:path*", }, + { + source: "/api/proxy/anthropic/:path*", + destination: "https://api.anthropic.com/:path*", + }, { source: "/google-fonts/:path*", destination: "https://fonts.googleapis.com/:path*", diff --git a/package.json b/package.json index d60797105..94d1de21a 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "@hello-pangea/dnd": "^16.5.0", "@langchain/cohere": "^0.0.6", "@langchain/community": "0.0.30", - "@langchain/openai": "0.0.14", + "@langchain/openai": "0.0.26", "@langchain/pinecone": "^0.0.4", "@next/third-parties": "^14.1.0", "@pinecone-database/pinecone": "^2.2.0", @@ -44,7 +44,7 @@ "html-to-text": "^9.0.5", "https-proxy-agent": "^7.0.2", "md5": "^2.3.0", - "langchain": "0.1.30", + "langchain": "0.1.33", "mammoth": "^1.7.1", "mermaid": "^10.6.1", "mime": "^4.0.1", @@ -92,7 +92,7 @@ }, "resolutions": { "lint-staged/yaml": "^2.2.2", - "@langchain/core": "0.1.53", + "@langchain/core": "0.1.57", "openai": "4.28.4" }, "packageManager": "yarn@1.22.19" diff --git a/yarn.lock b/yarn.lock index ac9e489f0..109d9384f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1939,13 +1939,13 @@ uuid "^9.0.0" zod "^3.22.3" -"@langchain/community@~0.0.41": - version "0.0.44" - resolved "https://registry.yarnpkg.com/@langchain/community/-/community-0.0.44.tgz#b4f3453e3fd0b7a8c704fc35b004d7d738bd3416" - integrity sha512-II9Hz90jJmfWRICtxTg1auQWzFw0npqacWiiOpaxNhzs6rptdf56gyfC48Z6n1ii4R8FfAlfX6YxhOE7lGGKXg== +"@langchain/community@~0.0.47": + version "0.0.47" + resolved "https://registry.yarnpkg.com/@langchain/community/-/community-0.0.47.tgz#e7b00b3f98e3217932785c70e9926abf750f8cfc" + integrity sha512-j20KhnWs16K/qxaY3QccOFMfcEmiTXN00NB1IPeO1pexRsvHL7GfqZ8rR5cOSSBfft2iuoGxaj1EZIyxh5ftgA== dependencies: - "@langchain/core" "~0.1.44" - "@langchain/openai" "~0.0.19" + "@langchain/core" "~0.1.56" + "@langchain/openai" "~0.0.28" expr-eval "^2.0.2" flat "^5.0.2" langsmith "~0.1.1" @@ -1953,10 +1953,10 @@ zod "^3.22.3" zod-to-json-schema "^3.22.5" -"@langchain/core@0.1.53", "@langchain/core@~0.1", "@langchain/core@~0.1.13", "@langchain/core@~0.1.29", "@langchain/core@~0.1.44", "@langchain/core@~0.1.45": - version "0.1.53" - resolved "https://registry.yarnpkg.com/@langchain/core/-/core-0.1.53.tgz#40bf273b6d5e1426c60ce9cc259562fe656573f1" - integrity sha512-khfRTu2DSCNMPUmnKx7iH0TpEaunW/4BgR6STTteRRDd0NFtXGfAwUuY9sm0+EKi/XKhdAmpGnfLwSfNg5F0Qw== +"@langchain/core@0.1.57", "@langchain/core@~0.1", "@langchain/core@~0.1.13", "@langchain/core@~0.1.29", "@langchain/core@~0.1.45", "@langchain/core@~0.1.56": + version "0.1.57" + resolved "https://registry.yarnpkg.com/@langchain/core/-/core-0.1.57.tgz#c592ad6715e373e4203f590f20b8b999e64cfb16" + integrity sha512-6wOwidPkkRcANrOKl88+YYpm3jHfpg6W8EqZHQCImSAlxdEhyDSq2eeQKHOPCFCrfNWkClaNn+Wlzzz4Qwf9Tg== dependencies: ansi-styles "^5.0.0" camelcase "6" @@ -1981,7 +1981,7 @@ zod "^3.22.4" zod-to-json-schema "^3.22.3" -"@langchain/openai@~0.0.14", "@langchain/openai@~0.0.19": +"@langchain/openai@~0.0.14": version "0.0.23" resolved "https://registry.npmjs.org/@langchain/openai/-/openai-0.0.23.tgz" integrity sha512-H5yv2hKQ5JVa6jC1wQxiN2299lJbPc5JUv93c6IUw+0jr0kFqH48NWbcythz1UFj2rOpZdaFJSYJs2nr9bhVLg== @@ -1992,6 +1992,17 @@ zod "^3.22.4" zod-to-json-schema "^3.22.3" +"@langchain/openai@~0.0.28": + version "0.0.28" + resolved "https://registry.yarnpkg.com/@langchain/openai/-/openai-0.0.28.tgz#afaeec61b44816935db9ae937496c964c81ab571" + integrity sha512-2s1RA3/eAnz4ahdzsMPBna9hfAqpFNlWdHiPxVGZ5yrhXsbLWWoPcF+22LCk9t0HJKtazi2GCIWc0HVXH9Abig== + dependencies: + "@langchain/core" "~0.1.56" + js-tiktoken "^1.0.7" + openai "^4.32.1" + zod "^3.22.4" + zod-to-json-schema "^3.22.3" + "@langchain/pinecone@^0.0.4": version "0.0.4" resolved "https://registry.yarnpkg.com/@langchain/pinecone/-/pinecone-0.0.4.tgz#312f3ff4286b1278c47c676d7be5a4f0f5c1409c" @@ -6091,15 +6102,15 @@ kleur@^4.0.3: resolved "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz" integrity sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ== -langchain@0.1.30: - version "0.1.30" - resolved "https://registry.yarnpkg.com/langchain/-/langchain-0.1.30.tgz#e1adb3f1849fcd5c596c668300afd5dc8cb37a97" - integrity sha512-5h/vNMmutQ98tbB0sPDlAileZVca6A2McFgGa3+D56Dm8mSSCzTQL2DngPA6h09DlKDpSr7+6PdFw5Hoj0ZDSw== +langchain@0.1.33: + version "0.1.33" + resolved "https://registry.yarnpkg.com/langchain/-/langchain-0.1.33.tgz#959f0f023975092569b49f1f07fe60c11c643530" + integrity sha512-IrRd839x8eAmDutHNDMHGzIjufyWt/ckJrnRB2WifIJmtLWNRNQo5jZd7toeBU0UOVOQxoPtQGYf8lR0ar7vIQ== dependencies: "@anthropic-ai/sdk" "^0.9.1" - "@langchain/community" "~0.0.41" - "@langchain/core" "~0.1.44" - "@langchain/openai" "~0.0.19" + "@langchain/community" "~0.0.47" + "@langchain/core" "~0.1.56" + "@langchain/openai" "~0.0.28" binary-extensions "^2.2.0" js-tiktoken "^1.0.7" js-yaml "^4.1.0" @@ -7135,7 +7146,7 @@ onetime@^6.0.0: dependencies: mimic-fn "^4.0.0" -openai@4.28.4, openai@^4.26.0: +openai@4.28.4, openai@^4.26.0, openai@^4.32.1: version "4.28.4" resolved "https://registry.yarnpkg.com/openai/-/openai-4.28.4.tgz#d4bf1f53a89ef151bf066ef284489e12e7dd1657" integrity sha512-RNIwx4MT/F0zyizGcwS+bXKLzJ8QE9IOyigDG/ttnwB220d58bYjYFp0qjvGwEFBO6+pvFVIDABZPGDl46RFsg==