From 6a9beb418ad33b106363c7bfd2eadbbe68bee59a Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 8 Jun 2026 20:13:12 +0800 Subject: [PATCH] fix(ruxa): resolve runtime model ids from config for image flows --- app/api/canvas/text-edit/route.ts | 6 +++- app/api/scene-fusion/route.ts | 18 ++++++++++- lib/ai/dynamic-provider.ts | 6 +++- lib/ai/model-runtime.ts | 54 +++++++++++++++++++++++++++++++ lib/ai/ruxa-image-models.ts | 5 ++- lib/canvas/generate-image.ts | 6 +++- 6 files changed, 88 insertions(+), 7 deletions(-) create mode 100644 lib/ai/model-runtime.ts diff --git a/app/api/canvas/text-edit/route.ts b/app/api/canvas/text-edit/route.ts index d7c8039da..4771e9e6d 100644 --- a/app/api/canvas/text-edit/route.ts +++ b/app/api/canvas/text-edit/route.ts @@ -7,6 +7,7 @@ import { createHuankeGemini } from "@/lib/ai/providers/huanke-gemini"; import { createAlibabaImage } from "@/lib/ai/providers/alibaba-image"; import { createBflImage } from "@/lib/ai/providers/bfl-image"; import { createRuxaImage } from "@/lib/ai/providers/ruxa-image"; +import { resolveModelRuntimeId } from "@/lib/ai/model-runtime"; import { createFalImage } from "@/lib/ai/providers/fal-image"; import { createOpenAIImage } from "@/lib/ai/providers/openai-image"; import { createStepfunImage } from "@/lib/ai/providers/stepfun-image"; @@ -426,7 +427,10 @@ async function generateTextEditImageWithFailover(params: { }); const provider = createTextEditProvider(selected.provider); - const result = await provider.chat(selected.model.modelId).doGenerate({ + const runtimeModelId = resolveModelRuntimeId(selected.model, { + providerType: provider.type, + }); + const result = await provider.chat(runtimeModelId).doGenerate({ messages: [ { role: "user", diff --git a/app/api/scene-fusion/route.ts b/app/api/scene-fusion/route.ts index 3ee33a733..87a0cdd83 100644 --- a/app/api/scene-fusion/route.ts +++ b/app/api/scene-fusion/route.ts @@ -33,6 +33,7 @@ import { } from "@/lib/scene-fusion/output-capabilities"; import { acquireCanvasImageModelSlot } from "@/lib/canvas/image-concurrency"; import { createLogger } from "@/lib/observability/logger"; +import { resolveModelRuntimeId } from "@/lib/ai/model-runtime"; // 请求体 Schema const sceneFusionRequestSchema = z.object({ @@ -81,6 +82,7 @@ interface SceneFusionModelCandidate { supportsVision: boolean; imageGenConfig?: SceneFusionImageGenConfig | null; excludeFromFailover?: boolean; + defaultParams?: Record | null; }; provider: { id?: string; @@ -901,6 +903,7 @@ function buildSceneFusionCandidatePool( supportsVision: Boolean(entry.model.supportsVision), imageGenConfig: (entry.model.imageGenConfig || null) as SceneFusionImageGenConfig | null, excludeFromFailover: Boolean(entry.model.excludeFromFailover), + defaultParams: (entry.model.defaultParams as Record | null) || null, }, provider: { id: String(entry.provider.id || ""), @@ -1475,6 +1478,7 @@ async function processSceneWithProvider( sceneMode: SceneFusionMode, provider: { type: string; baseUrl: string; apiKey: string }, modelId: string, + runtimeModelId: string, aspectRatio?: string, imageSize?: string ): Promise<{ thumbnail: string; original: string; prompt: string }> { @@ -1501,7 +1505,15 @@ async function processSceneWithProvider( return processWithAlibabaImage(productImageUrl, sceneImageUrl, prompt, provider, modelId, aspectRatio, imageSize); case "ruxa-image": - return processWithRuxaImage(productImageUrl, sceneImageUrl, prompt, provider, modelId, aspectRatio, imageSize); + return processWithRuxaImage( + productImageUrl, + sceneImageUrl, + prompt, + provider, + runtimeModelId || modelId, + aspectRatio, + imageSize, + ); case "fal-image": return processWithFalImage(productImageUrl, sceneImageUrl, prompt, provider, modelId, aspectRatio, imageSize); @@ -1957,6 +1969,9 @@ async function processSceneWithDiversity( }); let result: Awaited>; try { + const runtimeModelId = resolveModelRuntimeId(candidate.model, { + providerType: candidate.provider.type, + }); await params.onProcessingStart?.(); result = await processSceneWithProvider( params.productImageUrl, @@ -1974,6 +1989,7 @@ async function processSceneWithDiversity( apiKey: candidate.provider.apiKey, }, candidate.model.modelId, + runtimeModelId, normalizedOutputParams.aspectRatio, normalizedOutputParams.imageSize ); diff --git a/lib/ai/dynamic-provider.ts b/lib/ai/dynamic-provider.ts index 8239c0076..4e742521b 100644 --- a/lib/ai/dynamic-provider.ts +++ b/lib/ai/dynamic-provider.ts @@ -37,6 +37,7 @@ import { isMoonshotOfficialBaseUrl, } from "./detail-agent-kimi-profile"; import { isBltcyVideoBaseUrl } from "./bltcy-video-models"; +import { resolveModelRuntimeId } from "./model-runtime"; // 缓存已创建的 provider 实例 const providerCache = new Map(); @@ -387,7 +388,10 @@ export async function getDynamicModel( return null; } - const resolvedModelId = String(overrideModelId || "").trim() || model.modelId; + const resolvedModelId = resolveModelRuntimeId(model, { + overrideModelId, + providerType: provider.type, + }); // 创建基础模型 logger.debug("model creation started", { diff --git a/lib/ai/model-runtime.ts b/lib/ai/model-runtime.ts new file mode 100644 index 000000000..69d4bcef9 --- /dev/null +++ b/lib/ai/model-runtime.ts @@ -0,0 +1,54 @@ +interface ModelRuntimeLike { + modelId?: string | null; + defaultParams?: unknown; +} + +interface ResolveModelRuntimeIdOptions { + overrideModelId?: string | null; + providerType?: string | null; +} + +function fallbackRuxaRuntimeId(modelId: string) { + if (!modelId.startsWith("google/")) { + return modelId; + } + + const candidate = modelId.slice("google/".length).trim(); + return candidate || modelId; +} + +function toRecord(value: unknown): Record | null { + if (typeof value !== "object" || value === null || Array.isArray(value)) { + return null; + } + return value as Record; +} + +function normalizeRuntimeModelId(value: unknown): string | null { + if (typeof value !== "string") return null; + const normalized = value.trim(); + return normalized || null; +} + +export function resolveModelRuntimeId( + model: ModelRuntimeLike | null | undefined, + options?: ResolveModelRuntimeIdOptions, +): string { + const overrideModelId = normalizeRuntimeModelId(options?.overrideModelId); + if (overrideModelId) { + return overrideModelId; + } + + const defaultParams = toRecord(model?.defaultParams); + const endpointModelId = normalizeRuntimeModelId(defaultParams?.endpoint); + if (endpointModelId) { + return endpointModelId; + } + + const resolvedModelId = normalizeRuntimeModelId(model?.modelId) || ""; + if (options?.providerType === "ruxa-image") { + return fallbackRuxaRuntimeId(resolvedModelId); + } + + return resolvedModelId; +} diff --git a/lib/ai/ruxa-image-models.ts b/lib/ai/ruxa-image-models.ts index 808305edc..e5c5a1b26 100644 --- a/lib/ai/ruxa-image-models.ts +++ b/lib/ai/ruxa-image-models.ts @@ -38,7 +38,7 @@ export const RUXA_IMAGE_MODEL_PRESETS: RuxaImageModelPreset[] = [ supportsImageGen: true, defaultParams: { provider: "ruxa", - endpoint: RUXA_NANO_BANANA_2_MODEL_ID, + endpoint: "nano-banana-2", imageSize: "2K", aspectRatio: "1:1", }, @@ -55,7 +55,7 @@ export const RUXA_IMAGE_MODEL_PRESETS: RuxaImageModelPreset[] = [ supportsImageGen: true, defaultParams: { provider: "ruxa", - endpoint: RUXA_NANO_BANANA_PRO_MODEL_ID, + endpoint: "nano-banana-pro", imageSize: "2K", aspectRatio: "1:1", }, @@ -71,4 +71,3 @@ export function getRuxaImageModelPresetById(modelId: string) { (preset) => preset.modelId === String(modelId || "").trim() ); } - diff --git a/lib/canvas/generate-image.ts b/lib/canvas/generate-image.ts index 90e083bc9..d77c59cf3 100644 --- a/lib/canvas/generate-image.ts +++ b/lib/canvas/generate-image.ts @@ -14,6 +14,7 @@ import { createMidjourney } from "@/lib/ai/providers/midjourney"; import { createRunway } from "@/lib/ai/providers/runway"; import { createKrea } from "@/lib/ai/providers/krea"; import { createVolcanoVision } from "@/lib/ai/providers/volcano-vision"; +import { resolveModelRuntimeId } from "@/lib/ai/model-runtime"; import { filterReachableRemoteUrls } from "@/lib/net/url-access"; import { acquireCanvasImageModelSlot, @@ -579,7 +580,10 @@ export async function generateCanvasImage( apiKey: provider.apiKey, baseURL: provider.baseUrl, }); - const modelInstance = ruxaProvider.chat(selectedModel.model.modelId); + const runtimeModelId = resolveModelRuntimeId(selectedModel.model, { + providerType: provider.type, + }); + const modelInstance = ruxaProvider.chat(runtimeModelId); executeGenerateBatch = async (batchImageCount) => { const result = await modelInstance.doGenerate({ messages,