chore: remove openspec and update axios
This commit is contained in:
@@ -19,7 +19,7 @@
|
||||
"@stripe/stripe-js": "^9.0.1",
|
||||
"@tanstack/vue-virtual": "^3.13.23",
|
||||
"@vueuse/core": "^10.7.0",
|
||||
"axios": "^1.15.0",
|
||||
"axios": "^1.16.0",
|
||||
"chart.js": "^4.4.1",
|
||||
"dompurify": "^3.3.1",
|
||||
"driver.js": "^1.4.0",
|
||||
|
||||
18
frontend/pnpm-lock.yaml
generated
18
frontend/pnpm-lock.yaml
generated
@@ -21,8 +21,8 @@ importers:
|
||||
specifier: ^10.7.0
|
||||
version: 10.11.1(vue@3.5.26(typescript@5.6.3))
|
||||
axios:
|
||||
specifier: ^1.15.0
|
||||
version: 1.15.0
|
||||
specifier: ^1.16.0
|
||||
version: 1.16.0
|
||||
chart.js:
|
||||
specifier: ^4.4.1
|
||||
version: 4.5.1
|
||||
@@ -1858,8 +1858,8 @@ packages:
|
||||
peerDependencies:
|
||||
postcss: ^8.1.0
|
||||
|
||||
axios@1.15.0:
|
||||
resolution: {integrity: sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==}
|
||||
axios@1.16.0:
|
||||
resolution: {integrity: sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==}
|
||||
|
||||
babel-plugin-macros@3.1.0:
|
||||
resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==}
|
||||
@@ -2534,8 +2534,8 @@ packages:
|
||||
flatted@3.3.3:
|
||||
resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
|
||||
|
||||
follow-redirects@1.15.11:
|
||||
resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==}
|
||||
follow-redirects@1.16.0:
|
||||
resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==}
|
||||
engines: {node: '>=4.0'}
|
||||
peerDependencies:
|
||||
debug: '*'
|
||||
@@ -6484,9 +6484,9 @@ snapshots:
|
||||
postcss: 8.5.6
|
||||
postcss-value-parser: 4.2.0
|
||||
|
||||
axios@1.15.0:
|
||||
axios@1.16.0:
|
||||
dependencies:
|
||||
follow-redirects: 1.15.11
|
||||
follow-redirects: 1.16.0
|
||||
form-data: 4.0.5
|
||||
proxy-from-env: 2.1.0
|
||||
transitivePeerDependencies:
|
||||
@@ -7228,7 +7228,7 @@ snapshots:
|
||||
|
||||
flatted@3.3.3: {}
|
||||
|
||||
follow-redirects@1.15.11: {}
|
||||
follow-redirects@1.16.0: {}
|
||||
|
||||
for-in@1.0.2: {}
|
||||
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
schema: spec-driven
|
||||
created: 2026-04-29
|
||||
@@ -1,227 +0,0 @@
|
||||
## Context
|
||||
|
||||
当前代码已经具备图片价格字段和部分图片转发能力,但边界不完整:
|
||||
|
||||
- `backend/ent/schema/group.go` 只有 `rate_multiplier` 和 `image_price_1k/2k/4k`,没有分组级生图能力开关,也没有“图片是否共享分组倍率”的开关。
|
||||
- `backend/internal/handler/openai_images.go` 在解析 `/v1/images/*` 后只做通用余额/订阅资格检查,没有检查分组是否允许生图。
|
||||
- `backend/internal/service/openai_gateway_service.go` 对 Codex CLI 会自动注入 `image_generation` tool;通用 `/v1/responses` 只记录日志,没有把图片工具产物数量写入 `OpenAIForwardResult.ImageCount`。
|
||||
- `backend/internal/service/billing_service.go` 的 `CalculateImageCost` 当前使用 `image_price_* * image_count * rate_multiplier`。这个行为本身可以作为默认兼容模式,但普通编码分组 `rate_multiplier=0.15` 且希望图片最终价为 `0.2/张` 时,管理员必须填写 `image_price=0.2/0.15`,不可读且不适合长期运营。
|
||||
- `backend/internal/service/openai_gateway_service.go` 和 `backend/internal/service/gateway_service.go` 的渠道图片计费路径当前传 `RequestCount: 1`,多图请求会按 1 次收费。
|
||||
- `backend/internal/service/openai_images.go` 的 OpenAI 图片尺寸分层此前只覆盖少量固定尺寸;`gpt-image-2` 官方文档已经支持满足约束的自定义 `size`,因此本地计费必须能够对未知尺寸做稳定分档,同时不能因为本地映射不认识就提前拦截请求。
|
||||
|
||||
用户澄清后的业务要求是:普通编码分组可以关闭生图,也可以开启生图;开启后默认继续共享现有分组倍率以保持兼容,但管理员可以打开“生图倍率独立”开关,改用单独的图片倍率输入框。图片分组是推荐的运营隔离方式,但不是唯一承载方式。
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- 分组具备明确的 `allow_image_generation` 开关,所有已知生图入口在调度上游前执行同一个权限判断。
|
||||
- 分组具备“生图倍率是否独立”的开关;默认 `false`,即共享当前代码里的有效分组倍率。
|
||||
- 生图倍率独立开关打开后,图片费用使用单独的 `image_rate_multiplier`,不再使用普通编码分组的倍率。
|
||||
- 保留现有 `image_price_1k/2k/4k` 字段作为图片单价配置,不强制把它们迁移成新的语义。
|
||||
- 普通编码分组在 `allow_image_generation=false` 时仍可正常使用 `gpt-5.4` / `gpt-5.5` 文本能力,但不能使用图片工具。
|
||||
- 普通编码分组在 `allow_image_generation=true` 时可使用 `gpt-5.4` / `gpt-5.5 + image_generation`,且按实际图片数量收费。
|
||||
- 通用 `/v1/responses`、OpenAI Images API、流式、非流式、透传路径全部把成功产出的图片数量写入 `ImageCount`。
|
||||
- 渠道 `billing_mode=image` 使用真实 `ImageCount`,不再固定按 1 次收费。
|
||||
|
||||
**Non-Goals:**
|
||||
- 不引入新的第三方依赖。
|
||||
- 不改变 OpenAI 上游协议;只在现有请求转发、响应解析和计费归因层补齐控制。
|
||||
- 不把“图片分组”做成唯一安全边界;分组开关和图片计费逻辑必须适用于任意开启生图的分组。
|
||||
- 不在本变更中实现预扣费/资金冻结;失败请求仍不收费,成功请求按实际产物后扣费。
|
||||
- 不改变默认历史图片价格行为;默认共享现有有效倍率,历史 `图片价格 * 分组/用户有效倍率` 的扣费行为保持。
|
||||
- 不在本变更中新增用户级图片独立倍率覆盖;用户专属普通倍率只在共享倍率模式下继续影响图片。
|
||||
|
||||
## Decisions
|
||||
|
||||
### 0. 兼容性优先原则
|
||||
|
||||
本变更的默认行为必须以“不改变现有已配置分组的最终扣费”为优先级:
|
||||
|
||||
- 迁移不修改现有 `image_price_1k/2k/4k`。
|
||||
- 迁移把所有现有分组设置为 `image_rate_independent=false`,因此现有图片路径继续使用当前有效分组倍率。
|
||||
- 管理员不传新字段更新分组时,不得覆盖已保存的 `allow_image_generation`、`image_rate_independent`、`image_rate_multiplier`。
|
||||
- 前端编辑旧分组时必须回显服务端值;不能因为表单默认值把旧分组从共享倍率误改成独立倍率,或把允许生图误改成禁止生图。
|
||||
- 只有管理员显式打开 `image_rate_independent` 后,图片扣费才从共享倍率切换到图片独立倍率。
|
||||
|
||||
### 1. 分组字段与迁移策略
|
||||
|
||||
新增三个分组字段,对应“2 个开关 + 1 个输入框”:
|
||||
|
||||
- `allow_image_generation BOOLEAN NOT NULL DEFAULT false`
|
||||
- `image_rate_independent BOOLEAN NOT NULL DEFAULT false`
|
||||
- `image_rate_multiplier DECIMAL(10,4) NOT NULL DEFAULT 1.0`
|
||||
|
||||
字段语义:
|
||||
|
||||
- `allow_image_generation`:是否支持当前分组生图。
|
||||
- `image_rate_independent=false`:图片计费共享当前普通计费链路里的有效倍率,即当前 `userGroupRateResolver.Resolve(ctx, user.ID, groupID, group.RateMultiplier)` 得到的倍率;这保持现有行为。
|
||||
- `image_rate_independent=true`:图片计费使用 `group.image_rate_multiplier`;普通编码的 `rate_multiplier` 和用户专属普通倍率不参与图片扣费。
|
||||
- `image_price_1k/2k/4k`:继续表示图片基础单价,由选中的图片倍率模式继续相乘。
|
||||
|
||||
新建分组默认 `allow_image_generation=false`,避免新普通编码分组意外获得生图能力。为避免升级后立即打断已有图片业务,迁移对现有 `openai`、`gemini`、`antigravity` 分组回填 `allow_image_generation=true`,`anthropic` 分组保持 `false`。该回填只是兼容现状;上线后管理员必须按业务策略关闭不允许生图的普通编码分组。
|
||||
|
||||
迁移不改写已有 `image_price_1k/2k/4k`,并将所有现有分组设为 `image_rate_independent=false`、`image_rate_multiplier=1`。这样现有最终扣费公式保持不变:
|
||||
|
||||
```text
|
||||
历史/默认模式图片最终扣费 = image_price_* * image_count * 当前有效分组倍率
|
||||
```
|
||||
|
||||
普通编码分组 `rate_multiplier=0.15` 且希望图片 1K 最终扣费 `0.2/张` 时,管理员不再需要填写 `0.2/0.15`,而是设置:
|
||||
|
||||
```text
|
||||
image_rate_independent = true
|
||||
image_rate_multiplier = 1
|
||||
image_price_1k = 0.2
|
||||
```
|
||||
|
||||
如果希望图片也打折,例如图片标价 `0.2/张`、图片折扣 `0.8`,则设置:
|
||||
|
||||
```text
|
||||
image_rate_independent = true
|
||||
image_rate_multiplier = 0.8
|
||||
image_price_1k = 0.2
|
||||
```
|
||||
|
||||
### 2. 生图意图统一识别
|
||||
|
||||
新增一个服务层 helper,输入至少包含 endpoint、请求模型、请求体,输出是否为生图意图:
|
||||
|
||||
```text
|
||||
isImageGenerationIntent =
|
||||
endpoint 是 /v1/images/generations 或 /v1/images/edits
|
||||
OR requested model 以 gpt-image- 开头
|
||||
OR tools[] 存在 type == image_generation
|
||||
OR tool_choice 显式指向 image_generation
|
||||
```
|
||||
|
||||
生图意图判断必须在请求体被 Codex 注入、模型改写、渠道映射改写之前执行一次,并在这些改写之后再对最终请求体执行一次。原因是当前代码会在 `backend/internal/service/openai_gateway_service.go` 中注入 `image_generation` tool,也会在 `normalizeOpenAIResponsesImageOnlyModel` 中把 `gpt-image-*` 改写为文本模型 + 图片工具;只检查改写前或只检查改写后都可能漏掉场景。
|
||||
|
||||
`tool_choice` 判断只把明确指向 `image_generation` 的值视为生图意图;`auto`、`none`、`required` 本身不构成生图意图,但如果 `tools[]` 中存在 `image_generation`,仍由 `tools[]` 规则命中。
|
||||
|
||||
该判断必须在以下位置使用:
|
||||
|
||||
- `/v1/images/*` handler 解析请求后、账号调度前。
|
||||
- `/v1/responses` 解析 body 后、Codex 自动注入 `image_generation` tool 前。
|
||||
- `normalizeOpenAIResponsesImageOnlyModel` 把 `gpt-image-*` 改写为 Responses 文本模型前。
|
||||
- OpenAI 高级 scheduler 入口保留现有账号能力检查,同时补齐渠道 restriction 检查,避免启用高级调度时绕过渠道模型限制。
|
||||
|
||||
当 `allow_image_generation=false` 时:
|
||||
|
||||
- 显式生图意图返回 HTTP 403,错误类型使用现有 `permission_error` 风格。
|
||||
- Codex CLI 请求不自动注入 `image_generation` tool,也不追加图片桥接指令;如果请求没有显式生图意图,则继续按普通文本请求处理。
|
||||
|
||||
### 3. gpt-5.4 / gpt-5.5 生图承载方式
|
||||
|
||||
`gpt-5.4` / `gpt-5.5` 生图通过现有 OpenAI Responses API 的 `image_generation` tool 承载,不新增专用 endpoint:
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "gpt-5.4",
|
||||
"input": "生成一张图片",
|
||||
"tools": [
|
||||
{
|
||||
"type": "image_generation",
|
||||
"model": "gpt-image-2",
|
||||
"size": "1024x1024",
|
||||
"output_format": "png"
|
||||
}
|
||||
],
|
||||
"tool_choice": { "type": "image_generation" }
|
||||
}
|
||||
```
|
||||
|
||||
`model=gpt-image-*` 发到 `/v1/responses` 时保留现有改写方向:主模型改为 Responses 文本模型,图片模型放入 `image_generation` tool。计费时如果能从工具配置得到 `gpt-image-*`,图片默认价格按该图片模型解析;如果工具未指定图片模型,则使用当前转发结果的 billing model,并优先使用分组/渠道配置价格。
|
||||
|
||||
### 4. 图片数量归因
|
||||
|
||||
新增统一图片输出解析 helper,返回去重后的图片数量和可用图片元信息。必须覆盖以下已有或可借鉴的事件形态:
|
||||
|
||||
- 非流式 Responses JSON:`output[]` 中 `type == image_generation_call` 且 `result` 非空。
|
||||
- Responses SSE:`response.output_item.done` 中 `item.type == image_generation_call` 且 `item.result` 非空。
|
||||
- Responses SSE 完成事件:`response.completed.response.output[]` 中图片工具结果。
|
||||
- Images API 非流式:顶层 `data[]`。
|
||||
- Images API 流式:顶层 `data[]`、`image_generation.completed`、`response.output_item.done`、`response.completed`。
|
||||
|
||||
去重键按优先级使用 `item.id`、`call_id`、`result` 内容 hash。只统计最终图片,不统计 `partial_image`。
|
||||
|
||||
`openaiStreamingResult` 增加 `imageCount`、`imageSize`、`imageBillingModel`。`handleStreamingResponse`、`handleStreamingResponsePassthrough`、`handleNonStreamingResponse`、`handleNonStreamingResponsePassthrough` 都必须把解析结果带回 `OpenAIForwardResult`。当 `ImageCount > 0` 时,即使上游 usage 为 0,也必须写 usage log 并进入图片计费。
|
||||
|
||||
### 5. 图片价格公式
|
||||
|
||||
图片计费先确定单价,再确定倍率:
|
||||
|
||||
```text
|
||||
unit_price = 渠道 image 模式价格 或 分组 image_price_* 或 默认图片价格
|
||||
image_multiplier =
|
||||
如果 group.image_rate_independent == true: group.image_rate_multiplier
|
||||
否则: 当前有效分组倍率
|
||||
total_cost = unit_price * image_count
|
||||
actual_cost = total_cost * image_multiplier
|
||||
```
|
||||
|
||||
“当前有效分组倍率”必须沿用当前代码的倍率解析方式:默认配置倍率 → 分组 `rate_multiplier` → 用户专属分组倍率覆盖。这样 `image_rate_independent=false` 时完全保留当前行为。
|
||||
|
||||
`billing_mode=image` 的渠道价格是图片单价来源之一,仍优先于分组图片价格。图片渠道价格也必须按 `ImageCount` 计数,并使用同一套 `image_multiplier` 选择逻辑。
|
||||
|
||||
`billing_mode=per_request` 的非图片请求保持当前普通按次语义,继续使用普通 token 倍率;只有已经识别为图片请求且 `ImageCount > 0` 的路径使用图片计费逻辑。
|
||||
|
||||
`usage_logs.rate_multiplier` 继续表示“本次扣费实际使用的倍率”。因此:
|
||||
|
||||
- token 日志记录普通 token 有效倍率。
|
||||
- image 日志在共享模式记录普通有效倍率。
|
||||
- image 日志在独立模式记录 `image_rate_multiplier`。
|
||||
|
||||
专用 `/v1/images/*` 仍按图片请求语义计费:当 `ImageCount > 0` 时,图片价格决定费用,伴随的上游 token usage 只记录不额外计 token 费用。这保持当前 Images API 的行为。
|
||||
|
||||
通用 `/v1/responses + image_generation` 的混合文本+图片输出存在一个明确取舍:如果继续沿用“`ImageCount > 0` 时只按图片计费”的当前计费分支,用户可以在一次图片请求中夹带大量文本输出而只付图片费用;如果改成“图片费用 + 非图片 token 费用”,会改变当前 `billing_mode=image` 的单一计费语义,并可能让渠道图片单价不再是全包价格。本变更为最大兼容性不引入混合计费模式,但必须在 usage log 中完整记录 token 与 image_count,便于后续按数据决定是否新增 `image_plus_token` 计费模式。
|
||||
|
||||
### 6. 尺寸档位与参数透传
|
||||
|
||||
OpenAI 图片请求的 `size` 参数必须透传给上游;本地只做计费分档,不做 OpenAI 尺寸合法性校验。无论尺寸是否满足官方约束,本地都不能因为未知尺寸或 provider-invalid 尺寸返回 400;如果上游不接受该尺寸,由上游响应错误。
|
||||
|
||||
官方 `gpt-image-2` 文档给出的常用尺寸与约束是本地计费分档的依据:
|
||||
|
||||
- 常用尺寸:`1024x1024`、`1536x1024`、`1024x1536`、`2048x2048`、`2048x1152`、`3840x2160`、`2160x3840`、`auto`。
|
||||
- 自定义尺寸:官方支持满足约束的任意 `size`,包括边长、16 像素倍数、长短边比例、总像素范围等约束。
|
||||
- `2560x1440` 是 2K/QHD 参考边界;超过 `2560x1440` 总像素的输出进入更高档位风险区。
|
||||
|
||||
OpenAI 图片尺寸分层必须按以下规则:
|
||||
|
||||
```text
|
||||
empty, auto => 2K
|
||||
1024x1024 => 1K
|
||||
1536x1024, 1024x1536 => 2K
|
||||
1792x1024, 1024x1792 => 2K
|
||||
2048x2048, 2048x1152, 1152x2048 => 2K
|
||||
3840x2160, 2160x3840 => 4K
|
||||
未知且无法解析为正整数 WIDTHxHEIGHT => 2K
|
||||
未知且 WIDTH * HEIGHT <= 2560*1440 => 2K
|
||||
未知且 WIDTH * HEIGHT > 2560*1440 => 4K
|
||||
```
|
||||
|
||||
这个规则只决定 `ImageSize` 和扣费档位,不修改请求体,不删除未知参数,不把未知尺寸改写成预设尺寸。
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- 历史普通编码分组迁移后仍默认允许生图 → 通过管理员可见开关、上线核对清单和新建分组默认关闭来控制;代码无法可靠判断“普通编码分组”和“图片分组”的业务意图。
|
||||
- 默认共享现有有效倍率仍保留“图片最终价不直观”的问题 → 这是兼容性选择;需要直观设置图片最终价的分组必须打开 `image_rate_independent`。
|
||||
- 独立图片倍率不会读取用户专属普通倍率 → 这是目标行为;如需要用户级图片独立倍率,应作为后续独立需求实现。
|
||||
- 通用 Responses 图片工具可能同时输出文本和图片 → 本变更默认仍按图片请求语义计费并完整记录 token;若业务要求文本也收费,应新增独立的混合计费模式,不能混入本次兼容性变更。
|
||||
- 本地不再拦截未知或 provider-invalid OpenAI 尺寸 → 非法尺寸会消耗一次上游请求失败成本和用户体验往返,但这是为了保证参数透传、兼容官方新增尺寸和第三方兼容提供商;计费只在成功产出最终图片后发生。
|
||||
- Responses 流式解析需要在客户端断开后继续 drain 上游以完成计费 → 沿用当前流式处理“客户端断开后继续读取上游用于计费”的模式,并只新增轻量 JSON 路径提取。
|
||||
- 预扣费不在本变更中实现 → 继续使用现有成功后扣费模型,避免失败请求退款、流式中断退款和图片数量未确定时预估错误。
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. 新增数据库迁移,添加 `groups.allow_image_generation`、`groups.image_rate_independent` 和 `groups.image_rate_multiplier`。
|
||||
2. 回填现有分组:`openai`、`gemini`、`antigravity` 的 `allow_image_generation=true`,`anthropic=false`;所有现有分组 `image_rate_independent=false`、`image_rate_multiplier=1`。
|
||||
3. 不改写现有 `image_price_1k/2k/4k`,保持默认共享倍率模式下的历史扣费结果。
|
||||
4. 更新 Ent schema 与生成代码,更新后端 service/handler DTO 和前端类型。
|
||||
5. 先接入权限判断,确保未开启生图的分组不会到达上游。
|
||||
6. 再接入图片数量解析和图片计费倍率选择,确保开启生图的分组按图片数量收费。
|
||||
7. 最后更新前端管理界面、i18n、文档和测试。
|
||||
8. 回滚时只能通过新迁移回滚字段行为;不能修改已应用迁移文件。
|
||||
|
||||
## Open Questions
|
||||
|
||||
无。当前方案不依赖未确认的上游新尺寸、新模型或新 endpoint。
|
||||
@@ -1,29 +0,0 @@
|
||||
## Why
|
||||
|
||||
当前代码把“能否生图”和“如何按图片收费”混在模型、分组倍率、渠道定价与 Responses 工具调用里,导致 OpenAI 普通编码分组在允许 `gpt-5.4` / `gpt-5.5` 时也能通过 `image_generation` tool 产图,并且通用 `/v1/responses` 产图不会稳定写入 `ImageCount`。需要把生图能力、图片倍率模式、图片产出数量归因拆成独立能力,保证普通编码分组可按业务开关生图,开启后既能沿用现有倍率行为,也能按需切换到图片独立倍率。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 新增分组级生图能力开关,明确控制 `/v1/images/*`、`gpt-image-*`、显式 `image_generation` tool、Codex 自动注入图片工具等所有生图入口。
|
||||
- 新增分组级图片倍率模式开关,默认继续共享现有分组有效倍率;打开独立模式后使用图片独立倍率输入框。
|
||||
- 保留现有 `image_price_1k/2k/4k` 图片价格配置;图片最终扣费由“图片价格 × 当前倍率模式选出的倍率 × 图片数量”决定。
|
||||
- 统一统计 OpenAI Responses 图片工具产物数量,使 `gpt-5.4` / `gpt-5.5` 通过 `image_generation` tool 产图时进入图片计费,而不是退化成普通 token 计费或无 usage 时不计费。
|
||||
- 修正专用 Images API 与渠道图片计费场景,按实际图片数量和明确尺寸档位计费,避免固定 `RequestCount=1` 或未知尺寸静默落到 `2K`。
|
||||
- 更新后台分组配置、前端类型、使用说明和测试,覆盖普通编码分组关闭生图、普通编码分组开启生图、独立图片分组承载、生图流式/非流式等场景。
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `image-generation-access-control`: 定义分组级生图能力开关、所有生图意图识别规则、拒绝行为与 Codex 自动注入规则。
|
||||
- `image-generation-billing-accounting`: 定义图片倍率模式、图片数量归因、尺寸档位、渠道图片价格和用量日志要求。
|
||||
|
||||
### Modified Capabilities
|
||||
- 无。
|
||||
|
||||
## Impact
|
||||
|
||||
- Backend schema/API: `backend/ent/schema/group.go`、Ent 生成代码、数据库迁移、管理员分组 create/update/list DTO、分组缓存/序列化。
|
||||
- Backend request gates: `backend/internal/handler/openai_images.go`、`backend/internal/service/openai_gateway_service.go`、`backend/internal/service/openai_codex_transform.go`、OpenAI account scheduler 相关模型/图片能力调度入口。
|
||||
- Backend billing: `backend/internal/service/billing_service.go`、`backend/internal/service/openai_gateway_service.go`、`backend/internal/service/gateway_service.go`、usage log 与 account stats 成本计算路径。
|
||||
- Frontend admin: `frontend/src/types/index.ts`、`frontend/src/views/admin/GroupsView.vue`、相关 i18n 文案与图片计费展示。
|
||||
- Tests: OpenAI Images API、OpenAI Responses stream/non-stream/passthrough、分组开关、图片倍率模式、渠道图片计数、尺寸档位与 usage log 断言。
|
||||
@@ -1,118 +0,0 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Group image generation capability
|
||||
The system SHALL store a group-level `allow_image_generation` capability flag and SHALL expose it through admin group create, update, list, and detail APIs.
|
||||
|
||||
#### Scenario: New group defaults to image generation disabled
|
||||
- **WHEN** an admin creates a group without providing `allow_image_generation`
|
||||
- **THEN** the persisted group has `allow_image_generation=false`
|
||||
|
||||
#### Scenario: Existing image-capable platform groups are backfilled
|
||||
- **WHEN** the migration is applied to existing groups
|
||||
- **THEN** existing `openai`, `gemini`, and `antigravity` groups have `allow_image_generation=true`
|
||||
- **AND** existing `anthropic` groups have `allow_image_generation=false`
|
||||
|
||||
#### Scenario: Admin enables image generation on an ordinary coding group
|
||||
- **WHEN** an admin updates an `openai` group with `allow_image_generation=true`
|
||||
- **THEN** the group can use image generation paths subject to the billing requirements
|
||||
|
||||
#### Scenario: Admin disables image generation on an ordinary coding group
|
||||
- **WHEN** an admin updates an `openai` group with `allow_image_generation=false`
|
||||
- **THEN** the group can still use non-image text model requests
|
||||
- **AND** image generation intents are denied before upstream dispatch
|
||||
|
||||
### Requirement: Image generation intent detection
|
||||
The system SHALL classify a request as an image generation intent before upstream account scheduling when the endpoint or request body can produce generated images.
|
||||
|
||||
#### Scenario: Images endpoint is an image generation intent
|
||||
- **WHEN** a request targets `/v1/images/generations`, `/v1/images/edits`, `/images/generations`, or `/images/edits`
|
||||
- **THEN** the request is classified as an image generation intent
|
||||
|
||||
#### Scenario: Responses request with image-only model is an image generation intent
|
||||
- **WHEN** a `/v1/responses` request has a requested model whose normalized name starts with `gpt-image-`
|
||||
- **THEN** the request is classified as an image generation intent before any model rewrite
|
||||
|
||||
#### Scenario: Responses request with image_generation tool is an image generation intent
|
||||
- **WHEN** a `/v1/responses` request contains any `tools[]` entry with `type == "image_generation"`
|
||||
- **THEN** the request is classified as an image generation intent
|
||||
|
||||
#### Scenario: Responses request with image_generation tool_choice is an image generation intent
|
||||
- **WHEN** a `/v1/responses` request contains `tool_choice` that explicitly selects `image_generation`
|
||||
- **THEN** the request is classified as an image generation intent even if `tools[]` is malformed or absent
|
||||
|
||||
#### Scenario: Generic tool_choice required is not sufficient by itself
|
||||
- **WHEN** a `/v1/responses` request contains `tool_choice="required"`
|
||||
- **AND** the request does not contain an `image_generation` tool
|
||||
- **THEN** the request is not classified as an image generation intent because of `tool_choice` alone
|
||||
|
||||
#### Scenario: Text-only gpt-5.4 request is not an image generation intent
|
||||
- **WHEN** a `/v1/responses` request uses `model="gpt-5.4"` or `model="gpt-5.5"` without `image_generation` tool and without image `tool_choice`
|
||||
- **THEN** the request is not classified as an image generation intent
|
||||
|
||||
#### Scenario: Intent is checked before and after service-side mutation
|
||||
- **WHEN** the service mutates a `/v1/responses` request by injecting `image_generation` or rewriting `gpt-image-*` to a Responses text model plus image tool
|
||||
- **THEN** the final mutated request is checked against the same image generation intent rules before upstream dispatch
|
||||
|
||||
### Requirement: Disabled groups reject explicit image generation
|
||||
The system SHALL reject explicit image generation intents for groups with `allow_image_generation=false` before selecting or calling an upstream account.
|
||||
|
||||
#### Scenario: Disabled group rejects Images API
|
||||
- **WHEN** a group has `allow_image_generation=false`
|
||||
- **AND** a user calls `/v1/images/generations`
|
||||
- **THEN** the system returns HTTP 403 with error type `permission_error`
|
||||
- **AND** no upstream account is selected
|
||||
- **AND** no usage log is written
|
||||
|
||||
#### Scenario: Disabled group rejects Responses image tool
|
||||
- **WHEN** a group has `allow_image_generation=false`
|
||||
- **AND** a user calls `/v1/responses` with `tools:[{"type":"image_generation"}]`
|
||||
- **THEN** the system returns HTTP 403 with error type `permission_error`
|
||||
- **AND** no upstream account is selected
|
||||
- **AND** no usage log is written
|
||||
|
||||
#### Scenario: Disabled group rejects Responses image-only model rewrite
|
||||
- **WHEN** a group has `allow_image_generation=false`
|
||||
- **AND** a user calls `/v1/responses` with `model` starting with `gpt-image-`
|
||||
- **THEN** the system returns HTTP 403 with error type `permission_error`
|
||||
- **AND** the request is not rewritten to a text Responses model
|
||||
|
||||
#### Scenario: Disabled group permits normal coding request
|
||||
- **WHEN** a group has `allow_image_generation=false`
|
||||
- **AND** a user calls `/v1/responses` with `model="gpt-5.4"` and no image generation intent
|
||||
- **THEN** the request proceeds through the normal text forwarding path
|
||||
|
||||
### Requirement: Codex image tool injection respects group capability
|
||||
The system SHALL only inject the OpenAI Responses `image_generation` tool and bridge instructions for Codex clients when the request group has `allow_image_generation=true`.
|
||||
|
||||
#### Scenario: Codex request in enabled group receives image tool
|
||||
- **WHEN** a Codex CLI `/v1/responses` request belongs to a group with `allow_image_generation=true`
|
||||
- **AND** the request has no `image_generation` tool
|
||||
- **THEN** the system injects the existing `image_generation` tool payload
|
||||
- **AND** the system appends the existing Codex image bridge instructions
|
||||
|
||||
#### Scenario: Codex request in disabled group does not receive image tool
|
||||
- **WHEN** a Codex CLI `/v1/responses` request belongs to a group with `allow_image_generation=false`
|
||||
- **AND** the request has no explicit image generation intent
|
||||
- **THEN** the system does not inject `image_generation`
|
||||
- **AND** the system does not append image bridge instructions
|
||||
- **AND** the request proceeds as a text request
|
||||
|
||||
#### Scenario: Codex explicit image request in disabled group is denied
|
||||
- **WHEN** a Codex CLI `/v1/responses` request belongs to a group with `allow_image_generation=false`
|
||||
- **AND** the request explicitly contains `image_generation`
|
||||
- **THEN** the system returns HTTP 403 with error type `permission_error`
|
||||
|
||||
### Requirement: Channel model restrictions remain enforced
|
||||
The system SHALL keep existing channel model restriction behavior for image and non-image OpenAI requests, including when the advanced OpenAI account scheduler is enabled.
|
||||
|
||||
#### Scenario: Advanced scheduler blocks restricted requested model
|
||||
- **WHEN** a channel has `restrict_models=true`
|
||||
- **AND** the requested model is not allowed by channel pricing or mapping rules
|
||||
- **AND** the OpenAI advanced scheduler path is used
|
||||
- **THEN** the request is rejected before upstream account selection succeeds
|
||||
|
||||
#### Scenario: Image generation flag does not bypass channel restrictions
|
||||
- **WHEN** a group has `allow_image_generation=true`
|
||||
- **AND** the channel restriction rejects the requested or billing model
|
||||
- **THEN** the image generation request is rejected
|
||||
- **AND** no upstream image request is sent
|
||||
@@ -1,225 +0,0 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Image multiplier mode
|
||||
The system SHALL calculate image generation cost with group image prices and a selectable image multiplier mode. By default image billing SHALL share the existing effective group multiplier; when `image_rate_independent=true`, image billing SHALL use `image_rate_multiplier`.
|
||||
|
||||
#### Scenario: Default image billing shares current effective group multiplier
|
||||
- **WHEN** a group has `rate_multiplier=0.15`
|
||||
- **AND** `image_rate_independent=false`
|
||||
- **AND** `image_price_1k=0.2`
|
||||
- **AND** a successful image request produces one `1K` image
|
||||
- **THEN** `actual_cost` is `0.03`
|
||||
- **AND** the calculation matches current default behavior
|
||||
|
||||
#### Scenario: User-specific token multiplier still applies in shared mode
|
||||
- **WHEN** a user has a user-group token multiplier override of `0.2`
|
||||
- **AND** the group has `image_rate_independent=false`
|
||||
- **AND** `image_price_1k=0.5`
|
||||
- **AND** a successful image request produces one `1K` image
|
||||
- **THEN** `actual_cost` is `0.1`
|
||||
- **AND** the applied image multiplier is the same effective multiplier used by token billing
|
||||
|
||||
#### Scenario: Independent image multiplier allows direct final price
|
||||
- **WHEN** a group has `rate_multiplier=0.15`
|
||||
- **AND** `image_rate_independent=true`
|
||||
- **AND** `image_rate_multiplier=1`
|
||||
- **AND** `image_price_1k=0.2`
|
||||
- **AND** a successful image request produces one `1K` image
|
||||
- **THEN** `actual_cost` is `0.2`
|
||||
- **AND** ordinary `rate_multiplier=0.15` is not applied to the image cost
|
||||
|
||||
#### Scenario: Independent image multiplier supports image discounts
|
||||
- **WHEN** a group has `image_rate_independent=true`
|
||||
- **AND** `image_rate_multiplier=0.5`
|
||||
- **AND** `image_price_1k=0.2`
|
||||
- **AND** a successful image request produces two `1K` images
|
||||
- **THEN** `total_cost` is `0.4`
|
||||
- **AND** `actual_cost` is `0.2`
|
||||
|
||||
#### Scenario: Migration preserves existing image price behavior
|
||||
- **WHEN** an existing group has `rate_multiplier=0.15` and `image_price_1k=1.3333333333`
|
||||
- **AND** the migration is applied
|
||||
- **THEN** the stored `image_price_1k` remains `1.3333333333`
|
||||
- **AND** the stored `image_rate_independent` is `false`
|
||||
- **AND** the stored `image_rate_multiplier` is `1`
|
||||
- **AND** default-mode image billing still produces the historical final price within decimal precision
|
||||
|
||||
#### Scenario: Omitted update fields preserve existing multiplier mode
|
||||
- **WHEN** an admin updates a group without sending `image_rate_independent`
|
||||
- **AND** without sending `image_rate_multiplier`
|
||||
- **THEN** the stored image multiplier mode and image multiplier value remain unchanged
|
||||
|
||||
#### Scenario: Image multiplier can be zero only by explicit independent mode configuration
|
||||
- **WHEN** a group has `image_rate_independent=true`
|
||||
- **AND** `image_rate_multiplier=0`
|
||||
- **AND** a successful image request produces one image
|
||||
- **THEN** the image request is free
|
||||
- **AND** this free-image behavior does not occur unless the group explicitly enables independent image multiplier mode with zero multiplier
|
||||
|
||||
### Requirement: Responses image output accounting
|
||||
The system SHALL count generated image outputs from OpenAI Responses stream, non-stream, and passthrough paths and SHALL return the count in `OpenAIForwardResult.ImageCount`.
|
||||
|
||||
#### Scenario: Non-stream Responses image tool output is counted
|
||||
- **WHEN** a non-stream `/v1/responses` upstream response contains `output[]` item with `type == "image_generation_call"` and non-empty `result`
|
||||
- **THEN** `OpenAIForwardResult.ImageCount` equals the number of unique final image outputs
|
||||
- **AND** `OpenAIForwardResult.ImageSize` is the normalized image size tier
|
||||
|
||||
#### Scenario: Stream Responses output item is counted
|
||||
- **WHEN** a stream `/v1/responses` upstream SSE event has `type == "response.output_item.done"`
|
||||
- **AND** the event item has `type == "image_generation_call"` and non-empty `result`
|
||||
- **THEN** the streaming result increments the unique final image output count
|
||||
|
||||
#### Scenario: Stream Responses completed output is counted
|
||||
- **WHEN** a stream `/v1/responses` upstream SSE event has `type == "response.completed"`
|
||||
- **AND** `response.output[]` contains final image generation outputs
|
||||
- **THEN** the streaming result counts those images without double-counting images already seen in `response.output_item.done`
|
||||
|
||||
#### Scenario: Partial image events are not billed as completed images
|
||||
- **WHEN** a stream response contains `partial_image` events
|
||||
- **THEN** those partial events do not increment `ImageCount`
|
||||
- **AND** only final image generation outputs increment `ImageCount`
|
||||
|
||||
#### Scenario: gpt-5.4 image tool request is billed as image
|
||||
- **WHEN** a `/v1/responses` request uses `model="gpt-5.4"` or `model="gpt-5.5"`
|
||||
- **AND** the request includes an `image_generation` tool
|
||||
- **AND** the upstream response contains one final image output
|
||||
- **THEN** the usage log has `image_count=1`
|
||||
- **AND** the usage log has `billing_mode="image"`
|
||||
- **AND** image pricing, not token pricing, determines `actual_cost`
|
||||
|
||||
#### Scenario: Image output with zero usage is still billed
|
||||
- **WHEN** an upstream Responses result contains final image output
|
||||
- **AND** the upstream result has zero or missing token usage
|
||||
- **THEN** the system writes a usage log
|
||||
- **AND** the system bills using image pricing
|
||||
|
||||
#### Scenario: Responses image request records accompanying token usage
|
||||
- **WHEN** a `/v1/responses` image tool request returns final images and token usage
|
||||
- **THEN** the usage log records input tokens, output tokens, image output tokens, and image count
|
||||
- **AND** the applied billing mode remains `image`
|
||||
|
||||
#### Scenario: Responses image request does not introduce hybrid billing by default
|
||||
- **WHEN** a `/v1/responses` image tool request returns final images and text tokens
|
||||
- **THEN** the request is billed by image pricing under this change
|
||||
- **AND** non-image token charges are not added unless a future explicit hybrid billing mode is implemented
|
||||
|
||||
### Requirement: OpenAI Images API output accounting
|
||||
The system SHALL count generated images from dedicated OpenAI Images API stream and non-stream paths and SHALL set `ImageCount` for successful image responses.
|
||||
|
||||
#### Scenario: Images non-stream data array is counted
|
||||
- **WHEN** `/v1/images/generations` returns a non-stream JSON response with top-level `data[]`
|
||||
- **THEN** `ImageCount` equals the length of `data[]`
|
||||
|
||||
#### Scenario: Images stream data array is counted
|
||||
- **WHEN** `/v1/images/generations` stream response emits SSE data containing top-level `data[]`
|
||||
- **THEN** `ImageCount` equals the maximum final data array count observed for the request
|
||||
|
||||
#### Scenario: Images stream completed event is counted
|
||||
- **WHEN** `/v1/images/generations` stream response emits `image_generation.completed` with a final image payload
|
||||
- **THEN** the stream result counts one final image output
|
||||
|
||||
#### Scenario: Images stream Responses-form event is counted
|
||||
- **WHEN** an Images API upstream path emits Responses-form `response.output_item.done` or `response.completed` events with final image outputs
|
||||
- **THEN** the stream result counts final image outputs using the same de-duplication rules as Responses
|
||||
|
||||
### Requirement: Channel image billing uses actual image count
|
||||
The system SHALL use actual generated image count for channel `billing_mode=image` pricing and SHALL NOT bill multi-image requests as a single request.
|
||||
|
||||
#### Scenario: OpenAI channel image billing counts multiple images
|
||||
- **WHEN** a channel image pricing entry resolves to unit price `0.25`
|
||||
- **AND** an OpenAI image request produces three images
|
||||
- **THEN** `total_cost` is `0.75` before the selected image multiplier is applied
|
||||
- **AND** `RequestCount` passed into unified pricing is `3`
|
||||
|
||||
#### Scenario: Gateway channel image billing counts multiple images
|
||||
- **WHEN** a non-OpenAI gateway image path produces two images
|
||||
- **AND** channel image pricing resolves for the billing model
|
||||
- **THEN** `RequestCount` passed into unified pricing is `2`
|
||||
|
||||
#### Scenario: Channel image pricing uses shared multiplier by default
|
||||
- **WHEN** a channel image pricing entry resolves to unit price `0.25`
|
||||
- **AND** the group has ordinary effective multiplier `0.15`
|
||||
- **AND** the group has `image_rate_independent=false`
|
||||
- **AND** the image request produces one image
|
||||
- **THEN** `actual_cost` is `0.0375`
|
||||
|
||||
#### Scenario: Channel image pricing uses independent image multiplier when enabled
|
||||
- **WHEN** a channel image pricing entry resolves to unit price `0.25`
|
||||
- **AND** the group has ordinary effective multiplier `0.15`
|
||||
- **AND** the group has `image_rate_independent=true`
|
||||
- **AND** the group has `image_rate_multiplier=1`
|
||||
- **AND** the image request produces one image
|
||||
- **THEN** `actual_cost` is `0.25`
|
||||
- **AND** ordinary effective multiplier `0.15` is not applied
|
||||
|
||||
#### Scenario: Account stats image pricing receives image count
|
||||
- **WHEN** account stats pricing uses `billing_mode=image`
|
||||
- **AND** the request produces multiple images
|
||||
- **THEN** account stats cost is calculated with the actual image count
|
||||
|
||||
### Requirement: Image size tier normalization
|
||||
The system SHALL normalize OpenAI image sizes to explicit billing tiers for billing only. The system SHALL NOT reject requests locally because of an unknown or provider-invalid `size`; it SHALL forward the original size parameter upstream and let the official upstream API decide whether the request is valid.
|
||||
|
||||
#### Scenario: OpenAI 1024 square maps to 1K
|
||||
- **WHEN** an OpenAI image request specifies `size="1024x1024"`
|
||||
- **THEN** `ImageSize` is `1K`
|
||||
|
||||
#### Scenario: OpenAI landscape and portrait large sizes map to 2K
|
||||
- **WHEN** an OpenAI image request specifies `1536x1024`, `1024x1536`, `1792x1024`, `1024x1792`, `2048x2048`, `2048x1152`, or `1152x2048`
|
||||
- **THEN** `ImageSize` is `2K`
|
||||
|
||||
#### Scenario: OpenAI gpt-image-2 4K presets map to 4K
|
||||
- **WHEN** an OpenAI `gpt-image-2` image request specifies `3840x2160` or `2160x3840`
|
||||
- **THEN** `ImageSize` is `4K`
|
||||
|
||||
#### Scenario: OpenAI auto size maps to 2K
|
||||
- **WHEN** an OpenAI image request omits size or specifies `size="auto"`
|
||||
- **THEN** `ImageSize` is `2K`
|
||||
|
||||
#### Scenario: Custom OpenAI size is forwarded without local validation
|
||||
- **WHEN** an OpenAI image request specifies a custom explicit `WIDTHxHEIGHT` size
|
||||
- **THEN** the system forwards the request upstream
|
||||
- **AND** `ImageSize` is normalized to `2K` or `4K` for billing
|
||||
|
||||
#### Scenario: Responses image tool without model uses default image billing model
|
||||
- **WHEN** a `/v1/responses` request uses an `image_generation` tool without `tool.model`
|
||||
- **THEN** image size validation and image billing use `gpt-image-2` as the image billing model
|
||||
|
||||
#### Scenario: Invalid OpenAI size constraints are delegated upstream
|
||||
- **WHEN** an OpenAI image request specifies an explicit size that fails OpenAI size constraints
|
||||
- **THEN** the system forwards the request upstream
|
||||
- **AND** any invalid-size error comes from the upstream provider response
|
||||
|
||||
#### Scenario: Custom OpenAI size tier mapping
|
||||
- **WHEN** a custom size cannot be parsed as positive `WIDTHxHEIGHT`
|
||||
- **THEN** `ImageSize` is `2K`
|
||||
- **WHEN** a custom size parses as positive `WIDTHxHEIGHT`
|
||||
- **AND** `WIDTH * HEIGHT` is no more than `2560x1440`
|
||||
- **THEN** `ImageSize` is `2K`
|
||||
- **WHEN** a custom size parses as positive `WIDTHxHEIGHT`
|
||||
- **AND** `WIDTH * HEIGHT` exceeds `2560x1440`
|
||||
- **THEN** `ImageSize` is `4K`
|
||||
|
||||
### Requirement: Image usage log semantics
|
||||
The system SHALL write usage logs for successful image generation with image billing metadata that matches the applied image pricing path.
|
||||
|
||||
#### Scenario: Image usage log records image billing mode
|
||||
- **WHEN** a successful request has `ImageCount > 0`
|
||||
- **THEN** the usage log has `billing_mode="image"`
|
||||
- **AND** the usage log records `image_count`
|
||||
- **AND** the usage log records `image_size` when a normalized size tier is available
|
||||
|
||||
#### Scenario: Shared mode image usage log records shared multiplier
|
||||
- **WHEN** a successful image request is billed with `image_rate_independent=false`
|
||||
- **AND** the effective ordinary multiplier is `0.15`
|
||||
- **THEN** `usage_logs.rate_multiplier` is `0.15`
|
||||
|
||||
#### Scenario: Independent mode image usage log records image multiplier
|
||||
- **WHEN** a successful image request is billed with `image_rate_independent=true`
|
||||
- **AND** `image_rate_multiplier=0.5`
|
||||
- **THEN** `usage_logs.rate_multiplier` is `0.5`
|
||||
|
||||
#### Scenario: Token request usage log is unchanged
|
||||
- **WHEN** a successful non-image token request is billed
|
||||
- **THEN** `usage_logs.rate_multiplier` continues to record the ordinary token multiplier
|
||||
- **AND** `image_count` is `0`
|
||||
@@ -1,72 +0,0 @@
|
||||
## 1. Data Model And Migration
|
||||
|
||||
- [x] 1.1 Add `allow_image_generation`, `image_rate_independent`, and `image_rate_multiplier` to `backend/ent/schema/group.go`.
|
||||
- [x] 1.2 Create a new idempotent SQL migration after `133_affiliate_rebate_freeze.sql` for the three group columns.
|
||||
- [x] 1.3 Backfill existing `openai`, `gemini`, and `antigravity` groups to `allow_image_generation=true` and `anthropic` groups to `false`.
|
||||
- [x] 1.4 Backfill all existing groups to `image_rate_independent=false` and `image_rate_multiplier=1` without changing existing `image_price_1k/2k/4k`.
|
||||
- [x] 1.5 Regenerate or update Ent generated group fields, predicates, create/update setters, and query projections.
|
||||
- [x] 1.6 Add the new fields to backend group domain/service structs, admin create/update inputs, admin responses, and group serialization.
|
||||
|
||||
## 2. Admin API And Frontend
|
||||
|
||||
- [x] 2.1 Add `allow_image_generation`, `image_rate_independent`, and `image_rate_multiplier` to `CreateGroupRequest` and `UpdateGroupRequest`.
|
||||
- [x] 2.2 Validate `image_rate_multiplier >= 0` and keep negative image prices using the existing clear-price behavior only for `image_price_*`.
|
||||
- [x] 2.3 Add the new fields to `frontend/src/types/index.ts` group, create, and update interfaces.
|
||||
- [x] 2.4 Ensure omitted update fields do not overwrite existing image generation and multiplier mode settings.
|
||||
- [x] 2.5 Update `frontend/src/views/admin/GroupsView.vue` create/edit forms with a 生图开关, 生图倍率是否独立开关, and conditional image multiplier input.
|
||||
- [x] 2.6 Add a live final-price preview for `image_price_1k/2k/4k` under shared and independent multiplier modes.
|
||||
- [x] 2.7 Update group form help text to state that default image billing shares the existing group effective multiplier and independent mode uses the image multiplier input.
|
||||
- [x] 2.8 Update i18n strings for the new controls and image multiplier mode explanation.
|
||||
|
||||
## 3. Image Generation Access Control
|
||||
|
||||
- [x] 3.1 Implement a shared helper that detects image generation intent from endpoint, requested model, `tools[]`, and `tool_choice`.
|
||||
- [x] 3.2 Gate `/v1/images/generations` and `/v1/images/edits` in `backend/internal/handler/openai_images.go` after request parsing and before billing eligibility/account scheduling.
|
||||
- [x] 3.3 Gate `/v1/responses` explicit `image_generation` tool requests in `backend/internal/service/openai_gateway_service.go` before upstream account scheduling.
|
||||
- [x] 3.4 Prevent `normalizeOpenAIResponsesImageOnlyModel` from rewriting `gpt-image-*` Responses requests when the group does not allow image generation.
|
||||
- [x] 3.5 Skip Codex `image_generation` auto-injection and image bridge instructions when the group does not allow image generation.
|
||||
- [x] 3.6 Re-run image intent detection after service-side request mutation and before upstream dispatch.
|
||||
- [x] 3.7 Ensure OpenAI advanced scheduler paths apply the same channel `RestrictModels` checks as the load-aware path.
|
||||
|
||||
## 4. Responses Image Output Accounting
|
||||
|
||||
- [x] 4.1 Add shared parsers for final `image_generation_call.result` outputs in non-stream JSON and SSE payloads.
|
||||
- [x] 4.2 Extend `openaiStreamingResult` with image count, image size tier, and image billing model fields.
|
||||
- [x] 4.3 Update `handleStreamingResponse` to count final image outputs while preserving existing stream forwarding and usage parsing.
|
||||
- [x] 4.4 Update `handleStreamingResponsePassthrough` with the same image output counting.
|
||||
- [x] 4.5 Update `handleNonStreamingResponse` to count final image outputs from `output[]`.
|
||||
- [x] 4.6 Update `handleNonStreamingResponsePassthrough` with the same non-stream image output counting.
|
||||
- [x] 4.7 Populate `OpenAIForwardResult.ImageCount`, `ImageSize`, and image billing model for `gpt-5.4` / `gpt-5.5 + image_generation` requests.
|
||||
|
||||
## 5. Images API Accounting And Size Tiers
|
||||
|
||||
- [x] 5.1 Extend OpenAI Images API-key stream counting to handle `image_generation.completed`, `response.output_item.done`, and `response.completed`.
|
||||
- [x] 5.2 Reuse the same final-image de-duplication rules across Images API and Responses API paths.
|
||||
- [x] 5.3 Keep unknown explicit OpenAI image sizes pass-through and delegate invalid-size errors to upstream.
|
||||
- [x] 5.4 Map documented OpenAI image sizes to `1K`/`2K`/`4K` billing tiers without rewriting request parameters.
|
||||
- [x] 5.5 Classify custom OpenAI `WIDTHxHEIGHT` sizes by `2560x1440` total-pixel boundary, falling back to `2K` when unparseable.
|
||||
|
||||
## 6. Billing And Usage Logs
|
||||
|
||||
- [x] 6.1 Add an image multiplier resolver: shared mode uses the current effective group multiplier, independent mode uses `apiKey.Group.ImageRateMultiplier`.
|
||||
- [x] 6.2 Update `CalculateImageCost` or its caller contract so image costs use the resolved image multiplier.
|
||||
- [x] 6.3 Set image usage log `RateMultiplier` to the applied image multiplier; keep token logs unchanged.
|
||||
- [x] 6.4 Change OpenAI channel image billing `RequestCount` from `1` to `result.ImageCount`.
|
||||
- [x] 6.5 Change non-OpenAI gateway channel image billing `RequestCount` from `1` to `result.ImageCount`.
|
||||
- [x] 6.6 Pass actual image count into account stats pricing for `billing_mode=image`.
|
||||
- [x] 6.7 Ensure `ImageCount > 0` writes a usage log and bills even when upstream token usage is zero.
|
||||
- [x] 6.8 Record accompanying token usage for Responses image tool requests while keeping default billing mode as `image`.
|
||||
|
||||
## 7. Tests And Documentation
|
||||
|
||||
- [x] 7.1 Add backend tests for disabled group rejecting `/v1/images/*`, `gpt-image-*` Responses, explicit `image_generation`, and image `tool_choice`.
|
||||
- [x] 7.2 Add backend tests proving disabled Codex groups do not receive injected image tools while enabled Codex groups still do.
|
||||
- [x] 7.3 Add backend tests proving omitted group update fields preserve existing image generation and multiplier mode settings.
|
||||
- [x] 7.4 Add Responses stream and non-stream tests for `gpt-5.4` / `gpt-5.5 + image_generation` image counting and image billing.
|
||||
- [x] 7.5 Add Images API stream tests for `image_generation.completed`, `response.output_item.done`, and `response.completed` counting.
|
||||
- [x] 7.6 Add billing tests for shared mode `rate_multiplier=0.15`, `image_price_1k=0.2`, final `actual_cost=0.03`.
|
||||
- [x] 7.7 Add billing tests for independent mode `rate_multiplier=0.15`, `image_rate_multiplier=1`, `image_price_1k=0.2`, final `actual_cost=0.2`.
|
||||
- [x] 7.8 Add channel image billing tests proving multi-image requests use `RequestCount=ImageCount` in both shared and independent multiplier modes.
|
||||
- [x] 7.9 Add size-tier tests for known OpenAI sizes and unknown explicit size pass-through.
|
||||
- [x] 7.10 Add Responses image tool tests proving token usage is recorded but default billing remains image-mode only.
|
||||
- [x] 7.11 Update `2ue/image-billing-risk-analysis.md` or add a linked follow-up note that points to this OpenSpec change as the normalized solution.
|
||||
@@ -1,2 +0,0 @@
|
||||
schema: spec-driven
|
||||
created: 2026-05-03
|
||||
@@ -1,70 +0,0 @@
|
||||
## 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 明确提示图片并发达到上限。
|
||||
@@ -1,28 +0,0 @@
|
||||
## 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` 本地分析文档,记录外部独立图片网关只作为后续部署方案,不在本次代码落地。
|
||||
@@ -1,82 +0,0 @@
|
||||
# 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
|
||||
@@ -1,28 +0,0 @@
|
||||
## 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.
|
||||
@@ -1,2 +0,0 @@
|
||||
schema: spec-driven
|
||||
created: 2026-05-02
|
||||
@@ -1,46 +0,0 @@
|
||||
## Context
|
||||
|
||||
现有普通 Responses 流式已经具备较完整的断连续写能力:客户端写失败后可继续 drain 上游,并且具备数据间隔超时和 keepalive。图片流式路径目前仍然采用更直接的读写方式,客户端写失败会立即返回,上游读取也更容易跟随客户端取消而结束。
|
||||
|
||||
本次变更只针对图片流式路径,不改变普通文本流式路径的配置和行为。系统已经存在普通流式的后端超时配置,因此这里不引入页面级超时设置;图片流式只需要独立的后端默认值,让图片生成有更长的容忍窗口。
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- 图片流式在客户端断开后继续读取上游,尽量保留最终图片结果与计费结果。
|
||||
- 图片流式使用独立于普通流式的超时与 keepalive 默认值。
|
||||
- 不修改现有普通流式配置项的含义,不要求管理员新增页面配置。
|
||||
- 维持图片计费与图片结果计数的一致性。
|
||||
|
||||
**Non-Goals:**
|
||||
- 不设计新的前端配置页面。
|
||||
- 不修改普通文本流式的超时策略。
|
||||
- 不改变图片计费公式或分组倍率语义。
|
||||
|
||||
## Decisions
|
||||
|
||||
1. **使用独立的图片流式配置键**
|
||||
- 选择:在后端配置中增加图片流式专用 `image_stream_data_interval_timeout` / `image_stream_keepalive_interval`。
|
||||
- 原因:图片流式耗时显著更长,复用普通流式默认值会过早触发超时;独立键能避免影响现有文本流式。
|
||||
- 备选方案:直接复用普通流式配置并在代码里按路径放大倍数。这个方案会让普通流式和图片流式共享语义,后续难以维护。
|
||||
|
||||
2. **继续使用上下文 detach,而不是依赖客户端上下文**
|
||||
- 选择:图片流式请求向上游发起时使用 `context.WithoutCancel` 派生的上下文。
|
||||
- 原因:客户端断开时不应自动取消上游请求,否则无法收集最终图片结果,也无法完成图片计费。
|
||||
- 备选方案:仍使用 `c.Request.Context()` 并只在写失败后继续 drain。这个方案在客户端取消场景下无法保证上游读取继续进行。
|
||||
|
||||
3. **只改图片流式路径,不改普通流式路径**
|
||||
- 选择:`/v1/images/*` 与 `Responses + image_generation` 两条图片流式链路单独处理。
|
||||
- 原因:风险最小,避免回归普通文本流式和现有超时配置。
|
||||
- 备选方案:统一重构所有流式处理。这个方案范围更大,验证成本更高,不符合本次“尽量少改现有行为”的目标。
|
||||
|
||||
4. **不新增页面配置**
|
||||
- 选择:图片流式独立超时默认值写入后端配置,沿用当前配置加载方式。
|
||||
- 原因:用户明确要求和当前设置行为统一,不需要额外页面输入项。
|
||||
- 备选方案:前端增加图片超时配置项。这个方案会改变现有运维方式,也容易引入误配。
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [Risk] 图片流式继续 drain 上游后,客户端已经断开但服务端仍占用连接与协程资源。→ [Mitigation] 只对图片流式启用更长但仍有限的专用超时,并保持与普通流式同样的 keepalive/超时退出机制。
|
||||
- [Risk] 图片流式与普通流式的默认超时不同,运维如果只关注通用配置可能忽略图片专用值。→ [Mitigation] 在配置示例中明确标注图片流式专用默认值和用途。
|
||||
- [Risk] 断连后继续读取可能导致日志中出现“客户端断开但最终成功”的状态。→ [Mitigation] 保留现有图片计费结果返回语义,同时让调用方在结果与错误并存时优先使用结果对象。
|
||||
@@ -1,25 +0,0 @@
|
||||
## Why
|
||||
|
||||
图片流式路径目前没有和普通 Responses 流式一致的断连续写策略,也没有独立于普通流式的超时控制。由于图片生成耗时更长,如果继续沿用普通流式处理方式,客户端断开时容易中断上游读取,影响图片产物收集与按图计费的准确性。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 为 OpenAI Images API 和 `Responses + image_generation` 流式路径补充独立的上游续读策略,客户端断开后继续 drain 上游,尽量保留最终图片结果和计费结果。
|
||||
- 为图片流式路径使用独立的流数据间隔超时与 keepalive 策略,默认比普通流式更长,不新增页面配置项。
|
||||
- 保持现有普通流式配置与行为不变,避免影响已经配置好的普通文本分组。
|
||||
- 让图片流式路径在超时、断连、写入失败等场景下保持图片计费语义一致。
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `image-stream-resilience`: 图片流式路径的断连续读、独立超时和计费保留能力。
|
||||
|
||||
### Modified Capabilities
|
||||
- `image-generation-billing-accounting`: 图片流式结果计数和计费结果的稳定性行为发生改变,但计费契约不变。
|
||||
|
||||
## Impact
|
||||
|
||||
- 影响 `backend/internal/service/openai_images.go` 和 `backend/internal/service/openai_images_responses.go` 的流式实现。
|
||||
- 影响 `backend/internal/config/config.go` 与 `deploy/config.example.yaml` 中图片流式默认值和校验逻辑。
|
||||
- 影响 `backend/internal/service/openai_images_test.go`、`backend/internal/config/config_test.go` 以及新增的图片流式稳定性测试。
|
||||
- 不新增前端页面设置,不改变普通流式配置项名称和语义。
|
||||
@@ -1,53 +0,0 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Image stream resilience
|
||||
The system SHALL keep image generation stream processing active after downstream client disconnects so long as upstream reading can continue, in order to preserve final image outputs and billing results.
|
||||
|
||||
#### Scenario: Images API stream survives downstream disconnect
|
||||
- **WHEN** `/v1/images/generations` is streamed to a client
|
||||
- **AND** the downstream writer returns an error before the upstream stream completes
|
||||
- **THEN** the service continues draining the upstream stream
|
||||
- **AND** it still counts final image outputs if the upstream later emits them
|
||||
- **AND** the request can still complete with image billing metadata
|
||||
|
||||
#### Scenario: Responses image tool stream survives downstream disconnect
|
||||
- **WHEN** a `/v1/responses` request uses `image_generation` and is streamed to a client
|
||||
- **AND** the downstream writer returns an error before the upstream stream completes
|
||||
- **THEN** the service continues draining the upstream stream
|
||||
- **AND** it still counts final image outputs if the upstream later emits them
|
||||
- **AND** the request can still complete with image billing metadata
|
||||
|
||||
#### Scenario: Client disconnect does not force image stream to downgrade to text billing
|
||||
- **WHEN** a successful image stream request has already produced final image outputs
|
||||
- **AND** the downstream client disconnects before the final flush
|
||||
- **THEN** the request remains billed as an image request
|
||||
- **AND** the image count is preserved in the forward result
|
||||
|
||||
### Requirement: Image stream timeout isolation
|
||||
The system SHALL use image-specific streaming timeout settings for image generation stream paths, and these settings SHALL be independent from the ordinary text streaming timeout values.
|
||||
|
||||
#### Scenario: Image stream uses dedicated timeout defaults
|
||||
- **WHEN** an image generation stream path is executed
|
||||
- **THEN** it uses the image-specific data interval timeout and keepalive interval defaults
|
||||
- **AND** it does not rely on the ordinary text stream timeout defaults
|
||||
|
||||
#### Scenario: Ordinary stream settings remain unchanged
|
||||
- **WHEN** a normal non-image streaming request is executed
|
||||
- **THEN** the existing ordinary stream timeout configuration and behavior remain unchanged
|
||||
|
||||
#### Scenario: Image stream timeout is longer than ordinary stream timeout
|
||||
- **WHEN** the image streaming timeout defaults are compared with the ordinary streaming defaults
|
||||
- **THEN** the image streaming timeout is configured to allow a longer wait window than ordinary text streaming
|
||||
|
||||
### Requirement: Image stream billing consistency
|
||||
The system SHALL keep the image billing result consistent even when image stream handling uses retries, keepalive writes, or downstream disconnect recovery.
|
||||
|
||||
#### Scenario: Final image count is preserved after reconnect-unsafe downstream failure
|
||||
- **WHEN** the downstream client disconnects after at least one final image output has been observed upstream
|
||||
- **THEN** the forward result retains the final image count
|
||||
- **AND** usage recording can still proceed with image billing metadata
|
||||
|
||||
#### Scenario: Image stream timeout does not silently switch billing mode
|
||||
- **WHEN** an image stream times out before any final image output is observed
|
||||
- **THEN** the request is handled as a failed image stream
|
||||
- **AND** it does not fall back to ordinary text billing semantics
|
||||
@@ -1,20 +0,0 @@
|
||||
## 1. Config and defaults
|
||||
|
||||
- [x] 1.1 Add image-specific stream timeout fields to gateway config.
|
||||
- [x] 1.2 Register image stream timeout defaults in the config loader.
|
||||
- [x] 1.3 Add config validation for image stream timeout ranges.
|
||||
- [x] 1.4 Expose image stream timeout defaults in `deploy/config.example.yaml`.
|
||||
|
||||
## 2. Image stream runtime behavior
|
||||
|
||||
- [x] 2.1 Detach image stream upstream contexts from client cancellation.
|
||||
- [x] 2.2 Add image-specific data interval timeout handling to `/v1/images/*` streaming.
|
||||
- [x] 2.3 Add image-specific data interval timeout handling to `Responses + image_generation` streaming.
|
||||
- [x] 2.4 Preserve upstream draining after downstream write failures in both image stream paths.
|
||||
|
||||
## 3. Tests and verification
|
||||
|
||||
- [x] 3.1 Add config tests for image stream timeout defaults and validation.
|
||||
- [x] 3.2 Add image streaming disconnect tests for the Images API path.
|
||||
- [x] 3.3 Add image streaming disconnect tests for the Responses image tool path.
|
||||
- [x] 3.4 Run focused Go tests for the touched config and image service paths.
|
||||
@@ -1,20 +0,0 @@
|
||||
schema: spec-driven
|
||||
|
||||
# Project context (optional)
|
||||
# This is shown to AI when creating artifacts.
|
||||
# Add your tech stack, conventions, style guides, domain knowledge, etc.
|
||||
# Example:
|
||||
# context: |
|
||||
# Tech stack: TypeScript, React, Node.js
|
||||
# We use conventional commits
|
||||
# Domain: e-commerce platform
|
||||
|
||||
# Per-artifact rules (optional)
|
||||
# Add custom rules for specific artifacts.
|
||||
# Example:
|
||||
# rules:
|
||||
# proposal:
|
||||
# - Keep proposals under 500 words
|
||||
# - Always include a "Non-goals" section
|
||||
# tasks:
|
||||
# - Break tasks into chunks of max 2 hours
|
||||
Reference in New Issue
Block a user