mirror of
				https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
				synced 2025-11-04 08:13:43 +08:00 
			
		
		
		
	Compare commits
	
		
			4 Commits
		
	
	
		
			b2965e1deb
			...
			078305f5ac
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					078305f5ac | ||
| 
						 | 
					801b62543a | ||
| 
						 | 
					877668b629 | ||
| 
						 | 
					f652f73260 | 
@@ -10,6 +10,8 @@ import { handle as alibabaHandler } from "../../alibaba";
 | 
			
		||||
import { handle as moonshotHandler } from "../../moonshot";
 | 
			
		||||
import { handle as stabilityHandler } from "../../stability";
 | 
			
		||||
import { handle as iflytekHandler } from "../../iflytek";
 | 
			
		||||
import { handle as proxyHandler } from "../../proxy";
 | 
			
		||||
 | 
			
		||||
async function handle(
 | 
			
		||||
  req: NextRequest,
 | 
			
		||||
  { params }: { params: { provider: string; path: string[] } },
 | 
			
		||||
@@ -36,8 +38,10 @@ async function handle(
 | 
			
		||||
      return stabilityHandler(req, { params });
 | 
			
		||||
    case ApiPath.Iflytek:
 | 
			
		||||
      return iflytekHandler(req, { params });
 | 
			
		||||
    default:
 | 
			
		||||
    case ApiPath.OpenAI:
 | 
			
		||||
      return openaiHandler(req, { params });
 | 
			
		||||
    default:
 | 
			
		||||
      return proxyHandler(req, { params });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -38,6 +38,7 @@ export function auth(req: NextRequest, modelProvider: ModelProvider) {
 | 
			
		||||
  console.log("[Auth] hashed access code:", hashedCode);
 | 
			
		||||
  console.log("[User IP] ", getIP(req));
 | 
			
		||||
  console.log("[Time] ", new Date().toLocaleString());
 | 
			
		||||
  console.log("[ModelProvider] ", modelProvider);
 | 
			
		||||
 | 
			
		||||
  if (serverConfig.needCode && !serverConfig.codes.has(hashedCode) && !apiKey) {
 | 
			
		||||
    return {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										75
									
								
								app/api/proxy.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								app/api/proxy.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,75 @@
 | 
			
		||||
import { NextRequest, NextResponse } from "next/server";
 | 
			
		||||
 | 
			
		||||
export async function handle(
 | 
			
		||||
  req: NextRequest,
 | 
			
		||||
  { params }: { params: { path: string[] } },
 | 
			
		||||
) {
 | 
			
		||||
  console.log("[Proxy Route] params ", params);
 | 
			
		||||
 | 
			
		||||
  if (req.method === "OPTIONS") {
 | 
			
		||||
    return NextResponse.json({ body: "OK" }, { status: 200 });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // remove path params from searchParams
 | 
			
		||||
  req.nextUrl.searchParams.delete("path");
 | 
			
		||||
  req.nextUrl.searchParams.delete("provider");
 | 
			
		||||
 | 
			
		||||
  const subpath = params.path.join("/");
 | 
			
		||||
  const fetchUrl = `${req.headers.get(
 | 
			
		||||
    "x-base-url",
 | 
			
		||||
  )}/${subpath}?${req.nextUrl.searchParams.toString()}`;
 | 
			
		||||
  const skipHeaders = ["connection", "host", "origin", "referer", "cookie"];
 | 
			
		||||
  const headers = new Headers(
 | 
			
		||||
    Array.from(req.headers.entries()).filter((item) => {
 | 
			
		||||
      if (
 | 
			
		||||
        item[0].indexOf("x-") > -1 ||
 | 
			
		||||
        item[0].indexOf("sec-") > -1 ||
 | 
			
		||||
        skipHeaders.includes(item[0])
 | 
			
		||||
      ) {
 | 
			
		||||
        return false;
 | 
			
		||||
      }
 | 
			
		||||
      return true;
 | 
			
		||||
    }),
 | 
			
		||||
  );
 | 
			
		||||
  const controller = new AbortController();
 | 
			
		||||
  const fetchOptions: RequestInit = {
 | 
			
		||||
    headers,
 | 
			
		||||
    method: req.method,
 | 
			
		||||
    body: req.body,
 | 
			
		||||
    // to fix #2485: https://stackoverflow.com/questions/55920957/cloudflare-worker-typeerror-one-time-use-body
 | 
			
		||||
    redirect: "manual",
 | 
			
		||||
    // @ts-ignore
 | 
			
		||||
    duplex: "half",
 | 
			
		||||
    signal: controller.signal,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const timeoutId = setTimeout(
 | 
			
		||||
    () => {
 | 
			
		||||
      controller.abort();
 | 
			
		||||
    },
 | 
			
		||||
    10 * 60 * 1000,
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  try {
 | 
			
		||||
    const res = await fetch(fetchUrl, fetchOptions);
 | 
			
		||||
    // 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");
 | 
			
		||||
 | 
			
		||||
    // The latest version of the OpenAI API forced the content-encoding to be "br" in json response
 | 
			
		||||
    // So if the streaming is disabled, we need to remove the content-encoding header
 | 
			
		||||
    // Because Vercel uses gzip to compress the response, if we don't remove the content-encoding header
 | 
			
		||||
    // The browser will try to decode the response with brotli and fail
 | 
			
		||||
    newHeaders.delete("content-encoding");
 | 
			
		||||
 | 
			
		||||
    return new Response(res.body, {
 | 
			
		||||
      status: res.status,
 | 
			
		||||
      statusText: res.statusText,
 | 
			
		||||
      headers: newHeaders,
 | 
			
		||||
    });
 | 
			
		||||
  } finally {
 | 
			
		||||
    clearTimeout(timeoutId);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,6 +1,12 @@
 | 
			
		||||
import { ACCESS_CODE_PREFIX, Anthropic, ApiPath } from "@/app/constant";
 | 
			
		||||
import { ChatOptions, getHeaders, LLMApi, MultimodalContent } from "../api";
 | 
			
		||||
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
 | 
			
		||||
import {
 | 
			
		||||
  useAccessStore,
 | 
			
		||||
  useAppConfig,
 | 
			
		||||
  useChatStore,
 | 
			
		||||
  usePluginStore,
 | 
			
		||||
  ChatMessageTool,
 | 
			
		||||
} from "@/app/store";
 | 
			
		||||
import { getClientConfig } from "@/app/config/client";
 | 
			
		||||
import { DEFAULT_API_HOST } from "@/app/constant";
 | 
			
		||||
import {
 | 
			
		||||
@@ -11,8 +17,9 @@ import {
 | 
			
		||||
import Locale from "../../locales";
 | 
			
		||||
import { prettyObject } from "@/app/utils/format";
 | 
			
		||||
import { getMessageTextContent, isVisionModel } from "@/app/utils";
 | 
			
		||||
import { preProcessImageContent } from "@/app/utils/chat";
 | 
			
		||||
import { preProcessImageContent, stream } from "@/app/utils/chat";
 | 
			
		||||
import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
 | 
			
		||||
import { RequestPayload } from "./openai";
 | 
			
		||||
 | 
			
		||||
export type MultiBlockContent = {
 | 
			
		||||
  type: "image" | "text";
 | 
			
		||||
@@ -191,112 +198,123 @@ export class ClaudeApi implements LLMApi {
 | 
			
		||||
    const controller = new AbortController();
 | 
			
		||||
    options.onController?.(controller);
 | 
			
		||||
 | 
			
		||||
    const payload = {
 | 
			
		||||
      method: "POST",
 | 
			
		||||
      body: JSON.stringify(requestBody),
 | 
			
		||||
      signal: controller.signal,
 | 
			
		||||
      headers: {
 | 
			
		||||
        ...getHeaders(), // get common headers
 | 
			
		||||
        "anthropic-version": accessStore.anthropicApiVersion,
 | 
			
		||||
        // do not send `anthropicApiKey` in browser!!!
 | 
			
		||||
        // 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;
 | 
			
		||||
      let index = -1;
 | 
			
		||||
      const [tools, funcs] = usePluginStore
 | 
			
		||||
        .getState()
 | 
			
		||||
        .getAsTools(
 | 
			
		||||
          useChatStore.getState().currentSession().mask?.plugin as string[],
 | 
			
		||||
        );
 | 
			
		||||
      console.log("getAsTools", tools, funcs);
 | 
			
		||||
      return stream(
 | 
			
		||||
        path,
 | 
			
		||||
        requestBody,
 | 
			
		||||
        {
 | 
			
		||||
          ...getHeaders(),
 | 
			
		||||
          "anthropic-version": accessStore.anthropicApiVersion,
 | 
			
		||||
        },
 | 
			
		||||
        // @ts-ignore
 | 
			
		||||
        tools.map((tool) => ({
 | 
			
		||||
          name: tool?.function?.name,
 | 
			
		||||
          description: tool?.function?.description,
 | 
			
		||||
          input_schema: tool?.function?.parameters,
 | 
			
		||||
        })),
 | 
			
		||||
        funcs,
 | 
			
		||||
        controller,
 | 
			
		||||
        // parseSSE
 | 
			
		||||
        (text: string, runTools: ChatMessageTool[]) => {
 | 
			
		||||
          // console.log("parseSSE", text, runTools);
 | 
			
		||||
          let chunkJson:
 | 
			
		||||
            | undefined
 | 
			
		||||
            | {
 | 
			
		||||
                type: "content_block_delta" | "content_block_stop";
 | 
			
		||||
                content_block?: {
 | 
			
		||||
                  type: "tool_use";
 | 
			
		||||
                  id: string;
 | 
			
		||||
                  name: string;
 | 
			
		||||
                };
 | 
			
		||||
            try {
 | 
			
		||||
              chunkJson = JSON.parse(msg.data);
 | 
			
		||||
            } catch (e) {
 | 
			
		||||
              console.error("[Response] parse error", msg.data);
 | 
			
		||||
            }
 | 
			
		||||
                delta?: {
 | 
			
		||||
                  type: "text_delta" | "input_json_delta";
 | 
			
		||||
                  text?: string;
 | 
			
		||||
                  partial_json?: string;
 | 
			
		||||
                };
 | 
			
		||||
                index: number;
 | 
			
		||||
              };
 | 
			
		||||
          chunkJson = JSON.parse(text);
 | 
			
		||||
 | 
			
		||||
            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);
 | 
			
		||||
      }
 | 
			
		||||
          if (chunkJson?.content_block?.type == "tool_use") {
 | 
			
		||||
            index += 1;
 | 
			
		||||
            const id = chunkJson?.content_block.id;
 | 
			
		||||
            const name = chunkJson?.content_block.name;
 | 
			
		||||
            runTools.push({
 | 
			
		||||
              id,
 | 
			
		||||
              type: "function",
 | 
			
		||||
              function: {
 | 
			
		||||
                name,
 | 
			
		||||
                arguments: "",
 | 
			
		||||
              },
 | 
			
		||||
            });
 | 
			
		||||
          }
 | 
			
		||||
          if (
 | 
			
		||||
            chunkJson?.delta?.type == "input_json_delta" &&
 | 
			
		||||
            chunkJson?.delta?.partial_json
 | 
			
		||||
          ) {
 | 
			
		||||
            // @ts-ignore
 | 
			
		||||
            runTools[index]["function"]["arguments"] +=
 | 
			
		||||
              chunkJson?.delta?.partial_json;
 | 
			
		||||
          }
 | 
			
		||||
          return chunkJson?.delta?.text;
 | 
			
		||||
        },
 | 
			
		||||
        // processToolMessage, include tool_calls message and tool call results
 | 
			
		||||
        (
 | 
			
		||||
          requestPayload: RequestPayload,
 | 
			
		||||
          toolCallMessage: any,
 | 
			
		||||
          toolCallResult: any[],
 | 
			
		||||
        ) => {
 | 
			
		||||
          // @ts-ignore
 | 
			
		||||
          requestPayload?.messages?.splice(
 | 
			
		||||
            // @ts-ignore
 | 
			
		||||
            requestPayload?.messages?.length,
 | 
			
		||||
            0,
 | 
			
		||||
            {
 | 
			
		||||
              role: "assistant",
 | 
			
		||||
              content: toolCallMessage.tool_calls.map(
 | 
			
		||||
                (tool: ChatMessageTool) => ({
 | 
			
		||||
                  type: "tool_use",
 | 
			
		||||
                  id: tool.id,
 | 
			
		||||
                  name: tool?.function?.name,
 | 
			
		||||
                  input: JSON.parse(tool?.function?.arguments as string),
 | 
			
		||||
                }),
 | 
			
		||||
              ),
 | 
			
		||||
            },
 | 
			
		||||
            // @ts-ignore
 | 
			
		||||
            ...toolCallResult.map((result) => ({
 | 
			
		||||
              role: "user",
 | 
			
		||||
              content: [
 | 
			
		||||
                {
 | 
			
		||||
                  type: "tool_result",
 | 
			
		||||
                  tool_use_id: result.tool_call_id,
 | 
			
		||||
                  content: result.content,
 | 
			
		||||
                },
 | 
			
		||||
              ],
 | 
			
		||||
            })),
 | 
			
		||||
          );
 | 
			
		||||
        },
 | 
			
		||||
        options,
 | 
			
		||||
      );
 | 
			
		||||
    } else {
 | 
			
		||||
      const payload = {
 | 
			
		||||
        method: "POST",
 | 
			
		||||
        body: JSON.stringify(requestBody),
 | 
			
		||||
        signal: controller.signal,
 | 
			
		||||
        headers: {
 | 
			
		||||
          ...getHeaders(), // get common headers
 | 
			
		||||
          "anthropic-version": accessStore.anthropicApiVersion,
 | 
			
		||||
          // do not send `anthropicApiKey` in browser!!!
 | 
			
		||||
          // Authorization: getAuthKey(accessStore.anthropicApiKey),
 | 
			
		||||
        },
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      try {
 | 
			
		||||
        controller.signal.onabort = () => options.onFinish("");
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -8,9 +8,15 @@ import {
 | 
			
		||||
  REQUEST_TIMEOUT_MS,
 | 
			
		||||
  ServiceProvider,
 | 
			
		||||
} from "@/app/constant";
 | 
			
		||||
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
 | 
			
		||||
import {
 | 
			
		||||
  useAccessStore,
 | 
			
		||||
  useAppConfig,
 | 
			
		||||
  useChatStore,
 | 
			
		||||
  ChatMessageTool,
 | 
			
		||||
  usePluginStore,
 | 
			
		||||
} from "@/app/store";
 | 
			
		||||
import { collectModelsWithDefaultModel } from "@/app/utils/model";
 | 
			
		||||
import { preProcessImageContent } from "@/app/utils/chat";
 | 
			
		||||
import { preProcessImageContent, stream } from "@/app/utils/chat";
 | 
			
		||||
import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
@@ -116,115 +122,67 @@ export class MoonshotApi implements LLMApi {
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      if (shouldStream) {
 | 
			
		||||
        let responseText = "";
 | 
			
		||||
        let remainText = "";
 | 
			
		||||
        let finished = false;
 | 
			
		||||
 | 
			
		||||
        // animate response to make it looks smooth
 | 
			
		||||
        function animateResponseText() {
 | 
			
		||||
          if (finished || controller.signal.aborted) {
 | 
			
		||||
            responseText += remainText;
 | 
			
		||||
            console.log("[Response Animation] finished");
 | 
			
		||||
            if (responseText?.length === 0) {
 | 
			
		||||
              options.onError?.(new Error("empty response from server"));
 | 
			
		||||
        const [tools, funcs] = usePluginStore
 | 
			
		||||
          .getState()
 | 
			
		||||
          .getAsTools(
 | 
			
		||||
            useChatStore.getState().currentSession().mask?.plugin as string[],
 | 
			
		||||
          );
 | 
			
		||||
        console.log("getAsTools", tools, funcs);
 | 
			
		||||
        return stream(
 | 
			
		||||
          chatPath,
 | 
			
		||||
          requestPayload,
 | 
			
		||||
          getHeaders(),
 | 
			
		||||
          tools as any,
 | 
			
		||||
          funcs,
 | 
			
		||||
          controller,
 | 
			
		||||
          // parseSSE
 | 
			
		||||
          (text: string, runTools: ChatMessageTool[]) => {
 | 
			
		||||
            // console.log("parseSSE", text, runTools);
 | 
			
		||||
            const json = JSON.parse(text);
 | 
			
		||||
            const choices = json.choices as Array<{
 | 
			
		||||
              delta: {
 | 
			
		||||
                content: string;
 | 
			
		||||
                tool_calls: ChatMessageTool[];
 | 
			
		||||
              };
 | 
			
		||||
            }>;
 | 
			
		||||
            const tool_calls = choices[0]?.delta?.tool_calls;
 | 
			
		||||
            if (tool_calls?.length > 0) {
 | 
			
		||||
              const index = tool_calls[0]?.index;
 | 
			
		||||
              const id = tool_calls[0]?.id;
 | 
			
		||||
              const args = tool_calls[0]?.function?.arguments;
 | 
			
		||||
              if (id) {
 | 
			
		||||
                runTools.push({
 | 
			
		||||
                  id,
 | 
			
		||||
                  type: tool_calls[0]?.type,
 | 
			
		||||
                  function: {
 | 
			
		||||
                    name: tool_calls[0]?.function?.name as string,
 | 
			
		||||
                    arguments: args,
 | 
			
		||||
                  },
 | 
			
		||||
                });
 | 
			
		||||
              } else {
 | 
			
		||||
                // @ts-ignore
 | 
			
		||||
                runTools[index]["function"]["arguments"] += args;
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
            return;
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          if (remainText.length > 0) {
 | 
			
		||||
            const fetchCount = Math.max(1, Math.round(remainText.length / 60));
 | 
			
		||||
            const fetchText = remainText.slice(0, fetchCount);
 | 
			
		||||
            responseText += fetchText;
 | 
			
		||||
            remainText = remainText.slice(fetchCount);
 | 
			
		||||
            options.onUpdate?.(responseText, fetchText);
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          requestAnimationFrame(animateResponseText);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // start animaion
 | 
			
		||||
        animateResponseText();
 | 
			
		||||
 | 
			
		||||
        const finish = () => {
 | 
			
		||||
          if (!finished) {
 | 
			
		||||
            finished = true;
 | 
			
		||||
            options.onFinish(responseText + remainText);
 | 
			
		||||
          }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        controller.signal.onabort = finish;
 | 
			
		||||
 | 
			
		||||
        fetchEventSource(chatPath, {
 | 
			
		||||
          ...chatPayload,
 | 
			
		||||
          async onopen(res) {
 | 
			
		||||
            clearTimeout(requestTimeoutId);
 | 
			
		||||
            const contentType = res.headers.get("content-type");
 | 
			
		||||
            console.log(
 | 
			
		||||
              "[OpenAI] request response content type: ",
 | 
			
		||||
              contentType,
 | 
			
		||||
            return choices[0]?.delta?.content;
 | 
			
		||||
          },
 | 
			
		||||
          // processToolMessage, include tool_calls message and tool call results
 | 
			
		||||
          (
 | 
			
		||||
            requestPayload: RequestPayload,
 | 
			
		||||
            toolCallMessage: any,
 | 
			
		||||
            toolCallResult: any[],
 | 
			
		||||
          ) => {
 | 
			
		||||
            // @ts-ignore
 | 
			
		||||
            requestPayload?.messages?.splice(
 | 
			
		||||
              // @ts-ignore
 | 
			
		||||
              requestPayload?.messages?.length,
 | 
			
		||||
              0,
 | 
			
		||||
              toolCallMessage,
 | 
			
		||||
              ...toolCallResult,
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            if (contentType?.startsWith("text/plain")) {
 | 
			
		||||
              responseText = await res.clone().text();
 | 
			
		||||
              return finish();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (
 | 
			
		||||
              !res.ok ||
 | 
			
		||||
              !res.headers
 | 
			
		||||
                .get("content-type")
 | 
			
		||||
                ?.startsWith(EventStreamContentType) ||
 | 
			
		||||
              res.status !== 200
 | 
			
		||||
            ) {
 | 
			
		||||
              const responseTexts = [responseText];
 | 
			
		||||
              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);
 | 
			
		||||
              }
 | 
			
		||||
 | 
			
		||||
              responseText = responseTexts.join("\n\n");
 | 
			
		||||
 | 
			
		||||
              return finish();
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
          onmessage(msg) {
 | 
			
		||||
            if (msg.data === "[DONE]" || finished) {
 | 
			
		||||
              return finish();
 | 
			
		||||
            }
 | 
			
		||||
            const text = msg.data;
 | 
			
		||||
            try {
 | 
			
		||||
              const json = JSON.parse(text);
 | 
			
		||||
              const choices = json.choices as Array<{
 | 
			
		||||
                delta: { content: string };
 | 
			
		||||
              }>;
 | 
			
		||||
              const delta = choices[0]?.delta?.content;
 | 
			
		||||
              const textmoderation = json?.prompt_filter_results;
 | 
			
		||||
 | 
			
		||||
              if (delta) {
 | 
			
		||||
                remainText += delta;
 | 
			
		||||
              }
 | 
			
		||||
            } catch (e) {
 | 
			
		||||
              console.error("[Request] parse error", text, msg);
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
          onclose() {
 | 
			
		||||
            finish();
 | 
			
		||||
          },
 | 
			
		||||
          onerror(e) {
 | 
			
		||||
            options.onError?.(e);
 | 
			
		||||
            throw e;
 | 
			
		||||
          },
 | 
			
		||||
          openWhenHidden: true,
 | 
			
		||||
        });
 | 
			
		||||
          options,
 | 
			
		||||
        );
 | 
			
		||||
      } else {
 | 
			
		||||
        const res = await fetch(chatPath, chatPayload);
 | 
			
		||||
        clearTimeout(requestTimeoutId);
 | 
			
		||||
 
 | 
			
		||||
@@ -246,7 +246,7 @@ export class ChatGPTApi implements LLMApi {
 | 
			
		||||
          .getAsTools(
 | 
			
		||||
            useChatStore.getState().currentSession().mask?.plugin as string[],
 | 
			
		||||
          );
 | 
			
		||||
        console.log("getAsTools", tools, funcs);
 | 
			
		||||
        // console.log("getAsTools", tools, funcs);
 | 
			
		||||
        stream(
 | 
			
		||||
          chatPath,
 | 
			
		||||
          requestPayload,
 | 
			
		||||
 
 | 
			
		||||
@@ -66,6 +66,7 @@ import {
 | 
			
		||||
  getMessageImages,
 | 
			
		||||
  isVisionModel,
 | 
			
		||||
  isDalle3,
 | 
			
		||||
  showPlugins,
 | 
			
		||||
} from "../utils";
 | 
			
		||||
 | 
			
		||||
import { uploadImage as uploadImageRemote } from "@/app/utils/chat";
 | 
			
		||||
@@ -741,12 +742,14 @@ export function ChatActions(props: {
 | 
			
		||||
              value: ArtifactsPlugin.Artifacts as string,
 | 
			
		||||
            },
 | 
			
		||||
          ].concat(
 | 
			
		||||
            pluginStore.getAll().map((item) => ({
 | 
			
		||||
              // @ts-ignore
 | 
			
		||||
              title: `${item?.title}@${item?.version}`,
 | 
			
		||||
              // @ts-ignore
 | 
			
		||||
              value: item?.id,
 | 
			
		||||
            })),
 | 
			
		||||
            showPlugins(currentProviderName, currentModel)
 | 
			
		||||
              ? pluginStore.getAll().map((item) => ({
 | 
			
		||||
                  // @ts-ignore
 | 
			
		||||
                  title: `${item?.title}@${item?.version}`,
 | 
			
		||||
                  // @ts-ignore
 | 
			
		||||
                  value: item?.id,
 | 
			
		||||
                }))
 | 
			
		||||
              : [],
 | 
			
		||||
          )}
 | 
			
		||||
          onClose={() => setShowPluginSelector(false)}
 | 
			
		||||
          onSelection={(s) => {
 | 
			
		||||
 
 | 
			
		||||
@@ -14,10 +14,12 @@ import CloseIcon from "../icons/close.svg";
 | 
			
		||||
import DeleteIcon from "../icons/delete.svg";
 | 
			
		||||
import EyeIcon from "../icons/eye.svg";
 | 
			
		||||
import CopyIcon from "../icons/copy.svg";
 | 
			
		||||
import ConfirmIcon from "../icons/confirm.svg";
 | 
			
		||||
 | 
			
		||||
import { Plugin, usePluginStore, FunctionToolService } from "../store/plugin";
 | 
			
		||||
import {
 | 
			
		||||
  Input,
 | 
			
		||||
  PasswordInput,
 | 
			
		||||
  List,
 | 
			
		||||
  ListItem,
 | 
			
		||||
  Modal,
 | 
			
		||||
@@ -191,55 +193,102 @@ export function PluginPage() {
 | 
			
		||||
            onClose={closePluginModal}
 | 
			
		||||
            actions={[
 | 
			
		||||
              <IconButton
 | 
			
		||||
                icon={<DownloadIcon />}
 | 
			
		||||
                text={Locale.Plugin.EditModal.Download}
 | 
			
		||||
                icon={<ConfirmIcon />}
 | 
			
		||||
                text={Locale.UI.Confirm}
 | 
			
		||||
                key="export"
 | 
			
		||||
                bordered
 | 
			
		||||
                onClick={() =>
 | 
			
		||||
                  downloadAs(
 | 
			
		||||
                    JSON.stringify(editingPlugin),
 | 
			
		||||
                    `${editingPlugin.title}@${editingPlugin.version}.json`,
 | 
			
		||||
                  )
 | 
			
		||||
                }
 | 
			
		||||
                onClick={() => setEditingPluginId("")}
 | 
			
		||||
              />,
 | 
			
		||||
            ]}
 | 
			
		||||
          >
 | 
			
		||||
            <div className={styles["mask-page"]}>
 | 
			
		||||
              <div className={pluginStyles["plugin-title"]}>
 | 
			
		||||
                {Locale.Plugin.EditModal.Content}
 | 
			
		||||
              </div>
 | 
			
		||||
              <div
 | 
			
		||||
                className={`markdown-body ${pluginStyles["plugin-content"]}`}
 | 
			
		||||
                dir="auto"
 | 
			
		||||
            <List>
 | 
			
		||||
              <ListItem title={Locale.Plugin.EditModal.Auth}>
 | 
			
		||||
                <select
 | 
			
		||||
                  value={editingPlugin?.authType}
 | 
			
		||||
                  onChange={(e) => {
 | 
			
		||||
                    pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
 | 
			
		||||
                      plugin.authType = e.target.value;
 | 
			
		||||
                    });
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  <option value="">{Locale.Plugin.Auth.None}</option>
 | 
			
		||||
                  <option value="bearer">{Locale.Plugin.Auth.Bearer}</option>
 | 
			
		||||
                  <option value="basic">{Locale.Plugin.Auth.Basic}</option>
 | 
			
		||||
                  <option value="custom">{Locale.Plugin.Auth.Custom}</option>
 | 
			
		||||
                </select>
 | 
			
		||||
              </ListItem>
 | 
			
		||||
              {editingPlugin.authType == "custom" && (
 | 
			
		||||
                <ListItem title={Locale.Plugin.Auth.CustomHeader}>
 | 
			
		||||
                  <input
 | 
			
		||||
                    type="text"
 | 
			
		||||
                    value={editingPlugin?.authHeader}
 | 
			
		||||
                    onChange={(e) => {
 | 
			
		||||
                      pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
 | 
			
		||||
                        plugin.authHeader = e.target.value;
 | 
			
		||||
                      });
 | 
			
		||||
                    }}
 | 
			
		||||
                  ></input>
 | 
			
		||||
                </ListItem>
 | 
			
		||||
              )}
 | 
			
		||||
              {["bearer", "basic", "custom"].includes(
 | 
			
		||||
                editingPlugin.authType as string,
 | 
			
		||||
              ) && (
 | 
			
		||||
                <ListItem title={Locale.Plugin.Auth.Token}>
 | 
			
		||||
                  <PasswordInput
 | 
			
		||||
                    type="text"
 | 
			
		||||
                    value={editingPlugin?.authToken}
 | 
			
		||||
                    onChange={(e) => {
 | 
			
		||||
                      pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
 | 
			
		||||
                        plugin.authToken = e.currentTarget.value;
 | 
			
		||||
                      });
 | 
			
		||||
                    }}
 | 
			
		||||
                  ></PasswordInput>
 | 
			
		||||
                </ListItem>
 | 
			
		||||
              )}
 | 
			
		||||
              <ListItem
 | 
			
		||||
                title={Locale.Plugin.Auth.Proxy}
 | 
			
		||||
                subTitle={Locale.Plugin.Auth.ProxyDescription}
 | 
			
		||||
              >
 | 
			
		||||
                <pre>
 | 
			
		||||
                  <code
 | 
			
		||||
                    contentEditable={true}
 | 
			
		||||
                    dangerouslySetInnerHTML={{ __html: editingPlugin.content }}
 | 
			
		||||
                    onBlur={onChangePlugin}
 | 
			
		||||
                  ></code>
 | 
			
		||||
                </pre>
 | 
			
		||||
              </div>
 | 
			
		||||
              <div className={pluginStyles["plugin-title"]}>
 | 
			
		||||
                {Locale.Plugin.EditModal.Method}
 | 
			
		||||
              </div>
 | 
			
		||||
              <div className={styles["mask-page-body"]} style={{ padding: 0 }}>
 | 
			
		||||
                {editingPluginTool?.tools.map((tool, index) => (
 | 
			
		||||
                  <div className={styles["mask-item"]} key={index}>
 | 
			
		||||
                    <div className={styles["mask-header"]}>
 | 
			
		||||
                      <div className={styles["mask-title"]}>
 | 
			
		||||
                        <div className={styles["mask-name"]}>
 | 
			
		||||
                          {tool?.function?.name}
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div className={styles["mask-info"] + " one-line"}>
 | 
			
		||||
                          {tool?.function?.description}
 | 
			
		||||
                        </div>
 | 
			
		||||
                      </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                <input
 | 
			
		||||
                  type="checkbox"
 | 
			
		||||
                  checked={editingPlugin?.usingProxy}
 | 
			
		||||
                  style={{ minWidth: 16 }}
 | 
			
		||||
                  onChange={(e) => {
 | 
			
		||||
                    pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
 | 
			
		||||
                      plugin.usingProxy = e.currentTarget.checked;
 | 
			
		||||
                    });
 | 
			
		||||
                  }}
 | 
			
		||||
                ></input>
 | 
			
		||||
              </ListItem>
 | 
			
		||||
            </List>
 | 
			
		||||
            <List>
 | 
			
		||||
              <ListItem
 | 
			
		||||
                title={Locale.Plugin.EditModal.Content}
 | 
			
		||||
                subTitle={
 | 
			
		||||
                  <div
 | 
			
		||||
                    className={`markdown-body ${pluginStyles["plugin-content"]}`}
 | 
			
		||||
                    dir="auto"
 | 
			
		||||
                  >
 | 
			
		||||
                    <pre>
 | 
			
		||||
                      <code
 | 
			
		||||
                        contentEditable={true}
 | 
			
		||||
                        dangerouslySetInnerHTML={{
 | 
			
		||||
                          __html: editingPlugin.content,
 | 
			
		||||
                        }}
 | 
			
		||||
                        onBlur={onChangePlugin}
 | 
			
		||||
                      ></code>
 | 
			
		||||
                    </pre>
 | 
			
		||||
                  </div>
 | 
			
		||||
                ))}
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
                }
 | 
			
		||||
              ></ListItem>
 | 
			
		||||
              {editingPluginTool?.tools.map((tool, index) => (
 | 
			
		||||
                <ListItem
 | 
			
		||||
                  key={index}
 | 
			
		||||
                  title={tool?.function?.name}
 | 
			
		||||
                  subTitle={tool?.function?.description}
 | 
			
		||||
                />
 | 
			
		||||
              ))}
 | 
			
		||||
            </List>
 | 
			
		||||
          </Modal>
 | 
			
		||||
        </div>
 | 
			
		||||
      )}
 | 
			
		||||
 
 | 
			
		||||
@@ -51,7 +51,7 @@ export function Card(props: { children: JSX.Element[]; className?: string }) {
 | 
			
		||||
 | 
			
		||||
export function ListItem(props: {
 | 
			
		||||
  title: string;
 | 
			
		||||
  subTitle?: string;
 | 
			
		||||
  subTitle?: string | JSX.Element;
 | 
			
		||||
  children?: JSX.Element | JSX.Element[];
 | 
			
		||||
  icon?: JSX.Element;
 | 
			
		||||
  className?: string;
 | 
			
		||||
 
 | 
			
		||||
@@ -546,9 +546,20 @@ const cn = {
 | 
			
		||||
      Delete: "删除",
 | 
			
		||||
      DeleteConfirm: "确认删除?",
 | 
			
		||||
    },
 | 
			
		||||
    Auth: {
 | 
			
		||||
      None: "不需要授权",
 | 
			
		||||
      Basic: "Basic",
 | 
			
		||||
      Bearer: "Bearer",
 | 
			
		||||
      Custom: "自定义",
 | 
			
		||||
      CustomHeader: "自定义头",
 | 
			
		||||
      Token: "Token",
 | 
			
		||||
      Proxy: "使用代理",
 | 
			
		||||
      ProxyDescription: "使用代理解决 CORS 错误",
 | 
			
		||||
    },
 | 
			
		||||
    EditModal: {
 | 
			
		||||
      Title: (readonly: boolean) => `编辑插件 ${readonly ? "(只读)" : ""}`,
 | 
			
		||||
      Download: "下载",
 | 
			
		||||
      Auth: "授权方式",
 | 
			
		||||
      Content: "OpenAPI Schema",
 | 
			
		||||
      Method: "方法",
 | 
			
		||||
      Error: "格式错误",
 | 
			
		||||
 
 | 
			
		||||
@@ -554,10 +554,21 @@ const en: LocaleType = {
 | 
			
		||||
      Delete: "Delete",
 | 
			
		||||
      DeleteConfirm: "Confirm to delete?",
 | 
			
		||||
    },
 | 
			
		||||
    Auth: {
 | 
			
		||||
      None: "None",
 | 
			
		||||
      Basic: "Basic",
 | 
			
		||||
      Bearer: "Bearer",
 | 
			
		||||
      Custom: "Custom",
 | 
			
		||||
      CustomHeader: "Custom Header",
 | 
			
		||||
      Token: "Token",
 | 
			
		||||
      Proxy: "Using Proxy",
 | 
			
		||||
      ProxyDescription: "Using proxies to solve CORS error",
 | 
			
		||||
    },
 | 
			
		||||
    EditModal: {
 | 
			
		||||
      Title: (readonly: boolean) =>
 | 
			
		||||
        `Edit Plugin ${readonly ? "(readonly)" : ""}`,
 | 
			
		||||
      Download: "Download",
 | 
			
		||||
      Auth: "Authentication Type",
 | 
			
		||||
      Content: "OpenAPI Schema",
 | 
			
		||||
      Method: "Method",
 | 
			
		||||
      Error: "OpenAPI Schema Error",
 | 
			
		||||
 
 | 
			
		||||
@@ -12,6 +12,10 @@ export type Plugin = {
 | 
			
		||||
  version: string;
 | 
			
		||||
  content: string;
 | 
			
		||||
  builtin: boolean;
 | 
			
		||||
  authType?: string;
 | 
			
		||||
  authHeader?: string;
 | 
			
		||||
  authToken?: string;
 | 
			
		||||
  usingProxy?: boolean;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type FunctionToolItem = {
 | 
			
		||||
@@ -34,10 +38,30 @@ export const FunctionToolService = {
 | 
			
		||||
  tools: {} as Record<string, FunctionToolServiceItem>,
 | 
			
		||||
  add(plugin: Plugin, replace = false) {
 | 
			
		||||
    if (!replace && this.tools[plugin.id]) return this.tools[plugin.id];
 | 
			
		||||
    const headerName = (
 | 
			
		||||
      plugin?.authType == "custom" ? plugin?.authHeader : "Authorization"
 | 
			
		||||
    ) as string;
 | 
			
		||||
    const tokenValue =
 | 
			
		||||
      plugin?.authType == "basic"
 | 
			
		||||
        ? `Basic ${plugin?.authToken}`
 | 
			
		||||
        : plugin?.authType == "bearer"
 | 
			
		||||
        ? ` Bearer ${plugin?.authToken}`
 | 
			
		||||
        : plugin?.authToken;
 | 
			
		||||
    const definition = yaml.load(plugin.content) as any;
 | 
			
		||||
    const serverURL = definition?.servers?.[0]?.url;
 | 
			
		||||
    const baseURL = !!plugin?.usingProxy ? "/api/proxy" : serverURL;
 | 
			
		||||
    const api = new OpenAPIClientAxios({
 | 
			
		||||
      definition: yaml.load(plugin.content) as any,
 | 
			
		||||
      axiosConfigDefaults: {
 | 
			
		||||
        baseURL,
 | 
			
		||||
        headers: {
 | 
			
		||||
          // 'Cache-Control': 'no-cache',
 | 
			
		||||
          // 'Content-Type': 'application/json',  // TODO
 | 
			
		||||
          [headerName]: tokenValue,
 | 
			
		||||
          "X-Base-URL": !!plugin?.usingProxy ? serverURL : undefined,
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
    console.log("add", plugin, api);
 | 
			
		||||
    try {
 | 
			
		||||
      api.initSync();
 | 
			
		||||
    } catch (e) {}
 | 
			
		||||
@@ -79,14 +103,29 @@ export const FunctionToolService = {
 | 
			
		||||
          type: "function",
 | 
			
		||||
          function: {
 | 
			
		||||
            name: o.operationId,
 | 
			
		||||
            description: o.description,
 | 
			
		||||
            description: o.description || o.summary,
 | 
			
		||||
            parameters: parameters,
 | 
			
		||||
          },
 | 
			
		||||
        } as FunctionToolItem;
 | 
			
		||||
      }),
 | 
			
		||||
      funcs: operations.reduce((s, o) => {
 | 
			
		||||
        // @ts-ignore
 | 
			
		||||
        s[o.operationId] = api.client[o.operationId];
 | 
			
		||||
        s[o.operationId] = function (args) {
 | 
			
		||||
          const argument = [];
 | 
			
		||||
          if (o.parameters instanceof Array) {
 | 
			
		||||
            o.parameters.forEach((p) => {
 | 
			
		||||
              // @ts-ignore
 | 
			
		||||
              argument.push(args[p?.name]);
 | 
			
		||||
              // @ts-ignore
 | 
			
		||||
              delete args[p?.name];
 | 
			
		||||
            });
 | 
			
		||||
          } else {
 | 
			
		||||
            argument.push(null);
 | 
			
		||||
          }
 | 
			
		||||
          argument.push(args);
 | 
			
		||||
          // @ts-ignore
 | 
			
		||||
          return api.client[o.operationId].apply(null, argument);
 | 
			
		||||
        };
 | 
			
		||||
        return s;
 | 
			
		||||
      }, {}),
 | 
			
		||||
    });
 | 
			
		||||
@@ -136,6 +175,7 @@ export const usePluginStore = createPersistStore(
 | 
			
		||||
      const updatePlugin = { ...plugin };
 | 
			
		||||
      updater(updatePlugin);
 | 
			
		||||
      plugins[id] = updatePlugin;
 | 
			
		||||
      FunctionToolService.add(updatePlugin, true);
 | 
			
		||||
      set(() => ({ plugins }));
 | 
			
		||||
      get().markUpdate();
 | 
			
		||||
    },
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										15
									
								
								app/utils.ts
									
									
									
									
									
								
							
							
						
						
									
										15
									
								
								app/utils.ts
									
									
									
									
									
								
							@@ -2,6 +2,7 @@ import { useEffect, useState } from "react";
 | 
			
		||||
import { showToast } from "./components/ui-lib";
 | 
			
		||||
import Locale from "./locales";
 | 
			
		||||
import { RequestMessage } from "./client/api";
 | 
			
		||||
import { ServiceProvider } from "./constant";
 | 
			
		||||
 | 
			
		||||
export function trimTopic(topic: string) {
 | 
			
		||||
  // Fix an issue where double quotes still show in the Indonesian language
 | 
			
		||||
@@ -270,3 +271,17 @@ export function isVisionModel(model: string) {
 | 
			
		||||
export function isDalle3(model: string) {
 | 
			
		||||
  return "dall-e-3" === model;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function showPlugins(provider: ServiceProvider, model: string) {
 | 
			
		||||
  if (
 | 
			
		||||
    provider == ServiceProvider.OpenAI ||
 | 
			
		||||
    provider == ServiceProvider.Azure ||
 | 
			
		||||
    provider == ServiceProvider.Moonshot
 | 
			
		||||
  ) {
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
  if (provider == ServiceProvider.Anthropic && !model.includes("claude-2")) {
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
  return false;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -334,7 +334,7 @@ export function stream(
 | 
			
		||||
            remainText += chunk;
 | 
			
		||||
          }
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
          console.error("[Request] parse error", text, msg);
 | 
			
		||||
          console.error("[Request] parse error", text, msg, e);
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      onclose() {
 | 
			
		||||
 
 | 
			
		||||
@@ -86,10 +86,6 @@ if (mode !== "export") {
 | 
			
		||||
        source: "/api/proxy/anthropic/:path*",
 | 
			
		||||
        destination: "https://api.anthropic.com/:path*",
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        source: "/api/proxy/gapier/:path*",
 | 
			
		||||
        destination: "https://a.gapier.com/:path*",
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        source: "/google-fonts/:path*",
 | 
			
		||||
        destination: "https://fonts.googleapis.com/:path*",
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user