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:
decolua
2026-04-25 16:22:30 +07:00
parent cca615eaff
commit 0b8bed5793
19 changed files with 1039 additions and 130 deletions
@@ -0,0 +1,183 @@
"use client";
import { useState, useEffect } from "react";
import PropTypes from "prop-types";
import { Modal, Input, Button, Badge } from "@/shared/components";
const DEFAULT_BASE_URL = "https://api.openai.com/v1";
// Dual-mode modal: edit when `node` provided, add otherwise
export default function AddCustomEmbeddingModal({ isOpen, onClose, onCreated, onSaved, node }) {
const isEdit = !!node;
const [formData, setFormData] = useState({
name: "",
prefix: "",
baseUrl: DEFAULT_BASE_URL,
});
const [submitting, setSubmitting] = useState(false);
const [checkKey, setCheckKey] = useState("");
const [checkModelId, setCheckModelId] = useState("");
const [validating, setValidating] = useState(false);
const [validationResult, setValidationResult] = useState(null);
useEffect(() => {
if (!isOpen) return;
setValidationResult(null);
setCheckKey("");
setCheckModelId("");
if (isEdit) {
setFormData({
name: node.name || "",
prefix: node.prefix || "",
baseUrl: node.baseUrl || DEFAULT_BASE_URL,
});
} else {
setFormData({ name: "", prefix: "", baseUrl: DEFAULT_BASE_URL });
}
}, [isOpen, isEdit, node]);
const handleSubmit = async () => {
if (!formData.name.trim() || !formData.prefix.trim() || !formData.baseUrl.trim()) return;
setSubmitting(true);
try {
const url = isEdit ? `/api/provider-nodes/${node.id}` : "/api/provider-nodes";
const method = isEdit ? "PUT" : "POST";
const payload = {
name: formData.name,
prefix: formData.prefix,
baseUrl: formData.baseUrl,
};
if (!isEdit) payload.type = "custom-embedding";
const res = await fetch(url, {
method,
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
const data = await res.json();
if (res.ok) {
if (isEdit) onSaved?.(data.node);
else onCreated?.(data.node);
}
} catch (error) {
console.log("Error saving custom embedding node:", error);
} finally {
setSubmitting(false);
}
};
const handleValidate = async () => {
setValidating(true);
try {
const res = await fetch("/api/provider-nodes/validate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
baseUrl: formData.baseUrl,
apiKey: checkKey,
type: "custom-embedding",
modelId: checkModelId.trim() || undefined,
}),
});
const data = await res.json();
setValidationResult(data);
} catch {
setValidationResult({ valid: false, error: "Network error" });
} finally {
setValidating(false);
}
};
const renderValidationResult = () => {
if (!validationResult) return null;
const { valid, error, dimensions } = validationResult;
if (valid) {
return (
<>
<Badge variant="success">Valid</Badge>
{dimensions && <span className="text-sm text-text-muted">{dimensions} dims</span>}
</>
);
}
return (
<div className="flex flex-col gap-1">
<Badge variant="error">Invalid</Badge>
{error && <span className="text-sm text-red-500">{error}</span>}
</div>
);
};
return (
<Modal isOpen={isOpen} title={isEdit ? "Edit Custom Embedding" : "Add Custom Embedding"} onClose={onClose}>
<div className="flex flex-col gap-4">
<Input
label="Name"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder="Voyage AI"
hint="Required. A friendly label for this embedding provider."
/>
<Input
label="Prefix"
value={formData.prefix}
onChange={(e) => setFormData({ ...formData, prefix: e.target.value })}
placeholder="voyage"
hint="Required. Used as the provider prefix for model IDs (e.g. voyage/voyage-3)."
/>
<Input
label="Base URL"
value={formData.baseUrl}
onChange={(e) => setFormData({ ...formData, baseUrl: e.target.value })}
placeholder="https://api.voyageai.com/v1"
hint="Most embedding APIs are OpenAI-compatible: Voyage, Cohere, Jina, Mistral, Together..."
/>
<Input
label="API Key (for Check)"
type="password"
value={checkKey}
onChange={(e) => setCheckKey(e.target.value)}
/>
<Input
label="Model ID (for Check)"
value={checkModelId}
onChange={(e) => setCheckModelId(e.target.value)}
placeholder="e.g. voyage-3, embed-english-v3.0, text-embedding-3-small"
hint="Required for validation. Will send a test embeddings request."
/>
<div className="flex items-center gap-3">
<Button
onClick={handleValidate}
disabled={!checkKey || !checkModelId.trim() || validating || !formData.baseUrl.trim()}
variant="secondary"
>
{validating ? "Checking..." : "Check"}
</Button>
{renderValidationResult()}
</div>
<div className="flex gap-2">
<Button
onClick={handleSubmit}
fullWidth
disabled={!formData.name.trim() || !formData.prefix.trim() || !formData.baseUrl.trim() || submitting}
>
{submitting ? (isEdit ? "Saving..." : "Creating...") : (isEdit ? "Save" : "Create")}
</Button>
<Button onClick={onClose} variant="ghost" fullWidth>Cancel</Button>
</div>
</div>
</Modal>
);
}
AddCustomEmbeddingModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
onCreated: PropTypes.func,
onSaved: PropTypes.func,
node: PropTypes.shape({
id: PropTypes.string,
name: PropTypes.string,
prefix: PropTypes.string,
baseUrl: PropTypes.string,
}),
};
+1
View File
@@ -29,6 +29,7 @@ export { default as CursorAuthModal } from "./CursorAuthModal";
export { default as IFlowCookieModal } from "./IFlowCookieModal";
export { default as GitLabAuthModal } from "./GitLabAuthModal";
export { default as EditConnectionModal } from "./EditConnectionModal";
export { default as AddCustomEmbeddingModal } from "./AddCustomEmbeddingModal";
export { default as SegmentedControl } from "./SegmentedControl";
export { default as Tooltip } from "./Tooltip";
+6 -1
View File
@@ -42,7 +42,7 @@ export const THINKING_CONFIG = {
export const OAUTH_PROVIDERS = {
claude: { id: "claude", alias: "cc", name: "Claude Code", icon: "smart_toy", color: "#D97757" },
antigravity: { id: "antigravity", alias: "ag", name: "Antigravity", icon: "rocket_launch", color: "#F59E0B", deprecated: true, deprecationNotice: "AG is designed exclusively for Antigravity IDE. Using it with other tools (OpenClaw, Claude, Codex...) may result in account restrictions or bans." },
codex: { id: "codex", alias: "cx", name: "OpenAI Codex", icon: "code", color: "#3B82F6", thinkingConfig: THINKING_CONFIG.effort },
codex: { id: "codex", alias: "cx", name: "OpenAI Codex", icon: "code", color: "#3B82F6", thinkingConfig: THINKING_CONFIG.effort, serviceKinds: ["llm", "image"], kindNotice: { image: "Requires a ChatGPT Plus (or higher) account. Free accounts are not supported for image generation." } },
github: { id: "github", alias: "gh", name: "GitHub Copilot", icon: "code", color: "#333333" },
cursor: { id: "cursor", alias: "cu", name: "Cursor IDE", icon: "edit_note", color: "#00D4AA" },
// "kimi-coding": { id: "kimi-coding", alias: "kmc", name: "Kimi Coding", icon: "psychology", color: "#1E40AF", textIcon: "KC" },
@@ -122,6 +122,7 @@ export const MEDIA_PROVIDER_KINDS = [
export const OPENAI_COMPATIBLE_PREFIX = "openai-compatible-";
export const ANTHROPIC_COMPATIBLE_PREFIX = "anthropic-compatible-";
export const CUSTOM_EMBEDDING_PREFIX = "custom-embedding-";
export function isOpenAICompatibleProvider(providerId) {
return typeof providerId === "string" && providerId.startsWith(OPENAI_COMPATIBLE_PREFIX);
@@ -131,6 +132,10 @@ export function isAnthropicCompatibleProvider(providerId) {
return typeof providerId === "string" && providerId.startsWith(ANTHROPIC_COMPATIBLE_PREFIX);
}
export function isCustomEmbeddingProvider(providerId) {
return typeof providerId === "string" && providerId.startsWith(CUSTOM_EMBEDDING_PREFIX);
}
// All providers (combined)
export const AI_PROVIDERS = { ...FREE_PROVIDERS, ...FREE_TIER_PROVIDERS, ...OAUTH_PROVIDERS, ...APIKEY_PROVIDERS, ...WEB_COOKIE_PROVIDERS };