visual-journal / src /lib /agent-api-contracts.ts
gpt-image-playground deploy
Deploy 4770498 to Docker Space
a2c232b
Raw
History Blame Contribute Delete
54.3 kB
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<string, unknown>;
upstream_status?: number;
diagnostics?: AgentErrorDiagnostics;
};
};
};
export type AgentCapabilities = {
api_version: string;
schema_version: string;
auth: {
required: boolean;
schemes: AgentAuthScheme[];
};
endpoints: Record<string, string>;
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<ChannelRequestMode, readonly string[]>;
};
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<ImageGenerationBackend, { min: number; max: number }>;
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<ImageGenerationBackend, ImageBackendRuntimeRequirement>;
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<string, string>;
function isOneOf<T extends readonly string[]>(value: string, allowed: T): value is T[number] {
return (allowed as readonly string[]).includes(value);
}
function readStringField(body: Record<string, unknown>, 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<string, unknown>,
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<string, unknown>, 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<string, unknown>,
model: GptImageModel,
fields: FieldErrors,
fallback: string,
upstreamProfile: Pick<ImageUpstreamProfile, 'gptImage2'>,
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<string, unknown>, 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<string, unknown>, 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<string, unknown>, 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<string, unknown>, 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<string, unknown>, 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<string, unknown>,
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<string, unknown>, 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<string, unknown>, 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<string, unknown>, 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<string, unknown>,
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<string, unknown>,
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<string, unknown>,
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<string, unknown>, 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<string, unknown>,
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<string, unknown>;
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<string, string | undefined>): 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<string, string | undefined>): number {
return readPositiveIntegerEnv(env, 'AGENT_REQUEST_TTL_SECONDS', AGENT_DEFAULT_REQUEST_TTL_SECONDS);
}
export function readAgentLeaseMs(env: Record<string, string | undefined>): number {
return readPositiveIntegerEnv(env, 'AGENT_REQUEST_LEASE_MS', AGENT_DEFAULT_LEASE_MS);
}
export function readAgentRecoveryIntervalMs(env: Record<string, string | undefined>): number {
return readPositiveIntegerEnv(env, 'AGENT_RECOVERY_INTERVAL_MS', AGENT_DEFAULT_RECOVERY_INTERVAL_MS);
}
export function readAgentSqlitePath(env: Record<string, string | undefined>): string {
return env.AGENT_SQLITE_PATH?.trim() || AGENT_DEFAULT_SQLITE_PATH;
}
export function readAgentPublicBaseUrl(env: Record<string, string | undefined>): 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<string, string | undefined>): 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<string, string | undefined>
): 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<string, string | undefined>
): 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<string, string | undefined>
): AgentPageRequestDiagnosticsRetention {
return {
...readAppLogRetentionMetadata(env),
bounded: true,
not_agent_state_backend: true
};
}
export function buildAgentRequestDiagnosticsCapabilities(
env: Record<string, string | undefined>
): 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<string, string | undefined>
): 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, ImageBackendRuntimeRequirement>
): ImageGenerationBackend[] {
return requirements['responses-image-generation'].enabled
? ['images-api', 'responses-image-generation']
: ['images-api'];
}
function buildImageBackendRequirements(
env: Record<string, string | undefined>
): Record<ImageGenerationBackend, ImageBackendRuntimeRequirement> {
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, string | undefined>): 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<string, string | undefined>, 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<string, string | undefined>): 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<string, string | undefined>
): 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<string, string | undefined>): {
profile: ImageUpstreamProfile;
summary: ImageUpstreamProfileSummary;
} {
const serverProfiles = readServerProfiles(env);
const summary = summarizeImageUpstreamProfile({ serverProfiles });
return {
profile: summary.activeConstraints,
summary
};
}
function readServerProfiles(env: Record<string, string | undefined>): 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<ImageGenerationBackend, { min: number; max: number }> {
return {
'images-api': getPartialImagesRangeForBackend(profile, 'images-api'),
'responses-image-generation': getPartialImagesRangeForBackend(profile, 'responses-image-generation')
};
}