feat(ui): show gateway URL and agent setup instructions after creating a key (#47199)

* feat(ui): show gateway URL and agent setup instructions after creating a key

Co-Authored-By: Ishaan Jaffer <155045088+ishaan-berri@users.noreply.github.com>

* fix(ui): make the selected agent button visibly distinct in dark mode

Co-Authored-By: Ishaan Jaffer <155045088+ishaan-berri@users.noreply.github.com>

* fix(ui): hide agent instructions by default and use a generic title

Co-Authored-By: Ishaan Jaffer <155045088+ishaan-berri@users.noreply.github.com>

* fix(ui): use one shared agent instructions card with Claude Code and Codex logos

Co-Authored-By: Ishaan Jaffer <155045088+ishaan-berri@users.noreply.github.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
ishaan-berri 2026-10-17 17:34:03 -07:00 • committed by GitHub
parent d5a8c5fd51
commit 4a8b47542c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 197 additions and 2 deletions

View file

@ -10,6 +10,7 @@ import { chooseSelectOption, renderWithProviders as render } from "../../../../.
vi.mock("@/components/networking", () => ({
apiClient: { get: vi.fn() },
createAgentCall: vi.fn(),
getProxyBaseUrl: vi.fn(() => "http://gw.test:4000/"),
getAgentCreateMetadata: vi.fn(),
getAgentsList: vi.fn(),
keyCreateForAgentCall: vi.fn(),

View file

@ -8,6 +8,7 @@ import type { AgentCreateInfo } from "@/components/networking";
vi.mock("@/components/networking", () => ({
createAgentCall: vi.fn(),
getProxyBaseUrl: vi.fn(() => "http://gw.test:4000/"),
getAgentCreateMetadata: vi.fn(),
getAgentsList: vi.fn(),
keyCreateForAgentCall: vi.fn(),

View file

@ -1914,7 +1914,7 @@ const CreateKey: React.FC<CreateKeyProps> = ({ team, teams, data, addKey, autoOp
{apiKey && (
<Dialog open={isModalVisible} onOpenChange={(open) => !open && handleCancel()}>
<DialogContent className="max-h-[calc(100dvh-2rem)] overflow-y-auto">
<DialogContent className="max-h-[calc(100dvh-2rem)] overflow-y-auto sm:max-w-2xl">
<div className="grid grid-cols-1 gap-2 w-full">
<DialogTitle className="text-lg font-medium text-foreground">Save your Key</DialogTitle>
{apiKey != null ? (

View file

@ -1,13 +1,43 @@
import { cloneElement, type MouseEventHandler, type ReactElement, type ReactNode } from "react";
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { render, screen, act } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import CreatedKeyDisplay from "./CreatedKeyDisplay";
import CreatedKeyDisplay, { buildAgentInstructions } from "./CreatedKeyDisplay";
import { toast } from "@/lib/toast";
const mockCopy = vi.hoisted(() => vi.fn<(text: string) => void>());
vi.mock("@/components/networking", () => ({
getProxyBaseUrl: vi.fn(() => "http://gw.test:4000/"),
}));
vi.mock("react-copy-to-clipboard", () => ({
CopyToClipboard: ({
text,
onCopy,
children,
}: {
text: string;
onCopy?: (text: string, result: boolean) => void;
children: ReactNode;
}) => {
const child = children as ReactElement<{ onClick?: MouseEventHandler<HTMLButtonElement> }>;
return cloneElement(child, {
onClick: (event) => {
child.props.onClick?.(event);
mockCopy(text);
onCopy?.(text, true);
},
});
},
}));
describe("CreatedKeyDisplay", () => {
beforeEach(() => {
vi.useFakeTimers({ shouldAdvanceTime: true });
mockCopy.mockReset();
vi.mocked(toast.success).mockClear();
});
afterEach(() => {
@ -69,4 +99,87 @@ describe("CreatedKeyDisplay", () => {
expect(screen.getByRole("button", { name: /copy virtual key/i })).toBeInTheDocument();
});
it("shows the gateway URL without its trailing slash and copies it", async () => {
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });
render(<CreatedKeyDisplay apiKey="sk-test-123" />);
expect(screen.getByText("http://gw.test:4000")).toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "Copy URL" }));
expect(mockCopy).toHaveBeenCalledWith("http://gw.test:4000");
expect(toast.success).toHaveBeenCalledWith("Gateway URL copied to clipboard");
});
it("shows one shared instruction card with both agent logos and expands on demand", async () => {
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });
render(<CreatedKeyDisplay apiKey="sk-test-123" />);
expect(screen.getByRole("img", { name: "Claude Code" })).toBeInTheDocument();
expect(screen.getByRole("img", { name: "Codex" })).toBeInTheDocument();
expect(screen.getByText("Agent instructions")).toBeInTheDocument();
expect(screen.queryByText("Set up your agent")).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Claude Code" })).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Codex" })).not.toBeInTheDocument();
const showButton = screen.getByRole("button", { name: "Show" });
expect(showButton).toHaveAttribute("aria-expanded", "false");
expect(screen.queryByText(/POST http:\/\/gw\.test:4000\/v1\/responses/)).not.toBeInTheDocument();
await user.click(showButton);
expect(screen.getByRole("button", { name: "Hide" })).toHaveAttribute("aria-expanded", "true");
expect(screen.getByText(/POST http:\/\/gw\.test:4000\/v1\/responses/)).toBeInTheDocument();
expect(screen.getByText(/ANTHROPIC_BASE_URL=http:\/\/gw\.test:4000/)).toBeInTheDocument();
expect(screen.getByText(/OPENAI_BASE_URL=http:\/\/gw\.test:4000\/v1/)).toBeInTheDocument();
});
it("copies complete shared instructions while the card is collapsed", async () => {
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });
render(<CreatedKeyDisplay apiKey="sk-test-123" />);
expect(screen.getByRole("button", { name: "Show" })).toHaveAttribute("aria-expanded", "false");
expect(screen.queryByText(/POST http:\/\/gw\.test:4000\/v1\/responses/)).not.toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "Copy instructions" }));
const copiedInstructions = mockCopy.mock.calls.at(-1)?.[0] ?? "";
expect(copiedInstructions).toContain("Gateway URL: http://gw.test:4000");
expect(copiedInstructions).toContain("Virtual key: sk-test-123");
expect(copiedInstructions).toContain("POST http://gw.test:4000/v1/chat/completions");
expect(copiedInstructions).toContain("POST http://gw.test:4000/v1/responses");
expect(copiedInstructions).toContain("POST http://gw.test:4000/v1/messages");
expect(copiedInstructions).toContain("GET http://gw.test:4000/v1/models");
expect(copiedInstructions).toContain(
"To run Claude Code through this gateway, set ANTHROPIC_BASE_URL=http://gw.test:4000 and ANTHROPIC_AUTH_TOKEN=sk-test-123.",
);
expect(copiedInstructions).toContain(
"To run Codex through this gateway, set OPENAI_BASE_URL=http://gw.test:4000/v1 and OPENAI_API_KEY=sk-test-123.",
);
expect(screen.getByRole("button", { name: "Copied!" })).toBeInTheDocument();
expect(toast.success).toHaveBeenCalledWith("Instructions copied");
});
});
describe("buildAgentInstructions", () => {
it("builds shared instructions with both agent settings and a normalized URL", () => {
expect(buildAgentInstructions("http://gw.test:4000/", "sk-test-123")).toBe(
[
"Use this LiteLLM AI gateway for model inference.",
"",
"Gateway URL: http://gw.test:4000",
"Virtual key: sk-test-123",
"",
"LiteLLM is a gateway that exposes OpenAI and Anthropic compatible APIs. Send the virtual key as a Bearer token (Authorization: Bearer sk-test-123) to:",
"- POST http://gw.test:4000/v1/chat/completions (OpenAI Chat Completions)",
"- POST http://gw.test:4000/v1/responses (OpenAI Responses)",
"- POST http://gw.test:4000/v1/messages (Anthropic Messages)",
"- GET http://gw.test:4000/v1/models (models this key can use)",
"",
"To run Claude Code through this gateway, set ANTHROPIC_BASE_URL=http://gw.test:4000 and ANTHROPIC_AUTH_TOKEN=sk-test-123.",
"To run Codex through this gateway, set OPENAI_BASE_URL=http://gw.test:4000/v1 and OPENAI_API_KEY=sk-test-123.",
"",
"Docs for everything else LiteLLM supports: https://docs.litellm.ai/docs/",
].join("\n"),
);
});
});

View file

@ -1,5 +1,9 @@
import React, { useState } from "react";
import { CopyToClipboard } from "react-copy-to-clipboard";
import { ChevronDown, ChevronUp } from "lucide-react";
import anthropicLogo from "../../../public/assets/logos/anthropic.svg";
import openaiLogo from "../../../public/assets/logos/openai_small.svg";
import { getProxyBaseUrl } from "@/components/networking";
import { Button } from "@/components/ui/button";
import { toast } from "@/lib/toast";
@ -7,12 +11,41 @@ interface CreatedKeyDisplayProps {
apiKey: string;
}
export const buildAgentInstructions = (baseUrl: string, apiKey: string): string => {
const normalizedBaseUrl = baseUrl.replace(/\/+$/, "");
const endpointIntroduction =
"LiteLLM is a gateway that exposes OpenAI and Anthropic compatible APIs. " +
`Send the virtual key as a Bearer token (Authorization: Bearer ${apiKey}) to:`;
return [
"Use this LiteLLM AI gateway for model inference.",
"",
`Gateway URL: ${normalizedBaseUrl}`,
`Virtual key: ${apiKey}`,
"",
endpointIntroduction,
`- POST ${normalizedBaseUrl}/v1/chat/completions (OpenAI Chat Completions)`,
`- POST ${normalizedBaseUrl}/v1/responses (OpenAI Responses)`,
`- POST ${normalizedBaseUrl}/v1/messages (Anthropic Messages)`,
`- GET ${normalizedBaseUrl}/v1/models (models this key can use)`,
"",
`To run Claude Code through this gateway, set ANTHROPIC_BASE_URL=${normalizedBaseUrl} and ANTHROPIC_AUTH_TOKEN=${apiKey}.`,
`To run Codex through this gateway, set OPENAI_BASE_URL=${normalizedBaseUrl}/v1 and OPENAI_API_KEY=${apiKey}.`,
"",
"Docs for everything else LiteLLM supports: https://docs.litellm.ai/docs/",
].join("\n");
};
/**
* 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 [instructionsCopied, setInstructionsCopied] = useState(false);
const [showInstructions, setShowInstructions] = useState(false);
const baseUrl = getProxyBaseUrl().replace(/\/+$/, "");
const instructions = buildAgentInstructions(baseUrl, apiKey);
const handleCopy = () => {
setCopied(true);
@ -20,6 +53,12 @@ const CreatedKeyDisplay: React.FC<CreatedKeyDisplayProps> = ({ apiKey }) => {
setTimeout(() => setCopied(false), 2000);
};
const handleCopyInstructions = () => {
setInstructionsCopied(true);
toast.success("Instructions copied");
setTimeout(() => setInstructionsCopied(false), 2000);
};
return (
<div>
<p className="mb-2">
@ -36,6 +75,47 @@ const CreatedKeyDisplay: React.FC<CreatedKeyDisplayProps> = ({ apiKey }) => {
<CopyToClipboard text={apiKey} onCopy={handleCopy}>
<Button className="mt-3">{copied ? "Copied!" : "Copy Virtual Key"}</Button>
</CopyToClipboard>
<p className="text-sm text-muted-foreground mt-4 mb-1">Gateway URL:</p>
<div className="bg-muted rounded-md p-2.5 mb-2.5 flex items-center justify-between gap-2">
<pre className="m-0 whitespace-normal break-words text-foreground">{baseUrl}</pre>
<CopyToClipboard text={baseUrl} onCopy={() => toast.success("Gateway URL copied to clipboard")}>
<Button type="button" variant="outline" className="shrink-0">
Copy URL
</Button>
</CopyToClipboard>
</div>
<div className="mt-4 rounded-md border border-border p-3">
<div className="flex flex-wrap items-center justify-between gap-2">
<div className="flex items-center gap-2">
<p className="text-sm font-medium">Agent instructions</p>
<img src={anthropicLogo.src} alt="Claude Code" title="Claude Code" className="size-5 object-contain" />
<img src={openaiLogo.src} alt="Codex" title="Codex" className="size-5 object-contain" />
</div>
<div className="flex items-center gap-2">
<CopyToClipboard text={instructions} onCopy={handleCopyInstructions}>
<Button type="button">{instructionsCopied ? "Copied!" : "Copy instructions"}</Button>
</CopyToClipboard>
<Button
type="button"
variant="outline"
aria-expanded={showInstructions}
onClick={() => setShowInstructions((expanded) => !expanded)}
>
{showInstructions ? "Hide" : "Show"}
{showInstructions ? (
<ChevronUp aria-hidden="true" className="size-4" />
) : (
<ChevronDown aria-hidden="true" className="size-4" />
)}
</Button>
</div>
</div>
{showInstructions && (
<pre className="mt-3 whitespace-pre-wrap break-words text-sm text-foreground">{instructions}</pre>
)}
</div>
</div>
);
};