@lobehub/lobehub 2.0.5 → 2.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/.eslintrc.js +1 -0
  2. package/CHANGELOG.md +50 -0
  3. package/changelog/v2.json +14 -0
  4. package/docs/self-hosting/advanced/auth/clerk-to-betterauth.mdx +2 -0
  5. package/docs/self-hosting/advanced/auth/clerk-to-betterauth.zh-CN.mdx +2 -0
  6. package/docs/self-hosting/advanced/auth/nextauth-to-betterauth.mdx +2 -0
  7. package/docs/self-hosting/advanced/auth/nextauth-to-betterauth.zh-CN.mdx +2 -0
  8. package/docs/self-hosting/platform/docker.mdx +6 -0
  9. package/docs/self-hosting/platform/docker.zh-CN.mdx +6 -0
  10. package/docs/self-hosting/platform/vercel.mdx +0 -49
  11. package/docs/self-hosting/platform/vercel.zh-CN.mdx +0 -47
  12. package/docs/self-hosting/start.mdx +0 -20
  13. package/docs/self-hosting/start.zh-CN.mdx +0 -18
  14. package/package.json +1 -1
  15. package/packages/database/src/repositories/aiInfra/index.test.ts +52 -0
  16. package/packages/database/src/repositories/aiInfra/index.ts +103 -0
  17. package/packages/model-runtime/src/core/streams/protocol.ts +3 -1
  18. package/src/app/(backend)/api/workflows/memory-user-memory/pipelines/persona/update-writing/route.ts +19 -19
  19. package/src/app/[variants]/(main)/agent/features/Conversation/AgentWelcome/ToolAuthAlert.tsx +3 -2
  20. package/src/app/[variants]/(main)/settings/skill/features/KlavisSkillItem.tsx +3 -3
  21. package/src/app/[variants]/onboarding/components/KlavisServerList/hooks/useKlavisOAuth.ts +12 -5
  22. package/src/components/DebugNode.tsx +21 -0
  23. package/src/features/ChatInput/ActionBar/Tools/KlavisServerItem.tsx +3 -3
  24. package/src/features/ChatInput/ActionBar/Tools/ToolItem.tsx +16 -13
  25. package/src/features/ChatInput/ActionBar/Tools/ToolsList.tsx +51 -40
  26. package/src/features/ChatInput/ActionBar/components/ActionDropdown.tsx +7 -1
  27. package/src/features/ChatInput/ActionBar/components/ActionPopover.tsx +14 -11
  28. package/src/layout/GlobalProvider/useUserStateRedirect.ts +6 -2
  29. package/src/libs/observability/traceparent.test.ts +46 -7
  30. package/src/libs/observability/traceparent.ts +12 -10
  31. package/src/server/routers/lambda/klavis.ts +38 -10
  32. package/src/server/services/memory/userMemory/__tests__/extract.runtime.test.ts +181 -26
  33. package/src/server/services/memory/userMemory/extract.ts +120 -96
  34. package/src/server/services/memory/userMemory/persona/__tests__/service.test.ts +46 -0
  35. package/src/server/services/memory/userMemory/persona/service.ts +47 -6
  36. package/src/store/tool/slices/klavisStore/action.ts +20 -0
  37. package/docs/self-hosting/server-database.mdx +0 -157
  38. package/docs/self-hosting/server-database.zh-CN.mdx +0 -146
package/.eslintrc.js CHANGED
@@ -20,6 +20,7 @@ config.rules['unicorn/no-array-for-each'] = 0;
20
20
  config.rules['unicorn/prefer-number-properties'] = 0;
21
21
  config.rules['unicorn/prefer-query-selector'] = 0;
22
22
  config.rules['unicorn/no-array-callback-reference'] = 0;
23
+ config.rules['@typescript-eslint/no-use-before-define'] = 0;
23
24
  // FIXME: Linting error in src/app/[variants]/(main)/chat/features/Migration/DBReader.ts, the fundamental solution should be upgrading typescript-eslint
24
25
  config.rules['@typescript-eslint/no-useless-constructor'] = 0;
25
26
  config.rules['@next/next/no-img-element'] = 0;
package/CHANGELOG.md CHANGED
@@ -2,6 +2,56 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ### [Version 2.0.7](https://github.com/lobehub/lobe-chat/compare/v2.0.6...v2.0.7)
6
+
7
+ <sup>Released on **2026-01-28**</sup>
8
+
9
+ #### 🐛 Bug Fixes
10
+
11
+ - **model-runtime**: Include tool_calls in speed metrics & add getActiveTraceId.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### What's fixed
19
+
20
+ - **model-runtime**: Include tool_calls in speed metrics & add getActiveTraceId, closes [#11927](https://github.com/lobehub/lobe-chat/issues/11927) ([b24da44](https://github.com/lobehub/lobe-chat/commit/b24da44))
21
+
22
+ </details>
23
+
24
+ <div align="right">
25
+
26
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
27
+
28
+ </div>
29
+
30
+ ### [Version 2.0.6](https://github.com/lobehub/lobe-chat/compare/v2.0.5...v2.0.6)
31
+
32
+ <sup>Released on **2026-01-27**</sup>
33
+
34
+ #### 🐛 Bug Fixes
35
+
36
+ - **misc**: The klavis in onboarding connect timeout fixed.
37
+
38
+ <br/>
39
+
40
+ <details>
41
+ <summary><kbd>Improvements and Fixes</kbd></summary>
42
+
43
+ #### What's fixed
44
+
45
+ - **misc**: The klavis in onboarding connect timeout fixed, closes [#11918](https://github.com/lobehub/lobe-chat/issues/11918) ([bc165be](https://github.com/lobehub/lobe-chat/commit/bc165be))
46
+
47
+ </details>
48
+
49
+ <div align="right">
50
+
51
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
52
+
53
+ </div>
54
+
5
55
  ### [Version 2.0.5](https://github.com/lobehub/lobe-chat/compare/v2.0.4...v2.0.5)
6
56
 
7
57
  <sup>Released on **2026-01-27**</sup>
package/changelog/v2.json CHANGED
@@ -1,4 +1,18 @@
1
1
  [
2
+ {
3
+ "children": {},
4
+ "date": "2026-01-28",
5
+ "version": "2.0.7"
6
+ },
7
+ {
8
+ "children": {
9
+ "fixes": [
10
+ "The klavis in onboarding connect timeout fixed."
11
+ ]
12
+ },
13
+ "date": "2026-01-27",
14
+ "version": "2.0.6"
15
+ },
2
16
  {
3
17
  "children": {
4
18
  "fixes": [
@@ -63,6 +63,8 @@ For small self-hosted deployments, the simplest approach is to let users reset t
63
63
 
64
64
  Remove Clerk variables and add Better Auth variables:
65
65
 
66
+ <GenerateSecret envName="AUTH_SECRET" />
67
+
66
68
  ```bash
67
69
  # Remove these
68
70
  # NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=xxx
@@ -61,6 +61,8 @@ tags:
61
61
 
62
62
  移除 Clerk 变量并添加 Better Auth 变量:
63
63
 
64
+ <GenerateSecret envName="AUTH_SECRET" />
65
+
64
66
  ```bash
65
67
  # 移除这些
66
68
  # NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=xxx
@@ -116,6 +116,8 @@ For small self-hosted deployments, the simplest approach is to let users re-logi
116
116
 
117
117
  Remove NextAuth variables and add Better Auth variables:
118
118
 
119
+ <GenerateSecret envName="AUTH_SECRET" />
120
+
119
121
  ```bash
120
122
  # Remove these
121
123
  # NEXT_AUTH_SECRET=xxx
@@ -111,6 +111,8 @@ Better Auth 支持更多功能,以下是新增的环境变量:
111
111
 
112
112
  移除 NextAuth 变量并添加 Better Auth 变量:
113
113
 
114
+ <GenerateSecret envName="AUTH_SECRET" />
115
+
114
116
  ```bash
115
117
  # 移除这些
116
118
  # NEXT_AUTH_SECRET=xxx
@@ -58,6 +58,12 @@ Here is the process for deploying the LobeHub server database version on a Linux
58
58
 
59
59
  ### Create a file named `lobe-chat.env` to store environment variables:
60
60
 
61
+ Click the buttons below to generate required secrets:
62
+
63
+ <GenerateSecret envName="KEY_VAULTS_SECRET" />
64
+
65
+ <GenerateSecret envName="AUTH_SECRET" />
66
+
61
67
  ```shell
62
68
  # Website domain
63
69
  APP_URL=https://your-prod-domain.com
@@ -54,6 +54,12 @@ tags:
54
54
 
55
55
  ### 创建名为 `lobe-chat.env` 文件用于存放环境变量:
56
56
 
57
+ 点击下方按钮生成所需密钥:
58
+
59
+ <GenerateSecret envName="KEY_VAULTS_SECRET" />
60
+
61
+ <GenerateSecret envName="AUTH_SECRET" />
62
+
57
63
  ```shell
58
64
  # 网站域名
59
65
  APP_URL=https://your-prod-domain.com
@@ -114,55 +114,6 @@ The server-side database needs to be paired with a user authentication service t
114
114
  <Callout type={'info'}>
115
115
  For advanced features like SSO providers, magic link login, and email verification, see [Authentication Service](/docs/self-hosting/advanced/auth).
116
116
  </Callout>
117
-
118
- ### Add Public and Private Key Environment Variables in Vercel
119
-
120
- In Vercel's deployment environment variables, add the `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` and `CLERK_SECRET_KEY` environment variables. You can click on "API Keys" in the menu, then copy the corresponding values and paste them into Vercel's environment variables.
121
-
122
- <Image alt={'Find the corresponding public and private key environment variables in Clerk'} src={'/blog/assets28616219/89883703-7a1a-4a11-b944-5d804544e57c.webp'} />
123
-
124
- The environment variables required for this step are as follows:
125
-
126
- ```shell
127
- NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_live_xxxxxxxxxxx
128
- CLERK_SECRET_KEY=sk_live_xxxxxxxxxxxxxxxxxxxxxx
129
- ```
130
-
131
- Add the above variables to Vercel:
132
-
133
- <Image alt={'Add Clerk public and private key environment variables in Vercel'} src={'/blog/assets28616219/2bfa13df-6e20-4768-97c0-4dad06c85a2f.webp'} />
134
-
135
- ### Create and Configure Webhook in Clerk
136
-
137
- Since we let Clerk fully handle user authentication and management, we need Clerk to notify our application and store data in the database when there are changes in the user's lifecycle (create, update, delete). We achieve this requirement through the Webhook provided by Clerk.
138
-
139
- We need to add an endpoint in Clerk's Webhooks to inform Clerk to send notifications to this endpoint when a user's information changes.
140
-
141
- <Image alt={'Add Webhooks endpoint in Clerk'} src={'/blog/assets28616219/f50f47fb-5e8e-4930-bf4e-8cf6f5b8afb9.webp'} />
142
-
143
- Fill in the endpoint with the URL of your Vercel project, such as `https://your-project.vercel.app/api/webhooks/clerk`. Then, subscribe to events by checking the three user events (`user.created`, `user.deleted`, `user.updated`), and click create.
144
-
145
- <Callout type={'warning'}>
146
- The `https://` in the URL is essential to maintain the integrity of the URL.
147
- </Callout>
148
-
149
- <Image alt={'Configure URL and user events when adding Clerk Webhooks'} src={'/blog/assets28616219/0249ea56-ab17-4aa9-a56c-9ebd556c2645.webp'} />
150
-
151
- ### Add Webhook Secret to Vercel Environment Variables
152
-
153
- After creation, you can find the secret of this Webhook in the bottom right corner:
154
-
155
- <Image alt={'View Clerk Webhooks secret'} src={'/blog/assets28616219/fab4abb2-584b-49de-9340-813382951635.webp'} />
156
-
157
- The environment variable corresponding to this secret is `CLERK_WEBHOOK_SECRET`:
158
-
159
- ```shell
160
- CLERK_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxxxxxxxxxx
161
- ```
162
-
163
- Add it to Vercel's environment variables:
164
-
165
- <Image alt={'Add Clerk Webhooks secret in Vercel'} src={'/blog/assets28616219/5fdc9479-007f-46ab-9d6e-a9603e949116.webp'} />
166
117
  </Steps>
167
118
 
168
119
  By completing these steps, you have successfully configured the authentication service. Next, we will configure the S3 storage service.
@@ -114,53 +114,6 @@ tags:
114
114
  <Callout type={'info'}>
115
115
  如需 SSO 登录、魔法链接登录、邮箱验证等高级功能,请参阅 [身份验证服务](/zh/docs/self-hosting/advanced/auth)。
116
116
  </Callout>
117
-
118
- ### 在 Vercel 中添加公、私钥环境变量
119
-
120
- 在 Vercel 的部署环境变量中,添加 `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` 和 `CLERK_SECRET_KEY` 环境变量。你可以在菜单中点击「API Keys」,然后复制对应的值填入 Vercel 的环境变量中。
121
-
122
- <Image alt={'在 Clerk 中找到对应的公私钥环境变量'} src={'/blog/assets28616219/89883703-7a1a-4a11-b944-5d804544e57c.webp'} />
123
-
124
- 此步骤所需的环境变量如下:
125
-
126
- ```shell
127
- NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_live_xxxxxxxxxxx
128
- CLERK_SECRET_KEY=sk_live_xxxxxxxxxxxxxxxxxxxxxx
129
- ```
130
-
131
- 添加上述变量到 Vercel 中:
132
-
133
- <Image alt={'在 Vercel 中添加 Clerk 公私钥环境变量'} src={'/blog/assets28616219/2bfa13df-6e20-4768-97c0-4dad06c85a2f.webp'} />
134
-
135
- ### 在 Clerk 中创建并配置 Webhook
136
-
137
- 由于我们让 Clerk 完全接管用户鉴权与管理,因此我们需要在 Clerk 用户生命周期变更时(创建、更新、删除)中通知我们的应用并存储落库。我们通过 Clerk 提供的 Webhook 来实现这一诉求。
138
-
139
- 我们需要在 Clerk 的 Webhooks 中添加一个端点(Endpoint),告诉 Clerk 当用户发生变更时,向这个端点发送通知。
140
-
141
- <Image alt={'Clerk 添加 Webhooks 端点'} src={'/blog/assets28616219/f50f47fb-5e8e-4930-bf4e-8cf6f5b8afb9.webp'} />
142
-
143
- 在 endpoint 中填写你的 Vercel 项目的 URL,如 `https://your-project.vercel.app/api/webhooks/clerk`。然后在订阅事件(Subscribe to events)中,勾选 user 的三个事件(`user.created` 、`user.deleted`、`user.updated`),然后点击创建。
144
-
145
- <Callout type={'warning'}>URL 的`https://`不可缺失,须保持 URL 的完整性</Callout>
146
-
147
- <Image alt={'添加 Clerk Webhooks 时,配置 URL 和用户事件'} src={'/blog/assets28616219/0249ea56-ab17-4aa9-a56c-9ebd556c2645.webp'} />
148
-
149
- ### 将 Webhook 秘钥添加到 Vercel 环境变量
150
-
151
- 创建完毕后,可以在右下角找到该 Webhook 的秘钥:
152
-
153
- <Image alt={'查看 Clerk Webhooks 秘钥'} src={'/blog/assets28616219/fab4abb2-584b-49de-9340-813382951635.webp'} />
154
-
155
- 这个秘钥所对应的环境变量名为 `CLERK_WEBHOOK_SECRET`:
156
-
157
- ```shell
158
- CLERK_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxxxxxxxxxx
159
- ```
160
-
161
- 将其添加到 Vercel 的环境变量中:
162
-
163
- <Image alt={'在 Vercel 中 添加 Clerk Webhooks 秘钥'} src={'/blog/assets28616219/5fdc9479-007f-46ab-9d6e-a9603e949116.webp'} />
164
117
  </Steps>
165
118
 
166
119
  这样,你已经成功配置了身份验证服务。接下来我们将配置 S3 存储服务。
@@ -16,24 +16,4 @@ tags:
16
16
 
17
17
  LobeHub supports various deployment platforms, including Vercel, Docker, and Docker Compose. You can choose a deployment platform that suits you to build your own Lobe Chat.
18
18
 
19
- ## Quick Deployment
20
-
21
- For users who are new to LobeHub, we recommend using the client-side database mode for quick deployment. The advantage of this mode is that deployment can be quickly completed with just one command/button, making it easy for you to quickly get started and experience LobeHub.
22
-
23
- You can follow the guide below for quick deployment of LobeHub:
24
-
25
19
  <PlatformCards urlPrefix={'platform'} />
26
-
27
- <Callout>
28
- In the client-side database mode, data is stored locally on the user's device, without
29
- cross-device synchronization, and does not support advanced features such as file uploads and
30
- knowledge base.
31
- </Callout>
32
-
33
- ## Advanced Mode: Server-Side Database
34
-
35
- For users who are already familiar with LobeHub or need cross-device synchronization, you can deploy a version with a server-side database to access a more complete and powerful LobeHub.
36
-
37
- <Cards>
38
- <Card href={'/docs/self-hosting/server-database'} title={'Server-Side Database Deployment Guide'} />
39
- </Cards>
@@ -20,22 +20,4 @@ tags:
20
20
 
21
21
  LobeHub 支持多种部署平台,包括 Vercel、Docker、 Docker Compose 、阿里云计算巢 和腾讯轻量云 等,你可以选择适合自己的部署平台进行部署,构建属于自己的 Lobe Chat。
22
22
 
23
- ## 快速部署
24
-
25
- 对于第一次了解 LobeHub 的用户,我们推荐使用客户端数据库的模式快速部署,该模式的优势是一行指令 / 一个按钮即可快捷完成部署,便于你快速上手与体验 LobeHub。
26
-
27
- 你可以通过以下指南快速部署 LobeHub:
28
-
29
23
  <PlatformCards urlPrefix={'platform'} />
30
-
31
- <Callout>
32
- 客户端数据库模式下数据均保留在用户本地,不会跨多端同步,也不支持文件上传、知识库等进阶功能。
33
- </Callout>
34
-
35
- ## 进阶模式:服务端数据库
36
-
37
- 针对已经了解 LobeHub 的用户,或需要多端同步的用户,可以自行部署带有服务端数据库的版本,进而获得更完整、功能更强大的 LobeHub。
38
-
39
- <Cards rows={1}>
40
- <Card href={'/zh/docs/self-hosting/server-database'} title={'服务端数据库部署指南'} />
41
- </Cards>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/lobehub",
3
- "version": "2.0.5",
3
+ "version": "2.0.7",
4
4
  "description": "LobeHub - an open-source,comprehensive AI Agent framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
5
5
  "keywords": [
6
6
  "framework",
@@ -2,6 +2,7 @@ import type {
2
2
  AiProviderDetailItem,
3
3
  AiProviderListItem,
4
4
  AiProviderRuntimeConfig,
5
+ AiProviderRuntimeState,
5
6
  EnabledProvider,
6
7
  } from '@lobechat/types';
7
8
  import { AiProviderModelListItem, EnabledAiModel, ExtendParamsType } from 'model-bank';
@@ -1774,4 +1775,55 @@ describe('AiInfraRepos', () => {
1774
1775
  });
1775
1776
  });
1776
1777
  });
1778
+
1779
+ describe('AiInfraRepos.tryMatchingProviderFrom', () => {
1780
+ const createRuntimeState = (models: EnabledAiModel[]): AiProviderRuntimeState => ({
1781
+ enabledAiModels: models,
1782
+ enabledAiProviders: [],
1783
+ enabledChatAiProviders: [],
1784
+ enabledImageAiProviders: [],
1785
+ runtimeConfig: {},
1786
+ });
1787
+
1788
+ it('prefers provider order when multiple providers have model', async () => {
1789
+ const runtimeState = createRuntimeState([
1790
+ { abilities: {}, enabled: true, id: 'm-1', type: 'chat', providerId: 'provider-b' },
1791
+ { abilities: {}, enabled: true, id: 'm-1', type: 'chat', providerId: 'provider-a' },
1792
+ ]);
1793
+
1794
+ const providerId = await AiInfraRepos.tryMatchingProviderFrom(runtimeState, {
1795
+ modelId: 'm-1',
1796
+ preferredProviders: ['provider-b', 'provider-a'],
1797
+ });
1798
+
1799
+ expect(providerId).toBe('provider-b');
1800
+ });
1801
+
1802
+ it('ignores disabled models when matching', async () => {
1803
+ const runtimeState = createRuntimeState([
1804
+ { abilities: {}, enabled: false, id: 'm-1', type: 'chat', providerId: 'provider-disabled' },
1805
+ { abilities: {}, enabled: true, id: 'm-1', type: 'chat', providerId: 'provider-a' },
1806
+ ]);
1807
+
1808
+ const providerId = await AiInfraRepos.tryMatchingProviderFrom(runtimeState, {
1809
+ modelId: 'm-1',
1810
+ preferredProviders: ['provider-disabled', 'provider-a'],
1811
+ });
1812
+
1813
+ expect(providerId).toBe('provider-a');
1814
+ });
1815
+
1816
+ it('falls back to provided fallback provider when no match', async () => {
1817
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
1818
+ const runtimeState = createRuntimeState([]);
1819
+
1820
+ const providerId = await AiInfraRepos.tryMatchingProviderFrom(runtimeState, {
1821
+ modelId: 'm-1',
1822
+ fallbackProvider: 'provider-fallback',
1823
+ });
1824
+
1825
+ expect(providerId).toBe('provider-fallback');
1826
+ warnSpy.mockRestore();
1827
+ });
1828
+ });
1777
1829
  });
@@ -24,6 +24,8 @@ import { LobeChatDatabase } from '../../type';
24
24
 
25
25
  type DecryptUserKeyVaults = (encryptKeyVaultsStr: string | null) => Promise<any>;
26
26
 
27
+ const normalizeProvider = (provider: string) => provider.toLowerCase();
28
+
27
29
  /**
28
30
  * Provider-level search defaults (only used when built-in models don't provide settings.searchImpl and settings.searchProvider)
29
31
  * Note: Not stored in DB, only injected during read
@@ -282,6 +284,107 @@ export class AiInfraRepos {
282
284
  };
283
285
  };
284
286
 
287
+ /**
288
+ * Resolve the best provider for a given model.
289
+ *
290
+ * Matching pipeline:
291
+ * 1) Build a map of provider -> enabled model ids (disabled models are ignored).
292
+ * 2) Walk providers in priority order: preferred providers (if any) -> explicit fallback provider -> remaining providers that have enabled models.
293
+ * 3) For each provider, look for an exact modelId match or any preferred model alias.
294
+ * 4) If nothing matches, fall back to the configured provider (with a warning) or throw when no fallback exists.
295
+ *
296
+ * Handles:
297
+ * - Preferred provider ordering (case-insensitive).
298
+ * - Preferred model aliases.
299
+ * - Disabled models are skipped.
300
+ * - Missing matches: falls back when possible, otherwise surfaces an error.
301
+ *
302
+ * Edge cases to note:
303
+ * - If preferredProviders are set, non-preferred providers are skipped unless they are also the explicit fallback.
304
+ * - If fallbackProvider lacks enabled models, it is still returned (caller should ensure runtimeConfig has credentials).
305
+ */
306
+ static async tryMatchingProviderFrom(
307
+ runtimeState: AiProviderRuntimeState,
308
+ options: {
309
+ fallbackProvider?: string;
310
+ label?: string;
311
+ modelId: string;
312
+ preferredModels?: string[];
313
+ preferredProviders?: string[];
314
+ },
315
+ ): Promise<string> {
316
+ const { modelId, fallbackProvider, preferredModels, preferredProviders, label } = options;
317
+
318
+ // Build a map of provider -> enabled model ids for quick membership checks; skip disabled models entirely
319
+ const providerModels = runtimeState.enabledAiModels.reduce<Record<string, Set<string>>>(
320
+ (acc, model) => {
321
+ if (model.enabled === false) return acc;
322
+
323
+ const providerId = normalizeProvider(model.providerId);
324
+ acc[providerId] = acc[providerId] || new Set<string>();
325
+ acc[providerId].add(model.id);
326
+
327
+ return acc;
328
+ },
329
+ {},
330
+ );
331
+
332
+ // Normalize preferred providers so ordering is stable and comparisons are case-insensitive
333
+ const normalizedPreferredProviders = (preferredProviders || [])
334
+ .map(normalizeProvider)
335
+ .filter(Boolean);
336
+
337
+ // Provider search pipeline:
338
+ // 1) iterate preferred providers (if given)
339
+ // 2) fall back to the explicitly configured fallback provider
340
+ // 3) consider any provider that has enabled models
341
+ const providerOrder = Array.from(
342
+ new Set(
343
+ [
344
+ ...normalizedPreferredProviders,
345
+ fallbackProvider ? normalizeProvider(fallbackProvider) : undefined,
346
+ ...Object.keys(providerModels),
347
+ ].filter(Boolean) as string[],
348
+ ),
349
+ );
350
+
351
+ // Candidate models include the requested modelId plus any preferred model aliases
352
+ const modelTargets = new Set([modelId, ...(preferredModels || [])]);
353
+
354
+ for (const providerId of providerOrder) {
355
+ // If preferred providers are specified, skip non-preferred providers unless they are the explicit fallback
356
+ if (
357
+ normalizedPreferredProviders.length > 0 &&
358
+ providerId !== normalizeProvider(fallbackProvider || '') &&
359
+ !normalizedPreferredProviders.includes(providerId)
360
+ ) {
361
+ continue;
362
+ }
363
+
364
+ const models = providerModels[providerId];
365
+ if (!models) {
366
+ continue;
367
+ }
368
+
369
+ // Accept the first provider in order whose enabled models contain either the requested id or any preferred alias
370
+ const match = Array.from(modelTargets).find((target) => models.has(target));
371
+ if (match) {
372
+ return providerId;
373
+ }
374
+ }
375
+
376
+ if (fallbackProvider) {
377
+ console.warn(
378
+ `[ai-infra] no enabled provider found for ${label || 'model'} "${modelId}" (preferred ${preferredProviders}), falling back to server-configured provider "${fallbackProvider}".`,
379
+ );
380
+ return normalizeProvider(fallbackProvider);
381
+ }
382
+
383
+ throw new Error(
384
+ `Unable to resolve provider for ${label || 'model'} "${modelId}". Check preferred providers/models configuration.`,
385
+ );
386
+ }
387
+
285
388
  getAiProviderModelList = async (
286
389
  providerId: string,
287
390
  options?: {
@@ -472,12 +472,14 @@ export const createTokenSpeedCalculator = (
472
472
  // - text/reasoning: standard text output events
473
473
  // - content_part/reasoning_part: multimodal output events used by Gemini 3+ models
474
474
  // which emit structured parts instead of plain text events
475
+ // - tool_calls: function calling output events
475
476
  if (
476
477
  !outputStartAt &&
477
478
  (chunk.type === 'text' ||
478
479
  chunk.type === 'reasoning' ||
479
480
  chunk.type === 'content_part' ||
480
- chunk.type === 'reasoning_part')
481
+ chunk.type === 'reasoning_part' ||
482
+ chunk.type === 'tool_calls')
481
483
  ) {
482
484
  outputStartAt = Date.now();
483
485
  }
@@ -8,39 +8,39 @@ import {
8
8
  } from '@/server/services/memory/userMemory/persona/service';
9
9
 
10
10
  const workflowPayloadSchema = z.object({
11
- userId: z.string().optional(),
12
11
  userIds: z.array(z.string()).optional(),
13
12
  });
14
13
 
15
14
  export const { POST } = serve(async (context) => {
16
- const payload = workflowPayloadSchema.parse(context.requestPayload || {});
15
+ const payload = await context.run('memory:pipelines:persona:update-writing:parse-payload', () =>
16
+ workflowPayloadSchema.parse(context.requestPayload || {}),
17
+ );
17
18
  const db = await getServerDB();
18
19
 
19
- const userIds = Array.from(
20
- new Set([...(payload.userIds || []), ...(payload.userId ? [payload.userId] : [])]),
21
- ).filter(Boolean);
22
-
20
+ const userIds = Array.from(new Set(payload.userIds || [])).filter(Boolean);
23
21
  if (userIds.length === 0) {
24
- return { message: 'userId or userIds is required', processedUsers: 0 };
22
+ throw new Error('No user IDs provided for persona update.');
25
23
  }
26
24
 
27
25
  const service = new UserPersonaService(db);
28
- const results = [];
29
26
 
30
- for (const userId of userIds) {
31
- const context = await buildUserPersonaJobInput(db, userId);
32
- const result = await service.composeWriting({ ...context, userId });
33
- results.push({
34
- diffId: result.diff?.id,
35
- documentId: result.document.id,
36
- userId,
37
- version: result.document.version,
38
- });
39
- }
27
+ await Promise.all(
28
+ userIds.map(async (userId) =>
29
+ context.run(`memory:pipelines:persona:update-writing:users:${userId}`, async () => {
30
+ const context = await buildUserPersonaJobInput(db, userId);
31
+ const result = await service.composeWriting({ ...context, userId });
32
+ return {
33
+ diffId: result.diff?.id,
34
+ documentId: result.document.id,
35
+ userId,
36
+ version: result.document.version,
37
+ };
38
+ }),
39
+ ),
40
+ );
40
41
 
41
42
  return {
42
43
  message: 'User persona processed via workflow.',
43
44
  processedUsers: userIds.length,
44
- results,
45
45
  };
46
46
  });
@@ -104,7 +104,7 @@ const KlavisToolAuthItem = memo<KlavisToolAuthItemProps>(({ tool, onAuthComplete
104
104
  try {
105
105
  await refreshKlavisServerTools(identifier);
106
106
  } catch (error) {
107
- console.error('[Klavis] Failed to check auth status:', error);
107
+ console.debug('[Klavis] Polling check (expected during auth):', error);
108
108
  }
109
109
  }, POLL_INTERVAL_MS);
110
110
 
@@ -129,7 +129,8 @@ const KlavisToolAuthItem = memo<KlavisToolAuthItemProps>(({ tool, onAuthComplete
129
129
  windowCheckIntervalRef.current = null;
130
130
  }
131
131
  oauthWindowRef.current = null;
132
- refreshKlavisServerTools(identifier);
132
+ // Start polling after window closes
133
+ startFallbackPolling(identifier);
133
134
  }
134
135
  } catch {
135
136
  if (windowCheckIntervalRef.current) {
@@ -120,7 +120,7 @@ const KlavisSkillItem = memo<KlavisSkillItemProps>(({ serverType, server }) => {
120
120
  try {
121
121
  await refreshKlavisServerTools(serverName);
122
122
  } catch (error) {
123
- console.error('[Klavis] Failed to check auth status:', error);
123
+ console.debug('[Klavis] Polling check (expected during auth):', error);
124
124
  }
125
125
  }, POLL_INTERVAL_MS);
126
126
 
@@ -145,8 +145,8 @@ const KlavisSkillItem = memo<KlavisSkillItemProps>(({ serverType, server }) => {
145
145
  windowCheckIntervalRef.current = null;
146
146
  }
147
147
  oauthWindowRef.current = null;
148
- await refreshKlavisServerTools(serverName);
149
- setIsWaitingAuth(false);
148
+ // Start polling after window closes
149
+ startFallbackPolling(serverName);
150
150
  }
151
151
  } catch {
152
152
  console.log('[Klavis] COOP blocked window.closed access, falling back to polling');