diff --git a/src/app/api/generate/providers/__tests__/newapiwg.test.ts b/src/app/api/generate/providers/__tests__/newapiwg.test.ts index d03cbb3f..d14458d5 100644 --- a/src/app/api/generate/providers/__tests__/newapiwg.test.ts +++ b/src/app/api/generate/providers/__tests__/newapiwg.test.ts @@ -57,6 +57,19 @@ describe("NewApiWG model capability inference", () => { supported_endpoint_types: ["openai"], })).toEqual(["text-to-video", "image-to-video"]); + expect(inferNewApiWGCapabilities({ + id: "doubao-seed-2-0-pro-260215", + name: "Doubao Seed 2.0 Pro", + owned_by: "doubao", + supported_endpoint_types: ["openai"], + })).toEqual([]); + + expect(inferNewApiWGCapabilities({ + id: "doubao-1-5-vision-pro", + owned_by: "doubao", + supported_endpoint_types: ["openai"], + })).toEqual([]); + expect(inferNewApiWGCapabilities({ id: "speech-2.8-hd", owned_by: "minimax", @@ -89,6 +102,7 @@ describe("NewApiWG model capability inference", () => { vi.stubGlobal("fetch", vi.fn(async () => new Response(JSON.stringify({ data: [ { id: "kimi-k2.6", owned_by: "moonshot", supported_endpoint_types: ["openai"] }, + { id: "doubao-seed-2-0-pro-260215", owned_by: "doubao", supported_endpoint_types: ["openai"] }, { id: "doubao-seedance-2-0-260128", owned_by: "doubao-video", supported_endpoint_types: ["openai"] }, { id: "apiyi_sora_image", owned_by: "custom", supported_endpoint_types: ["openai"] }, ], @@ -361,6 +375,25 @@ describe("NewApiWG generation payloads", () => { expect(capturedBody!.image).toBe("data:image/png;base64,input"); }); + it("blocks cached Doubao chat models before calling media generation endpoints", async () => { + const result = await generateWithNewApiWG("test-key", "https://newapi.example/v1", makeInput({ + model: { + id: "doubao-seed-2-0-pro-260215", + name: "Doubao Seed 2.0 Pro", + description: null, + provider: "newapiwg", + capabilities: ["text-to-image", "image-to-image"], + }, + prompt: "draw a banana", + })); + + expect(result.success).toBe(false); + expect(result.error).toBe( + "Doubao Seed 2.0 Pro: this is a text/chat model, not an image, video, audio, or 3D generation model. Choose a generation model such as PopiArt Nano Pro, Seedream, or Seedance." + ); + expect(capturedBody).toBeNull(); + }); + it("normalizes transient provider overload errors", () => { expect(normalizeNewApiWGError("system disk overloaded", "Nano Banana")).toBe( "Nano Banana: provider is temporarily overloaded. Please retry in a moment or switch models." diff --git a/src/app/api/generate/providers/newapiwg.ts b/src/app/api/generate/providers/newapiwg.ts index 25125b40..cb7c5396 100644 --- a/src/app/api/generate/providers/newapiwg.ts +++ b/src/app/api/generate/providers/newapiwg.ts @@ -71,6 +71,10 @@ export function normalizeNewApiWGBaseUrl(baseUrl?: string | null): string { return (baseUrl || process.env.NEWAPIWG_BASE_URL || DEFAULT_NEWAPIWG_BASE_URL).replace(/\/+$/, ""); } +function isDoubaoChatModelName(text: string): boolean { + return /(^|[\s_\-/])doubao[\s_\-/](?!seedance|seedream)(?:seed|lite|pro|vision|flash|thinking|1|[0-9])/i.test(text); +} + function flattenTags(value: unknown): string[] { if (!value) return []; if (Array.isArray(value)) return value.flatMap(flattenTags); @@ -119,6 +123,9 @@ export function inferNewApiWGCapabilities(model: NewApiWGModelObject): ModelCapa ...flattenTags(model.supported_endpoint_types), ...rawTags, ].filter(Boolean).join(" ").toLowerCase(); + const modelNameText = [model.id, model.name].filter(Boolean).join(" ").toLowerCase(); + const isDoubaoChatOnly = isDoubaoChatModelName(modelNameText); + if (isDoubaoChatOnly) return []; const isImageNamedSora = /(^|[\s_\-/])sora[_\-/]?image(?=$|[\s_\-/])/.test(text); const hasImage = /image|img|vision|picture|photo|flux|sdxl|stable-diffusion|midjourney|dall-e|gpt-image|imagen|seedream|banana|ernie-image/.test(text); @@ -738,6 +745,13 @@ export async function generateWithNewApiWG( input: GenerationInput ): Promise { const normalizedBaseUrl = normalizeNewApiWGBaseUrl(baseUrl); + if (isDoubaoChatModelName(`${input.model.id} ${input.model.name}`)) { + return { + success: false, + error: `${input.model.name}: this is a text/chat model, not an image, video, audio, or 3D generation model. Choose a generation model such as PopiArt Nano Pro, Seedream, or Seedance.`, + }; + } + const isVideo = input.model.capabilities.some((cap) => cap.includes("video")); const isAudio = !isVideo && input.model.capabilities.includes("text-to-audio"); const prompt = promptFromInput(input); diff --git a/src/app/api/models/route.ts b/src/app/api/models/route.ts index 13e292df..2e2064a3 100644 --- a/src/app/api/models/route.ts +++ b/src/app/api/models/route.ts @@ -1241,7 +1241,7 @@ export async function GET( // For fal.ai, include search in cache key since their API supports search const cacheKey = provider === "newapiwg" - ? `${getCacheKey(provider)}:${newApiWGBaseUrl}:${newApiWGKey?.slice(-8) ?? "no-key"}` + ? `${getCacheKey(provider)}:cap-v2:${newApiWGBaseUrl}:${newApiWGKey?.slice(-8) ?? "no-key"}` : provider === "replicate" || provider === "wavespeed" ? getCacheKey(provider) : getCacheKey(provider, searchQuery); diff --git a/src/components/modals/ModelSearchDialog.tsx b/src/components/modals/ModelSearchDialog.tsx index 51a95ebf..2571047e 100644 --- a/src/components/modals/ModelSearchDialog.tsx +++ b/src/components/modals/ModelSearchDialog.tsx @@ -10,7 +10,7 @@ import { ProviderModel, ModelCapability } from "@/lib/providers/types"; import { useI18n } from "@/i18n"; // localStorage cache for models (persists across dev server restarts) -const MODELS_CACHE_KEY = "node-banana-models-cache"; +const MODELS_CACHE_KEY = "node-banana-models-cache-v2"; const MODELS_CACHE_TTL = 48 * 60 * 60 * 1000; // 48 hours interface ModelsCacheEntry {