mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-03 07:23:33 +00:00
* feat(ui): add clean-slate playground chat prototype
* feat(ui): replace the playground chat with the redesigned composer and inspector
* fix(ui): close the playground chat gaps found in self-review
Resend only complete text exchanges, so empty replies, MCP direct calls and
transcriptions no longer leak into chat history. A stopped request can no longer
append output, attach its gateway trace to a newer reply or clear the newer
request's loading state. MCP selections are only pruned once both the server and
toolset lists load. The MCP tool picker lists toolsets again and calls the server
that backs the chosen tool, and a saved model the current key cannot use is
cleared when the model list loads, matching the old chat
Also vendors the scroll-fade-x utility the attachment primitive uses
* fix(ui): resolve playground toolset tools per server and drop late session ids
A toolset now lists only its own tools, each resolved on the server the toolset
names, so the args form and the call always target the same tool. A saved
toolset no longer asks for tools with its own id while the toolset list loads,
and a failed tool list says so instead of claiming the server has no tools
A stopped request can no longer overwrite the Responses session id, starting a
new chat drops a request that is still running, embeddings replies are no longer
resent as chat history, and a saved model missing from an already cached model
list is cleared too. A failing toolset list falls back right away instead of
retrying while the MCP pickers show loading
* fix(ui): guard playground sends and label inspector stats
Pressing Enter twice no longer sends the same message twice, an attachment the
new endpoint cannot take is dropped when the endpoint changes, a stopped
Responses request can no longer show its code output on a newer reply, and a
reply that broke mid-stream is no longer resent as history
The inspector's speed, cost and routing stats get the same icons as the reply
footer, and the footer's metrics now line up with its copy and Inspect buttons
* fix(ui): write playground key settings only through the encoded storage path
Key source, virtual key and custom headers now go through their own write loop, separate from the plain settings, so a key value can never take the plain sessionStorage branch
* fix(ui): restore playground BYOK connect and reconnect
Picking a BYOK server for a direct tool call now shows a key button next to the
server picker, so the key can be connected without leaving the MCP endpoint. A
connected BYOK server gets a Replace key action in the MCP dialog, also shown
when its tools fail to load, and saving a key reloads that server's tool list
* revert: restore the single playground settings storage loop
Reverts ca4e742115. The CodeQL finding covers the same encoded sessionStorage handling the old chat used, so it is left as is
* test(ui): type the BYOK credential mock without deep instantiation
* fix(ui): let users back out of replacing a BYOK key
The replace-key form in the MCP dialog now has Cancel, and it resets when another server is picked or the dialog closes, so a working key is never forced to be overwritten
* refactor(ui): name the MCP dialog row focus handler
* fix(ui): accept exact custom models, tags and decimal temperatures in the playground
A typed model or tag that is a prefix of a listed one could not be used,
because Enter picked the partial match and the custom action only showed
when nothing matched. The custom value is now a list option whenever no
exact match exists. Temperature gets an editable input beside the slider
so values finer than the 0.1 step can be sent again.
* fix(ui): stop claiming no guardrails ran when the gateway sent no headers
Direct MCP calls, A2A agents and any request that fails before headers
arrive built their trace from an empty header set, so the inspector said
no guardrails or policies ran. The gateway fields are now left unset in
that case and the inspector says the endpoint does not report them.
* fix(ui): leave policies unknown when the gateway headers are hidden
The proxy exposes x-litellm-applied-guardrails to other origins but not
x-litellm-applied-policies, so a dashboard on another origin read every
request as having run no policies. Policies are now unknown unless the
call id header is readable, and the inspector says so.
59 lines
2.7 KiB
TypeScript
59 lines
2.7 KiB
TypeScript
import { expect, type Locator, type Page as PlaywrightPage } from "@playwright/test";
|
|
import { navigateToPage, dismissFeedbackPopup } from "./navigation";
|
|
import { Page } from "../fixtures/pages";
|
|
|
|
export const onlyVisible = (locator: Locator): Locator => locator.filter({ visible: true }).first();
|
|
|
|
export const composer = (page: PlaywrightPage): Locator =>
|
|
onlyVisible(page.getByRole("group").filter({ has: page.getByRole("textbox", { name: "Message" }) }));
|
|
|
|
export const modelSelect = (page: PlaywrightPage): Locator =>
|
|
onlyVisible(page.getByRole("combobox", { name: "Model", exact: true }));
|
|
|
|
export const sendButton = (page: PlaywrightPage): Locator =>
|
|
composer(page).getByRole("button", { name: "Send message" });
|
|
|
|
export const keySourceSelect = (page: PlaywrightPage): Locator => onlyVisible(page.getByLabel("Send as"));
|
|
|
|
export async function openPlayground(page: PlaywrightPage): Promise<void> {
|
|
await navigateToPage(page, Page.LlmPlayground);
|
|
await dismissFeedbackPopup(page);
|
|
await expect(modelSelect(page)).toBeVisible({ timeout: 20_000 });
|
|
const searchHint = page.getByRole("button", { name: "Dismiss search hint" });
|
|
if (await searchHint.isVisible()) {
|
|
await searchHint.click();
|
|
}
|
|
}
|
|
|
|
export async function openRequestSettings(page: PlaywrightPage): Promise<void> {
|
|
await composer(page).getByRole("button", { name: "Settings", exact: true }).click();
|
|
await expect(keySourceSelect(page)).toBeVisible({ timeout: 10_000 });
|
|
}
|
|
|
|
export async function enterVirtualKey(page: PlaywrightPage, key: string): Promise<void> {
|
|
await openRequestSettings(page);
|
|
await keySourceSelect(page).click();
|
|
await onlyVisible(page.getByRole("option", { name: "Virtual key", exact: true })).click({ timeout: 15_000 });
|
|
const keyInput = onlyVisible(page.getByLabel("Virtual key", { exact: true }));
|
|
await expect(keyInput).toBeVisible({ timeout: 10_000 });
|
|
await keyInput.fill(key);
|
|
await page.keyboard.press("Escape");
|
|
}
|
|
|
|
export async function searchModels(page: PlaywrightPage, query: string): Promise<void> {
|
|
await modelSelect(page).click();
|
|
await onlyVisible(page.getByPlaceholder("Search models")).fill(query);
|
|
}
|
|
|
|
export async function selectModel(page: PlaywrightPage, model: string): Promise<void> {
|
|
await searchModels(page, model);
|
|
await onlyVisible(page.getByRole("option", { name: model, exact: true })).click({ timeout: 15_000 });
|
|
await expect(modelSelect(page)).toContainText(model);
|
|
}
|
|
|
|
export async function sendMessage(page: PlaywrightPage, message: string): Promise<void> {
|
|
const input = composer(page).getByRole("textbox", { name: "Message" });
|
|
await expect(input).toBeVisible({ timeout: 15_000 });
|
|
await input.fill(message);
|
|
await sendButton(page).click();
|
|
}
|