export const TOOL_CALL_START = '<|tool_call_start|>'; export const TOOL_CALL_END = '<|tool_call_end|>'; const CONTROL_TOKENS = /<\|(?:im_start|im_end|tool_call_start|tool_call_end)\|>/g; export function parseToolCalls(rawText) { const calls = []; let cursor = 0; while (cursor < rawText.length) { const start = rawText.indexOf(TOOL_CALL_START, cursor); if (start < 0) break; const contentStart = start + TOOL_CALL_START.length; const end = rawText.indexOf(TOOL_CALL_END, contentStart); if (end < 0) throw new Error('The model emitted an incomplete tool call.'); calls.push(...parseCallList(rawText.slice(contentStart, end))); cursor = end + TOOL_CALL_END.length; } return calls; } export function displayTextFromRaw(rawText) { let output = ''; let cursor = 0; while (cursor < rawText.length) { const start = rawText.indexOf(TOOL_CALL_START, cursor); if (start < 0) { output += rawText.slice(cursor); break; } output += rawText.slice(cursor, start); const end = rawText.indexOf(TOOL_CALL_END, start + TOOL_CALL_START.length); if (end < 0) break; cursor = end + TOOL_CALL_END.length; } return output.replace(CONTROL_TOKENS, '').replace(/^assistant\s*\n/i, '').trim(); } export function createToolStreamFilter(onText, { onToolCallStart, onToolCallEnd } = {}) { let buffer = ''; let insideCall = false; const reserve = Math.max(TOOL_CALL_START.length, TOOL_CALL_END.length) - 1; function flush(force = false) { while (buffer) { const marker = insideCall ? TOOL_CALL_END : TOOL_CALL_START; const markerIndex = buffer.indexOf(marker); if (markerIndex >= 0) { if (!insideCall && markerIndex > 0) emit(buffer.slice(0, markerIndex)); buffer = buffer.slice(markerIndex + marker.length); insideCall = !insideCall; if (insideCall) onToolCallStart?.(); else onToolCallEnd?.(); continue; } const length = force ? buffer.length : Math.max(0, buffer.length - reserve); if (!length) break; if (!insideCall) emit(buffer.slice(0, length)); buffer = buffer.slice(length); } } function emit(value) { const clean = value.replace(CONTROL_TOKENS, ''); if (clean) onText(clean); } return { push(chunk) { buffer += chunk; flush(false); }, finish() { flush(true); }, }; } function parseCallList(source) { const trimmed = source.trim(); const body = trimmed.startsWith('[') && trimmed.endsWith(']') ? trimmed.slice(1, -1) : trimmed; if (!body.trim()) return []; return splitTopLevel(body).map(parseCall); } function parseCall(source) { const match = /^([A-Za-z_][A-Za-z0-9_]*)\s*\(([\s\S]*)\)$/.exec(source.trim()); if (!match) throw new Error(`Malformed tool call: ${source.trim().slice(0, 80)}`); const args = {}; const positional = []; for (const part of splitTopLevel(match[2])) { const assignment = findTopLevelAssignment(part); if (assignment < 0) positional.push(parsePythonValue(part)); else { const key = part.slice(0, assignment).trim(); if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) throw new Error(`Invalid argument name: ${key}`); if (Object.hasOwn(args, key)) throw new Error(`Duplicate argument: ${key}`); args[key] = parsePythonValue(part.slice(assignment + 1)); } } return { name: match[1], arguments: args, positional }; } function splitTopLevel(source) { if (!source.trim()) return []; const parts = []; let start = 0; let quote = ''; let escaped = false; const stack = []; for (let index = 0; index < source.length; index += 1) { const character = source[index]; if (quote) { if (escaped) escaped = false; else if (character === '\\') escaped = true; else if (character === quote) quote = ''; continue; } if (character === '"' || character === "'") { quote = character; continue; } if ('([{'.includes(character)) stack.push(character); else if (')]}'.includes(character)) stack.pop(); else if (character === ',' && stack.length === 0) { parts.push(source.slice(start, index).trim()); start = index + 1; } } if (quote || stack.length) throw new Error('Unbalanced tool-call arguments.'); const finalPart = source.slice(start).trim(); if (finalPart) parts.push(finalPart); return parts; } function findTopLevelAssignment(source) { let quote = ''; let escaped = false; let depth = 0; for (let index = 0; index < source.length; index += 1) { const character = source[index]; if (quote) { if (escaped) escaped = false; else if (character === '\\') escaped = true; else if (character === quote) quote = ''; } else if (character === '"' || character === "'") quote = character; else if ('([{'.includes(character)) depth += 1; else if (')]}'.includes(character)) depth -= 1; else if (character === '=' && depth === 0) return index; } return -1; } function parsePythonValue(source) { const value = source.trim(); if (!value) throw new Error('Missing tool argument value.'); if (value === 'True' || value === 'true') return true; if (value === 'False' || value === 'false') return false; if (value === 'None' || value === 'null') return null; if (/^-?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?$/i.test(value)) return Number(value); if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) { if (value[0] === '"') return JSON.parse(value); return value.slice(1, -1).replace(/\\(['\\nrt])/g, (_, escaped) => ({ "'": "'", '\\': '\\', n: '\n', r: '\r', t: '\t' })[escaped]); } if (value.startsWith('[') && value.endsWith(']')) return splitTopLevel(value.slice(1, -1)).map(parsePythonValue); if (value.startsWith('{') && value.endsWith('}')) { try { return JSON.parse(value); } catch { throw new Error('Object arguments must use valid JSON.'); } } throw new Error(`Unsupported tool argument value: ${value.slice(0, 80)}`); }