mirror of
https://github.com/lobehub/lobehub.git
synced 2026-03-27 13:29:15 +07:00
✨ feat(image): improve image generation with new models and bug fixes (#11311)
This commit is contained in:
2
.github/workflows/bundle-analyzer.yml
vendored
2
.github/workflows/bundle-analyzer.yml
vendored
@@ -98,7 +98,7 @@ jobs:
|
||||
echo "- \`pnpm-lock.yaml\` - pnpm lockfile (for reproducible builds)" >> bundle-report/README.md
|
||||
|
||||
- name: Upload bundle analyzer reports
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: bundle-report-${{ github.run_id }}
|
||||
path: bundle-report/
|
||||
|
||||
119
.github/workflows/e2e.yml
vendored
119
.github/workflows/e2e.yml
vendored
@@ -1,14 +1,14 @@
|
||||
name: E2E CI
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
permissions:
|
||||
actions: write
|
||||
contents: read
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
|
||||
concurrency:
|
||||
group: e2e-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/postgres
|
||||
@@ -24,59 +24,74 @@ env:
|
||||
S3_ENDPOINT: https://e2e-mock-s3.localhost
|
||||
|
||||
jobs:
|
||||
e2e:
|
||||
name: Test Web App
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
postgres:
|
||||
image: paradedb/paradedb:latest
|
||||
env:
|
||||
POSTGRES_PASSWORD: postgres
|
||||
options: >-
|
||||
--health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
|
||||
# Check for duplicate runs
|
||||
check-duplicate-run:
|
||||
name: Check Duplicate Run
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
should_skip: ${{ steps.skip_check.outputs.should_skip }}
|
||||
steps:
|
||||
- id: skip_check
|
||||
uses: fkirc/skip-duplicate-actions@v5
|
||||
with:
|
||||
concurrent_skipping: 'same_content_newer'
|
||||
skip_after_successful_duplicate: 'true'
|
||||
do_not_skip: '["workflow_dispatch", "schedule"]'
|
||||
|
||||
ports:
|
||||
- 5432:5432
|
||||
e2e:
|
||||
needs: check-duplicate-run
|
||||
if: needs.check-duplicate-run.outputs.should_skip != 'true'
|
||||
name: Test Web App
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
postgres:
|
||||
image: paradedb/paradedb:latest
|
||||
env:
|
||||
POSTGRES_PASSWORD: postgres
|
||||
options: >-
|
||||
--health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
|
||||
ports:
|
||||
- 5432:5432
|
||||
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: latest
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: latest
|
||||
|
||||
- name: Install dependencies (bun)
|
||||
run: bun install
|
||||
- name: Install dependencies (bun)
|
||||
run: bun install
|
||||
|
||||
- name: Install Playwright browsers (with system deps)
|
||||
run: bunx playwright install --with-deps chromium
|
||||
- name: Install Playwright browsers (with system deps)
|
||||
run: bunx playwright install --with-deps chromium
|
||||
|
||||
- name: Run database migrations
|
||||
run: bun run db:migrate
|
||||
- name: Run database migrations
|
||||
run: bun run db:migrate
|
||||
|
||||
- name: Build application
|
||||
run: bun run build
|
||||
env:
|
||||
SKIP_LINT: '1'
|
||||
- name: Build application
|
||||
run: bun run build
|
||||
env:
|
||||
SKIP_LINT: '1'
|
||||
|
||||
- name: Run E2E tests
|
||||
run: bun run e2e
|
||||
- name: Run E2E tests
|
||||
run: bun run e2e
|
||||
|
||||
- name: Upload Cucumber HTML report (on failure)
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: cucumber-report
|
||||
path: e2e/reports
|
||||
if-no-files-found: ignore
|
||||
- name: Upload Cucumber HTML report (on failure)
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: cucumber-report
|
||||
path: e2e/reports
|
||||
if-no-files-found: ignore
|
||||
|
||||
- name: Upload screenshots (on failure)
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: test-screenshots
|
||||
path: e2e/screenshots
|
||||
if-no-files-found: ignore
|
||||
- name: Upload screenshots (on failure)
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: test-screenshots
|
||||
path: e2e/screenshots
|
||||
if-no-files-found: ignore
|
||||
|
||||
10
.github/workflows/manual-build-desktop.yml
vendored
10
.github/workflows/manual-build-desktop.yml
vendored
@@ -171,7 +171,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: release-${{ matrix.os }}
|
||||
path: |
|
||||
@@ -224,7 +224,7 @@ jobs:
|
||||
TMP: C:\temp
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: release-windows-2025
|
||||
path: |
|
||||
@@ -275,7 +275,7 @@ jobs:
|
||||
NEXT_PUBLIC_DESKTOP_UMAMI_BASE_URL: ${{ inputs.channel == 'beta' && secrets.UMAMI_BETA_DESKTOP_BASE_URL || secrets.UMAMI_NIGHTLY_DESKTOP_BASE_URL }}
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: release-ubuntu-latest
|
||||
path: |
|
||||
@@ -309,7 +309,7 @@ jobs:
|
||||
package-manager-cache: 'false'
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v6
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
path: release
|
||||
pattern: release-*
|
||||
@@ -330,7 +330,7 @@ jobs:
|
||||
run: bun run scripts/electronWorkflow/mergeMacReleaseFiles.js
|
||||
|
||||
- name: Upload artifacts with merged macOS files
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: merged-release-manual
|
||||
path: release/
|
||||
|
||||
8
.github/workflows/pr-build-desktop.yml
vendored
8
.github/workflows/pr-build-desktop.yml
vendored
@@ -194,7 +194,7 @@ jobs:
|
||||
|
||||
# 上传构建产物
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: release-${{ matrix.os }}
|
||||
path: |
|
||||
@@ -229,7 +229,7 @@ jobs:
|
||||
|
||||
# 下载所有平台的构建产物
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v6
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
path: release
|
||||
pattern: release-*
|
||||
@@ -255,7 +255,7 @@ jobs:
|
||||
|
||||
# 上传合并后的构建产物
|
||||
- name: Upload artifacts with merged macOS files
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: merged-release-pr
|
||||
path: release/
|
||||
@@ -280,7 +280,7 @@ jobs:
|
||||
|
||||
# 下载合并后的构建产物
|
||||
- name: Download merged artifacts
|
||||
uses: actions/download-artifact@v6
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: merged-release-pr
|
||||
path: release
|
||||
|
||||
4
.github/workflows/pr-build-docker.yml
vendored
4
.github/workflows/pr-build-docker.yml
vendored
@@ -91,7 +91,7 @@ jobs:
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: digest-${{ env.PLATFORM_PAIR }}
|
||||
path: /tmp/digests/*
|
||||
@@ -111,7 +111,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v6
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
path: /tmp/digests
|
||||
pattern: digest-*
|
||||
|
||||
8
.github/workflows/release-desktop-beta.yml
vendored
8
.github/workflows/release-desktop-beta.yml
vendored
@@ -181,7 +181,7 @@ jobs:
|
||||
|
||||
# 上传构建产物 (工作流处理重命名,不依赖 electron-builder 钩子)
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: release-${{ matrix.os }}
|
||||
path: |
|
||||
@@ -220,7 +220,7 @@ jobs:
|
||||
|
||||
# 下载所有平台的构建产物
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v6
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
path: release
|
||||
pattern: release-*
|
||||
@@ -246,7 +246,7 @@ jobs:
|
||||
|
||||
# 上传合并后的构建产物
|
||||
- name: Upload artifacts with merged macOS files
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: merged-release
|
||||
path: release/
|
||||
@@ -262,7 +262,7 @@ jobs:
|
||||
steps:
|
||||
# 下载合并后的构建产物
|
||||
- name: Download merged artifacts
|
||||
uses: actions/download-artifact@v6
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: merged-release
|
||||
path: release
|
||||
|
||||
4
.github/workflows/release-docker.yml
vendored
4
.github/workflows/release-docker.yml
vendored
@@ -80,7 +80,7 @@ jobs:
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: digest-${{ env.PLATFORM_PAIR }}
|
||||
path: /tmp/digests/*
|
||||
@@ -98,7 +98,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v6
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
path: /tmp/digests
|
||||
pattern: digest-*
|
||||
|
||||
51
.github/workflows/test.yml
vendored
51
.github/workflows/test.yml
vendored
@@ -76,14 +76,15 @@ jobs:
|
||||
fi
|
||||
done
|
||||
|
||||
# App tests
|
||||
test-website:
|
||||
# App tests - run sharded tests
|
||||
test-app:
|
||||
needs: check-duplicate-run
|
||||
if: needs.check-duplicate-run.outputs.should_skip != 'true'
|
||||
name: Test Website
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
shard: [1, 2]
|
||||
name: Test App (shard ${{ matrix.shard }}/2)
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
@@ -101,8 +102,44 @@ jobs:
|
||||
- name: Install deps
|
||||
run: bun i
|
||||
|
||||
- name: Test App Coverage
|
||||
run: bun run test-app:coverage
|
||||
- name: Run tests with blob reporter
|
||||
run: bunx vitest --coverage --reporter=blob --silent='passed-only' --shard=${{ matrix.shard }}/2
|
||||
|
||||
- name: Upload blob report
|
||||
if: ${{ !cancelled() }}
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: blob-report-${{ matrix.shard }}
|
||||
path: .vitest-reports
|
||||
include-hidden-files: true
|
||||
retention-days: 1
|
||||
|
||||
# Merge sharded test reports and upload coverage
|
||||
merge-app-coverage:
|
||||
needs: test-app
|
||||
if: ${{ !cancelled() && needs.test-app.result == 'success' }}
|
||||
name: Merge and Upload App Coverage
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Install bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: latest
|
||||
|
||||
- name: Install deps
|
||||
run: bun i
|
||||
|
||||
- name: Download blob reports
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
path: .vitest-reports
|
||||
pattern: blob-report-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Merge reports
|
||||
run: bunx vitest --merge-reports --coverage
|
||||
|
||||
- name: Upload App Coverage to Codecov
|
||||
uses: codecov/codecov-action@v5
|
||||
|
||||
@@ -63,6 +63,13 @@ LobeChat provides a complete authentication service capability when deployed. Th
|
||||
|
||||
<OIDCJWKs />
|
||||
|
||||
#### `INTERNAL_JWT_EXPIRATION`
|
||||
|
||||
- Type: Optional
|
||||
- Description: Expiration time for internal JWT tokens used in lambda → async calls. Format: number followed by unit (s=seconds, m=minutes, h=hours). Should be as short as possible for security, but long enough to account for network latency and server processing time.
|
||||
- Default: `30s`
|
||||
- Example: `30s`, `1m`, `1h`
|
||||
|
||||
### Email Service (SMTP)
|
||||
|
||||
These settings are required for email verification and password reset features.
|
||||
|
||||
@@ -61,6 +61,13 @@ LobeChat 在部署时提供了完善的身份验证服务能力,以下是相
|
||||
|
||||
<OIDCJWKs />
|
||||
|
||||
#### `INTERNAL_JWT_EXPIRATION`
|
||||
|
||||
- 类型:可选
|
||||
- 描述:内部 JWT 令牌的过期时间,用于 lambda → async 调用。格式:数字后跟单位(s = 秒,m = 分钟,h = 小时)。为了安全性应尽可能短,但需要足够长以应对网络延迟和服务器处理时间。
|
||||
- 默认值:`30s`
|
||||
- 示例:`30s`、`1m`、`1h`
|
||||
|
||||
### 邮件服务(SMTP)
|
||||
|
||||
启用邮箱验证和密码重置功能需要配置以下设置。
|
||||
|
||||
@@ -23,11 +23,16 @@ const genUserLLMConfig = (specificConfig: Record<any, any>): UserModelProviderCo
|
||||
};
|
||||
|
||||
export const DEFAULT_LLM_CONFIG = genUserLLMConfig({
|
||||
anthropic: {
|
||||
enabled: true,
|
||||
},
|
||||
google: {
|
||||
enabled: true,
|
||||
},
|
||||
lmstudio: {
|
||||
fetchOnClient: true,
|
||||
},
|
||||
ollama: {
|
||||
enabled: true,
|
||||
fetchOnClient: true,
|
||||
},
|
||||
openai: {
|
||||
|
||||
@@ -4,5 +4,5 @@ export const MIN_DEFAULT_IMAGE_NUM = 1;
|
||||
export const MAX_DEFAULT_IMAGE_NUM = 20;
|
||||
|
||||
export const DEFAULT_IMAGE_CONFIG: UserImageConfig = {
|
||||
defaultImageNum: 4,
|
||||
defaultImageNum: 2,
|
||||
};
|
||||
|
||||
@@ -218,7 +218,8 @@ const azureChatModels: AIChatModelCard[] = [
|
||||
deploymentName: 'gpt-4.1',
|
||||
},
|
||||
contextWindowTokens: 1_047_576,
|
||||
description: 'GPT-4.1 is our flagship model for complex tasks and cross-domain problem solving.',
|
||||
description:
|
||||
'GPT-4.1 is our flagship model for complex tasks and cross-domain problem solving.',
|
||||
displayName: 'GPT-4.1',
|
||||
enabled: true,
|
||||
id: 'gpt-4.1',
|
||||
@@ -467,7 +468,6 @@ const azureImageModels: AIImageModelCard[] = [
|
||||
enum: ['auto', '1024x1024', '1792x1024', '1024x1792'],
|
||||
},
|
||||
},
|
||||
resolutions: ['1024x1024', '1024x1792', '1792x1024'],
|
||||
type: 'image',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -951,6 +951,7 @@ const googleImageModels: AIImageModelCard[] = [
|
||||
{
|
||||
displayName: 'Nano Banana',
|
||||
id: 'gemini-2.5-flash-image:image',
|
||||
enabled: true,
|
||||
type: 'image',
|
||||
description:
|
||||
'Nano Banana is Google’s newest, fastest, and most efficient native multimodal model, enabling conversational image generation and editing.',
|
||||
|
||||
@@ -1063,6 +1063,14 @@ export const nanoBananaProParameters: ModelParamsSchema = {
|
||||
},
|
||||
};
|
||||
|
||||
const gptImage1Schema = {
|
||||
imageUrls: { default: [], maxCount: 1, maxFileSize: 5 },
|
||||
prompt: { default: '' },
|
||||
size: {
|
||||
default: 'auto',
|
||||
enum: ['auto', '1024x1024', '1536x1024', '1024x1536'],
|
||||
},
|
||||
};
|
||||
const lobehubImageModels: AIImageModelCard[] = [
|
||||
{
|
||||
description:
|
||||
@@ -1104,7 +1112,7 @@ const lobehubImageModels: AIImageModelCard[] = [
|
||||
{
|
||||
description: 'Imagen 4th generation text-to-image model series',
|
||||
displayName: 'Imagen 4 Fast',
|
||||
enabled: false,
|
||||
enabled: true,
|
||||
id: 'imagen-4.0-fast-generate-001',
|
||||
organization: 'Deepmind',
|
||||
parameters: imagenBaseParameters,
|
||||
@@ -1117,7 +1125,7 @@ const lobehubImageModels: AIImageModelCard[] = [
|
||||
{
|
||||
description: 'Imagen 4th generation text-to-image model series',
|
||||
displayName: 'Imagen 4',
|
||||
enabled: false,
|
||||
enabled: true,
|
||||
id: 'imagen-4.0-generate-001',
|
||||
organization: 'Deepmind',
|
||||
parameters: imagenBaseParameters,
|
||||
@@ -1130,7 +1138,7 @@ const lobehubImageModels: AIImageModelCard[] = [
|
||||
{
|
||||
description: 'Imagen 4th generation text-to-image model series Ultra version',
|
||||
displayName: 'Imagen 4 Ultra',
|
||||
enabled: false,
|
||||
enabled: true,
|
||||
id: 'imagen-4.0-ultra-generate-001',
|
||||
organization: 'Deepmind',
|
||||
parameters: imagenBaseParameters,
|
||||
@@ -1140,19 +1148,32 @@ const lobehubImageModels: AIImageModelCard[] = [
|
||||
releasedAt: '2025-08-15',
|
||||
type: 'image',
|
||||
},
|
||||
{
|
||||
description:
|
||||
'An enhanced GPT Image 1 model with 4× faster generation, more precise editing, and improved text rendering.',
|
||||
displayName: 'GPT Image 1.5',
|
||||
enabled: true,
|
||||
id: 'gpt-image-1.5',
|
||||
parameters: gptImage1Schema,
|
||||
pricing: {
|
||||
approximatePricePerImage: 0.034,
|
||||
units: [
|
||||
{ name: 'textInput', rate: 5, strategy: 'fixed', unit: 'millionTokens' },
|
||||
{ name: 'textInput_cacheRead', rate: 1.25, strategy: 'fixed', unit: 'millionTokens' },
|
||||
{ name: 'imageInput', rate: 8, strategy: 'fixed', unit: 'millionTokens' },
|
||||
{ name: 'imageInput_cacheRead', rate: 2, strategy: 'fixed', unit: 'millionTokens' },
|
||||
{ name: 'imageOutput', rate: 32, strategy: 'fixed', unit: 'millionTokens' },
|
||||
],
|
||||
},
|
||||
releasedAt: '2025-12-16',
|
||||
type: 'image',
|
||||
},
|
||||
{
|
||||
description: 'ChatGPT native multimodal image generation model.',
|
||||
displayName: 'GPT Image 1',
|
||||
enabled: true,
|
||||
id: 'gpt-image-1',
|
||||
parameters: {
|
||||
imageUrls: { default: [], maxCount: 1, maxFileSize: 5 },
|
||||
prompt: { default: '' },
|
||||
size: {
|
||||
default: 'auto',
|
||||
enum: ['auto', '1024x1024', '1536x1024', '1024x1536'],
|
||||
},
|
||||
},
|
||||
parameters: gptImage1Schema,
|
||||
pricing: {
|
||||
approximatePricePerImage: 0.042,
|
||||
units: [
|
||||
@@ -1169,7 +1190,7 @@ const lobehubImageModels: AIImageModelCard[] = [
|
||||
description:
|
||||
'The latest DALL·E model, released in November 2023, supports more realistic, accurate image generation with stronger detail.',
|
||||
displayName: 'DALL·E 3',
|
||||
enabled: false,
|
||||
enabled: true,
|
||||
id: 'dall-e-3',
|
||||
parameters: {
|
||||
prompt: { default: '' },
|
||||
@@ -1203,7 +1224,6 @@ const lobehubImageModels: AIImageModelCard[] = [
|
||||
},
|
||||
],
|
||||
},
|
||||
resolutions: ['1024x1024', '1024x1792', '1792x1024'],
|
||||
type: 'image',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1219,6 +1219,26 @@ export const openaiSTTModels: AISTTModelCard[] = [
|
||||
|
||||
// Image generation models
|
||||
export const openaiImageModels: AIImageModelCard[] = [
|
||||
{
|
||||
description:
|
||||
'An enhanced GPT Image 1 model with 4× faster generation, more precise editing, and improved text rendering.',
|
||||
displayName: 'GPT Image 1.5',
|
||||
enabled: true,
|
||||
id: 'gpt-image-1.5',
|
||||
parameters: gptImage1ParamsSchema,
|
||||
pricing: {
|
||||
approximatePricePerImage: 0.034,
|
||||
units: [
|
||||
{ name: 'textInput', rate: 5, strategy: 'fixed', unit: 'millionTokens' },
|
||||
{ name: 'textInput_cacheRead', rate: 1.25, strategy: 'fixed', unit: 'millionTokens' },
|
||||
{ name: 'imageInput', rate: 8, strategy: 'fixed', unit: 'millionTokens' },
|
||||
{ name: 'imageInput_cacheRead', rate: 2, strategy: 'fixed', unit: 'millionTokens' },
|
||||
{ name: 'imageOutput', rate: 32, strategy: 'fixed', unit: 'millionTokens' },
|
||||
],
|
||||
},
|
||||
releasedAt: '2025-12-16',
|
||||
type: 'image',
|
||||
},
|
||||
// https://platform.openai.com/docs/models/gpt-image-1
|
||||
{
|
||||
description: 'ChatGPT native multimodal image generation model.',
|
||||
@@ -1236,7 +1256,6 @@ export const openaiImageModels: AIImageModelCard[] = [
|
||||
{ name: 'imageOutput', rate: 40, strategy: 'fixed', unit: 'millionTokens' },
|
||||
],
|
||||
},
|
||||
resolutions: ['1024x1024', '1024x1536', '1536x1024'],
|
||||
type: 'image',
|
||||
},
|
||||
{
|
||||
@@ -1257,13 +1276,13 @@ export const openaiImageModels: AIImageModelCard[] = [
|
||||
],
|
||||
},
|
||||
releasedAt: '2025-10-06',
|
||||
resolutions: ['1024x1024', '1024x1536', '1536x1024'],
|
||||
type: 'image',
|
||||
},
|
||||
{
|
||||
description:
|
||||
'The latest DALL·E model, released in November 2023, supports more realistic, accurate image generation with stronger detail.',
|
||||
displayName: 'DALL·E 3',
|
||||
enabled: true,
|
||||
id: 'dall-e-3',
|
||||
parameters: {
|
||||
prompt: { default: '' },
|
||||
@@ -1296,7 +1315,6 @@ export const openaiImageModels: AIImageModelCard[] = [
|
||||
},
|
||||
],
|
||||
},
|
||||
resolutions: ['1024x1024', '1024x1792', '1792x1024'],
|
||||
type: 'image',
|
||||
},
|
||||
{
|
||||
@@ -1329,7 +1347,6 @@ export const openaiImageModels: AIImageModelCard[] = [
|
||||
},
|
||||
],
|
||||
},
|
||||
resolutions: ['256x256', '512x512', '1024x1024'],
|
||||
type: 'image',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -67,7 +67,10 @@ async function generateByImageMode(
|
||||
const defaultInput = {
|
||||
n: 1,
|
||||
...(model.includes('dall-e') ? { response_format: 'b64_json' } : {}),
|
||||
...(isImageEdit && model === 'gpt-image-1' ? { input_fidelity: 'high' } : {}),
|
||||
// https://platform.openai.com/docs/api-reference/images/createEdit#images_createedit-input_fidelity
|
||||
...(isImageEdit && model.includes('gpt-image-') && !model.includes('mini')
|
||||
? { input_fidelity: 'high' }
|
||||
: {}),
|
||||
};
|
||||
|
||||
const options = cleanObject({
|
||||
|
||||
@@ -311,7 +311,7 @@ exports[`LobeOpenAI > models > should get models 1`] = `
|
||||
"contextWindowTokens": undefined,
|
||||
"description": "The latest DALL·E model, released in November 2023, supports more realistic, accurate image generation with stronger detail.",
|
||||
"displayName": "DALL·E 3",
|
||||
"enabled": false,
|
||||
"enabled": true,
|
||||
"functionCall": false,
|
||||
"id": "dall-e-3",
|
||||
"imageOutput": false,
|
||||
|
||||
@@ -49,14 +49,7 @@ describe('ssrfSafeFetch', () => {
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
'https://httpbin.org/get',
|
||||
expect.objectContaining({
|
||||
agent: expect.objectContaining({
|
||||
requestFilterOptions: expect.objectContaining({
|
||||
allowIPAddressList: [],
|
||||
allowMetaIPAddress: false,
|
||||
allowPrivateIPAddress: false,
|
||||
denyIPAddressList: [],
|
||||
}),
|
||||
}),
|
||||
agent: expect.any(Function),
|
||||
}),
|
||||
);
|
||||
expect(response).toBeInstanceOf(Response);
|
||||
@@ -80,14 +73,7 @@ describe('ssrfSafeFetch', () => {
|
||||
'https://httpbin.org/post',
|
||||
expect.objectContaining({
|
||||
...requestOptions,
|
||||
agent: expect.objectContaining({
|
||||
requestFilterOptions: expect.objectContaining({
|
||||
allowIPAddressList: [],
|
||||
allowMetaIPAddress: false,
|
||||
allowPrivateIPAddress: false,
|
||||
denyIPAddressList: [],
|
||||
}),
|
||||
}),
|
||||
agent: expect.any(Function),
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -302,14 +288,7 @@ describe('ssrfSafeFetch', () => {
|
||||
'https://api.example.com/data',
|
||||
expect.objectContaining({
|
||||
...requestOptions,
|
||||
agent: expect.objectContaining({
|
||||
requestFilterOptions: expect.objectContaining({
|
||||
allowIPAddressList: ['127.0.0.1'],
|
||||
allowMetaIPAddress: true,
|
||||
allowPrivateIPAddress: true,
|
||||
denyIPAddressList: [],
|
||||
}),
|
||||
}),
|
||||
agent: expect.any(Function),
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -323,19 +302,11 @@ describe('ssrfSafeFetch', () => {
|
||||
|
||||
await ssrfSafeFetch('https://secure.example.com/api');
|
||||
|
||||
// Verify that the agent is properly configured for HTTPS
|
||||
// Verify that the agent function is passed
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
'https://secure.example.com/api',
|
||||
expect.objectContaining({
|
||||
agent: expect.objectContaining({
|
||||
protocol: 'https:',
|
||||
requestFilterOptions: expect.objectContaining({
|
||||
allowIPAddressList: [],
|
||||
allowMetaIPAddress: false,
|
||||
allowPrivateIPAddress: false,
|
||||
denyIPAddressList: [],
|
||||
}),
|
||||
}),
|
||||
agent: expect.any(Function),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import fetch from 'node-fetch';
|
||||
import { RequestFilteringAgentOptions, useAgent as ssrfAgent } from 'request-filtering-agent';
|
||||
import {
|
||||
RequestFilteringAgentOptions,
|
||||
RequestFilteringHttpAgent,
|
||||
RequestFilteringHttpsAgent,
|
||||
} from 'request-filtering-agent';
|
||||
|
||||
/**
|
||||
* Options for per-call SSRF configuration overrides
|
||||
@@ -42,10 +46,16 @@ export const ssrfSafeFetch = async (
|
||||
denyIPAddressList: [],
|
||||
};
|
||||
|
||||
// Create agents for both protocols
|
||||
const httpAgent = new RequestFilteringHttpAgent(agentOptions);
|
||||
const httpsAgent = new RequestFilteringHttpsAgent(agentOptions);
|
||||
|
||||
// Use node-fetch with SSRF protection agent
|
||||
// Pass a function to dynamically select agent based on URL protocol
|
||||
// This handles redirects from HTTP to HTTPS correctly
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
agent: ssrfAgent(url, agentOptions),
|
||||
agent: (parsedURL: URL) => (parsedURL.protocol === 'https:' ? httpsAgent : httpAgent),
|
||||
} as any);
|
||||
|
||||
// Convert node-fetch Response to standard Response
|
||||
|
||||
@@ -93,8 +93,8 @@ const styles = createStaticStyles(({ css }) => {
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
width: ${thumbnailSize};
|
||||
height: ${thumbnailSize};
|
||||
width: ${thumbnailSize}px;
|
||||
height: ${thumbnailSize}px;
|
||||
border-radius: ${cssVar.borderRadius};
|
||||
|
||||
background: ${cssVar.colorBgContainer};
|
||||
@@ -112,7 +112,7 @@ const styles = createStaticStyles(({ css }) => {
|
||||
gap: 8px;
|
||||
|
||||
width: 100%;
|
||||
height: ${thumbnailSize};
|
||||
height: ${thumbnailSize}px;
|
||||
padding: 0;
|
||||
border-radius: ${cssVar.borderRadiusLG};
|
||||
|
||||
|
||||
@@ -77,15 +77,8 @@ const GenerationFeed = memo(() => {
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flexbox
|
||||
gap={16}
|
||||
ref={parent}
|
||||
style={{
|
||||
minHeight: 'calc(100vh - 180px)',
|
||||
}}
|
||||
width="100%"
|
||||
>
|
||||
<Flexbox flex={1}>
|
||||
<Flexbox gap={16} ref={parent} width="100%">
|
||||
{currentGenerationBatches.map((batch, index) => (
|
||||
<Fragment key={batch.id}>
|
||||
{Boolean(index !== 0) && <Divider dashed style={{ margin: 0 }} />}
|
||||
@@ -95,7 +88,7 @@ const GenerationFeed = memo(() => {
|
||||
</Flexbox>
|
||||
{/* Invisible element for scroll target */}
|
||||
<div ref={containerRef} style={{ height: 1 }} />
|
||||
</>
|
||||
</Flexbox>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ const DesktopImagePage = memo(() => {
|
||||
<>
|
||||
<NavHeader right={<WideScreenButton />} />
|
||||
<Flexbox height={'100%'} style={{ overflowY: 'auto', position: 'relative' }} width={'100%'}>
|
||||
<WideScreenContainer>
|
||||
<WideScreenContainer height={'100%'} wrapperStyle={{ height: '100%' }}>
|
||||
<Suspense fallback={<SkeletonList />}>
|
||||
<ImageWorkspace />
|
||||
</Suspense>
|
||||
|
||||
@@ -158,6 +158,15 @@ declare global {
|
||||
* Can be generated using `node scripts/generate-oidc-jwk.mjs`.
|
||||
*/
|
||||
JWKS_KEY?: string;
|
||||
|
||||
/**
|
||||
* Internal JWT expiration time for lambda → async calls.
|
||||
* Format: number followed by unit (s=seconds, m=minutes, h=hours)
|
||||
* Examples: '10s', '1m', '1h'
|
||||
* Should be as short as possible for security, but long enough to account for network latency and server processing time.
|
||||
* @default '30s'
|
||||
*/
|
||||
INTERNAL_JWT_EXPIRATION?: string;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -285,6 +294,9 @@ export const getAuthConfig = () => {
|
||||
// Generic JWKS key for signing/verifying JWTs
|
||||
JWKS_KEY: z.string().optional(),
|
||||
ENABLE_OIDC: z.boolean(),
|
||||
|
||||
// Internal JWT expiration time (e.g., '10s', '1m', '1h')
|
||||
INTERNAL_JWT_EXPIRATION: z.string().default('30s'),
|
||||
},
|
||||
|
||||
runtimeEnv: {
|
||||
@@ -415,6 +427,9 @@ export const getAuthConfig = () => {
|
||||
// Generic JWKS key (fallback to OIDC_JWKS_KEY for backward compatibility)
|
||||
JWKS_KEY: process.env.JWKS_KEY || process.env.OIDC_JWKS_KEY,
|
||||
ENABLE_OIDC: !!(process.env.JWKS_KEY || process.env.OIDC_JWKS_KEY),
|
||||
|
||||
// Internal JWT expiration time
|
||||
INTERNAL_JWT_EXPIRATION: process.env.INTERNAL_JWT_EXPIRATION,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,12 +1,26 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
|
||||
import { aiProviderSelectors, useAiInfraStore } from '@/store/aiInfra';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
import { systemStatusSelectors } from '@/store/global/selectors';
|
||||
import { useImageStore } from '@/store/image';
|
||||
import {
|
||||
DEFAULT_AI_IMAGE_MODEL,
|
||||
DEFAULT_AI_IMAGE_PROVIDER,
|
||||
} from '@/store/image/slices/generationConfig/initialState';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { authSelectors } from '@/store/user/selectors';
|
||||
|
||||
const checkModelEnabled = (
|
||||
enabledImageModelList: ReturnType<typeof aiProviderSelectors.enabledImageModelList>,
|
||||
provider: string,
|
||||
model: string,
|
||||
) => {
|
||||
return enabledImageModelList.some(
|
||||
(p) => p.id === provider && p.children.some((m) => m.id === model),
|
||||
);
|
||||
};
|
||||
|
||||
export const useFetchAiImageConfig = () => {
|
||||
const isStatusInit = useGlobalStore(systemStatusSelectors.isStatusInit);
|
||||
const isInitAiProviderRuntimeState = useAiInfraStore(
|
||||
@@ -29,16 +43,46 @@ export const useFetchAiImageConfig = () => {
|
||||
const isInitializedImageConfig = useImageStore((s) => s.isInit);
|
||||
const initializeImageConfig = useImageStore((s) => s.initializeImageConfig);
|
||||
|
||||
const enabledImageModelList = useAiInfraStore(aiProviderSelectors.enabledImageModelList);
|
||||
|
||||
// Determine which model/provider to use for initialization
|
||||
const initParams = useMemo(() => {
|
||||
// 1. Try lastSelected if enabled
|
||||
if (
|
||||
lastSelectedImageModel &&
|
||||
lastSelectedImageProvider &&
|
||||
checkModelEnabled(enabledImageModelList, lastSelectedImageProvider, lastSelectedImageModel)
|
||||
) {
|
||||
return { model: lastSelectedImageModel, provider: lastSelectedImageProvider };
|
||||
}
|
||||
|
||||
// 2. Try default model from any enabled provider (prefer default provider first)
|
||||
if (
|
||||
checkModelEnabled(enabledImageModelList, DEFAULT_AI_IMAGE_PROVIDER, DEFAULT_AI_IMAGE_MODEL)
|
||||
) {
|
||||
return { model: undefined, provider: undefined }; // Use initialState defaults
|
||||
}
|
||||
const providerWithDefaultModel = enabledImageModelList.find((p) =>
|
||||
p.children.some((m) => m.id === DEFAULT_AI_IMAGE_MODEL),
|
||||
);
|
||||
if (providerWithDefaultModel) {
|
||||
return { model: DEFAULT_AI_IMAGE_MODEL, provider: providerWithDefaultModel.id };
|
||||
}
|
||||
|
||||
// 3. Fallback to first enabled model
|
||||
const firstProvider = enabledImageModelList[0];
|
||||
const firstModel = firstProvider?.children[0];
|
||||
if (firstProvider && firstModel) {
|
||||
return { model: firstModel.id, provider: firstProvider.id };
|
||||
}
|
||||
|
||||
// No enabled models
|
||||
return { model: undefined, provider: undefined };
|
||||
}, [lastSelectedImageModel, lastSelectedImageProvider, enabledImageModelList]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isInitializedImageConfig && isReadyForInit) {
|
||||
initializeImageConfig(isLogin, lastSelectedImageModel, lastSelectedImageProvider);
|
||||
initializeImageConfig(isLogin, initParams.model, initParams.provider);
|
||||
}
|
||||
}, [
|
||||
isReadyForInit,
|
||||
isInitializedImageConfig,
|
||||
isLogin,
|
||||
lastSelectedImageModel,
|
||||
lastSelectedImageProvider,
|
||||
initializeImageConfig,
|
||||
]);
|
||||
}, [isReadyForInit, isInitializedImageConfig, isLogin, initParams, initializeImageConfig]);
|
||||
};
|
||||
|
||||
@@ -66,7 +66,7 @@ const getVerificationKey = async () => {
|
||||
|
||||
/**
|
||||
* Sign JWT for internal lambda → async calls
|
||||
* Uses JWKS private key with short expiration (3s)
|
||||
* Uses JWKS private key with configurable expiration (default: 30s)
|
||||
* The JWT only proves the request is from lambda, payload is sent via LOBE_CHAT_AUTH_HEADER
|
||||
*/
|
||||
export const signInternalJWT = async (): Promise<string> => {
|
||||
@@ -75,7 +75,7 @@ export const signInternalJWT = async (): Promise<string> => {
|
||||
return new SignJWT({ purpose: INTERNAL_JWT_PURPOSE })
|
||||
.setProtectedHeader({ alg: 'RS256', kid })
|
||||
.setIssuedAt()
|
||||
.setExpirationTime('3s')
|
||||
.setExpirationTime(authEnv.INTERNAL_JWT_EXPIRATION)
|
||||
.sign(key);
|
||||
};
|
||||
|
||||
|
||||
@@ -4,13 +4,13 @@ import {
|
||||
ModelProvider,
|
||||
type RuntimeImageGenParams,
|
||||
extractDefaultValues,
|
||||
gptImage1ParamsSchema,
|
||||
} from 'model-bank';
|
||||
import { nanoBananaProParameters } from 'model-bank/google';
|
||||
|
||||
import { DEFAULT_IMAGE_CONFIG } from '@/const/settings';
|
||||
|
||||
export const DEFAULT_AI_IMAGE_PROVIDER = ModelProvider.OpenAI;
|
||||
export const DEFAULT_AI_IMAGE_MODEL = 'gpt-image-1';
|
||||
export const DEFAULT_AI_IMAGE_PROVIDER = ModelProvider.Google;
|
||||
export const DEFAULT_AI_IMAGE_MODEL = 'gemini-3-pro-image-preview:image';
|
||||
|
||||
export interface GenerationConfigState {
|
||||
parameters: RuntimeImageGenParams;
|
||||
@@ -30,14 +30,14 @@ export interface GenerationConfigState {
|
||||
}
|
||||
|
||||
export const DEFAULT_IMAGE_GENERATION_PARAMETERS: RuntimeImageGenParams =
|
||||
extractDefaultValues(gptImage1ParamsSchema);
|
||||
extractDefaultValues(nanoBananaProParameters);
|
||||
|
||||
export const initialGenerationConfigState: GenerationConfigState = {
|
||||
model: DEFAULT_AI_IMAGE_MODEL,
|
||||
provider: DEFAULT_AI_IMAGE_PROVIDER,
|
||||
imageNum: DEFAULT_IMAGE_CONFIG.defaultImageNum,
|
||||
parameters: DEFAULT_IMAGE_GENERATION_PARAMETERS,
|
||||
parametersSchema: gptImage1ParamsSchema,
|
||||
parametersSchema: nanoBananaProParameters,
|
||||
isAspectRatioLocked: false,
|
||||
activeAspectRatio: null,
|
||||
isInit: false,
|
||||
|
||||
@@ -6,6 +6,7 @@ import { ImageStore } from '@/store/image';
|
||||
import { initialState } from '@/store/image/initialState';
|
||||
import { merge } from '@/utils/merge';
|
||||
|
||||
import { DEFAULT_AI_IMAGE_MODEL, DEFAULT_AI_IMAGE_PROVIDER } from './initialState';
|
||||
import { imageGenerationConfigSelectors } from './selectors';
|
||||
|
||||
// Mock external dependencies
|
||||
@@ -57,7 +58,7 @@ describe('imageGenerationConfigSelectors', () => {
|
||||
|
||||
it('should return the default model from initial state', () => {
|
||||
const result = imageGenerationConfigSelectors.model(initialStore);
|
||||
expect(result).toBe('gpt-image-1'); // Default model from initialState
|
||||
expect(result).toBe(DEFAULT_AI_IMAGE_MODEL);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -70,7 +71,7 @@ describe('imageGenerationConfigSelectors', () => {
|
||||
|
||||
it('should return the default provider from initial state', () => {
|
||||
const result = imageGenerationConfigSelectors.provider(initialStore);
|
||||
expect(result).toBe('openai'); // Default provider from initialState
|
||||
expect(result).toBe(DEFAULT_AI_IMAGE_PROVIDER);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -98,7 +99,10 @@ describe('imageGenerationConfigSelectors', () => {
|
||||
it('should return the current parameters', () => {
|
||||
const state = merge(initialStore, { parameters: testParameters });
|
||||
const result = imageGenerationConfigSelectors.parameters(state);
|
||||
expect(result).toEqual(testParameters);
|
||||
// merge does deep merge, so result contains both default and test values
|
||||
expect(result.prompt).toBe(testParameters.prompt);
|
||||
expect(result.size).toBe(testParameters.size);
|
||||
expect(result.imageUrls).toEqual(testParameters.imageUrls);
|
||||
});
|
||||
|
||||
it('should return the default parameters from initial state', () => {
|
||||
@@ -120,7 +124,10 @@ describe('imageGenerationConfigSelectors', () => {
|
||||
it('should return the current parametersSchema', () => {
|
||||
const state = merge(initialStore, { parametersSchema: testModelSchema });
|
||||
const result = imageGenerationConfigSelectors.parametersSchema(state);
|
||||
expect(result).toEqual(testModelSchema);
|
||||
// merge does deep merge, so result contains both default and test values
|
||||
expect(result.prompt).toEqual(testModelSchema.prompt);
|
||||
expect(result.size).toEqual(testModelSchema.size);
|
||||
expect(result.imageUrls).toEqual(testModelSchema.imageUrls);
|
||||
});
|
||||
|
||||
it('should return default parametersSchema when not explicitly overridden', () => {
|
||||
|
||||
@@ -2,6 +2,10 @@ import { dirname, join, resolve } from 'node:path';
|
||||
import { coverageConfigDefaults, defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
optimizeDeps: {
|
||||
exclude: ['crypto', 'util', 'tty'],
|
||||
include: ['@lobehub/tts'],
|
||||
},
|
||||
plugins: [
|
||||
/**
|
||||
* @lobehub/fluent-emoji@4.0.0 ships `es/FluentEmoji/style.js` but its `es/FluentEmoji/index.js`
|
||||
@@ -27,16 +31,13 @@ export default defineConfig({
|
||||
id.endsWith('/FluentEmoji/style/index.js') ||
|
||||
id.endsWith('/FluentEmoji/style/index.js?');
|
||||
|
||||
if (isFluentEmojiEntry && isMissingStyleIndex) return resolve(dirname(importer), 'style.js');
|
||||
if (isFluentEmojiEntry && isMissingStyleIndex)
|
||||
return resolve(dirname(importer), 'style.js');
|
||||
|
||||
return null;
|
||||
},
|
||||
},
|
||||
],
|
||||
optimizeDeps: {
|
||||
exclude: ['crypto', 'util', 'tty'],
|
||||
include: ['@lobehub/tts'],
|
||||
},
|
||||
test: {
|
||||
alias: {
|
||||
/* eslint-disable sort-keys-fix/sort-keys-fix */
|
||||
@@ -77,11 +78,14 @@ export default defineConfig({
|
||||
environment: 'happy-dom',
|
||||
exclude: [
|
||||
'**/node_modules/**',
|
||||
'**/.*/**',
|
||||
'**/dist/**',
|
||||
'**/build/**',
|
||||
'**/tmp/**',
|
||||
'**/temp/**',
|
||||
'**/.temp/**',
|
||||
'**/docs/**',
|
||||
'**/locales/**',
|
||||
'**/public/**',
|
||||
'**/apps/desktop/**',
|
||||
'**/apps/mobile/**',
|
||||
'**/packages/**',
|
||||
|
||||
Reference in New Issue
Block a user