misonL commited on
Commit
f23ee1d
·
verified ·
1 Parent(s): 30346a3

Deploy bf62eed to Docker Space

Browse files

Source: MisonL/gpt-image-playground-customer@bf62eed15cedbc56fb8df227f2d76273004d494e

public/hf-space-deploy-marker.json CHANGED
@@ -1,6 +1,6 @@
1
  {
2
  "schema_version": 1,
3
- "local_sha": "ea6a3d8444044cb6d5357f63688e29b8448df8c1",
4
- "created_at": "2026-06-26T08:40:21.663Z",
5
- "deploy_id": "6abe5272-c17b-447f-8990-e37a807c71aa"
6
  }
 
1
  {
2
  "schema_version": 1,
3
+ "local_sha": "bf62eed15cedbc56fb8df227f2d76273004d494e",
4
+ "created_at": "2026-06-26T09:26:30.691Z",
5
+ "deploy_id": "5b30e427-0d60-4ce4-b6ce-667ada74c8c0"
6
  }
src/app/api/agent/agent-routes.test.ts CHANGED
@@ -785,6 +785,42 @@ describe('Agent route integration', () => {
785
  }
786
  });
787
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
788
  it('uses force-sse for Agent Responses image_generation SSE while keeping the final JSON contract', async () => {
789
  const { generateImage } = await loadAgentRoutes();
790
  let upstreamBody = '';
@@ -3462,6 +3498,29 @@ async function startStreamingResponsesImageUpstream(
3462
  };
3463
  }
3464
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3465
  function readStoredResponseJson(idempotencyKey: string): string {
3466
  const db = new Database(path.join(tempDir, 'agent.sqlite'), { readonly: true });
3467
  try {
 
785
  }
786
  });
787
 
788
+ it('marks Responses image_generation-disabled 403s as unavailable request modes', async () => {
789
+ process.env.AGENT_STATE_BACKEND = 'memory';
790
+ delete process.env.AGENT_SQLITE_PATH;
791
+ const { generateImage } = await loadAgentRoutes();
792
+ const upstream = await startResponsesImageJsonUpstream(403, {
793
+ error: { message: 'Image generation is not enabled for this group' }
794
+ });
795
+ process.env.ENABLE_RESPONSES_IMAGE_BACKEND = 'true';
796
+ process.env.OPENAI_RESPONSES_API_MODEL = 'gpt-5.4';
797
+ process.env.OPENAI_API_KEY = 'test-key';
798
+ process.env.OPENAI_API_BASE_URL = upstream.baseUrl;
799
+
800
+ try {
801
+ const response = await generateImage(
802
+ agentJsonRequest('agent-responses-image-disabled-key', {
803
+ prompt: 'agent responses disabled',
804
+ image_backend: 'responses-image-generation',
805
+ streaming_strategy: 'responses-sse',
806
+ partial_images: 2
807
+ })
808
+ );
809
+
810
+ assert.equal(response.status, 502);
811
+ const body = await response.json();
812
+ assert.equal(body.error.code, 'upstream_unavailable');
813
+ assert.equal(body.error.upstream_status, 403);
814
+ assert.equal(body.error.diagnostics.channel_request_mode, 'responses-sse');
815
+ assert.equal(body.error.diagnostics.channel_cooldown_scope, 'channel');
816
+ assert.equal(body.error.diagnostics.cooldown_target.channel_id, 'default');
817
+ assert.equal(body.error.diagnostics.cooldown_target.request_mode, 'responses-sse');
818
+ assert.equal(JSON.stringify(body).includes('test-key'), false);
819
+ } finally {
820
+ await upstream.close();
821
+ }
822
+ });
823
+
824
  it('uses force-sse for Agent Responses image_generation SSE while keeping the final JSON contract', async () => {
825
  const { generateImage } = await loadAgentRoutes();
826
  let upstreamBody = '';
 
3498
  };
3499
  }
3500
 
3501
+ async function startResponsesImageJsonUpstream(
3502
+ status: number,
3503
+ body: unknown
3504
+ ): Promise<{ baseUrl: string; close: () => Promise<void> }> {
3505
+ const server = http.createServer((request, response) => {
3506
+ if (request.method !== 'POST' || !request.url?.endsWith('/responses')) {
3507
+ response.writeHead(404, { 'Content-Type': 'application/json' });
3508
+ response.end(JSON.stringify({ error: { message: 'not found' } }));
3509
+ return;
3510
+ }
3511
+ request.resume();
3512
+ response.writeHead(status, { 'Content-Type': 'application/json' });
3513
+ response.end(JSON.stringify(body));
3514
+ });
3515
+ await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve));
3516
+ const address = server.address();
3517
+ assert.ok(address && typeof address === 'object');
3518
+ return {
3519
+ baseUrl: `http://127.0.0.1:${address.port}/v1`,
3520
+ close: () => new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())))
3521
+ };
3522
+ }
3523
+
3524
  function readStoredResponseJson(idempotencyKey: string): string {
3525
  const db = new Database(path.join(tempDir, 'agent.sqlite'), { readonly: true });
3526
  try {
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":"ea6a3d8444044cb6d5357f63688e29b8448df8c1","created_at":"2026-06-26T08:40:21.663Z","deploy_id":"6abe5272-c17b-447f-8990-e37a807c71aa"} as const;
4
 
5
  export const dynamic = 'force-dynamic';
6
 
 
1
  import { NextResponse } from 'next/server';
2
 
3
+ const deployMarker = {"schema_version":1,"local_sha":"bf62eed15cedbc56fb8df227f2d76273004d494e","created_at":"2026-06-26T09:26:30.691Z","deploy_id":"5b30e427-0d60-4ce4-b6ce-667ada74c8c0"} as const;
4
 
5
  export const dynamic = 'force-dynamic';
6
 
src/lib/agent-image-service.ts CHANGED
@@ -45,6 +45,7 @@ import {
45
  type ChannelFailureReport,
46
  describeChannelFailure,
47
  isChannelFailure,
 
48
  isCredentialFailure,
49
  resolveEffectiveCredential
50
  } from './channel-router';
@@ -1067,6 +1068,20 @@ function reportServerCredentialFailure(
1067
  ): ChannelFailureReport | undefined {
1068
  const serverChannelRouter = getServerChannelState().router;
1069
  if (!credential || !serverChannelRouter) return undefined;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1070
  if (isChannelFailure(error)) {
1071
  const reason = {
1072
  ...describeChannelFailure(error, 'channel'),
 
45
  type ChannelFailureReport,
46
  describeChannelFailure,
47
  isChannelFailure,
48
+ isChannelRequestModeFailure,
49
  isCredentialFailure,
50
  resolveEffectiveCredential
51
  } from './channel-router';
 
1068
  ): ChannelFailureReport | undefined {
1069
  const serverChannelRouter = getServerChannelState().router;
1070
  if (!credential || !serverChannelRouter) return undefined;
1071
+ if (isChannelRequestModeFailure(error, requestMode)) {
1072
+ const reason = {
1073
+ ...describeChannelFailure(error, 'channel'),
1074
+ ...(requestMode ? { requestMode } : {})
1075
+ };
1076
+ const report = serverChannelRouter.reportFailure(credential, { scope: 'channel', requestMode, reason });
1077
+ appLogger.warn(
1078
+ report.cooldownApplied
1079
+ ? `Temporarily cooling down API channel request mode: ${credential.channelId}/${requestMode}`
1080
+ : `Recording API channel request mode failure without cooldown: ${credential.channelId}/${requestMode}`,
1081
+ reason
1082
+ );
1083
+ return report;
1084
+ }
1085
  if (isChannelFailure(error)) {
1086
  const reason = {
1087
  ...describeChannelFailure(error, 'channel'),
src/lib/api-error-response.test.ts CHANGED
@@ -7,6 +7,7 @@ import {
7
  toTerminalAgentErrorBody
8
  } from './api-error-response';
9
  import { RequestValidationError } from './image-request-utils';
 
10
  import assert from 'node:assert/strict';
11
  import { describe, it } from 'node:test';
12
 
@@ -76,6 +77,23 @@ describe('normalizeAgentError', () => {
76
  assert.equal(error.diagnostics?.channel_cooldown_scope, 'channel');
77
  });
78
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  it('classifies transport failures into machine-readable kinds', () => {
80
  assert.equal(
81
  normalizeAgentError(Object.assign(new Error('fetch failed'), { code: 'ENOTFOUND' })).diagnostics
 
7
  toTerminalAgentErrorBody
8
  } from './api-error-response';
9
  import { RequestValidationError } from './image-request-utils';
10
+ import { type ChannelRequestMode } from './channel-request-mode';
11
  import assert from 'node:assert/strict';
12
  import { describe, it } from 'node:test';
13
 
 
77
  assert.equal(error.diagnostics?.channel_cooldown_scope, 'channel');
78
  });
79
 
80
+ it('maps Responses image generation disabled 403s to unavailable channel failures', () => {
81
+ const error = normalizeAgentError(
82
+ Object.assign(new Error('status_code=403, Image generation is not enabled for this group'), {
83
+ status: 403
84
+ }),
85
+ {
86
+ channel_request_mode: 'responses-sse' as ChannelRequestMode
87
+ }
88
+ );
89
+
90
+ assert.equal(error.code, 'upstream_unavailable');
91
+ assert.equal(error.status, 502);
92
+ assert.equal(error.retryable, true);
93
+ assert.equal(error.upstreamStatus, 403);
94
+ assert.equal(error.diagnostics?.channel_cooldown_scope, 'channel');
95
+ });
96
+
97
  it('classifies transport failures into machine-readable kinds', () => {
98
  assert.equal(
99
  normalizeAgentError(Object.assign(new Error('fetch failed'), { code: 'ENOTFOUND' })).diagnostics
src/lib/api-error-response.ts CHANGED
@@ -1,5 +1,5 @@
1
  import { RequestValidationError } from './image-request-utils';
2
- import { isChannelFailure } from './channel-router';
3
  import { ChannelCapacityQueueError } from './channel-capacity-queue';
4
  import {
5
  CHANNEL_REQUEST_MODES,
@@ -469,6 +469,23 @@ export function normalizeAgentError(error: unknown, diagnostics: AgentErrorDiagn
469
 
470
  const status = readNumberField(error, 'status') ?? readNumberField(error, 'statusCode');
471
  const message = error instanceof Error ? error.message : (readStringField(error, 'message') ?? '发生未知错误。');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
472
  if (error instanceof ChannelCapacityQueueError) {
473
  const retryAfterSeconds = Math.max(1, Math.ceil(Number(error.details.max_wait_ms ?? 30_000) / 1000));
474
  return new AgentApiError({
 
1
  import { RequestValidationError } from './image-request-utils';
2
+ import { isChannelFailure, isChannelRequestModeFailure } from './channel-router';
3
  import { ChannelCapacityQueueError } from './channel-capacity-queue';
4
  import {
5
  CHANNEL_REQUEST_MODES,
 
469
 
470
  const status = readNumberField(error, 'status') ?? readNumberField(error, 'statusCode');
471
  const message = error instanceof Error ? error.message : (readStringField(error, 'message') ?? '发生未知错误。');
472
+ if (isChannelRequestModeFailure(error, diagnostics.channel_request_mode)) {
473
+ const retryAfterSeconds = readRetryAfterSeconds(error) ?? 15;
474
+ return new AgentApiError({
475
+ code: 'upstream_unavailable',
476
+ message,
477
+ status: 502,
478
+ retryable: true,
479
+ upstreamStatus: 403,
480
+ retryAfterSeconds,
481
+ diagnostics: buildDiagnostics(error, {
482
+ ...diagnostics,
483
+ upstreamStatus: 403,
484
+ retryAfterSeconds,
485
+ channel_cooldown_scope: 'channel'
486
+ })
487
+ });
488
+ }
489
  if (error instanceof ChannelCapacityQueueError) {
490
  const retryAfterSeconds = Math.max(1, Math.ceil(Number(error.details.max_wait_ms ?? 30_000) / 1000));
491
  return new AgentApiError({
src/lib/channel-router.test.ts CHANGED
@@ -3,6 +3,7 @@ import {
3
  describeChannelFailure,
4
  getChannelPoolSummary,
5
  isChannelFailure,
 
6
  isCredentialFailure,
7
  parseChannelPoolConfig,
8
  resolveEffectiveCredential,
@@ -1559,6 +1560,35 @@ describe('isCredentialFailure', () => {
1559
  });
1560
  });
1561
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1562
  describe('isChannelFailure', () => {
1563
  it('treats upstream 5xx and CDN timeout statuses as channel failures', () => {
1564
  [500, 502, 503, 504, 520, 522, 523, 524].forEach((status) => {
 
3
  describeChannelFailure,
4
  getChannelPoolSummary,
5
  isChannelFailure,
6
+ isChannelRequestModeFailure,
7
  isCredentialFailure,
8
  parseChannelPoolConfig,
9
  resolveEffectiveCredential,
 
1560
  });
1561
  });
1562
 
1563
+ describe('isChannelRequestModeFailure', () => {
1564
+ it('treats Responses image_generation-disabled 403 as a request-mode failure', () => {
1565
+ assert.equal(
1566
+ isChannelRequestModeFailure(
1567
+ new Error('status_code=403, Image generation is not enabled for this group'),
1568
+ 'responses-sse'
1569
+ ),
1570
+ true
1571
+ );
1572
+ assert.equal(
1573
+ isChannelRequestModeFailure(
1574
+ { status: 403, error: { message: 'Image generation is not enabled for this group' } },
1575
+ 'responses-non-stream'
1576
+ ),
1577
+ true
1578
+ );
1579
+ });
1580
+
1581
+ it('does not apply the Responses image_generation-disabled rule to Images API modes', () => {
1582
+ assert.equal(
1583
+ isChannelRequestModeFailure(
1584
+ { status: 403, message: 'Image generation is not enabled for this group' },
1585
+ 'images-sse'
1586
+ ),
1587
+ false
1588
+ );
1589
+ });
1590
+ });
1591
+
1592
  describe('isChannelFailure', () => {
1593
  it('treats upstream 5xx and CDN timeout statuses as channel failures', () => {
1594
  [500, 502, 503, 504, 520, 522, 523, 524].forEach((status) => {
src/lib/channel-router.ts CHANGED
@@ -755,6 +755,18 @@ export function isCredentialFailure(error: unknown): boolean {
755
  );
756
  }
757
 
 
 
 
 
 
 
 
 
 
 
 
 
758
  export function isChannelFailure(error: unknown): boolean {
759
  if (error instanceof ChannelCapacityQueueError) {
760
  return false;
@@ -1044,7 +1056,7 @@ function readErrorNumber(error: unknown, fieldName: string): number | undefined
1044
  }
1045
 
1046
  function readStatusField(error: unknown): { status?: number } {
1047
- const value = readErrorNumber(error, 'status') ?? readNestedErrorNumber(error, 'status');
1048
  return value === undefined ? {} : { status: value };
1049
  }
1050
 
@@ -1090,6 +1102,39 @@ function readNestedErrorNumber(error: unknown, fieldName: string): number | unde
1090
  return readErrorNumber((error as { error?: unknown }).error, fieldName);
1091
  }
1092
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1093
  function isConnectionFailure(error: unknown): boolean {
1094
  const name = readErrorString(error, 'name') || readConstructorName(error);
1095
  if (name === 'APIConnectionError' || name === 'APIConnectionTimeoutError') {
 
755
  );
756
  }
757
 
758
+ export function isChannelRequestModeFailure(error: unknown, requestMode?: ChannelRequestMode): boolean {
759
+ if (!requestMode || !requestMode.startsWith('responses-')) {
760
+ return false;
761
+ }
762
+ const status = readErrorStatusCode(error);
763
+ if (status !== 403) {
764
+ return false;
765
+ }
766
+ const message = readErrorMessage(error);
767
+ return /image generation is not enabled for this group/i.test(message);
768
+ }
769
+
770
  export function isChannelFailure(error: unknown): boolean {
771
  if (error instanceof ChannelCapacityQueueError) {
772
  return false;
 
1056
  }
1057
 
1058
  function readStatusField(error: unknown): { status?: number } {
1059
+ const value = readErrorStatusCode(error);
1060
  return value === undefined ? {} : { status: value };
1061
  }
1062
 
 
1102
  return readErrorNumber((error as { error?: unknown }).error, fieldName);
1103
  }
1104
 
1105
+ function readErrorStatusCode(error: unknown): number | undefined {
1106
+ return (
1107
+ readErrorNumber(error, 'status') ??
1108
+ readErrorNumber(error, 'statusCode') ??
1109
+ readErrorNumber(error, 'status_code') ??
1110
+ readNestedErrorNumber(error, 'status') ??
1111
+ readNestedErrorNumber(error, 'statusCode') ??
1112
+ readNestedErrorNumber(error, 'status_code') ??
1113
+ readStatusCodeFromMessage(readErrorMessage(error))
1114
+ );
1115
+ }
1116
+
1117
+ function readErrorMessage(error: unknown): string {
1118
+ if (typeof error === 'string') return error;
1119
+ return [
1120
+ readErrorString(error, 'message'),
1121
+ readNestedErrorString(error, 'message'),
1122
+ readErrorString(error, 'detail'),
1123
+ readNestedErrorString(error, 'detail'),
1124
+ readErrorString(error, 'raw'),
1125
+ readNestedErrorString(error, 'raw')
1126
+ ]
1127
+ .filter((value): value is string => Boolean(value))
1128
+ .join('\n');
1129
+ }
1130
+
1131
+ function readStatusCodeFromMessage(message: string): number | undefined {
1132
+ const match = /\bstatus(?:_code)?\s*=\s*(\d{3})\b/i.exec(message);
1133
+ if (!match) return undefined;
1134
+ const status = Number(match[1]);
1135
+ return Number.isInteger(status) ? status : undefined;
1136
+ }
1137
+
1138
  function isConnectionFailure(error: unknown): boolean {
1139
  const name = readErrorString(error, 'name') || readConstructorName(error);
1140
  if (name === 'APIConnectionError' || name === 'APIConnectionTimeoutError') {
src/lib/image-route-support.ts CHANGED
@@ -4,6 +4,7 @@ import {
4
  type ChannelCredential,
5
  describeChannelFailure,
6
  isChannelFailure,
 
7
  isCredentialFailure
8
  } from './channel-router';
9
  import type { ChannelRequestMode } from './channel-request-mode';
@@ -52,6 +53,20 @@ export function reportServerCredentialFailure(
52
  ) {
53
  const serverChannelRouter = getServerChannelState().router;
54
  if (!credential || !serverChannelRouter) return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  if (isChannelFailure(error)) {
56
  const reason = {
57
  ...describeChannelFailure(error, 'channel'),
 
4
  type ChannelCredential,
5
  describeChannelFailure,
6
  isChannelFailure,
7
+ isChannelRequestModeFailure,
8
  isCredentialFailure
9
  } from './channel-router';
10
  import type { ChannelRequestMode } from './channel-request-mode';
 
53
  ) {
54
  const serverChannelRouter = getServerChannelState().router;
55
  if (!credential || !serverChannelRouter) return;
56
+ if (isChannelRequestModeFailure(error, requestMode)) {
57
+ const reason = {
58
+ ...describeChannelFailure(error, 'channel'),
59
+ ...(requestMode ? { requestMode } : {})
60
+ };
61
+ const report = serverChannelRouter.reportFailure(credential, { scope: 'channel', requestMode, reason });
62
+ appLogger.warn(
63
+ report.cooldownApplied
64
+ ? `暂时冷却 API 渠道请求方式:${credential.channelId}/${requestMode}`
65
+ : `记录 API 渠道请求方式失败,未启用冷却:${credential.channelId}/${requestMode}`,
66
+ reason
67
+ );
68
+ return;
69
+ }
70
  if (isChannelFailure(error)) {
71
  const reason = {
72
  ...describeChannelFailure(error, 'channel'),