"use client"; import { useParams, notFound, useRouter } from "next/navigation"; import Link from "next/link"; import { useState, useEffect } from "react"; import { Card, Badge, Button, AddCustomEmbeddingModal } from "@/shared/components"; import ProviderIcon from "@/shared/components/ProviderIcon"; import { MEDIA_PROVIDER_KINDS, AI_PROVIDERS, getProviderAlias, isCustomEmbeddingProvider } from "@/shared/constants/providers"; import { getModelsByProviderId } from "@/shared/constants/models"; import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard"; import ConnectionsCard from "@/app/(dashboard)/dashboard/providers/components/ConnectionsCard"; import ModelsCard from "@/app/(dashboard)/dashboard/providers/components/ModelsCard"; import { TTS_PROVIDER_CONFIG } from "@/shared/constants/ttsProviders"; import { getTtsVoicesForModel } from "open-sse/config/ttsModels.js"; // Shared row layout — defined outside components to avoid re-mount on re-render function Row({ label, children }) { return (
{label}
{children}
); } const DEFAULT_TTS_RESPONSE_EXAMPLE = `// Audio will appear here after running. // Example JSON response (response_format=json): { "format": "mp3", "audio": "//NExAANaAIIAUAAANNNNNNNN..." // base64 encoded MP3 }`; const DEFAULT_RESPONSE_EXAMPLE = `{ "object": "list", "data": [{ "object": "embedding", "index": 0, "embedding": [0.002301, -0.019212, 0.004815, -0.031249, ...] }], "model": "...", "usage": { "prompt_tokens": 9, "total_tokens": 9 } }`; // Config-driven example defaults per kind const KIND_EXAMPLE_CONFIG = { webSearch: { inputLabel: "Query", inputPlaceholder: "What is the latest news about AI?", defaultInput: "What is the latest news about AI?", bodyKey: "query", defaultResponse: `{\n "results": [\n { "title": "...", "url": "...", "snippet": "..." }\n ]\n}`, }, webFetch: { inputLabel: "URL", inputPlaceholder: "https://example.com", defaultInput: "https://example.com", bodyKey: "url", defaultResponse: `{\n "content": "...",\n "title": "...",\n "url": "..."\n}`, }, image: { inputLabel: "Prompt", inputPlaceholder: "A cute cat wearing a hat", defaultInput: "A cute cat wearing a hat", bodyKey: "prompt", defaultResponse: `{\n "data": [\n { "url": "...", "b64_json": "..." }\n ]\n}`, extraFields: [ { key: "n", label: "n", type: "number", default: 1, min: 1, max: 4 }, { key: "size", label: "Size", type: "select", default: "1024x1024", options: ["1024x1024", "1024x1792", "1792x1024", "auto"] }, { key: "quality", label: "Quality", type: "select", default: "", options: ["", "standard", "hd", "high", "low", "auto"] }, { key: "style", label: "Style", type: "select", default: "", options: ["", "vivid", "natural"] }, { key: "response_format", label: "Format", type: "select", default: "", options: ["", "url", "b64_json"] }, ], }, imageToText: { inputLabel: "Image URL", inputPlaceholder: "https://example.com/image.png", defaultInput: "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg", bodyKey: "url", extraBody: { prompt: "Describe this image in detail" }, defaultResponse: `{\n "text": "A cat sitting on a windowsill...",\n "model": "..."\n}`, }, stt: { inputLabel: "Audio URL", inputPlaceholder: "https://example.com/audio.mp3", defaultInput: "", bodyKey: "url", defaultResponse: `{\n "text": "Hello world...",\n "model": "..."\n}`, }, video: { inputLabel: "Prompt", inputPlaceholder: "A serene lake at sunset", defaultInput: "A serene lake at sunset", bodyKey: "prompt", defaultResponse: `{\n "data": [\n { "url": "..." }\n ]\n}`, }, music: { inputLabel: "Prompt", inputPlaceholder: "A calm piano melody", defaultInput: "A calm piano melody", bodyKey: "prompt", defaultResponse: `{\n "data": [\n { "url": "...", "format": "mp3" }\n ]\n}`, }, }; // EmbeddingExampleCard function EmbeddingExampleCard({ providerId, customAlias }) { const isCustom = isCustomEmbeddingProvider(providerId); const providerAlias = isCustom ? (customAlias || providerId) : getProviderAlias(providerId); const embeddingModels = isCustom ? [] : getModelsByProviderId(providerId).filter((m) => m.type === "embedding"); const [selectedModel, setSelectedModel] = useState(embeddingModels[0]?.id ?? ""); const [input, setInput] = useState("The quick brown fox jumps over the lazy dog"); const [dimensions, setDimensions] = useState(""); const [apiKey, setApiKey] = useState(""); const [useTunnel, setUseTunnel] = useState(false); const [localEndpoint, setLocalEndpoint] = useState(""); const [tunnelEndpoint, setTunnelEndpoint] = useState(""); const [result, setResult] = useState(null); const [running, setRunning] = useState(false); const [error, setError] = useState(""); const { copied: copiedCurl, copy: copyCurl } = useCopyToClipboard(); const { copied: copiedRes, copy: copyRes } = useCopyToClipboard(); useEffect(() => { setLocalEndpoint(window.location.origin); fetch("/api/keys") .then((r) => r.json()) .then((d) => { setApiKey((d.keys || []).find((k) => k.isActive !== false)?.key || ""); }) .catch(() => {}); fetch("/api/tunnel/status") .then((r) => r.json()) .then((d) => { if (d.publicUrl) setTunnelEndpoint(d.publicUrl); }) .catch(() => {}); }, []); const endpoint = useTunnel ? tunnelEndpoint : localEndpoint; const modelFull = selectedModel ? `${providerAlias}/${selectedModel}` : ""; // Build request body — include dimensions only if user provided a positive number const buildBody = () => { const body = { model: modelFull, input: input.trim() }; const dim = Number(dimensions); if (dimensions && Number.isFinite(dim) && dim > 0) body.dimensions = dim; return body; }; const curlSnippet = `curl -X POST ${endpoint}/v1/embeddings \\ -H "Content-Type: application/json" \\ -H "Authorization: Bearer ${apiKey || "YOUR_KEY"}" \\ -d '${JSON.stringify(buildBody())}'`; const handleRun = async () => { if (!input.trim() || !modelFull) return; setRunning(true); setError(""); setResult(null); const start = Date.now(); try { const headers = { "Content-Type": "application/json" }; if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`; const res = await fetch("/api/v1/embeddings", { method: "POST", headers, body: JSON.stringify(buildBody()), }); const latencyMs = Date.now() - start; const data = await res.json(); if (!res.ok) { setError(data?.error?.message || data?.error || `HTTP ${res.status}`); return; } setResult({ data, latencyMs }); } catch (e) { setError(e.message || "Network error"); } finally { setRunning(false); } }; // Compact embedding array: first 4 values + count const formatResultJson = (data) => { if (!data) return DEFAULT_RESPONSE_EXAMPLE; const clone = JSON.parse(JSON.stringify(data)); (clone.data || []).forEach((item) => { if (Array.isArray(item.embedding) && item.embedding.length > 4) { item.embedding = [...item.embedding.slice(0, 4).map((v) => parseFloat(v.toFixed(6))), `... (${item.embedding.length} dims)`]; } }); return JSON.stringify(clone, null, 2); }; const resultJson = result ? JSON.stringify(result.data, null, 2) : ""; return (

Example

{/* Model — text input for custom node, dropdown otherwise */} {isCustom ? ( setSelectedModel(e.target.value)} placeholder="e.g. voyage-3, embed-english-v3.0, text-embedding-3-small" className="w-full px-3 py-1.5 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary font-mono" /> ) : ( )} {/* Endpoint */}
useTunnel ? setTunnelEndpoint(e.target.value) : setLocalEndpoint(e.target.value)} className="flex-1 px-3 py-1.5 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary font-mono" placeholder="http://localhost:3000" /> {/* Tunnel toggle — only show if tunnel URL is available */} {tunnelEndpoint && ( )}
{/* API Key */} setApiKey(e.target.value)} placeholder="sk-..." className="w-full px-3 py-1.5 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary font-mono" /> {/* Input */}
setInput(e.target.value)} className="w-full px-3 py-1.5 pr-7 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary" /> {input && ( )}
{/* Dimensions (optional) — truncate embedding vector length */} setDimensions(e.target.value)} placeholder="optional, e.g. 512, 1024 (leave empty for default)" className="w-full px-3 py-1.5 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary" /> {/* Curl + Run */}
Request
{curlSnippet}
{/* Error */} {error &&

{error}

} {/* Response — default example or real result */}
Response {result && ⚡ {result.latencyMs}ms} {result && ( )}
            {formatResultJson(result?.data)}
          
); } // ─── TTS Example Card ──────────────────────────────────────────────────────── function TtsExampleCard({ providerId }) { const providerAlias = getProviderAlias(providerId); const config = TTS_PROVIDER_CONFIG[providerId] || TTS_PROVIDER_CONFIG["edge-tts"]; // Voice state const [selectedVoice, setSelectedVoice] = useState(""); const [selectedVoiceName, setSelectedVoiceName] = useState(""); const [voiceId, setVoiceId] = useState(""); // editable voice id (elevenlabs) // Voices shown below Voice row after language selected const [countryVoices, setCountryVoices] = useState([]); const [selectedLang, setSelectedLang] = useState(""); const [selectedModel, setSelectedModel] = useState(() => { if (config.hasModelSelector && config.modelKey) { const models = getModelsByProviderId(config.modelKey); return models?.[0]?.id || ""; } return ""; }); // Form state const [input, setInput] = useState("Hello, this is a text to speech test."); const [apiKey, setApiKey] = useState(""); const [useTunnel, setUseTunnel] = useState(false); const [localEndpoint, setLocalEndpoint] = useState(""); const [tunnelEndpoint, setTunnelEndpoint] = useState(""); const [responseFormat, setResponseFormat] = useState("mp3"); // mp3 | json const [audioUrl, setAudioUrl] = useState(""); const [jsonResponse, setJsonResponse] = useState(null); // Store JSON response const [running, setRunning] = useState(false); const [error, setError] = useState(""); const [latency, setLatency] = useState(null); const { copied: copiedCurl, copy: copyCurl } = useCopyToClipboard(); // Country picker modal state const [modalOpen, setModalOpen] = useState(false); const [languages, setLanguages] = useState([]); const [modalLoading, setModalLoading] = useState(false); const [modalSearch, setModalSearch] = useState(""); const [modalError, setModalError] = useState(""); const [byLang, setByLang] = useState({}); useEffect(() => { setLocalEndpoint(window.location.origin); fetch("/api/keys") .then((r) => r.json()) .then((d) => { setApiKey((d.keys || []).find((k) => k.isActive !== false)?.key || ""); }) .catch(() => {}); fetch("/api/tunnel/status") .then((r) => r.json()) .then((d) => { if (d.publicUrl) setTunnelEndpoint(d.publicUrl); }) .catch(() => {}); // Pre-select default voice based on provider config if (config.voiceSource === "hardcoded") { const defaultModel = config.hasModelSelector && config.modelKey ? (getModelsByProviderId(config.modelKey)?.[0]?.id || "") : ""; // Use per-model voices if available, else flat list const voices = (config.voicesPerModel && defaultModel) ? (getTtsVoicesForModel(providerId, defaultModel) || []) : getModelsByProviderId(config.voiceKey || providerId).filter((m) => m.type === "tts"); if (voices.length) { if (config.hasBrowseButton) { // Google TTS: pre-select "en" (English) as default, show as single voice chip const defaultVoice = voices.find((v) => v.id === "en") || voices[0]; setSelectedLang(defaultVoice.id); setSelectedVoice(defaultVoice.id); setSelectedVoiceName(defaultVoice.name); setCountryVoices([{ id: defaultVoice.id, name: defaultVoice.name }]); } else { // OpenAI/OpenRouter: set voice chips directly (no language picker) setCountryVoices(voices); setSelectedVoice(voices[0].id); setSelectedVoiceName(voices[0].name || voices[0].id); } } } // api-language (edge-tts, local-device, elevenlabs): NO default load, wait for user to pick language }, [providerId]); // Update voices when model changes (voicesPerModel providers) useEffect(() => { if (!config.voicesPerModel || !selectedModel) return; const voices = getTtsVoicesForModel(providerId, selectedModel) || []; setCountryVoices(voices); if (voices.length) { setSelectedVoice(voices[0].id); setSelectedVoiceName(voices[0].name || voices[0].id); } }, [selectedModel]); // Open modal — load language list const openModal = async () => { setModalOpen(true); setModalSearch(""); setModalError(""); if (languages.length) return; // already loaded setModalLoading(true); try { if (config.voiceSource === "hardcoded") { // Build languages/byLang from static providerModels data const voiceKey = config.voiceKey || providerId; const voices = getModelsByProviderId(voiceKey).filter((m) => m.type === "tts"); const byLangMap = {}; for (const v of voices) { if (!byLangMap[v.id]) byLangMap[v.id] = { code: v.id, name: v.name, voices: [{ id: v.id, name: v.name }] }; } setByLang(byLangMap); setLanguages(Object.values(byLangMap).sort((a, b) => a.name.localeCompare(b.name))); } else { // Use provider-specific apiEndpoint if available, else default to edge-tts voices API const url = config.apiEndpoint ? config.apiEndpoint : `/api/media-providers/tts/voices?provider=${providerId === "local-device" ? "local-device" : "edge-tts"}`; const r = await fetch(url); const d = await r.json(); if (d.error) { setModalError(d.error); return; } setLanguages(d.languages || []); setByLang(d.byLang || {}); } } catch (e) { setModalError(e.message); } finally { setModalLoading(false); } }; // Click language → close modal → show voices below const handlePickLanguage = (lang) => { setModalOpen(false); setSelectedLang(lang.code); const voices = byLang[lang.code]?.voices || []; setCountryVoices(voices); // Auto-select first voice if (voices.length) { setSelectedVoice(voices[0].id); setSelectedVoiceName(voices[0].name); if (config.hasVoiceIdInput) setVoiceId(voices[0].id); } }; const filteredLanguages = modalSearch ? languages.filter((c) => c.name.toLowerCase().includes(modalSearch.toLowerCase()) || c.code.toLowerCase().includes(modalSearch.toLowerCase()) ) : languages; const endpoint = useTunnel ? tunnelEndpoint : localEndpoint; // For ElevenLabs: use voiceId (editable) instead of selectedVoice const activeVoiceId = config.hasVoiceIdInput ? voiceId : selectedVoice; const modelFull = config.hasModelSelector && activeVoiceId && selectedModel ? `${providerAlias}/${selectedModel}/${activeVoiceId}` : activeVoiceId ? `${providerAlias}/${activeVoiceId}` : ""; const curlSnippet = `curl -X POST ${endpoint}/v1/audio/speech${responseFormat === "json" ? "?response_format=json" : ""} \\ -H "Content-Type: application/json" \\ -H "Authorization: Bearer ${apiKey || "YOUR_KEY"}" \\ -d '{"model": "${modelFull}", "input": "${input}"}' \\ ${responseFormat === "json" ? "" : "--output speech.mp3"}`; const handleRun = async () => { if (!input.trim() || !modelFull) return; setRunning(true); setError(""); setAudioUrl(""); setJsonResponse(null); const start = Date.now(); try { const headers = { "Content-Type": "application/json" }; if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`; const url = `/api/v1/audio/speech${responseFormat === "json" ? "?response_format=json" : ""}`; const res = await fetch(url, { method: "POST", headers, body: JSON.stringify({ model: modelFull, input: input.trim() }), }); setLatency(Date.now() - start); if (!res.ok) { const d = await res.json().catch(() => ({})); setError(d?.error?.message || d?.error || `HTTP ${res.status}`); return; } if (responseFormat === "json") { const data = await res.json(); setJsonResponse(data); // Store full JSON response const audioBlob = await fetch(`data:audio/mp3;base64,${data.audio}`).then(r => r.blob()); setAudioUrl(URL.createObjectURL(audioBlob)); } else { const blob = await res.blob(); setAudioUrl(URL.createObjectURL(blob)); } } catch (e) { setError(e.message || "Network error"); } finally { setRunning(false); } }; return ( <>

Example

{/* Endpoint + API Key as read-only text */}
{endpoint}/v1/audio/speech {tunnelEndpoint && ( )}
{apiKey ? `${apiKey.slice(0, 8)}${"•".repeat(Math.min(20, apiKey.length - 8))}` : No key configured} {/* Model selector (OpenAI, ElevenLabs) */} {config.hasModelSelector && config.modelKey && ( )} {/* Language row + Browse button (edge-tts, local-device, elevenlabs) */} {config.hasBrowseButton && (
)} {/* Voice chips — shown after language picked (edge-tts, local-device) or always (OpenAI/ElevenLabs) */} {countryVoices.length > 0 && (
{countryVoices.map((v) => ( ))}
)} {/* Voice ID input (ElevenLabs) — manual entry or auto-fill from chip */} {config.hasVoiceIdInput && (
{ setVoiceId(e.target.value); setSelectedVoice(e.target.value); }} placeholder="e.g. CwhRBWXzGAHq8TQ4Fs17" className="w-full px-3 py-1.5 pr-7 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary font-mono" /> {voiceId && ( )}
)} {/* Google TTS: Language dropdown */} {config.hasLanguageDropdown && ( )} {/* Input */}
setInput(e.target.value)} className="w-full px-3 py-1.5 pr-7 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary" /> {input && ( )}
{/* Output Format */} {/* Curl + Run */}
Request
{curlSnippet}
{error &&

{error}

} {/* Audio player */} {audioUrl ? (
Response {latency && ⚡ {latency}ms} download Download
) : (
Response
{DEFAULT_TTS_RESPONSE_EXAMPLE}
)}
{/* Country Picker Modal */} {modalOpen && (
setModalOpen(false)} >
e.stopPropagation()} > {/* Header */}

Select Language

{/* Search */}
setModalSearch(e.target.value)} placeholder="Search language..." className="w-full px-3 py-1.5 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary" />
{/* Language list */}
{modalError &&

{modalError}

} {modalLoading ? (

Loading...

) : (
{filteredLanguages.map((c) => ( ))} {filteredLanguages.length === 0 && (

No languages found.

)}
)}
)} ); } // Generic Example Card — config-driven for webSearch, webFetch, image, imageToText, stt, video, music function GenericExampleCard({ providerId, kind }) { const providerAlias = getProviderAlias(providerId); const kindConfig = MEDIA_PROVIDER_KINDS.find((k) => k.id === kind); const exConfig = KIND_EXAMPLE_CONFIG[kind]; const safeExConfig = exConfig || {}; // Get models for this kind (e.g., type="image") const kindModels = getModelsByProviderId(providerId).filter((m) => m.type === kind); const [selectedModel, setSelectedModel] = useState(kindModels[0]?.id ?? ""); const selectedModelObj = kindModels.find((m) => m.id === selectedModel); const supportsEdit = !!selectedModelObj?.capabilities?.includes("edit"); const [input, setInput] = useState(safeExConfig.defaultInput || ""); const [refImage, setRefImage] = useState(""); const [extraValues, setExtraValues] = useState(() => (safeExConfig.extraFields || []).reduce((acc, f) => { acc[f.key] = f.default ?? ""; return acc; }, {}) ); const [apiKey, setApiKey] = useState(""); const [useTunnel, setUseTunnel] = useState(false); const [localEndpoint, setLocalEndpoint] = useState(""); const [tunnelEndpoint, setTunnelEndpoint] = useState(""); const [result, setResult] = useState(null); const [progress, setProgress] = useState(null); // { stage, bytesReceived } const [partialImage, setPartialImage] = useState(null); const [running, setRunning] = useState(false); const [error, setError] = useState(""); const [connections, setConnections] = useState([]); const [pinnedConnectionId, setPinnedConnectionId] = useState(""); const { copied: copiedCurl, copy: copyCurl } = useCopyToClipboard(); const { copied: copiedRes, copy: copyRes } = useCopyToClipboard(); useEffect(() => { setLocalEndpoint(window.location.origin); fetch("/api/keys") .then((r) => r.json()) .then((d) => { setApiKey((d.keys || []).find((k) => k.isActive !== false)?.key || ""); }) .catch(() => {}); fetch("/api/tunnel/status") .then((r) => r.json()) .then((d) => { if (d.publicUrl) setTunnelEndpoint(d.publicUrl); }) .catch(() => {}); // Load active connections of this provider for pinning fetch("/api/providers/client") .then((r) => r.json()) .then((d) => { const conns = (d.connections || []).filter((c) => c.provider === providerId && c.isActive !== false); setConnections(conns); }) .catch(() => {}); }, [providerId]); // Safe to early-return now that all hooks are declared if (!kindConfig || !exConfig) return null; const endpoint = useTunnel ? tunnelEndpoint : localEndpoint; const apiPath = kindConfig.endpoint.path; const modelFull = selectedModel ? `${providerAlias}/${selectedModel}` : ""; // Build request body with optional extra fields (only non-empty values) const extraBodyFromFields = Object.entries(extraValues).reduce((acc, [k, v]) => { if (v === "" || v === null || v === undefined) return acc; if (typeof v === "number" && Number.isNaN(v)) return acc; acc[k] = v; return acc; }, {}); const requestBody = { model: modelFull, [exConfig.bodyKey]: input, ...exConfig.extraBody, ...extraBodyFromFields, ...(supportsEdit && refImage.trim() ? { image: refImage.trim() } : {}), }; // Streaming supported for codex image (Plus/Pro accounts) const useStreaming = kind === "image" && providerId === "codex"; const headersPreview = `-H "Content-Type: application/json" \\\n -H "Authorization: Bearer ${apiKey || "YOUR_KEY"}"${pinnedConnectionId ? ` \\\n -H "x-connection-id: ${pinnedConnectionId}"` : ""}${useStreaming ? ` \\\n -H "Accept: text/event-stream"` : ""}`; const curlSnippet = `curl -X ${kindConfig.endpoint.method} ${endpoint}${apiPath} \\ ${headersPreview.replace(/\\\n /g, "\\\n ")} \\ -d '${JSON.stringify(requestBody)}'`; const handleRun = async () => { if (!input.trim() || !modelFull) return; setRunning(true); setError(""); setResult(null); setProgress(null); setPartialImage(null); const start = Date.now(); try { const headers = { "Content-Type": "application/json" }; if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`; if (pinnedConnectionId) headers["x-connection-id"] = pinnedConnectionId; if (useStreaming) headers["Accept"] = "text/event-stream"; const body = { ...requestBody, model: modelFull }; const res = await fetch(`/api${apiPath}`, { method: kindConfig.endpoint.method, headers, body: JSON.stringify(body), }); if (!res.ok) { const data = await res.json().catch(() => ({})); setError(data?.error?.message || data?.error || `HTTP ${res.status}`); return; } const isSse = (res.headers.get("content-type") || "").includes("text/event-stream"); if (isSse && res.body) { // Parse SSE: progress / partial_image / done / error const reader = res.body.getReader(); const decoder = new TextDecoder(); let buf = ""; let finalData = null; let streamErr = null; while (true) { const { done, value } = await reader.read(); if (done) break; buf += decoder.decode(value, { stream: true }); let sep; while ((sep = buf.indexOf("\n\n")) !== -1) { const block = buf.slice(0, sep); buf = buf.slice(sep + 2); let evt = null, dataStr = ""; for (const line of block.split("\n")) { if (line.startsWith("event:")) evt = line.slice(6).trim(); else if (line.startsWith("data:")) dataStr += line.slice(5).trim(); } if (!evt) continue; try { const payload = dataStr ? JSON.parse(dataStr) : {}; if (evt === "progress") setProgress(payload); else if (evt === "partial_image") setPartialImage(payload); else if (evt === "done") finalData = payload; else if (evt === "error") streamErr = payload?.message || "Stream error"; } catch {} } } const latencyMs = Date.now() - start; if (streamErr) { setError(streamErr); return; } if (finalData) setResult({ data: finalData, latencyMs }); } else { const data = await res.json(); const latencyMs = Date.now() - start; setResult({ data, latencyMs }); } } catch (e) { setError(e.message || "Network error"); } finally { setRunning(false); } }; // Mask large b64_json strings in JSON view to keep it readable const maskB64 = (obj) => { if (!obj || typeof obj !== "object") return obj; if (Array.isArray(obj)) return obj.map(maskB64); const out = {}; for (const [k, v] of Object.entries(obj)) { out[k] = (k === "b64_json" && typeof v === "string" && v.length > 100) ? `<${v.length} chars base64>` : maskB64(v); } return out; }; const resultJson = result ? JSON.stringify(maskB64(result.data), null, 2) : ""; return (

Example

{/* Model selector - only show if models available */} {kindModels.length > 0 && ( )} {/* Endpoint */}
{endpoint}{apiPath} {tunnelEndpoint && ( )}
{/* API Key */} {apiKey ? `${apiKey.slice(0, 8)}${"\u2022".repeat(Math.min(20, apiKey.length - 8))}` : No key configured} {/* Connection picker - only show when 2+ connections (or any with email) */} {connections.length > 0 && ( )} {/* Input */}
setInput(e.target.value)} placeholder={exConfig.inputPlaceholder} className="w-full px-3 py-1.5 pr-7 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary" /> {input && ( )}
{/* Reference image (only for edit-capable image models) */} {supportsEdit && (
setRefImage(e.target.value)} placeholder="https://example.com/source.png" className="w-full px-3 py-1.5 pr-7 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary" /> {refImage && ( )}
{refImage.trim() && ( Reference { e.currentTarget.style.display = "none"; }} onLoad={(e) => { e.currentTarget.style.display = "block"; }} /> )}
)} {/* Extra fields (filtered by model.params; if undefined → none shown) */} {(exConfig.extraFields || []) .filter((f) => Array.isArray(selectedModelObj?.params) && selectedModelObj.params.includes(f.key)) .map((f) => ( {f.type === "select" ? ( ) : ( setExtraValues((s) => ({ ...s, [f.key]: e.target.value === "" ? "" : Number(e.target.value) }))} className="w-full px-3 py-1.5 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary" /> )} ))} {/* Curl + Run */}
Request
{curlSnippet}
{/* Streaming progress */} {(running || progress) && useStreaming && (
{running ? "progress_activity" : "check_circle"} {progress?.stage || "starting"} {progress?.bytesReceived ? ` · ${(progress.bytesReceived / 1024).toFixed(1)} KB` : ""}
)} {/* Partial image preview (codex stream) */} {partialImage?.b64_json && !result && (
Partial preview Partial
)} {/* Error */} {error &&

{error}

} {/* Response */}
Response {result && ⚡ {result.latencyMs}ms} {result && ( )}
            {result ? resultJson : exConfig.defaultResponse}
          
{kind === "image" && result?.data?.data?.[0] && ( Generated )}
); } // MediaProviderDetailPage export default function MediaProviderDetailPage() { const { kind, id } = useParams(); const router = useRouter(); const kindConfig = MEDIA_PROVIDER_KINDS.find((k) => k.id === kind); const isCustom = isCustomEmbeddingProvider(id) && kind === "embedding"; const handleDeleteCustom = async () => { if (!confirm("Delete this Custom Embedding node?")) return; try { const res = await fetch(`/api/provider-nodes/${id}`, { method: "DELETE" }); if (res.ok) router.push(`/dashboard/media-providers/${kind}`); } catch (error) { console.log("Error deleting custom embedding node:", error); } }; const [customNode, setCustomNode] = useState(null); const [customLoading, setCustomLoading] = useState(isCustom); const [showEditModal, setShowEditModal] = useState(false); // Fetch custom node info from API for custom embedding nodes useEffect(() => { if (!isCustom) return; let cancelled = false; fetch("/api/provider-nodes", { cache: "no-store" }) .then((r) => r.json()) .then((d) => { if (cancelled) return; setCustomNode((d.nodes || []).find((n) => n.id === id) || null); setCustomLoading(false); }) .catch(() => { if (!cancelled) setCustomLoading(false); }); return () => { cancelled = true; }; }, [id, isCustom]); if (!kindConfig) return notFound(); const builtInProvider = AI_PROVIDERS[id]; // For custom embedding nodes, build a synthetic provider object const provider = isCustom ? (customNode ? { id, name: customNode.name || "Custom Embedding", color: "#6366F1", textIcon: "CE" } : null) : builtInProvider; if (!isCustom && !builtInProvider) return notFound(); if (isCustom && !customLoading && !customNode) return notFound(); if (isCustom && customLoading) { return
Loading...
; } const kinds = isCustom ? ["embedding"] : (provider.serviceKinds ?? ["llm"]); if (!isCustom && !kinds.includes(kind)) return notFound(); return (
{/* Back */}
arrow_back {kindConfig.label} {/* Header */}

{provider.name}

{isCustom && Custom · {customNode?.prefix}} {kinds.map((k) => ( {k.toUpperCase()} ))}
{isCustom && (
)}
{/* Kind-specific notice (e.g. codex/image requires Plus) */} {!isCustom && provider.kindNotice?.[kind] && (
warning

{provider.kindNotice[kind]}

)} {/* Connections */} {!isCustom && provider.noAuth ? (
lock_open

No authentication required

This provider is ready to use.

) : ( )} {/* Models - only for non-tts kinds; custom uses prefix as alias */} {kind !== "tts" && ( )} {/* Example — per kind */} {kind === "embedding" && ( )} {kind === "tts" && } {!isCustom && KIND_EXAMPLE_CONFIG[kind] && } {isCustom && ( setShowEditModal(false)} onSaved={(updated) => { setCustomNode(updated); setShowEditModal(false); }} /> )}
); }