| 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)}`); |
| } |
|
|