@lobehub/chat 1.134.6 → 1.135.0

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 (60) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/changelog/v1.json +18 -0
  3. package/docs/development/basic/feature-development-frontend.zh-CN.mdx +1 -1
  4. package/docs/development/basic/folder-structure.mdx +67 -16
  5. package/docs/development/basic/folder-structure.zh-CN.mdx +67 -16
  6. package/locales/ar/modelProvider.json +15 -0
  7. package/locales/ar/models.json +3 -0
  8. package/locales/bg-BG/modelProvider.json +15 -0
  9. package/locales/bg-BG/models.json +3 -0
  10. package/locales/de-DE/modelProvider.json +15 -0
  11. package/locales/de-DE/models.json +3 -0
  12. package/locales/en-US/modelProvider.json +15 -0
  13. package/locales/en-US/models.json +3 -0
  14. package/locales/es-ES/modelProvider.json +15 -0
  15. package/locales/es-ES/models.json +3 -0
  16. package/locales/fa-IR/modelProvider.json +15 -0
  17. package/locales/fa-IR/models.json +3 -0
  18. package/locales/fr-FR/modelProvider.json +15 -0
  19. package/locales/fr-FR/models.json +3 -0
  20. package/locales/it-IT/modelProvider.json +15 -0
  21. package/locales/it-IT/models.json +3 -0
  22. package/locales/ja-JP/modelProvider.json +15 -0
  23. package/locales/ja-JP/models.json +3 -0
  24. package/locales/ko-KR/modelProvider.json +15 -0
  25. package/locales/ko-KR/models.json +3 -0
  26. package/locales/nl-NL/modelProvider.json +15 -0
  27. package/locales/nl-NL/models.json +3 -0
  28. package/locales/pl-PL/modelProvider.json +15 -0
  29. package/locales/pl-PL/models.json +3 -0
  30. package/locales/pt-BR/modelProvider.json +15 -0
  31. package/locales/pt-BR/models.json +3 -0
  32. package/locales/ru-RU/modelProvider.json +15 -0
  33. package/locales/ru-RU/models.json +3 -0
  34. package/locales/tr-TR/modelProvider.json +15 -0
  35. package/locales/tr-TR/models.json +3 -0
  36. package/locales/vi-VN/modelProvider.json +15 -0
  37. package/locales/vi-VN/models.json +3 -0
  38. package/locales/zh-CN/modelProvider.json +15 -0
  39. package/locales/zh-CN/models.json +3 -0
  40. package/locales/zh-TW/modelProvider.json +15 -0
  41. package/locales/zh-TW/models.json +3 -0
  42. package/package.json +1 -1
  43. package/packages/model-bank/src/aiModels/fal.ts +28 -0
  44. package/packages/model-runtime/src/core/openaiCompatibleFactory/createImage.ts +16 -27
  45. package/packages/model-runtime/src/core/openaiCompatibleFactory/index.test.ts +51 -11
  46. package/packages/model-runtime/src/core/streams/protocol.ts +2 -15
  47. package/packages/model-runtime/src/providers/azureOpenai/index.ts +5 -1
  48. package/packages/model-runtime/src/providers/azureai/index.ts +5 -1
  49. package/packages/model-runtime/src/providers/fal/index.ts +12 -7
  50. package/packages/model-runtime/src/providers/newapi/index.test.ts +28 -3
  51. package/packages/model-runtime/src/providers/newapi/index.ts +34 -88
  52. package/packages/model-runtime/src/types/index.ts +0 -1
  53. package/packages/model-runtime/src/utils/sanitizeError.test.ts +109 -0
  54. package/packages/model-runtime/src/utils/sanitizeError.ts +59 -0
  55. package/packages/types/src/message/base.ts +1 -0
  56. package/packages/utils/package.json +2 -1
  57. package/src/app/[variants]/(main)/image/@menu/components/SizeSelect/index.tsx +24 -1
  58. package/src/server/modules/EdgeConfig/index.ts +15 -33
  59. package/src/server/modules/EdgeConfig/types.ts +13 -0
  60. package/packages/model-runtime/src/types/usage.ts +0 -27
@@ -294,6 +294,21 @@
294
294
  "title": "최대 컨텍스트 창",
295
295
  "unlimited": "제한 없음"
296
296
  },
297
+ "type": {
298
+ "extra": "다양한 모델 유형은 차별화된 사용 시나리오와 기능을 제공합니다",
299
+ "options": {
300
+ "chat": "대화",
301
+ "embedding": "벡터화",
302
+ "image": "이미지 생성",
303
+ "realtime": "실시간 대화",
304
+ "stt": "음성 인식",
305
+ "text2music": "텍스트에서 음악으로",
306
+ "text2video": "텍스트에서 비디오로",
307
+ "tts": "음성 합성"
308
+ },
309
+ "placeholder": "모델 유형을 선택하세요",
310
+ "title": "모델 유형"
311
+ },
297
312
  "vision": {
298
313
  "extra": "이 설정은 애플리케이션 내에서 이미지 업로드 기능만 활성화합니다. 인식 지원 여부는 모델 자체에 따라 다르므로, 해당 모델의 시각 인식 가능성을 스스로 테스트하세요.",
299
314
  "title": "시각 인식 지원"
@@ -1253,6 +1253,9 @@
1253
1253
  "fal-ai/flux/schnell": {
1254
1254
  "description": "FLUX.1 [schnell]은 120억 개의 매개변수를 가진 이미지 생성 모델로, 빠른 고품질 이미지 생성을 중점으로 합니다."
1255
1255
  },
1256
+ "fal-ai/hunyuan-image/v3": {
1257
+ "description": "강력한 네이티브 멀티모달 이미지 생성 모델"
1258
+ },
1256
1259
  "fal-ai/imagen4/preview": {
1257
1260
  "description": "Google에서 제공하는 고품질 이미지 생성 모델입니다."
1258
1261
  },
@@ -294,6 +294,21 @@
294
294
  "title": "Maximale contextvenster",
295
295
  "unlimited": "Onbeperkt"
296
296
  },
297
+ "type": {
298
+ "extra": "Verschillende modeltypen hebben verschillende toepassingsscenario's en mogelijkheden",
299
+ "options": {
300
+ "chat": "Gesprek",
301
+ "embedding": "Vectorisatie",
302
+ "image": "Afbeeldingsgeneratie",
303
+ "realtime": "Realtime gesprek",
304
+ "stt": "Spraak naar tekst",
305
+ "text2music": "Tekst naar muziek",
306
+ "text2video": "Tekst naar video",
307
+ "tts": "Spraaksynthese"
308
+ },
309
+ "placeholder": "Selecteer een modeltype",
310
+ "title": "Modeltype"
311
+ },
297
312
  "vision": {
298
313
  "extra": "Deze configuratie zal alleen de afbeeldinguploadcapaciteit in de applicatie inschakelen, of herkenning wordt ondersteund hangt volledig af van het model zelf, test de beschikbaarheid van de visuele herkenningscapaciteit van dit model zelf.",
299
314
  "title": "Ondersteuning voor visuele herkenning"
@@ -1253,6 +1253,9 @@
1253
1253
  "fal-ai/flux/schnell": {
1254
1254
  "description": "FLUX.1 [schnell] is een beeldgeneratiemodel met 12 miljard parameters, gericht op het snel genereren van hoogwaardige beelden."
1255
1255
  },
1256
+ "fal-ai/hunyuan-image/v3": {
1257
+ "description": "Een krachtig native multimodaal beeldgeneratiemodel"
1258
+ },
1256
1259
  "fal-ai/imagen4/preview": {
1257
1260
  "description": "Hoogwaardig beeldgeneratiemodel aangeboden door Google."
1258
1261
  },
@@ -294,6 +294,21 @@
294
294
  "title": "Maksymalne okno kontekstu",
295
295
  "unlimited": "Bez ograniczeń"
296
296
  },
297
+ "type": {
298
+ "extra": "Różne typy modeli mają różne scenariusze użycia i możliwości",
299
+ "options": {
300
+ "chat": "Czat",
301
+ "embedding": "Wektoryzacja",
302
+ "image": "Generowanie obrazów",
303
+ "realtime": "Czat w czasie rzeczywistym",
304
+ "stt": "Rozpoznawanie mowy",
305
+ "text2music": "Tekst na muzykę",
306
+ "text2video": "Tekst na wideo",
307
+ "tts": "Synteza mowy"
308
+ },
309
+ "placeholder": "Wybierz typ modelu",
310
+ "title": "Typ modelu"
311
+ },
297
312
  "vision": {
298
313
  "extra": "Ta konfiguracja włączy tylko możliwość przesyłania obrazów w aplikacji, czy model obsługuje rozpoznawanie zależy od samego modelu, proszę samodzielnie przetestować dostępność rozpoznawania wizualnego tego modelu.",
299
314
  "title": "Wsparcie dla rozpoznawania wizualnego"
@@ -1253,6 +1253,9 @@
1253
1253
  "fal-ai/flux/schnell": {
1254
1254
  "description": "FLUX.1 [schnell] to model generowania obrazów z 12 miliardami parametrów, skoncentrowany na szybkim tworzeniu wysokiej jakości obrazów."
1255
1255
  },
1256
+ "fal-ai/hunyuan-image/v3": {
1257
+ "description": "Potężny natywny model generowania obrazów multimodalnych"
1258
+ },
1256
1259
  "fal-ai/imagen4/preview": {
1257
1260
  "description": "Wysokiej jakości model generowania obrazów udostępniony przez Google."
1258
1261
  },
@@ -294,6 +294,21 @@
294
294
  "title": "Janela de contexto máxima",
295
295
  "unlimited": "Ilimitado"
296
296
  },
297
+ "type": {
298
+ "extra": "Diferentes tipos de modelos possuem cenários de uso e capacidades diferenciadas",
299
+ "options": {
300
+ "chat": "Conversa",
301
+ "embedding": "Vetorização",
302
+ "image": "Geração de imagem",
303
+ "realtime": "Conversa em tempo real",
304
+ "stt": "Reconhecimento de voz para texto",
305
+ "text2music": "Texto para música",
306
+ "text2video": "Texto para vídeo",
307
+ "tts": "Síntese de voz"
308
+ },
309
+ "placeholder": "Por favor, selecione o tipo de modelo",
310
+ "title": "Tipo de modelo"
311
+ },
297
312
  "vision": {
298
313
  "extra": "Esta configuração apenas habilitará a configuração de upload de imagens no aplicativo, se o reconhecimento for suportado depende do modelo em si, teste a capacidade de reconhecimento visual desse modelo.",
299
314
  "title": "Suporte a Reconhecimento Visual"
@@ -1253,6 +1253,9 @@
1253
1253
  "fal-ai/flux/schnell": {
1254
1254
  "description": "FLUX.1 [schnell] é um modelo de geração de imagens com 12 bilhões de parâmetros, focado em gerar imagens de alta qualidade rapidamente."
1255
1255
  },
1256
+ "fal-ai/hunyuan-image/v3": {
1257
+ "description": "Um poderoso modelo nativo de geração de imagens multimodais"
1258
+ },
1256
1259
  "fal-ai/imagen4/preview": {
1257
1260
  "description": "Modelo de geração de imagens de alta qualidade fornecido pelo Google."
1258
1261
  },
@@ -294,6 +294,21 @@
294
294
  "title": "Максимальное окно контекста",
295
295
  "unlimited": "Без ограничений"
296
296
  },
297
+ "type": {
298
+ "extra": "Различные типы моделей имеют разные сценарии использования и возможности",
299
+ "options": {
300
+ "chat": "Диалог",
301
+ "embedding": "Векторизация",
302
+ "image": "Генерация изображений",
303
+ "realtime": "Реальное время",
304
+ "stt": "Распознавание речи",
305
+ "text2music": "Текст в музыку",
306
+ "text2video": "Текст в видео",
307
+ "tts": "Синтез речи"
308
+ },
309
+ "placeholder": "Пожалуйста, выберите тип модели",
310
+ "title": "Тип модели"
311
+ },
297
312
  "vision": {
298
313
  "extra": "Эта настройка только активирует возможность загрузки изображений в приложении, поддержка распознавания полностью зависит от самой модели, пожалуйста, протестируйте доступность визуального распознавания этой модели.",
299
314
  "title": "Поддержка визуального распознавания"
@@ -1253,6 +1253,9 @@
1253
1253
  "fal-ai/flux/schnell": {
1254
1254
  "description": "FLUX.1 [schnell] — модель генерации изображений с 12 миллиардами параметров, ориентированная на быструю генерацию высококачественных изображений."
1255
1255
  },
1256
+ "fal-ai/hunyuan-image/v3": {
1257
+ "description": "Мощная нативная мультимодальная модель генерации изображений"
1258
+ },
1256
1259
  "fal-ai/imagen4/preview": {
1257
1260
  "description": "Высококачественная модель генерации изображений от Google."
1258
1261
  },
@@ -294,6 +294,21 @@
294
294
  "title": "Maksimum bağlam penceresi",
295
295
  "unlimited": "Sınırsız"
296
296
  },
297
+ "type": {
298
+ "extra": "Farklı model türleri, farklı kullanım senaryoları ve yeteneklere sahiptir",
299
+ "options": {
300
+ "chat": "Sohbet",
301
+ "embedding": "Vektörleştirme",
302
+ "image": "Görüntü oluşturma",
303
+ "realtime": "Gerçek zamanlı sohbet",
304
+ "stt": "Ses metne dönüştürme",
305
+ "text2music": "Metinden müziğe",
306
+ "text2video": "Metinden videoya",
307
+ "tts": "Ses sentezi"
308
+ },
309
+ "placeholder": "Lütfen model türünü seçin",
310
+ "title": "Model Türü"
311
+ },
297
312
  "vision": {
298
313
  "extra": "Bu yapılandırma yalnızca uygulamadaki resim yükleme yapılandırmasını açacaktır, tanıma desteği tamamen modele bağlıdır, lütfen bu modelin görsel tanıma yeteneğini test edin.",
299
314
  "title": "Görsel Tanımayı Destekle"
@@ -1253,6 +1253,9 @@
1253
1253
  "fal-ai/flux/schnell": {
1254
1254
  "description": "FLUX.1 [schnell], 12 milyar parametreye sahip bir görüntü oluşturma modelidir ve hızlı yüksek kaliteli görüntü üretimine odaklanır."
1255
1255
  },
1256
+ "fal-ai/hunyuan-image/v3": {
1257
+ "description": "Güçlü bir yerel çok modlu görüntü oluşturma modeli"
1258
+ },
1256
1259
  "fal-ai/imagen4/preview": {
1257
1260
  "description": "Google tarafından sunulan yüksek kaliteli görüntü oluşturma modeli."
1258
1261
  },
@@ -294,6 +294,21 @@
294
294
  "title": "Cửa sổ ngữ cảnh tối đa",
295
295
  "unlimited": "Không giới hạn"
296
296
  },
297
+ "type": {
298
+ "extra": "Các loại mô hình khác nhau có các kịch bản sử dụng và khả năng khác biệt",
299
+ "options": {
300
+ "chat": "Đối thoại",
301
+ "embedding": "Vector hóa",
302
+ "image": "Tạo hình ảnh",
303
+ "realtime": "Đối thoại thời gian thực",
304
+ "stt": "Chuyển giọng nói thành văn bản",
305
+ "text2music": "Chuyển văn bản thành nhạc",
306
+ "text2video": "Chuyển văn bản thành video",
307
+ "tts": "Tổng hợp giọng nói"
308
+ },
309
+ "placeholder": "Vui lòng chọn loại mô hình",
310
+ "title": "Loại mô hình"
311
+ },
297
312
  "vision": {
298
313
  "extra": "Cấu hình này chỉ mở khả năng tải lên hình ảnh trong ứng dụng, việc hỗ trợ nhận diện hoàn toàn phụ thuộc vào mô hình, xin hãy tự kiểm tra khả năng nhận diện hình ảnh của mô hình này.",
299
314
  "title": "Hỗ trợ nhận diện hình ảnh"
@@ -1253,6 +1253,9 @@
1253
1253
  "fal-ai/flux/schnell": {
1254
1254
  "description": "FLUX.1 [schnell] là mô hình tạo ảnh với 12 tỷ tham số, tập trung vào việc tạo ảnh chất lượng cao nhanh chóng."
1255
1255
  },
1256
+ "fal-ai/hunyuan-image/v3": {
1257
+ "description": "Một mô hình tạo hình ảnh đa phương thức gốc mạnh mẽ"
1258
+ },
1256
1259
  "fal-ai/imagen4/preview": {
1257
1260
  "description": "Mô hình tạo ảnh chất lượng cao do Google cung cấp."
1258
1261
  },
@@ -294,6 +294,21 @@
294
294
  "title": "最大上下文窗口",
295
295
  "unlimited": "无限制"
296
296
  },
297
+ "type": {
298
+ "extra": "不同模型类型拥有差异化的使用场景与能力",
299
+ "options": {
300
+ "chat": "对话",
301
+ "embedding": "向量化",
302
+ "image": "图片生成",
303
+ "realtime": "实时对话",
304
+ "stt": "语音转文本",
305
+ "text2music": "文本转音乐",
306
+ "text2video": "文本转视频",
307
+ "tts": "语音合成"
308
+ },
309
+ "placeholder": "请选择模型类型",
310
+ "title": "模型类型"
311
+ },
297
312
  "vision": {
298
313
  "extra": "此配置将仅开启应用中的图片上传配置,是否支持识别完全取决于模型本身,请自行测试该模型的视觉识别能力可用性",
299
314
  "title": "支持视觉识别"
@@ -1253,6 +1253,9 @@
1253
1253
  "fal-ai/flux/schnell": {
1254
1254
  "description": "FLUX.1 [schnell] 是一个具有120亿参数的图像生成模型,专注于快速生成高质量图像。"
1255
1255
  },
1256
+ "fal-ai/hunyuan-image/v3": {
1257
+ "description": "一个强大的原生多模态图像生成模型"
1258
+ },
1256
1259
  "fal-ai/imagen4/preview": {
1257
1260
  "description": "Google 提供的高质量的图像生成模型"
1258
1261
  },
@@ -294,6 +294,21 @@
294
294
  "title": "最大上下文窗口",
295
295
  "unlimited": "無限制"
296
296
  },
297
+ "type": {
298
+ "extra": "不同模型類型擁有差異化的使用場景與能力",
299
+ "options": {
300
+ "chat": "對話",
301
+ "embedding": "向量化",
302
+ "image": "圖片生成",
303
+ "realtime": "即時對話",
304
+ "stt": "語音轉文字",
305
+ "text2music": "文本轉音樂",
306
+ "text2video": "文本轉影片",
307
+ "tts": "語音合成"
308
+ },
309
+ "placeholder": "請選擇模型類型",
310
+ "title": "模型類型"
311
+ },
297
312
  "vision": {
298
313
  "extra": "此配置將僅開啟應用中的圖片上傳配置,是否支持識別完全取決於模型本身,請自行測試該模型的視覺識別能力可用性",
299
314
  "title": "支持視覺識別"
@@ -1253,6 +1253,9 @@
1253
1253
  "fal-ai/flux/schnell": {
1254
1254
  "description": "FLUX.1 [schnell] 是一個具有120億參數的圖像生成模型,專注於快速生成高品質圖像。"
1255
1255
  },
1256
+ "fal-ai/hunyuan-image/v3": {
1257
+ "description": "一個強大的原生多模態圖像生成模型"
1258
+ },
1256
1259
  "fal-ai/imagen4/preview": {
1257
1260
  "description": "Google 提供的高品質圖像生成模型"
1258
1261
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "1.134.6",
3
+ "version": "1.135.0",
4
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.",
5
5
  "keywords": [
6
6
  "framework",
@@ -79,6 +79,34 @@ const falImageModels: AIImageModelCard[] = [
79
79
  releasedAt: '2025-09-09',
80
80
  type: 'image',
81
81
  },
82
+ {
83
+ description: '一个强大的原生多模态图像生成模型',
84
+ displayName: 'HunyuanImage 3.0',
85
+ enabled: true,
86
+ id: 'fal-ai/hunyuan-image/v3',
87
+ parameters: {
88
+ cfg: { default: 7.5, max: 20, min: 1, step: 0.1 },
89
+ prompt: { default: '' },
90
+ seed: { default: null },
91
+ size: {
92
+ default: 'square_hd',
93
+ enum: [
94
+ 'square_hd',
95
+ 'square',
96
+ 'portrait_4_3',
97
+ 'portrait_16_9',
98
+ 'landscape_4_3',
99
+ 'landscape_16_9',
100
+ ],
101
+ },
102
+ steps: { default: 28, max: 50, min: 1, step: 1 },
103
+ },
104
+ pricing: {
105
+ units: [{ name: 'imageGeneration', rate: 0.1, strategy: 'fixed', unit: 'megapixel' }],
106
+ },
107
+ releasedAt: '2025-09-28',
108
+ type: 'image',
109
+ },
82
110
  {
83
111
  description: '专注于图像编辑任务的FLUX.1模型,支持文本和图像输入。',
84
112
  displayName: 'FLUX.1 Kontext [dev]',
@@ -1,3 +1,4 @@
1
+ import { cleanObject } from '@lobechat/utils/object';
1
2
  import createDebug from 'debug';
2
3
  import { RuntimeImageGenParamsValue } from 'model-bank';
3
4
  import OpenAI from 'openai';
@@ -34,24 +35,25 @@ async function generateByImageMode(
34
35
  value,
35
36
  ]),
36
37
  );
38
+ // unify image input to array
39
+ if (typeof userInput.image === 'string' && userInput.image.trim() !== '') {
40
+ userInput.image = [userInput.image];
41
+ }
37
42
 
38
43
  // https://platform.openai.com/docs/api-reference/images/createEdit
39
44
  const isImageEdit = Array.isArray(userInput.image) && userInput.image.length > 0;
45
+ log('isImageEdit: %O, userInput.image: %O', isImageEdit, userInput.image);
40
46
  // If there are imageUrls parameters, convert them to File objects
41
47
  if (isImageEdit) {
42
- log('Converting imageUrls to File objects: %O', userInput.image);
43
48
  try {
44
49
  // Convert all image URLs to File objects
45
50
  const imageFiles = await Promise.all(
46
51
  userInput.image.map((url: string) => convertImageUrlToFile(url)),
47
52
  );
48
53
 
49
- log('Successfully converted %d images to File objects', imageFiles.length);
50
-
51
54
  // According to official docs, if there are multiple images, pass an array; if only one, pass a single File
52
55
  userInput.image = imageFiles.length === 1 ? imageFiles[0] : imageFiles;
53
56
  } catch (error) {
54
- log('Error converting imageUrls to File objects: %O', error);
55
57
  throw new Error(`Failed to convert image URLs to File objects: ${error}`);
56
58
  }
57
59
  } else {
@@ -68,11 +70,11 @@ async function generateByImageMode(
68
70
  ...(isImageEdit ? { input_fidelity: 'high' } : {}),
69
71
  };
70
72
 
71
- const options = {
73
+ const options = cleanObject({
72
74
  model,
73
75
  ...defaultInput,
74
76
  ...userInput,
75
- };
77
+ });
76
78
 
77
79
  log('options: %O', options);
78
80
 
@@ -83,13 +85,11 @@ async function generateByImageMode(
83
85
 
84
86
  // Check the integrity of response data
85
87
  if (!img || !img.data || !Array.isArray(img.data) || img.data.length === 0) {
86
- log('Invalid image response: missing data array');
87
88
  throw new Error('Invalid image response: missing or empty data array');
88
89
  }
89
90
 
90
91
  const imageData = img.data[0];
91
92
  if (!imageData) {
92
- log('Invalid image response: first data item is null/undefined');
93
93
  throw new Error('Invalid image response: first data item is null or undefined');
94
94
  }
95
95
 
@@ -111,12 +111,9 @@ async function generateByImageMode(
111
111
  }
112
112
  // If neither format exists, throw error
113
113
  else {
114
- log('Invalid image response: missing both b64_json and url fields');
115
114
  throw new Error('Invalid image response: missing both b64_json and url fields');
116
115
  }
117
116
 
118
- log('provider: %s', provider);
119
-
120
117
  return {
121
118
  imageUrl,
122
119
  ...(img.usage
@@ -180,7 +177,6 @@ async function generateByChatModel(
180
177
  });
181
178
  log('Successfully processed image URL for chat input');
182
179
  } catch (error) {
183
- log('Error processing image URL: %O', error);
184
180
  throw new Error(`Failed to process image URL: ${error}`);
185
181
  }
186
182
  }
@@ -218,7 +214,6 @@ async function generateByChatModel(
218
214
  }
219
215
 
220
216
  // If no images found, throw error
221
- log('No images found in chat completion response');
222
217
  throw new Error('No image generated in chat completion response');
223
218
  }
224
219
 
@@ -228,21 +223,15 @@ async function generateByChatModel(
228
223
  export async function createOpenAICompatibleImage(
229
224
  client: OpenAI,
230
225
  payload: CreateImagePayload,
231
- provider: string, // eslint-disable-line @typescript-eslint/no-unused-vars
226
+ provider: string,
232
227
  ): Promise<CreateImageResponse> {
233
- try {
234
- const { model } = payload;
235
-
236
- // Check if it's a chat model for image generation (via :image suffix)
237
- if (model.endsWith(':image')) {
238
- return await generateByChatModel(client, payload);
239
- }
228
+ const { model } = payload;
240
229
 
241
- // Default to traditional images API
242
- return await generateByImageMode(client, payload, provider);
243
- } catch (error) {
244
- const err = error as Error;
245
- log('Error in createImage: %O', err);
246
- throw err;
230
+ // Check if it's a chat model for image generation (via :image suffix)
231
+ if (model.endsWith(':image')) {
232
+ return await generateByChatModel(client, payload);
247
233
  }
234
+
235
+ // Default to traditional images API
236
+ return await generateByImageMode(client, payload, provider);
248
237
  }
@@ -296,6 +296,8 @@ describe('LobeOpenAICompatibleFactory', () => {
296
296
  });
297
297
 
298
298
  it('should transform non-streaming response to stream correctly', async () => {
299
+ vi.useFakeTimers();
300
+
299
301
  const mockResponse = {
300
302
  id: 'a',
301
303
  object: 'chat.completion',
@@ -319,13 +321,18 @@ describe('LobeOpenAICompatibleFactory', () => {
319
321
  mockResponse as any,
320
322
  );
321
323
 
322
- const result = await instance.chat({
324
+ const chatPromise = instance.chat({
323
325
  messages: [{ content: 'Hello', role: 'user' }],
324
326
  model: 'mistralai/mistral-7b-instruct:free',
325
327
  temperature: 0,
326
328
  stream: false,
327
329
  });
328
330
 
331
+ // Advance time to simulate processing delay
332
+ vi.advanceTimersByTime(10);
333
+
334
+ const result = await chatPromise;
335
+
329
336
  const decoder = new TextDecoder();
330
337
  const reader = result.body!.getReader();
331
338
  const stream: string[] = [];
@@ -345,16 +352,20 @@ describe('LobeOpenAICompatibleFactory', () => {
345
352
  'data: {"inputTextTokens":5,"outputTextTokens":5,"totalInputTokens":5,"totalOutputTokens":5,"totalTokens":10}\n\n',
346
353
  'id: output_speed\n',
347
354
  'event: speed\n',
348
- expect.stringMatching(/^data: \{.*"tps":.*,"ttft":.*}\n\n$/), // tps ttft 测试结果不一样
355
+ expect.stringMatching(/^data: \{.*"tps":.*,"ttft":.*}\n\n$/), // tps ttft should be calculated with elapsed time
349
356
  'id: a\n',
350
357
  'event: stop\n',
351
358
  'data: "stop"\n\n',
352
359
  ]);
353
360
 
354
361
  expect((await reader.read()).done).toBe(true);
362
+
363
+ vi.useRealTimers();
355
364
  });
356
365
 
357
366
  it('should transform non-streaming response to stream correctly with reasoning content', async () => {
367
+ vi.useFakeTimers();
368
+
358
369
  const mockResponse = {
359
370
  id: 'a',
360
371
  object: 'chat.completion',
@@ -382,13 +393,18 @@ describe('LobeOpenAICompatibleFactory', () => {
382
393
  mockResponse as any,
383
394
  );
384
395
 
385
- const result = await instance.chat({
396
+ const chatPromise = instance.chat({
386
397
  messages: [{ content: 'Hello', role: 'user' }],
387
398
  model: 'deepseek/deepseek-reasoner',
388
399
  temperature: 0,
389
400
  stream: false,
390
401
  });
391
402
 
403
+ // Advance time to simulate processing delay
404
+ vi.advanceTimersByTime(10);
405
+
406
+ const result = await chatPromise;
407
+
392
408
  const decoder = new TextDecoder();
393
409
  const reader = result.body!.getReader();
394
410
  const stream: string[] = [];
@@ -411,13 +427,15 @@ describe('LobeOpenAICompatibleFactory', () => {
411
427
  'data: {"inputTextTokens":5,"outputTextTokens":5,"totalInputTokens":5,"totalOutputTokens":5,"totalTokens":10}\n\n',
412
428
  'id: output_speed\n',
413
429
  'event: speed\n',
414
- expect.stringMatching(/^data: \{.*"tps":.*,"ttft":.*}\n\n$/), // tps ttft 测试结果不一样
430
+ expect.stringMatching(/^data: \{.*"tps":.*,"ttft":.*}\n\n$/), // tps ttft should be calculated with elapsed time
415
431
  'id: a\n',
416
432
  'event: stop\n',
417
433
  'data: "stop"\n\n',
418
434
  ]);
419
435
 
420
436
  expect((await reader.read()).done).toBe(true);
437
+
438
+ vi.useRealTimers();
421
439
  });
422
440
  });
423
441
 
@@ -974,7 +992,11 @@ describe('LobeOpenAICompatibleFactory', () => {
974
992
  .spyOn(inst['client'].responses, 'create')
975
993
  .mockResolvedValue({ tee: () => [prod, debug] } as any);
976
994
 
977
- await inst.chat({ messages: [{ content: 'hi', role: 'user' }], model: 'any-model', temperature: 0 });
995
+ await inst.chat({
996
+ messages: [{ content: 'hi', role: 'user' }],
997
+ model: 'any-model',
998
+ temperature: 0,
999
+ });
978
1000
 
979
1001
  expect(mockResponsesCreate).toHaveBeenCalled();
980
1002
  });
@@ -990,20 +1012,38 @@ describe('LobeOpenAICompatibleFactory', () => {
990
1012
  const inst = new LobeMockProviderUseResponseModels({ apiKey: 'test' });
991
1013
  const spy = vi.spyOn(inst['client'].responses, 'create');
992
1014
  // Prevent hanging by mocking normal chat completion stream
993
- vi.spyOn(inst['client'].chat.completions, 'create').mockResolvedValue(new ReadableStream() as any);
1015
+ vi.spyOn(inst['client'].chat.completions, 'create').mockResolvedValue(
1016
+ new ReadableStream() as any,
1017
+ );
994
1018
 
995
1019
  // First invocation: model contains the string
996
- spy.mockResolvedValueOnce({ tee: () => [new ReadableStream(), new ReadableStream()] } as any);
997
- await inst.chat({ messages: [{ content: 'hi', role: 'user' }], model: 'prefix-special-model-suffix', temperature: 0 });
1020
+ spy.mockResolvedValueOnce({
1021
+ tee: () => [new ReadableStream(), new ReadableStream()],
1022
+ } as any);
1023
+ await inst.chat({
1024
+ messages: [{ content: 'hi', role: 'user' }],
1025
+ model: 'prefix-special-model-suffix',
1026
+ temperature: 0,
1027
+ });
998
1028
  expect(spy).toHaveBeenCalledTimes(1);
999
1029
 
1000
1030
  // Second invocation: model matches the RegExp
1001
- spy.mockResolvedValueOnce({ tee: () => [new ReadableStream(), new ReadableStream()] } as any);
1002
- await inst.chat({ messages: [{ content: 'hi', role: 'user' }], model: 'special-xyz', temperature: 0 });
1031
+ spy.mockResolvedValueOnce({
1032
+ tee: () => [new ReadableStream(), new ReadableStream()],
1033
+ } as any);
1034
+ await inst.chat({
1035
+ messages: [{ content: 'hi', role: 'user' }],
1036
+ model: 'special-xyz',
1037
+ temperature: 0,
1038
+ });
1003
1039
  expect(spy).toHaveBeenCalledTimes(2);
1004
1040
 
1005
1041
  // Third invocation: model does not match any useResponseModels patterns
1006
- await inst.chat({ messages: [{ content: 'hi', role: 'user' }], model: 'unrelated-model', temperature: 0 });
1042
+ await inst.chat({
1043
+ messages: [{ content: 'hi', role: 'user' }],
1044
+ model: 'unrelated-model',
1045
+ temperature: 0,
1046
+ });
1007
1047
  expect(spy).toHaveBeenCalledTimes(2); // Ensure no additional calls were made
1008
1048
  });
1009
1049
  });
@@ -384,7 +384,6 @@ export const createTokenSpeedCalculator = (
384
384
  }: { enableStreaming?: boolean; inputStartAt?: number; streamStack?: StreamContext } = {},
385
385
  ) => {
386
386
  let outputStartAt: number | undefined;
387
- let outputThinking: boolean | undefined;
388
387
 
389
388
  const process = (chunk: StreamProtocolChunk) => {
390
389
  let result = [chunk];
@@ -393,24 +392,12 @@ export const createTokenSpeedCalculator = (
393
392
  outputStartAt = Date.now();
394
393
  }
395
394
 
396
- /**
397
- * 部分 provider 在正式输出 reasoning 前,可能会先输出 content 为空字符串的 chunk,
398
- * 其中 reasoning 可能为 null,会导致判断是否输出思考内容错误,所以过滤掉 null 或者空字符串。
399
- * 也可能是某些特殊 token,所以不修改 outputStartAt 的逻辑。
400
- */
401
- if (
402
- outputThinking === undefined &&
403
- (chunk.type === 'text' || chunk.type === 'reasoning') &&
404
- typeof chunk.data === 'string' &&
405
- chunk.data.length > 0
406
- ) {
407
- outputThinking = chunk.type === 'reasoning';
408
- }
409
395
  // if the chunk is the stop chunk, set as output finish
410
396
  if (inputStartAt && outputStartAt && chunk.type === 'usage') {
411
397
  // TPS should always include all generated tokens (including reasoning tokens)
412
398
  // because it measures generation speed, not just visible content
413
- const outputTokens = chunk.data?.totalOutputTokens ?? 0;
399
+ const usage = chunk.data as ModelUsage;
400
+ const outputTokens = usage?.totalOutputTokens ?? 0;
414
401
  const now = Date.now();
415
402
  const elapsed = now - (enableStreaming ? outputStartAt : inputStartAt);
416
403
  const duration = now - outputStartAt;