mirror of
https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
synced 2025-11-13 20:53:45 +08:00
Merge remote-tracking branch 'upstream/main'
This commit is contained in:
23
app/utils/baidu.ts
Normal file
23
app/utils/baidu.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { BAIDU_OATUH_URL } from "../constant";
|
||||
/**
|
||||
* 使用 AK,SK 生成鉴权签名(Access Token)
|
||||
* @return 鉴权签名信息
|
||||
*/
|
||||
export async function getAccessToken(
|
||||
clientId: string,
|
||||
clientSecret: string,
|
||||
): Promise<{
|
||||
access_token: string;
|
||||
expires_in: number;
|
||||
error?: number;
|
||||
}> {
|
||||
const res = await fetch(
|
||||
`${BAIDU_OATUH_URL}?grant_type=client_credentials&client_id=${clientId}&client_secret=${clientSecret}`,
|
||||
{
|
||||
method: "POST",
|
||||
mode: "cors",
|
||||
},
|
||||
);
|
||||
const resJson = await res.json();
|
||||
return resJson;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import heic2any from "heic2any";
|
||||
import { CACHE_URL_PREFIX, UPLOAD_URL } from "@/app/constant";
|
||||
import { RequestMessage } from "@/app/client/api";
|
||||
|
||||
export function compressImage(file: File, maxSize: number): Promise<string> {
|
||||
export function compressImage(file: Blob, maxSize: number): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (readerEvent: any) => {
|
||||
@@ -40,15 +41,104 @@ export function compressImage(file: File, maxSize: number): Promise<string> {
|
||||
reader.onerror = reject;
|
||||
|
||||
if (file.type.includes("heic")) {
|
||||
heic2any({ blob: file, toType: "image/jpeg" })
|
||||
.then((blob) => {
|
||||
reader.readAsDataURL(blob as Blob);
|
||||
})
|
||||
.catch((e) => {
|
||||
reject(e);
|
||||
});
|
||||
try {
|
||||
const heic2any = require("heic2any");
|
||||
heic2any({ blob: file, toType: "image/jpeg" })
|
||||
.then((blob: Blob) => {
|
||||
reader.readAsDataURL(blob);
|
||||
})
|
||||
.catch((e: any) => {
|
||||
reject(e);
|
||||
});
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
}
|
||||
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
|
||||
export async function preProcessImageContent(
|
||||
content: RequestMessage["content"],
|
||||
) {
|
||||
if (typeof content === "string") {
|
||||
return content;
|
||||
}
|
||||
const result = [];
|
||||
for (const part of content) {
|
||||
if (part?.type == "image_url" && part?.image_url?.url) {
|
||||
try {
|
||||
const url = await cacheImageToBase64Image(part?.image_url?.url);
|
||||
result.push({ type: part.type, image_url: { url } });
|
||||
} catch (error) {
|
||||
console.error("Error processing image URL:", error);
|
||||
}
|
||||
} else {
|
||||
result.push({ ...part });
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const imageCaches: Record<string, string> = {};
|
||||
export function cacheImageToBase64Image(imageUrl: string) {
|
||||
if (imageUrl.includes(CACHE_URL_PREFIX)) {
|
||||
if (!imageCaches[imageUrl]) {
|
||||
const reader = new FileReader();
|
||||
return fetch(imageUrl, {
|
||||
method: "GET",
|
||||
mode: "cors",
|
||||
credentials: "include",
|
||||
})
|
||||
.then((res) => res.blob())
|
||||
.then(
|
||||
async (blob) =>
|
||||
(imageCaches[imageUrl] = await compressImage(blob, 256 * 1024)),
|
||||
); // compressImage
|
||||
}
|
||||
return Promise.resolve(imageCaches[imageUrl]);
|
||||
}
|
||||
return Promise.resolve(imageUrl);
|
||||
}
|
||||
|
||||
export function base64Image2Blob(base64Data: string, contentType: string) {
|
||||
const byteCharacters = atob(base64Data);
|
||||
const byteNumbers = new Array(byteCharacters.length);
|
||||
for (let i = 0; i < byteCharacters.length; i++) {
|
||||
byteNumbers[i] = byteCharacters.charCodeAt(i);
|
||||
}
|
||||
const byteArray = new Uint8Array(byteNumbers);
|
||||
return new Blob([byteArray], { type: contentType });
|
||||
}
|
||||
|
||||
export function uploadImage(file: File): Promise<string> {
|
||||
if (!window._SW_ENABLED) {
|
||||
// if serviceWorker register error, using compressImage
|
||||
return compressImage(file, 256 * 1024);
|
||||
}
|
||||
const body = new FormData();
|
||||
body.append("file", file);
|
||||
return fetch(UPLOAD_URL, {
|
||||
method: "post",
|
||||
body,
|
||||
mode: "cors",
|
||||
credentials: "include",
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((res) => {
|
||||
console.log("res", res);
|
||||
if (res?.code == 0 && res?.data) {
|
||||
return res?.data;
|
||||
}
|
||||
throw Error(`upload Error: ${res?.msg}`);
|
||||
});
|
||||
}
|
||||
|
||||
export function removeImage(imageUrl: string) {
|
||||
return fetch(imageUrl, {
|
||||
method: "DELETE",
|
||||
mode: "cors",
|
||||
credentials: "include",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import { useAccessStore } from "../store/access";
|
||||
import { useAppConfig } from "../store/config";
|
||||
import { collectModels } from "./model";
|
||||
|
||||
export function identifyDefaultClaudeModel(modelName: string) {
|
||||
const accessStore = useAccessStore.getState();
|
||||
const configStore = useAppConfig.getState();
|
||||
|
||||
const allModals = collectModels(
|
||||
configStore.models,
|
||||
[configStore.customModels, accessStore.customModels].join(","),
|
||||
);
|
||||
|
||||
const modelMeta = allModals.find((m) => m.name === modelName);
|
||||
|
||||
return (
|
||||
modelName.startsWith("claude") &&
|
||||
modelMeta &&
|
||||
modelMeta.provider?.providerType === "anthropic"
|
||||
);
|
||||
}
|
||||
26
app/utils/cloudflare.ts
Normal file
26
app/utils/cloudflare.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
export function cloudflareAIGatewayUrl(fetchUrl: string) {
|
||||
// rebuild fetchUrl, if using cloudflare ai gateway
|
||||
// document: https://developers.cloudflare.com/ai-gateway/providers/openai/
|
||||
|
||||
const paths = fetchUrl.split("/");
|
||||
if ("gateway.ai.cloudflare.com" == paths[2]) {
|
||||
// is cloudflare.com ai gateway
|
||||
// https://gateway.ai.cloudflare.com/v1/{account_id}/{gateway_id}/azure-openai/{resource_name}/{deployment_name}/chat/completions?api-version=2023-05-15'
|
||||
if ("azure-openai" == paths[6]) {
|
||||
// is azure gateway
|
||||
return paths.slice(0, 8).concat(paths.slice(-3)).join("/"); // rebuild ai gateway azure_url
|
||||
}
|
||||
// https://gateway.ai.cloudflare.com/v1/{account_id}/{gateway_id}/openai/chat/completions
|
||||
if ("openai" == paths[6]) {
|
||||
// is openai gateway
|
||||
return paths.slice(0, 7).concat(paths.slice(-2)).join("/"); // rebuild ai gateway openai_url
|
||||
}
|
||||
// https://gateway.ai.cloudflare.com/v1/{account_id}/{gateway_id}/anthropic/v1/messages \
|
||||
if ("anthropic" == paths[6]) {
|
||||
// is anthropic gateway
|
||||
return paths.slice(0, 7).concat(paths.slice(-2)).join("/"); // rebuild ai gateway anthropic_url
|
||||
}
|
||||
// TODO: Amazon Bedrock, Groq, HuggingFace...
|
||||
}
|
||||
return fetchUrl;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo } from "react";
|
||||
import { useAccessStore, useAppConfig } from "../store";
|
||||
import { collectModels, collectModelsWithDefaultModel } from "./model";
|
||||
import { collectModelsWithDefaultModel } from "./model";
|
||||
|
||||
export function useAllModels() {
|
||||
const accessStore = useAccessStore();
|
||||
@@ -11,7 +11,12 @@ export function useAllModels() {
|
||||
[configStore.customModels, accessStore.customModels].join(","),
|
||||
accessStore.defaultModel,
|
||||
);
|
||||
}, [accessStore.customModels, configStore.customModels, configStore.models]);
|
||||
}, [
|
||||
accessStore.customModels,
|
||||
accessStore.defaultModel,
|
||||
configStore.customModels,
|
||||
configStore.models,
|
||||
]);
|
||||
|
||||
return models;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { DEFAULT_MODELS } from "../constant";
|
||||
import { LLMModel } from "../client/api";
|
||||
|
||||
const customProvider = (modelName: string) => ({
|
||||
id: modelName,
|
||||
providerName: "",
|
||||
const customProvider = (providerName: string) => ({
|
||||
id: providerName.toLowerCase(),
|
||||
providerName: providerName,
|
||||
providerType: "custom",
|
||||
});
|
||||
|
||||
@@ -23,7 +24,8 @@ export function collectModelTable(
|
||||
|
||||
// default models
|
||||
models.forEach((m) => {
|
||||
modelTable[m.name] = {
|
||||
// using <modelName>@<providerId> as fullName
|
||||
modelTable[`${m.name}@${m?.provider?.id}`] = {
|
||||
...m,
|
||||
displayName: m.name, // 'provider' is copied over if it exists
|
||||
};
|
||||
@@ -37,7 +39,7 @@ export function collectModelTable(
|
||||
const available = !m.startsWith("-");
|
||||
const nameConfig =
|
||||
m.startsWith("+") || m.startsWith("-") ? m.slice(1) : m;
|
||||
const [name, displayName] = nameConfig.split("=");
|
||||
let [name, displayName] = nameConfig.split("=");
|
||||
|
||||
// enable or disable all models
|
||||
if (name === "all") {
|
||||
@@ -45,12 +47,45 @@ export function collectModelTable(
|
||||
(model) => (model.available = available),
|
||||
);
|
||||
} else {
|
||||
modelTable[name] = {
|
||||
name,
|
||||
displayName: displayName || name,
|
||||
available,
|
||||
provider: modelTable[name]?.provider ?? customProvider(name), // Use optional chaining
|
||||
};
|
||||
// 1. find model by name, and set available value
|
||||
const [customModelName, customProviderName] = name.split("@");
|
||||
let count = 0;
|
||||
for (const fullName in modelTable) {
|
||||
const [modelName, providerName] = fullName.split("@");
|
||||
if (
|
||||
customModelName == modelName &&
|
||||
(customProviderName === undefined ||
|
||||
customProviderName === providerName)
|
||||
) {
|
||||
count += 1;
|
||||
modelTable[fullName]["available"] = available;
|
||||
// swap name and displayName for bytedance
|
||||
if (providerName === "bytedance") {
|
||||
[name, displayName] = [displayName, modelName];
|
||||
modelTable[fullName]["name"] = name;
|
||||
}
|
||||
if (displayName) {
|
||||
modelTable[fullName]["displayName"] = displayName;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 2. if model not exists, create new model with available value
|
||||
if (count === 0) {
|
||||
let [customModelName, customProviderName] = name.split("@");
|
||||
const provider = customProvider(
|
||||
customProviderName || customModelName,
|
||||
);
|
||||
// swap name and displayName for bytedance
|
||||
if (displayName && provider.providerName == "ByteDance") {
|
||||
[customModelName, displayName] = [displayName, customModelName];
|
||||
}
|
||||
modelTable[`${customModelName}@${provider?.id}`] = {
|
||||
name: customModelName,
|
||||
displayName: displayName || customModelName,
|
||||
available,
|
||||
provider, // Use optional chaining
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -100,3 +135,13 @@ export function collectModelsWithDefaultModel(
|
||||
const allModels = Object.values(modelTable);
|
||||
return allModels;
|
||||
}
|
||||
|
||||
export function isModelAvailableInServer(
|
||||
customModels: string,
|
||||
modelName: string,
|
||||
providerName: string,
|
||||
) {
|
||||
const fullName = `${modelName}@${providerName}`;
|
||||
const modelTable = collectModelTable(DEFAULT_MODELS, customModels);
|
||||
return modelTable[fullName]?.available === false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user