Skip to main content

dynamo_renderer/
inkling.rs

1// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Native Inkling chat renderer.
5//!
6//! Inkling does not publish a Hugging Face `chat_template`. Its wire format is
7//! a sequence of role and content-kind special tokens, so rendering it through
8//! Jinja is both unnecessary and lossy. This formatter emits those special
9//! token spellings as text; the consumer's tokenizer turns them into the exact
10//! token IDs before dispatching the request to the backend.
11
12use std::collections::HashMap;
13
14use anyhow::{Context, Result, bail};
15use minijinja::Value;
16use serde_json::{Map, Value as JsonValue, json};
17
18use crate::{OAIChatLikeRequest, OAIPromptFormatter};
19
20const MESSAGE_USER: &str = "<|message_user|>";
21const MESSAGE_MODEL: &str = "<|message_model|>";
22const MESSAGE_SYSTEM: &str = "<|message_system|>";
23const MESSAGE_TOOL: &str = "<|message_tool|>";
24const CONTENT_TEXT: &str = "<|content_text|>";
25const CONTENT_IMAGE: &str = "<|content_image|>";
26const CONTENT_MODEL_END_SAMPLING: &str = "<|content_model_end_sampling|>";
27const CONTENT_AUDIO_INPUT: &str = "<|content_audio_input|>";
28const CONTENT_THINKING: &str = "<|content_thinking|>";
29const CONTENT_XML: &str = "<|content_xml|>";
30const CONTENT_INVOKE_TOOL_JSON: &str = "<|content_invoke_tool_json|>";
31const END_MESSAGE: &str = "<|end_message|>";
32const AUDIO_END: &str = "<|audio_end|>";
33const MAX_REASONING_EFFORT: f64 = 0.99;
34
35#[derive(Debug, Clone, Copy, Default)]
36pub struct InklingFormatter;
37
38impl OAIPromptFormatter for InklingFormatter {
39    fn supports_add_generation_prompt(&self) -> bool {
40        true
41    }
42
43    fn render(&self, req: &dyn OAIChatLikeRequest) -> Result<String> {
44        let mut messages = json_value(req.messages()).context("serialize Inkling messages")?;
45        let messages = messages
46            .as_array_mut()
47            .context("Inkling messages must be an array")?;
48
49        let args = req.chat_template_args();
50        if args
51            .and_then(|args| args.get("continue_final_message"))
52            .and_then(JsonValue::as_bool)
53            == Some(true)
54        {
55            bail!("Inkling renderer does not support continue_final_message");
56        }
57
58        let tools_enabled = req.tool_choice().as_ref().and_then(Value::as_str) != Some("none");
59        let request_tools = if tools_enabled {
60            req.tools()
61                .map(json_value)
62                .transpose()
63                .context("serialize Inkling tools")?
64                .unwrap_or_else(|| JsonValue::Array(Vec::new()))
65        } else {
66            JsonValue::Array(Vec::new())
67        };
68
69        let mut output = String::new();
70        let all_tools = if tools_enabled {
71            collect_tools(messages, &request_tools)?
72        } else {
73            Vec::new()
74        };
75        write_tool_declarations(&mut output, &all_tools)?;
76
77        let effort_value = req
78            .reasoning_effort()
79            .map(json_value)
80            .transpose()
81            .context("serialize Inkling reasoning_effort")?
82            .or_else(|| args.and_then(|args| args.get("reasoning_effort").cloned()));
83        let mut reasoning_effort = resolve_reasoning_effort(effort_value.as_ref());
84        let mut tool_call_id_to_name = HashMap::new();
85
86        for message in messages {
87            let role = message
88                .get("role")
89                .and_then(JsonValue::as_str)
90                .context("Inkling message is missing a string role")?;
91
92            if !matches!(role, "system" | "developer")
93                && let Some(effort) = reasoning_effort.take()
94            {
95                write_reasoning_effort(&mut output, effort)?;
96            }
97
98            match role {
99                "system" | "developer" => write_content(
100                    &mut output,
101                    MESSAGE_SYSTEM,
102                    message.get("content").unwrap_or(&JsonValue::Null),
103                )?,
104                "user" => write_content(
105                    &mut output,
106                    MESSAGE_USER,
107                    message.get("content").unwrap_or(&JsonValue::Null),
108                )?,
109                "assistant" => write_assistant(&mut output, message, &mut tool_call_id_to_name)?,
110                "tool" => write_tool_response(&mut output, message, &tool_call_id_to_name)?,
111                other => bail!(
112                    "unsupported Inkling message role {other:?}; expected system, developer, user, assistant, or tool"
113                ),
114            }
115        }
116
117        if let Some(effort) = reasoning_effort {
118            write_reasoning_effort(&mut output, effort)?;
119        }
120        if req.should_add_generation_prompt() {
121            output.push_str(MESSAGE_MODEL);
122        }
123        Ok(output)
124    }
125}
126
127fn json_value(value: Value) -> Result<JsonValue> {
128    serde_json::to_value(value).context("convert minijinja value to JSON")
129}
130
131fn collect_tools(messages: &[JsonValue], request_tools: &JsonValue) -> Result<Vec<JsonValue>> {
132    let mut tools = request_tools
133        .as_array()
134        .context("Inkling tools must be an array")?
135        .clone();
136    for message in messages {
137        if message.get("role").and_then(JsonValue::as_str) == Some("developer")
138            && let Some(local_tools) = message.get("tools")
139        {
140            tools.extend(
141                local_tools
142                    .as_array()
143                    .context("developer message tools must be an array")?
144                    .iter()
145                    .cloned(),
146            );
147        }
148    }
149    Ok(tools)
150}
151
152fn write_tool_declarations(output: &mut String, tools: &[JsonValue]) -> Result<()> {
153    if tools.is_empty() {
154        return Ok(());
155    }
156
157    let mut specs = Vec::with_capacity(tools.len());
158    for tool in tools {
159        let tool = tool
160            .as_object()
161            .context("Inkling tool declaration must be an object")?;
162        let function = tool
163            .get("function")
164            .and_then(JsonValue::as_object)
165            .context("Inkling tool declaration is missing function")?;
166        let name = function
167            .get("name")
168            .and_then(JsonValue::as_str)
169            .context("Inkling tool function is missing name")?;
170        specs.push(json!({
171            "description": function
172                .get("description")
173                .and_then(JsonValue::as_str)
174                .unwrap_or(""),
175            "name": name,
176            "parameters": function
177                .get("parameters")
178                .cloned()
179                .unwrap_or_else(|| JsonValue::Object(Map::new())),
180            "type": tool
181                .get("type")
182                .and_then(JsonValue::as_str)
183                .unwrap_or("function"),
184        }));
185    }
186    let payload = canonical_json(&JsonValue::Array(specs))?;
187    write_block(
188        output,
189        MESSAGE_SYSTEM,
190        Some("tool_declare"),
191        CONTENT_XML,
192        &payload,
193    );
194    Ok(())
195}
196
197fn write_content(output: &mut String, role_token: &str, content: &JsonValue) -> Result<()> {
198    match content {
199        JsonValue::Null => {}
200        JsonValue::String(text) => {
201            if !text.is_empty() {
202                write_block(output, role_token, None, CONTENT_TEXT, text);
203            }
204        }
205        JsonValue::Array(parts) => {
206            for part in parts {
207                if let Some(text) = part.as_str() {
208                    write_block(output, role_token, None, CONTENT_TEXT, text);
209                    continue;
210                }
211                let part = part
212                    .as_object()
213                    .context("Inkling content part must be an object")?;
214                let part_type = part
215                    .get("type")
216                    .and_then(JsonValue::as_str)
217                    .unwrap_or("text");
218                match part_type {
219                    "text" | "input_text" => write_block(
220                        output,
221                        role_token,
222                        None,
223                        CONTENT_TEXT,
224                        part.get("text").and_then(JsonValue::as_str).unwrap_or(""),
225                    ),
226                    "image" | "input_image" | "image_url" => {
227                        write_block(output, role_token, None, CONTENT_IMAGE, "")
228                    }
229                    "audio" | "input_audio" | "audio_url" => {
230                        output.push_str(role_token);
231                        output.push_str(CONTENT_AUDIO_INPUT);
232                        output.push_str(AUDIO_END);
233                        output.push_str(END_MESSAGE);
234                    }
235                    "video" | "input_video" | "video_url" => {
236                        bail!("Inkling does not support video content")
237                    }
238                    other => bail!("unsupported Inkling content part type {other:?}"),
239                }
240            }
241        }
242        _ => bail!("Inkling message content must be a string or array"),
243    }
244    Ok(())
245}
246
247fn write_assistant(
248    output: &mut String,
249    message: &JsonValue,
250    tool_call_id_to_name: &mut HashMap<String, String>,
251) -> Result<()> {
252    let tool_calls = message
253        .get("tool_calls")
254        .and_then(JsonValue::as_array)
255        .map(Vec::as_slice)
256        .unwrap_or(&[]);
257    let reasoning = message
258        .get("reasoning_content")
259        .or_else(|| message.get("reasoning"));
260
261    match reasoning {
262        Some(JsonValue::Array(segments)) => {
263            for (index, tool_call) in tool_calls.iter().enumerate() {
264                if let Some(text) = segments.get(index).and_then(JsonValue::as_str) {
265                    write_reasoning_block(output, text);
266                }
267                write_tool_call(output, tool_call, tool_call_id_to_name)?;
268            }
269            for segment in segments.iter().skip(tool_calls.len()) {
270                let text = segment
271                    .as_str()
272                    .context("Inkling reasoning_content segments must be strings")?;
273                write_reasoning_block(output, text);
274            }
275            write_content(
276                output,
277                MESSAGE_MODEL,
278                message.get("content").unwrap_or(&JsonValue::Null),
279            )?;
280        }
281        Some(JsonValue::String(text)) => {
282            write_reasoning_block(output, text);
283            write_content(
284                output,
285                MESSAGE_MODEL,
286                message.get("content").unwrap_or(&JsonValue::Null),
287            )?;
288            for tool_call in tool_calls {
289                write_tool_call(output, tool_call, tool_call_id_to_name)?;
290            }
291        }
292        None | Some(JsonValue::Null) => {
293            write_content(
294                output,
295                MESSAGE_MODEL,
296                message.get("content").unwrap_or(&JsonValue::Null),
297            )?;
298            for tool_call in tool_calls {
299                write_tool_call(output, tool_call, tool_call_id_to_name)?;
300            }
301        }
302        Some(_) => bail!("Inkling reasoning_content must be a string or array of strings"),
303    }
304
305    output.push_str(CONTENT_MODEL_END_SAMPLING);
306    Ok(())
307}
308
309fn write_reasoning_block(output: &mut String, text: &str) {
310    if !text.is_empty() {
311        write_block(output, MESSAGE_MODEL, None, CONTENT_THINKING, text);
312    }
313}
314
315fn write_tool_call(
316    output: &mut String,
317    tool_call: &JsonValue,
318    tool_call_id_to_name: &mut HashMap<String, String>,
319) -> Result<()> {
320    let tool_call = tool_call
321        .as_object()
322        .context("Inkling tool call must be an object")?;
323    let function = tool_call
324        .get("function")
325        .and_then(JsonValue::as_object)
326        .context("Inkling tool call is missing function")?;
327    let name = function
328        .get("name")
329        .and_then(JsonValue::as_str)
330        .context("Inkling tool call function is missing name")?;
331    if let Some(id) = tool_call.get("id").and_then(JsonValue::as_str)
332        && !id.is_empty()
333    {
334        tool_call_id_to_name.insert(id.to_string(), name.to_string());
335    }
336
337    let arguments = match function.get("arguments") {
338        None | Some(JsonValue::Null) => JsonValue::Object(Map::new()),
339        Some(JsonValue::String(arguments)) if arguments.trim().is_empty() => {
340            JsonValue::Object(Map::new())
341        }
342        Some(JsonValue::String(arguments)) => serde_json::from_str(arguments)
343            .context("Inkling tool call arguments must be valid JSON")?,
344        Some(arguments) => arguments.clone(),
345    };
346    if !arguments.is_object() {
347        bail!("Inkling tool call arguments must decode to a JSON object");
348    }
349
350    let name_json = serde_json::to_string(name)?;
351    let args_json = canonical_json(&arguments)?;
352    let payload = format!("{{\"name\":{name_json},\"args\":{args_json}}}");
353    write_block(
354        output,
355        MESSAGE_MODEL,
356        Some(name),
357        CONTENT_INVOKE_TOOL_JSON,
358        &payload,
359    );
360    Ok(())
361}
362
363fn write_tool_response(
364    output: &mut String,
365    message: &JsonValue,
366    tool_call_id_to_name: &HashMap<String, String>,
367) -> Result<()> {
368    let tool_call_id = message
369        .get("tool_call_id")
370        .and_then(JsonValue::as_str)
371        .unwrap_or("");
372    let name = message
373        .get("name")
374        .and_then(JsonValue::as_str)
375        .or_else(|| tool_call_id_to_name.get(tool_call_id).map(String::as_str))
376        .unwrap_or("");
377    let text = flatten_text_content(message.get("content").unwrap_or(&JsonValue::Null))?;
378    write_block(output, MESSAGE_TOOL, Some(name), CONTENT_TEXT, &text);
379    Ok(())
380}
381
382fn flatten_text_content(content: &JsonValue) -> Result<String> {
383    match content {
384        JsonValue::Null => Ok(String::new()),
385        JsonValue::String(text) => Ok(text.clone()),
386        JsonValue::Array(parts) => {
387            let mut output = String::new();
388            for part in parts {
389                if let Some(text) = part.as_str() {
390                    output.push_str(text);
391                    continue;
392                }
393                let part = part
394                    .as_object()
395                    .context("Inkling tool response part must be an object")?;
396                let kind = part
397                    .get("type")
398                    .and_then(JsonValue::as_str)
399                    .unwrap_or("text");
400                if !matches!(kind, "text" | "input_text") {
401                    bail!("Inkling tool response content must be text, got {kind:?}");
402                }
403                output.push_str(part.get("text").and_then(JsonValue::as_str).unwrap_or(""));
404            }
405            Ok(output)
406        }
407        _ => bail!("Inkling tool response content must be text"),
408    }
409}
410
411fn write_reasoning_effort(output: &mut String, effort: f64) -> Result<()> {
412    if !(0.0..=MAX_REASONING_EFFORT).contains(&effort) {
413        bail!("Inkling reasoning_effort must be in [0.0, 0.99], got {effort}");
414    }
415    let formatted = format!("{effort:.2}");
416    let effort = formatted.trim_end_matches('0').trim_end_matches('.');
417    let effort = if matches!(effort, "0" | "-0") {
418        "0.0"
419    } else {
420        effort
421    };
422    write_block(
423        output,
424        MESSAGE_SYSTEM,
425        None,
426        CONTENT_TEXT,
427        &format!("Thinking effort level: {effort}"),
428    );
429    Ok(())
430}
431
432fn resolve_reasoning_effort(value: Option<&JsonValue>) -> Option<f64> {
433    let Some(value) = value else {
434        return Some(0.9);
435    };
436    match value {
437        JsonValue::String(name) => match name.as_str() {
438            "none" => Some(0.0),
439            "minimal" => Some(0.1),
440            "low" => Some(0.2),
441            "medium" => Some(0.7),
442            "high" => Some(0.9),
443            "xhigh" | "max" => Some(0.99),
444            _ => None,
445        },
446        JsonValue::Number(number) => number.as_f64(),
447        _ => None,
448    }
449}
450
451fn write_block(
452    output: &mut String,
453    role_token: &str,
454    author_name: Option<&str>,
455    content_token: &str,
456    text: &str,
457) {
458    output.push_str(role_token);
459    if let Some(author_name) = author_name
460        && !author_name.is_empty()
461    {
462        output.push_str(author_name);
463    }
464    output.push_str(content_token);
465    output.push_str(text);
466    output.push_str(END_MESSAGE);
467}
468
469fn canonical_json(value: &JsonValue) -> Result<String> {
470    serde_json::to_string(&sort_json(value)).context("serialize Inkling JSON payload")
471}
472
473fn sort_json(value: &JsonValue) -> JsonValue {
474    match value {
475        JsonValue::Array(items) => JsonValue::Array(items.iter().map(sort_json).collect()),
476        JsonValue::Object(map) => {
477            let mut sorted = Map::new();
478            let mut keys = map.keys().collect::<Vec<_>>();
479            keys.sort();
480            for key in keys {
481                sorted.insert(key.clone(), sort_json(&map[key]));
482            }
483            JsonValue::Object(sorted)
484        }
485        _ => value.clone(),
486    }
487}
488
489#[cfg(test)]
490mod tests {
491    use super::*;
492    use crate::{OAIChatLikeRequest, PromptFormatter};
493    use std::collections::HashMap;
494
495    #[derive(Default)]
496    struct Request {
497        messages: JsonValue,
498        tools: Option<JsonValue>,
499        tool_choice: Option<JsonValue>,
500        args: Option<HashMap<String, JsonValue>>,
501        reasoning_effort: Option<JsonValue>,
502        add_generation_prompt: bool,
503    }
504
505    impl Request {
506        fn new(messages: JsonValue) -> Self {
507            Self {
508                messages,
509                add_generation_prompt: true,
510                ..Default::default()
511            }
512        }
513    }
514
515    impl OAIChatLikeRequest for Request {
516        fn model(&self) -> String {
517            "thinkingmachines/Inkling-NVFP4".to_string()
518        }
519
520        fn messages(&self) -> Value {
521            Value::from_serialize(&self.messages)
522        }
523
524        fn tools(&self) -> Option<Value> {
525            self.tools.as_ref().map(Value::from_serialize)
526        }
527
528        fn tool_choice(&self) -> Option<Value> {
529            self.tool_choice.as_ref().map(Value::from_serialize)
530        }
531
532        fn response_format(&self) -> Option<Value> {
533            None
534        }
535
536        fn reasoning_effort(&self) -> Option<Value> {
537            self.reasoning_effort.as_ref().map(Value::from_serialize)
538        }
539
540        fn should_add_generation_prompt(&self) -> bool {
541            self.add_generation_prompt
542        }
543
544        fn chat_template_args(&self) -> Option<&HashMap<String, JsonValue>> {
545            self.args.as_ref()
546        }
547    }
548
549    #[test]
550    fn renders_text_and_image_like_vllm_fixture() {
551        let request = Request::new(json!([{
552            "role": "user",
553            "content": [
554                {"type": "text", "text": "look"},
555                {"type": "image_url", "image_url": {"url": "data:image/png;base64,"}}
556            ]
557        }]));
558        assert_eq!(
559            InklingFormatter.render(&request).unwrap(),
560            "<|message_system|><|content_text|>Thinking effort level: 0.9<|end_message|><|message_user|><|content_text|>look<|end_message|><|message_user|><|content_image|><|end_message|><|message_model|>"
561        );
562    }
563
564    #[test]
565    fn renders_audio_markers_like_vllm_fixture() {
566        let request = Request::new(json!([{
567            "role": "user",
568            "content": [
569                {"type": "text", "text": "transcribe"},
570                {"type": "input_audio", "input_audio": {"data": "", "format": "wav"}},
571                {"type": "audio_url", "audio_url": {"url": "data:audio/wav;base64,"}}
572            ]
573        }]));
574        assert_eq!(
575            InklingFormatter.render(&request).unwrap(),
576            "<|message_system|><|content_text|>Thinking effort level: 0.9<|end_message|><|message_user|><|content_text|>transcribe<|end_message|><|message_user|><|content_audio_input|><|audio_end|><|end_message|><|message_user|><|content_audio_input|><|audio_end|><|end_message|><|message_model|>"
577        );
578    }
579
580    #[test]
581    fn renders_tool_declaration_and_round_trip_like_vllm_fixtures() {
582        let mut request = Request::new(json!([
583            {
584                "role": "developer",
585                "content": "rules",
586                "tools": [{
587                    "type": "function",
588                    "function": {
589                        "name": "local_tool",
590                        "parameters": {"z": 1, "a": {"b": 2}}
591                    }
592                }]
593            },
594            {"role": "user", "content": "hi"}
595        ]));
596        request.tools = Some(json!([{
597            "type": "function",
598            "function": {
599                "name": "get_weather",
600                "description": "Get weather information",
601                "parameters": {
602                    "type": "object",
603                    "required": ["city"],
604                    "properties": {"city": {"type": "string"}}
605                }
606            }
607        }]));
608        assert_eq!(
609            InklingFormatter.render(&request).unwrap(),
610            "<|message_system|>tool_declare<|content_xml|>[{\"description\":\"Get weather information\",\"name\":\"get_weather\",\"parameters\":{\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"type\":\"object\"},\"type\":\"function\"},{\"description\":\"\",\"name\":\"local_tool\",\"parameters\":{\"a\":{\"b\":2},\"z\":1},\"type\":\"function\"}]<|end_message|><|message_system|><|content_text|>rules<|end_message|><|message_system|><|content_text|>Thinking effort level: 0.9<|end_message|><|message_user|><|content_text|>hi<|end_message|><|message_model|>"
611        );
612
613        let mut round_trip = Request::new(json!([
614            {
615                "role": "assistant",
616                "reasoning_content": "think",
617                "content": "answer",
618                "tool_calls": [{
619                    "id": "call_1",
620                    "type": "function",
621                    "function": {"name": "get_weather", "arguments": "{\"city\":\"SF\"}"}
622                }]
623            },
624            {"role": "tool", "tool_call_id": "call_1", "content": "sunny"}
625        ]));
626        round_trip.add_generation_prompt = false;
627        assert_eq!(
628            InklingFormatter.render(&round_trip).unwrap(),
629            "<|message_system|><|content_text|>Thinking effort level: 0.9<|end_message|><|message_model|><|content_thinking|>think<|end_message|><|message_model|><|content_text|>answer<|end_message|><|message_model|>get_weather<|content_invoke_tool_json|>{\"name\":\"get_weather\",\"args\":{\"city\":\"SF\"}}<|end_message|><|content_model_end_sampling|><|message_tool|>get_weather<|content_text|>sunny<|end_message|>"
630        );
631    }
632
633    #[test]
634    fn interleaves_segmented_reasoning_with_tool_calls() {
635        let mut request = Request::new(json!([{
636            "role": "assistant",
637            "reasoning_content": ["first", "second", "after"],
638            "content": "done",
639            "tool_calls": [
640                {"id": "a", "function": {"name": "one", "arguments": "{\"b\":2,\"a\":1}"}},
641                {"id": "b", "function": {"name": "two", "arguments": "{}"}}
642            ]
643        }]));
644        request.reasoning_effort = Some(json!("none"));
645        request.add_generation_prompt = false;
646        assert_eq!(
647            InklingFormatter.render(&request).unwrap(),
648            "<|message_system|><|content_text|>Thinking effort level: 0.0<|end_message|><|message_model|><|content_thinking|>first<|end_message|><|message_model|>one<|content_invoke_tool_json|>{\"name\":\"one\",\"args\":{\"a\":1,\"b\":2}}<|end_message|><|message_model|><|content_thinking|>second<|end_message|><|message_model|>two<|content_invoke_tool_json|>{\"name\":\"two\",\"args\":{}}<|end_message|><|message_model|><|content_thinking|>after<|end_message|><|message_model|><|content_text|>done<|end_message|><|content_model_end_sampling|>"
649        );
650    }
651
652    #[test]
653    fn ignores_unsupported_reasoning_effort_values() {
654        for value in [json!(true), json!("invalid"), json!(null)] {
655            let mut request = Request::new(json!([{
656                "role": "user",
657                "content": "test"
658            }]));
659            request.reasoning_effort = Some(value);
660
661            assert_eq!(
662                InklingFormatter.render(&request).unwrap(),
663                "<|message_user|><|content_text|>test<|end_message|><|message_model|>"
664            );
665        }
666    }
667
668    #[test]
669    fn tool_choice_none_suppresses_declarations() {
670        let mut request = Request::new(json!([
671            {
672                "role": "developer",
673                "content": "rules",
674                "tools": [{
675                    "type": "function",
676                    "function": {"name": "also_hidden", "parameters": {}}
677                }]
678            },
679            {"role": "user", "content": "hi"}
680        ]));
681        request.tools = Some(json!([{
682            "type": "function",
683            "function": {"name": "hidden", "parameters": {}}
684        }]));
685        request.tool_choice = Some(json!("none"));
686        let rendered = InklingFormatter.render(&request).unwrap();
687        assert!(!rendered.contains("tool_declare"));
688        assert!(!rendered.contains("hidden"));
689        assert!(!rendered.contains("also_hidden"));
690    }
691
692    #[test]
693    fn native_selection_uses_exact_model_type_not_display_name() {
694        assert!(matches!(
695            crate::native_formatter_for(&Some("inkling_mm_model".to_string()), "renamed"),
696            Some(PromptFormatter::OAI(_))
697        ));
698        assert!(crate::native_formatter_for(&None, "inkling-nvfp4").is_none());
699    }
700}