refactor(app): RISKY pass R1-R3 — config-driven modal, cursor frame dedup, chunk helper
R1: merge AddOpenAICompatibleModal + AddAnthropicCompatibleModal → AddCompatibleModal (variant config-driven, ~180 dup removed, preserves per-variant useEffect behavior) R3: extract readCursorFrame() helper — dedup protobuf frame header/decompress loop (JSON+SSE transforms, byte-identical) R2: add chatChunkSse() helper, wire 7 cursor SSE scaffolds (byte-identical, cursor golden pass) Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Card } from "@/shared/components";
|
||||
import { MEDIA_PROVIDER_KINDS, getProviderAlias, resolveProviderId } from "@/shared/constants/providers";
|
||||
import { getModelsByProviderId } from "@/shared/constants/models";
|
||||
import { getModelsByProviderId, getModelKind } from "@/shared/constants/models";
|
||||
import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
|
||||
import { Row, KIND_EXAMPLE_CONFIG } from "./exampleShared";
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Card } from "@/shared/components";
|
||||
import { AI_PROVIDERS, getProviderAlias } from "@/shared/constants/providers";
|
||||
import { getModelsByProviderId } from "@/shared/constants/models";
|
||||
import { getModelsByProviderId, getModelKind } from "@/shared/constants/models";
|
||||
import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
|
||||
import { TTS_PROVIDER_CONFIG } from "@/shared/constants/ttsProviders";
|
||||
import { getTtsVoicesForModel } from "open-sse/config/ttsModels.js";
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { Badge, Button, Input, Modal, Select } from "@/shared/components";
|
||||
|
||||
const VARIANT_CONFIG = {
|
||||
openai: {
|
||||
title: "Add OpenAI Compatible",
|
||||
type: "openai-compatible",
|
||||
defaultBaseUrl: "https://api.openai.com/v1",
|
||||
namePlaceholder: "OpenAI Compatible (Prod)",
|
||||
prefixPlaceholder: "oc-prod",
|
||||
baseUrlHint: "Use the base URL (ending in /v1) for your OpenAI-compatible API.",
|
||||
modelIdPlaceholder: "e.g. gpt-4, claude-3-opus",
|
||||
errorLabel: "OpenAI Compatible",
|
||||
hasApiType: true,
|
||||
},
|
||||
anthropic: {
|
||||
title: "Add Anthropic Compatible",
|
||||
type: "anthropic-compatible",
|
||||
defaultBaseUrl: "https://api.anthropic.com/v1",
|
||||
namePlaceholder: "Anthropic Compatible (Prod)",
|
||||
prefixPlaceholder: "ac-prod",
|
||||
baseUrlHint: "Use the base URL (ending in /v1) for your Anthropic-compatible API. The system will append /messages.",
|
||||
modelIdPlaceholder: "e.g. claude-3-opus",
|
||||
errorLabel: "Anthropic Compatible",
|
||||
hasApiType: false,
|
||||
},
|
||||
};
|
||||
|
||||
const API_TYPE_OPTIONS = [
|
||||
{ value: "chat", label: "Chat Completions" },
|
||||
{ value: "responses", label: "Responses API" },
|
||||
];
|
||||
|
||||
function AddCompatibleModal({ variant, isOpen, onClose, onCreated }) {
|
||||
const config = VARIANT_CONFIG[variant];
|
||||
const initialFormData = () => ({
|
||||
name: "",
|
||||
prefix: "",
|
||||
...(config.hasApiType ? { apiType: "chat" } : {}),
|
||||
baseUrl: config.defaultBaseUrl,
|
||||
});
|
||||
|
||||
const [formData, setFormData] = useState(initialFormData);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [checkKey, setCheckKey] = useState("");
|
||||
const [checkModelId, setCheckModelId] = useState("");
|
||||
const [validating, setValidating] = useState(false);
|
||||
const [validationResult, setValidationResult] = useState(null);
|
||||
|
||||
// openai: reset baseUrl when apiType changes; anthropic: reset checks when opened
|
||||
useEffect(() => {
|
||||
if (config.hasApiType) {
|
||||
setFormData((prev) => ({ ...prev, baseUrl: config.defaultBaseUrl }));
|
||||
} else if (isOpen) {
|
||||
setValidationResult(null);
|
||||
setCheckKey("");
|
||||
setCheckModelId("");
|
||||
}
|
||||
}, [config.hasApiType ? formData.apiType : isOpen]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!formData.name.trim() || !formData.prefix.trim() || !formData.baseUrl.trim()) return;
|
||||
setSubmitting(true);
|
||||
try {
|
||||
const res = await fetch("/api/provider-nodes", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
name: formData.name,
|
||||
prefix: formData.prefix,
|
||||
...(config.hasApiType ? { apiType: formData.apiType } : {}),
|
||||
baseUrl: formData.baseUrl,
|
||||
type: config.type,
|
||||
}),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
onCreated(data.node);
|
||||
setFormData(initialFormData());
|
||||
setCheckKey("");
|
||||
setValidationResult(null);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`Error creating ${config.errorLabel} 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: config.type,
|
||||
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, method } = validationResult;
|
||||
if (valid) {
|
||||
return (
|
||||
<>
|
||||
<Badge variant="success">Valid</Badge>
|
||||
{method === "chat" && (
|
||||
<span className="text-sm text-text-muted">(via inference test)</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={config.title} onClose={onClose}>
|
||||
<div className="flex flex-col gap-4">
|
||||
<Input
|
||||
label="Name"
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
placeholder={config.namePlaceholder}
|
||||
hint="Required. A friendly label for this node."
|
||||
/>
|
||||
<Input
|
||||
label="Prefix"
|
||||
value={formData.prefix}
|
||||
onChange={(e) => setFormData({ ...formData, prefix: e.target.value })}
|
||||
placeholder={config.prefixPlaceholder}
|
||||
hint="Required. Used as the provider prefix for model IDs."
|
||||
/>
|
||||
{config.hasApiType && (
|
||||
<Select
|
||||
label="API Type"
|
||||
options={API_TYPE_OPTIONS}
|
||||
value={formData.apiType}
|
||||
onChange={(e) => setFormData({ ...formData, apiType: e.target.value })}
|
||||
/>
|
||||
)}
|
||||
<Input
|
||||
label="Base URL"
|
||||
value={formData.baseUrl}
|
||||
onChange={(e) => setFormData({ ...formData, baseUrl: e.target.value })}
|
||||
placeholder={config.defaultBaseUrl}
|
||||
hint={config.baseUrlHint}
|
||||
/>
|
||||
<Input
|
||||
label="API Key (for Check)"
|
||||
type="password"
|
||||
value={checkKey}
|
||||
onChange={(e) => setCheckKey(e.target.value)}
|
||||
/>
|
||||
<Input
|
||||
label="Model ID (optional)"
|
||||
value={checkModelId}
|
||||
onChange={(e) => setCheckModelId(e.target.value)}
|
||||
placeholder={config.modelIdPlaceholder}
|
||||
hint="If provider lacks /models endpoint, enter a model ID to validate via chat/completions instead."
|
||||
/>
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center">
|
||||
<Button
|
||||
onClick={handleValidate}
|
||||
disabled={!checkKey || validating || !formData.baseUrl.trim()}
|
||||
variant="secondary"
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
{validating ? "Checking..." : "Check"}
|
||||
</Button>
|
||||
{renderValidationResult()}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 sm:flex-row">
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
fullWidth
|
||||
disabled={
|
||||
!formData.name.trim() ||
|
||||
!formData.prefix.trim() ||
|
||||
!formData.baseUrl.trim() ||
|
||||
submitting
|
||||
}
|
||||
>
|
||||
{submitting ? "Creating..." : "Create"}
|
||||
</Button>
|
||||
<Button onClick={onClose} variant="ghost" fullWidth>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
AddCompatibleModal.propTypes = {
|
||||
variant: PropTypes.oneOf(["openai", "anthropic"]).isRequired,
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
onCreated: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default AddCompatibleModal;
|
||||
@@ -7,9 +7,6 @@ import {
|
||||
CardSkeleton,
|
||||
Badge,
|
||||
Button,
|
||||
Input,
|
||||
Modal,
|
||||
Select,
|
||||
Toggle,
|
||||
} from "@/shared/components";
|
||||
import ProviderIcon from "@/shared/components/ProviderIcon";
|
||||
@@ -26,6 +23,7 @@ import { getErrorCode, getRelativeTime } from "@/shared/utils";
|
||||
import { useNotificationStore } from "@/store/notificationStore";
|
||||
import { useHeaderSearchStore } from "@/store/headerSearchStore";
|
||||
import ModelAvailabilityBadge from "./components/ModelAvailabilityBadge";
|
||||
import AddCompatibleModal from "./components/AddCompatibleModal";
|
||||
|
||||
function getStatusDisplay(connected, error, errorCode) {
|
||||
const parts = [];
|
||||
@@ -557,7 +555,8 @@ export default function ProvidersPage() {
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
<AddOpenAICompatibleModal
|
||||
<AddCompatibleModal
|
||||
variant="openai"
|
||||
isOpen={showAddCompatibleModal}
|
||||
onClose={() => setShowAddCompatibleModal(false)}
|
||||
onCreated={(node) => {
|
||||
@@ -565,7 +564,8 @@ export default function ProvidersPage() {
|
||||
setShowAddCompatibleModal(false);
|
||||
}}
|
||||
/>
|
||||
<AddAnthropicCompatibleModal
|
||||
<AddCompatibleModal
|
||||
variant="anthropic"
|
||||
isOpen={showAddAnthropicCompatibleModal}
|
||||
onClose={() => setShowAddAnthropicCompatibleModal(false)}
|
||||
onCreated={(node) => {
|
||||
@@ -854,383 +854,6 @@ ApiKeyProviderCard.propTypes = {
|
||||
onToggle: PropTypes.func,
|
||||
};
|
||||
|
||||
function AddOpenAICompatibleModal({ isOpen, onClose, onCreated }) {
|
||||
const [formData, setFormData] = useState({
|
||||
name: "",
|
||||
prefix: "",
|
||||
apiType: "chat",
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
});
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [checkKey, setCheckKey] = useState("");
|
||||
const [checkModelId, setCheckModelId] = useState("");
|
||||
const [validating, setValidating] = useState(false);
|
||||
const [validationResult, setValidationResult] = useState(null);
|
||||
|
||||
const apiTypeOptions = [
|
||||
{ value: "chat", label: "Chat Completions" },
|
||||
{ value: "responses", label: "Responses API" },
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
const defaultBaseUrl = "https://api.openai.com/v1";
|
||||
setFormData((prev) => ({ ...prev, baseUrl: defaultBaseUrl }));
|
||||
}, [formData.apiType]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (
|
||||
!formData.name.trim() ||
|
||||
!formData.prefix.trim() ||
|
||||
!formData.baseUrl.trim()
|
||||
)
|
||||
return;
|
||||
setSubmitting(true);
|
||||
try {
|
||||
const res = await fetch("/api/provider-nodes", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
name: formData.name,
|
||||
prefix: formData.prefix,
|
||||
apiType: formData.apiType,
|
||||
baseUrl: formData.baseUrl,
|
||||
type: "openai-compatible",
|
||||
}),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
onCreated(data.node);
|
||||
setFormData({
|
||||
name: "",
|
||||
prefix: "",
|
||||
apiType: "chat",
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
});
|
||||
setCheckKey("");
|
||||
setValidationResult(null);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error creating OpenAI Compatible 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: "openai-compatible",
|
||||
modelId: checkModelId.trim() || undefined,
|
||||
}),
|
||||
});
|
||||
const data = await res.json();
|
||||
setValidationResult(data);
|
||||
} catch {
|
||||
setValidationResult({ valid: false, error: "Network error" });
|
||||
} finally {
|
||||
setValidating(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Helper to render validation result
|
||||
const renderValidationResult = () => {
|
||||
if (!validationResult) return null;
|
||||
const { valid, error, method } = validationResult;
|
||||
|
||||
if (valid) {
|
||||
return (
|
||||
<>
|
||||
<Badge variant="success">Valid</Badge>
|
||||
{method === "chat" && (
|
||||
<span className="text-sm text-text-muted">
|
||||
(via inference test)
|
||||
</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="Add OpenAI Compatible" onClose={onClose}>
|
||||
<div className="flex flex-col gap-4">
|
||||
<Input
|
||||
label="Name"
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
placeholder="OpenAI Compatible (Prod)"
|
||||
hint="Required. A friendly label for this node."
|
||||
/>
|
||||
<Input
|
||||
label="Prefix"
|
||||
value={formData.prefix}
|
||||
onChange={(e) => setFormData({ ...formData, prefix: e.target.value })}
|
||||
placeholder="oc-prod"
|
||||
hint="Required. Used as the provider prefix for model IDs."
|
||||
/>
|
||||
<Select
|
||||
label="API Type"
|
||||
options={apiTypeOptions}
|
||||
value={formData.apiType}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, apiType: e.target.value })
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
label="Base URL"
|
||||
value={formData.baseUrl}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, baseUrl: e.target.value })
|
||||
}
|
||||
placeholder="https://api.openai.com/v1"
|
||||
hint="Use the base URL (ending in /v1) for your OpenAI-compatible API."
|
||||
/>
|
||||
<Input
|
||||
label="API Key (for Check)"
|
||||
type="password"
|
||||
value={checkKey}
|
||||
onChange={(e) => setCheckKey(e.target.value)}
|
||||
/>
|
||||
<Input
|
||||
label="Model ID (optional)"
|
||||
value={checkModelId}
|
||||
onChange={(e) => setCheckModelId(e.target.value)}
|
||||
placeholder="e.g. gpt-4, claude-3-opus"
|
||||
hint="If provider lacks /models endpoint, enter a model ID to validate via chat/completions instead."
|
||||
/>
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center">
|
||||
<Button
|
||||
onClick={handleValidate}
|
||||
disabled={!checkKey || validating || !formData.baseUrl.trim()}
|
||||
variant="secondary"
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
{validating ? "Checking..." : "Check"}
|
||||
</Button>
|
||||
{renderValidationResult()}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 sm:flex-row">
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
fullWidth
|
||||
disabled={
|
||||
!formData.name.trim() ||
|
||||
!formData.prefix.trim() ||
|
||||
!formData.baseUrl.trim() ||
|
||||
submitting
|
||||
}
|
||||
>
|
||||
{submitting ? "Creating..." : "Create"}
|
||||
</Button>
|
||||
<Button onClick={onClose} variant="ghost" fullWidth>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
AddOpenAICompatibleModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
onCreated: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
function AddAnthropicCompatibleModal({ isOpen, onClose, onCreated }) {
|
||||
const [formData, setFormData] = useState({
|
||||
name: "",
|
||||
prefix: "",
|
||||
baseUrl: "https://api.anthropic.com/v1",
|
||||
});
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [checkKey, setCheckKey] = useState("");
|
||||
const [checkModelId, setCheckModelId] = useState("");
|
||||
const [validating, setValidating] = useState(false);
|
||||
const [validationResult, setValidationResult] = useState(null); // { valid, error, method }
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setValidationResult(null);
|
||||
setCheckKey("");
|
||||
setCheckModelId("");
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (
|
||||
!formData.name.trim() ||
|
||||
!formData.prefix.trim() ||
|
||||
!formData.baseUrl.trim()
|
||||
)
|
||||
return;
|
||||
setSubmitting(true);
|
||||
try {
|
||||
const res = await fetch("/api/provider-nodes", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
name: formData.name,
|
||||
prefix: formData.prefix,
|
||||
baseUrl: formData.baseUrl,
|
||||
type: "anthropic-compatible",
|
||||
}),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
onCreated(data.node);
|
||||
setFormData({
|
||||
name: "",
|
||||
prefix: "",
|
||||
baseUrl: "https://api.anthropic.com/v1",
|
||||
});
|
||||
setCheckKey("");
|
||||
setValidationResult(null);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error creating Anthropic Compatible 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: "anthropic-compatible",
|
||||
modelId: checkModelId.trim() || undefined,
|
||||
}),
|
||||
});
|
||||
const data = await res.json();
|
||||
setValidationResult(data);
|
||||
} catch {
|
||||
setValidationResult({ valid: false, error: "Network error" });
|
||||
} finally {
|
||||
setValidating(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Helper to render validation result
|
||||
const renderValidationResult = () => {
|
||||
if (!validationResult) return null;
|
||||
const { valid, error, method } = validationResult;
|
||||
|
||||
if (valid) {
|
||||
return (
|
||||
<>
|
||||
<Badge variant="success">Valid</Badge>
|
||||
{method === "chat" && (
|
||||
<span className="text-sm text-text-muted">
|
||||
(via inference test)
|
||||
</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="Add Anthropic Compatible" onClose={onClose}>
|
||||
<div className="flex flex-col gap-4">
|
||||
<Input
|
||||
label="Name"
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
placeholder="Anthropic Compatible (Prod)"
|
||||
hint="Required. A friendly label for this node."
|
||||
/>
|
||||
<Input
|
||||
label="Prefix"
|
||||
value={formData.prefix}
|
||||
onChange={(e) => setFormData({ ...formData, prefix: e.target.value })}
|
||||
placeholder="ac-prod"
|
||||
hint="Required. Used as the provider prefix for model IDs."
|
||||
/>
|
||||
<Input
|
||||
label="Base URL"
|
||||
value={formData.baseUrl}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, baseUrl: e.target.value })
|
||||
}
|
||||
placeholder="https://api.anthropic.com/v1"
|
||||
hint="Use the base URL (ending in /v1) for your Anthropic-compatible API. The system will append /messages."
|
||||
/>
|
||||
<Input
|
||||
label="API Key (for Check)"
|
||||
type="password"
|
||||
value={checkKey}
|
||||
onChange={(e) => setCheckKey(e.target.value)}
|
||||
/>
|
||||
<Input
|
||||
label="Model ID (optional)"
|
||||
value={checkModelId}
|
||||
onChange={(e) => setCheckModelId(e.target.value)}
|
||||
placeholder="e.g. claude-3-opus"
|
||||
hint="If provider lacks /models endpoint, enter a model ID to validate via chat/completions instead."
|
||||
/>
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center">
|
||||
<Button
|
||||
onClick={handleValidate}
|
||||
disabled={!checkKey || validating || !formData.baseUrl.trim()}
|
||||
variant="secondary"
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
{validating ? "Checking..." : "Check"}
|
||||
</Button>
|
||||
{renderValidationResult()}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 sm:flex-row">
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
fullWidth
|
||||
disabled={
|
||||
!formData.name.trim() ||
|
||||
!formData.prefix.trim() ||
|
||||
!formData.baseUrl.trim() ||
|
||||
submitting
|
||||
}
|
||||
>
|
||||
{submitting ? "Creating..." : "Create"}
|
||||
</Button>
|
||||
<Button onClick={onClose} variant="ghost" fullWidth>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
AddAnthropicCompatibleModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
onCreated: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
function ProviderTestResultsView({ results }) {
|
||||
if (results.error && !results.results) {
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user