# v0.4.28 (2026-05-10)

## Features
- Add bun:sqlite adapter with automatic runtime detection (Bun/Node)
- Add bulk API key import (format: `name|sk-key`, one per line)
## Fixes
- Fix add API key for custom providers
This commit is contained in:
decolua committed 2026-05-10 08:44:14 +07:00
1 parent b39eb61c33
commit 530dc9cb3b
14 files changed
+282 -71

No files matched your search

@@ -4,7 +4,9 @@ import { useState } from "react";
import PropTypes from "prop-types";
import { Button, Badge, Input, Modal, Select } from "@/shared/components";
export default function AddApiKeyModal({ isOpen, provider, providerName, isCompatible, isAnthropic, authType, authHint, website, proxyPools, error, onSave, onClose }) {
const BULK_PLACEHOLDER = `name1|sk-key1\nname2|sk-key2\nsk-key-only-auto-named`;
export default function AddApiKeyModal({ isOpen, provider, providerName, isCompatible, isAnthropic, authType, authHint, website, proxyPools, error, onSave, onBulkDone, onClose }) {
const NONE_PROXY_POOL_VALUE = "__none__";
const isOllamaLocal = provider === "ollama-local";
const isCookie = authType === "cookie";
@@ -34,6 +36,9 @@ export default function AddApiKeyModal({ isOpen, provider, providerName, isCompa
const [validating, setValidating] = useState(false);
const [validationResult, setValidationResult] = useState(null);
const [saving, setSaving] = useState(false);
const [mode, setMode] = useState("single"); // "single" | "bulk"
const [bulkText, setBulkText] = useState("");
const [bulkResult, setBulkResult] = useState(null); // { success, failed }
const buildProviderSpecificData = () => {
if (isOllamaLocal && formData.ollamaHostUrl.trim()) {
@@ -113,11 +118,70 @@ export default function AddApiKeyModal({ isOpen, provider, providerName, isCompa
}
};
const handleBulkSubmit = async () => {
const lines = bulkText.split("\n").map(l => l.trim()).filter(Boolean);
if (!lines.length) return;
setSaving(true);
setBulkResult(null);
let success = 0;
let failed = 0;
for (let i = 0; i < lines.length; i++) {
const parts = lines[i].split("|");
const apiKey = parts.length >= 2 ? parts.slice(1).join("|").trim() : parts[0].trim();
const baseName = parts.length >= 2 ? parts[0].trim() : "Key";
const name = `${baseName} ${i + 1}`;
try {
const res = await fetch("/api/providers", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ provider, apiKey, name, priority: 1, testStatus: "unknown" }),
});
if (res.ok) success++;
else failed++;
} catch {
failed++;
}
}
setSaving(false);
setBulkResult({ success, failed });
if (success > 0 && onBulkDone) onBulkDone();
};
if (!provider) return null;
return (
<Modal isOpen={isOpen} title={`Add ${providerName || provider} ${credentialLabel}`} onClose={onClose}>
<div className="flex flex-col gap-4">
{/* Mode switcher */}
<div className="flex gap-2">
<Button size="sm" variant={mode === "single" ? "primary" : "ghost"} onClick={() => { setMode("single"); setBulkResult(null); }}>Single</Button>
<Button size="sm" variant={mode === "bulk" ? "primary" : "ghost"} onClick={() => { setMode("bulk"); setBulkResult(null); }}>Bulk Add</Button>
</div>
{mode === "bulk" && (
<div className="flex flex-col gap-3">
<p className="text-xs text-text-muted">One key per line. Format: <code>name|apiKey</code> or just <code>apiKey</code> (auto-named by index).</p>
<textarea
className="w-full rounded border border-accent/30 bg-sidebar p-2 text-sm font-mono resize-y min-h-[140px] focus:outline-none focus:ring-1 focus:ring-primary"
placeholder={BULK_PLACEHOLDER}
value={bulkText}
onChange={(e) => setBulkText(e.target.value)}
/>
{bulkResult && (
<div className={`text-sm font-medium ${bulkResult.failed > 0 ? "text-yellow-400" : "text-green-400"}`}>
✓ {bulkResult.success} added{bulkResult.failed > 0 ? `, ✗ ${bulkResult.failed} failed` : ""}
</div>
)}
<div className="flex gap-2">
<Button onClick={handleBulkSubmit} fullWidth disabled={saving || !bulkText.trim()}>
{saving ? "Adding..." : "Add All Keys"}
</Button>
<Button onClick={onClose} variant="ghost" fullWidth>Cancel</Button>
</div>
</div>
)}
{mode === "single" && (<>
<Input
label="Name"
value={formData.name}
@@ -278,6 +342,7 @@ export default function AddApiKeyModal({ isOpen, provider, providerName, isCompa
Cancel
</Button>
</div>
</>)}
</div>
</Modal>
);
@@ -298,5 +363,6 @@ AddApiKeyModal.propTypes = {
})),
error: PropTypes.string,
onSave: PropTypes.func.isRequired,
onBulkDone: PropTypes.func,
onClose: PropTypes.func.isRequired,
};
@@ -936,7 +936,6 @@ export default function ProviderDetailPage() {
setAddConnectionError("");
setShowAddApiKeyModal(true);
}}
disabled={connections.length > 0}
className="w-full sm:w-auto"
>
Add API Key
@@ -971,11 +970,6 @@ export default function ProviderDetailPage() {
</Button>
</div>
</div>
{connections.length > 0 && (
<p className="text-sm text-text-muted">
Only one connection is allowed per compatible node. Add another node if you need more connections.
</p>
)}
</Card>
)}
@@ -1190,6 +1184,7 @@ export default function ProviderDetailPage() {
proxyPools={proxyPools}
error={addConnectionError}
onSave={handleSaveApiKey}
onBulkDone={fetchConnections}
onClose={() => {
setAddConnectionError("");
setShowAddApiKeyModal(false);