Skip to main content

openai_harmony/
encoding.rs

1use crate::{
2    chat::{Author, Content, Message, ReasoningEffort, Role, SystemContent, TextContent},
3    tiktoken::{CoreBPE, Rank},
4};
5use anyhow::Context as _;
6use std::{
7    collections::{HashMap, HashSet},
8    sync::Arc,
9    vec,
10};
11
12const REPLACEMENT: &str = "\u{FFFD}";
13
14// Parsed representation of a message header.
15#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
16pub struct ParsedHeader {
17    author: Author,
18    recipient: Option<String>,
19    channel: Option<String>,
20    content_type: Option<String>,
21}
22
23#[derive(thiserror::Error, Debug)]
24pub(crate) enum RenderFormattingTokenError {
25    #[error("tried to render unmapped formatting token {0}")]
26    UnmappedToken(FormattingToken),
27
28    #[error(
29        "Expected encoding of formatting token {token} to be a single token, but got {encoding:?}"
30    )]
31    InvalidEncoding {
32        token: FormattingToken,
33        encoding: Vec<Rank>,
34    },
35}
36
37/// These are formatting tokens that the renderer can use to generically
38/// format the output of the model, but at formatting time, they are replaced
39/// by actual tokens from the tokenizers vocabulary.
40#[allow(dead_code)]
41#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
42pub(crate) enum FormattingToken {
43    Start,
44    Message,
45    EndMessage,
46    EndMessageDoneSampling,
47    EndMessageAssistantToTool,
48    Refusal,
49    ConstrainedFormat,
50    Channel,
51    BeginUntrusted,
52    EndUntrusted,
53    MetaSep,
54    MetaEnd,
55}
56
57impl FormattingToken {
58    fn as_str(&self) -> &str {
59        match self {
60            FormattingToken::Start => "<|start|>",
61            FormattingToken::Message => "<|message|>",
62            FormattingToken::EndMessage => "<|end|>",
63            FormattingToken::EndMessageDoneSampling => "<|return|>",
64            FormattingToken::EndMessageAssistantToTool => "<|call|>",
65            FormattingToken::Refusal => "<|refusal|>",
66            FormattingToken::ConstrainedFormat => "<|constrain|>",
67            FormattingToken::Channel => "<|channel|>",
68            FormattingToken::BeginUntrusted => "<|untrusted|>",
69            FormattingToken::EndUntrusted => "<|end_untrusted|>",
70            FormattingToken::MetaSep => "<|channel|>",
71            FormattingToken::MetaEnd => "<|meta_end|>",
72        }
73    }
74}
75
76impl std::fmt::Display for FormattingToken {
77    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78        write!(f, "{}", self.as_str())
79    }
80}
81
82#[allow(dead_code)]
83#[derive(Clone)]
84pub struct HarmonyEncoding {
85    pub(crate) name: String,
86    pub(crate) n_ctx: usize,
87    pub(crate) max_message_tokens: usize,
88    pub(crate) max_action_length: usize,
89    pub(crate) tokenizer_name: String,
90    pub(crate) tokenizer: Arc<CoreBPE>,
91    pub(crate) format_token_mapping: HashMap<FormattingToken, String>,
92    pub(crate) stop_formatting_tokens: HashSet<FormattingToken>,
93    pub(crate) stop_formatting_tokens_for_assistant_actions: HashSet<FormattingToken>,
94}
95
96impl std::fmt::Debug for HarmonyEncoding {
97    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
98        f.debug_struct("HarmonyEncoding")
99            .field("name", &self.name)
100            .field("tokenizer_name", &self.tokenizer_name)
101            .field("n_ctx", &self.n_ctx)
102            .field("max_message_tokens", &self.max_message_tokens)
103            .field("max_action_length", &self.max_action_length)
104            .finish()
105    }
106}
107
108impl std::fmt::Display for HarmonyEncoding {
109    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110        write!(f, "Renderer({})", self.name)
111    }
112}
113
114// General methods
115impl HarmonyEncoding {
116    pub fn name(&self) -> &str {
117        &self.name
118    }
119
120    pub fn tokenizer_name(&self) -> &str {
121        &self.tokenizer_name
122    }
123
124    pub fn max_message_tokens(&self) -> usize {
125        self.max_message_tokens
126    }
127
128    pub fn tokenizer(&self) -> &CoreBPE {
129        &self.tokenizer
130    }
131
132    pub fn stop_tokens(&self) -> anyhow::Result<HashSet<Rank>> {
133        self.stop_formatting_tokens
134            .iter()
135            .copied()
136            .map(|t| match self.render_formatting_token(t) {
137                Ok(t) => Ok(t),
138                Err(RenderFormattingTokenError::UnmappedToken(_)) => Err(anyhow::anyhow!(
139                    "token {t} was specified as a stop token, but is not mapped"
140                )),
141                Err(e) => Err(anyhow::anyhow!(e).context("could not render stop token")),
142            })
143            .collect()
144    }
145
146    pub fn stop_tokens_for_assistant_actions(&self) -> anyhow::Result<HashSet<Rank>> {
147        self.stop_formatting_tokens_for_assistant_actions
148            .iter()
149            .copied()
150            .map(|t| match self.render_formatting_token(t) {
151                Ok(t) => Ok(t),
152                Err(RenderFormattingTokenError::UnmappedToken(_)) => Err(anyhow::anyhow!(
153                    "token {t} was specified as a stop token, but is not mapped"
154                )),
155                Err(e) => Err(anyhow::anyhow!(e).context("could not render stop token")),
156            })
157            .collect()
158    }
159}
160
161// Methods for rendering conversations
162impl HarmonyEncoding {
163    /// Renders a conversation into a collection of tokens.
164    pub fn render_conversation_into<'a, I, B>(
165        &self,
166        conversation: I,
167        into: &mut B,
168        config: Option<&RenderConversationConfig>,
169    ) -> anyhow::Result<()>
170    where
171        I: IntoIterator<Item = &'a Message>,
172        B: Extend<Rank>,
173    {
174        let messages: Vec<_> = conversation.into_iter().collect();
175        let has_function_tools = messages.iter().any(|msg| {
176            msg.content.iter().any(|c| {
177                if let Content::DeveloperContent(dev) = c {
178                    if let Some(tools) = &dev.tools {
179                        if let Some(ns) = tools.get("functions") {
180                            !ns.tools.is_empty()
181                        } else {
182                            false
183                        }
184                    } else {
185                        false
186                    }
187                } else {
188                    false
189                }
190            })
191        });
192        let render_options = RenderOptions {
193            conversation_has_function_tools: has_function_tools,
194        };
195        let last_assistant_is_final = messages
196            .iter()
197            .rev()
198            .find_map(|msg| {
199                (msg.author.role == Role::Assistant)
200                    .then(|| msg.channel.as_deref() == Some("final"))
201            })
202            .unwrap_or(false);
203
204        let should_drop_analysis =
205            config.is_some_and(|c| c.auto_drop_analysis && last_assistant_is_final);
206
207        let first_final_idx = messages
208            .iter()
209            .position(|msg| msg.channel.as_deref() == Some("final"));
210
211        let result = messages
212            .iter()
213            .enumerate()
214            .filter(|(idx, msg)| {
215                !(should_drop_analysis
216                    && first_final_idx.is_some_and(|first| *idx < first)
217                    && msg.channel.as_deref() == Some("analysis"))
218            })
219            .try_for_each(|(_, msg)| self.render_into(msg, into, Some(&render_options)));
220        result?;
221        Ok(())
222    }
223
224    /// Renders a conversation into a collection of tokens, adding the next turn role.
225    ///
226    /// This method is used to prepare a conversation for inference.
227    pub fn render_conversation_for_completion_into<'a, I, B>(
228        &self,
229        conversation: I,
230        next_turn_role: Role,
231        into: &mut B,
232        config: Option<&RenderConversationConfig>,
233    ) -> anyhow::Result<()>
234    where
235        I: IntoIterator<Item = &'a Message>,
236        B: Extend<Rank>,
237    {
238        let _config = config.unwrap_or(&RenderConversationConfig::default());
239        self.render_conversation_into(conversation, into, config)?;
240        self.render_formatting_token_into(FormattingToken::Start, into)?;
241        self.render_text_into(next_turn_role.as_str(), into)?;
242        Ok(())
243    }
244
245    pub fn render_conversation_for_completion<'a, I>(
246        &self,
247        conversation: I,
248        next_turn_role: Role,
249        config: Option<&RenderConversationConfig>,
250    ) -> anyhow::Result<Vec<Rank>>
251    where
252        I: IntoIterator<Item = &'a Message>,
253    {
254        let mut into = vec![];
255        self.render_conversation_for_completion_into(
256            conversation,
257            next_turn_role,
258            &mut into,
259            config,
260        )?;
261        Ok(into)
262    }
263
264    /// Render a conversation for training.
265    ///
266    /// If the last message in the conversation is an assistant message to the
267    /// `final` channel, replace the trailing `<|end|>` token with `<|return|>`.
268    pub fn render_conversation_for_training<'a, I>(
269        &self,
270        conversation: I,
271        config: Option<&RenderConversationConfig>,
272    ) -> anyhow::Result<Vec<Rank>>
273    where
274        I: IntoIterator<Item = &'a Message>,
275    {
276        let messages: Vec<&Message> = conversation.into_iter().collect();
277        let mut out = vec![];
278        self.render_conversation_into(messages.iter().copied(), &mut out, config)?;
279        if let Some(last) = messages.last() {
280            if last.author.role == Role::Assistant && last.channel.as_deref() == Some("final") {
281                if let Some(last_token) = out.last_mut() {
282                    *last_token =
283                        self.render_formatting_token(FormattingToken::EndMessageDoneSampling)?;
284                }
285            }
286        }
287        Ok(out)
288    }
289
290    /// Render a conversation without appending a new role.
291    pub fn render_conversation<'a, I>(
292        &self,
293        conversation: I,
294        config: Option<&RenderConversationConfig>,
295    ) -> anyhow::Result<Vec<Rank>>
296    where
297        I: IntoIterator<Item = &'a Message>,
298    {
299        let mut out = vec![];
300        self.render_conversation_into(conversation, &mut out, config)?;
301        Ok(out)
302    }
303
304    /// Render a single message into tokens.
305    pub fn render(
306        &self,
307        message: &Message,
308        render_options: Option<&RenderOptions>,
309    ) -> anyhow::Result<Vec<Rank>> {
310        let mut out = vec![];
311        Render::<Message>::render(self, message, &mut out, render_options)?;
312        Ok(out)
313    }
314
315    /// Render a single message into the provided buffer.
316    pub fn render_into<B>(
317        &self,
318        message: &Message,
319        into: &mut B,
320        render_options: Option<&RenderOptions>,
321    ) -> anyhow::Result<()>
322    where
323        B: Extend<Rank>,
324    {
325        Render::<Message>::render(self, message, into, render_options)
326    }
327}
328
329// Rendering helper methods
330impl HarmonyEncoding {
331    fn mapped_format_token(&self, t: FormattingToken) -> Option<&str> {
332        self.format_token_mapping.get(&t).map(|s| s.as_str())
333    }
334
335    fn render_formatting_token(
336        &self,
337        t: FormattingToken,
338    ) -> Result<Rank, RenderFormattingTokenError> {
339        let mapped = self
340            .mapped_format_token(t)
341            .ok_or(RenderFormattingTokenError::UnmappedToken(t))?;
342        let encoded = self.tokenizer.encode_with_special_tokens(mapped);
343        if encoded.len() != 1 {
344            return Err(RenderFormattingTokenError::InvalidEncoding {
345                token: t,
346                encoding: encoded,
347            });
348        }
349        Ok(encoded[0])
350    }
351
352    fn render_formatting_token_into<B>(
353        &self,
354        t: FormattingToken,
355        into: &mut B,
356    ) -> anyhow::Result<()>
357    where
358        B: Extend<Rank>,
359    {
360        let r = self.render_formatting_token(t)?;
361        into.extend(std::iter::once(r));
362        Ok(())
363    }
364
365    fn render_text_into<T, B>(&self, text: T, into: &mut B) -> anyhow::Result<()>
366    where
367        T: AsRef<str>,
368        B: Extend<Rank>,
369    {
370        into.extend(self.tokenizer.encode_ordinary(text.as_ref()));
371        Ok(())
372    }
373
374    pub fn parse_messages_from_completion_tokens_with_options<I>(
375        &self,
376        tokens: I,
377        role: Option<Role>,
378        options: ParseOptions,
379    ) -> anyhow::Result<Vec<Message>>
380    where
381        I: IntoIterator<Item = Rank>,
382    {
383        let mut parser = StreamableParser::new_with_options(self.clone(), role, options)?;
384        for token in tokens {
385            parser.process(token)?;
386        }
387        parser.process_eos()?;
388        Ok(parser.into_messages())
389    }
390
391    pub fn parse_messages_from_completion_tokens<I>(
392        &self,
393        tokens: I,
394        role: Option<Role>,
395    ) -> anyhow::Result<Vec<Message>>
396    where
397        I: IntoIterator<Item = Rank>,
398    {
399        self.parse_messages_from_completion_tokens_with_options(
400            tokens,
401            role,
402            ParseOptions::default(),
403        )
404    }
405
406    /// Helper to convert a JSON schema (OpenAPI style) to a TypeScript type definition.
407    fn json_schema_to_typescript(schema: &serde_json::Value, indent: &str) -> String {
408        // Helper to check if this schema is an enum
409        fn is_enum(schema: &serde_json::Value) -> bool {
410            schema
411                .get("enum")
412                .and_then(|e| e.as_array())
413                .is_some_and(|arr| !arr.is_empty())
414        }
415
416        // Handle oneOf at the top level
417        if let Some(one_of) = schema.get("oneOf") {
418            if let Some(arr) = one_of.as_array() {
419                let mut out = String::new();
420                let mut first = true;
421                for variant in arr {
422                    if !first {
423                        out.push('\n');
424                        out.push_str(&format!("{indent} | "));
425                    } else {
426                        out.push_str(&format!("\n{indent} | "));
427                        first = false;
428                    }
429                    let type_str =
430                        Self::json_schema_to_typescript(variant, &format!("{indent}   "));
431                    let mut type_str = type_str;
432                    if variant
433                        .get("nullable")
434                        .and_then(|n| n.as_bool())
435                        .unwrap_or(false)
436                        && !type_str.contains("null")
437                    {
438                        type_str = format!("{type_str} | null");
439                    }
440                    out.push_str(&type_str);
441                    // Add trailing comments (description, default)
442                    let mut trailing_comments = Vec::new();
443                    if let Some(desc) = variant.get("description") {
444                        if let Some(desc_str) = desc.as_str() {
445                            trailing_comments.push(desc_str.to_string());
446                        }
447                    }
448                    if let Some(default) = variant.get("default") {
449                        if default.is_string() && !is_enum(variant) {
450                            trailing_comments
451                                .push(format!("default: \"{}\"", default.as_str().unwrap()));
452                        } else {
453                            trailing_comments.push(format!("default: {default}"));
454                        }
455                    }
456                    if !trailing_comments.is_empty() {
457                        out.push_str(&format!(" // {}", trailing_comments.join(" ")));
458                    }
459                }
460                return out;
461            }
462        }
463        // Handle type as array (e.g., ["number", "string"])
464        if let Some(types) = schema.get("type").and_then(|v| v.as_array()) {
465            let mut type_strings = Vec::new();
466            for ty in types {
467                if let Some(ty_str) = ty.as_str() {
468                    let mapped = match ty_str {
469                        "integer" => "number",
470                        other => other,
471                    };
472                    type_strings.push(mapped.to_string());
473                }
474            }
475            if !type_strings.is_empty() {
476                return type_strings.join(" | ");
477            }
478        }
479        // Handle type
480        if let Some(ty) = schema.get("type").and_then(|v| v.as_str()) {
481            match ty {
482                "object" => {
483                    let mut out = String::new();
484                    // Render object-level description as comment
485                    if let Some(desc) = schema.get("description") {
486                        if let Some(desc_str) = desc.as_str() {
487                            out.push_str(&format!("{indent}// {desc_str}\n"));
488                        }
489                    }
490                    out.push_str("{\n");
491
492                    if let Some(props) = schema.get("properties") {
493                        if let Some(props_map) = props.as_object() {
494                            // Determine required fields
495                            let mut required = std::collections::HashSet::new();
496                            if let Some(req) = schema.get("required") {
497                                if let Some(req_arr) = req.as_array() {
498                                    for r in req_arr {
499                                        if let Some(s) = r.as_str() {
500                                            required.insert(s);
501                                        }
502                                    }
503                                }
504                            }
505                            for (key, val) in props_map {
506                                // Render title, description, and examples as comments
507                                if let Some(title) = val.get("title") {
508                                    if let Some(title_str) = title.as_str() {
509                                        out.push_str(&format!(
510                                            "{indent}// {title_str}\n{indent}//\n"
511                                        ));
512                                    }
513                                }
514                                // Only render description here if not a oneOf property
515                                if val.get("oneOf").is_none() {
516                                    if let Some(desc) = val.get("description") {
517                                        if let Some(desc_str) = desc.as_str() {
518                                            out.push_str(&format!("{indent}// {desc_str}\n"));
519                                        }
520                                    }
521                                }
522                                if let Some(examples) = val.get("examples") {
523                                    if let Some(arr) = examples.as_array() {
524                                        if !arr.is_empty() {
525                                            out.push_str(&format!("{indent}// Examples:\n"));
526                                            for ex in arr {
527                                                if let Some(ex_str) = ex.as_str() {
528                                                    out.push_str(&format!(
529                                                        "{indent}// - \"{ex_str}\"\n"
530                                                    ));
531                                                }
532                                            }
533                                        }
534                                    }
535                                }
536                                // Handle oneOf at the property level
537                                if let Some(one_of) = val.get("oneOf") {
538                                    if let Some(arr) = one_of.as_array() {
539                                        // Deduplicate property-level description if it matches the first variant's description
540                                        let mut property_desc: Option<&str> = None;
541                                        if let Some(desc) = val.get("description") {
542                                            if let Some(desc_str) = desc.as_str() {
543                                                property_desc = Some(desc_str);
544                                            }
545                                        }
546                                        let mut skip_property_desc = false;
547                                        if let Some(desc_str) = property_desc {
548                                            if let Some(first_variant) = arr.first() {
549                                                if let Some(variant_desc) =
550                                                    first_variant.get("description")
551                                                {
552                                                    if let Some(variant_desc_str) =
553                                                        variant_desc.as_str()
554                                                    {
555                                                        if desc_str == variant_desc_str {
556                                                            skip_property_desc = true;
557                                                        }
558                                                    }
559                                                }
560                                            }
561                                        }
562                                        // Add property-level comments above the property name if not skipped
563                                        let mut rendered_property_desc_above = false;
564                                        if !skip_property_desc {
565                                            if let Some(desc_str) = property_desc {
566                                                out.push_str(&format!("{indent}// {desc_str}\n"));
567                                                rendered_property_desc_above = true;
568                                            }
569                                        }
570                                        if let Some(default) = val.get("default") {
571                                            if default.is_string() && !is_enum(val) {
572                                                out.push_str(&format!(
573                                                    "{}// default: \"{}\"\n",
574                                                    indent,
575                                                    default.as_str().unwrap()
576                                                ));
577                                            } else if default.is_string() {
578                                                out.push_str(&format!(
579                                                    "{}// default: {}\n",
580                                                    indent,
581                                                    default.as_str().unwrap()
582                                                ));
583                                            } else {
584                                                out.push_str(&format!(
585                                                    "{indent}// default: {default}\n"
586                                                ));
587                                            }
588                                        }
589                                        // Add property name and optional marker
590                                        out.push_str(&format!(
591                                            "{}{}{}:\n",
592                                            indent,
593                                            key,
594                                            if required.contains(key.as_str()) {
595                                                ""
596                                            } else {
597                                                "?"
598                                            }
599                                        ));
600                                        // Render each variant
601                                        for (i, variant) in arr.iter().enumerate() {
602                                            out.push_str(&format!("{indent} | "));
603                                            let type_str = Self::json_schema_to_typescript(
604                                                variant,
605                                                &format!("{indent}   "),
606                                            );
607                                            // Handle nullable in variant
608                                            let mut type_str = type_str;
609                                            if variant
610                                                .get("nullable")
611                                                .and_then(|n| n.as_bool())
612                                                .unwrap_or(false)
613                                                && !type_str.contains("null")
614                                            {
615                                                type_str = format!("{type_str} | null");
616                                            }
617                                            out.push_str(&type_str);
618                                            // Add variant-level comments after the type
619                                            let mut trailing_comments = Vec::new();
620                                            if i == 0 && rendered_property_desc_above {
621                                                // Do not add any description for the first variant if property-level description was rendered above
622                                            } else if let Some(desc) = variant.get("description") {
623                                                if let Some(desc_str) = desc.as_str() {
624                                                    // Only render if not equal to property-level description
625                                                    if Some(desc_str) != property_desc {
626                                                        trailing_comments
627                                                            .push(desc_str.to_string());
628                                                    }
629                                                }
630                                            }
631                                            if let Some(default) = variant.get("default") {
632                                                if default.is_string() && !is_enum(variant) {
633                                                    trailing_comments.push(format!(
634                                                        "default: \"{}\"",
635                                                        default.as_str().unwrap()
636                                                    ));
637                                                } else if default.is_string() {
638                                                    trailing_comments.push(format!(
639                                                        "default: {}",
640                                                        default.as_str().unwrap()
641                                                    ));
642                                                } else {
643                                                    trailing_comments
644                                                        .push(format!("default: {default}"));
645                                                }
646                                            }
647                                            if !trailing_comments.is_empty() {
648                                                out.push_str(&format!(
649                                                    " // {}",
650                                                    trailing_comments.join(" ")
651                                                ));
652                                            }
653                                            out.push('\n');
654                                        }
655                                        out.push_str(&format!("{indent},\n"));
656                                        continue;
657                                    }
658                                }
659                                // Normal property rendering
660                                out.push_str(&format!(
661                                    "{}{}{}: ",
662                                    indent,
663                                    key,
664                                    if required.contains(key.as_str()) {
665                                        ""
666                                    } else {
667                                        "?"
668                                    }
669                                ));
670                                // Handle nullable
671                                let mut type_str =
672                                    Self::json_schema_to_typescript(val, &format!("{indent}    "));
673                                if val
674                                    .get("nullable")
675                                    .and_then(|n| n.as_bool())
676                                    .unwrap_or(false)
677                                    && !type_str.contains("null")
678                                {
679                                    type_str = format!("{type_str} | null");
680                                }
681                                out.push_str(&type_str);
682                                out.push(',');
683                                // Add default as comment if present (and not already handled)
684                                if val.get("oneOf").is_none() {
685                                    if let Some(default) = val.get("default") {
686                                        if default.is_string() && !is_enum(val) {
687                                            out.push_str(&format!(
688                                                " // default: \"{}\"",
689                                                default.as_str().unwrap()
690                                            ));
691                                        } else if default.is_string() {
692                                            out.push_str(&format!(
693                                                " // default: {}",
694                                                default.as_str().unwrap()
695                                            ));
696                                        } else {
697                                            out.push_str(&format!(" // default: {default}"));
698                                        }
699                                    }
700                                }
701                                out.push('\n');
702                            }
703                        }
704                    }
705                    out.push_str(&format!("{indent}}}"));
706                    out
707                }
708                "string" => {
709                    if let Some(enum_vals) = schema.get("enum") {
710                        if let Some(arr) = enum_vals.as_array() {
711                            let enums: Vec<String> = arr
712                                .iter()
713                                .filter_map(|v| v.as_str().map(|s| format!("\"{s}\"")))
714                                .collect();
715                            if !enums.is_empty() {
716                                return enums.join(" | ");
717                            }
718                        }
719                    }
720                    "string".to_string()
721                }
722                "number" => "number".to_string(),
723                "integer" => "number".to_string(),
724                "boolean" => "boolean".to_string(),
725                "array" => {
726                    if let Some(items) = schema.get("items") {
727                        format!("{}[]", Self::json_schema_to_typescript(items, indent))
728                    } else {
729                        "Array<any>".to_string()
730                    }
731                }
732                _ => "any".to_string(),
733            }
734        } else if let Some(one_of) = schema.get("oneOf") {
735            // Defensive: already handled above, but just in case
736            if let Some(arr) = one_of.as_array() {
737                let mut out = String::new();
738                let mut first = true;
739                for variant in arr {
740                    if !first {
741                        out.push_str("\n | ");
742                    } else {
743                        first = false;
744                    }
745                    out.push_str(&Self::json_schema_to_typescript(variant, indent));
746                }
747                return out;
748            }
749            "any".to_string()
750        } else {
751            "any".to_string()
752        }
753    }
754
755    /// Helper to template the tools section for system content rendering.
756    fn template_tools_section(
757        tools: &std::collections::BTreeMap<String, crate::chat::ToolNamespaceConfig>,
758    ) -> String {
759        let mut tool_sections = Vec::<String>::new();
760        tool_sections.push("# Tools".to_string());
761        for ns_config in tools.values() {
762            let mut tool_section_content = Vec::<String>::new();
763            tool_section_content.push(format!("## {}\n", ns_config.name));
764            if let Some(desc) = &ns_config.description {
765                for line in desc.lines() {
766                    if !ns_config.tools.is_empty() {
767                        tool_section_content.push(format!("// {line}"));
768                    } else {
769                        tool_section_content.push(line.to_string());
770                    }
771                }
772            }
773            if !ns_config.tools.is_empty() {
774                tool_section_content.push(format!("namespace {} {{\n", ns_config.name));
775                for tool in &ns_config.tools {
776                    for line in tool.description.lines() {
777                        tool_section_content.push(format!("// {line}"));
778                    }
779                    if let Some(params) = &tool.parameters {
780                        let param_type = Self::json_schema_to_typescript(params, "");
781                        tool_section_content.push(format!(
782                            "type {} = (_: {}) => any;\n",
783                            tool.name, param_type
784                        ));
785                    } else {
786                        tool_section_content.push(format!("type {} = () => any;\n", tool.name));
787                    }
788                }
789                tool_section_content.push(format!("}} // namespace {}", ns_config.name));
790            }
791            tool_sections.push(tool_section_content.join("\n"));
792        }
793        tool_sections.join("\n\n")
794    }
795}
796
797#[derive(Clone, Copy, Debug, Default)]
798pub struct RenderOptions {
799    pub conversation_has_function_tools: bool,
800}
801
802trait Render<T: ?Sized> {
803    fn render<B>(
804        &self,
805        item: &T,
806        into: &mut B,
807        render_options: Option<&RenderOptions>,
808    ) -> anyhow::Result<()>
809    where
810        B: Extend<Rank>;
811}
812
813impl Render<Message> for HarmonyEncoding {
814    fn render<B>(
815        &self,
816        message: &Message,
817        into: &mut B,
818        render_options: Option<&RenderOptions>,
819    ) -> anyhow::Result<()>
820    where
821        B: Extend<Rank>,
822    {
823        self.render_formatting_token_into(FormattingToken::Start, into)?;
824
825        // render role then username
826        if matches!(message.author.role, Role::Tool) {
827            // for tools we only put the name
828            if let Some(name) = &message.author.name {
829                self.render_text_into(name, into)?;
830            } else {
831                anyhow::bail!("Tools should have a name!");
832            }
833        } else {
834            // For users and assistants we put both the role, and optionally the user name.
835            self.render_text_into(message.author.role.as_str(), into)?;
836            if let Some(name) = &message.author.name {
837                self.render_text_into(format!(":{name}"), into)?;
838            }
839        };
840
841        // next render the header recipient, if there is one
842        if let Some(recipient) = &message.recipient {
843            if recipient != "all" {
844                self.render_text_into(format!(" to={recipient}"), into)?;
845            }
846        }
847
848        // next header channel
849        if let Some(channel) = &message.channel {
850            self.render_formatting_token_into(FormattingToken::Channel, into)?;
851            self.render_text_into(channel, into)?;
852        }
853
854        // finally content type
855        if let Some(content_type) = &message.content_type {
856            // <|constrain|> is a unique case which needs to be tokenized as a special token
857            if let Some(constrain_marker) =
858                self.mapped_format_token(FormattingToken::ConstrainedFormat)
859            {
860                if let Some(rest) = content_type.strip_prefix(constrain_marker) {
861                    // Render the space, then the constrain marker as a special token, then the rest as text (if any)
862                    self.render_text_into(" ", into)?;
863                    self.render_formatting_token_into(FormattingToken::ConstrainedFormat, into)?;
864                    if !rest.is_empty() {
865                        self.render_text_into(rest, into)?;
866                    }
867                } else {
868                    self.render_text_into(format!(" {content_type}"), into)?;
869                }
870            } else {
871                self.render_text_into(format!(" {content_type}"), into)?;
872            }
873        }
874
875        self.render_formatting_token_into(FormattingToken::Message, into)?;
876        for content in message.content.iter() {
877            // SystemContent is only allowed in system messages
878            if let crate::chat::Content::SystemContent(_) = content {
879                anyhow::ensure!(
880                    message.author.role == crate::chat::Role::System,
881                    "SystemContent may only appear in system messages, found in {:?}",
882                    message.author.role
883                );
884            }
885            if let crate::chat::Content::DeveloperContent(_) = content {
886                anyhow::ensure!(
887                    message.author.role == crate::chat::Role::Developer,
888                    "DeveloperContent may only appear in developer messages, found in {:?}",
889                    message.author.role
890                );
891            }
892            Render::<Content>::render(self, content, into, render_options)?;
893        }
894
895        // If there is a tool call we should render a tool call token
896        if message.author.role == crate::chat::Role::Assistant && message.recipient.is_some() {
897            self.render_formatting_token_into(FormattingToken::EndMessageAssistantToTool, into)?;
898        } else {
899            self.render_formatting_token_into(FormattingToken::EndMessage, into)?;
900        }
901        Ok(())
902    }
903}
904
905// Dispatch Content variants to their specific Render implementations
906impl Render<Content> for HarmonyEncoding {
907    fn render<B>(
908        &self,
909        content: &Content,
910        into: &mut B,
911        render_options: Option<&RenderOptions>,
912    ) -> anyhow::Result<()>
913    where
914        B: Extend<Rank>,
915    {
916        match content {
917            Content::Text(text) => Render::<TextContent>::render(self, text, into, render_options),
918            Content::SystemContent(sys) => {
919                Render::<SystemContent>::render(self, sys, into, render_options)
920            }
921            Content::DeveloperContent(dev) => {
922                Render::<crate::chat::DeveloperContent>::render(self, dev, into, render_options)
923            }
924        }
925    }
926}
927
928// Render plain text content
929impl Render<TextContent> for HarmonyEncoding {
930    fn render<B>(
931        &self,
932        text: &TextContent,
933        into: &mut B,
934        _render_options: Option<&RenderOptions>,
935    ) -> anyhow::Result<()>
936    where
937        B: Extend<Rank>,
938    {
939        self.render_text_into(&text.text, into)
940    }
941}
942
943// Render system-specific content (model identity, instructions, effort)
944impl Render<SystemContent> for HarmonyEncoding {
945    fn render<B>(
946        &self,
947        sys: &SystemContent,
948        into: &mut B,
949        render_options: Option<&RenderOptions>,
950    ) -> anyhow::Result<()>
951    where
952        B: Extend<Rank>,
953    {
954        let mut sections = Vec::<String>::new();
955
956        let mut top_section = Vec::<String>::new();
957        if let Some(model_id) = &sys.model_identity {
958            top_section.push(model_id.clone());
959        }
960        if let Some(knowledge_cutoff) = &sys.knowledge_cutoff {
961            top_section.push(format!("Knowledge cutoff: {knowledge_cutoff}"));
962        }
963        if let Some(conversation_start_date) = &sys.conversation_start_date {
964            top_section.push(format!("Current date: {conversation_start_date}"));
965        }
966        if !top_section.is_empty() {
967            sections.push(top_section.join("\n"));
968        }
969
970        let mut instructions_and_reasoning = Vec::<String>::new();
971        if let Some(effort) = sys.reasoning_effort {
972            let effort_str = match effort {
973                ReasoningEffort::Low => "low",
974                ReasoningEffort::Medium => "medium",
975                ReasoningEffort::High => "high",
976            };
977            instructions_and_reasoning.push(format!("Reasoning: {effort_str}"));
978        }
979        if !instructions_and_reasoning.is_empty() {
980            sections.push(instructions_and_reasoning.join("\n"));
981        }
982
983        if let Some(tools) = &sys.tools {
984            if !tools.is_empty() {
985                sections.push(Self::template_tools_section(tools));
986            }
987        }
988
989        if let Some(channel_config) = &sys.channel_config {
990            if !channel_config.valid_channels.is_empty() {
991                let channels_str = channel_config.valid_channels.join(", ");
992                let mut channels_header = format!("# Valid channels: {channels_str}.");
993                if channel_config.channel_required {
994                    channels_header.push_str(" Channel must be included for every message.");
995                }
996                if render_options.is_some_and(|o| o.conversation_has_function_tools) {
997                    channels_header.push('\n');
998                    channels_header.push_str(
999                        "Calls to these tools must go to the commentary channel: 'functions'.",
1000                    );
1001                }
1002                sections.push(channels_header);
1003            }
1004        }
1005        let formatted = sections.join("\n\n");
1006        self.render_text_into(&formatted, into)?;
1007        Ok(())
1008    }
1009}
1010
1011// Render developer-specific content (instructions, tools)
1012impl Render<crate::chat::DeveloperContent> for HarmonyEncoding {
1013    fn render<B>(
1014        &self,
1015        dev: &crate::chat::DeveloperContent,
1016        into: &mut B,
1017        _render_options: Option<&RenderOptions>,
1018    ) -> anyhow::Result<()>
1019    where
1020        B: Extend<Rank>,
1021    {
1022        let mut sections = Vec::<String>::new();
1023
1024        if let Some(instr) = &dev.instructions {
1025            sections.push("# Instructions".to_string());
1026            sections.push(instr.clone());
1027        }
1028
1029        if let Some(tools) = &dev.tools {
1030            if !tools.is_empty() {
1031                sections.push(Self::template_tools_section(tools));
1032            }
1033        }
1034        let formatted = sections.join("\n\n");
1035        self.render_text_into(&formatted, into)?;
1036        Ok(())
1037    }
1038}
1039
1040#[derive(Clone, Copy, Debug)]
1041pub struct ParseOptions {
1042    pub strict: bool,
1043}
1044
1045impl Default for ParseOptions {
1046    fn default() -> Self {
1047        Self { strict: true }
1048    }
1049}
1050
1051/// Incremental parser that can consume tokens one by one.
1052///
1053/// It keeps track of all tokens seen so far, exposes all fully parsed messages
1054/// and retains the partially parsed state of the current message.
1055pub struct StreamableParser {
1056    encoding: HarmonyEncoding,
1057    next_role: Option<Role>,
1058    tokens: Vec<Rank>,
1059    messages: Vec<Message>,
1060    state: StreamState,
1061    stop_tokens: HashSet<Rank>,
1062    last_content_delta: Option<String>,
1063    undecoded_tokens: Vec<Rank>,
1064    undecoded_bytes: Vec<u8>,
1065    options: ParseOptions,
1066}
1067
1068#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
1069pub enum StreamState {
1070    ExpectStart,
1071    Header {
1072        header_tokens: Vec<Rank>,
1073    },
1074    Content {
1075        header: ParsedHeader,
1076        content_tokens: Vec<Rank>,
1077    },
1078}
1079
1080impl StreamableParser {
1081    /// Create a new streaming parser starting with the given role.
1082    pub fn new(encoding: HarmonyEncoding, role: Option<Role>) -> anyhow::Result<Self> {
1083        Self::new_with_options(encoding, role, ParseOptions::default())
1084    }
1085
1086    /// Create a new streaming parser with explicit options.
1087    pub fn new_with_options(
1088        encoding: HarmonyEncoding,
1089        role: Option<Role>,
1090        options: ParseOptions,
1091    ) -> anyhow::Result<Self> {
1092        let stop_tokens = encoding.stop_tokens()?;
1093        let (state, next_role) = match role {
1094            Some(role) => (
1095                StreamState::Header {
1096                    header_tokens: Vec::new(),
1097                },
1098                Some(role),
1099            ),
1100            None => (StreamState::ExpectStart, None),
1101        };
1102        Ok(Self {
1103            encoding,
1104            next_role,
1105            tokens: Vec::new(),
1106            messages: Vec::new(),
1107            state,
1108            stop_tokens,
1109            last_content_delta: None,
1110            undecoded_tokens: Vec::new(),
1111            undecoded_bytes: Vec::new(),
1112            options,
1113        })
1114    }
1115
1116    /// Consume a single token and update the internal state.
1117    /// Consume a single token and update the internal state.
1118    fn process_next(&mut self, token: Option<Rank>) -> anyhow::Result<&mut Self> {
1119        if let Some(token) = token {
1120            self.tokens.push(token);
1121        }
1122        // Clone next_role up front to avoid borrow checker issues
1123        let next_role_clone = self.next_role.clone();
1124        match &mut self.state {
1125            StreamState::ExpectStart => {
1126                let start = self
1127                    .encoding
1128                    .render_formatting_token(FormattingToken::Start)?;
1129                match token {
1130                    Some(token) if token == start => {
1131                        self.state = StreamState::Header {
1132                            header_tokens: Vec::new(),
1133                        };
1134                    }
1135                    Some(token) => {
1136                        anyhow::bail!(
1137                            "Unexpected token {} while expecting start token {}",
1138                            token,
1139                            start
1140                        );
1141                    }
1142                    None => {
1143                        // receiving EOS while waiting for start token is actually fine
1144                        // as we may have just parsed a stop token. in this case we can
1145                        // simple keep state as is
1146                    }
1147                }
1148            }
1149            StreamState::Header { header_tokens } => {
1150                let msg_tok = self
1151                    .encoding
1152                    .render_formatting_token(FormattingToken::Message)?;
1153                match token {
1154                    Some(token) if token == msg_tok => {
1155                        // Clone the tokens and next_role, then clear the state before parsing
1156                        let header_tokens_cloned = header_tokens.clone();
1157                        let next_role_cloned = next_role_clone;
1158                        // Set state to dummy to drop mutable borrow
1159                        self.state = StreamState::ExpectStart;
1160                        let header =
1161                            self.parse_header_from_tokens(&header_tokens_cloned, next_role_cloned)?;
1162                        self.next_role = None;
1163                        self.state = StreamState::Content {
1164                            header,
1165                            content_tokens: Vec::new(),
1166                        };
1167                    }
1168                    Some(token) if !self.options.strict && self.stop_tokens.contains(&token) => {
1169                        // Encountered a stop token while in Header state. This means we have
1170                        // accumulated header tokens but never saw a <|message|> token, so the
1171                        // message is malformed. If we have a role, parse header metadata and
1172                        // treat remaining tokens as content.
1173                        if let Some(role) = next_role_clone {
1174                            if !header_tokens.is_empty() {
1175                                let decoded =
1176                                    self.encoding.tokenizer().decode_utf8(header_tokens)?;
1177                                let (header, remaining_content) =
1178                                    self.parse_header_from_string(decoded, Some(role), false)?;
1179
1180                                // Use remaining content if present, otherwise empty string
1181                                let text = remaining_content.unwrap_or_default();
1182                                let message = Message {
1183                                    author: header.author.clone(),
1184                                    recipient: header.recipient.clone(),
1185                                    channel: header.channel.clone(),
1186                                    content_type: header.content_type.clone(),
1187                                    content: vec![Content::Text(TextContent { text })],
1188                                };
1189                                self.messages.push(message);
1190                            }
1191                        }
1192                        // Transition to ExpectStart to wait for the next message
1193                        self.state = StreamState::ExpectStart;
1194                        self.next_role = None;
1195                    }
1196                    Some(token) => {
1197                        header_tokens.push(token);
1198                    }
1199                    None => {
1200                        anyhow::bail!(
1201                            "Unexpected EOS while waiting for message header to complete"
1202                        );
1203                    }
1204                }
1205            }
1206            StreamState::Content {
1207                header,
1208                content_tokens,
1209            } => {
1210                let is_eos = if let Some(token) = token {
1211                    if self.stop_tokens.contains(&token) {
1212                        // this is a stop token, dont parse and mark EOS
1213                        true
1214                    } else {
1215                        self.undecoded_tokens.push(token);
1216                        // some tokens might not appropriately decode on their own. If they don't
1217                        // we will collect them until they eventually decode
1218                        match self
1219                            .encoding
1220                            .tokenizer()
1221                            .decode_bytes(&self.undecoded_tokens)
1222                        {
1223                            Ok(decoded_bytes) => {
1224                                self.undecoded_bytes.extend(decoded_bytes.iter().copied());
1225                                match String::from_utf8(self.undecoded_bytes.clone()) {
1226                                    Ok(decoded_str) => {
1227                                        self.encoding
1228                                            .render_text_into(&decoded_str, content_tokens)?;
1229                                        self.last_content_delta = Some(decoded_str);
1230                                        self.undecoded_bytes.clear();
1231                                    }
1232                                    Err(e) => {
1233                                        let utf8_error = e.utf8_error();
1234                                        let decoded_bytes = e.into_bytes();
1235
1236                                        let valid_len = utf8_error.valid_up_to();
1237
1238                                        let mut content_delta = String::new();
1239                                        if valid_len > 0 {
1240                                            let valid_str = String::from_utf8(
1241                                                decoded_bytes[..valid_len].to_vec(),
1242                                            )
1243                                            .unwrap();
1244                                            self.encoding
1245                                                .render_text_into(&valid_str, content_tokens)?;
1246                                            content_delta.push_str(&valid_str);
1247                                            self.undecoded_bytes.drain(..valid_len);
1248                                        }
1249
1250                                        match utf8_error.error_len() {
1251                                            Some(error_len) => {
1252                                                self.encoding.render_text_into(
1253                                                    REPLACEMENT,
1254                                                    content_tokens,
1255                                                )?;
1256                                                content_delta.push_str(REPLACEMENT);
1257                                                self.undecoded_bytes.drain(..error_len);
1258                                            }
1259                                            None => {
1260                                                // waiting on next byte in our utf-8 sequence
1261                                                self.last_content_delta = None;
1262                                            }
1263                                        }
1264
1265                                        if !content_delta.is_empty() {
1266                                            self.last_content_delta = Some(content_delta);
1267                                        }
1268                                    }
1269                                }
1270                                self.undecoded_tokens.clear();
1271                            }
1272                            Err(_) => {
1273                                // Bytes not yet valid utf-8, wait on the next token
1274                                self.last_content_delta = None;
1275                            }
1276                        }
1277                        // this was not an EOS
1278                        false
1279                    }
1280                } else {
1281                    // token = None signals EOS to this function
1282                    true
1283                };
1284                if is_eos {
1285                    // Our rendered content tokens are valid utf-8, so we can decode them directly
1286                    let content_text = self.encoding.tokenizer().decode_utf8(content_tokens)?;
1287                    // Decode any remaining undecoded tokens, replacing any invalid tokens with the replacement character
1288                    let tokens_text = match self
1289                        .encoding
1290                        .tokenizer()
1291                        .decode_utf8(self.undecoded_tokens.clone())
1292                    {
1293                        Ok(text) => text,
1294                        Err(_) => REPLACEMENT.to_string(),
1295                    };
1296                    // Decode any remaining undecoded bytes, replacing any invalid bytes with the replacement character
1297                    let bytes_text = String::from_utf8_lossy(&self.undecoded_bytes);
1298                    let text = content_text + &tokens_text + &bytes_text;
1299                    let message = Message {
1300                        author: header.author.clone(),
1301                        recipient: header.recipient.clone(),
1302                        channel: header.channel.clone(),
1303                        content_type: header.content_type.clone(),
1304                        content: vec![Content::Text(TextContent { text })],
1305                    };
1306                    self.messages.push(message);
1307                    self.state = StreamState::ExpectStart;
1308                    self.last_content_delta = None;
1309                    self.undecoded_tokens.clear();
1310                    self.undecoded_bytes.clear();
1311                }
1312            }
1313        }
1314        Ok(self)
1315    }
1316
1317    pub fn process(&mut self, token: Rank) -> anyhow::Result<&mut Self> {
1318        self.process_next(Some(token))
1319    }
1320
1321    pub fn process_eos(&mut self) -> anyhow::Result<&mut Self> {
1322        self.process_next(None)?;
1323        Ok(self)
1324    }
1325
1326    /// Helper to parse header metadata from a decoded string.
1327    /// Returns the parsed header and any remaining content after extracting header parts.
1328    ///
1329    /// If `parse_recipient_and_type` is true, tries to parse recipient and content_type from
1330    /// whitespace-separated tokens (normal header parsing). If false, treats all remaining
1331    /// text after extracting channel as content (for malformed messages).
1332    fn parse_header_from_string(
1333        &self,
1334        mut header_string: String,
1335        role: Option<Role>,
1336        parse_recipient_and_type: bool,
1337    ) -> anyhow::Result<(ParsedHeader, Option<String>)> {
1338        let mut channel: Option<String> = None;
1339        if let Some(channel_marker) = self.encoding.mapped_format_token(FormattingToken::Channel) {
1340            if let Some(idx) = header_string.find(channel_marker) {
1341                let after_marker = &header_string[idx + channel_marker.len()..];
1342                let channel_end = after_marker
1343                    .find(|c: char| c.is_whitespace() || c == '<')
1344                    .unwrap_or(after_marker.len());
1345                let channel_value = &after_marker[..channel_end];
1346                if channel_value.is_empty() {
1347                    anyhow::bail!("channel marker present but no channel value found in header");
1348                }
1349                channel = Some(channel_value.to_string());
1350
1351                let mut new_header = String::new();
1352                new_header.push_str(&header_string[..idx]);
1353                new_header.push_str(&after_marker[channel_end..]);
1354                header_string = new_header;
1355            }
1356        }
1357
1358        // Trim extraneous whitespace that may have been introduced when we
1359        // removed the channel section.
1360        header_string = header_string.trim().to_string();
1361
1362        // If the constrained format marker is present but not preceded by
1363        // whitespace (e.g. "to=foo<|constrain|>json"), insert a space before
1364        // the marker so that splitting on whitespace treats the content type
1365        // as a separate token.
1366        if let Some(constrain_marker) = self
1367            .encoding
1368            .mapped_format_token(FormattingToken::ConstrainedFormat)
1369        {
1370            if header_string.contains(constrain_marker) {
1371                header_string = header_string
1372                    .replace(constrain_marker, &format!(" {constrain_marker}"))
1373                    .trim()
1374                    .to_string();
1375            }
1376        }
1377
1378        let mut parts: Vec<&str> = header_string.split_ascii_whitespace().collect();
1379
1380        let mut role_str_opt: Option<String> = None;
1381        let role = match role {
1382            Some(r) => r,
1383            None => {
1384                let role_str = parts
1385                    .first()
1386                    .context("message header did not contain a role")?;
1387                role_str_opt = Some((*role_str).to_string());
1388                let parsed_role = Role::try_from(*role_str);
1389                let out = match parsed_role {
1390                    Ok(r) => r,
1391                    Err(_) => {
1392                        // If recipient is present, treat as tool call
1393                        if parts.len() > 1 || (parts.len() == 1 && parts[0].starts_with("to=")) {
1394                            parts.remove(0); // Remove the unknown role string
1395                            Role::Tool
1396                        } else {
1397                            return Err(anyhow::anyhow!("Unknown role: {}", role_str));
1398                        }
1399                    }
1400                };
1401                out
1402            }
1403        };
1404
1405        if let Some(&first) = parts.first() {
1406            if first == role.as_str() {
1407                parts.remove(0);
1408            }
1409        }
1410
1411        let mut recipient: Option<String> = None;
1412        let mut content_type: Option<String> = None;
1413        let remaining_content: Option<String>;
1414
1415        if parse_recipient_and_type && !parts.is_empty() {
1416            let num_parts = parts.len();
1417            // SAFETY: we know that there is at least one part remaining, because of is_empty check above
1418            let last_part = parts.pop().unwrap();
1419
1420            if let Some(stripped) = last_part.strip_prefix("to=") {
1421                // The header contains a recipient but *no* content-type.
1422                recipient = Some(stripped.to_string());
1423            } else if num_parts == 1 {
1424                // Only one part total (after potential role removal) and it doesn't start
1425                // with "to=" => interpret it as a standalone recipient.
1426                recipient = Some(last_part.to_string());
1427            } else {
1428                // More than one token and the last one is not a recipient -> treat as content-type.
1429                content_type = Some(last_part.to_string());
1430
1431                // After removing the content-type there may be exactly one token describing the recipient.
1432                if let Some(raw_recipient) = parts.pop() {
1433                    recipient = if let Some(stripped) = raw_recipient.strip_prefix("to=") {
1434                        Some(stripped.to_string())
1435                    } else {
1436                        Some(raw_recipient.to_string())
1437                    };
1438                }
1439            }
1440
1441            // Any remaining parts are content (not header metadata)
1442            remaining_content = if !parts.is_empty() {
1443                Some(parts.join(" "))
1444            } else {
1445                None
1446            };
1447        } else {
1448            // Treat all remaining parts as content when not parsing recipient and content type
1449            remaining_content = if !parts.is_empty() {
1450                Some(parts.join(" "))
1451            } else {
1452                None
1453            };
1454        }
1455
1456        let author = if role == Role::Tool {
1457            let name = role_str_opt;
1458            Author { role, name }
1459        } else {
1460            Author { role, name: None }
1461        };
1462        Ok((
1463            ParsedHeader {
1464                author,
1465                recipient,
1466                channel,
1467                content_type,
1468            },
1469            remaining_content,
1470        ))
1471    }
1472
1473    fn parse_header_from_tokens(
1474        &self,
1475        header_tokens: &[Rank],
1476        role: Option<Role>,
1477    ) -> anyhow::Result<ParsedHeader> {
1478        let header_string = self
1479            .encoding
1480            .tokenizer()
1481            .decode_utf8(header_tokens)
1482            .context("could not decode header")?;
1483
1484        let (header, remaining_content) =
1485            self.parse_header_from_string(header_string, role, true)?;
1486
1487        if remaining_content.is_some() {
1488            anyhow::bail!(
1489                "unexpected tokens remaining in message header: {:?}",
1490                remaining_content
1491            );
1492        }
1493
1494        Ok(header)
1495    }
1496
1497    /// Return the textual content of the current message so far.
1498    pub fn current_content(&self) -> anyhow::Result<String> {
1499        match &self.state {
1500            StreamState::Content { content_tokens, .. } => self
1501                .encoding
1502                .tokenizer()
1503                .decode_utf8(content_tokens)
1504                .map_err(|e| anyhow::anyhow!(e)),
1505            _ => Ok(String::new()),
1506        }
1507    }
1508
1509    /// Role of the current message if it has been parsed.
1510    pub fn current_role(&self) -> Option<Role> {
1511        match &self.state {
1512            StreamState::Content { header, .. } => Some(header.author.role.clone()),
1513            _ => self.next_role.clone(),
1514        }
1515    }
1516
1517    /// Current content type if known.
1518    pub fn current_content_type(&self) -> Option<String> {
1519        match &self.state {
1520            StreamState::Content { header, .. } => header.content_type.clone(),
1521            _ => None,
1522        }
1523    }
1524
1525    /// Decode the last content delta if available.
1526    pub fn last_content_delta(&self) -> anyhow::Result<Option<String>> {
1527        Ok(self.last_content_delta.clone())
1528    }
1529
1530    /// Consume the parser and return all parsed messages.
1531    pub fn into_messages(self) -> Vec<Message> {
1532        self.messages
1533    }
1534
1535    /// All fully parsed messages so far.
1536    pub fn messages(&self) -> &[Message] {
1537        &self.messages
1538    }
1539
1540    /// All tokens that were fed into the parser.
1541    pub fn tokens(&self) -> &[Rank] {
1542        &self.tokens
1543    }
1544
1545    /// Expose the current state as a JSON string for Python interop.
1546    pub fn state_json(&self) -> anyhow::Result<String> {
1547        #[derive(serde::Serialize)]
1548        #[serde(tag = "state")]
1549        enum SerializableStreamState<'a> {
1550            ExpectStart,
1551            Header {
1552                header_tokens: &'a [Rank],
1553            },
1554            Content {
1555                header: &'a ParsedHeader,
1556                content_tokens: &'a [Rank],
1557            },
1558        }
1559        let serializable = match &self.state {
1560            StreamState::ExpectStart => SerializableStreamState::ExpectStart,
1561            StreamState::Header { header_tokens } => {
1562                SerializableStreamState::Header { header_tokens }
1563            }
1564            StreamState::Content {
1565                header,
1566                content_tokens,
1567            } => SerializableStreamState::Content {
1568                header,
1569                content_tokens,
1570            },
1571        };
1572        Ok(serde_json::to_string(&serializable)?)
1573    }
1574
1575    /// Return the current recipient if known.
1576    pub fn current_recipient(&self) -> Option<String> {
1577        match &self.state {
1578            StreamState::Content { header, .. } => header.recipient.clone(),
1579            _ => None,
1580        }
1581    }
1582
1583    /// Return the current channel if known.
1584    pub fn current_channel(&self) -> Option<String> {
1585        match &self.state {
1586            StreamState::Content { header, .. } => header.channel.clone(),
1587            _ => None,
1588        }
1589    }
1590}
1591
1592// Add config struct for rendering
1593#[derive(Clone, Debug)]
1594pub struct RenderConversationConfig {
1595    pub auto_drop_analysis: bool,
1596}
1597
1598impl Default for RenderConversationConfig {
1599    fn default() -> Self {
1600        Self {
1601            auto_drop_analysis: true,
1602        }
1603    }
1604}