fix: route grsai gpt image provider
This commit is contained in:
parent
85c0f2c096
commit
e58d37015e
@ -455,6 +455,7 @@ type AiProvider = {
|
||||
| "custom"
|
||||
| "local"
|
||||
| "gemini"
|
||||
| "grsai-gpt-image"
|
||||
| "huanke-gemini"
|
||||
| "qianwen-video"
|
||||
| "doubao-video"
|
||||
@ -1186,6 +1187,7 @@ const PROVIDER_TYPES = [
|
||||
{ value: "ruxa-image", label: "RUXA生图" },
|
||||
{ value: "fal-image", label: "FAL 生图模型" },
|
||||
{ value: "openai-image", label: "OpenAI 图片兼容" },
|
||||
{ value: "grsai-gpt-image", label: "Grsai GPT Image" },
|
||||
{ value: "midjourney", label: "Midjourney" },
|
||||
{ value: "tripo-3d", label: "Tripo3D 模型生成" },
|
||||
{ value: "worldlabs-3d", label: "World Labs 3D 世界生成" },
|
||||
@ -1215,6 +1217,7 @@ const PROVIDER_DEFAULT_BASE_URL: Record<string, string> = {
|
||||
"ruxa-image": "https://api.ruxa.ai",
|
||||
"fal-image": "https://fal.run",
|
||||
"openai-image": "https://api.openai.com/v1",
|
||||
"grsai-gpt-image": "https://grsai.dakka.com.cn",
|
||||
midjourney: "https://your-midjourney-api.example.com",
|
||||
"fal-video": "https://fal.run",
|
||||
"tripo-3d": "https://api.tripo3d.ai/v2/openapi",
|
||||
@ -3008,6 +3011,7 @@ export function AdminSettingsClient() {
|
||||
| "custom"
|
||||
| "local"
|
||||
| "gemini"
|
||||
| "grsai-gpt-image"
|
||||
| "huanke-gemini"
|
||||
| "qianwen-video"
|
||||
| "doubao-video"
|
||||
|
||||
@ -15,6 +15,7 @@ const providerTypes = [
|
||||
"custom",
|
||||
"local",
|
||||
"gemini",
|
||||
"grsai-gpt-image",
|
||||
"huanke-gemini",
|
||||
"qianwen-video",
|
||||
"doubao-video",
|
||||
@ -69,6 +70,7 @@ const WORLDLABS_3D_BASE_URL = "https://api.worldlabs.ai";
|
||||
const KITTYCAD_CAD_BASE_URL = "https://api.zoo.dev";
|
||||
const RUNWAY_BASE_URL = "https://api.dev.runwayml.com";
|
||||
const KREA_BASE_URL = "https://api.krea.ai";
|
||||
const GRSAI_GPT_IMAGE_BASE_URL = "https://grsai.dakka.com.cn";
|
||||
const RUNWAY_API_KEY_PATTERN = /^key_[0-9a-f]{128}$/;
|
||||
|
||||
function normalizeProviderTypeForStorage(
|
||||
@ -222,6 +224,8 @@ export async function POST(request: Request) {
|
||||
? data.baseUrl || RUNWAY_BASE_URL
|
||||
: data.type === "krea"
|
||||
? data.baseUrl || KREA_BASE_URL
|
||||
: data.type === "grsai-gpt-image"
|
||||
? data.baseUrl || GRSAI_GPT_IMAGE_BASE_URL
|
||||
: data.baseUrl,
|
||||
updatedBy: session.user.id,
|
||||
});
|
||||
|
||||
@ -1132,7 +1132,12 @@ export function Chat({
|
||||
|
||||
let modelType: PendingGeneration["modelType"] = "other";
|
||||
// 🆕 gemini-image 和 huanke-gemini 都视为 gemini 类型
|
||||
if (model?.providerType === "gemini" || model?.providerType === "gemini-image" || model?.providerType === "huanke-gemini") modelType = "gemini";
|
||||
if (
|
||||
model?.providerType === "gemini" ||
|
||||
model?.providerType === "gemini-image" ||
|
||||
model?.providerType === "grsai-gpt-image" ||
|
||||
model?.providerType === "huanke-gemini"
|
||||
) modelType = "gemini";
|
||||
else if (model?.providerType === "volcano-vision") modelType = "volcano-vision";
|
||||
else if (
|
||||
isVideoModelDescriptor({
|
||||
|
||||
@ -108,9 +108,14 @@ function PureMessages({
|
||||
);
|
||||
|
||||
// 判断当前选中的模型类型
|
||||
// 🆕 包含 gemini 和 huanke-gemini 两种 Gemini 图片生成 provider
|
||||
// 🆕 包含 Gemini 和 Grsai GPT Image 图片生成 provider
|
||||
const isGeminiModel = modelsData?.models?.some(
|
||||
(m: DynamicModel) => m.id === selectedModelId && (m.providerType === "gemini" || m.providerType === "gemini-image" || m.providerType === "huanke-gemini")
|
||||
(m: DynamicModel) => m.id === selectedModelId && (
|
||||
m.providerType === "gemini" ||
|
||||
m.providerType === "gemini-image" ||
|
||||
m.providerType === "grsai-gpt-image" ||
|
||||
m.providerType === "huanke-gemini"
|
||||
)
|
||||
) ?? false;
|
||||
|
||||
const isQianwenVideoModel = modelsData?.models?.some(
|
||||
|
||||
@ -128,6 +128,15 @@ function getOrCreateProvider(provider: Pick<AiProvider, "id" | "type" | "baseUrl
|
||||
return geminiProvider as any;
|
||||
}
|
||||
|
||||
case "grsai-gpt-image": {
|
||||
const grsaiGptImageProvider = createGeminiImage({
|
||||
baseURL: resolvedBaseUrl,
|
||||
apiKey: provider.apiKey,
|
||||
});
|
||||
console.log(`[Provider] Created Grsai GPT Image provider: ${resolvedBaseUrl}`);
|
||||
return grsaiGptImageProvider as any;
|
||||
}
|
||||
|
||||
case "huanke-gemini": {
|
||||
// 环科 Gemini 原生格式 API
|
||||
// 使用 /v1beta/models/{model}:generateContent 端点
|
||||
@ -340,13 +349,14 @@ export async function getDynamicModel(
|
||||
// customProvider (gemini, local) 使用 .languageModel()
|
||||
// OpenAI-compatible 使用 .chat()
|
||||
let baseModel;
|
||||
if (provider.type === "gemini") {
|
||||
// Gemini 图片生成使用 chat() 方法(与 local-qwen 相同的调用方式)
|
||||
console.log("[getDynamicModel] Getting Gemini chat model..."); // 调试日志
|
||||
if (provider.type === "gemini" || provider.type === "grsai-gpt-image") {
|
||||
const imageProviderLabel = provider.type === "grsai-gpt-image" ? "Grsai GPT Image" : "Gemini image";
|
||||
// 图片生成 provider 使用 chat() 方法(与 local-qwen 相同的调用方式)
|
||||
console.log(`[getDynamicModel] Getting ${imageProviderLabel} chat model...`); // 调试日志
|
||||
baseModel = providerInstance.chat(resolvedModelId);
|
||||
console.log("[getDynamicModel] Gemini model retrieved:", baseModel ? "success" : "failed"); // 调试日志
|
||||
console.log("[getDynamicModel] Gemini model type:", typeof baseModel); // 调试
|
||||
console.log("[getDynamicModel] Gemini model keys:", baseModel ? Object.keys(baseModel).join(', ') : 'N/A'); // 调试
|
||||
console.log(`[getDynamicModel] ${imageProviderLabel} model retrieved:`, baseModel ? "success" : "failed"); // 调试日志
|
||||
console.log(`[getDynamicModel] ${imageProviderLabel} model type:`, typeof baseModel); // 调试
|
||||
console.log(`[getDynamicModel] ${imageProviderLabel} model keys:`, baseModel ? Object.keys(baseModel).join(', ') : 'N/A'); // 调试
|
||||
} else if (provider.type === "local") {
|
||||
// 本地模型使用 chat() 方法
|
||||
console.log("[getDynamicModel] Getting local chat model..."); // 调试日志
|
||||
|
||||
@ -3,6 +3,7 @@ import type { AiProvider } from "@/lib/db/schema";
|
||||
const PROVIDER_DEFAULT_BASE_URL: Partial<Record<AiProvider["type"], string>> = {
|
||||
alibaba: "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||||
openai: "https://api.openai.com/v1",
|
||||
"grsai-gpt-image": "https://grsai.dakka.com.cn",
|
||||
"huanke-gemini": "https://api.huandutech.com/v1",
|
||||
"qianwen-video": "https://dashscope.aliyuncs.com/api/v1/services/aigc/video-generation",
|
||||
"doubao-video": "https://ark.cn-beijing.volces.com/api/v3",
|
||||
|
||||
@ -418,8 +418,12 @@ async function sendGeminiRequest(
|
||||
imageSize?: string;
|
||||
}
|
||||
): Promise<GeminiResponse> {
|
||||
// 使用 Nano Banana 原生端点(支持完整的图片生成参数)
|
||||
const url = `${baseURL}/v1/draw/nano-banana`;
|
||||
const isGptImageModel = /^gpt-image/i.test(modelId);
|
||||
const apiLogPrefix = isGptImageModel ? "[Grsai GPT Image]" : "[Gemini Image]";
|
||||
// GPT Image 和 Nano Banana 在 Grsai 上是两套接口,不能混用同一个 endpoint。
|
||||
const url = isGptImageModel
|
||||
? `${baseURL}/v1/draw/completions`
|
||||
: `${baseURL}/v1/draw/nano-banana`;
|
||||
|
||||
// 提取最后一条消息的文本作为prompt
|
||||
const lastMessage = messages[messages.length - 1];
|
||||
@ -453,20 +457,20 @@ async function sendGeminiRequest(
|
||||
// 🆕 处理参考图:上传 Base64 到 S3
|
||||
const processedUrls: string[] = [];
|
||||
if (urls.length > 0) {
|
||||
console.log(`[Gemini Image] Processing ${urls.length} reference images...`);
|
||||
console.log(`${apiLogPrefix} Processing ${urls.length} reference images...`);
|
||||
for (const url of urls) {
|
||||
const s3Url = await ensureImageUrl(url);
|
||||
processedUrls.push(s3Url);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("[Gemini Image] Sending request to:", url);
|
||||
console.log("[Gemini Image] Using model:", modelId);
|
||||
console.log("[Gemini Image] Prompt:", prompt.substring(0, 100));
|
||||
console.log("[Gemini Image] Reference image count:", urls.length);
|
||||
console.log("[Gemini Image] Image params:", imageParams);
|
||||
console.log(`${apiLogPrefix} Sending request to:`, url);
|
||||
console.log(`${apiLogPrefix} Using model:`, modelId);
|
||||
console.log(`${apiLogPrefix} Prompt:`, prompt.substring(0, 100));
|
||||
console.log(`${apiLogPrefix} Reference image count:`, urls.length);
|
||||
console.log(`${apiLogPrefix} Image params:`, imageParams);
|
||||
|
||||
// 使用 Nano Banana 原生格式
|
||||
// 使用对应模型的原生格式
|
||||
const requestBody: any = {
|
||||
model: modelId,
|
||||
prompt: prompt,
|
||||
@ -477,19 +481,24 @@ async function sendGeminiRequest(
|
||||
requestBody.urls = processedUrls;
|
||||
}
|
||||
|
||||
// 添加 Nano Banana 特定参数
|
||||
if (imageParams?.aspectRatio) {
|
||||
requestBody.aspectRatio = imageParams.aspectRatio;
|
||||
}
|
||||
if (imageParams?.imageSize) {
|
||||
requestBody.imageSize = imageParams.imageSize;
|
||||
if (isGptImageModel) {
|
||||
requestBody.size = imageParams?.aspectRatio || "auto";
|
||||
requestBody.shutProgress = false;
|
||||
} else {
|
||||
// 添加 Nano Banana 特定参数
|
||||
if (imageParams?.aspectRatio) {
|
||||
requestBody.aspectRatio = imageParams.aspectRatio;
|
||||
}
|
||||
if (imageParams?.imageSize) {
|
||||
requestBody.imageSize = imageParams.imageSize;
|
||||
}
|
||||
}
|
||||
|
||||
console.log("[Gemini Image] Request body (truncated):", JSON.stringify(requestBody).substring(0, 500) + "...");
|
||||
console.log(`${apiLogPrefix} Request body (truncated):`, JSON.stringify(requestBody).substring(0, 500) + "...");
|
||||
|
||||
// 根据分辨率动态设置超时时间
|
||||
const timeout = imageParams?.imageSize === "4K" ? 300000 : 120000; // 4K: 5分钟, 其他: 2分钟
|
||||
console.log(`[Gemini Image] Timeout set to ${timeout/1000} seconds for ${imageParams?.imageSize || "default"}`);
|
||||
console.log(`${apiLogPrefix} Timeout set to ${timeout/1000} seconds for ${imageParams?.imageSize || "default"}`);
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
||||
@ -509,14 +518,14 @@ async function sendGeminiRequest(
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error("[Gemini Image] Request failed:", response.status, errorText);
|
||||
console.error(`${apiLogPrefix} Request failed:`, response.status, errorText);
|
||||
throw new Error(
|
||||
`Gemini API error: ${response.status} ${response.statusText}`
|
||||
`${isGptImageModel ? "Grsai GPT Image" : "Gemini"} API error: ${response.status} ${response.statusText}`
|
||||
);
|
||||
}
|
||||
|
||||
// 处理流式响应(SSE格式)
|
||||
console.log("[Gemini Image] Reading stream response...");
|
||||
console.log(`${apiLogPrefix} Reading stream response...`);
|
||||
const reader = response.body?.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = "";
|
||||
|
||||
@ -41,6 +41,7 @@ export interface CanvasGenerateImageResult {
|
||||
|
||||
const SUPPORTED_CANVAS_PROVIDER_TYPES = new Set([
|
||||
"gemini",
|
||||
"grsai-gpt-image",
|
||||
"alibaba-image",
|
||||
"bfl-image",
|
||||
"ruxa-image",
|
||||
@ -92,6 +93,7 @@ function getProviderBatchLimit(providerType: string) {
|
||||
case "krea":
|
||||
return 4;
|
||||
case "gemini":
|
||||
case "grsai-gpt-image":
|
||||
return 2;
|
||||
case "alibaba-image":
|
||||
case "fal-image":
|
||||
@ -173,7 +175,7 @@ export async function generateCanvasImage(
|
||||
}
|
||||
if (!SUPPORTED_CANVAS_PROVIDER_TYPES.has(fallbackProvider.type)) {
|
||||
throw new Error(
|
||||
"当前图片模型类型暂不支持画布生成,请在后台选择 Gemini/阿里/BFL/RUXA/FAL/OpenAI/Runway/Krea 图片模型。"
|
||||
"当前图片模型类型暂不支持画布生成,请在后台选择 Gemini/Grsai GPT Image/阿里/BFL/RUXA/FAL/OpenAI/Runway/Krea 图片模型。"
|
||||
);
|
||||
}
|
||||
if (fallbackModel.model.id !== selectedModel.model.id) {
|
||||
@ -252,7 +254,7 @@ export async function generateCanvasImage(
|
||||
| ((batchImageCount: number) => Promise<string>)
|
||||
| null = null;
|
||||
|
||||
if (provider.type === "gemini") {
|
||||
if (provider.type === "gemini" || provider.type === "grsai-gpt-image") {
|
||||
const geminiProvider = createGeminiImage({
|
||||
apiKey: provider.apiKey,
|
||||
baseURL: provider.baseUrl,
|
||||
@ -392,7 +394,7 @@ export async function generateCanvasImage(
|
||||
};
|
||||
} else {
|
||||
throw new Error(
|
||||
`不支持的图片生成模型类型: ${provider.type || "unknown"},请改用 Gemini/阿里/BFL/RUXA/FAL/OpenAI/Midjourney/Runway/Krea 图片模型`
|
||||
`不支持的图片生成模型类型: ${provider.type || "unknown"},请改用 Gemini/Grsai GPT Image/阿里/BFL/RUXA/FAL/OpenAI/Midjourney/Runway/Krea 图片模型`
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -13,6 +13,7 @@ config({
|
||||
const providerBaseUrlDefaults = [
|
||||
{ type: "alibaba", baseUrl: "https://dashscope.aliyuncs.com/compatible-mode/v1" },
|
||||
{ type: "openai", baseUrl: "https://api.openai.com/v1" },
|
||||
{ type: "grsai-gpt-image", baseUrl: "https://grsai.dakka.com.cn" },
|
||||
{ type: "huanke-gemini", baseUrl: "https://api.huandutech.com/v1" },
|
||||
{
|
||||
type: "qianwen-video",
|
||||
@ -96,7 +97,7 @@ const runMigrate = async () => {
|
||||
ADD CONSTRAINT "AiProvider_type_check"
|
||||
CHECK (type IN (
|
||||
'alibaba', 'openai', 'custom', 'local', 'gemini',
|
||||
'huanke-gemini', 'qianwen-video', 'doubao-video', 'ruxa-video', 'volcano-vision', 'volcano-video', 'alibaba-image', 'bfl-image', 'ruxa-image', 'fal-image', 'openai-image', 'midjourney', 'fal-video', 'zhipu', 'tripo-3d', 'worldlabs-3d', 'kittycad-cad', 'runway', 'krea'
|
||||
'grsai-gpt-image', 'huanke-gemini', 'qianwen-video', 'doubao-video', 'ruxa-video', 'volcano-vision', 'volcano-video', 'alibaba-image', 'bfl-image', 'ruxa-image', 'fal-image', 'openai-image', 'midjourney', 'fal-video', 'zhipu', 'tripo-3d', 'worldlabs-3d', 'kittycad-cad', 'runway', 'krea'
|
||||
));
|
||||
`);
|
||||
await db.execute(sql`
|
||||
|
||||
@ -3750,7 +3750,7 @@ export async function createAiProvider({
|
||||
updatedBy,
|
||||
}: {
|
||||
name: string;
|
||||
type: "alibaba" | "openai" | "custom" | "local" | "gemini" | "huanke-gemini" | "qianwen-video" | "doubao-video" | "ruxa-video" | "volcano-vision" | "volcano-video" | "alibaba-image" | "bfl-image" | "ruxa-image" | "fal-image" | "openai-image" | "midjourney" | "fal-video" | "zhipu" | "tripo-3d" | "worldlabs-3d" | "kittycad-cad" | "runway" | "krea";
|
||||
type: "alibaba" | "openai" | "custom" | "local" | "gemini" | "grsai-gpt-image" | "huanke-gemini" | "qianwen-video" | "doubao-video" | "ruxa-video" | "volcano-vision" | "volcano-video" | "alibaba-image" | "bfl-image" | "ruxa-image" | "fal-image" | "openai-image" | "midjourney" | "fal-video" | "zhipu" | "tripo-3d" | "worldlabs-3d" | "kittycad-cad" | "runway" | "krea";
|
||||
baseUrl: string;
|
||||
apiKey: string;
|
||||
isEnabled?: boolean;
|
||||
@ -3788,7 +3788,7 @@ export async function updateAiProvider({
|
||||
}: {
|
||||
id: string;
|
||||
name?: string;
|
||||
type?: "alibaba" | "openai" | "custom" | "local" | "gemini" | "huanke-gemini" | "qianwen-video" | "doubao-video" | "ruxa-video" | "volcano-vision" | "volcano-video" | "alibaba-image" | "bfl-image" | "ruxa-image" | "fal-image" | "openai-image" | "midjourney" | "fal-video" | "zhipu" | "tripo-3d" | "worldlabs-3d" | "kittycad-cad" | "runway" | "krea";
|
||||
type?: "alibaba" | "openai" | "custom" | "local" | "gemini" | "grsai-gpt-image" | "huanke-gemini" | "qianwen-video" | "doubao-video" | "ruxa-video" | "volcano-vision" | "volcano-video" | "alibaba-image" | "bfl-image" | "ruxa-image" | "fal-image" | "openai-image" | "midjourney" | "fal-video" | "zhipu" | "tripo-3d" | "worldlabs-3d" | "kittycad-cad" | "runway" | "krea";
|
||||
baseUrl?: string;
|
||||
apiKey?: string;
|
||||
isEnabled?: boolean;
|
||||
|
||||
@ -276,7 +276,7 @@ export type ToolConfig = InferSelectModel<typeof toolConfig>;
|
||||
export const aiProvider = pgTable("AiProvider", {
|
||||
id: uuid("id").primaryKey().notNull().defaultRandom(),
|
||||
name: varchar("name", { length: 128 }).notNull(), // 提供商名称,如 "阿里云通义千问"
|
||||
type: varchar("type", { enum: ["alibaba", "openai", "custom", "local", "gemini", "huanke-gemini", "qianwen-video", "doubao-video", "ruxa-video", "volcano-vision", "volcano-video", "alibaba-image", "ruxa-image", "fal-image", "openai-image", "midjourney", "fal-video", "zhipu", "tripo-3d", "worldlabs-3d", "kittycad-cad", "runway", "krea", "bfl-image"] })
|
||||
type: varchar("type", { enum: ["alibaba", "openai", "custom", "local", "gemini", "grsai-gpt-image", "huanke-gemini", "qianwen-video", "doubao-video", "ruxa-video", "volcano-vision", "volcano-video", "alibaba-image", "ruxa-image", "fal-image", "openai-image", "midjourney", "fal-video", "zhipu", "tripo-3d", "worldlabs-3d", "kittycad-cad", "runway", "krea", "bfl-image"] })
|
||||
.notNull()
|
||||
.default("custom"), // 提供商类型
|
||||
baseUrl: text("baseUrl").notNull(), // API 基础 URL
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user