Skip to content

Commit 1e595d2

Browse files
committed
gpt-oss : simplify parsing
1 parent da23e9b commit 1e595d2

File tree

3 files changed

+61
-264
lines changed

3 files changed

+61
-264
lines changed

common/chat-parser.h

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,6 @@ class common_chat_msg_partial_exception : public std::runtime_error {
1515
common_chat_msg_partial_exception(const std::string & message) : std::runtime_error(message) {}
1616
};
1717

18-
class common_chat_msg_parse_exception : public std::runtime_error {
19-
public:
20-
common_chat_msg_parse_exception(const std::string & message) : std::runtime_error(message) {}
21-
};
22-
2318
class common_chat_msg_parser {
2419
std::string input_;
2520
bool is_partial_;

common/chat.cpp

Lines changed: 59 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -1413,150 +1413,91 @@ static common_chat_params common_chat_params_init_gpt_oss(const common_chat_temp
14131413
return data;
14141414
}
14151415
static void common_chat_parse_gpt_oss(common_chat_msg_parser & builder) {
1416-
static const common_regex message_regex("<\\|message\\|>");
1417-
static const common_regex channel_regex("<\\|channel\\|>(final|analysis|commentary)");
1418-
static const common_regex tool_call_channel_regex("<\\|channel\\|>(commentary|analysis)");
1416+
static const std::string constraint = "(?: (<\\|constrain\\|>)?([a-zA-Z0-9_-]+))";
1417+
static const std::string recipient("(?: to=functions\\.([^<\\s]+))");
1418+
14191419
static const common_regex start_regex("<\\|start\\|>assistant");
1420-
static const common_regex end_regex("<\\|end\\|>");
1421-
static const common_regex to_regex(" to=");
1422-
static const common_regex function_regex("functions\\.([^<\\s]+)");
1423-
static const common_regex user_tool_call_regex("(?: <\\|constrain\\|>([a-zA-Z]+))?<\\|message\\|>");
1424-
static const common_regex builtin_tool_call_regex("(?:browser|python)[\\s\\S]*<\\|message\\|>");
1425-
1426-
// Save the channel start so we can roll back to delegate reasoning parsing to builder.
1427-
size_t channel_start_pos = 0;
1428-
1429-
auto consume_until_next = [&](size_t from = std::string::npos) {
1430-
if (auto res = builder.try_find_regex(start_regex, from, false)) {
1431-
auto begin = res->groups[0].begin;
1432-
builder.move_to(begin);
1433-
return res->prelude;
1420+
static const common_regex analysis_regex("<\\|channel\\|>analysis");
1421+
static const common_regex final_regex("<\\|channel\\|>final" + constraint + "?");
1422+
static const common_regex preamble_regex("<\\|channel\\|>commentary");
1423+
static const common_regex tool_call1_regex(recipient + "<\\|channel\\|>(analysis|commentary)" + constraint + "?");
1424+
static const common_regex tool_call2_regex("<\\|channel\\|>(analysis|commentary)" + recipient + constraint + "?");
1425+
1426+
auto consume_end = [&](bool include_end = false) {
1427+
if (auto res = builder.try_find_literal("<|end|>")) {
1428+
return res->prelude + (include_end ? builder.str(res->groups[0]) : "");
14341429
}
14351430
return builder.consume_rest();
14361431
};
14371432

1438-
auto try_consume_message = [&]() {
1439-
if (builder.try_consume_regex(message_regex)) {
1440-
if (!builder.try_find_regex(end_regex)) {
1441-
builder.add_content(builder.consume_rest());
1433+
auto handle_tool_call = [&](const std::string & name) {
1434+
if (auto args = builder.try_consume_json_with_dumped_args({{}})) {
1435+
if (builder.syntax().parse_tool_calls) {
1436+
if (!builder.add_tool_call(name, "", args->value) || args->is_partial) {
1437+
throw common_chat_msg_partial_exception("incomplete tool call");
1438+
}
1439+
} else if (args->is_partial) {
1440+
throw common_chat_msg_partial_exception("incomplete tool call");
14421441
}
1443-
return true;
14441442
}
1445-
return false;
14461443
};
14471444

1448-
auto tool_call = [&](bool recipient_in_role) {
1449-
if (auto res = builder.try_consume_regex(function_regex)) {
1450-
auto name = builder.str(res->groups[1]);
1445+
auto regex_match = [](const common_regex & regex, const std::string & input) -> std::optional<common_regex_match> {
1446+
auto match = regex.search(input, 0, true);
1447+
if (match.type == COMMON_REGEX_MATCH_TYPE_FULL) {
1448+
return match;
1449+
}
1450+
return std::nullopt;
1451+
};
14511452

1452-
if (recipient_in_role) {
1453-
if (!builder.try_consume_regex(tool_call_channel_regex)) {
1454-
throw common_chat_msg_parse_exception("expected <|channel|>(commentary|analysis), got: " + consume_until_next());
1455-
}
1456-
}
1453+
do {
1454+
auto header_start_pos = builder.pos();
1455+
auto content_start = builder.try_find_literal("<|message|>");
1456+
if (!content_start) {
1457+
throw common_chat_msg_partial_exception("incomplete header");
1458+
}
14571459

1458-
if (builder.try_consume_regex(user_tool_call_regex)) {
1459-
if (auto args = builder.try_consume_json_with_dumped_args({{}})) {
1460-
if (builder.syntax().parse_tool_calls) {
1461-
if (!builder.add_tool_call(name, "", args->value) || args->is_partial) {
1462-
throw common_chat_msg_partial_exception("incomplete tool call");
1463-
}
1464-
} else {
1465-
std::string args_as_string;
1466-
if (args->value.is_object()) {
1467-
args_as_string = args->value.dump();
1468-
} else {
1469-
args_as_string = args->value;
1470-
}
1460+
auto header = content_start->prelude;
14711461

1472-
// simulate tool call in content
1473-
builder.add_content("<tool_call>");
1474-
builder.add_content("{\"name\": " + json(name).dump() + ", \"arguments\": ");
1475-
builder.add_content(args_as_string);
1476-
if (!args->is_partial) {
1477-
builder.add_content("}");
1478-
builder.add_content("</tool_call>");
1479-
} else {
1480-
throw common_chat_msg_partial_exception("incomplete tool call");
1481-
}
1482-
}
1483-
}
1484-
} else {
1485-
throw common_chat_msg_parse_exception("expected function args, got: " + consume_until_next());
1486-
}
1487-
} else if (builder.try_consume_regex(builtin_tool_call_regex)) {
1488-
builder.consume_rest();
1489-
LOG_ERR("builtin tool calls not implemented\n");
1490-
} else {
1491-
throw common_chat_msg_parse_exception("expected function name, got: " + consume_until_next());
1462+
if (auto match = regex_match(tool_call1_regex, header)) {
1463+
auto group = match->groups[1];
1464+
auto name = header.substr(group.begin, group.end - group.begin);
1465+
handle_tool_call(name);
1466+
continue;
14921467
}
1493-
};
14941468

1495-
auto commentary = [&]() {
1496-
if (builder.try_consume_regex(to_regex)) {
1497-
tool_call(false);
1498-
} else if (!try_consume_message()) {
1499-
throw common_chat_msg_parse_exception("expected: \" to=\" or <|message|>, got: " + consume_until_next());
1469+
if (auto match = regex_match(tool_call2_regex, header)) {
1470+
auto group = match->groups[2];
1471+
auto name = header.substr(group.begin, group.end - group.begin);
1472+
handle_tool_call(name);
1473+
continue;
15001474
}
1501-
};
15021475

1503-
auto analysis = [&]() {
1504-
if (builder.try_consume_regex(to_regex)) {
1505-
tool_call(false); // built-in tools can be called in the analysis channel
1506-
} else if (builder.try_consume_regex(message_regex)) {
1507-
builder.move_to(channel_start_pos);
1476+
if (regex_match(analysis_regex, header)) {
1477+
builder.move_to(header_start_pos);
15081478
if (builder.syntax().reasoning_format == COMMON_REASONING_FORMAT_NONE || builder.syntax().reasoning_in_content) {
1509-
builder.add_content(consume_until_next());
1479+
builder.add_content(consume_end(true));
15101480
} else {
15111481
builder.try_parse_reasoning("<|channel|>analysis<|message|>", "<|end|>");
15121482
}
1513-
} else {
1514-
throw common_chat_msg_parse_exception("expected: <|message|>, got: " + consume_until_next());
1515-
}
1516-
};
1517-
1518-
auto channel = [&](const common_chat_msg_parser::find_regex_result & match) {
1519-
auto type = builder.str(match.groups[1]);
1520-
if (type == "analysis") {
1521-
analysis();
1522-
} else if (type == "commentary") {
1523-
commentary();
1524-
} else if (type == "final") {
1525-
if (!try_consume_message()) {
1526-
throw common_chat_msg_parse_exception("expected: <|message|>, got: " + consume_until_next());
1527-
}
1528-
} else {
1529-
throw common_chat_msg_parse_exception("expected one of: [analysis, commentary, final], got: " + consume_until_next());
1483+
continue;
15301484
}
1531-
};
15321485

1533-
auto message = [&]() {
1534-
if (auto res = builder.try_consume_regex(channel_regex)) {
1535-
channel_start_pos = res->groups[0].begin;
1536-
channel(*res);
1537-
} else if (builder.try_consume_regex(to_regex)) {
1538-
tool_call(true);
1539-
} else {
1540-
throw common_chat_msg_parse_exception("expected: <|channel|> or \" to\", got: " + consume_until_next());
1486+
if(regex_match(final_regex, header) || regex_match(preamble_regex, header)) {
1487+
builder.add_content(consume_end());
1488+
continue;
15411489
}
1542-
};
15431490

1544-
try {
1545-
message();
1546-
} catch (const common_chat_msg_parse_exception & e) {
1547-
LOG_DBG("Parse error: %s\n", e.what());
1548-
}
1491+
// Possibly a malformed message, attempt to recover by rolling
1492+
// back to pick up the next <|start|>
1493+
LOG_DBG("%s: unknown header from message: %s\n", __func__, header.c_str());
1494+
builder.move_to(header_start_pos);
1495+
} while (builder.try_find_regex(start_regex, std::string::npos, false));
15491496

1550-
// Read in complete messages until done or partial exception raised
1551-
while (auto res = builder.try_consume_regex(start_regex)) {
1552-
try {
1553-
message();
1554-
} catch (const common_chat_msg_parse_exception & e) {
1555-
LOG_DBG("Parse error: %s\n", e.what());
1556-
}
1497+
auto remaining = builder.consume_rest();
1498+
if (!remaining.empty()) {
1499+
LOG_DBG("%s: content after last message: %s\n", __func__, remaining.c_str());
15571500
}
1558-
1559-
builder.consume_rest();
15601501
}
15611502

15621503
static common_chat_params common_chat_params_init_firefunction_v2(const common_chat_template & tmpl, const struct templates_params & inputs) {

tests/test-chat.cpp

Lines changed: 2 additions & 141 deletions
Original file line numberDiff line numberDiff line change
@@ -1446,33 +1446,6 @@ static void test_template_output_parsers() {
14461446
assert_equals(COMMON_CHAT_FORMAT_GPT_OSS, common_chat_templates_apply(tmpls.get(), inputs_no_tools).format);
14471447
assert_equals(COMMON_CHAT_FORMAT_GPT_OSS, common_chat_templates_apply(tmpls.get(), inputs_tools).format);
14481448

1449-
assert_msg_equals(message_assist_empty,
1450-
common_chat_parse(
1451-
"<|channel|>",
1452-
/* is_partial= */ true,
1453-
{
1454-
/* .format = */ COMMON_CHAT_FORMAT_GPT_OSS,
1455-
/* .reasoning_format = */ COMMON_REASONING_FORMAT_AUTO,
1456-
}));
1457-
1458-
assert_msg_equals(message_assist_empty,
1459-
common_chat_parse(
1460-
"<|channel|>analysis",
1461-
/* is_partial= */ true,
1462-
{
1463-
/* .format = */ COMMON_CHAT_FORMAT_GPT_OSS,
1464-
/* .reasoning_format = */ COMMON_REASONING_FORMAT_AUTO,
1465-
}));
1466-
1467-
assert_msg_equals(message_assist_empty,
1468-
common_chat_parse(
1469-
"<|channel|>analysis<|message|>",
1470-
/* is_partial= */ true,
1471-
{
1472-
/* .format = */ COMMON_CHAT_FORMAT_GPT_OSS,
1473-
/* .reasoning_format = */ COMMON_REASONING_FORMAT_AUTO,
1474-
}));
1475-
14761449
assert_msg_equals(simple_assist_msg("", "I'm\nthink"),
14771450
common_chat_parse(
14781451
"<|channel|>analysis<|message|>I'm\nthink",
@@ -1489,69 +1462,6 @@ static void test_template_output_parsers() {
14891462
/* .format = */ COMMON_CHAT_FORMAT_GPT_OSS,
14901463
/* .reasoning_format = */ COMMON_REASONING_FORMAT_AUTO,
14911464
}));
1492-
assert_msg_equals(simple_assist_msg("", "I'm\nthinking"),
1493-
common_chat_parse(
1494-
"<|channel|>analysis<|message|>I'm\nthinking<|end|>"
1495-
"<|start|>",
1496-
/* is_partial= */ true,
1497-
{
1498-
/* .format = */ COMMON_CHAT_FORMAT_GPT_OSS,
1499-
/* .reasoning_format = */ COMMON_REASONING_FORMAT_AUTO,
1500-
}));
1501-
assert_msg_equals(simple_assist_msg("", "I'm\nthinking"),
1502-
common_chat_parse(
1503-
"<|channel|>analysis<|message|>I'm\nthinking<|end|>"
1504-
"<|start|>assistant",
1505-
/* is_partial= */ true,
1506-
{
1507-
/* .format = */ COMMON_CHAT_FORMAT_GPT_OSS,
1508-
/* .reasoning_format = */ COMMON_REASONING_FORMAT_AUTO,
1509-
}));
1510-
assert_msg_equals(simple_assist_msg("", "I'm\nthinking"),
1511-
common_chat_parse(
1512-
"<|channel|>analysis<|message|>I'm\nthinking<|end|>"
1513-
"<|start|>assistant<|channel|>",
1514-
/* is_partial= */ true,
1515-
{
1516-
/* .format = */ COMMON_CHAT_FORMAT_GPT_OSS,
1517-
/* .reasoning_format = */ COMMON_REASONING_FORMAT_AUTO,
1518-
}));
1519-
assert_msg_equals(simple_assist_msg("", "I'm\nthinking"),
1520-
common_chat_parse(
1521-
"<|channel|>analysis<|message|>I'm\nthinking<|end|>"
1522-
"<|start|>assistant<|channel|>final",
1523-
/* is_partial= */ true,
1524-
{
1525-
/* .format = */ COMMON_CHAT_FORMAT_GPT_OSS,
1526-
/* .reasoning_format = */ COMMON_REASONING_FORMAT_AUTO,
1527-
}));
1528-
assert_msg_equals(simple_assist_msg("", "I'm\nthinking"),
1529-
common_chat_parse(
1530-
"<|channel|>analysis<|message|>I'm\nthinking<|end|>"
1531-
"<|start|>assistant<|channel|>final<|message|>",
1532-
/* is_partial= */ true,
1533-
{
1534-
/* .format = */ COMMON_CHAT_FORMAT_GPT_OSS,
1535-
/* .reasoning_format = */ COMMON_REASONING_FORMAT_AUTO,
1536-
}));
1537-
assert_msg_equals(simple_assist_msg("Hello", "I'm\nthinking"),
1538-
common_chat_parse(
1539-
"<|channel|>analysis<|message|>I'm\nthinking<|end|>"
1540-
"<|start|>assistant<|channel|>final<|message|>Hello",
1541-
/* is_partial= */ true,
1542-
{
1543-
/* .format = */ COMMON_CHAT_FORMAT_GPT_OSS,
1544-
/* .reasoning_format = */ COMMON_REASONING_FORMAT_AUTO,
1545-
}));
1546-
assert_msg_equals(simple_assist_msg("Hello, world!\nWhat's up?", "I'm\nthinking"),
1547-
common_chat_parse(
1548-
"<|channel|>analysis<|message|>I'm\nthinking<|end|>"
1549-
"<|start|>assistant<|channel|>final<|message|>Hello, world!\nWhat's up?",
1550-
/* is_partial= */ true,
1551-
{
1552-
/* .format = */ COMMON_CHAT_FORMAT_GPT_OSS,
1553-
/* .reasoning_format = */ COMMON_REASONING_FORMAT_AUTO,
1554-
}));
15551465
assert_msg_equals(simple_assist_msg("Hello, world!\nWhat's up?", "I'm\nthinking"),
15561466
common_chat_parse(
15571467
"<|channel|>analysis<|message|>I'm\nthinking<|end|>"
@@ -1561,51 +1471,6 @@ static void test_template_output_parsers() {
15611471
/* .format = */ COMMON_CHAT_FORMAT_GPT_OSS,
15621472
/* .reasoning_format = */ COMMON_REASONING_FORMAT_AUTO,
15631473
}));
1564-
assert_msg_equals(simple_assist_msg("", "I'm\nthinking"),
1565-
common_chat_parse(
1566-
"<|channel|>analysis<|message|>I'm\nthinking<|end|>"
1567-
"<|start|>",
1568-
/* is_partial= */ true,
1569-
{
1570-
/* .format = */ COMMON_CHAT_FORMAT_GPT_OSS,
1571-
/* .reasoning_format = */ COMMON_REASONING_FORMAT_AUTO,
1572-
}));
1573-
assert_msg_equals(simple_assist_msg("", "I'm\nthinking"),
1574-
common_chat_parse(
1575-
"<|channel|>analysis<|message|>I'm\nthinking<|end|>"
1576-
"<|start|>assistant",
1577-
/* is_partial= */ true,
1578-
{
1579-
/* .format = */ COMMON_CHAT_FORMAT_GPT_OSS,
1580-
/* .reasoning_format = */ COMMON_REASONING_FORMAT_AUTO,
1581-
}));
1582-
assert_msg_equals(simple_assist_msg("", "I'm\nthinking"),
1583-
common_chat_parse(
1584-
"<|channel|>analysis<|message|>I'm\nthinking<|end|>"
1585-
"<|start|>assistant<|channel|>",
1586-
/* is_partial= */ true,
1587-
{
1588-
/* .format = */ COMMON_CHAT_FORMAT_GPT_OSS,
1589-
/* .reasoning_format = */ COMMON_REASONING_FORMAT_AUTO,
1590-
}));
1591-
assert_msg_equals(simple_assist_msg("", "I'm\nthinking"),
1592-
common_chat_parse(
1593-
"<|channel|>analysis<|message|>I'm\nthinking<|end|>"
1594-
"<|start|>assistant<|channel|>commentary",
1595-
/* is_partial= */ true,
1596-
{
1597-
/* .format = */ COMMON_CHAT_FORMAT_GPT_OSS,
1598-
/* .reasoning_format = */ COMMON_REASONING_FORMAT_AUTO,
1599-
}));
1600-
assert_msg_equals(simple_assist_msg("", "I'm\nthinking"),
1601-
common_chat_parse(
1602-
"<|channel|>analysis<|message|>I'm\nthinking<|end|>"
1603-
"<|start|>assistant<|channel|>commentary",
1604-
/* is_partial= */ true,
1605-
{
1606-
/* .format = */ COMMON_CHAT_FORMAT_GPT_OSS,
1607-
/* .reasoning_format = */ COMMON_REASONING_FORMAT_AUTO,
1608-
}));
16091474
assert_msg_equals(simple_assist_msg("", "I'm\nthinking", "special_function", "{\"arg1"),
16101475
common_chat_parse(
16111476
"<|channel|>analysis<|message|>I'm\nthinking<|end|>"
@@ -1677,9 +1542,7 @@ static void test_template_output_parsers() {
16771542
/* .parse_tool_calls = */ false,
16781543
}));
16791544
assert_msg_equals(
1680-
simple_assist_msg(
1681-
"<tool_call>{\"name\": \"special_function\", \"arguments\": {\"arg1",
1682-
"I'm\nthinking"),
1545+
simple_assist_msg("", "I'm\nthinking"),
16831546
common_chat_parse(
16841547
"<|channel|>analysis<|message|>I'm\nthinking<|end|>"
16851548
"<|start|>assistant<|channel|>commentary to=functions.special_function<|message|>{\"arg1",
@@ -1692,9 +1555,7 @@ static void test_template_output_parsers() {
16921555
/* .parse_tool_calls = */ false,
16931556
}));
16941557
assert_msg_equals(
1695-
simple_assist_msg(
1696-
"<tool_call>{\"name\": \"special_function\", \"arguments\": {\"arg1\":1}}</tool_call>",
1697-
"I'm\nthinking"),
1558+
simple_assist_msg("", "I'm\nthinking"),
16981559
common_chat_parse(
16991560
"<|channel|>analysis<|message|>I'm\nthinking<|end|>"
17001561
"<|start|>assistant<|channel|>commentary to=functions.special_function <|constrain|>json<|message|>{\"arg1\": 1}",

0 commit comments

Comments
 (0)