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:
Cursor Agent 2026-05-30 16:59:10 +00:00
parent 4cc3dd7aad
commit 7a6de62c82
No known key found for this signature in database
6 changed files with 183 additions and 63 deletions

View file

@ -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.

View file

@ -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 });

View file

@ -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", () => ({

View file

@ -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>

View file

@ -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" />);

View file

@ -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&apos;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>