ai-chatbot/lib/realtime/config.ts
Your Name cd91094b0f
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
feat: add fal provider for realtime canvas
2026-03-31 13:36:28 +08:00

356 lines
11 KiB
TypeScript

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<string, unknown>,
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,
};
}