@lobehub/chat 1.133.2 → 1.133.4

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 (112) hide show
  1. package/.github/workflows/claude-translator.yml +2 -3
  2. package/.github/workflows/issue-auto-comments.yml +4 -9
  3. package/.github/workflows/issue-close-require.yml +3 -6
  4. package/CHANGELOG.md +66 -0
  5. package/changelog/v1.json +24 -0
  6. package/locales/ar/image.json +7 -0
  7. package/locales/ar/models.json +1 -1
  8. package/locales/bg-BG/image.json +7 -0
  9. package/locales/de-DE/image.json +7 -0
  10. package/locales/en-US/image.json +7 -0
  11. package/locales/es-ES/image.json +7 -0
  12. package/locales/es-ES/tool.json +1 -1
  13. package/locales/fa-IR/image.json +7 -0
  14. package/locales/fa-IR/models.json +1 -1
  15. package/locales/fr-FR/image.json +7 -0
  16. package/locales/fr-FR/models.json +1 -1
  17. package/locales/it-IT/image.json +7 -0
  18. package/locales/ja-JP/image.json +7 -0
  19. package/locales/ko-KR/image.json +7 -0
  20. package/locales/nl-NL/image.json +7 -0
  21. package/locales/pl-PL/image.json +7 -0
  22. package/locales/pt-BR/image.json +7 -0
  23. package/locales/ru-RU/image.json +7 -0
  24. package/locales/ru-RU/tool.json +1 -1
  25. package/locales/tr-TR/image.json +7 -0
  26. package/locales/tr-TR/models.json +1 -1
  27. package/locales/vi-VN/image.json +7 -0
  28. package/locales/zh-CN/image.json +7 -0
  29. package/locales/zh-TW/image.json +7 -0
  30. package/package.json +4 -5
  31. package/packages/const/package.json +4 -0
  32. package/packages/const/src/currency.ts +2 -0
  33. package/packages/const/src/index.ts +1 -0
  34. package/packages/model-bank/package.json +2 -1
  35. package/packages/model-bank/src/aiModels/aihubmix.ts +34 -1
  36. package/packages/model-bank/src/aiModels/anthropic.ts +3 -64
  37. package/packages/model-bank/src/aiModels/google.ts +6 -0
  38. package/packages/model-bank/src/aiModels/novita.ts +2 -2
  39. package/packages/model-bank/src/aiModels/openai.ts +6 -22
  40. package/packages/model-bank/src/aiModels/qwen.ts +21 -0
  41. package/packages/model-bank/src/aiModels/zhipu.ts +255 -62
  42. package/packages/model-bank/src/standard-parameters/index.ts +56 -46
  43. package/packages/model-runtime/package.json +1 -0
  44. package/packages/model-runtime/src/core/RouterRuntime/createRuntime.ts +4 -2
  45. package/packages/model-runtime/src/core/openaiCompatibleFactory/createImage.ts +12 -2
  46. package/packages/model-runtime/src/core/openaiCompatibleFactory/index.ts +16 -5
  47. package/packages/model-runtime/src/core/streams/anthropic.ts +25 -36
  48. package/packages/model-runtime/src/core/streams/google/google-ai.test.ts +1 -1
  49. package/packages/model-runtime/src/core/streams/google/index.ts +18 -42
  50. package/packages/model-runtime/src/core/streams/openai/openai.test.ts +7 -10
  51. package/packages/model-runtime/src/core/streams/openai/openai.ts +14 -11
  52. package/packages/model-runtime/src/core/streams/openai/responsesStream.ts +11 -5
  53. package/packages/model-runtime/src/core/streams/protocol.ts +25 -6
  54. package/packages/model-runtime/src/core/streams/qwen.ts +2 -2
  55. package/packages/model-runtime/src/core/streams/spark.ts +3 -3
  56. package/packages/model-runtime/src/core/streams/vertex-ai.test.ts +2 -2
  57. package/packages/model-runtime/src/core/streams/vertex-ai.ts +14 -23
  58. package/packages/model-runtime/src/core/usageConverters/anthropic.test.ts +99 -0
  59. package/packages/model-runtime/src/core/usageConverters/anthropic.ts +73 -0
  60. package/packages/model-runtime/src/core/usageConverters/google-ai.test.ts +88 -0
  61. package/packages/model-runtime/src/core/usageConverters/google-ai.ts +55 -0
  62. package/packages/model-runtime/src/core/usageConverters/index.ts +4 -0
  63. package/packages/model-runtime/src/core/usageConverters/openai.test.ts +429 -0
  64. package/packages/model-runtime/src/core/usageConverters/openai.ts +152 -0
  65. package/packages/model-runtime/src/core/usageConverters/utils/computeChatCost.test.ts +455 -0
  66. package/packages/model-runtime/src/core/usageConverters/utils/computeChatCost.ts +293 -0
  67. package/packages/model-runtime/src/core/usageConverters/utils/computeImageCost.test.ts +47 -0
  68. package/packages/model-runtime/src/core/usageConverters/utils/computeImageCost.ts +121 -0
  69. package/packages/model-runtime/src/core/usageConverters/utils/index.ts +11 -0
  70. package/packages/model-runtime/src/core/usageConverters/utils/withUsageCost.ts +19 -0
  71. package/packages/model-runtime/src/index.ts +2 -0
  72. package/packages/model-runtime/src/providers/anthropic/index.test.ts +0 -12
  73. package/packages/model-runtime/src/providers/anthropic/index.ts +48 -1
  74. package/packages/model-runtime/src/providers/google/createImage.ts +11 -2
  75. package/packages/model-runtime/src/providers/google/index.ts +8 -1
  76. package/packages/model-runtime/src/providers/novita/index.ts +2 -1
  77. package/packages/model-runtime/src/providers/novita/type.ts +4 -0
  78. package/packages/model-runtime/src/providers/ollamacloud/index.ts +1 -1
  79. package/packages/model-runtime/src/providers/openai/__snapshots__/index.test.ts.snap +7 -0
  80. package/packages/model-runtime/src/providers/openrouter/index.ts +11 -4
  81. package/packages/model-runtime/src/providers/zhipu/index.ts +3 -1
  82. package/packages/model-runtime/src/types/chat.ts +5 -3
  83. package/packages/model-runtime/src/types/image.ts +20 -9
  84. package/packages/model-runtime/src/utils/getModelPricing.ts +36 -0
  85. package/packages/obervability-otel/package.json +2 -2
  86. package/packages/ssrf-safe-fetch/index.test.ts +343 -0
  87. package/packages/ssrf-safe-fetch/index.ts +37 -0
  88. package/packages/ssrf-safe-fetch/package.json +17 -0
  89. package/packages/ssrf-safe-fetch/vitest.config.mts +10 -0
  90. package/packages/types/src/message/base.ts +43 -17
  91. package/packages/utils/package.json +0 -1
  92. package/packages/utils/src/client/apiKeyManager.test.ts +70 -0
  93. package/packages/utils/src/client/apiKeyManager.ts +41 -0
  94. package/packages/utils/src/client/index.ts +2 -0
  95. package/packages/utils/src/fetch/fetchSSE.ts +4 -4
  96. package/packages/utils/src/index.ts +1 -0
  97. package/packages/utils/src/toolManifest.ts +2 -1
  98. package/src/app/(backend)/webapi/proxy/route.ts +2 -13
  99. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatMinimap/index.tsx +51 -23
  100. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/QualitySelect.tsx +23 -0
  101. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/index.tsx +9 -0
  102. package/src/config/modelProviders/anthropic.ts +0 -30
  103. package/src/config/modelProviders/ollamacloud.ts +1 -0
  104. package/src/config/modelProviders/zhipu.ts +4 -21
  105. package/src/features/Conversation/Extras/Usage/UsageDetail/tokens.test.ts +13 -13
  106. package/src/features/Conversation/Extras/Usage/UsageDetail/tokens.ts +1 -1
  107. package/src/features/Conversation/components/WideScreenContainer/index.tsx +3 -0
  108. package/src/locales/default/image.ts +7 -0
  109. package/src/server/modules/EdgeConfig/index.ts +1 -1
  110. package/src/server/routers/async/image.ts +9 -1
  111. package/src/services/_auth.ts +12 -12
  112. package/src/services/chat/contextEngineering.ts +2 -3
@@ -1,3 +1,4 @@
1
+ /* eslint-disable sort-keys-fix/sort-keys-fix, typescript-sort-keys/interface */
1
2
  import type { Simplify } from 'type-fest';
2
3
  import { z } from 'zod';
3
4
 
@@ -53,36 +54,14 @@ export const CHAT_MODEL_IMAGE_GENERATION_PARAMS: ModelParamsSchema = {
53
54
 
54
55
  // 定义顶层的元规范 - 平铺结构
55
56
  export const ModelParamsMetaSchema = z.object({
56
- aspectRatio: z
57
- .object({
58
- default: z.string(),
59
- description: z.string().optional(),
60
- enum: z.array(z.string()),
61
- type: z.literal('string').optional(),
62
- })
63
- .optional(),
64
-
65
- cfg: z
66
- .object({
67
- default: z.number(),
68
- description: z.string().optional(),
69
- max: z.number(),
70
- min: z.number(),
71
- step: z.number(),
72
- type: z.literal('number').optional(),
73
- })
74
- .optional(),
75
-
76
- height: z
77
- .object({
78
- default: z.number(),
79
- description: z.string().optional(),
80
- max: z.number(),
81
- min: z.number(),
82
- step: z.number().optional().default(1),
83
- type: z.literal('number').optional(),
84
- })
85
- .optional(),
57
+ /**
58
+ * Prompt 是唯一一个每个模型都有的参数
59
+ */
60
+ prompt: z.object({
61
+ default: z.string().optional().default(''),
62
+ description: z.string().optional(),
63
+ type: z.literal('string').optional(),
64
+ }),
86
65
 
87
66
  imageUrl: z
88
67
  .object({
@@ -106,22 +85,25 @@ export const ModelParamsMetaSchema = z.object({
106
85
  })
107
86
  .optional(),
108
87
 
109
- /**
110
- * Prompt 是唯一一个每个模型都有的参数
111
- */
112
- prompt: z.object({
113
- default: z.string().optional().default(''),
114
- description: z.string().optional(),
115
- type: z.literal('string').optional(),
116
- }),
88
+ width: z
89
+ .object({
90
+ default: z.number(),
91
+ description: z.string().optional(),
92
+ max: z.number(),
93
+ min: z.number(),
94
+ step: z.number().optional().default(1),
95
+ type: z.literal('number').optional(),
96
+ })
97
+ .optional(),
117
98
 
118
- seed: z
99
+ height: z
119
100
  .object({
120
- default: z.number().nullable().default(null),
101
+ default: z.number(),
121
102
  description: z.string().optional(),
122
- max: z.number().optional().default(MAX_SEED),
123
- min: z.number().optional().default(0),
124
- type: z.tuple([z.literal('number'), z.literal('null')]).optional(),
103
+ max: z.number(),
104
+ min: z.number(),
105
+ step: z.number().optional().default(1),
106
+ type: z.literal('number').optional(),
125
107
  })
126
108
  .optional(),
127
109
 
@@ -134,18 +116,27 @@ export const ModelParamsMetaSchema = z.object({
134
116
  })
135
117
  .optional(),
136
118
 
137
- steps: z
119
+ aspectRatio: z
120
+ .object({
121
+ default: z.string(),
122
+ description: z.string().optional(),
123
+ enum: z.array(z.string()),
124
+ type: z.literal('string').optional(),
125
+ })
126
+ .optional(),
127
+
128
+ cfg: z
138
129
  .object({
139
130
  default: z.number(),
140
131
  description: z.string().optional(),
141
132
  max: z.number(),
142
133
  min: z.number(),
143
- step: z.number().optional().default(1),
134
+ step: z.number(),
144
135
  type: z.literal('number').optional(),
145
136
  })
146
137
  .optional(),
147
138
 
148
- width: z
139
+ steps: z
149
140
  .object({
150
141
  default: z.number(),
151
142
  description: z.string().optional(),
@@ -155,6 +146,25 @@ export const ModelParamsMetaSchema = z.object({
155
146
  type: z.literal('number').optional(),
156
147
  })
157
148
  .optional(),
149
+
150
+ quality: z
151
+ .object({
152
+ default: z.string(),
153
+ description: z.string().optional(),
154
+ enum: z.array(z.string()),
155
+ type: z.literal('string').optional(),
156
+ })
157
+ .optional(),
158
+
159
+ seed: z
160
+ .object({
161
+ default: z.number().nullable().default(null),
162
+ description: z.string().optional(),
163
+ max: z.number().optional().default(MAX_SEED),
164
+ min: z.number().optional().default(0),
165
+ type: z.tuple([z.literal('number'), z.literal('null')]).optional(),
166
+ })
167
+ .optional(),
158
168
  });
159
169
  // 导出推断出的类型,供定义对象使用
160
170
  export type ModelParamsSchema = z.input<typeof ModelParamsMetaSchema>;
@@ -13,6 +13,7 @@
13
13
  },
14
14
  "dependencies": {
15
15
  "@aws-sdk/client-bedrock-runtime": "^3.862.0",
16
+ "@lobechat/const": "workspace:*",
16
17
  "@lobechat/types": "workspace:*",
17
18
  "@lobechat/utils": "workspace:*",
18
19
  "debug": "^4.4.1",
@@ -129,7 +129,7 @@ export const createRouterRuntime = ({
129
129
  ...params
130
130
  }: CreateRouterRuntimeOptions) => {
131
131
  return class UniformRuntime implements LobeRuntimeAI {
132
- private _options: ClientOptions & Record<string, any>;
132
+ public _options: ClientOptions & Record<string, any>;
133
133
  private _routers: Routers;
134
134
  private _params: any;
135
135
  private _id: string;
@@ -148,7 +148,7 @@ export const createRouterRuntime = ({
148
148
  }
149
149
 
150
150
  /**
151
- * TODO: routers 如果是静态对象,可以提前生成 runtimes, 避免运行时生成开销
151
+ * TODO: 考虑添加缓存机制,避免重复创建相同配置的 runtimes
152
152
  */
153
153
  private async createRuntimesByRouters(model?: string): Promise<RuntimeItem[]> {
154
154
  // 动态获取 routers,支持传入 model
@@ -181,9 +181,11 @@ export const createRouterRuntime = ({
181
181
  for (const runtimeItem of runtimes) {
182
182
  const models = runtimeItem.models || [];
183
183
  if (models.includes(model)) {
184
+ console.log(`get runtime ${runtimeItem.id} ${model}`);
184
185
  return runtimeItem.runtime;
185
186
  }
186
187
  }
188
+
187
189
  return runtimes.at(-1)!.runtime;
188
190
  }
189
191
 
@@ -3,9 +3,11 @@ import { RuntimeImageGenParamsValue } from 'model-bank';
3
3
  import OpenAI from 'openai';
4
4
 
5
5
  import { CreateImagePayload, CreateImageResponse } from '../../types/image';
6
+ import { getModelPricing } from '../../utils/getModelPricing';
6
7
  import { imageUrlToBase64 } from '../../utils/imageToBase64';
7
8
  import { convertImageUrlToFile } from '../../utils/openaiHelpers';
8
9
  import { parseDataUri } from '../../utils/uriParser';
10
+ import { convertOpenAIImageUsage } from '../usageConverters/openai';
9
11
 
10
12
  const log = createDebug('lobe-image:openai-compatible');
11
13
 
@@ -15,6 +17,7 @@ const log = createDebug('lobe-image:openai-compatible');
15
17
  async function generateByImageMode(
16
18
  client: OpenAI,
17
19
  payload: CreateImagePayload,
20
+ provider: string,
18
21
  ): Promise<CreateImageResponse> {
19
22
  const { model, params } = payload;
20
23
 
@@ -112,8 +115,15 @@ async function generateByImageMode(
112
115
  throw new Error('Invalid image response: missing both b64_json and url fields');
113
116
  }
114
117
 
118
+ log('provider: %s', provider);
119
+
115
120
  return {
116
121
  imageUrl,
122
+ ...(img.usage
123
+ ? {
124
+ modelUsage: convertOpenAIImageUsage(img.usage, await getModelPricing(model, provider)),
125
+ }
126
+ : {}),
117
127
  };
118
128
  }
119
129
 
@@ -218,7 +228,7 @@ async function generateByChatModel(
218
228
  export async function createOpenAICompatibleImage(
219
229
  client: OpenAI,
220
230
  payload: CreateImagePayload,
221
- _provider: string, // eslint-disable-line @typescript-eslint/no-unused-vars
231
+ provider: string, // eslint-disable-line @typescript-eslint/no-unused-vars
222
232
  ): Promise<CreateImageResponse> {
223
233
  try {
224
234
  const { model } = payload;
@@ -229,7 +239,7 @@ export async function createOpenAICompatibleImage(
229
239
  }
230
240
 
231
241
  // Default to traditional images API
232
- return await generateByImageMode(client, payload);
242
+ return await generateByImageMode(client, payload, provider);
233
243
  } catch (error) {
234
244
  const err = error as Error;
235
245
  log('Error in createImage: %O', err);
@@ -27,6 +27,7 @@ import { AgentRuntimeError } from '../../utils/createError';
27
27
  import { debugResponse, debugStream } from '../../utils/debugStream';
28
28
  import { desensitizeUrl } from '../../utils/desensitizeUrl';
29
29
  import { getModelPropertyWithFallback } from '../../utils/getFallbackModelProperty';
30
+ import { getModelPricing } from '../../utils/getModelPricing';
30
31
  import { handleOpenAIError } from '../../utils/handleOpenAIError';
31
32
  import { convertOpenAIMessages, convertOpenAIResponseInputs } from '../../utils/openaiHelpers';
32
33
  import { postProcessModelList } from '../../utils/postProcessModelList';
@@ -228,7 +229,11 @@ export const createOpenAICompatibleRuntime = <T extends Record<string, any> = an
228
229
  const streamOptions: OpenAIStreamOptions = {
229
230
  bizErrorTypeTransformer: chatCompletion?.handleStreamBizErrorType,
230
231
  callbacks: options?.callback,
231
- provider: this.id,
232
+ payload: {
233
+ model: payload.model,
234
+ pricing: await getModelPricing(payload.model, this.id),
235
+ provider: this.id,
236
+ },
232
237
  };
233
238
 
234
239
  if (customClient?.createChatCompletionStream) {
@@ -276,7 +281,10 @@ export const createOpenAICompatibleRuntime = <T extends Record<string, any> = an
276
281
  callbacks: streamOptions.callbacks,
277
282
  inputStartAt,
278
283
  })
279
- : OpenAIStream(prod, { ...streamOptions, inputStartAt }),
284
+ : OpenAIStream(prod, {
285
+ ...streamOptions,
286
+ inputStartAt,
287
+ }),
280
288
  {
281
289
  headers: options?.headers,
282
290
  },
@@ -320,7 +328,7 @@ export const createOpenAICompatibleRuntime = <T extends Record<string, any> = an
320
328
  }
321
329
 
322
330
  // Use the new createOpenAICompatibleImage function
323
- return createOpenAICompatibleImage(this.client, payload, provider);
331
+ return createOpenAICompatibleImage(this.client, payload, this.id);
324
332
  }
325
333
 
326
334
  async models() {
@@ -453,7 +461,6 @@ export const createOpenAICompatibleRuntime = <T extends Record<string, any> = an
453
461
  headers: options?.headers,
454
462
  signal: options?.signal,
455
463
  });
456
-
457
464
  return mp3.arrayBuffer();
458
465
  } catch (error) {
459
466
  throw this.handleError(error);
@@ -585,7 +592,11 @@ export const createOpenAICompatibleRuntime = <T extends Record<string, any> = an
585
592
  const streamOptions: OpenAIStreamOptions = {
586
593
  bizErrorTypeTransformer: chatCompletion?.handleStreamBizErrorType,
587
594
  callbacks: options?.callback,
588
- provider: this.id,
595
+ payload: {
596
+ model: payload.model,
597
+ pricing: await getModelPricing(payload.model, this.id),
598
+ provider: this.id,
599
+ },
589
600
  };
590
601
 
591
602
  if (isStreaming) {
@@ -1,9 +1,12 @@
1
1
  import Anthropic from '@anthropic-ai/sdk';
2
2
  import type { Stream } from '@anthropic-ai/sdk/streaming';
3
- import { ChatCitationItem, ModelTokensUsage } from '@lobechat/types';
3
+
4
+ import { ChatCitationItem } from '@/types/message';
4
5
 
5
6
  import { ChatStreamCallbacks } from '../../types';
7
+ import { convertAnthropicUsage } from '../usageConverters';
6
8
  import {
9
+ ChatPayloadForTransformStream,
7
10
  StreamContext,
8
11
  StreamProtocolChunk,
9
12
  StreamProtocolToolCallChunk,
@@ -17,31 +20,20 @@ import {
17
20
  export const transformAnthropicStream = (
18
21
  chunk: Anthropic.MessageStreamEvent,
19
22
  context: StreamContext,
23
+ payload?: ChatPayloadForTransformStream,
20
24
  ): StreamProtocolChunk | StreamProtocolChunk[] => {
21
25
  // maybe need another structure to add support for multiple choices
22
26
  switch (chunk.type) {
23
27
  case 'message_start': {
24
28
  context.id = chunk.message.id;
25
29
  context.returnedCitationArray = [];
26
- let totalInputTokens = chunk.message.usage?.input_tokens;
27
-
28
- if (
29
- chunk.message.usage?.cache_creation_input_tokens ||
30
- chunk.message.usage?.cache_read_input_tokens
31
- ) {
32
- totalInputTokens =
33
- chunk.message.usage?.input_tokens +
34
- (chunk.message.usage.cache_creation_input_tokens || 0) +
35
- (chunk.message.usage.cache_read_input_tokens || 0);
36
- }
30
+ const usage = convertAnthropicUsage(chunk, undefined, payload);
37
31
 
38
- context.usage = {
39
- inputCacheMissTokens: chunk.message.usage?.input_tokens,
40
- inputCachedTokens: chunk.message.usage?.cache_read_input_tokens || undefined,
41
- inputWriteCacheTokens: chunk.message.usage?.cache_creation_input_tokens || undefined,
42
- totalInputTokens,
43
- totalOutputTokens: chunk.message.usage?.output_tokens,
44
- };
32
+ if (usage) {
33
+ context.usage = usage;
34
+ } else {
35
+ delete context.usage;
36
+ }
45
37
 
46
38
  return { data: chunk.message, id: chunk.message.id, type: 'data' };
47
39
  }
@@ -193,26 +185,19 @@ export const transformAnthropicStream = (
193
185
  }
194
186
 
195
187
  case 'message_delta': {
196
- const totalOutputTokens =
197
- chunk.usage?.output_tokens + (context.usage?.totalOutputTokens || 0);
198
- const totalInputTokens = context.usage?.totalInputTokens || 0;
199
- const totalTokens = totalInputTokens + totalOutputTokens;
188
+ const aggregatedUsage = convertAnthropicUsage(chunk, context.usage, payload);
189
+
190
+ if (aggregatedUsage) {
191
+ context.usage = aggregatedUsage;
192
+ }
200
193
 
201
- if (totalTokens > 0) {
194
+ if (aggregatedUsage && (aggregatedUsage.totalTokens ?? 0) > 0) {
202
195
  return [
203
196
  { data: chunk.delta.stop_reason, id: context.id, type: 'stop' },
204
- {
205
- data: {
206
- ...context.usage,
207
- totalInputTokens,
208
- totalOutputTokens,
209
- totalTokens,
210
- } as ModelTokensUsage,
211
- id: context.id,
212
- type: 'usage',
213
- },
197
+ { data: aggregatedUsage, id: context.id, type: 'usage' },
214
198
  ];
215
199
  }
200
+
216
201
  return { data: chunk.delta.stop_reason, id: context.id, type: 'stop' };
217
202
  }
218
203
 
@@ -241,20 +226,24 @@ export interface AnthropicStreamOptions {
241
226
  callbacks?: ChatStreamCallbacks;
242
227
  enableStreaming?: boolean; // 选择 TPS 计算方式(非流式时传 false)
243
228
  inputStartAt?: number;
229
+ payload?: ChatPayloadForTransformStream;
244
230
  }
245
231
 
246
232
  export const AnthropicStream = (
247
233
  stream: Stream<Anthropic.MessageStreamEvent> | ReadableStream,
248
- { callbacks, inputStartAt, enableStreaming = true }: AnthropicStreamOptions = {},
234
+ { callbacks, inputStartAt, enableStreaming = true, payload }: AnthropicStreamOptions = {},
249
235
  ) => {
250
236
  const streamStack: StreamContext = { id: '' };
251
237
 
252
238
  const readableStream =
253
239
  stream instanceof ReadableStream ? stream : convertIterableToStream(stream);
254
240
 
241
+ const transformWithPayload: typeof transformAnthropicStream = (chunk, ctx) =>
242
+ transformAnthropicStream(chunk, ctx, payload);
243
+
255
244
  return readableStream
256
245
  .pipeThrough(
257
- createTokenSpeedCalculator(transformAnthropicStream, {
246
+ createTokenSpeedCalculator(transformWithPayload, {
258
247
  enableStreaming: enableStreaming,
259
248
  inputStartAt,
260
249
  streamStack,
@@ -281,7 +281,7 @@ describe('GoogleGenerativeAIStream', () => {
281
281
  // usage
282
282
  'id: chat_1\n',
283
283
  'event: usage\n',
284
- `data: {"inputCachedTokens":14286,"inputTextTokens":15725,"outputImageTokens":0,"outputTextTokens":1053,"totalInputTokens":15725,"totalOutputTokens":1053,"totalTokens":16778}\n\n`,
284
+ `data: {"inputCacheMissTokens":1439,"inputCachedTokens":14286,"inputTextTokens":15725,"outputImageTokens":0,"outputTextTokens":1053,"totalInputTokens":15725,"totalOutputTokens":1053,"totalTokens":16778}\n\n`,
285
285
  ]);
286
286
  });
287
287
 
@@ -1,9 +1,11 @@
1
1
  import { GenerateContentResponse } from '@google/genai';
2
- import { GroundingSearch, ModelTokensUsage } from '@lobechat/types';
2
+ import { GroundingSearch } from '@lobechat/types';
3
3
 
4
4
  import { ChatStreamCallbacks } from '../../../types';
5
5
  import { nanoid } from '../../../utils/uuid';
6
+ import { convertGoogleAIUsage } from '../../usageConverters/google-ai';
6
7
  import {
8
+ ChatPayloadForTransformStream,
7
9
  StreamContext,
8
10
  StreamProtocolChunk,
9
11
  StreamToolCallChunkData,
@@ -28,6 +30,7 @@ const getBlockReasonMessage = (blockReason: string): string => {
28
30
  const transformGoogleGenerativeAIStream = (
29
31
  chunk: GenerateContentResponse,
30
32
  context: StreamContext,
33
+ payload?: ChatPayloadForTransformStream,
31
34
  ): StreamProtocolChunk | StreamProtocolChunk[] => {
32
35
  // Handle injected internal error marker to pass through detailed error info
33
36
  if ((chunk as any)?.[LOBE_ERROR_KEY]) {
@@ -60,46 +63,15 @@ const transformGoogleGenerativeAIStream = (
60
63
 
61
64
  // maybe need another structure to add support for multiple choices
62
65
  const candidate = chunk.candidates?.[0];
63
- const usage = chunk.usageMetadata;
66
+ const { usageMetadata } = chunk;
64
67
  const usageChunks: StreamProtocolChunk[] = [];
65
- if (candidate?.finishReason && usage) {
66
- // totalTokenCount = promptTokenCount + candidatesTokenCount + thoughtsTokenCount
67
- const reasoningTokens = usage.thoughtsTokenCount;
68
-
69
- const candidatesDetails = usage.candidatesTokensDetails;
70
- const candidatesTotal =
71
- usage.candidatesTokenCount ??
72
- candidatesDetails?.reduce((s: number, i: any) => s + (i?.tokenCount ?? 0), 0) ??
73
- 0;
74
-
75
- const outputImageTokens =
76
- candidatesDetails?.find((i: any) => i.modality === 'IMAGE')?.tokenCount ?? 0;
77
- const outputTextTokens =
78
- candidatesDetails?.find((i: any) => i.modality === 'TEXT')?.tokenCount ??
79
- Math.max(0, candidatesTotal - outputImageTokens);
80
-
81
- const totalOutputTokens = candidatesTotal + (reasoningTokens ?? 0);
82
-
83
- usageChunks.push(
84
- { data: candidate.finishReason, id: context?.id, type: 'stop' },
85
- {
86
- data: {
87
- inputCachedTokens: usage.cachedContentTokenCount,
88
- inputImageTokens: usage.promptTokensDetails?.find((i) => i.modality === 'IMAGE')
89
- ?.tokenCount,
90
- inputTextTokens: usage.promptTokensDetails?.find((i) => i.modality === 'TEXT')
91
- ?.tokenCount,
92
- outputImageTokens,
93
- outputReasoningTokens: reasoningTokens,
94
- outputTextTokens,
95
- totalInputTokens: usage.promptTokenCount,
96
- totalOutputTokens,
97
- totalTokens: usage.totalTokenCount,
98
- } as ModelTokensUsage,
99
- id: context?.id,
100
- type: 'usage',
101
- },
102
- );
68
+ if (candidate?.finishReason && usageMetadata) {
69
+ usageChunks.push({ data: candidate.finishReason, id: context?.id, type: 'stop' });
70
+
71
+ const convertedUsage = convertGoogleAIUsage(usageMetadata, payload?.pricing);
72
+ if (convertedUsage) {
73
+ usageChunks.push({ data: convertedUsage, id: context?.id, type: 'usage' });
74
+ }
103
75
  }
104
76
 
105
77
  const functionCalls = chunk.functionCalls;
@@ -213,17 +185,21 @@ export interface GoogleAIStreamOptions {
213
185
  callbacks?: ChatStreamCallbacks;
214
186
  enableStreaming?: boolean; // 选择 TPS 计算方式(非流式时传 false)
215
187
  inputStartAt?: number;
188
+ payload?: ChatPayloadForTransformStream;
216
189
  }
217
190
 
218
191
  export const GoogleGenerativeAIStream = (
219
192
  rawStream: ReadableStream<GenerateContentResponse>,
220
- { callbacks, inputStartAt, enableStreaming = true }: GoogleAIStreamOptions = {},
193
+ { callbacks, inputStartAt, enableStreaming = true, payload }: GoogleAIStreamOptions = {},
221
194
  ) => {
222
195
  const streamStack: StreamContext = { id: 'chat_' + nanoid() };
223
196
 
197
+ const transformWithPayload: typeof transformGoogleGenerativeAIStream = (chunk, ctx) =>
198
+ transformGoogleGenerativeAIStream(chunk, ctx, payload);
199
+
224
200
  return rawStream
225
201
  .pipeThrough(
226
- createTokenSpeedCalculator(transformGoogleGenerativeAIStream, {
202
+ createTokenSpeedCalculator(transformWithPayload, {
227
203
  enableStreaming: enableStreaming,
228
204
  inputStartAt,
229
205
  streamStack,
@@ -360,14 +360,9 @@ describe('OpenAIStream', () => {
360
360
  }
361
361
 
362
362
  expect(chunks).toEqual(
363
- [
364
- 'id: 1',
365
- 'event: text',
366
- `data: "Hello"\n`,
367
- 'id: 1',
368
- 'event: data',
369
- `data: {"id":"1"}\n`,
370
- ].map((i) => `${i}\n`),
363
+ ['id: 1', 'event: text', `data: "Hello"\n`, 'id: 1', 'event: data', `data: {"id":"1"}\n`].map(
364
+ (i) => `${i}\n`,
365
+ ),
371
366
  );
372
367
  });
373
368
 
@@ -413,7 +408,9 @@ describe('OpenAIStream', () => {
413
408
 
414
409
  const protocolStream = OpenAIStream(mockOpenAIStream, {
415
410
  bizErrorTypeTransformer: () => AgentRuntimeErrorType.PermissionDenied,
416
- provider: 'grok',
411
+ payload: {
412
+ provider: 'grok',
413
+ },
417
414
  });
418
415
 
419
416
  const decoder = new TextDecoder();
@@ -2481,4 +2478,4 @@ describe('OpenAIStream', () => {
2481
2478
  `data: "${base64_2}"\n\n`,
2482
2479
  ]);
2483
2480
  });
2484
- });
2481
+ });
@@ -4,8 +4,9 @@ import type { Stream } from 'openai/streaming';
4
4
 
5
5
  import { ChatStreamCallbacks } from '../../../types';
6
6
  import { AgentRuntimeErrorType, ILobeAgentRuntimeErrorType } from '../../../types/error';
7
- import { convertUsage } from '../../../utils/usageConverter';
7
+ import { convertOpenAIUsage } from '../../usageConverters';
8
8
  import {
9
+ ChatPayloadForTransformStream,
9
10
  FIRST_CHUNK_ERROR_KEY,
10
11
  StreamContext,
11
12
  StreamProtocolChunk,
@@ -44,7 +45,7 @@ const processMarkdownBase64Images = (text: string): { cleanedText: string; urls:
44
45
  const transformOpenAIStream = (
45
46
  chunk: OpenAI.ChatCompletionChunk,
46
47
  streamContext: StreamContext,
47
- provider?: string,
48
+ payload?: ChatPayloadForTransformStream,
48
49
  ): StreamProtocolChunk | StreamProtocolChunk[] => {
49
50
  // handle the first chunk error
50
51
  if (FIRST_CHUNK_ERROR_KEY in chunk) {
@@ -75,7 +76,7 @@ const transformOpenAIStream = (
75
76
  if (!Array.isArray(chunk.choices) || chunk.choices.length === 0) {
76
77
  if (chunk.usage) {
77
78
  const usage = chunk.usage;
78
- return { data: convertUsage(usage, provider), id: chunk.id, type: 'usage' };
79
+ return { data: convertOpenAIUsage(usage, payload), id: chunk.id, type: 'usage' };
79
80
  }
80
81
 
81
82
  return { data: chunk, id: chunk.id, type: 'data' };
@@ -232,7 +233,7 @@ const transformOpenAIStream = (
232
233
 
233
234
  if (chunk.usage) {
234
235
  const usage = chunk.usage;
235
- return { data: convertUsage(usage, provider), id: chunk.id, type: 'usage' };
236
+ return { data: convertOpenAIUsage(usage, payload), id: chunk.id, type: 'usage' };
236
237
  }
237
238
 
238
239
  // xAI Live Search 功能返回引用源
@@ -312,7 +313,7 @@ const transformOpenAIStream = (
312
313
  // 如果 content 是空字符串但 chunk 带有 usage,则优先返回 usage(例如 Gemini image-preview 最终会在单独的 chunk 中返回 usage)
313
314
  if (content === '' && chunk.usage) {
314
315
  const usage = chunk.usage;
315
- return { data: convertUsage(usage, provider), id: chunk.id, type: 'usage' };
316
+ return { data: convertOpenAIUsage(usage, payload), id: chunk.id, type: 'usage' };
316
317
  }
317
318
 
318
319
  // 判断是否有 citations 内容,更新 returnedCitation 状态
@@ -387,7 +388,7 @@ const transformOpenAIStream = (
387
388
  // litellm 的返回结果中,存在 delta 为空,但是有 usage 的情况
388
389
  if (chunk.usage) {
389
390
  const usage = chunk.usage;
390
- return { data: convertUsage(usage, provider), id: chunk.id, type: 'usage' };
391
+ return { data: convertOpenAIUsage(usage, payload), id: chunk.id, type: 'usage' };
391
392
  }
392
393
 
393
394
  // 其余情况下,返回 delta 和 index
@@ -426,23 +427,25 @@ export interface OpenAIStreamOptions {
426
427
  callbacks?: ChatStreamCallbacks;
427
428
  enableStreaming?: boolean; // 选择 TPS 计算方式(非流式时传 false)
428
429
  inputStartAt?: number;
429
- provider?: string;
430
+ payload?: ChatPayloadForTransformStream;
430
431
  }
431
432
 
432
433
  export const OpenAIStream = (
433
434
  stream: Stream<OpenAI.ChatCompletionChunk> | ReadableStream,
434
435
  {
435
436
  callbacks,
436
- provider,
437
437
  bizErrorTypeTransformer,
438
+ payload,
438
439
  inputStartAt,
439
440
  enableStreaming = true,
440
441
  }: OpenAIStreamOptions = {},
441
442
  ) => {
442
- const streamStack: StreamContext = { id: '' };
443
+ const streamStack: StreamContext = {
444
+ id: '',
445
+ };
443
446
 
444
447
  const transformWithProvider = (chunk: OpenAI.ChatCompletionChunk, streamContext: StreamContext) =>
445
- transformOpenAIStream(chunk, streamContext, provider);
448
+ transformOpenAIStream(chunk, streamContext, payload);
446
449
 
447
450
  const readableStream =
448
451
  stream instanceof ReadableStream ? stream : convertIterableToStream(stream);
@@ -452,7 +455,7 @@ export const OpenAIStream = (
452
455
  // 1. handle the first error if exist
453
456
  // provider like huggingface or minimax will return error in the stream,
454
457
  // so in the first Transformer, we need to handle the error
455
- .pipeThrough(createFirstErrorHandleTransformer(bizErrorTypeTransformer, provider))
458
+ .pipeThrough(createFirstErrorHandleTransformer(bizErrorTypeTransformer, payload?.provider))
456
459
  .pipeThrough(
457
460
  createTokenSpeedCalculator(transformWithProvider, {
458
461
  enableStreaming: enableStreaming,