feat: add OpenAI image generation controls

This commit is contained in:
2ue
2026-05-05 03:26:54 +08:00
parent 4de28fec8c
commit 6faa344916
85 changed files with 6086 additions and 568 deletions
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-05-03
@@ -0,0 +1,70 @@
## Overview
本次只实现“图片独立并发开关”,不实现外部图片网关的运行时代码。目标是在最大程度不改变现有行为的前提下,为图片流式长连接提供服务级资源保护。
## Current Constraints
- 当前 Redis 并发槽位只有用户和账号维度,键语义是 `concurrency:user:*``concurrency:account:*`
- 图片接口和普通 Responses 在同一个 Go 服务内运行,共享进程、HTTP 上游连接池和账号调度。
- Codex OAuth 路径会自动注入 `image_generation` tool;这个注入表示“模型具备工具能力”,不等价于当前请求一定会生图。
- `/v1/responses` 在 handler 入口只能可靠识别显式图片意图:image 模型、请求体已有 image tool、或 tool_choice 明确选择 image_generation。
- 图片实际产物计数与计费仍以 service 层的最终输出解析为准。
## Decisions
### 1. 默认关闭,保持兼容
新增配置:
- `gateway.image_concurrency.enabled`,默认 `false`
- `gateway.image_concurrency.max_concurrent_requests`,默认 `0`,表示不限制。
- `gateway.image_concurrency.overflow_mode`,默认 `reject`,可选 `reject` / `wait`
- `gateway.image_concurrency.wait_timeout_seconds`,默认 `30`,仅 `overflow_mode=wait` 生效。
- `gateway.image_concurrency.max_waiting_requests`,默认 `100`,仅 `overflow_mode=wait` 生效,限制当前进程内图片等待队列。
只有当 `enabled=true``max_concurrent_requests>0` 时才启用图片独立并发限制。默认配置不改变任何现有流量行为。
### 2. 进程级信号量作为第一阶段隔离
本次使用进程内有界信号量做服务级图片并发限制。原因:
- 不扩展现有 Redis `ConcurrencyCache` 接口,避免影响用户/账号并发的既有语义。
- 不新增迁移,不改变分组已有字段。
- 单实例部署可立即保护进程资源。
- 多实例部署时该限制按实例生效;文档必须明确总图片并发约等于 `实例数 × max_concurrent_requests`
### 3. 限制对象只包含明确图片意图
纳入限制:
- `/v1/images/generations`
- `/v1/images/edits`
- `/v1/responses` 中入口请求已明确包含图片意图:image 模型、`tools[].type=image_generation``tool_choice` 明确选择 image_generation。
暂不纳入限制:
- 普通 Codex 请求因为服务端自动注入 image tool 而具备生图能力,但入口请求本身未明确要求生图。
这样避免把普通编码请求错误算作图片并发。后续若要对“模型运行中动态调用 image tool”做更细粒度隔离,需要在工具调用实际发生时获得可阻塞的事件,目前当前代码没有这种入口级阻塞点。
### 4. 限流行为
- `overflow_mode=reject` 时,未开始流式响应直接返回 HTTP `429`,错误类型 `rate_limit_error`
- `overflow_mode=wait` 时,请求在当前进程内等待图片并发槽位,超过 `wait_timeout_seconds` 或超过 `max_waiting_requests` 后返回 HTTP `429`
- 已开始流式响应时,使用现有 `handleStreamingAwareError` 写 SSE 错误事件。
- 图片并发限制命中或等待超时不触发账号 failover,不记录为上游账号失败。
- `gateway.image_stream_data_interval_timeout` 是上游图片流数据空闲超时,不用于图片排队等待。
### 5. 与外部图片网关的关系
本次不实现外部图片网关代码。外部网关方案沉淀到 `2ue` 文档:
- 推荐由 Caddy/Nginx/API Gateway 按 `/v1/images/*` 分流。
- `/v1/responses` 的图片 tool 请求不能仅靠 path 分流,必须在前置层读取 body 或保留主服务兜底。
- 即使未来拆出图片网关,主网关仍保留图片 intent 检测、开关和计费兜底,避免直连或漏判绕过。
## Risks And Mitigations
- 风险:进程级限制在多实例部署下不是全局严格限制。缓解:文档明确容量计算,后续可基于 Redis 扩展为集群级图片并发。
- 风险:Codex 自动注入 image tool 后,普通编码请求未被图片限流。缓解:这是有意选择,避免误伤普通请求;实际输出图片仍按图片计费。
- 风险:图片请求在账号槽位前被拒绝可能改变排队体验。缓解:仅当独立开关启用时生效,默认关闭;429 明确提示图片并发达到上限。
@@ -0,0 +1,28 @@
## Why
图片生成流式请求会比普通文本流式请求占用更长的连接、goroutine、HTTP 上游连接和账号/用户槽位。当前图片能力已经具备独立计费与更长流式超时,但仍缺少默认关闭的图片专属并发隔离开关,图片高并发时仍可能挤压普通文本流式接口。
## What Changes
- 新增服务级图片独立并发开关,默认关闭,不改变现有已部署分组和普通文本请求行为。
- 新增图片全局并发上限配置;开启后仅限制已明确是图片生成意图的请求。
- 新增图片并发满载后的溢出策略配置:默认立即拒绝,也可配置等待槽位和等待超时。
- 将图片并发限制覆盖 `/v1/images/generations``/v1/images/edits``/v1/responses` 显式图片生成请求。
- 保留当前图片生成开关、图片计费、图片流式续读与超时语义。
- 不在本次代码实现外部独立图片网关;只把外部网关拆分方案沉淀到本地文档。
## Capabilities
### New Capabilities
- `image-generation-concurrency-isolation`: 图片生成请求的独立并发开关、并发上限、429 行为和外部网关落地建议。
### Modified Capabilities
- `image-stream-resilience`: 图片流式续读能力在独立并发开启时受到图片专属并发上限保护,但流式续读与计费契约不变。
## Impact
- 影响 `backend/internal/config/config.go` 的 gateway 配置字段、默认值和校验。
- 影响 `backend/internal/handler/openai_images.go``backend/internal/handler/openai_gateway_handler.go` 的图片请求入口限流。
- 影响 `deploy/config.example.yaml` 的示例配置与说明。
- 影响后端测试:配置默认值/校验、图片接口限流、Responses 显式 image tool 限流。
- 新增或更新 `2ue` 本地分析文档,记录外部独立图片网关只作为后续部署方案,不在本次代码落地。
@@ -0,0 +1,82 @@
# image-generation-concurrency-isolation Specification
## ADDED Requirements
### Requirement: Image concurrency isolation is opt-in
The system SHALL keep image concurrency isolation disabled by default.
#### Scenario: default config keeps existing behavior
- **GIVEN** the deployment does not set `gateway.image_concurrency.enabled`
- **WHEN** image generation requests are received
- **THEN** no new image-specific concurrency limit is applied
- **AND** existing user/account concurrency and billing behavior remains unchanged
### Requirement: Dedicated image concurrency limit
The system SHALL provide an opt-in service-level image concurrency limit controlled by gateway configuration.
#### Scenario: explicit image endpoint is limited
- **GIVEN** `gateway.image_concurrency.enabled=true`
- **AND** `gateway.image_concurrency.max_concurrent_requests=1`
- **AND** one image generation request is already active
- **WHEN** another `/v1/images/generations` or `/v1/images/edits` request arrives
- **THEN** the second request is rejected with HTTP `429`
- **AND** the error type is `rate_limit_error`
#### Scenario: explicit Responses image generation request is limited
- **GIVEN** `gateway.image_concurrency.enabled=true`
- **AND** `gateway.image_concurrency.max_concurrent_requests=1`
- **AND** `gateway.image_concurrency.overflow_mode=reject`
- **AND** one image generation request is already active
- **WHEN** a `/v1/responses` request explicitly contains `tools[].type=image_generation`, an image model, or `tool_choice` selecting `image_generation`
- **THEN** the request is rejected with HTTP `429`
- **AND** it is not retried through account failover
#### Scenario: image request waits for a slot
- **GIVEN** `gateway.image_concurrency.enabled=true`
- **AND** `gateway.image_concurrency.max_concurrent_requests=1`
- **AND** `gateway.image_concurrency.overflow_mode=wait`
- **AND** `gateway.image_concurrency.wait_timeout_seconds` is greater than zero
- **AND** one image generation request is already active
- **WHEN** another explicit image generation request arrives
- **AND** the active image generation request releases its slot before the wait timeout
- **THEN** the waiting image generation request acquires the slot and continues
#### Scenario: image wait times out
- **GIVEN** `gateway.image_concurrency.enabled=true`
- **AND** `gateway.image_concurrency.max_concurrent_requests=1`
- **AND** `gateway.image_concurrency.overflow_mode=wait`
- **AND** one image generation request is already active
- **WHEN** another explicit image generation request waits longer than `gateway.image_concurrency.wait_timeout_seconds`
- **THEN** the waiting request is rejected with HTTP `429`
- **AND** the error type is `rate_limit_error`
#### Scenario: image waiting queue is full
- **GIVEN** `gateway.image_concurrency.enabled=true`
- **AND** `gateway.image_concurrency.overflow_mode=wait`
- **AND** `gateway.image_concurrency.max_waiting_requests` is already reached
- **WHEN** another explicit image generation request arrives
- **THEN** the request is rejected with HTTP `429`
- **AND** it does not wait for account scheduling
### Requirement: Text requests are not image-limited
The system SHALL NOT apply the image concurrency limit to requests without explicit image generation intent.
#### Scenario: normal coding request bypasses image limit
- **GIVEN** `gateway.image_concurrency.enabled=true`
- **AND** the image concurrency limit is full
- **WHEN** a `/v1/responses` request uses a text model and does not explicitly contain image generation intent
- **THEN** the image concurrency limiter does not reject it
- **AND** normal user/account concurrency handling continues
### Requirement: External image gateway remains a deployment pattern
The system SHALL document external image gateway routing as a deployment option without adding runtime forwarding code in this change.
#### Scenario: operator reads local design note
- **GIVEN** the repository documentation is available
- **WHEN** an operator evaluates isolating image traffic into a separate service
- **THEN** local `2ue` documentation describes which paths are safe to route by path
- **AND** explains why `/v1/responses` image tool requests require body-aware routing or main-gateway fallback
@@ -0,0 +1,28 @@
## 1. Spec and documentation
- [x] 1.1 Create OpenSpec proposal, design, tasks, and capability spec for image concurrency isolation.
- [x] 1.2 Add a local `2ue` note for the external image gateway deployment pattern and current non-goals.
## 2. Config
- [x] 2.1 Add `gateway.image_concurrency.enabled` and `gateway.image_concurrency.max_concurrent_requests` config fields.
- [x] 2.2 Register defaults that keep existing behavior unchanged.
- [x] 2.3 Validate max concurrent requests as non-negative.
- [x] 2.4 Update `deploy/config.example.yaml` with safe usage notes.
- [x] 2.5 Add image concurrency overflow mode, wait timeout, and max waiting request config.
## 3. Runtime limiter
- [x] 3.1 Implement a process-level image concurrency limiter with resize-on-config-read behavior.
- [x] 3.2 Acquire/release the limiter around `/v1/images/generations` and `/v1/images/edits` before account scheduling.
- [x] 3.3 Acquire/release the limiter around explicit `/v1/responses` image generation intent before account scheduling.
- [x] 3.4 Ensure limiter rejections return `429 rate_limit_error` and do not trigger account failover.
- [x] 3.5 Support `reject` and `wait` overflow modes with bounded wait timeout and waiting queue size.
## 4. Tests and verification
- [x] 4.1 Add config default and validation tests.
- [x] 4.2 Add handler tests for image endpoint limiter rejection.
- [x] 4.3 Add handler tests proving text-only Responses requests are not rejected by the image limiter.
- [x] 4.4 Run focused Go tests for config and OpenAI handler/service paths.
- [x] 4.5 Add limiter tests for wait success, wait timeout, and waiting queue overflow.