feat: add OpenAI image generation controls
This commit is contained in:
@@ -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` 本地分析文档,记录外部独立图片网关只作为后续部署方案,不在本次代码落地。
|
||||
+82
@@ -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.
|
||||
Reference in New Issue
Block a user