mirror of
https://github.com/lobehub/lobe-chat.git
synced 2026-06-14 19:50:09 +00:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f3a48ea4db | |||
| 73f3066b11 | |||
| 96784b68b5 | |||
| eb0b8a56c7 | |||
| aee5d7144f | |||
| 60924d7742 | |||
| a2a097fbec | |||
| 197118347c | |||
| 2e2b9c4c88 |
@@ -13,6 +13,17 @@
|
||||
# Default is '0' (enabled)
|
||||
# ENABLED_CSP=1
|
||||
|
||||
# SSRF Protection Settings
|
||||
# Set to '1' to allow connections to private IP addresses (disable SSRF protection)
|
||||
# WARNING: Only enable this in trusted environments
|
||||
# Default is '0' (SSRF protection enabled)
|
||||
# SSRF_ALLOW_PRIVATE_IP_ADDRESS=0
|
||||
|
||||
# Whitelist of allowed private IP addresses (comma-separated)
|
||||
# Only takes effect when SSRF_ALLOW_PRIVATE_IP_ADDRESS is '0'
|
||||
# Example: Allow specific internal servers while keeping SSRF protection
|
||||
# SSRF_ALLOW_IP_ADDRESS_LIST=192.168.1.100,10.0.0.50
|
||||
|
||||
########################################
|
||||
########## AI Provider Service #########
|
||||
########################################
|
||||
|
||||
@@ -2,6 +2,81 @@
|
||||
|
||||
# Changelog
|
||||
|
||||
### [Version 1.143.1](https://github.com/lobehub/lobe-chat/compare/v1.143.0...v1.143.1)
|
||||
|
||||
<sup>Released on **2025-12-02**</sup>
|
||||
|
||||
#### 🐛 Bug Fixes
|
||||
|
||||
- **misc**: Deepseek interleaved thinking.
|
||||
|
||||
<br/>
|
||||
|
||||
<details>
|
||||
<summary><kbd>Improvements and Fixes</kbd></summary>
|
||||
|
||||
#### What's fixed
|
||||
|
||||
- **misc**: Deepseek interleaved thinking, closes [#10550](https://github.com/lobehub/lobe-chat/issues/10550) ([73f3066](https://github.com/lobehub/lobe-chat/commit/73f3066))
|
||||
|
||||
</details>
|
||||
|
||||
<div align="right">
|
||||
|
||||
[](#readme-top)
|
||||
|
||||
</div>
|
||||
|
||||
## [Version 1.143.0](https://github.com/lobehub/lobe-chat/compare/v1.142.9...v1.143.0)
|
||||
|
||||
<sup>Released on **2025-12-01**</sup>
|
||||
|
||||
#### ✨ Features
|
||||
|
||||
- **misc**: Support DeepSeek Interleaved thinking.
|
||||
|
||||
<br/>
|
||||
|
||||
<details>
|
||||
<summary><kbd>Improvements and Fixes</kbd></summary>
|
||||
|
||||
#### What's improved
|
||||
|
||||
- **misc**: Support DeepSeek Interleaved thinking, closes [#10478](https://github.com/lobehub/lobe-chat/issues/10478) [#10219](https://github.com/lobehub/lobe-chat/issues/10219) [#10152](https://github.com/lobehub/lobe-chat/issues/10152) ([aee5d71](https://github.com/lobehub/lobe-chat/commit/aee5d71))
|
||||
|
||||
</details>
|
||||
|
||||
<div align="right">
|
||||
|
||||
[](#readme-top)
|
||||
|
||||
</div>
|
||||
|
||||
### [Version 1.142.9](https://github.com/lobehub/lobe-chat/compare/v1.142.8...v1.142.9)
|
||||
|
||||
<sup>Released on **2025-11-02**</sup>
|
||||
|
||||
#### 🐛 Bug Fixes
|
||||
|
||||
- **misc**: OIDC error when connecting to self-host instance.
|
||||
|
||||
<br/>
|
||||
|
||||
<details>
|
||||
<summary><kbd>Improvements and Fixes</kbd></summary>
|
||||
|
||||
#### What's fixed
|
||||
|
||||
- **misc**: OIDC error when connecting to self-host instance, closes [#9916](https://github.com/lobehub/lobe-chat/issues/9916) ([2e2b9c4](https://github.com/lobehub/lobe-chat/commit/2e2b9c4))
|
||||
|
||||
</details>
|
||||
|
||||
<div align="right">
|
||||
|
||||
[](#readme-top)
|
||||
|
||||
</div>
|
||||
|
||||
### [Version 1.142.8](https://github.com/lobehub/lobe-chat/compare/v1.142.7...v1.142.8)
|
||||
|
||||
<sup>Released on **2025-10-30**</sup>
|
||||
|
||||
@@ -381,14 +381,14 @@ In addition, these plugins are not limited to news aggregation, but can also ext
|
||||
|
||||
<!-- PLUGIN LIST -->
|
||||
|
||||
| Recent Submits | Description |
|
||||
| ---------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| [Shopping tools](https://lobechat.com/discover/plugin/ShoppingTools)<br/><sup>By **shoppingtools** on **2025-10-27**</sup> | Search for products on eBay & AliExpress, find eBay events & coupons. Get prompt examples.<br/>`shopping` `e-bay` `ali-express` `coupons` |
|
||||
| [PortfolioMeta](https://lobechat.com/discover/plugin/StockData)<br/><sup>By **portfoliometa** on **2025-09-27**</sup> | Analyze stocks and get comprehensive real-time investment data and analytics.<br/>`stock` |
|
||||
| [Web](https://lobechat.com/discover/plugin/web)<br/><sup>By **Proghit** on **2025-01-24**</sup> | Smart web search that reads and analyzes pages to deliver comprehensive answers from Google results.<br/>`web` `search` |
|
||||
| [Bing_websearch](https://lobechat.com/discover/plugin/Bingsearch-identifier)<br/><sup>By **FineHow** on **2024-12-22**</sup> | Search for information from the internet base BingApi<br/>`bingsearch` |
|
||||
| Recent Submits | Description |
|
||||
| -------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| [PortfolioMeta](https://lobechat.com/discover/plugin/StockData)<br/><sup>By **portfoliometa** on **2025-11-28**</sup> | Analyze stocks and get comprehensive real-time investment data and analytics.<br/>`stock` |
|
||||
| [SEO](https://lobechat.com/discover/plugin/SEO)<br/><sup>By **orrenprunckun** on **2025-11-14**</sup> | Enter any URL and keyword and get an On-Page SEO analysis & insights!<br/>`seo` |
|
||||
| [Shopping tools](https://lobechat.com/discover/plugin/ShoppingTools)<br/><sup>By **shoppingtools** on **2025-10-27**</sup> | Search for products on eBay & AliExpress, find eBay events & coupons. Get prompt examples.<br/>`shopping` `e-bay` `ali-express` `coupons` |
|
||||
| [Web](https://lobechat.com/discover/plugin/web)<br/><sup>By **Proghit** on **2025-01-24**</sup> | Smart web search that reads and analyzes pages to deliver comprehensive answers from Google results.<br/>`web` `search` |
|
||||
|
||||
> 📊 Total plugins: [<kbd>**42**</kbd>](https://lobechat.com/discover/plugins)
|
||||
> 📊 Total plugins: [<kbd>**41**</kbd>](https://lobechat.com/discover/plugins)
|
||||
|
||||
<!-- PLUGIN LIST -->
|
||||
|
||||
|
||||
+7
-7
@@ -374,14 +374,14 @@ LobeChat 的插件生态系统是其核心功能的重要扩展,它极大地
|
||||
|
||||
<!-- PLUGIN LIST -->
|
||||
|
||||
| 最近新增 | 描述 |
|
||||
| -------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
||||
| [购物工具](https://lobechat.com/discover/plugin/ShoppingTools)<br/><sup>By **shoppingtools** on **2025-10-27**</sup> | 在 eBay 和 AliExpress 上搜索产品,查找 eBay 活动和优惠券。获取快速示例。<br/>`购物` `e-bay` `ali-express` `优惠券` |
|
||||
| [PortfolioMeta](https://lobechat.com/discover/plugin/StockData)<br/><sup>By **portfoliometa** on **2025-09-27**</sup> | 分析股票并获取全面的实时投资数据和分析。<br/>`股票` |
|
||||
| [网页](https://lobechat.com/discover/plugin/web)<br/><sup>By **Proghit** on **2025-01-24**</sup> | 智能网页搜索,读取和分析页面,以提供来自 Google 结果的全面答案。<br/>`网页` `搜索` |
|
||||
| [必应网页搜索](https://lobechat.com/discover/plugin/Bingsearch-identifier)<br/><sup>By **FineHow** on **2024-12-22**</sup> | 通过 BingApi 搜索互联网上的信息<br/>`bingsearch` |
|
||||
| 最近新增 | 描述 |
|
||||
| --------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
||||
| [PortfolioMeta](https://lobechat.com/discover/plugin/StockData)<br/><sup>By **portfoliometa** on **2025-11-28**</sup> | 分析股票并获取全面的实时投资数据和分析。<br/>`股票` |
|
||||
| [SEO](https://lobechat.com/discover/plugin/SEO)<br/><sup>By **orrenprunckun** on **2025-11-14**</sup> | 输入任何 URL 和关键词,获取页面 SEO 分析和见解!<br/>`seo` |
|
||||
| [购物工具](https://lobechat.com/discover/plugin/ShoppingTools)<br/><sup>By **shoppingtools** on **2025-10-27**</sup> | 在 eBay 和 AliExpress 上搜索产品,查找 eBay 活动和优惠券。获取快速示例。<br/>`购物` `e-bay` `ali-express` `优惠券` |
|
||||
| [网页](https://lobechat.com/discover/plugin/web)<br/><sup>By **Proghit** on **2025-01-24**</sup> | 智能网页搜索,读取和分析页面,以提供来自 Google 结果的全面答案。<br/>`网页` `搜索` |
|
||||
|
||||
> 📊 Total plugins: [<kbd>**42**</kbd>](https://lobechat.com/discover/plugins)
|
||||
> 📊 Total plugins: [<kbd>**41**</kbd>](https://lobechat.com/discover/plugins)
|
||||
|
||||
<!-- PLUGIN LIST -->
|
||||
|
||||
|
||||
@@ -1,4 +1,18 @@
|
||||
[
|
||||
{
|
||||
"children": {
|
||||
"features": ["Support DeepSeek Interleaved thinking."]
|
||||
},
|
||||
"date": "2025-12-01",
|
||||
"version": "1.143.0"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"fixes": ["OIDC error when connecting to self-host instance."]
|
||||
},
|
||||
"date": "2025-11-02",
|
||||
"version": "1.142.9"
|
||||
},
|
||||
{
|
||||
"children": {},
|
||||
"date": "2025-10-30",
|
||||
|
||||
@@ -127,16 +127,62 @@ For specific content, please refer to the [Feature Flags](/docs/self-hosting/adv
|
||||
### `SSRF_ALLOW_PRIVATE_IP_ADDRESS`
|
||||
|
||||
- Type: Optional
|
||||
- Description: Allow to connect private IP address. In a trusted environment, it can be set to true to turn off SSRF protection.
|
||||
- Description: Controls whether to allow connections to private IP addresses. Set to `1` to disable SSRF protection and allow all private IP addresses. In a trusted environment (e.g., internal network), this can be enabled to allow access to internal resources.
|
||||
- Default: `0`
|
||||
- Example: `1` or `0`
|
||||
|
||||
<Callout type="warning">
|
||||
**Security Notice**: Enabling this option will disable SSRF protection and allow connections to private
|
||||
IP addresses (127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, etc.). Only enable this in
|
||||
trusted environments where you need to access internal network resources.
|
||||
</Callout>
|
||||
|
||||
**Use Cases**:
|
||||
|
||||
LobeChat performs SSRF security checks in the following scenarios:
|
||||
|
||||
1. **Image/Video URL to Base64 Conversion**: When processing media messages (e.g., vision models, multimodal models), LobeChat converts image and video URLs to base64 format. This check prevents malicious users from accessing internal network resources.
|
||||
|
||||
Examples:
|
||||
|
||||
- Image: A user sends an image message with URL `http://192.168.1.100/admin/secrets.png`
|
||||
- Video: A user sends a video message with URL `http://10.0.0.50/internal/meeting.mp4`
|
||||
|
||||
Without SSRF protection, these requests could expose internal network resources.
|
||||
|
||||
2. **Web Crawler**: When using web crawling features to fetch external content.
|
||||
|
||||
3. **Proxy Requests**: When proxying external API requests.
|
||||
|
||||
**Configuration Examples**:
|
||||
|
||||
```bash
|
||||
# Scenario 1: Public deployment (recommended)
|
||||
# Block all private IP addresses for security
|
||||
SSRF_ALLOW_PRIVATE_IP_ADDRESS=0
|
||||
|
||||
# Scenario 2: Internal deployment
|
||||
# Allow all private IP addresses to access internal image servers
|
||||
SSRF_ALLOW_PRIVATE_IP_ADDRESS=1
|
||||
|
||||
# Scenario 3: Hybrid deployment (most common)
|
||||
# Block private IPs by default, but allow specific trusted internal servers
|
||||
SSRF_ALLOW_PRIVATE_IP_ADDRESS=0
|
||||
SSRF_ALLOW_IP_ADDRESS_LIST=192.168.1.100,10.0.0.50
|
||||
```
|
||||
|
||||
### `SSRF_ALLOW_IP_ADDRESS_LIST`
|
||||
|
||||
- Type: Optional
|
||||
- Description: Allow private IP address list, multiple IP addresses are separated by commas. Only when `SSRF_ALLOW_PRIVATE_IP_ADDRESS` is `0`, it takes effect.
|
||||
- Description: Whitelist of allowed IP addresses, separated by commas. Only takes effect when `SSRF_ALLOW_PRIVATE_IP_ADDRESS` is `0`. Use this to allow specific internal IP addresses while keeping SSRF protection enabled for other private IPs.
|
||||
- Default: -
|
||||
- Example: `198.18.1.62,224.0.0.3`
|
||||
- Example: `192.168.1.100,10.0.0.50,172.16.0.10`
|
||||
|
||||
**Common Use Cases**:
|
||||
|
||||
- Allow access to internal image storage server: `192.168.1.100`
|
||||
- Allow access to internal API gateway: `10.0.0.50`
|
||||
- Allow access to internal documentation server: `172.16.0.10`
|
||||
|
||||
### `ENABLE_AUTH_PROTECTION`
|
||||
|
||||
|
||||
@@ -123,16 +123,61 @@ LobeChat 在部署时提供了一些额外的配置项,你可以使用环境
|
||||
### `SSRF_ALLOW_PRIVATE_IP_ADDRESS`
|
||||
|
||||
- 类型:可选
|
||||
- 描述:是否允许连接私有 IP 地址。在可信环境中可以设置为 true 来关闭 SSRF 防护。
|
||||
- 描述:控制是否允许连接私有 IP 地址。设置为 `1` 时将关闭 SSRF 防护并允许所有私有 IP 地址。在可信环境(如内网部署)中,可以启用此选项以访问内部资源。
|
||||
- 默认值:`0`
|
||||
- 示例:`1` or `0`
|
||||
- 示例:`1` 或 `0`
|
||||
|
||||
<Callout type="warning">
|
||||
**安全提示**:启用此选项将关闭 SSRF 防护,允许连接私有 IP 地址段(127.0.0.0/8、10.0.0.0/8、172.16.0.0/12、192.168.0.0/16
|
||||
等)。仅在需要访问内网资源的可信环境中启用。
|
||||
</Callout>
|
||||
|
||||
**应用场景**:
|
||||
|
||||
LobeChat 会在以下场景执行 SSRF 安全检查:
|
||||
|
||||
1. **图片 / 视频 URL 转 Base64**:在处理媒体消息时(例如视觉模型、多模态模型),LobeChat 会将图片和视频 URL 转换为 base64 格式。此检查可防止恶意用户通过媒体 URL 访问内网资源。
|
||||
|
||||
举例:
|
||||
|
||||
- 图片:用户发送图片消息,URL 为 `http://192.168.1.100/admin/secrets.png`
|
||||
- 视频:用户发送视频消息,URL 为 `http://10.0.0.50/internal/meeting.mp4`
|
||||
|
||||
若无 SSRF 防护,这些请求可能导致内网资源泄露。
|
||||
|
||||
2. **网页爬取**:使用网页爬取功能获取外部内容时。
|
||||
|
||||
3. **代理请求**:代理外部 API 请求时。
|
||||
|
||||
**配置示例**:
|
||||
|
||||
```bash
|
||||
# 场景 1:公网部署(推荐)
|
||||
# 阻止所有私有 IP 访问,保证安全
|
||||
SSRF_ALLOW_PRIVATE_IP_ADDRESS=0
|
||||
|
||||
# 场景 2:内网部署
|
||||
# 允许所有私有 IP,可访问内网图片服务器等资源
|
||||
SSRF_ALLOW_PRIVATE_IP_ADDRESS=1
|
||||
|
||||
# 场景 3:混合部署(最常见)
|
||||
# 默认阻止私有 IP,但允许特定可信的内网服务器
|
||||
SSRF_ALLOW_PRIVATE_IP_ADDRESS=0
|
||||
SSRF_ALLOW_IP_ADDRESS_LIST=192.168.1.100,10.0.0.50
|
||||
```
|
||||
|
||||
### `SSRF_ALLOW_IP_ADDRESS_LIST`
|
||||
|
||||
- 类型:可选
|
||||
- 说明:允许的私有 IP 地址列表,多个 IP 地址用逗号分隔。仅在 `SSRF_ALLOW_PRIVATE_IP_ADDRESS` 为 `0` 时生效。
|
||||
- 描述:允许访问的 IP 地址白名单,多个 IP 地址用逗号分隔。仅在 `SSRF_ALLOW_PRIVATE_IP_ADDRESS` 为 `0` 时生效。使用此选项可以在保持 SSRF 防护的同时,允许访问特定的内网 IP 地址。
|
||||
- 默认值:-
|
||||
- 示例:`198.18.1.62,224.0.0.3`
|
||||
- 示例:`192.168.1.100,10.0.0.50,172.16.0.10`
|
||||
|
||||
**常见使用场景**:
|
||||
|
||||
- 允许访问内网图片存储服务器:`192.168.1.100`
|
||||
- 允许访问内网 API 网关:`10.0.0.50`
|
||||
- 允许访问内网文档服务器:`172.16.0.10`
|
||||
|
||||
### `ENABLE_AUTH_PROTECTION`
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ tags:
|
||||
|
||||
# Using ComfyUI in LobeChat
|
||||
|
||||
<Image alt={'Using ComfyUI in LobeChat'} cover src={'https://github.com/lobehub/lobe-chat/assets/17870709/c9e5eafc-ca22-496b-a88d-cc0ae53bf720'} />
|
||||
<Image alt={'Using ComfyUI in LobeChat'} cover src={'https://hub-apac-1.lobeobjects.space/docs/e9b811f248a1db2bd1be1af888cf9b9d.png'} />
|
||||
|
||||
This documentation will guide you on how to use [ComfyUI](https://github.com/comfyanonymous/ComfyUI) in LobeChat for high-quality AI image generation and editing.
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ tags:
|
||||
|
||||
# 在 LobeChat 中使用 ComfyUI
|
||||
|
||||
<Image alt={'在 LobeChat 中使用 ComfyUI'} cover src={'https://github.com/lobehub/lobe-chat/assets/17870709/c9e5eafc-ca22-496b-a88d-cc0ae53bf720'} />
|
||||
<Image alt={'在 LobeChat 中使用 ComfyUI'} cover src={'https://hub-apac-1.lobeobjects.space/docs/e9b811f248a1db2bd1be1af888cf9b9d.png'} />
|
||||
|
||||
本文档将指导你如何在 LobeChat 中使用 [ComfyUI](https://github.com/comfyanonymous/ComfyUI) 进行高质量的 AI 图像生成和编辑。
|
||||
|
||||
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@lobehub/chat",
|
||||
"version": "1.142.8",
|
||||
"version": "1.143.1",
|
||||
"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.",
|
||||
"keywords": [
|
||||
"framework",
|
||||
@@ -156,6 +156,7 @@
|
||||
"@lobechat/database": "workspace:*",
|
||||
"@lobechat/electron-client-ipc": "workspace:*",
|
||||
"@lobechat/electron-server-ipc": "workspace:*",
|
||||
"@lobechat/fetch-sse": "workspace:*",
|
||||
"@lobechat/file-loaders": "workspace:*",
|
||||
"@lobechat/model-runtime": "workspace:*",
|
||||
"@lobechat/observability-otel": "workspace:*",
|
||||
|
||||
@@ -66,6 +66,7 @@ export class MessageCleanupProcessor extends BaseProcessor {
|
||||
content: message.content,
|
||||
role: message.role,
|
||||
...(message.tool_calls && { tool_calls: message.tool_calls }),
|
||||
...(message.reasoning && { reasoning: message.reasoning }),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "@lobechat/fetch-sse",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "SSE fetch utilities with streaming support",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./src/index.ts",
|
||||
"default": "./src/index.ts"
|
||||
},
|
||||
"./parseError": {
|
||||
"types": "./src/parseError.ts",
|
||||
"default": "./src/parseError.ts"
|
||||
}
|
||||
},
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"scripts": {
|
||||
"test": "vitest",
|
||||
"test:coverage": "vitest --coverage --silent='passed-only'"
|
||||
},
|
||||
"dependencies": {
|
||||
"@lobechat/const": "workspace:*",
|
||||
"@lobechat/model-runtime": "workspace:*",
|
||||
"@lobechat/types": "workspace:*",
|
||||
"@lobechat/utils": "workspace:*",
|
||||
"i18next": "^24.2.1"
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -1,10 +1,10 @@
|
||||
import { MESSAGE_CANCEL_FLAT } from '@lobechat/const';
|
||||
import { ChatMessageError } from '@lobechat/types';
|
||||
import { FetchEventSourceInit } from '@lobechat/utils/client/fetchEventSource/index';
|
||||
import { fetchEventSource } from '@lobechat/utils/client/fetchEventSource/index';
|
||||
import { sleep } from '@lobechat/utils/sleep';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { FetchEventSourceInit } from '../../client/fetchEventSource';
|
||||
import { fetchEventSource } from '../../client/fetchEventSource';
|
||||
import { sleep } from '../../sleep';
|
||||
import { fetchSSE } from '../fetchSSE';
|
||||
|
||||
// 模拟 i18next
|
||||
@@ -12,7 +12,7 @@ vi.mock('i18next', () => ({
|
||||
t: vi.fn((key) => `translated_${key}`),
|
||||
}));
|
||||
|
||||
vi.mock('../../client/fetchEventSource', () => ({
|
||||
vi.mock('@lobechat/utils/client/fetchEventSource/index', () => ({
|
||||
fetchEventSource: vi.fn(),
|
||||
}));
|
||||
|
||||
+7
-4
@@ -1,14 +1,14 @@
|
||||
import { ErrorResponse } from '@lobechat/types';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { getMessageError } from '../parseError';
|
||||
|
||||
// 模拟 i18next
|
||||
// Mock i18next
|
||||
vi.mock('i18next', () => ({
|
||||
t: vi.fn((key) => `translated_${key}`),
|
||||
}));
|
||||
|
||||
// 模拟 Response
|
||||
// Mock Response
|
||||
const createMockResponse = (body: any, ok: boolean, status: number = 200) => ({
|
||||
ok,
|
||||
status,
|
||||
@@ -38,11 +38,14 @@ const createMockResponse = (body: any, ok: boolean, status: number = 200) => ({
|
||||
},
|
||||
});
|
||||
|
||||
// 在每次测试后清理所有模拟
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('getMessageError', () => {
|
||||
it('should handle business error correctly', async () => {
|
||||
const mockErrorResponse: ErrorResponse = {
|
||||
@@ -12,9 +12,9 @@ import {
|
||||
ResponseAnimation,
|
||||
ResponseAnimationStyle,
|
||||
} from '@lobechat/types';
|
||||
import { fetchEventSource } from '@lobechat/utils/client/fetchEventSource/index';
|
||||
import { nanoid } from '@lobechat/utils/uuid';
|
||||
|
||||
import { fetchEventSource } from '../client/fetchEventSource';
|
||||
import { nanoid } from '../uuid';
|
||||
import { getMessageError } from './parseError';
|
||||
|
||||
type SSEFinishType = 'done' | 'error' | 'abort';
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ChatMessageError, ErrorResponse, ErrorType } from '@lobechat/types';
|
||||
import { t } from 'i18next';
|
||||
|
||||
export const getMessageError = async (response: Response) => {
|
||||
export const getMessageError = async (response: Response): Promise<ChatMessageError> => {
|
||||
let chatMessageError: ChatMessageError;
|
||||
|
||||
// try to get the biz error
|
||||
@@ -9,13 +9,13 @@ export const getMessageError = async (response: Response) => {
|
||||
const data = (await response.json()) as ErrorResponse;
|
||||
chatMessageError = {
|
||||
body: data.body,
|
||||
message: t(`response.${data.errorType}` as any, { ns: 'error' }),
|
||||
message: t(`response.${data.errorType}`, { ns: 'error' }),
|
||||
type: data.errorType,
|
||||
};
|
||||
} catch {
|
||||
// if not return, then it's a common error
|
||||
chatMessageError = {
|
||||
message: t(`response.${response.status}` as any, { ns: 'error' }),
|
||||
message: t(`response.${response.status}`, { ns: 'error' }),
|
||||
type: response.status as ErrorType,
|
||||
};
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import { imageUrlToBase64 } from '@lobechat/utils';
|
||||
import { OpenAI } from 'openai';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { OpenAIChatMessage, UserMessageContentPart } from '../../types/chat';
|
||||
import { imageUrlToBase64 } from '../../utils/imageToBase64';
|
||||
import { parseDataUri } from '../../utils/uriParser';
|
||||
import {
|
||||
buildAnthropicBlock,
|
||||
@@ -12,16 +12,22 @@ import {
|
||||
} from './anthropic';
|
||||
|
||||
// Mock the parseDataUri function since it's an implementation detail
|
||||
vi.mock('../../utils/uriParser', () => ({
|
||||
parseDataUri: vi.fn().mockReturnValue({
|
||||
mimeType: 'image/jpeg',
|
||||
base64: 'base64EncodedString',
|
||||
type: 'base64',
|
||||
}),
|
||||
vi.mock('../../utils/uriParser');
|
||||
vi.mock('@lobechat/utils', () => ({
|
||||
imageUrlToBase64: vi.fn(),
|
||||
}));
|
||||
vi.mock('../../utils/imageToBase64');
|
||||
|
||||
describe('anthropicHelpers', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
// Set default mock implementation for parseDataUri
|
||||
vi.mocked(parseDataUri).mockReturnValue({
|
||||
mimeType: 'image/jpeg',
|
||||
base64: 'base64EncodedString',
|
||||
type: 'base64',
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildAnthropicBlock', () => {
|
||||
it('should return the content as is for text type', async () => {
|
||||
const content: UserMessageContentPart = { type: 'text', text: 'Hello!' };
|
||||
@@ -52,7 +58,7 @@ describe('anthropicHelpers', () => {
|
||||
base64: null,
|
||||
type: 'url',
|
||||
});
|
||||
vi.mocked(imageUrlToBase64).mockResolvedValue({
|
||||
vi.mocked(imageUrlToBase64).mockResolvedValueOnce({
|
||||
base64: 'convertedBase64String',
|
||||
mimeType: 'image/jpg',
|
||||
});
|
||||
@@ -82,7 +88,7 @@ describe('anthropicHelpers', () => {
|
||||
base64: null,
|
||||
type: 'url',
|
||||
});
|
||||
vi.mocked(imageUrlToBase64).mockResolvedValue({
|
||||
vi.mocked(imageUrlToBase64).mockResolvedValueOnce({
|
||||
base64: 'convertedBase64String',
|
||||
mimeType: 'image/png',
|
||||
});
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import Anthropic from '@anthropic-ai/sdk';
|
||||
import { imageUrlToBase64 } from '@lobechat/utils';
|
||||
import OpenAI from 'openai';
|
||||
|
||||
import { OpenAIChatMessage, UserMessageContentPart } from '../../types';
|
||||
import { imageUrlToBase64 } from '../../utils/imageToBase64';
|
||||
import { parseDataUri } from '../../utils/uriParser';
|
||||
|
||||
export const buildAnthropicBlock = async (
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// @vitest-environment node
|
||||
import { Type as SchemaType } from '@google/genai';
|
||||
import * as imageToBase64Module from '@lobechat/utils';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { ChatCompletionTool, OpenAIChatMessage, UserMessageContentPart } from '../../types';
|
||||
import * as imageToBase64Module from '../../utils/imageToBase64';
|
||||
import { parseDataUri } from '../../utils/uriParser';
|
||||
import {
|
||||
buildGoogleMessage,
|
||||
|
||||
@@ -5,9 +5,9 @@ import {
|
||||
Part,
|
||||
Type as SchemaType,
|
||||
} from '@google/genai';
|
||||
import { imageUrlToBase64 } from '@lobechat/utils';
|
||||
|
||||
import { ChatCompletionTool, OpenAIChatMessage, UserMessageContentPart } from '../../types';
|
||||
import { imageUrlToBase64 } from '../../utils/imageToBase64';
|
||||
import { safeParseJSON } from '../../utils/safeParseJSON';
|
||||
import { parseDataUri } from '../../utils/uriParser';
|
||||
|
||||
@@ -64,12 +64,9 @@ export const buildGooglePart = async (
|
||||
}
|
||||
|
||||
if (type === 'url') {
|
||||
// For video URLs, we need to fetch and convert to base64
|
||||
// Use imageUrlToBase64 for SSRF protection (works for any binary data including videos)
|
||||
// Note: This might need size/duration limits for practical use
|
||||
const response = await fetch(content.video_url.url);
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
const base64 = Buffer.from(arrayBuffer).toString('base64');
|
||||
const mimeType = response.headers.get('content-type') || 'video/mp4';
|
||||
const { base64, mimeType } = await imageUrlToBase64(content.video_url.url);
|
||||
|
||||
return {
|
||||
inlineData: { data: base64, mimeType },
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { imageUrlToBase64 } from '@lobechat/utils';
|
||||
import OpenAI from 'openai';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { imageUrlToBase64 } from '../../utils/imageToBase64';
|
||||
import { OpenAIChatMessage } from '../../types';
|
||||
import { parseDataUri } from '../../utils/uriParser';
|
||||
import {
|
||||
convertImageUrlToFile,
|
||||
@@ -11,7 +12,9 @@ import {
|
||||
} from './openai';
|
||||
|
||||
// 模拟依赖
|
||||
vi.mock('../../utils/imageToBase64');
|
||||
vi.mock('@lobechat/utils', () => ({
|
||||
imageUrlToBase64: vi.fn(),
|
||||
}));
|
||||
vi.mock('../../utils/uriParser');
|
||||
|
||||
describe('convertMessageContent', () => {
|
||||
@@ -147,11 +150,71 @@ describe('convertOpenAIMessages', () => {
|
||||
|
||||
expect(Promise.all).toHaveBeenCalledTimes(2); // 一次用于消息数组,一次用于内容数组
|
||||
});
|
||||
|
||||
it('should filter out reasoning field from messages', async () => {
|
||||
const messages = [
|
||||
{
|
||||
role: 'assistant',
|
||||
content: 'Hello',
|
||||
reasoning: { content: 'some reasoning', duration: 100 },
|
||||
},
|
||||
{ role: 'user', content: 'Hi' },
|
||||
] as any;
|
||||
|
||||
const result = await convertOpenAIMessages(messages);
|
||||
|
||||
expect(result).toEqual([
|
||||
{ role: 'assistant', content: 'Hello' },
|
||||
{ role: 'user', content: 'Hi' },
|
||||
]);
|
||||
// Ensure reasoning field is removed
|
||||
expect((result[0] as any).reasoning).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should preserve reasoning_content field from messages (for DeepSeek compatibility)', async () => {
|
||||
const messages = [
|
||||
{
|
||||
role: 'assistant',
|
||||
content: 'Hello',
|
||||
reasoning_content: 'some reasoning content',
|
||||
},
|
||||
{ role: 'user', content: 'Hi' },
|
||||
] as any;
|
||||
|
||||
const result = await convertOpenAIMessages(messages);
|
||||
|
||||
expect(result).toEqual([
|
||||
{ role: 'assistant', content: 'Hello', reasoning_content: 'some reasoning content' },
|
||||
{ role: 'user', content: 'Hi' },
|
||||
]);
|
||||
// Ensure reasoning_content field is preserved
|
||||
expect((result[0] as any).reasoning_content).toBe('some reasoning content');
|
||||
});
|
||||
|
||||
it('should filter out reasoning but preserve reasoning_content field', async () => {
|
||||
const messages = [
|
||||
{
|
||||
role: 'assistant',
|
||||
content: 'Hello',
|
||||
reasoning: { content: 'some reasoning', duration: 100 },
|
||||
reasoning_content: 'some reasoning content',
|
||||
},
|
||||
] as any;
|
||||
|
||||
const result = await convertOpenAIMessages(messages);
|
||||
|
||||
expect(result).toEqual([
|
||||
{ role: 'assistant', content: 'Hello', reasoning_content: 'some reasoning content' },
|
||||
]);
|
||||
// Ensure reasoning object is removed but reasoning_content is preserved
|
||||
expect((result[0] as any).reasoning).toBeUndefined();
|
||||
expect((result[0] as any).reasoning_content).toBe('some reasoning content');
|
||||
});
|
||||
});
|
||||
|
||||
describe('convertOpenAIResponseInputs', () => {
|
||||
it('应该正确转换普通文本消息', async () => {
|
||||
const messages: OpenAI.ChatCompletionMessageParam[] = [
|
||||
const messages: OpenAIChatMessage[] = [
|
||||
{ role: 'user', content: 'Hello' },
|
||||
{ role: 'assistant', content: 'Hi there!' },
|
||||
];
|
||||
@@ -165,7 +228,7 @@ describe('convertOpenAIResponseInputs', () => {
|
||||
});
|
||||
|
||||
it('应该正确转换带有工具调用的消息', async () => {
|
||||
const messages: OpenAI.ChatCompletionMessageParam[] = [
|
||||
const messages: OpenAIChatMessage[] = [
|
||||
{
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
@@ -195,7 +258,7 @@ describe('convertOpenAIResponseInputs', () => {
|
||||
});
|
||||
|
||||
it('应该正确转换工具响应消息', async () => {
|
||||
const messages: OpenAI.ChatCompletionMessageParam[] = [
|
||||
const messages: OpenAIChatMessage[] = [
|
||||
{
|
||||
role: 'tool',
|
||||
content: 'Function result',
|
||||
@@ -215,7 +278,7 @@ describe('convertOpenAIResponseInputs', () => {
|
||||
});
|
||||
|
||||
it('应该正确转换包含图片的消息', async () => {
|
||||
const messages: OpenAI.ChatCompletionMessageParam[] = [
|
||||
const messages: OpenAIChatMessage[] = [
|
||||
{
|
||||
role: 'user',
|
||||
content: [
|
||||
@@ -247,7 +310,7 @@ describe('convertOpenAIResponseInputs', () => {
|
||||
});
|
||||
|
||||
it('应该正确处理混合类型的消息序列', async () => {
|
||||
const messages: OpenAI.ChatCompletionMessageParam[] = [
|
||||
const messages: OpenAIChatMessage[] = [
|
||||
{ role: 'user', content: 'I need help with a function' },
|
||||
{
|
||||
role: 'assistant',
|
||||
@@ -287,6 +350,29 @@ describe('convertOpenAIResponseInputs', () => {
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should extract reasoning.content into a separate reasoning item', async () => {
|
||||
const messages: OpenAIChatMessage[] = [
|
||||
{ content: 'system prompts', role: 'system' },
|
||||
{ content: '你好', role: 'user' },
|
||||
{
|
||||
content: 'hello',
|
||||
role: 'assistant',
|
||||
reasoning: { content: 'reasoning content', duration: 2706 },
|
||||
},
|
||||
{ content: '杭州天气如何', role: 'user' },
|
||||
];
|
||||
|
||||
const result = await convertOpenAIResponseInputs(messages);
|
||||
|
||||
expect(result).toEqual([
|
||||
{ content: 'system prompts', role: 'developer' },
|
||||
{ content: '你好', role: 'user' },
|
||||
{ summary: [{ text: 'reasoning content', type: 'summary_text' }], type: 'reasoning' },
|
||||
{ content: 'hello', role: 'assistant' },
|
||||
{ content: '杭州天气如何', role: 'user' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('convertImageUrlToFile', () => {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { imageUrlToBase64 } from '@lobechat/utils';
|
||||
import OpenAI, { toFile } from 'openai';
|
||||
|
||||
import { disableStreamModels, systemToUserModels } from '../../const/models';
|
||||
import { ChatStreamPayload, OpenAIChatMessage } from '../../types';
|
||||
import { imageUrlToBase64 } from '../../utils/imageToBase64';
|
||||
import { parseDataUri } from '../../utils/uriParser';
|
||||
|
||||
export const convertMessageContent = async (
|
||||
@@ -26,26 +26,49 @@ export const convertMessageContent = async (
|
||||
|
||||
export const convertOpenAIMessages = async (messages: OpenAI.ChatCompletionMessageParam[]) => {
|
||||
return (await Promise.all(
|
||||
messages.map(async (message) => ({
|
||||
...message,
|
||||
content:
|
||||
typeof message.content === 'string'
|
||||
? message.content
|
||||
: await Promise.all(
|
||||
(message.content || []).map((c) =>
|
||||
convertMessageContent(c as OpenAI.ChatCompletionContentPart),
|
||||
messages.map(async (message) => {
|
||||
const msg = message as any;
|
||||
|
||||
// Explicitly map only valid ChatCompletionMessageParam fields
|
||||
// Exclude reasoning and reasoning_content fields as they should not be sent in requests
|
||||
const result: any = {
|
||||
content:
|
||||
typeof message.content === 'string'
|
||||
? message.content
|
||||
: await Promise.all(
|
||||
(message.content || []).map((c) =>
|
||||
convertMessageContent(c as OpenAI.ChatCompletionContentPart),
|
||||
),
|
||||
),
|
||||
),
|
||||
})),
|
||||
role: msg.role,
|
||||
};
|
||||
|
||||
// Add optional fields if they exist
|
||||
if (msg.name !== undefined) result.name = msg.name;
|
||||
if (msg.tool_calls !== undefined) result.tool_calls = msg.tool_calls;
|
||||
if (msg.tool_call_id !== undefined) result.tool_call_id = msg.tool_call_id;
|
||||
if (msg.function_call !== undefined) result.function_call = msg.function_call;
|
||||
|
||||
// it's compatible for DeepSeek
|
||||
if (msg.reasoning_content !== undefined) result.reasoning_content = msg.reasoning_content;
|
||||
|
||||
return result;
|
||||
}),
|
||||
)) as OpenAI.ChatCompletionMessageParam[];
|
||||
};
|
||||
|
||||
export const convertOpenAIResponseInputs = async (
|
||||
messages: OpenAI.ChatCompletionMessageParam[],
|
||||
) => {
|
||||
export const convertOpenAIResponseInputs = async (messages: OpenAIChatMessage[]) => {
|
||||
let input: OpenAI.Responses.ResponseInputItem[] = [];
|
||||
await Promise.all(
|
||||
messages.map(async (message) => {
|
||||
// if message has reasoning, add it as a separate reasoning item
|
||||
if (message.reasoning?.content) {
|
||||
input.push({
|
||||
summary: [{ text: message.reasoning.content, type: 'summary_text' }],
|
||||
type: 'reasoning',
|
||||
} as OpenAI.Responses.ResponseReasoningItem);
|
||||
}
|
||||
|
||||
// if message is assistant messages with tool calls , transform it to function type item
|
||||
if (message.role === 'assistant' && message.tool_calls && message.tool_calls?.length > 0) {
|
||||
message.tool_calls?.forEach((tool) => {
|
||||
@@ -70,6 +93,11 @@ export const convertOpenAIResponseInputs = async (
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.role === 'system') {
|
||||
input.push({ ...message, role: 'developer' } as OpenAI.Responses.ResponseInputItem);
|
||||
return;
|
||||
}
|
||||
|
||||
// default item
|
||||
// also need handle image
|
||||
const item = {
|
||||
@@ -92,6 +120,9 @@ export const convertOpenAIResponseInputs = async (
|
||||
),
|
||||
} as OpenAI.Responses.ResponseInputItem;
|
||||
|
||||
// remove reasoning field from the message item
|
||||
delete (item as any).reasoning;
|
||||
|
||||
input.push(item);
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// @vitest-environment node
|
||||
import * as imageToBase64Module from '@lobechat/utils';
|
||||
import OpenAI from 'openai';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { CreateImagePayload } from '../../types/image';
|
||||
import * as imageToBase64Module from '../../utils/imageToBase64';
|
||||
import * as uriParserModule from '../../utils/uriParser';
|
||||
import { createOpenAICompatibleImage } from './createImage';
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { imageUrlToBase64 } from '@lobechat/utils';
|
||||
import { cleanObject } from '@lobechat/utils/object';
|
||||
import createDebug from 'debug';
|
||||
import { RuntimeImageGenParamsValue } from 'model-bank';
|
||||
@@ -5,7 +6,6 @@ import OpenAI from 'openai';
|
||||
|
||||
import { CreateImagePayload, CreateImageResponse } from '../../types/image';
|
||||
import { getModelPricing } from '../../utils/getModelPricing';
|
||||
import { imageUrlToBase64 } from '../../utils/imageToBase64';
|
||||
import { parseDataUri } from '../../utils/uriParser';
|
||||
import { convertImageUrlToFile } from '../contextBuilders/openai';
|
||||
import { convertOpenAIImageUsage } from '../usageConverters/openai';
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
// @vitest-environment node
|
||||
import {
|
||||
AgentRuntimeErrorType,
|
||||
ChatStreamCallbacks,
|
||||
ChatStreamPayload,
|
||||
LobeOpenAICompatibleRuntime,
|
||||
} from '@lobechat/model-runtime';
|
||||
import { ModelProvider } from 'model-bank';
|
||||
import OpenAI from 'openai';
|
||||
import type { Stream } from 'openai/streaming';
|
||||
import { Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { LobeOpenAICompatibleRuntime } from '../../core/BaseAI';
|
||||
import { ChatStreamCallbacks, ChatStreamPayload } from '../../types/chat';
|
||||
import { AgentRuntimeErrorType } from '../../types/error';
|
||||
import * as debugStreamModule from '../../utils/debugStream';
|
||||
import * as openaiHelpers from '../contextBuilders/openai';
|
||||
import { createOpenAICompatibleRuntime } from './index';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { AgentRuntimeErrorType } from '@lobechat/model-runtime';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { AgentRuntimeErrorType } from '../../../types/error';
|
||||
import { FIRST_CHUNK_ERROR_KEY } from '../protocol';
|
||||
import { createReadableStream, readStreamChunk } from '../utils';
|
||||
import { OpenAIResponsesStream } from './responsesStream';
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ChatMethodOptions } from '@lobechat/model-runtime';
|
||||
import debug from 'debug';
|
||||
|
||||
import { ChatMethodOptions } from '../types/chat';
|
||||
|
||||
const log = debug('model-runtime:helpers:mergeChatMethodOptions');
|
||||
|
||||
export const mergeMultipleChatMethodOptions = (options: ChatMethodOptions[]): ChatMethodOptions => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @vitest-environment edge-runtime
|
||||
// @vitest-environment node
|
||||
import { ModelProvider } from 'model-bank';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @vitest-environment edge-runtime
|
||||
// @vitest-environment node
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { createAnthropicGenerateObject } from './generateObject';
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// @vitest-environment node
|
||||
import { ChatCompletionTool, ChatStreamPayload } from '@lobechat/model-runtime';
|
||||
import { Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import * as anthropicHelpers from '../../core/contextBuilders/anthropic';
|
||||
import { ChatCompletionTool, ChatStreamPayload } from '../../types/chat';
|
||||
import * as debugStreamModule from '../../utils/debugStream';
|
||||
import { LobeAnthropicAI } from './index';
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// @vitest-environment node
|
||||
import { LobeOpenAICompatibleRuntime } from '@lobechat/model-runtime';
|
||||
import { ModelProvider } from 'model-bank';
|
||||
import { Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { LobeOpenAICompatibleRuntime } from '../../core/BaseAI';
|
||||
import { testProvider } from '../../providerTestUtils';
|
||||
import { LobeBaichuanAI, params } from './index';
|
||||
|
||||
|
||||
@@ -3,10 +3,10 @@ import {
|
||||
InvokeModelCommand,
|
||||
InvokeModelWithResponseStreamCommand,
|
||||
} from '@aws-sdk/client-bedrock-runtime';
|
||||
import { AgentRuntimeErrorType } from '@lobechat/model-runtime';
|
||||
import { ModelProvider } from 'model-bank';
|
||||
import { Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { AgentRuntimeErrorType } from '../../types/error';
|
||||
import * as debugStreamModule from '../../utils/debugStream';
|
||||
import { LobeBedrockAI, experimental_buildLlama2Prompt } from './index';
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { createBflImage } from './createImage';
|
||||
import { BflStatusResponse } from './types';
|
||||
|
||||
// Mock external dependencies
|
||||
vi.mock('../../utils/imageToBase64', () => ({
|
||||
vi.mock('@lobechat/utils', () => ({
|
||||
imageUrlToBase64: vi.fn(),
|
||||
}));
|
||||
|
||||
@@ -187,7 +187,7 @@ describe('createBflImage', () => {
|
||||
it('should convert single imageUrl to image_prompt base64', async () => {
|
||||
// Arrange
|
||||
const { parseDataUri } = await import('../../utils/uriParser');
|
||||
const { imageUrlToBase64 } = await import('../../utils/imageToBase64');
|
||||
const { imageUrlToBase64 } = await import('@lobechat/utils');
|
||||
const { asyncifyPolling } = await import('../../utils/asyncifyPolling');
|
||||
|
||||
const mockParseDataUri = vi.mocked(parseDataUri);
|
||||
@@ -290,7 +290,7 @@ describe('createBflImage', () => {
|
||||
it('should convert multiple imageUrls for Kontext models', async () => {
|
||||
// Arrange
|
||||
const { parseDataUri } = await import('../../utils/uriParser');
|
||||
const { imageUrlToBase64 } = await import('../../utils/imageToBase64');
|
||||
const { imageUrlToBase64 } = await import('@lobechat/utils');
|
||||
const { asyncifyPolling } = await import('../../utils/asyncifyPolling');
|
||||
|
||||
const mockParseDataUri = vi.mocked(parseDataUri);
|
||||
@@ -350,7 +350,7 @@ describe('createBflImage', () => {
|
||||
it('should limit imageUrls to maximum 4 images', async () => {
|
||||
// Arrange
|
||||
const { parseDataUri } = await import('../../utils/uriParser');
|
||||
const { imageUrlToBase64 } = await import('../../utils/imageToBase64');
|
||||
const { imageUrlToBase64 } = await import('@lobechat/utils');
|
||||
const { asyncifyPolling } = await import('../../utils/asyncifyPolling');
|
||||
|
||||
const mockParseDataUri = vi.mocked(parseDataUri);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { imageUrlToBase64 } from '@lobechat/utils';
|
||||
import createDebug from 'debug';
|
||||
import { RuntimeImageGenParamsValue } from 'model-bank';
|
||||
|
||||
@@ -5,7 +6,6 @@ import { AgentRuntimeErrorType } from '../../types/error';
|
||||
import { CreateImagePayload, CreateImageResponse } from '../../types/image';
|
||||
import { type TaskResult, asyncifyPolling } from '../../utils/asyncifyPolling';
|
||||
import { AgentRuntimeError } from '../../utils/createError';
|
||||
import { imageUrlToBase64 } from '../../utils/imageToBase64';
|
||||
import { parseDataUri } from '../../utils/uriParser';
|
||||
import {
|
||||
BFL_ENDPOINTS,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @vitest-environment node
|
||||
import { ChatCompletionTool } from '@lobechat/model-runtime';
|
||||
import { Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { ChatCompletionTool } from '../../types/chat';
|
||||
import * as debugStreamModule from '../../utils/debugStream';
|
||||
import { LobeCloudflareAI } from './index';
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// @vitest-environment node
|
||||
import { LobeOpenAICompatibleRuntime } from '@lobechat/model-runtime';
|
||||
import { ModelProvider } from 'model-bank';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { LobeOpenAICompatibleRuntime } from '../../core/BaseAI';
|
||||
import { testProvider } from '../../providerTestUtils';
|
||||
import { LobeCohereAI, params } from './index';
|
||||
|
||||
|
||||
@@ -20,6 +20,92 @@ testProvider({
|
||||
});
|
||||
|
||||
describe('LobeDeepSeekAI - custom features', () => {
|
||||
describe('chatCompletion.handlePayload', () => {
|
||||
it('should transform reasoning object to reasoning_content string', () => {
|
||||
const payload = {
|
||||
messages: [
|
||||
{ role: 'user', content: 'Hello' },
|
||||
{
|
||||
role: 'assistant',
|
||||
content: 'Hi there',
|
||||
reasoning: { content: 'Let me think...', duration: 1000 },
|
||||
},
|
||||
{ role: 'user', content: 'How are you?' },
|
||||
],
|
||||
model: 'deepseek-r1',
|
||||
};
|
||||
|
||||
const result = params.chatCompletion!.handlePayload!(payload as any);
|
||||
|
||||
expect(result.messages).toEqual([
|
||||
{ role: 'user', content: 'Hello' },
|
||||
{
|
||||
role: 'assistant',
|
||||
content: 'Hi there',
|
||||
reasoning_content: 'Let me think...',
|
||||
},
|
||||
{ role: 'user', content: 'How are you?' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not modify messages without reasoning field', () => {
|
||||
const payload = {
|
||||
messages: [
|
||||
{ role: 'user', content: 'Hello' },
|
||||
{ role: 'assistant', content: 'Hi there' },
|
||||
],
|
||||
model: 'deepseek-chat',
|
||||
};
|
||||
|
||||
const result = params.chatCompletion!.handlePayload!(payload as any);
|
||||
|
||||
expect(result.messages).toEqual(payload.messages);
|
||||
});
|
||||
|
||||
it('should handle empty reasoning content', () => {
|
||||
const payload = {
|
||||
messages: [
|
||||
{
|
||||
role: 'assistant',
|
||||
content: 'Response',
|
||||
reasoning: { duration: 1000 },
|
||||
},
|
||||
],
|
||||
model: 'deepseek-r1',
|
||||
};
|
||||
|
||||
const result = params.chatCompletion!.handlePayload!(payload as any);
|
||||
|
||||
expect(result.messages[0]).toEqual({
|
||||
role: 'assistant',
|
||||
content: 'Response',
|
||||
});
|
||||
});
|
||||
|
||||
it('should set stream to true by default', () => {
|
||||
const payload = {
|
||||
messages: [{ role: 'user', content: 'Hello' }],
|
||||
model: 'deepseek-chat',
|
||||
};
|
||||
|
||||
const result = params.chatCompletion!.handlePayload!(payload as any);
|
||||
|
||||
expect(result.stream).toBe(true);
|
||||
});
|
||||
|
||||
it('should preserve existing stream value', () => {
|
||||
const payload = {
|
||||
messages: [{ role: 'user', content: 'Hello' }],
|
||||
model: 'deepseek-chat',
|
||||
stream: false,
|
||||
};
|
||||
|
||||
const result = params.chatCompletion!.handlePayload!(payload as any);
|
||||
|
||||
expect(result.stream).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Debug Configuration', () => {
|
||||
it('should disable debug by default', () => {
|
||||
delete process.env.DEBUG_DEEPSEEK_CHAT_COMPLETION;
|
||||
|
||||
@@ -12,6 +12,30 @@ export interface DeepSeekModelCard {
|
||||
|
||||
export const params = {
|
||||
baseURL: 'https://api.deepseek.com/v1',
|
||||
chatCompletion: {
|
||||
handlePayload: (payload) => {
|
||||
// Transform reasoning object to reasoning_content string for multi-turn conversations
|
||||
const messages = payload.messages.map((message: any) => {
|
||||
// Only transform if message has reasoning.content
|
||||
if (message.reasoning?.content) {
|
||||
const { reasoning, ...rest } = message;
|
||||
return {
|
||||
...rest,
|
||||
reasoning_content: reasoning.content,
|
||||
};
|
||||
}
|
||||
// If message has reasoning but no content, remove reasoning field entirely
|
||||
delete message.reasoning;
|
||||
return message;
|
||||
});
|
||||
|
||||
return {
|
||||
...payload,
|
||||
messages,
|
||||
stream: payload.stream ?? true,
|
||||
} as any;
|
||||
},
|
||||
},
|
||||
debug: {
|
||||
chatCompletion: () => process.env.DEBUG_DEEPSEEK_CHAT_COMPLETION === '1',
|
||||
},
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// @vitest-environment edge-runtime
|
||||
// @vitest-environment node
|
||||
import { GoogleGenAI } from '@google/genai';
|
||||
import * as imageToBase64Module from '@lobechat/utils';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { CreateImagePayload } from '../../types/image';
|
||||
import * as imageToBase64Module from '../../utils/imageToBase64';
|
||||
import { createGoogleImage } from './createImage';
|
||||
|
||||
const provider = 'google';
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Content, GenerateContentConfig, GoogleGenAI, Part } from '@google/genai';
|
||||
import { imageUrlToBase64 } from '@lobechat/utils';
|
||||
|
||||
import { convertGoogleAIUsage } from '../../core/usageConverters/google-ai';
|
||||
import { CreateImagePayload, CreateImageResponse } from '../../types/image';
|
||||
import { AgentRuntimeError } from '../../utils/createError';
|
||||
import { getModelPricing } from '../../utils/getModelPricing';
|
||||
import { parseGoogleErrorMessage } from '../../utils/googleErrorParser';
|
||||
import { imageUrlToBase64 } from '../../utils/imageToBase64';
|
||||
import { parseDataUri } from '../../utils/uriParser';
|
||||
|
||||
// Maximum number of images allowed for processing
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @vitest-environment edge-runtime
|
||||
// @vitest-environment node
|
||||
import { Type as SchemaType } from '@google/genai';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
// @vitest-environment edge-runtime
|
||||
// @vitest-environment node
|
||||
import { GenerateContentResponse, Tool } from '@google/genai';
|
||||
import { OpenAIChatMessage } from '@lobechat/model-runtime';
|
||||
import { ChatStreamPayload } from '@lobechat/types';
|
||||
import OpenAI from 'openai';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { LOBE_ERROR_KEY } from '../../core/streams';
|
||||
import { AgentRuntimeErrorType } from '../../types/error';
|
||||
import * as debugStreamModule from '../../utils/debugStream';
|
||||
import * as imageToBase64Module from '../../utils/imageToBase64';
|
||||
import { LobeGoogleAI, resolveModelThinkingBudget } from './index';
|
||||
|
||||
const provider = 'google';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @vitest-environment node
|
||||
import { LobeOpenAICompatibleRuntime } from '@lobechat/model-runtime';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { LobeOpenAICompatibleRuntime } from '../../core/BaseAI';
|
||||
import { testProvider } from '../../providerTestUtils';
|
||||
import { AgentRuntimeErrorType } from '../../types/error';
|
||||
import { LobeGroq, params } from './index';
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// @vitest-environment node
|
||||
import { LobeOpenAICompatibleRuntime } from '@lobechat/model-runtime';
|
||||
import { ModelProvider } from 'model-bank';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { LobeOpenAICompatibleRuntime } from '../../core/BaseAI';
|
||||
import { testProvider } from '../../providerTestUtils';
|
||||
import { LobeHunyuanAI, params } from './index';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @vitest-environment edge-runtime
|
||||
// @vitest-environment node
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { CreateImageOptions } from '../../core/openaiCompatibleFactory';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @vitest-environment node
|
||||
import { LobeOpenAICompatibleRuntime } from '@lobechat/model-runtime';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { LobeOpenAICompatibleRuntime } from '../../core/BaseAI';
|
||||
import { testProvider } from '../../providerTestUtils';
|
||||
import { LobeMistralAI, params } from './index';
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @vitest-environment node
|
||||
import { LobeOpenAICompatibleRuntime } from '@lobechat/model-runtime';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { LobeOpenAICompatibleRuntime } from '../../core/BaseAI';
|
||||
import { testProvider } from '../../providerTestUtils';
|
||||
import { LobeMoonshotAI, params } from './index';
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// @vitest-environment node
|
||||
import { LobeOpenAICompatibleRuntime } from '@lobechat/model-runtime';
|
||||
import { ModelProvider } from 'model-bank';
|
||||
import { Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { LobeOpenAICompatibleRuntime } from '../../core/BaseAI';
|
||||
import { testProvider } from '../../providerTestUtils';
|
||||
import models from './fixtures/models.json';
|
||||
import { LobeNovitaAI } from './index';
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// @vitest-environment node
|
||||
import { imageUrlToBase64 } from '@lobechat/utils';
|
||||
import { ModelProvider } from 'model-bank';
|
||||
import { Ollama } from 'ollama/browser';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
@@ -9,6 +10,13 @@ import * as debugStreamModule from '../../utils/debugStream';
|
||||
import { LobeOllamaAI, params } from './index';
|
||||
|
||||
vi.mock('ollama/browser');
|
||||
vi.mock('@lobechat/utils', async () => {
|
||||
const actual = await vi.importActual('@lobechat/utils');
|
||||
return {
|
||||
...actual,
|
||||
imageUrlToBase64: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
// Mock the console.error to avoid polluting test output
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
@@ -462,13 +470,13 @@ describe('LobeOllamaAI', () => {
|
||||
});
|
||||
|
||||
describe('buildOllamaMessages', () => {
|
||||
it('should convert OpenAIChatMessage array to OllamaMessage array', () => {
|
||||
it('should convert OpenAIChatMessage array to OllamaMessage array', async () => {
|
||||
const messages = [
|
||||
{ content: 'Hello', role: 'user' },
|
||||
{ content: 'Hi there!', role: 'assistant' },
|
||||
];
|
||||
|
||||
const ollamaMessages = ollamaAI['buildOllamaMessages'](messages as any);
|
||||
const ollamaMessages = await ollamaAI['buildOllamaMessages'](messages as any);
|
||||
|
||||
expect(ollamaMessages).toEqual([
|
||||
{ content: 'Hello', role: 'user' },
|
||||
@@ -476,15 +484,15 @@ describe('LobeOllamaAI', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle empty message array', () => {
|
||||
it('should handle empty message array', async () => {
|
||||
const messages: any[] = [];
|
||||
|
||||
const ollamaMessages = ollamaAI['buildOllamaMessages'](messages);
|
||||
const ollamaMessages = await ollamaAI['buildOllamaMessages'](messages);
|
||||
|
||||
expect(ollamaMessages).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle multiple messages with different roles', () => {
|
||||
it('should handle multiple messages with different roles', async () => {
|
||||
const messages = [
|
||||
{ content: 'Hello', role: 'system' },
|
||||
{ content: 'Hi', role: 'user' },
|
||||
@@ -492,7 +500,7 @@ describe('LobeOllamaAI', () => {
|
||||
{ content: 'How are you?', role: 'user' },
|
||||
];
|
||||
|
||||
const ollamaMessages = ollamaAI['buildOllamaMessages'](messages as any);
|
||||
const ollamaMessages = await ollamaAI['buildOllamaMessages'](messages as any);
|
||||
|
||||
expect(ollamaMessages).toHaveLength(4);
|
||||
expect(ollamaMessages[0].role).toBe('system');
|
||||
@@ -503,26 +511,26 @@ describe('LobeOllamaAI', () => {
|
||||
});
|
||||
|
||||
describe('convertContentToOllamaMessage', () => {
|
||||
it('should convert string content to OllamaMessage', () => {
|
||||
it('should convert string content to OllamaMessage', async () => {
|
||||
const message = { content: 'Hello', role: 'user' };
|
||||
|
||||
const ollamaMessage = ollamaAI['convertContentToOllamaMessage'](message as any);
|
||||
const ollamaMessage = await ollamaAI['convertContentToOllamaMessage'](message as any);
|
||||
|
||||
expect(ollamaMessage).toEqual({ content: 'Hello', role: 'user' });
|
||||
});
|
||||
|
||||
it('should convert text content to OllamaMessage', () => {
|
||||
it('should convert text content to OllamaMessage', async () => {
|
||||
const message = {
|
||||
content: [{ type: 'text', text: 'Hello' }],
|
||||
role: 'user',
|
||||
};
|
||||
|
||||
const ollamaMessage = ollamaAI['convertContentToOllamaMessage'](message as any);
|
||||
const ollamaMessage = await ollamaAI['convertContentToOllamaMessage'](message as any);
|
||||
|
||||
expect(ollamaMessage).toEqual({ content: 'Hello', role: 'user' });
|
||||
});
|
||||
|
||||
it('should convert image_url content to OllamaMessage with images', () => {
|
||||
it('should convert image_url content to OllamaMessage with images', async () => {
|
||||
const message = {
|
||||
content: [
|
||||
{
|
||||
@@ -533,7 +541,7 @@ describe('LobeOllamaAI', () => {
|
||||
role: 'user',
|
||||
};
|
||||
|
||||
const ollamaMessage = ollamaAI['convertContentToOllamaMessage'](message as any);
|
||||
const ollamaMessage = await ollamaAI['convertContentToOllamaMessage'](message as any);
|
||||
|
||||
expect(ollamaMessage).toEqual({
|
||||
content: '',
|
||||
@@ -542,7 +550,7 @@ describe('LobeOllamaAI', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should ignore invalid image_url content', () => {
|
||||
it('should ignore invalid image_url content', async () => {
|
||||
const message = {
|
||||
content: [
|
||||
{
|
||||
@@ -553,7 +561,7 @@ describe('LobeOllamaAI', () => {
|
||||
role: 'user',
|
||||
};
|
||||
|
||||
const ollamaMessage = ollamaAI['convertContentToOllamaMessage'](message as any);
|
||||
const ollamaMessage = await ollamaAI['convertContentToOllamaMessage'](message as any);
|
||||
|
||||
expect(ollamaMessage).toEqual({
|
||||
content: '',
|
||||
@@ -561,7 +569,7 @@ describe('LobeOllamaAI', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle mixed text and image content', () => {
|
||||
it('should handle mixed text and image content', async () => {
|
||||
const message = {
|
||||
content: [
|
||||
{ type: 'text', text: 'First text' },
|
||||
@@ -578,7 +586,7 @@ describe('LobeOllamaAI', () => {
|
||||
role: 'user',
|
||||
};
|
||||
|
||||
const ollamaMessage = ollamaAI['convertContentToOllamaMessage'](message as any);
|
||||
const ollamaMessage = await ollamaAI['convertContentToOllamaMessage'](message as any);
|
||||
|
||||
expect(ollamaMessage).toEqual({
|
||||
content: 'Second text', // Should keep latest text
|
||||
@@ -587,13 +595,13 @@ describe('LobeOllamaAI', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle content with empty text', () => {
|
||||
it('should handle content with empty text', async () => {
|
||||
const message = {
|
||||
content: [{ type: 'text', text: '' }],
|
||||
role: 'user',
|
||||
};
|
||||
|
||||
const ollamaMessage = ollamaAI['convertContentToOllamaMessage'](message as any);
|
||||
const ollamaMessage = await ollamaAI['convertContentToOllamaMessage'](message as any);
|
||||
|
||||
expect(ollamaMessage).toEqual({
|
||||
content: '',
|
||||
@@ -601,7 +609,7 @@ describe('LobeOllamaAI', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle content with only images (no text)', () => {
|
||||
it('should handle content with only images (no text)', async () => {
|
||||
const message = {
|
||||
content: [
|
||||
{
|
||||
@@ -612,7 +620,7 @@ describe('LobeOllamaAI', () => {
|
||||
role: 'user',
|
||||
};
|
||||
|
||||
const ollamaMessage = ollamaAI['convertContentToOllamaMessage'](message as any);
|
||||
const ollamaMessage = await ollamaAI['convertContentToOllamaMessage'](message as any);
|
||||
|
||||
expect(ollamaMessage).toEqual({
|
||||
content: '',
|
||||
@@ -621,7 +629,7 @@ describe('LobeOllamaAI', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle multiple images without text', () => {
|
||||
it('should handle multiple images without text', async () => {
|
||||
const message = {
|
||||
content: [
|
||||
{
|
||||
@@ -640,7 +648,7 @@ describe('LobeOllamaAI', () => {
|
||||
role: 'user',
|
||||
};
|
||||
|
||||
const ollamaMessage = ollamaAI['convertContentToOllamaMessage'](message as any);
|
||||
const ollamaMessage = await ollamaAI['convertContentToOllamaMessage'](message as any);
|
||||
|
||||
expect(ollamaMessage).toEqual({
|
||||
content: '',
|
||||
@@ -649,7 +657,10 @@ describe('LobeOllamaAI', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should ignore images with invalid data URIs', () => {
|
||||
it('should ignore images with invalid data URIs', async () => {
|
||||
// Mock imageUrlToBase64 to simulate conversion failure for external URLs
|
||||
vi.mocked(imageUrlToBase64).mockRejectedValue(new Error('Network error'));
|
||||
|
||||
const message = {
|
||||
content: [
|
||||
{ type: 'text', text: 'Hello' },
|
||||
@@ -665,7 +676,7 @@ describe('LobeOllamaAI', () => {
|
||||
role: 'user',
|
||||
};
|
||||
|
||||
const ollamaMessage = ollamaAI['convertContentToOllamaMessage'](message as any);
|
||||
const ollamaMessage = await ollamaAI['convertContentToOllamaMessage'](message as any);
|
||||
|
||||
expect(ollamaMessage).toEqual({
|
||||
content: 'Hello',
|
||||
@@ -674,7 +685,7 @@ describe('LobeOllamaAI', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle complex interleaved content', () => {
|
||||
it('should handle complex interleaved content', async () => {
|
||||
const message = {
|
||||
content: [
|
||||
{ type: 'text', text: 'Text 1' },
|
||||
@@ -692,7 +703,7 @@ describe('LobeOllamaAI', () => {
|
||||
role: 'user',
|
||||
};
|
||||
|
||||
const ollamaMessage = ollamaAI['convertContentToOllamaMessage'](message as any);
|
||||
const ollamaMessage = await ollamaAI['convertContentToOllamaMessage'](message as any);
|
||||
|
||||
expect(ollamaMessage).toEqual({
|
||||
content: 'Text 3', // Should keep latest text
|
||||
@@ -701,7 +712,7 @@ describe('LobeOllamaAI', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle assistant role with images', () => {
|
||||
it('should handle assistant role with images', async () => {
|
||||
const message = {
|
||||
content: [
|
||||
{ type: 'text', text: 'Here is the image' },
|
||||
@@ -713,7 +724,7 @@ describe('LobeOllamaAI', () => {
|
||||
role: 'assistant',
|
||||
};
|
||||
|
||||
const ollamaMessage = ollamaAI['convertContentToOllamaMessage'](message as any);
|
||||
const ollamaMessage = await ollamaAI['convertContentToOllamaMessage'](message as any);
|
||||
|
||||
expect(ollamaMessage).toEqual({
|
||||
content: 'Here is the image',
|
||||
@@ -722,13 +733,13 @@ describe('LobeOllamaAI', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle system role with text', () => {
|
||||
it('should handle system role with text', async () => {
|
||||
const message = {
|
||||
content: [{ type: 'text', text: 'You are a helpful assistant' }],
|
||||
role: 'system',
|
||||
};
|
||||
|
||||
const ollamaMessage = ollamaAI['convertContentToOllamaMessage'](message as any);
|
||||
const ollamaMessage = await ollamaAI['convertContentToOllamaMessage'](message as any);
|
||||
|
||||
expect(ollamaMessage).toEqual({
|
||||
content: 'You are a helpful assistant',
|
||||
@@ -736,13 +747,13 @@ describe('LobeOllamaAI', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle empty content array', () => {
|
||||
it('should handle empty content array', async () => {
|
||||
const message = {
|
||||
content: [],
|
||||
role: 'user',
|
||||
};
|
||||
|
||||
const ollamaMessage = ollamaAI['convertContentToOllamaMessage'](message as any);
|
||||
const ollamaMessage = await ollamaAI['convertContentToOllamaMessage'](message as any);
|
||||
|
||||
expect(ollamaMessage).toEqual({
|
||||
content: '',
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { ChatModelCard } from '@lobechat/types';
|
||||
import { imageUrlToBase64 } from '@lobechat/utils';
|
||||
import { ModelProvider } from 'model-bank';
|
||||
import { Ollama, Tool } from 'ollama/browser';
|
||||
import { ClientOptions } from 'openai';
|
||||
@@ -61,7 +62,7 @@ export class LobeOllamaAI implements LobeRuntimeAI {
|
||||
options?.signal?.addEventListener('abort', abort);
|
||||
|
||||
const response = await this.client.chat({
|
||||
messages: this.buildOllamaMessages(payload.messages),
|
||||
messages: await this.buildOllamaMessages(payload.messages),
|
||||
model: payload.model,
|
||||
options: {
|
||||
frequency_penalty: payload.frequency_penalty,
|
||||
@@ -169,11 +170,13 @@ export class LobeOllamaAI implements LobeRuntimeAI {
|
||||
}
|
||||
};
|
||||
|
||||
private buildOllamaMessages(messages: OpenAIChatMessage[]) {
|
||||
return messages.map((message) => this.convertContentToOllamaMessage(message));
|
||||
private async buildOllamaMessages(messages: OpenAIChatMessage[]) {
|
||||
return Promise.all(messages.map((message) => this.convertContentToOllamaMessage(message)));
|
||||
}
|
||||
|
||||
private convertContentToOllamaMessage = (message: OpenAIChatMessage): OllamaMessage => {
|
||||
private convertContentToOllamaMessage = async (
|
||||
message: OpenAIChatMessage,
|
||||
): Promise<OllamaMessage> => {
|
||||
if (typeof message.content === 'string') {
|
||||
return { content: message.content, role: message.role };
|
||||
}
|
||||
@@ -183,6 +186,9 @@ export class LobeOllamaAI implements LobeRuntimeAI {
|
||||
role: message.role,
|
||||
};
|
||||
|
||||
// Collect image processing tasks for parallel execution
|
||||
const imagePromises: Array<Promise<string | null> | string> = [];
|
||||
|
||||
for (const content of message.content) {
|
||||
switch (content.type) {
|
||||
case 'text': {
|
||||
@@ -191,16 +197,34 @@ export class LobeOllamaAI implements LobeRuntimeAI {
|
||||
break;
|
||||
}
|
||||
case 'image_url': {
|
||||
const { base64 } = parseDataUri(content.image_url.url);
|
||||
const { base64, type } = parseDataUri(content.image_url.url);
|
||||
|
||||
// If already base64 format, use it directly
|
||||
if (base64) {
|
||||
ollamaMessage.images ??= [];
|
||||
ollamaMessage.images.push(base64);
|
||||
imagePromises.push(base64);
|
||||
}
|
||||
// If it's a URL, add async conversion task with error handling
|
||||
else if (type === 'url') {
|
||||
imagePromises.push(
|
||||
imageUrlToBase64(content.image_url.url)
|
||||
.then((result) => result.base64)
|
||||
.catch(() => null), // Silently ignore failed conversions
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process all images in parallel and filter out failed conversions
|
||||
if (imagePromises.length > 0) {
|
||||
const results = await Promise.all(imagePromises);
|
||||
const validImages = results.filter((img): img is string => img !== null);
|
||||
if (validImages.length > 0) {
|
||||
ollamaMessage.images = validImages;
|
||||
}
|
||||
}
|
||||
|
||||
return ollamaMessage;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @vitest-environment node
|
||||
import { LobeOpenAICompatibleRuntime } from '@lobechat/model-runtime';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { LobeOpenAICompatibleRuntime } from '../../core/BaseAI';
|
||||
import { testProvider } from '../../providerTestUtils';
|
||||
import { LobeOpenRouterAI, params } from './index';
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// @vitest-environment node
|
||||
import { LobeOpenAICompatibleRuntime } from '@lobechat/model-runtime';
|
||||
import { ModelProvider } from 'model-bank';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { LobeOpenAICompatibleRuntime } from '../../core/BaseAI';
|
||||
import { testProvider } from '../../providerTestUtils';
|
||||
import { LobePerplexityAI } from './index';
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// @vitest-environment node
|
||||
import { LobeOpenAICompatibleRuntime } from '@lobechat/model-runtime';
|
||||
import { ModelProvider } from 'model-bank';
|
||||
import { Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { LobeOpenAICompatibleRuntime } from '../../core/BaseAI';
|
||||
import { testProvider } from '../../providerTestUtils';
|
||||
import models from './fixtures/models.json';
|
||||
import { LobePPIOAI } from './index';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @vitest-environment edge-runtime
|
||||
// @vitest-environment node
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { CreateImageOptions } from '../../core/openaiCompatibleFactory';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @vitest-environment node
|
||||
import { LobeOpenAICompatibleRuntime } from '@lobechat/model-runtime';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { LobeOpenAICompatibleRuntime } from '../../core/BaseAI';
|
||||
import { testProvider } from '../../providerTestUtils';
|
||||
import { LobeSearch1API, params } from './index';
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { imageUrlToBase64 } from '@lobechat/utils';
|
||||
import createDebug from 'debug';
|
||||
import { RuntimeImageGenParamsValue } from 'model-bank';
|
||||
|
||||
@@ -5,7 +6,6 @@ import { CreateImageOptions } from '../../core/openaiCompatibleFactory';
|
||||
import { CreateImagePayload, CreateImageResponse } from '../../types';
|
||||
import { AgentRuntimeErrorType } from '../../types/error';
|
||||
import { AgentRuntimeError } from '../../utils/createError';
|
||||
import { imageUrlToBase64 } from '../../utils/imageToBase64';
|
||||
import { parseDataUri } from '../../utils/uriParser';
|
||||
|
||||
const log = createDebug('lobe-image:siliconcloud');
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// @vitest-environment node
|
||||
import { LobeOpenAICompatibleRuntime } from '@lobechat/model-runtime';
|
||||
import { ModelProvider } from 'model-bank';
|
||||
import OpenAI from 'openai';
|
||||
import { Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { LobeOpenAICompatibleRuntime } from '../../core/BaseAI';
|
||||
import { testProvider } from '../../providerTestUtils';
|
||||
import * as debugStreamModule from '../../utils/debugStream';
|
||||
import { LobeTaichuAI } from './index';
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// @vitest-environment node
|
||||
import { LobeOpenAICompatibleRuntime } from '@lobechat/model-runtime';
|
||||
import { ModelProvider } from 'model-bank';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { LobeOpenAICompatibleRuntime } from '../../core/BaseAI';
|
||||
import { testProvider } from '../../providerTestUtils';
|
||||
import { LobeWenxinAI, params } from './index';
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @vitest-environment node
|
||||
import { LobeOpenAICompatibleRuntime } from '@lobechat/model-runtime';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { LobeOpenAICompatibleRuntime } from '../../core/BaseAI';
|
||||
import { testProvider } from '../../providerTestUtils';
|
||||
import { LobeZhipuAI, params } from './index';
|
||||
|
||||
|
||||
@@ -47,6 +47,10 @@ export type UserMessageContentPart =
|
||||
export interface OpenAIChatMessage {
|
||||
content: string | UserMessageContentPart[];
|
||||
name?: string;
|
||||
reasoning?: {
|
||||
content?: string;
|
||||
duration?: number;
|
||||
};
|
||||
role: LLMRoleType;
|
||||
tool_call_id?: string;
|
||||
tool_calls?: MessageToolCall[];
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { AgentRuntimeErrorType } from '@lobechat/model-runtime';
|
||||
import { ChatErrorType } from '@lobechat/types';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { AgentRuntimeErrorType } from '../types/error';
|
||||
import { createErrorResponse } from './errorResponse';
|
||||
|
||||
describe('createErrorResponse', () => {
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { imageToBase64, imageUrlToBase64 } from './imageToBase64';
|
||||
|
||||
describe('imageToBase64', () => {
|
||||
let mockImage: HTMLImageElement;
|
||||
let mockCanvas: HTMLCanvasElement;
|
||||
let mockContext: CanvasRenderingContext2D;
|
||||
|
||||
beforeEach(() => {
|
||||
mockImage = {
|
||||
width: 200,
|
||||
height: 100,
|
||||
} as HTMLImageElement;
|
||||
|
||||
mockContext = {
|
||||
drawImage: vi.fn(),
|
||||
} as unknown as CanvasRenderingContext2D;
|
||||
|
||||
mockCanvas = {
|
||||
width: 0,
|
||||
height: 0,
|
||||
getContext: vi.fn().mockReturnValue(mockContext),
|
||||
toDataURL: vi.fn().mockReturnValue('data:image/webp;base64,mockBase64Data'),
|
||||
} as unknown as HTMLCanvasElement;
|
||||
|
||||
vi.spyOn(document, 'createElement').mockReturnValue(mockCanvas);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should convert image to base64 with correct size and type', () => {
|
||||
const result = imageToBase64({ img: mockImage, size: 100, type: 'image/jpeg' });
|
||||
|
||||
expect(document.createElement).toHaveBeenCalledWith('canvas');
|
||||
expect(mockCanvas.width).toBe(100);
|
||||
expect(mockCanvas.height).toBe(100);
|
||||
expect(mockCanvas.getContext).toHaveBeenCalledWith('2d');
|
||||
expect(mockContext.drawImage).toHaveBeenCalledWith(mockImage, 50, 0, 100, 100, 0, 0, 100, 100);
|
||||
expect(mockCanvas.toDataURL).toHaveBeenCalledWith('image/jpeg');
|
||||
expect(result).toBe('data:image/webp;base64,mockBase64Data');
|
||||
});
|
||||
|
||||
it('should use default type when not specified', () => {
|
||||
imageToBase64({ img: mockImage, size: 100 });
|
||||
expect(mockCanvas.toDataURL).toHaveBeenCalledWith('image/webp');
|
||||
});
|
||||
|
||||
it('should handle taller images correctly', () => {
|
||||
mockImage.width = 100;
|
||||
mockImage.height = 200;
|
||||
imageToBase64({ img: mockImage, size: 100 });
|
||||
expect(mockContext.drawImage).toHaveBeenCalledWith(mockImage, 0, 50, 100, 100, 0, 0, 100, 100);
|
||||
});
|
||||
});
|
||||
|
||||
describe('imageUrlToBase64', () => {
|
||||
const mockFetch = vi.fn();
|
||||
const mockArrayBuffer = new ArrayBuffer(8);
|
||||
|
||||
beforeEach(() => {
|
||||
global.fetch = mockFetch;
|
||||
global.btoa = vi.fn().mockReturnValue('mockBase64String');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should convert image URL to base64 string', async () => {
|
||||
mockFetch.mockResolvedValue({
|
||||
arrayBuffer: () => Promise.resolve(mockArrayBuffer),
|
||||
blob: () => Promise.resolve(new Blob([mockArrayBuffer], { type: 'image/jpg' })),
|
||||
});
|
||||
|
||||
const result = await imageUrlToBase64('https://example.com/image.jpg');
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith('https://example.com/image.jpg');
|
||||
expect(global.btoa).toHaveBeenCalled();
|
||||
expect(result).toEqual({ base64: 'mockBase64String', mimeType: 'image/jpg' });
|
||||
});
|
||||
|
||||
it('should throw an error when fetch fails', async () => {
|
||||
const mockError = new Error('Fetch failed');
|
||||
mockFetch.mockRejectedValue(mockError);
|
||||
|
||||
await expect(imageUrlToBase64('https://example.com/image.jpg')).rejects.toThrow('Fetch failed');
|
||||
});
|
||||
});
|
||||
@@ -1,62 +0,0 @@
|
||||
export const imageToBase64 = ({
|
||||
size,
|
||||
img,
|
||||
type = 'image/webp',
|
||||
}: {
|
||||
img: HTMLImageElement;
|
||||
size: number;
|
||||
type?: string;
|
||||
}) => {
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
|
||||
let startX = 0;
|
||||
let startY = 0;
|
||||
|
||||
if (img.width > img.height) {
|
||||
startX = (img.width - img.height) / 2;
|
||||
} else {
|
||||
startY = (img.height - img.width) / 2;
|
||||
}
|
||||
|
||||
canvas.width = size;
|
||||
canvas.height = size;
|
||||
|
||||
ctx.drawImage(
|
||||
img,
|
||||
startX,
|
||||
startY,
|
||||
Math.min(img.width, img.height),
|
||||
Math.min(img.width, img.height),
|
||||
0,
|
||||
0,
|
||||
size,
|
||||
size,
|
||||
);
|
||||
|
||||
return canvas.toDataURL(type);
|
||||
};
|
||||
|
||||
export const imageUrlToBase64 = async (
|
||||
imageUrl: string,
|
||||
): Promise<{ base64: string; mimeType: string }> => {
|
||||
try {
|
||||
const res = await fetch(imageUrl);
|
||||
const blob = await res.blob();
|
||||
const arrayBuffer = await blob.arrayBuffer();
|
||||
|
||||
const base64 =
|
||||
typeof btoa === 'function'
|
||||
? btoa(
|
||||
new Uint8Array(arrayBuffer).reduce(
|
||||
(data, byte) => data + String.fromCharCode(byte),
|
||||
'',
|
||||
),
|
||||
)
|
||||
: Buffer.from(arrayBuffer).toString('base64');
|
||||
|
||||
return { base64, mimeType: blob.type };
|
||||
} catch (error) {
|
||||
console.error('Error converting image to base64:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Browser version of SSRF-safe fetch
|
||||
* In browser environments, we simply use the native fetch API
|
||||
* as SSRF attacks are not applicable in client-side code
|
||||
*/
|
||||
|
||||
/**
|
||||
* Browser-safe fetch implementation
|
||||
* Uses native fetch API in browser environments
|
||||
*/
|
||||
// eslint-disable-next-line no-undef
|
||||
export const ssrfSafeFetch = async (url: string, options?: RequestInit): Promise<Response> => {
|
||||
return fetch(url, options);
|
||||
};
|
||||
@@ -2,7 +2,14 @@
|
||||
"name": "ssrf-safe-fetch",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "",
|
||||
"description": "SSRF-safe fetch implementation with browser/node conditional exports",
|
||||
"exports": {
|
||||
".": {
|
||||
"browser": "./index.browser.ts",
|
||||
"node": "./index.ts",
|
||||
"default": "./index.ts"
|
||||
}
|
||||
},
|
||||
"main": "index.ts",
|
||||
"scripts": {
|
||||
"test": "vitest run"
|
||||
|
||||
@@ -36,23 +36,30 @@ export const imageToBase64 = ({
|
||||
return canvas.toDataURL(type);
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert image URL to base64
|
||||
* Uses SSRF-safe fetch on server-side to prevent SSRF attacks
|
||||
*/
|
||||
export const imageUrlToBase64 = async (
|
||||
imageUrl: string,
|
||||
): Promise<{ base64: string; mimeType: string }> => {
|
||||
try {
|
||||
const res = await fetch(imageUrl);
|
||||
const isServer = typeof window === 'undefined';
|
||||
|
||||
// Use SSRF-safe fetch on server-side to prevent SSRF attacks
|
||||
const res = isServer
|
||||
? await import('ssrf-safe-fetch').then((m) => m.ssrfSafeFetch(imageUrl))
|
||||
: await fetch(imageUrl);
|
||||
|
||||
const blob = await res.blob();
|
||||
const arrayBuffer = await blob.arrayBuffer();
|
||||
|
||||
const base64 =
|
||||
typeof btoa === 'function'
|
||||
? btoa(
|
||||
new Uint8Array(arrayBuffer).reduce(
|
||||
(data, byte) => data + String.fromCharCode(byte),
|
||||
'',
|
||||
),
|
||||
)
|
||||
: Buffer.from(arrayBuffer).toString('base64');
|
||||
// Client-side uses btoa, server-side uses Buffer
|
||||
const base64 = isServer
|
||||
? Buffer.from(arrayBuffer).toString('base64')
|
||||
: btoa(
|
||||
new Uint8Array(arrayBuffer).reduce((data, byte) => data + String.fromCharCode(byte), ''),
|
||||
);
|
||||
|
||||
return { base64, mimeType: blob.type };
|
||||
} catch (error) {
|
||||
|
||||
@@ -4,9 +4,9 @@ export * from './detectChinese';
|
||||
export * from './format';
|
||||
export * from './imageToBase64';
|
||||
export * from './keyboard';
|
||||
export * from './merge';
|
||||
export * from './number';
|
||||
export * from './object';
|
||||
export * from './parseModels';
|
||||
export * from './pricing';
|
||||
export * from './safeParseJSON';
|
||||
export * from './sleep';
|
||||
|
||||
@@ -122,7 +122,6 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
|
||||
return NextResponse.redirect(finalRedirectUrl, {
|
||||
headers: request.headers,
|
||||
status: 303,
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { getMessageError } from '@lobechat/fetch-sse';
|
||||
import { ChatMessageError } from '@lobechat/types';
|
||||
import { AudioPlayer } from '@lobehub/tts/react';
|
||||
import { Alert, Button, Highlighter, Select, SelectProps } from '@lobehub/ui';
|
||||
@@ -9,7 +10,6 @@ import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import { useTTS } from '@/hooks/useTTS';
|
||||
import { TTSServer } from '@/types/agent';
|
||||
import { getMessageError } from '@/utils/fetch';
|
||||
|
||||
interface SelectWithTTSPreviewProps extends SelectProps {
|
||||
server: TTSServer;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { MessageTextChunk } from '@lobechat/fetch-sse';
|
||||
import {
|
||||
chainPickEmoji,
|
||||
chainSummaryAgentName,
|
||||
@@ -16,7 +17,6 @@ import { systemAgentSelectors } from '@/store/user/slices/settings/selectors';
|
||||
import { LobeAgentChatConfig, LobeAgentConfig } from '@/types/agent';
|
||||
import { MetaData } from '@/types/meta';
|
||||
import { SystemAgentItem } from '@/types/user/settings';
|
||||
import { MessageTextChunk } from '@/utils/fetch';
|
||||
import { merge } from '@/utils/merge';
|
||||
import { setNamespace } from '@/utils/storeDebug';
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { getMessageError } from '@lobechat/fetch-sse';
|
||||
import { ChatMessageError } from '@lobechat/types';
|
||||
import { SpeechRecognitionOptions, useSpeechRecognition } from '@lobehub/tts/react';
|
||||
import isEqual from 'fast-deep-equal';
|
||||
@@ -13,7 +14,6 @@ import { useGlobalStore } from '@/store/global';
|
||||
import { globalGeneralSelectors } from '@/store/global/selectors';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { settingsSelectors } from '@/store/user/selectors';
|
||||
import { getMessageError } from '@/utils/fetch';
|
||||
|
||||
import CommonSTT from './common';
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { getMessageError } from '@lobechat/fetch-sse';
|
||||
import { ChatMessageError } from '@lobechat/types';
|
||||
import { getRecordMineType } from '@lobehub/tts';
|
||||
import { OpenAISTTOptions, useOpenAISTT } from '@lobehub/tts/react';
|
||||
@@ -16,7 +17,6 @@ import { useGlobalStore } from '@/store/global';
|
||||
import { globalGeneralSelectors } from '@/store/global/selectors';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { settingsSelectors } from '@/store/user/selectors';
|
||||
import { getMessageError } from '@/utils/fetch';
|
||||
|
||||
import CommonSTT from './common';
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { getMessageError } from '@lobechat/fetch-sse';
|
||||
import { ChatMessageError, ChatTTS } from '@lobechat/types';
|
||||
import { memo, useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -5,7 +6,6 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useTTS } from '@/hooks/useTTS';
|
||||
import { useChatStore } from '@/store/chat';
|
||||
import { useFileStore } from '@/store/file';
|
||||
import { getMessageError } from '@/utils/fetch';
|
||||
|
||||
import Player from './Player';
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@ exports[`MCPClient > Stdio Transport > should list tools via stdio 1`] = `
|
||||
"description": "Lists all available tools and methods",
|
||||
"inputSchema": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"additionalProperties": false,
|
||||
"properties": {},
|
||||
"type": "object",
|
||||
},
|
||||
|
||||
@@ -19,7 +19,7 @@ vi.mock('@/envs/llm', () => ({
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock('@/utils/parseModels', () => ({
|
||||
vi.mock('@/utils/server/parseModels', () => ({
|
||||
extractEnabledModels: vi.fn(async (providerId: string, modelString?: string) => {
|
||||
if (!modelString) return undefined;
|
||||
return [`${providerId}-model-1`, `${providerId}-model-2`];
|
||||
@@ -98,7 +98,7 @@ describe('genServerAiProvidersConfig', () => {
|
||||
it('should use environment variables for model lists', async () => {
|
||||
process.env.OPENAI_MODEL_LIST = '+gpt-4,+gpt-3.5-turbo';
|
||||
|
||||
const { extractEnabledModels } = vi.mocked(await import('@/utils/parseModels'));
|
||||
const { extractEnabledModels } = vi.mocked(await import('@/utils/server/parseModels'));
|
||||
extractEnabledModels.mockResolvedValue(['gpt-4', 'gpt-3.5-turbo']);
|
||||
|
||||
const result = await genServerAiProvidersConfig({});
|
||||
@@ -116,7 +116,7 @@ describe('genServerAiProvidersConfig', () => {
|
||||
|
||||
process.env.CUSTOM_OPENAI_MODELS = '+custom-model';
|
||||
|
||||
const { extractEnabledModels } = vi.mocked(await import('@/utils/parseModels'));
|
||||
const { extractEnabledModels } = vi.mocked(await import('@/utils/server/parseModels'));
|
||||
|
||||
await genServerAiProvidersConfig(specificConfig);
|
||||
|
||||
@@ -133,7 +133,7 @@ describe('genServerAiProvidersConfig', () => {
|
||||
process.env.OPENAI_MODEL_LIST = '+gpt-4->deployment1';
|
||||
|
||||
const { extractEnabledModels, transformToAiModelList } = vi.mocked(
|
||||
await import('@/utils/parseModels'),
|
||||
await import('@/utils/server/parseModels'),
|
||||
);
|
||||
|
||||
await genServerAiProvidersConfig(specificConfig);
|
||||
@@ -206,7 +206,7 @@ describe('genServerAiProvidersConfig Error Handling', () => {
|
||||
getLLMConfig: vi.fn(() => ({})),
|
||||
}));
|
||||
|
||||
vi.doMock('@/utils/parseModels', () => ({
|
||||
vi.doMock('@/utils/server/parseModels', () => ({
|
||||
extractEnabledModels: vi.fn(async () => undefined),
|
||||
transformToAiModelList: vi.fn(async () => []),
|
||||
}));
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { ProviderConfig } from '@lobechat/types';
|
||||
import { extractEnabledModels, transformToAiModelList } from '@lobechat/utils';
|
||||
import { AiFullModelCard, ModelProvider } from 'model-bank';
|
||||
import * as AiModels from 'model-bank';
|
||||
|
||||
import { getLLMConfig } from '@/envs/llm';
|
||||
import { extractEnabledModels, transformToAiModelList } from '@/utils/server/parseModels';
|
||||
|
||||
interface ProviderSpecificConfig {
|
||||
enabled?: boolean;
|
||||
|
||||
@@ -33,7 +33,7 @@ vi.stubGlobal(
|
||||
);
|
||||
|
||||
// Mock image processing utilities
|
||||
vi.mock('@/utils/fetch', async (importOriginal) => {
|
||||
vi.mock('@lobechat/fetch-sse', async (importOriginal) => {
|
||||
const module = await importOriginal();
|
||||
|
||||
return { ...(module as any), getMessageError: vi.fn() };
|
||||
@@ -1015,7 +1015,7 @@ describe('ChatService', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
// Setup common fetchSSE mock for getChatCompletion tests
|
||||
const { fetchSSE } = await import('@/utils/fetch');
|
||||
const { fetchSSE } = await import('@lobechat/fetch-sse');
|
||||
mockFetchSSE = vi.fn().mockResolvedValue(new Response('mock response'));
|
||||
vi.mocked(fetchSSE).mockImplementation(mockFetchSSE);
|
||||
});
|
||||
@@ -1076,7 +1076,7 @@ describe('ChatService', () => {
|
||||
|
||||
it('should return InvalidAccessCode error when enableFetchOnClient is true and auth is enabled but user is not signed in', async () => {
|
||||
// Mock fetchSSE to call onErrorHandle with the error
|
||||
const { fetchSSE } = await import('@/utils/fetch');
|
||||
const { fetchSSE } = await import('@lobechat/fetch-sse');
|
||||
|
||||
const mockFetchSSEWithError = vi.fn().mockImplementation((url, options) => {
|
||||
// Simulate the error being caught and passed to onErrorHandle
|
||||
@@ -1238,8 +1238,8 @@ vi.mock('../_auth', async (importOriginal) => {
|
||||
describe('ChatService private methods', () => {
|
||||
describe('getChatCompletion', () => {
|
||||
it('should merge responseAnimation styles correctly', async () => {
|
||||
const { fetchSSE } = await import('@/utils/fetch');
|
||||
vi.mock('@/utils/fetch', async (importOriginal) => {
|
||||
const { fetchSSE } = await import('@lobechat/fetch-sse');
|
||||
vi.mock('@lobechat/fetch-sse', async (importOriginal) => {
|
||||
const module = await importOriginal();
|
||||
return {
|
||||
...(module as any),
|
||||
|
||||
@@ -38,7 +38,7 @@ vi.stubGlobal(
|
||||
vi.fn(() => Promise.resolve(new Response(JSON.stringify({ some: 'data' })))),
|
||||
);
|
||||
|
||||
vi.mock('@/utils/fetch', async (importOriginal) => {
|
||||
vi.mock('@lobechat/fetch-sse', async (importOriginal) => {
|
||||
const module = await importOriginal();
|
||||
|
||||
return { ...(module as any), getMessageError: vi.fn() };
|
||||
|
||||
@@ -243,6 +243,10 @@ describe('contextEngineering', () => {
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
reasoning: {
|
||||
content: 'I need to calculate the answer to life, universe, and everything.',
|
||||
signature: 'thinking_process',
|
||||
},
|
||||
role: 'assistant',
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
import {
|
||||
FetchSSEOptions,
|
||||
fetchSSE,
|
||||
getMessageError,
|
||||
standardizeAnimationStyle,
|
||||
} from '@lobechat/fetch-sse';
|
||||
import { AgentRuntimeError, ChatCompletionErrorPayload } from '@lobechat/model-runtime';
|
||||
import { ChatErrorType, TracePayload, TraceTagMap, UIChatMessage } from '@lobechat/types';
|
||||
import { PluginRequestPayload, createHeadersWithPluginSettings } from '@lobehub/chat-plugin-sdk';
|
||||
@@ -25,12 +31,6 @@ import {
|
||||
import type { ChatStreamPayload, OpenAIChatMessage } from '@/types/openai/chat';
|
||||
import { fetchWithInvokeStream } from '@/utils/electron/desktopRemoteRPCFetch';
|
||||
import { createErrorResponse } from '@/utils/errorResponse';
|
||||
import {
|
||||
FetchSSEOptions,
|
||||
fetchSSE,
|
||||
getMessageError,
|
||||
standardizeAnimationStyle,
|
||||
} from '@/utils/fetch';
|
||||
import { createTraceHeader, getTraceId } from '@/utils/trace';
|
||||
|
||||
import { createHeaderWithAuth } from '../_auth';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { FetchSSEOptions } from '@lobechat/fetch-sse';
|
||||
import { TracePayload } from '@lobechat/types';
|
||||
import { FetchSSEOptions } from '@/utils/fetch';
|
||||
|
||||
export interface FetchOptions extends FetchSSEOptions {
|
||||
historySummary?: string;
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { getMessageError } from '@lobechat/fetch-sse';
|
||||
|
||||
import { isDeprecatedEdition } from '@/const/version';
|
||||
import { createHeaderWithAuth } from '@/services/_auth';
|
||||
import { aiProviderSelectors, getAiInfraStoreState } from '@/store/aiInfra';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { modelConfigSelectors } from '@/store/user/selectors';
|
||||
import { ChatModelCard } from '@/types/llm';
|
||||
import { getMessageError } from '@/utils/fetch';
|
||||
|
||||
import { API_ENDPOINTS } from './_url';
|
||||
import { initializeWithClientStore } from './chat/clientModelRuntime';
|
||||
|
||||
+1
-1
@@ -1,10 +1,10 @@
|
||||
import { isDesktop } from '@lobechat/const';
|
||||
import { ProxyTRPCRequestParams, dispatch, streamInvoke } from '@lobechat/electron-client-ipc';
|
||||
import { getRequestBody, headersToRecord } from '@lobechat/fetch-sse';
|
||||
import debug from 'debug';
|
||||
|
||||
import { getElectronStoreState } from '@/store/electron';
|
||||
import { electronSyncSelectors } from '@/store/electron/selectors';
|
||||
import { getRequestBody, headersToRecord } from '@/utils/fetch';
|
||||
|
||||
const log = debug('utils:desktopRemoteRPCFetch');
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { getModelPropertyWithFallback } from '@lobechat/model-runtime';
|
||||
import { merge } from '@lobechat/utils';
|
||||
import { produce } from 'immer';
|
||||
import { AiFullModelCard, AiModelType } from 'model-bank';
|
||||
|
||||
import { merge } from './merge';
|
||||
|
||||
/**
|
||||
* Parse model string to add or remove models.
|
||||
*/
|
||||
@@ -16,6 +16,8 @@ export default defineConfig({
|
||||
// TODO: after refactor the errorResponse, we can remove it
|
||||
'@/utils/errorResponse': resolve(__dirname, './src/utils/errorResponse'),
|
||||
'@/utils/unzipFile': resolve(__dirname, './src/utils/unzipFile'),
|
||||
'@/utils/server': resolve(__dirname, './src/utils/server'),
|
||||
'@/utils/electron': resolve(__dirname, './src/utils/electron'),
|
||||
'@/utils': resolve(__dirname, './packages/utils/src'),
|
||||
'@/types': resolve(__dirname, './packages/types/src'),
|
||||
'@/const': resolve(__dirname, './packages/const/src'),
|
||||
|
||||
Reference in New Issue
Block a user