mirror of
https://github.com/Yidadaa/ChatGPT-Next-Web.git
synced 2025-09-04 22:46:55 +08:00
feat: 1) Present 'maxtokens' as properties tied to a single model. 2) Remove the original author's implementation of the send verification logic and replace it with a user input validator. Pre-verification 3) Provides the ability to pull the 'User Visible modellist' provided by 'provider' 4) Provider-related parameters are passed in the constructor of 'providerClient'. Not passed in the 'chat' method
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { SettingItem } from "../../core/types";
|
||||
import { SettingItem } from "../../common";
|
||||
import Locale from "./locale";
|
||||
|
||||
export type SettingKeys =
|
||||
@@ -13,6 +13,12 @@ export const AnthropicMetas = {
|
||||
Vision: "2023-06-01",
|
||||
};
|
||||
|
||||
export const ClaudeMapper = {
|
||||
assistant: "assistant",
|
||||
user: "user",
|
||||
system: "user",
|
||||
} as const;
|
||||
|
||||
export const modelConfigs = [
|
||||
{
|
||||
name: "claude-instant-1.2",
|
||||
@@ -58,6 +64,8 @@ export const modelConfigs = [
|
||||
},
|
||||
];
|
||||
|
||||
const defaultEndpoint = "/api/anthropic";
|
||||
|
||||
export const settingItems: SettingItem<SettingKeys>[] = [
|
||||
{
|
||||
name: "anthropicUrl",
|
||||
@@ -65,7 +73,22 @@ export const settingItems: SettingItem<SettingKeys>[] = [
|
||||
description: Locale.Endpoint.SubTitle + AnthropicMetas.ExampleEndpoint,
|
||||
placeholder: AnthropicMetas.ExampleEndpoint,
|
||||
type: "input",
|
||||
validators: ["required"],
|
||||
defaultValue: defaultEndpoint,
|
||||
validators: [
|
||||
"required",
|
||||
async (v: any) => {
|
||||
if (typeof v === "string" && !v.startsWith(defaultEndpoint)) {
|
||||
try {
|
||||
new URL(v);
|
||||
} catch (e) {
|
||||
return Locale.Endpoint.Error.IllegalURL;
|
||||
}
|
||||
}
|
||||
if (typeof v === "string" && v.endsWith("/")) {
|
||||
return Locale.Endpoint.Error.EndWithBackslash;
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "anthropicApiKey",
|
||||
@@ -74,7 +97,7 @@ export const settingItems: SettingItem<SettingKeys>[] = [
|
||||
placeholder: Locale.ApiKey.Placeholder,
|
||||
type: "input",
|
||||
inputType: "password",
|
||||
validators: ["required"],
|
||||
// validators: ["required"],
|
||||
},
|
||||
{
|
||||
name: "anthropicApiVersion",
|
||||
@@ -82,6 +105,6 @@ export const settingItems: SettingItem<SettingKeys>[] = [
|
||||
description: Locale.ApiVerion.SubTitle,
|
||||
placeholder: AnthropicMetas.Vision,
|
||||
type: "input",
|
||||
validators: ["required"],
|
||||
// validators: ["required"],
|
||||
},
|
||||
];
|
||||
|
@@ -1,29 +1,27 @@
|
||||
import { getMessageTextContent } from "@/app/utils";
|
||||
import {
|
||||
AnthropicMetas,
|
||||
ClaudeMapper,
|
||||
SettingKeys,
|
||||
modelConfigs,
|
||||
settingItems,
|
||||
} from "./config";
|
||||
import {
|
||||
ChatHandlers,
|
||||
InternalChatRequestPayload,
|
||||
IProviderTemplate,
|
||||
} from "../../core/types";
|
||||
getMessageTextContent,
|
||||
RequestMessage,
|
||||
} from "../../common";
|
||||
import {
|
||||
EventStreamContentType,
|
||||
fetchEventSource,
|
||||
} from "@fortaine/fetch-event-source";
|
||||
import Locale from "@/app/locales";
|
||||
import { prettyObject } from "@/app/utils/format";
|
||||
import { getAuthKey, trimEnd, prettyObject } from "./utils";
|
||||
import { cloneDeep } from "lodash-es";
|
||||
|
||||
export type AnthropicProviderSettingKeys = SettingKeys;
|
||||
|
||||
const ClaudeMapper = {
|
||||
assistant: "assistant",
|
||||
user: "user",
|
||||
system: "user",
|
||||
} as const;
|
||||
|
||||
export type MultiBlockContent = {
|
||||
type: "image" | "text";
|
||||
source?: {
|
||||
@@ -75,64 +73,25 @@ export default class AnthropicProvider
|
||||
settingItems,
|
||||
};
|
||||
|
||||
models = modelConfigs.map((c) => ({ ...c, providerTemplateName: this.name }));
|
||||
defaultModels = modelConfigs;
|
||||
|
||||
readonly REQUEST_TIMEOUT_MS = 60000;
|
||||
|
||||
private path(payload: InternalChatRequestPayload<SettingKeys>) {
|
||||
const {
|
||||
providerConfig: { anthropicUrl },
|
||||
context: { isApp },
|
||||
} = payload;
|
||||
|
||||
let baseUrl: string = anthropicUrl;
|
||||
|
||||
// if endpoint is empty, use default endpoint
|
||||
if (baseUrl.trim().length === 0) {
|
||||
baseUrl = "/api/anthropic";
|
||||
}
|
||||
|
||||
if (!baseUrl.startsWith("http") && !baseUrl.startsWith("/api")) {
|
||||
baseUrl = "https://" + baseUrl;
|
||||
}
|
||||
|
||||
baseUrl = trimEnd(baseUrl, "/");
|
||||
|
||||
return `${baseUrl}/${AnthropicMetas.ChatPath}`;
|
||||
return `${trimEnd(anthropicUrl!)}/${AnthropicMetas.ChatPath}`;
|
||||
}
|
||||
|
||||
private formatChatPayload(payload: InternalChatRequestPayload<SettingKeys>) {
|
||||
const {
|
||||
messages,
|
||||
isVisionModel,
|
||||
model,
|
||||
stream,
|
||||
modelConfig,
|
||||
providerConfig,
|
||||
} = payload;
|
||||
const { anthropicApiKey, anthropicApiVersion, anthropicUrl } =
|
||||
providerConfig;
|
||||
const { temperature, top_p, max_tokens } = modelConfig;
|
||||
private formatMessage(
|
||||
messages: RequestMessage[],
|
||||
payload: InternalChatRequestPayload<SettingKeys>,
|
||||
) {
|
||||
const { isVisionModel } = payload;
|
||||
|
||||
const keys = ["system", "user"];
|
||||
|
||||
// roles must alternate between "user" and "assistant" in claude, so add a fake assistant message between two user messages
|
||||
for (let i = 0; i < messages.length - 1; i++) {
|
||||
const message = messages[i];
|
||||
const nextMessage = messages[i + 1];
|
||||
|
||||
if (keys.includes(message.role) && keys.includes(nextMessage.role)) {
|
||||
messages[i] = [
|
||||
message,
|
||||
{
|
||||
role: "assistant",
|
||||
content: ";",
|
||||
},
|
||||
] as any;
|
||||
}
|
||||
}
|
||||
|
||||
const prompt = messages
|
||||
return messages
|
||||
.flat()
|
||||
.filter((v) => {
|
||||
if (!v.content) return false;
|
||||
@@ -180,6 +139,40 @@ export default class AnthropicProvider
|
||||
}),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private formatChatPayload(payload: InternalChatRequestPayload<SettingKeys>) {
|
||||
const {
|
||||
messages: outsideMessages,
|
||||
model,
|
||||
stream,
|
||||
modelConfig,
|
||||
providerConfig,
|
||||
} = payload;
|
||||
const { anthropicApiKey, anthropicApiVersion } = providerConfig;
|
||||
const { temperature, top_p, max_tokens } = modelConfig;
|
||||
|
||||
const keys = ["system", "user"];
|
||||
|
||||
// roles must alternate between "user" and "assistant" in claude, so add a fake assistant message between two user messages
|
||||
const messages = cloneDeep(outsideMessages);
|
||||
|
||||
for (let i = 0; i < messages.length - 1; i++) {
|
||||
const message = messages[i];
|
||||
const nextMessage = messages[i + 1];
|
||||
|
||||
if (keys.includes(message.role) && keys.includes(nextMessage.role)) {
|
||||
messages[i] = [
|
||||
message,
|
||||
{
|
||||
role: "assistant",
|
||||
content: ";",
|
||||
},
|
||||
] as any;
|
||||
}
|
||||
}
|
||||
|
||||
const prompt = this.formatMessage(messages, payload);
|
||||
|
||||
const requestBody: AnthropicChatRequest = {
|
||||
messages: prompt,
|
||||
@@ -196,7 +189,7 @@ export default class AnthropicProvider
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
"x-api-key": anthropicApiKey ?? "",
|
||||
"anthropic-version": anthropicApiVersion,
|
||||
"anthropic-version": anthropicApiVersion ?? "",
|
||||
Authorization: getAuthKey(anthropicApiKey),
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
@@ -204,6 +197,7 @@ export default class AnthropicProvider
|
||||
url: this.path(payload),
|
||||
};
|
||||
}
|
||||
|
||||
private readWholeMessageResponseBody(res: any) {
|
||||
return {
|
||||
message: res?.content?.[0]?.text ?? "",
|
||||
@@ -259,50 +253,12 @@ export default class AnthropicProvider
|
||||
|
||||
streamChat(
|
||||
payload: InternalChatRequestPayload<SettingKeys>,
|
||||
onProgress: (message: string, chunk: string) => void,
|
||||
onFinish: (message: string) => void,
|
||||
onError: (err: Error) => void,
|
||||
handlers: ChatHandlers,
|
||||
) {
|
||||
const requestPayload = this.formatChatPayload(payload);
|
||||
|
||||
let responseText = "";
|
||||
let remainText = "";
|
||||
let finished = false;
|
||||
|
||||
const timer = this.getTimer();
|
||||
|
||||
// animate response to make it looks smooth
|
||||
const animateResponseText = () => {
|
||||
if (finished || timer.signal.aborted) {
|
||||
responseText += remainText;
|
||||
console.log("[Response Animation] finished");
|
||||
if (responseText?.length === 0) {
|
||||
onError(new Error("empty response from server"));
|
||||
}
|
||||
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);
|
||||
onProgress(responseText, fetchText);
|
||||
}
|
||||
|
||||
requestAnimationFrame(animateResponseText);
|
||||
};
|
||||
|
||||
// start animaion
|
||||
animateResponseText();
|
||||
|
||||
const finish = () => {
|
||||
if (!finished) {
|
||||
finished = true;
|
||||
onFinish(responseText + remainText);
|
||||
}
|
||||
};
|
||||
|
||||
fetchEventSource(requestPayload.url, {
|
||||
...requestPayload,
|
||||
async onopen(res) {
|
||||
@@ -311,8 +267,8 @@ export default class AnthropicProvider
|
||||
console.log("[OpenAI] request response content type: ", contentType);
|
||||
|
||||
if (contentType?.startsWith("text/plain")) {
|
||||
responseText = await res.clone().text();
|
||||
return finish();
|
||||
const responseText = await res.clone().text();
|
||||
return handlers.onFlash(responseText);
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -322,29 +278,29 @@ export default class AnthropicProvider
|
||||
?.startsWith(EventStreamContentType) ||
|
||||
res.status !== 200
|
||||
) {
|
||||
const responseTexts = [responseText];
|
||||
const responseTexts = [];
|
||||
if (res.status === 401) {
|
||||
responseTexts.push(Locale.Error.Unauthorized);
|
||||
}
|
||||
|
||||
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");
|
||||
const responseText = responseTexts.join("\n\n");
|
||||
|
||||
return finish();
|
||||
return handlers.onFlash(responseText);
|
||||
}
|
||||
},
|
||||
onmessage(msg) {
|
||||
if (msg.data === "[DONE]" || finished) {
|
||||
return finish();
|
||||
if (msg.data === "[DONE]") {
|
||||
return;
|
||||
}
|
||||
const text = msg.data;
|
||||
try {
|
||||
@@ -353,20 +309,19 @@ export default class AnthropicProvider
|
||||
delta: { content: string };
|
||||
}>;
|
||||
const delta = choices[0]?.delta?.content;
|
||||
const textmoderation = json?.prompt_filter_results;
|
||||
|
||||
if (delta) {
|
||||
remainText += delta;
|
||||
handlers.onProgress(delta);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("[Request] parse error", text, msg);
|
||||
}
|
||||
},
|
||||
onclose() {
|
||||
finish();
|
||||
handlers.onFinish();
|
||||
},
|
||||
onerror(e) {
|
||||
onError(e);
|
||||
handlers.onError(e);
|
||||
throw e;
|
||||
},
|
||||
openWhenHidden: true,
|
||||
@@ -375,28 +330,3 @@ export default class AnthropicProvider
|
||||
return timer;
|
||||
}
|
||||
}
|
||||
|
||||
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 = "") {
|
||||
let authKey = "";
|
||||
|
||||
if (apiKey) {
|
||||
// use user's api key first
|
||||
authKey = bearer(apiKey);
|
||||
}
|
||||
|
||||
return authKey;
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { getLocaleText } from "../../core/locale";
|
||||
import { getLocaleText } from "../../common";
|
||||
|
||||
export default getLocaleText<
|
||||
{
|
||||
@@ -10,6 +10,10 @@ export default getLocaleText<
|
||||
Endpoint: {
|
||||
Title: string;
|
||||
SubTitle: string;
|
||||
Error: {
|
||||
EndWithBackslash: string;
|
||||
IllegalURL: string;
|
||||
};
|
||||
};
|
||||
ApiVerion: {
|
||||
Title: string;
|
||||
@@ -29,6 +33,10 @@ export default getLocaleText<
|
||||
Endpoint: {
|
||||
Title: "接口地址",
|
||||
SubTitle: "样例:",
|
||||
Error: {
|
||||
EndWithBackslash: "不能以「/」结尾",
|
||||
IllegalURL: "请输入一个完整可用的url",
|
||||
},
|
||||
},
|
||||
|
||||
ApiVerion: {
|
||||
@@ -47,6 +55,10 @@ export default getLocaleText<
|
||||
Endpoint: {
|
||||
Title: "Endpoint Address",
|
||||
SubTitle: "Example:",
|
||||
Error: {
|
||||
EndWithBackslash: "Cannot end with '/'",
|
||||
IllegalURL: "Please enter a complete available url",
|
||||
},
|
||||
},
|
||||
|
||||
ApiVerion: {
|
||||
@@ -64,6 +76,10 @@ export default getLocaleText<
|
||||
Endpoint: {
|
||||
Title: "Endpoint Address",
|
||||
SubTitle: "Exemplo: ",
|
||||
Error: {
|
||||
EndWithBackslash: "Não é possível terminar com '/'",
|
||||
IllegalURL: "Insira um URL completo disponível",
|
||||
},
|
||||
},
|
||||
|
||||
ApiVerion: {
|
||||
@@ -81,6 +97,10 @@ export default getLocaleText<
|
||||
Endpoint: {
|
||||
Title: "Adresa koncového bodu",
|
||||
SubTitle: "Príklad:",
|
||||
Error: {
|
||||
EndWithBackslash: "Nemôže končiť znakom „/“",
|
||||
IllegalURL: "Zadajte úplnú dostupnú adresu URL",
|
||||
},
|
||||
},
|
||||
|
||||
ApiVerion: {
|
||||
@@ -98,6 +118,10 @@ export default getLocaleText<
|
||||
Endpoint: {
|
||||
Title: "終端地址",
|
||||
SubTitle: "範例:",
|
||||
Error: {
|
||||
EndWithBackslash: "不能以「/」結尾",
|
||||
IllegalURL: "請輸入一個完整可用的url",
|
||||
},
|
||||
},
|
||||
|
||||
ApiVerion: {
|
||||
|
38
app/client/providers/anthropic/utils.ts
Normal file
38
app/client/providers/anthropic/utils.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
export function trimEnd(s: string, end = " ") {
|
||||
if (end.length === 0) return s;
|
||||
|
||||
while (s.endsWith(end)) {
|
||||
s = s.slice(0, -end.length);
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
export function bearer(value: string) {
|
||||
return `Bearer ${value.trim()}`;
|
||||
}
|
||||
|
||||
export function getAuthKey(apiKey = "") {
|
||||
let authKey = "";
|
||||
|
||||
if (apiKey) {
|
||||
// use user's api key first
|
||||
authKey = bearer(apiKey);
|
||||
}
|
||||
|
||||
return authKey;
|
||||
}
|
||||
|
||||
export function prettyObject(msg: any) {
|
||||
const obj = msg;
|
||||
if (typeof msg !== "string") {
|
||||
msg = JSON.stringify(msg, null, " ");
|
||||
}
|
||||
if (msg === "{}") {
|
||||
return obj.toString();
|
||||
}
|
||||
if (msg.startsWith("```json")) {
|
||||
return msg;
|
||||
}
|
||||
return ["```json", msg, "```"].join("\n");
|
||||
}
|
Reference in New Issue
Block a user