import { AGENT_ENDPOINTS, AGENT_JOB_ENDPOINTS } from './agent-api-paths.mjs'; import type { AgentErrorDiagnostics } from './api-error-response'; import { readAppLogRetentionMetadata, type AppLogRetentionMetadata } from './app-log-retention'; import { CHANNEL_REQUEST_MODES, CHANNEL_REQUEST_MODE_ADMIN_CONTROL, type ChannelRequestMode, type ChannelRequestModeDecision } from './channel-request-mode'; import { getChannelPoolSummary, parseChannelPoolConfig } from './channel-router'; import { MAX_IMAGE_COUNT, MAX_PROMPT_LENGTH, RequestValidationError, validateApiBaseUrl, type GptImageModel, type ValidOutputFormat } from './image-request-utils'; import { summarizeImageUpstreamProfile, summarizeUpstreamRequestHeaders, clampIntegerToRange, getPartialImagesRangeForBackend, type ImageUpstreamProfile, type ImageUpstreamProfileId, type ImageUpstreamProfileSummary, type PartialImagesCount, type UpstreamRequestHeaderSummary } from './image-upstream-profile'; import { parseImageGenerationBackendValue, parseImageStreamModeValue, parseImageStreamingStrategyValue, resolveImageStreamEnabled, type ImageGenerationBackend, type ImageStreamMode, type ImageStreamingStrategy } from './image-upstream-strategy'; import { summarizeOpenAIImageTransport, type UpstreamProxySummary } from './openai-image-transport'; import { CHINESE_POSITIVE_INTEGER_MESSAGES, readPositiveIntegerFromEnv } from './positive-integer-config.mjs'; import { readBooleanEnv } from './server-runtime'; import { GPT_IMAGE_2_EDGE_MULTIPLE, GPT_IMAGE_2_MAX_ASPECT, GPT_IMAGE_2_MAX_EDGE, GPT_IMAGE_2_MAX_PIXELS, GPT_IMAGE_2_MIN_PIXELS, validateGptImage2Size, validatePositiveIntegerImageSize } from './size-utils'; export const AGENT_API_VERSION = '1.0.0'; export const AGENT_SCHEMA_VERSION = '2026-05-28'; export const AGENT_DEFAULT_SQLITE_PATH = 'generated-images/.agent-state/agent.sqlite'; export const AGENT_DEFAULT_LEASE_MS = 10 * 60 * 1000; export const AGENT_DEFAULT_REQUEST_TTL_SECONDS = 24 * 60 * 60; export const AGENT_DEFAULT_RECOVERY_INTERVAL_MS = 30 * 1000; export const AGENT_PAGE_REQUEST_DIAGNOSTICS_NO_MATCH_HINT = 'matched_log_count=0 表示当前保留窗口内没有匹配日志;可能已被保留条数淘汰、被 APP_LOG_LEVEL 过滤,或本地日志文件被清理。'; export const AGENT_MODELS = ['gpt-image-1', 'gpt-image-1-mini', 'gpt-image-1.5', 'gpt-image-2'] as const; export const AGENT_OUTPUT_FORMATS = ['png', 'jpeg', 'webp'] as const; const AGENT_DEFAULT_OUTPUT_FORMAT = 'webp'; const AGENT_DEFAULT_LOSSY_OUTPUT_COMPRESSION = 100; export const AGENT_RESPONSE_MODES = ['path', 'base64', 'both'] as const; export const AGENT_QUALITIES = ['low', 'medium', 'high', 'auto'] as const; export const AGENT_BACKGROUNDS = ['transparent', 'opaque', 'auto'] as const; export const AGENT_MODERATIONS = ['low', 'auto'] as const; export const AGENT_LEGACY_SIZES = ['auto', '1024x1024', '1536x1024', '1024x1536'] as const; export const AGENT_JOB_STATES = ['queued', 'running', 'succeeded', 'failed', 'expired'] as const; export const AGENT_IMAGE_BACKENDS = ['images-api', 'responses-image-generation'] as const; export const PAGE_SSE_CLIENT_REQUEST_ID_MAX_LENGTH = 128; export const AGENT_STREAMING_STRATEGIES = [ 'off', 'auto', 'openai-sse', 'newapi-keepalive-sse', 'responses-sse', 'force-sse' ] as const; export const AGENT_STREAM_MODES = ['auto', 'stream', 'non_stream'] as const; export const AGENT_UPSTREAM_SSE_ACTIVATION_STRATEGIES = [ 'auto', 'openai-sse', 'newapi-keepalive-sse', 'responses-sse', 'force-sse' ] as const; export const AGENT_UPSTREAM_SSE_GENERATE_REQUEST_FIELDS = [ 'image_backend', 'stream_mode', 'streaming_strategy', 'partial_images' ] as const; export const AGENT_UPSTREAM_SSE_EDIT_REQUEST_FIELDS = ['stream_mode', 'streaming_strategy', 'partial_images'] as const; export const AGENT_PAGE_SSE_AGENT_USAGE = 'explicit_diagnostics_for_generate_recommended_for_default_webp_edit_high_resolution_edit_long_image_recovery_and_complex_batch' as const; const AGENT_DEFAULT_PARTIAL_IMAGES = 2; export type AgentStateBackend = 'memory' | 'sqlite' | 'postgres'; export type AgentAuthScheme = 'bearer' | 'x-app-password-hash'; export type PageSseAuthScheme = 'form-password-hash'; export type AgentResponseMode = (typeof AGENT_RESPONSE_MODES)[number]; export type AgentQuality = (typeof AGENT_QUALITIES)[number]; export type AgentBackground = (typeof AGENT_BACKGROUNDS)[number]; export type AgentModeration = (typeof AGENT_MODERATIONS)[number]; export type AgentJobState = (typeof AGENT_JOB_STATES)[number]; export type AgentRoutingTransport = 'agent_json' | 'agent_job_polling' | 'page_sse'; export type AgentRoutingStrength = 'default' | 'recommended' | 'explicit'; export type AgentOrchestrationPolicy = 'server_orchestrated_generate_v1'; export type ImageBackendRuntimeRequirement = { supported: true; enabled: boolean; required_env: string[]; missing_env: string[]; }; export type AgentPageRequestDiagnosticsRetention = AppLogRetentionMetadata & { bounded: true; not_agent_state_backend: true; }; export type AgentRequestDiagnosticsRetention = { storage: 'agent_state'; ttl_seconds: number; bounded: true; loss_modes: ['request_expired_by_ttl', 'artifact_deleted_or_purged', 'state_backend_reset']; }; export type AgentPageRequestDiagnosticsNote = { code: 'no_matching_logs_in_retention_window'; message: string; retention: AgentPageRequestDiagnosticsRetention; }; export type AgentGenerateRequest = { model: GptImageModel; prompt: string; n: number; size: string; quality: AgentQuality; output_format: ValidOutputFormat; output_compression?: number; background: AgentBackground; moderation: AgentModeration; response_mode: AgentResponseMode; image_backend: ImageGenerationBackend; stream_mode: ImageStreamMode; streaming_strategy: ImageStreamingStrategy; partial_images: 0 | 1 | 2 | 3 | 4; responsesModel?: string; thinking?: 'minimal' | 'none' | 'low' | 'medium' | 'high' | 'xhigh'; promptOptimization?: boolean; force_web?: boolean; force_request?: boolean; }; export type AgentImageResponseItem = { id: string; filename: string; content_url: string; metadata_url: string; output_format: string; mime_type: string; size_bytes: number; width: number | null; height: number | null; b64_json?: string; }; export type AgentImageResponseTiming = { started_at: string; completed_at: string; elapsed_ms: number; server_elapsed_ms: number; }; export type AgentImageResponseExecution = { transport: AgentRoutingTransport; endpoint: string; route_mode: 'agent' | 'job' | 'page_sse'; operation: 'generate' | 'edit'; image_backend: ImageGenerationBackend; stream_mode: ImageStreamMode; streaming_strategy: ImageStreamingStrategy; channel_request_mode?: ChannelRequestMode; channel_request_mode_fallback_applied?: boolean; route_decision?: ChannelRequestModeDecision; selected_channel_id?: string; upstream_host?: string; request_headers: UpstreamRequestHeaderSummary; }; export type AgentImageResponse = { request_id: string; idempotency_key: string; cached: boolean; images: AgentImageResponseItem[]; usage?: unknown; created_at: string; timing?: AgentImageResponseTiming; execution?: AgentImageResponseExecution; }; export type AgentJobStatusResponse = { job: { id: string; request_id: string; idempotency_key: string; mode: 'generate' | 'edit'; state: AgentJobState; created_at: string; updated_at: string; expires_at: string; result_url?: string; retry_after_seconds?: number; error?: { code: string; message: string; retryable: boolean; details?: Record; upstream_status?: number; diagnostics?: AgentErrorDiagnostics; }; }; }; export type AgentCapabilities = { api_version: string; schema_version: string; auth: { required: boolean; schemes: AgentAuthScheme[]; }; endpoints: Record; image_transport: { upstream_timeout_ms: number; stream_data_interval_timeout_ms: number; upstream_max_retries: number; upstream_proxy: UpstreamProxySummary; }; upstream_profile: ImageUpstreamProfileSummary; upstream_request_headers: { default: UpstreamRequestHeaderSummary; channels: Array<{ id: string; upstream_proxy: UpstreamProxySummary; request_modes: readonly ChannelRequestMode[]; request_mode_priority: readonly ChannelRequestMode[]; request_headers: UpstreamRequestHeaderSummary; }>; }; request_mode_controls: { source: 'admin_env_whitelist'; global_env: string; channel_env_pattern: string; global_priority_env: string; channel_priority_env_pattern: string; default_priority: readonly ChannelRequestMode[]; default_priority_policy: 'lowest_cost_first'; mutable_at_runtime: false; agent_client_policy: 'diagnostics_only'; final_gate_command: string; smoke_gate_commands: Record; }; defaults: { model: GptImageModel; response_mode: AgentResponseMode; state_backend: AgentStateBackend; image_backend: ImageGenerationBackend; stream_mode: ImageStreamMode; streaming_strategy: ImageStreamingStrategy; partial_images: PartialImagesCount; }; limits: { max_prompt_length: number; max_images: number; generate_images: { min: number; max: number }; edit_images: { min: number; max: number }; upload_images: { max: number }; max_upload_mb: number; max_total_upload_mb?: number; partial_images: { min: number; max: number }; partial_images_by_backend: Record; upstream_profile: ImageUpstreamProfileId; upstream_profile_mixed: boolean; }; force_request_controls: { field: 'force_request'; cli_flag: '--force-request'; default: false; effect: 'skip_local_upstream_profile_size_and_background_validation'; still_enforced: readonly string[]; intended_for: readonly string[]; }; model_limits: { 'gpt-image-2': { max_edge: number; max_pixels: number; edge_multiple: number; max_aspect: number; min_pixels: number; size_policy: ImageUpstreamProfile['gptImage2']['sizePolicy']; allow_transparent_background: boolean; recommended_presets: Array<{ name: string; size: string; purpose: string }>; large_image_risk: { applies_to: string[]; guidance: string; }; }; }; agent_streaming: { generate: { supported: false; mode: 'non_streaming_only'; endpoint: string; }; edit: { supported: false; mode: 'non_streaming_only'; endpoint: string; }; upstream_sse: { supported: true; mode: 'internal_upstream_sse'; endpoint: string; request_fields: typeof AGENT_UPSTREAM_SSE_GENERATE_REQUEST_FIELDS; request_fields_by_mode: { generate: typeof AGENT_UPSTREAM_SSE_GENERATE_REQUEST_FIELDS; edit: typeof AGENT_UPSTREAM_SSE_EDIT_REQUEST_FIELDS; }; image_backends: readonly ImageGenerationBackend[]; enabled_image_backends: readonly ImageGenerationBackend[]; streaming_strategies: readonly ImageStreamingStrategy[]; stream_modes: readonly ImageStreamMode[]; activation_strategies: readonly ImageStreamingStrategy[]; final_response_contract: 'AgentImageResponse'; }; page_sse: { supported: true; mode: 'form_data_sse'; endpoint: string; contract: 'page_ui_only'; transport_contract: 'page_form_data_sse'; auth: { required: boolean; schemes: PageSseAuthScheme[]; form_field: 'passwordHash'; }; client_request_id: { form_field: 'clientRequestId'; source_header: 'Idempotency-Key'; max_length: number; }; agent_usage: typeof AGENT_PAGE_SSE_AGENT_USAGE; }; }; orchestration: { supported: true; policy: AgentOrchestrationPolicy; endpoint: string; client_contract: 'intent_only'; transport_selection: 'server_owned'; result_mode: 'job_polling'; hidden_controls: readonly string[]; diagnostics: { job_result: string; request_lookup: string; }; current_guidance: string; }; routing_rules: { high_resolution_edit: AgentRoutingRule; complex_ui_batch: AgentRoutingRule; long_image_recovery: AgentRoutingRule; agent_generate_small_smoke: AgentRoutingRule; page_sse_generate_diagnostics: AgentRoutingRule; retry_recovery: { reuse_failed_idempotency_key: false; new_attempt_guidance: string; }; }; agent_jobs: { supported: true; mode: 'job_polling'; intended_for: string[]; endpoints: { create_generate_job: string; get_job: string; get_job_result: string; }; states: readonly string[]; current_guidance: string; }; supported: { models: readonly string[]; output_formats: readonly string[]; response_modes: readonly string[]; qualities: readonly string[]; backgrounds: readonly string[]; moderations: readonly string[]; legacy_sizes: readonly string[]; image_backends: readonly ImageGenerationBackend[]; enabled_image_backends: readonly ImageGenerationBackend[]; image_backend_requirements: Record; request_modes: readonly ChannelRequestMode[]; streaming_strategies: readonly ImageStreamingStrategy[]; stream_modes: readonly ImageStreamMode[]; }; storage: { image_storage_mode: string; postgres_configured: boolean; }; page_request_diagnostics: { supported: true; source: 'app_log'; endpoints: { single: string; batch: string; }; retention: AgentPageRequestDiagnosticsRetention; no_match_hint: string; }; agent_request_diagnostics: { supported: true; source: 'agent_state'; endpoints: { lookup: string; single: string; }; lookup: { by_request_id: true; by_idempotency_key: true; }; retention: AgentRequestDiagnosticsRetention; }; channel_health_diagnostics: { supported: true; endpoint: string; source: 'in_process_channel_router'; state_scope: 'process_local'; billable: false; }; idempotency: { required: boolean; header: string; ttl_seconds: number; }; }; export type AgentRoutingRule = { when: string[]; conditions: AgentRoutingCondition; endpoint: string; transport: AgentRoutingTransport; strength: AgentRoutingStrength; action: AgentRoutingAction; reason: string; }; export type AgentRoutingCondition = { operation: 'generate' | 'edit' | 'generate_or_edit'; explicit_page_sse?: boolean; max_edge?: { operator: 'gt' | 'lte'; value: number; }; batch?: boolean; single_request?: boolean; complex_ui?: boolean; long_image?: boolean; resume_or_recover?: boolean; }; export type AgentRoutingAction = { endpoint: string; transport: AgentRoutingTransport; strength: AgentRoutingStrength; fallback_endpoint?: string; fallback_mode?: 'manual_after_diagnosis' | 'fix_request_before_retry'; requires_new_idempotency_key_on_retry: boolean; no_automatic_fallback: boolean; }; type FieldErrors = Record; function isOneOf(value: string, allowed: T): value is T[number] { return (allowed as readonly string[]).includes(value); } function readStringField(body: Record, field: string, fallback?: string): string | undefined { const value = body[field]; if (value === undefined || value === null) return fallback; return typeof value === 'string' ? value : undefined; } function readIntegerField( body: Record, field: string, fallback: number, min: number, max: number, fields: FieldErrors ): number { const value = body[field]; if (value === undefined || value === null || value === '') return fallback; const parsed = typeof value === 'number' ? value : typeof value === 'string' && /^\d+$/.test(value) ? Number(value) : NaN; if (!Number.isInteger(parsed) || parsed < min || parsed > max) { fields[field] = `必须是 ${min} 到 ${max} 之间的整数`; return fallback; } return parsed; } function validatePrompt(value: unknown, fields: FieldErrors): string { if (typeof value !== 'string' || value.trim().length === 0) { fields.prompt = '必填'; return ''; } if (value.length > MAX_PROMPT_LENGTH) { fields.prompt = `长度不能超过 ${MAX_PROMPT_LENGTH} 个字符`; } return value; } function readModel(body: Record, fields: FieldErrors): GptImageModel { const value = readStringField(body, 'model', 'gpt-image-2'); if (!value || !isOneOf(value, AGENT_MODELS)) { fields.model = `必须是以下值之一:${AGENT_MODELS.join(', ')}`; return 'gpt-image-2'; } return value; } function readSize( body: Record, model: GptImageModel, fields: FieldErrors, fallback: string, upstreamProfile: Pick, options: { forceRequest?: boolean } = {} ): string { const value = readStringField(body, 'size', fallback); if (!value) { fields.size = '必须是字符串'; return fallback; } if (model !== 'gpt-image-2' && !isOneOf(value, AGENT_LEGACY_SIZES)) { fields.size = `${model} 的 size 必须是以下值之一:${AGENT_LEGACY_SIZES.join(', ')}`; return fallback; } if (model === 'gpt-image-2' && value !== 'auto') { const match = /^(\d+)x(\d+)$/.exec(value); if (!match) { fields.size = '必须是 auto 或 WxH 格式的尺寸值'; return fallback; } const validation = options.forceRequest || upstreamProfile.gptImage2.sizePolicy === 'positive-integer' ? validatePositiveIntegerImageSize(Number(match[1]), Number(match[2])) : validateGptImage2Size(Number(match[1]), Number(match[2])); if (!validation.valid) { fields.size = validation.reason; return fallback; } } return value; } function readOutputFormat(body: Record, fields: FieldErrors): ValidOutputFormat { const rawValue = readStringField(body, 'output_format', AGENT_DEFAULT_OUTPUT_FORMAT); const normalized = rawValue?.toLowerCase() === 'jpg' ? 'jpeg' : rawValue?.toLowerCase(); if (!normalized || !isOneOf(normalized, AGENT_OUTPUT_FORMATS)) { fields.output_format = `必须是以下值之一:${AGENT_OUTPUT_FORMATS.join(', ')}`; return 'png'; } return normalized; } function readResponseMode(body: Record, fields: FieldErrors): AgentResponseMode { const value = readStringField(body, 'response_mode', 'path'); if (!value || !isOneOf(value, AGENT_RESPONSE_MODES)) { fields.response_mode = `必须是以下值之一:${AGENT_RESPONSE_MODES.join(', ')}`; return 'path'; } return value; } function readAgentImageBackend(body: Record, fields: FieldErrors): ImageGenerationBackend { const rawValue = body.image_backend; if (rawValue !== undefined && rawValue !== null && typeof rawValue !== 'string') { fields.image_backend = '必须是字符串'; return 'images-api'; } const value = readStringField(body, 'image_backend', 'images-api'); if (!value) return 'images-api'; try { return parseImageGenerationBackendValue(value); } catch (error) { fields.image_backend = error instanceof Error ? error.message : 'image_backend 无效'; return 'images-api'; } } function readAgentStreamingStrategy(body: Record, fields: FieldErrors): ImageStreamingStrategy { const rawValue = body.streaming_strategy; if (rawValue !== undefined && rawValue !== null && typeof rawValue !== 'string') { fields.streaming_strategy = '必须是字符串'; return 'auto'; } const value = readStringField(body, 'streaming_strategy', 'auto'); if (!value) return 'auto'; try { return parseImageStreamingStrategyValue(value); } catch (error) { fields.streaming_strategy = error instanceof Error ? error.message : 'streaming_strategy 无效'; return 'auto'; } } function readAgentStreamMode(body: Record, fields: FieldErrors): ImageStreamMode { const rawValue = body.stream_mode; if (rawValue !== undefined && rawValue !== null && typeof rawValue !== 'string') { fields.stream_mode = '必须是字符串'; return 'auto'; } if ((rawValue === undefined || rawValue === null || rawValue === '') && body.streaming_strategy === 'off') { return 'non_stream'; } const value = readStringField(body, 'stream_mode', 'auto'); if (!value) return 'auto'; try { return parseImageStreamModeValue(value); } catch (error) { fields.stream_mode = error instanceof Error ? error.message : 'stream_mode 无效'; return 'auto'; } } function validateAgentImageUpstreamStrategy(input: { imageBackend: ImageGenerationBackend; streamMode: ImageStreamMode; streamingStrategy: ImageStreamingStrategy; fields: FieldErrors; }) { if (input.streamMode === 'non_stream') return; try { resolveImageStreamEnabled({ imageBackend: input.imageBackend, requestedStream: true, streamingStrategy: input.streamingStrategy }); } catch (error) { input.fields.streaming_strategy = error instanceof Error ? error.message : 'streaming_strategy 与 image_backend 不兼容'; } } function readPartialImagesForBackend( body: Record, imageBackend: ImageGenerationBackend, fields: FieldErrors, profile: ImageUpstreamProfile ): PartialImagesCount { const limits = getPartialImagesRangeForBackend(profile, imageBackend); const fallback = clampDefaultPartialImages(limits); return readIntegerField(body, 'partial_images', fallback, limits.min, limits.max, fields) as PartialImagesCount; } function readQuality(body: Record, fields: FieldErrors): AgentQuality { const value = readStringField(body, 'quality', 'high'); if (!value || !isOneOf(value, AGENT_QUALITIES)) { fields.quality = `必须是以下值之一:${AGENT_QUALITIES.join(', ')}`; return 'high'; } return value; } function readBackground(body: Record, fields: FieldErrors): AgentBackground { const value = readStringField(body, 'background', 'auto'); if (!value || !isOneOf(value, AGENT_BACKGROUNDS)) { fields.background = `必须是以下值之一:${AGENT_BACKGROUNDS.join(', ')}`; return 'auto'; } return value; } function readModeration(body: Record, fields: FieldErrors): AgentModeration { const value = readStringField(body, 'moderation', 'auto'); if (!value || !isOneOf(value, AGENT_MODERATIONS)) { fields.moderation = `必须是以下值之一:${AGENT_MODERATIONS.join(', ')}`; return 'auto'; } return value; } function readOptionalStringField( body: Record, field: string, fields: FieldErrors, options: { maxLength?: number } = {} ): string | undefined { const value = body[field]; if (value === undefined || value === null || value === '') return undefined; if (typeof value !== 'string') { fields[field] = '必须是字符串'; return undefined; } const trimmed = value.trim(); if (!trimmed) { fields[field] = '必须是非空字符串'; return undefined; } if (options.maxLength !== undefined && trimmed.length > options.maxLength) { fields[field] = `长度不能超过 ${options.maxLength} 个字符`; return undefined; } return trimmed; } function readOptionalBooleanField( body: Record, field: string, fields: FieldErrors ): boolean | undefined { const value = body[field]; if (value === undefined || value === null || value === '') return undefined; if (typeof value === 'boolean') return value; fields[field] = '必须是布尔值'; return undefined; } function readAgentResponsesModel( body: Record, imageBackend: ImageGenerationBackend, fields: FieldErrors ): string | undefined { const value = readOptionalStringField(body, 'responsesModel', fields, { maxLength: 128 }); if (value && imageBackend !== 'responses-image-generation') { fields.responsesModel = '仅适用于 image_backend=responses-image-generation'; } return value; } function readAgentThinking(body: Record, fields: FieldErrors): AgentGenerateRequest['thinking'] { const value = readOptionalStringField(body, 'thinking', fields); if (value === undefined) return undefined; if (['minimal', 'none', 'low', 'medium', 'high', 'xhigh'].includes(value)) { return value as AgentGenerateRequest['thinking']; } fields.thinking = '必须是 minimal、none、low、medium、high 或 xhigh'; return undefined; } function readOutputCompression( body: Record, outputFormat: ValidOutputFormat, fields: FieldErrors ): number | undefined { const value = body.output_compression; if (value === undefined || value === null || value === '') { return outputFormat === 'png' ? undefined : AGENT_DEFAULT_LOSSY_OUTPUT_COMPRESSION; } if (outputFormat === 'png') { fields.output_compression = '仅适用于 jpeg 或 webp 输出'; return undefined; } const parsed = typeof value === 'number' ? value : typeof value === 'string' && /^\d+$/.test(value) ? Number(value) : NaN; if (!Number.isInteger(parsed) || parsed < 0 || parsed > 100) { fields.output_compression = '必须是 0 到 100 之间的整数'; return undefined; } return parsed; } export function validateAgentGenerateRequest(body: unknown): AgentGenerateRequest { if (typeof body !== 'object' || body === null || Array.isArray(body)) { throw new RequestValidationError('请求正文必须是 JSON 对象。'); } const objectBody = body as Record; const fields: FieldErrors = {}; const upstreamLimits = buildAgentUpstreamLimits(process.env); const model = readModel(objectBody, fields); const prompt = validatePrompt(objectBody.prompt, fields); const n = readIntegerField(objectBody, 'n', 1, 1, MAX_IMAGE_COUNT, fields); const forceRequest = readOptionalBooleanField(objectBody, 'force_request', fields); const size = readSize(objectBody, model, fields, '1024x1024', upstreamLimits.profile, { forceRequest: forceRequest === true }); const quality = readQuality(objectBody, fields); const outputFormat = readOutputFormat(objectBody, fields); const outputCompression = readOutputCompression(objectBody, outputFormat, fields); const background = readBackground(objectBody, fields); const moderation = readModeration(objectBody, fields); const responseMode = readResponseMode(objectBody, fields); const imageBackend = readAgentImageBackend(objectBody, fields); const streamMode = readAgentStreamMode(objectBody, fields); const streamingStrategy = readAgentStreamingStrategy(objectBody, fields); const partialImages = readPartialImagesForBackend(objectBody, imageBackend, fields, upstreamLimits.profile); const responsesModel = readAgentResponsesModel(objectBody, imageBackend, fields); const thinking = readAgentThinking(objectBody, fields); const promptOptimization = readOptionalBooleanField(objectBody, 'promptOptimization', fields); const forceWeb = readOptionalBooleanField(objectBody, 'force_web', fields); validateAgentImageUpstreamStrategy({ imageBackend, streamMode, streamingStrategy, fields }); if (Object.keys(fields).length > 0) { throw new RequestValidationError(JSON.stringify({ fields }), 422); } return { model, prompt, n, size, quality, output_format: outputFormat, ...(outputCompression !== undefined ? { output_compression: outputCompression } : {}), background, moderation, response_mode: responseMode, image_backend: imageBackend, stream_mode: streamMode, streaming_strategy: streamingStrategy, partial_images: partialImages, ...(responsesModel ? { responsesModel } : {}), ...(thinking ? { thinking } : {}), ...(promptOptimization !== undefined ? { promptOptimization } : {}), ...(forceWeb !== undefined ? { force_web: forceWeb } : {}), ...(forceRequest !== undefined ? { force_request: forceRequest } : {}) }; } export function readAgentStateBackend(env: Record): AgentStateBackend { const backend = env.AGENT_STATE_BACKEND?.trim().toLowerCase(); if (!backend) return 'sqlite'; if (backend === 'memory' || backend === 'sqlite' || backend === 'postgres') return backend; throw new RequestValidationError('AGENT_STATE_BACKEND 必须是 memory、sqlite 或 postgres。', 500); } export function readAgentRequestTtlSeconds(env: Record): number { return readPositiveIntegerEnv(env, 'AGENT_REQUEST_TTL_SECONDS', AGENT_DEFAULT_REQUEST_TTL_SECONDS); } export function readAgentLeaseMs(env: Record): number { return readPositiveIntegerEnv(env, 'AGENT_REQUEST_LEASE_MS', AGENT_DEFAULT_LEASE_MS); } export function readAgentRecoveryIntervalMs(env: Record): number { return readPositiveIntegerEnv(env, 'AGENT_RECOVERY_INTERVAL_MS', AGENT_DEFAULT_RECOVERY_INTERVAL_MS); } export function readAgentSqlitePath(env: Record): string { return env.AGENT_SQLITE_PATH?.trim() || AGENT_DEFAULT_SQLITE_PATH; } export function readAgentPublicBaseUrl(env: Record): string { const value = env.AGENT_PUBLIC_BASE_URL?.trim(); if (!value) return '/'; let parsed: URL; try { parsed = new URL(value); } catch { throw new RequestValidationError('AGENT_PUBLIC_BASE_URL 格式无效。', 500); } if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') { throw new RequestValidationError('AGENT_PUBLIC_BASE_URL 必须是 http 或 https URL。', 500); } if (parsed.username || parsed.password || parsed.search || parsed.hash) { throw new RequestValidationError('AGENT_PUBLIC_BASE_URL 不能包含凭据、查询参数或片段。', 500); } return parsed.toString().replace(/\/$/, ''); } export function validateOptionalAgentApiBaseUrl(baseUrl: string | undefined): void { if (baseUrl) validateApiBaseUrl(baseUrl); } export function buildAgentAuthCapabilities(env: Record): AgentCapabilities['auth'] { if (env.AGENT_API_TOKEN?.trim()) { return { required: true, schemes: ['bearer'] }; } if (env.APP_PASSWORD?.trim()) { return { required: true, schemes: ['x-app-password-hash'] }; } return { required: false, schemes: [] }; } export function buildPageSseAuthCapabilities( env: Record ): AgentCapabilities['agent_streaming']['page_sse']['auth'] { return { required: Boolean(env.APP_PASSWORD?.trim()), schemes: env.APP_PASSWORD?.trim() ? ['form-password-hash'] : [], form_field: 'passwordHash' }; } export function buildPageRequestDiagnosticsCapabilities( env: Record ): AgentCapabilities['page_request_diagnostics'] { const retention = buildPageRequestDiagnosticsRetention(env); return { supported: true, source: 'app_log', endpoints: { single: AGENT_ENDPOINTS.page_request_diagnostics, batch: AGENT_ENDPOINTS.page_request_diagnostics_batch }, retention, no_match_hint: AGENT_PAGE_REQUEST_DIAGNOSTICS_NO_MATCH_HINT }; } export function buildPageRequestDiagnosticsRetention( env: Record ): AgentPageRequestDiagnosticsRetention { return { ...readAppLogRetentionMetadata(env), bounded: true, not_agent_state_backend: true }; } export function buildAgentRequestDiagnosticsCapabilities( env: Record ): AgentCapabilities['agent_request_diagnostics'] { return { supported: true, source: 'agent_state', endpoints: { lookup: AGENT_ENDPOINTS.agent_request_diagnostics_lookup, single: AGENT_ENDPOINTS.agent_request_diagnostics }, lookup: { by_request_id: true, by_idempotency_key: true }, retention: buildAgentRequestDiagnosticsRetention(env) }; } export function buildAgentRequestDiagnosticsRetention( env: Record ): AgentRequestDiagnosticsRetention { return { storage: 'agent_state', ttl_seconds: readAgentRequestTtlSeconds(env), bounded: true, loss_modes: ['request_expired_by_ttl', 'artifact_deleted_or_purged', 'state_backend_reset'] }; } export function buildPageRequestDiagnosticsNoMatchNote(input: { matchedLogCount: number; retention: AgentPageRequestDiagnosticsRetention; }): AgentPageRequestDiagnosticsNote | undefined { if (input.matchedLogCount !== 0) return undefined; return { code: 'no_matching_logs_in_retention_window', message: AGENT_PAGE_REQUEST_DIAGNOSTICS_NO_MATCH_HINT, retention: input.retention }; } function readEnabledImageBackends( requirements: Record ): ImageGenerationBackend[] { return requirements['responses-image-generation'].enabled ? ['images-api', 'responses-image-generation'] : ['images-api']; } function buildImageBackendRequirements( env: Record ): Record { const responsesMissingEnv = readResponsesImageBackendMissingEnv(env); return { 'images-api': { supported: true, enabled: true, required_env: [], missing_env: [] }, 'responses-image-generation': { supported: true, enabled: responsesMissingEnv.length === 0, required_env: ['ENABLE_RESPONSES_IMAGE_BACKEND', 'OPENAI_RESPONSES_API_MODEL'], missing_env: responsesMissingEnv } }; } function readResponsesImageBackendMissingEnv(env: Record): string[] { const missing: string[] = []; if (!readBooleanEnv(env, 'ENABLE_RESPONSES_IMAGE_BACKEND')) { missing.push('ENABLE_RESPONSES_IMAGE_BACKEND'); } if (!env.OPENAI_RESPONSES_API_MODEL?.trim()) { missing.push('OPENAI_RESPONSES_API_MODEL'); } return missing; } function readPositiveIntegerEnv(env: Record, fieldName: string, fallback: number): number { try { return readPositiveIntegerFromEnv(env, fieldName, fallback, { messages: CHINESE_POSITIVE_INTEGER_MESSAGES }); } catch (error) { throw new RequestValidationError(error instanceof Error ? error.message : `${fieldName} 必须是正整数。`, 500); } } export function buildAgentCapabilities(env: Record): AgentCapabilities { const imageBackendRequirements = buildImageBackendRequirements(env); const enabledImageBackends = readEnabledImageBackends(imageBackendRequirements); const upstreamLimits = buildAgentUpstreamLimits(env); const partialImagesByBackend = buildAgentPartialImagesByBackend(upstreamLimits.profile); const defaultPartialImages = clampDefaultPartialImages(partialImagesByBackend['images-api']); return { api_version: AGENT_API_VERSION, schema_version: AGENT_SCHEMA_VERSION, auth: buildAgentAuthCapabilities(env), endpoints: { ...AGENT_ENDPOINTS }, image_transport: summarizeOpenAIImageTransport(env), upstream_profile: upstreamLimits.summary, upstream_request_headers: buildAgentUpstreamRequestHeadersCapabilities(env), request_mode_controls: buildAgentRequestModeControlsCapabilities(), defaults: { model: 'gpt-image-2', response_mode: 'path', state_backend: readAgentStateBackend(env), image_backend: 'images-api', stream_mode: 'auto', streaming_strategy: 'auto', partial_images: defaultPartialImages }, limits: { max_prompt_length: MAX_PROMPT_LENGTH, max_images: Math.min(upstreamLimits.profile.generateCount.max, upstreamLimits.profile.editCount.max), generate_images: upstreamLimits.profile.generateCount, edit_images: upstreamLimits.profile.editCount, upload_images: { max: upstreamLimits.profile.upload.maxImages }, max_upload_mb: upstreamLimits.profile.upload.maxSingleBytes / 1024 / 1024, ...(upstreamLimits.profile.upload.maxTotalBytes !== undefined ? { max_total_upload_mb: upstreamLimits.profile.upload.maxTotalBytes / 1024 / 1024 } : {}), partial_images: upstreamLimits.profile.partialImages, partial_images_by_backend: partialImagesByBackend, upstream_profile: upstreamLimits.profile.id, upstream_profile_mixed: upstreamLimits.summary.serverProfileMixed }, force_request_controls: { field: 'force_request', cli_flag: '--force-request', default: false, effect: 'skip_local_upstream_profile_size_and_background_validation', still_enforced: [ 'authentication', 'idempotency_key', 'billable_confirmation', 'api_base_url_safety', 'channel_request_mode_whitelist', 'image_count_limits', 'partial_image_limits', 'non_gpt_image2_size_allowlist', 'positive_integer_size_syntax', 'upload_file_size_and_type', 'mask_integrity_checks' ], intended_for: [ 'upstream_compatibility_probe', 'administrator_confirmed_provider_contract', 'requests_where_real_upstream_should_decide' ] }, model_limits: { 'gpt-image-2': { max_edge: GPT_IMAGE_2_MAX_EDGE, max_pixels: GPT_IMAGE_2_MAX_PIXELS, edge_multiple: GPT_IMAGE_2_EDGE_MULTIPLE, max_aspect: GPT_IMAGE_2_MAX_ASPECT, min_pixels: GPT_IMAGE_2_MIN_PIXELS, size_policy: upstreamLimits.profile.gptImage2.sizePolicy, allow_transparent_background: upstreamLimits.profile.gptImage2.allowTransparentBackground, recommended_presets: [ { name: 'square', size: '2048x2048', purpose: '通用正方形构图' }, { name: 'landscape', size: '3072x2048', purpose: '横向宽幅构图' }, { name: 'portrait', size: '2048x3072', purpose: '纵向主体构图' } ], large_image_risk: { applies_to: ['max_edge>2048', 'long_running_upstream'], guidance: '大尺寸请求可能耗时数分钟;失败应归类为上游长耗时风险,不代表低负载路径不可用。' } } }, agent_streaming: { generate: { supported: false, mode: 'non_streaming_only', endpoint: AGENT_ENDPOINTS.generate }, edit: { supported: false, mode: 'non_streaming_only', endpoint: AGENT_ENDPOINTS.edit }, upstream_sse: { supported: true, mode: 'internal_upstream_sse', endpoint: AGENT_ENDPOINTS.generate, request_fields: AGENT_UPSTREAM_SSE_GENERATE_REQUEST_FIELDS, request_fields_by_mode: { generate: AGENT_UPSTREAM_SSE_GENERATE_REQUEST_FIELDS, edit: AGENT_UPSTREAM_SSE_EDIT_REQUEST_FIELDS }, image_backends: AGENT_IMAGE_BACKENDS, enabled_image_backends: enabledImageBackends, streaming_strategies: AGENT_STREAMING_STRATEGIES, stream_modes: AGENT_STREAM_MODES, activation_strategies: AGENT_UPSTREAM_SSE_ACTIVATION_STRATEGIES, final_response_contract: 'AgentImageResponse' }, page_sse: { supported: true, mode: 'form_data_sse', endpoint: '/api/images', contract: 'page_ui_only', transport_contract: 'page_form_data_sse', auth: buildPageSseAuthCapabilities(env), client_request_id: { form_field: 'clientRequestId', source_header: 'Idempotency-Key', max_length: PAGE_SSE_CLIENT_REQUEST_ID_MAX_LENGTH }, agent_usage: AGENT_PAGE_SSE_AGENT_USAGE } }, orchestration: { supported: true, policy: 'server_orchestrated_generate_v1', endpoint: AGENT_ENDPOINTS.create_image_request, client_contract: 'intent_only', transport_selection: 'server_owned', result_mode: 'job_polling', hidden_controls: ['transport', 'route_mode', 'client_endpoint_selection'], diagnostics: { job_result: AGENT_ENDPOINTS.job_result, request_lookup: AGENT_ENDPOINTS.agent_request_diagnostics_lookup }, current_guidance: 'Agent 客户端默认只提交生成意图到 /api/agent/image-requests;服务端负责选择内部执行路径、上游策略和轮询结果。显式 /api/images、Agent JSON 或 job endpoint 仅作为诊断/兼容入口。' }, routing_rules: { high_resolution_edit: { when: ['operation=edit', 'max_edge>2048'], conditions: { operation: 'edit', max_edge: { operator: 'gt', value: 2048 } }, endpoint: '/api/images', transport: 'page_sse', strength: 'default', action: { endpoint: '/api/images', transport: 'page_sse', strength: 'default', fallback_endpoint: AGENT_ENDPOINTS.edit, fallback_mode: 'manual_after_diagnosis', requires_new_idempotency_key_on_retry: true, no_automatic_fallback: true }, reason: 'High-resolution edit uses the page form-data SSE endpoint because Agent edit has a fixed output contract; if streaming has issues, diagnose first and explicitly fall back to Agent edit.' }, complex_ui_batch: { when: ['operation=generate_or_edit', 'complex_ui=true', 'batch=true'], conditions: { operation: 'generate_or_edit', complex_ui: true, batch: true }, endpoint: '/api/images', transport: 'page_sse', strength: 'recommended', action: { endpoint: '/api/images', transport: 'page_sse', strength: 'recommended', fallback_mode: 'manual_after_diagnosis', requires_new_idempotency_key_on_retry: true, no_automatic_fallback: true }, reason: 'Page form-data SSE keeps long-running image production observable and recoverable for complex UI batches.' }, long_image_recovery: { when: ['operation=generate_or_edit', 'long_image=true', 'resume_or_recover=true'], conditions: { operation: 'generate_or_edit', long_image: true, resume_or_recover: true }, endpoint: '/api/images', transport: 'page_sse', strength: 'recommended', action: { endpoint: '/api/images', transport: 'page_sse', strength: 'recommended', fallback_mode: 'manual_after_diagnosis', requires_new_idempotency_key_on_retry: true, no_automatic_fallback: true }, reason: 'Page form-data SSE exposes partial progress and final-image diagnostics needed for long-image recovery runs.' }, agent_generate_small_smoke: { when: ['operation=generate', 'max_edge<=2048', 'single_request=true'], conditions: { operation: 'generate', max_edge: { operator: 'lte', value: 2048 }, single_request: true }, endpoint: AGENT_ENDPOINTS.generate, transport: 'agent_json', strength: 'explicit', action: { endpoint: AGENT_ENDPOINTS.generate, transport: 'agent_json', strength: 'explicit', requires_new_idempotency_key_on_retry: true, no_automatic_fallback: true }, reason: 'Agent JSON generate remains available for compatibility and explicit diagnostics; ordinary generate clients should use orchestration.endpoint.' }, page_sse_generate_diagnostics: { when: ['operation=generate', 'explicit_page_sse=true'], conditions: { operation: 'generate', explicit_page_sse: true, single_request: true }, endpoint: '/api/images', transport: 'page_sse', strength: 'explicit', action: { endpoint: '/api/images', transport: 'page_sse', strength: 'explicit', fallback_endpoint: AGENT_ENDPOINTS.create_image_request, fallback_mode: 'manual_after_diagnosis', requires_new_idempotency_key_on_retry: true, no_automatic_fallback: true }, reason: 'Page form-data SSE is available for explicit page-workbench or diagnostic generate runs; ordinary generate clients should use orchestration.endpoint.' }, retry_recovery: { reuse_failed_idempotency_key: false, new_attempt_guidance: 'A failed terminal Agent request only replays the stored failure. Diagnose the failure, then create a new business operation with a new Idempotency-Key.' } }, agent_jobs: { supported: true, mode: 'job_polling', intended_for: [ 'explicit_job_route', 'manual_after_diagnosis', 'long_running_upstream_when_page_sse_not_selected' ], endpoints: { ...AGENT_JOB_ENDPOINTS }, states: AGENT_JOB_STATES, current_guidance: 'Agent 客户端默认使用 orchestration.endpoint;直接创建 job 是兼容和诊断入口。当前执行模型为同实例后台任务,不是跨实例持久队列。' }, supported: { models: AGENT_MODELS, output_formats: AGENT_OUTPUT_FORMATS, response_modes: AGENT_RESPONSE_MODES, qualities: AGENT_QUALITIES, backgrounds: AGENT_BACKGROUNDS, moderations: AGENT_MODERATIONS, legacy_sizes: AGENT_LEGACY_SIZES, image_backends: AGENT_IMAGE_BACKENDS, enabled_image_backends: enabledImageBackends, image_backend_requirements: imageBackendRequirements, request_modes: CHANNEL_REQUEST_MODES, streaming_strategies: AGENT_STREAMING_STRATEGIES, stream_modes: AGENT_STREAM_MODES }, storage: { image_storage_mode: env.NEXT_PUBLIC_IMAGE_STORAGE_MODE || (env.VERCEL === '1' ? 'indexeddb' : 'fs'), postgres_configured: Boolean(env.AGENT_DATABASE_URL || env.AGENT_DB_PASSWORD || env.AGENT_DB_PASSWORD_FILE) }, page_request_diagnostics: buildPageRequestDiagnosticsCapabilities(env), agent_request_diagnostics: buildAgentRequestDiagnosticsCapabilities(env), channel_health_diagnostics: { supported: true, endpoint: AGENT_ENDPOINTS.channel_health_diagnostics, source: 'in_process_channel_router', state_scope: 'process_local', billable: false }, idempotency: { required: true, header: 'Idempotency-Key', ttl_seconds: readAgentRequestTtlSeconds(env) } }; } function buildAgentRequestModeControlsCapabilities(): AgentCapabilities['request_mode_controls'] { return { source: CHANNEL_REQUEST_MODE_ADMIN_CONTROL.source, global_env: CHANNEL_REQUEST_MODE_ADMIN_CONTROL.globalEnv, channel_env_pattern: CHANNEL_REQUEST_MODE_ADMIN_CONTROL.channelEnvPattern, global_priority_env: CHANNEL_REQUEST_MODE_ADMIN_CONTROL.globalPriorityEnv, channel_priority_env_pattern: CHANNEL_REQUEST_MODE_ADMIN_CONTROL.channelPriorityEnvPattern, default_priority: CHANNEL_REQUEST_MODE_ADMIN_CONTROL.defaultPriority, default_priority_policy: CHANNEL_REQUEST_MODE_ADMIN_CONTROL.defaultPriorityPolicy, mutable_at_runtime: CHANNEL_REQUEST_MODE_ADMIN_CONTROL.mutableAtRuntime, agent_client_policy: 'diagnostics_only', final_gate_command: CHANNEL_REQUEST_MODE_ADMIN_CONTROL.finalGateCommand, smoke_gate_commands: CHANNEL_REQUEST_MODE_ADMIN_CONTROL.smokeGateCommands }; } function buildAgentUpstreamRequestHeadersCapabilities( env: Record ): AgentCapabilities['upstream_request_headers'] { const channelSummary = getChannelPoolSummary(parseChannelPoolConfig(env)); return { default: summarizeUpstreamRequestHeaders(undefined, env), channels: channelSummary.channels.map((channel) => ({ id: channel.id, upstream_proxy: channel.upstreamProxy, request_modes: channel.requestModes, request_mode_priority: channel.requestModePriority, request_headers: channel.requestHeaders })) }; } function buildAgentUpstreamLimits(env: Record): { profile: ImageUpstreamProfile; summary: ImageUpstreamProfileSummary; } { const serverProfiles = readServerProfiles(env); const summary = summarizeImageUpstreamProfile({ serverProfiles }); return { profile: summary.activeConstraints, summary }; } function readServerProfiles(env: Record): ImageUpstreamProfile[] { return getChannelPoolSummary(parseChannelPoolConfig(env)).channels.map((channel) => channel.effectiveProfile); } function clampDefaultPartialImages(limits: ImageUpstreamProfile['partialImages']): PartialImagesCount { return clampIntegerToRange(AGENT_DEFAULT_PARTIAL_IMAGES, limits) as PartialImagesCount; } function buildAgentPartialImagesByBackend( profile: ImageUpstreamProfile ): Record { return { 'images-api': getPartialImagesRangeForBackend(profile, 'images-api'), 'responses-image-generation': getPartialImagesRangeForBackend(profile, 'responses-image-generation') }; }