export const DEFAULT_BFL_MODEL = "flux-2-klein-4b"; export const DEFAULT_BFL_BASE_URL = "https://api.bfl.ai"; export const DEFAULT_REPLICATE_MODEL = "black-forest-labs/flux-2-klein-4b"; export const DEFAULT_REPLICATE_BASE_URL = "https://api.replicate.com/v1"; export const DEFAULT_FAL_BASE_URL = "https://fal.run"; export const DEFAULT_FAL_REALTIME_MODEL = ""; export type RealtimeProviderType = "bfl" | "replicate" | "fal"; export type RealtimeCanvasConfig = { enabled?: boolean; providerType?: RealtimeProviderType; bflApiKey?: string; bflModel?: string; bflBaseUrl?: string; runwareApiKey?: string; runwareModel?: string; falApiKey?: string; falModel?: string; falBaseUrl?: string; apiKey?: string; replicateApiToken?: string; replicateModel?: string; fallbackEnabled?: boolean; fallbackProviderType?: RealtimeProviderType; fallbackBflApiKey?: string; fallbackBflModel?: string; fallbackBflBaseUrl?: string; fallbackRunwareApiKey?: string; fallbackRunwareModel?: string; fallbackFalApiKey?: string; fallbackFalModel?: string; fallbackFalBaseUrl?: string; fallbackApiKey?: string; fallbackReplicateApiToken?: string; fallbackReplicateModel?: string; negativePrompt?: string; renderMode?: "strict-structure" | "balanced" | "stylize"; strictSketch?: boolean; throttleInterval?: number; maxBuffering?: number; maxInputResolution?: number; jpegQuality?: number; previewMegapixels?: number; detailMegapixels?: number; outputJpegQuality?: number; defaults?: { steps?: number; cfgScale?: number; strength?: number; scheduleMu?: number; feedback?: number; imageSize?: "square" | "square_hd"; }; }; export type RealtimeProviderConfig = { type: RealtimeProviderType; apiKey: string; baseUrl: string; model: string; source: "primary" | "fallback"; label: string; }; function trimString(value: unknown) { return String(value || "").trim(); } function normalizeProviderType(raw: unknown): RealtimeProviderType { if (raw === "fal") return "fal"; return raw === "replicate" ? "replicate" : "bfl"; } export function normalizeBaseUrl(raw: string | undefined) { const next = trimString(raw || DEFAULT_BFL_BASE_URL); return (next || DEFAULT_BFL_BASE_URL).replace(/\/+$/, ""); } function hasFallbackIntent(config: RealtimeCanvasConfig) { if (config.fallbackEnabled === true) return true; if (config.fallbackEnabled === false) return false; return Boolean( trimString( config.fallbackBflApiKey || config.fallbackRunwareApiKey || config.fallbackFalApiKey || config.fallbackApiKey || config.fallbackBflBaseUrl || config.fallbackBflModel || config.fallbackRunwareModel || config.fallbackFalModel || config.fallbackReplicateApiToken || config.fallbackReplicateModel ) ); } export function resolvePrimaryRealtimeProvider( config: RealtimeCanvasConfig, env: NodeJS.ProcessEnv = process.env ): RealtimeProviderConfig | null { const type = normalizeProviderType(config.providerType); if (type === "replicate") { const apiKey = trimString(config.replicateApiToken || env.REPLICATE_API_TOKEN); if (!apiKey) return null; return { type, apiKey, model: trimString(config.replicateModel) || DEFAULT_REPLICATE_MODEL, baseUrl: DEFAULT_REPLICATE_BASE_URL, source: "primary", label: "主实时服务", }; } if (type === "fal") { const apiKey = trimString(config.falApiKey || config.apiKey || env.FAL_KEY || env.FAL_API_KEY); const model = trimString(config.falModel || env.FAL_REALTIME_MODEL || env.FAL_MODEL); if (!apiKey || !model) return null; return { type, apiKey, model, baseUrl: trimString(config.falBaseUrl || env.FAL_BASE_URL || DEFAULT_FAL_BASE_URL), source: "primary", label: "主实时服务", }; } const apiKey = trimString( config.bflApiKey || config.runwareApiKey || config.falApiKey || config.apiKey || env.BFL_API_KEY || env.BFL_KEY ); if (!apiKey) return null; const model = trimString(config.bflModel || config.runwareModel || env.BFL_MODEL || env.RUNWARE_MODEL) || DEFAULT_BFL_MODEL; const baseUrl = normalizeBaseUrl(config.bflBaseUrl || env.BFL_BASE_URL); return { type, apiKey, model, baseUrl, source: "primary", label: "主实时服务", }; } export function resolveFallbackRealtimeProvider( config: RealtimeCanvasConfig ): RealtimeProviderConfig | null { if (!hasFallbackIntent(config)) return null; const type = normalizeProviderType(config.fallbackProviderType); if (type === "replicate") { const apiKey = trimString(config.fallbackReplicateApiToken); if (!apiKey) return null; return { type, apiKey, model: trimString(config.fallbackReplicateModel) || DEFAULT_REPLICATE_MODEL, baseUrl: DEFAULT_REPLICATE_BASE_URL, source: "fallback", label: "备用实时服务", }; } if (type === "fal") { const apiKey = trimString(config.fallbackFalApiKey || config.fallbackApiKey); const model = trimString(config.fallbackFalModel); if (!apiKey || !model) return null; return { type, apiKey, model, baseUrl: trimString(config.fallbackFalBaseUrl || DEFAULT_FAL_BASE_URL), source: "fallback", label: "备用实时服务", }; } const apiKey = trimString( config.fallbackBflApiKey || config.fallbackRunwareApiKey || config.fallbackFalApiKey || config.fallbackApiKey ); if (!apiKey) return null; const model = trimString(config.fallbackBflModel || config.fallbackRunwareModel) || DEFAULT_BFL_MODEL; const baseUrl = normalizeBaseUrl(config.fallbackBflBaseUrl || DEFAULT_BFL_BASE_URL); return { type, apiKey, model, baseUrl, source: "fallback", label: "备用实时服务", }; } export function resolveRealtimeProviders( config: RealtimeCanvasConfig, env: NodeJS.ProcessEnv = process.env ) { const providers: RealtimeProviderConfig[] = []; const primary = resolvePrimaryRealtimeProvider(config, env); const fallback = resolveFallbackRealtimeProvider(config); if (primary) { providers.push(primary); } if ( fallback && !providers.some( (provider) => provider.apiKey === fallback.apiKey && provider.baseUrl === fallback.baseUrl && provider.model === fallback.model ) ) { providers.push(fallback); } return providers; } export function normalizeRealtimeCanvasConfigForStorage( value: Record, env: NodeJS.ProcessEnv = process.env ): RealtimeCanvasConfig { const renderMode = value.renderMode === "strict-structure" || value.renderMode === "stylize" || value.renderMode === "balanced" ? value.renderMode : "balanced"; const primary = resolvePrimaryRealtimeProvider(value as RealtimeCanvasConfig, env); const fallback = resolveFallbackRealtimeProvider(value as RealtimeCanvasConfig); return { ...value, providerType: primary?.type || normalizeProviderType(value.providerType), bflApiKey: primary?.type === "bfl" ? primary.apiKey : trimString((value as RealtimeCanvasConfig).bflApiKey), bflModel: primary?.type === "bfl" ? primary.model : trimString((value as RealtimeCanvasConfig).bflModel) || DEFAULT_BFL_MODEL, bflBaseUrl: primary?.type === "bfl" ? primary.baseUrl : normalizeBaseUrl((value as RealtimeCanvasConfig).bflBaseUrl || env.BFL_BASE_URL), runwareApiKey: primary?.type === "bfl" ? primary.apiKey : trimString((value as RealtimeCanvasConfig).runwareApiKey), falApiKey: primary?.type === "bfl" || primary?.type === "fal" ? primary.apiKey : trimString((value as RealtimeCanvasConfig).falApiKey), falModel: primary?.type === "fal" ? primary.model : trimString((value as RealtimeCanvasConfig).falModel) || DEFAULT_FAL_REALTIME_MODEL, falBaseUrl: primary?.type === "fal" ? primary.baseUrl : trimString((value as RealtimeCanvasConfig).falBaseUrl || env.FAL_BASE_URL || DEFAULT_FAL_BASE_URL), apiKey: primary?.type === "bfl" || primary?.type === "fal" ? primary.apiKey : trimString((value as RealtimeCanvasConfig).apiKey), runwareModel: primary?.type === "bfl" ? primary.model : trimString((value as RealtimeCanvasConfig).runwareModel) || DEFAULT_BFL_MODEL, replicateApiToken: primary?.type === "replicate" ? primary.apiKey : trimString((value as RealtimeCanvasConfig).replicateApiToken), replicateModel: primary?.type === "replicate" ? primary.model : trimString((value as RealtimeCanvasConfig).replicateModel) || DEFAULT_REPLICATE_MODEL, fallbackEnabled: Boolean(fallback), fallbackProviderType: fallback?.type || normalizeProviderType(value.fallbackProviderType), fallbackBflApiKey: fallback?.type === "bfl" ? fallback.apiKey : trimString((value as RealtimeCanvasConfig).fallbackBflApiKey), fallbackBflModel: fallback?.type === "bfl" ? fallback.model : trimString((value as RealtimeCanvasConfig).fallbackBflModel) || DEFAULT_BFL_MODEL, fallbackBflBaseUrl: fallback?.type === "bfl" ? fallback.baseUrl : normalizeBaseUrl((value as RealtimeCanvasConfig).fallbackBflBaseUrl || DEFAULT_BFL_BASE_URL), fallbackRunwareApiKey: fallback?.type === "bfl" ? fallback.apiKey : trimString((value as RealtimeCanvasConfig).fallbackRunwareApiKey), fallbackFalApiKey: fallback?.type === "bfl" || fallback?.type === "fal" ? fallback.apiKey : trimString((value as RealtimeCanvasConfig).fallbackFalApiKey), fallbackFalModel: fallback?.type === "fal" ? fallback.model : trimString((value as RealtimeCanvasConfig).fallbackFalModel) || DEFAULT_FAL_REALTIME_MODEL, fallbackFalBaseUrl: fallback?.type === "fal" ? fallback.baseUrl : trimString((value as RealtimeCanvasConfig).fallbackFalBaseUrl || DEFAULT_FAL_BASE_URL), fallbackApiKey: fallback?.type === "bfl" || fallback?.type === "fal" ? fallback.apiKey : trimString((value as RealtimeCanvasConfig).fallbackApiKey), fallbackRunwareModel: fallback?.type === "bfl" ? fallback.model : trimString((value as RealtimeCanvasConfig).fallbackRunwareModel) || DEFAULT_BFL_MODEL, fallbackReplicateApiToken: fallback?.type === "replicate" ? fallback.apiKey : trimString((value as RealtimeCanvasConfig).fallbackReplicateApiToken), fallbackReplicateModel: fallback?.type === "replicate" ? fallback.model : trimString((value as RealtimeCanvasConfig).fallbackReplicateModel) || DEFAULT_REPLICATE_MODEL, renderMode, }; }