Skip to content

Commit f7638ea

Browse files
committed
Reasoning and tool-calling support for Seed OSS
1 parent b730706 commit f7638ea

File tree

4 files changed

+472
-4
lines changed

4 files changed

+472
-4
lines changed

common/chat.cpp

Lines changed: 187 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -622,6 +622,7 @@ const char * common_chat_format_name(common_chat_format format) {
622622
case COMMON_CHAT_FORMAT_COMMAND_R7B: return "Command R7B";
623623
case COMMON_CHAT_FORMAT_GRANITE: return "Granite";
624624
case COMMON_CHAT_FORMAT_GPT_OSS: return "GPT-OSS";
625+
case COMMON_CHAT_FORMAT_SEED_OSS: return "Seed-OSS";
625626
default:
626627
throw std::runtime_error("Unknown chat format");
627628
}
@@ -2059,6 +2060,130 @@ static void common_chat_parse_granite(common_chat_msg_parser & builder) {
20592060
}
20602061
}
20612062

2063+
static void purge_healing_marker(json & json_obj, const std::string & healing_marker) {
2064+
if (json_obj.is_array()) {
2065+
// Check if the last element is the healing marker and remove it
2066+
if (!json_obj.empty() && json_obj.back().is_string() && json_obj.back() == healing_marker) {
2067+
json_obj.erase(json_obj.size() - 1);
2068+
}
2069+
} else if (json_obj.is_object()) {
2070+
// Remove healing marker if it's a key
2071+
if (json_obj.contains(healing_marker)) {
2072+
json_obj.erase(healing_marker);
2073+
}
2074+
2075+
// Remove healing marker if it's a value
2076+
for (auto it = json_obj.begin(); it != json_obj.end();) {
2077+
if (it.value().is_string() && it.value() == healing_marker) {
2078+
it = json_obj.erase(it);
2079+
} else {
2080+
++it;
2081+
}
2082+
}
2083+
}
2084+
}
2085+
2086+
static void common_chat_parse_seed_oss(common_chat_msg_parser & builder) {
2087+
// Parse thinking tags first - this handles the main reasoning content
2088+
builder.try_parse_reasoning("<seed:think>", "</seed:think>");
2089+
2090+
if (!builder.syntax().parse_tool_calls) {
2091+
builder.add_content(builder.consume_rest());
2092+
return;
2093+
}
2094+
2095+
// Parse tool calls - Seed-OSS uses <seed:tool_call> format
2096+
static const common_regex tool_call_begin_regex("<seed:tool_call>");
2097+
static const common_regex tool_call_end_regex("</seed:tool_call>");
2098+
static const common_regex function_regex("<function=([^>]+)>");
2099+
static const common_regex close_function_regex("</function>");
2100+
2101+
while (auto tool_res = builder.try_find_regex(tool_call_begin_regex)) {
2102+
builder.consume_spaces(); // Consume whitespace after <seed:tool_call>
2103+
2104+
// Look for function call inside tool call, ignore any content before it
2105+
if (auto func_res = builder.try_find_regex(function_regex, std::string::npos, false)) {
2106+
auto function_name = builder.str(func_res->groups[1]);
2107+
2108+
// Parse Seed-OSS parameters <parameter=name>value</parameter>
2109+
json args = json::object();
2110+
static const common_regex param_regex("<parameter=([^>]+)>");
2111+
// Parse all parameters
2112+
while (auto param_res = builder.try_find_regex(param_regex, std::string::npos, false)) {
2113+
// again, ignore noise around parameters
2114+
auto param_name = builder.str(param_res->groups[1]);
2115+
builder.move_to(param_res->groups[0].end);
2116+
builder.consume_spaces(); // Consume whitespace after parameter
2117+
auto savedPos = builder.pos();
2118+
if (auto param_parse = builder.try_find_literal("</parameter>")) {
2119+
auto param = param_parse->prelude;
2120+
builder.move_to(savedPos);
2121+
try {
2122+
if (auto param_res = builder.try_consume_json()) {
2123+
args[param_name] = param_res->json;
2124+
} else {
2125+
args[param_name] = param;
2126+
}
2127+
} catch (json::exception &) {
2128+
args[param_name] = param;
2129+
}
2130+
} else {
2131+
try {
2132+
if (auto param_res = builder.try_consume_json()) {
2133+
auto json_obj = param_res->json;
2134+
purge_healing_marker(json_obj, builder.healing_marker());
2135+
if (!json_obj.is_null()) {
2136+
args[param_name] = json_obj;
2137+
}
2138+
} else {
2139+
auto rest = builder.consume_rest();
2140+
args[param_name] = rest;
2141+
}
2142+
} catch (json::exception &) {
2143+
auto rest = builder.consume_rest();
2144+
args[param_name] = rest;
2145+
}
2146+
builder.add_tool_call(function_name, "", args.dump());
2147+
throw common_chat_msg_partial_exception("Incomplete tool parameter");
2148+
}
2149+
}
2150+
// Look for closing function tag
2151+
auto end_func = builder.try_find_literal("</function>");
2152+
if (end_func) {
2153+
builder.move_to(end_func->groups[0].end);
2154+
builder.consume_spaces(); // Consume whitespace after </function>
2155+
2156+
// Add the tool call with parsed arguments
2157+
if (!builder.add_tool_call(function_name, "", args.dump())) {
2158+
throw common_chat_msg_partial_exception("Incomplete tool call");
2159+
}
2160+
} else {
2161+
builder.add_tool_call(function_name, "", args.dump()); // add partial tool parse
2162+
throw common_chat_msg_partial_exception("Incomplete tool call");
2163+
}
2164+
2165+
// Look for closing tool call tag
2166+
if (auto end_tool = builder.try_find_regex(tool_call_end_regex, std::string::npos, false)) {
2167+
builder.move_to(end_tool->groups[0].end);
2168+
builder.consume_spaces(); // Consume trailing whitespace after tool call
2169+
} else {
2170+
throw common_chat_msg_partial_exception("Incomplete tool call");
2171+
}
2172+
} else {
2173+
// No function found - don't consume content here, let it be handled at the end
2174+
break;
2175+
}
2176+
}
2177+
2178+
// Consume any remaining whitespace after all tool call processing
2179+
builder.consume_spaces();
2180+
auto remaining = builder.consume_rest();
2181+
// If there's any non-whitespace content remaining, add it as content
2182+
if (!string_strip(remaining).empty()) {
2183+
builder.add_content(remaining);
2184+
}
2185+
}
2186+
20622187
static common_chat_params common_chat_params_init_without_tools(const common_chat_template & tmpl, const struct templates_params & inputs) {
20632188
common_chat_params data;
20642189
data.prompt = apply(tmpl, inputs);
@@ -2075,10 +2200,60 @@ static common_chat_params common_chat_params_init_without_tools(const common_cha
20752200
return data;
20762201
}
20772202

2078-
static common_chat_params common_chat_templates_apply_jinja(
2079-
const struct common_chat_templates * tmpls,
2080-
const struct common_chat_templates_inputs & inputs)
2081-
{
2203+
static common_chat_params common_chat_params_init_seed_oss( const common_chat_template & tmpl,
2204+
templates_params & params,
2205+
const common_chat_templates_inputs & inputs) {
2206+
common_chat_params data;
2207+
data.prompt = apply(tmpl, params);
2208+
data.format = COMMON_CHAT_FORMAT_SEED_OSS;
2209+
if (string_ends_with(data.prompt, "<seed:think>")) {
2210+
if (!inputs.enable_thinking) {
2211+
data.prompt += "</seed:think>";
2212+
} else {
2213+
data.thinking_forced_open = true;
2214+
}
2215+
}
2216+
2217+
if (params.tools.is_array() && !params.tools.empty()) {
2218+
data.grammar_lazy = inputs.tool_choice != COMMON_CHAT_TOOL_CHOICE_REQUIRED;
2219+
data.grammar = build_grammar([&](const common_grammar_builder & builder) {
2220+
std::vector<std::string> tool_rules;
2221+
foreach_function(params.tools, [&](const json & tool) {
2222+
const auto & function = tool.at("function");
2223+
std::string name = function.at("name");
2224+
auto parameters = function.at("parameters");
2225+
builder.resolve_refs(parameters);
2226+
2227+
// Create rule for Seed-OSS function call format
2228+
std::string param_rules;
2229+
if (parameters.contains("properties")) {
2230+
for (const auto & [key, value] : parameters.at("properties").items()) {
2231+
param_rules += "<parameter=" + key + ">" + builder.add_schema(name + "-arg-" + key, value) +
2232+
"</parameter>";
2233+
}
2234+
}
2235+
2236+
tool_rules.push_back(builder.add_rule(name + "-call",
2237+
"\"<seed:tool_call>\" space \"<function=" + name + ">\" space " +
2238+
param_rules +
2239+
" \"</function>\" space \"</seed:tool_call>\""));
2240+
});
2241+
2242+
data.grammar_triggers.push_back({ COMMON_GRAMMAR_TRIGGER_TYPE_WORD, "<seed:tool_call>" });
2243+
2244+
data.preserved_tokens = {
2245+
"<seed:think>", "</seed:think>", "<seed:tool_call>", "</seed:tool_call>",
2246+
"<function=", "</function>", "<parameter=", "</parameter>",
2247+
};
2248+
2249+
builder.add_rule("root", string_join(tool_rules, " | "));
2250+
});
2251+
}
2252+
return data;
2253+
}
2254+
2255+
static common_chat_params common_chat_templates_apply_jinja(const struct common_chat_templates * tmpls,
2256+
const struct common_chat_templates_inputs & inputs) {
20822257
templates_params params;
20832258
params.tools = common_chat_tools_to_json_oaicompat<json>(inputs.tools);
20842259
const auto & tmpl = params.tools.is_array() && tmpls->template_tool_use
@@ -2145,6 +2320,11 @@ static common_chat_params common_chat_templates_apply_jinja(
21452320
return common_chat_params_init_gpt_oss(tmpl, params);
21462321
}
21472322

2323+
// Seed-OSS
2324+
if (src.find("<seed:think>") != std::string::npos) {
2325+
return common_chat_params_init_seed_oss(tmpl, params, inputs);
2326+
}
2327+
21482328
// Use generic handler when mixing tools + JSON schema.
21492329
// TODO: support that mix in handlers below.
21502330
if ((params.tools.is_array() && params.json_schema.is_object())) {
@@ -2303,6 +2483,9 @@ static void common_chat_parse(common_chat_msg_parser & builder) {
23032483
case COMMON_CHAT_FORMAT_GPT_OSS:
23042484
common_chat_parse_gpt_oss(builder);
23052485
break;
2486+
case COMMON_CHAT_FORMAT_SEED_OSS:
2487+
common_chat_parse_seed_oss(builder);
2488+
break;
23062489
default:
23072490
throw std::runtime_error(std::string("Unsupported format: ") + common_chat_format_name(builder.syntax().format));
23082491
}

common/chat.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ enum common_chat_format {
111111
COMMON_CHAT_FORMAT_COMMAND_R7B,
112112
COMMON_CHAT_FORMAT_GRANITE,
113113
COMMON_CHAT_FORMAT_GPT_OSS,
114+
COMMON_CHAT_FORMAT_SEED_OSS,
114115

115116
COMMON_CHAT_FORMAT_COUNT, // Not a format, just the # formats
116117
};

0 commit comments

Comments
 (0)