fix(router): redact cookies and preserve legacy classifier logs

This commit is contained in:
moe-berri 2026-09-10 13:12:04 -07:00
parent 83b1e9e0d7
commit c2176c786e
6 changed files with 80 additions and 14 deletions

View file

@ -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".

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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()} />);

View file

@ -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}
/>