Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,6 @@ def response_api_handler(
**kwargs,
)

completion_args = {}
completion_args.update(kwargs)
completion_args.update(litellm_completion_request)

litellm_completion_response: Union[
ModelResponse, litellm.CustomStreamWrapper
] = litellm.completion(
Expand Down
1 change: 1 addition & 0 deletions litellm/responses/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -1115,6 +1115,7 @@ def responses(
stream=stream,
extra_headers=extra_headers,
extra_body=extra_body,
timeout=timeout or request_timeout,
**kwargs,
)

Expand Down
36 changes: 36 additions & 0 deletions tests/llm_responses_api_testing/test_anthropic_responses_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,3 +158,39 @@ async def fake_session_handler(previous_response_id, litellm_completion_request)
assert (
mock_acompletion.call_args.kwargs["litellm_trace_id"] == "session-trace"
)


@pytest.mark.asyncio
async def test_aresponses_forwards_timeout_to_acompletion():
"""Regression test: timeout passed to aresponses() must reach acompletion()
on the completion transformation path (Anthropic, Bedrock, Vertex etc.).

Previously, `timeout` was a named param of `responses()` but was NOT
forwarded to `litellm_completion_transformation_handler.response_api_handler`,
so it was silently dropped — `Router(timeout=N)` was a no-op for Anthropic
and similar providers, with calls falling back to the provider SDK default
(~600s for Anthropic).
"""
with patch("litellm.acompletion", new_callable=AsyncMock) as mock_acompletion:
mock_acompletion.return_value = ModelResponse(
id="id",
created=0,
model="anthropic/claude-sonnet-4-5",
object="chat.completion",
choices=[],
)

await litellm.aresponses(
model="anthropic/claude-sonnet-4-5",
input="hello",
timeout=42,
api_key="sk-ant-fake",
)

assert mock_acompletion.call_count == 1
forwarded_timeout = mock_acompletion.call_args.kwargs.get("timeout")
assert forwarded_timeout == 42, (
f"timeout was not forwarded to acompletion (got {forwarded_timeout!r}); "
"this means Router(timeout=N) silently fails for providers on the "
"completion transformation path."
)
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { columns } from "@/components/molecules/models/columns";
import { getDisplayModelName } from "@/components/view_model/model_name_display";
import DeleteResourceModal from "@/components/common_components/DeleteResourceModal";
import NotificationsManager from "@/components/molecules/notifications_manager";
import { modelDeleteCall } from "@/components/networking";
import { modelDeleteCall, modelPatchUpdateCall } from "@/components/networking";
import { InfoCircleOutlined, SettingOutlined } from "@ant-design/icons";
import { PaginationState, SortingState } from "@tanstack/react-table";
import { useQueryClient } from "@tanstack/react-query";
Expand Down Expand Up @@ -220,6 +220,25 @@ const AllModelsTab = ({
}
};

const [pausingModelId, setPausingModelId] = useState<string | null>(null);

const handleTogglePause = async (modelId: string, blocked: boolean) => {
if (!accessToken) return;
try {
setPausingModelId(modelId);
await modelPatchUpdateCall(accessToken, { blocked }, modelId);
NotificationsManager.success(blocked ? "Model paused" : "Model resumed");
// invalidateQueries already schedules a refetch for active observers
// on this key — no need to also call refetchModels() (would double-fetch).
queryClient.invalidateQueries({ queryKey: ["models", "list"] });
} catch (error) {
console.error("Error toggling model pause state:", error);
NotificationsManager.fromBackend(error);
} finally {
setPausingModelId(null);
}
};

return (
<TabPanel>
<Grid>
Expand Down Expand Up @@ -536,6 +555,8 @@ const AllModelsTab = ({
expandedRows,
setExpandedRows,
setDeleteModalModelId,
handleTogglePause,
pausingModelId,
)}
data={filteredData}
isLoading={isLoadingModelsInfo}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export interface ModelInfo {
team_id: string;
db_model: boolean;
access_groups: string[] | null;
blocked?: boolean;
}

export interface LiteLLMParams {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -944,4 +944,108 @@ describe("columns", () => {
expect(screen.getByText("Out: $0.03")).toBeInTheDocument();
expect(screen.queryByText(/In:/)).not.toBeInTheDocument();
});

describe("pause/resume toggle", () => {
const renderWithToggle = (
overrides: Partial<ReturnType<typeof createMockModel>["model_info"]> = {},
togglePauseHandler?: ReturnType<typeof vi.fn>,
userRole: string = "Admin",
) => {
const handler = togglePauseHandler ?? vi.fn();
const cols = columns(
userRole,
defaultProps.userID,
defaultProps.premiumUser,
defaultProps.setSelectedModelId,
defaultProps.setSelectedTeamId,
defaultProps.getDisplayModelName,
defaultProps.handleEditClick,
defaultProps.handleRefreshClick,
defaultProps.expandedRows,
defaultProps.setExpandedRows,
vi.fn(),
handler,
);
const model = createMockModel({
model_info: { ...createMockModel().model_info, ...overrides },
});
render(<TestTable data={[model]} columns={cols} />);
return { handler };
};

it("renders the toggle ON for a db_model that is not blocked", () => {
renderWithToggle({ db_model: true, blocked: false });
const toggle = screen.getByRole("switch", { name: /pause model/i });
expect(toggle).toBeEnabled();
expect(toggle).toHaveAttribute("aria-checked", "true");
});

it("renders the toggle OFF for a db_model that is blocked", () => {
renderWithToggle({ db_model: true, blocked: true });
const toggle = screen.getByRole("switch", { name: /resume model/i });
expect(toggle).toBeEnabled();
expect(toggle).toHaveAttribute("aria-checked", "false");
});

it("calls the handler with blocked=true when an admin flips an active toggle off", async () => {
const handler = vi.fn();
renderWithToggle({ db_model: true, blocked: false }, handler);
await userEvent.click(screen.getByRole("switch", { name: /pause model/i }));
expect(handler).toHaveBeenCalledWith("test-model-id", true);
});

it("calls the handler with blocked=false when an admin flips a paused toggle on", async () => {
const handler = vi.fn();
renderWithToggle({ db_model: true, blocked: true }, handler);
await userEvent.click(screen.getByRole("switch", { name: /resume model/i }));
expect(handler).toHaveBeenCalledWith("test-model-id", false);
});

it("disables the toggle for non-admin users", () => {
const handler = vi.fn();
renderWithToggle({ db_model: true, blocked: false }, handler, "User");
const toggle = screen.getByRole("switch", { name: /pause model/i });
expect(toggle).toBeDisabled();
});

it("disables the toggle for config models", () => {
const handler = vi.fn();
renderWithToggle({ db_model: false, blocked: false }, handler, "Admin");
const toggle = screen.getByRole("switch", { name: /pause model/i });
expect(toggle).toBeDisabled();
});

it("disables the toggle while a PATCH for the same row is in-flight", () => {
// Regression for Greptile P1 on PR #28151 — antd's `loading` prop is
// visual only and does not prevent click events, so the row needs to
// be explicitly disabled while its PATCH is pending to avoid
// racing/conflicting PATCH calls on double-click.
const handler = vi.fn();
const model = createMockModel({
model_info: {
...createMockModel().model_info,
db_model: true,
blocked: false,
},
});
const cols = columns(
"Admin",
defaultProps.userID,
defaultProps.premiumUser,
defaultProps.setSelectedModelId,
defaultProps.setSelectedTeamId,
defaultProps.getDisplayModelName,
defaultProps.handleEditClick,
defaultProps.handleRefreshClick,
defaultProps.expandedRows,
defaultProps.setExpandedRows,
vi.fn(),
handler,
model.model_info.id, // pausingModelId matches this row
);
render(<TestTable data={[model]} columns={cols} />);
const toggle = screen.getByRole("switch", { name: /pause model/i });
expect(toggle).toBeDisabled();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { EditOutlined, InfoCircleOutlined, SyncOutlined } from "@ant-design/icon
import { TrashIcon } from "@heroicons/react/outline";
import { ColumnDef } from "@tanstack/react-table";
import { Badge, Button, Icon } from "@tremor/react";
import { Divider, Flex, Popover, Space, Tooltip, Typography } from "antd";
import { Divider, Flex, Popover, Space, Switch, Tooltip, Typography } from "antd";
import { ModelData } from "../../model_dashboard/types";
import { ProviderLogo } from "./ProviderLogo";

Expand Down Expand Up @@ -53,6 +53,8 @@ export const columns = (
expandedRows: Set<string>,
setExpandedRows: (expandedRows: Set<string>) => void,
onDeleteClick?: (modelId: string) => void,
onTogglePauseClick?: (modelId: string, blocked: boolean) => void | Promise<void>,
pausingModelId?: string | null,
): ColumnDef<ModelData>[] => [
{
header: () => <span className="text-sm font-semibold">Model ID</span>,
Expand Down Expand Up @@ -398,15 +400,47 @@ export const columns = (
{
id: "actions",
header: () => <span className="text-sm font-semibold">Actions</span>,
size: 60,
minSize: 40,
size: 100,
minSize: 80,
enableResizing: false,
cell: ({ row }) => {
const model = row.original;
const canEditModel = userRole === "Admin" || model.model_info?.created_by === userID;
const isConfigModel = !model.model_info?.db_model;
const isAdmin = userRole === "Admin";
const isBlocked = model.model_info?.blocked === true;
const isPauseToggleable = !isConfigModel && isAdmin && Boolean(onTogglePauseClick);
const pauseTooltip = isConfigModel
? "Config models cannot be paused from the dashboard. Pause is DB-backed."
: !isAdmin
? "Only proxy admins can pause or resume a model."
: isBlocked
? "Resume model — restore normal routing."
: "Pause model — stop routing requests until resumed.";
// antd's `loading` prop on Switch is purely cosmetic — it does not block
// clicks. Pair `loading` with `disabled` derived from the same condition
// so a double-click during a pending PATCH cannot send a second,
// conflicting `blocked` value.
const isPausing = pausingModelId === model.model_info.id;
return (
<div className="flex items-center justify-end gap-2 pr-4">
<Tooltip title={pauseTooltip}>
<Switch
size="small"
checked={!isBlocked}
disabled={!isPauseToggleable || isPausing}
loading={isPausing}
aria-label={isBlocked ? "Resume model" : "Pause model"}
onClick={(_, e) => {
e.stopPropagation();
}}
onChange={(nextChecked) => {
if (isPauseToggleable && onTogglePauseClick) {
void onTogglePauseClick(model.model_info.id, !nextChecked);
}
}}
/>
</Tooltip>
{isConfigModel ? (
<Tooltip title="Config model cannot be deleted on the dashboard. Please delete it from the config file.">
<Icon icon={TrashIcon} size="sm" className="opacity-50 cursor-not-allowed" />
Expand Down
Loading