Skip to content
Merged
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
29 changes: 26 additions & 3 deletions src/actions/active-sessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
} from "@/lib/cache/session-cache";
import { logger } from "@/lib/logger";
import { normalizeRequestSequence } from "@/lib/utils/request-sequence";
import { buildUnifiedSpecialSettings } from "@/lib/utils/special-settings";
import type { ActiveSessionInfo } from "@/types/session";
import type { SpecialSetting } from "@/types/special-settings";
import { summarizeTerminateSessionsBatch } from "./active-sessions-utils";
Expand Down Expand Up @@ -588,7 +589,8 @@ export async function getSessionDetails(
const normalizedSequence = normalizeRequestSequence(requestSequence);
const effectiveSequence = normalizedSequence ?? (requestCount > 0 ? requestCount : undefined);

const { findAdjacentRequestSequences } = await import("@/repository/message");
const { findAdjacentRequestSequences, findMessageRequestAuditBySessionIdAndSequence } =
await import("@/repository/message");
const adjacent =
effectiveSequence == null
? { prevSequence: null, nextSequence: null }
Expand Down Expand Up @@ -618,7 +620,8 @@ export async function getSessionDetails(
clientReqMeta,
upstreamReqMeta,
upstreamResMeta,
specialSettings,
redisSpecialSettings,
requestAudit,
] = await Promise.all([
SessionManager.getSessionRequestBody(sessionId, effectiveSequence),
SessionManager.getSessionMessages(sessionId, effectiveSequence),
Expand All @@ -629,6 +632,9 @@ export async function getSessionDetails(
SessionManager.getSessionUpstreamRequestMeta(sessionId, effectiveSequence),
SessionManager.getSessionUpstreamResponseMeta(sessionId, effectiveSequence),
SessionManager.getSessionSpecialSettings(sessionId, effectiveSequence),
effectiveSequence
? findMessageRequestAuditBySessionIdAndSequence(sessionId, effectiveSequence)
: Promise.resolve(null),
]);

// 兼容:历史/异常数据可能是 JSON 字符串(前端需要根级对象/数组)
Expand All @@ -646,6 +652,23 @@ export async function getSessionDetails(
statusCode: upstreamResMeta?.statusCode ?? null,
};

const mergedSpecialSettings: SpecialSetting[] = [
...(Array.isArray(redisSpecialSettings) ? (redisSpecialSettings as SpecialSetting[]) : []),
...(Array.isArray(requestAudit?.specialSettings)
? (requestAudit.specialSettings as SpecialSetting[])
: []),
];
const existingSpecialSettings = mergedSpecialSettings.length > 0 ? mergedSpecialSettings : null;

const unifiedSpecialSettings = buildUnifiedSpecialSettings({
existing: existingSpecialSettings,
blockedBy: requestAudit?.blockedBy ?? null,
blockedReason: requestAudit?.blockedReason ?? null,
statusCode: requestAudit?.statusCode ?? null,
cacheTtlApplied: requestAudit?.cacheTtlApplied ?? null,
context1mApplied: requestAudit?.context1mApplied ?? null,
});

return {
ok: true,
data: {
Expand All @@ -656,7 +679,7 @@ export async function getSessionDetails(
responseHeaders,
requestMeta,
responseMeta,
specialSettings,
specialSettings: unifiedSpecialSettings,
sessionStats,
currentSequence: effectiveSequence ?? null,
prevSequence: adjacent.prevSequence,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ const messages = {
processing: "Processing",
success: "Success",
error: "Error",
specialSettings: {
title: "Special settings",
},
billingDetails: {
title: "Billing details",
},
Expand Down Expand Up @@ -121,6 +124,33 @@ function getBillingAndPerformanceGrid(document: ReturnType<typeof parseHtml>) {
}

describe("error-details-dialog layout", () => {
test("renders special settings section when specialSettings exists", () => {
const html = renderWithIntl(
<ErrorDetailsDialog
externalOpen
statusCode={200}
errorMessage={null}
providerChain={null}
sessionId={null}
specialSettings={[
{
type: "provider_parameter_override",
scope: "provider",
providerId: 1,
providerName: "p",
providerType: "codex",
hit: true,
changed: true,
changes: [{ path: "temperature", before: 1, after: 0.2, changed: true }],
},
]}
/>
);

expect(html).toContain("Special settings");
expect(html).toContain("provider_parameter_override");
});

test("renders billing + performance as two-column grid on md when both present", () => {
const html = renderWithIntl(
<ErrorDetailsDialog
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -234,14 +234,6 @@ export function UsageLogsTable({
setDialogState({ logId: log.id, scrollToRedirect: true })
}
/>
{log.specialSettings && log.specialSettings.length > 0 ? (
<Badge
variant="outline"
className="text-[10px] leading-tight px-1 shrink-0"
>
{t("logs.table.specialSettings")}
</Badge>
) : null}
</div>
</TooltipTrigger>
<TooltipContent>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,7 @@ export function VirtualizedLogsTable({
messagesCount={log.messagesCount}
endpoint={log.endpoint}
billingModelSource={billingModelSource}
specialSettings={log.specialSettings}
inputTokens={log.inputTokens}
outputTokens={log.outputTokens}
cacheCreationInputTokens={log.cacheCreationInputTokens}
Expand Down
126 changes: 126 additions & 0 deletions src/lib/utils/special-settings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { CONTEXT_1M_BETA_HEADER } from "@/lib/special-attributes";
import type { SpecialSetting } from "@/types/special-settings";

type BuildUnifiedSpecialSettingsParams = {
/**
* 已有 specialSettings(通常来自 DB special_settings 或 Session Redis 缓存)
*/
existing?: SpecialSetting[] | null;
/**
* 拦截类型(如 warmup / sensitive_word)
*/
blockedBy?: string | null;
/**
* 拦截原因(通常为 JSON 字符串)
*/
blockedReason?: string | null;
/**
* HTTP 状态码(用于补齐守卫拦截信息)
*/
statusCode?: number | null;
/**
* Cache TTL 实际应用值(用于展示 TTL/标头覆写命中)
*/
cacheTtlApplied?: string | null;
/**
* 1M 上下文是否应用(用于展示 1M 标头覆写命中)
*/
context1mApplied?: boolean | null;
};

function buildSettingKey(setting: SpecialSetting): string {
switch (setting.type) {
case "provider_parameter_override":
return JSON.stringify([
setting.type,
setting.providerId ?? null,
setting.providerType ?? null,
setting.hit,
setting.changed,
[...setting.changes]
.map((change) => [change.path, change.before, change.after, change.changed] as const)
.sort((a, b) => a[0].localeCompare(b[0])),
]);
case "response_fixer":
return JSON.stringify([
setting.type,
setting.hit,
[...setting.fixersApplied]
.map((fixer) => [fixer.fixer, fixer.applied] as const)
.sort((a, b) => a[0].localeCompare(b[0])),
]);
case "guard_intercept":
return JSON.stringify([setting.type, setting.guard, setting.action, setting.statusCode]);
case "anthropic_cache_ttl_header_override":
return JSON.stringify([setting.type, setting.ttl]);
case "anthropic_context_1m_header_override":
return JSON.stringify([setting.type, setting.header, setting.flag]);
default: {
// 兜底:保证即使未来扩展类型也不会导致运行时崩溃
const _exhaustive: never = setting;
return JSON.stringify(_exhaustive);
}
}
}

/**
* 构建“统一特殊设置”展示数据
*
* 目标:把 DB 字段(blockedBy/cacheTtlApplied/context1mApplied)与既有 special_settings 合并,
* 统一在以下位置展示:日志列表/日志详情弹窗/Session 详情页。
*/
export function buildUnifiedSpecialSettings(
params: BuildUnifiedSpecialSettingsParams
): SpecialSetting[] | null {
const base = params.existing ?? [];
const derived: SpecialSetting[] = [];

if (params.blockedBy) {
const guard = params.blockedBy;
const action = guard === "warmup" ? "intercept_response" : "block_request";

derived.push({
type: "guard_intercept",
scope: "guard",
hit: true,
guard,
action,
statusCode: params.statusCode ?? null,
reason: params.blockedReason ?? null,
});
}

if (params.cacheTtlApplied) {
derived.push({
type: "anthropic_cache_ttl_header_override",
scope: "request_header",
hit: true,
ttl: params.cacheTtlApplied,
});
}

if (params.context1mApplied === true) {
derived.push({
type: "anthropic_context_1m_header_override",
scope: "request_header",
hit: true,
header: "anthropic-beta",
flag: CONTEXT_1M_BETA_HEADER,
});
}

if (base.length === 0 && derived.length === 0) {
return null;
}

const seen = new Set<string>();
const result: SpecialSetting[] = [];
for (const item of [...base, ...derived]) {
const key = buildSettingKey(item);
if (seen.has(key)) continue;
seen.add(key);
result.push(item);
}
Comment on lines +116 to +123
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The deduplication logic assumes that if two settings have the same buildSettingKey() result, they are duplicates and should be deduplicated. However, there's a subtle issue with the guard_intercept type deduplication.

Consider this scenario:

  1. existing contains: { type: "guard_intercept", guard: "warmup", action: "intercept_response", statusCode: 200, reason: "original_reason" }
  2. params.blockedBy: "warmup", params.blockedReason: "different_reason", params.statusCode: 200

The buildSettingKey for guard_intercept (line 51-57) uses the reason field in the key, so:

  • existing key: "guard_intercept:warmup:intercept_response:200:original_reason"
  • derived key: "guard_intercept:warmup:intercept_response:200:different_reason"

These have different keys, so both would be included! This violates the intention to deduplicate.

The issue is that the reason field (which contains JSON string like {"reason": "..."}) is being used in the deduplication key. If the same guard intercepts a request multiple times with different reasons, they would all be kept rather than deduplicated.

A better approach would be to not include the reason in the deduplication key, or to normalize the reason data before comparison:

Suggested change
const seen = new Set<string>();
const result: SpecialSetting[] = [];
for (const item of [...base, ...derived]) {
const key = buildSettingKey(item);
if (seen.has(key)) continue;
seen.add(key);
result.push(item);
}
case "guard_intercept":
return [
setting.type,
setting.guard,
setting.action,
setting.statusCode ?? "null",
// Excluded: reason
].join(":");

Or if you want to keep the reason, at least normalize it by parsing the JSON first to make the comparison more robust.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/utils/special-settings.ts
Line: 120:127

Comment:
The deduplication logic assumes that if two settings have the same `buildSettingKey()` result, they are duplicates and should be deduplicated. However, there's a subtle issue with the guard_intercept type deduplication.

Consider this scenario:
1. `existing` contains: `{ type: "guard_intercept", guard: "warmup", action: "intercept_response", statusCode: 200, reason: "original_reason" }`
2. `params.blockedBy: "warmup"`, `params.blockedReason: "different_reason"`, `params.statusCode: 200`

The buildSettingKey for guard_intercept (line 51-57) uses the `reason` field in the key, so:
- existing key: `"guard_intercept:warmup:intercept_response:200:original_reason"`
- derived key: `"guard_intercept:warmup:intercept_response:200:different_reason"`

These have different keys, so both would be included! This violates the intention to deduplicate.

The issue is that the `reason` field (which contains JSON string like `{"reason": "..."}`) is being used in the deduplication key. If the same guard intercepts a request multiple times with different reasons, they would all be kept rather than deduplicated.

A better approach would be to not include the reason in the deduplication key, or to normalize the reason data before comparison:

```suggestion
    case "guard_intercept":
      return [
        setting.type,
        setting.guard,
        setting.action,
        setting.statusCode ?? "null",
        // Excluded: reason
      ].join(":");
```

Or if you want to keep the reason, at least normalize it by parsing the JSON first to make the comparison more robust.

How can I resolve this? If you propose a fix, please make it concise.


return result.length > 0 ? result : null;
}
47 changes: 47 additions & 0 deletions src/repository/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { keys as keysTable, messageRequest, providers, users } from "@/drizzle/s
import { getEnvConfig } from "@/lib/config/env.schema";
import { formatCostForStorage } from "@/lib/utils/currency";
import type { CreateMessageRequestData, MessageRequest } from "@/types/message";
import type { SpecialSetting } from "@/types/special-settings";
import { EXCLUDE_WARMUP_CONDITION } from "./_shared/message-request-conditions";
import { toMessageRequest } from "./_shared/transformers";
import { enqueueMessageRequestUpdate } from "./message-write-buffer";
Expand Down Expand Up @@ -270,6 +271,52 @@ export async function findMessageRequestBySessionId(
return toMessageRequest(result);
}

/**
* 按 (sessionId, requestSequence) 获取请求的审计字段(用于 Session 详情页补齐特殊设置展示)
*/
export async function findMessageRequestAuditBySessionIdAndSequence(
sessionId: string,
requestSequence: number
): Promise<{
statusCode: number | null;
blockedBy: string | null;
blockedReason: string | null;
cacheTtlApplied: string | null;
context1mApplied: boolean | null;
specialSettings: SpecialSetting[] | null;
} | null> {
const [row] = await db
.select({
statusCode: messageRequest.statusCode,
blockedBy: messageRequest.blockedBy,
blockedReason: messageRequest.blockedReason,
cacheTtlApplied: messageRequest.cacheTtlApplied,
context1mApplied: messageRequest.context1mApplied,
specialSettings: messageRequest.specialSettings,
})
.from(messageRequest)
.where(
and(
eq(messageRequest.sessionId, sessionId),
eq(messageRequest.requestSequence, requestSequence),
isNull(messageRequest.deletedAt)
)
)
.limit(1);

if (!row) return null;
return {
statusCode: row.statusCode,
blockedBy: row.blockedBy,
blockedReason: row.blockedReason,
cacheTtlApplied: row.cacheTtlApplied,
context1mApplied: row.context1mApplied,
specialSettings: Array.isArray(row.specialSettings)
? (row.specialSettings as SpecialSetting[])
: null,
};
}

/**
* 聚合查询指定 session 的所有请求数据
* 返回总成本、总 Token、请求次数、供应商列表等
Expand Down
29 changes: 29 additions & 0 deletions src/repository/usage-logs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { and, desc, eq, isNull, sql } from "drizzle-orm";
import { db } from "@/drizzle/db";
import { keys as keysTable, messageRequest, providers, users } from "@/drizzle/schema";
import { buildUnifiedSpecialSettings } from "@/lib/utils/special-settings";
import type { ProviderChainItem } from "@/types/message";
import type { SpecialSetting } from "@/types/special-settings";
import { EXCLUDE_WARMUP_CONDITION } from "./_shared/message-request-conditions";
Expand Down Expand Up @@ -241,6 +242,19 @@ export async function findUsageLogsBatch(
(row.cacheCreationInputTokens ?? 0) +
(row.cacheReadInputTokens ?? 0);

const existingSpecialSettings = Array.isArray(row.specialSettings)
? (row.specialSettings as SpecialSetting[])
: null;

const unifiedSpecialSettings = buildUnifiedSpecialSettings({
existing: existingSpecialSettings,
blockedBy: row.blockedBy,
blockedReason: row.blockedReason,
statusCode: row.statusCode,
cacheTtlApplied: row.cacheTtlApplied,
context1mApplied: row.context1mApplied,
});

return {
...row,
requestSequence: row.requestSequence ?? null,
Expand All @@ -251,6 +265,7 @@ export async function findUsageLogsBatch(
costUsd: row.costUsd?.toString() ?? null,
providerChain: row.providerChain as ProviderChainItem[] | null,
endpoint: row.endpoint,
specialSettings: unifiedSpecialSettings,
};
});

Expand Down Expand Up @@ -447,6 +462,19 @@ export async function findUsageLogsWithDetails(filters: UsageLogFilters): Promis
(row.cacheCreationInputTokens ?? 0) +
(row.cacheReadInputTokens ?? 0);

const existingSpecialSettings = Array.isArray(row.specialSettings)
? (row.specialSettings as SpecialSetting[])
: null;

const unifiedSpecialSettings = buildUnifiedSpecialSettings({
existing: existingSpecialSettings,
blockedBy: row.blockedBy,
blockedReason: row.blockedReason,
statusCode: row.statusCode,
cacheTtlApplied: row.cacheTtlApplied,
context1mApplied: row.context1mApplied,
});

return {
...row,
requestSequence: row.requestSequence ?? null,
Expand All @@ -457,6 +485,7 @@ export async function findUsageLogsWithDetails(filters: UsageLogFilters): Promis
costUsd: row.costUsd?.toString() ?? null,
providerChain: row.providerChain as ProviderChainItem[] | null,
endpoint: row.endpoint,
specialSettings: unifiedSpecialSettings,
};
});

Expand Down
Loading
Loading