feat: dev done
This commit is contained in:
commit
69b079c86e
|
@ -0,0 +1,198 @@
|
||||||
|
import { type OpenAIListModelResponse } from "@/app/client/platforms/openai";
|
||||||
|
import { getServerSideConfig } from "@/app/config/server";
|
||||||
|
import {
|
||||||
|
ANTHROPIC_BASE_URL,
|
||||||
|
Anthropic,
|
||||||
|
ApiPath,
|
||||||
|
DEFAULT_MODELS,
|
||||||
|
ModelProvider,
|
||||||
|
OpenaiPath,
|
||||||
|
} from "@/app/constant";
|
||||||
|
import { prettyObject } from "@/app/utils/format";
|
||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { auth } from "../../auth";
|
||||||
|
import { requestOpenai } from "../../common";
|
||||||
|
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();
|
||||||
|
|
||||||
|
export 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 gpt4 request
|
||||||
|
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("[OpenAI] gpt4 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");
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -57,12 +57,31 @@ export function auth(req: NextRequest, modelProvider: ModelProvider) {
|
||||||
if (!apiKey) {
|
if (!apiKey) {
|
||||||
const serverConfig = getServerSideConfig();
|
const serverConfig = getServerSideConfig();
|
||||||
|
|
||||||
const systemApiKey =
|
// const systemApiKey =
|
||||||
modelProvider === ModelProvider.GeminiPro
|
// modelProvider === ModelProvider.GeminiPro
|
||||||
? serverConfig.googleApiKey
|
// ? serverConfig.googleApiKey
|
||||||
: serverConfig.isAzure
|
// : serverConfig.isAzure
|
||||||
? serverConfig.azureApiKey
|
// ? serverConfig.azureApiKey
|
||||||
: serverConfig.apiKey;
|
// : 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) {
|
if (systemApiKey) {
|
||||||
console.log("[Auth] use system api key");
|
console.log("[Auth] use system api key");
|
||||||
req.headers.set("Authorization", `Bearer ${systemApiKey}`);
|
req.headers.set("Authorization", `Bearer ${systemApiKey}`);
|
||||||
|
|
|
@ -8,6 +8,7 @@ import {
|
||||||
import { ChatMessage, ModelType, useAccessStore, useChatStore } from "../store";
|
import { ChatMessage, ModelType, useAccessStore, useChatStore } from "../store";
|
||||||
import { ChatGPTApi } from "./platforms/openai";
|
import { ChatGPTApi } from "./platforms/openai";
|
||||||
import { GeminiProApi } from "./platforms/google";
|
import { GeminiProApi } from "./platforms/google";
|
||||||
|
import { ClaudeApi } from "./platforms/anthropic";
|
||||||
export const ROLES = ["system", "user", "assistant"] as const;
|
export const ROLES = ["system", "user", "assistant"] as const;
|
||||||
export type MessageRole = (typeof ROLES)[number];
|
export type MessageRole = (typeof ROLES)[number];
|
||||||
|
|
||||||
|
@ -94,12 +95,17 @@ export class ClientApi {
|
||||||
public llm: LLMApi;
|
public llm: LLMApi;
|
||||||
|
|
||||||
constructor(provider: ModelProvider = ModelProvider.GPT) {
|
constructor(provider: ModelProvider = ModelProvider.GPT) {
|
||||||
if (provider === ModelProvider.GeminiPro) {
|
switch (provider) {
|
||||||
|
case ModelProvider.GeminiPro:
|
||||||
this.llm = new GeminiProApi();
|
this.llm = new GeminiProApi();
|
||||||
return;
|
break;
|
||||||
}
|
case ModelProvider.Claude:
|
||||||
|
this.llm = new ClaudeApi();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
this.llm = new ChatGPTApi();
|
this.llm = new ChatGPTApi();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
config() {}
|
config() {}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,515 @@
|
||||||
|
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 { MessageRole, 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;
|
||||||
|
|
||||||
|
export class ClaudeApi implements LLMApi {
|
||||||
|
extractMessage(res: any) {
|
||||||
|
console.log("[Response] claude response: ", res);
|
||||||
|
|
||||||
|
return res.completion;
|
||||||
|
}
|
||||||
|
async chatComplete(options: ChatOptions): Promise<void> {
|
||||||
|
const ClaudeMapper: Record<RequestMessage["role"], string> = {
|
||||||
|
assistant: "Assistant",
|
||||||
|
user: "Human",
|
||||||
|
system: "Human",
|
||||||
|
};
|
||||||
|
|
||||||
|
const accessStore = useAccessStore.getState();
|
||||||
|
|
||||||
|
const shouldStream = !!options.config.stream;
|
||||||
|
|
||||||
|
const prompt = options.messages
|
||||||
|
.map((v) => ({
|
||||||
|
role: ClaudeMapper[v.role] ?? "Human",
|
||||||
|
content: v.content,
|
||||||
|
}))
|
||||||
|
.map((v) => `\n\n${v.role}: ${v.content}`)
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
const modelConfig = {
|
||||||
|
...useAppConfig.getState().modelConfig,
|
||||||
|
...useChatStore.getState().currentSession().mask.modelConfig,
|
||||||
|
...{
|
||||||
|
model: options.config.model,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const requestBody: ChatRequest = {
|
||||||
|
prompt,
|
||||||
|
stream: shouldStream,
|
||||||
|
|
||||||
|
model: modelConfig.model,
|
||||||
|
max_tokens_to_sample: modelConfig.max_tokens,
|
||||||
|
temperature: modelConfig.temperature,
|
||||||
|
top_p: modelConfig.top_p,
|
||||||
|
// top_k: modelConfig.top_k,
|
||||||
|
top_k: 5,
|
||||||
|
};
|
||||||
|
|
||||||
|
const path = this.path(Anthropic.ChatPath1);
|
||||||
|
|
||||||
|
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),
|
||||||
|
},
|
||||||
|
// mode: "no-cors" as RequestMode,
|
||||||
|
};
|
||||||
|
|
||||||
|
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) {
|
||||||
|
if (msg.data === "[DONE]" || context.finished) {
|
||||||
|
return finish();
|
||||||
|
}
|
||||||
|
const chunk = msg.data;
|
||||||
|
try {
|
||||||
|
const chunkJson = JSON.parse(chunk) as ChatStreamResponse;
|
||||||
|
const delta = chunkJson.completion;
|
||||||
|
if (delta) {
|
||||||
|
context.text += delta;
|
||||||
|
options.onUpdate?.(context.text, delta);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[Request] parse error", chunk, msg);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onclose() {
|
||||||
|
finish();
|
||||||
|
},
|
||||||
|
onerror(e) {
|
||||||
|
options.onError?.(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 chat(options: ChatOptions): Promise<void> {
|
||||||
|
const visionModel = isVisionModel(options.config.model);
|
||||||
|
|
||||||
|
const accessStore = useAccessStore.getState();
|
||||||
|
|
||||||
|
const shouldStream = !!options.config.stream;
|
||||||
|
|
||||||
|
const prompt = options.messages.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.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 modelConfig = {
|
||||||
|
...useAppConfig.getState().modelConfig,
|
||||||
|
...useChatStore.getState().currentSession().mask.modelConfig,
|
||||||
|
...{
|
||||||
|
model: options.config.model,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
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),
|
||||||
|
},
|
||||||
|
// mode: (!clientConfig?.isApp && pathObj.hostname === location.hostname ? "same-origin" : "cors") as RequestMode,
|
||||||
|
// mode: "no-cors" as RequestMode,
|
||||||
|
credentials: "include" as RequestCredentials,
|
||||||
|
};
|
||||||
|
|
||||||
|
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) {
|
||||||
|
if (msg.data === "[DONE]" || context.finished) {
|
||||||
|
return finish();
|
||||||
|
}
|
||||||
|
const chunk = msg.data;
|
||||||
|
try {
|
||||||
|
const chunkJson = JSON.parse(chunk) as ChatStreamResponse;
|
||||||
|
const delta = chunkJson.completion;
|
||||||
|
if (delta) {
|
||||||
|
context.text += delta;
|
||||||
|
options.onUpdate?.(context.text, delta);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[Request] parse error", chunk, msg);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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",
|
||||||
|
available: true,
|
||||||
|
provider,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "claude-2",
|
||||||
|
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" + ApiPath.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;
|
||||||
|
}
|
|
@ -315,6 +315,8 @@ export function PreviewActions(props: {
|
||||||
var api: ClientApi;
|
var api: ClientApi;
|
||||||
if (config.modelConfig.model.startsWith("gemini")) {
|
if (config.modelConfig.model.startsWith("gemini")) {
|
||||||
api = new ClientApi(ModelProvider.GeminiPro);
|
api = new ClientApi(ModelProvider.GeminiPro);
|
||||||
|
} else if (config.modelConfig.model.startsWith("claude")) {
|
||||||
|
api = new ClientApi(ModelProvider.Claude);
|
||||||
} else {
|
} else {
|
||||||
api = new ClientApi(ModelProvider.GPT);
|
api = new ClientApi(ModelProvider.GPT);
|
||||||
}
|
}
|
||||||
|
|
|
@ -173,6 +173,8 @@ export function useLoadData() {
|
||||||
var api: ClientApi;
|
var api: ClientApi;
|
||||||
if (config.modelConfig.model.startsWith("gemini")) {
|
if (config.modelConfig.model.startsWith("gemini")) {
|
||||||
api = new ClientApi(ModelProvider.GeminiPro);
|
api = new ClientApi(ModelProvider.GeminiPro);
|
||||||
|
} else if (config.modelConfig.model.startsWith("claude")) {
|
||||||
|
api = new ClientApi(ModelProvider.Claude);
|
||||||
} else {
|
} else {
|
||||||
api = new ClientApi(ModelProvider.GPT);
|
api = new ClientApi(ModelProvider.GPT);
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,6 +51,7 @@ import Locale, {
|
||||||
import { copyToClipboard } from "../utils";
|
import { copyToClipboard } from "../utils";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import {
|
import {
|
||||||
|
Anthropic,
|
||||||
Azure,
|
Azure,
|
||||||
Google,
|
Google,
|
||||||
OPENAI_BASE_URL,
|
OPENAI_BASE_URL,
|
||||||
|
@ -963,7 +964,7 @@ export function Settings() {
|
||||||
</Select>
|
</Select>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
{accessStore.provider === "OpenAI" ? (
|
{accessStore.provider === ServiceProvider.OpenAI && (
|
||||||
<>
|
<>
|
||||||
<ListItem
|
<ListItem
|
||||||
title={Locale.Settings.Access.OpenAI.Endpoint.Title}
|
title={Locale.Settings.Access.OpenAI.Endpoint.Title}
|
||||||
|
@ -1002,7 +1003,8 @@ export function Settings() {
|
||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
</>
|
</>
|
||||||
) : accessStore.provider === "Azure" ? (
|
)}
|
||||||
|
{accessStore.provider === ServiceProvider.Azure && (
|
||||||
<>
|
<>
|
||||||
<ListItem
|
<ListItem
|
||||||
title={Locale.Settings.Access.Azure.Endpoint.Title}
|
title={Locale.Settings.Access.Azure.Endpoint.Title}
|
||||||
|
@ -1061,7 +1063,8 @@ export function Settings() {
|
||||||
></input>
|
></input>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
</>
|
</>
|
||||||
) : accessStore.provider === "Google" ? (
|
)}
|
||||||
|
{accessStore.provider === ServiceProvider.Google && (
|
||||||
<>
|
<>
|
||||||
<ListItem
|
<ListItem
|
||||||
title={Locale.Settings.Access.Google.Endpoint.Title}
|
title={Locale.Settings.Access.Google.Endpoint.Title}
|
||||||
|
@ -1120,7 +1123,70 @@ export function Settings() {
|
||||||
></input>
|
></input>
|
||||||
</ListItem>
|
</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 isAzure = !!process.env.AZURE_URL;
|
||||||
const isGoogle = !!process.env.GOOGLE_API_KEY;
|
const isGoogle = !!process.env.GOOGLE_API_KEY;
|
||||||
|
const isAnthropic = !!process.env.ANTHROPIC_API_KEY;
|
||||||
|
|
||||||
const apiKeyEnvVar = process.env.OPENAI_API_KEY ?? "";
|
const apiKeyEnvVar = process.env.OPENAI_API_KEY ?? "";
|
||||||
const apiKeys = apiKeyEnvVar.split(",").map((v) => v.trim());
|
const apiKeys = apiKeyEnvVar.split(",").map((v) => v.trim());
|
||||||
|
@ -92,6 +93,11 @@ export const getServerSideConfig = () => {
|
||||||
googleApiKey: process.env.GOOGLE_API_KEY,
|
googleApiKey: process.env.GOOGLE_API_KEY,
|
||||||
googleUrl: process.env.GOOGLE_URL,
|
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,
|
gtmId: process.env.GTM_ID,
|
||||||
|
|
||||||
needCode: ACCESS_CODES.size > 0,
|
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 DEFAULT_API_HOST = "https://api.nextchat.dev";
|
||||||
export const OPENAI_BASE_URL = "https://api.openai.com";
|
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/";
|
export const GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/";
|
||||||
|
|
||||||
|
@ -25,6 +26,7 @@ export enum Path {
|
||||||
export enum ApiPath {
|
export enum ApiPath {
|
||||||
Cors = "",
|
Cors = "",
|
||||||
OpenAI = "/api/openai",
|
OpenAI = "/api/openai",
|
||||||
|
Anthropic = "/api/anthropic",
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum SlotID {
|
export enum SlotID {
|
||||||
|
@ -67,13 +69,22 @@ export enum ServiceProvider {
|
||||||
OpenAI = "OpenAI",
|
OpenAI = "OpenAI",
|
||||||
Azure = "Azure",
|
Azure = "Azure",
|
||||||
Google = "Google",
|
Google = "Google",
|
||||||
|
Anthropic = "Anthropic",
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ModelProvider {
|
export enum ModelProvider {
|
||||||
GPT = "GPT",
|
GPT = "GPT",
|
||||||
GeminiPro = "GeminiPro",
|
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 = {
|
export const OpenaiPath = {
|
||||||
ChatPath: "v1/chat/completions",
|
ChatPath: "v1/chat/completions",
|
||||||
UsagePath: "dashboard/billing/usage",
|
UsagePath: "dashboard/billing/usage",
|
||||||
|
@ -289,6 +300,60 @@ export const DEFAULT_MODELS = [
|
||||||
providerType: "google",
|
providerType: "google",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "claude-instant-1",
|
||||||
|
available: true,
|
||||||
|
provider: {
|
||||||
|
id: "anthropic",
|
||||||
|
providerName: "Anthropic",
|
||||||
|
providerType: "anthropic",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "claude-2",
|
||||||
|
available: true,
|
||||||
|
provider: {
|
||||||
|
id: "anthropic",
|
||||||
|
providerName: "Anthropic",
|
||||||
|
providerType: "anthropic",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "claude-3",
|
||||||
|
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;
|
] as const;
|
||||||
|
|
||||||
export const CHAT_PAGE_SIZE = 15;
|
export const CHAT_PAGE_SIZE = 15;
|
||||||
|
|
|
@ -313,6 +313,23 @@ const cn = {
|
||||||
SubTitle: "选择指定的部分版本",
|
SubTitle: "选择指定的部分版本",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Anthropic: {
|
||||||
|
ApiKey: {
|
||||||
|
Title: "接口密钥",
|
||||||
|
SubTitle: "使用自定义 Anthropic Key 绕过密码访问限制",
|
||||||
|
Placeholder: "Anthropic API Key",
|
||||||
|
},
|
||||||
|
|
||||||
|
Endpoint: {
|
||||||
|
Title: "接口地址",
|
||||||
|
SubTitle: "样例:",
|
||||||
|
},
|
||||||
|
|
||||||
|
ApiVerion: {
|
||||||
|
Title: "接口版本 (claude api version)",
|
||||||
|
SubTitle: "选择一个特定的 API 版本输入",
|
||||||
|
},
|
||||||
|
},
|
||||||
Google: {
|
Google: {
|
||||||
ApiKey: {
|
ApiKey: {
|
||||||
Title: "API 密钥",
|
Title: "API 密钥",
|
||||||
|
|
|
@ -316,6 +316,24 @@ const en: LocaleType = {
|
||||||
SubTitle: "Check your api version from azure console",
|
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: {
|
CustomModel: {
|
||||||
Title: "Custom Models",
|
Title: "Custom Models",
|
||||||
SubTitle: "Custom model options, seperated by comma",
|
SubTitle: "Custom model options, seperated by comma",
|
||||||
|
|
|
@ -316,6 +316,23 @@ const pt: PartialLocaleType = {
|
||||||
SubTitle: "Verifique sua versão API do console Azure",
|
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: {
|
CustomModel: {
|
||||||
Title: "Modelos Personalizados",
|
Title: "Modelos Personalizados",
|
||||||
SubTitle: "Opções de modelo personalizado, separados por vírgula",
|
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",
|
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: {
|
CustomModel: {
|
||||||
Title: "Vlastné modely",
|
Title: "Vlastné modely",
|
||||||
SubTitle: "Možnosti vlastného modelu, oddelené čiarkou",
|
SubTitle: "Možnosti vlastného modelu, oddelené čiarkou",
|
||||||
|
|
|
@ -314,6 +314,23 @@ const tw = {
|
||||||
SubTitle: "選擇指定的部分版本",
|
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: {
|
Google: {
|
||||||
ApiKey: {
|
ApiKey: {
|
||||||
Title: "API 密鑰",
|
Title: "API 密鑰",
|
||||||
|
|
|
@ -36,6 +36,11 @@ const DEFAULT_ACCESS_STATE = {
|
||||||
googleApiKey: "",
|
googleApiKey: "",
|
||||||
googleApiVersion: "v1",
|
googleApiVersion: "v1",
|
||||||
|
|
||||||
|
// anthropic
|
||||||
|
anthropicApiKey: "",
|
||||||
|
anthropicApiVersion: "2023-06-01",
|
||||||
|
anthropicUrl: "",
|
||||||
|
|
||||||
// server config
|
// server config
|
||||||
needCode: true,
|
needCode: true,
|
||||||
hideUserApiKey: false,
|
hideUserApiKey: false,
|
||||||
|
@ -67,6 +72,10 @@ export const useAccessStore = createPersistStore(
|
||||||
return ensure(get(), ["googleApiKey"]);
|
return ensure(get(), ["googleApiKey"]);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
isValidAnthropic() {
|
||||||
|
return ensure(get(), ["anthropicApiKey"]);
|
||||||
|
},
|
||||||
|
|
||||||
isAuthorized() {
|
isAuthorized() {
|
||||||
this.fetch();
|
this.fetch();
|
||||||
|
|
||||||
|
@ -75,6 +84,7 @@ export const useAccessStore = createPersistStore(
|
||||||
this.isValidOpenAI() ||
|
this.isValidOpenAI() ||
|
||||||
this.isValidAzure() ||
|
this.isValidAzure() ||
|
||||||
this.isValidGoogle() ||
|
this.isValidGoogle() ||
|
||||||
|
this.isValidAnthropic() ||
|
||||||
!this.enabledAccessControl() ||
|
!this.enabledAccessControl() ||
|
||||||
(this.enabledAccessControl() && ensure(get(), ["accessCode"]))
|
(this.enabledAccessControl() && ensure(get(), ["accessCode"]))
|
||||||
);
|
);
|
||||||
|
|
|
@ -287,7 +287,7 @@ export const useChatStore = createPersistStore(
|
||||||
session.lastUpdate = Date.now();
|
session.lastUpdate = Date.now();
|
||||||
});
|
});
|
||||||
get().updateStat(message);
|
get().updateStat(message);
|
||||||
get().summarizeSession();
|
// get().summarizeSession();
|
||||||
},
|
},
|
||||||
|
|
||||||
async onUserInput(content: string, attachImages?: string[]) {
|
async onUserInput(content: string, attachImages?: string[]) {
|
||||||
|
@ -348,6 +348,8 @@ export const useChatStore = createPersistStore(
|
||||||
var api: ClientApi;
|
var api: ClientApi;
|
||||||
if (modelConfig.model.startsWith("gemini")) {
|
if (modelConfig.model.startsWith("gemini")) {
|
||||||
api = new ClientApi(ModelProvider.GeminiPro);
|
api = new ClientApi(ModelProvider.GeminiPro);
|
||||||
|
} else if (modelConfig.model.startsWith("claude")) {
|
||||||
|
api = new ClientApi(ModelProvider.Claude);
|
||||||
} else {
|
} else {
|
||||||
api = new ClientApi(ModelProvider.GPT);
|
api = new ClientApi(ModelProvider.GPT);
|
||||||
}
|
}
|
||||||
|
@ -533,6 +535,8 @@ export const useChatStore = createPersistStore(
|
||||||
var api: ClientApi;
|
var api: ClientApi;
|
||||||
if (modelConfig.model.startsWith("gemini")) {
|
if (modelConfig.model.startsWith("gemini")) {
|
||||||
api = new ClientApi(ModelProvider.GeminiPro);
|
api = new ClientApi(ModelProvider.GeminiPro);
|
||||||
|
} else if (modelConfig.model.startsWith("claude")) {
|
||||||
|
api = new ClientApi(ModelProvider.Claude);
|
||||||
} else {
|
} else {
|
||||||
api = new ClientApi(ModelProvider.GPT);
|
api = new ClientApi(ModelProvider.GPT);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +1,9 @@
|
||||||
export type Updater<T> = (updater: (value: T) => void) => void;
|
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;
|
||||||
|
}
|
||||||
|
|
19
app/utils.ts
19
app/utils.ts
|
@ -2,16 +2,17 @@ import { useEffect, useState } from "react";
|
||||||
import { showToast } from "./components/ui-lib";
|
import { showToast } from "./components/ui-lib";
|
||||||
import Locale from "./locales";
|
import Locale from "./locales";
|
||||||
import { RequestMessage } from "./client/api";
|
import { RequestMessage } from "./client/api";
|
||||||
import { DEFAULT_MODELS } from "./constant";
|
|
||||||
|
|
||||||
export function trimTopic(topic: string) {
|
export function trimTopic(topic: string) {
|
||||||
// Fix an issue where double quotes still show in the Indonesian language
|
// Fix an issue where double quotes still show in the Indonesian language
|
||||||
// This will remove the specified punctuation from the end of the string
|
// 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.
|
// and also trim quotes from both the start and end if they exist.
|
||||||
return topic
|
return (
|
||||||
|
topic
|
||||||
// fix for gemini
|
// fix for gemini
|
||||||
.replace(/^["“”*]+|["“”*]+$/g, "")
|
.replace(/^["“”*]+|["“”*]+$/g, "")
|
||||||
.replace(/[,。!?”“"、,.!?*]*$/, "");
|
.replace(/[,。!?”“"、,.!?*]*$/, "")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function copyToClipboard(text: string) {
|
export async function copyToClipboard(text: string) {
|
||||||
|
@ -57,10 +58,7 @@ export async function downloadAs(text: string, filename: string) {
|
||||||
|
|
||||||
if (result !== null) {
|
if (result !== null) {
|
||||||
try {
|
try {
|
||||||
await window.__TAURI__.fs.writeTextFile(
|
await window.__TAURI__.fs.writeTextFile(result, text);
|
||||||
result,
|
|
||||||
text
|
|
||||||
);
|
|
||||||
showToast(Locale.Download.Success);
|
showToast(Locale.Download.Success);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showToast(Locale.Download.Failed);
|
showToast(Locale.Download.Failed);
|
||||||
|
@ -293,10 +291,7 @@ export function getMessageImages(message: RequestMessage): string[] {
|
||||||
|
|
||||||
export function isVisionModel(model: 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)
|
// 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 = [
|
const visionKeywords = ["vision", "claude-3"];
|
||||||
"vision",
|
|
||||||
"claude-3",
|
|
||||||
];
|
|
||||||
|
|
||||||
return visionKeywords.some(keyword => model.includes(keyword));
|
return visionKeywords.some((keyword) => model.includes(keyword));
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
Loading…
Reference in New Issue