@lobehub/lobehub 2.0.0-next.101 → 2.0.0-next.103

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 (41) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/changelog/v1.json +18 -0
  3. package/package.json +1 -1
  4. package/packages/database/src/models/aiModel.ts +1 -0
  5. package/packages/model-bank/package.json +1 -0
  6. package/packages/model-bank/src/aiModels/aihubmix.ts +29 -0
  7. package/packages/model-bank/src/aiModels/google.ts +72 -10
  8. package/packages/model-bank/src/aiModels/index.ts +3 -0
  9. package/packages/model-bank/src/aiModels/infiniai.ts +5 -22
  10. package/packages/model-bank/src/aiModels/ollamacloud.ts +13 -0
  11. package/packages/model-bank/src/aiModels/siliconcloud.ts +0 -61
  12. package/packages/model-bank/src/aiModels/vertexai.ts +90 -1
  13. package/packages/model-bank/src/aiModels/zenmux.ts +1426 -0
  14. package/packages/model-bank/src/const/modelProvider.ts +1 -0
  15. package/packages/model-bank/src/standard-parameters/index.ts +9 -0
  16. package/packages/model-bank/src/types/aiModel.ts +1 -0
  17. package/packages/model-runtime/src/core/openaiCompatibleFactory/index.test.ts +2 -2
  18. package/packages/model-runtime/src/core/streams/google/index.ts +7 -2
  19. package/packages/model-runtime/src/core/streams/openai/__snapshots__/responsesStream.test.ts.snap +166 -166
  20. package/packages/model-runtime/src/index.ts +1 -1
  21. package/packages/model-runtime/src/providers/google/createImage.ts +1 -0
  22. package/packages/model-runtime/src/providers/google/index.ts +11 -1
  23. package/packages/model-runtime/src/providers/zenmux/index.test.ts +320 -0
  24. package/packages/model-runtime/src/providers/zenmux/index.ts +84 -0
  25. package/packages/model-runtime/src/runtimeMap.ts +2 -0
  26. package/packages/types/src/user/settings/keyVaults.ts +1 -0
  27. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/ModelSelect/ImageModelItem.tsx +16 -1
  28. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/ModelSelect/index.tsx +10 -9
  29. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/ResolutionSelect.tsx +88 -0
  30. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/index.tsx +9 -0
  31. package/src/app/[variants]/(main)/settings/hooks/useCategory.tsx +3 -3
  32. package/src/app/[variants]/(main)/settings/provider/features/ModelList/ModelItem.tsx +2 -7
  33. package/src/components/ModelSelect/NewModelBadge.tsx +23 -0
  34. package/src/components/ModelSelect/index.tsx +4 -0
  35. package/src/config/modelProviders/index.ts +3 -0
  36. package/src/config/modelProviders/zenmux.ts +21 -0
  37. package/src/envs/llm.ts +6 -0
  38. package/src/locales/default/image.ts +8 -0
  39. package/src/store/aiInfra/slices/aiProvider/action.ts +3 -0
  40. package/src/store/chat/slices/aiChat/actions/__tests__/conversationLifecycle.test.ts +3 -0
  41. package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +11 -0
@@ -19,6 +19,7 @@ import ImageUrl from './components/ImageUrl';
19
19
  import ImageUrlsUpload from './components/ImageUrlsUpload';
20
20
  import ModelSelect from './components/ModelSelect';
21
21
  import QualitySelect from './components/QualitySelect';
22
+ import ResolutionSelect from './components/ResolutionSelect';
22
23
  import SeedNumberInput from './components/SeedNumberInput';
23
24
  import SizeSelect from './components/SizeSelect';
24
25
  import StepsSliderInput from './components/StepsSliderInput';
@@ -54,6 +55,7 @@ const ConfigPanel = memo(() => {
54
55
  const isSupportImageUrl = useImageStore(isSupportedParamSelector('imageUrl'));
55
56
  const isSupportSize = useImageStore(isSupportedParamSelector('size'));
56
57
  const isSupportQuality = useImageStore(isSupportedParamSelector('quality'));
58
+ const isSupportResolution = useImageStore(isSupportedParamSelector('resolution'));
57
59
  const isSupportSeed = useImageStore(isSupportedParamSelector('seed'));
58
60
  const isSupportSteps = useImageStore(isSupportedParamSelector('steps'));
59
61
  const isSupportCfg = useImageStore(isSupportedParamSelector('cfg'));
@@ -78,6 +80,7 @@ const ConfigPanel = memo(() => {
78
80
  isSupportImageUrl,
79
81
  isSupportSize,
80
82
  isSupportQuality,
83
+ isSupportResolution,
81
84
  isSupportSeed,
82
85
  isSupportSteps,
83
86
  isSupportCfg,
@@ -168,6 +171,12 @@ const ConfigPanel = memo(() => {
168
171
  </ConfigItemLayout>
169
172
  )}
170
173
 
174
+ {isSupportResolution && (
175
+ <ConfigItemLayout label={t('config.resolution.label')}>
176
+ <ResolutionSelect />
177
+ </ConfigItemLayout>
178
+ )}
179
+
171
180
  {showDimensionControl && <DimensionControlGroup />}
172
181
 
173
182
  {isSupportSteps && (
@@ -22,7 +22,7 @@ import { featureFlagsSelectors, useServerConfigStore } from '@/store/serverConfi
22
22
  export const useCategory = () => {
23
23
  const { t } = useTranslation('setting');
24
24
  const mobile = useServerConfigStore((s) => s.isMobile);
25
- const { enableSTT, hideDocs } = useServerConfigStore(featureFlagsSelectors);
25
+ const { enableSTT, hideDocs, showAiImage } = useServerConfigStore(featureFlagsSelectors);
26
26
 
27
27
  const cateItems: MenuProps['items'] = useMemo(
28
28
  () =>
@@ -50,7 +50,7 @@ export const useCategory = () => {
50
50
  key: SettingsTabs.Provider,
51
51
  label: t('tab.provider'),
52
52
  },
53
- {
53
+ showAiImage && {
54
54
  icon: <Icon icon={ImageIcon} />,
55
55
  key: SettingsTabs.Image,
56
56
  label: t('tab.image'),
@@ -84,7 +84,7 @@ export const useCategory = () => {
84
84
  label: t('tab.about'),
85
85
  },
86
86
  ].filter(Boolean) as MenuProps['items'],
87
- [t, enableSTT, hideDocs, mobile],
87
+ [t, enableSTT, hideDocs, mobile, showAiImage],
88
88
  );
89
89
 
90
90
  return cateItems;
@@ -9,6 +9,7 @@ import { useTranslation } from 'react-i18next';
9
9
  import { Flexbox } from 'react-layout-kit';
10
10
 
11
11
  import { ModelInfoTags } from '@/components/ModelSelect';
12
+ import NewModelBadge from '@/components/ModelSelect/NewModelBadge';
12
13
  import { useIsMobile } from '@/hooks/useIsMobile';
13
14
  import { aiModelSelectors, useAiInfraStore } from '@/store/aiInfra';
14
15
  import { formatPriceByCurrency } from '@/utils/format';
@@ -17,7 +18,6 @@ import {
17
18
  getTextInputUnitRate,
18
19
  getTextOutputUnitRate,
19
20
  } from '@/utils/pricing';
20
- import { isNewReleaseDate } from '@/utils/time';
21
21
 
22
22
  import ModelConfigModal from './ModelConfigModal';
23
23
  import { ProviderSettingsContext } from './ProviderSettingsContext';
@@ -163,12 +163,7 @@ const ModelItem = memo<ModelItemProps>(
163
163
 
164
164
  const isMobile = useIsMobile();
165
165
 
166
- const NewTag =
167
- releasedAt && isNewReleaseDate(releasedAt) ? (
168
- <Tag color="blue" style={{ marginLeft: 8 }}>
169
- {t('new', { ns: 'common' })}
170
- </Tag>
171
- ) : null;
166
+ const NewTag = <NewModelBadge releasedAt={releasedAt} />;
172
167
 
173
168
  const ModelIdTag = (
174
169
  <Tag onClick={copyModelId} style={{ cursor: 'pointer', marginRight: 0 }}>
@@ -0,0 +1,23 @@
1
+ import { Tag } from '@lobehub/ui';
2
+ import { memo } from 'react';
3
+ import { useTranslation } from 'react-i18next';
4
+
5
+ import { isNewReleaseDate } from '@/utils/time';
6
+
7
+ interface NewModelBadgeProps {
8
+ releasedAt?: string;
9
+ }
10
+
11
+ const NewModelBadge = memo<NewModelBadgeProps>(({ releasedAt }) => {
12
+ const { t } = useTranslation('common');
13
+
14
+ if (!releasedAt || !isNewReleaseDate(releasedAt)) return null;
15
+
16
+ return (
17
+ <Tag color="blue" size="small">
18
+ {t('new')}
19
+ </Tag>
20
+ );
21
+ });
22
+
23
+ export default NewModelBadge;
@@ -21,6 +21,8 @@ import { Flexbox } from 'react-layout-kit';
21
21
  import { AiProviderSourceType } from '@/types/aiProvider';
22
22
  import { formatTokenNumber } from '@/utils/format';
23
23
 
24
+ import NewModelBadge from './NewModelBadge';
25
+
24
26
  export const TAG_CLASSNAME = 'lobe-model-info-tags';
25
27
 
26
28
  const useStyles = createStyles(({ css, token }) => ({
@@ -179,6 +181,7 @@ interface ModelItemRenderProps extends ChatModelCard {
179
181
 
180
182
  export const ModelItemRender = memo<ModelItemRenderProps>(({ showInfoTag = true, ...model }) => {
181
183
  const { mobile } = useResponsive();
184
+
182
185
  return (
183
186
  <Flexbox
184
187
  align={'center'}
@@ -202,6 +205,7 @@ export const ModelItemRender = memo<ModelItemRenderProps>(({ showInfoTag = true,
202
205
  <Text style={mobile ? { maxWidth: '60vw', overflowX: 'auto', whiteSpace: 'nowrap' } : {}}>
203
206
  {model.displayName || model.id}
204
207
  </Text>
208
+ <NewModelBadge releasedAt={model.releasedAt} />
205
209
  </Flexbox>
206
210
  {showInfoTag && <ModelInfoTags {...model} />}
207
211
  </Flexbox>
@@ -64,6 +64,7 @@ import VolcengineProvider from './volcengine';
64
64
  import WenxinProvider from './wenxin';
65
65
  import XAIProvider from './xai';
66
66
  import XinferenceProvider from './xinference';
67
+ import ZenMuxProvider from './zenmux';
67
68
  import ZeroOneProvider from './zeroone';
68
69
  import ZhiPuProvider from './zhipu';
69
70
 
@@ -190,6 +191,7 @@ export const DEFAULT_MODEL_PROVIDER_LIST = [
190
191
  CometAPIProvider,
191
192
  VercelAIGatewayProvider,
192
193
  CerebrasProvider,
194
+ ZenMuxProvider,
193
195
  ];
194
196
 
195
197
  export const filterEnabledModels = (provider: ModelProviderCard) => {
@@ -266,5 +268,6 @@ export { default as VolcengineProviderCard } from './volcengine';
266
268
  export { default as WenxinProviderCard } from './wenxin';
267
269
  export { default as XAIProviderCard } from './xai';
268
270
  export { default as XinferenceProviderCard } from './xinference';
271
+ export { default as ZenMuxProviderCard } from './zenmux';
269
272
  export { default as ZeroOneProviderCard } from './zeroone';
270
273
  export { default as ZhiPuProviderCard } from './zhipu';
@@ -0,0 +1,21 @@
1
+ import { ModelProviderCard } from '@/types/llm';
2
+
3
+ const ZenMux: ModelProviderCard = {
4
+ chatModels: [],
5
+ checkModel: 'openai/gpt-5-nano',
6
+ description:
7
+ 'ZenMux 是一个统一的 AI 服务聚合平台,支持 OpenAI、Anthropic、Google VertexAI 等多种主流 AI 服务接口。提供灵活的路由能力,让您可以轻松切换和管理不同的 AI 模型。',
8
+ id: 'zenmux',
9
+ name: 'ZenMux',
10
+ settings: {
11
+ disableBrowserRequest: true, // CORS error
12
+ proxyUrl: {
13
+ placeholder: 'https://zenmux.ai',
14
+ },
15
+ sdkType: 'router',
16
+ showModelFetcher: true,
17
+ },
18
+ url: 'https://zenmux.ai',
19
+ };
20
+
21
+ export default ZenMux;
package/src/envs/llm.ts CHANGED
@@ -209,6 +209,9 @@ export const getLLMConfig = () => {
209
209
 
210
210
  ENABLED_CEREBRAS: z.boolean(),
211
211
  CEREBRAS_API_KEY: z.string().optional(),
212
+
213
+ ENABLED_ZENMUX: z.boolean(),
214
+ ZENMUX_API_KEY: z.string().optional(),
212
215
  },
213
216
  runtimeEnv: {
214
217
  API_KEY_SELECT_MODE: process.env.API_KEY_SELECT_MODE,
@@ -416,6 +419,9 @@ export const getLLMConfig = () => {
416
419
 
417
420
  ENABLED_CEREBRAS: !!process.env.CEREBRAS_API_KEY,
418
421
  CEREBRAS_API_KEY: process.env.CEREBRAS_API_KEY,
422
+
423
+ ENABLED_ZENMUX: !!process.env.ZENMUX_API_KEY,
424
+ ZENMUX_API_KEY: process.env.ZENMUX_API_KEY,
419
425
  },
420
426
  });
421
427
  };
@@ -37,6 +37,14 @@ export default {
37
37
  standard: '标准',
38
38
  },
39
39
  },
40
+ resolution: {
41
+ label: '分辨率',
42
+ options: {
43
+ '1K': '1K',
44
+ '2K': '2K',
45
+ '4K': '4K',
46
+ },
47
+ },
40
48
  seed: {
41
49
  label: '种子',
42
50
  random: '随机种子',
@@ -40,6 +40,7 @@ export type ProviderModelListItem = {
40
40
  parameters?: ModelParamsSchema;
41
41
  pricePerImage?: number;
42
42
  pricing?: Pricing;
43
+ releasedAt?: string;
43
44
  };
44
45
 
45
46
  type ModelNormalizer = (model: EnabledAiModel) => Promise<ProviderModelListItem>;
@@ -67,6 +68,7 @@ export const normalizeChatModel = (model: EnabledAiModel): ProviderModelListItem
67
68
  contextWindowTokens: model.contextWindowTokens,
68
69
  displayName: model.displayName ?? '',
69
70
  id: model.id,
71
+ releasedAt: model.releasedAt,
70
72
  });
71
73
 
72
74
  export const normalizeImageModel = async (
@@ -107,6 +109,7 @@ export const normalizeImageModel = async (
107
109
  contextWindowTokens: model.contextWindowTokens,
108
110
  displayName: model.displayName ?? '',
109
111
  id: model.id,
112
+ releasedAt: model.releasedAt,
110
113
  ...(parameters && { parameters }),
111
114
  ...(description && { description }),
112
115
  ...(pricing && { pricing }),
@@ -2,6 +2,7 @@ import { act, renderHook } from '@testing-library/react';
2
2
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
3
 
4
4
  import { aiChatService } from '@/services/aiChat';
5
+ import { getSessionStoreState } from '@/store/session';
5
6
 
6
7
  import { useChatStore } from '../../../../store';
7
8
  import { TEST_CONTENT, TEST_IDS, createMockMessage } from './fixtures';
@@ -30,6 +31,8 @@ beforeEach(() => {
30
31
  resetTestEnvironment();
31
32
  setupMockSelectors();
32
33
  spyOnMessageService();
34
+ const sessionStore = getSessionStoreState();
35
+ vi.spyOn(sessionStore, 'triggerSessionUpdate').mockResolvedValue(undefined);
33
36
 
34
37
  act(() => {
35
38
  useChatStore.setState({
@@ -530,6 +530,17 @@ export const streamingExecutor: StateCreator<
530
530
  get().completeOperation(reasoningOperationId);
531
531
  reasoningOperationId = undefined;
532
532
  }
533
+ break;
534
+ }
535
+
536
+ case 'stop': {
537
+ // Complete reasoning operation when receiving stop signal
538
+ if (!duration && reasoningOperationId) {
539
+ duration = Date.now() - thinkingStartAt;
540
+ get().completeOperation(reasoningOperationId);
541
+ reasoningOperationId = undefined;
542
+ }
543
+ break;
533
544
  }
534
545
  }
535
546
  },