Parse {"name": ..., "arguments": ...} or {"function": {"name": ..., "arguments": ...}}
| 341 | return r; |
| 342 | } |
| 343 | |
| 344 | // Pattern 5: `call:<ns>?<verb>{` opener. The sentinel alternation in front |
| 345 | // rejects narrative usages like "I'll call:foo{x:1}" where `call:` is glued |
| 346 | // to a preceding word — whitespace, common punctuation, and open/close |
| 347 | // brackets are the realistic boundaries seen in the snapshot data. `\s` |
| 348 | // covers `\n` so a `call:` at the start of any line is matched without |
| 349 | // relying on std::regex multiline support (which is non-portable). |
| 350 | // |
| 351 | // Note that `}` is in the sentinel list — gemma frequently emits multiple |
| 352 | // invocations back-to-back: `call:a{x:1}call:b{y:2}`. Without `}` as a |
| 353 | // sentinel the second match would be missed. |
| 354 | // |
| 355 | // `_` is also in the sentinel list to handle a SentencePiece / chat-template |
| 356 | // artifact: post-bragi-channel-routing (commit 4b757d1) the gemma server |
| 357 | // occasionally emits raw tokens like `_call:get_country_info{...}` where |
| 358 | // the leading `_` is residual tokenizer serialization. Without `_` here |
| 359 | // the parser misses every such invocation — empirically confirmed against |
| 360 | // gemma-4-26b 2026-05-31 smoke test. Tradeoff: `my_call:foo{}` mid- |
| 361 | // identifier could match, but real model output doesn't emit `my_call:` |
| 362 | // strings (tool names come from the request's tool definitions). |
| 363 | static const std::regex & re_call_verb_open() { |
| 364 | static std::regex r(R"((^|[\s,;:\(\[\{\}\)\]\>_])call:([A-Za-z0-9_.:\-]+)\s*\{)"); |
| 365 | return r; |
| 366 | } |
| 367 | |
| 368 | // Find the index one past the `}` that matches `text[open] == '{'`. |
| 369 | // Respects nested {}/[] depth and skips over "..." / '...' / `...` |
| 370 | // string literals (with backslash escapes). Returns std::string::npos if |
| 371 | // no matching close is found. |
| 372 | static size_t balanced_braces_end(const std::string & text, size_t open) { |
| 373 | int depth = 0; |
| 374 | char in_str = 0; // 0, or one of '"', '\'', '`' |
| 375 | for (size_t i = open; i < text.size(); i++) { |
| 376 | char c = text[i]; |
| 377 | if (in_str) { |
| 378 | if (c == '\\' && i + 1 < text.size()) { i++; continue; } |
| 379 | if (c == in_str) in_str = 0; |
| 380 | continue; |
| 381 | } |
| 382 | if (c == '"' || c == '\'' || c == '`') { in_str = c; continue; } |
| 383 | if (c == '{' || c == '[') { |
| 384 | depth++; |
| 385 | } else if (c == '}' || c == ']') { |
| 386 | depth--; |
no test coverage detected