(raw: string, format: 'json' | 'find-all')
| 182 | // - 'json' → parse as JSON; if array, iterate; if object, treat as single |
| 183 | // - 'find-all' → scan for every balanced JSON object and try each |
| 184 | const tryExtractMultiple = (raw: string, format: 'json' | 'find-all'): ToolCall[] => { |
| 185 | const trimmed = raw.trim(); |
| 186 | if (!trimmed) return []; |
| 187 | if (format === 'json') { |
| 188 | let parsed: any; |
| 189 | try { |
| 190 | parsed = JSON.parse(trimmed); |
| 191 | } catch { |
| 192 | // Fallback: maybe the array has trailing junk — try first balanced object |
| 193 | const objText = findFirstJsonObject(trimmed); |
| 194 | if (!objText) return []; |
| 195 | try { parsed = JSON.parse(objText); } catch { return []; } |
| 196 | } |
| 197 | const out: ToolCall[] = []; |
| 198 | if (Array.isArray(parsed)) { |
| 199 | for (const item of parsed) { |
| 200 | const call = parsedObjectToToolCall(item, knownToolNames); |
| 201 | if (call) out.push(call); |
| 202 | } |
| 203 | } else { |
| 204 | const single = parsedObjectToToolCall(parsed, knownToolNames); |
| 205 | if (single) out.push(single); |
| 206 | } |
| 207 | return out; |
| 208 | } |
| 209 | // 'find-all' |
| 210 | const objects = findAllJsonObjects(trimmed); |
| 211 | const out: ToolCall[] = []; |
| 212 | for (const obj of objects) { |
| 213 | try { |
| 214 | const parsed = JSON.parse(obj.text); |
| 215 | const call = parsedObjectToToolCall(parsed, knownToolNames); |
| 216 | if (call) out.push(call); |
| 217 | } catch { |
| 218 | /* skip unparsable */ |
| 219 | } |
| 220 | } |
| 221 | return out; |
| 222 | }; |
| 223 | |
| 224 | // 1. Multi-call wrappers FIRST (Mistral [TOOL_CALLS], <tools> plural, DeepSeek-V3 |
| 225 | // `<|tool▁calls▁begin|>` wrapper). A wrapper can ENCLOSE singular XML markers |
no test coverage detected