Merge branch 'main' of https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web
This commit is contained in:
commit
e1afdf43c1
|
@ -7,21 +7,25 @@ import {
|
||||||
LLMUsage,
|
LLMUsage,
|
||||||
SpeechOptions,
|
SpeechOptions,
|
||||||
} from "../api";
|
} from "../api";
|
||||||
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
|
import {
|
||||||
|
useAccessStore,
|
||||||
|
useAppConfig,
|
||||||
|
useChatStore,
|
||||||
|
usePluginStore,
|
||||||
|
ChatMessageTool,
|
||||||
|
} from "@/app/store";
|
||||||
|
import { stream } from "@/app/utils/chat";
|
||||||
import { getClientConfig } from "@/app/config/client";
|
import { getClientConfig } from "@/app/config/client";
|
||||||
import { GEMINI_BASE_URL } from "@/app/constant";
|
import { GEMINI_BASE_URL } from "@/app/constant";
|
||||||
import Locale from "../../locales";
|
|
||||||
import {
|
|
||||||
EventStreamContentType,
|
|
||||||
fetchEventSource,
|
|
||||||
} from "@fortaine/fetch-event-source";
|
|
||||||
import { prettyObject } from "@/app/utils/format";
|
|
||||||
import {
|
import {
|
||||||
getMessageTextContent,
|
getMessageTextContent,
|
||||||
getMessageImages,
|
getMessageImages,
|
||||||
isVisionModel,
|
isVisionModel,
|
||||||
} from "@/app/utils";
|
} from "@/app/utils";
|
||||||
import { preProcessImageContent } from "@/app/utils/chat";
|
import { preProcessImageContent } from "@/app/utils/chat";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
import { RequestPayload } from "./openai";
|
||||||
import { fetch } from "@/app/utils/stream";
|
import { fetch } from "@/app/utils/stream";
|
||||||
|
|
||||||
export class GeminiProApi implements LLMApi {
|
export class GeminiProApi implements LLMApi {
|
||||||
|
@ -178,115 +182,81 @@ export class GeminiProApi implements LLMApi {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (shouldStream) {
|
if (shouldStream) {
|
||||||
let responseText = "";
|
const [tools, funcs] = usePluginStore
|
||||||
let remainText = "";
|
.getState()
|
||||||
let finished = false;
|
.getAsTools(
|
||||||
|
useChatStore.getState().currentSession().mask?.plugin || [],
|
||||||
|
);
|
||||||
|
return stream(
|
||||||
|
chatPath,
|
||||||
|
requestPayload,
|
||||||
|
getHeaders(),
|
||||||
|
// @ts-ignore
|
||||||
|
[{ functionDeclarations: tools.map((tool) => tool.function) }],
|
||||||
|
funcs,
|
||||||
|
controller,
|
||||||
|
// parseSSE
|
||||||
|
(text: string, runTools: ChatMessageTool[]) => {
|
||||||
|
// console.log("parseSSE", text, runTools);
|
||||||
|
const chunkJson = JSON.parse(text);
|
||||||
|
|
||||||
const finish = () => {
|
const functionCall = chunkJson?.candidates
|
||||||
if (!finished) {
|
?.at(0)
|
||||||
finished = true;
|
?.content.parts.at(0)?.functionCall;
|
||||||
options.onFinish(responseText + remainText);
|
if (functionCall) {
|
||||||
}
|
const { name, args } = functionCall;
|
||||||
};
|
runTools.push({
|
||||||
|
id: nanoid(),
|
||||||
// animate response to make it looks smooth
|
type: "function",
|
||||||
function animateResponseText() {
|
function: {
|
||||||
if (finished || controller.signal.aborted) {
|
name,
|
||||||
responseText += remainText;
|
arguments: JSON.stringify(args), // utils.chat call function, using JSON.parse
|
||||||
finish();
|
},
|
||||||
return;
|
});
|
||||||
}
|
}
|
||||||
|
return chunkJson?.candidates?.at(0)?.content.parts.at(0)?.text;
|
||||||
if (remainText.length > 0) {
|
},
|
||||||
const fetchCount = Math.max(1, Math.round(remainText.length / 60));
|
// processToolMessage, include tool_calls message and tool call results
|
||||||
const fetchText = remainText.slice(0, fetchCount);
|
(
|
||||||
responseText += fetchText;
|
requestPayload: RequestPayload,
|
||||||
remainText = remainText.slice(fetchCount);
|
toolCallMessage: any,
|
||||||
options.onUpdate?.(responseText, fetchText);
|
toolCallResult: any[],
|
||||||
}
|
) => {
|
||||||
|
// @ts-ignore
|
||||||
requestAnimationFrame(animateResponseText);
|
requestPayload?.contents?.splice(
|
||||||
}
|
// @ts-ignore
|
||||||
|
requestPayload?.contents?.length,
|
||||||
// start animaion
|
0,
|
||||||
animateResponseText();
|
{
|
||||||
|
role: "model",
|
||||||
controller.signal.onabort = finish;
|
parts: toolCallMessage.tool_calls.map(
|
||||||
|
(tool: ChatMessageTool) => ({
|
||||||
fetchEventSource(chatPath, {
|
functionCall: {
|
||||||
fetch: fetch as any,
|
name: tool?.function?.name,
|
||||||
...chatPayload,
|
args: JSON.parse(tool?.function?.arguments as string),
|
||||||
async onopen(res) {
|
},
|
||||||
clearTimeout(requestTimeoutId);
|
}),
|
||||||
const contentType = res.headers.get("content-type");
|
),
|
||||||
console.log(
|
},
|
||||||
"[Gemini] request response content type: ",
|
// @ts-ignore
|
||||||
contentType,
|
...toolCallResult.map((result) => ({
|
||||||
|
role: "function",
|
||||||
|
parts: [
|
||||||
|
{
|
||||||
|
functionResponse: {
|
||||||
|
name: result.name,
|
||||||
|
response: {
|
||||||
|
name: result.name,
|
||||||
|
content: result.content, // TODO just text content...
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})),
|
||||||
);
|
);
|
||||||
|
|
||||||
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) {
|
options,
|
||||||
if (msg.data === "[DONE]" || finished) {
|
);
|
||||||
return finish();
|
|
||||||
}
|
|
||||||
const text = msg.data;
|
|
||||||
try {
|
|
||||||
const json = JSON.parse(text);
|
|
||||||
const delta = apiClient.extractMessage(json);
|
|
||||||
|
|
||||||
if (delta) {
|
|
||||||
remainText += delta;
|
|
||||||
}
|
|
||||||
|
|
||||||
const blockReason = json?.promptFeedback?.blockReason;
|
|
||||||
if (blockReason) {
|
|
||||||
// being blocked
|
|
||||||
console.log(`[Google] [Safety Ratings] result:`, blockReason);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error("[Request] parse error", text, msg);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onclose() {
|
|
||||||
finish();
|
|
||||||
},
|
|
||||||
onerror(e) {
|
|
||||||
options.onError?.(e);
|
|
||||||
throw e;
|
|
||||||
},
|
|
||||||
openWhenHidden: true,
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
const res = await fetch(chatPath, chatPayload);
|
const res = await fetch(chatPath, chatPayload);
|
||||||
clearTimeout(requestTimeoutId);
|
clearTimeout(requestTimeoutId);
|
||||||
|
|
|
@ -285,6 +285,9 @@ export function showPlugins(provider: ServiceProvider, model: string) {
|
||||||
if (provider == ServiceProvider.Anthropic && !model.includes("claude-2")) {
|
if (provider == ServiceProvider.Anthropic && !model.includes("claude-2")) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (provider == ServiceProvider.Google && !model.includes("vision")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -223,6 +223,11 @@ export function stream(
|
||||||
)
|
)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
let content = res.data || res?.statusText;
|
let content = res.data || res?.statusText;
|
||||||
|
// hotfix #5614
|
||||||
|
content =
|
||||||
|
typeof content === "string"
|
||||||
|
? content
|
||||||
|
: JSON.stringify(content);
|
||||||
if (res.status >= 300) {
|
if (res.status >= 300) {
|
||||||
return Promise.reject(content);
|
return Promise.reject(content);
|
||||||
}
|
}
|
||||||
|
@ -245,6 +250,7 @@ export function stream(
|
||||||
return e.toString();
|
return e.toString();
|
||||||
})
|
})
|
||||||
.then((content) => ({
|
.then((content) => ({
|
||||||
|
name: tool.function.name,
|
||||||
role: "tool",
|
role: "tool",
|
||||||
content,
|
content,
|
||||||
tool_call_id: tool.id,
|
tool_call_id: tool.id,
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
import type { Config } from "jest";
|
||||||
|
import nextJest from "next/jest.js";
|
||||||
|
|
||||||
|
const createJestConfig = nextJest({
|
||||||
|
// Provide the path to your Next.js app to load next.config.js and .env files in your test environment
|
||||||
|
dir: "./",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add any custom config to be passed to Jest
|
||||||
|
const config: Config = {
|
||||||
|
coverageProvider: "v8",
|
||||||
|
testEnvironment: "jsdom",
|
||||||
|
testMatch: ["**/*.test.js", "**/*.test.ts", "**/*.test.jsx", "**/*.test.tsx"],
|
||||||
|
setupFilesAfterEnv: ["<rootDir>/jest.setup.ts"],
|
||||||
|
moduleNameMapper: {
|
||||||
|
"^@/(.*)$": "<rootDir>/$1",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
|
||||||
|
export default createJestConfig(config);
|
|
@ -0,0 +1,2 @@
|
||||||
|
// Learn more: https://github.com/testing-library/jest-dom
|
||||||
|
import "@testing-library/jest-dom";
|
16
package.json
16
package.json
|
@ -6,16 +6,18 @@
|
||||||
"mask": "npx tsx app/masks/build.ts",
|
"mask": "npx tsx app/masks/build.ts",
|
||||||
"mask:watch": "npx watch \"yarn mask\" app/masks",
|
"mask:watch": "npx watch \"yarn mask\" app/masks",
|
||||||
"dev": "concurrently -r \"yarn run mask:watch\" \"next dev\"",
|
"dev": "concurrently -r \"yarn run mask:watch\" \"next dev\"",
|
||||||
"build": "yarn mask && cross-env BUILD_MODE=standalone next build",
|
"build": "yarn test:ci && yarn mask && cross-env BUILD_MODE=standalone next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
"export": "yarn mask && cross-env BUILD_MODE=export BUILD_APP=1 next build",
|
"export": "yarn test:ci && yarn mask && cross-env BUILD_MODE=export BUILD_APP=1 next build",
|
||||||
"export:dev": "concurrently -r \"yarn mask:watch\" \"cross-env BUILD_MODE=export BUILD_APP=1 next dev\"",
|
"export:dev": "concurrently -r \"yarn mask:watch\" \"cross-env BUILD_MODE=export BUILD_APP=1 next dev\"",
|
||||||
"app:dev": "concurrently -r \"yarn mask:watch\" \"yarn tauri dev\"",
|
"app:dev": "concurrently -r \"yarn mask:watch\" \"yarn tauri dev\"",
|
||||||
"app:build": "yarn mask && yarn tauri build",
|
"app:build": "yarn test:ci && yarn mask && yarn tauri build",
|
||||||
"prompts": "node ./scripts/fetch-prompts.mjs",
|
"prompts": "node ./scripts/fetch-prompts.mjs",
|
||||||
"prepare": "husky install",
|
"prepare": "husky install",
|
||||||
"proxy-dev": "sh ./scripts/init-proxy.sh && proxychains -f ./scripts/proxychains.conf yarn dev"
|
"proxy-dev": "sh ./scripts/init-proxy.sh && proxychains -f ./scripts/proxychains.conf yarn dev",
|
||||||
|
"test": "jest --watch",
|
||||||
|
"test:ci": "jest --ci"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortaine/fetch-event-source": "^3.0.6",
|
"@fortaine/fetch-event-source": "^3.0.6",
|
||||||
|
@ -54,6 +56,9 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/api": "^1.6.0",
|
"@tauri-apps/api": "^1.6.0",
|
||||||
"@tauri-apps/cli": "1.5.11",
|
"@tauri-apps/cli": "1.5.11",
|
||||||
|
"@testing-library/jest-dom": "^6.4.8",
|
||||||
|
"@testing-library/react": "^16.0.0",
|
||||||
|
"@types/jest": "^29.5.12",
|
||||||
"@types/js-yaml": "4.0.9",
|
"@types/js-yaml": "4.0.9",
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/node": "^20.11.30",
|
"@types/node": "^20.11.30",
|
||||||
|
@ -69,8 +74,11 @@
|
||||||
"eslint-plugin-prettier": "^5.1.3",
|
"eslint-plugin-prettier": "^5.1.3",
|
||||||
"eslint-plugin-unused-imports": "^3.2.0",
|
"eslint-plugin-unused-imports": "^3.2.0",
|
||||||
"husky": "^8.0.0",
|
"husky": "^8.0.0",
|
||||||
|
"jest": "^29.7.0",
|
||||||
|
"jest-environment-jsdom": "^29.7.0",
|
||||||
"lint-staged": "^13.2.2",
|
"lint-staged": "^13.2.2",
|
||||||
"prettier": "^3.0.2",
|
"prettier": "^3.0.2",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
"tsx": "^4.16.0",
|
"tsx": "^4.16.0",
|
||||||
"typescript": "5.2.2",
|
"typescript": "5.2.2",
|
||||||
"watch": "^1.0.2",
|
"watch": "^1.0.2",
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
function sum(a: number, b: number) {
|
||||||
|
return a + b;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("sum module", () => {
|
||||||
|
test("adds 1 + 2 to equal 3", () => {
|
||||||
|
expect(sum(1, 2)).toBe(3);
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue