Skip to content

Commit c7bd058

Browse files
committed
gpt-oss : simulate thinking and tool call tags
1 parent 767d099 commit c7bd058

File tree

2 files changed

+43
-38
lines changed

2 files changed

+43
-38
lines changed

common/chat.cpp

Lines changed: 39 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1423,12 +1423,6 @@ static void common_chat_parse_gpt_oss(common_chat_msg_parser & builder) {
14231423
static const common_regex user_tool_call_regex("(?: <\\|constrain\\|>([a-zA-Z]+))?<\\|message\\|>");
14241424
static const common_regex builtin_tool_call_regex("(?:browser|python)[\\s\\S]*<\\|message\\|>");
14251425

1426-
// Save the start of the message so we can roll back when we encounter a tool call and parse_tool_calls == false.
1427-
size_t message_start_pos = 0;
1428-
1429-
// Similarly, save the channel start so we can roll back to defer reasoning parsing to builder.
1430-
size_t channel_start_pos = 0;
1431-
14321426
auto consume_until_next = [&](size_t from = std::string::npos) {
14331427
if (auto res = builder.try_find_regex(start_regex, from, false)) {
14341428
auto begin = res->groups[0].begin;
@@ -1449,13 +1443,6 @@ static void common_chat_parse_gpt_oss(common_chat_msg_parser & builder) {
14491443
};
14501444

14511445
auto tool_call = [&](bool recipient_in_role) {
1452-
if (!builder.syntax().parse_tool_calls) {
1453-
// Move back to the start and consume up to the next message
1454-
builder.move_to(message_start_pos);
1455-
builder.add_content(consume_until_next(message_start_pos + 1));
1456-
return;
1457-
}
1458-
14591446
if (auto res = builder.try_consume_regex(function_regex)) {
14601447
auto name = builder.str(res->groups[1]);
14611448

@@ -1467,8 +1454,28 @@ static void common_chat_parse_gpt_oss(common_chat_msg_parser & builder) {
14671454

14681455
if (builder.try_consume_regex(user_tool_call_regex)) {
14691456
if (auto args = builder.try_consume_json_with_dumped_args({{}})) {
1470-
if (!builder.add_tool_call(name, "", args->value) || args->is_partial) {
1471-
throw common_chat_msg_partial_exception("incomplete tool call");
1457+
if (builder.syntax().parse_tool_calls) {
1458+
if (!builder.add_tool_call(name, "", args->value) || args->is_partial) {
1459+
throw common_chat_msg_partial_exception("incomplete tool call");
1460+
}
1461+
} else {
1462+
std::string args_as_string;
1463+
if (args->value.is_object()) {
1464+
args_as_string = args->value.dump();
1465+
} else {
1466+
args_as_string = args->value;
1467+
}
1468+
1469+
// simulate tool call in content
1470+
builder.add_content("<tool_call>");
1471+
builder.add_content("{\"name\": " + json(name).dump() + ", \"arguments\": ");
1472+
builder.add_content(args_as_string);
1473+
if (!args->is_partial) {
1474+
builder.add_content("}");
1475+
builder.add_content("</tool_call>");
1476+
} else {
1477+
throw common_chat_msg_partial_exception("incomplete tool call");
1478+
}
14721479
}
14731480
}
14741481
} else {
@@ -1494,13 +1501,25 @@ static void common_chat_parse_gpt_oss(common_chat_msg_parser & builder) {
14941501
if (builder.try_consume_regex(to_regex)) {
14951502
tool_call(false); // built-in tools can be called in the analysis channel
14961503
} else if (builder.try_consume_regex(message_regex)) {
1497-
// Defer reasoning parsing to builder
1498-
builder.move_to(channel_start_pos);
1504+
std::string reasoning;
1505+
bool has_end = false;
1506+
if (auto res = builder.try_find_regex(end_regex, std::string::npos, false)) {
1507+
reasoning = res->prelude;
1508+
has_end = true;
1509+
} else {
1510+
reasoning = builder.consume_rest();
1511+
}
14991512

1500-
if (builder.syntax().reasoning_format != COMMON_REASONING_FORMAT_NONE) {
1501-
builder.try_parse_reasoning("<|channel|>analysis<|message|>", "<|end|>");
1513+
if (builder.syntax().reasoning_format == COMMON_REASONING_FORMAT_NONE || builder.syntax().reasoning_in_content) {
1514+
// the templates raise an exception if <|channel|> is present
1515+
// an assistant's content, so wrap it in think tags
1516+
builder.add_content("<think>");
1517+
builder.add_content(reasoning);
1518+
if (has_end) {
1519+
builder.add_content("</think>");
1520+
}
15021521
} else {
1503-
builder.add_content(consume_until_next());
1522+
builder.add_reasoning_content(reasoning);
15041523
}
15051524
} else {
15061525
throw common_chat_msg_parse_exception("expected: <|message|>, got: " + consume_until_next());
@@ -1524,7 +1543,6 @@ static void common_chat_parse_gpt_oss(common_chat_msg_parser & builder) {
15241543

15251544
auto message = [&]() {
15261545
if (auto res = builder.try_consume_regex(channel_regex)) {
1527-
channel_start_pos = res->groups[0].begin;
15281546
channel(*res);
15291547
} else if (builder.try_consume_regex(to_regex)) {
15301548
tool_call(true);
@@ -1541,7 +1559,6 @@ static void common_chat_parse_gpt_oss(common_chat_msg_parser & builder) {
15411559

15421560
// Read in complete messages until done or partial exception raised
15431561
while (auto res = builder.try_consume_regex(start_regex)) {
1544-
message_start_pos = res->groups[0].begin;
15451562
try {
15461563
message();
15471564
} catch (const common_chat_msg_parse_exception & e) {

tests/test-chat.cpp

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1678,7 +1678,7 @@ static void test_template_output_parsers() {
16781678
}));
16791679
assert_msg_equals(
16801680
simple_assist_msg(
1681-
"<|start|>assistant<|channel|>commentary to=functions.special_function<|message|>{\"arg1",
1681+
"<tool_call>{\"name\": \"special_function\", \"arguments\": {\"arg1",
16821682
"I'm\nthinking"),
16831683
common_chat_parse(
16841684
"<|channel|>analysis<|message|>I'm\nthinking<|end|>"
@@ -1693,7 +1693,7 @@ static void test_template_output_parsers() {
16931693
}));
16941694
assert_msg_equals(
16951695
simple_assist_msg(
1696-
"<|start|>assistant<|channel|>commentary to=functions.special_function <|constrain|>json<|message|>{\"arg1\": 1}",
1696+
"<tool_call>{\"name\": \"special_function\", \"arguments\": {\"arg1\":1}}</tool_call>",
16971697
"I'm\nthinking"),
16981698
common_chat_parse(
16991699
"<|channel|>analysis<|message|>I'm\nthinking<|end|>"
@@ -1710,7 +1710,7 @@ static void test_template_output_parsers() {
17101710
// Test reasoning formats
17111711
assert_msg_equals(
17121712
simple_assist_msg(
1713-
"<|channel|>analysis<|message|>I'm\nthinking<|end|>Hello, world!\nWhat's up?"),
1713+
"<think>I'm\nthinking</think>Hello, world!\nWhat's up?"),
17141714
common_chat_parse(
17151715
"<|channel|>analysis<|message|>I'm\nthinking<|end|>"
17161716
"<|start|>assistant<|channel|>final<|message|>Hello, world!\nWhat's up?",
@@ -1720,18 +1720,6 @@ static void test_template_output_parsers() {
17201720
/* .reasoning_format = */ COMMON_REASONING_FORMAT_NONE,
17211721
}));
17221722

1723-
assert_msg_equals(
1724-
simple_assist_msg(
1725-
"Hello, world!\nWhat's up?", "I'm\nthinking"),
1726-
common_chat_parse(
1727-
"<|channel|>analysis<|message|>I'm\nthinking<|end|>"
1728-
"<|start|>assistant<|channel|>final<|message|>Hello, world!\nWhat's up?",
1729-
/* is_partial= */ false,
1730-
{
1731-
/* .format = */ COMMON_CHAT_FORMAT_GPT_OSS,
1732-
/* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK,
1733-
}));
1734-
17351723
assert_msg_equals(
17361724
simple_assist_msg(
17371725
"<think>I'm\nthinking</think>Hello, world!\nWhat's up?"),
@@ -1741,7 +1729,7 @@ static void test_template_output_parsers() {
17411729
/* is_partial= */ false,
17421730
{
17431731
/* .format = */ COMMON_CHAT_FORMAT_GPT_OSS,
1744-
/* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK,
1732+
/* .reasoning_format = */ COMMON_REASONING_FORMAT_AUTO,
17451733
/* .reasoning_in_content = */ true,
17461734
}));
17471735

0 commit comments

Comments
 (0)