- Each configured tier routes to a saved model group. Test Connection sends a minimal request through the proxy to
- each one, exactly as the auto router would.
+ Test Connection sends a minimal request to every configured tier, classifier, default, and embedding model. The
+ classifier probe includes its reasoning effort override.
{targets.map((target, index) => {
const result = results[index] ?? { status: "pending" };
return (
diff --git a/ui/litellm-dashboard/src/components/add_model/build_auto_router_test_targets.test.ts b/ui/litellm-dashboard/src/components/add_model/build_auto_router_test_targets.test.ts
index 7c29ea53060..6eb1825710d 100644
--- a/ui/litellm-dashboard/src/components/add_model/build_auto_router_test_targets.test.ts
+++ b/ui/litellm-dashboard/src/components/add_model/build_auto_router_test_targets.test.ts
@@ -126,4 +126,32 @@ describe("buildAutoRouterTestTargets", () => {
});
expect(targets).toEqual([{ labels: ["SIMPLE"], modelGroup: "gpt-4o-mini", mode: "chat" }]);
});
+
+ it("adds a distinct classifier probe with its reasoning effort", () => {
+ const targets = buildAutoRouterTestTargets({
+ tiers: tierEntries(["gpt-5-mini"]),
+ semanticMatchingEnabled: false,
+ embeddingModel: undefined,
+ classifier: { model: "gpt-5-mini", reasoningEffort: "low" },
+ });
+ expect(targets).toEqual([
+ { labels: ["SIMPLE"], modelGroup: "gpt-5-mini", mode: "chat" },
+ {
+ labels: ["Classifier"],
+ modelGroup: "gpt-5-mini",
+ mode: "chat",
+ requestParams: { reasoning_effort: "low" },
+ },
+ ]);
+ });
+
+ it("omits an empty classifier and omits params when the classifier uses provider defaults", () => {
+ const base = { tiers: tierEntries([]), semanticMatchingEnabled: false, embeddingModel: undefined };
+ const emptyClassifier = { ...base, classifier: { model: " " } };
+ const providerDefaultClassifier = { ...base, classifier: { model: "gpt-5-mini" } };
+ expect(buildAutoRouterTestTargets(emptyClassifier)).toEqual([]);
+ expect(buildAutoRouterTestTargets(providerDefaultClassifier)).toEqual([
+ { labels: ["Classifier"], modelGroup: "gpt-5-mini", mode: "chat" },
+ ]);
+ });
});
diff --git a/ui/litellm-dashboard/src/components/add_model/build_auto_router_test_targets.ts b/ui/litellm-dashboard/src/components/add_model/build_auto_router_test_targets.ts
index 70b92dbf8cc..295a3b7f400 100644
--- a/ui/litellm-dashboard/src/components/add_model/build_auto_router_test_targets.ts
+++ b/ui/litellm-dashboard/src/components/add_model/build_auto_router_test_targets.ts
@@ -4,6 +4,7 @@ export interface AutoRouterTestTarget {
labels: string[];
modelGroup: string;
mode: AutoRouterTestMode;
+ requestParams?: Record
;
}
export interface BuildAutoRouterTestTargetsParams {
@@ -14,6 +15,7 @@ export interface BuildAutoRouterTestTargetsParams {
/** The resolved default model - see resolveComplexityDefaultModel. A live fallback destination,
* so it is probed even when no tier lists it. */
defaultModel?: string;
+ classifier?: { model: string; reasoningEffort?: string };
}
export const buildAutoRouterTestTargets = ({
@@ -21,6 +23,7 @@ export const buildAutoRouterTestTargets = ({
semanticMatchingEnabled,
embeddingModel,
defaultModel,
+ classifier,
}: BuildAutoRouterTestTargetsParams): AutoRouterTestTarget[] => {
const tieredByModel = tiers.reduce>((acc, [tier, models]) => {
return models.reduce((tierAcc, rawModel) => {
@@ -50,5 +53,17 @@ export const buildAutoRouterTestTargets = ({
? [{ labels: ["Embedding"], modelGroup: embeddingModel.trim(), mode: "embedding" as const }]
: [];
- return [...tierTargets, ...embeddingTarget];
+ const classifierModel = classifier?.model.trim();
+ const classifierTarget: AutoRouterTestTarget[] = classifierModel
+ ? [
+ {
+ labels: ["Classifier"],
+ modelGroup: classifierModel,
+ mode: "chat",
+ ...(classifier?.reasoningEffort && { requestParams: { reasoning_effort: classifier.reasoningEffort } }),
+ },
+ ]
+ : [];
+
+ return [...tierTargets, ...embeddingTarget, ...classifierTarget];
};
diff --git a/ui/litellm-dashboard/src/components/add_model/build_complexity_router_config.test.ts b/ui/litellm-dashboard/src/components/add_model/build_complexity_router_config.test.ts
index 3ef55b9295d..53ab859baa0 100644
--- a/ui/litellm-dashboard/src/components/add_model/build_complexity_router_config.test.ts
+++ b/ui/litellm-dashboard/src/components/add_model/build_complexity_router_config.test.ts
@@ -4,6 +4,7 @@ import {
normalizeClassifierLlmConfig,
getKeywordTierRulesError,
getClassifierModelError,
+ getClassifierReasoningEffortError,
getMissingTiersError,
hydrateCustomTierSet,
getSemanticConfigError,
@@ -506,6 +507,19 @@ describe("classifier prompt and fallback", () => {
expect(buildComplexityRouterConfig(llmParams)).not.toHaveProperty("classifier_fallback");
});
+ it("keeps an explicit classifier reasoning effort", () => {
+ const config = buildComplexityRouterConfig({
+ ...llmParams,
+ classifierLlmConfig: { model: "haiku-classifier", timeout_ms: 400, reasoning_effort: "low" },
+ });
+ expect(config.classifier_llm_config?.reasoning_effort).toBe("low");
+ });
+
+ it("omits classifier reasoning effort when the provider default is selected", () => {
+ const config = buildComplexityRouterConfig(llmParams);
+ expect(config.classifier_llm_config).not.toHaveProperty("reasoning_effort");
+ });
+
it("sends the chat preset the operator picked", () => {
const config = buildComplexityRouterConfig({
...llmParams,
@@ -540,12 +554,16 @@ describe("classifier prompt and fallback", () => {
});
it("normalizeClassifierLlmConfig leaves a real prompt untouched and strips an empty one", () => {
- expect(normalizeClassifierLlmConfig({ model: "m", timeout_ms: 1, system_prompt: "x" })).toEqual({
+ const customPromptConfig = { model: "m", timeout_ms: 1, reasoning_effort: "none" as const, system_prompt: "x" };
+ const emptyPromptConfig = { model: "m", timeout_ms: 1, system_prompt: "" };
+ const expectedCustomPromptConfig = {
model: "m",
timeout_ms: 1,
+ reasoning_effort: "none",
system_prompt: "x",
- });
- expect(normalizeClassifierLlmConfig({ model: "m", timeout_ms: 1, system_prompt: "" })).toEqual({
+ };
+ expect(normalizeClassifierLlmConfig(customPromptConfig)).toEqual(expectedCustomPromptConfig);
+ expect(normalizeClassifierLlmConfig(emptyPromptConfig)).toEqual({
model: "m",
timeout_ms: 1,
});
@@ -767,6 +785,26 @@ describe("getClassifierModelError", () => {
});
});
+describe("getClassifierReasoningEffortError", () => {
+ const classifier = {
+ classifier_type: "llm" as const,
+ classifier_llm_config: { model: "classifier", timeout_ms: 3000, reasoning_effort: "low" },
+ };
+
+ it.each([
+ [["low", "medium"], null],
+ [["medium", "high"], "low reasoning effort is not supported"],
+ [null, null],
+ [undefined, null],
+ ])("validates capability levels %o", (supportedReasoningEfforts, expectedError) => {
+ const error = getClassifierReasoningEffortError(classifier, [
+ { model_group: "classifier", supported_reasoning_efforts: supportedReasoningEfforts },
+ ]);
+ if (expectedError) expect(error).toContain(expectedError);
+ else expect(error).toBeNull();
+ });
+});
+
describe("getKeywordTierRulesError orphaned tiers", () => {
const rows = activeTierRows({ tiers });
@@ -944,11 +982,16 @@ describe("buildComplexityRouterConfig with an edited tier set", () => {
classifierLlmConfig: {
model: "gpt-4o-mini",
timeout_ms: 3000,
+ reasoning_effort: "low",
system_prompt: "replace the whole rubric",
classification_rubric: "agentic",
},
});
- expect(payload.classifier_llm_config).toEqual({ model: "gpt-4o-mini", timeout_ms: 3000 });
+ expect(payload.classifier_llm_config).toEqual({
+ model: "gpt-4o-mini",
+ timeout_ms: 3000,
+ reasoning_effort: "low",
+ });
});
it.each(
diff --git a/ui/litellm-dashboard/src/components/add_model/build_complexity_router_config.ts b/ui/litellm-dashboard/src/components/add_model/build_complexity_router_config.ts
index 91e4fdb23b5..b8af55e8c8a 100644
--- a/ui/litellm-dashboard/src/components/add_model/build_complexity_router_config.ts
+++ b/ui/litellm-dashboard/src/components/add_model/build_complexity_router_config.ts
@@ -1,4 +1,5 @@
import { KeywordTierRule } from "./KeywordTierRules";
+import type { ModelGroup } from "../llm_calls/fetch_models";
import {
type CustomTierSet,
type TierRow,
@@ -55,12 +56,18 @@ import {
export const normalizeClassifierLlmConfig = ({
model,
timeout_ms,
+ reasoning_effort,
classification_rubric,
system_prompt,
}: ClassifierLLMConfig): ClassifierLLMConfig =>
system_prompt?.trim()
- ? { model, timeout_ms, system_prompt }
- : { model, timeout_ms, ...(classification_rubric && { classification_rubric }) };
+ ? { model, timeout_ms, ...(reasoning_effort && { reasoning_effort }), system_prompt }
+ : {
+ model,
+ timeout_ms,
+ ...(reasoning_effort && { reasoning_effort }),
+ ...(classification_rubric && { classification_rubric }),
+ };
interface ScorerKnobInputs {
classifierType: ClassifierType;
@@ -268,6 +275,20 @@ export const getClassifierModelError = (
: "Please select a classifier model, or switch back to Heuristic";
};
+export const getClassifierReasoningEffortError = (
+ config: Pick,
+ modelInfo: readonly ModelGroup[],
+): string | null => {
+ if (!usesLlmClassifier(effectiveClassifierType(config))) return null;
+ const classifierConfig = config.classifier_llm_config;
+ if (!classifierConfig?.model || !classifierConfig.reasoning_effort) return null;
+ const supported = modelInfo.find(
+ (model) => model.model_group === classifierConfig.model,
+ )?.supported_reasoning_efforts;
+ if (!Array.isArray(supported) || supported.includes(classifierConfig.reasoning_effort)) return null;
+ return `${classifierConfig.reasoning_effort} reasoning effort is not supported by every deployment in ${classifierConfig.model}. Choose Default or a supported value.`;
+};
+
export const getSemanticConfigError = ({
semanticMatchingEnabled,
embeddingModel,
@@ -295,11 +316,15 @@ export const customTierWireFields = (
tier_definitions: tierDefinitionsFromRows(rows),
...(fallback && { fallback_tier: activeTierName(fallback) }),
classifier_type: "llm",
- // Rebuilt from the two fields an edited tier set allows. The backend rejects system_prompt and
+ // Rebuilt from the fields an edited tier set allows. The backend rejects system_prompt and
// classification_rubric beside tier_definitions, and both live inside this object rather than at
// the top level the omit list covers. The opening instructions ride classification_prompt below.
...(classifierLlmConfig && {
- classifier_llm_config: { model: classifierLlmConfig.model, timeout_ms: classifierLlmConfig.timeout_ms },
+ classifier_llm_config: {
+ model: classifierLlmConfig.model,
+ timeout_ms: classifierLlmConfig.timeout_ms,
+ ...(classifierLlmConfig.reasoning_effort && { reasoning_effort: classifierLlmConfig.reasoning_effort }),
+ },
}),
session_affinity: false,
...(classificationPrompt?.trim() && { classification_prompt: classificationPrompt.trim() }),
diff --git a/ui/litellm-dashboard/src/components/add_model/complexity_router_tiers.ts b/ui/litellm-dashboard/src/components/add_model/complexity_router_tiers.ts
index 8cdb730cfa6..eb58c94b789 100644
--- a/ui/litellm-dashboard/src/components/add_model/complexity_router_tiers.ts
+++ b/ui/litellm-dashboard/src/components/add_model/complexity_router_tiers.ts
@@ -1,4 +1,5 @@
import type { ComplexityTier } from "./KeywordTierRules";
+import type { ModelGroup } from "@/components/llm_calls/fetch_models";
import { TIER_ORDER } from "./tier_rows";
export type TierModelParams = Record;
@@ -18,6 +19,23 @@ export const REASONING_EFFORT_OPTIONS = ["none", "minimal", "low", "medium", "hi
*/
export type ReasoningEffort = (typeof REASONING_EFFORT_OPTIONS)[number] | (string & {});
+export const tierEffortOptionsForModels = (modelInfo: ModelGroup[]): Record =>
+ Object.fromEntries(
+ modelInfo.map((model) => [
+ model.model_group,
+ model.supported_reasoning_efforts ?? (model.supports_reasoning ? [...REASONING_EFFORT_OPTIONS] : []),
+ ]),
+ );
+
+/**
+ * Stricter than the tier variant on purpose: classifier overrides are new in this release, so an
+ * unknown capability list stays unknown instead of inventing provider levels.
+ */
+export const classifierEffortOptionsForModels = (
+ modelInfo: ModelGroup[],
+): Record =>
+ Object.fromEntries(modelInfo.map((model) => [model.model_group, model.supported_reasoning_efforts]));
+
const asRecord = (raw: unknown): Record | undefined =>
typeof raw === "object" && raw !== null && !Array.isArray(raw) ? (raw as Record) : undefined;
diff --git a/ui/litellm-dashboard/src/components/edit_auto_router/build_updated_complexity_router_config.test.ts b/ui/litellm-dashboard/src/components/edit_auto_router/build_updated_complexity_router_config.test.ts
index 01198bcc118..f24f3033901 100644
--- a/ui/litellm-dashboard/src/components/edit_auto_router/build_updated_complexity_router_config.test.ts
+++ b/ui/litellm-dashboard/src/components/edit_auto_router/build_updated_complexity_router_config.test.ts
@@ -16,7 +16,7 @@ const storedCustomConfig = (overrides: Record = {}) => ({
],
fallback_tier: "CASUAL",
classifier_type: "llm",
- classifier_llm_config: { model: "gpt-4o-mini", timeout_ms: 3000 },
+ classifier_llm_config: { model: "gpt-4o-mini", timeout_ms: 3000, reasoning_effort: "low" },
...overrides,
});
@@ -111,7 +111,7 @@ describe("buildUpdatedComplexityRouterConfig keyword matching", () => {
const STORED_LLM = {
tiers: { SIMPLE: ["gpt-4o-mini"], MEDIUM: [], COMPLEX: [], REASONING: [] },
classifier_type: "llm",
- classifier_llm_config: { model: "gpt-4o-mini", timeout_ms: 3000 },
+ classifier_llm_config: { model: "gpt-4o-mini", timeout_ms: 3000, reasoning_effort: "low" },
classifier_context_window_size: 5,
classifier_context_per_turn_chars: 300,
};
@@ -522,7 +522,7 @@ describe("managed keys survive an untouched open-and-save", () => {
tier_labels: { SIMPLE: "Cheap" },
classifier_type: "heuristic_first",
heuristic_first_max_tier: "SIMPLE",
- classifier_llm_config: { model: "gpt-4o-mini", timeout_ms: 3000 },
+ classifier_llm_config: { model: "gpt-4o-mini", timeout_ms: 3000, reasoning_effort: "low" },
classifier_context_window_size: 5,
classifier_context_budget_chars: 4000,
classifier_context_include_assistant_turns: true,
diff --git a/ui/litellm-dashboard/src/components/edit_auto_router/edit_auto_router_modal.tsx b/ui/litellm-dashboard/src/components/edit_auto_router/edit_auto_router_modal.tsx
index 442e1ec6039..54766df93ec 100644
--- a/ui/litellm-dashboard/src/components/edit_auto_router/edit_auto_router_modal.tsx
+++ b/ui/litellm-dashboard/src/components/edit_auto_router/edit_auto_router_modal.tsx
@@ -28,6 +28,7 @@ import {
type BuildComplexityRouterConfigParams,
buildComplexityRouterConfig,
getClassifierModelError,
+ getClassifierReasoningEffortError,
getKeywordTierRulesError,
getMissingTiersError,
getSemanticConfigError,
@@ -560,6 +561,12 @@ const EditAutoRouterModal: React.FC = ({
toast.fromError(classifierError);
return;
}
+ const classifierEffortError = getClassifierReasoningEffortError(complexityRouterConfig, modelInfo);
+ if (classifierEffortError) {
+ setShowValidationErrors(true);
+ toast.fromError(classifierEffortError);
+ return;
+ }
// Same guards the create form applies (add_auto_router_tab.tsx). The backend rejects a
// keyword rule with no keyword, and semantic_keyword_matching without an embedding model
// or keyword rules (complexity_router/config.py), so without these a save fails as a raw
diff --git a/ui/litellm-dashboard/src/components/llm_calls/fetch_models.test.tsx b/ui/litellm-dashboard/src/components/llm_calls/fetch_models.test.tsx
index 998924ce36d..3cb15dc3144 100644
--- a/ui/litellm-dashboard/src/components/llm_calls/fetch_models.test.tsx
+++ b/ui/litellm-dashboard/src/components/llm_calls/fetch_models.test.tsx
@@ -52,6 +52,24 @@ describe("fetchAvailableModels", () => {
]);
});
+ it("preserves absent, unknown, empty, and explicit effort capability states", async () => {
+ modelHubCallMock.mockResolvedValue({
+ data: [
+ { model_group: "absent", supports_reasoning: true },
+ { model_group: "unknown", supports_reasoning: true, supported_reasoning_efforts: null },
+ { model_group: "empty", supports_reasoning: true, supported_reasoning_efforts: [] },
+ { model_group: "known", supports_reasoning: true, supported_reasoning_efforts: ["low"] },
+ ],
+ });
+
+ expect(await fetchAvailableModels("token")).toEqual([
+ { model_group: "absent", supports_reasoning: true },
+ { model_group: "empty", supports_reasoning: true, supported_reasoning_efforts: [] },
+ { model_group: "known", supports_reasoning: true, supported_reasoning_efforts: ["low"] },
+ { model_group: "unknown", supports_reasoning: true, supported_reasoning_efforts: null },
+ ]);
+ });
+
it.each([
["an error payload in place of the list", { data: { error: "no access" } }],
["a missing data key", {}],
diff --git a/ui/litellm-dashboard/src/components/llm_calls/fetch_models.tsx b/ui/litellm-dashboard/src/components/llm_calls/fetch_models.tsx
index 1f812cf0377..a25055703bf 100644
--- a/ui/litellm-dashboard/src/components/llm_calls/fetch_models.tsx
+++ b/ui/litellm-dashboard/src/components/llm_calls/fetch_models.tsx
@@ -7,7 +7,7 @@ export interface ModelGroup {
model_group: string;
mode?: string;
supports_reasoning?: boolean;
- supported_reasoning_efforts?: string[];
+ supported_reasoning_efforts?: string[] | null;
}
interface AvailableModel {
@@ -25,7 +25,9 @@ const toModelGroup = (item: AvailableModel): ModelGroup => {
model_group: groupName,
...(item.mode && { mode: item.mode }),
...(item.supports_reasoning === true && { supports_reasoning: true }),
- ...(item.supported_reasoning_efforts && { supported_reasoning_efforts: item.supported_reasoning_efforts }),
+ ...(item.supported_reasoning_efforts !== undefined && {
+ supported_reasoning_efforts: item.supported_reasoning_efforts,
+ }),
};
};
diff --git a/ui/litellm-dashboard/src/components/networking.test.ts b/ui/litellm-dashboard/src/components/networking.test.ts
index 71296bc1dfd..335a5f8b816 100644
--- a/ui/litellm-dashboard/src/components/networking.test.ts
+++ b/ui/litellm-dashboard/src/components/networking.test.ts
@@ -617,6 +617,15 @@ describe("buildModelGroupTestRequest", () => {
expect(path).toBe("/v1/embeddings");
expect(body).toEqual({ model: "text-embedding-3-small", input: "test from litellm" });
});
+
+ it("adds classifier request parameters to a chat probe", () => {
+ const { body } = Networking.buildModelGroupTestRequest("gpt-5-mini", "chat", { reasoning_effort: "low" });
+ expect(body).toEqual({
+ model: "gpt-5-mini",
+ messages: [{ role: "user", content: "test from litellm" }],
+ reasoning_effort: "low",
+ });
+ });
});
describe("testMCPToolsListRequest auth headers", () => {
diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx
index eaffa2b4802..7e0f6c7e4f5 100644
--- a/ui/litellm-dashboard/src/components/networking.tsx
+++ b/ui/litellm-dashboard/src/components/networking.tsx
@@ -2053,6 +2053,7 @@ interface UiSpendLogsParams {
max_spend?: number;
exclude_internal_health_checks?: boolean;
group_by_session?: boolean;
+ session_cursor?: string;
}
interface UiSpendLogsCallOptions {
@@ -2381,20 +2382,22 @@ export type ModelGroupConnectionResult = { status: "success" } | { status: "erro
export const buildModelGroupTestRequest = (
modelGroup: string,
mode: "chat" | "embedding",
+ requestParams: Record = {},
): { path: string; body: Record } =>
mode === "embedding"
? { path: "/v1/embeddings", body: { model: modelGroup, input: "test from litellm" } }
: {
path: "/v1/chat/completions",
- body: { model: modelGroup, messages: [{ role: "user", content: "test from litellm" }] },
+ body: { ...requestParams, model: modelGroup, messages: [{ role: "user", content: "test from litellm" }] },
};
export const testModelGroupConnection = async (
accessToken: string,
modelGroup: string,
mode: "chat" | "embedding",
+ requestParams?: Record,
): Promise => {
- const { path, body } = buildModelGroupTestRequest(modelGroup, mode);
+ const { path, body } = buildModelGroupTestRequest(modelGroup, mode, requestParams);
try {
await apiClient.post(path, { accessToken, body });
return { status: "success" };
diff --git a/ui/litellm-dashboard/src/components/organisms/create_key_button.integration.test.tsx b/ui/litellm-dashboard/src/components/organisms/create_key_button.integration.test.tsx
index 8630be8548f..045dcfa3ceb 100644
--- a/ui/litellm-dashboard/src/components/organisms/create_key_button.integration.test.tsx
+++ b/ui/litellm-dashboard/src/components/organisms/create_key_button.integration.test.tsx
@@ -750,6 +750,19 @@ describe("CreateKey", () => {
expect((await createdPayload()).organization_id).toBe("org-1");
});
+
+ it("drops organization_id when the chosen organization is cleared again", async () => {
+ state.organizations = [{ organization_id: "org-1", organization_alias: "Engineering" }];
+ await openModal();
+ await nameTheKey();
+
+ await userEvent.click(await screen.findByLabelText("Organization"));
+ await userEvent.click(await screen.findByRole("option", { name: /Engineering/ }));
+ await userEvent.click(await screen.findByRole("button", { name: "Clear" }));
+ await submit();
+
+ expect((await createdPayload()).organization_id).toBeUndefined();
+ });
});
describe("policy and prompt fields", () => {
diff --git a/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx b/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx
index d38a8995c1b..5749541dcea 100644
--- a/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx
+++ b/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx
@@ -588,7 +588,7 @@ const CreateKey: React.FC = ({ team, teams, data, addKey, autoOp
};
const changeOrganization = (write: FieldWrite) => (orgId: string) => {
- write(orgId);
+ write(orgId || undefined);
setSelectedOrganizationId(orgId || null);
// Clear team and project when org changes
setSelectedCreateKeyTeam(null);
diff --git a/ui/litellm-dashboard/src/components/templates/key_edit_view.test.tsx b/ui/litellm-dashboard/src/components/templates/key_edit_view.test.tsx
index 97b09e00808..10b1983b54b 100644
--- a/ui/litellm-dashboard/src/components/templates/key_edit_view.test.tsx
+++ b/ui/litellm-dashboard/src/components/templates/key_edit_view.test.tsx
@@ -1457,6 +1457,32 @@ describe("KeyEditView", () => {
expect(screen.getByLabelText("Organization")).toHaveValue("Engineering");
});
});
+
+ it("submits organization_id as null after the organization is cleared", async () => {
+ const onSubmit = vi.fn().mockResolvedValue(undefined);
+ renderWithProviders(
+ {}}
+ onSubmit={onSubmit}
+ accessToken=""
+ userID=""
+ userRole="Admin"
+ premiumUser={false}
+ />,
+ );
+
+ await waitFor(() => {
+ expect(screen.getByLabelText("Organization")).toHaveValue("Engineering");
+ });
+ await userEvent.click(screen.getByRole("button", { name: "Clear" }));
+ await userEvent.click(screen.getByRole("button", { name: /save changes/i }));
+
+ await waitFor(() => {
+ expect(onSubmit).toHaveBeenCalledWith(expect.objectContaining({ organization_id: null }));
+ });
+ expect(JSON.parse(JSON.stringify(onSubmit.mock.calls[0][0]))).toHaveProperty("organization_id", null);
+ });
});
describe("models dropdown team gating", () => {
diff --git a/ui/litellm-dashboard/src/components/templates/key_edit_view.tsx b/ui/litellm-dashboard/src/components/templates/key_edit_view.tsx
index df1af2ca8e9..3e772fd0e9b 100644
--- a/ui/litellm-dashboard/src/components/templates/key_edit_view.tsx
+++ b/ui/litellm-dashboard/src/components/templates/key_edit_view.tsx
@@ -303,8 +303,8 @@ export function KeyEditView({
}
};
- const handleOrganizationChange = (setField: (value: string | undefined) => void, orgId: string | undefined) => {
- setField(orgId);
+ const handleOrganizationChange = (setField: (value: string | null) => void, orgId: string | undefined) => {
+ setField(orgId || null);
setSelectedOrganizationId(orgId || null);
form.setValue("team_id", undefined);
};
diff --git a/ui/litellm-dashboard/src/components/view_logs/LogsTableToolbar.tsx b/ui/litellm-dashboard/src/components/view_logs/LogsTableToolbar.tsx
index cb7836d603f..60657872ccc 100644
--- a/ui/litellm-dashboard/src/components/view_logs/LogsTableToolbar.tsx
+++ b/ui/litellm-dashboard/src/components/view_logs/LogsTableToolbar.tsx
@@ -92,7 +92,10 @@ export function LogsTableToolbar({
diff --git a/ui/litellm-dashboard/src/components/view_logs/RequestLogsPanel.test.tsx b/ui/litellm-dashboard/src/components/view_logs/RequestLogsPanel.test.tsx
index c368a43dd1e..93066c106ee 100644
--- a/ui/litellm-dashboard/src/components/view_logs/RequestLogsPanel.test.tsx
+++ b/ui/litellm-dashboard/src/components/view_logs/RequestLogsPanel.test.tsx
@@ -139,19 +139,20 @@ describe("RequestLogsPanel", () => {
});
describe("server-grouped session pagination (#38060)", () => {
- it("requests session-grouped pages of 10 rows by default", async () => {
+ it("requests session-grouped pages of 10 rows by default without a cursor", async () => {
renderPanel();
await waitFor(() => expect(uiSpendLogsCall).toHaveBeenCalled());
expect(lastCall()?.params?.group_by_session).toBe(true);
+ expect(lastCall()?.params?.session_cursor).toBeUndefined();
expect(lastCall()?.page_size).toBe(10);
});
it("renders every row the server returns without client-side collapsing", async () => {
respondWith([
- logEntry({ request_id: "req-a", call_type: "acompletion", session_id: "sess-1", session_total_count: 3 }),
- logEntry({ request_id: "req-b", call_type: "acompletion", session_id: "sess-1", session_total_count: 3 }),
- logEntry({ request_id: "req-c", call_type: "acompletion", session_id: "sess-1", session_total_count: 3 }),
+ logEntry({ request_id: "req-a", session_id: "sess-1", session_total_count: 3 }),
+ logEntry({ request_id: "req-b", session_id: "sess-1", session_total_count: 3 }),
+ logEntry({ request_id: "req-c", session_id: "sess-1", session_total_count: 3 }),
]);
renderPanel();
@@ -177,6 +178,138 @@ describe("RequestLogsPanel", () => {
expect(within(row("req-llm") as HTMLElement).getByText("3")).toBeInTheDocument();
});
+ it("passes the server keyset cursor when navigating to the next page", async () => {
+ const firstPage = Array.from({ length: 50 }, (_, index) => logEntry({ request_id: `req-${index}` }));
+ vi.mocked(uiSpendLogsCall).mockResolvedValue({
+ data: firstPage,
+ total: 80,
+ page: 1,
+ page_size: 50,
+ total_pages: 2,
+ next_session_cursor: "2026-07-07 09:50:13|key-1|sess-1",
+ has_more: true,
+ });
+ renderPanel();
+
+ await waitFor(() => expect(row("req-0")).not.toBeNull());
+ fireEvent.click(screen.getByTestId("pagination-next"));
+
+ await waitFor(() => {
+ const call = lastCall();
+ expect(call?.params?.session_cursor).toBe("2026-07-07 09:50:13|key-1|sess-1");
+ expect(call?.page).toBe(2);
+ });
+ });
+
+ it("drops the cursor and returns to the first page when a filter changes", async () => {
+ const firstPage = Array.from({ length: 50 }, (_, index) => logEntry({ request_id: `req-${index}` }));
+ vi.mocked(uiSpendLogsCall).mockResolvedValue({
+ data: firstPage,
+ total: 80,
+ page: 1,
+ page_size: 50,
+ total_pages: 2,
+ next_session_cursor: "2026-07-07 09:50:13|key-1|sess-1",
+ has_more: true,
+ });
+ renderPanel();
+
+ await waitFor(() => expect(row("req-0")).not.toBeNull());
+ fireEvent.click(screen.getByTestId("pagination-next"));
+ await waitFor(() => expect(lastCall()?.page).toBe(2));
+
+ fireEvent.change(screen.getByTestId("datatable-search"), { target: { value: "req-elsewhere" } });
+
+ await waitFor(() => {
+ const call = lastCall();
+ expect(call?.page).toBe(1);
+ expect(call?.params?.session_cursor).toBeUndefined();
+ });
+ });
+
+ it("ignores another next click while the next page is still fetching", async () => {
+ const firstPage = Array.from({ length: 50 }, (_, index) => logEntry({ request_id: `req-${index}` }));
+ const firstResponse = {
+ data: firstPage,
+ total: 150,
+ page: 1,
+ page_size: 50,
+ total_pages: 3,
+ next_session_cursor: "2026-07-07 09:50:13|key-1|sess-1",
+ has_more: true,
+ };
+ vi.mocked(uiSpendLogsCall)
+ .mockResolvedValueOnce(firstResponse)
+ .mockImplementation(() => new Promise(() => {}));
+ renderPanel();
+
+ await waitFor(() => expect(row("req-0")).not.toBeNull());
+ fireEvent.click(screen.getByTestId("pagination-next"));
+ await waitFor(() => expect(lastCall()?.page).toBe(2));
+
+ fireEvent.click(screen.getByTestId("pagination-next"));
+
+ expect(lastCall()?.page).toBe(2);
+ expect(vi.mocked(uiSpendLogsCall).mock.calls.filter(([options]) => options.page === 3)).toHaveLength(0);
+ });
+
+ it("still moves to the next page while a live-tail refetch of the current page is in flight", async () => {
+ const firstPage = Array.from({ length: 50 }, (_, index) => logEntry({ request_id: `req-${index}` }));
+ const firstResponse = {
+ data: firstPage,
+ total: 150,
+ page: 1,
+ page_size: 50,
+ total_pages: 3,
+ next_session_cursor: "2026-07-07 09:50:13|key-1|sess-1",
+ has_more: true,
+ };
+ vi.mocked(uiSpendLogsCall)
+ .mockResolvedValueOnce(firstResponse)
+ .mockImplementation(() => new Promise(() => {}));
+ renderPanel();
+
+ await waitFor(() => expect(row("req-0")).not.toBeNull());
+ void testQueryClient.refetchQueries({ queryKey: ["logs", "table"] });
+ await waitFor(() => expect(uiSpendLogsCall).toHaveBeenCalledTimes(2));
+
+ fireEvent.click(screen.getByTestId("pagination-next"));
+
+ await waitFor(() => {
+ const call = lastCall();
+ expect(call?.page).toBe(2);
+ expect(call?.params?.session_cursor).toBe("2026-07-07 09:50:13|key-1|sess-1");
+ });
+ });
+
+ it("drops the cursor and returns to the first page when Custom Range is toggled", async () => {
+ const user = userEvent.setup();
+ const firstPage = Array.from({ length: 50 }, (_, index) => logEntry({ request_id: `req-${index}` }));
+ vi.mocked(uiSpendLogsCall).mockResolvedValue({
+ data: firstPage,
+ total: 80,
+ page: 1,
+ page_size: 50,
+ total_pages: 2,
+ next_session_cursor: "2026-07-07 09:50:13|key-1|sess-1",
+ has_more: true,
+ });
+ renderPanel();
+
+ await waitFor(() => expect(row("req-0")).not.toBeNull());
+ fireEvent.click(screen.getByTestId("pagination-next"));
+ await waitFor(() => expect(lastCall()?.page).toBe(2));
+
+ await user.click(screen.getByRole("button", { name: /Last 24 Hours/i }));
+ await user.click(await screen.findByRole("button", { name: "Custom Range" }));
+
+ await waitFor(() => {
+ const call = lastCall();
+ expect(call?.page).toBe(1);
+ expect(call?.params?.session_cursor).toBeUndefined();
+ });
+ });
+
it("leaves single-call rows untouched", async () => {
respondWith([
logEntry({ request_id: "req-solo-a", session_id: "sess-a", session_total_count: 1 }),
@@ -395,9 +528,7 @@ describe("RequestLogsPanel", () => {
});
it("opens a deep-linked multi-call session log in session mode", async () => {
- respondWith([
- logEntry({ request_id: "req-llm", call_type: "acompletion", session_id: "sess-1", session_total_count: 3 }),
- ]);
+ respondWith([logEntry({ request_id: "req-llm", session_id: "sess-1", session_total_count: 3 })]);
renderPanel("?log_id=req-llm");
await waitFor(() => {
@@ -410,8 +541,8 @@ describe("RequestLogsPanel", () => {
it("clicking a multi-call session's row writes ?session_id= alongside ?log_id=", async () => {
const user = userEvent.setup();
respondWith([
- logEntry({ request_id: "req-llm", call_type: "acompletion", session_id: "sess-1", session_total_count: 3 }),
- logEntry({ request_id: "req-llm-2", call_type: "acompletion", session_id: "sess-1", session_total_count: 3 }),
+ logEntry({ request_id: "req-llm", session_id: "sess-1", session_total_count: 3 }),
+ logEntry({ request_id: "req-llm-2", session_id: "sess-1", session_total_count: 3 }),
]);
renderPanel();
@@ -426,7 +557,7 @@ describe("RequestLogsPanel", () => {
it("selecting another log while a session view is open keeps the session open", async () => {
const user = userEvent.setup();
respondWith([
- logEntry({ request_id: "req-llm", call_type: "acompletion", session_id: "sess-1", session_total_count: 3 }),
+ logEntry({ request_id: "req-llm", session_id: "sess-1", session_total_count: 3 }),
logEntry({ request_id: "req-unenriched" }),
]);
renderPanel("?log_id=req-llm");
diff --git a/ui/litellm-dashboard/src/components/view_logs/RequestLogsPanel.tsx b/ui/litellm-dashboard/src/components/view_logs/RequestLogsPanel.tsx
index 2ebabf64ab0..96cf2bd5dde 100644
--- a/ui/litellm-dashboard/src/components/view_logs/RequestLogsPanel.tsx
+++ b/ui/litellm-dashboard/src/components/view_logs/RequestLogsPanel.tsx
@@ -39,6 +39,7 @@ export default function RequestLogsPanel({ accessToken, token, userRole, userID,
const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: PAGE_SIZE });
const [sorting, setSorting] = useState(DEFAULT_LOGS_SORTING);
const [columnFilters, setColumnFilters] = useState([]);
+ const [sessionCursors, setSessionCursors] = useState>({});
const [startTime, setStartTime] = useState(moment().subtract(24, "hours").format("YYYY-MM-DDTHH:mm"));
const [endTime, setEndTime] = useState(moment().format("YYYY-MM-DDTHH:mm"));
@@ -74,7 +75,7 @@ export default function RequestLogsPanel({ accessToken, token, userRole, userID,
sessionStorage.setItem("excludeInternalHealthChecks", JSON.stringify(excludeInternalHealthChecks));
}, [excludeInternalHealthChecks]);
- const { logsQuery, filteredLogs, allTeams } = useLogFilterLogic({
+ const { logsQuery, filteredLogs, allTeams, usesSessionCursor } = useLogFilterLogic({
accessToken,
token,
userRole,
@@ -88,6 +89,7 @@ export default function RequestLogsPanel({ accessToken, token, userRole, userID,
pagination,
isCustomDate,
sorting,
+ sessionCursors,
});
// Follow the table's own last fetch so a live-tail refresh carries the filter
@@ -163,23 +165,52 @@ export default function RequestLogsPanel({ accessToken, token, userRole, userID,
const others = previous.filter((filter) => filter.id !== LOG_FILTER_IDS.REQUEST_ID);
return value === "" ? others : [...others, { id: LOG_FILTER_IDS.REQUEST_ID, value }];
});
+ setSessionCursors({});
setPagination((previous) => ({ ...previous, pageIndex: 0 }));
}, []);
const handleSortingChange = useCallback>((updaterOrValue) => {
setSorting(updaterOrValue);
+ setSessionCursors({});
setPagination((previous) => ({ ...previous, pageIndex: 0 }));
}, []);
const handleColumnFiltersChange = useCallback>((updaterOrValue) => {
setColumnFilters(updaterOrValue);
+ setSessionCursors({});
setPagination((previous) => ({ ...previous, pageIndex: 0 }));
}, []);
const resetToFirstPage = useCallback(() => {
+ setSessionCursors({});
setPagination((previous) => ({ ...previous, pageIndex: 0 }));
}, []);
+ const handlePaginationChange = useCallback>(
+ (updaterOrValue) => {
+ const requested = typeof updaterOrValue === "function" ? updaterOrValue(pagination) : updaterOrValue;
+ if (!usesSessionCursor) {
+ setPagination(requested);
+ return;
+ }
+ if (requested.pageSize !== pagination.pageSize) {
+ setSessionCursors({});
+ setPagination({ ...requested, pageIndex: 0 });
+ return;
+ }
+ if (requested.pageIndex <= pagination.pageIndex) {
+ setPagination(requested);
+ return;
+ }
+ const nextCursor = filteredLogs.next_session_cursor;
+ if (!nextCursor || logsQuery.isPlaceholderData) return;
+ const nextPageIndex = pagination.pageIndex + 1;
+ setSessionCursors((previous) => ({ ...previous, [nextPageIndex]: nextCursor }));
+ setPagination({ ...requested, pageIndex: nextPageIndex });
+ },
+ [usesSessionCursor, pagination, filteredLogs.next_session_cursor, logsQuery.isPlaceholderData],
+ );
+
const handleExcludeInternalHealthChecksChange = useCallback(
(value: boolean) => {
setExcludeInternalHealthChecks(value);
@@ -256,7 +287,7 @@ export default function RequestLogsPanel({ accessToken, token, userRole, userID,
isLoading={logsQuery.isLoading}
isRefreshing={logsQuery.isFetching}
pagination={pagination}
- onPaginationChange={setPagination}
+ onPaginationChange={handlePaginationChange}
sorting={sorting}
onSortingChange={handleSortingChange}
columnFilters={columnFilters}
diff --git a/ui/litellm-dashboard/src/components/view_logs/RequestLogsTableColumns.test.tsx b/ui/litellm-dashboard/src/components/view_logs/RequestLogsTableColumns.test.tsx
index ce59c62f1c4..e3bacc0908a 100644
--- a/ui/litellm-dashboard/src/components/view_logs/RequestLogsTableColumns.test.tsx
+++ b/ui/litellm-dashboard/src/components/view_logs/RequestLogsTableColumns.test.tsx
@@ -75,6 +75,68 @@ describe("Cost column", () => {
});
});
+describe("Type column", () => {
+ it("shows the conversation badge and composition even when an MCP call represents the conversation", async () => {
+ const user = userEvent.setup();
+ const mcpRepresentative = {
+ request_id: "req-mcp-rep",
+ call_type: "call_mcp_tool",
+ session_id: "sess-edge",
+ session_total_count: 3,
+ session_llm_count: 2,
+ mcp_tool_call_count: 1,
+ session_agent_count: 0,
+ };
+ renderRows([logEntry(mcpRepresentative)]);
+
+ expect(screen.queryByText("MCP")).not.toBeInTheDocument();
+ await user.hover(screen.getByText("3"));
+ expect(await screen.findByText("2 LLM • 1 MCP")).toBeInTheDocument();
+ });
+
+ it("keeps the plain MCP badge for a single MCP call", () => {
+ renderRows([logEntry({ request_id: "req-mcp-solo", call_type: "call_mcp_tool", session_total_count: 1 })]);
+
+ expect(screen.getByText("MCP")).toBeInTheDocument();
+ });
+});
+
+describe("Model column", () => {
+ it("lists every model used across a conversation, not only the representative call's model", () => {
+ const conversationCall: Partial = {
+ request_id: "req-session",
+ model: "gpt-5.6",
+ session_id: "sess-1",
+ session_total_count: 3,
+ session_models: ["claude-sonnet-5", "gpt-5.6"],
+ };
+ renderRows([logEntry(conversationCall)]);
+
+ expect(screen.getByText("claude-sonnet-5, gpt-5.6")).toBeInTheDocument();
+ expect(screen.queryByText("gpt-5.6")).not.toBeInTheDocument();
+ });
+
+ it("marks a conversation whose model list was capped by the server", () => {
+ const cappedCall = {
+ request_id: "req-capped",
+ model: "gpt-5.6",
+ session_id: "sess-2",
+ session_total_count: 30,
+ session_models: ["claude-sonnet-5", "gpt-5.6"],
+ session_models_truncated: true,
+ };
+ renderRows([logEntry(cappedCall)]);
+
+ expect(screen.getByText("claude-sonnet-5, gpt-5.6, ...")).toBeInTheDocument();
+ });
+
+ it("keeps a single call's own model", () => {
+ renderRows([logEntry({ request_id: "req-single", model: "gpt-5.6" })]);
+
+ expect(screen.getByText("gpt-5.6")).toBeInTheDocument();
+ });
+});
+
describe("row action cells", () => {
it("reports the key hash through the injected dependency rather than a row field", async () => {
const user = userEvent.setup();
diff --git a/ui/litellm-dashboard/src/components/view_logs/RequestLogsTableColumns.tsx b/ui/litellm-dashboard/src/components/view_logs/RequestLogsTableColumns.tsx
index b9058d02a6b..8db0b106851 100644
--- a/ui/litellm-dashboard/src/components/view_logs/RequestLogsTableColumns.tsx
+++ b/ui/litellm-dashboard/src/components/view_logs/RequestLogsTableColumns.tsx
@@ -63,9 +63,11 @@ export const getRequestLogsTableColumns = ({
const sessionAgentCount = log.session_agent_count ?? (isAgent ? sessionCount : 0);
const sessionMcpCount = log.mcp_tool_call_count ?? (isMcp ? sessionCount : 0);
- if (isMcp) return ;
- if (isAgent && sessionCount <= 1) return ;
- if (sessionCount <= 1) return ;
+ if (sessionCount <= 1) {
+ if (isMcp) return ;
+ if (isAgent) return ;
+ return ;
+ }
const sessionTypeBadge = (
@@ -224,10 +226,13 @@ export const getRequestLogsTableColumns = ({
cell: ({ row }) => {
const log = row.original;
const provider = log.custom_llm_provider;
- const modelName = log.model ?? "";
+ const sessionModels = log.session_models ?? [];
+ const modelNames = sessionModels.length > 0 ? sessionModels : [log.model ?? ""];
+ const modelLabel = log.session_models_truncated ? `${modelNames.join(", ")}, ...` : modelNames.join(", ");
+ const isSingleModel = modelNames.length === 1;
return (
- {provider && (
+ {provider && isSingleModel && (

)}
-
{modelName}} />
+
+ {modelLabel}
+
+ }
+ />
);
},
diff --git a/ui/litellm-dashboard/src/components/view_logs/columns.tsx b/ui/litellm-dashboard/src/components/view_logs/columns.tsx
index 2f3a3681352..21e09faf454 100644
--- a/ui/litellm-dashboard/src/components/view_logs/columns.tsx
+++ b/ui/litellm-dashboard/src/components/view_logs/columns.tsx
@@ -47,4 +47,6 @@ export type LogEntry = {
mcp_tool_call_spend?: number;
session_llm_count?: number;
session_agent_count?: number;
+ session_models?: string[];
+ session_models_truncated?: boolean;
};
diff --git a/ui/litellm-dashboard/src/components/view_logs/log_filter_logic.tsx b/ui/litellm-dashboard/src/components/view_logs/log_filter_logic.tsx
index acd5be06593..90f0f0a60f1 100644
--- a/ui/litellm-dashboard/src/components/view_logs/log_filter_logic.tsx
+++ b/ui/litellm-dashboard/src/components/view_logs/log_filter_logic.tsx
@@ -15,6 +15,8 @@ export interface PaginatedResponse {
page_size: number;
total_pages: number;
total_is_capped?: boolean;
+ next_session_cursor?: string | null;
+ has_more?: boolean;
}
export const LOG_FILTER_IDS = {
@@ -109,6 +111,7 @@ export function useLogFilterLogic({
pagination,
isCustomDate,
sorting,
+ sessionCursors = {},
}: {
accessToken: string | null;
token: string | null;
@@ -123,11 +126,14 @@ export function useLogFilterLogic({
pagination: PaginationState;
isCustomDate: boolean;
sorting: SortingState;
+ sessionCursors?: Record;
}) {
const pageSize = pagination.pageSize || defaultPageSize;
const activeSort = sorting[0] ?? DEFAULT_LOGS_SORTING[0];
const sortBy: LogsSortField = isSortField(activeSort.id) ? activeSort.id : "startTime";
const sortOrder: "asc" | "desc" = activeSort.desc ? "desc" : "asc";
+ const usesSessionCursor = sortBy === "startTime";
+ const sessionCursor = usesSessionCursor ? sessionCursors[pagination.pageIndex] : undefined;
const logsQueryOptions: UseQueryOptions = {
queryKey: [
@@ -142,6 +148,7 @@ export function useLogFilterLogic({
sortBy,
sortOrder,
excludeInternalHealthChecks,
+ sessionCursor,
],
queryFn: async () => {
if (!accessToken || !token || !userRole || !userID) {
@@ -182,6 +189,7 @@ export function useLogFilterLogic({
sort_order: sortOrder,
exclude_internal_health_checks: excludeInternalHealthChecks,
group_by_session: true,
+ session_cursor: sessionCursor,
},
});
},
@@ -219,5 +227,6 @@ export function useLogFilterLogic({
logsQuery,
filteredLogs,
allTeams,
+ usesSessionCursor,
};
}
diff --git a/ui/litellm-dashboard/src/lib/http/schema.d.ts b/ui/litellm-dashboard/src/lib/http/schema.d.ts
index b0f8e618645..098b43f6433 100644
--- a/ui/litellm-dashboard/src/lib/http/schema.d.ts
+++ b/ui/litellm-dashboard/src/lib/http/schema.d.ts
@@ -25211,6 +25211,11 @@ export interface components {
* @description Model name (from the router's model_list) to call for classification
*/
model: string;
+ /**
+ * Reasoning Effort
+ * @description Reasoning effort override for classifier calls. Leave unset to use the classifier deployment or provider default.
+ */
+ reasoning_effort?: ("none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max") | null;
/**
* System Prompt
* @description Replaces the built-in complexity rubric as the classifier's entire system role. When set, neither the default rubric nor the context-window closing line is appended, so the prompt owns the whole taxonomy and the tier names SIMPLE/MEDIUM/COMPLEX/REASONING become whatever buckets it defines: a prompt that classifies data sensitivity routes on that instead of on difficulty. Two consequences of full replacement. The default rubric's closing paragraph is the classifier's prompt-injection defense, telling it that the caller's quoted system prompt and prior turns are material to judge and never instructions; a replacement that omits it lets a caller ask for a tier and get it. And the heuristic fallback still scores complexity, so a router on some other taxonomy wants classifier_fallback='default_model'. Leave unset for the built-in rubric. Only applies when classifier_type is 'llm'.
@@ -56858,6 +56863,8 @@ export interface operations {
exclude_internal_health_checks?: boolean;
/** @description Paginate over sessions instead of raw logs: one representative row per session, total counts sessions */
group_by_session?: boolean;
+ /** @description Keyset cursor '|