misonL commited on
Commit
01f44cd
·
verified ·
1 Parent(s): 09ad34c

Deploy 34af419 to Docker Space

Browse files

Source: MisonL/gpt-image-playground-customer@34af419d1e35b93f1874ba494453e0fab7ea54fe

README.md CHANGED
@@ -141,7 +141,7 @@ OPENAI_CHANNEL_3_UPSTREAM_PROFILE=matsca
141
  - `OPENAI_CHANNEL_N_REQUEST_MODES` 是管理员基于真实上游 smoke 设置的服务端白名单;全局默认可用 `OPENAI_UPSTREAM_REQUEST_MODES`。`/api/runtime-capabilities` 的 `channelRouting.requestModeControls`、`channelRouting.requestModeHealth` 和 Agent capabilities 的 `request_mode_controls` 会暴露配置入口、健康覆盖和对应真实 smoke gate。Agent 客户端只提交业务意图,不应自行选择 Images、Responses、SSE 或非流式路径。`stream_mode=auto` 可由服务端在 SSE 不可用时显式退到非流式并标记 fallback;`stream_mode=stream` 或显式页面 SSE 诊断必须失败可见,不会静默降级。真实执行后可从 `execution.channel_request_mode`、`execution.channel_request_mode_fallback_applied` 和 `execution.route_decision` 读取服务端实际选路结果;失败冷却若能关联到本次服务端 request mode,会只冷却该渠道或凭证的对应 request mode,并在 `error.diagnostics.cooldown_target.request_mode` 暴露。
142
  - 渠道恢复探测使用非计费 `GET /models` 只确认 host、鉴权和 models 端点恢复;它不能替代 request mode 的真实 Images/Responses/SSE smoke。管理员应以真实 smoke 结果决定 `OPENAI_CHANNEL_N_REQUEST_MODES`。
143
  - Docker compose 本身不把默认图片后端改成 Responses;未在 `.env.local` 显式配置时仍是 `images-api` 和 `auto`。
144
- - Responses image backend 需要 `ENABLE_RESPONSES_IMAGE_BACKEND=true` 和 `OPENAI_RESPONSES_API_MODEL`。页面请求也可以用 `responsesModel`、`responses_model`、`gptModel` 或 `gpt_model` 覆盖单次 `/responses` 顶层模型;这些字段只影响本项目的 `responses-image-generation` 路径,不会改变兼容上游自身 `/v1/images/generations` 桥接层内部选择的模型。
145
  - Matsca、extra headers、provider manifest、真实上游 smoke 等高级配置以 [.env.example](./.env.example) 为准。
146
 
147
  ## Agent API
@@ -156,7 +156,7 @@ Agent API 是机器接口,不是自治 Agent 平台。自动化客户端应先
156
  | --- | --- |
157
  | `GET /api/agent/capabilities` | 查询模型、限制、认证方式和路由规则。 |
158
  | `GET /api/agent/openapi.json` | 获取 OpenAPI 描述。 |
159
- | `POST /api/agent/images/generate` | JSON 文生图。 |
160
  | `POST /api/agent/images/edit` | multipart 图片编辑,支持源图和 mask。 |
161
  | `POST /api/agent/jobs/images/generate` | 创建文生图 job。 |
162
  | `GET /api/agent/jobs/{id}` | 查询 job 状态。 |
@@ -168,7 +168,7 @@ Agent API 是机器接口,不是自治 Agent 平台。自动化客户端应先
168
  生成示例:
169
 
170
  ```bash
171
- curl -s http://localhost:4783/api/agent/images/generate \
172
  -H "Authorization: Bearer your-agent-token" \
173
  -H "Idempotency-Key: demo-$(date +%s)" \
174
  -H "Content-Type: application/json" \
 
141
  - `OPENAI_CHANNEL_N_REQUEST_MODES` 是管理员基于真实上游 smoke 设置的服务端白名单;全局默认可用 `OPENAI_UPSTREAM_REQUEST_MODES`。`/api/runtime-capabilities` 的 `channelRouting.requestModeControls`、`channelRouting.requestModeHealth` 和 Agent capabilities 的 `request_mode_controls` 会暴露配置入口、健康覆盖和对应真实 smoke gate。Agent 客户端只提交业务意图,不应自行选择 Images、Responses、SSE 或非流式路径。`stream_mode=auto` 可由服务端在 SSE 不可用时显式退到非流式并标记 fallback;`stream_mode=stream` 或显式页面 SSE 诊断必须失败可见,不会静默降级。真实执行后可从 `execution.channel_request_mode`、`execution.channel_request_mode_fallback_applied` 和 `execution.route_decision` 读取服务端实际选路结果;失败冷却若能关联到本次服务端 request mode,会只冷却该渠道或凭证的对应 request mode,并在 `error.diagnostics.cooldown_target.request_mode` 暴露。
142
  - 渠道恢复探测使用非计费 `GET /models` 只确认 host、鉴权和 models 端点恢复;它不能替代 request mode 的真实 Images/Responses/SSE smoke。管理员应以真实 smoke 结果决定 `OPENAI_CHANNEL_N_REQUEST_MODES`。
143
  - Docker compose 本身不把默认图片后端改成 Responses;未在 `.env.local` 显式配置时仍是 `images-api` 和 `auto`。
144
+ - Responses image backend 需要 `ENABLE_RESPONSES_IMAGE_BACKEND=true` 和 `OPENAI_RESPONSES_API_MODEL`。生成意图也可以用 `responsesModel`、`responses_model`、`gptModel` 或 `gpt_model` 覆盖单次 `/responses` 顶层模型;这些字段只影响本项目的 `responses-image-generation` 路径,不会改变兼容上游自身 `/v1/images/generations` 桥接层内部选择的模型。
145
  - Matsca、extra headers、provider manifest、真实上游 smoke 等高级配置以 [.env.example](./.env.example) 为准。
146
 
147
  ## Agent API
 
156
  | --- | --- |
157
  | `GET /api/agent/capabilities` | 查询模型、限制、认证方式和路由规则。 |
158
  | `GET /api/agent/openapi.json` | 获取 OpenAPI 描述。 |
159
+ | `POST /api/agent/images/generate` | JSON 文生图(显式 Agent / 诊断)。 |
160
  | `POST /api/agent/images/edit` | multipart 图片编辑,支持源图和 mask。 |
161
  | `POST /api/agent/jobs/images/generate` | 创建文生图 job。 |
162
  | `GET /api/agent/jobs/{id}` | 查询 job 状态。 |
 
168
  生成示例:
169
 
170
  ```bash
171
+ curl -s http://localhost:4783/api/agent/image-requests \
172
  -H "Authorization: Bearer your-agent-token" \
173
  -H "Idempotency-Key: demo-$(date +%s)" \
174
  -H "Content-Type: application/json" \
docs/deployment/huggingface-space-free.md CHANGED
@@ -199,7 +199,7 @@ node skills/gpt-image-playground-agent/scripts/generate-image.mjs \
199
 
200
  脚本会先读取 `GET /api/agent/capabilities`,再调用 Agent API。成功响应会保留相对 `content_url`,同时补充 `absolute_content_url` 和 `absolute_metadata_url`,便于在桌面环境直接下载产物。
201
 
202
- 远端 Agent 调用不要硬编码路径。普通文生图默认提交业务意图到 capabilities 声明的 `orchestration.endpoint`,由服务端选择内部执行路径、上游 request mode 和轮询方式;Agent 客户端不按尺寸、远端 HTTPS 或流式参数自行选择 Images、Responses、SSE 或非流式路径。页面高级字段、默认 WebP edit、高分辨率 edit 和复杂批量仍按 Skill 规则使用页面端 `/api/images` SSE;页面流式失败或不可用时,先诊断结构化错误,再用新的 `Idempotency-Key` 显式选择 Agent JSON、Agent edit 或 job 路径对照。job polling 只在显式选择时使用。需要诊断对照时可用 `--agent` 或 `--streaming-strategy off` 强制 Agent JSON,也可用 `--page-sse` 或 `--job` 显式选择路径。
203
 
204
  如果 Space 同时配置了 `APP_PASSWORD` 和 `AGENT_API_TOKEN`,Agent JSON 端点用 `GPT_IMAGE_AGENT_TOKEN` 发送 Bearer token;页面端 `/api/images` SSE 仍按 capabilities 的 `agent_streaming.page_sse.auth` 判断,可能需要额外设置 `GPT_IMAGE_APP_PASSWORD_HASH`,并通过 form-data `passwordHash` 发送页面访问码哈希。页面 SSE 会把业务 key 写入 `clientRequestId`,长度上限以 capabilities 中的 `agent_streaming.page_sse.client_request_id.max_length` 为准。
205
 
 
199
 
200
  脚本会先读取 `GET /api/agent/capabilities`,再调用 Agent API。成功响应会保留相对 `content_url`,同时补充 `absolute_content_url` 和 `absolute_metadata_url`,便于在桌面环境直接下载产物。
201
 
202
+ 远端 Agent 调用不要硬编码路径。普通文生图默认提交业务意图到 capabilities 声明的 `orchestration.endpoint`,由服务端选择内部执行路径、上游 request mode 和轮询方式;Agent 客户端不按尺寸、远端 HTTPS 或流式参数自行选择 Images、Responses、SSE 或非流式路径。显式 page_sse 诊断、默认 WebP edit、高分辨率 edit 和复杂批量仍按 Skill 规则使用页面端 `/api/images` SSE;页面流式失败或不可用时,先诊断结构化错误,再用新的 `Idempotency-Key` 显式选择 Agent JSON、Agent edit 或 job 路径对照。job polling 只在显式选择时使用。需要诊断对照时可用 `--agent` 或 `--streaming-strategy off` 强制 Agent JSON,也可用 `--page-sse` 或 `--job` 显式选择路径。
203
 
204
  如果 Space 同时配置了 `APP_PASSWORD` 和 `AGENT_API_TOKEN`,Agent JSON 端点用 `GPT_IMAGE_AGENT_TOKEN` 发送 Bearer token;页面端 `/api/images` SSE 仍按 capabilities 的 `agent_streaming.page_sse.auth` 判断,可能需要额外设置 `GPT_IMAGE_APP_PASSWORD_HASH`,并通过 form-data `passwordHash` 发送页面访问码哈希。页面 SSE 会把业务 key 写入 `clientRequestId`,长度上限以 capabilities 中的 `agent_streaming.page_sse.client_request_id.max_length` 为准。
205
 
public/hf-space-deploy-marker.json CHANGED
@@ -1,6 +1,6 @@
1
  {
2
  "schema_version": 1,
3
- "local_sha": "07209608cdbddc4207aa853b7eb6ea60db1f1827",
4
- "created_at": "2026-06-26T01:51:20.764Z",
5
- "deploy_id": "fce636f2-7a61-4ff6-8f38-d69ac5edeb35"
6
  }
 
1
  {
2
  "schema_version": 1,
3
+ "local_sha": "34af419d1e35b93f1874ba494453e0fab7ea54fe",
4
+ "created_at": "2026-06-26T04:13:04.694Z",
5
+ "deploy_id": "cff00c2a-9ee2-45f8-bca2-6a8465a84274"
6
  }
scripts/agent-skill-scripts.test.mjs CHANGED
@@ -555,7 +555,7 @@ describe('Agent skill script argument validation', () => {
555
  assert.equal(result.stderr.trim(), '');
556
  });
557
 
558
- it('routes GPT2Image-compatible generate options through page SSE dry-runs', () => {
559
  const result = runSkillScript('generate-image.mjs', [
560
  '--image-backend',
561
  'responses',
@@ -571,8 +571,8 @@ describe('Agent skill script argument validation', () => {
571
 
572
  assert.equal(result.status, 0);
573
  const body = JSON.parse(result.stdout);
574
- assert.equal(body.endpoint, 'http://localhost:4783/api/images');
575
- assert.equal(body.routing_guidance.transport, 'page_sse');
576
  assert.equal(body.request.image_backend, 'responses');
577
  assert.equal(body.request.responsesModel, 'gpt-5.4-mini');
578
  assert.equal(body.request.thinking, 'medium');
@@ -816,6 +816,15 @@ describe('Agent skill script argument validation', () => {
816
  '3072x2048',
817
  '--streaming-strategy',
818
  'off',
 
 
 
 
 
 
 
 
 
819
  'prompt'
820
  ],
821
  { GPT_IMAGE_PLAYGROUND_URL: baseUrl }
@@ -854,6 +863,11 @@ describe('Agent skill script argument validation', () => {
854
  const requestBody = JSON.parse(imageRequestBody);
855
  assert.equal(requestBody.size, '3072x2048');
856
  assert.equal(requestBody.streaming_strategy, 'off');
 
 
 
 
 
857
  }
858
  );
859
  });
@@ -1050,8 +1064,8 @@ describe('Agent skill script argument validation', () => {
1050
  assert.equal(body.error.code, 'page_sse_failed');
1051
  assert.match(body.error.message, /stream failed/);
1052
  assert.match(body.next_step, /新的 Idempotency-Key/);
1053
- assert.match(body.next_step, /重新校验输出尺寸和格式/);
1054
- assert.equal(body.routing.fallback_endpoint, '/api/agent/images/generate');
1055
  assert.equal(body.routing.fallback_mode, 'manual_after_diagnosis');
1056
  assert.deepEqual(
1057
  requests.map((item) => `${item.method} ${item.url}`),
@@ -2556,7 +2570,7 @@ describe('Agent skill script argument validation', () => {
2556
  transport: 'page_sse',
2557
  endpoint: '/api/images',
2558
  route_mode: 'page_sse',
2559
- fallback_endpoint: '/api/agent/images/generate',
2560
  fallback_mode: 'manual_after_diagnosis',
2561
  stream_mode: null,
2562
  streaming_strategy: null
@@ -2577,7 +2591,7 @@ describe('Agent skill script argument validation', () => {
2577
  );
2578
  });
2579
 
2580
- it('passes GPT2Image-compatible generate options to page SSE form-data', async () => {
2581
  const requests = [];
2582
  let pageSseRequestBody = '';
2583
  await withServer(
@@ -2615,6 +2629,7 @@ describe('Agent skill script argument validation', () => {
2615
  'generate-image.mjs',
2616
  [
2617
  '--allow-billable',
 
2618
  '--image-backend',
2619
  'responses',
2620
  '--responses-model',
@@ -2726,13 +2741,77 @@ describe('Agent skill script argument validation', () => {
2726
  );
2727
  });
2728
 
2729
- it('rejects generate page-only options for explicit Agent routes before network requests', async () => {
2730
  const requests = [];
 
 
2731
  await withServer(
2732
- (request, response) => {
2733
  requests.push({ method: request.method, url: request.url });
2734
- response.writeHead(500, { 'content-type': 'application/json' });
2735
- response.end(JSON.stringify({ error: 'unexpected request' }));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2736
  },
2737
  async (baseUrl) => {
2738
  const result = await runSkillScriptAsync(
@@ -2749,39 +2828,108 @@ describe('Agent skill script argument validation', () => {
2749
  { GPT_IMAGE_PLAYGROUND_URL: baseUrl }
2750
  );
2751
 
2752
- assert.equal(result.status, 2);
2753
- assert.equal(result.stdout.trim(), '');
2754
- assert.match(result.stderr, /Agent generate 不接受文生图高级页面字段/);
2755
- assert.deepEqual(requests, []);
 
2756
 
2757
- const jobResult = runSkillScript('generate-image.mjs', [
2758
- '--job',
2759
- '--image-backend',
2760
- 'responses',
2761
- '--responses-model',
2762
- 'gpt-5.4-mini',
2763
- 'prompt'
2764
- ]);
2765
- assert.equal(jobResult.status, 2);
2766
- assert.match(jobResult.stderr, /Agent generate job 不接受文生图高级页面字段/);
2767
- assert.equal(jobResult.stdout.trim(), '');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2768
  }
2769
  );
2770
  });
2771
 
2772
- it('rejects generate page-only options when streaming is disabled before network requests', async () => {
2773
  const requests = [];
 
2774
  await withServer(
2775
- (request, response) => {
2776
  requests.push({ method: request.method, url: request.url });
2777
- response.writeHead(500, { 'content-type': 'application/json' });
2778
- response.end(JSON.stringify({ error: 'unexpected request' }));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2779
  },
2780
  async (baseUrl) => {
2781
  const result = await runSkillScriptAsync(
2782
  'generate-image.mjs',
2783
  [
2784
  '--allow-billable',
 
 
2785
  '--image-backend',
2786
  'responses',
2787
  '--responses-model',
@@ -2793,10 +2941,20 @@ describe('Agent skill script argument validation', () => {
2793
  { GPT_IMAGE_PLAYGROUND_URL: baseUrl }
2794
  );
2795
 
2796
- assert.equal(result.status, 2);
2797
- assert.equal(result.stdout.trim(), '');
2798
- assert.match(result.stderr, /文生图高级页面参数需要页面 SSE/);
2799
- assert.deepEqual(requests, []);
 
 
 
 
 
 
 
 
 
 
2800
  }
2801
  );
2802
  });
@@ -3910,7 +4068,7 @@ describe('Agent skill script argument validation', () => {
3910
  assert.equal(body.guardrails.repeat_ordered_prefix_on_real_run, true);
3911
  assert.equal(body.guardrails.dimension_check_recommended, true);
3912
  assert.match(body.guardrails.dimension_check_reason, /--dimension-check/);
3913
- assert.equal(body.tasks[0].endpoint, '/api/agent/images/generate');
3914
  assert.equal(body.tasks[0].idempotency_key, 'demo-0001-first-item');
3915
  assert.equal(body.tasks[0].request.model, 'gpt-image-2');
3916
  assert.equal(body.tasks[0].request.output_format, 'jpeg');
@@ -3963,7 +4121,7 @@ describe('Agent skill script argument validation', () => {
3963
  response.end(JSON.stringify({ ok: true }));
3964
  return;
3965
  }
3966
- if (request.url === '/api/agent/images/generate') {
3967
  response.writeHead(500, { 'content-type': 'application/json' });
3968
  response.end(JSON.stringify({ error: { code: 'unexpected_error', message: 'fetch failed', retryable: false } }));
3969
  return;
@@ -4116,7 +4274,7 @@ describe('Agent skill script argument validation', () => {
4116
  response.end(JSON.stringify({ ok: true }));
4117
  return;
4118
  }
4119
- if (request.url === '/api/agent/images/generate') {
4120
  agentRequestBody = await readRequestText(request);
4121
  response.writeHead(200, { 'content-type': 'application/json' });
4122
  response.end(JSON.stringify({ images: [{ id: 'compressed-image', filename: 'compressed.jpg' }] }));
@@ -4170,7 +4328,7 @@ describe('Agent skill script argument validation', () => {
4170
  );
4171
  return;
4172
  }
4173
- if (request.url === '/api/agent/images/generate') {
4174
  response.writeHead(500, { 'content-type': 'application/json' });
4175
  response.end(JSON.stringify({ error: 'generate should not be called' }));
4176
  return;
@@ -4297,7 +4455,7 @@ describe('Agent skill script argument validation', () => {
4297
  response.end(JSON.stringify({ ok: true }));
4298
  return;
4299
  }
4300
- if (request.url === '/api/agent/images/generate') {
4301
  response.writeHead(200, { 'content-type': 'application/json' });
4302
  response.end(
4303
  JSON.stringify({
@@ -4329,7 +4487,7 @@ describe('Agent skill script argument validation', () => {
4329
  assert.equal(body.results[1].status, 'succeeded');
4330
  assert.deepEqual(
4331
  requests.map((item) => `${item.method} ${item.url}`),
4332
- ['GET /api/agent/capabilities', 'POST /api/agent/images/generate']
4333
  );
4334
  const manifestLines = readFileSync(manifestPath, 'utf8').trim().split(/\r?\n/).map(JSON.parse);
4335
  assert.equal(manifestLines.length, 3);
@@ -4368,7 +4526,7 @@ describe('Agent skill script argument validation', () => {
4368
  response.end(JSON.stringify({ ok: true }));
4369
  return;
4370
  }
4371
- if (request.url === '/api/agent/images/generate') {
4372
  activeGenerateRequests += 1;
4373
  enteredGenerateRequests += 1;
4374
  maxActiveGenerateRequests = Math.max(maxActiveGenerateRequests, activeGenerateRequests);
@@ -4478,7 +4636,7 @@ describe('Agent skill script argument validation', () => {
4478
  response.end(JSON.stringify({ ok: true }));
4479
  return;
4480
  }
4481
- if (request.url === '/api/agent/images/generate') {
4482
  activeGenerateRequests += 1;
4483
  maxActiveGenerateRequests = Math.max(maxActiveGenerateRequests, activeGenerateRequests);
4484
  await new Promise((resolve) => setTimeout(resolve, 20));
@@ -4521,8 +4679,8 @@ describe('Agent skill script argument validation', () => {
4521
  [
4522
  'GET /api/runtime-capabilities',
4523
  'GET /api/agent/capabilities',
4524
- 'POST /api/agent/images/generate',
4525
- 'POST /api/agent/images/generate'
4526
  ]
4527
  );
4528
  }
@@ -4576,7 +4734,7 @@ describe('Agent skill script argument validation', () => {
4576
  response.end(JSON.stringify({ ok: true }));
4577
  return;
4578
  }
4579
- if (request.url === '/api/agent/images/generate') {
4580
  response.writeHead(500, { 'content-type': 'application/json' });
4581
  response.end(JSON.stringify({ error: 'unexpected resumed request' }));
4582
  return;
@@ -4747,7 +4905,7 @@ describe('Agent skill script argument validation', () => {
4747
  }
4748
  });
4749
 
4750
- it('routes batch generate requests with responsesModel through page SSE', async () => {
4751
  const tempRoot = mkdtempSync(join(tmpdir(), 'gpt-image-batch-responses-model-'));
4752
  try {
4753
  const inputPath = join(tempRoot, 'tasks.jsonl');
@@ -4772,6 +4930,12 @@ describe('Agent skill script argument validation', () => {
4772
  response.writeHead(200, { 'content-type': 'application/json' });
4773
  response.end(
4774
  JSON.stringify({
 
 
 
 
 
 
4775
  agent_streaming: {
4776
  page_sse: { supported: true, endpoint: '/api/images' }
4777
  }
@@ -4779,23 +4943,56 @@ describe('Agent skill script argument validation', () => {
4779
  );
4780
  return;
4781
  }
4782
- if (request.url === '/api/images') {
4783
  pageSseRequestBody = await readRequestText(request);
4784
- response.writeHead(200, { 'content-type': 'text/event-stream' });
4785
  response.end(
4786
- [
4787
- 'data: {"type":"completed","filename":"responses.png","path":"/generated/responses.png"}',
4788
- '',
4789
- 'data: {"type":"done","images":[{"filename":"responses.png","path":"/generated/responses.png"}]}',
4790
- '',
4791
- ''
4792
- ].join('\n')
 
4793
  );
4794
  return;
4795
  }
4796
- if (request.url === '/api/agent/images/generate') {
4797
- response.writeHead(500, { 'content-type': 'application/json' });
4798
- response.end(JSON.stringify({ error: 'unexpected Agent generate call' }));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4799
  return;
4800
  }
4801
  response.writeHead(404, { 'content-type': 'application/json' });
@@ -4809,14 +5006,26 @@ describe('Agent skill script argument validation', () => {
4809
  assert.equal(result.status, 0);
4810
  assert.equal(result.stderr.trim(), '');
4811
  const body = JSON.parse(result.stdout);
4812
- assert.equal(body.results[0].routing.transport, 'page_sse');
 
 
 
 
 
 
 
 
 
 
 
4813
  assert.deepEqual(
4814
  requests.map((item) => `${item.method} ${item.url}`),
4815
- ['GET /api/agent/capabilities', 'POST /api/images']
4816
  );
4817
- assert.match(pageSseRequestBody, /name="image_backend"\r?\n\r?\nresponses/);
4818
- assert.match(pageSseRequestBody, /name="responsesModel"\r?\n\r?\ngpt-4\.1-responses/);
4819
- assert.match(pageSseRequestBody, /name="image_streaming_strategy"\r?\n\r?\nresponses-sse/);
 
4820
  }
4821
  );
4822
  } finally {
@@ -4918,7 +5127,7 @@ describe('Agent skill script argument validation', () => {
4918
  }
4919
  });
4920
 
4921
- it('fails batch responsesModel routing when page SSE capability is unavailable', async () => {
4922
  const tempRoot = mkdtempSync(join(tmpdir(), 'gpt-image-batch-responses-model-no-sse-'));
4923
  try {
4924
  const inputPath = join(tempRoot, 'tasks.jsonl');
@@ -4938,12 +5147,67 @@ describe('Agent skill script argument validation', () => {
4938
  requests.push({ method: request.method, url: request.url });
4939
  if (request.url === '/api/agent/capabilities') {
4940
  response.writeHead(200, { 'content-type': 'application/json' });
4941
- response.end(JSON.stringify({ agent_streaming: {} }));
 
 
 
 
 
 
 
 
 
4942
  return;
4943
  }
4944
- if (request.url === '/api/agent/images/generate') {
4945
- response.writeHead(500, { 'content-type': 'application/json' });
4946
- response.end(JSON.stringify({ error: 'unexpected Agent generate call' }));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4947
  return;
4948
  }
4949
  response.writeHead(404, { 'content-type': 'application/json' });
@@ -4954,15 +5218,23 @@ describe('Agent skill script argument validation', () => {
4954
  GPT_IMAGE_PLAYGROUND_URL: baseUrl
4955
  });
4956
 
4957
- assert.equal(result.status, 1);
4958
  assert.equal(result.stderr.trim(), '');
4959
  const body = JSON.parse(result.stdout);
4960
- assert.equal(body.results[0].ok, false);
4961
- assert.equal(body.results[0].error.code, 'page_sse_unavailable');
4962
- assert.equal(body.results[0].routing.transport, 'page_sse');
 
 
 
 
 
 
 
 
4963
  assert.deepEqual(
4964
  requests.map((item) => `${item.method} ${item.url}`),
4965
- ['GET /api/agent/capabilities']
4966
  );
4967
  }
4968
  );
@@ -4971,7 +5243,7 @@ describe('Agent skill script argument validation', () => {
4971
  }
4972
  });
4973
 
4974
- it('does not route batch tasks through page SSE when streaming is explicitly disabled', () => {
4975
  const tempRoot = mkdtempSync(join(tmpdir(), 'gpt-image-batch-disabled-page-sse-'));
4976
  try {
4977
  const inputPath = join(tempRoot, 'tasks.jsonl');
@@ -4989,8 +5261,9 @@ describe('Agent skill script argument validation', () => {
4989
 
4990
  assert.equal(result.status, 0);
4991
  const body = JSON.parse(result.stdout);
4992
- assert.equal(body.tasks[0].routing.transport, 'agent_json');
4993
- assert.equal(body.tasks[0].endpoint, '/api/agent/images/generate');
 
4994
  } finally {
4995
  rmSync(tempRoot, { recursive: true, force: true });
4996
  }
@@ -5221,7 +5494,7 @@ describe('Agent skill script argument validation', () => {
5221
  response.end(JSON.stringify({ ok: true }));
5222
  return;
5223
  }
5224
- if (request.url === '/api/agent/images/generate') {
5225
  idempotencyKeys.push(request.headers['idempotency-key']);
5226
  response.writeHead(500, { 'content-type': 'application/json' });
5227
  response.end(JSON.stringify({ error: { message: 'upstream failed', code: 'upstream_failed' } }));
@@ -5285,7 +5558,7 @@ describe('Agent skill script argument validation', () => {
5285
  response.end(JSON.stringify({ ok: true }));
5286
  return;
5287
  }
5288
- if (request.url === '/api/agent/images/generate') {
5289
  idempotencyKeys.push(request.headers['idempotency-key']);
5290
  response.writeHead(500, { 'content-type': 'application/json' });
5291
  response.end(JSON.stringify({ error: { message: 'upstream failed', code: 'upstream_failed' } }));
@@ -5341,7 +5614,7 @@ describe('Agent skill script argument validation', () => {
5341
  response.end(JSON.stringify({ ok: true }));
5342
  return;
5343
  }
5344
- if (request.url === '/api/agent/images/generate') {
5345
  idempotencyKeys.push(request.headers['idempotency-key']);
5346
  response.writeHead(500, { 'content-type': 'application/json' });
5347
  response.end(JSON.stringify({ error: { message: 'upstream failed', code: 'upstream_failed' } }));
@@ -5392,7 +5665,7 @@ describe('Agent skill script argument validation', () => {
5392
  response.end(JSON.stringify({ ok: true }));
5393
  return;
5394
  }
5395
- if (request.url === '/api/agent/images/generate') {
5396
  idempotencyKeys.push(request.headers['idempotency-key']);
5397
  response.writeHead(500, { 'content-type': 'application/json' });
5398
  response.end(JSON.stringify({ error: 'upstream failed' }));
@@ -5452,7 +5725,7 @@ describe('Agent skill script argument validation', () => {
5452
  response.end(JSON.stringify({ ok: true }));
5453
  return;
5454
  }
5455
- if (request.url === '/api/agent/images/generate') {
5456
  response.writeHead(200, { 'content-type': 'application/json' });
5457
  response.end(JSON.stringify({ images: [{ id: 'dim-image', filename: 'dim.png', content_url: '/artifact/dim.png' }] }));
5458
  return;
@@ -5861,9 +6134,13 @@ describe('Agent skill script argument validation', () => {
5861
  '--input',
5862
  responsesModelNonStreamInputPath
5863
  ]);
5864
- assert.equal(responsesModelNonStreamResult.status, 2);
5865
- assert.match(responsesModelNonStreamResult.stderr, /responses-model-non-stream\.responsesModel 需要页面 SSE 路径/);
5866
- assert.equal(responsesModelNonStreamResult.stdout.trim(), '');
 
 
 
 
5867
 
5868
  const responsesModelWithoutBackendInputPath = join(tempRoot, 'responses-model-without-backend.jsonl');
5869
  writeFileSync(
@@ -5915,7 +6192,7 @@ describe('Agent skill script argument validation', () => {
5915
  response.end(JSON.stringify({ ok: true }));
5916
  return;
5917
  }
5918
- if (request.url === '/api/agent/images/generate') {
5919
  response.writeHead(200, { 'content-type': 'application/json' });
5920
  response.end(
5921
  JSON.stringify({
 
555
  assert.equal(result.stderr.trim(), '');
556
  });
557
 
558
+ it('keeps GPT2Image-compatible generate options on server orchestration dry-runs', () => {
559
  const result = runSkillScript('generate-image.mjs', [
560
  '--image-backend',
561
  'responses',
 
571
 
572
  assert.equal(result.status, 0);
573
  const body = JSON.parse(result.stdout);
574
+ assert.equal(body.endpoint, 'http://localhost:4783/api/agent/image-requests');
575
+ assert.equal(body.routing_guidance.transport, 'server_orchestrated');
576
  assert.equal(body.request.image_backend, 'responses');
577
  assert.equal(body.request.responsesModel, 'gpt-5.4-mini');
578
  assert.equal(body.request.thinking, 'medium');
 
816
  '3072x2048',
817
  '--streaming-strategy',
818
  'off',
819
+ '--image-backend',
820
+ 'responses',
821
+ '--responses-model',
822
+ 'gpt-5.4-mini',
823
+ '--thinking',
824
+ 'medium',
825
+ '--prompt-optimization',
826
+ 'false',
827
+ '--force-web',
828
  'prompt'
829
  ],
830
  { GPT_IMAGE_PLAYGROUND_URL: baseUrl }
 
863
  const requestBody = JSON.parse(imageRequestBody);
864
  assert.equal(requestBody.size, '3072x2048');
865
  assert.equal(requestBody.streaming_strategy, 'off');
866
+ assert.equal(requestBody.image_backend, 'responses');
867
+ assert.equal(requestBody.responsesModel, 'gpt-5.4-mini');
868
+ assert.equal(requestBody.thinking, 'medium');
869
+ assert.equal(requestBody.promptOptimization, false);
870
+ assert.equal(requestBody.force_web, true);
871
  }
872
  );
873
  });
 
1064
  assert.equal(body.error.code, 'page_sse_failed');
1065
  assert.match(body.error.message, /stream failed/);
1066
  assert.match(body.next_step, /新的 Idempotency-Key/);
1067
+ assert.match(body.next_step, /服务端编排入口/);
1068
+ assert.equal(body.routing.fallback_endpoint, '/api/agent/image-requests');
1069
  assert.equal(body.routing.fallback_mode, 'manual_after_diagnosis');
1070
  assert.deepEqual(
1071
  requests.map((item) => `${item.method} ${item.url}`),
 
2570
  transport: 'page_sse',
2571
  endpoint: '/api/images',
2572
  route_mode: 'page_sse',
2573
+ fallback_endpoint: '/api/agent/image-requests',
2574
  fallback_mode: 'manual_after_diagnosis',
2575
  stream_mode: null,
2576
  streaming_strategy: null
 
2591
  );
2592
  });
2593
 
2594
+ it('passes GPT2Image-compatible generate options to page SSE only when explicitly requested', async () => {
2595
  const requests = [];
2596
  let pageSseRequestBody = '';
2597
  await withServer(
 
2629
  'generate-image.mjs',
2630
  [
2631
  '--allow-billable',
2632
+ '--page-sse',
2633
  '--image-backend',
2634
  'responses',
2635
  '--responses-model',
 
2741
  );
2742
  });
2743
 
2744
+ it('passes generate response controls through explicit Agent and job diagnostic routes', async () => {
2745
  const requests = [];
2746
+ let agentRequestBody = '';
2747
+ let jobRequestBody = '';
2748
  await withServer(
2749
+ async (request, response) => {
2750
  requests.push({ method: request.method, url: request.url });
2751
+ if (request.url === '/api/agent/capabilities') {
2752
+ response.writeHead(200, { 'content-type': 'application/json' });
2753
+ response.end(JSON.stringify(agentGenerateCapabilities()));
2754
+ return;
2755
+ }
2756
+ if (request.url === '/api/agent/images/generate') {
2757
+ agentRequestBody = await readRequestText(request);
2758
+ response.writeHead(200, { 'content-type': 'application/json' });
2759
+ response.end(
2760
+ JSON.stringify({
2761
+ images: [
2762
+ {
2763
+ id: 'agent-artifact',
2764
+ filename: 'agent.webp',
2765
+ content_url: '/api/agent/artifacts/agent-artifact/content'
2766
+ }
2767
+ ]
2768
+ })
2769
+ );
2770
+ return;
2771
+ }
2772
+ if (request.url === '/api/agent/jobs/images/generate') {
2773
+ jobRequestBody = await readRequestText(request);
2774
+ response.writeHead(202, { 'content-type': 'application/json' });
2775
+ response.end(
2776
+ JSON.stringify({
2777
+ job: {
2778
+ id: 'job-diagnostic-1',
2779
+ state: 'running',
2780
+ result_url: '/api/agent/jobs/job-diagnostic-1/result',
2781
+ retry_after_seconds: 1
2782
+ }
2783
+ })
2784
+ );
2785
+ return;
2786
+ }
2787
+ if (request.url === '/api/agent/jobs/job-diagnostic-1/result') {
2788
+ response.writeHead(200, { 'content-type': 'application/json' });
2789
+ response.end(
2790
+ JSON.stringify({
2791
+ request_id: 'job-diagnostic-1',
2792
+ idempotency_key: 'job-diagnostic-key',
2793
+ cached: false,
2794
+ images: [
2795
+ {
2796
+ id: 'job-artifact',
2797
+ filename: 'job.webp',
2798
+ content_url: '/api/agent/artifacts/job-artifact/content'
2799
+ }
2800
+ ],
2801
+ execution: {
2802
+ transport: 'agent_job_polling',
2803
+ endpoint: '/api/agent/jobs/images/generate',
2804
+ route_mode: 'job',
2805
+ image_backend: 'responses-image-generation',
2806
+ stream_mode: 'auto',
2807
+ streaming_strategy: 'auto'
2808
+ }
2809
+ })
2810
+ );
2811
+ return;
2812
+ }
2813
+ response.writeHead(404, { 'content-type': 'application/json' });
2814
+ response.end(JSON.stringify({ error: 'missing' }));
2815
  },
2816
  async (baseUrl) => {
2817
  const result = await runSkillScriptAsync(
 
2828
  { GPT_IMAGE_PLAYGROUND_URL: baseUrl }
2829
  );
2830
 
2831
+ assert.equal(result.status, 0);
2832
+ assert.equal(result.stderr.trim(), '');
2833
+ const agentBody = JSON.parse(agentRequestBody);
2834
+ assert.equal(agentBody.image_backend, 'responses');
2835
+ assert.equal(agentBody.responsesModel, 'gpt-5.4-mini');
2836
 
2837
+ const jobResult = await runSkillScriptAsync(
2838
+ 'generate-image.mjs',
2839
+ [
2840
+ '--allow-billable',
2841
+ '--job',
2842
+ '--idempotency-key',
2843
+ 'job-diagnostic-key',
2844
+ '--image-backend',
2845
+ 'responses',
2846
+ '--responses-model',
2847
+ 'gpt-5.4-mini',
2848
+ 'prompt'
2849
+ ],
2850
+ { GPT_IMAGE_PLAYGROUND_URL: baseUrl }
2851
+ );
2852
+ assert.equal(jobResult.status, 0);
2853
+ assert.equal(jobResult.stderr.trim(), '');
2854
+ const jobBody = JSON.parse(jobRequestBody);
2855
+ assert.equal(jobBody.image_backend, 'responses');
2856
+ assert.equal(jobBody.responsesModel, 'gpt-5.4-mini');
2857
+ assert.deepEqual(
2858
+ requests.map((item) => `${item.method} ${item.url}`),
2859
+ [
2860
+ 'GET /api/agent/capabilities',
2861
+ 'POST /api/agent/images/generate',
2862
+ 'GET /api/agent/capabilities',
2863
+ 'POST /api/agent/jobs/images/generate',
2864
+ 'GET /api/agent/jobs/job-diagnostic-1/result'
2865
+ ]
2866
+ );
2867
  }
2868
  );
2869
  });
2870
 
2871
+ it('keeps generate response controls on server orchestration when streaming is disabled', async () => {
2872
  const requests = [];
2873
+ let imageRequestBody = '';
2874
  await withServer(
2875
+ async (request, response) => {
2876
  requests.push({ method: request.method, url: request.url });
2877
+ if (request.url === '/api/agent/capabilities') {
2878
+ response.writeHead(200, { 'content-type': 'application/json' });
2879
+ response.end(JSON.stringify(agentGenerateCapabilities()));
2880
+ return;
2881
+ }
2882
+ if (request.url === '/api/agent/image-requests') {
2883
+ imageRequestBody = await readRequestText(request);
2884
+ response.writeHead(202, { 'content-type': 'application/json' });
2885
+ response.end(
2886
+ JSON.stringify({
2887
+ job: {
2888
+ id: 'job-nonstream-controls',
2889
+ state: 'running',
2890
+ result_url: '/api/agent/jobs/job-nonstream-controls/result',
2891
+ retry_after_seconds: 1
2892
+ }
2893
+ })
2894
+ );
2895
+ return;
2896
+ }
2897
+ if (request.url === '/api/agent/jobs/job-nonstream-controls/result') {
2898
+ response.writeHead(200, { 'content-type': 'application/json' });
2899
+ response.end(
2900
+ JSON.stringify({
2901
+ request_id: 'job-nonstream-controls',
2902
+ idempotency_key: 'nonstream-controls-key',
2903
+ cached: false,
2904
+ images: [
2905
+ {
2906
+ id: 'artifact-nonstream-controls',
2907
+ filename: 'nonstream.webp',
2908
+ content_url: '/api/agent/artifacts/artifact-nonstream-controls/content'
2909
+ }
2910
+ ],
2911
+ execution: {
2912
+ transport: 'agent_job_polling',
2913
+ endpoint: '/api/agent/image-requests',
2914
+ route_mode: 'job',
2915
+ image_backend: 'responses-image-generation',
2916
+ stream_mode: 'non_stream',
2917
+ streaming_strategy: 'auto'
2918
+ }
2919
+ })
2920
+ );
2921
+ return;
2922
+ }
2923
+ response.writeHead(404, { 'content-type': 'application/json' });
2924
+ response.end(JSON.stringify({ error: 'missing' }));
2925
  },
2926
  async (baseUrl) => {
2927
  const result = await runSkillScriptAsync(
2928
  'generate-image.mjs',
2929
  [
2930
  '--allow-billable',
2931
+ '--idempotency-key',
2932
+ 'nonstream-controls-key',
2933
  '--image-backend',
2934
  'responses',
2935
  '--responses-model',
 
2941
  { GPT_IMAGE_PLAYGROUND_URL: baseUrl }
2942
  );
2943
 
2944
+ assert.equal(result.status, 0);
2945
+ assert.equal(result.stderr.trim(), '');
2946
+ const requestBody = JSON.parse(imageRequestBody);
2947
+ assert.equal(requestBody.image_backend, 'responses');
2948
+ assert.equal(requestBody.responsesModel, 'gpt-5.4-mini');
2949
+ assert.equal(requestBody.stream_mode, 'non_stream');
2950
+ assert.deepEqual(
2951
+ requests.map((item) => `${item.method} ${item.url}`),
2952
+ [
2953
+ 'GET /api/agent/capabilities',
2954
+ 'POST /api/agent/image-requests',
2955
+ 'GET /api/agent/jobs/job-nonstream-controls/result'
2956
+ ]
2957
+ );
2958
  }
2959
  );
2960
  });
 
4068
  assert.equal(body.guardrails.repeat_ordered_prefix_on_real_run, true);
4069
  assert.equal(body.guardrails.dimension_check_recommended, true);
4070
  assert.match(body.guardrails.dimension_check_reason, /--dimension-check/);
4071
+ assert.equal(body.tasks[0].endpoint, '/api/agent/image-requests');
4072
  assert.equal(body.tasks[0].idempotency_key, 'demo-0001-first-item');
4073
  assert.equal(body.tasks[0].request.model, 'gpt-image-2');
4074
  assert.equal(body.tasks[0].request.output_format, 'jpeg');
 
4121
  response.end(JSON.stringify({ ok: true }));
4122
  return;
4123
  }
4124
+ if (request.url === '/api/agent/image-requests') {
4125
  response.writeHead(500, { 'content-type': 'application/json' });
4126
  response.end(JSON.stringify({ error: { code: 'unexpected_error', message: 'fetch failed', retryable: false } }));
4127
  return;
 
4274
  response.end(JSON.stringify({ ok: true }));
4275
  return;
4276
  }
4277
+ if (request.url === '/api/agent/image-requests') {
4278
  agentRequestBody = await readRequestText(request);
4279
  response.writeHead(200, { 'content-type': 'application/json' });
4280
  response.end(JSON.stringify({ images: [{ id: 'compressed-image', filename: 'compressed.jpg' }] }));
 
4328
  );
4329
  return;
4330
  }
4331
+ if (request.url === '/api/agent/image-requests') {
4332
  response.writeHead(500, { 'content-type': 'application/json' });
4333
  response.end(JSON.stringify({ error: 'generate should not be called' }));
4334
  return;
 
4455
  response.end(JSON.stringify({ ok: true }));
4456
  return;
4457
  }
4458
+ if (request.url === '/api/agent/image-requests') {
4459
  response.writeHead(200, { 'content-type': 'application/json' });
4460
  response.end(
4461
  JSON.stringify({
 
4487
  assert.equal(body.results[1].status, 'succeeded');
4488
  assert.deepEqual(
4489
  requests.map((item) => `${item.method} ${item.url}`),
4490
+ ['GET /api/agent/capabilities', 'POST /api/agent/image-requests']
4491
  );
4492
  const manifestLines = readFileSync(manifestPath, 'utf8').trim().split(/\r?\n/).map(JSON.parse);
4493
  assert.equal(manifestLines.length, 3);
 
4526
  response.end(JSON.stringify({ ok: true }));
4527
  return;
4528
  }
4529
+ if (request.url === '/api/agent/image-requests') {
4530
  activeGenerateRequests += 1;
4531
  enteredGenerateRequests += 1;
4532
  maxActiveGenerateRequests = Math.max(maxActiveGenerateRequests, activeGenerateRequests);
 
4636
  response.end(JSON.stringify({ ok: true }));
4637
  return;
4638
  }
4639
+ if (request.url === '/api/agent/image-requests') {
4640
  activeGenerateRequests += 1;
4641
  maxActiveGenerateRequests = Math.max(maxActiveGenerateRequests, activeGenerateRequests);
4642
  await new Promise((resolve) => setTimeout(resolve, 20));
 
4679
  [
4680
  'GET /api/runtime-capabilities',
4681
  'GET /api/agent/capabilities',
4682
+ 'POST /api/agent/image-requests',
4683
+ 'POST /api/agent/image-requests'
4684
  ]
4685
  );
4686
  }
 
4734
  response.end(JSON.stringify({ ok: true }));
4735
  return;
4736
  }
4737
+ if (request.url === '/api/agent/image-requests') {
4738
  response.writeHead(500, { 'content-type': 'application/json' });
4739
  response.end(JSON.stringify({ error: 'unexpected resumed request' }));
4740
  return;
 
4905
  }
4906
  });
4907
 
4908
+ it('routes batch generate requests with responsesModel through server orchestration', async () => {
4909
  const tempRoot = mkdtempSync(join(tmpdir(), 'gpt-image-batch-responses-model-'));
4910
  try {
4911
  const inputPath = join(tempRoot, 'tasks.jsonl');
 
4930
  response.writeHead(200, { 'content-type': 'application/json' });
4931
  response.end(
4932
  JSON.stringify({
4933
+ orchestration: {
4934
+ supported: true,
4935
+ transport_selection: 'server_owned',
4936
+ endpoint: '/api/agent/image-requests'
4937
+ },
4938
+ agent_jobs: { supported: true, mode: 'job_polling' },
4939
  agent_streaming: {
4940
  page_sse: { supported: true, endpoint: '/api/images' }
4941
  }
 
4943
  );
4944
  return;
4945
  }
4946
+ if (request.url === '/api/agent/image-requests') {
4947
  pageSseRequestBody = await readRequestText(request);
4948
+ response.writeHead(202, { 'content-type': 'application/json' });
4949
  response.end(
4950
+ JSON.stringify({
4951
+ job: {
4952
+ id: 'batch-job-responses',
4953
+ state: 'running',
4954
+ result_url: '/api/agent/jobs/batch-job-responses/result',
4955
+ retry_after_seconds: 1
4956
+ }
4957
+ })
4958
  );
4959
  return;
4960
  }
4961
+ if (request.url === '/api/agent/jobs/batch-job-responses/result') {
4962
+ response.writeHead(200, { 'content-type': 'application/json' });
4963
+ response.end(
4964
+ JSON.stringify({
4965
+ request_id: 'batch-job-responses',
4966
+ idempotency_key: 'batch-responses-model-key',
4967
+ cached: false,
4968
+ images: [
4969
+ {
4970
+ id: 'responses-artifact',
4971
+ filename: 'responses.png',
4972
+ content_url: '/generated/responses.png'
4973
+ }
4974
+ ],
4975
+ execution: {
4976
+ transport: 'agent_job_polling',
4977
+ endpoint: '/api/agent/image-requests',
4978
+ route_mode: 'job',
4979
+ image_backend: 'responses-image-generation',
4980
+ stream_mode: 'auto',
4981
+ streaming_strategy: 'auto',
4982
+ channel_request_mode: 'responses-sse',
4983
+ channel_request_mode_fallback_applied: false,
4984
+ route_decision: {
4985
+ requested_backend: 'responses-image-generation',
4986
+ preferred_channel_request_mode: 'responses-sse',
4987
+ selected_channel_request_mode: 'responses-sse',
4988
+ fallback_applied: false,
4989
+ selected_channel_id: 'responses-channel',
4990
+ upstream_host: 'upstream.example.test'
4991
+ }
4992
+ },
4993
+ timing: { server_elapsed_ms: 1234 }
4994
+ })
4995
+ );
4996
  return;
4997
  }
4998
  response.writeHead(404, { 'content-type': 'application/json' });
 
5006
  assert.equal(result.status, 0);
5007
  assert.equal(result.stderr.trim(), '');
5008
  const body = JSON.parse(result.stdout);
5009
+ assert.deepEqual(body.results[0].routing, {
5010
+ strength: 'default',
5011
+ transport: 'server_orchestrated',
5012
+ endpoint: '/api/agent/image-requests',
5013
+ route_mode: 'job',
5014
+ result_mode: 'job_polling',
5015
+ reason: 'Batch generate tasks submit intent to the server orchestration endpoint; the service owns internal route selection.'
5016
+ });
5017
+ assert.equal(body.results[0].summary.transport, 'agent_job_polling');
5018
+ assert.equal(body.results[0].summary.endpoint, '/api/agent/image-requests');
5019
+ assert.equal(body.results[0].summary.route_mode, 'job');
5020
+ assert.equal(body.results[0].summary.channel_request_mode, 'responses-sse');
5021
  assert.deepEqual(
5022
  requests.map((item) => `${item.method} ${item.url}`),
5023
+ ['GET /api/agent/capabilities', 'POST /api/agent/image-requests', 'GET /api/agent/jobs/batch-job-responses/result']
5024
  );
5025
+ const requestBody = JSON.parse(pageSseRequestBody);
5026
+ assert.equal(requestBody.image_backend, 'responses-image-generation');
5027
+ assert.equal(requestBody.responsesModel, 'gpt-4.1-responses');
5028
+ assert.equal(requestBody.streaming_strategy, 'responses-sse');
5029
  }
5030
  );
5031
  } finally {
 
5127
  }
5128
  });
5129
 
5130
+ it('keeps batch responsesModel routing on server orchestration when page SSE capability is unavailable', async () => {
5131
  const tempRoot = mkdtempSync(join(tmpdir(), 'gpt-image-batch-responses-model-no-sse-'));
5132
  try {
5133
  const inputPath = join(tempRoot, 'tasks.jsonl');
 
5147
  requests.push({ method: request.method, url: request.url });
5148
  if (request.url === '/api/agent/capabilities') {
5149
  response.writeHead(200, { 'content-type': 'application/json' });
5150
+ response.end(
5151
+ JSON.stringify({
5152
+ orchestration: {
5153
+ supported: true,
5154
+ transport_selection: 'server_owned',
5155
+ endpoint: '/api/agent/image-requests'
5156
+ },
5157
+ agent_jobs: { supported: true, mode: 'job_polling' }
5158
+ })
5159
+ );
5160
  return;
5161
  }
5162
+ if (request.url === '/api/agent/image-requests') {
5163
+ response.writeHead(202, { 'content-type': 'application/json' });
5164
+ response.end(
5165
+ JSON.stringify({
5166
+ job: {
5167
+ id: 'batch-no-page-sse-job',
5168
+ state: 'running',
5169
+ result_url: '/api/agent/jobs/batch-no-page-sse-job/result',
5170
+ retry_after_seconds: 1
5171
+ }
5172
+ })
5173
+ );
5174
+ return;
5175
+ }
5176
+ if (request.url === '/api/agent/jobs/batch-no-page-sse-job/result') {
5177
+ response.writeHead(200, { 'content-type': 'application/json' });
5178
+ response.end(
5179
+ JSON.stringify({
5180
+ request_id: 'batch-no-page-sse-job',
5181
+ idempotency_key: 'responses-no-page-sse',
5182
+ cached: false,
5183
+ images: [
5184
+ {
5185
+ id: 'batch-no-page-sse-artifact',
5186
+ filename: 'responses.png',
5187
+ content_url: '/generated/responses.png'
5188
+ }
5189
+ ],
5190
+ execution: {
5191
+ transport: 'agent_job_polling',
5192
+ endpoint: '/api/agent/image-requests',
5193
+ route_mode: 'job',
5194
+ image_backend: 'responses-image-generation',
5195
+ stream_mode: 'auto',
5196
+ streaming_strategy: 'auto',
5197
+ channel_request_mode: 'images-non-stream',
5198
+ channel_request_mode_fallback_applied: false,
5199
+ route_decision: {
5200
+ requested_backend: 'responses-image-generation',
5201
+ preferred_channel_request_mode: 'images-non-stream',
5202
+ selected_channel_request_mode: 'images-non-stream',
5203
+ fallback_applied: false,
5204
+ selected_channel_id: 'images-channel',
5205
+ upstream_host: 'upstream.example.test'
5206
+ }
5207
+ },
5208
+ timing: { server_elapsed_ms: 987 }
5209
+ })
5210
+ );
5211
  return;
5212
  }
5213
  response.writeHead(404, { 'content-type': 'application/json' });
 
5218
  GPT_IMAGE_PLAYGROUND_URL: baseUrl
5219
  });
5220
 
5221
+ assert.equal(result.status, 0);
5222
  assert.equal(result.stderr.trim(), '');
5223
  const body = JSON.parse(result.stdout);
5224
+ assert.equal(body.results[0].ok, true);
5225
+ assert.deepEqual(body.results[0].routing, {
5226
+ strength: 'default',
5227
+ transport: 'server_orchestrated',
5228
+ endpoint: '/api/agent/image-requests',
5229
+ route_mode: 'job',
5230
+ result_mode: 'job_polling',
5231
+ reason: 'Batch generate tasks submit intent to the server orchestration endpoint; the service owns internal route selection.'
5232
+ });
5233
+ assert.equal(body.results[0].summary.transport, 'agent_job_polling');
5234
+ assert.equal(body.results[0].summary.channel_request_mode, 'images-non-stream');
5235
  assert.deepEqual(
5236
  requests.map((item) => `${item.method} ${item.url}`),
5237
+ ['GET /api/agent/capabilities', 'POST /api/agent/image-requests', 'GET /api/agent/jobs/batch-no-page-sse-job/result']
5238
  );
5239
  }
5240
  );
 
5243
  }
5244
  });
5245
 
5246
+ it('does not route batch generate tasks through page SSE when streaming is explicitly disabled', () => {
5247
  const tempRoot = mkdtempSync(join(tmpdir(), 'gpt-image-batch-disabled-page-sse-'));
5248
  try {
5249
  const inputPath = join(tempRoot, 'tasks.jsonl');
 
5261
 
5262
  assert.equal(result.status, 0);
5263
  const body = JSON.parse(result.stdout);
5264
+ assert.equal(body.tasks[0].routing.transport, 'server_orchestrated');
5265
+ assert.equal(body.tasks[0].routing.endpoint, '/api/agent/image-requests');
5266
+ assert.equal(body.tasks[0].routing.result_mode, 'job_polling');
5267
  } finally {
5268
  rmSync(tempRoot, { recursive: true, force: true });
5269
  }
 
5494
  response.end(JSON.stringify({ ok: true }));
5495
  return;
5496
  }
5497
+ if (request.url === '/api/agent/image-requests') {
5498
  idempotencyKeys.push(request.headers['idempotency-key']);
5499
  response.writeHead(500, { 'content-type': 'application/json' });
5500
  response.end(JSON.stringify({ error: { message: 'upstream failed', code: 'upstream_failed' } }));
 
5558
  response.end(JSON.stringify({ ok: true }));
5559
  return;
5560
  }
5561
+ if (request.url === '/api/agent/image-requests') {
5562
  idempotencyKeys.push(request.headers['idempotency-key']);
5563
  response.writeHead(500, { 'content-type': 'application/json' });
5564
  response.end(JSON.stringify({ error: { message: 'upstream failed', code: 'upstream_failed' } }));
 
5614
  response.end(JSON.stringify({ ok: true }));
5615
  return;
5616
  }
5617
+ if (request.url === '/api/agent/image-requests') {
5618
  idempotencyKeys.push(request.headers['idempotency-key']);
5619
  response.writeHead(500, { 'content-type': 'application/json' });
5620
  response.end(JSON.stringify({ error: { message: 'upstream failed', code: 'upstream_failed' } }));
 
5665
  response.end(JSON.stringify({ ok: true }));
5666
  return;
5667
  }
5668
+ if (request.url === '/api/agent/image-requests') {
5669
  idempotencyKeys.push(request.headers['idempotency-key']);
5670
  response.writeHead(500, { 'content-type': 'application/json' });
5671
  response.end(JSON.stringify({ error: 'upstream failed' }));
 
5725
  response.end(JSON.stringify({ ok: true }));
5726
  return;
5727
  }
5728
+ if (request.url === '/api/agent/image-requests') {
5729
  response.writeHead(200, { 'content-type': 'application/json' });
5730
  response.end(JSON.stringify({ images: [{ id: 'dim-image', filename: 'dim.png', content_url: '/artifact/dim.png' }] }));
5731
  return;
 
6134
  '--input',
6135
  responsesModelNonStreamInputPath
6136
  ]);
6137
+ assert.equal(responsesModelNonStreamResult.status, 0);
6138
+ assert.equal(responsesModelNonStreamResult.stderr.trim(), '');
6139
+ const responsesModelNonStreamBody = JSON.parse(responsesModelNonStreamResult.stdout);
6140
+ assert.equal(responsesModelNonStreamBody.tasks[0].routing.transport, 'server_orchestrated');
6141
+ assert.equal(responsesModelNonStreamBody.tasks[0].routing.endpoint, '/api/agent/image-requests');
6142
+ assert.equal(responsesModelNonStreamBody.tasks[0].request.responsesModel, 'gpt-4.1');
6143
+ assert.equal(responsesModelNonStreamBody.tasks[0].request.stream_mode, 'non_stream');
6144
 
6145
  const responsesModelWithoutBackendInputPath = join(tempRoot, 'responses-model-without-backend.jsonl');
6146
  writeFileSync(
 
6192
  response.end(JSON.stringify({ ok: true }));
6193
  return;
6194
  }
6195
+ if (request.url === '/api/agent/image-requests') {
6196
  response.writeHead(200, { 'content-type': 'application/json' });
6197
  response.end(
6198
  JSON.stringify({
skills/gpt-image-playground-agent/SKILL.md CHANGED
@@ -50,7 +50,7 @@ Agent API 只作为自动化客户端接口,不作为首战场景或用户验
50
  - 复杂 UI、长 prompt、高质量图生图遇到 5 分钟级超时、连接中断或上游 503 时,不要把失败归因到提示词质量;先读 `summary` 和诊断,再用新 key 显式尝试压缩 prompt 或改为 `quality=medium` 的对照请求,并记录这是稳定性取舍。
51
  - 长图恢复或需要续跑锚点的生产请求优先使用页面端 `POST /api/images` SSE,保留局部进度和缺最终图诊断。
52
  - 普通单次文生图默认使用 `POST /api/agent/image-requests`。`--agent`、`--job`、`--page-sse` 是显式诊断或兼容开关:`--agent` 直连 `/api/agent/images/generate`,`--job` 直连 `/api/agent/jobs/images/generate`,`--page-sse` 直连页面端 `/api/images` SSE。不要把这些显式开关当成默认自动路由。
53
- - 单张文生图使用 `--responses-model`/`--gpt-model`、`--thinking`、`--prompt-optimization` 或 `--force-web` 时走页面 `/api/images` SSE因为这些是页面高级字段,不属于 Agent JSON schema。`--responses-model` 覆盖本次请求的 Responses 顶层模型;未传时使用服务端 `OPENAI_RESPONSES_API_MODEL`。该字段只影响本项目 `responses-image-generation` 路径,不改变兼容上游自身 Images API 桥接层内部选择的模型。`--responses-model` 必须同时设置 `--image-backend responses-image-generation` 或兼容别名 `responses`;显式 `--agent`、`--job`、`stream_mode=non_stream` `streaming_strategy=off` 会被脚本前置拒绝
54
  - 同一个已进入终态 `failed` 的 `Idempotency-Key` 只会回放失败;重新尝试必须先诊断原因,再创建新的业务操作和新的 key。
55
 
56
  ## 执行流程
@@ -63,10 +63,10 @@ Agent API 只作为自动化客户端接口,不作为首战场景或用户验
63
  6. 让脚本请求 `GET /api/agent/capabilities`。如果所选地址不可达、404、不是 JSON 或不是 Agent capabilities 响应,交互式任务中向用户询问实际部署地址、端口、域名和是否需要鉴权;非交互式任务中显式失败并输出下一步动作。
64
  7. 读取 capabilities 中的认证方式、模型、模型级限制、`image_transport`、`orchestration`、`routing_rules`、Agent 流式边界、页面 SSE 鉴权、后端 runtime enablement、状态后端和端点路径;不要硬编码假设部署方式。
65
  8. 为每个业务操作生成稳定的 `Idempotency-Key`。网络中断、运行中轮询或非终态重试复用原 key;同一 key 已进入 `failed` 终态后不再用于触发新执行,必须先诊断原因,再创建新的业务操作和新的 key。
66
- 9. 文生图默认使用 `POST /api/agent/image-requests`,请求体为 JSON 业务意图,服务端返回 job 状态并由脚本轮询 `job.result_url`。服务端内部仍会返回标准 `AgentImageResponse`,并在 `execution` 中暴露真实执行路径。显式 `--agent` 才使用 `POST /api/agent/images/generate`;不要把 `responsesModel`、`thinking`、`promptOptimization` `force_web` 发送到 Agent JSON,这些字段需要页面端 `/api/images` form-data 路径
67
  10. 图片编辑若走 Agent edit,使用 `POST /api/agent/images/edit`,请求体为 `multipart/form-data`,源图字段必须使用从 `image_0` 开始的连续字段,最大数量以 capabilities 的 `limits.upload_images.max` 为准;跳号、超过当前 profile 上限、`image_01` 或 `image_foo` 会被显式拒绝。该 Agent 端点同样是非流式端点;上游 SSE 字段按 `agent_streaming.upstream_sse.request_fields_by_mode.edit` 发送,不要给 Agent edit 传 `image_backend`。需要 `image_backend=responses-image-generation` 或页面表单字段 `image_streaming_strategy=responses-sse` 的 edit,一律走页面端 `/api/images` form-data SSE;脚本参数仍写作 `--streaming-strategy responses-sse`。
68
  11. 默认使用 `response_mode: "path"`,只���用户明确需要图片内联数据时使用 `base64` 或 `both`。
69
- 12. 不要把页面端 `POST /api/images` 当成普通 Agent JSON 路径。它是页面表单和 SSE 路径,capabilities 会以 `agent_streaming.page_sse` 单独声明;在默认 WebP edit、复杂 UI 批量、长图恢复、页面高级字段、显式 `--page-sse` 或明确诊断后使用。
70
  13. 读取 `agent_jobs` 只用于理解服务端编排结果和显式 `--job` 诊断路径。普通 generate 不再由 Agent 客户端根据本地/远端或尺寸选择 Agent JSON、page SSE 或 job。
71
  14. 处理失败时读取结构化 `error.code`、`error.retryable`、`error.diagnostics` 和 `Retry-After`。仅当 `retryable=true` 时等待后重试。页面 SSE 返回 `503`、断流,或 `summary` 里的 `selected_channel_id`、`upstream_host` 为空时,先按结构化失败诊断,再用新 key 显式换路径,不要把它当成已自动回退成功。
72
  15. 返回结果时优先给出 `summary`、`content_url`、`metadata_url`、`absolute_content_url`、`absolute_metadata_url`、产物 ID、尺寸、格式和是否命中幂等缓存。回答“4K 非流式花了多久”时优先读 `summary.elapsed_ms`,服务端返回 timing 时也读 `summary.server_elapsed_ms`。
@@ -194,7 +194,7 @@ node "<skill-root>/scripts/convert-image-format.mjs" --format webp --quality 100
194
  node "<skill-root>/scripts/convert-image-format.mjs" --format png --output ./source.png ./source.webp --overwrite
195
  ```
196
 
197
- 生成脚本默认把文生图业务意图提交到服务端编排入口 `/api/agent/image-requests`;服务端内部决定使用 Agent JSON、内部上游 SSE、job polling 或其他可观测路径。脚本不再根据 `max_edge>2048`、公网 HTTPS 或 `streaming_strategy=off` 自行切换默认端点。单张 generate 支持 `--responses-model`/`--gpt-model`、`--thinking`、`--prompt-optimization` 和 `--force-web`;这些字段属于页面 SSE form-datadry-run 会显示 page SSE 路由,真实请求会发送 `responsesModel`、`thinking`、`promptOptimization` 和 `force_web`。`--agent`、`--job`、`--page-sse` 是显式诊断或兼容开关,使用时必须记录原因,并用新的 `Idempotency-Key` 避免混淆业务操作。默认 WebP edit 走页面 SSE;显式 `--agent` 才走 Agent multipart 最终 JSON,输出格式固定为 Agent 契约。Responses image_generation edit 属于页面 SSE 路径:可显式传 `--page-sse --image-backend responses-image-generation --streaming-strategy responses-sse`;如果运行时已显式配置 `IMAGE_GENERATION_BACKEND=responses-image-generation` 或兼容别名 `responses`,且 `IMAGE_STREAMING_STRATEGY=responses-sse`,也可以依赖服务端默认。Docker compose 本身不设置这两个默认值,未配置 `.env.local` 时仍是 `images-api` 和 `auto`。不要为 Responses edit 加 `--agent`。默认 WebP edit 与非流式策略冲突时脚本前置拒绝,除非显式添加 `--agent` 做 Agent JSON 对照。页面高级 edit 字段与非流式策略冲突时脚本同样前置拒绝。上游流式字段优先读取 `agent_streaming.upstream_sse.request_fields_by_mode`:generate 支持 `--image-backend`、`--stream-mode`、`--streaming-strategy`、`--partial-images`;Agent edit 只支持 `--stream-mode`、`--streaming-strategy`、`--partial-images`。页面 SSE edit 可发送 `image_backend` 和表单字段 `image_streaming_strategy`;CLI 参数是 `--streaming-strategy`,batch JSONL 字段是 `streaming_strategy`。
198
 
199
  generate 或页面 SSE 请求包含 `image_backend` 时,`partial_images` 必须先按 `limits.partial_images_by_backend[image_backend]` 校验;capabilities 没有该字段时才退回 `limits.partial_images`。Agent edit 不接受 `image_backend`,其内部上游流式字段按默认 Images API/profile 范围校验;Responses backend edit 需要页面 SSE。不要把 Matsca `limits.partial_images=0..4` 误套到 `responses-image-generation`,Responses backend 当前使用自己的 `1..3` 范围。
200
 
@@ -220,7 +220,7 @@ node "<skill-root>/scripts/batch-images.mjs" --base-url https://your-space.hf.sp
220
 
221
  `--manifest` 使用 JSONL append-only 记录每条任务的 `index`、`id`、`idempotency_key`、`attempt`、`status`、响应或错误以及机器可读 `summary`;Agent JSON 失败时 manifest 也会记录只读诊断补采样得到的 `agent_failure_diagnostics`。`--resume` 会读取已成功记录并跳过同一 `id` 或 `idempotency_key`。dry-run 不写 manifest,输出会声明 `manifest_written=false`、`manifest_write_reason=dry_run` 和 `guardrails`;真实执行应复用 dry-run 中的同一个 `--ordered-prefix`,否则未显式写 `idempotency_key` 的任务会生成不同 key。`--dimension-check` 会读取响应里的 `b64_json` 或同 origin `content_url`,校验 PNG/JPEG/WebP 尺寸是否等于任务 `size`;通过时成功摘要写入 `image_dimensions` 和单图 `actual_dimensions`,失败时 `error.code=dimension_check_failed`、`validation_failure_kind=generated_artifact_failed_dimension_check`、产物 URL、`summary.expected_dimensions`、`summary.actual_dimensions`、`summary.dimension_check_failed=true` 会同时写入 manifest。固定尺寸任务没有开启时,dry-run 会在 `guardrails.dimension_check_recommended` 中提示。`--max-attempts` 会为第二次及以后尝试追加新的 attempt 级 idempotency key,避免复用终态失败 key;`--concurrency` 大于 `1` 时会先读取运行态并发建议,并发执行任务并按输入顺序输出结果。服务端 `recommendedConcurrency` 或 `channelQueue.capacityPerCredential` 小于请求值时,脚本会把有效并发降到建议值并在输出中写入 `capacity_feedback`;不要再另开多个单张脚本绕过这个限制。`failure_summary` 会区分 `validation_failure_count` 和 `request_failure_count`,避免把已生成但验收失败误判为上游调用失败。`--max-consecutive-failures` 会在连续失败达到阈值后跳过后续任务并输出 `failure_summary` 与 `resume_fix_list`,且只能与顺序执行的 `--concurrency 1` 同用。任务级 `sse_log_path` 会把页面 SSE 原始事件按 JSONL 追加保存;即使 fetch 或 SSE 收集阶段失败,也会记录 `request_started`、`request_failed`、`elapsed_ms`、`client_request_id` 和 `endpoint`,便于区分上游未给终图和解析/断流问题。
222
 
223
- 批量 JSONL 字段按模式区分:`background` 只适用于 `generate`;`image_path`、`image_paths`、`mask_path` 只适用于 `edit`。默认 WebP edit 任务走页面 SSE;如需 Agent edit 固定输出,请拆成单张 `edit-image.mjs --agent`。`output_format`、`format`、`output_compression`、`moderation`、`image_backend`、`streaming_strategy`、`partial_images`、`responsesModel`/`gptModel`/`gpt_model`、`thinking`、`promptOptimization`/`prompt_optimization`、`force_web`/`forceWeb` 可用于页面 SSE 路径。edit 任务设置 `image_backend=responses-image-generation` 时会走页面 SSE;不要把它改成 Agent edit。`responsesModel` 必须同时设置 `image_backend=responses-image-generation` 或兼容值 `responses`。JSONL 字段名必须使用 `streaming_strategy`;`image_streaming_strategy` 是页面 form-data 字段名,不是 batch JSONL 字段,会被脚本在真实请求前拒绝。PNG 搭配 `output_compression` 会在 dry-run 标记 normalization,真实请求不会发送压缩字段。`page_sse`、`complex_ui`、`long_image`、`resume_or_recover` 必须是 JSON 布尔值,`transport` 目前只接受 `page_sse`。脚本会在 dry-run 阶段显式拒绝跨模式字段、未知字段和无效路由控制字段。
224
 
225
  编辑脚本支持位置参数 `<image-path> <prompt>`,也支持 `--image <path> <prompt>` 别名;不要同时传两种图片路径。常用选项包括 `--model`、`--size`、`--quality`、`--response-mode`、`--format`、`--output-compression`、`--moderation`、`--image-backend`、`--responses-model`、`--thinking`、`--prompt-optimization`、`--force-web`、`--stream-mode`、`--streaming-strategy`、`--partial-images`、`--timeout-ms`、`--idempotency-key`、`--page-sse`、`--agent`、`--dry-run` 和 `--allow-billable`。其中 `--image-backend responses-image-generation` 只用于页面 SSE edit。
226
 
 
50
  - 复杂 UI、长 prompt、高质量图生图遇到 5 分钟级超时、连接中断或上游 503 时,不要把失败归因到提示词质量;先读 `summary` 和诊断,再用新 key 显式尝试压缩 prompt 或改为 `quality=medium` 的对照请求,并记录这是稳定性取舍。
51
  - 长图恢复或需要续跑锚点的生产请求优先使用页面端 `POST /api/images` SSE,保留局部进度和缺最终图诊断。
52
  - 普通单次文生图默认使用 `POST /api/agent/image-requests`。`--agent`、`--job`、`--page-sse` 是显式诊断或兼容开关:`--agent` 直连 `/api/agent/images/generate`,`--job` 直连 `/api/agent/jobs/images/generate`,`--page-sse` 直连页面端 `/api/images` SSE。不要把这些显式开关当成默认自动路由。
53
+ - 单张文生图使用 `--responses-model`/`--gpt-model`、`--thinking`、`--prompt-optimization` 或 `--force-web` 时仍提交到服务编排入口由服务端选择 Responses image_generation、Images API、SSE 或非流式执行方式。`--responses-model` 覆盖本次请求的 Responses 顶层模型;未传时使用服务端 `OPENAI_RESPONSES_API_MODEL`。该字段只影响本项目 `responses-image-generation` 路径,不改变兼容上游自身 Images API 桥接层内部选择的模型。`--responses-model` 必须同时设置 `--image-backend responses-image-generation` 或兼容别名 `responses`;显式 `--page-sse` 才直连页面端 `/api/images` SSE 做诊断
54
  - 同一个已进入终态 `failed` 的 `Idempotency-Key` 只会回放失败;重新尝试必须先诊断原因,再创建新的业务操作和新的 key。
55
 
56
  ## 执行流程
 
63
  6. 让脚本请求 `GET /api/agent/capabilities`。如果所选地址不可达、404、不是 JSON 或不是 Agent capabilities 响应,交互式任务中向用户询问实际部署地址、端口、域名和是否需要鉴权;非交互式任务中显式失败并输出下一步动作。
64
  7. 读取 capabilities 中的认证方式、模型、模型级限制、`image_transport`、`orchestration`、`routing_rules`、Agent 流式边界、页面 SSE 鉴权、后端 runtime enablement、状态后端和端点路径;不要硬编码假设部署方式。
65
  8. 为每个业务操作生成稳定的 `Idempotency-Key`。网络中断、运行中轮询或非终态重试复用原 key;同一 key 已进入 `failed` 终态后不再用于触发新执行,必须先诊断原因,再创建新的业务操作和新的 key。
66
+ 9. 文生图默认使用 `POST /api/agent/image-requests`,请求体为 JSON 业务意图,服务端返回 job 状态并由脚本轮询 `job.result_url`。服务端内部仍会返回标准 `AgentImageResponse`,并在 `execution` 中暴露真实执行路径。`responsesModel`、`thinking`、`promptOptimization` `force_web` generate 意图字段;脚本默认把它们交给服务端编排入口处理,不因此自行改用页面端 `/api/images`。
67
  10. 图片编辑若走 Agent edit,使用 `POST /api/agent/images/edit`,请求体为 `multipart/form-data`,源图字段必须使用从 `image_0` 开始的连续字段,最大数量以 capabilities 的 `limits.upload_images.max` 为准;跳号、超过当前 profile 上限、`image_01` 或 `image_foo` 会被显式拒绝。该 Agent 端点同样是非流式端点;上游 SSE 字段按 `agent_streaming.upstream_sse.request_fields_by_mode.edit` 发送,不要给 Agent edit 传 `image_backend`。需要 `image_backend=responses-image-generation` 或页面表单字段 `image_streaming_strategy=responses-sse` 的 edit,一律走页面端 `/api/images` form-data SSE;脚本参数仍写作 `--streaming-strategy responses-sse`。
68
  11. 默认使用 `response_mode: "path"`,只���用户明确需要图片内联数据时使用 `base64` 或 `both`。
69
+ 12. 不要把页面端 `POST /api/images` 当成普通 Agent JSON 路径。它是页面表单和 SSE 路径,capabilities 会以 `agent_streaming.page_sse` 单独声明;generate 只显式 `--page-sse` 或页面工作台诊断时使用它,默认 WebP edit、Responses edit、长图恢复或需要原始 SSE 日志的 edit 仍可使用页面 SSE
70
  13. 读取 `agent_jobs` 只用于理解服务端编排结果和显式 `--job` 诊断路径。普通 generate 不再由 Agent 客户端根据本地/远端或尺寸选择 Agent JSON、page SSE 或 job。
71
  14. 处理失败时读取结构化 `error.code`、`error.retryable`、`error.diagnostics` 和 `Retry-After`。仅当 `retryable=true` 时等待后重试。页面 SSE 返回 `503`、断流,或 `summary` 里的 `selected_channel_id`、`upstream_host` 为空时,先按结构化失败诊断,再用新 key 显式换路径,不要把它当成已自动回退成功。
72
  15. 返回结果时优先给出 `summary`、`content_url`、`metadata_url`、`absolute_content_url`、`absolute_metadata_url`、产物 ID、尺寸、格式和是否命中幂等缓存。回答“4K 非流式花了多久”时优先读 `summary.elapsed_ms`,服务端返回 timing 时也读 `summary.server_elapsed_ms`。
 
194
  node "<skill-root>/scripts/convert-image-format.mjs" --format png --output ./source.png ./source.webp --overwrite
195
  ```
196
 
197
+ 生成脚本默认把文生图业务意图提交到服务端编排入口 `/api/agent/image-requests`;服务端内部决定使用 Agent JSON、内部上游 SSE、job polling 或其他可观测路径。脚本不再根据 `max_edge>2048`、公网 HTTPS、页面高级字段或 `streaming_strategy=off` 自行切换默认端点。单张 generate 支持 `--responses-model`/`--gpt-model`、`--thinking`、`--prompt-optimization` 和 `--force-web`;这些字段会作为生成意图提交到服务端编排入口由服务端选择 Responses image_generation、Images API、SSE 或非流式执行方式。`--agent`、`--job`、`--page-sse` 是显式诊断或兼容开关,使用时必须记录原因,并用新的 `Idempotency-Key` 避免混淆业务操作。默认 WebP edit 走页面 SSE;显式 `--agent` 才走 Agent multipart 最终 JSON,输出格式固定为 Agent 契约。Responses image_generation edit 属于页面 SSE 路径:可显式传 `--page-sse --image-backend responses-image-generation --streaming-strategy responses-sse`;如果运行时已显式配置 `IMAGE_GENERATION_BACKEND=responses-image-generation` 或兼容别名 `responses`,且 `IMAGE_STREAMING_STRATEGY=responses-sse`,也可以依赖服务端默认���。Docker compose 本身不设置这两个默认值,未配置 `.env.local` 时仍是 `images-api` 和 `auto`。不要为 Responses edit 加 `--agent`。默认 WebP edit 与非流式策略冲突时脚本前置拒绝,除非显式添加 `--agent` 做 Agent JSON 对照。页面高级 edit 字段与非流式策略冲突时脚本同样前置拒绝。上游流式字段优先读取 `agent_streaming.upstream_sse.request_fields_by_mode`:generate 支持 `--image-backend`、`--stream-mode`、`--streaming-strategy`、`--partial-images`;Agent edit 只支持 `--stream-mode`、`--streaming-strategy`、`--partial-images`。页面 SSE edit 可发送 `image_backend` 和表单字段 `image_streaming_strategy`;CLI 参数是 `--streaming-strategy`,batch JSONL 字段是 `streaming_strategy`。
198
 
199
  generate 或页面 SSE 请求包含 `image_backend` 时,`partial_images` 必须先按 `limits.partial_images_by_backend[image_backend]` 校验;capabilities 没有该字段时才退回 `limits.partial_images`。Agent edit 不接受 `image_backend`,其内部上游流式字段按默认 Images API/profile 范围校验;Responses backend edit 需要页面 SSE。不要把 Matsca `limits.partial_images=0..4` 误套到 `responses-image-generation`,Responses backend 当前使用自己的 `1..3` 范围。
200
 
 
220
 
221
  `--manifest` 使用 JSONL append-only 记录每条任务的 `index`、`id`、`idempotency_key`、`attempt`、`status`、响应或错误以及机器可读 `summary`;Agent JSON 失败时 manifest 也会记录只读诊断补采样得到的 `agent_failure_diagnostics`。`--resume` 会读取已成功记录并跳过同一 `id` 或 `idempotency_key`。dry-run 不写 manifest,输出会声明 `manifest_written=false`、`manifest_write_reason=dry_run` 和 `guardrails`;真实执行应复用 dry-run 中的同一个 `--ordered-prefix`,否则未显式写 `idempotency_key` 的任务会生成不同 key。`--dimension-check` 会读取响应里的 `b64_json` 或同 origin `content_url`,校验 PNG/JPEG/WebP 尺寸是否等于任务 `size`;通过时成功摘要写入 `image_dimensions` 和单图 `actual_dimensions`,失败时 `error.code=dimension_check_failed`、`validation_failure_kind=generated_artifact_failed_dimension_check`、产物 URL、`summary.expected_dimensions`、`summary.actual_dimensions`、`summary.dimension_check_failed=true` 会同时写入 manifest。固定尺寸任务没有开启时,dry-run 会在 `guardrails.dimension_check_recommended` 中提示。`--max-attempts` 会为第二次及以后尝试追加新的 attempt 级 idempotency key,避免复用终态失败 key;`--concurrency` 大于 `1` 时会先读取运行态并发建议,并发执行任务并按输入顺序输出结果。服务端 `recommendedConcurrency` 或 `channelQueue.capacityPerCredential` 小于请求值时,脚本会把有效并发降到建议值并在输出中写入 `capacity_feedback`;不要再另开多个单张脚本绕过这个限制。`failure_summary` 会区分 `validation_failure_count` 和 `request_failure_count`,避免把已生成但验收失败误判为上游调用失败。`--max-consecutive-failures` 会在连续失败达到阈值后跳过后续任务并输出 `failure_summary` 与 `resume_fix_list`,且只能与顺序执行的 `--concurrency 1` 同用。任务级 `sse_log_path` 会把页面 SSE 原始事件按 JSONL 追加保存;即使 fetch 或 SSE 收集阶段失败,也会记录 `request_started`、`request_failed`、`elapsed_ms`、`client_request_id` 和 `endpoint`,便于区分上游未给终图和解析/断流问题。
222
 
223
+ 批量 JSONL 字段按模式区分:`background` 只适用于 `generate`;`image_path`、`image_paths`、`mask_path` 只适用于 `edit`。批量 generate 默认提交到 `/api/agent/image-requests`,`responsesModel`/`gptModel`/`gpt_model`、`thinking`、`promptOptimization`/`prompt_optimization`、`force_web`/`forceWeb` 会作为 generate 意图字段随 JSON 发送给服务端编排入口。默认 WebP edit 任务走页面 SSE;如需 Agent edit 固定输出,请拆成单张 `edit-image.mjs --agent`。`output_format`、`format`、`output_compression`、`moderation`、`image_backend`、`streaming_strategy`、`partial_images` 和上述 Responses 控制字段仍可用于页面 SSE edit 路径。edit 任务设置 `image_backend=responses-image-generation` 时会走页面 SSE;不要把它改成 Agent edit。`responsesModel` 必须同时设置 `image_backend=responses-image-generation` 或兼容值 `responses`。JSONL 字段名必须使用 `streaming_strategy`;`image_streaming_strategy` 是页面 form-data 字段名,不是 batch JSONL 字段,会被脚本在真实请求前拒绝。PNG 搭配 `output_compression` 会在 dry-run 标记 normalization,真实请求不会发送压缩字段。`page_sse`、`complex_ui`、`long_image`、`resume_or_recover` 必须是 JSON 布尔值,`transport` 目前只接受 `page_sse`。脚本会在 dry-run 阶段显式拒绝跨模式字段、未知字段和无效路由控制字段。
224
 
225
  编辑脚本支持位置参数 `<image-path> <prompt>`,也支持 `--image <path> <prompt>` 别名;不要同时传两种图片路径。常用选项包括 `--model`、`--size`、`--quality`、`--response-mode`、`--format`、`--output-compression`、`--moderation`、`--image-backend`、`--responses-model`、`--thinking`、`--prompt-optimization`、`--force-web`、`--stream-mode`、`--streaming-strategy`、`--partial-images`、`--timeout-ms`、`--idempotency-key`、`--page-sse`、`--agent`、`--dry-run` 和 `--allow-billable`。其中 `--image-backend responses-image-generation` 只用于页面 SSE edit。
226
 
skills/gpt-image-playground-agent/references/api.md CHANGED
@@ -59,10 +59,10 @@ npm run env:summary -- --file .env.local --container gpt-image-playground-custom
59
  - `--output-compression`:默认 `100`,仅适用于 `jpeg` 或 `webp`。
60
  - `--response-mode`:默认 `path`。
61
  - `--image-backend`:可选,显式选择 `images-api`、`images`、`responses` 或 `responses-image-generation`。
62
- - `--responses-model` / `--gpt-model`:页面 SSE 专属字段,覆盖本次请求的 Responses 顶层模型;未传时使用服务端 `OPENAI_RESPONSES_API_MODEL`。必须同时设置 `--image-backend responses-image-generation` 或兼容别名 `responses`。该字段只影响本项目的 `responses-image-generation` 路径,不改变兼容上游自身 Images API 桥接层内部选择的模型。
63
- - `--thinking`:页面 SSE 专属字段,可选值为 `minimal`、`none`、`low`、`medium`、`high` 或 `xhigh`。
64
- - `--prompt-optimization`:页面 SSE 专属字段,必须是 `true` 或 `false`。
65
- - `--force-web`:页面 SSE 专属字段,发送为 form-data `force_web=true`。
66
  - `--stream-mode`:可选,显式选择 `auto`、`stream` 或 `non_stream`。
67
  - `--streaming-strategy`:可选,显式选择 `off`、`auto`、`openai-sse`、`newapi-keepalive-sse`、`responses-sse` 或 `force-sse`。
68
  - `--partial-images`:可选,显式设置上游 SSE partial image 数量。generate 或页面 SSE 请求包含 `image_backend` 时优先按 capabilities 的 `limits.partial_images_by_backend[image_backend]` 校验;缺少 backend 专属范围时才使用 `limits.partial_images`。
@@ -77,7 +77,7 @@ npm run env:summary -- --file .env.local --container gpt-image-playground-custom
77
  - `--preset`:常用 dry-run/真实调用参数集,当前包括 `1k-smoke-agent`、`4k-agent-nonstream`、`4k-page-sse` 和 `4k-upstream-sse-newapi`。dry-run 会展开真实请求字段,不触发计费。
78
 
79
  普通单次文生图默认提交到 `/api/agent/image-requests` 服务端编排入口;脚本不再按 `max_edge>2048`、公网 HTTPS 或 `--streaming-strategy off` 自行选择 page SSE、Agent JSON 或 job endpoint。需要对照时显式使用 `--page-sse`、`--agent` 或 `--job`。
80
- 单张 generate 脚本使用 `--responses-model`/`--gpt-model`、`--thinking`、`--prompt-optimization` 或 `--force-web` 时会选择页面端 `/api/images` SSE显式 `--agent``--job``stream_mode=non_stream``streaming_strategy=off` 与这些页面高级字段同时出现时会在网络请求前失败,避免字段被 Agent JSON 忽略
81
  当服务端默认 `IMAGE_STREAMING_STRATEGY=off` 且请求未覆盖 `streaming_strategy` 时,运行时默认策略为 `off`;WebUI 会把 server-default 流式请求切到 `non_stream`,并发批量开关不可用。generate 脚本显式传 `--streaming-strategy off` 时仍提交给服务端编排入口,除非同时显式使用 `--agent`。
82
 
83
  编辑脚本参数:
@@ -125,7 +125,7 @@ npm run env:summary -- --file .env.local --container gpt-image-playground-custom
125
 
126
  批量 dry-run 不写 manifest,输出会声明 `manifest_written=false`、`manifest_write_reason=dry_run` 和 `guardrails`。`guardrails.ordered_prefix` 是本次 dry-run 用于自动生成幂等键的前缀,真实执行应复用同一个 `--ordered-prefix`;`guardrails.dimension_check_recommended=true` 表示输入包含固定尺寸但未启用 `--dimension-check`。只有真实执行时 manifest 才作为 append-only 续跑记录写入;Agent JSON 失败时 manifest 会同时保存增强后的 `summary` 和 `agent_failure_diagnostics`。尺寸门禁失败同样写入结构化 summary 和可审查产物 URL,避免只能从中文错误文本解析期望和实际尺寸。批量总摘要会输出 `failure_summary.validation_failure_count` 和 `failure_summary.request_failure_count`,用于区分“上游已生成但本地验收失败”和“请求未成功完成”。当 `validation_failure_count>0` 而 `request_failure_count=0` 时,要按验收失败处理,不能当成上游不可用。
127
 
128
- 批量 JSONL 每行字段按 `mode` 区分。`background` 只适用于 `generate`;`image_path`、`image_paths`、`mask_path` 只适用于 `edit`。默认 WebP edit 任务走页面 SSE;如需 Agent edit 固定输出,请拆成单张 `edit-image.mjs --agent`。`output_format`、`format`、`output_compression`、`moderation`、`image_backend`、`streaming_strategy`、`partial_images`、`responsesModel`/`gptModel`/`gpt_model`、`thinking`、`promptOptimization`/`prompt_optimization`、`force_web`/`forceWeb` 可用于页面 SSE 路径。edit 任务设置 `image_backend=responses-image-generation` 时会走页面 SSE;不要把它改成 Agent edit。`responsesModel` 会选择页面 SSE 路径,且必须同时设置 `image_backend=responses-image-generation` 或兼容值 `responses`,因为 Agent JSON 不接收请求级 Responses 顶层模型。JSONL 字段名必须使用 `streaming_strategy`;`image_streaming_strategy` 是页面 form-data 字段名,不是 batch JSONL 字段,会被脚本在真实请求前拒绝。PNG 搭配 `output_compression` 会在 dry-run 标记 normalization,真实请求不会发送压缩字段。`page_sse`、`complex_ui`、`long_image`、`resume_or_recover` 必须是 JSON 布尔值,`transport` 目前只接受 `page_sse`。脚本会在 dry-run 阶段显式拒绝跨模式字段、未知字段和无效路由控制字段,避免参数被真实接口忽略。
129
 
130
  Responses edit JSONL 正例:
131
 
@@ -216,7 +216,7 @@ GET /api/agent/capabilities
216
  - `orchestration.transport_selection`:当前为 `server_owned`,表示 Agent 客户端不应按尺寸、远端 HTTPS 或流式参数自行选择 page SSE、Agent JSON 或 job endpoint。
217
  - `orchestration.result_mode`:当前为 `job_polling`,脚本会轮询 `job.result_url` 并输出标准 `AgentImageResponse`。
218
  - `routing_rules.agent_generate_small_smoke`:`strength=explicit`,兼容旧客户端和显式 `--agent` 诊断路径;不是普通 generate 默认入口。
219
- - `routing_rules.page_sse_large_generate`:`strength=explicit`,显式 page SSE 诊断和页面工作台路径的参考规则;普通 generate 默认仍走 `orchestration.endpoint`。
220
  - `routing_rules.retry_recovery`:终态失败不会用同一 `Idempotency-Key` 重新执行,必须诊断后创建新的业务操作和新的 key。
221
  - 批量 JSONL 路由控制字段:`page_sse`、`complex_ui`、`long_image`、`resume_or_recover` 必须是 JSON 布尔值,`transport` 目前只接受 `page_sse`;脚本会在 dry-run 阶段拒绝字符串布尔值和未知 transport。
222
  - `GET /api/runtime-capabilities` 不属于 Agent capabilities。它是页面工作台读取的运行态能力摘要,用于展示流式默认值、图片上游传输配置、渠道健康、渠道队列、并发建议、Responses 后端 enablement 和缺失环境变量,不进入 Agent OpenAPI。
@@ -336,7 +336,7 @@ Agent JSON 生成端点对外始终返回最终 JSON,不会对客户端返回
336
 
337
  - 页面 SSE 使用独立的 `POST /api/images` form-data 路径。
338
  - 若 capabilities 中 `agent_streaming.upstream_sse.supported=true`,generate 可通过 `request_fields_by_mode.generate` 声明的字段控制服务端内部上游 SSE 消费。Agent JSON 的 `image_backend=responses-image-generation` 当前只支持 generate;Responses backend edit 使用页面端 `/api/images` form-data SSE。
339
- - 不要向 Agent 生成端点发送 `responsesModel`、`gptModel`、`gpt_model`、`thinking`、`promptOptimization`、`prompt_optimization`、`force_web` 或 `forceWeb`。这些是页面 form-data 高级字段;单张 generate 脚本会在需要时走 `/api/images` SSE。
340
  - `/api/agent/image-requests` 的最终轮询结果和 Agent JSON 生成端点最终响应都使用 `AgentImageResponse`。
341
  - `stream_mode=stream` 强制流式并直接暴露失败。
342
  - `stream_mode=non_stream` 直接非流式。
 
59
  - `--output-compression`:默认 `100`,仅适用于 `jpeg` 或 `webp`。
60
  - `--response-mode`:默认 `path`。
61
  - `--image-backend`:可选,显式选择 `images-api`、`images`、`responses` 或 `responses-image-generation`。
62
+ - `--responses-model` / `--gpt-model`:生成意图字段,覆盖本次请求的 Responses 顶层模型;未传时使用服务端 `OPENAI_RESPONSES_API_MODEL`。必须同时设置 `--image-backend responses-image-generation` 或兼容别名 `responses`。该字段只影响本项目的 `responses-image-generation` 路径,不改变兼容上游自身 Images API 桥接层内部选择的模型。
63
+ - `--thinking`:生成意图字段,可选值为 `minimal`、`none`、`low`、`medium`、`high` 或 `xhigh`。
64
+ - `--prompt-optimization`:生成意图字段,必须是 `true` 或 `false`。
65
+ - `--force-web`:生成意图字段,服务端在 Images API 路径发送为 `force_web=true`。
66
  - `--stream-mode`:可选,显式选择 `auto`、`stream` 或 `non_stream`。
67
  - `--streaming-strategy`:可选,显式选择 `off`、`auto`、`openai-sse`、`newapi-keepalive-sse`、`responses-sse` 或 `force-sse`。
68
  - `--partial-images`:可选,显式设置上游 SSE partial image 数量。generate 或页面 SSE 请求包含 `image_backend` 时优先按 capabilities 的 `limits.partial_images_by_backend[image_backend]` 校验;缺少 backend 专属范围时才使用 `limits.partial_images`。
 
77
  - `--preset`:常用 dry-run/真实调用参数集,当前包括 `1k-smoke-agent`、`4k-agent-nonstream`、`4k-page-sse` 和 `4k-upstream-sse-newapi`。dry-run 会展开真实请求字段,不触发计费。
78
 
79
  普通单次文生图默认提交到 `/api/agent/image-requests` 服务端编排入口;脚本不再按 `max_edge>2048`、公网 HTTPS 或 `--streaming-strategy off` 自行选择 page SSE、Agent JSON 或 job endpoint。需要对照时显式使用 `--page-sse`、`--agent` 或 `--job`。
80
+ 单张 generate 脚本使用 `--responses-model`/`--gpt-model`、`--thinking`、`--prompt-optimization` 或 `--force-web` 时仍默认提交到 `/api/agent/image-requests`。服务端会在内部决定使用 Responses image_generationImages APISSE非流式路径;Agent 客户端不应因为这些字段自行选择 `/api/images`
81
  当服务端默认 `IMAGE_STREAMING_STRATEGY=off` 且请求未覆盖 `streaming_strategy` 时,运行时默认策略为 `off`;WebUI 会把 server-default 流式请求切到 `non_stream`,并发批量开关不可用。generate 脚本显式传 `--streaming-strategy off` 时仍提交给服务端编排入口,除非同时显式使用 `--agent`。
82
 
83
  编辑脚本参数:
 
125
 
126
  批量 dry-run 不写 manifest,输出会声明 `manifest_written=false`、`manifest_write_reason=dry_run` 和 `guardrails`。`guardrails.ordered_prefix` 是本次 dry-run 用于自动生成幂等键的前缀,真实执行应复用同一个 `--ordered-prefix`;`guardrails.dimension_check_recommended=true` 表示输入包含固定尺寸但未启用 `--dimension-check`。只有真实执行时 manifest 才作为 append-only 续跑记录写入;Agent JSON 失败时 manifest 会同时保存增强后的 `summary` 和 `agent_failure_diagnostics`。尺寸门禁失败同样写入结构化 summary 和可审查产物 URL,避免只能从中文错误文本解析期望和实际尺寸。批量总摘要会输出 `failure_summary.validation_failure_count` 和 `failure_summary.request_failure_count`,用于区分“上游已生成但本地验收失败”和“请求未成功完成”。当 `validation_failure_count>0` 而 `request_failure_count=0` 时,要按验收失败处理,不能当成上游不可用。
127
 
128
+ 批量 JSONL 每行字段按 `mode` 区分。`background` 只适用于 `generate`;`image_path`、`image_paths`、`mask_path` 只适用于 `edit`。批量 generate 默认提交到 `/api/agent/image-requests`,`responsesModel`/`gptModel`/`gpt_model`、`thinking`、`promptOptimization`/`prompt_optimization`、`force_web`/`forceWeb` 会随 JSON 业务意图提交给服务端编排入口。默认 WebP edit 任务走页面 SSE;如需 Agent edit 固定输出,请拆成单张 `edit-image.mjs --agent`。`output_format`、`format`、`output_compression`、`moderation`、`image_backend`、`streaming_strategy`、`partial_images` 以及上述 Responses 控制字段可用于页面 SSE edit 路径。edit 任务设置 `image_backend=responses-image-generation` 时会走页面 SSE;不要把它改成 Agent edit。`responsesModel` 必须同时设置 `image_backend=responses-image-generation` 或兼容值 `responses`。JSONL 字段名必须使用 `streaming_strategy`;`image_streaming_strategy` 是页面 form-data 字段名,不是 batch JSONL 字段,会被脚本在真实请求前拒绝。PNG 搭配 `output_compression` 会在 dry-run 标记 normalization,真实请求不会发送压缩字段。`page_sse`、`complex_ui`、`long_image`、`resume_or_recover` 必须是 JSON 布尔值,`transport` 目前只接受 `page_sse`。脚本会在 dry-run 阶段显式拒绝跨模式字段、未知字段和无效路由控制字段,避免参数被真实接口忽略。
129
 
130
  Responses edit JSONL 正例:
131
 
 
216
  - `orchestration.transport_selection`:当前为 `server_owned`,表示 Agent 客户端不应按尺寸、远端 HTTPS 或流式参数自行选择 page SSE、Agent JSON 或 job endpoint。
217
  - `orchestration.result_mode`:当前为 `job_polling`,脚本会轮询 `job.result_url` 并输出标准 `AgentImageResponse`。
218
  - `routing_rules.agent_generate_small_smoke`:`strength=explicit`,兼容旧客户端和显式 `--agent` 诊断路径;不是普通 generate 默认入口。
219
+ - `routing_rules.page_sse_generate_diagnostics`:`strength=explicit`,显式 page SSE 诊断和页面工作台路径的参考规则;普通 generate 默认仍走 `orchestration.endpoint`。
220
  - `routing_rules.retry_recovery`:终态失败不会用同一 `Idempotency-Key` 重新执行,必须诊断后创建新的业务操作和新的 key。
221
  - 批量 JSONL 路由控制字段:`page_sse`、`complex_ui`、`long_image`、`resume_or_recover` 必须是 JSON 布尔值,`transport` 目前只接受 `page_sse`;脚本会在 dry-run 阶段拒绝字符串布尔值和未知 transport。
222
  - `GET /api/runtime-capabilities` 不属于 Agent capabilities。它是页面工作台读取的运行态能力摘要,用于展示流式默认值、图片上游传输配置、渠道健康、渠道队列、并发建议、Responses 后端 enablement 和缺失环境变量,不进入 Agent OpenAPI。
 
336
 
337
  - 页面 SSE 使用独立的 `POST /api/images` form-data 路径。
338
  - 若 capabilities 中 `agent_streaming.upstream_sse.supported=true`,generate 可通过 `request_fields_by_mode.generate` 声明的字段控制服务端内部上游 SSE 消费。Agent JSON 的 `image_backend=responses-image-generation` 当前只支持 generate;Responses backend edit 使用页面端 `/api/images` form-data SSE。
339
+ - 不要向 Agent 生成端点发送 `responsesModel`、`gptModel`、`gpt_model`、`thinking`、`promptOptimization`、`prompt_optimization`、`force_web` 或 `forceWeb`。这些是 generate 意图字段;脚本默认把它们交给服务端编排入口处理,只有显式 `--page-sse` 才进入 `/api/images` SSE。
340
  - `/api/agent/image-requests` 的最终轮询结果和 Agent JSON 生成端点最终响应都使用 `AgentImageResponse`。
341
  - `stream_mode=stream` 强制流式并直接暴露失败。
342
  - `stream_mode=non_stream` 直接非流式。
skills/gpt-image-playground-agent/scripts/batch-images.mjs CHANGED
@@ -1,5 +1,5 @@
1
  #!/usr/bin/env node
2
- import { AGENT_ENDPOINTS } from './lib/agent-api-paths.mjs';
3
  import { enrichFailureWithAgentDiagnostics } from './lib/agent-diagnostics-summary.mjs';
4
  import {
5
  errorMessage,
@@ -547,9 +547,6 @@ function validateTaskRoutingFields(raw, id, mode) {
547
  if ((raw.page_sse === true || raw.transport === 'page_sse') && (raw.stream_mode === 'non_stream' || raw.streaming_strategy === 'off')) {
548
  throw new Error(`${id} stream_mode=non_stream 或 streaming_strategy=off 时不能强制使用页面 SSE。`);
549
  }
550
- if (readResponsesModel(raw, id) && (raw.stream_mode === 'non_stream' || raw.streaming_strategy === 'off')) {
551
- throw new Error(`${id}.responsesModel 需要页面 SSE 路径,不能同时设置 stream_mode=non_stream 或 streaming_strategy=off。`);
552
- }
553
  if (hasOwn(raw, 'sse_log_path') && (raw.stream_mode === 'non_stream' || raw.streaming_strategy === 'off')) {
554
  throw new Error(`${id}.sse_log_path 需要页面 SSE 路径,不能同时设置 stream_mode=non_stream 或 streaming_strategy=off。`);
555
  }
@@ -894,7 +891,11 @@ function buildTaskFailureOutput(error, routing) {
894
  }
895
 
896
  function shouldEnrichAgentFailure(failure) {
897
- return failure.billable !== false && failure.routing?.transport === 'agent_json';
 
 
 
 
898
  }
899
 
900
  function buildCircuitBreakerSkippedTask(task, consecutiveFailures) {
@@ -960,7 +961,7 @@ function normalizeFailureError(error) {
960
 
961
  function buildFailureNextStep(error) {
962
  if (error.code === 'page_sse_request_rejected') return '修正请求参数或鉴权后,使用新的 Idempotency-Key 重试失败任务。';
963
- if (error.code === 'page_sse_unavailable') return '补齐 page_sse capability 显式改Agent JSON,再重试失败任务。';
964
  return '诊断失败原因后,用新的 Idempotency-Key 重试失败任务;不要复用终态失败 key。';
965
  }
966
 
@@ -992,16 +993,26 @@ function buildTaskRouting(task) {
992
  endpoint: PAGE_SSE_ENDPOINT,
993
  transport: 'page_sse',
994
  strength: task.mode === 'edit' && readTaskMaxEdge(task) > 2048 ? 'default' : 'recommended',
995
- fallback_endpoint: task.mode === 'edit' ? AGENT_ENDPOINTS.edit : AGENT_ENDPOINTS.generate,
996
  fallback_mode: 'manual_after_diagnosis',
997
  reason
998
  };
999
  }
 
 
 
 
 
 
 
 
 
 
1000
  return {
1001
- endpoint: task.mode === 'edit' ? AGENT_ENDPOINTS.edit : AGENT_ENDPOINTS.generate,
1002
  transport: 'agent_json',
1003
  strength: 'default',
1004
- reason: 'Agent JSON-compatible batch tasks use the Agent response contract.'
1005
  };
1006
  }
1007
 
@@ -1085,14 +1096,10 @@ function shouldUsePageSseForTask(task) {
1085
  }
1086
  if (!pageSseAllowed) return false;
1087
  if (task.raw.complex_ui === true || task.raw.long_image === true || task.raw.resume_or_recover === true) return true;
1088
- if (readResponsesModel(task.raw, task.id)) return true;
1089
  if (task.raw.sse_log_path) return true;
1090
  if (task.mode === 'edit' && hasPageAdvancedFields(task.raw)) return true;
1091
  if (task.mode === 'edit') return true;
1092
  if (task.mode === 'edit' && readTaskMaxEdge(task) > 2048) return true;
1093
- if (task.mode === 'generate' && readTaskMaxEdge(task) > 2048) {
1094
- return true;
1095
- }
1096
  return false;
1097
  }
1098
 
@@ -1114,13 +1121,52 @@ async function postGenerateTask(task) {
1114
  taskCapabilities
1115
  )
1116
  );
1117
- const { response, result, text } = await fetchJson(`${baseUrl}${AGENT_ENDPOINTS.generate}`, {
1118
  method: 'POST',
1119
  headers: { 'Content-Type': 'application/json', 'Idempotency-Key': task.idempotencyKey, ...authHeaders() },
1120
  body: JSON.stringify(body)
1121
  });
1122
  if (!response.ok) throw new Error(readErrorMessage(result) || `generate 请求失败,状态码 ${response.status}:${text}`);
1123
- return enrichImageUrls(result);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1124
  }
1125
 
1126
  async function postEditTask(task) {
@@ -1238,6 +1284,10 @@ function buildGenerateBody(raw) {
1238
  ? normalizeOutputFormat(raw.output_format ?? raw.format)
1239
  : DEFAULT_OUTPUT_FORMAT;
1240
  const outputCompression = readOutputCompression(raw, String(raw.id || 'generate'));
 
 
 
 
1241
  const body = {
1242
  prompt: raw.prompt,
1243
  model: raw.model || 'gpt-image-2',
@@ -1250,6 +1300,10 @@ function buildGenerateBody(raw) {
1250
  ...(raw.background ? { background: raw.background } : {}),
1251
  ...(raw.moderation ? { moderation: raw.moderation } : {}),
1252
  ...(hasOwn(raw, 'image_backend') ? { image_backend: normalizeEnumValue(raw.image_backend, IMAGE_BACKENDS, 'image_backend') } : {}),
 
 
 
 
1253
  ...(hasOwn(raw, 'stream_mode') ? { stream_mode: normalizeEnumValue(raw.stream_mode, STREAM_MODES, 'stream_mode') } : {}),
1254
  ...(hasOwn(raw, 'streaming_strategy')
1255
  ? { streaming_strategy: normalizeEnumValue(raw.streaming_strategy, STREAMING_STRATEGIES, 'streaming_strategy') }
 
1
  #!/usr/bin/env node
2
+ import { AGENT_ENDPOINTS, buildAgentJobResultPath } from './lib/agent-api-paths.mjs';
3
  import { enrichFailureWithAgentDiagnostics } from './lib/agent-diagnostics-summary.mjs';
4
  import {
5
  errorMessage,
 
547
  if ((raw.page_sse === true || raw.transport === 'page_sse') && (raw.stream_mode === 'non_stream' || raw.streaming_strategy === 'off')) {
548
  throw new Error(`${id} stream_mode=non_stream 或 streaming_strategy=off 时不能强制使用页面 SSE。`);
549
  }
 
 
 
550
  if (hasOwn(raw, 'sse_log_path') && (raw.stream_mode === 'non_stream' || raw.streaming_strategy === 'off')) {
551
  throw new Error(`${id}.sse_log_path 需要页面 SSE 路径,不能同时设置 stream_mode=non_stream 或 streaming_strategy=off。`);
552
  }
 
891
  }
892
 
893
  function shouldEnrichAgentFailure(failure) {
894
+ return (
895
+ failure.billable !== false &&
896
+ failure.routing?.transport !== 'page_sse' &&
897
+ failure.validation_failure_kind !== FAILURE_KIND_DIMENSION_CHECK
898
+ );
899
  }
900
 
901
  function buildCircuitBreakerSkippedTask(task, consecutiveFailures) {
 
961
 
962
  function buildFailureNextStep(error) {
963
  if (error.code === 'page_sse_request_rejected') return '修正请求参数或鉴权后,使用新的 Idempotency-Key 重试失败任务。';
964
+ if (error.code === 'page_sse_unavailable') return '补齐 page_sse capability或用新的 Idempotency-Key 改走服端编排或显式诊断路径。';
965
  return '诊断失败原因后,用新的 Idempotency-Key 重试失败任务;不要复用终态失败 key。';
966
  }
967
 
 
993
  endpoint: PAGE_SSE_ENDPOINT,
994
  transport: 'page_sse',
995
  strength: task.mode === 'edit' && readTaskMaxEdge(task) > 2048 ? 'default' : 'recommended',
996
+ fallback_endpoint: task.mode === 'edit' ? AGENT_ENDPOINTS.edit : AGENT_ENDPOINTS.create_image_request,
997
  fallback_mode: 'manual_after_diagnosis',
998
  reason
999
  };
1000
  }
1001
+ if (task.mode === 'generate') {
1002
+ return {
1003
+ endpoint: AGENT_ENDPOINTS.create_image_request,
1004
+ transport: 'server_orchestrated',
1005
+ strength: 'default',
1006
+ route_mode: 'job',
1007
+ result_mode: 'job_polling',
1008
+ reason: 'Batch generate tasks submit intent to the server orchestration endpoint; the service owns internal route selection.'
1009
+ };
1010
+ }
1011
  return {
1012
+ endpoint: AGENT_ENDPOINTS.edit,
1013
  transport: 'agent_json',
1014
  strength: 'default',
1015
+ reason: 'Agent JSON-compatible batch edit tasks use the Agent response contract.'
1016
  };
1017
  }
1018
 
 
1096
  }
1097
  if (!pageSseAllowed) return false;
1098
  if (task.raw.complex_ui === true || task.raw.long_image === true || task.raw.resume_or_recover === true) return true;
 
1099
  if (task.raw.sse_log_path) return true;
1100
  if (task.mode === 'edit' && hasPageAdvancedFields(task.raw)) return true;
1101
  if (task.mode === 'edit') return true;
1102
  if (task.mode === 'edit' && readTaskMaxEdge(task) > 2048) return true;
 
 
 
1103
  return false;
1104
  }
1105
 
 
1121
  taskCapabilities
1122
  )
1123
  );
1124
+ const { response, result, text } = await fetchJson(`${baseUrl}${AGENT_ENDPOINTS.create_image_request}`, {
1125
  method: 'POST',
1126
  headers: { 'Content-Type': 'application/json', 'Idempotency-Key': task.idempotencyKey, ...authHeaders() },
1127
  body: JSON.stringify(body)
1128
  });
1129
  if (!response.ok) throw new Error(readErrorMessage(result) || `generate 请求失败,状态码 ${response.status}:${text}`);
1130
+ if (!result?.job) return enrichImageUrls(result);
1131
+ return pollJobResult(result.job);
1132
+ }
1133
+
1134
+ async function pollJobResult(job) {
1135
+ if (!job || typeof job.id !== 'string') {
1136
+ throw new Error('创建 job 的响应缺少 job.id。');
1137
+ }
1138
+ const resultUrl = resolveSameOriginUrl(baseUrl, job.result_url || buildAgentJobResultPath(job.id), 'job.result_url');
1139
+ const deadlineMs = Date.now() + timeoutMs;
1140
+ let lastResult;
1141
+
1142
+ while (Date.now() < deadlineMs) {
1143
+ const { response, result, text } = await fetchJson(resultUrl, {
1144
+ headers: authHeaders()
1145
+ });
1146
+ if (response.ok) return enrichImageUrls(result);
1147
+ lastResult = result;
1148
+ if (result?.error?.code !== 'request_in_progress' || result?.error?.retryable !== true) {
1149
+ throw new Error(readErrorMessage(result) || `job result 请求失败,状态码 ${response.status}:${text}`);
1150
+ }
1151
+ await sleep(readJobRetryAfterSeconds(result, job));
1152
+ }
1153
+
1154
+ throw new Error(readErrorMessage(lastResult) || '等待 job result 超时。');
1155
+ }
1156
+
1157
+ function readJobRetryAfterSeconds(result, job) {
1158
+ const value = result?.retry_after ?? result?.error?.retry_after_seconds ?? job?.retry_after_seconds;
1159
+ return readRetryAfterSeconds(value);
1160
+ }
1161
+
1162
+ function readRetryAfterSeconds(value) {
1163
+ if (typeof value === 'number' && Number.isFinite(value) && value >= 0) return Math.min(value, 30);
1164
+ if (typeof value === 'string' && /^\d+(\.\d+)?$/.test(value)) return Math.min(Number(value), 30);
1165
+ return 1;
1166
+ }
1167
+
1168
+ async function sleep(seconds) {
1169
+ await new Promise((resolve) => setTimeout(resolve, Math.max(0, seconds) * 1000));
1170
  }
1171
 
1172
  async function postEditTask(task) {
 
1284
  ? normalizeOutputFormat(raw.output_format ?? raw.format)
1285
  : DEFAULT_OUTPUT_FORMAT;
1286
  const outputCompression = readOutputCompression(raw, String(raw.id || 'generate'));
1287
+ const responsesModel = readResponsesModel(raw, String(raw.id || 'generate'));
1288
+ const thinking = hasOwn(raw, 'thinking') ? normalizeEnumValue(raw.thinking, THINKING_VALUES, 'thinking') : undefined;
1289
+ const promptOptimization = readPromptOptimization(raw, String(raw.id || 'generate'));
1290
+ const forceWeb = readForceWeb(raw, String(raw.id || 'generate'));
1291
  const body = {
1292
  prompt: raw.prompt,
1293
  model: raw.model || 'gpt-image-2',
 
1300
  ...(raw.background ? { background: raw.background } : {}),
1301
  ...(raw.moderation ? { moderation: raw.moderation } : {}),
1302
  ...(hasOwn(raw, 'image_backend') ? { image_backend: normalizeEnumValue(raw.image_backend, IMAGE_BACKENDS, 'image_backend') } : {}),
1303
+ ...(responsesModel ? { responsesModel } : {}),
1304
+ ...(thinking ? { thinking } : {}),
1305
+ ...(promptOptimization !== undefined ? { promptOptimization } : {}),
1306
+ ...(forceWeb !== undefined ? { force_web: forceWeb } : {}),
1307
  ...(hasOwn(raw, 'stream_mode') ? { stream_mode: normalizeEnumValue(raw.stream_mode, STREAM_MODES, 'stream_mode') } : {}),
1308
  ...(hasOwn(raw, 'streaming_strategy')
1309
  ? { streaming_strategy: normalizeEnumValue(raw.streaming_strategy, STREAMING_STRATEGIES, 'streaming_strategy') }
skills/gpt-image-playground-agent/scripts/generate-image.mjs CHANGED
@@ -213,7 +213,7 @@ try {
213
  JSON.stringify(
214
  buildSuccessOutput(formatPageSseOutput(result), {
215
  ...buildPageSseSummaryRouting(),
216
- fallback_endpoint: AGENT_ENDPOINTS.generate,
217
  fallback_mode: 'manual_after_diagnosis'
218
  }, completeScriptTiming(scriptTiming)),
219
  null,
@@ -412,19 +412,10 @@ function validateUpstreamStrategyOptions(parsed) {
412
  if (parsed.routeMode === 'page_sse' && (parsed.streamingStrategy === 'off' || parsed.streamMode === 'non_stream')) {
413
  throw new Error('stream_mode=non_stream 或 streaming_strategy=off 时不能强制使用页面 SSE。');
414
  }
415
- if (hasPageOnlyGenerateOptions(parsed) && !isPageSseAllowed(parsed)) {
416
- throw new Error('文生图高级页面参数需要页面 SSE,不能同时设置 stream_mode=non_stream 或 streaming_strategy=off。');
417
- }
418
- if (parsed.routeMode === 'agent') {
419
- assertNoPageOnlyGenerateOptions(parsed, 'Agent generate');
420
- }
421
  if (!OUTPUT_FORMATS.has(normalizeOutputFormat(parsed.format))) {
422
  throw new Error('--format 必须是 png、jpeg 或 webp。');
423
  }
424
  if (parsed.outputCompression !== undefined) readOutputCompression(parsed);
425
- if (parsed.routeMode === 'job') {
426
- assertNoPageOnlyGenerateOptions(parsed, 'Agent generate job');
427
- }
428
  }
429
 
430
  function validateResponsesModelBackend(parsed) {
@@ -449,21 +440,6 @@ function readOutputCompression(parsed) {
449
  return parsedValue;
450
  }
451
 
452
- function hasPageOnlyGenerateOptions(value) {
453
- return Boolean(
454
- value.responsesModel ||
455
- value.thinking ||
456
- value.promptOptimization !== undefined ||
457
- value.forceWeb !== undefined ||
458
- value.force_web !== undefined
459
- );
460
- }
461
-
462
- function assertNoPageOnlyGenerateOptions(parsed, context) {
463
- if (!hasPageOnlyGenerateOptions(parsed)) return;
464
- throw new Error(`${context} 不接受文生图高级页面字段;请去掉这些字段或使用 --page-sse。`);
465
- }
466
-
467
  function readBooleanOption(value, name) {
468
  if (value === true || value === 'true') return true;
469
  if (value === false || value === 'false') return false;
@@ -582,9 +558,7 @@ function dryRunEndpoint(body, routeMode) {
582
  if (routeMode === 'job') return `${baseUrl}${AGENT_ENDPOINTS.create_generate_job}`;
583
  if (routeMode === 'agent') return `${baseUrl}${AGENT_ENDPOINTS.generate}`;
584
  if (routeMode === 'page_sse') return `${baseUrl}${PAGE_SSE_ENDPOINT}`;
585
- return (hasPageOnlyGenerateOptions(body) || isLargeGenerate(body)) && isPageSseAllowed(body)
586
- ? `${baseUrl}${PAGE_SSE_ENDPOINT}`
587
- : `${baseUrl}${AGENT_ENDPOINTS.generate}`;
588
  }
589
 
590
  function buildGenerateRoutingGuidance(body, routeMode) {
@@ -608,23 +582,14 @@ function buildGenerateRoutingGuidance(body, routeMode) {
608
  if (routeMode === 'page_sse' && !isPageSseAllowed(body)) {
609
  throw new Error('stream_mode=non_stream 或 streaming_strategy=off 时不能强制使用页面 SSE。');
610
  }
611
- if (
612
- (routeMode === 'page_sse' ||
613
- (routeMode !== 'agent' && (hasPageOnlyGenerateOptions(body) || isLargeGenerate(body)))) &&
614
- isPageSseAllowed(body)
615
- ) {
616
- const reason = routeMode === 'page_sse'
617
- ? 'Explicit --page-sse requests use the page form-data SSE endpoint.'
618
- : hasPageOnlyGenerateOptions(body)
619
- ? 'Responses/GPT2Image-compatible generate options require the page form-data SSE endpoint; Agent JSON generate does not accept those fields.'
620
- : 'Generate requests with page-workbench-only requirements should use the page form-data SSE endpoint; if the stream fails, diagnose first and rerun manually with Agent JSON.';
621
  return {
622
  recommended_endpoint: PAGE_SSE_ENDPOINT,
623
  transport: 'page_sse',
624
  strength: 'recommended',
625
- fallback_endpoint: AGENT_ENDPOINTS.generate,
626
  fallback_mode: 'manual_after_diagnosis',
627
- reason
628
  };
629
  }
630
  return {
@@ -1101,7 +1066,7 @@ function buildPageSseFailureOutput(error, timing = completeScriptTiming(scriptTi
1101
  function buildPageSseRouting(fallbackMode) {
1102
  return {
1103
  ...buildPageSseSummaryRouting(),
1104
- fallback_endpoint: AGENT_ENDPOINTS.generate,
1105
  fallback_mode: fallbackMode
1106
  };
1107
  }
@@ -1127,7 +1092,7 @@ function buildPageSseScriptFailure(error, diagnostics, timing) {
1127
  ...(diagnostics ? { diagnostics } : {})
1128
  },
1129
  routing: buildPageSseRouting('manual_after_diagnosis'),
1130
- next_step: '先补齐页面流式 capability 或��问码哈希,再重新执行;不要静默切换到 Agent JSON。'
1131
  };
1132
  return attachSummary(output, buildFailureSummary({
1133
  errorBody: output,
@@ -1174,7 +1139,7 @@ function buildBillablePageSseFailure(error, diagnostics, timing) {
1174
  },
1175
  routing: buildPageSseRouting('manual_after_diagnosis'),
1176
  next_step:
1177
- '先用 diagnose-request 诊断页面流式失败原因,再用新的 Idempotency-Key 显式选择备用路径;若改用 Agent JSON 对照,必须重新校验输出尺寸和格式。'
1178
  };
1179
  return attachSummary(output, buildFailureSummary({
1180
  errorBody: output,
@@ -1554,19 +1519,15 @@ function shouldUsePageSse(capabilitiesValue, request, routeMode) {
1554
  }
1555
  return false;
1556
  }
1557
- if (routeMode === 'page_sse' || hasPageOnlyGenerateOptions(request) || isLargeGenerate(request)) {
1558
  assertPageSseReady(capabilitiesValue);
1559
  return true;
1560
  }
1561
  return false;
1562
  }
1563
 
1564
- function isLargeGenerate(request) {
1565
- return readMaxImageEdge(request.size) > 2048;
1566
- }
1567
-
1568
  function shouldPreferServerOrchestration(request) {
1569
- return !hasPageOnlyGenerateOptions(request);
1570
  }
1571
 
1572
  function isPageSseAllowed(request) {
 
213
  JSON.stringify(
214
  buildSuccessOutput(formatPageSseOutput(result), {
215
  ...buildPageSseSummaryRouting(),
216
+ fallback_endpoint: AGENT_ENDPOINTS.create_image_request,
217
  fallback_mode: 'manual_after_diagnosis'
218
  }, completeScriptTiming(scriptTiming)),
219
  null,
 
412
  if (parsed.routeMode === 'page_sse' && (parsed.streamingStrategy === 'off' || parsed.streamMode === 'non_stream')) {
413
  throw new Error('stream_mode=non_stream 或 streaming_strategy=off 时不能强制使用页面 SSE。');
414
  }
 
 
 
 
 
 
415
  if (!OUTPUT_FORMATS.has(normalizeOutputFormat(parsed.format))) {
416
  throw new Error('--format 必须是 png、jpeg 或 webp。');
417
  }
418
  if (parsed.outputCompression !== undefined) readOutputCompression(parsed);
 
 
 
419
  }
420
 
421
  function validateResponsesModelBackend(parsed) {
 
440
  return parsedValue;
441
  }
442
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
443
  function readBooleanOption(value, name) {
444
  if (value === true || value === 'true') return true;
445
  if (value === false || value === 'false') return false;
 
558
  if (routeMode === 'job') return `${baseUrl}${AGENT_ENDPOINTS.create_generate_job}`;
559
  if (routeMode === 'agent') return `${baseUrl}${AGENT_ENDPOINTS.generate}`;
560
  if (routeMode === 'page_sse') return `${baseUrl}${PAGE_SSE_ENDPOINT}`;
561
+ return `${baseUrl}${AGENT_ENDPOINTS.generate}`;
 
 
562
  }
563
 
564
  function buildGenerateRoutingGuidance(body, routeMode) {
 
582
  if (routeMode === 'page_sse' && !isPageSseAllowed(body)) {
583
  throw new Error('stream_mode=non_stream 或 streaming_strategy=off 时不能强制使用页面 SSE。');
584
  }
585
+ if (routeMode === 'page_sse' && isPageSseAllowed(body)) {
 
 
 
 
 
 
 
 
 
586
  return {
587
  recommended_endpoint: PAGE_SSE_ENDPOINT,
588
  transport: 'page_sse',
589
  strength: 'recommended',
590
+ fallback_endpoint: AGENT_ENDPOINTS.create_image_request,
591
  fallback_mode: 'manual_after_diagnosis',
592
+ reason: 'Explicit --page-sse requests use the page form-data SSE endpoint.'
593
  };
594
  }
595
  return {
 
1066
  function buildPageSseRouting(fallbackMode) {
1067
  return {
1068
  ...buildPageSseSummaryRouting(),
1069
+ fallback_endpoint: AGENT_ENDPOINTS.create_image_request,
1070
  fallback_mode: fallbackMode
1071
  };
1072
  }
 
1092
  ...(diagnostics ? { diagnostics } : {})
1093
  },
1094
  routing: buildPageSseRouting('manual_after_diagnosis'),
1095
+ next_step: '先补齐页面流式 capability 或访问码哈希,再重新执行;不要静默切换到其他端点。'
1096
  };
1097
  return attachSummary(output, buildFailureSummary({
1098
  errorBody: output,
 
1139
  },
1140
  routing: buildPageSseRouting('manual_after_diagnosis'),
1141
  next_step:
1142
+ '先用 diagnose-request 诊断页面流式失败原因,再用新的 Idempotency-Key 显式选择服务端编排入口或其他诊断路径。'
1143
  };
1144
  return attachSummary(output, buildFailureSummary({
1145
  errorBody: output,
 
1519
  }
1520
  return false;
1521
  }
1522
+ if (routeMode === 'page_sse') {
1523
  assertPageSseReady(capabilitiesValue);
1524
  return true;
1525
  }
1526
  return false;
1527
  }
1528
 
 
 
 
 
1529
  function shouldPreferServerOrchestration(request) {
1530
+ return true;
1531
  }
1532
 
1533
  function isPageSseAllowed(request) {
src/app/api/deploy-marker/route.ts CHANGED
@@ -1,6 +1,6 @@
1
  import { NextResponse } from 'next/server';
2
 
3
- const deployMarker = {"schema_version":1,"local_sha":"07209608cdbddc4207aa853b7eb6ea60db1f1827","created_at":"2026-06-26T01:51:20.764Z","deploy_id":"fce636f2-7a61-4ff6-8f38-d69ac5edeb35"} as const;
4
 
5
  export const dynamic = 'force-dynamic';
6
 
 
1
  import { NextResponse } from 'next/server';
2
 
3
+ const deployMarker = {"schema_version":1,"local_sha":"34af419d1e35b93f1874ba494453e0fab7ea54fe","created_at":"2026-06-26T04:13:04.694Z","deploy_id":"cff00c2a-9ee2-45f8-bca2-6a8465a84274"} as const;
4
 
5
  export const dynamic = 'force-dynamic';
6
 
src/lib/agent-api-contracts.test.ts CHANGED
@@ -74,6 +74,39 @@ describe('validateAgentGenerateRequest', () => {
74
  );
75
  });
76
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  it('rejects Responses backend partial image counts outside the backend contract', () => {
78
  assert.throws(
79
  () =>
@@ -489,13 +522,17 @@ describe('buildAgentCapabilities', () => {
489
  requires_new_idempotency_key_on_retry: true,
490
  no_automatic_fallback: true
491
  });
492
- assert.equal(capabilities.routing_rules.page_sse_large_generate.endpoint, '/api/images');
493
- assert.equal(capabilities.routing_rules.page_sse_large_generate.transport, 'page_sse');
494
- assert.equal(capabilities.routing_rules.page_sse_large_generate.strength, 'explicit');
495
- assert.equal(capabilities.routing_rules.page_sse_large_generate.action.strength, 'explicit');
496
- assert.deepEqual(capabilities.routing_rules.page_sse_large_generate.conditions, {
 
 
 
 
497
  operation: 'generate',
498
- max_edge: { operator: 'gt', value: 2048 },
499
  single_request: true
500
  });
501
  assert.equal(capabilities.routing_rules.retry_recovery.reuse_failed_idempotency_key, false);
@@ -927,6 +964,10 @@ describe('buildAgentCapabilities', () => {
927
  'force-sse'
928
  ]);
929
  assert.deepEqual(generateProperties.stream_mode.enum, ['auto', 'stream', 'non_stream']);
 
 
 
 
930
  assert.deepEqual(generateProperties.n, { type: 'integer', minimum: 1, maximum: 10 });
931
  assert.deepEqual(generateProperties.partial_images, { type: 'integer', minimum: 1, maximum: 3, default: 2 });
932
  assert.deepEqual(document.components.schemas.GenerateRequest.allOf[0].then.properties.partial_images, {
@@ -1051,6 +1092,7 @@ describe('buildAgentCapabilities', () => {
1051
  'gt',
1052
  'lte'
1053
  ]);
 
1054
  assert.equal(
1055
  document.components.schemas.AgentRoutingAction.properties.requires_new_idempotency_key_on_retry.type,
1056
  'boolean'
 
74
  );
75
  });
76
 
77
+ it('accepts Agent generate response controls as service-owned intent fields', () => {
78
+ assert.deepEqual(
79
+ validateAgentGenerateRequest({
80
+ prompt: 'draw a stable generate request',
81
+ image_backend: 'responses-image-generation',
82
+ responsesModel: 'gpt-5.4-mini',
83
+ thinking: 'medium',
84
+ promptOptimization: false,
85
+ force_web: true
86
+ }),
87
+ {
88
+ model: 'gpt-image-2',
89
+ prompt: 'draw a stable generate request',
90
+ n: 1,
91
+ size: '1024x1024',
92
+ quality: 'high',
93
+ output_format: 'webp',
94
+ output_compression: 100,
95
+ background: 'auto',
96
+ moderation: 'auto',
97
+ response_mode: 'path',
98
+ image_backend: 'responses-image-generation',
99
+ stream_mode: 'auto',
100
+ streaming_strategy: 'auto',
101
+ partial_images: 2,
102
+ responsesModel: 'gpt-5.4-mini',
103
+ thinking: 'medium',
104
+ promptOptimization: false,
105
+ force_web: true
106
+ }
107
+ );
108
+ });
109
+
110
  it('rejects Responses backend partial image counts outside the backend contract', () => {
111
  assert.throws(
112
  () =>
 
522
  requires_new_idempotency_key_on_retry: true,
523
  no_automatic_fallback: true
524
  });
525
+ assert.equal(capabilities.routing_rules.page_sse_generate_diagnostics.endpoint, '/api/images');
526
+ assert.equal(capabilities.routing_rules.page_sse_generate_diagnostics.transport, 'page_sse');
527
+ assert.equal(capabilities.routing_rules.page_sse_generate_diagnostics.strength, 'explicit');
528
+ assert.equal(capabilities.routing_rules.page_sse_generate_diagnostics.action.strength, 'explicit');
529
+ assert.equal(
530
+ capabilities.routing_rules.page_sse_generate_diagnostics.action.fallback_endpoint,
531
+ AGENT_ENDPOINTS.create_image_request
532
+ );
533
+ assert.deepEqual(capabilities.routing_rules.page_sse_generate_diagnostics.conditions, {
534
  operation: 'generate',
535
+ explicit_page_sse: true,
536
  single_request: true
537
  });
538
  assert.equal(capabilities.routing_rules.retry_recovery.reuse_failed_idempotency_key, false);
 
964
  'force-sse'
965
  ]);
966
  assert.deepEqual(generateProperties.stream_mode.enum, ['auto', 'stream', 'non_stream']);
967
+ assert.deepEqual(generateProperties.thinking.enum, ['minimal', 'none', 'low', 'medium', 'high', 'xhigh']);
968
+ assert.equal(generateProperties.responsesModel.maxLength, 128);
969
+ assert.equal(generateProperties.promptOptimization.type, 'boolean');
970
+ assert.equal(generateProperties.force_web.type, 'boolean');
971
  assert.deepEqual(generateProperties.n, { type: 'integer', minimum: 1, maximum: 10 });
972
  assert.deepEqual(generateProperties.partial_images, { type: 'integer', minimum: 1, maximum: 3, default: 2 });
973
  assert.deepEqual(document.components.schemas.GenerateRequest.allOf[0].then.properties.partial_images, {
 
1092
  'gt',
1093
  'lte'
1094
  ]);
1095
+ assert.equal(document.components.schemas.AgentRoutingCondition.properties.explicit_page_sse.type, 'boolean');
1096
  assert.equal(
1097
  document.components.schemas.AgentRoutingAction.properties.requires_new_idempotency_key_on_retry.type,
1098
  'boolean'
src/lib/agent-api-contracts.ts CHANGED
@@ -98,7 +98,7 @@ export const AGENT_UPSTREAM_SSE_EDIT_REQUEST_FIELDS = [
98
  'partial_images'
99
  ] as const;
100
  export const AGENT_PAGE_SSE_AGENT_USAGE =
101
- 'explicit_for_generate_recommended_for_page_only_fields_default_webp_edit_high_resolution_edit_long_image_recovery_and_complex_batch' as const;
102
  const AGENT_DEFAULT_PARTIAL_IMAGES = 2;
103
 
104
  export type AgentStateBackend = 'memory' | 'sqlite' | 'postgres';
@@ -152,6 +152,10 @@ export type AgentGenerateRequest = {
152
  stream_mode: ImageStreamMode;
153
  streaming_strategy: ImageStreamingStrategy;
154
  partial_images: 0 | 1 | 2 | 3 | 4;
 
 
 
 
155
  };
156
 
157
  export type AgentImageResponseItem = {
@@ -358,7 +362,7 @@ export type AgentCapabilities = {
358
  complex_ui_batch: AgentRoutingRule;
359
  long_image_recovery: AgentRoutingRule;
360
  agent_generate_small_smoke: AgentRoutingRule;
361
- page_sse_large_generate: AgentRoutingRule;
362
  retry_recovery: {
363
  reuse_failed_idempotency_key: false;
364
  new_attempt_guidance: string;
@@ -437,6 +441,7 @@ export type AgentRoutingRule = {
437
 
438
  export type AgentRoutingCondition = {
439
  operation: 'generate' | 'edit' | 'generate_or_edit';
 
440
  max_edge?: {
441
  operator: 'gt' | 'lte';
442
  value: number;
@@ -669,6 +674,60 @@ function readModeration(body: Record<string, unknown>, fields: FieldErrors): Age
669
  return value;
670
  }
671
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
672
  function readOutputCompression(
673
  body: Record<string, unknown>,
674
  outputFormat: ValidOutputFormat,
@@ -712,6 +771,10 @@ export function validateAgentGenerateRequest(body: unknown): AgentGenerateReques
712
  const streamMode = readAgentStreamMode(objectBody, fields);
713
  const streamingStrategy = readAgentStreamingStrategy(objectBody, fields);
714
  const partialImages = readPartialImagesForBackend(objectBody, imageBackend, fields, upstreamLimits.profile);
 
 
 
 
715
  validateAgentImageUpstreamStrategy({ imageBackend, streamMode, streamingStrategy, fields });
716
 
717
  if (Object.keys(fields).length > 0) {
@@ -732,7 +795,11 @@ export function validateAgentGenerateRequest(body: unknown): AgentGenerateReques
732
  image_backend: imageBackend,
733
  stream_mode: streamMode,
734
  streaming_strategy: streamingStrategy,
735
- partial_images: partialImages
 
 
 
 
736
  };
737
  }
738
 
@@ -1113,11 +1180,11 @@ export function buildAgentCapabilities(env: Record<string, string | undefined>):
1113
  },
1114
  reason: 'Agent JSON generate remains available for compatibility and explicit diagnostics; ordinary generate clients should use orchestration.endpoint.'
1115
  },
1116
- page_sse_large_generate: {
1117
- when: ['operation=generate', 'max_edge>2048', 'single_request=true'],
1118
  conditions: {
1119
  operation: 'generate',
1120
- max_edge: { operator: 'gt', value: 2048 },
1121
  single_request: true
1122
  },
1123
  endpoint: '/api/images',
@@ -1127,12 +1194,12 @@ export function buildAgentCapabilities(env: Record<string, string | undefined>):
1127
  endpoint: '/api/images',
1128
  transport: 'page_sse',
1129
  strength: 'explicit',
1130
- fallback_endpoint: AGENT_ENDPOINTS.generate,
1131
  fallback_mode: 'manual_after_diagnosis',
1132
  requires_new_idempotency_key_on_retry: true,
1133
  no_automatic_fallback: true
1134
  },
1135
- reason: 'Page form-data SSE is available for explicit page-workbench or diagnostic large generate runs; ordinary generate clients should use orchestration.endpoint.'
1136
  },
1137
  retry_recovery: {
1138
  reuse_failed_idempotency_key: false,
 
98
  'partial_images'
99
  ] as const;
100
  export const AGENT_PAGE_SSE_AGENT_USAGE =
101
+ 'explicit_diagnostics_for_generate_recommended_for_default_webp_edit_high_resolution_edit_long_image_recovery_and_complex_batch' as const;
102
  const AGENT_DEFAULT_PARTIAL_IMAGES = 2;
103
 
104
  export type AgentStateBackend = 'memory' | 'sqlite' | 'postgres';
 
152
  stream_mode: ImageStreamMode;
153
  streaming_strategy: ImageStreamingStrategy;
154
  partial_images: 0 | 1 | 2 | 3 | 4;
155
+ responsesModel?: string;
156
+ thinking?: 'minimal' | 'none' | 'low' | 'medium' | 'high' | 'xhigh';
157
+ promptOptimization?: boolean;
158
+ force_web?: boolean;
159
  };
160
 
161
  export type AgentImageResponseItem = {
 
362
  complex_ui_batch: AgentRoutingRule;
363
  long_image_recovery: AgentRoutingRule;
364
  agent_generate_small_smoke: AgentRoutingRule;
365
+ page_sse_generate_diagnostics: AgentRoutingRule;
366
  retry_recovery: {
367
  reuse_failed_idempotency_key: false;
368
  new_attempt_guidance: string;
 
441
 
442
  export type AgentRoutingCondition = {
443
  operation: 'generate' | 'edit' | 'generate_or_edit';
444
+ explicit_page_sse?: boolean;
445
  max_edge?: {
446
  operator: 'gt' | 'lte';
447
  value: number;
 
674
  return value;
675
  }
676
 
677
+ function readOptionalStringField(
678
+ body: Record<string, unknown>,
679
+ field: string,
680
+ fields: FieldErrors,
681
+ options: { maxLength?: number } = {}
682
+ ): string | undefined {
683
+ const value = body[field];
684
+ if (value === undefined || value === null || value === '') return undefined;
685
+ if (typeof value !== 'string') {
686
+ fields[field] = '必须是字符串';
687
+ return undefined;
688
+ }
689
+ const trimmed = value.trim();
690
+ if (!trimmed) {
691
+ fields[field] = '必须是非空字符串';
692
+ return undefined;
693
+ }
694
+ if (options.maxLength !== undefined && trimmed.length > options.maxLength) {
695
+ fields[field] = `长度不能超过 ${options.maxLength} 个字符`;
696
+ return undefined;
697
+ }
698
+ return trimmed;
699
+ }
700
+
701
+ function readOptionalBooleanField(body: Record<string, unknown>, field: string, fields: FieldErrors): boolean | undefined {
702
+ const value = body[field];
703
+ if (value === undefined || value === null || value === '') return undefined;
704
+ if (typeof value === 'boolean') return value;
705
+ fields[field] = '必须是布尔值';
706
+ return undefined;
707
+ }
708
+
709
+ function readAgentResponsesModel(
710
+ body: Record<string, unknown>,
711
+ imageBackend: ImageGenerationBackend,
712
+ fields: FieldErrors
713
+ ): string | undefined {
714
+ const value = readOptionalStringField(body, 'responsesModel', fields, { maxLength: 128 });
715
+ if (value && imageBackend !== 'responses-image-generation') {
716
+ fields.responsesModel = '仅适用于 image_backend=responses-image-generation';
717
+ }
718
+ return value;
719
+ }
720
+
721
+ function readAgentThinking(body: Record<string, unknown>, fields: FieldErrors): AgentGenerateRequest['thinking'] {
722
+ const value = readOptionalStringField(body, 'thinking', fields);
723
+ if (value === undefined) return undefined;
724
+ if (['minimal', 'none', 'low', 'medium', 'high', 'xhigh'].includes(value)) {
725
+ return value as AgentGenerateRequest['thinking'];
726
+ }
727
+ fields.thinking = '必须是 minimal、none、low、medium、high 或 xhigh';
728
+ return undefined;
729
+ }
730
+
731
  function readOutputCompression(
732
  body: Record<string, unknown>,
733
  outputFormat: ValidOutputFormat,
 
771
  const streamMode = readAgentStreamMode(objectBody, fields);
772
  const streamingStrategy = readAgentStreamingStrategy(objectBody, fields);
773
  const partialImages = readPartialImagesForBackend(objectBody, imageBackend, fields, upstreamLimits.profile);
774
+ const responsesModel = readAgentResponsesModel(objectBody, imageBackend, fields);
775
+ const thinking = readAgentThinking(objectBody, fields);
776
+ const promptOptimization = readOptionalBooleanField(objectBody, 'promptOptimization', fields);
777
+ const forceWeb = readOptionalBooleanField(objectBody, 'force_web', fields);
778
  validateAgentImageUpstreamStrategy({ imageBackend, streamMode, streamingStrategy, fields });
779
 
780
  if (Object.keys(fields).length > 0) {
 
795
  image_backend: imageBackend,
796
  stream_mode: streamMode,
797
  streaming_strategy: streamingStrategy,
798
+ partial_images: partialImages,
799
+ ...(responsesModel ? { responsesModel } : {}),
800
+ ...(thinking ? { thinking } : {}),
801
+ ...(promptOptimization !== undefined ? { promptOptimization } : {}),
802
+ ...(forceWeb !== undefined ? { force_web: forceWeb } : {})
803
  };
804
  }
805
 
 
1180
  },
1181
  reason: 'Agent JSON generate remains available for compatibility and explicit diagnostics; ordinary generate clients should use orchestration.endpoint.'
1182
  },
1183
+ page_sse_generate_diagnostics: {
1184
+ when: ['operation=generate', 'explicit_page_sse=true'],
1185
  conditions: {
1186
  operation: 'generate',
1187
+ explicit_page_sse: true,
1188
  single_request: true
1189
  },
1190
  endpoint: '/api/images',
 
1194
  endpoint: '/api/images',
1195
  transport: 'page_sse',
1196
  strength: 'explicit',
1197
+ fallback_endpoint: AGENT_ENDPOINTS.create_image_request,
1198
  fallback_mode: 'manual_after_diagnosis',
1199
  requires_new_idempotency_key_on_retry: true,
1200
  no_automatic_fallback: true
1201
  },
1202
+ reason: 'Page form-data SSE is available for explicit page-workbench or diagnostic generate runs; ordinary generate clients should use orchestration.endpoint.'
1203
  },
1204
  retry_recovery: {
1205
  reuse_failed_idempotency_key: false,
src/lib/agent-image-service.ts CHANGED
@@ -468,7 +468,8 @@ async function executeAgentGenerateUpstream(
468
  output_format: request.output_format,
469
  background: request.background as OpenAI.Images.ImageGenerateParams['background'],
470
  moderation: request.moderation as OpenAI.Images.ImageGenerateParams['moderation'],
471
- ...(request.output_compression !== undefined ? { output_compression: request.output_compression } : {})
 
472
  };
473
  if (!shouldUseAgentUpstreamStream(streamOptions)) {
474
  return openai.images.generate(
@@ -557,7 +558,7 @@ async function executeAgentResponsesGenerate(
557
  const input: ResponsesImageGenerateInput = {
558
  responses: openai.responses,
559
  prompt: request.prompt,
560
- responsesModel: readAgentResponsesApiModel(),
561
  imageModel: request.model,
562
  size: readAgentResponsesImageSize(request.size),
563
  quality: request.quality,
@@ -565,7 +566,9 @@ async function executeAgentResponsesGenerate(
565
  background: request.background,
566
  moderation: request.moderation,
567
  abortSignal,
568
- ...(request.output_compression !== undefined ? { outputCompression: request.output_compression } : {})
 
 
569
  };
570
  const streamOptions: AgentStreamOptions = {
571
  mode: 'generate',
@@ -660,14 +663,17 @@ function isAbortLikeError(error: unknown, abortSignal?: AbortSignal): boolean {
660
  return name === 'AbortError' || name === 'CanceledError';
661
  }
662
 
663
- function readAgentResponsesApiModel(): string {
664
- const model = process.env.OPENAI_RESPONSES_API_MODEL?.trim();
665
  if (!model) {
666
  throw new RequestValidationError(
667
- 'Responses API 图片后端必须配置 OPENAI_RESPONSES_API_MODEL,作为 /responses 顶层模型。',
668
  500
669
  );
670
  }
 
 
 
671
  return model;
672
  }
673
 
 
468
  output_format: request.output_format,
469
  background: request.background as OpenAI.Images.ImageGenerateParams['background'],
470
  moderation: request.moderation as OpenAI.Images.ImageGenerateParams['moderation'],
471
+ ...(request.output_compression !== undefined ? { output_compression: request.output_compression } : {}),
472
+ ...(request.force_web !== undefined ? { force_web: request.force_web } : {})
473
  };
474
  if (!shouldUseAgentUpstreamStream(streamOptions)) {
475
  return openai.images.generate(
 
558
  const input: ResponsesImageGenerateInput = {
559
  responses: openai.responses,
560
  prompt: request.prompt,
561
+ responsesModel: readAgentResponsesApiModel(request.responsesModel),
562
  imageModel: request.model,
563
  size: readAgentResponsesImageSize(request.size),
564
  quality: request.quality,
 
566
  background: request.background,
567
  moderation: request.moderation,
568
  abortSignal,
569
+ ...(request.output_compression !== undefined ? { outputCompression: request.output_compression } : {}),
570
+ ...(request.promptOptimization !== undefined ? { promptOptimization: request.promptOptimization } : {}),
571
+ ...(request.thinking ? { thinking: request.thinking } : {})
572
  };
573
  const streamOptions: AgentStreamOptions = {
574
  mode: 'generate',
 
663
  return name === 'AbortError' || name === 'CanceledError';
664
  }
665
 
666
+ function readAgentResponsesApiModel(requestModel?: string): string {
667
+ const model = requestModel?.trim() || process.env.OPENAI_RESPONSES_API_MODEL?.trim();
668
  if (!model) {
669
  throw new RequestValidationError(
670
+ 'Responses API 图片后端必须配置 OPENAI_RESPONSES_API_MODEL 或请求字段 responsesModel,作为 /responses 顶层模型。',
671
  500
672
  );
673
  }
674
+ if (model.length > 128) {
675
+ throw new RequestValidationError('Responses API 顶层模型名称不能超过 128 个字符。', 400);
676
+ }
677
  return model;
678
  }
679
 
src/lib/agent-openapi.ts CHANGED
@@ -1098,7 +1098,7 @@ export function buildAgentOpenApiDocument(env: Record<string, string | undefined
1098
  'complex_ui_batch',
1099
  'long_image_recovery',
1100
  'agent_generate_small_smoke',
1101
- 'page_sse_large_generate',
1102
  'retry_recovery'
1103
  ],
1104
  properties: {
@@ -1106,7 +1106,7 @@ export function buildAgentOpenApiDocument(env: Record<string, string | undefined
1106
  complex_ui_batch: { $ref: '#/components/schemas/AgentRoutingRule' },
1107
  long_image_recovery: { $ref: '#/components/schemas/AgentRoutingRule' },
1108
  agent_generate_small_smoke: { $ref: '#/components/schemas/AgentRoutingRule' },
1109
- page_sse_large_generate: { $ref: '#/components/schemas/AgentRoutingRule' },
1110
  retry_recovery: {
1111
  type: 'object',
1112
  required: ['reuse_failed_idempotency_key', 'new_attempt_guidance'],
@@ -1154,6 +1154,7 @@ export function buildAgentOpenApiDocument(env: Record<string, string | undefined
1154
  },
1155
  batch: { type: 'boolean' },
1156
  single_request: { type: 'boolean' },
 
1157
  complex_ui: { type: 'boolean' },
1158
  long_image: { type: 'boolean' },
1159
  resume_or_recover: { type: 'boolean' }
@@ -1305,6 +1306,17 @@ export function buildAgentOpenApiDocument(env: Record<string, string | undefined
1305
  enum: AGENT_IMAGE_BACKENDS,
1306
  default: 'images-api'
1307
  },
 
 
 
 
 
 
 
 
 
 
 
1308
  streaming_strategy: {
1309
  type: 'string',
1310
  enum: AGENT_STREAMING_STRATEGIES,
 
1098
  'complex_ui_batch',
1099
  'long_image_recovery',
1100
  'agent_generate_small_smoke',
1101
+ 'page_sse_generate_diagnostics',
1102
  'retry_recovery'
1103
  ],
1104
  properties: {
 
1106
  complex_ui_batch: { $ref: '#/components/schemas/AgentRoutingRule' },
1107
  long_image_recovery: { $ref: '#/components/schemas/AgentRoutingRule' },
1108
  agent_generate_small_smoke: { $ref: '#/components/schemas/AgentRoutingRule' },
1109
+ page_sse_generate_diagnostics: { $ref: '#/components/schemas/AgentRoutingRule' },
1110
  retry_recovery: {
1111
  type: 'object',
1112
  required: ['reuse_failed_idempotency_key', 'new_attempt_guidance'],
 
1154
  },
1155
  batch: { type: 'boolean' },
1156
  single_request: { type: 'boolean' },
1157
+ explicit_page_sse: { type: 'boolean' },
1158
  complex_ui: { type: 'boolean' },
1159
  long_image: { type: 'boolean' },
1160
  resume_or_recover: { type: 'boolean' }
 
1306
  enum: AGENT_IMAGE_BACKENDS,
1307
  default: 'images-api'
1308
  },
1309
+ responsesModel: {
1310
+ type: 'string',
1311
+ maxLength: 128,
1312
+ description: 'Responses image_generation 顶层模型;仅适用于 image_backend=responses-image-generation。'
1313
+ },
1314
+ thinking: {
1315
+ type: 'string',
1316
+ enum: ['minimal', 'none', 'low', 'medium', 'high', 'xhigh']
1317
+ },
1318
+ promptOptimization: { type: 'boolean' },
1319
+ force_web: { type: 'boolean' },
1320
  streaming_strategy: {
1321
  type: 'string',
1322
  enum: AGENT_STREAMING_STRATEGIES,