@lobehub/chat 1.134.7 → 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.
- package/CHANGELOG.md +25 -0
- package/changelog/v1.json +9 -0
- package/locales/ar/models.json +3 -0
- package/locales/bg-BG/models.json +3 -0
- package/locales/de-DE/models.json +3 -0
- package/locales/en-US/models.json +3 -0
- package/locales/es-ES/models.json +3 -0
- package/locales/fa-IR/models.json +3 -0
- package/locales/fr-FR/models.json +3 -0
- package/locales/it-IT/models.json +3 -0
- package/locales/ja-JP/models.json +3 -0
- package/locales/ko-KR/models.json +3 -0
- package/locales/nl-NL/models.json +3 -0
- package/locales/pl-PL/models.json +3 -0
- package/locales/pt-BR/models.json +3 -0
- package/locales/ru-RU/models.json +3 -0
- package/locales/tr-TR/models.json +3 -0
- package/locales/vi-VN/models.json +3 -0
- package/locales/zh-CN/models.json +3 -0
- package/locales/zh-TW/models.json +3 -0
- package/package.json +1 -1
- package/packages/model-bank/src/aiModels/fal.ts +28 -0
- package/packages/model-runtime/src/core/openaiCompatibleFactory/createImage.ts +16 -27
- package/packages/model-runtime/src/core/openaiCompatibleFactory/index.test.ts +51 -11
- package/packages/model-runtime/src/core/streams/protocol.ts +2 -15
- package/packages/model-runtime/src/providers/fal/index.ts +12 -7
- package/packages/model-runtime/src/providers/newapi/index.test.ts +28 -3
- package/packages/model-runtime/src/providers/newapi/index.ts +34 -88
- package/packages/model-runtime/src/types/index.ts +0 -1
- package/packages/types/src/message/base.ts +1 -0
- package/packages/utils/package.json +2 -1
- package/src/app/[variants]/(main)/image/@menu/components/SizeSelect/index.tsx +24 -1
- package/src/server/modules/EdgeConfig/index.ts +15 -33
- package/src/server/modules/EdgeConfig/types.ts +13 -0
- package/packages/model-runtime/src/types/usage.ts +0 -27
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
# Changelog
|
|
4
4
|
|
|
5
|
+
## [Version 1.135.0](https://github.com/lobehub/lobe-chat/compare/v1.134.7...v1.135.0)
|
|
6
|
+
|
|
7
|
+
<sup>Released on **2025-10-06**</sup>
|
|
8
|
+
|
|
9
|
+
#### ✨ Features
|
|
10
|
+
|
|
11
|
+
- **misc**: Huanyuan text-to-image 3.
|
|
12
|
+
|
|
13
|
+
<br/>
|
|
14
|
+
|
|
15
|
+
<details>
|
|
16
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
17
|
+
|
|
18
|
+
#### What's improved
|
|
19
|
+
|
|
20
|
+
- **misc**: Huanyuan text-to-image 3, closes [#9589](https://github.com/lobehub/lobe-chat/issues/9589) ([1dd0e5e](https://github.com/lobehub/lobe-chat/commit/1dd0e5e))
|
|
21
|
+
|
|
22
|
+
</details>
|
|
23
|
+
|
|
24
|
+
<div align="right">
|
|
25
|
+
|
|
26
|
+
[](#readme-top)
|
|
27
|
+
|
|
28
|
+
</div>
|
|
29
|
+
|
|
5
30
|
### [Version 1.134.7](https://github.com/lobehub/lobe-chat/compare/v1.134.6...v1.134.7)
|
|
6
31
|
|
|
7
32
|
<sup>Released on **2025-10-06**</sup>
|
package/changelog/v1.json
CHANGED
package/locales/ar/models.json
CHANGED
|
@@ -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": "نموذج توليد صور عالي الجودة مقدم من جوجل."
|
|
1258
1261
|
},
|
|
@@ -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
|
},
|
|
@@ -1253,6 +1253,9 @@
|
|
|
1253
1253
|
"fal-ai/flux/schnell": {
|
|
1254
1254
|
"description": "FLUX.1 [schnell] ist ein bildgenerierendes Modell mit 12 Milliarden Parametern, das sich auf die schnelle Erzeugung hochwertiger Bilder konzentriert."
|
|
1255
1255
|
},
|
|
1256
|
+
"fal-ai/hunyuan-image/v3": {
|
|
1257
|
+
"description": "Ein leistungsstarkes natives multimodales Bildgenerierungsmodell"
|
|
1258
|
+
},
|
|
1256
1259
|
"fal-ai/imagen4/preview": {
|
|
1257
1260
|
"description": "Hochwertiges Bildgenerierungsmodell von Google."
|
|
1258
1261
|
},
|
|
@@ -1253,6 +1253,9 @@
|
|
|
1253
1253
|
"fal-ai/flux/schnell": {
|
|
1254
1254
|
"description": "FLUX.1 [schnell] is a 12-billion-parameter image generation model focused on fast generation of high-quality images."
|
|
1255
1255
|
},
|
|
1256
|
+
"fal-ai/hunyuan-image/v3": {
|
|
1257
|
+
"description": "A powerful native multimodal image generation model"
|
|
1258
|
+
},
|
|
1256
1259
|
"fal-ai/imagen4/preview": {
|
|
1257
1260
|
"description": "High-quality image generation model provided by Google."
|
|
1258
1261
|
},
|
|
@@ -1253,6 +1253,9 @@
|
|
|
1253
1253
|
"fal-ai/flux/schnell": {
|
|
1254
1254
|
"description": "FLUX.1 [schnell] es un modelo generador de imágenes con 12 mil millones de parámetros, enfocado en la generación rápida de imágenes de alta calidad."
|
|
1255
1255
|
},
|
|
1256
|
+
"fal-ai/hunyuan-image/v3": {
|
|
1257
|
+
"description": "Un potente modelo nativo de generación de imágenes multimodales"
|
|
1258
|
+
},
|
|
1256
1259
|
"fal-ai/imagen4/preview": {
|
|
1257
1260
|
"description": "Modelo de generación de imágenes de alta calidad proporcionado por Google."
|
|
1258
1261
|
},
|
|
@@ -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": "مدل تولید تصویر با کیفیت بالا ارائه شده توسط گوگل"
|
|
1258
1261
|
},
|
|
@@ -1253,6 +1253,9 @@
|
|
|
1253
1253
|
"fal-ai/flux/schnell": {
|
|
1254
1254
|
"description": "FLUX.1 [schnell] est un modèle de génération d'images de 12 milliards de paramètres, spécialisé dans la génération rapide d'images de haute qualité."
|
|
1255
1255
|
},
|
|
1256
|
+
"fal-ai/hunyuan-image/v3": {
|
|
1257
|
+
"description": "Un puissant modèle natif de génération d'images multimodales"
|
|
1258
|
+
},
|
|
1256
1259
|
"fal-ai/imagen4/preview": {
|
|
1257
1260
|
"description": "Modèle de génération d'images de haute qualité fourni par Google."
|
|
1258
1261
|
},
|
|
@@ -1253,6 +1253,9 @@
|
|
|
1253
1253
|
"fal-ai/flux/schnell": {
|
|
1254
1254
|
"description": "FLUX.1 [schnell] è un modello di generazione immagini con 12 miliardi di parametri, focalizzato sulla generazione rapida di immagini di alta qualità."
|
|
1255
1255
|
},
|
|
1256
|
+
"fal-ai/hunyuan-image/v3": {
|
|
1257
|
+
"description": "Un potente modello nativo di generazione di immagini multimodali"
|
|
1258
|
+
},
|
|
1256
1259
|
"fal-ai/imagen4/preview": {
|
|
1257
1260
|
"description": "Modello di generazione immagini di alta qualità fornito da Google."
|
|
1258
1261
|
},
|
|
@@ -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
|
},
|
|
@@ -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
|
},
|
|
@@ -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
|
},
|
|
@@ -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
|
},
|
|
@@ -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
|
},
|
|
@@ -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
|
},
|
|
@@ -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
|
},
|
|
@@ -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
|
},
|
|
@@ -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
|
},
|
|
@@ -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.
|
|
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,
|
|
226
|
+
provider: string,
|
|
232
227
|
): Promise<CreateImageResponse> {
|
|
233
|
-
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
|
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
|
|
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({
|
|
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(
|
|
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({
|
|
997
|
-
|
|
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({
|
|
1002
|
-
|
|
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({
|
|
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
|
|
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;
|
|
@@ -33,6 +33,7 @@ export class LobeFalAI implements LobeRuntimeAI {
|
|
|
33
33
|
['cfg', 'guidance_scale'],
|
|
34
34
|
['imageUrl', 'image_url'],
|
|
35
35
|
['imageUrls', 'image_urls'],
|
|
36
|
+
['size', 'image_size'],
|
|
36
37
|
]);
|
|
37
38
|
|
|
38
39
|
const defaultInput: Record<string, unknown> = {
|
|
@@ -50,12 +51,16 @@ export class LobeFalAI implements LobeRuntimeAI {
|
|
|
50
51
|
);
|
|
51
52
|
|
|
52
53
|
if ('width' in userInput && 'height' in userInput) {
|
|
53
|
-
userInput.
|
|
54
|
-
height
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
54
|
+
if (userInput.size) {
|
|
55
|
+
throw new Error('width/height and size are not supported at the same time');
|
|
56
|
+
} else {
|
|
57
|
+
userInput.image_size = {
|
|
58
|
+
height: userInput.height,
|
|
59
|
+
width: userInput.width,
|
|
60
|
+
};
|
|
61
|
+
delete userInput.width;
|
|
62
|
+
delete userInput.height;
|
|
63
|
+
}
|
|
59
64
|
}
|
|
60
65
|
|
|
61
66
|
const modelsAcceleratedByDefault = new Set<string>(['flux/krea']);
|
|
@@ -66,7 +71,7 @@ export class LobeFalAI implements LobeRuntimeAI {
|
|
|
66
71
|
// Ensure model has fal-ai/ prefix
|
|
67
72
|
let endpoint = model.startsWith('fal-ai/') ? model : `fal-ai/${model}`;
|
|
68
73
|
const hasImageUrls = (params.imageUrls?.length ?? 0) > 0;
|
|
69
|
-
if (
|
|
74
|
+
if (['fal-ai/bytedance/seedream/v4', 'fal-ai/hunyuan-image/v3'].includes(endpoint)) {
|
|
70
75
|
endpoint += hasImageUrls ? '/edit' : '/text-to-image';
|
|
71
76
|
} else if (endpoint === 'fal-ai/nano-banana' && hasImageUrls) {
|
|
72
77
|
endpoint += '/edit';
|
|
@@ -563,7 +563,22 @@ describe('NewAPI Runtime - 100% Branch Coverage', () => {
|
|
|
563
563
|
|
|
564
564
|
if (inputPrice !== undefined) {
|
|
565
565
|
const outputPrice = inputPrice * (pricing.completion_ratio || 1);
|
|
566
|
-
enhancedModel.pricing = {
|
|
566
|
+
enhancedModel.pricing = {
|
|
567
|
+
units: [
|
|
568
|
+
{
|
|
569
|
+
name: 'textInput',
|
|
570
|
+
unit: 'millionTokens',
|
|
571
|
+
strategy: 'fixed',
|
|
572
|
+
rate: inputPrice,
|
|
573
|
+
},
|
|
574
|
+
{
|
|
575
|
+
name: 'textOutput',
|
|
576
|
+
unit: 'millionTokens',
|
|
577
|
+
strategy: 'fixed',
|
|
578
|
+
rate: outputPrice,
|
|
579
|
+
},
|
|
580
|
+
],
|
|
581
|
+
};
|
|
567
582
|
}
|
|
568
583
|
}
|
|
569
584
|
|
|
@@ -582,8 +597,18 @@ describe('NewAPI Runtime - 100% Branch Coverage', () => {
|
|
|
582
597
|
});
|
|
583
598
|
|
|
584
599
|
// Verify pricing results
|
|
585
|
-
expect(enrichedModels[0].pricing).toEqual({
|
|
586
|
-
|
|
600
|
+
expect(enrichedModels[0].pricing).toEqual({
|
|
601
|
+
units: [
|
|
602
|
+
{ name: 'textInput', unit: 'millionTokens', strategy: 'fixed', rate: 40 },
|
|
603
|
+
{ name: 'textOutput', unit: 'millionTokens', strategy: 'fixed', rate: 120 },
|
|
604
|
+
],
|
|
605
|
+
}); // model_price * 2, input * completion_ratio
|
|
606
|
+
expect(enrichedModels[1].pricing).toEqual({
|
|
607
|
+
units: [
|
|
608
|
+
{ name: 'textInput', unit: 'millionTokens', strategy: 'fixed', rate: 10 },
|
|
609
|
+
{ name: 'textOutput', unit: 'millionTokens', strategy: 'fixed', rate: 10 },
|
|
610
|
+
],
|
|
611
|
+
}); // model_ratio * 2, input * 1 (default)
|
|
587
612
|
expect(enrichedModels[2].pricing).toBeUndefined(); // quota_type = 1, skipped
|
|
588
613
|
|
|
589
614
|
// Verify provider detection
|
|
@@ -20,41 +20,21 @@ export interface NewAPIPricing {
|
|
|
20
20
|
model_name: string;
|
|
21
21
|
model_price?: number;
|
|
22
22
|
model_ratio?: number;
|
|
23
|
-
|
|
23
|
+
/** 0: Pay-per-token, 1: Pay-per-call */
|
|
24
|
+
quota_type: number;
|
|
24
25
|
supported_endpoint_types?: string[];
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
const handlePayload = (payload: ChatStreamPayload) => {
|
|
28
|
-
//
|
|
29
|
+
// Handle OpenAI responses API mode
|
|
29
30
|
if (
|
|
30
31
|
responsesAPIModels.has(payload.model) ||
|
|
31
32
|
payload.model.includes('gpt-') ||
|
|
32
33
|
/^o\d/.test(payload.model)
|
|
33
34
|
) {
|
|
34
|
-
return { ...payload, apiMode: 'responses' }
|
|
35
|
+
return { ...payload, apiMode: 'responses' };
|
|
35
36
|
}
|
|
36
|
-
return payload
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
// 根据 owned_by 字段判断提供商(基于 NewAPI 的 channel name)
|
|
40
|
-
const getProviderFromOwnedBy = (ownedBy: string): string => {
|
|
41
|
-
const normalizedOwnedBy = ownedBy.toLowerCase();
|
|
42
|
-
|
|
43
|
-
if (normalizedOwnedBy.includes('claude') || normalizedOwnedBy.includes('anthropic')) {
|
|
44
|
-
return 'anthropic';
|
|
45
|
-
}
|
|
46
|
-
if (normalizedOwnedBy.includes('google') || normalizedOwnedBy.includes('gemini')) {
|
|
47
|
-
return 'google';
|
|
48
|
-
}
|
|
49
|
-
if (normalizedOwnedBy.includes('xai') || normalizedOwnedBy.includes('grok')) {
|
|
50
|
-
return 'xai';
|
|
51
|
-
}
|
|
52
|
-
if (normalizedOwnedBy.includes('ali') || normalizedOwnedBy.includes('qwen')) {
|
|
53
|
-
return 'qwen';
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// 默认为 openai
|
|
57
|
-
return 'openai';
|
|
37
|
+
return payload;
|
|
58
38
|
};
|
|
59
39
|
|
|
60
40
|
export const LobeNewAPIAI = createRouterRuntime({
|
|
@@ -66,16 +46,16 @@ export const LobeNewAPIAI = createRouterRuntime({
|
|
|
66
46
|
},
|
|
67
47
|
id: ModelProvider.NewAPI,
|
|
68
48
|
models: async ({ client: openAIClient }) => {
|
|
69
|
-
//
|
|
49
|
+
// Get base URL (remove trailing API version paths like /v1, /v1beta, etc.)
|
|
70
50
|
const baseURL = openAIClient.baseURL.replace(/\/v\d+[a-z]*\/?$/, '');
|
|
71
51
|
|
|
72
52
|
const modelsPage = (await openAIClient.models.list()) as any;
|
|
73
53
|
const modelList: NewAPIModelCard[] = modelsPage.data || [];
|
|
74
54
|
|
|
75
|
-
//
|
|
55
|
+
// Try to get pricing information to enrich model details
|
|
76
56
|
let pricingMap: Map<string, NewAPIPricing> = new Map();
|
|
77
57
|
try {
|
|
78
|
-
//
|
|
58
|
+
// Use saved baseURL
|
|
79
59
|
const pricingResponse = await fetch(`${baseURL}/api/pricing`, {
|
|
80
60
|
headers: {
|
|
81
61
|
Authorization: `Bearer ${openAIClient.apiKey}`,
|
|
@@ -99,22 +79,22 @@ export const LobeNewAPIAI = createRouterRuntime({
|
|
|
99
79
|
const enrichedModelList = modelList.map((model) => {
|
|
100
80
|
let enhancedModel: any = { ...model };
|
|
101
81
|
|
|
102
|
-
//
|
|
82
|
+
// add pricing info
|
|
103
83
|
const pricing = pricingMap.get(model.id);
|
|
104
84
|
if (pricing) {
|
|
105
|
-
// NewAPI
|
|
106
|
-
// - quota_type: 0
|
|
107
|
-
// - model_ratio:
|
|
108
|
-
// - model_price:
|
|
109
|
-
// - completion_ratio:
|
|
85
|
+
// NewAPI pricing calculation logic:
|
|
86
|
+
// - quota_type: 0 means pay-per-token, 1 means pay-per-call
|
|
87
|
+
// - model_ratio: multiplier relative to base price (base price = $0.002/1K tokens)
|
|
88
|
+
// - model_price: directly specified price (takes priority)
|
|
89
|
+
// - completion_ratio: output price multiplier relative to input price
|
|
110
90
|
//
|
|
111
|
-
// LobeChat
|
|
91
|
+
// LobeChat required format: USD per million tokens
|
|
112
92
|
|
|
113
93
|
let inputPrice: number | undefined;
|
|
114
94
|
let outputPrice: number | undefined;
|
|
115
95
|
|
|
116
96
|
if (pricing.quota_type === 0) {
|
|
117
|
-
//
|
|
97
|
+
// Pay-per-token
|
|
118
98
|
if (pricing.model_price && pricing.model_price > 0) {
|
|
119
99
|
// model_price is a direct price value; need to confirm its unit.
|
|
120
100
|
// Assumption: model_price is the price per 1,000 tokens (i.e., $/1K tokens).
|
|
@@ -124,62 +104,38 @@ export const LobeNewAPIAI = createRouterRuntime({
|
|
|
124
104
|
inputPrice = pricing.model_price * 2;
|
|
125
105
|
} else if (pricing.model_ratio) {
|
|
126
106
|
// model_ratio × $0.002/1K = model_ratio × $2/1M
|
|
127
|
-
inputPrice = pricing.model_ratio * 2; //
|
|
107
|
+
inputPrice = pricing.model_ratio * 2; // Convert to $/1M tokens
|
|
128
108
|
}
|
|
129
109
|
|
|
130
110
|
if (inputPrice !== undefined) {
|
|
131
|
-
//
|
|
111
|
+
// Calculate output price
|
|
132
112
|
outputPrice = inputPrice * (pricing.completion_ratio || 1);
|
|
133
113
|
|
|
134
114
|
enhancedModel.pricing = {
|
|
135
|
-
|
|
136
|
-
|
|
115
|
+
units: [
|
|
116
|
+
{
|
|
117
|
+
name: 'textInput',
|
|
118
|
+
rate: inputPrice,
|
|
119
|
+
strategy: 'fixed',
|
|
120
|
+
unit: 'millionTokens',
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
name: 'textOutput',
|
|
124
|
+
rate: outputPrice,
|
|
125
|
+
strategy: 'fixed',
|
|
126
|
+
unit: 'millionTokens',
|
|
127
|
+
},
|
|
128
|
+
],
|
|
137
129
|
};
|
|
138
130
|
}
|
|
139
131
|
}
|
|
140
|
-
// quota_type === 1
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// 2. 根据优先级处理 provider 信息并缓存路由
|
|
144
|
-
let detectedProvider = 'openai'; // 默认
|
|
145
|
-
|
|
146
|
-
// 优先级1:使用 supported_endpoint_types
|
|
147
|
-
if (model.supported_endpoint_types && model.supported_endpoint_types.length > 0) {
|
|
148
|
-
if (model.supported_endpoint_types.includes('anthropic')) {
|
|
149
|
-
detectedProvider = 'anthropic';
|
|
150
|
-
} else if (model.supported_endpoint_types.includes('gemini')) {
|
|
151
|
-
detectedProvider = 'google';
|
|
152
|
-
} else if (model.supported_endpoint_types.includes('xai')) {
|
|
153
|
-
detectedProvider = 'xai';
|
|
154
|
-
} else if (model.supported_endpoint_types.includes('qwen')) {
|
|
155
|
-
detectedProvider = 'qwen';
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
// 优先级2:使用 owned_by 字段
|
|
159
|
-
else if (model.owned_by) {
|
|
160
|
-
detectedProvider = getProviderFromOwnedBy(model.owned_by);
|
|
161
|
-
}
|
|
162
|
-
// 优先级3:基于模型名称检测
|
|
163
|
-
else {
|
|
164
|
-
detectedProvider = detectModelProvider(model.id);
|
|
132
|
+
// quota_type === 1 pay-per-call is not currently supported
|
|
165
133
|
}
|
|
166
134
|
|
|
167
|
-
// 将检测到的 provider 信息附加到模型上
|
|
168
|
-
enhancedModel._detectedProvider = detectedProvider;
|
|
169
|
-
|
|
170
135
|
return enhancedModel;
|
|
171
136
|
});
|
|
172
137
|
|
|
173
|
-
|
|
174
|
-
const processedModels = await processMultiProviderModelList(enrichedModelList, 'newapi');
|
|
175
|
-
|
|
176
|
-
// 清理临时字段
|
|
177
|
-
return processedModels.map((model: any) => {
|
|
178
|
-
if (model._detectedProvider) {
|
|
179
|
-
delete model._detectedProvider;
|
|
180
|
-
}
|
|
181
|
-
return model;
|
|
182
|
-
});
|
|
138
|
+
return processMultiProviderModelList(enrichedModelList, 'newapi');
|
|
183
139
|
},
|
|
184
140
|
routers: (options) => {
|
|
185
141
|
const userBaseURL = options.baseURL?.replace(/\/v\d+[a-z]*\/?$/, '') || '';
|
|
@@ -215,16 +171,6 @@ export const LobeNewAPIAI = createRouterRuntime({
|
|
|
215
171
|
baseURL: urlJoin(userBaseURL, '/v1'),
|
|
216
172
|
},
|
|
217
173
|
},
|
|
218
|
-
{
|
|
219
|
-
apiType: 'qwen',
|
|
220
|
-
models: LOBE_DEFAULT_MODEL_LIST.map((m) => m.id).filter(
|
|
221
|
-
(id) => detectModelProvider(id) === 'qwen',
|
|
222
|
-
),
|
|
223
|
-
options: {
|
|
224
|
-
...options,
|
|
225
|
-
baseURL: urlJoin(userBaseURL, '/v1'),
|
|
226
|
-
},
|
|
227
|
-
},
|
|
228
174
|
{
|
|
229
175
|
apiType: 'openai',
|
|
230
176
|
options: {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { Block, Grid, GridProps, Text } from '@lobehub/ui';
|
|
3
|
+
import { Block, Grid, GridProps, Select, Text } from '@lobehub/ui';
|
|
4
4
|
import { useTheme } from 'antd-style';
|
|
5
5
|
import { ReactNode, memo } from 'react';
|
|
6
6
|
import { Center } from 'react-layout-kit';
|
|
@@ -13,6 +13,19 @@ export interface SizeSelectProps extends Omit<GridProps, 'children' | 'onChange'
|
|
|
13
13
|
value?: 'auto' | string;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Check if a size value can be parsed as valid aspect ratio
|
|
18
|
+
*/
|
|
19
|
+
const canParseAsRatio = (value: string): boolean => {
|
|
20
|
+
if (value === 'auto') return true;
|
|
21
|
+
|
|
22
|
+
const parts = value.split('x');
|
|
23
|
+
if (parts.length !== 2) return false;
|
|
24
|
+
|
|
25
|
+
const [width, height] = parts.map(Number);
|
|
26
|
+
return !isNaN(width) && !isNaN(height) && width > 0 && height > 0;
|
|
27
|
+
};
|
|
28
|
+
|
|
16
29
|
const SizeSelect = memo<SizeSelectProps>(({ options, onChange, value, defaultValue, ...rest }) => {
|
|
17
30
|
const theme = useTheme();
|
|
18
31
|
const [active, setActive] = useMergeState('auto', {
|
|
@@ -20,6 +33,16 @@ const SizeSelect = memo<SizeSelectProps>(({ options, onChange, value, defaultVal
|
|
|
20
33
|
onChange,
|
|
21
34
|
value,
|
|
22
35
|
});
|
|
36
|
+
|
|
37
|
+
// Check if all options can be parsed as valid ratios
|
|
38
|
+
const hasInvalidRatio = options?.some((item) => !canParseAsRatio(item.value));
|
|
39
|
+
|
|
40
|
+
// If any option cannot be parsed as ratio, fallback to regular Select
|
|
41
|
+
if (hasInvalidRatio) {
|
|
42
|
+
return (
|
|
43
|
+
<Select onChange={onChange} options={options} style={{ width: '100%' }} value={active} />
|
|
44
|
+
);
|
|
45
|
+
}
|
|
23
46
|
return (
|
|
24
47
|
<Block padding={4} variant={'filled'} {...rest}>
|
|
25
48
|
<Grid gap={4} maxItemWidth={72} rows={16}>
|
|
@@ -3,22 +3,9 @@ import createDebug from 'debug';
|
|
|
3
3
|
|
|
4
4
|
import { appEnv } from '@/envs/app';
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
import { EdgeConfigData, EdgeConfigKeys } from './types';
|
|
7
7
|
|
|
8
|
-
const
|
|
9
|
-
/**
|
|
10
|
-
* Assistant whitelist
|
|
11
|
-
*/
|
|
12
|
-
AssistantBlacklist: 'assistant_blacklist',
|
|
13
|
-
/**
|
|
14
|
-
* Assistant whitelist
|
|
15
|
-
*/
|
|
16
|
-
AssistantWhitelist: 'assistant_whitelist',
|
|
17
|
-
/**
|
|
18
|
-
* Feature flags configuration
|
|
19
|
-
*/
|
|
20
|
-
FeatureFlags: 'feature_flags',
|
|
21
|
-
};
|
|
8
|
+
const debug = createDebug('lobe-server:edge-config');
|
|
22
9
|
|
|
23
10
|
export class EdgeConfig {
|
|
24
11
|
get client(): EdgeConfigClient {
|
|
@@ -38,29 +25,24 @@ export class EdgeConfig {
|
|
|
38
25
|
return isEnabled;
|
|
39
26
|
}
|
|
40
27
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
EdgeConfigKeys.AssistantWhitelist,
|
|
45
|
-
EdgeConfigKeys.AssistantBlacklist,
|
|
46
|
-
]);
|
|
28
|
+
private async getValue<K extends EdgeConfigKeys>(key: K) {
|
|
29
|
+
return this.client.get<EdgeConfigData[K]>(key);
|
|
30
|
+
}
|
|
47
31
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
};
|
|
52
|
-
};
|
|
32
|
+
private async getValues<const K extends EdgeConfigKeys>(keys: K[]) {
|
|
33
|
+
return this.client.getAll<Pick<EdgeConfigData, K>>(keys);
|
|
34
|
+
}
|
|
53
35
|
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
|
|
36
|
+
getAgentRestrictions = async () => {
|
|
37
|
+
const { assistant_blacklist: blacklist, assistant_whitelist: whitelist } = await this.getValues(
|
|
38
|
+
['assistant_blacklist', 'assistant_whitelist'],
|
|
39
|
+
);
|
|
40
|
+
return { blacklist, whitelist };
|
|
57
41
|
};
|
|
58
42
|
|
|
59
43
|
getFeatureFlags = async () => {
|
|
60
|
-
const featureFlags = await this.
|
|
44
|
+
const featureFlags = await this.getValue('feature_flags');
|
|
61
45
|
debug('Feature flags retrieved: %O', featureFlags);
|
|
62
|
-
return featureFlags
|
|
46
|
+
return featureFlags;
|
|
63
47
|
};
|
|
64
48
|
}
|
|
65
|
-
|
|
66
|
-
export { EdgeConfigKeys };
|
|
@@ -4,6 +4,19 @@
|
|
|
4
4
|
* EdgeConfig 完整配置类型
|
|
5
5
|
*/
|
|
6
6
|
export interface EdgeConfigData {
|
|
7
|
+
/**
|
|
8
|
+
* Assistant blacklist
|
|
9
|
+
*/
|
|
7
10
|
assistant_blacklist?: string[];
|
|
11
|
+
/**
|
|
12
|
+
* Assistant whitelist
|
|
13
|
+
*/
|
|
8
14
|
assistant_whitelist?: string[];
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Feature flags configuration
|
|
18
|
+
*/
|
|
19
|
+
feature_flags?: Record<string, boolean | string[]>;
|
|
9
20
|
}
|
|
21
|
+
|
|
22
|
+
export type EdgeConfigKeys = keyof EdgeConfigData;
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
export interface ModelTokensUsage {
|
|
2
|
-
acceptedPredictionTokens?: number;
|
|
3
|
-
inputAudioTokens?: number;
|
|
4
|
-
inputCacheMissTokens?: number;
|
|
5
|
-
inputCachedTokens?: number;
|
|
6
|
-
/**
|
|
7
|
-
* currently only pplx has citation_tokens
|
|
8
|
-
*/
|
|
9
|
-
inputCitationTokens?: number;
|
|
10
|
-
/**
|
|
11
|
-
* user prompt image
|
|
12
|
-
*/
|
|
13
|
-
inputImageTokens?: number;
|
|
14
|
-
/**
|
|
15
|
-
* user prompt input
|
|
16
|
-
*/
|
|
17
|
-
inputTextTokens?: number;
|
|
18
|
-
inputWriteCacheTokens?: number;
|
|
19
|
-
outputAudioTokens?: number;
|
|
20
|
-
outputImageTokens?: number;
|
|
21
|
-
outputReasoningTokens?: number;
|
|
22
|
-
outputTextTokens?: number;
|
|
23
|
-
rejectedPredictionTokens?: number;
|
|
24
|
-
totalInputTokens?: number;
|
|
25
|
-
totalOutputTokens?: number;
|
|
26
|
-
totalTokens?: number;
|
|
27
|
-
}
|