| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
|
|
| import config from './config.js';
|
| import { serializeTools, serializeToolsAnthropic } from './tool-prompt.js';
|
|
|
| |
| |
| |
|
|
| const MAX_TEXT_LEN = 2400;
|
|
|
| |
| |
| |
|
|
| function extractText(content) {
|
| if (typeof content === 'string') return content;
|
| if (Array.isArray(content)) {
|
| return content
|
| .filter(part => part.type === 'text')
|
| .map(part => part.text)
|
| .join('\n');
|
| }
|
| return String(content || '');
|
| }
|
|
|
| |
| |
| |
|
|
| function truncateToolResults(text, maxLen = 600) {
|
| return text.replace(/\[Tool Result: [^\]]*\]\n([\s\S]*?)(?=\n\[(?:User|Assistant|Tool Result|System)\]|\n```tool_calls|$)/g,
|
| (match, content) => {
|
| if (match.length <= maxLen) return match;
|
| const header = match.substring(0, match.indexOf('\n') + 1);
|
| const keep = maxLen - header.length - 30;
|
| if (keep <= 0) return header + '(content truncated)';
|
| const half = Math.floor(keep / 2);
|
| return header + content.substring(0, half) + '\n...(truncated)...\n' + content.substring(content.length - half);
|
| });
|
| }
|
|
|
| |
| |
|
|
| function parseSegments(text) {
|
| const segments = [];
|
| const segRegex = /\[(System|User|Assistant)\] /g;
|
| let m;
|
| while ((m = segRegex.exec(text)) !== null) {
|
| if (segments.length > 0) {
|
| segments[segments.length - 1].text = text.substring(segments[segments.length - 1].start, m.index).trimEnd();
|
| }
|
| segments.push({ role: m[1], start: m.index, text: '' });
|
| }
|
| if (segments.length > 0) {
|
| segments[segments.length - 1].text = text.substring(segments[segments.length - 1].start).trimEnd();
|
| }
|
| return segments;
|
| }
|
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| function compressToSingle(text) {
|
| if (text.length <= MAX_TEXT_LEN) return text;
|
|
|
| const originalLen = text.length;
|
|
|
|
|
| let result = text;
|
| for (const maxToolLen of [600, 300, 100, 40]) {
|
| result = truncateToolResults(result, maxToolLen);
|
| if (result.length <= MAX_TEXT_LEN) {
|
| console.log(`[Convert] 压缩 ${originalLen} → ${result.length} (截断 tool results)`);
|
| return result;
|
| }
|
| }
|
|
|
|
|
| result = result.replace(/\[Tool Result: [^\]]*\]\n[\s\S]*?(?=\n\[(?:User|Assistant|System)\]|\n```tool_calls|$)/g,
|
| (match) => {
|
| const nl = match.indexOf('\n');
|
| return match.substring(0, nl > 0 ? nl : match.length) + '\n(omitted)';
|
| });
|
| if (result.length <= MAX_TEXT_LEN) {
|
| console.log(`[Convert] 压缩 ${originalLen} → ${result.length} (清除 tool results)`);
|
| return result;
|
| }
|
|
|
|
|
| const segments = parseSegments(result);
|
| if (segments.length <= 2) {
|
|
|
| } else {
|
| const systemSeg = segments[0]?.role === 'System' ? segments[0] : null;
|
| let lastUserIdx = -1;
|
| for (let i = segments.length - 1; i >= 0; i--) {
|
| if (segments[i].role === 'User') { lastUserIdx = i; break; }
|
| }
|
|
|
|
|
| const mustKeep = new Set();
|
| if (systemSeg) mustKeep.add(0);
|
| if (lastUserIdx >= 0) mustKeep.add(lastUserIdx);
|
| mustKeep.add(segments.length - 1);
|
|
|
| const kept = segments.filter((_, i) => mustKeep.has(i));
|
| const rebuilt = kept.map(s => s.text).join('\n\n');
|
| if (rebuilt.length <= MAX_TEXT_LEN) {
|
| console.log(`[Convert] 压缩 ${originalLen} → ${rebuilt.length} (删除中间轮次)`);
|
| return rebuilt;
|
| }
|
|
|
|
|
| if (systemSeg) {
|
| const lastMsg = lastUserIdx >= 0 ? segments[lastUserIdx].text : segments[segments.length - 1].text;
|
| const budgetForSystem = MAX_TEXT_LEN - lastMsg.length - 20;
|
| if (budgetForSystem > 200) {
|
| const truncated = systemSeg.text.substring(0, budgetForSystem) + '\n\n' + lastMsg;
|
| console.log(`[Convert] 压缩 ${originalLen} → ${truncated.length} (截断 system)`);
|
| return truncated;
|
| }
|
|
|
| if (lastMsg.length <= MAX_TEXT_LEN) {
|
| console.log(`[Convert] 压缩 ${originalLen} → ${lastMsg.length} (仅 user 消息)`);
|
| return lastMsg;
|
| }
|
| }
|
| }
|
|
|
|
|
| const truncated = text.substring(text.length - MAX_TEXT_LEN);
|
| console.log(`[Convert] 压缩 ${originalLen} → ${MAX_TEXT_LEN} (硬截断末尾)`);
|
| return truncated;
|
| }
|
|
|
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| export function splitToChunks(text) {
|
| if (!text) return [''];
|
|
|
|
|
| const single = compressToSingle(text);
|
| if (single && single.length <= MAX_TEXT_LEN) return [single];
|
|
|
|
|
| const originalLen = text.length;
|
|
|
|
|
| const compressed = truncateToolResults(text, 300);
|
|
|
| const segments = parseSegments(compressed);
|
| if (segments.length === 0) return hardSplit(compressed);
|
|
|
|
|
| const chunks = [];
|
| let currentChunk = '';
|
|
|
| for (const seg of segments) {
|
| if (seg.text.length > MAX_TEXT_LEN) {
|
| if (currentChunk) { chunks.push(currentChunk.trimEnd()); currentChunk = ''; }
|
| chunks.push(...hardSplit(seg.text));
|
| continue;
|
| }
|
| const candidate = currentChunk ? currentChunk + '\n\n' + seg.text : seg.text;
|
| if (candidate.length <= MAX_TEXT_LEN) {
|
| currentChunk = candidate;
|
| } else {
|
| if (currentChunk) chunks.push(currentChunk.trimEnd());
|
| currentChunk = seg.text;
|
| }
|
| }
|
| if (currentChunk) chunks.push(currentChunk.trimEnd());
|
|
|
| console.log(`[Convert] 多消息拆分 ${originalLen} → ${chunks.length} 条 [${chunks.map(c => c.length).join(', ')}]`);
|
| return chunks;
|
| }
|
|
|
| |
| |
|
|
| function hardSplit(text) {
|
| const chunks = [];
|
| let pos = 0;
|
| while (pos < text.length) {
|
| if (text.length - pos <= MAX_TEXT_LEN) {
|
| chunks.push(text.substring(pos));
|
| break;
|
| }
|
| let cut = pos + MAX_TEXT_LEN;
|
| const lastNL = text.lastIndexOf('\n', cut);
|
| if (lastNL > pos + 500) cut = lastNL;
|
| chunks.push(text.substring(pos, cut));
|
| pos = cut;
|
| while (pos < text.length && text[pos] === '\n') pos++;
|
| }
|
| return chunks;
|
| }
|
|
|
| |
| |
| |
| |
| |
|
|
| export function openaiToText(messages, tools, toolChoice) {
|
| if (!messages || !messages.length) return '';
|
|
|
| const system = messages.filter(m => m.role === 'system').map(m => extractText(m.content)).join('\n');
|
| const conversation = messages.filter(m => m.role !== 'system');
|
|
|
|
|
| const toolPrompt = serializeTools(tools, toolChoice);
|
|
|
|
|
| const processedConversation = [];
|
| for (const msg of conversation) {
|
| if (msg.role === 'tool') {
|
|
|
| const toolName = msg.name || msg.tool_call_id || 'unknown';
|
| processedConversation.push({
|
| role: 'user',
|
| content: `[Tool Result: ${toolName}]\n${extractText(msg.content)}`,
|
| });
|
| } else if (msg.role === 'assistant' && msg.tool_calls) {
|
|
|
| let callText = extractText(msg.content) || '';
|
| for (const tc of msg.tool_calls) {
|
| const fn = tc.function || {};
|
| callText += `\n\`\`\`tool_calls\n[{"name": "${fn.name}", "arguments": ${fn.arguments || '{}'}}]\n\`\`\``;
|
| }
|
| processedConversation.push({ role: 'assistant', content: callText.trim() });
|
| } else {
|
| processedConversation.push(msg);
|
| }
|
| }
|
|
|
| const fullSystem = system + toolPrompt;
|
|
|
|
|
| if (processedConversation.length === 1 && processedConversation[0].role === 'user') {
|
| const userText = extractText(processedConversation[0].content);
|
| return fullSystem ? `${fullSystem}\n\n${userText}` : userText;
|
| }
|
|
|
|
|
| let text = '';
|
| if (fullSystem) text += `[System] ${fullSystem}\n\n`;
|
|
|
| for (const msg of processedConversation) {
|
| const role = msg.role === 'assistant' ? 'Assistant' : 'User';
|
| text += `[${role}] ${extractText(msg.content)}\n\n`;
|
| }
|
|
|
| return text.trim();
|
| }
|
|
|
| |
| |
| |
| |
| |
| |
|
|
| export function anthropicToText(system, messages, tools, toolChoice) {
|
| if (!messages || !messages.length) return extractText(system) || '';
|
|
|
|
|
| const toolPrompt = serializeToolsAnthropic(tools, toolChoice);
|
| const systemText = extractText(system) || '';
|
| const fullSystem = systemText + toolPrompt;
|
|
|
|
|
| const processedMessages = [];
|
| for (const msg of messages) {
|
| if (Array.isArray(msg.content)) {
|
|
|
| const parts = [];
|
| for (const block of msg.content) {
|
| if (block.type === 'text') {
|
| parts.push(block.text);
|
| } else if (block.type === 'tool_use') {
|
| parts.push(`\`\`\`tool_calls\n[{"name": "${block.name}", "arguments": ${JSON.stringify(block.input || {})}}]\n\`\`\``);
|
| } else if (block.type === 'tool_result') {
|
| const resultContent = typeof block.content === 'string'
|
| ? block.content
|
| : Array.isArray(block.content)
|
| ? block.content.filter(c => c.type === 'text').map(c => c.text).join('\n')
|
| : JSON.stringify(block.content);
|
| parts.push(`[Tool Result: ${block.tool_use_id || 'unknown'}]\n${resultContent}`);
|
| }
|
| }
|
| processedMessages.push({ role: msg.role, content: parts.join('\n') });
|
| } else {
|
| processedMessages.push(msg);
|
| }
|
| }
|
|
|
|
|
| if (processedMessages.length === 1 && processedMessages[0].role === 'user') {
|
| const userText = typeof processedMessages[0].content === 'string'
|
| ? processedMessages[0].content
|
| : extractText(processedMessages[0].content);
|
| return fullSystem ? `${fullSystem}\n\n${userText}` : userText;
|
| }
|
|
|
|
|
| let text = '';
|
| if (fullSystem) text += `[System] ${fullSystem}\n\n`;
|
|
|
| for (const msg of processedMessages) {
|
| const role = msg.role === 'assistant' ? 'Assistant' : 'User';
|
| const content = typeof msg.content === 'string' ? msg.content : extractText(msg.content);
|
| text += `[${role}] ${content}\n\n`;
|
| }
|
|
|
| return text.trim();
|
| }
|
|
|
| |
| |
|
|
| export function resolveModel(requestModel) {
|
| if (!requestModel) return 'gpt-4o';
|
|
|
| if (config.modelMapping[requestModel]) return config.modelMapping[requestModel];
|
|
|
| const base = requestModel.replace(/-\d{8}$/, '');
|
| if (config.modelMapping[base]) return config.modelMapping[base];
|
|
|
| return requestModel;
|
| }
|
|
|
| |
| |
|
|
| function getOwner(model) {
|
| if (model.startsWith('gpt-') || model.startsWith('o1') || model.startsWith('o3') || model.startsWith('o4')) return 'OpenAI';
|
| if (model.startsWith('claude-')) return 'Anthropic';
|
| if (model.startsWith('gemini-')) return 'Google';
|
| return '其他';
|
| }
|
|
|
| |
| |
|
|
| export function getModelList() {
|
| const seen = new Set();
|
| const list = [];
|
| for (const target of Object.values(config.modelMapping)) {
|
| if (seen.has(target)) continue;
|
| seen.add(target);
|
| list.push({
|
| id: target,
|
| object: 'model',
|
| created: 1700000000,
|
| owned_by: getOwner(target),
|
| });
|
| }
|
| return list;
|
| }
|
|
|