Skip to content
This repository was archived by the owner on Jan 2, 2026. It is now read-only.
Closed
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
19 changes: 13 additions & 6 deletions src/api/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -403,8 +403,11 @@ function parseAndEmitStreamChunk(line, state, callback) {
if (parts) {
for (const part of parts) {
if (part.thought === true) {
// 思维链内容 - 不添加标签,直接发送
callback({ type: 'thinking', content: part.text || '' });
// 思维链内容 - 直接发送
if (part.text) {
callback({ type: 'thinking', content: part.text });
}
// 思维阶段的中间图片,跳过(只发送最终图片)
} else if (part.text !== undefined) {
if (part.thoughtSignature) {
registerTextThoughtSignature(part.text, part.thoughtSignature);
Expand All @@ -415,6 +418,10 @@ function parseAndEmitStreamChunk(line, state, callback) {
} else if (part.functionCall) {
// 工具调用
state.toolCalls.push(convertToToolCallWithSignature(part.functionCall, part.thoughtSignature));
} else if (part.inlineData) {
// 图片数据
const imageUrl = saveBase64Image(part.inlineData.data, part.inlineData.mimeType);
callback({ type: 'image', url: imageUrl });
}
}
}
Expand Down Expand Up @@ -682,19 +689,19 @@ export async function generateAssistantResponseNoStream(requestBody, token) {
}
}

// 拼接思维链标签
if (thinkingContent) {
// 拼接思维链标签(用于非图像模型的普通响应)
if (thinkingContent && imageUrls.length === 0) {
content = `<think>\n${thinkingContent}\n</think>\n${content}`;
}
if (aggregatedText && aggregatedTextSignature) {
registerTextThoughtSignature(aggregatedText, aggregatedTextSignature);
}

// 生图模型:转换为 markdown 格式
// 生图模型:转换为 markdown 格式,并返回独立的 thinking 字段
if (imageUrls.length > 0) {
let markdown = content ? content + '\n\n' : '';
markdown += imageUrls.map(url => `![image](${url})`).join('\n\n');
return { content: markdown, toolCalls };
return { content: markdown, toolCalls, thinking: thinkingContent || null };
}

return { content, toolCalls, usage };
Expand Down
38 changes: 32 additions & 6 deletions src/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -1605,9 +1605,14 @@ const createChatCompletionHandler = (resolveToken, options = {}) => async (req,
const requestBody = generateRequestBody(messages, model, params, tools, token);

if (isImageModel) {
// 为图像模型配置思维链和响应模态,使 gemini-3-pro-image 能返回思维内容
requestBody.request.generationConfig = {
candidateCount: 1
// imageConfig: { aspectRatio: '1:1' }
candidateCount: 1,
responseModalities: ["TEXT", "IMAGE"],
thinkingConfig: {
includeThoughts: true,
thinkingBudget: 1024
}
};
requestBody.requestType = 'image_gen';
requestBody.request.systemInstruction.parts[0].text +=
Expand All @@ -1622,11 +1627,32 @@ const createChatCompletionHandler = (resolveToken, options = {}) => async (req,
setStreamHeaders(res);

if (isImageModel) {
const { content, usage } = await generateAssistantResponseNoStream(requestBody, token);
writeStreamData(res, createStreamChunk(id, created, model, { content }));
// 图像模型使用流式API,实现思维链实时传输
const imageUrls = [];
const { usage } = await generateAssistantResponse(requestBody, token, data => {
streamEventsForLog.push(data);

if (data.type === 'thinking') {
// 思维链内容实时发送
writeStreamData(res, createStreamChunk(id, created, model, { reasoning_content: data.content }));
} else if (data.type === 'image') {
// 收集图片URL,最后统一发送
imageUrls.push(data.url);
} else if (data.type === 'text') {
// 文本内容
writeStreamData(res, createStreamChunk(id, created, model, { content: data.content }));
}
});

// 发送所有图片
if (imageUrls.length > 0) {
const markdown = imageUrls.map(url => `![image](${url})`).join('\n\n');
writeStreamData(res, createStreamChunk(id, created, model, { content: markdown }));
}

endStream(res, id, created, model, 'stop', usage);
responseBodyForLog = { stream: true, image: true, usage, content };
responseSummaryForLog = { text: content };
responseBodyForLog = { stream: true, image: true, usage, events: streamEventsForLog };
responseSummaryForLog = summarizeStreamEvents(streamEventsForLog);
} else {
let hasToolCall = false;
const { usage } = await generateAssistantResponse(requestBody, token, data => {
Expand Down
45 changes: 43 additions & 2 deletions src/utils/logger.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,43 @@ function logDetail(data) {
console.log('----------------------------------------------------');
}

/**
* 截断 base64 数据,只保留前 maxLength 个字符
* @param {any} obj - 要处理的对象
* @param {number} maxLength - 最大保留长度,默认 100
* @returns {any} - 处理后的对象
*/
function truncateBase64(obj, maxLength = 100) {
if (obj === null || obj === undefined) {
return obj;
}

if (typeof obj === 'string') {
// 检测是否为 base64 数据(长度较长且符合 base64 格式)
// base64 通常只包含 A-Za-z0-9+/= 字符
const base64Regex = /^[A-Za-z0-9+/=]{200,}$/;
if (base64Regex.test(obj)) {
const totalLength = obj.length;
return obj.substring(0, maxLength) + `...[已截断, 共 ${totalLength} 字符]`;
}
return obj;
}

if (Array.isArray(obj)) {
return obj.map(item => truncateBase64(item, maxLength));
}

if (typeof obj === 'object') {
const result = {};
for (const key of Object.keys(obj)) {
result[key] = truncateBase64(obj[key], maxLength);
}
return result;
}

return obj;
}

/**
* 记录后端 API 的请求和响应(仅 debug=high 时生效)
* @param {Object} data - 日志数据
Expand Down Expand Up @@ -124,15 +161,19 @@ function logBackend(data) {
}
if (body) {
console.log(`${colors.yellow}Body:${colors.reset}`);
const bodyStr = typeof body === 'string' ? body : JSON.stringify(body, null, 2);
// 截断 base64 数据
const truncatedBody = truncateBase64(body);
const bodyStr = typeof truncatedBody === 'string' ? truncatedBody : JSON.stringify(truncatedBody, null, 2);
console.log(bodyStr);
}
} else if (type === 'response') {
const statusColor = status >= 500 ? colors.red : status >= 400 ? colors.yellow : colors.green;
console.log(`${colors.green}[Backend Response]${colors.reset} ${statusColor}${status}${colors.reset} ${colors.gray}${durationMs}ms${colors.reset}`);
if (body) {
console.log(`${colors.green}Body:${colors.reset}`);
const bodyStr = typeof body === 'string' ? body : JSON.stringify(body, null, 2);
// 截断 base64 数据
const truncatedBody = truncateBase64(body);
const bodyStr = typeof truncatedBody === 'string' ? truncatedBody : JSON.stringify(truncatedBody, null, 2);
console.log(bodyStr);
}
}
Expand Down