Compare commits

..

19 Commits

Author SHA1 Message Date
Arvin Xu 69dac2fc6e Merge branch 'main' into chore/try-react-compiler 2025-03-09 19:06:06 +08:00
Jonas Siewertsen 75c88425e0 📝 docs: Add NEXT_PUBLIC_ENABLE_NEXT_AUTH to clerk example (#6309) 2025-03-09 19:05:37 +08:00
samanhappy 30b2639c4b 💄 style(chat): auto send message from URL (#6497)
*  feat(chat): auto send message from URL

* introduce MessageFromUrl component
2025-03-09 19:01:59 +08:00
Rylan Cai 81867c413c 👷 chore: fix re-init in setup.sh (#6714) 2025-03-09 19:00:22 +08:00
Aloxaf f1ffc2c60c 💄 style: support openrouter claude 3.7 sonnet reasoning (#6806)
* feat: openrouter reasoning

* test: add test
2025-03-09 18:57:50 +08:00
gru-agent[bot] a9eadafd9f test: Add unit tests for the browserless function in the web crawler module (#6830)
Co-authored-by: gru-agent[bot] <185149714+gru-agent[bot]@users.noreply.github.com>
2025-03-09 18:11:24 +08:00
renovate[bot] 0e1146aa8d Update dependency lucide-react to ^0.479.0 (#6822)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-09 17:07:45 +08:00
lobehubbot 997ba159d2 📝 docs(bot): Auto sync agents & plugin to readme 2025-03-09 08:48:02 +00:00
semantic-release-bot ab8aa0a485 🔖 chore(release): v1.69.4 [skip ci]
### [Version&nbsp;1.69.4](https://github.com/lobehub/lobe-chat/compare/v1.69.3...v1.69.4)
<sup>Released on **2025-03-09**</sup>

#### 🐛 Bug Fixes

- **misc**: Fix mistral can not chat.

<br/>

<details>
<summary><kbd>Improvements and Fixes</kbd></summary>

#### What's fixed

* **misc**: Fix mistral can not chat, closes [#6828](https://github.com/lobehub/lobe-chat/issues/6828) ([00cba71](https://github.com/lobehub/lobe-chat/commit/00cba71))

</details>

<div align="right">

[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)

</div>
2025-03-09 08:46:55 +00:00
Arvin Xu 00cba71e27 🐛 fix: fix mistral can not chat (#6828)
* ♻️ refactor: refactor the implement

* fix mistral issue

* improve log

* refactor to the getXXXStoreState

* fix tests
2025-03-09 16:38:20 +08:00
renovate[bot] 86544a8455 Update dependency @google/generative-ai to ^0.24.0 (#6820)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-09 11:50:57 +08:00
lobehubbot f3c49cfd6a 📝 docs(bot): Auto sync agents & plugin to readme 2025-03-08 02:17:04 +00:00
semantic-release-bot b6eeba5850 🔖 chore(release): v1.69.3 [skip ci]
### [Version&nbsp;1.69.3](https://github.com/lobehub/lobe-chat/compare/v1.69.2...v1.69.3)
<sup>Released on **2025-03-08**</sup>

#### 💄 Styles

- **misc**: Add login ui for next-auth.

<br/>

<details>
<summary><kbd>Improvements and Fixes</kbd></summary>

#### Styles

* **misc**: Add login ui for next-auth, closes [#6434](https://github.com/lobehub/lobe-chat/issues/6434) ([541f275](https://github.com/lobehub/lobe-chat/commit/541f275))

</details>

<div align="right">

[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)

</div>
2025-03-08 02:16:01 +00:00
renovate[bot] 963f7889de Update dependency framer-motion to v12 (#5597) 2025-03-08 10:07:29 +08:00
Rylan Cai 541f27591a 💄 style: Add login ui for next-auth (#6434) 2025-03-08 10:02:51 +08:00
lobehubbot 3fb966cf37 📝 docs(bot): Auto sync agents & plugin to readme 2025-03-07 18:51:56 +00:00
Arvin Xu b30f55705f push 2025-02-20 03:19:08 +00:00
Arvin Xu fbe9ec0e48 Merge branch 'main' into chore/try-react-compiler 2025-02-20 11:13:12 +08:00
arvinxx e5b4b60c3d add config 2025-01-22 22:03:27 +08:00
43 changed files with 769 additions and 188 deletions
+50
View File
@@ -2,6 +2,56 @@
# Changelog
### [Version 1.69.4](https://github.com/lobehub/lobe-chat/compare/v1.69.3...v1.69.4)
<sup>Released on **2025-03-09**</sup>
#### 🐛 Bug Fixes
- **misc**: Fix mistral can not chat.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### What's fixed
- **misc**: Fix mistral can not chat, closes [#6828](https://github.com/lobehub/lobe-chat/issues/6828) ([00cba71](https://github.com/lobehub/lobe-chat/commit/00cba71))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
### [Version 1.69.3](https://github.com/lobehub/lobe-chat/compare/v1.69.2...v1.69.3)
<sup>Released on **2025-03-08**</sup>
#### 💄 Styles
- **misc**: Add login ui for next-auth.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### Styles
- **misc**: Add login ui for next-auth, closes [#6434](https://github.com/lobehub/lobe-chat/issues/6434) ([541f275](https://github.com/lobehub/lobe-chat/commit/541f275))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
### [Version 1.69.2](https://github.com/lobehub/lobe-chat/compare/v1.69.1...v1.69.2)
<sup>Released on **2025-03-07**</sup>
+21
View File
@@ -1,4 +1,25 @@
[
{
"children": {
"fixes": ["Fix mistral can not chat."]
},
"date": "2025-03-09",
"version": "1.69.4"
},
{
"children": {
"improvements": ["Add login ui for next-auth."]
},
"date": "2025-03-08",
"version": "1.69.3"
},
{
"children": {
"improvements": ["Refactor the agent runtime implement."]
},
"date": "2025-03-07",
"version": "1.69.2"
},
{
"children": {
"improvements": ["Add Qwen QwQ model."]
+92 -1
View File
@@ -170,6 +170,16 @@ show_message() {
;;
esac
;;
tips_download_failed)
case $LANGUAGE in
zh_CN)
echo "$2 下载失败,请检查网络连接。"
;;
*)
echo "$2 Download failed, please check the network connection."
;;
esac
;;
tips_already_installed)
case $LANGUAGE in
zh_CN)
@@ -260,6 +270,30 @@ show_message() {
;;
esac
;;
tips_no_docker_permission)
case $LANGUAGE in
zh_CN)
echo "WARN: 看起来当前用户没有 Docker 权限。"
echo "使用 'sudo usermod -aG docker $USER' 为用户分配 Docker 权限(可能需要重新启动 shell)。"
;;
*)
echo "WARN: It look like the current user does not have Docker permissions."
echo "Use 'sudo usermod -aG docker $USER' to assign Docker permissions to the user (may require restarting shell)."
;;
esac
;;
tips_init_database_failed)
case $LANGUAGE in
zh_CN)
echo "无法初始化数据库,为了避免你的数据重复初始化,请在首次成功启动时运行以下指令清空 Casdoor 初始配置文件:"
echo "echo '{}' > init_data.json"
;;
*)
echo "Failed to initialize the database. To avoid your data being initialized repeatedly, run the following command to unmount the initial configuration file of Casdoor when you first start successfully:"
echo "echo '{}' > init_data.json"
;;
esac
;;
ask_regenerate_secrets)
case $LANGUAGE in
zh_CN)
@@ -320,12 +354,27 @@ show_message() {
;;
esac
;;
ask_init_database)
case $LANGUAGE in
zh_CN)
echo "是否初始化数据库?"
;;
*)
echo "Do you want to initialize the database?"
;;
esac
;;
esac
}
# Function to download files
download_file() {
wget -q --show-progress "$1" -O "$2"
wget --show-progress "$1" -O "$2"
# If run failed, exit
if [ $? -ne 0 ]; then
show_message "tips_download_failed" "$2"
exit 1
fi
}
print_centered() {
@@ -629,12 +678,54 @@ section_regenerate_secrets() {
fi
fi
}
show_message "ask_regenerate_secrets"
ask "(y/n)" "y"
if [[ "$ask_result" == "y" ]]; then
section_regenerate_secrets
fi
section_init_database() {
if ! command -v docker &> /dev/null ; then
echo "docker" $(show_message "tips_no_executable")
return 1
fi
if ! docker compose &> /dev/null ; then
echo "docker compose" $(show_message "tips_no_executable")
return 1
fi
# Check if user has permissions to run Docker by trying to get the status of Docker (docker status).
# If this fails, the user probably does not have permissions for Docker.
# ref: https://github.com/paperless-ngx/paperless-ngx/blob/89e5c08a1fe4ca0b7641ae8fbd5554502199ae40/install-paperless-ngx.sh#L64-L72
if ! docker stats --no-stream &> /dev/null ; then
echo $(show_message "tips_no_docker_permission")
return 1
fi
docker compose pull
docker compose up --detach postgresql casdoor
# hopefully enough time for even the slower systems
sleep 15
docker compose stop
# Init finished, remove init mount
echo '{}' > init_data.json
}
show_message "ask_init_database"
ask "(y/n)" "y"
if [[ "$ask_result" == "y" ]]; then
# If return 1 means failed
section_init_database
if [ $? -ne 0 ]; then
echo $(show_message "tips_init_database_failed")
fi
else
show_message "tips_init_database_failed"
fi
section_display_configurated_report() {
# Display configuration reports
echo $(show_message "security_secrect_regenerate_report")
@@ -27,6 +27,7 @@ Go to [Clerk](https://clerk.com?utm_source=lobehub\&utm_medium=docs) to register
```shell
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_live_xxxxxxxxxxx
CLERK_SECRET_KEY=sk_live_xxxxxxxxxxxxxxxxxxxxxx
NEXT_PUBLIC_ENABLE_NEXT_AUTH=0
```
### Create and Configure Webhook in Clerk
+1
View File
@@ -25,6 +25,7 @@ const nextConfig: NextConfig = {
'@lobehub/ui',
'gpt-tokenizer',
],
reactCompiler: true,
webVitalsAttribution: ['CLS', 'LCP'],
webpackMemoryOptimizations: true,
},
+5 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@lobehub/chat",
"version": "1.69.2",
"version": "1.69.4",
"description": "Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
"keywords": [
"framework",
@@ -122,7 +122,7 @@
"@cyntler/react-doc-viewer": "^1.17.0",
"@electric-sql/pglite": "0.2.13",
"@google-cloud/vertexai": "^1.9.2",
"@google/generative-ai": "^0.22.0",
"@google/generative-ai": "^0.24.0",
"@huggingface/inference": "^2.8.1",
"@icons-pack/react-simple-icons": "9.6.0",
"@khmyznikov/pwa-install": "0.3.9",
@@ -165,7 +165,7 @@
"epub2": "^3.0.2",
"fast-deep-equal": "^3.1.3",
"file-type": "^20.0.0",
"framer-motion": "^11.16.0",
"framer-motion": "^12.0.0",
"gpt-tokenizer": "^2.8.1",
"html-to-text": "^9.0.5",
"i18next": "^24.2.1",
@@ -180,7 +180,7 @@
"langfuse": "3.29.1",
"langfuse-core": "3.29.1",
"lodash-es": "^4.17.21",
"lucide-react": "^0.477.0",
"lucide-react": "^0.479.0",
"mammoth": "^1.9.0",
"mdast-util-to-markdown": "^2.1.2",
"modern-screenshot": "^4.5.5",
@@ -286,6 +286,7 @@
"@types/ws": "^8.5.13",
"@vitest/coverage-v8": "~1.2.2",
"ajv-keywords": "^5.1.0",
"babel-plugin-react-compiler": "19.0.0-beta-21e868a-20250216",
"commitlint": "^19.6.1",
"consola": "^3.3.3",
"crypto-js": "^4.2.0",
+2 -1
View File
@@ -8,6 +8,7 @@
"@mozilla/readability": "^0.5.0",
"happy-dom": "^17.0.0",
"node-html-markdown": "^1.3.0",
"query-string": "^9.1.1"
"query-string": "^9.1.1",
"url-join": "^5"
}
}
@@ -0,0 +1,94 @@
import { describe, expect, it, vi } from 'vitest';
import { browserless } from '../browserless';
describe('browserless', () => {
it('should throw BrowserlessInitError when env vars not set', async () => {
const originalEnv = { ...process.env };
process.env = { ...originalEnv };
delete process.env.BROWSERLESS_URL;
delete process.env.BROWSERLESS_TOKEN;
await expect(browserless('https://example.com', { filterOptions: {} })).rejects.toThrow(
'`BROWSERLESS_URL` or `BROWSERLESS_TOKEN` are required',
);
process.env = originalEnv;
});
it('should return undefined on fetch error', async () => {
process.env.BROWSERLESS_TOKEN = 'test-token';
global.fetch = vi.fn().mockRejectedValue(new Error('Fetch error'));
const result = await browserless('https://example.com', { filterOptions: {} });
expect(result).toBeUndefined();
});
it('should return undefined when content is empty', async () => {
process.env.BROWSERLESS_TOKEN = 'test-token';
global.fetch = vi.fn().mockResolvedValue({
text: vi.fn().mockResolvedValue('<html></html>'),
} as any);
const result = await browserless('https://example.com', { filterOptions: {} });
expect(result).toBeUndefined();
});
it('should return undefined when title is "Just a moment..."', async () => {
process.env.BROWSERLESS_TOKEN = 'test-token';
global.fetch = vi.fn().mockResolvedValue({
text: vi.fn().mockResolvedValue('<html><title>Just a moment...</title></html>'),
} as any);
const result = await browserless('https://example.com', { filterOptions: {} });
expect(result).toBeUndefined();
});
it('should return crawl result on successful fetch', async () => {
process.env.BROWSERLESS_TOKEN = 'test-token';
global.fetch = vi.fn().mockResolvedValue({
text: vi.fn().mockResolvedValue(`
<html>
<head>
<title>Test Title</title>
<meta name="description" content="Test Description">
</head>
<body>
<h1>Test Content</h1>
</body>
</html>
`),
} as any);
const result = await browserless('https://example.com', { filterOptions: {} });
expect(result).toEqual({
content: expect.any(String),
contentType: 'text',
description: expect.any(String),
length: expect.any(Number),
siteName: null,
title: 'Test Title',
url: 'https://example.com',
});
});
it('should use correct URL when BROWSERLESS_URL is provided', async () => {
const customUrl = 'https://custom.browserless.io';
const originalEnv = { ...process.env };
process.env.BROWSERLESS_TOKEN = 'test-token';
process.env.BROWSERLESS_URL = customUrl;
global.fetch = vi.fn().mockImplementation((url) => {
expect(url).toContain(customUrl);
return Promise.resolve({
text: () => Promise.resolve('<html><title>Test</title></html>'),
});
});
await browserless('https://example.com', { filterOptions: {} });
expect(global.fetch).toHaveBeenCalled();
process.env = originalEnv;
});
});
@@ -1,4 +1,5 @@
import qs from 'query-string';
import urlJoin from 'url-join';
import { CrawlImpl, CrawlSuccessResult } from '../type';
import { htmlToMarkdown } from '../utils/htmlToMarkdown';
@@ -25,7 +26,7 @@ export const browserless: CrawlImpl = async (url, { filterOptions }) => {
try {
const res = await fetch(
qs.stringifyUrl({ query: { token: BROWSERLESS_TOKEN }, url: `${BASE_URL}/content` }),
qs.stringifyUrl({ query: { token: BROWSERLESS_TOKEN }, url: urlJoin(BASE_URL, '/content') }),
{
body: JSON.stringify(input),
headers: {
+6
View File
@@ -61,4 +61,10 @@ export const crawUrlRules: CrawlUrlRule[] = [
},
urlPattern: 'https://www.qiumiwu.com/standings/(.*)',
},
// mozilla use jina
{
impls: ['jina'],
urlPattern: 'https://developer.mozilla.org(.*)',
},
];
@@ -0,0 +1,161 @@
'use client';
import { LobeChat } from '@lobehub/ui/brand';
import { Button, Col, Flex, Row, Skeleton, Typography } from 'antd';
import { createStyles } from 'antd-style';
import { AuthError } from 'next-auth';
import { signIn } from 'next-auth/react';
import { useRouter, useSearchParams } from 'next/navigation';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import BrandWatermark from '@/components/BrandWatermark';
import AuthIcons from '@/components/NextAuth/AuthIcons';
import { DOCUMENTS_REFER_URL, PRIVACY_URL, TERMS_URL } from '@/const/url';
import { useUserStore } from '@/store/user';
const { Title, Paragraph } = Typography;
const useStyles = createStyles(({ css, token }) => ({
button: css`
text-transform: capitalize;
`,
container: css`
min-width: 360px;
border: 1px solid ${token.colorBorder};
border-radius: ${token.borderRadiusLG}px;
background: ${token.colorBgContainer};
`,
contentCard: css`
padding-block: 2.5rem;
padding-inline: 2rem;
`,
description: css`
margin: 0;
color: ${token.colorTextSecondary};
`,
footer: css`
padding: 1rem;
border-block-start: 1px solid ${token.colorBorder};
border-radius: 0 0 8px 8px;
color: ${token.colorTextDescription};
background: ${token.colorBgElevated};
`,
text: css`
text-align: center;
`,
title: css`
margin: 0;
color: ${token.colorTextHeading};
`,
}));
const BtnListLoading = memo(() => {
return (
<Flex gap={'small'} vertical>
<Skeleton.Button active style={{ minWidth: 300 }} />
<Skeleton.Button active style={{ minWidth: 300 }} />
<Skeleton.Button active style={{ minWidth: 300 }} />
</Flex>
);
});
/**
* Follow the implementation from AuthJS official documentation,
* but using client components.
* ref: https://authjs.dev/guides/pages/signin
*/
export default memo(() => {
const { styles } = useStyles();
const { t } = useTranslation('clerk');
const router = useRouter();
const oAuthSSOProviders = useUserStore((s) => s.oAuthSSOProviders);
const searchParams = useSearchParams();
// Redirect back to the page url
const callbackUrl = searchParams.get('callbackUrl') ?? '';
const handleSignIn = async (provider: string) => {
try {
await signIn(provider, { redirectTo: callbackUrl });
} catch (error) {
// Signin can fail for a number of reasons, such as the user
// not existing, or the user not having the correct role.
// In some cases, you may want to redirect to a custom error
if (error instanceof AuthError) {
return router.push(`/next-auth/?error=${error.type}`);
}
// Otherwise if a redirects happens Next.js can handle it
// so you can just re-thrown the error and let Next.js handle it.
// Docs: https://nextjs.org/docs/app/api-reference/functions/redirect#server-component
throw error;
}
};
const footerBtns = [
{ href: DOCUMENTS_REFER_URL, id: 0, label: t('footerPageLink__help') },
{ href: PRIVACY_URL, id: 1, label: t('footerPageLink__privacy') },
{ href: TERMS_URL, id: 2, label: t('footerPageLink__terms') },
];
return (
<div className={styles.container}>
<div className={styles.contentCard}>
{/* Card Body */}
<Flex gap="large" vertical>
{/* Header */}
<div className={styles.text}>
<Title className={styles.title} level={4}>
<div>
<LobeChat size={48} />
</div>
{t('signIn.start.title', { applicationName: 'LobeChat' })}
</Title>
<Paragraph className={styles.description}>{t('signIn.start.subtitle')}</Paragraph>
</div>
{/* Content */}
<Flex gap="small" vertical>
{oAuthSSOProviders ? (
oAuthSSOProviders.map((provider) => (
<Button
className={styles.button}
icon={AuthIcons(provider, 16)}
key={provider}
onClick={() => handleSignIn(provider)}
>
{provider}
</Button>
))
) : (
<BtnListLoading />
)}
</Flex>
</Flex>
</div>
<div className={styles.footer}>
{/* Footer */}
<Row>
<Col span={12}>
<Flex justify="left" style={{ height: '100%' }}>
<BrandWatermark />
</Flex>
</Col>
<Col offset={4} span={8}>
<Flex justify="right">
{footerBtns.map((btn) => (
<Button key={btn.id} onClick={() => router.push(btn.href)} size="small" type="text">
{btn.label}
</Button>
))}
</Flex>
</Col>
</Row>
</div>
</div>
);
});
@@ -0,0 +1,11 @@
import { Suspense } from 'react';
import Loading from '@/components/Loading/BrandTextLoading';
import AuthSignInBox from './AuthSignInBox';
export default () => (
<Suspense fallback={<Loading />}>
<AuthSignInBox />
</Suspense>
);
@@ -0,0 +1,31 @@
'use client';
import { useSearchParams } from 'next/navigation';
import { useEffect } from 'react';
import { useSendMessage } from '@/features/ChatInput/useSend';
import { useChatStore } from '@/store/chat';
const MessageFromUrl = () => {
const updateInputMessage = useChatStore((s) => s.updateInputMessage);
const { send: sendMessage } = useSendMessage();
const searchParams = useSearchParams();
useEffect(() => {
const message = searchParams.get('message');
if (message) {
// Remove message from URL
const params = new URLSearchParams(searchParams.toString());
params.delete('message');
const newUrl = `${window.location.pathname}?${params.toString()}`;
window.history.replaceState({}, '', newUrl);
updateInputMessage(message);
sendMessage();
}
}, [searchParams, updateInputMessage, sendMessage]);
return null;
};
export default MessageFromUrl;
@@ -1,7 +1,7 @@
import { Button, Space } from 'antd';
import { createStyles } from 'antd-style';
import { rgba } from 'polished';
import { memo, useEffect, useState } from 'react';
import { Suspense, memo, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Flexbox } from 'react-layout-kit';
@@ -13,6 +13,7 @@ import { useChatStore } from '@/store/chat';
import { chatSelectors } from '@/store/chat/selectors';
import { isMacOS } from '@/utils/platform';
import MessageFromUrl from './MessageFromUrl';
import SendMore from './SendMore';
import ShortcutHint from './ShortcutHint';
@@ -67,49 +68,54 @@ const Footer = memo<FooterProps>(({ onExpandChange, expand }) => {
}, [setIsMac]);
return (
<Flexbox
align={'end'}
className={styles.overrideAntdIcon}
distribution={'space-between'}
flex={'none'}
gap={8}
horizontal
padding={'0 24px'}
>
<Flexbox align={'center'} gap={8} horizontal style={{ overflow: 'hidden' }}>
{expand && <LocalFiles />}
</Flexbox>
<Flexbox align={'center'} flex={'none'} gap={8} horizontal>
<ShortcutHint />
<SaveTopic />
<Flexbox style={{ minWidth: 92 }}>
{isAIGenerating ? (
<Button
className={styles.loadingButton}
icon={<StopLoadingIcon />}
onClick={stopGenerateMessage}
>
{t('input.stop')}
</Button>
) : (
<Space.Compact>
<>
<Suspense fallback={null}>
<MessageFromUrl />
</Suspense>
<Flexbox
align={'end'}
className={styles.overrideAntdIcon}
distribution={'space-between'}
flex={'none'}
gap={8}
horizontal
padding={'0 24px'}
>
<Flexbox align={'center'} gap={8} horizontal style={{ overflow: 'hidden' }}>
{expand && <LocalFiles />}
</Flexbox>
<Flexbox align={'center'} flex={'none'} gap={8} horizontal>
<ShortcutHint />
<SaveTopic />
<Flexbox style={{ minWidth: 92 }}>
{isAIGenerating ? (
<Button
disabled={!canSend}
loading={!canSend}
onClick={() => {
sendMessage();
onExpandChange?.(false);
}}
type={'primary'}
className={styles.loadingButton}
icon={<StopLoadingIcon />}
onClick={stopGenerateMessage}
>
{t('input.send')}
{t('input.stop')}
</Button>
<SendMore disabled={!canSend} isMac={isMac} />
</Space.Compact>
)}
) : (
<Space.Compact>
<Button
disabled={!canSend}
loading={!canSend}
onClick={() => {
sendMessage();
onExpandChange?.(false);
}}
type={'primary'}
>
{t('input.send')}
</Button>
<SendMore disabled={!canSend} isMac={isMac} />
</Space.Compact>
)}
</Flexbox>
</Flexbox>
</Flexbox>
</Flexbox>
</>
);
});
@@ -10,7 +10,7 @@ import { userService } from '@/services/user';
import { useUserStore } from '@/store/user';
import { userProfileSelectors } from '@/store/user/selectors';
import AuthIcons from './AuthIcons';
import AuthIcons from '@/components/NextAuth/AuthIcons';
const { Item } = List;
@@ -12,10 +12,6 @@ import {
} from '@lobehub/ui/icons';
import React from 'react';
const iconProps = {
size: 32,
};
const iconComponents: { [key: string]: React.ElementType } = {
'auth0': Auth0,
'authelia': Authelia.Color,
@@ -29,9 +25,15 @@ const iconComponents: { [key: string]: React.ElementType } = {
'zitadel': Zitadel.Color,
};
const AuthIcons = (id: string) => {
/**
* Get the auth icons component for the given id
* @param id
* @param size default is 36
* @returns
*/
const AuthIcons = (id: string, size = 36) => {
const IconComponent = iconComponents[id] || iconComponents.default;
return <IconComponent {...iconProps} />;
return <IconComponent size={size}/>;
};
export default AuthIcons;
+26 -1
View File
@@ -137,6 +137,31 @@ const openrouterChatModels: AIChatModelCard[] = [
releasedAt: '2024-06-20',
type: 'chat',
},
{
abilities: {
functionCall: true,
reasoning: true,
vision: true,
},
contextWindowTokens: 200_000,
description:
'Claude 3.7 Sonnet 是 Anthropic 迄今为止最智能的模型,也是市场上首个混合推理模型。Claude 3.7 Sonnet 可以产生近乎即时的响应或延长的逐步思考,用户可以清晰地看到这些过程。Sonnet 特别擅长编程、数据科学、视觉处理、代理任务。',
displayName: 'Claude 3.7 Sonnet',
enabled: true,
id: 'anthropic/claude-3.7-sonnet',
maxOutput: 8192,
pricing: {
cachedInput: 0.3,
input: 3,
output: 15,
writeCacheInput: 3.75,
},
releasedAt: '2025-02-24',
settings: {
extendParams: ['enableReasoning', 'reasoningBudgetToken'],
},
type: 'chat',
},
{
abilities: {
functionCall: true,
@@ -258,7 +283,7 @@ const openrouterChatModels: AIChatModelCard[] = [
id: 'deepseek/deepseek-r1:free',
releasedAt: '2025-01-20',
type: 'chat',
},
},
{
abilities: {
vision: true,
@@ -112,12 +112,6 @@ const ModelCard = memo<ModelCardProps>(({ pricing, id, provider, displayName })
</Flexbox>
</Tooltip>
)}
<Tooltip title={t('messages.modelCard.pricing.inputTokens', { amount: inputPrice })}>
<Flexbox gap={2} horizontal>
<Icon icon={ArrowUpFromDot} />
{inputPrice}
</Flexbox>
</Tooltip>
{pricing?.writeCacheInput && (
<Tooltip
title={t('messages.modelCard.pricing.writeCacheInputTokens', {
@@ -130,6 +124,12 @@ const ModelCard = memo<ModelCardProps>(({ pricing, id, provider, displayName })
</Flexbox>
</Tooltip>
)}
<Tooltip title={t('messages.modelCard.pricing.inputTokens', { amount: inputPrice })}>
<Flexbox gap={2} horizontal>
<Icon icon={ArrowUpFromDot} />
{inputPrice}
</Flexbox>
</Tooltip>
<Tooltip title={t('messages.modelCard.pricing.outputTokens', { amount: outputPrice })}>
<Flexbox gap={2} horizontal>
<Icon icon={ArrowDownToDot} />
@@ -138,7 +138,7 @@ const TokenDetail = memo<TokenDetailProps>(({ usage, model, provider }) => {
</Flexbox>
)}
{outputDetails.length > 1 && (
<>
<Flexbox gap={4}>
<Flexbox
align={'center'}
gap={4}
@@ -146,12 +146,12 @@ const TokenDetail = memo<TokenDetailProps>(({ usage, model, provider }) => {
justify={'space-between'}
width={'100%'}
>
<div style={{ color: theme.colorTextDescription }}>
<div style={{ color: theme.colorTextDescription, fontSize: 12 }}>
{t('messages.tokenDetails.outputTitle')}
</div>
</Flexbox>
<TokenProgress data={outputDetails} showIcon />
</>
</Flexbox>
)}
<Flexbox>
<TokenProgress data={totalDetail} showIcon />
@@ -57,9 +57,6 @@ describe('specific LobeMistralAI tests', () => {
messages: [{ content: 'Hello', role: 'user' }],
model: 'open-mistral-7b',
stream: true,
stream_options: {
include_usage: true,
},
temperature: 0.35,
top_p: 1,
},
+10 -7
View File
@@ -1,8 +1,8 @@
import type { ChatModelCard } from '@/types/llm';
import { ModelProvider } from '../types';
import { LobeOpenAICompatibleFactory } from '../utils/openaiCompatibleFactory';
import type { ChatModelCard } from '@/types/llm';
export interface MistralModelCard {
capabilities: {
function_calling: boolean;
@@ -16,6 +16,9 @@ export interface MistralModelCard {
export const LobeMistralAI = LobeOpenAICompatibleFactory({
baseURL: 'https://api.mistral.ai/v1',
chatCompletion: {
// Mistral API does not support stream_options: { include_usage: true }
// refs: https://github.com/lobehub/lobe-chat/issues/6825
excludeUsage: true,
handlePayload: (payload) => ({
...(payload.max_tokens !== undefined && { max_tokens: payload.max_tokens }),
messages: payload.messages as any,
@@ -33,12 +36,14 @@ export const LobeMistralAI = LobeOpenAICompatibleFactory({
models: async ({ client }) => {
const { LOBE_DEFAULT_MODEL_LIST } = await import('@/config/aiModels');
const modelsPage = await client.models.list() as any;
const modelsPage = (await client.models.list()) as any;
const modelList: MistralModelCard[] = modelsPage.data;
return modelList
.map((model) => {
const knownModel = LOBE_DEFAULT_MODEL_LIST.find((m) => model.id.toLowerCase() === m.id.toLowerCase());
const knownModel = LOBE_DEFAULT_MODEL_LIST.find(
(m) => model.id.toLowerCase() === m.id.toLowerCase(),
);
return {
contextWindowTokens: model.max_context_length,
@@ -47,9 +52,7 @@ export const LobeMistralAI = LobeOpenAICompatibleFactory({
enabled: knownModel?.enabled || false,
functionCall: model.capabilities.function_calling,
id: model.id,
reasoning:
knownModel?.abilities?.reasoning
|| false,
reasoning: knownModel?.abilities?.reasoning || false,
vision: model.capabilities.vision,
};
})
@@ -92,6 +92,39 @@ describe('LobeOpenRouterAI', () => {
expect(result).toBeInstanceOf(Response);
});
it('should add reasoning field when thinking is enabled', async () => {
// Arrange
const mockStream = new ReadableStream();
const mockResponse = Promise.resolve(mockStream);
(instance['client'].chat.completions.create as Mock).mockResolvedValue(mockResponse);
// Act
const result = await instance.chat({
messages: [{ content: 'Hello', role: 'user' }],
model: 'mistralai/mistral-7b-instruct:free',
temperature: 0.7,
thinking: {
type: 'enabled',
budget_tokens: 1500,
},
});
// Assert
expect(instance['client'].chat.completions.create).toHaveBeenCalledWith(
expect.objectContaining({
messages: [{ content: 'Hello', role: 'user' }],
model: 'mistralai/mistral-7b-instruct:free',
reasoning: {
max_tokens: 1500,
},
temperature: 0.7,
}),
{ headers: { Accept: '*/*' } },
);
expect(result).toBeInstanceOf(Response);
});
describe('Error', () => {
it('should return OpenRouterBizError with an openai error response when OpenAI.APIError is thrown', async () => {
// Arrange
+11 -2
View File
@@ -2,7 +2,7 @@ import type { ChatModelCard } from '@/types/llm';
import { ModelProvider } from '../types';
import { LobeOpenAICompatibleFactory } from '../utils/openaiCompatibleFactory';
import { OpenRouterModelCard, OpenRouterModelExtraInfo } from './type';
import { OpenRouterModelCard, OpenRouterModelExtraInfo, OpenRouterReasoning } from './type';
const formatPrice = (price: string) => {
if (price === '-1') return undefined;
@@ -13,10 +13,19 @@ export const LobeOpenRouterAI = LobeOpenAICompatibleFactory({
baseURL: 'https://openrouter.ai/api/v1',
chatCompletion: {
handlePayload: (payload) => {
const { thinking } = payload;
let reasoning: OpenRouterReasoning = {};
if (thinking?.type === 'enabled') {
reasoning = {
max_tokens: thinking.budget_tokens,
};
}
return {
...payload,
include_reasoning: true,
model: payload.enabledSearch ? `${payload.model}:online` : payload.model,
reasoning,
stream: payload.stream ?? true,
} as any;
},
+19
View File
@@ -37,3 +37,22 @@ export interface OpenRouterModelExtraInfo {
endpoint?: OpenRouterModelEndpoint;
slug: string;
}
interface OpenRouterOpenAIReasoning {
effort: 'high' | 'medium' | 'low';
exclude?: boolean;
}
interface OpenRouterAnthropicReasoning {
exclude?: boolean;
max_tokens: number;
}
interface OpenRouterCommonReasoning {
exclude?: boolean;
}
export type OpenRouterReasoning =
| OpenRouterOpenAIReasoning
| OpenRouterAnthropicReasoning
| OpenRouterCommonReasoning;
+1 -1
View File
@@ -35,7 +35,7 @@ export const debugStream = async (stream: ReadableStream) => {
console.log(`[chunk ${chunk}] ${getTime()}`);
console.log(chunkValue);
console.log(`\n`);
console.log('');
finished = done;
chunk++;
@@ -57,6 +57,7 @@ interface OpenAICompatibleFactoryOptions<T extends Record<string, any> = any> {
apiKey?: string;
baseURL?: string;
chatCompletion?: {
excludeUsage?: boolean;
handleError?: (
error: any,
options: ConstructorOptions<T>,
@@ -224,12 +225,17 @@ export const LobeOpenAICompatibleFactory = <T extends Record<string, any> = any>
...postPayload,
messages,
...(chatCompletion?.noUserId ? {} : { user: options?.user }),
stream_options: postPayload.stream ? { include_usage: true } : undefined,
stream_options:
postPayload.stream && !chatCompletion?.excludeUsage
? { include_usage: true }
: undefined,
};
if (debug?.chatCompletion?.()) {
console.log('[requestPayload]:', JSON.stringify(finalPayload, null, 2));
console.log('[requestPayload]');
console.log(JSON.stringify(finalPayload), '\n');
}
response = await this.client.chat.completions.create(finalPayload, {
// https://github.com/lobehub/lobe-chat/pull/318
headers: { Accept: '*/*', ...options?.requestHeaders },
+1
View File
@@ -42,6 +42,7 @@ export default {
debug: authEnv.NEXT_AUTH_DEBUG,
pages: {
error: '/next-auth/error',
signIn: '/next-auth/signin',
},
providers: initSSOProviders(),
secret: authEnv.NEXT_AUTH_SECRET,
+1 -1
View File
@@ -36,7 +36,7 @@ export const config = {
'/login(.*)',
'/signup(.*)',
'/next-auth/error',
'/next-auth/(.*)',
// ↓ cloud ↓
],
};
+19 -13
View File
@@ -17,13 +17,19 @@ import {
} from '@/libs/agent-runtime';
import { filesPrompts } from '@/prompts/files';
import { BuiltinSystemRolePrompts } from '@/prompts/systemRole';
import { aiModelSelectors, aiProviderSelectors, useAiInfraStore } from '@/store/aiInfra';
import { getAgentChatConfig } from '@/store/chat/slices/aiChat/actions/helpers';
import { useSessionStore } from '@/store/session';
import { getAgentStoreState } from '@/store/agent';
import { agentChatConfigSelectors } from '@/store/agent/selectors';
import {
aiModelSelectors,
aiProviderSelectors,
getAiInfraStoreState,
useAiInfraStore,
} from '@/store/aiInfra';
import { getSessionStoreState } from '@/store/session';
import { sessionMetaSelectors } from '@/store/session/selectors';
import { useToolStore } from '@/store/tool';
import { getToolStoreState } from '@/store/tool';
import { pluginSelectors, toolSelectors } from '@/store/tool/selectors';
import { useUserStore } from '@/store/user';
import { getUserStoreState, useUserStore } from '@/store/user';
import {
modelConfigSelectors,
modelProviderSelectors,
@@ -46,10 +52,10 @@ import { API_ENDPOINTS } from './_url';
const isCanUseFC = (model: string, provider: string) => {
// TODO: remove isDeprecatedEdition condition in V2.0
if (isDeprecatedEdition) {
return modelProviderSelectors.isModelEnabledFunctionCall(model)(useUserStore.getState());
return modelProviderSelectors.isModelEnabledFunctionCall(model)(getUserStoreState());
}
return aiModelSelectors.isModelSupportToolUse(model, provider)(useAiInfraStore.getState());
return aiModelSelectors.isModelSupportToolUse(model, provider)(getAiInfraStoreState());
};
/**
@@ -169,7 +175,7 @@ class ChatService {
);
// =================== 0. process search =================== //
const chatConfig = getAgentChatConfig();
const chatConfig = agentChatConfigSelectors.currentChatConfig(getAgentStoreState());
const enabledSearch = chatConfig.searchMode !== 'off';
const isModelHasBuiltinSearch = aiModelSelectors.isModelHasBuiltinSearch(
@@ -200,7 +206,7 @@ class ChatService {
// ============ 2. preprocess tools ============ //
let filterTools = toolSelectors.enabledSchema(pluginIds)(useToolStore.getState());
let filterTools = toolSelectors.enabledSchema(pluginIds)(getToolStoreState());
// check this model can use function call
const canUseFC = isCanUseFC(payload.model, payload.provider!);
@@ -378,7 +384,7 @@ class ChatService {
* @param options
*/
runPluginApi = async (params: PluginRequestPayload, options?: FetchOptions) => {
const s = useToolStore.getState();
const s = getToolStoreState();
const settings = pluginSelectors.getPluginSettingsById(params.identifier)(s);
const manifest = pluginSelectors.getToolManifestById(params.identifier)(s);
@@ -537,7 +543,7 @@ class ChatService {
const hasTools = tools && tools?.length > 0;
const hasFC = hasTools && isCanUseFC(model, provider);
const toolsSystemRoles =
hasFC && toolSelectors.enabledSystemRoles(tools)(useToolStore.getState());
hasFC && toolSelectors.enabledSystemRoles(tools)(getToolStoreState());
const injectSystemRoles = BuiltinSystemRolePrompts({
historySummary: options?.historySummary,
@@ -565,9 +571,9 @@ class ChatService {
};
private mapTrace = (trace?: TracePayload, tag?: TraceTagMap): TracePayload => {
const tags = sessionMetaSelectors.currentAgentMeta(useSessionStore.getState()).tags || [];
const tags = sessionMetaSelectors.currentAgentMeta(getSessionStoreState()).tags || [];
const enabled = preferenceSelectors.userAllowTrace(useUserStore.getState());
const enabled = preferenceSelectors.userAllowTrace(getUserStoreState());
if (!enabled) return { ...trace, enabled: false };
+1 -1
View File
@@ -1,2 +1,2 @@
export type { AgentStore } from './store';
export { useAgentStore } from './store';
export { getAgentStoreState, useAgentStore } from './store';
@@ -1,4 +1,5 @@
import { contextCachingModels, thinkingWithToolClaudeModels } from '@/const/models';
import { DEFAULT_AGENT_CHAT_CONFIG } from '@/const/settings';
import { AgentStoreState } from '@/store/agent/initialState';
import { LobeAgentChatConfig } from '@/types/agent';
@@ -30,10 +31,10 @@ const enableHistoryCount = (s: AgentStoreState) => {
return chatConfig.enableHistoryCount;
};
const historyCount = (s: AgentStoreState) => {
const historyCount = (s: AgentStoreState): number => {
const chatConfig = currentAgentChatConfig(s);
return chatConfig.historyCount;
return chatConfig.historyCount || (DEFAULT_AGENT_CHAT_CONFIG.historyCount as number);
};
const displayMode = (s: AgentStoreState) => {
+2
View File
@@ -20,3 +20,5 @@ const createStore: StateCreator<AgentStore, [['zustand/devtools', never]]> = (..
const devtools = createDevtools('agent');
export const useAgentStore = createWithEqualityFn<AgentStore>()(devtools(createStore), shallow);
export const getAgentStoreState = () => useAgentStore.getState();
+1 -1
View File
@@ -1,2 +1,2 @@
export * from './selectors';
export { useAiInfraStore } from './store';
export { getAiInfraStoreState,useAiInfraStore } from './store';
+2
View File
@@ -23,3 +23,5 @@ const createStore: StateCreator<AiInfraStore, [['zustand/devtools', never]]> = (
const devtools = createDevtools('aiInfra');
export const useAiInfraStore = createWithEqualityFn<AiInfraStore>()(devtools(createStore), shallow);
export const getAiInfraStoreState = () => useAiInfraStore.getState();
@@ -5,13 +5,14 @@ import { template } from 'lodash-es';
import { StateCreator } from 'zustand/vanilla';
import { LOADING_FLAT, MESSAGE_CANCEL_FLAT } from '@/const/message';
import { DEFAULT_AGENT_CHAT_CONFIG } from '@/const/settings';
import { TraceEventType, TraceNameMap } from '@/const/trace';
import { isServerMode } from '@/const/version';
import { knowledgeBaseQAPrompts } from '@/prompts/knowledgeBaseQA';
import { chatService } from '@/services/chat';
import { messageService } from '@/services/message';
import { useAgentStore } from '@/store/agent';
import { agentChatConfigSelectors, agentSelectors } from '@/store/agent/selectors';
import { getAgentStoreState } from '@/store/agent/store';
import { chatHelpers } from '@/store/chat/helpers';
import { ChatStore } from '@/store/chat/store';
import { messageMapKey } from '@/store/chat/utils/messageMapKey';
@@ -21,12 +22,6 @@ import { MessageSemanticSearchChunk } from '@/types/rag';
import { setNamespace } from '@/utils/storeDebug';
import { chatSelectors, topicSelectors } from '../../../selectors';
import {
getAgentChatConfig,
getAgentConfig,
getAgentEnableHistoryCount,
getAgentKnowledge,
} from './helpers';
const n = setNamespace('ai');
@@ -163,7 +158,7 @@ export const generateAIChat: StateCreator<
threadId: activeThreadId,
};
const agentConfig = getAgentChatConfig();
const agentConfig = agentChatConfigSelectors.currentChatConfig(getAgentStoreState());
let tempMessageId: string | undefined = undefined;
let newTopicId: string | undefined = undefined;
@@ -288,7 +283,7 @@ export const generateAIChat: StateCreator<
// create a new array to avoid the original messages array change
const messages = [...originalMessages];
const { model, provider, chatConfig } = getAgentConfig();
const { model, provider, chatConfig } = agentSelectors.currentAgentConfig(getAgentStoreState());
let fileChunks: MessageSemanticSearchChunk[] | undefined;
let ragQueryId;
@@ -312,7 +307,7 @@ export const generateAIChat: StateCreator<
chunks,
userQuery: lastMsg.content,
rewriteQuery,
knowledge: getAgentKnowledge(),
knowledge: agentSelectors.currentEnabledKnowledge(getAgentStoreState()),
});
// 3. add the retrieve context messages to the messages history
@@ -354,11 +349,10 @@ export const generateAIChat: StateCreator<
}
// 5. summary history if context messages is larger than historyCount
const historyCount =
chatConfig.historyCount || (DEFAULT_AGENT_CHAT_CONFIG.historyCount as number);
const historyCount = agentChatConfigSelectors.historyCount(getAgentStoreState());
if (
chatConfig.enableHistoryCount &&
agentChatConfigSelectors.enableHistoryCount(getAgentStoreState()) &&
chatConfig.enableCompressHistory &&
originalMessages.length > historyCount
) {
@@ -387,7 +381,7 @@ export const generateAIChat: StateCreator<
n('generateMessage(start)', { messageId, messages }) as string,
);
const agentConfig = getAgentConfig();
const agentConfig = agentSelectors.currentAgentConfig(getAgentStoreState());
const chatConfig = agentConfig.chatConfig;
const compiler = template(chatConfig.inputTemplate, { interpolate: /{{([\S\s]+?)}}/g });
@@ -397,10 +391,13 @@ export const generateAIChat: StateCreator<
// ================================== //
// 1. slice messages with config
const historyCount = agentChatConfigSelectors.historyCount(getAgentStoreState());
const enableHistoryCount = agentChatConfigSelectors.enableHistoryCount(getAgentStoreState());
let preprocessMsgs = chatHelpers.getSlicedMessages(messages, {
includeNewUserMessage: true,
enableHistoryCount: getAgentEnableHistoryCount(),
historyCount: chatConfig.historyCount,
enableHistoryCount,
historyCount,
});
// 2. replace inputMessage template
@@ -1,13 +0,0 @@
import { useAgentStore } from '@/store/agent';
import { agentChatConfigSelectors, agentSelectors } from '@/store/agent/selectors';
export const getAgentConfig = () => agentSelectors.currentAgentConfig(useAgentStore.getState());
export const getAgentChatConfig = () =>
agentChatConfigSelectors.currentChatConfig(useAgentStore.getState());
export const getAgentEnableHistoryCount = () =>
agentChatConfigSelectors.enableHistoryCount(useAgentStore.getState());
export const getAgentKnowledge = () =>
agentSelectors.currentEnabledKnowledge(useAgentStore.getState());
+1 -1
View File
@@ -1,2 +1,2 @@
export type { SessionStore } from './store';
export { useSessionStore } from './store';
export { getSessionStoreState,useSessionStore } from './store';
+2
View File
@@ -31,3 +31,5 @@ export const useSessionStore = createWithEqualityFn<SessionStore>()(
),
shallow,
);
export const getSessionStoreState = () => useSessionStore.getState();
+1 -1
View File
@@ -1,2 +1,2 @@
export * from './helpers';
export { useToolStore } from './store';
export { getToolStoreState, useToolStore } from './store';
+2
View File
@@ -30,3 +30,5 @@ const createStore: StateCreator<ToolStore, [['zustand/devtools', never]]> = (...
const devtools = createDevtools('tools');
export const useToolStore = createWithEqualityFn<ToolStore>()(devtools(createStore), shallow);
export const getToolStoreState = () => useToolStore.getState();
+2
View File
@@ -40,3 +40,5 @@ export const useUserStore = createWithEqualityFn<UserStore>()(
subscribeWithSelector(devtools(createStore)),
shallow,
);
export const getUserStoreState = () => useUserStore.getState();
+66
View File
@@ -0,0 +1,66 @@
/* eslint-disable sort-keys-fix/sort-keys-fix, typescript-sort-keys/interface */
import { z } from 'zod';
import { SearchMode } from '@/types/search';
export interface WorkingModel {
model: string;
provider: string;
}
export interface LobeAgentChatConfig {
displayMode?: 'chat' | 'docs';
enableAutoCreateTopic?: boolean;
autoCreateTopicThreshold: number;
enableMaxTokens?: boolean;
/**
* 是否开启推理
*/
enableReasoning?: boolean;
/**
* 自定义推理强度
*/
enableReasoningEffort?: boolean;
reasoningBudgetToken?: number;
/**
* 禁用上下文缓存
*/
disableContextCaching?: boolean;
/**
* 历史消息条数
*/
historyCount?: number;
/**
* 开启历史记录条数
*/
enableHistoryCount?: boolean;
/**
* 历史消息长度压缩阈值
*/
enableCompressHistory?: boolean;
inputTemplate?: string;
searchMode?: SearchMode;
searchFCModel?: WorkingModel;
useModelBuiltinSearch?: boolean;
}
/* eslint-enable */
export const AgentChatConfigSchema = z.object({
autoCreateTopicThreshold: z.number().default(2),
displayMode: z.enum(['chat', 'docs']).optional(),
enableAutoCreateTopic: z.boolean().optional(),
enableCompressHistory: z.boolean().optional(),
enableHistoryCount: z.boolean().optional(),
enableMaxTokens: z.boolean().optional(),
enableReasoning: z.boolean().optional(),
enableReasoningEffort: z.boolean().optional(),
historyCount: z.number().optional(),
reasoningBudgetToken: z.number().optional(),
searchMode: z.enum(['off', 'on', 'auto']).optional(),
});
+4 -61
View File
@@ -1,9 +1,8 @@
import { z } from 'zod';
import { FileItem } from '@/types/files';
import { KnowledgeBaseItem } from '@/types/knowledgeBase';
import { FewShots, LLMParams } from '@/types/llm';
import { SearchMode } from '@/types/search';
import { LobeAgentChatConfig } from './chatConfig';
export type TTSServer = 'openai' | 'edge' | 'microsoft';
@@ -55,64 +54,8 @@ export interface LobeAgentConfig {
tts: LobeAgentTTSConfig;
}
/* eslint-disable sort-keys-fix/sort-keys-fix, typescript-sort-keys/interface */
export interface LobeAgentChatConfig {
displayMode?: 'chat' | 'docs';
enableAutoCreateTopic?: boolean;
autoCreateTopicThreshold: number;
enableMaxTokens?: boolean;
/**
* 是否开启推理
*/
enableReasoning?: boolean;
/**
* 自定义推理强度
*/
enableReasoningEffort?: boolean;
reasoningBudgetToken?: number;
/**
* 禁用上下文缓存
*/
disableContextCaching?: boolean;
/**
* 历史消息条数
*/
historyCount?: number;
/**
* 开启历史记录条数
*/
enableHistoryCount?: boolean;
/**
* 历史消息长度压缩阈值
*/
enableCompressHistory?: boolean;
inputTemplate?: string;
searchMode?: SearchMode;
useModelBuiltinSearch?: boolean;
}
/* eslint-enable */
export const AgentChatConfigSchema = z.object({
autoCreateTopicThreshold: z.number().default(2),
displayMode: z.enum(['chat', 'docs']).optional(),
enableAutoCreateTopic: z.boolean().optional(),
enableCompressHistory: z.boolean().optional(),
enableHistoryCount: z.boolean().optional(),
enableMaxTokens: z.boolean().optional(),
enableReasoning: z.boolean().optional(),
enableReasoningEffort: z.boolean().optional(),
historyCount: z.number().optional(),
reasoningBudgetToken: z.number().optional(),
searchMode: z.enum(['off', 'on', 'auto']).optional(),
});
export type LobeAgentConfigKeys =
| keyof LobeAgentConfig
| ['params', keyof LobeAgentConfig['params']];
export * from './chatConfig';