mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-01 02:02:20 +00:00
feat(ui): show coding-agent prompt after creating a virtual key
After a key is created, the dialog now mirrors the Browser Use flow: title 'API Key Created', a yellow callout warning that the key is only shown once, the key itself with a copy icon, and a 'Prompt for coding agents' card containing a ready-to-paste prompt that bundles the proxy base URL, the new key, and links to the LiteLLM llms.txt docs. The primary 'Copy Virtual Key' button is preserved. The base URL is taken from proxyBaseUrl, falling back to window.location.origin when the UI is co-hosted with the proxy. e2e tests and the create-key-button mock are updated for the new heading and the WarningOutlined/CopyOutlined icons used by the revamped CreatedKeyDisplay. Co-authored-by: ishaan-berri <ishaan-berri@users.noreply.github.com>
This commit is contained in:
parent
4cc3dd7aad
commit
7a6de62c82
6 changed files with 183 additions and 63 deletions
|
|
@ -41,8 +41,8 @@ test.describe("Proxy Admin - Keys", () => {
|
|||
// Submit
|
||||
await page.getByRole("button", { name: "Create Key", exact: true }).click();
|
||||
|
||||
// Success shows "Save your Key" in a second dialog
|
||||
await expect(page.getByText("Save your Key")).toBeVisible({ timeout: 10_000 });
|
||||
// Success shows "API Key Created" in a second dialog
|
||||
await expect(page.getByText("API Key Created")).toBeVisible({ timeout: 10_000 });
|
||||
await page.keyboard.press("Escape");
|
||||
|
||||
// Verify the new key appears in the table
|
||||
|
|
@ -150,7 +150,7 @@ test.describe("Proxy Admin - Keys", () => {
|
|||
|
||||
await page.getByRole("button", { name: "Create Key", exact: true }).click();
|
||||
|
||||
await expect(page.getByText("Save your Key")).toBeVisible({ timeout: 10_000 });
|
||||
await expect(page.getByText("API Key Created")).toBeVisible({ timeout: 10_000 });
|
||||
await page.keyboard.press("Escape");
|
||||
|
||||
await expect(page.getByText(keyName)).toBeVisible({ timeout: 10_000 });
|
||||
|
|
@ -182,7 +182,7 @@ test.describe("Proxy Admin - Keys", () => {
|
|||
|
||||
await page.getByRole("button", { name: "Create Key", exact: true }).click();
|
||||
|
||||
await expect(page.getByText("Save your Key")).toBeVisible({ timeout: 10_000 });
|
||||
await expect(page.getByText("API Key Created")).toBeVisible({ timeout: 10_000 });
|
||||
|
||||
// Grab the new key from the success modal (rendered inside a <pre>) and
|
||||
// verify it can call /chat/completions for the model it was scoped to.
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@ test.describe("Team Admin", () => {
|
|||
|
||||
await page.getByRole("button", { name: "Create Key", exact: true }).click();
|
||||
|
||||
await expect(page.getByText("Save your Key")).toBeVisible({ timeout: 10_000 });
|
||||
await expect(page.getByText("API Key Created")).toBeVisible({ timeout: 10_000 });
|
||||
await page.keyboard.press("Escape");
|
||||
|
||||
await expect(page.getByText(keyName)).toBeVisible({ timeout: 10_000 });
|
||||
|
|
|
|||
|
|
@ -52,6 +52,8 @@ vi.mock("@/app/(dashboard)/hooks/keys/useKeys", () => ({
|
|||
|
||||
vi.mock("@ant-design/icons", () => ({
|
||||
InfoCircleOutlined: () => null,
|
||||
CopyOutlined: () => null,
|
||||
WarningOutlined: () => null,
|
||||
}));
|
||||
|
||||
vi.mock("react-copy-to-clipboard", () => ({
|
||||
|
|
|
|||
|
|
@ -1657,17 +1657,12 @@ const CreateKey: React.FC<CreateKeyProps> = ({ team, teams, data, addKey, autoOp
|
|||
)}
|
||||
|
||||
{apiKey && (
|
||||
<Modal open={isModalVisible} onOk={handleOk} onCancel={handleCancel} footer={null}>
|
||||
<Grid numItems={1} className="gap-2 w-full">
|
||||
<Title>Save your Key</Title>
|
||||
<Col numColSpan={1}>
|
||||
{apiKey != null ? (
|
||||
<CreatedKeyDisplay apiKey={apiKey} />
|
||||
) : (
|
||||
<Text>Key being created, this might take 30s</Text>
|
||||
)}
|
||||
</Col>
|
||||
</Grid>
|
||||
<Modal open={isModalVisible} onOk={handleOk} onCancel={handleCancel} footer={null} width={560}>
|
||||
{apiKey != null ? (
|
||||
<CreatedKeyDisplay apiKey={apiKey} />
|
||||
) : (
|
||||
<Text>Key being created, this might take 30s</Text>
|
||||
)}
|
||||
</Modal>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -7,51 +7,74 @@ vi.mock("@/components/molecules/message_manager", () => ({
|
|||
default: { success: vi.fn(), error: vi.fn(), warning: vi.fn(), info: vi.fn(), loading: vi.fn(), destroy: vi.fn() },
|
||||
}));
|
||||
|
||||
vi.mock("@/components/networking", () => ({
|
||||
proxyBaseUrl: "https://litellm.example.com",
|
||||
}));
|
||||
|
||||
import MessageManager from "@/components/molecules/message_manager";
|
||||
|
||||
describe("CreatedKeyDisplay", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers({ shouldAdvanceTime: true });
|
||||
vi.mocked(MessageManager.success).mockClear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("should render", () => {
|
||||
it("renders the API key value", () => {
|
||||
render(<CreatedKeyDisplay apiKey="sk-test-123" />);
|
||||
expect(screen.getByText("sk-test-123")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("created-key-value")).toHaveTextContent("sk-test-123");
|
||||
});
|
||||
|
||||
it("should display the security warning", () => {
|
||||
it("displays the security warning and modal heading", () => {
|
||||
render(<CreatedKeyDisplay apiKey="sk-test-123" />);
|
||||
expect(screen.getByText(/you will not be able to view it again/i)).toBeInTheDocument();
|
||||
expect(screen.getByRole("heading", { name: /api key created/i })).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(/make sure to copy your api key now/i),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText(/you won't be able to see it again/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show the copy button with initial label", () => {
|
||||
it("builds a coding agent prompt that embeds the key, base URL, and docs links", () => {
|
||||
render(<CreatedKeyDisplay apiKey="sk-test-123" />);
|
||||
expect(screen.getByRole("button", { name: /copy virtual key/i })).toBeInTheDocument();
|
||||
const prompt = screen.getByTestId("coding-agent-prompt").textContent ?? "";
|
||||
|
||||
expect(prompt).toMatch(/Base URL: https:\/\/litellm\.example\.com/);
|
||||
expect(prompt).toMatch(/API key: sk-test-123/);
|
||||
expect(prompt).toContain("Authorization: Bearer sk-test-123");
|
||||
expect(prompt).toContain("https://litellm.example.com/v1/models");
|
||||
expect(prompt).toContain("https://docs.litellm.ai/llms.txt");
|
||||
expect(prompt).toContain("https://docs.litellm.ai/llms-full.txt");
|
||||
});
|
||||
|
||||
it("should change button text to Copied after clicking copy", async () => {
|
||||
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });
|
||||
it("does not produce a trailing slash in the base URL when proxyBaseUrl is provided", () => {
|
||||
render(<CreatedKeyDisplay apiKey="sk-test-123" />);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /copy virtual key/i }));
|
||||
|
||||
expect(screen.getByRole("button", { name: /copied/i })).toBeInTheDocument();
|
||||
const prompt = screen.getByTestId("coding-agent-prompt").textContent ?? "";
|
||||
expect(prompt).not.toMatch(/Base URL: https:\/\/litellm\.example\.com\//);
|
||||
});
|
||||
|
||||
it("should show a success message when the key is copied", async () => {
|
||||
it("copies the API key to clipboard when the primary button is clicked", async () => {
|
||||
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });
|
||||
render(<CreatedKeyDisplay apiKey="sk-test-123" />);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /copy virtual key/i }));
|
||||
|
||||
expect(MessageManager.success).toHaveBeenCalledWith("Key copied to clipboard");
|
||||
expect(screen.getByRole("button", { name: /copied/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should revert button text back after 2 seconds", async () => {
|
||||
it("copies the coding agent prompt to clipboard when its copy button is clicked", async () => {
|
||||
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });
|
||||
render(<CreatedKeyDisplay apiKey="sk-test-123" />);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /copy prompt for coding agents/i }));
|
||||
|
||||
expect(MessageManager.success).toHaveBeenCalledWith("Prompt copied to clipboard");
|
||||
});
|
||||
|
||||
it("reverts the primary button label after 2 seconds", async () => {
|
||||
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });
|
||||
render(<CreatedKeyDisplay apiKey="sk-test-123" />);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,50 +1,150 @@
|
|||
import React, { useState } from "react";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import { CopyToClipboard } from "react-copy-to-clipboard";
|
||||
import { Button } from "antd";
|
||||
import { Button, Tooltip } from "antd";
|
||||
import { CopyOutlined, WarningOutlined } from "@ant-design/icons";
|
||||
import MessageManager from "@/components/molecules/message_manager";
|
||||
import { proxyBaseUrl } from "@/components/networking";
|
||||
|
||||
interface CreatedKeyDisplayProps {
|
||||
apiKey: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared component for displaying a newly-created virtual key.
|
||||
* Used on the Virtual Keys page and in the Add Agent wizard.
|
||||
*/
|
||||
const CreatedKeyDisplay: React.FC<CreatedKeyDisplayProps> = ({ apiKey }) => {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const buildCodingAgentPrompt = (apiKey: string, baseUrl: string): string => {
|
||||
return `You have access to LiteLLM, an OpenAI-compatible AI gateway that lets you call 100+ LLMs (OpenAI, Anthropic, Gemini, Bedrock, etc.) through a single API.
|
||||
|
||||
const handleCopy = () => {
|
||||
setCopied(true);
|
||||
Use these credentials:
|
||||
- Base URL: ${baseUrl}
|
||||
- API key: ${apiKey}
|
||||
|
||||
LiteLLM is a drop-in replacement for the OpenAI SDK. Point the SDK's \`base_url\` at the URL above and use the key as your \`OPENAI_API_KEY\`. To list the models available to this key, GET ${baseUrl}/v1/models with header \`Authorization: Bearer ${apiKey}\`.
|
||||
|
||||
LiteLLM docs:
|
||||
- llms.txt (overview + all doc links): https://docs.litellm.ai/llms.txt
|
||||
- llms-full.txt (complete reference with inline code examples): https://docs.litellm.ai/llms-full.txt`;
|
||||
};
|
||||
|
||||
const resolveProxyBaseUrl = (): string => {
|
||||
if (proxyBaseUrl) return proxyBaseUrl.replace(/\/$/, "");
|
||||
if (typeof window !== "undefined" && window.location?.origin) {
|
||||
return window.location.origin;
|
||||
}
|
||||
return "https://your-litellm-proxy";
|
||||
};
|
||||
|
||||
const CreatedKeyDisplay: React.FC<CreatedKeyDisplayProps> = ({ apiKey }) => {
|
||||
const [copiedKey, setCopiedKey] = useState(false);
|
||||
const [copiedPrompt, setCopiedPrompt] = useState(false);
|
||||
|
||||
const codingAgentPrompt = useMemo(
|
||||
() => buildCodingAgentPrompt(apiKey, resolveProxyBaseUrl()),
|
||||
[apiKey],
|
||||
);
|
||||
|
||||
const handleCopyKey = () => {
|
||||
setCopiedKey(true);
|
||||
MessageManager.success("Key copied to clipboard");
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
setTimeout(() => setCopiedKey(false), 2000);
|
||||
};
|
||||
|
||||
const handleCopyPrompt = () => {
|
||||
setCopiedPrompt(true);
|
||||
MessageManager.success("Prompt copied to clipboard");
|
||||
setTimeout(() => setCopiedPrompt(false), 2000);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p className="mb-2">
|
||||
Please save this secret key somewhere safe and accessible. For security reasons,{" "}
|
||||
<b>you will not be able to view it again</b> through your LiteLLM account. If you
|
||||
lose this secret key, you will need to generate a new one.
|
||||
</p>
|
||||
|
||||
<p className="text-sm text-gray-600 mt-3 mb-1">Virtual Key:</p>
|
||||
<div
|
||||
style={{
|
||||
background: "#f8f8f8",
|
||||
padding: "10px",
|
||||
borderRadius: "5px",
|
||||
marginBottom: "10px",
|
||||
}}
|
||||
>
|
||||
<pre style={{ wordWrap: "break-word", whiteSpace: "normal", margin: 0 }}>
|
||||
{apiKey}
|
||||
</pre>
|
||||
<div className="created-key-display">
|
||||
<div className="mb-2">
|
||||
<h2 className="text-lg font-semibold m-0">API Key Created</h2>
|
||||
<p className="text-sm text-gray-500 mt-1 mb-0">
|
||||
Paste this prompt into any coding agent to start using LiteLLM.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<CopyToClipboard text={apiKey} onCopy={handleCopy}>
|
||||
<Button type="primary" style={{ marginTop: 12 }}>
|
||||
{copied ? "Copied!" : "Copy Virtual Key"}
|
||||
<div
|
||||
className="flex items-start gap-2 px-3 py-2 mb-4 rounded-md border"
|
||||
style={{ background: "#fffbeb", borderColor: "#fde68a" }}
|
||||
role="alert"
|
||||
>
|
||||
<WarningOutlined style={{ color: "#b45309", marginTop: 3 }} />
|
||||
<span className="text-sm" style={{ color: "#92400e" }}>
|
||||
Make sure to copy your API key now. You won't be able to see it again.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="rounded-md border mb-4"
|
||||
style={{ borderColor: "#e5e7eb", background: "#fafafa" }}
|
||||
>
|
||||
<div className="flex items-center justify-between px-3 pt-3 pb-1">
|
||||
<span className="text-sm font-medium text-gray-700">Your API Key</span>
|
||||
<Tooltip title={copiedKey ? "Copied!" : "Copy API key"}>
|
||||
<CopyToClipboard text={apiKey} onCopy={handleCopyKey}>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
aria-label="Copy API key"
|
||||
icon={<CopyOutlined />}
|
||||
/>
|
||||
</CopyToClipboard>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="px-3 pb-3">
|
||||
<pre
|
||||
className="m-0 text-sm"
|
||||
style={{
|
||||
wordBreak: "break-all",
|
||||
whiteSpace: "pre-wrap",
|
||||
fontFamily:
|
||||
"ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
|
||||
}}
|
||||
data-testid="created-key-value"
|
||||
>
|
||||
{apiKey}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="rounded-md border"
|
||||
style={{ borderColor: "#e5e7eb", background: "#fafafa" }}
|
||||
>
|
||||
<div className="flex items-center justify-between px-3 pt-3 pb-1">
|
||||
<span className="text-sm font-medium text-gray-700">
|
||||
Prompt for coding agents
|
||||
</span>
|
||||
<Tooltip title={copiedPrompt ? "Copied!" : "Copy prompt"}>
|
||||
<CopyToClipboard text={codingAgentPrompt} onCopy={handleCopyPrompt}>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
aria-label="Copy prompt for coding agents"
|
||||
icon={<CopyOutlined />}
|
||||
/>
|
||||
</CopyToClipboard>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="px-3 pb-3">
|
||||
<pre
|
||||
className="m-0 text-sm text-gray-700"
|
||||
style={{
|
||||
maxHeight: 200,
|
||||
overflowY: "auto",
|
||||
whiteSpace: "pre-wrap",
|
||||
wordBreak: "break-word",
|
||||
fontFamily:
|
||||
"ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
|
||||
}}
|
||||
data-testid="coding-agent-prompt"
|
||||
>
|
||||
{codingAgentPrompt}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CopyToClipboard text={apiKey} onCopy={handleCopyKey}>
|
||||
<Button type="primary" style={{ marginTop: 16 }}>
|
||||
{copiedKey ? "Copied!" : "Copy Virtual Key"}
|
||||
</Button>
|
||||
</CopyToClipboard>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue