mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-14 23:21:35 +00:00
fix(router): redact cookies and preserve legacy classifier logs
This commit is contained in:
parent
83b1e9e0d7
commit
c2176c786e
6 changed files with 80 additions and 14 deletions
|
|
@ -15,6 +15,7 @@ _DEFAULT_SENSITIVE_PATTERNS: Final = frozenset(
|
|||
"token",
|
||||
"auth",
|
||||
"authorization",
|
||||
"cookie",
|
||||
"credential",
|
||||
# Plural form: Vertex uses ``vertex_credentials``; segment-exact
|
||||
# matching otherwise misses it because "credential" != "credentials".
|
||||
|
|
|
|||
|
|
@ -33,6 +33,19 @@ def test_originating_snapshot_masks_nested_credentials_without_altering_source()
|
|||
assert body["metadata"]["nested"][0]["Authorization"] == "Bearer secret"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("header", ["Cookie", "cookie", "COOKIE", "sEt-CoOkIe"])
|
||||
def test_originating_snapshot_redacts_cookie_headers_shared_with_caller_metadata(header: str) -> None:
|
||||
headers: Final = {header: "session=synthetic-session-credential", "content-type": "application/json"}
|
||||
body: Final = {"messages": [{"role": "user", "content": "hello"}], "metadata": {"headers": headers}}
|
||||
snapshot: Final = masked_originating_request({"proxy_server_request": {"body": body, "headers": headers}})
|
||||
assert snapshot == {
|
||||
"messages": [{"role": "user", "content": "hello"}],
|
||||
"metadata": {"headers": {header: "REDACTED", "content-type": "application/json"}},
|
||||
}
|
||||
assert headers[header] == "session=synthetic-session-credential"
|
||||
assert body["metadata"]["headers"][header] == "session=synthetic-session-credential"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", [None, "not-json", [], {"messages": object()}])
|
||||
def test_invalid_provider_payload_is_not_reported_as_captured(value: object) -> None:
|
||||
assert classifier_input_snapshot(value) is None
|
||||
|
|
|
|||
|
|
@ -358,7 +358,12 @@ def test_redact_credentials_in_payload_leaves_no_fragment_of_the_secret():
|
|||
"azure_ad_token": fake_token,
|
||||
"aws_secret_access_key": "fake-aws-secret-0000",
|
||||
"vertex_credentials": {"private_key": "fake-pem"},
|
||||
"extra_headers": {"Authorization": "Bearer fake-bearer-0000", "x-request-id": "abc123"},
|
||||
"extra_headers": {
|
||||
"Authorization": "Bearer fake-bearer-0000",
|
||||
"Cookie": "session=fake-session",
|
||||
"Set-Cookie": "session=fake-session; HttpOnly",
|
||||
"x-request-id": "abc123",
|
||||
},
|
||||
"model": "gpt-4o-mini",
|
||||
"max_tokens": 17,
|
||||
"temperature": 0.25,
|
||||
|
|
@ -373,6 +378,8 @@ def test_redact_credentials_in_payload_leaves_no_fragment_of_the_secret():
|
|||
assert "fake-bearer-0000" not in str(result)
|
||||
assert result["api_key"] == "REDACTED"
|
||||
assert result["extra_headers"]["Authorization"] == "REDACTED"
|
||||
assert result["extra_headers"]["Cookie"] == "REDACTED"
|
||||
assert result["extra_headers"]["Set-Cookie"] == "REDACTED"
|
||||
assert result["extra_headers"]["x-request-id"] == "abc123"
|
||||
assert result["model"] == "gpt-4o-mini"
|
||||
assert result["max_tokens"] == 17
|
||||
|
|
|
|||
|
|
@ -2325,11 +2325,6 @@
|
|||
"count": 4
|
||||
}
|
||||
},
|
||||
"src/components/view_logs/LogDetailsDrawer/LogDetailContent.tsx": {
|
||||
"no-nested-ternary": {
|
||||
"count": 3
|
||||
}
|
||||
},
|
||||
"src/components/view_logs/LogDetailsDrawer/LogDetailsDrawer.tsx": {
|
||||
"no-nested-ternary": {
|
||||
"count": 2
|
||||
|
|
|
|||
|
|
@ -272,6 +272,57 @@ describe("LogDetailContent", () => {
|
|||
expect(screen.queryByRole("tab", { name: "Request" })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it.each(["object", "serialized", "messages only", "null captures"])(
|
||||
"preserves request inspection and copying for classifier logs with %s data",
|
||||
async (shape) => {
|
||||
const user = userEvent.setup();
|
||||
const messages = [{ role: "user", content: "legacy classifier prompt" }];
|
||||
const request = {
|
||||
messages,
|
||||
temperature: 0.5,
|
||||
...(shape === "null captures" ? { classifier_input: null, originating_request_masked: null } : {}),
|
||||
};
|
||||
const storedRequest = shape === "serialized" ? JSON.stringify(request) : request;
|
||||
const logEntry: Partial<LogEntry> = {
|
||||
call_type: "acompletion",
|
||||
messages,
|
||||
proxy_server_request: shape === "messages only" ? undefined : storedRequest,
|
||||
metadata: { status: "success", internal_call_origin: "autorouter_classifier" },
|
||||
};
|
||||
render(<LogDetailContent logEntry={createLogEntry(logEntry)} />);
|
||||
|
||||
expect(screen.getByText("legacy classifier prompt")).toBeInTheDocument();
|
||||
expect(screen.queryByRole("region", { name: "Classifier input" })).not.toBeInTheDocument();
|
||||
await user.click(screen.getByRole("tab", { name: "JSON", exact: true }));
|
||||
await user.click(screen.getByRole("button", { name: "Copy JSON", exact: true }));
|
||||
expect(await navigator.clipboard.readText()).toBe(
|
||||
JSON.stringify(shape === "messages only" ? messages : request, null, 2),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it.each(["classifier_input", "originating_request_masked"])(
|
||||
"shows partial classifier audits when only %s is captured",
|
||||
(field) => {
|
||||
render(
|
||||
<LogDetailContent
|
||||
logEntry={createLogEntry({
|
||||
call_type: "acompletion",
|
||||
proxy_server_request: JSON.stringify({
|
||||
[field]: { messages: [{ role: "user", content: "captured prompt" }] },
|
||||
}),
|
||||
metadata: { status: "success", internal_call_origin: "autorouter_classifier" },
|
||||
})}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByRole("region", { name: "Classifier input" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("region", { name: "Originating request, credentials masked" })).toBeInTheDocument();
|
||||
expect(screen.getByText("Not captured or message logging disabled")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Request & Response")).not.toBeInTheDocument();
|
||||
},
|
||||
);
|
||||
|
||||
it("should display Request and Response tabs when JSON view is selected", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<LogDetailContent logEntry={createLogEntry()} />);
|
||||
|
|
@ -73,6 +73,9 @@ export function LogDetailContent({ logEntry, isLoadingDetails = false, accessTok
|
|||
const isClassifier =
|
||||
metadata.internal_call_origin === AUTOROUTER_CLASSIFIER_ORIGIN &&
|
||||
(logEntry.call_type === "completion" || logEntry.call_type === "acompletion");
|
||||
const rawRequest = formatData(logEntry.proxy_server_request || logEntry.messages);
|
||||
const hasClassifierAudit =
|
||||
isClassifier && (rawRequest?.classifier_input != null || rawRequest?.originating_request_masked != null);
|
||||
|
||||
const hasMessages = checkHasMessages(logEntry.messages);
|
||||
const hasResponse = checkHasResponse(logEntry.response);
|
||||
|
|
@ -93,10 +96,6 @@ export function LogDetailContent({ logEntry, isLoadingDetails = false, accessTok
|
|||
// Vector store data
|
||||
const hasVectorStoreData = checkHasVectorStoreData(metadata);
|
||||
|
||||
const getRawRequest = () => {
|
||||
return formatData(logEntry.proxy_server_request || logEntry.messages);
|
||||
};
|
||||
|
||||
const getFormattedResponse = () => {
|
||||
if (hasError && errorInfo) {
|
||||
return {
|
||||
|
|
@ -202,14 +201,14 @@ export function LogDetailContent({ logEntry, isLoadingDetails = false, accessTok
|
|||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{!isLoadingDetails && isClassifier && (
|
||||
<ClassifierAuditView request={getRawRequest()} response={getFormattedResponse()} />
|
||||
{!isLoadingDetails && hasClassifierAudit && (
|
||||
<ClassifierAuditView request={rawRequest} response={getFormattedResponse()} />
|
||||
)}
|
||||
{!isLoadingDetails && !isClassifier && (
|
||||
{!isLoadingDetails && !hasClassifierAudit && (
|
||||
<RequestResponseSection
|
||||
hasResponse={hasResponse}
|
||||
hasError={hasError}
|
||||
getRawRequest={getRawRequest}
|
||||
getRawRequest={() => rawRequest}
|
||||
getFormattedResponse={getFormattedResponse}
|
||||
logEntry={logEntry}
|
||||
/>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue