Files
Shayne Wong e566f514c7 feat: add support for GPT Image 2.5 models with extended quality options and new UI components
- Introduced new image model picker component for selecting models and qualities.
- Updated image model utilities to handle new models and quality options.
- Implemented backend logic for validating image model options and handling requests.
- Added tests for image model selection, quality handling, and API interactions.
- Adjusted existing tests to accommodate new model features and ensure compatibility.
2026-09-10 19:23:50 +08:00

315 lines
10 KiB
Python
Executable File

import os
import statistics
import time
from pathlib import Path
import pytest
from backend.app.core import settings as config
from backend.app.core.observability import metrics
from backend.app.repositories import db as db_repo
from backend.app.repositories import image_jobs as image_jobs_repo
from backend.app.repositories.gallery import mutations as gallery_mutations
from backend.app.repositories.gallery import queries as gallery_queries
pytestmark = pytest.mark.skipif(
os.getenv("RUN_PERFORMANCE_TESTS") != "true",
reason="set RUN_PERFORMANCE_TESTS=true to run performance baselines",
)
def _configure_runtime(tmp_path: Path):
images_dir = tmp_path / "images"
data_dir = tmp_path / "data"
images_dir.mkdir(parents=True, exist_ok=True)
data_dir.mkdir(parents=True, exist_ok=True)
config.IMAGES_DIR = str(images_dir)
config.THUMBNAILS_DIR = str(images_dir / "thumbs")
config.DATA_DIR = str(data_dir)
config.DATABASE_FILE = str(data_dir / "app.sqlite3")
config.DEFAULT_UPSTREAM_SOCKS5_PROXY = ""
db_repo.close_database_connections()
metrics.reset()
db_repo.verify_storage_writable()
def _seed_gallery_rows(row_count: int):
now_prefix = "2026-05-18T12:"
sizes = ("1024x1024", "1536x1024", "1024x1536")
models = ("gpt-image-2", "gpt-image-1")
presets = ("Default", "Studio", "Draft")
with db_repo._connect() as conn:
for start in range(0, row_count, 5_000):
rows = [
{
"id": f"img-{index:06d}",
"prompt": f"benchmark prompt {index % 100} p{index % 10}",
"size": sizes[index % len(sizes)],
"filename": f"img-{index:06d}.png",
"created_at": f"{now_prefix}{index % 60:02d}:{index % 60:02d}",
"model": models[index % len(models)],
"quality": "auto",
"output_format": "png",
"n": 1,
"api_path": "/v1/images/generations",
"api_preset_name": presets[index % len(presets)],
"favorite": index % 7 == 0,
"bytes": 128 + (index % 8192),
}
for index in range(start, min(start + 5_000, row_count))
]
with db_repo._transaction(conn):
gallery_mutations._insert_gallery_entries_on_conn(conn, rows)
def _seed_job_rows(row_count: int):
for index in range(row_count):
image_jobs_repo.upsert_generate_job(
{
"job_id": f"job-{index:04d}",
"status": "success",
"stage": "completed",
"message": "completed",
"operation": "generation",
"prompt": f"history prompt {index}",
"size": "1024x1024",
"created_at": f"2026-05-18T12:{index % 60:02d}:00",
"updated_at": f"2026-05-18T12:{index % 60:02d}:01",
"completed_at": f"2026-05-18T20:{index % 60:02d}:01+08:00",
"model": "gpt-image-2",
"duration": "1.00s",
}
)
def _measure_ms(callback, iterations: int = 30) -> tuple[float, float]:
durations = []
for _ in range(iterations):
started_at = time.perf_counter()
callback()
durations.append((time.perf_counter() - started_at) * 1000)
p50 = statistics.median(durations)
p95 = statistics.quantiles(durations, n=100, method="inclusive")[94]
return p50, p95
@pytest.mark.parametrize("row_count", [1_000, 10_000])
def test_gallery_page_query_baseline(tmp_path, row_count, record_property):
_configure_runtime(tmp_path)
_seed_gallery_rows(row_count)
def query():
page = gallery_queries.get_gallery_page(
page=1,
page_size=9,
filters={"prompt": "benchmark prompt 4"},
include_total_bytes=True,
)
assert page.total > 0
p50, p95 = _measure_ms(query)
record_property(f"gallery_{row_count}_rows_p50_ms", round(p50, 2))
record_property(f"gallery_{row_count}_rows_p95_ms", round(p95, 2))
assert p95 < 500
@pytest.mark.parametrize("row_count", [50_000, 100_000])
@pytest.mark.parametrize(
("case_name", "page", "filters", "include_total_bytes", "include_counts", "include_filter_options"),
[
("first_page_no_prompt", 1, {}, False, True, True),
("deep_page_no_prompt", 5_000, {}, False, True, False),
(
"combined_filters",
1,
{
"model": "gpt-image-2",
"preset": "Default",
"size": "1024x1024",
"favorite": True,
},
False,
True,
False,
),
("short_prompt_like", 1, {"prompt": "p4"}, False, True, False),
("total_bytes", 1, {}, True, True, False),
("lightweight_cursor_page", 2, {}, False, False, False),
],
)
def test_gallery_large_query_baselines(
tmp_path,
row_count,
case_name,
page,
filters,
include_total_bytes,
include_counts,
include_filter_options,
record_property,
):
_configure_runtime(tmp_path)
_seed_gallery_rows(row_count)
cursor = None
direction = "next"
if case_name == "lightweight_cursor_page":
first_page = gallery_queries.get_gallery_page(
page=1,
page_size=9,
filters=filters,
include_counts=False,
include_filter_options=False,
)
assert first_page.next_cursor
cursor = first_page.next_cursor
def query():
gallery_page = gallery_queries.get_gallery_page(
page=page,
page_size=9,
filters=filters,
include_total_bytes=include_total_bytes,
include_counts=include_counts,
include_filter_options=include_filter_options,
cursor=cursor,
direction=direction,
)
assert gallery_page.images or page > gallery_page.total_pages
if include_counts:
assert gallery_page.total >= 0
if include_total_bytes:
assert gallery_page.total_bytes > 0
p50, p95 = _measure_ms(query, iterations=12)
prefix = f"gallery_{row_count}_rows_{case_name}"
record_property(f"{prefix}_p50_ms", round(p50, 2))
record_property(f"{prefix}_p95_ms", round(p95, 2))
assert p95 < 3000
def test_gallery_deep_page_anchor_cache_100k(tmp_path, record_property):
_configure_runtime(tmp_path)
_seed_gallery_rows(100_000)
started_at = time.perf_counter()
first_page = gallery_queries.get_gallery_page(
page=5_000,
page_size=9,
include_filter_options=False,
)
first_ms = (time.perf_counter() - started_at) * 1000
assert first_page.images
assert first_page.timings_ms.get("anchor_seeded_by_offset") == 1.0
def query():
page = gallery_queries.get_gallery_page(
page=5_000,
page_size=9,
include_filter_options=False,
)
assert page.images
assert page.timings_ms.get("anchor_seeded_by_offset", 0.0) == 0.0
p50, p95 = _measure_ms(query, iterations=12)
record_property("gallery_100000_rows_deep_page_first_ms", round(first_ms, 2))
record_property("gallery_100000_rows_deep_page_cached_p50_ms", round(p50, 2))
record_property("gallery_100000_rows_deep_page_cached_p95_ms", round(p95, 2))
assert p95 < 1000
@pytest.mark.parametrize(
("case_name", "page", "filters"),
[
(
"favorite_model_size",
200,
{
"model": "gpt-image-2",
"size": "1024x1024",
"favorite": True,
},
),
("prompt_fts", 50, {"prompt": "benchmark prompt 4"}),
],
)
def test_gallery_filtered_deep_page_anchor_baselines(
tmp_path,
monkeypatch,
case_name,
page,
filters,
record_property,
):
_configure_runtime(tmp_path)
monkeypatch.setattr(gallery_queries, "GALLERY_PAGE_ANCHOR_SMALL_OFFSET_THRESHOLD", 100)
monkeypatch.setattr(gallery_queries, "GALLERY_PAGE_ANCHOR_INTERVAL_PAGES", 25)
_seed_gallery_rows(100_000)
first_page = gallery_queries.get_gallery_page(
page=page,
page_size=9,
filters=filters,
include_filter_options=False,
)
assert first_page.images
assert first_page.timings_ms.get("anchor_seeded_by_offset") == 1.0
def query():
gallery_page = gallery_queries.get_gallery_page(
page=page,
page_size=9,
filters=filters,
include_filter_options=False,
)
assert gallery_page.images
assert gallery_page.timings_ms.get("anchor_seeded_by_offset", 0.0) == 0.0
p50, p95 = _measure_ms(query, iterations=12)
prefix = f"gallery_100000_rows_{case_name}_deep_anchor"
record_property(f"{prefix}_p50_ms", round(p50, 2))
record_property(f"{prefix}_p95_ms", round(p95, 2))
assert p95 < 1000
def test_gallery_cursor_query_baseline(tmp_path, record_property):
_configure_runtime(tmp_path)
_seed_gallery_rows(10_000)
first_page = gallery_queries.get_gallery_page(
page=1,
page_size=9,
filters={"prompt": "benchmark prompt 4"},
)
assert first_page.next_cursor
def query():
page = gallery_queries.get_gallery_page(
page=2,
page_size=9,
filters={"prompt": "benchmark prompt 4"},
cursor=first_page.next_cursor,
direction="next",
)
assert page.total > 0
p50, p95 = _measure_ms(query)
record_property("gallery_10000_rows_cursor_p50_ms", round(p50, 2))
record_property("gallery_10000_rows_cursor_p95_ms", round(p95, 2))
assert p95 < 250
def test_job_history_query_baseline(tmp_path, record_property):
_configure_runtime(tmp_path)
_seed_job_rows(500)
def query():
rows = image_jobs_repo.list_generate_jobs(limit=50, offset=0)
assert len(rows) == 50
p50, p95 = _measure_ms(query)
record_property("job_history_500_rows_p50_ms", round(p50, 2))
record_property("job_history_500_rows_p95_ms", round(p95, 2))
assert p95 < 200