Skip to content

Commit 6343a7f

Browse files
committed
gpt-oss : simulate thinking and tool call tags
1 parent 379b652 commit 6343a7f

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
@@ -1399,12 +1399,6 @@ static void common_chat_parse_gpt_oss(common_chat_msg_parser & builder) {
13991399
static const common_regex user_tool_call_regex("(?: <\\|constrain\\|>([a-zA-Z]+))?<\\|message\\|>");
14001400
static const common_regex builtin_tool_call_regex("(?:browser|python)[\\s\\S]*<\\|message\\|>");
14011401

1402-
// Save the start of the message so we can roll back when we encounter a tool call and parse_tool_calls == false.
1403-
size_t message_start_pos = 0;
1404-
1405-
// Similarly, save the channel start so we can roll back to defer reasoning parsing to builder.
1406-
size_t channel_start_pos = 0;
1407-
14081402
auto consume_until_next = [&](size_t from = std::string::npos) {
14091403
if (auto res = builder.try_find_regex(start_regex, from, false)) {
14101404
auto begin = res->groups[0].begin;
@@ -1425,13 +1419,6 @@ static void common_chat_parse_gpt_oss(common_chat_msg_parser & builder) {
14251419
};
14261420

14271421
auto tool_call = [&](bool recipient_in_role) {
1428-
if (!builder.syntax().parse_tool_calls) {
1429-
// Move back to the start and consume up to the next message
1430-
builder.move_to(message_start_pos);
1431-
builder.add_content(consume_until_next(message_start_pos + 1));
1432-
return;
1433-
}
1434-
14351422
if (auto res = builder.try_consume_regex(function_regex)) {
14361423
auto name = builder.str(res->groups[1]);
14371424

@@ -1443,8 +1430,28 @@ static void common_chat_parse_gpt_oss(common_chat_msg_parser & builder) {
14431430

14441431
if (builder.try_consume_regex(user_tool_call_regex)) {
14451432
if (auto args = builder.try_consume_json_with_dumped_args({{}})) {
1446-
if (!builder.add_tool_call(name, "", args->value) || args->is_partial) {
1447-
throw common_chat_msg_partial_exception("incomplete tool call");
1433+
if (builder.syntax().parse_tool_calls) {
1434+
if (!builder.add_tool_call(name, "", args->value) || args->is_partial) {
1435+
throw common_chat_msg_partial_exception("incomplete tool call");
1436+
}
1437+
} else {
1438+
std::string args_as_string;
1439+
if (args->value.is_object()) {
1440+
args_as_string = args->value.dump();
1441+
} else {
1442+
args_as_string = args->value;
1443+
}
1444+
1445+
// simulate tool call in content
1446+
builder.add_content("<tool_call>");
1447+
builder.add_content("{\"name\": " + json(name).dump() + ", \"arguments\": ");
1448+
builder.add_content(args_as_string);
1449+
if (!args->is_partial) {
1450+
builder.add_content("}");
1451+
builder.add_content("</tool_call>");
1452+
} else {
1453+
throw common_chat_msg_partial_exception("incomplete tool call");
1454+
}
14481455
}
14491456
}
14501457
} else {
@@ -1470,13 +1477,25 @@ static void common_chat_parse_gpt_oss(common_chat_msg_parser & builder) {
14701477
if (builder.try_consume_regex(to_regex)) {
14711478
tool_call(false); // built-in tools can be called in the analysis channel
14721479
} else if (builder.try_consume_regex(message_regex)) {
1473-
// Defer reasoning parsing to builder
1474-
builder.move_to(channel_start_pos);
1480+
std::string reasoning;
1481+
bool has_end = false;
1482+
if (auto res = builder.try_find_regex(end_regex, std::string::npos, false)) {
1483+
reasoning = res->prelude;
1484+
has_end = true;
1485+
} else {
1486+
reasoning = builder.consume_rest();
1487+
}
14751488

1476-
if (builder.syntax().reasoning_format != COMMON_REASONING_FORMAT_NONE) {
1477-
builder.try_parse_reasoning("<|channel|>analysis<|message|>", "<|end|>");
1489+
if (builder.syntax().reasoning_format == COMMON_REASONING_FORMAT_NONE || builder.syntax().reasoning_in_content) {
1490+
// the templates raise an exception if <|channel|> is present
1491+
// an assistant's content, so wrap it in think tags
1492+
builder.add_content("<think>");
1493+
builder.add_content(reasoning);
1494+
if (has_end) {
1495+
builder.add_content("</think>");
1496+
}
14781497
} else {
1479-
builder.add_content(consume_until_next());
1498+
builder.add_reasoning_content(reasoning);
14801499
}
14811500
} else {
14821501
throw common_chat_msg_parse_exception("expected: <|message|>, got: " + consume_until_next());
@@ -1500,7 +1519,6 @@ static void common_chat_parse_gpt_oss(common_chat_msg_parser & builder) {
15001519

15011520
auto message = [&]() {
15021521
if (auto res = builder.try_consume_regex(channel_regex)) {
1503-
channel_start_pos = res->groups[0].begin;
15041522
channel(*res);
15051523
} else if (builder.try_consume_regex(to_regex)) {
15061524
tool_call(true);
@@ -1517,7 +1535,6 @@ static void common_chat_parse_gpt_oss(common_chat_msg_parser & builder) {
15171535

15181536
// Read in complete messages until done or partial exception raised
15191537
while (auto res = builder.try_consume_regex(start_regex)) {
1520-
message_start_pos = res->groups[0].begin;
15211538
try {
15221539
message();
15231540
} 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)