misonL commited on
Commit
77bcff5
·
verified ·
1 Parent(s): f9a28cd

Deploy 7af3d19 to Docker Space

Browse files

Source: MisonL/gpt-image-playground-customer@7af3d19e476e03dfd25eef49672c982b46c64be8

README.md CHANGED
@@ -279,7 +279,7 @@ node skills/gpt-image-playground-agent/scripts/diagnose-request.mjs \
279
  - 远程 Space、云服务或内网服务必须显式传 `--base-url`,避免误用本机默认服务。
280
  - Agent CLI 默认读取当前仓库根目录的 `.env.agent.local`;shell 环境变量优先。首次配置可复制 `.env.agent.local.example`。不要提交 token、访问码或哈希。
281
  - `GPT_IMAGE_AGENT_TOKEN` 只用于 `/api/agent/*`;页面 SSE `/api/images` 可能还需要 `GPT_IMAGE_APP_PASSWORD_HASH`。
282
- - dry-run 只验证本地请求构造;`verification_scope.mode=local_planning_only` 不是远端已可执行。远端合同检查用 `--contract-check`,真实执行必须加 `--allow-billable`。
283
  - 多张真实任务优先用 `batch-images.mjs`、`--manifest`、`--resume` 和 `--dimension-check`;不要手动并行启动多个单张脚本。需要并发时先看 `/api/runtime-capabilities` 的 `streamingBatch.recommendedConcurrency` 和 `channelQueue.capacityPerCredential`。
284
  - 选择 `responses-image-generation` 或兼容别名 `responses` 时,`partial_images` 必须优先按 `partial_images_by_backend["responses-image-generation"]` 校验,不能套用 Matsca Images API 的范围。
285
  - 页面 SSE 返回 503 或断流时,先用诊断脚本读取结构化摘要,再用新的幂等键显式选择备用路径。`page_sse_supported=true` 只是声明支持,不代表实测一定成功。Agent edit 输出格式和尺寸可能与页面 SSE 不完全一致,尺寸敏感任务必须重新校验或用 `--dimension-check`。
 
279
  - 远程 Space、云服务或内网服务必须显式传 `--base-url`,避免误用本机默认服务。
280
  - Agent CLI 默认读取当前仓库根目录的 `.env.agent.local`;shell 环境变量优先。首次配置可复制 `.env.agent.local.example`。不要提交 token、访问码或哈希。
281
  - `GPT_IMAGE_AGENT_TOKEN` 只用于 `/api/agent/*`;页面 SSE `/api/images` 可能还需要 `GPT_IMAGE_APP_PASSWORD_HASH`。
282
+ - dry-run 只验证本地请求构造;`verification_scope.mode=local_planning_only` 不是远端已可执行。需要只读读取远端 capabilities 和 runtime 时加 `--check-remote`,输出 `verification_scope.mode=remote_contract_and_local_planning`。远端合同检查用 `--contract-check`,真实执行必须加 `--allow-billable`。
283
  - 多张真实任务优先用 `batch-images.mjs`、`--manifest`、`--resume` 和 `--dimension-check`;不要手动并行启动多个单张脚本。需要并发时先看 `/api/runtime-capabilities` 的 `streamingBatch.recommendedConcurrency` 和 `channelQueue.capacityPerCredential`。
284
  - 选择 `responses-image-generation` 或兼容别名 `responses` 时,`partial_images` 必须优先按 `partial_images_by_backend["responses-image-generation"]` 校验,不能套用 Matsca Images API 的范围。
285
  - 页面 SSE 返回 503 或断流时,先用诊断脚本读取结构化摘要,再用新的幂等键显式选择备用路径。`page_sse_supported=true` 只是声明支持,不代表实测一定成功。Agent edit 输出格式和尺寸可能与页面 SSE 不完全一致,尺寸敏感任务必须重新校验或用 `--dimension-check`。
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 的 `routing_rules`、`agent_streaming``agent_jobs` 判断默认路径:普通小图走 Agent JSON,`max_edge>2048` 的单次文生图和高分辨率 edit 默认优先走页面端 `/api/images` SSE;页面流式失败或不可用时,先诊断结构化错误,再显式回退到 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 或非流式路径。页面高级字段、默认 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": "34544ccd35f4321f92ab73ef5ac36603ee9f7720",
4
- "created_at": "2026-06-24T14:38:59.964Z",
5
- "deploy_id": "dee10b1a-4b9c-47bb-a7f3-89875a64183f"
6
  }
 
1
  {
2
  "schema_version": 1,
3
+ "local_sha": "7af3d19e476e03dfd25eef49672c982b46c64be8",
4
+ "created_at": "2026-06-26T01:06:33.777Z",
5
+ "deploy_id": "46b8343b-982b-4f3b-8526-ca1feabfd227"
6
  }
scripts/agent-doctor.mjs CHANGED
@@ -391,6 +391,10 @@ function buildSummary({ capabilities, runtime, contract, smoke }) {
391
  runtime: runtime.ok ? runtime.body : undefined,
392
  smoke
393
  }),
 
 
 
 
394
  responses_gpt2image_ready:
395
  capabilities.ok && runtime.ok
396
  ? capabilities.body?.supported?.image_backend_requirements?.['responses-image-generation']?.enabled === true &&
@@ -403,6 +407,26 @@ function buildSummary({ capabilities, runtime, contract, smoke }) {
403
  };
404
  }
405
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
406
  function summarizeSmokeCheck(smoke, name) {
407
  const check = smoke.checks?.find((item) => item.name === name);
408
  if (!check || check.skipped) return 'skipped';
 
391
  runtime: runtime.ok ? runtime.body : undefined,
392
  smoke
393
  }),
394
+ runtime_environment: buildRuntimeEnvironmentSummary({
395
+ capabilities: capabilities.ok ? capabilities.body : undefined,
396
+ runtime: runtime.ok ? runtime.body : undefined
397
+ }),
398
  responses_gpt2image_ready:
399
  capabilities.ok && runtime.ok
400
  ? capabilities.body?.supported?.image_backend_requirements?.['responses-image-generation']?.enabled === true &&
 
407
  };
408
  }
409
 
410
+ function buildRuntimeEnvironmentSummary({ capabilities, runtime }) {
411
+ return {
412
+ state_backend: capabilities?.defaults?.state_backend || 'unknown',
413
+ image_storage_mode: capabilities?.storage?.image_storage_mode || 'unknown',
414
+ postgres_configured: capabilities?.storage?.postgres_configured === true,
415
+ page_sse_auth_required: capabilities?.agent_streaming?.page_sse?.auth?.required === true,
416
+ agent_auth_schemes: Array.isArray(capabilities?.auth?.schemes) ? capabilities.auth.schemes : [],
417
+ orchestration_endpoint: capabilities?.orchestration?.endpoint || null,
418
+ orchestration_transport_selection: capabilities?.orchestration?.transport_selection || null,
419
+ request_mode_control_policy: capabilities?.request_mode_controls?.agent_client_policy || null,
420
+ request_mode_controls: runtime?.channelRouting?.requestModeControls || capabilities?.request_mode_controls || null,
421
+ runtime_strategy: runtime?.channelRouting?.strategy || null,
422
+ effective_request_modes: readRequestModeList(runtime?.channelRouting?.effectiveRequestModes),
423
+ streaming_batch_enabled: runtime?.streamingBatch?.enabled === true,
424
+ recommended_concurrency: runtime?.streamingBatch?.recommendedConcurrency ?? null,
425
+ channel_queue_enabled: runtime?.channelQueue?.enabled === true,
426
+ channel_queue_capacity_per_credential: runtime?.channelQueue?.capacityPerCredential ?? null
427
+ };
428
+ }
429
+
430
  function summarizeSmokeCheck(smoke, name) {
431
  const check = smoke.checks?.find((item) => item.name === name);
432
  if (!check || check.skipped) return 'skipped';
scripts/agent-skill-scripts.test.mjs CHANGED
@@ -618,6 +618,18 @@ describe('Agent skill script argument validation', () => {
618
  assert.equal(result.stderr.trim(), '');
619
  });
620
 
 
 
 
 
 
 
 
 
 
 
 
 
621
  it('prints server orchestration guidance for remote HTTPS small generate dry-runs', () => {
622
  const result = runSkillScript('generate-image.mjs', ['--base-url', 'https://space.example.test', 'prompt']);
623
 
@@ -631,6 +643,70 @@ describe('Agent skill script argument validation', () => {
631
  assert.equal(result.stderr.trim(), '');
632
  });
633
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
634
  it('keeps default generate on server orchestration when request modes are declared', async () => {
635
  const requests = [];
636
  let imageRequestBody = '';
@@ -789,7 +865,19 @@ describe('Agent skill script argument validation', () => {
789
  requests.push({ method: request.method, url: request.url });
790
  if (request.url === '/api/agent/capabilities') {
791
  response.writeHead(200, { 'content-type': 'application/json' });
792
- response.end(JSON.stringify(agentGenerateCapabilities()));
 
 
 
 
 
 
 
 
 
 
 
 
793
  return;
794
  }
795
  if (
@@ -809,6 +897,11 @@ describe('Agent skill script argument validation', () => {
809
  );
810
  return;
811
  }
 
 
 
 
 
812
  response.writeHead(404, { 'content-type': 'application/json' });
813
  response.end(JSON.stringify({ error: 'missing' }));
814
  },
@@ -827,7 +920,8 @@ describe('Agent skill script argument validation', () => {
827
  [
828
  '/api/agent/images/generate',
829
  '/api/agent/image-requests',
830
- '/api/agent/jobs/images/generate'
 
831
  ]
832
  );
833
  assert.deepEqual(
@@ -836,7 +930,8 @@ describe('Agent skill script argument validation', () => {
836
  'GET /api/agent/capabilities',
837
  'POST /api/agent/images/generate',
838
  'POST /api/agent/image-requests',
839
- 'POST /api/agent/jobs/images/generate'
 
840
  ]
841
  );
842
  }
@@ -3618,6 +3713,8 @@ describe('Agent skill script argument validation', () => {
3618
  assert.match(skillText, /Codex 会话日志会持久保存命令输出/);
3619
  assert.match(skillText, /npm run env:summary/);
3620
  assert.match(skillText, /verification_scope\.mode=local_planning_only/);
 
 
3621
  assert.match(skillText, /manifest_written=false/);
3622
  assert.match(skillText, /Hugging Face Space Secrets 只能写入和列出名称/);
3623
 
@@ -3628,6 +3725,8 @@ describe('Agent skill script argument validation', () => {
3628
  assert.match(apiReference, /channel_capacity_queue_aborted/);
3629
  assert.match(apiReference, /npm run env:summary/);
3630
  assert.match(apiReference, /verification_scope\.mode=local_planning_only/);
 
 
3631
  assert.match(apiReference, /manifest_written=false/);
3632
  assert.match(apiReference, /Hugging Face Space Secrets 只能写入和列出名称/);
3633
  assert.match(apiReference, /这个失败表示上游已生成但本地验收未通过/);
@@ -3831,6 +3930,25 @@ describe('Agent skill script argument validation', () => {
3831
  }
3832
  });
3833
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3834
  it('records enriched Agent diagnostics for failed batch tasks and manifests', async () => {
3835
  const tempRoot = mkdtempSync(join(tmpdir(), 'gpt-image-batch-diagnostics-'));
3836
  try {
@@ -5990,6 +6108,9 @@ describe('Agent skill script argument validation', () => {
5990
  assert.equal(result.stderr.trim(), '');
5991
  const body = JSON.parse(result.stdout);
5992
  assert.equal(body.client_request_id, 'web-diag');
 
 
 
5993
  assert.equal(body.feedback.value, 'usable');
5994
  assert.equal(body.diagnostics.matched_log_count, 1);
5995
  assert.equal(body.request_count, 1);
@@ -6479,7 +6600,11 @@ describe('Agent skill script argument validation', () => {
6479
  assert.equal(result.stderr.trim(), '');
6480
  const body = JSON.parse(result.stdout);
6481
  assert.deepEqual(body.diagnostics_retention, retention);
 
 
 
6482
  assert.equal(body.diagnostics_note.code, 'no_matching_logs_in_retention_window');
 
6483
  assert.equal(body.requests[0].diagnostics_note.code, 'no_matching_logs_in_retention_window');
6484
  assert.equal(body.requests[0].diagnostics_note.retention.max_entries, 123);
6485
  assert.match(body.requests[0].diagnostics_note.message, /最近 123 条本地应用日志/);
@@ -6570,7 +6695,8 @@ function runSkillScript(filename, args, env = {}, options = {}) {
6570
  return spawnSync(process.execPath, [join(skillScriptsRoot, filename), ...args], {
6571
  cwd: options.cwd || repoRoot,
6572
  encoding: 'utf8',
6573
- env: { ...baseEnv, ...env }
 
6574
  });
6575
  }
6576
 
 
618
  assert.equal(result.stderr.trim(), '');
619
  });
620
 
621
+ it('explains explicit page SSE dry-runs without blaming high-resolution routing', () => {
622
+ const result = runSkillScript('generate-image.mjs', ['--page-sse', '--size', '1024x1024', 'prompt']);
623
+
624
+ assert.equal(result.status, 0);
625
+ const body = JSON.parse(result.stdout);
626
+ assert.equal(body.endpoint, 'http://localhost:4783/api/images');
627
+ assert.equal(body.routing_guidance.transport, 'page_sse');
628
+ assert.match(body.routing_guidance.reason, /Explicit --page-sse/);
629
+ assert.doesNotMatch(body.routing_guidance.reason, /max_edge>2048/);
630
+ assert.equal(result.stderr.trim(), '');
631
+ });
632
+
633
  it('prints server orchestration guidance for remote HTTPS small generate dry-runs', () => {
634
  const result = runSkillScript('generate-image.mjs', ['--base-url', 'https://space.example.test', 'prompt']);
635
 
 
643
  assert.equal(result.stderr.trim(), '');
644
  });
645
 
646
+ it('can combine generate dry-run planning with non-billable remote checks', async () => {
647
+ const requests = [];
648
+ await withServer(
649
+ (request, response) => {
650
+ requests.push({ method: request.method, url: request.url });
651
+ if (request.url === '/api/agent/capabilities') {
652
+ response.writeHead(200, { 'content-type': 'application/json' });
653
+ response.end(
654
+ JSON.stringify(
655
+ agentGenerateCapabilities({
656
+ supported: {
657
+ request_modes: ['images-non-stream', 'images-sse']
658
+ },
659
+ agent_streaming: {
660
+ page_sse: { supported: true, endpoint: '/api/images' }
661
+ }
662
+ })
663
+ )
664
+ );
665
+ return;
666
+ }
667
+ if (request.url === '/api/runtime-capabilities') {
668
+ response.writeHead(200, { 'content-type': 'application/json' });
669
+ response.end(
670
+ JSON.stringify({
671
+ streamingBatch: { enabled: true, recommendedConcurrency: 2 },
672
+ channelRouting: { effectiveRequestModes: ['images-non-stream'] }
673
+ })
674
+ );
675
+ return;
676
+ }
677
+ response.writeHead(404, { 'content-type': 'application/json' });
678
+ response.end(JSON.stringify({ error: 'missing' }));
679
+ },
680
+ async (baseUrl) => {
681
+ const result = await runSkillScriptAsync(
682
+ 'generate-image.mjs',
683
+ ['--check-remote', 'prompt'],
684
+ { GPT_IMAGE_PLAYGROUND_URL: baseUrl }
685
+ );
686
+
687
+ assert.equal(result.status, 0);
688
+ assert.equal(result.stderr.trim(), '');
689
+ const body = JSON.parse(result.stdout);
690
+ assert.equal(body.verification_scope.mode, 'remote_contract_and_local_planning');
691
+ assert.equal(body.verification_scope.remote_capabilities_verified, true);
692
+ assert.equal(body.verification_scope.runtime_capacity_verified, true);
693
+ assert.equal(body.verification_scope.auth_verified, true);
694
+ assert.equal(body.verification_scope.billable_request_sent, false);
695
+ assert.deepEqual(body.verification_scope.remote_check.capabilities.request_modes, [
696
+ 'images-non-stream',
697
+ 'images-sse'
698
+ ]);
699
+ assert.deepEqual(body.verification_scope.remote_check.runtime.effective_request_modes, [
700
+ 'images-non-stream'
701
+ ]);
702
+ assert.deepEqual(
703
+ requests.map((item) => `${item.method} ${item.url}`),
704
+ ['GET /api/agent/capabilities', 'GET /api/runtime-capabilities']
705
+ );
706
+ }
707
+ );
708
+ });
709
+
710
  it('keeps default generate on server orchestration when request modes are declared', async () => {
711
  const requests = [];
712
  let imageRequestBody = '';
 
865
  requests.push({ method: request.method, url: request.url });
866
  if (request.url === '/api/agent/capabilities') {
867
  response.writeHead(200, { 'content-type': 'application/json' });
868
+ response.end(
869
+ JSON.stringify(
870
+ agentGenerateCapabilities({
871
+ agent_streaming: {
872
+ page_sse: {
873
+ supported: true,
874
+ endpoint: '/api/images',
875
+ client_request_id: { max_length: 128 }
876
+ }
877
+ }
878
+ })
879
+ )
880
+ );
881
  return;
882
  }
883
  if (
 
897
  );
898
  return;
899
  }
900
+ if (request.url === '/api/images') {
901
+ response.writeHead(400, { 'content-type': 'application/json' });
902
+ response.end(JSON.stringify({ error: 'clientRequestId 长度不能超过 128 个字符。' }));
903
+ return;
904
+ }
905
  response.writeHead(404, { 'content-type': 'application/json' });
906
  response.end(JSON.stringify({ error: 'missing' }));
907
  },
 
920
  [
921
  '/api/agent/images/generate',
922
  '/api/agent/image-requests',
923
+ '/api/agent/jobs/images/generate',
924
+ '/api/images'
925
  ]
926
  );
927
  assert.deepEqual(
 
930
  'GET /api/agent/capabilities',
931
  'POST /api/agent/images/generate',
932
  'POST /api/agent/image-requests',
933
+ 'POST /api/agent/jobs/images/generate',
934
+ 'POST /api/images'
935
  ]
936
  );
937
  }
 
3713
  assert.match(skillText, /Codex 会话日志会持久保存命令输出/);
3714
  assert.match(skillText, /npm run env:summary/);
3715
  assert.match(skillText, /verification_scope\.mode=local_planning_only/);
3716
+ assert.match(skillText, /--check-remote/);
3717
+ assert.match(skillText, /remote_contract_and_local_planning/);
3718
  assert.match(skillText, /manifest_written=false/);
3719
  assert.match(skillText, /Hugging Face Space Secrets 只能写入和列出名称/);
3720
 
 
3725
  assert.match(apiReference, /channel_capacity_queue_aborted/);
3726
  assert.match(apiReference, /npm run env:summary/);
3727
  assert.match(apiReference, /verification_scope\.mode=local_planning_only/);
3728
+ assert.match(apiReference, /--check-remote/);
3729
+ assert.match(apiReference, /remote_contract_and_local_planning/);
3730
  assert.match(apiReference, /manifest_written=false/);
3731
  assert.match(apiReference, /Hugging Face Space Secrets 只能写入和列出名称/);
3732
  assert.match(apiReference, /这个失败表示上游已生成但本地验收未通过/);
 
3930
  }
3931
  });
3932
 
3933
+ it('prints stdin batch dry-run plans without inventing a /dev/stdin manifest path', () => {
3934
+ const input = `${JSON.stringify({
3935
+ id: 'stdin-task',
3936
+ prompt: 'stdin prompt',
3937
+ size: '1024x1024'
3938
+ })}\n`;
3939
+
3940
+ const result = runSkillScript('batch-images.mjs', ['--input', '-'], {}, { input });
3941
+
3942
+ assert.equal(result.status, 0);
3943
+ assert.equal(result.stderr.trim(), '');
3944
+ const body = JSON.parse(result.stdout);
3945
+ assert.equal(body.input, '/dev/stdin');
3946
+ assert.equal(body.manifest, null);
3947
+ assert.equal(body.manifest_written, false);
3948
+ assert.equal(body.total, 1);
3949
+ assert.equal(body.tasks[0].id, 'stdin-task');
3950
+ });
3951
+
3952
  it('records enriched Agent diagnostics for failed batch tasks and manifests', async () => {
3953
  const tempRoot = mkdtempSync(join(tmpdir(), 'gpt-image-batch-diagnostics-'));
3954
  try {
 
6108
  assert.equal(result.stderr.trim(), '');
6109
  const body = JSON.parse(result.stdout);
6110
  assert.equal(body.client_request_id, 'web-diag');
6111
+ assert.equal(body.found, true);
6112
+ assert.equal(body.page_request_query_count, 1);
6113
+ assert.equal(body.page_request_found_count, 1);
6114
  assert.equal(body.feedback.value, 'usable');
6115
  assert.equal(body.diagnostics.matched_log_count, 1);
6116
  assert.equal(body.request_count, 1);
 
6600
  assert.equal(result.stderr.trim(), '');
6601
  const body = JSON.parse(result.stdout);
6602
  assert.deepEqual(body.diagnostics_retention, retention);
6603
+ assert.equal(body.found, false);
6604
+ assert.equal(body.page_request_query_count, 1);
6605
+ assert.equal(body.page_request_found_count, 0);
6606
  assert.equal(body.diagnostics_note.code, 'no_matching_logs_in_retention_window');
6607
+ assert.equal(body.requests[0].found, false);
6608
  assert.equal(body.requests[0].diagnostics_note.code, 'no_matching_logs_in_retention_window');
6609
  assert.equal(body.requests[0].diagnostics_note.retention.max_entries, 123);
6610
  assert.match(body.requests[0].diagnostics_note.message, /最近 123 条本地应用日志/);
 
6695
  return spawnSync(process.execPath, [join(skillScriptsRoot, filename), ...args], {
6696
  cwd: options.cwd || repoRoot,
6697
  encoding: 'utf8',
6698
+ env: { ...baseEnv, ...env },
6699
+ ...(options.input !== undefined ? { input: options.input } : {})
6700
  });
6701
  }
6702
 
scripts/command-center.test.mjs CHANGED
@@ -752,6 +752,11 @@ describe('Command center scripts', () => {
752
  endpoint: '/api/agent/image-requests',
753
  transport_selection: 'server_owned'
754
  },
 
 
 
 
 
755
  routing_rules: {
756
  high_resolution_edit: {
757
  conditions: { operation: 'edit', max_edge: { operator: 'gt', value: 2048 } }
@@ -796,9 +801,16 @@ describe('Command center scripts', () => {
796
  defaultMode: 'auto',
797
  unavailableMarkScope: 'channel+backend+strategy+operation'
798
  },
799
- streamingBatch: { enabled: true },
 
800
  responsesImageBackend: { enabled: true, mode: 'experimental' },
801
  channelRouting: {
 
 
 
 
 
 
802
  configuredRequestModes: ['images-non-stream', 'images-sse', 'responses-sse'],
803
  effectiveRequestModes: ['images-non-stream', 'images-sse'],
804
  effectiveRequestModesByChannel: [
@@ -900,6 +912,27 @@ describe('Command center scripts', () => {
900
  assert.equal(body.summary.request_modes.smoke['responses-sse'].billable, false);
901
  assert.equal(body.summary.responses_gpt2image_ready, true);
902
  assert.equal(body.summary.responses_image_backend_declared_supported, true);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
903
  assert.equal(body.summary.billable_smoke, 'skipped');
904
  assert.equal(body.layers.find((layer) => layer.name === 'billable_smoke').skipped, true);
905
  assert.ok(
@@ -1013,16 +1046,27 @@ describe('Command center scripts', () => {
1013
  return;
1014
  }
1015
  if (request.url === '/api/images') {
1016
- response.writeHead(200, { 'content-type': 'text/event-stream' });
1017
- response.end(
1018
- [
1019
- 'data: {"type":"completed","filename":"doctor-page-sse.png","path":"/api/image/doctor-page-sse.png","output_format":"png"}',
1020
- '',
1021
- 'data: {"type":"done","client_request_id":"doctor-page-sse-request","images":[{"filename":"doctor-page-sse.png"}]}',
1022
- '',
1023
- ''
1024
- ].join('\n')
1025
- );
 
 
 
 
 
 
 
 
 
 
 
1026
  return;
1027
  }
1028
  if (request.url === '/api/agent/image-requests') {
@@ -1127,8 +1171,19 @@ describe('Command center scripts', () => {
1127
  return;
1128
  }
1129
  if (request.url === '/api/images') {
1130
- response.writeHead(503, { 'content-type': 'text/plain' });
1131
- response.end('503 Service temporarily unavailable');
 
 
 
 
 
 
 
 
 
 
 
1132
  return;
1133
  }
1134
  if (request.url === '/api/agent/jobs/images/generate') {
@@ -1356,3 +1411,15 @@ function runNodeCommandAsync(args, options = {}) {
1356
  });
1357
  });
1358
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
752
  endpoint: '/api/agent/image-requests',
753
  transport_selection: 'server_owned'
754
  },
755
+ request_mode_controls: {
756
+ global_env: 'OPENAI_UPSTREAM_REQUEST_MODES',
757
+ channel_env_pattern: 'OPENAI_CHANNEL_N_REQUEST_MODES',
758
+ agent_client_policy: 'diagnostics_only'
759
+ },
760
  routing_rules: {
761
  high_resolution_edit: {
762
  conditions: { operation: 'edit', max_edge: { operator: 'gt', value: 2048 } }
 
801
  defaultMode: 'auto',
802
  unavailableMarkScope: 'channel+backend+strategy+operation'
803
  },
804
+ streamingBatch: { enabled: true, recommendedConcurrency: 2 },
805
+ channelQueue: { enabled: true, capacityPerCredential: 1 },
806
  responsesImageBackend: { enabled: true, mode: 'experimental' },
807
  channelRouting: {
808
+ strategy: 'round_robin',
809
+ requestModeControls: {
810
+ global_env: 'OPENAI_UPSTREAM_REQUEST_MODES',
811
+ channel_env_pattern: 'OPENAI_CHANNEL_N_REQUEST_MODES',
812
+ agent_client_policy: 'diagnostics_only'
813
+ },
814
  configuredRequestModes: ['images-non-stream', 'images-sse', 'responses-sse'],
815
  effectiveRequestModes: ['images-non-stream', 'images-sse'],
816
  effectiveRequestModesByChannel: [
 
912
  assert.equal(body.summary.request_modes.smoke['responses-sse'].billable, false);
913
  assert.equal(body.summary.responses_gpt2image_ready, true);
914
  assert.equal(body.summary.responses_image_backend_declared_supported, true);
915
+ assert.deepEqual(body.summary.runtime_environment, {
916
+ state_backend: 'memory',
917
+ image_storage_mode: 'indexeddb',
918
+ postgres_configured: false,
919
+ page_sse_auth_required: true,
920
+ agent_auth_schemes: [],
921
+ orchestration_endpoint: '/api/agent/image-requests',
922
+ orchestration_transport_selection: 'server_owned',
923
+ request_mode_control_policy: 'diagnostics_only',
924
+ request_mode_controls: {
925
+ global_env: 'OPENAI_UPSTREAM_REQUEST_MODES',
926
+ channel_env_pattern: 'OPENAI_CHANNEL_N_REQUEST_MODES',
927
+ agent_client_policy: 'diagnostics_only'
928
+ },
929
+ runtime_strategy: 'round_robin',
930
+ effective_request_modes: ['images-non-stream', 'images-sse'],
931
+ streaming_batch_enabled: true,
932
+ recommended_concurrency: 2,
933
+ channel_queue_enabled: true,
934
+ channel_queue_capacity_per_credential: 1
935
+ });
936
  assert.equal(body.summary.billable_smoke, 'skipped');
937
  assert.equal(body.layers.find((layer) => layer.name === 'billable_smoke').skipped, true);
938
  assert.ok(
 
1046
  return;
1047
  }
1048
  if (request.url === '/api/images') {
1049
+ (async () => {
1050
+ const requestText = await readRequestText(request);
1051
+ if (requestText.includes('contract check')) {
1052
+ response.writeHead(400, { 'content-type': 'application/json' });
1053
+ response.end(JSON.stringify({ error: 'clientRequestId 长度不能超过 128 个字符。' }));
1054
+ return;
1055
+ }
1056
+ response.writeHead(200, { 'content-type': 'text/event-stream' });
1057
+ response.end(
1058
+ [
1059
+ 'data: {"type":"completed","filename":"doctor-page-sse.png","path":"/api/image/doctor-page-sse.png","output_format":"png"}',
1060
+ '',
1061
+ 'data: {"type":"done","client_request_id":"doctor-page-sse-request","images":[{"filename":"doctor-page-sse.png"}]}',
1062
+ '',
1063
+ ''
1064
+ ].join('\n')
1065
+ );
1066
+ })().catch((error) => {
1067
+ response.writeHead(500, { 'content-type': 'application/json' });
1068
+ response.end(JSON.stringify({ error: error.message }));
1069
+ });
1070
  return;
1071
  }
1072
  if (request.url === '/api/agent/image-requests') {
 
1171
  return;
1172
  }
1173
  if (request.url === '/api/images') {
1174
+ (async () => {
1175
+ const requestText = await readRequestText(request);
1176
+ if (requestText.includes('contract check')) {
1177
+ response.writeHead(400, { 'content-type': 'application/json' });
1178
+ response.end(JSON.stringify({ error: 'clientRequestId 长度不能超过 128 个字符。' }));
1179
+ return;
1180
+ }
1181
+ response.writeHead(503, { 'content-type': 'text/plain' });
1182
+ response.end('503 Service temporarily unavailable');
1183
+ })().catch((error) => {
1184
+ response.writeHead(500, { 'content-type': 'application/json' });
1185
+ response.end(JSON.stringify({ error: error.message }));
1186
+ });
1187
  return;
1188
  }
1189
  if (request.url === '/api/agent/jobs/images/generate') {
 
1411
  });
1412
  });
1413
  }
1414
+
1415
+ function readRequestText(request) {
1416
+ return new Promise((resolve, reject) => {
1417
+ let body = '';
1418
+ request.setEncoding('utf8');
1419
+ request.on('data', (chunk) => {
1420
+ body += chunk;
1421
+ });
1422
+ request.on('end', () => resolve(body));
1423
+ request.on('error', reject);
1424
+ });
1425
+ }
skills/gpt-image-playground-agent/SKILL.md CHANGED
@@ -19,7 +19,7 @@ Agent API 是给自动化客户端使用的机器接口,不是自治 Agent 平
19
  - 诊断上游图片接口:优先运行 `scripts/probe-upstream-image.mjs`。
20
  - 不要临时编写 Node/Python/shell 脚本、curl 命令或手写 fetch/FormData 来重复实现这些脚本已经覆盖的 API 调用。
21
  - 只有在内置脚本缺少用户明确需要的能力时,才修改或扩展 `scripts/` 内的预置脚本,并同步补测试;不要在仓库外留下 ad hoc 调用脚本。
22
- - 先用 dry-run 或 `--contract-check` 检查请求、路由、鉴权和服务声明的默认编排入口;只有用户明确允许真实计费时才加 `--allow-billable`。
23
  - 真实调用成功或失败后,优先读取脚本输出的 `summary`。它是面向 Agent 的机器摘要,包含 `billable`、请求 ID、幂等键、产物 URL、耗时、耗时拆分、路由、渠道、上游 host、脱敏请求头、重试和下一步动作;Agent JSON 失败时脚本会按幂等键做一次只读 Agent state 诊断补采样,补充 `agent_diagnostics_checked`、`agent_diagnostics_found`、`agent_diagnostics_unavailable_reason`、`agent_diagnostics_http_status`、`request_id`、渠道和上游 host。不要再先手查 SQLite、Docker logs 或上游后台。
24
  - 新增 probe、diagnostics、路由健康或请求旅程能力时,先在服务端定义机器 API 契约,并通过 `GET /api/agent/capabilities`、`GET /api/agent/openapi.json` 或明确的 `/api/agent/diagnostics/*` 端点声明;Skill 脚本只做薄封装,不能复制页面 API、运行态 API 和 Agent API 的边界判断。
25
 
@@ -125,7 +125,7 @@ Authorization: Bearer <token>
125
  - `scripts/diagnose-request.mjs`:按一个或多个页面 `clientRequestId` 只读查询结果反馈和脱敏日志诊断摘要,也可按 Agent `request_id` 或 `idempotency_key` 查询 Agent state 请求诊断;支持读取批量 manifest 和 `--base-url`,不触发生图计费。
126
  - `scripts/probe-upstream-image.mjs`:直接探测上游图片接口连通性。默认只检查 DNS、TLS 和 `/models`,必须添加 `--allow-billable` 才会真实调用 `/images/generations`。
127
 
128
- 生成、编辑和批量脚本的 dry-run 输出会包含 `verification_scope.mode=local_planning_only`,表示只验证了本地请求构造、参数归一化和静态路由规划;它不会读取远端 capabilities,不会验证远端鉴权、渠道容量或 manifest 写入。生成 dry-run 默认 `routing_guidance.transport=server_orchestrated`,表示真实请求只提交业务意图到服务端编排入口;显式 `--agent`、`--job`、`--page-sse` 才会显示对应诊断路径。批量 dry-run 还会包含 `guardrails`,提示真实执行要复用同一个 `--ordered-prefix`,固定尺寸任务是否建议加 `--dimension-check`。真实执行输出会包含 `summary`;成功摘要含 `ok=true`、`billable`、`request_id`、`idempotency_key`、`artifact_ids`、`content_urls`、`absolute_content_urls`、`image_dimensions`、`actual_dimensions`、`cached`、`elapsed_ms`、`server_elapsed_ms`、`elapsed_source`、`elapsed_breakdown`、`transport`、`endpoint`、`route_mode`、`image_backend`、`stream_mode`、`streaming_strategy`、`channel_request_mode`、`channel_request_mode_fallback_applied`、`route_decision`、`selected_channel_id`、`upstream_host` 和脱敏 `request_headers`。失败摘要含 `route_decision`、`transport_error_kind`、`retry_after_ms`、`cooldown_until`、`cooldown_target`、`retryable`、`dimension_check_failed`、`expected_dimensions`、`actual_dimensions`、`agent_diagnostics_checked`、`agent_diagnostics_found`、`agent_diagnostics_unavailable_reason`、`agent_diagnostics_http_status` 和 `next_action`;尺寸门禁失败时还会保留已生成产物的 `artifact_ids`、`content_urls`、`absolute_content_urls` 和 `image_dimensions`,便于人工审查。
129
  所有生成、编辑、批量和探针脚本在 dry-run 或真实请求前都会校验尺寸参数。`gpt-image-2` 支持 `auto` 或任意正整数 `WIDTHxHEIGHT`;默认 OpenAI-compatible 上游的更严格尺寸边界由服务端 profile 或真实上游显式报错。非 `gpt-image-2` 模型只接受 `auto`、`1024x1024`、`1536x1024` 或 `1024x1536`。生成、页面编辑、批量页面 SSE 和上游探针默认请求 `output_format=webp`、`output_compression=100`;普通 Agent edit 不发送输出格式字段,输出格式固定为 Agent 契约。
130
 
131
  如果当前上下文位于仓库根目录,管理员侧优先使用顶层命令:
 
19
  - 诊断上游图片接口:优先运行 `scripts/probe-upstream-image.mjs`。
20
  - 不要临时编写 Node/Python/shell 脚本、curl 命令或手写 fetch/FormData 来重复实现这些脚本已经覆盖的 API 调用。
21
  - 只有在内置脚本缺少用户明确需要的能力时,才修改或扩展 `scripts/` 内的预置脚本,并同步补测试;不要在仓库外留下 ad hoc 调用脚本。
22
+ - 先用 dry-run、`--check-remote` 或 `--contract-check` 检查请求、路由、鉴权和服务声明的默认编排入口;只有用户明确允许真实计费时才加 `--allow-billable`。
23
  - 真实调用成功或失败后,优先读取脚本输出的 `summary`。它是面向 Agent 的机器摘要,包含 `billable`、请求 ID、幂等键、产物 URL、耗时、耗时拆分、路由、渠道、上游 host、脱敏请求头、重试和下一步动作;Agent JSON 失败时脚本会按幂等键做一次只读 Agent state 诊断补采样,补充 `agent_diagnostics_checked`、`agent_diagnostics_found`、`agent_diagnostics_unavailable_reason`、`agent_diagnostics_http_status`、`request_id`、渠道和上游 host。不要再先手查 SQLite、Docker logs 或上游后台。
24
  - 新增 probe、diagnostics、路由健康或请求旅程能力时,先在服务端定义机器 API 契约,并通过 `GET /api/agent/capabilities`、`GET /api/agent/openapi.json` 或明确的 `/api/agent/diagnostics/*` 端点声明;Skill 脚本只做薄封装,不能复制页面 API、运行态 API 和 Agent API 的边界判断。
25
 
 
125
  - `scripts/diagnose-request.mjs`:按一个或多个页面 `clientRequestId` 只读查询结果反馈和脱敏日志诊断摘要,也可按 Agent `request_id` 或 `idempotency_key` 查询 Agent state 请求诊断;支持读取批量 manifest 和 `--base-url`,不触发生图计费。
126
  - `scripts/probe-upstream-image.mjs`:直接探测上游图片接口连通性。默认只检查 DNS、TLS 和 `/models`,必须添加 `--allow-billable` 才会真实调用 `/images/generations`。
127
 
128
+ 生成、编辑和批量脚本的 dry-run 输出会包含 `verification_scope.mode=local_planning_only`,表示只验证了本地请求构造、参数归一化和静态路由规划;它不会读取远端 capabilities,不会验证远端鉴权、渠道容量或 manifest 写入。生成 dry-run 添加 `--check-remote` 后会只读查询 `/api/agent/capabilities` 和 `/api/runtime-capabilities`,输出 `verification_scope.mode=remote_contract_and_local_planning`,仍不会发送真实生图请求。生成 dry-run 默认 `routing_guidance.transport=server_orchestrated`,表示真实请求只提交业务意图到服务端编排入口;显式 `--agent`、`--job`、`--page-sse` 才会显示对应诊断路径。批量 dry-run 还会包含 `guardrails`,提示真实执行要复用同一个 `--ordered-prefix`,固定尺寸任务是否建议加 `--dimension-check`。真实执行输出会包含 `summary`;成功摘要含 `ok=true`、`billable`、`request_id`、`idempotency_key`、`artifact_ids`、`content_urls`、`absolute_content_urls`、`image_dimensions`、`actual_dimensions`、`cached`、`elapsed_ms`、`server_elapsed_ms`、`elapsed_source`、`elapsed_breakdown`、`transport`、`endpoint`、`route_mode`、`image_backend`、`stream_mode`、`streaming_strategy`、`channel_request_mode`、`channel_request_mode_fallback_applied`、`route_decision`、`selected_channel_id`、`upstream_host` 和脱敏 `request_headers`。失败摘要含 `route_decision`、`transport_error_kind`、`retry_after_ms`、`cooldown_until`、`cooldown_target`、`retryable`、`dimension_check_failed`、`expected_dimensions`、`actual_dimensions`、`agent_diagnostics_checked`、`agent_diagnostics_found`、`agent_diagnostics_unavailable_reason`、`agent_diagnostics_http_status` 和 `next_action`;尺寸门禁失败时还会保留已生成产物的 `artifact_ids`、`content_urls`、`absolute_content_urls` 和 `image_dimensions`,便于人工审查。
129
  所有生成、编辑、批量和探针脚本在 dry-run 或真实请求前都会校验尺寸参数。`gpt-image-2` 支持 `auto` 或任意正整数 `WIDTHxHEIGHT`;默认 OpenAI-compatible 上游的更严格尺寸边界由服务端 profile 或真实上游显式报错。非 `gpt-image-2` 模型只接受 `auto`、`1024x1024`、`1536x1024` 或 `1024x1536`。生成、页面编辑、批量页面 SSE 和上游探针默认请求 `output_format=webp`、`output_compression=100`;普通 Agent edit 不发送输出格式字段,输出格式固定为 Agent 契约。
130
 
131
  如果当前上下文位于仓库根目录,管理员侧优先使用顶层命令:
skills/gpt-image-playground-agent/references/api.md CHANGED
@@ -29,9 +29,9 @@ Agent API 是给自动化客户端使用的机器接口,不是自治 Agent 平
29
  - `scripts/diagnose-request.mjs`:按页面 `clientRequestId` 只读查询结果反馈和脱敏日志诊断摘要,也可按 Agent `request_id` 或 `idempotency_key` 查询 Agent state 请求诊断,支持 `--base-url` 固定目标服务。
30
  - `scripts/probe-upstream-image.mjs`:上游图片接口连通性探针。
31
 
32
- 生成、编辑和批量脚本默认只做 dry-run,不触发真实生图或编辑。dry-run 输出的 `verification_scope.mode=local_planning_only` 表示只完成本地请求构造、参数归一化和静态路由规划;它不会读取远端 capabilities,不会验证远端鉴权、渠道容量或 manifest 写入。必须显式添加 `--allow-billable` 才会调用真实端点。generate 默认提交到 `/api/agent/image-requests` 服务端编排入口;`--agent`、`--job`、`--page-sse` 才会显式改用 `/api/agent/images/generate`、`/api/agent/jobs/images/generate` 或页面端 `/api/images` SSE。
33
  上游探针默认只检查 DNS、TLS 和 `/models`,必须显式添加 `--allow-billable` 才会调用上游 `/images/generations`。
34
- 脚本支持 `GPT_IMAGE_AGENT_CONTRACT_CHECK=1` 或 `--contract-check` 做只读契约检查,会覆盖服务声明的默认编排入口,不触发真实生图或编辑。
35
  位于仓库根目录且是首次配置、换机器、服务地址不确定或 token 不确定时,先运行 `npm run first-run`。它只读、非计费、不写 env 文件,默认输出中文摘要,并报告 `service_base_url_source`、`interactive_confirmation_required`、服务可达性、当前进程鉴权、页面 SSE 鉴权和下一步动作。
36
  自动化消费时使用 `npm run first-run -- --json`。
37
  Agent 端点鉴权以 capabilities 的 `auth.schemes` 为准。配置 `AGENT_API_TOKEN` 时只接受 Bearer token;只有未配置 `AGENT_API_TOKEN` 且配置了 `APP_PASSWORD` 时,Agent 端点才接受访问码哈希 `GPT_IMAGE_APP_PASSWORD_HASH`。页面端 `/api/images` SSE 另看 `agent_streaming.page_sse.auth`;当其声明 `required=true` 时,form-data 必须包含 `passwordHash`。`GPT_IMAGE_AGENT_TOKEN` 不能替代页面 SSE 表单鉴权。
 
29
  - `scripts/diagnose-request.mjs`:按页面 `clientRequestId` 只读查询结果反馈和脱敏日志诊断摘要,也可按 Agent `request_id` 或 `idempotency_key` 查询 Agent state 请求诊断,支持 `--base-url` 固定目标服务。
30
  - `scripts/probe-upstream-image.mjs`:上游图片接口连通性探针。
31
 
32
+ 生成、编辑和批量脚本默认只做 dry-run,不触发真实生图或编辑。dry-run 输出的 `verification_scope.mode=local_planning_only` 表示只完成本地请求构造、参数归一化和静态路由规划;它不会读取远端 capabilities,不会验证远端鉴权、渠道容量或 manifest 写入。generate 可添加 `--check-remote` 做只读远端检查,输出 `verification_scope.mode=remote_contract_and_local_planning`,仅访问 `/api/agent/capabilities` 和 `/api/runtime-capabilities`,不会发送真实生图请求。必须显式添加 `--allow-billable` 才会调用真实端点。generate 默认提交到 `/api/agent/image-requests` 服务端编排入口;`--agent`、`--job`、`--page-sse` 才会显式改用 `/api/agent/images/generate`、`/api/agent/jobs/images/generate` 或页面端 `/api/images` SSE。
33
  上游探针默认只检查 DNS、TLS 和 `/models`,必须显式添加 `--allow-billable` 才会调用上游 `/images/generations`。
34
+ 脚本支持 `GPT_IMAGE_AGENT_CONTRACT_CHECK=1` 或 `--contract-check` 做只读契约检查,会覆盖服务声明的默认编排入口和页面 SSE 边界,不触发真实生图或编辑。
35
  位于仓库根目录且是首次配置、换机器、服务地址不确定或 token 不确定时,先运行 `npm run first-run`。它只读、非计费、不写 env 文件,默认输出中文摘要,并报告 `service_base_url_source`、`interactive_confirmation_required`、服务可达性、当前进程鉴权、页面 SSE 鉴权和下一步动作。
36
  自动化消费时使用 `npm run first-run -- --json`。
37
  Agent 端点鉴权以 capabilities 的 `auth.schemes` 为准。配置 `AGENT_API_TOKEN` 时只接受 Bearer token;只有未配置 `AGENT_API_TOKEN` 且配置了 `APP_PASSWORD` 时,Agent 端点才接受访问码哈希 `GPT_IMAGE_APP_PASSWORD_HASH`。页面端 `/api/images` SSE 另看 `agent_streaming.page_sse.auth`;当其声明 `required=true` 时,form-data 必须包含 `passwordHash`。`GPT_IMAGE_AGENT_TOKEN` 不能替代页面 SSE 表单鉴权。
skills/gpt-image-playground-agent/scripts/batch-images.mjs CHANGED
@@ -173,7 +173,7 @@ try {
173
  process.exit(2);
174
  }
175
 
176
- const manifestPath = options.manifest || `${options.input}.manifest.jsonl`;
177
  let planned;
178
  try {
179
  planned = tasks.map((task, index) => normalizeTask(task, index, options));
@@ -220,6 +220,11 @@ if (!options.allowBillable || options.dryRun) {
220
  process.exit(0);
221
  }
222
 
 
 
 
 
 
223
  try {
224
  const capacityFeedback = await resolveBatchCapacityFeedback();
225
  const completed = options.resume ? readCompletedManifestKeys(manifestPath) : new Set();
@@ -275,7 +280,7 @@ function parseArgs(argv) {
275
  else if (arg === '--resume') parsed.resume = true;
276
  else if (arg === '--dimension-check') parsed.dimensionCheck = true;
277
  else if (arg === '--help' || arg === '-h') parsed.help = true;
278
- else if (arg === '--input') parsed.input = readOptionValue(argv, (index += 1), arg);
279
  else if (arg === '--manifest') parsed.manifest = readOptionValue(argv, (index += 1), arg);
280
  else if (arg === '--ordered-prefix') parsed.orderedPrefix = readOptionValue(argv, (index += 1), arg);
281
  else if (arg === '--timeout-ms') parsed.timeoutMs = readOptionValue(argv, (index += 1), arg);
@@ -286,12 +291,22 @@ function parseArgs(argv) {
286
  }
287
  else if (arg === '--concurrency') parsed.concurrency = readOptionValue(argv, (index += 1), arg);
288
  else if (arg.startsWith('--')) throw new Error(`未知参数:${arg}`);
289
- else if (!parsed.input) parsed.input = arg;
290
  else throw new Error(`未知位置参数:${arg}`);
291
  }
292
  return parsed;
293
  }
294
 
 
 
 
 
 
 
 
 
 
 
295
  function buildDryRunVerificationScope() {
296
  return {
297
  mode: 'local_planning_only',
 
173
  process.exit(2);
174
  }
175
 
176
+ const manifestPath = resolveManifestPath(options);
177
  let planned;
178
  try {
179
  planned = tasks.map((task, index) => normalizeTask(task, index, options));
 
220
  process.exit(0);
221
  }
222
 
223
+ if (!manifestPath) {
224
+ console.error('--input - 真实执行必须显式设置 --manifest,避免无法续跑或审计。');
225
+ process.exit(2);
226
+ }
227
+
228
  try {
229
  const capacityFeedback = await resolveBatchCapacityFeedback();
230
  const completed = options.resume ? readCompletedManifestKeys(manifestPath) : new Set();
 
280
  else if (arg === '--resume') parsed.resume = true;
281
  else if (arg === '--dimension-check') parsed.dimensionCheck = true;
282
  else if (arg === '--help' || arg === '-h') parsed.help = true;
283
+ else if (arg === '--input') parsed.input = normalizeInputPath(readOptionValue(argv, (index += 1), arg));
284
  else if (arg === '--manifest') parsed.manifest = readOptionValue(argv, (index += 1), arg);
285
  else if (arg === '--ordered-prefix') parsed.orderedPrefix = readOptionValue(argv, (index += 1), arg);
286
  else if (arg === '--timeout-ms') parsed.timeoutMs = readOptionValue(argv, (index += 1), arg);
 
291
  }
292
  else if (arg === '--concurrency') parsed.concurrency = readOptionValue(argv, (index += 1), arg);
293
  else if (arg.startsWith('--')) throw new Error(`未知参数:${arg}`);
294
+ else if (!parsed.input) parsed.input = normalizeInputPath(arg);
295
  else throw new Error(`未知位置参数:${arg}`);
296
  }
297
  return parsed;
298
  }
299
 
300
+ function normalizeInputPath(value) {
301
+ return value === '-' ? '/dev/stdin' : value;
302
+ }
303
+
304
+ function resolveManifestPath(parsed) {
305
+ if (parsed.manifest) return parsed.manifest;
306
+ if (parsed.input === '/dev/stdin') return null;
307
+ return `${parsed.input}.manifest.jsonl`;
308
+ }
309
+
310
  function buildDryRunVerificationScope() {
311
  return {
312
  mode: 'local_planning_only',
skills/gpt-image-playground-agent/scripts/diagnose-request.mjs CHANGED
@@ -52,6 +52,8 @@ try {
52
  service_base_url: baseUrl,
53
  service_base_url_source: baseUrlInfo.source,
54
  interactive_confirmation_required: baseUrlInfo.interactive_confirmation_required,
 
 
55
  page_request_count: requests.length,
56
  agent_request_count: agentRequests.length,
57
  request_count: requests.length,
@@ -62,6 +64,7 @@ try {
62
  if (requests.length === 1) {
63
  Object.assign(body, {
64
  client_request_id: requests[0].client_request_id,
 
65
  feedback: requests[0].feedback,
66
  diagnostics: requests[0].diagnostics,
67
  ...(requests[0].diagnostics_note ? { diagnostics_note: requests[0].diagnostics_note } : {})
@@ -354,8 +357,10 @@ function readDiagnosticsClientRequestId(value) {
354
 
355
  function buildDiagnosedRequest({ clientRequestId, feedback, diagnostics, diagnosticsCapabilities, diagnosticsRetention }) {
356
  const diagnosticsNote = buildDiagnosticsNoMatchNote(diagnostics, diagnosticsCapabilities, diagnosticsRetention);
 
357
  return {
358
  client_request_id: clientRequestId,
 
359
  feedback,
360
  diagnostics,
361
  ...(diagnosticsNote ? { diagnostics_note: diagnosticsNote } : {})
 
52
  service_base_url: baseUrl,
53
  service_base_url_source: baseUrlInfo.source,
54
  interactive_confirmation_required: baseUrlInfo.interactive_confirmation_required,
55
+ page_request_query_count: clientRequestIds.length,
56
+ page_request_found_count: requests.filter((request) => request.found).length,
57
  page_request_count: requests.length,
58
  agent_request_count: agentRequests.length,
59
  request_count: requests.length,
 
64
  if (requests.length === 1) {
65
  Object.assign(body, {
66
  client_request_id: requests[0].client_request_id,
67
+ found: requests[0].found,
68
  feedback: requests[0].feedback,
69
  diagnostics: requests[0].diagnostics,
70
  ...(requests[0].diagnostics_note ? { diagnostics_note: requests[0].diagnostics_note } : {})
 
357
 
358
  function buildDiagnosedRequest({ clientRequestId, feedback, diagnostics, diagnosticsCapabilities, diagnosticsRetention }) {
359
  const diagnosticsNote = buildDiagnosticsNoMatchNote(diagnostics, diagnosticsCapabilities, diagnosticsRetention);
360
+ const found = Boolean(feedback || (diagnostics && diagnostics.matched_log_count > 0));
361
  return {
362
  client_request_id: clientRequestId,
363
+ found,
364
  feedback,
365
  diagnostics,
366
  ...(diagnosticsNote ? { diagnostics_note: diagnosticsNote } : {})
skills/gpt-image-playground-agent/scripts/generate-image.mjs CHANGED
@@ -134,13 +134,20 @@ try {
134
  }
135
 
136
  if (isNonBillableDryRun(options, contractCheck)) {
 
 
 
 
 
 
 
137
  console.log(
138
  JSON.stringify(
139
  {
140
  ok: true,
141
  billable: false,
142
  dry_run: true,
143
- verification_scope: buildDryRunVerificationScope(),
144
  endpoint: dryRunEndpoint(requestBody, options.routeMode),
145
  route_mode: options.routeMode,
146
  routing_guidance: buildGenerateRoutingGuidance(requestBody, options.routeMode),
@@ -256,6 +263,7 @@ function parseArgs(argv) {
256
  routeMode: 'auto',
257
  dryRun: false,
258
  allowBillable: false,
 
259
  help: false,
260
  promptParts: []
261
  };
@@ -289,6 +297,7 @@ function parseArgs(argv) {
289
  else if (arg === '--base-url') parsed.baseUrl = readOptionValue(expandedArgv, (index += 1), arg);
290
  else if (arg === '--prompt-file') parsed.promptFile = readOptionValue(expandedArgv, (index += 1), arg);
291
  else if (arg === '--idempotency-key') parsed.idempotencyKey = readOptionValue(expandedArgv, (index += 1), arg);
 
292
  else if (arg.startsWith('--')) throw new Error(`未知参数:${arg}`);
293
  else parsed.promptParts.push(arg);
294
  }
@@ -497,20 +506,75 @@ function enrichImageUrls(result) {
497
  };
498
  }
499
 
500
- function buildDryRunVerificationScope() {
501
  return {
502
- mode: 'local_planning_only',
503
  service_base_url: baseUrl,
504
  service_base_url_source: baseUrlInfo.source,
505
  interactive_confirmation_required: baseUrlInfo.interactive_confirmation_required,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
506
  remote_capabilities_verified: false,
507
  runtime_capacity_verified: false,
508
  auth_verified: false,
509
- billable_request_sent: false,
510
- note: 'Dry-run validates local request construction and routing guidance only; run --contract-check or --allow-billable to verify the remote service.'
511
  };
512
  }
513
 
 
 
 
 
 
514
  function dryRunEndpoint(body, routeMode) {
515
  if (routeMode === 'auto' && shouldPreferServerOrchestration(body)) {
516
  return `${baseUrl}${AGENT_ENDPOINTS.create_image_request}`;
@@ -549,9 +613,11 @@ function buildGenerateRoutingGuidance(body, routeMode) {
549
  (routeMode !== 'agent' && (hasPageOnlyGenerateOptions(body) || isLargeGenerate(body)))) &&
550
  isPageSseAllowed(body)
551
  ) {
552
- const reason = hasPageOnlyGenerateOptions(body)
 
 
553
  ? 'Responses/GPT2Image-compatible generate options require the page form-data SSE endpoint; Agent JSON generate does not accept those fields.'
554
- : 'Generate requests with max_edge>2048 should use page form-data SSE first; if the stream fails, diagnose first and rerun manually with Agent JSON.';
555
  return {
556
  recommended_endpoint: PAGE_SSE_ENDPOINT,
557
  transport: 'page_sse',
@@ -724,7 +790,7 @@ function assertPageSseReady(capabilitiesValue) {
724
  if (!supportsPageSse(capabilitiesValue)) {
725
  throw createScriptError(
726
  'page_sse_unavailable',
727
- '大图默认路由需要 agent_streaming.page_sse.supported=true;capabilities 未声明时不能静默降级到 Agent JSON。'
728
  );
729
  }
730
  if (pageSse?.auth?.required === true && !passwordHash) {
@@ -1278,6 +1344,19 @@ async function runContractCheck(capabilitiesValue) {
1278
  checks.push(await checkContractEndpoint(AGENT_ENDPOINTS.create_generate_job));
1279
  }
1280
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1281
  console.log(JSON.stringify({ ok: true, billable: false, checks }, null, 2));
1282
  }
1283
 
@@ -1298,6 +1377,35 @@ async function checkContractEndpoint(endpoint) {
1298
  return { endpoint, status: response.status, error_code: result.error.code };
1299
  }
1300
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1301
  function readOrchestrationEndpoint(capabilitiesValue) {
1302
  const endpoint = capabilitiesValue?.orchestration?.endpoint;
1303
  if (typeof endpoint !== 'string' || !endpoint.startsWith('/')) {
@@ -1498,7 +1606,7 @@ function printUsage() {
1498
  console.error('用法:generate-image.mjs [options] <prompt>');
1499
  console.error('默认只输出 dry-run;添加 --allow-billable 才会真实生图。');
1500
  console.error(
1501
- '常用参数:--model --size --quality --n --format --output-compression --response-mode --image-backend --responses-model --gpt-model --thinking --prompt-optimization --force-web --stream-mode --streaming-strategy --partial-images --sse-log --timeout-ms --base-url --prompt-file --idempotency-key --page-sse --agent --job --no-job(兼容别名)'
1502
  );
1503
  console.error(
1504
  '契约检查:GPT_IMAGE_AGENT_CONTRACT_CHECK=1 generate-image.mjs 或 generate-image.mjs --contract-check'
 
134
  }
135
 
136
  if (isNonBillableDryRun(options, contractCheck)) {
137
+ let remoteCheck;
138
+ try {
139
+ remoteCheck = options.checkRemote ? await runRemotePlanningCheck() : buildSkippedRemotePlanningCheck();
140
+ } catch (error) {
141
+ console.error(errorMessage(error));
142
+ process.exit(1);
143
+ }
144
  console.log(
145
  JSON.stringify(
146
  {
147
  ok: true,
148
  billable: false,
149
  dry_run: true,
150
+ verification_scope: buildDryRunVerificationScope(remoteCheck),
151
  endpoint: dryRunEndpoint(requestBody, options.routeMode),
152
  route_mode: options.routeMode,
153
  routing_guidance: buildGenerateRoutingGuidance(requestBody, options.routeMode),
 
263
  routeMode: 'auto',
264
  dryRun: false,
265
  allowBillable: false,
266
+ checkRemote: false,
267
  help: false,
268
  promptParts: []
269
  };
 
297
  else if (arg === '--base-url') parsed.baseUrl = readOptionValue(expandedArgv, (index += 1), arg);
298
  else if (arg === '--prompt-file') parsed.promptFile = readOptionValue(expandedArgv, (index += 1), arg);
299
  else if (arg === '--idempotency-key') parsed.idempotencyKey = readOptionValue(expandedArgv, (index += 1), arg);
300
+ else if (arg === '--check-remote') parsed.checkRemote = true;
301
  else if (arg.startsWith('--')) throw new Error(`未知参数:${arg}`);
302
  else parsed.promptParts.push(arg);
303
  }
 
506
  };
507
  }
508
 
509
+ function buildDryRunVerificationScope(remoteCheck = buildSkippedRemotePlanningCheck()) {
510
  return {
511
+ mode: remoteCheck.remote_capabilities_verified ? 'remote_contract_and_local_planning' : 'local_planning_only',
512
  service_base_url: baseUrl,
513
  service_base_url_source: baseUrlInfo.source,
514
  interactive_confirmation_required: baseUrlInfo.interactive_confirmation_required,
515
+ remote_capabilities_verified: remoteCheck.remote_capabilities_verified,
516
+ runtime_capacity_verified: remoteCheck.runtime_capacity_verified,
517
+ auth_verified: remoteCheck.auth_verified,
518
+ billable_request_sent: false,
519
+ remote_check: {
520
+ capabilities: remoteCheck.capabilities,
521
+ runtime: remoteCheck.runtime
522
+ },
523
+ note: remoteCheck.remote_capabilities_verified
524
+ ? 'Dry-run validated local request construction and read non-billable remote capabilities/runtime state; it did not send a billable image request.'
525
+ : 'Dry-run validates local request construction and routing guidance only; run --check-remote, --contract-check or --allow-billable to verify the remote service.'
526
+ };
527
+ }
528
+
529
+ async function runRemotePlanningCheck() {
530
+ const capabilities = await readCapabilities();
531
+ const runtime = await readRuntimeCapabilities();
532
+ return {
533
+ remote_capabilities_verified: true,
534
+ runtime_capacity_verified: true,
535
+ auth_verified: true,
536
+ capabilities: {
537
+ endpoint: AGENT_ENDPOINTS.capabilities,
538
+ orchestration_supported: capabilities?.orchestration?.supported === true,
539
+ orchestration_endpoint: capabilities?.orchestration?.endpoint || null,
540
+ page_sse_supported: capabilities?.agent_streaming?.page_sse?.supported === true,
541
+ request_modes: readRequestModeList(capabilities?.supported?.request_modes)
542
+ },
543
+ runtime: {
544
+ endpoint: '/api/runtime-capabilities',
545
+ streaming_batch_enabled: runtime?.streamingBatch?.enabled === true,
546
+ recommended_concurrency: runtime?.streamingBatch?.recommendedConcurrency ?? null,
547
+ effective_request_modes: readRequestModeList(runtime?.channelRouting?.effectiveRequestModes)
548
+ }
549
+ };
550
+ }
551
+
552
+ async function readRuntimeCapabilities() {
553
+ const { response, result, text } = await fetchJson(`${baseUrl}/api/runtime-capabilities`, {
554
+ headers: authHeaders(),
555
+ timeoutMs
556
+ });
557
+ if (!response.ok) {
558
+ throw new Error(`runtime-capabilities 请求失败,状态码 ${response.status}:${text}`);
559
+ }
560
+ return result;
561
+ }
562
+
563
+ function buildSkippedRemotePlanningCheck() {
564
+ return {
565
  remote_capabilities_verified: false,
566
  runtime_capacity_verified: false,
567
  auth_verified: false,
568
+ capabilities: null,
569
+ runtime: null
570
  };
571
  }
572
 
573
+ function readRequestModeList(value) {
574
+ if (!Array.isArray(value)) return [];
575
+ return value.filter((item) => typeof item === 'string');
576
+ }
577
+
578
  function dryRunEndpoint(body, routeMode) {
579
  if (routeMode === 'auto' && shouldPreferServerOrchestration(body)) {
580
  return `${baseUrl}${AGENT_ENDPOINTS.create_image_request}`;
 
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',
 
790
  if (!supportsPageSse(capabilitiesValue)) {
791
  throw createScriptError(
792
  'page_sse_unavailable',
793
+ '显式页面 SSE 或页面专属字段需要 agent_streaming.page_sse.supported=true;capabilities 未声明时不能静默降级到 Agent JSON。'
794
  );
795
  }
796
  if (pageSse?.auth?.required === true && !passwordHash) {
 
1344
  checks.push(await checkContractEndpoint(AGENT_ENDPOINTS.create_generate_job));
1345
  }
1346
 
1347
+ if (supportsPageSse(capabilitiesValue)) {
1348
+ const pageSse = capabilitiesValue?.agent_streaming?.page_sse;
1349
+ if (pageSse?.auth?.required === true && !passwordHash) {
1350
+ checks.push({
1351
+ endpoint: PAGE_SSE_ENDPOINT,
1352
+ skipped: true,
1353
+ reason: '页面 SSE 需要 passwordHash;contract-check 在未提供 GPT_IMAGE_APP_PASSWORD_HASH 时跳过。'
1354
+ });
1355
+ } else {
1356
+ checks.push(await checkPageSseContract());
1357
+ }
1358
+ }
1359
+
1360
  console.log(JSON.stringify({ ok: true, billable: false, checks }, null, 2));
1361
  }
1362
 
 
1377
  return { endpoint, status: response.status, error_code: result.error.code };
1378
  }
1379
 
1380
+ async function checkPageSseContract() {
1381
+ const formData = new FormData();
1382
+ formData.append('mode', 'generate');
1383
+ formData.append('prompt', 'contract check');
1384
+ formData.append('model', requestBody.model);
1385
+ formData.append('n', String(requestBody.n));
1386
+ formData.append('size', requestBody.size);
1387
+ formData.append('quality', requestBody.quality);
1388
+ formData.append('output_format', requestBody.output_format);
1389
+ formData.append('response_mode', requestBody.response_mode);
1390
+ formData.append('stream', 'true');
1391
+ formData.append('clientRequestId', 'x'.repeat(pageSseClientRequestIdMaxLength + 1));
1392
+ if (passwordHash) formData.append('passwordHash', passwordHash);
1393
+ const { response, text } = await fetchJson(`${baseUrl}${PAGE_SSE_ENDPOINT}`, {
1394
+ method: 'POST',
1395
+ body: formData,
1396
+ timeoutMs
1397
+ });
1398
+ if (response.status !== 400 || !text.includes('clientRequestId')) {
1399
+ console.error(JSON.stringify({ ok: false, billable: false, endpoint: PAGE_SSE_ENDPOINT, status: response.status, text }, null, 2));
1400
+ process.exit(1);
1401
+ }
1402
+ return {
1403
+ endpoint: PAGE_SSE_ENDPOINT,
1404
+ status: response.status,
1405
+ error_code: 'page_sse_client_request_id_too_long'
1406
+ };
1407
+ }
1408
+
1409
  function readOrchestrationEndpoint(capabilitiesValue) {
1410
  const endpoint = capabilitiesValue?.orchestration?.endpoint;
1411
  if (typeof endpoint !== 'string' || !endpoint.startsWith('/')) {
 
1606
  console.error('用法:generate-image.mjs [options] <prompt>');
1607
  console.error('默认只输出 dry-run;添加 --allow-billable 才会真实生图。');
1608
  console.error(
1609
+ '常用参数:--model --size --quality --n --format --output-compression --response-mode --image-backend --responses-model --gpt-model --thinking --prompt-optimization --force-web --stream-mode --streaming-strategy --partial-images --sse-log --timeout-ms --base-url --prompt-file --idempotency-key --check-remote --page-sse --agent --job --no-job(兼容别名)'
1610
  );
1611
  console.error(
1612
  '契约检查:GPT_IMAGE_AGENT_CONTRACT_CHECK=1 generate-image.mjs 或 generate-image.mjs --contract-check'
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":"34544ccd35f4321f92ab73ef5ac36603ee9f7720","created_at":"2026-06-24T14:38:59.964Z","deploy_id":"dee10b1a-4b9c-47bb-a7f3-89875a64183f"} 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":"7af3d19e476e03dfd25eef49672c982b46c64be8","created_at":"2026-06-26T01:06:33.777Z","deploy_id":"46b8343b-982b-4f3b-8526-ca1feabfd227"} as const;
4
 
5
  export const dynamic = 'force-dynamic';
6
 
src/lib/agent-api-contracts.test.ts CHANGED
@@ -439,7 +439,7 @@ describe('buildAgentCapabilities', () => {
439
  });
440
  assert.equal(
441
  capabilities.agent_streaming.page_sse.agent_usage,
442
- 'explicit_for_generate_recommended_for_high_resolution_edit_and_complex_batch'
443
  );
444
  assert.deepEqual(capabilities.upstream_request_headers.default, {
445
  user_agent_effective: 'gpt-image-playground/2.1.0',
 
439
  });
440
  assert.equal(
441
  capabilities.agent_streaming.page_sse.agent_usage,
442
+ 'explicit_for_generate_recommended_for_page_only_fields_high_resolution_edit_and_complex_batch'
443
  );
444
  assert.deepEqual(capabilities.upstream_request_headers.default, {
445
  user_agent_effective: 'gpt-image-playground/2.1.0',
src/lib/agent-api-contracts.ts CHANGED
@@ -334,7 +334,7 @@ export type AgentCapabilities = {
334
  source_header: 'Idempotency-Key';
335
  max_length: number;
336
  };
337
- agent_usage: 'explicit_for_generate_recommended_for_high_resolution_edit_and_complex_batch';
338
  };
339
  };
340
  orchestration: {
@@ -1013,7 +1013,7 @@ export function buildAgentCapabilities(env: Record<string, string | undefined>):
1013
  source_header: 'Idempotency-Key',
1014
  max_length: PAGE_SSE_CLIENT_REQUEST_ID_MAX_LENGTH
1015
  },
1016
- agent_usage: 'explicit_for_generate_recommended_for_high_resolution_edit_and_complex_batch'
1017
  }
1018
  },
1019
  orchestration: {
@@ -1050,7 +1050,7 @@ export function buildAgentCapabilities(env: Record<string, string | undefined>):
1050
  requires_new_idempotency_key_on_retry: true,
1051
  no_automatic_fallback: true
1052
  },
1053
- reason: 'High-resolution edit defaults to the page form-data SSE endpoint; if streaming has issues, diagnose first and explicitly fall back to Agent edit.'
1054
  },
1055
  complex_ui_batch: {
1056
  when: ['operation=generate_or_edit', 'complex_ui=true', 'batch=true'],
 
334
  source_header: 'Idempotency-Key';
335
  max_length: number;
336
  };
337
+ agent_usage: 'explicit_for_generate_recommended_for_page_only_fields_high_resolution_edit_and_complex_batch';
338
  };
339
  };
340
  orchestration: {
 
1013
  source_header: 'Idempotency-Key',
1014
  max_length: PAGE_SSE_CLIENT_REQUEST_ID_MAX_LENGTH
1015
  },
1016
+ agent_usage: 'explicit_for_generate_recommended_for_page_only_fields_high_resolution_edit_and_complex_batch'
1017
  }
1018
  },
1019
  orchestration: {
 
1050
  requires_new_idempotency_key_on_retry: true,
1051
  no_automatic_fallback: true
1052
  },
1053
+ reason: 'High-resolution edit uses the page form-data SSE endpoint because Agent edit has a fixed output contract; if streaming has issues, diagnose first and explicitly fall back to Agent edit.'
1054
  },
1055
  complex_ui_batch: {
1056
  when: ['operation=generate_or_edit', 'complex_ui=true', 'batch=true'],