// v1.10.5: XML-tag tool-call fallback. Some models emit
// value
// in plain content instead of using the OpenAI tool_calls JSON channel.
// The streaming loop in inference.ts extracts these blocks via these helpers.
export const XML_TOOL_OPEN = '';
export const XML_TOOL_CLOSE = '';
export function parseXmlToolCall(
block: string,
): { name: string; args: Record } | null {
const nameMatch = block.match(/]+)>/);
if (!nameMatch || !nameMatch[1]) return null;
const name = nameMatch[1].trim();
if (!name) return null;
const args: Record = {};
// Non-greedy body so each … pair is matched
// independently even when multiple appear in the same block.
const paramRe = /]+)>([\s\S]*?)<\/parameter>/g;
for (const m of block.matchAll(paramRe)) {
const key = (m[1] ?? '').trim();
if (!key) continue;
const raw = (m[2] ?? '').trim();
try {
args[key] = JSON.parse(raw);
} catch {
args[key] = raw;
}
}
return { name, args };
}
// Locate the first character that begins (or completely contains) an
// unfinished opener in `s`. Returns -1 when `s` can be flushed
// to the client in full without risking a partial tag leak.
// Case 1: a full `` opener with no matching closer — caller
// must keep everything from that index forward until the next
// chunk arrives with the closer.
// Case 2: `s` ends with a strict prefix of `` (e.g. `… pair before reaching this check.
export function partialXmlOpenerStart(s: string): number {
const fullOpener = s.indexOf(XML_TOOL_OPEN);
if (fullOpener !== -1) return fullOpener;
const lastLt = s.lastIndexOf('<');
if (lastLt === -1) return -1;
const suffix = s.slice(lastLt);
if (XML_TOOL_OPEN.startsWith(suffix) && suffix.length < XML_TOOL_OPEN.length) {
return lastLt;
}
return -1;
}