Add Cloudflare AI provider support and enhance connection management
- Introduced Cloudflare AI as a new provider with specific configurations in providerModels.js and providers.js. - Updated DefaultExecutor to handle account ID resolution for Cloudflare AI connections. - Enhanced AddApiKeyModal and EditConnectionModal to include account ID input for Cloudflare AI. - Implemented validation for Cloudflare AI API key connections in testUtils.js and route.js. - Updated UI components to reflect changes in provider management and connection handling.
This commit is contained in:
parent
111e78940a
commit
1bb621317d
18 changed files with 325 additions and 71 deletions
|
|
@ -20,6 +20,7 @@ export default function EditConnectionModal({ isOpen, connection, proxyPools, on
|
|||
deployment: "",
|
||||
organization: "",
|
||||
});
|
||||
const [cloudflareData, setCloudflareData] = useState({ accountId: "" });
|
||||
const [testing, setTesting] = useState(false);
|
||||
const [testResult, setTestResult] = useState(null);
|
||||
const [validating, setValidating] = useState(false);
|
||||
|
|
@ -42,6 +43,9 @@ export default function EditConnectionModal({ isOpen, connection, proxyPools, on
|
|||
organization: connection.providerSpecificData.organization || "",
|
||||
});
|
||||
}
|
||||
if (connection.provider === "cloudflare-ai" && connection.providerSpecificData) {
|
||||
setCloudflareData({ accountId: connection.providerSpecificData.accountId || "" });
|
||||
}
|
||||
setTestResult(null);
|
||||
setValidationResult(null);
|
||||
}
|
||||
|
|
@ -49,6 +53,7 @@ export default function EditConnectionModal({ isOpen, connection, proxyPools, on
|
|||
|
||||
const isOAuth = connection?.authType === "oauth";
|
||||
const isAzure = connection?.provider === "azure";
|
||||
const isCloudflareAi = connection?.provider === "cloudflare-ai";
|
||||
const isCompatible = connection
|
||||
? (isOpenAICompatibleProvider(connection.provider) || isAnthropicCompatibleProvider(connection.provider))
|
||||
: false;
|
||||
|
|
@ -80,6 +85,7 @@ export default function EditConnectionModal({ isOpen, connection, proxyPools, on
|
|||
provider: connection.provider,
|
||||
apiKey: formData.apiKey,
|
||||
...(isAzure ? { providerSpecificData: azureData } : {}),
|
||||
...(isCloudflareAi ? { providerSpecificData: cloudflareData } : {}),
|
||||
}),
|
||||
});
|
||||
const data = await res.json();
|
||||
|
|
@ -113,6 +119,7 @@ export default function EditConnectionModal({ isOpen, connection, proxyPools, on
|
|||
provider: connection.provider,
|
||||
apiKey: formData.apiKey,
|
||||
...(isAzure ? { providerSpecificData: azureData } : {}),
|
||||
...(isCloudflareAi ? { providerSpecificData: cloudflareData } : {}),
|
||||
}),
|
||||
});
|
||||
const data = await res.json();
|
||||
|
|
@ -140,6 +147,9 @@ export default function EditConnectionModal({ isOpen, connection, proxyPools, on
|
|||
organization: azureData.organization,
|
||||
};
|
||||
}
|
||||
if (isCloudflareAi) {
|
||||
updates.providerSpecificData = { accountId: cloudflareData.accountId };
|
||||
}
|
||||
|
||||
await onSave(updates);
|
||||
} finally {
|
||||
|
|
@ -197,6 +207,19 @@ export default function EditConnectionModal({ isOpen, connection, proxyPools, on
|
|||
</>
|
||||
)}
|
||||
|
||||
{isCloudflareAi && (
|
||||
<div className="bg-sidebar/50 p-4 rounded-lg border border-accent/20">
|
||||
<h3 className="font-semibold mb-3 text-sm">Cloudflare Workers AI</h3>
|
||||
<Input
|
||||
label="Account ID"
|
||||
value={cloudflareData.accountId}
|
||||
onChange={(e) => setCloudflareData({ ...cloudflareData, accountId: e.target.value })}
|
||||
placeholder="abc123def456..."
|
||||
hint="Find in right sidebar of dash.cloudflare.com"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isAzure && (
|
||||
<div className="bg-sidebar/50 p-4 rounded-lg border border-accent/20">
|
||||
<h3 className="font-semibold mb-3 text-sm">Azure OpenAI Configuration</h3>
|
||||
|
|
@ -233,7 +256,7 @@ export default function EditConnectionModal({ isOpen, connection, proxyPools, on
|
|||
</div>
|
||||
)}
|
||||
|
||||
{!isCompatible && !isAzure && (
|
||||
{!isCompatible && !isAzure && !isCloudflareAi && (
|
||||
<div className="flex items-center gap-3">
|
||||
<Button onClick={handleTest} variant="secondary" disabled={testing}>
|
||||
{testing ? "Testing..." : "Test Connection"}
|
||||
|
|
|
|||
86
src/shared/components/NoAuthProxyCard.js
Normal file
86
src/shared/components/NoAuthProxyCard.js
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import Card from "./Card";
|
||||
import Select from "./Select";
|
||||
import Badge from "./Badge";
|
||||
|
||||
const NONE_PROXY_POOL_VALUE = "__none__";
|
||||
|
||||
export default function NoAuthProxyCard({ providerId }) {
|
||||
const [proxyPools, setProxyPools] = useState([]);
|
||||
const [proxyPoolId, setProxyPoolId] = useState(NONE_PROXY_POOL_VALUE);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [savedFlash, setSavedFlash] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
Promise.all([
|
||||
fetch("/api/proxy-pools?isActive=true", { cache: "no-store" }).then((r) => r.ok ? r.json() : { proxyPools: [] }),
|
||||
fetch("/api/settings", { cache: "no-store" }).then((r) => r.ok ? r.json() : {}),
|
||||
]).then(([poolData, settingsData]) => {
|
||||
if (cancelled) return;
|
||||
setProxyPools(poolData.proxyPools || []);
|
||||
const override = (settingsData.providerStrategies || {})[providerId] || {};
|
||||
setProxyPoolId(override.proxyPoolId || NONE_PROXY_POOL_VALUE);
|
||||
}).catch(() => {});
|
||||
return () => { cancelled = true; };
|
||||
}, [providerId]);
|
||||
|
||||
const handleChange = async (newValue) => {
|
||||
setProxyPoolId(newValue);
|
||||
setSaving(true);
|
||||
try {
|
||||
const res = await fetch("/api/settings", { cache: "no-store" });
|
||||
const data = res.ok ? await res.json() : {};
|
||||
const current = data.providerStrategies || {};
|
||||
const override = { ...(current[providerId] || {}) };
|
||||
if (newValue === NONE_PROXY_POOL_VALUE) delete override.proxyPoolId;
|
||||
else override.proxyPoolId = newValue;
|
||||
const updated = { ...current };
|
||||
if (Object.keys(override).length === 0) delete updated[providerId];
|
||||
else updated[providerId] = override;
|
||||
await fetch("/api/settings", {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ providerStrategies: updated }),
|
||||
});
|
||||
setSavedFlash(true);
|
||||
setTimeout(() => setSavedFlash(false), 1500);
|
||||
} catch (e) {
|
||||
console.log("Save proxyPoolId error:", e);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="inline-flex items-center justify-center w-10 h-10 rounded-full bg-green-500/10 text-green-500">
|
||||
<span className="material-symbols-outlined text-[20px]">lock_open</span>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium">No authentication required</p>
|
||||
<p className="text-xs text-text-muted">This provider is ready to use. Optionally route requests through a proxy pool to bypass IP-based limits.</p>
|
||||
</div>
|
||||
{savedFlash && <Badge variant="success" size="sm">Saved</Badge>}
|
||||
</div>
|
||||
<Select
|
||||
label="Proxy Pool"
|
||||
value={proxyPoolId}
|
||||
onChange={(e) => handleChange(e.target.value)}
|
||||
disabled={saving}
|
||||
options={[
|
||||
{ value: NONE_PROXY_POOL_VALUE, label: "None (direct)" },
|
||||
...proxyPools.map((pool) => ({ value: pool.id, label: pool.name })),
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
NoAuthProxyCard.propTypes = {
|
||||
providerId: PropTypes.string.isRequired,
|
||||
};
|
||||
62
src/shared/components/ProviderInfoCard.js
Normal file
62
src/shared/components/ProviderInfoCard.js
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
"use client";
|
||||
|
||||
import Card from "./Card";
|
||||
|
||||
// Field schema — config-driven, used for both searchConfig and fetchConfig
|
||||
const FIELD_SCHEMA = {
|
||||
baseUrl: { label: "Endpoint", format: (v) => v, isLink: true, mono: true },
|
||||
method: { label: "Method", format: (v) => v },
|
||||
authType: { label: "Auth", format: (v) => v },
|
||||
authHeader: { label: "Auth Header", format: (v) => v, mono: true },
|
||||
costPerQuery: { label: "Cost / call", format: (v) => v === 0 ? "Free" : `$${v.toFixed(4)}` },
|
||||
freeMonthlyQuota: { label: "Free quota", format: (v) => v === 0 ? "—" : v >= 999999 ? "Unlimited" : `${v.toLocaleString()} / mo` },
|
||||
searchTypes: { label: "Types", format: (v) => v.join(", ") },
|
||||
formats: { label: "Formats", format: (v) => v.join(", ") },
|
||||
defaultMaxResults: { label: "Default results", format: (v) => v },
|
||||
maxMaxResults: { label: "Max results", format: (v) => v },
|
||||
maxCharacters: { label: "Max chars", format: (v) => v.toLocaleString() },
|
||||
timeoutMs: { label: "Timeout", format: (v) => `${v / 1000}s` },
|
||||
cacheTTLMs: { label: "Cache TTL", format: (v) => `${v / 60000}m` },
|
||||
};
|
||||
|
||||
export default function ProviderInfoCard({ config, title = "Provider Info" }) {
|
||||
if (!config) return null;
|
||||
|
||||
const rows = Object.entries(FIELD_SCHEMA)
|
||||
.filter(([key]) => config[key] !== undefined && config[key] !== null && config[key] !== "")
|
||||
.map(([key, schema]) => ({
|
||||
key,
|
||||
label: schema.label,
|
||||
value: schema.format(config[key]),
|
||||
isLink: schema.isLink,
|
||||
mono: schema.mono,
|
||||
raw: config[key],
|
||||
}));
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<h2 className="text-lg font-semibold mb-3">{title}</h2>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-2">
|
||||
{rows.map((r) => (
|
||||
<div key={r.key} className="flex items-center gap-3 min-w-0">
|
||||
<span className="text-xs text-text-muted w-28 shrink-0">{r.label}</span>
|
||||
{r.isLink ? (
|
||||
<a
|
||||
href={r.raw}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={`text-sm text-primary hover:underline truncate ${r.mono ? "font-mono" : ""}`}
|
||||
>
|
||||
{r.value}
|
||||
</a>
|
||||
) : (
|
||||
<span className={`text-sm text-text-main truncate ${r.mono ? "font-mono" : ""}`}>
|
||||
{r.value}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
@ -12,7 +12,7 @@ import Button from "./Button";
|
|||
import { ConfirmModal } from "./Modal";
|
||||
|
||||
// const VISIBLE_MEDIA_KINDS = ["embedding", "image", "imageToText", "tts", "stt", "webSearch", "webFetch", "video", "music"];
|
||||
const VISIBLE_MEDIA_KINDS = ["embedding", "image", "tts"];
|
||||
const VISIBLE_MEDIA_KINDS = ["embedding", "image", "tts", "webSearch", "webFetch"];
|
||||
|
||||
const navItems = [
|
||||
{ href: "/dashboard/endpoint", label: "Endpoint", icon: "api" },
|
||||
|
|
|
|||
|
|
@ -30,8 +30,10 @@ 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 NoAuthProxyCard } from "./NoAuthProxyCard";
|
||||
export { default as SegmentedControl } from "./SegmentedControl";
|
||||
export { default as Tooltip } from "./Tooltip";
|
||||
export { default as ProviderInfoCard } from "./ProviderInfoCard";
|
||||
|
||||
// Layouts
|
||||
export * from "./layouts";
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue