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#[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#[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
114impl 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
161impl HarmonyEncoding {
163 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 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 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 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 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 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
329impl 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 fn json_schema_to_typescript(schema: &serde_json::Value, indent: &str) -> String {
408 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 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 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 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 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 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 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 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 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 if let Some(one_of) = val.get("oneOf") {
538 if let Some(arr) = one_of.as_array() {
539 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 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 out.push_str(&format!(
591 "{}{}{}:\n",
592 indent,
593 key,
594 if required.contains(key.as_str()) {
595 ""
596 } else {
597 "?"
598 }
599 ));
600 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 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 let mut trailing_comments = Vec::new();
620 if i == 0 && rendered_property_desc_above {
621 } else if let Some(desc) = variant.get("description") {
623 if let Some(desc_str) = desc.as_str() {
624 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 out.push_str(&format!(
661 "{}{}{}: ",
662 indent,
663 key,
664 if required.contains(key.as_str()) {
665 ""
666 } else {
667 "?"
668 }
669 ));
670 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 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 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 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 if matches!(message.author.role, Role::Tool) {
827 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 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 if let Some(recipient) = &message.recipient {
843 if recipient != "all" {
844 self.render_text_into(format!(" to={recipient}"), into)?;
845 }
846 }
847
848 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 if let Some(content_type) = &message.content_type {
856 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 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 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 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
905impl 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
928impl 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
943impl 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
1011impl 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
1051pub 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 pub fn new(encoding: HarmonyEncoding, role: Option<Role>) -> anyhow::Result<Self> {
1083 Self::new_with_options(encoding, role, ParseOptions::default())
1084 }
1085
1086 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 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 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 }
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 let header_tokens_cloned = header_tokens.clone();
1157 let next_role_cloned = next_role_clone;
1158 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 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 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 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 true
1214 } else {
1215 self.undecoded_tokens.push(token);
1216 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 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 self.last_content_delta = None;
1275 }
1276 }
1277 false
1279 }
1280 } else {
1281 true
1283 };
1284 if is_eos {
1285 let content_text = self.encoding.tokenizer().decode_utf8(content_tokens)?;
1287 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 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 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 header_string = header_string.trim().to_string();
1361
1362 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 parts.len() > 1 || (parts.len() == 1 && parts[0].starts_with("to=")) {
1394 parts.remove(0); 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 let last_part = parts.pop().unwrap();
1419
1420 if let Some(stripped) = last_part.strip_prefix("to=") {
1421 recipient = Some(stripped.to_string());
1423 } else if num_parts == 1 {
1424 recipient = Some(last_part.to_string());
1427 } else {
1428 content_type = Some(last_part.to_string());
1430
1431 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 remaining_content = if !parts.is_empty() {
1443 Some(parts.join(" "))
1444 } else {
1445 None
1446 };
1447 } else {
1448 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 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 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 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 pub fn last_content_delta(&self) -> anyhow::Result<Option<String>> {
1527 Ok(self.last_content_delta.clone())
1528 }
1529
1530 pub fn into_messages(self) -> Vec<Message> {
1532 self.messages
1533 }
1534
1535 pub fn messages(&self) -> &[Message] {
1537 &self.messages
1538 }
1539
1540 pub fn tokens(&self) -> &[Rank] {
1542 &self.tokens
1543 }
1544
1545 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 pub fn current_recipient(&self) -> Option<String> {
1577 match &self.state {
1578 StreamState::Content { header, .. } => header.recipient.clone(),
1579 _ => None,
1580 }
1581 }
1582
1583 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#[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}