fix: complete OpenAI logs and default persisted logs to summary

This commit is contained in:
majorcheng
2026-03-20 14:06:46 +08:00
parent d293d272ad
commit 310fd8672d
12 changed files with 1096 additions and 15 deletions

View File

@@ -74,10 +74,12 @@ function parseYamlConfig(defaults: AppConfig): { config: AppConfig; raw: Record<
}
// ★ 日志文件持久化
if (yaml.logging !== undefined) {
const persistModes = ['compact', 'full', 'summary'];
result.logging = {
file_enabled: yaml.logging.file_enabled === true, // 默认关闭
dir: yaml.logging.dir || './logs',
max_days: typeof yaml.logging.max_days === 'number' ? yaml.logging.max_days : 7,
persist_mode: persistModes.includes(yaml.logging.persist_mode) ? yaml.logging.persist_mode : 'summary',
};
}
// ★ 工具处理配置
@@ -139,13 +141,21 @@ function applyEnvOverrides(cfg: AppConfig): void {
}
// Logging 环境变量覆盖
if (process.env.LOG_FILE_ENABLED !== undefined) {
if (!cfg.logging) cfg.logging = { file_enabled: false, dir: './logs', max_days: 7 };
if (!cfg.logging) cfg.logging = { file_enabled: false, dir: './logs', max_days: 7, persist_mode: 'summary' };
cfg.logging.file_enabled = process.env.LOG_FILE_ENABLED === 'true' || process.env.LOG_FILE_ENABLED === '1';
}
if (process.env.LOG_DIR) {
if (!cfg.logging) cfg.logging = { file_enabled: false, dir: './logs', max_days: 7 };
if (!cfg.logging) cfg.logging = { file_enabled: false, dir: './logs', max_days: 7, persist_mode: 'summary' };
cfg.logging.dir = process.env.LOG_DIR;
}
if (process.env.LOG_PERSIST_MODE) {
if (!cfg.logging) cfg.logging = { file_enabled: false, dir: './logs', max_days: 7, persist_mode: 'summary' };
cfg.logging.persist_mode = process.env.LOG_PERSIST_MODE === 'full'
? 'full'
: process.env.LOG_PERSIST_MODE === 'summary'
? 'summary'
: 'compact';
}
// 工具透传模式环境变量覆盖
if (process.env.TOOLS_PASSTHROUGH !== undefined) {
if (!cfg.tools) cfg.tools = { schemaMode: 'full', descriptionMaxLength: 0 };

View File

@@ -153,7 +153,9 @@ loadLogsFromFiles();
app.listen(config.port, () => {
const auth = config.authTokens?.length ? `${config.authTokens.length} token(s)` : 'open';
const logPersist = config.logging?.file_enabled ? `file → ${config.logging.dir}` : 'memory only';
const logPersist = config.logging?.file_enabled
? `file(${config.logging.persist_mode || 'summary'}) → ${config.logging.dir}`
: 'memory only';
// Tools 配置摘要
const toolsCfg = config.tools;

View File

@@ -80,6 +80,14 @@ export interface RequestPayload {
retryResponses?: Array<{ attempt: number; response: string; reason: string }>;
/** 每次续写的原始响应 */
continuationResponses?: Array<{ index: number; response: string; dedupedLength: number }>;
/** summary 模式:最后一个用户问题 */
question?: string;
/** summary 模式:最终回答摘要 */
answer?: string;
/** summary 模式:回答类型 */
answerType?: 'text' | 'tool_calls' | 'empty';
/** summary 模式:工具调用名称列表 */
toolCallNames?: string[];
}
export interface RequestSummary {
@@ -133,12 +141,31 @@ function shortId(): string {
// ==================== 日志文件持久化 ====================
const DEFAULT_PERSIST_MODE: 'compact' | 'full' | 'summary' = 'summary';
const DISK_SYSTEM_PROMPT_CHARS = 2000;
const DISK_MESSAGE_PREVIEW_CHARS = 3000;
const DISK_CURSOR_MESSAGE_PREVIEW_CHARS = 2000;
const DISK_RESPONSE_CHARS = 8000;
const DISK_THINKING_CHARS = 4000;
const DISK_TOOL_DESC_CHARS = 500;
const DISK_RETRY_CHARS = 2000;
const DISK_TOOLCALL_STRING_CHARS = 1200;
const DISK_MAX_ARRAY_ITEMS = 20;
const DISK_MAX_OBJECT_DEPTH = 5;
const DISK_SUMMARY_QUESTION_CHARS = 2000;
const DISK_SUMMARY_ANSWER_CHARS = 4000;
function getLogDir(): string | null {
const cfg = getConfig();
if (!cfg.logging?.file_enabled) return null;
return cfg.logging.dir || './logs';
}
function getPersistMode(): 'compact' | 'full' | 'summary' {
const mode = getConfig().logging?.persist_mode;
return mode === 'full' || mode === 'summary' || mode === 'compact' ? mode : DEFAULT_PERSIST_MODE;
}
function getLogFilePath(): string | null {
const dir = getLogDir();
if (!dir) return null;
@@ -153,13 +180,256 @@ function ensureLogDir(): void {
}
}
function truncateMiddle(text: string, maxChars: number): string {
if (!text || text.length <= maxChars) return text;
const omitted = text.length - maxChars;
const marker = `\n...[截断 ${omitted} chars]...\n`;
const remain = Math.max(16, maxChars - marker.length);
const head = Math.ceil(remain * 0.7);
const tail = Math.max(8, remain - head);
return text.slice(0, head) + marker + text.slice(text.length - tail);
}
function compactUnknownValue(value: unknown, maxStringChars = DISK_TOOLCALL_STRING_CHARS, depth = 0): unknown {
if (value === null || value === undefined) return value;
if (typeof value === 'string') return truncateMiddle(value, maxStringChars);
if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') return value;
if (depth >= DISK_MAX_OBJECT_DEPTH) {
if (Array.isArray(value)) return `[array(${value.length})]`;
return '[object]';
}
if (Array.isArray(value)) {
const items = value.slice(0, DISK_MAX_ARRAY_ITEMS)
.map(item => compactUnknownValue(item, maxStringChars, depth + 1));
if (value.length > DISK_MAX_ARRAY_ITEMS) {
items.push(`[... ${value.length - DISK_MAX_ARRAY_ITEMS} more items]`);
}
return items;
}
if (typeof value === 'object') {
const result: Record<string, unknown> = {};
for (const [key, entry] of Object.entries(value as Record<string, unknown>)) {
const limit = /content|text|arguments|description|prompt|response|reasoning/i.test(key)
? maxStringChars
: Math.min(maxStringChars, 400);
result[key] = compactUnknownValue(entry, limit, depth + 1);
}
return result;
}
return String(value);
}
function extractTextParts(value: unknown): string {
if (typeof value === 'string') return value;
if (!value) return '';
if (Array.isArray(value)) {
return value
.map(item => extractTextParts(item))
.filter(Boolean)
.join('\n');
}
if (typeof value === 'object') {
const record = value as Record<string, unknown>;
if (typeof record.text === 'string') return record.text;
if (typeof record.output === 'string') return record.output;
if (typeof record.content === 'string') return record.content;
if (record.content !== undefined) return extractTextParts(record.content);
if (record.input !== undefined) return extractTextParts(record.input);
}
return '';
}
function extractLastUserQuestion(summary: RequestSummary, payload: RequestPayload): string | undefined {
const lastUser = payload.messages?.slice().reverse().find(m => m.role === 'user' && m.contentPreview?.trim());
if (lastUser?.contentPreview) {
return truncateMiddle(lastUser.contentPreview, DISK_SUMMARY_QUESTION_CHARS);
}
const original = payload.originalRequest && typeof payload.originalRequest === 'object' && !Array.isArray(payload.originalRequest)
? payload.originalRequest as Record<string, unknown>
: undefined;
if (!original) {
return summary.title ? truncateMiddle(summary.title, DISK_SUMMARY_QUESTION_CHARS) : undefined;
}
if (Array.isArray(original.messages)) {
for (let i = original.messages.length - 1; i >= 0; i--) {
const item = original.messages[i] as Record<string, unknown>;
if (item?.role === 'user') {
const text = extractTextParts(item.content);
if (text.trim()) return truncateMiddle(text, DISK_SUMMARY_QUESTION_CHARS);
}
}
}
if (typeof original.input === 'string' && original.input.trim()) {
return truncateMiddle(original.input, DISK_SUMMARY_QUESTION_CHARS);
}
if (Array.isArray(original.input)) {
for (let i = original.input.length - 1; i >= 0; i--) {
const item = original.input[i] as Record<string, unknown>;
if (!item) continue;
const role = typeof item.role === 'string' ? item.role : 'user';
if (role === 'user') {
const text = extractTextParts(item.content ?? item.input ?? item);
if (text.trim()) return truncateMiddle(text, DISK_SUMMARY_QUESTION_CHARS);
}
}
}
return summary.title ? truncateMiddle(summary.title, DISK_SUMMARY_QUESTION_CHARS) : undefined;
}
function extractToolCallNames(payload: RequestPayload): string[] {
if (!payload.toolCalls?.length) return [];
return payload.toolCalls
.map(call => {
if (call && typeof call === 'object') {
const record = call as Record<string, unknown>;
if (typeof record.name === 'string') return record.name;
const fn = record.function;
if (fn && typeof fn === 'object' && typeof (fn as Record<string, unknown>).name === 'string') {
return (fn as Record<string, unknown>).name as string;
}
}
return '';
})
.filter(Boolean);
}
function buildSummaryPayload(summary: RequestSummary, payload: RequestPayload): RequestPayload {
const question = extractLastUserQuestion(summary, payload);
const answerText = payload.finalResponse || payload.rawResponse || '';
const toolCallNames = extractToolCallNames(payload);
const answer = answerText
? truncateMiddle(answerText, DISK_SUMMARY_ANSWER_CHARS)
: toolCallNames.length > 0
? `[tool_calls] ${toolCallNames.join(', ')}`
: undefined;
return {
...(question ? { question } : {}),
...(answer ? { answer } : {}),
answerType: answerText ? 'text' : toolCallNames.length > 0 ? 'tool_calls' : 'empty',
...(toolCallNames.length > 0 ? { toolCallNames } : {}),
};
}
function buildCompactOriginalRequest(summary: RequestSummary, payload: RequestPayload): Record<string, unknown> | undefined {
const original = payload.originalRequest && typeof payload.originalRequest === 'object' && !Array.isArray(payload.originalRequest)
? payload.originalRequest as Record<string, unknown>
: undefined;
const result: Record<string, unknown> = {
model: summary.model,
stream: summary.stream,
apiFormat: summary.apiFormat,
messageCount: summary.messageCount,
toolCount: summary.toolCount,
};
if (summary.title) result.title = summary.title;
if (payload.systemPrompt) result.systemPromptPreview = truncateMiddle(payload.systemPrompt, DISK_SYSTEM_PROMPT_CHARS);
if (payload.messages?.some(m => m.hasImages)) result.hasImages = true;
const lastUser = payload.messages?.slice().reverse().find(m => m.role === 'user');
if (lastUser?.contentPreview) {
result.lastUserPreview = truncateMiddle(lastUser.contentPreview, 800);
}
if (original) {
for (const key of ['temperature', 'top_p', 'max_tokens', 'max_completion_tokens', 'max_output_tokens']) {
const value = original[key];
if (value !== undefined && typeof value !== 'object') result[key] = value;
}
if (typeof original.instructions === 'string') {
result.instructions = truncateMiddle(original.instructions, 1200);
}
if (typeof original.system === 'string') {
result.system = truncateMiddle(original.system, DISK_SYSTEM_PROMPT_CHARS);
}
}
return Object.keys(result).length > 0 ? result : undefined;
}
function compactPayloadForDisk(summary: RequestSummary, payload: RequestPayload): RequestPayload {
const compact: RequestPayload = {};
if (payload.originalRequest !== undefined) {
compact.originalRequest = buildCompactOriginalRequest(summary, payload);
}
if (payload.systemPrompt) {
compact.systemPrompt = truncateMiddle(payload.systemPrompt, DISK_SYSTEM_PROMPT_CHARS);
}
if (payload.messages?.length) {
compact.messages = payload.messages.map(msg => ({
...msg,
contentPreview: truncateMiddle(msg.contentPreview, DISK_MESSAGE_PREVIEW_CHARS),
}));
}
if (payload.tools?.length) {
compact.tools = payload.tools.map(tool => ({
name: tool.name,
...(tool.description ? { description: truncateMiddle(tool.description, DISK_TOOL_DESC_CHARS) } : {}),
}));
}
if (payload.cursorRequest !== undefined) {
compact.cursorRequest = payload.cursorRequest;
}
if (payload.cursorMessages?.length) {
compact.cursorMessages = payload.cursorMessages.map(msg => ({
...msg,
contentPreview: truncateMiddle(msg.contentPreview, DISK_CURSOR_MESSAGE_PREVIEW_CHARS),
}));
}
const compactFinalResponse = payload.finalResponse
? truncateMiddle(payload.finalResponse, DISK_RESPONSE_CHARS)
: undefined;
const compactRawResponse = payload.rawResponse
? truncateMiddle(payload.rawResponse, DISK_RESPONSE_CHARS)
: undefined;
if (compactFinalResponse) compact.finalResponse = compactFinalResponse;
if (compactRawResponse && compactRawResponse !== compactFinalResponse) {
compact.rawResponse = compactRawResponse;
}
if (payload.thinkingContent) {
compact.thinkingContent = truncateMiddle(payload.thinkingContent, DISK_THINKING_CHARS);
}
if (payload.toolCalls?.length) {
compact.toolCalls = compactUnknownValue(payload.toolCalls) as unknown[];
}
if (payload.retryResponses?.length) {
compact.retryResponses = payload.retryResponses.map(item => ({
...item,
response: truncateMiddle(item.response, DISK_RETRY_CHARS),
reason: truncateMiddle(item.reason, 300),
}));
}
if (payload.continuationResponses?.length) {
compact.continuationResponses = payload.continuationResponses.map(item => ({
...item,
response: truncateMiddle(item.response, DISK_RETRY_CHARS),
}));
}
return compact;
}
/** 将已完成的请求写入日志文件 */
function persistRequest(summary: RequestSummary, payload: RequestPayload): void {
const filepath = getLogFilePath();
if (!filepath) return;
try {
ensureLogDir();
const record = { timestamp: Date.now(), summary, payload };
const persistMode = getPersistMode();
const persistedPayload = persistMode === 'full'
? payload
: persistMode === 'summary'
? buildSummaryPayload(summary, payload)
: compactPayloadForDisk(summary, payload);
const record = { timestamp: Date.now(), summary, payload: persistedPayload };
appendFileSync(filepath, JSON.stringify(record) + '\n', 'utf-8');
} catch (e) {
console.warn('[Logger] 写入日志文件失败:', e);

View File

@@ -27,7 +27,7 @@ import type {
import { convertToCursorRequest, parseToolCalls, hasToolCalls } from './converter.js';
import { sendCursorRequest, sendCursorRequestFull } from './cursor-client.js';
import { getConfig } from './config.js';
import { createRequestLogger } from './logger.js';
import { createRequestLogger, type RequestLogger } from './logger.js';
import { createIncrementalTextStreamer, hasLeadingThinking, splitLeadingThinkingBlocks, stripThinkingTags } from './streaming-text.js';
import {
autoContinueCursorToolResponseFull,
@@ -488,11 +488,12 @@ export async function handleOpenAIChatCompletions(req: Request, res: Response):
// Step 2: Anthropic → Cursor 格式(复用现有管道)
const cursorReq = await convertToCursorRequest(anthropicReq);
log.recordCursorRequest(cursorReq);
if (body.stream) {
await handleOpenAIStream(res, cursorReq, body, anthropicReq);
await handleOpenAIStream(res, cursorReq, body, anthropicReq, log);
} else {
await handleOpenAINonStream(res, cursorReq, body, anthropicReq);
await handleOpenAINonStream(res, cursorReq, body, anthropicReq, log);
}
} catch (err: unknown) {
const message = err instanceof Error ? err.message : String(err);
@@ -609,6 +610,7 @@ async function handleOpenAIIncrementalTextStream(
body: OpenAIChatRequest,
anthropicReq: AnthropicRequest,
streamMeta: { id: string; created: number; model: string },
log: RequestLogger,
): Promise<void> {
let activeCursorReq = cursorReq;
let retryCount = 0;
@@ -739,6 +741,16 @@ async function handleOpenAIIncrementalTextStream(
usage: buildOpenAIUsage(anthropicReq, streamer.hasSentText() ? (finalVisibleText || finalRawResponse) : finalTextToSend),
});
log.recordRawResponse(finalRawResponse);
if (finalReasoningContent) {
log.recordThinking(finalReasoningContent);
}
const finalRecordedResponse = streamer.hasSentText()
? sanitizeResponse(finalVisibleText || finalRawResponse)
: finalTextToSend;
log.recordFinalResponse(finalRecordedResponse);
log.complete(finalRecordedResponse.length, 'stop');
res.write('data: [DONE]\n\n');
res.end();
}
@@ -750,6 +762,7 @@ async function handleOpenAIStream(
cursorReq: CursorChatRequest,
body: OpenAIChatRequest,
anthropicReq: AnthropicRequest,
log: RequestLogger,
): Promise<void> {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
@@ -790,7 +803,7 @@ async function handleOpenAIStream(
try {
if (!hasTools && (!body.response_format || body.response_format.type === 'text')) {
await handleOpenAIIncrementalTextStream(res, cursorReq, body, anthropicReq, { id, created, model });
await handleOpenAIIncrementalTextStream(res, cursorReq, body, anthropicReq, { id, created, model }, log);
return;
}
@@ -973,6 +986,8 @@ async function handleOpenAIStream(
if (toolCalls.length > 0) {
finishReason = 'tool_calls';
log.recordToolCalls(toolCalls);
log.updateSummary({ toolCallsDetected: toolCalls.length });
// 发送工具调用前的残余文本 — 如果混合流式已发送则跳过
if (!hybridTextSent) {
@@ -1083,10 +1098,18 @@ async function handleOpenAIStream(
usage: buildOpenAIUsage(anthropicReq, fullResponse),
});
log.recordRawResponse(fullResponse);
if (reasoningContent) {
log.recordThinking(reasoningContent);
}
log.recordFinalResponse(fullResponse);
log.complete(fullResponse.length, finishReason);
res.write('data: [DONE]\n\n');
} catch (err: unknown) {
const message = err instanceof Error ? err.message : String(err);
log.fail(message);
writeOpenAISSE(res, {
id, object: 'chat.completion.chunk', created, model,
choices: [{
@@ -1108,6 +1131,7 @@ async function handleOpenAINonStream(
cursorReq: CursorChatRequest,
body: OpenAIChatRequest,
anthropicReq: AnthropicRequest,
log: RequestLogger,
): Promise<void> {
let activeCursorReq = cursorReq;
let fullText = await sendCursorRequestFull(activeCursorReq);
@@ -1172,6 +1196,8 @@ async function handleOpenAINonStream(
if (parsed.toolCalls.length > 0) {
finishReason = 'tool_calls';
log.recordToolCalls(parsed.toolCalls);
log.updateSummary({ toolCallsDetected: parsed.toolCalls.length });
// 清洗拒绝文本
let cleanText = parsed.cleanText;
if (isRefusal(cleanText)) {
@@ -1224,6 +1250,13 @@ async function handleOpenAINonStream(
};
res.json(response);
log.recordRawResponse(fullText);
if (reasoningContent) {
log.recordThinking(reasoningContent);
}
log.recordFinalResponse(fullText);
log.complete(fullText.length, finishReason);
}
// ==================== 工具函数 ====================
@@ -1300,17 +1333,39 @@ function buildResponseObject(
* 而非 data: {"object":"chat.completion.chunk",...} 格式
*/
export async function handleOpenAIResponses(req: Request, res: Response): Promise<void> {
try {
const body = req.body;
const isStream = (body.stream as boolean) ?? true;
const body = req.body as Record<string, unknown>;
const isStream = (body.stream as boolean) ?? true;
const chatBody = responsesToChatCompletions(body);
const log = createRequestLogger({
method: req.method,
path: req.path,
model: chatBody.model,
stream: isStream,
hasTools: (chatBody.tools?.length ?? 0) > 0,
toolCount: chatBody.tools?.length ?? 0,
messageCount: chatBody.messages?.length ?? 0,
apiFormat: 'responses',
});
log.startPhase('receive', '接收请求');
log.recordOriginalRequest(body);
log.info('OpenAI', 'receive', '收到 OpenAI Responses 请求', {
model: chatBody.model,
stream: isStream,
toolCount: chatBody.tools?.length ?? 0,
messageCount: chatBody.messages?.length ?? 0,
});
try {
// Step 1: 转换请求格式 Responses → Chat Completions → Anthropic → Cursor
const chatBody = responsesToChatCompletions(body);
log.startPhase('convert', '格式转换 (ResponsesChat→Anthropic)');
const anthropicReq = convertToAnthropicRequest(chatBody);
const cursorReq = await convertToCursorRequest(anthropicReq);
log.endPhase();
log.recordCursorRequest(cursorReq);
// 身份探针拦截
if (isIdentityProbe(anthropicReq)) {
log.intercepted('身份探针拦截 (Responses)');
const mockText = "I am Claude, an advanced AI programming assistant created by Anthropic. I am ready to help you write code, debug, and answer your technical questions.";
if (isStream) {
return handleResponsesStreamMock(res, body, mockText);
@@ -1320,12 +1375,13 @@ export async function handleOpenAIResponses(req: Request, res: Response): Promis
}
if (isStream) {
await handleResponsesStream(res, cursorReq, body, anthropicReq);
await handleResponsesStream(res, cursorReq, body, anthropicReq, log);
} else {
await handleResponsesNonStream(res, cursorReq, body, anthropicReq);
await handleResponsesNonStream(res, cursorReq, body, anthropicReq, log);
}
} catch (err: unknown) {
const message = err instanceof Error ? err.message : String(err);
log.fail(message);
console.error(`[OpenAI] /v1/responses 处理失败:`, message);
const status = err instanceof OpenAIRequestError ? err.status : 500;
const type = err instanceof OpenAIRequestError ? err.type : 'server_error';
@@ -1471,6 +1527,7 @@ async function handleResponsesStream(
cursorReq: CursorChatRequest,
body: Record<string, unknown>,
anthropicReq: AnthropicRequest,
log: RequestLogger,
): Promise<void> {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
@@ -1482,6 +1539,7 @@ async function handleResponsesStream(
const respId = responsesId();
const model = (body.model as string) || 'gpt-4';
const hasTools = (anthropicReq.tools?.length ?? 0) > 0;
let toolCallsDetected = 0;
// 缓冲完整响应再处理(复用 Chat Completions 的逻辑)
let fullResponse = '';
@@ -1557,6 +1615,9 @@ async function handleResponsesStream(
const { toolCalls, cleanText } = parseToolCalls(fullResponse);
if (toolCalls.length > 0) {
toolCallsDetected = toolCalls.length;
log.recordToolCalls(toolCalls);
log.updateSummary({ toolCallsDetected: toolCalls.length });
// 1. response.created + response.in_progress
writeResponsesSSE(res, 'response.created', buildResponseObject(respId, model, 'in_progress', []));
writeResponsesSSE(res, 'response.in_progress', buildResponseObject(respId, model, 'in_progress', []));
@@ -1658,8 +1719,12 @@ async function handleResponsesStream(
const msgItemId = responsesItemId();
emitResponsesTextStream(res, respId, msgItemId, model, fullResponse, 0, usage);
}
log.recordRawResponse(fullResponse);
log.recordFinalResponse(fullResponse);
log.complete(fullResponse.length, toolCallsDetected > 0 ? 'tool_calls' : 'stop');
} catch (err: unknown) {
const message = err instanceof Error ? err.message : String(err);
log.fail(message);
// 尝试发送错误后的 response.completed确保 Codex 不会等待超时
try {
const errorText = `[Error: ${message}]`;
@@ -1707,6 +1772,7 @@ async function handleResponsesNonStream(
cursorReq: CursorChatRequest,
body: Record<string, unknown>,
anthropicReq: AnthropicRequest,
log: RequestLogger,
): Promise<void> {
let activeCursorReq = cursorReq;
let fullText = await sendCursorRequestFull(activeCursorReq);
@@ -1752,9 +1818,13 @@ async function handleResponsesNonStream(
const usage = { input_tokens: inputTokens, output_tokens: outputTokens, total_tokens: inputTokens + outputTokens };
const output: Record<string, unknown>[] = [];
let toolCallsDetected = 0;
if (hasTools && hasToolCalls(fullText)) {
const { toolCalls, cleanText } = parseToolCalls(fullText);
toolCallsDetected = toolCalls.length;
log.recordToolCalls(toolCalls);
log.updateSummary({ toolCallsDetected: toolCalls.length });
for (const tc of toolCalls) {
output.push({
id: responsesItemId(),
@@ -1786,6 +1856,10 @@ async function handleResponsesNonStream(
}
res.json(buildResponseObject(respId, model, 'completed', output, usage));
log.recordRawResponse(fullText);
log.recordFinalResponse(fullText);
log.complete(fullText.length, toolCallsDetected > 0 ? 'tool_calls' : 'stop');
}
/**

View File

@@ -129,6 +129,7 @@ export interface AppConfig {
file_enabled: boolean; // 是否启用日志文件持久化
dir: string; // 日志文件存储目录
max_days: number; // 日志保留天数
persist_mode: 'compact' | 'full' | 'summary'; // 落盘模式: compact=精简, full=完整, summary=仅问答摘要
};
tools?: {
schemaMode: 'compact' | 'full' | 'names_only'; // Schema 呈现模式