@lobehub/lobehub 2.0.0-next.100 → 2.0.0-next.102

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 (40) hide show
  1. package/AGENTS.md +1 -0
  2. package/CHANGELOG.md +50 -0
  3. package/CLAUDE.md +1 -0
  4. package/changelog/v1.json +18 -0
  5. package/package.json +1 -1
  6. package/packages/model-bank/package.json +1 -0
  7. package/packages/model-bank/src/aiModels/aihubmix.ts +27 -0
  8. package/packages/model-bank/src/aiModels/google.ts +69 -10
  9. package/packages/model-bank/src/aiModels/index.ts +3 -0
  10. package/packages/model-bank/src/aiModels/infiniai.ts +5 -22
  11. package/packages/model-bank/src/aiModels/ollamacloud.ts +12 -0
  12. package/packages/model-bank/src/aiModels/siliconcloud.ts +0 -61
  13. package/packages/model-bank/src/aiModels/vertexai.ts +88 -1
  14. package/packages/model-bank/src/aiModels/zenmux.ts +1423 -0
  15. package/packages/model-bank/src/const/modelProvider.ts +1 -0
  16. package/packages/model-bank/src/standard-parameters/index.ts +9 -0
  17. package/packages/model-runtime/src/core/RouterRuntime/createRuntime.ts +42 -18
  18. package/packages/model-runtime/src/core/openaiCompatibleFactory/index.test.ts +2 -2
  19. package/packages/model-runtime/src/core/streams/bedrock/claude.ts +17 -3
  20. package/packages/model-runtime/src/core/streams/google/index.ts +7 -2
  21. package/packages/model-runtime/src/core/streams/openai/__snapshots__/responsesStream.test.ts.snap +166 -166
  22. package/packages/model-runtime/src/index.ts +1 -1
  23. package/packages/model-runtime/src/providers/anthropic/index.ts +1 -38
  24. package/packages/model-runtime/src/providers/anthropic/resolveCacheTTL.ts +44 -0
  25. package/packages/model-runtime/src/providers/bedrock/index.test.ts +127 -11
  26. package/packages/model-runtime/src/providers/bedrock/index.ts +47 -13
  27. package/packages/model-runtime/src/providers/google/createImage.ts +1 -0
  28. package/packages/model-runtime/src/providers/google/index.ts +11 -1
  29. package/packages/model-runtime/src/providers/zenmux/index.test.ts +320 -0
  30. package/packages/model-runtime/src/providers/zenmux/index.ts +84 -0
  31. package/packages/model-runtime/src/runtimeMap.ts +2 -0
  32. package/packages/types/src/user/settings/keyVaults.ts +1 -0
  33. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/ResolutionSelect.tsx +88 -0
  34. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/index.tsx +9 -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/chat/slices/aiChat/actions/__tests__/conversationLifecycle.test.ts +3 -0
  40. package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +11 -0
@@ -64,6 +64,7 @@ export enum ModelProvider {
64
64
  Wenxin = 'wenxin',
65
65
  XAI = 'xai',
66
66
  Xinference = 'xinference',
67
+ ZenMux = 'zenmux',
67
68
  ZeroOne = 'zeroone',
68
69
  ZhiPu = 'zhipu',
69
70
  }
@@ -149,6 +149,15 @@ export const ModelParamsMetaSchema = z.object({
149
149
  })
150
150
  .optional(),
151
151
 
152
+ resolution: z
153
+ .object({
154
+ default: z.string(),
155
+ description: z.string().optional(),
156
+ enum: z.array(z.string()),
157
+ type: z.literal('string').optional(),
158
+ })
159
+ .optional(),
160
+
152
161
  cfg: z
153
162
  .object({
154
163
  default: z.number(),
@@ -148,10 +148,9 @@ export const createRouterRuntime = ({
148
148
  }
149
149
 
150
150
  /**
151
- * TODO: 考虑添加缓存机制,避免重复创建相同配置的 runtimes
151
+ * Resolve routers configuration and validate
152
152
  */
153
- private async createRuntimesByRouters(model?: string): Promise<RuntimeItem[]> {
154
- // 动态获取 routers,支持传入 model
153
+ private async resolveRouters(model?: string): Promise<RouterInstance[]> {
155
154
  const resolvedRouters =
156
155
  typeof this._routers === 'function'
157
156
  ? await this._routers(this._options, { model })
@@ -161,6 +160,41 @@ export const createRouterRuntime = ({
161
160
  throw new Error('empty providers');
162
161
  }
163
162
 
163
+ return resolvedRouters;
164
+ }
165
+
166
+ /**
167
+ * Create runtime for inference requests (chat, generateObject, etc.)
168
+ * Finds the router that matches the model, or uses the last router as fallback
169
+ */
170
+ private async createRuntimeForInference(model: string): Promise<RuntimeItem> {
171
+ const resolvedRouters = await this.resolveRouters(model);
172
+
173
+ const matchedRouter =
174
+ resolvedRouters.find((router) => {
175
+ if (router.models && router.models.length > 0) {
176
+ return router.models.includes(model);
177
+ }
178
+ return false;
179
+ }) ?? resolvedRouters.at(-1)!;
180
+
181
+ const providerAI =
182
+ matchedRouter.runtime ?? baseRuntimeMap[matchedRouter.apiType] ?? LobeOpenAI;
183
+ const finalOptions = { ...this._params, ...this._options, ...matchedRouter.options };
184
+ const runtime: LobeRuntimeAI = new providerAI({ ...finalOptions, id: this._id });
185
+
186
+ return {
187
+ id: matchedRouter.apiType,
188
+ models: matchedRouter.models,
189
+ runtime,
190
+ };
191
+ }
192
+
193
+ /**
194
+ * Create all runtimes for listing models
195
+ */
196
+ private async createRuntimes(): Promise<RuntimeItem[]> {
197
+ const resolvedRouters = await this.resolveRouters();
164
198
  return resolvedRouters.map((router) => {
165
199
  const providerAI = router.runtime ?? baseRuntimeMap[router.apiType] ?? LobeOpenAI;
166
200
  const finalOptions = { ...this._params, ...this._options, ...router.options };
@@ -176,16 +210,8 @@ export const createRouterRuntime = ({
176
210
 
177
211
  // Check if it can match a specific model, otherwise default to using the last runtime
178
212
  async getRuntimeByModel(model: string) {
179
- const runtimes = await this.createRuntimesByRouters(model);
180
-
181
- for (const runtimeItem of runtimes) {
182
- const models = runtimeItem.models || [];
183
- if (models.includes(model)) {
184
- return runtimeItem.runtime;
185
- }
186
- }
187
-
188
- return runtimes.at(-1)!.runtime;
213
+ const runtimeItem = await this.createRuntimeForInference(model);
214
+ return runtimeItem.runtime;
189
215
  }
190
216
 
191
217
  async chat(payload: ChatStreamPayload, options?: ChatMethodOptions) {
@@ -222,9 +248,8 @@ export const createRouterRuntime = ({
222
248
 
223
249
  async models() {
224
250
  if (modelsOption && typeof modelsOption === 'function') {
225
- // 延迟创建 runtimes
226
- const runtimes = await this.createRuntimesByRouters();
227
- // 如果是函数式配置,使用最后一个运行时的客户端来调用函数
251
+ const runtimes = await this.createRuntimes();
252
+ // If it's a functional configuration, use the last runtime's client to call the function
228
253
  const lastRuntime = runtimes.at(-1)?.runtime;
229
254
  if (lastRuntime && 'client' in lastRuntime) {
230
255
  const modelList = await modelsOption({ client: (lastRuntime as any).client });
@@ -232,8 +257,7 @@ export const createRouterRuntime = ({
232
257
  }
233
258
  }
234
259
 
235
- // 延迟创建 runtimes
236
- const runtimes = await this.createRuntimesByRouters();
260
+ const runtimes = await this.createRuntimes();
237
261
  return runtimes.at(-1)?.runtime.models?.();
238
262
  }
239
263
 
@@ -426,7 +426,7 @@ describe('LobeOpenAICompatibleFactory', () => {
426
426
  'data: "Hello"\n\n',
427
427
  'id: a\n',
428
428
  'event: usage\n',
429
- 'data: {"inputTextTokens":5,"outputTextTokens":5,"totalInputTokens":5,"totalOutputTokens":5,"totalTokens":10}\n\n',
429
+ 'data: {"inputTextTokens":5,"outputTextTokens":5,"totalInputTokens":5,"totalOutputTokens":5,"totalTokens":10,"cost":0.000005}\n\n',
430
430
  'id: output_speed\n',
431
431
  'event: speed\n',
432
432
  expect.stringMatching(/^data: {.*"tps":.*,"ttft":.*}\n\n$/), // tps ttft should be calculated with elapsed time
@@ -601,7 +601,7 @@ describe('LobeOpenAICompatibleFactory', () => {
601
601
  signal: controller.signal,
602
602
  }),
603
603
  );
604
- });
604
+ }, 10000);
605
605
  });
606
606
 
607
607
  describe('Error', () => {
@@ -7,18 +7,32 @@ import {
7
7
  StreamContext,
8
8
  createCallbacksTransformer,
9
9
  createSSEProtocolTransformer,
10
+ createTokenSpeedCalculator,
10
11
  } from '../protocol';
11
12
  import { createBedrockStream } from './common';
12
13
 
13
14
  export const AWSBedrockClaudeStream = (
14
15
  res: InvokeModelWithResponseStreamResponse | ReadableStream,
15
- cb?: ChatStreamCallbacks,
16
+ options?: {
17
+ callbacks?: ChatStreamCallbacks;
18
+ inputStartAt?: number;
19
+ payload?: Parameters<typeof transformAnthropicStream>[2];
20
+ },
16
21
  ): ReadableStream<string> => {
17
22
  const streamStack: StreamContext = { id: 'chat_' + nanoid() };
18
23
 
19
24
  const stream = res instanceof ReadableStream ? res : createBedrockStream(res);
20
25
 
26
+ const transformWithPayload: typeof transformAnthropicStream = (chunk, ctx) =>
27
+ transformAnthropicStream(chunk, ctx, options?.payload);
28
+
21
29
  return stream
22
- .pipeThrough(createSSEProtocolTransformer(transformAnthropicStream, streamStack))
23
- .pipeThrough(createCallbacksTransformer(cb));
30
+ .pipeThrough(
31
+ createTokenSpeedCalculator(transformWithPayload, {
32
+ inputStartAt: options?.inputStartAt,
33
+ streamStack,
34
+ }),
35
+ )
36
+ .pipeThrough(createSSEProtocolTransformer((c) => c, streamStack))
37
+ .pipeThrough(createCallbacksTransformer(options?.callbacks));
24
38
  };
@@ -148,7 +148,8 @@ const transformGoogleGenerativeAIStream = (
148
148
 
149
149
  // Check for image data before handling finishReason
150
150
  if (Array.isArray(candidate.content?.parts) && candidate.content.parts.length > 0) {
151
- const part = candidate.content.parts[0];
151
+ // Filter out reasoning content and get first non-reasoning part
152
+ const part = candidate.content.parts.find((p: any) => !p.thought);
152
153
 
153
154
  if (part && part.inlineData && part.inlineData.data && part.inlineData.mimeType) {
154
155
  const imageChunk = {
@@ -182,7 +183,11 @@ const transformGoogleGenerativeAIStream = (
182
183
  ...usageChunks,
183
184
  ].filter(Boolean) as StreamProtocolChunk[];
184
185
  }
185
- return { data: candidate.finishReason, id: context?.id, type: 'stop' };
186
+ // 当有 finishReason 但没有 text 内容时,发送一个空的 text 块以停止加载动画
187
+ return [
188
+ { data: '', id: context?.id, type: 'text' },
189
+ { data: candidate.finishReason, id: context?.id, type: 'stop' },
190
+ ];
186
191
  }
187
192
 
188
193
  if (!!text?.trim()) return { data: text, id: context?.id, type: 'text' };