mirror of
				https://github.com/Yidadaa/ChatGPT-Next-Web.git
				synced 2025-11-04 16:57:27 +08:00 
			
		
		
		
	Merge branch 'main' of https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web
This commit is contained in:
		
							
								
								
									
										189
									
								
								app/api/anthropic/[...path]/route.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										189
									
								
								app/api/anthropic/[...path]/route.ts
									
									
									
									
									
										Normal file
									
								
							@@ -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);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -57,12 +57,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}`);
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,7 @@ import {
 | 
			
		||||
import { ChatMessage, ModelType, useAccessStore, useChatStore } from "../store";
 | 
			
		||||
import { ChatGPTApi } from "./platforms/openai";
 | 
			
		||||
import { GeminiProApi } from "./platforms/google";
 | 
			
		||||
import { ClaudeApi } from "./platforms/anthropic";
 | 
			
		||||
export const ROLES = ["system", "user", "assistant"] as const;
 | 
			
		||||
export type MessageRole = (typeof ROLES)[number];
 | 
			
		||||
 | 
			
		||||
@@ -94,11 +95,16 @@ export class ClientApi {
 | 
			
		||||
  public llm: LLMApi;
 | 
			
		||||
 | 
			
		||||
  constructor(provider: ModelProvider = ModelProvider.GPT) {
 | 
			
		||||
    if (provider === ModelProvider.GeminiPro) {
 | 
			
		||||
      this.llm = new GeminiProApi();
 | 
			
		||||
      return;
 | 
			
		||||
    switch (provider) {
 | 
			
		||||
      case ModelProvider.GeminiPro:
 | 
			
		||||
        this.llm = new GeminiProApi();
 | 
			
		||||
        break;
 | 
			
		||||
      case ModelProvider.Claude:
 | 
			
		||||
        this.llm = new ClaudeApi();
 | 
			
		||||
        break;
 | 
			
		||||
      default:
 | 
			
		||||
        this.llm = new ChatGPTApi();
 | 
			
		||||
    }
 | 
			
		||||
    this.llm = new ChatGPTApi();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  config() {}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										404
									
								
								app/client/platforms/anthropic.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										404
									
								
								app/client/platforms/anthropic.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,404 @@
 | 
			
		||||
import { ACCESS_CODE_PREFIX, Anthropic, ApiPath } from "@/app/constant";
 | 
			
		||||
import { ChatOptions, LLMApi, MultimodalContent } 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 {
 | 
			
		||||
  extractMessage(res: any) {
 | 
			
		||||
    console.log("[Response] claude response: ", res);
 | 
			
		||||
 | 
			
		||||
    return res?.content?.[0]?.text;
 | 
			
		||||
  }
 | 
			
		||||
  async chat(options: ChatOptions): Promise<void> {
 | 
			
		||||
    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;
 | 
			
		||||
}
 | 
			
		||||
@@ -40,6 +40,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;
 | 
			
		||||
 | 
			
		||||
@@ -98,7 +112,7 @@ export class ChatGPTApi implements LLMApi {
 | 
			
		||||
      },
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const requestPayload = {
 | 
			
		||||
    const requestPayload: RequestPayload = {
 | 
			
		||||
      messages,
 | 
			
		||||
      stream: options.config.stream,
 | 
			
		||||
      model: modelConfig.model,
 | 
			
		||||
@@ -112,12 +126,7 @@ export class ChatGPTApi implements LLMApi {
 | 
			
		||||
 | 
			
		||||
    // 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);
 | 
			
		||||
@@ -229,7 +238,9 @@ export class ChatGPTApi implements LLMApi {
 | 
			
		||||
            const text = msg.data;
 | 
			
		||||
            try {
 | 
			
		||||
              const json = JSON.parse(text);
 | 
			
		||||
              const choices = json.choices as Array<{ delta: { content: string } }>;
 | 
			
		||||
              const choices = json.choices as Array<{
 | 
			
		||||
                delta: { content: string };
 | 
			
		||||
              }>;
 | 
			
		||||
              const delta = choices[0]?.delta?.content;
 | 
			
		||||
              const textmoderation = json?.prompt_filter_results;
 | 
			
		||||
 | 
			
		||||
@@ -237,9 +248,17 @@ export class ChatGPTApi implements LLMApi {
 | 
			
		||||
                remainText += delta;
 | 
			
		||||
              }
 | 
			
		||||
 | 
			
		||||
              if (textmoderation && textmoderation.length > 0 && ServiceProvider.Azure) {
 | 
			
		||||
                const contentFilterResults = textmoderation[0]?.content_filter_results;
 | 
			
		||||
                console.log(`[${ServiceProvider.Azure}] [Text Moderation] flagged categories result:`, contentFilterResults);
 | 
			
		||||
              if (
 | 
			
		||||
                textmoderation &&
 | 
			
		||||
                textmoderation.length > 0 &&
 | 
			
		||||
                ServiceProvider.Azure
 | 
			
		||||
              ) {
 | 
			
		||||
                const contentFilterResults =
 | 
			
		||||
                  textmoderation[0]?.content_filter_results;
 | 
			
		||||
                console.log(
 | 
			
		||||
                  `[${ServiceProvider.Azure}] [Text Moderation] flagged categories result:`,
 | 
			
		||||
                  contentFilterResults,
 | 
			
		||||
                );
 | 
			
		||||
              }
 | 
			
		||||
            } catch (e) {
 | 
			
		||||
              console.error("[Request] parse error", text, msg);
 | 
			
		||||
 
 | 
			
		||||
@@ -315,6 +315,8 @@ export function PreviewActions(props: {
 | 
			
		||||
    var api: ClientApi;
 | 
			
		||||
    if (config.modelConfig.model.startsWith("gemini")) {
 | 
			
		||||
      api = new ClientApi(ModelProvider.GeminiPro);
 | 
			
		||||
    } else if (config.modelConfig.model.startsWith("claude")) {
 | 
			
		||||
      api = new ClientApi(ModelProvider.Claude);
 | 
			
		||||
    } else {
 | 
			
		||||
      api = new ClientApi(ModelProvider.GPT);
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -173,6 +173,8 @@ export function useLoadData() {
 | 
			
		||||
  var api: ClientApi;
 | 
			
		||||
  if (config.modelConfig.model.startsWith("gemini")) {
 | 
			
		||||
    api = new ClientApi(ModelProvider.GeminiPro);
 | 
			
		||||
  } else if (config.modelConfig.model.startsWith("claude")) {
 | 
			
		||||
    api = new ClientApi(ModelProvider.Claude);
 | 
			
		||||
  } else {
 | 
			
		||||
    api = new ClientApi(ModelProvider.GPT);
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -135,10 +135,10 @@ function escapeBrackets(text: string) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function _MarkDownContent(props: { content: string }) {
 | 
			
		||||
  const escapedContent = useMemo(
 | 
			
		||||
    () => escapeBrackets(escapeDollarNumber(props.content)),
 | 
			
		||||
    [props.content],
 | 
			
		||||
  );
 | 
			
		||||
  const escapedContent = useMemo(() => {
 | 
			
		||||
    console.log("================", props.content);
 | 
			
		||||
    return escapeBrackets(escapeDollarNumber(props.content));
 | 
			
		||||
  }, [props.content]);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <ReactMarkdown
 | 
			
		||||
 
 | 
			
		||||
@@ -51,6 +51,7 @@ import Locale, {
 | 
			
		||||
import { copyToClipboard } from "../utils";
 | 
			
		||||
import Link from "next/link";
 | 
			
		||||
import {
 | 
			
		||||
  Anthropic,
 | 
			
		||||
  Azure,
 | 
			
		||||
  Google,
 | 
			
		||||
  OPENAI_BASE_URL,
 | 
			
		||||
@@ -963,7 +964,7 @@ export function Settings() {
 | 
			
		||||
                    </Select>
 | 
			
		||||
                  </ListItem>
 | 
			
		||||
 | 
			
		||||
                  {accessStore.provider === "OpenAI" ? (
 | 
			
		||||
                  {accessStore.provider === ServiceProvider.OpenAI && (
 | 
			
		||||
                    <>
 | 
			
		||||
                      <ListItem
 | 
			
		||||
                        title={Locale.Settings.Access.OpenAI.Endpoint.Title}
 | 
			
		||||
@@ -1002,7 +1003,8 @@ export function Settings() {
 | 
			
		||||
                        />
 | 
			
		||||
                      </ListItem>
 | 
			
		||||
                    </>
 | 
			
		||||
                  ) : accessStore.provider === "Azure" ? (
 | 
			
		||||
                  )}
 | 
			
		||||
                  {accessStore.provider === ServiceProvider.Azure && (
 | 
			
		||||
                    <>
 | 
			
		||||
                      <ListItem
 | 
			
		||||
                        title={Locale.Settings.Access.Azure.Endpoint.Title}
 | 
			
		||||
@@ -1061,7 +1063,8 @@ export function Settings() {
 | 
			
		||||
                        ></input>
 | 
			
		||||
                      </ListItem>
 | 
			
		||||
                    </>
 | 
			
		||||
                  ) : accessStore.provider === "Google" ? (
 | 
			
		||||
                  )}
 | 
			
		||||
                  {accessStore.provider === ServiceProvider.Google && (
 | 
			
		||||
                    <>
 | 
			
		||||
                      <ListItem
 | 
			
		||||
                        title={Locale.Settings.Access.Google.Endpoint.Title}
 | 
			
		||||
@@ -1120,7 +1123,70 @@ export function Settings() {
 | 
			
		||||
                        ></input>
 | 
			
		||||
                      </ListItem>
 | 
			
		||||
                    </>
 | 
			
		||||
                  ) : null}
 | 
			
		||||
                  )}
 | 
			
		||||
                  {accessStore.provider === ServiceProvider.Anthropic && (
 | 
			
		||||
                    <>
 | 
			
		||||
                      <ListItem
 | 
			
		||||
                        title={Locale.Settings.Access.Anthropic.Endpoint.Title}
 | 
			
		||||
                        subTitle={
 | 
			
		||||
                          Locale.Settings.Access.Anthropic.Endpoint.SubTitle +
 | 
			
		||||
                          Anthropic.ExampleEndpoint
 | 
			
		||||
                        }
 | 
			
		||||
                      >
 | 
			
		||||
                        <input
 | 
			
		||||
                          type="text"
 | 
			
		||||
                          value={accessStore.anthropicUrl}
 | 
			
		||||
                          placeholder={Anthropic.ExampleEndpoint}
 | 
			
		||||
                          onChange={(e) =>
 | 
			
		||||
                            accessStore.update(
 | 
			
		||||
                              (access) =>
 | 
			
		||||
                                (access.anthropicUrl = e.currentTarget.value),
 | 
			
		||||
                            )
 | 
			
		||||
                          }
 | 
			
		||||
                        ></input>
 | 
			
		||||
                      </ListItem>
 | 
			
		||||
                      <ListItem
 | 
			
		||||
                        title={Locale.Settings.Access.Anthropic.ApiKey.Title}
 | 
			
		||||
                        subTitle={
 | 
			
		||||
                          Locale.Settings.Access.Anthropic.ApiKey.SubTitle
 | 
			
		||||
                        }
 | 
			
		||||
                      >
 | 
			
		||||
                        <PasswordInput
 | 
			
		||||
                          value={accessStore.anthropicApiKey}
 | 
			
		||||
                          type="text"
 | 
			
		||||
                          placeholder={
 | 
			
		||||
                            Locale.Settings.Access.Anthropic.ApiKey.Placeholder
 | 
			
		||||
                          }
 | 
			
		||||
                          onChange={(e) => {
 | 
			
		||||
                            accessStore.update(
 | 
			
		||||
                              (access) =>
 | 
			
		||||
                                (access.anthropicApiKey =
 | 
			
		||||
                                  e.currentTarget.value),
 | 
			
		||||
                            );
 | 
			
		||||
                          }}
 | 
			
		||||
                        />
 | 
			
		||||
                      </ListItem>
 | 
			
		||||
                      <ListItem
 | 
			
		||||
                        title={Locale.Settings.Access.Anthropic.ApiVerion.Title}
 | 
			
		||||
                        subTitle={
 | 
			
		||||
                          Locale.Settings.Access.Anthropic.ApiVerion.SubTitle
 | 
			
		||||
                        }
 | 
			
		||||
                      >
 | 
			
		||||
                        <input
 | 
			
		||||
                          type="text"
 | 
			
		||||
                          value={accessStore.anthropicApiVersion}
 | 
			
		||||
                          placeholder={Anthropic.Vision}
 | 
			
		||||
                          onChange={(e) =>
 | 
			
		||||
                            accessStore.update(
 | 
			
		||||
                              (access) =>
 | 
			
		||||
                                (access.anthropicApiVersion =
 | 
			
		||||
                                  e.currentTarget.value),
 | 
			
		||||
                            )
 | 
			
		||||
                          }
 | 
			
		||||
                        ></input>
 | 
			
		||||
                      </ListItem>
 | 
			
		||||
                    </>
 | 
			
		||||
                  )}
 | 
			
		||||
                </>
 | 
			
		||||
              )}
 | 
			
		||||
            </>
 | 
			
		||||
 
 | 
			
		||||
@@ -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());
 | 
			
		||||
@@ -92,6 +93,11 @@ export const getServerSideConfig = () => {
 | 
			
		||||
    googleApiKey: process.env.GOOGLE_API_KEY,
 | 
			
		||||
    googleUrl: 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,
 | 
			
		||||
 
 | 
			
		||||
@@ -10,6 +10,7 @@ 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 ANTHROPIC_BASE_URL = "https://api.anthropic.com";
 | 
			
		||||
 | 
			
		||||
export const GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/";
 | 
			
		||||
 | 
			
		||||
@@ -25,6 +26,7 @@ export enum Path {
 | 
			
		||||
export enum ApiPath {
 | 
			
		||||
  Cors = "",
 | 
			
		||||
  OpenAI = "/api/openai",
 | 
			
		||||
  Anthropic = "/api/anthropic",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export enum SlotID {
 | 
			
		||||
@@ -67,13 +69,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",
 | 
			
		||||
  UsagePath: "dashboard/billing/usage",
 | 
			
		||||
@@ -94,12 +105,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$$
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
@@ -289,6 +308,60 @@ 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;
 | 
			
		||||
 
 | 
			
		||||
@@ -313,6 +313,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 密钥",
 | 
			
		||||
 
 | 
			
		||||
@@ -316,6 +316,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",
 | 
			
		||||
 
 | 
			
		||||
@@ -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",
 | 
			
		||||
 
 | 
			
		||||
@@ -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",
 | 
			
		||||
 
 | 
			
		||||
@@ -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 密鑰",
 | 
			
		||||
@@ -467,12 +484,12 @@ const tw = {
 | 
			
		||||
 | 
			
		||||
type DeepPartial<T> = T extends object
 | 
			
		||||
  ? {
 | 
			
		||||
    [P in keyof T]?: DeepPartial<T[P]>;
 | 
			
		||||
  }
 | 
			
		||||
      [P in keyof T]?: DeepPartial<T[P]>;
 | 
			
		||||
    }
 | 
			
		||||
  : T;
 | 
			
		||||
 | 
			
		||||
export type LocaleType = typeof tw;
 | 
			
		||||
export type PartialLocaleType = DeepPartial<typeof tw>;
 | 
			
		||||
 | 
			
		||||
export default tw;
 | 
			
		||||
// Translated by @chunkiuuu, feel free the submit new pr if there are typo/incorrect translations :D
 | 
			
		||||
// Translated by @chunkiuuu, feel free the submit new pr if there are typo/incorrect translations :D
 | 
			
		||||
 
 | 
			
		||||
@@ -36,6 +36,11 @@ const DEFAULT_ACCESS_STATE = {
 | 
			
		||||
  googleApiKey: "",
 | 
			
		||||
  googleApiVersion: "v1",
 | 
			
		||||
 | 
			
		||||
  // anthropic
 | 
			
		||||
  anthropicApiKey: "",
 | 
			
		||||
  anthropicApiVersion: "2023-06-01",
 | 
			
		||||
  anthropicUrl: "",
 | 
			
		||||
 | 
			
		||||
  // server config
 | 
			
		||||
  needCode: true,
 | 
			
		||||
  hideUserApiKey: false,
 | 
			
		||||
@@ -67,6 +72,10 @@ export const useAccessStore = createPersistStore(
 | 
			
		||||
      return ensure(get(), ["googleApiKey"]);
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    isValidAnthropic() {
 | 
			
		||||
      return ensure(get(), ["anthropicApiKey"]);
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    isAuthorized() {
 | 
			
		||||
      this.fetch();
 | 
			
		||||
 | 
			
		||||
@@ -75,6 +84,7 @@ export const useAccessStore = createPersistStore(
 | 
			
		||||
        this.isValidOpenAI() ||
 | 
			
		||||
        this.isValidAzure() ||
 | 
			
		||||
        this.isValidGoogle() ||
 | 
			
		||||
        this.isValidAnthropic() ||
 | 
			
		||||
        !this.enabledAccessControl() ||
 | 
			
		||||
        (this.enabledAccessControl() && ensure(get(), ["accessCode"]))
 | 
			
		||||
      );
 | 
			
		||||
 
 | 
			
		||||
@@ -126,6 +126,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)) {
 | 
			
		||||
@@ -348,6 +353,8 @@ export const useChatStore = createPersistStore(
 | 
			
		||||
        var api: ClientApi;
 | 
			
		||||
        if (modelConfig.model.startsWith("gemini")) {
 | 
			
		||||
          api = new ClientApi(ModelProvider.GeminiPro);
 | 
			
		||||
        } else if (modelConfig.model.startsWith("claude")) {
 | 
			
		||||
          api = new ClientApi(ModelProvider.Claude);
 | 
			
		||||
        } else {
 | 
			
		||||
          api = new ClientApi(ModelProvider.GPT);
 | 
			
		||||
        }
 | 
			
		||||
@@ -494,7 +501,6 @@ export const useChatStore = createPersistStore(
 | 
			
		||||
          tokenCount += estimateTokenLength(getMessageTextContent(msg));
 | 
			
		||||
          reversedRecentMessages.push(msg);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // concat all messages
 | 
			
		||||
        const recentMessages = [
 | 
			
		||||
          ...systemPrompts,
 | 
			
		||||
@@ -533,6 +539,8 @@ export const useChatStore = createPersistStore(
 | 
			
		||||
        var api: ClientApi;
 | 
			
		||||
        if (modelConfig.model.startsWith("gemini")) {
 | 
			
		||||
          api = new ClientApi(ModelProvider.GeminiPro);
 | 
			
		||||
        } else if (modelConfig.model.startsWith("claude")) {
 | 
			
		||||
          api = new ClientApi(ModelProvider.Claude);
 | 
			
		||||
        } else {
 | 
			
		||||
          api = new ClientApi(ModelProvider.GPT);
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -1 +1,9 @@
 | 
			
		||||
export type Updater<T> = (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;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										23
									
								
								app/utils.ts
									
									
									
									
									
								
							
							
						
						
									
										23
									
								
								app/utils.ts
									
									
									
									
									
								
							@@ -2,16 +2,17 @@ import { useEffect, useState } from "react";
 | 
			
		||||
import { showToast } from "./components/ui-lib";
 | 
			
		||||
import Locale from "./locales";
 | 
			
		||||
import { RequestMessage } from "./client/api";
 | 
			
		||||
import { DEFAULT_MODELS } from "./constant";
 | 
			
		||||
 | 
			
		||||
export function trimTopic(topic: string) {
 | 
			
		||||
  // Fix an issue where double quotes still show in the Indonesian language
 | 
			
		||||
  // This will remove the specified punctuation from the end of the string
 | 
			
		||||
  // and also trim quotes from both the start and end if they exist.
 | 
			
		||||
  return topic
 | 
			
		||||
    // fix for gemini
 | 
			
		||||
    .replace(/^["“”*]+|["“”*]+$/g, "")
 | 
			
		||||
    .replace(/[,。!?”“"、,.!?*]*$/, "");
 | 
			
		||||
  return (
 | 
			
		||||
    topic
 | 
			
		||||
      // fix for gemini
 | 
			
		||||
      .replace(/^["“”*]+|["“”*]+$/g, "")
 | 
			
		||||
      .replace(/[,。!?”“"、,.!?*]*$/, "")
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function copyToClipboard(text: string) {
 | 
			
		||||
@@ -57,10 +58,7 @@ export async function downloadAs(text: string, filename: string) {
 | 
			
		||||
 | 
			
		||||
    if (result !== null) {
 | 
			
		||||
      try {
 | 
			
		||||
        await window.__TAURI__.fs.writeTextFile(
 | 
			
		||||
          result,
 | 
			
		||||
          text
 | 
			
		||||
        );
 | 
			
		||||
        await window.__TAURI__.fs.writeTextFile(result, text);
 | 
			
		||||
        showToast(Locale.Download.Success);
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        showToast(Locale.Download.Failed);
 | 
			
		||||
@@ -293,10 +291,7 @@ 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 visionKeywords = ["vision", "claude-3"];
 | 
			
		||||
 | 
			
		||||
  return visionKeywords.some(keyword => model.includes(keyword));
 | 
			
		||||
  return visionKeywords.some((keyword) => model.includes(keyword));
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										17
									
								
								app/utils/object.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								app/utils/object.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
			
		||||
export function omit<T extends object, U extends (keyof T)[]>(
 | 
			
		||||
  obj: T,
 | 
			
		||||
  ...keys: U
 | 
			
		||||
): Omit<T, U[number]> {
 | 
			
		||||
  const ret: any = { ...obj };
 | 
			
		||||
  keys.forEach((key) => delete ret[key]);
 | 
			
		||||
  return ret;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function pick<T extends object, U extends (keyof T)[]>(
 | 
			
		||||
  obj: T,
 | 
			
		||||
  ...keys: U
 | 
			
		||||
): Pick<T, U[number]> {
 | 
			
		||||
  const ret: any = {};
 | 
			
		||||
  keys.forEach((key) => (ret[key] = obj[key]));
 | 
			
		||||
  return ret;
 | 
			
		||||
}
 | 
			
		||||
@@ -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*",
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user