Spaces:
Running
Running
Deploy 34af419 to Docker Space
Browse filesSource: MisonL/gpt-image-playground-customer@34af419d1e35b93f1874ba494453e0fab7ea54fe
- README.md +3 -3
- docs/deployment/huggingface-space-free.md +1 -1
- public/hf-space-deploy-marker.json +3 -3
- scripts/agent-skill-scripts.test.mjs +362 -85
- skills/gpt-image-playground-agent/SKILL.md +5 -5
- skills/gpt-image-playground-agent/references/api.md +8 -8
- skills/gpt-image-playground-agent/scripts/batch-images.mjs +69 -15
- skills/gpt-image-playground-agent/scripts/generate-image.mjs +10 -49
- src/app/api/deploy-marker/route.ts +1 -1
- src/lib/agent-api-contracts.test.ts +48 -6
- src/lib/agent-api-contracts.ts +75 -8
- src/lib/agent-image-service.ts +12 -6
- src/lib/agent-openapi.ts +14 -2
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`。
|
| 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/
|
| 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 或非流式路径。
|
| 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": "
|
| 4 |
-
"created_at": "2026-06-
|
| 5 |
-
"deploy_id": "
|
| 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('
|
| 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/
|
| 575 |
-
assert.equal(body.routing_guidance.transport, '
|
| 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/
|
| 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/
|
| 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
|
| 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('
|
| 2730 |
const requests = [];
|
|
|
|
|
|
|
| 2731 |
await withServer(
|
| 2732 |
-
(request, response) => {
|
| 2733 |
requests.push({ method: request.method, url: request.url });
|
| 2734 |
-
|
| 2735 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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,
|
| 2753 |
-
assert.equal(result.
|
| 2754 |
-
|
| 2755 |
-
assert.
|
|
|
|
| 2756 |
|
| 2757 |
-
const jobResult =
|
| 2758 |
-
'-
|
| 2759 |
-
|
| 2760 |
-
|
| 2761 |
-
|
| 2762 |
-
|
| 2763 |
-
|
| 2764 |
-
|
| 2765 |
-
|
| 2766 |
-
|
| 2767 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2768 |
}
|
| 2769 |
);
|
| 2770 |
});
|
| 2771 |
|
| 2772 |
-
it('
|
| 2773 |
const requests = [];
|
|
|
|
| 2774 |
await withServer(
|
| 2775 |
-
(request, response) => {
|
| 2776 |
requests.push({ method: request.method, url: request.url });
|
| 2777 |
-
|
| 2778 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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,
|
| 2797 |
-
assert.equal(result.
|
| 2798 |
-
|
| 2799 |
-
assert.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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/
|
| 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/
|
| 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/
|
| 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/
|
| 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/
|
| 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/
|
| 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/
|
| 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/
|
| 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/
|
| 4525 |
-
'POST /api/agent/
|
| 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/
|
| 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
|
| 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/
|
| 4783 |
pageSseRequestBody = await readRequestText(request);
|
| 4784 |
-
response.writeHead(
|
| 4785 |
response.end(
|
| 4786 |
-
|
| 4787 |
-
|
| 4788 |
-
|
| 4789 |
-
|
| 4790 |
-
|
| 4791 |
-
|
| 4792 |
-
|
|
|
|
| 4793 |
);
|
| 4794 |
return;
|
| 4795 |
}
|
| 4796 |
-
if (request.url === '/api/agent/
|
| 4797 |
-
response.writeHead(
|
| 4798 |
-
response.end(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4813 |
assert.deepEqual(
|
| 4814 |
requests.map((item) => `${item.method} ${item.url}`),
|
| 4815 |
-
['GET /api/agent/capabilities', 'POST /api/
|
| 4816 |
);
|
| 4817 |
-
|
| 4818 |
-
assert.
|
| 4819 |
-
assert.
|
|
|
|
| 4820 |
}
|
| 4821 |
);
|
| 4822 |
} finally {
|
|
@@ -4918,7 +5127,7 @@ describe('Agent skill script argument validation', () => {
|
|
| 4918 |
}
|
| 4919 |
});
|
| 4920 |
|
| 4921 |
-
it('
|
| 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(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4942 |
return;
|
| 4943 |
}
|
| 4944 |
-
if (request.url === '/api/agent/
|
| 4945 |
-
response.writeHead(
|
| 4946 |
-
response.end(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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,
|
| 4958 |
assert.equal(result.stderr.trim(), '');
|
| 4959 |
const body = JSON.parse(result.stdout);
|
| 4960 |
-
assert.equal(body.results[0].ok,
|
| 4961 |
-
assert.
|
| 4962 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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, '
|
| 4993 |
-
assert.equal(body.tasks[0].endpoint, '/api/agent/
|
|
|
|
| 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/
|
| 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/
|
| 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/
|
| 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/
|
| 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/
|
| 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,
|
| 5865 |
-
assert.
|
| 5866 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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/
|
| 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` 时
|
| 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` 中暴露真实执行路径。
|
| 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` 单独声明;
|
| 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
|
| 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`
|
| 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`:
|
| 63 |
-
- `--thinking`:
|
| 64 |
-
- `--prompt-optimization`:
|
| 65 |
-
- `--force-web`:
|
| 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` 时
|
| 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`
|
| 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.
|
| 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`。这些是
|
| 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_generation、Images API、SSE 或非流式路径;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
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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.
|
| 996 |
fallback_mode: 'manual_after_diagnosis',
|
| 997 |
reason
|
| 998 |
};
|
| 999 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1000 |
return {
|
| 1001 |
-
endpoint:
|
| 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.
|
| 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.
|
| 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
|
| 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.
|
| 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.
|
| 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 或
|
| 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 显式选择
|
| 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'
|
| 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
|
| 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":"
|
| 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.
|
| 493 |
-
assert.equal(capabilities.routing_rules.
|
| 494 |
-
assert.equal(capabilities.routing_rules.
|
| 495 |
-
assert.equal(capabilities.routing_rules.
|
| 496 |
-
assert.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 497 |
operation: 'generate',
|
| 498 |
-
|
| 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 |
-
'
|
| 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 |
-
|
| 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 |
-
|
| 1117 |
-
when: ['operation=generate', '
|
| 1118 |
conditions: {
|
| 1119 |
operation: 'generate',
|
| 1120 |
-
|
| 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.
|
| 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
|
| 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 |
-
'
|
| 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 |
-
|
| 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,
|