-
-
Notifications
You must be signed in to change notification settings - Fork 359
修复 issue 1283 内存泄漏根因 #1288
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
修复 issue 1283 内存泄漏根因 #1288
Changes from all commits
e065e2a
92bb359
0edd582
bd0e3a2
aa34336
0cbef88
a0ee9d8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,10 @@ import type { ProxySession } from "@/app/v1/_lib/proxy/session"; | |
| import { logger } from "@/lib/logger"; | ||
| import type { CostBreakdown } from "@/lib/utils/cost-calculation"; | ||
|
|
||
| const LANGFUSE_RESPONSE_TEXT_MAX_CHARS = 1024 * 1024; | ||
| const LANGFUSE_RESPONSE_TEXT_EDGE_CHARS = 128 * 1024; | ||
| const LANGFUSE_TRUNCATED_MARKER = "\n\n[langfuse_response_truncated]\n\n"; | ||
|
|
||
| export interface EmitProxyLangfuseTraceData { | ||
| responseHeaders: Headers; | ||
| responseText: string; | ||
|
|
@@ -16,6 +20,81 @@ export interface EmitProxyLangfuseTraceData { | |
| errorMessage?: string; | ||
| } | ||
|
|
||
| function truncateResponseTextForLangfuse(text: string): string { | ||
| if (text.length <= LANGFUSE_RESPONSE_TEXT_MAX_CHARS) { | ||
| return text; | ||
| } | ||
|
|
||
| return `${text.slice(0, LANGFUSE_RESPONSE_TEXT_EDGE_CHARS)}${LANGFUSE_TRUNCATED_MARKER}${text.slice( | ||
| -LANGFUSE_RESPONSE_TEXT_EDGE_CHARS | ||
| )}`; | ||
| } | ||
|
|
||
| function buildRequestMessagePreview(message: Record<string, unknown>): Record<string, unknown> { | ||
| return { | ||
| truncatedForLangfuse: true, | ||
| model: typeof message.model === "string" ? message.model : undefined, | ||
| stream: typeof message.stream === "boolean" ? message.stream : undefined, | ||
| max_tokens: typeof message.max_tokens === "number" ? message.max_tokens : undefined, | ||
| temperature: typeof message.temperature === "number" ? message.temperature : undefined, | ||
| messageCount: Array.isArray(message.messages) ? message.messages.length : undefined, | ||
| contentsCount: Array.isArray(message.contents) ? message.contents.length : undefined, | ||
| toolsCount: Array.isArray(message.tools) ? message.tools.length : undefined, | ||
| hasSystemPrompt: | ||
| (Array.isArray(message.system) && message.system.length > 0) || | ||
| (typeof message.system === "string" && message.system.length > 0), | ||
| }; | ||
| } | ||
|
|
||
| function buildLangfuseSessionSnapshot(session: ProxySession): ProxySession { | ||
| const providerChain = session.getProviderChain().map((item) => ({ ...item })); | ||
| const specialSettings = session.getSpecialSettings(); | ||
| const cacheTtlResolved = session.getCacheTtlResolved(); | ||
| const context1mApplied = session.getContext1mApplied(); | ||
| const currentModel = session.getCurrentModel(); | ||
| const originalModel = session.getOriginalModel(); | ||
| const modelRedirected = session.isModelRedirected(); | ||
| const endpoint = session.getEndpoint(); | ||
| const requestSequence = session.getRequestSequence(); | ||
| const messagesLength = session.getMessagesLength(); | ||
| const forwardedRequestBody = | ||
| typeof session.forwardedRequestBody === "string" | ||
| ? truncateResponseTextForLangfuse(session.forwardedRequestBody) | ||
| : null; | ||
| const requestMessage = buildRequestMessagePreview(session.request.message); | ||
|
|
||
| return { | ||
| startTime: session.startTime, | ||
| method: session.method, | ||
| headers: new Headers(session.headers), | ||
| request: { | ||
| message: requestMessage, | ||
| log: truncateResponseTextForLangfuse(session.request.log ?? ""), | ||
| note: session.request.note, | ||
| model: session.request.model, | ||
| imageRequestMetadata: null, | ||
| }, | ||
| userAgent: session.userAgent, | ||
| provider: session.provider, | ||
| messageContext: session.messageContext, | ||
| ttfbMs: session.ttfbMs, | ||
| forwardStartTime: session.forwardStartTime, | ||
| forwardedRequestBody, | ||
| sessionId: session.sessionId, | ||
| originalFormat: session.originalFormat, | ||
| getMessagesLength: () => messagesLength, | ||
| getEndpoint: () => endpoint, | ||
| getCurrentModel: () => currentModel, | ||
| getProviderChain: () => providerChain, | ||
| getRequestSequence: () => requestSequence, | ||
| getOriginalModel: () => originalModel, | ||
| isModelRedirected: () => modelRedirected, | ||
| getSpecialSettings: () => specialSettings, | ||
| getCacheTtlResolved: () => cacheTtlResolved, | ||
| getContext1mApplied: () => context1mApplied, | ||
| } as unknown as ProxySession; | ||
|
Comment on lines
+66
to
+95
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Prompt To Fix With AIThis is a comment left during a code review.
Path: src/lib/langfuse/emit-proxy-trace.ts
Line: 66-95
Comment:
**`buildRequestBodySummary` reads wrong keys from the snapshot object**
`buildLangfuseSessionSnapshot` replaces `session.request.message` with the output of `buildRequestMessagePreview`, which stores `hasSystemPrompt: bool` and `toolsCount: number` as flat scalar fields. But `buildRequestBodySummary` in `trace-proxy-request.ts` reads `msg.system` (an array) to compute `hasSystemPrompt` and `msg.tools` (an array) to compute `toolsCount`. Because the preview object never sets `system` or `tools`, every Langfuse trace will show `hasSystemPrompt: false` and `toolsCount: 0` regardless of what the original request contained, silently corrupting the `requestSummary` metadata for all requests after this change ships.
How can I resolve this? If you propose a fix, please make it concise. |
||
| } | ||
|
|
||
| /** | ||
| * 异步发送代理请求的 Langfuse trace。 | ||
| * | ||
|
|
@@ -27,20 +106,35 @@ export function emitProxyLangfuseTrace( | |
| ): void { | ||
| if (!process.env.LANGFUSE_PUBLIC_KEY || !process.env.LANGFUSE_SECRET_KEY) return; | ||
|
|
||
| // 必须在异步 import 之前截断,避免动态加载/SDK 发送期间闭包继续强引用完整大响应。 | ||
| const responseText = truncateResponseTextForLangfuse(data.responseText); | ||
| const sessionSnapshot = buildLangfuseSessionSnapshot(session); | ||
| const { | ||
| responseHeaders, | ||
| durationMs, | ||
| statusCode, | ||
| isStreaming, | ||
| usageMetrics, | ||
| costUsd, | ||
| costBreakdown, | ||
| sseEventCount, | ||
| errorMessage, | ||
| } = data; | ||
|
|
||
| void import("@/lib/langfuse/trace-proxy-request") | ||
| .then(({ traceProxyRequest }) => { | ||
| void traceProxyRequest({ | ||
| session, | ||
| responseHeaders: data.responseHeaders, | ||
| durationMs: data.durationMs, | ||
| statusCode: data.statusCode, | ||
| isStreaming: data.isStreaming, | ||
| responseText: data.responseText, | ||
| usageMetrics: data.usageMetrics, | ||
| costUsd: data.costUsd, | ||
| costBreakdown: data.costBreakdown, | ||
| sseEventCount: data.sseEventCount, | ||
| errorMessage: data.errorMessage, | ||
| session: sessionSnapshot, | ||
| responseHeaders, | ||
| durationMs, | ||
| statusCode, | ||
| isStreaming, | ||
| responseText, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When Langfuse is enabled for a large response, this passes the truncated Useful? React with 👍 / 👎. |
||
| usageMetrics, | ||
| costUsd, | ||
| costBreakdown, | ||
| sseEventCount, | ||
| errorMessage, | ||
| }); | ||
| }) | ||
| .catch((err) => { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The newly introduced
buildRequestMessagePreviewfunction truncates the request message to avoid holding large objects in memory. However, it completely omits thesystemandtoolsfields, only providinghasSystemPromptandtoolsCount.This causes a silent bug in
buildRequestBodySummary(insidetrace-proxy-request.ts), which directly accessesmsg.systemandmsg.toolsto computehasSystemPromptandtoolsCount. Because these fields are missing on the preview object,hasSystemPromptwill always evaluate tofalseandtoolsCountwill always evaluate to0in Langfuse traces for all snapshotted sessions.To fix this without modifying
trace-proxy-request.ts(which is outside the active diff hunk), we can return dummy arrays forsystemandtoolsinbuildRequestMessagePreviewthat match the expected checks (Array.isArrayand.length).