Enhance image and embedding provider support
- Added new image models for GPT 5.2, 5.3, and 5.4, including capabilities for text-to-image and editing. - Updated embedding handling to include optional dimensions in requests. - Introduced support for custom embedding providers, allowing dynamic fetching and validation of custom nodes. - Improved image generation handling with Codex integration, including progress tracking and error handling. - Enhanced UI components to support adding custom embeddings and displaying their status.
This commit is contained in:
@@ -40,6 +40,14 @@ export async function PUT(request, { params }) {
|
||||
}
|
||||
}
|
||||
|
||||
// Sanitize Base URL for Custom Embedding (strip trailing slash and /embeddings)
|
||||
if (node.type === "custom-embedding") {
|
||||
sanitizedBaseUrl = sanitizedBaseUrl.replace(/\/$/, "");
|
||||
if (sanitizedBaseUrl.endsWith("/embeddings")) {
|
||||
sanitizedBaseUrl = sanitizedBaseUrl.slice(0, -"/embeddings".length);
|
||||
}
|
||||
}
|
||||
|
||||
const updates = {
|
||||
name: name.trim(),
|
||||
prefix: prefix.trim(),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { createProviderNode, getProviderNodes } from "@/models";
|
||||
import { OPENAI_COMPATIBLE_PREFIX, ANTHROPIC_COMPATIBLE_PREFIX } from "@/shared/constants/providers";
|
||||
import { OPENAI_COMPATIBLE_PREFIX, ANTHROPIC_COMPATIBLE_PREFIX, CUSTOM_EMBEDDING_PREFIX } from "@/shared/constants/providers";
|
||||
import { generateId } from "@/shared/utils";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
@@ -13,6 +13,10 @@ const ANTHROPIC_COMPATIBLE_DEFAULTS = {
|
||||
baseUrl: "https://api.anthropic.com/v1",
|
||||
};
|
||||
|
||||
const CUSTOM_EMBEDDING_DEFAULTS = {
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
};
|
||||
|
||||
// GET /api/provider-nodes - List all provider nodes
|
||||
export async function GET() {
|
||||
try {
|
||||
@@ -57,6 +61,23 @@ export async function POST(request) {
|
||||
return NextResponse.json({ node }, { status: 201 });
|
||||
}
|
||||
|
||||
if (nodeType === "custom-embedding") {
|
||||
// Strip trailing slash and /embeddings if user pasted full endpoint
|
||||
let sanitizedBaseUrl = (baseUrl || CUSTOM_EMBEDDING_DEFAULTS.baseUrl).trim().replace(/\/$/, "");
|
||||
if (sanitizedBaseUrl.endsWith("/embeddings")) {
|
||||
sanitizedBaseUrl = sanitizedBaseUrl.slice(0, -"/embeddings".length);
|
||||
}
|
||||
|
||||
const node = await createProviderNode({
|
||||
id: `${CUSTOM_EMBEDDING_PREFIX}${generateId()}`,
|
||||
type: "custom-embedding",
|
||||
prefix: prefix.trim(),
|
||||
baseUrl: sanitizedBaseUrl,
|
||||
name: name.trim(),
|
||||
});
|
||||
return NextResponse.json({ node }, { status: 201 });
|
||||
}
|
||||
|
||||
if (nodeType === "anthropic-compatible") {
|
||||
// Sanitize Base URL: remove trailing slash, and remove trailing /messages if user added it
|
||||
// This prevents double-appending /messages at runtime
|
||||
|
||||
@@ -64,6 +64,36 @@ export async function POST(request) {
|
||||
return NextResponse.json({ error: "Invalid URL format" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Custom Embedding Validation - test POST /embeddings directly
|
||||
if (type === "custom-embedding") {
|
||||
const normalizedBase = baseUrl.trim().replace(/\/$/, "");
|
||||
if (!modelId?.trim()) {
|
||||
return NextResponse.json({ valid: false, error: "Model ID required for embedding validation" });
|
||||
}
|
||||
const embedRes = await fetchWithTimeout(`${normalizedBase}/embeddings`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Authorization": `Bearer ${apiKey}`,
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({ model: modelId.trim(), input: "ping" })
|
||||
});
|
||||
if (embedRes.ok) {
|
||||
const data = await embedRes.json().catch(() => null);
|
||||
const dims = Array.isArray(data?.data?.[0]?.embedding) ? data.data[0].embedding.length : null;
|
||||
return NextResponse.json({ valid: true, method: "embeddings", dimensions: dims });
|
||||
}
|
||||
if (embedRes.status === 401 || embedRes.status === 403) {
|
||||
return NextResponse.json({ valid: false, error: "API key unauthorized" });
|
||||
}
|
||||
const errBody = await embedRes.text().catch(() => "");
|
||||
return NextResponse.json({
|
||||
valid: false,
|
||||
error: `Embeddings request failed (${embedRes.status})${errBody ? `: ${errBody.slice(0, 200)}` : ""}`,
|
||||
method: "embeddings"
|
||||
});
|
||||
}
|
||||
|
||||
// Anthropic Compatible Validation
|
||||
if (type === "anthropic-compatible") {
|
||||
let normalizedBase = baseUrl.trim().replace(/\/$/, "");
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
getProxyPoolById,
|
||||
} from "@/models";
|
||||
import { APIKEY_PROVIDERS } from "@/shared/constants/config";
|
||||
import { FREE_TIER_PROVIDERS, WEB_COOKIE_PROVIDERS, isOpenAICompatibleProvider, isAnthropicCompatibleProvider } from "@/shared/constants/providers";
|
||||
import { FREE_TIER_PROVIDERS, WEB_COOKIE_PROVIDERS, isOpenAICompatibleProvider, isAnthropicCompatibleProvider, isCustomEmbeddingProvider } from "@/shared/constants/providers";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
@@ -104,7 +104,8 @@ export async function POST(request) {
|
||||
FREE_TIER_PROVIDERS[provider] ||
|
||||
isWebCookieProvider ||
|
||||
isOpenAICompatibleProvider(provider) ||
|
||||
isAnthropicCompatibleProvider(provider);
|
||||
isAnthropicCompatibleProvider(provider) ||
|
||||
isCustomEmbeddingProvider(provider);
|
||||
|
||||
if (!provider || !isValidProvider) {
|
||||
return NextResponse.json({ error: "Invalid provider" }, { status: 400 });
|
||||
@@ -146,6 +147,22 @@ export async function POST(request) {
|
||||
return NextResponse.json({ error: "Only one connection is allowed for this Anthropic Compatible node" }, { status: 400 });
|
||||
}
|
||||
|
||||
providerSpecificData = {
|
||||
prefix: node.prefix,
|
||||
baseUrl: node.baseUrl,
|
||||
nodeName: node.name,
|
||||
};
|
||||
} else if (isCustomEmbeddingProvider(provider)) {
|
||||
const node = await getProviderNodeById(provider);
|
||||
if (!node) {
|
||||
return NextResponse.json({ error: "Custom Embedding node not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
const existingConnections = await getProviderConnections({ provider });
|
||||
if (existingConnections.length > 0) {
|
||||
return NextResponse.json({ error: "Only one connection is allowed for this Custom Embedding node" }, { status: 400 });
|
||||
}
|
||||
|
||||
providerSpecificData = {
|
||||
prefix: node.prefix,
|
||||
baseUrl: node.baseUrl,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getProviderNodeById } from "@/models";
|
||||
import { isOpenAICompatibleProvider, isAnthropicCompatibleProvider } from "@/shared/constants/providers";
|
||||
import { isOpenAICompatibleProvider, isAnthropicCompatibleProvider, isCustomEmbeddingProvider } from "@/shared/constants/providers";
|
||||
import { getDefaultModel } from "open-sse/config/providerModels.js";
|
||||
import { resolveOllamaLocalHost } from "open-sse/config/providers.js";
|
||||
|
||||
@@ -35,6 +35,37 @@ export async function POST(request) {
|
||||
});
|
||||
}
|
||||
|
||||
// Custom Embedding nodes: probe /models (most embedding APIs are OpenAI-compatible)
|
||||
if (isCustomEmbeddingProvider(provider)) {
|
||||
const node = await getProviderNodeById(provider);
|
||||
if (!node) {
|
||||
return NextResponse.json({ error: "Custom Embedding node not found" }, { status: 404 });
|
||||
}
|
||||
const baseUrl = node.baseUrl?.replace(/\/$/, "");
|
||||
const modelsRes = await fetch(`${baseUrl}/models`, {
|
||||
headers: { "Authorization": `Bearer ${apiKey}` },
|
||||
});
|
||||
if (modelsRes.ok) {
|
||||
return NextResponse.json({ valid: true });
|
||||
}
|
||||
// Auth errors are definitive
|
||||
if (modelsRes.status === 401 || modelsRes.status === 403) {
|
||||
return NextResponse.json({ valid: false, error: "Invalid API key" });
|
||||
}
|
||||
// Fallback: probe /embeddings with a common test model — many providers lack /models
|
||||
const embedRes = await fetch(`${baseUrl}/embeddings`, {
|
||||
method: "POST",
|
||||
headers: { "Authorization": `Bearer ${apiKey}`, "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ model: "test", input: "ping" }),
|
||||
});
|
||||
// 401/403 = bad key; anything else (including 400 "model not found") means key works
|
||||
isValid = embedRes.status !== 401 && embedRes.status !== 403;
|
||||
return NextResponse.json({
|
||||
valid: isValid,
|
||||
error: isValid ? null : "Invalid API key",
|
||||
});
|
||||
}
|
||||
|
||||
if (isAnthropicCompatibleProvider(provider)) {
|
||||
const node = await getProviderNodeById(provider);
|
||||
if (!node) {
|
||||
|
||||
Reference in New Issue
Block a user