Skip to content

Commit 60e5eee

Browse files
pwilkinCISC
andauthored
chat : Seed OSS thinking + tool call support (ggml-org#15552)
* Reasoning and tool-calling support for Seed OSS * Fix grammar and partial parsing * Whitespace * New chat template * Update common/chat.cpp Co-authored-by: Sigbjørn Skjæret <[email protected]> * Update common/chat.cpp Co-authored-by: Sigbjørn Skjæret <[email protected]> * Remove unused 'purge_healing_marker' helper --------- Co-authored-by: Sigbjørn Skjæret <[email protected]>
1 parent 009b709 commit 60e5eee

File tree

4 files changed

+458
-1
lines changed

4 files changed

+458
-1
lines changed

common/chat.cpp

Lines changed: 152 additions & 1 deletion
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,94 @@ static void common_chat_parse_granite(common_chat_msg_parser & builder) {
20592060
}
20602061
}
20612062

2063+
static void common_chat_parse_seed_oss(common_chat_msg_parser & builder) {
2064+
// Parse thinking tags first - this handles the main reasoning content
2065+
builder.try_parse_reasoning("<seed:think>", "</seed:think>");
2066+
2067+
if (!builder.syntax().parse_tool_calls) {
2068+
builder.add_content(builder.consume_rest());
2069+
return;
2070+
}
2071+
2072+
// Parse tool calls - Seed-OSS uses <seed:tool_call> format
2073+
static const common_regex tool_call_begin_regex("<seed:tool_call>");
2074+
static const common_regex tool_call_end_regex("</seed:tool_call>");
2075+
static const common_regex function_regex("<function=([^>]+)>");
2076+
static const common_regex param_regex("<parameter=([^>]+)>");
2077+
2078+
while (auto tool_res = builder.try_find_regex(tool_call_begin_regex)) {
2079+
builder.consume_spaces(); // Consume whitespace after <seed:tool_call>
2080+
2081+
// Look for function call inside tool call, ignore any content before it
2082+
if (auto func_res = builder.try_find_regex(function_regex, std::string::npos, false)) {
2083+
auto function_name = builder.str(func_res->groups[1]);
2084+
2085+
// Parse Seed-OSS parameters <parameter=name>value</parameter>
2086+
json args = json::object();
2087+
// Parse all parameters
2088+
while (auto param_res = builder.try_find_regex(param_regex, std::string::npos, false)) {
2089+
// again, ignore noise around parameters
2090+
auto param_name = builder.str(param_res->groups[1]);
2091+
builder.move_to(param_res->groups[0].end);
2092+
builder.consume_spaces(); // Consume whitespace after parameter
2093+
auto savedPos = builder.pos();
2094+
if (auto param_parse = builder.try_find_literal("</parameter>")) {
2095+
auto param = param_parse->prelude;
2096+
builder.move_to(savedPos);
2097+
try {
2098+
if (auto param_res = builder.try_consume_json()) {
2099+
args[param_name] = param_res->json;
2100+
} else {
2101+
args[param_name] = param;
2102+
}
2103+
} catch (json::exception &) {
2104+
args[param_name] = param;
2105+
}
2106+
} else {
2107+
throw common_chat_msg_partial_exception("Incomplete tool parameter");
2108+
}
2109+
}
2110+
// Look for closing function tag
2111+
auto end_func = builder.try_find_literal("</function>");
2112+
if (end_func) {
2113+
builder.move_to(end_func->groups[0].end);
2114+
builder.consume_spaces(); // Consume whitespace after </function>
2115+
2116+
// Add the tool call with parsed arguments, but only if we REALLY got the literal
2117+
auto eaten_fragment = builder.input().substr(end_func->groups[0].begin, end_func->groups[0].end);
2118+
auto funlen = std::string("</function>").length();
2119+
if (eaten_fragment.length() >= funlen && eaten_fragment.substr(0, funlen) == std::string("</function>")) {
2120+
if (!builder.add_tool_call(function_name, "", args.dump())) {
2121+
throw common_chat_msg_partial_exception("Incomplete tool call");
2122+
}
2123+
} else {
2124+
throw common_chat_msg_partial_exception("Incomplete tool call");
2125+
}
2126+
} else {
2127+
throw common_chat_msg_partial_exception("Incomplete tool call");
2128+
}
2129+
// Look for closing tool call tag
2130+
if (auto end_tool = builder.try_find_regex(tool_call_end_regex, std::string::npos, false)) {
2131+
builder.move_to(end_tool->groups[0].end);
2132+
builder.consume_spaces(); // Consume trailing whitespace after tool call
2133+
} else {
2134+
throw common_chat_msg_partial_exception("Incomplete tool call");
2135+
}
2136+
} else {
2137+
// No function found - don't consume content here, let it be handled at the end
2138+
break;
2139+
}
2140+
}
2141+
2142+
// Consume any remaining whitespace after all tool call processing
2143+
builder.consume_spaces();
2144+
auto remaining = builder.consume_rest();
2145+
// If there's any non-whitespace content remaining, add it as content
2146+
if (!string_strip(remaining).empty()) {
2147+
builder.add_content(remaining);
2148+
}
2149+
}
2150+
20622151
static common_chat_params common_chat_params_init_without_tools(const common_chat_template & tmpl, const struct templates_params & inputs) {
20632152
common_chat_params data;
20642153
data.prompt = apply(tmpl, inputs);
@@ -2075,8 +2164,62 @@ static common_chat_params common_chat_params_init_without_tools(const common_cha
20752164
return data;
20762165
}
20772166

2167+
static common_chat_params common_chat_params_init_seed_oss(
2168+
const common_chat_template & tmpl,
2169+
templates_params & params,
2170+
const common_chat_templates_inputs & inputs)
2171+
{
2172+
common_chat_params data;
2173+
data.prompt = apply(tmpl, params);
2174+
data.format = COMMON_CHAT_FORMAT_SEED_OSS;
2175+
if (string_ends_with(data.prompt, "<seed:think>")) {
2176+
if (!inputs.enable_thinking) {
2177+
data.prompt += "</seed:think>";
2178+
} else {
2179+
data.thinking_forced_open = true;
2180+
}
2181+
}
2182+
2183+
if (params.tools.is_array() && !params.tools.empty()) {
2184+
data.grammar_lazy = inputs.tool_choice != COMMON_CHAT_TOOL_CHOICE_REQUIRED;
2185+
data.grammar = build_grammar([&](const common_grammar_builder & builder) {
2186+
std::vector<std::string> tool_rules;
2187+
foreach_function(params.tools, [&](const json & tool) {
2188+
const auto & function = tool.at("function");
2189+
std::string name = function.at("name");
2190+
auto parameters = function.at("parameters");
2191+
builder.resolve_refs(parameters);
2192+
2193+
// Create rule for Seed-OSS function call format
2194+
std::string param_rules;
2195+
if (parameters.contains("properties")) {
2196+
for (const auto & [key, value] : parameters.at("properties").items()) {
2197+
param_rules += "\"<parameter=" + key + ">\"" + builder.add_schema(name + "-arg-" + key, value) +
2198+
"\"</parameter>\"";
2199+
}
2200+
}
2201+
2202+
tool_rules.push_back(builder.add_rule(name + "-call",
2203+
"\"<seed:tool_call>\" space \"<function=" + name + ">\" space " +
2204+
param_rules +
2205+
" \"</function>\" space \"</seed:tool_call>\""));
2206+
});
2207+
2208+
data.grammar_triggers.push_back({ COMMON_GRAMMAR_TRIGGER_TYPE_WORD, "<seed:tool_call>" });
2209+
2210+
data.preserved_tokens = {
2211+
"<seed:think>", "</seed:think>", "<seed:tool_call>", "</seed:tool_call>",
2212+
"<function=", "</function>", "<parameter=", "</parameter>",
2213+
};
2214+
2215+
builder.add_rule("root", string_join(tool_rules, " | "));
2216+
});
2217+
}
2218+
return data;
2219+
}
2220+
20782221
static common_chat_params common_chat_templates_apply_jinja(
2079-
const struct common_chat_templates * tmpls,
2222+
const struct common_chat_templates * tmpls,
20802223
const struct common_chat_templates_inputs & inputs)
20812224
{
20822225
templates_params params;
@@ -2145,6 +2288,11 @@ static common_chat_params common_chat_templates_apply_jinja(
21452288
return common_chat_params_init_gpt_oss(tmpl, params);
21462289
}
21472290

2291+
// Seed-OSS
2292+
if (src.find("<seed:think>") != std::string::npos) {
2293+
return common_chat_params_init_seed_oss(tmpl, params, inputs);
2294+
}
2295+
21482296
// Use generic handler when mixing tools + JSON schema.
21492297
// TODO: support that mix in handlers below.
21502298
if ((params.tools.is_array() && params.json_schema.is_object())) {
@@ -2303,6 +2451,9 @@ static void common_chat_parse(common_chat_msg_parser & builder) {
23032451
case COMMON_CHAT_FORMAT_GPT_OSS:
23042452
common_chat_parse_gpt_oss(builder);
23052453
break;
2454+
case COMMON_CHAT_FORMAT_SEED_OSS:
2455+
common_chat_parse_seed_oss(builder);
2456+
break;
23062457
default:
23072458
throw std::runtime_error(std::string("Unsupported format: ") + common_chat_format_name(builder.syntax().format));
23082459
}

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
};
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
{# ----------‑‑‑ special token variables ‑‑‑---------- #}
2+
{%- set bos_token = '<seed:bos>' -%}
3+
{%- set eos_token = '<seed:eos>' -%}
4+
{%- set pad_token = '<seed:pad>' -%}
5+
{%- set toolcall_begin_token = '<seed:tool_call>' -%}
6+
{%- set toolcall_end_token = '</seed:tool_call>' -%}
7+
{%- set think_begin_token = '<seed:think>' -%}
8+
{%- set think_end_token = '</seed:think>' -%}
9+
{%- set budget_begin_token = '<seed:cot_budget_reflect>'-%}
10+
{%- set budget_end_token = '</seed:cot_budget_reflect>'-%}
11+
{# -------------- reflection-interval lookup -------------- #}
12+
{%- if not thinking_budget is defined %}
13+
{%- set thinking_budget = -1 -%}
14+
{%- endif -%}
15+
{%- set budget_reflections_v05 = {
16+
0: 0,
17+
512: 128,
18+
1024: 256,
19+
2048: 512,
20+
4096: 512,
21+
8192: 1024,
22+
16384: 1024
23+
} -%}
24+
{# Find the first gear that is greater than or equal to the thinking_budget. #}
25+
{%- set ns = namespace(interval = None) -%}
26+
{%- for k, v in budget_reflections_v05 | dictsort -%}
27+
{%- if ns.interval is none and thinking_budget <= k -%}
28+
{%- set ns.interval = v -%}
29+
{%- endif -%}
30+
{%- endfor -%}
31+
{# If it exceeds the maximum gear, use the value of the last gear #}
32+
{%- if ns.interval is none -%}
33+
{%- set ns.interval = budget_reflections_v05[16384] -%}
34+
{%- endif -%}
35+
{# ---------- Preprocess the system message ---------- #}
36+
{%- if messages[0]["role"] == "system" %}
37+
{%- set system_message = messages[0]["content"] %}
38+
{%- set loop_messages = messages[1:] %}
39+
{%- else %}
40+
{%- set loop_messages = messages %}
41+
{%- endif %}
42+
{# ---------- Ensure tools exist ---------- #}
43+
{%- if not tools is defined or tools is none %}
44+
{%- set tools = [] %}
45+
{%- endif %}
46+
{# tools2doc.jinja #}
47+
{%- macro py_type(t) -%}
48+
{%- if t == "string" -%}str
49+
{%- elif t in ("number", "integer") -%}int
50+
{%- elif t == "boolean" -%}bool
51+
{%- elif t == "array" -%}list
52+
{%- else -%}Any{%- endif -%}
53+
{%- endmacro -%}
54+
{# ---------- Output the system block ---------- #}
55+
{%- if system_message is defined %}
56+
{{ bos_token + "system\n" + system_message }}
57+
{%- else %}
58+
{%- if tools is iterable and tools | length > 0 %}
59+
{{ bos_token + "system\nYou are Doubao, a helpful AI assistant. You may call one or more functions to assist with the user query." }}
60+
{%- endif %}
61+
{%- endif %}
62+
{%- if use_json_tooldef is defined and use_json_tooldef %}
63+
64+
{{"Tool List:\nYou are authorized to use the following tools (described in JSON Schema format). Before performing any task, you must decide how to call them based on the descriptions and parameters of these tools."}}
65+
{{ tools | tojson(ensure_ascii=False) }}
66+
{%- else %}
67+
{%- for item in tools if item.type == "function" %}
68+
69+
70+
Function:
71+
def {{ item.function.name }}(
72+
{%- for name, spec in item.function.parameters.properties.items() %}
73+
{{- name }}: {{ py_type(spec.type) }}{% if not loop.last %},{% endif %}
74+
{%- endfor %}):
75+
"""
76+
{{ item.function.description | trim }}
77+
78+
{# ---------- Args ---------- #}
79+
{%- if item.function.parameters.properties %}
80+
Args:
81+
{%- for name, spec in item.function.parameters.properties.items() %}
82+
83+
- {{ name }} ({{ py_type(spec.type) }})
84+
{%- if name in item.function.parameters.required %} [必填]{% else %} [选填]{% endif %}:
85+
{{- " " ~ (spec.description or "") }}
86+
{%- endfor %}
87+
{%- endif %}
88+
89+
{# ---------- Returns ---------- #}
90+
{%- if item.function.returns is defined
91+
and item.function.returns.properties is defined
92+
and item.function.returns.properties %}
93+
Returns:
94+
{%- for name, spec in item.function.returns.properties.items() %}
95+
96+
- {{ name }} ({{ py_type(spec.type) }}):
97+
{{- " " ~ (spec.description or "") }}
98+
{%- endfor %}
99+
{%- endif %}
100+
101+
"""
102+
{%- endfor %}
103+
{%- endif %}
104+
{%- if tools is iterable and tools | length > 0 %}
105+
106+
{{"工具调用请遵循如下格式:\n<seed:tool_call>\n<function=example_function_name>\n<parameter=example_parameter_1>value_1</parameter>\n<parameter=example_parameter_2>This is the value for the second parameter\nthat can span\nmultiple lines</parameter>\n</function>\n</seed:tool_call>\n"}}
107+
{%- endif %}
108+
{# End the system block line #}
109+
{%- if system_message is defined or tools is iterable and tools | length > 0 %}
110+
{{ eos_token }}
111+
{%- endif %}
112+
{# ---------- Thinking Budget ---------- #}
113+
{%- if thinking_budget is defined %}
114+
{%- if thinking_budget == 0 %}
115+
{{ bos_token+"system" }}
116+
{{ "You are an intelligent assistant that can answer questions in one step without the need for reasoning and thinking, that is, your thinking budget is 0. Next, please skip the thinking process and directly start answering the user's questions." }}
117+
{{ eos_token }}
118+
{%- elif not thinking_budget == -1 %}
119+
{{ bos_token+"system" }}
120+
{{ "You are an intelligent assistant with reflective ability. In the process of thinking and reasoning, you need to strictly follow the thinking budget, which is "}}{{thinking_budget}}{{". That is, you need to complete your thinking within "}}{{thinking_budget}}{{" tokens and start answering the user's questions. You will reflect on your thinking process every "}}{{ns.interval}}{{" tokens, stating how many tokens have been used and how many are left."}}
121+
{{ eos_token }}
122+
{%- endif %}
123+
{%- endif %}
124+
{# ---------- List the historical messages one by one ---------- #}
125+
{%- for message in loop_messages %}
126+
{%- if message.role == "assistant"
127+
and message.tool_calls is defined
128+
and message.tool_calls is iterable
129+
and message.tool_calls | length > 0 %}
130+
{{ bos_token + message.role }}
131+
{%- if message.reasoning_content is defined and message.reasoning_content is string and message.reasoning_content | trim | length > 0 %}
132+
{{ "\n" + think_begin_token + message.reasoning_content | trim + think_end_token }}
133+
{%- endif %}
134+
{%- if message.content is defined and message.content is string and message.content | trim | length > 0 %}
135+
{{ "\n" + message.content | trim + "\n" }}
136+
{%- endif %}
137+
{%- for tool_call in message.tool_calls %}
138+
{%- if tool_call.function is defined %}{% set tool_call = tool_call.function %}{% endif %}
139+
{{ "\n" + toolcall_begin_token + "\n<function=" + tool_call.name + ">\n" }}
140+
{%- if tool_call.arguments is defined %}
141+
{%- for arg_name, arg_value in tool_call.arguments | items %}
142+
{{ "<parameter=" + arg_name + ">" }}
143+
{%- set arg_value = arg_value if arg_value is string else arg_value | string %}
144+
{{ arg_value+"</parameter>\n" }}
145+
{%- endfor %}
146+
{%- endif %}
147+
{{ "</function>\n" + toolcall_end_token }}
148+
{%- endfor %}
149+
{{ eos_token }}
150+
{%- elif message.role in ["user", "system"] %}
151+
{{ bos_token + message.role + "\n" + message.content + eos_token }}
152+
{%- elif message.role == "assistant" %}
153+
{{ bos_token + message.role }}
154+
{%- if message.reasoning_content is defined and message.reasoning_content is string and message.reasoning_content | trim | length > 0 %}
155+
{{ "\n" + think_begin_token + message.reasoning_content | trim + think_end_token }}
156+
{%- endif %}
157+
{%- if message.content is defined and message.content is string and message.content | trim | length > 0 %}
158+
{{ "\n" + message.content | trim + eos_token }}
159+
{%- endif %}
160+
{# Include the tool role #}
161+
{%- else %}
162+
{{ bos_token + message.role + "\n" + message.content + eos_token }}
163+
{%- endif %}
164+
{%- endfor %}
165+
{# ---------- Control the model to start continuation ---------- #}
166+
{%- if add_generation_prompt %}
167+
{{ bos_token+"assistant\n" }}
168+
{%- if thinking_budget == 0 %}
169+
{{ think_begin_token + "\n" + budget_begin_token + "The current thinking budget is 0, so I will directly start answering the question." + budget_end_token + "\n" + think_end_token }}
170+
{%- endif %}
171+
{%- endif %}

0 commit comments

Comments
 (0)