mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-23 04:09:49 +00:00
commit 8b2ab7c9e05689c1bf55002cc79db8d22a398c75 Author: kundeng <kundeng@live.com> Date: Mon Apr 20 11:26:58 2026 -0400 fix: send providerSpecificData in Edit modal validate calls The Check button in the Edit modal was sending only apiKey without the Azure endpoint/deployment/org, causing validation to fail. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> commit c894fa838d035ecd9a160339342371042697c327 Author: kundeng <kundeng@live.com> Date: Mon Apr 20 01:45:13 2026 -0400 fix: persist Azure providerSpecificData and add connection test - Read body.providerSpecificData in POST /api/providers so Azure fields (endpoint, deployment, apiVersion, organization) are actually stored - Add azure case to testApiKeyConnection so the Test button works correctly instead of falling through to "not supported" Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> commit 00bd1a4151f4e73616969e25d1786c87d1ec0d5e Author: kundeng <kundeng@live.com> Date: Mon Apr 20 01:24:39 2026 -0400 fix: add Azure validation and make Organization required - Add Azure case to /api/providers/validate that sends a test chat completion with api-key header and organization - Pass Azure-specific data (endpoint, deployment, apiVersion, org) from Add modal to validate endpoint - Make Organization field required (needed for billing) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> commit a66a04daab69b07baa4cc6b28772249e4b25ea19 Author: kundeng <kundeng@live.com> Date: Mon Apr 20 01:15:53 2026 -0400 fix: add Azure config fields to Add API Key modal The Add modal was missing the Azure-specific fields (endpoint, deployment, API version, organization) — only the Edit modal had them. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> commit 6ac3f4a97af8468d210594495ce754f5d7a7978a Author: kundeng <kundeng@live.com> Date: Mon Apr 20 01:06:45 2026 -0400 feat: add Azure OpenAI as a dedicated provider Azure OpenAI uses a different URL scheme (deployments-based) and api-key header auth instead of Bearer tokens. This adds a dedicated AzureExecutor that constructs the correct URL and headers, plus dashboard UI fields for endpoint, deployment, API version, and organization. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
287 lines
10 KiB
JavaScript
287 lines
10 KiB
JavaScript
"use client";
|
|
|
|
import { useState, useEffect } from "react";
|
|
import PropTypes from "prop-types";
|
|
import Modal from "@/shared/components/Modal";
|
|
import Input from "@/shared/components/Input";
|
|
import Button from "@/shared/components/Button";
|
|
import Badge from "@/shared/components/Badge";
|
|
import { isOpenAICompatibleProvider, isAnthropicCompatibleProvider } from "@/shared/constants/providers";
|
|
|
|
export default function EditConnectionModal({ isOpen, connection, proxyPools, onSave, onClose }) {
|
|
const [formData, setFormData] = useState({
|
|
name: "",
|
|
priority: 1,
|
|
apiKey: "",
|
|
});
|
|
const [azureData, setAzureData] = useState({
|
|
azureEndpoint: "",
|
|
apiVersion: "2024-10-01-preview",
|
|
deployment: "",
|
|
organization: "",
|
|
});
|
|
const [cloudflareData, setCloudflareData] = useState({ accountId: "" });
|
|
const [testing, setTesting] = useState(false);
|
|
const [testResult, setTestResult] = useState(null);
|
|
const [validating, setValidating] = useState(false);
|
|
const [validationResult, setValidationResult] = useState(null);
|
|
const [saving, setSaving] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (connection) {
|
|
setFormData({
|
|
name: connection.name || "",
|
|
priority: connection.priority || 1,
|
|
apiKey: "",
|
|
});
|
|
// Load Azure-specific data if present
|
|
if (connection.provider === "azure" && connection.providerSpecificData) {
|
|
setAzureData({
|
|
azureEndpoint: connection.providerSpecificData.azureEndpoint || "",
|
|
apiVersion: connection.providerSpecificData.apiVersion || "2024-10-01-preview",
|
|
deployment: connection.providerSpecificData.deployment || "",
|
|
organization: connection.providerSpecificData.organization || "",
|
|
});
|
|
}
|
|
if (connection.provider === "cloudflare-ai" && connection.providerSpecificData) {
|
|
setCloudflareData({ accountId: connection.providerSpecificData.accountId || "" });
|
|
}
|
|
setTestResult(null);
|
|
setValidationResult(null);
|
|
}
|
|
}, [connection]);
|
|
|
|
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;
|
|
|
|
const handleTest = async () => {
|
|
if (!connection?.provider) return;
|
|
setTesting(true);
|
|
setTestResult(null);
|
|
try {
|
|
const res = await fetch(`/api/providers/${connection.id}/test`, { method: "POST" });
|
|
const data = await res.json();
|
|
setTestResult(data.valid ? "success" : "failed");
|
|
} catch {
|
|
setTestResult("failed");
|
|
} finally {
|
|
setTesting(false);
|
|
}
|
|
};
|
|
|
|
const handleValidate = async () => {
|
|
if (!connection?.provider || !formData.apiKey) return;
|
|
setValidating(true);
|
|
setValidationResult(null);
|
|
try {
|
|
const res = await fetch("/api/providers/validate", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
provider: connection.provider,
|
|
apiKey: formData.apiKey,
|
|
...(isAzure ? { providerSpecificData: azureData } : {}),
|
|
...(isCloudflareAi ? { providerSpecificData: cloudflareData } : {}),
|
|
}),
|
|
});
|
|
const data = await res.json();
|
|
setValidationResult(data.valid ? "success" : "failed");
|
|
} catch {
|
|
setValidationResult("failed");
|
|
} finally {
|
|
setValidating(false);
|
|
}
|
|
};
|
|
|
|
const handleSubmit = async () => {
|
|
if (!connection) return;
|
|
setSaving(true);
|
|
try {
|
|
const updates = {
|
|
name: formData.name,
|
|
priority: formData.priority,
|
|
};
|
|
if (!isOAuth && formData.apiKey) {
|
|
updates.apiKey = formData.apiKey;
|
|
let isValid = validationResult === "success";
|
|
if (!isValid) {
|
|
try {
|
|
setValidating(true);
|
|
setValidationResult(null);
|
|
const res = await fetch("/api/providers/validate", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
provider: connection.provider,
|
|
apiKey: formData.apiKey,
|
|
...(isAzure ? { providerSpecificData: azureData } : {}),
|
|
...(isCloudflareAi ? { providerSpecificData: cloudflareData } : {}),
|
|
}),
|
|
});
|
|
const data = await res.json();
|
|
isValid = !!data.valid;
|
|
setValidationResult(isValid ? "success" : "failed");
|
|
} catch {
|
|
setValidationResult("failed");
|
|
} finally {
|
|
setValidating(false);
|
|
}
|
|
}
|
|
if (isValid) {
|
|
updates.testStatus = "active";
|
|
updates.lastError = null;
|
|
updates.lastErrorAt = null;
|
|
}
|
|
}
|
|
|
|
// Add Azure-specific data if this is an Azure connection
|
|
if (isAzure) {
|
|
updates.providerSpecificData = {
|
|
azureEndpoint: azureData.azureEndpoint,
|
|
apiVersion: azureData.apiVersion,
|
|
deployment: azureData.deployment,
|
|
organization: azureData.organization,
|
|
};
|
|
}
|
|
if (isCloudflareAi) {
|
|
updates.providerSpecificData = { accountId: cloudflareData.accountId };
|
|
}
|
|
|
|
await onSave(updates);
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
if (!connection) return null;
|
|
|
|
return (
|
|
<Modal isOpen={isOpen} title="Edit Connection" onClose={onClose}>
|
|
<div className="flex flex-col gap-4">
|
|
<Input
|
|
label="Name"
|
|
value={formData.name}
|
|
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
|
placeholder={isOAuth ? "Account name" : "Production Key"}
|
|
/>
|
|
{isOAuth && connection.email && (
|
|
<div className="bg-sidebar/50 p-3 rounded-lg">
|
|
<p className="text-sm text-text-muted mb-1">Email</p>
|
|
<p className="font-medium">{connection.email}</p>
|
|
</div>
|
|
)}
|
|
<Input
|
|
label="Priority"
|
|
type="number"
|
|
value={formData.priority}
|
|
onChange={(e) => setFormData({ ...formData, priority: Number.parseInt(e.target.value, 10) || 1 })}
|
|
/>
|
|
|
|
{!isOAuth && (
|
|
<>
|
|
<div className="flex gap-2">
|
|
<Input
|
|
label="API Key"
|
|
type="password"
|
|
value={formData.apiKey}
|
|
onChange={(e) => setFormData({ ...formData, apiKey: e.target.value })}
|
|
placeholder="Enter new API key"
|
|
hint="Leave blank to keep the current API key."
|
|
className="flex-1"
|
|
/>
|
|
<div className="pt-6">
|
|
<Button onClick={handleValidate} disabled={!formData.apiKey || validating || saving} variant="secondary">
|
|
{validating ? "Checking..." : "Check"}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
{validationResult && (
|
|
<Badge variant={validationResult === "success" ? "success" : "error"}>
|
|
{validationResult === "success" ? "Valid" : "Invalid"}
|
|
</Badge>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
{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>
|
|
<div className="flex flex-col gap-3">
|
|
<Input
|
|
label="Azure Endpoint"
|
|
value={azureData.azureEndpoint}
|
|
onChange={(e) => setAzureData({ ...azureData, azureEndpoint: e.target.value })}
|
|
placeholder="https://your-resource.openai.azure.com"
|
|
hint="Your Azure OpenAI resource endpoint URL"
|
|
/>
|
|
<Input
|
|
label="Deployment Name"
|
|
value={azureData.deployment}
|
|
onChange={(e) => setAzureData({ ...azureData, deployment: e.target.value })}
|
|
placeholder="gpt-4"
|
|
hint="The deployment name in your Azure resource"
|
|
/>
|
|
<Input
|
|
label="API Version"
|
|
value={azureData.apiVersion}
|
|
onChange={(e) => setAzureData({ ...azureData, apiVersion: e.target.value })}
|
|
placeholder="2024-10-01-preview"
|
|
hint="Azure OpenAI API version to use"
|
|
/>
|
|
<Input
|
|
label="Organization"
|
|
value={azureData.organization}
|
|
onChange={(e) => setAzureData({ ...azureData, organization: e.target.value })}
|
|
placeholder="Organization ID"
|
|
hint="Required for billing"
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{!isCompatible && !isAzure && !isCloudflareAi && (
|
|
<div className="flex items-center gap-3">
|
|
<Button onClick={handleTest} variant="secondary" disabled={testing}>
|
|
{testing ? "Testing..." : "Test Connection"}
|
|
</Button>
|
|
{testResult && (
|
|
<Badge variant={testResult === "success" ? "success" : "error"}>
|
|
{testResult === "success" ? "Valid" : "Failed"}
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex gap-2">
|
|
<Button onClick={handleSubmit} fullWidth disabled={saving}>{saving ? "Saving..." : "Save"}</Button>
|
|
<Button onClick={onClose} variant="ghost" fullWidth>Cancel</Button>
|
|
</div>
|
|
</div>
|
|
</Modal>
|
|
);
|
|
}
|
|
|
|
EditConnectionModal.propTypes = {
|
|
isOpen: PropTypes.bool.isRequired,
|
|
connection: PropTypes.shape({
|
|
id: PropTypes.string,
|
|
name: PropTypes.string,
|
|
email: PropTypes.string,
|
|
priority: PropTypes.number,
|
|
authType: PropTypes.string,
|
|
provider: PropTypes.string,
|
|
providerSpecificData: PropTypes.object,
|
|
}),
|
|
proxyPools: PropTypes.arrayOf(PropTypes.shape({
|
|
id: PropTypes.string,
|
|
name: PropTypes.string,
|
|
})),
|
|
onSave: PropTypes.func.isRequired,
|
|
onClose: PropTypes.func.isRequired,
|
|
};
|
|
|