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
12#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
14pub struct ParsedHeader {
15 author: Author,
16 recipient: Option<String>,
17 channel: Option<String>,
18 content_type: Option<String>,
19}
20
21#[derive(thiserror::Error, Debug)]
22pub(crate) enum RenderFormattingTokenError {
23 #[error("tried to render unmapped formatting token {0}")]
24 UnmappedToken(FormattingToken),
25
26 #[error(
27 "Expected encoding of formatting token {token} to be a single token, but got {encoding:?}"
28 )]
29 InvalidEncoding {
30 token: FormattingToken,
31 encoding: Vec<Rank>,
32 },
33}
34
35#[allow(dead_code)]
39#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
40pub(crate) enum FormattingToken {
41 Start,
42 Message,
43 EndMessage,
44 EndMessageDoneSampling,
45 EndMessageAssistantToTool,
46 Refusal,
47 ConstrainedFormat,
48 Channel,
49 BeginUntrusted,
50 EndUntrusted,
51 MetaSep,
52 MetaEnd,
53}
54
55impl FormattingToken {
56 fn as_str(&self) -> &str {
57 match self {
58 FormattingToken::Start => "<|start|>",
59 FormattingToken::Message => "<|message|>",
60 FormattingToken::EndMessage => "<|end|>",
61 FormattingToken::EndMessageDoneSampling => "<|return|>",
62 FormattingToken::EndMessageAssistantToTool => "<|call|>",
63 FormattingToken::Refusal => "<|refusal|>",
64 FormattingToken::ConstrainedFormat => "<|constrain|>",
65 FormattingToken::Channel => "<|channel|>",
66 FormattingToken::BeginUntrusted => "<|untrusted|>",
67 FormattingToken::EndUntrusted => "<|end_untrusted|>",
68 FormattingToken::MetaSep => "<|channel|>",
69 FormattingToken::MetaEnd => "<|meta_end|>",
70 }
71 }
72}
73
74impl std::fmt::Display for FormattingToken {
75 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76 write!(f, "{}", self.as_str())
77 }
78}
79
80#[allow(dead_code)]
81#[derive(Clone)]
82pub struct HarmonyEncoding {
83 pub(crate) name: String,
84 pub(crate) n_ctx: usize,
85 pub(crate) max_message_tokens: usize,
86 pub(crate) max_action_length: usize,
87 pub(crate) tokenizer_name: String,
88 pub(crate) tokenizer: Arc<CoreBPE>,
89 pub(crate) format_token_mapping: HashMap<FormattingToken, String>,
90 pub(crate) stop_formatting_tokens: HashSet<FormattingToken>,
91 pub(crate) stop_formatting_tokens_for_assistant_actions: HashSet<FormattingToken>,
92}
93
94impl std::fmt::Debug for HarmonyEncoding {
95 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96 f.debug_struct("HarmonyEncoding")
97 .field("name", &self.name)
98 .field("tokenizer_name", &self.tokenizer_name)
99 .field("n_ctx", &self.n_ctx)
100 .field("max_message_tokens", &self.max_message_tokens)
101 .field("max_action_length", &self.max_action_length)
102 .finish()
103 }
104}
105
106impl std::fmt::Display for HarmonyEncoding {
107 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
108 write!(f, "Renderer({})", self.name)
109 }
110}
111
112impl HarmonyEncoding {
114 pub fn name(&self) -> &str {
115 &self.name
116 }
117
118 pub fn tokenizer_name(&self) -> &str {
119 &self.tokenizer_name
120 }
121
122 pub fn max_message_tokens(&self) -> usize {
123 self.max_message_tokens
124 }
125
126 pub fn tokenizer(&self) -> &CoreBPE {
127 &self.tokenizer
128 }
129
130 pub fn stop_tokens(&self) -> anyhow::Result<HashSet<Rank>> {
131 self.stop_formatting_tokens
132 .iter()
133 .copied()
134 .map(|t| match self.render_formatting_token(t) {
135 Ok(t) => Ok(t),
136 Err(RenderFormattingTokenError::UnmappedToken(_)) => Err(anyhow::anyhow!(
137 "token {t} was specified as a stop token, but is not mapped"
138 )),
139 Err(e) => Err(anyhow::anyhow!(e).context("could not render stop token")),
140 })
141 .collect()
142 }
143
144 pub fn stop_tokens_for_assistant_actions(&self) -> anyhow::Result<HashSet<Rank>> {
145 self.stop_formatting_tokens_for_assistant_actions
146 .iter()
147 .copied()
148 .map(|t| match self.render_formatting_token(t) {
149 Ok(t) => Ok(t),
150 Err(RenderFormattingTokenError::UnmappedToken(_)) => Err(anyhow::anyhow!(
151 "token {t} was specified as a stop token, but is not mapped"
152 )),
153 Err(e) => Err(anyhow::anyhow!(e).context("could not render stop token")),
154 })
155 .collect()
156 }
157}
158
159impl HarmonyEncoding {
161 pub fn render_conversation_into<'a, I, B>(
163 &self,
164 conversation: I,
165 into: &mut B,
166 config: Option<&RenderConversationConfig>,
167 ) -> anyhow::Result<()>
168 where
169 I: IntoIterator<Item = &'a Message>,
170 B: Extend<Rank>,
171 {
172 let messages: Vec<_> = conversation.into_iter().collect();
173 let has_function_tools = messages.iter().any(|msg| {
174 msg.content.iter().any(|c| {
175 if let Content::DeveloperContent(dev) = c {
176 if let Some(tools) = &dev.tools {
177 if let Some(ns) = tools.get("functions") {
178 !ns.tools.is_empty()
179 } else {
180 false
181 }
182 } else {
183 false
184 }
185 } else {
186 false
187 }
188 })
189 });
190 let render_options = RenderOptions {
191 conversation_has_function_tools: has_function_tools,
192 };
193 let last_assistant_is_final = messages
194 .iter()
195 .rev()
196 .find_map(|msg| {
197 (msg.author.role == Role::Assistant)
198 .then(|| msg.channel.as_deref() == Some("final"))
199 })
200 .unwrap_or(false);
201
202 let should_drop_analysis =
203 config.is_some_and(|c| c.auto_drop_analysis && last_assistant_is_final);
204
205 let first_final_idx = messages
206 .iter()
207 .position(|msg| msg.channel.as_deref() == Some("final"));
208
209 let result = messages
210 .iter()
211 .enumerate()
212 .filter(|(idx, msg)| {
213 !(should_drop_analysis
214 && first_final_idx.is_some_and(|first| *idx < first)
215 && msg.channel.as_deref() == Some("analysis"))
216 })
217 .try_for_each(|(_, msg)| self.render_into(msg, into, Some(&render_options)));
218 result?;
219 Ok(())
220 }
221
222 pub fn render_conversation_for_completion_into<'a, I, B>(
226 &self,
227 conversation: I,
228 next_turn_role: Role,
229 into: &mut B,
230 config: Option<&RenderConversationConfig>,
231 ) -> anyhow::Result<()>
232 where
233 I: IntoIterator<Item = &'a Message>,
234 B: Extend<Rank>,
235 {
236 let _config = config.unwrap_or(&RenderConversationConfig::default());
237 self.render_conversation_into(conversation, into, config)?;
238 self.render_formatting_token_into(FormattingToken::Start, into)?;
239 self.render_text_into(next_turn_role.as_str(), into)?;
240 Ok(())
241 }
242
243 pub fn render_conversation_for_completion<'a, I>(
244 &self,
245 conversation: I,
246 next_turn_role: Role,
247 config: Option<&RenderConversationConfig>,
248 ) -> anyhow::Result<Vec<Rank>>
249 where
250 I: IntoIterator<Item = &'a Message>,
251 {
252 let mut into = vec![];
253 self.render_conversation_for_completion_into(
254 conversation,
255 next_turn_role,
256 &mut into,
257 config,
258 )?;
259 Ok(into)
260 }
261
262 pub fn render_conversation_for_training<'a, I>(
267 &self,
268 conversation: I,
269 config: Option<&RenderConversationConfig>,
270 ) -> anyhow::Result<Vec<Rank>>
271 where
272 I: IntoIterator<Item = &'a Message>,
273 {
274 let messages: Vec<&Message> = conversation.into_iter().collect();
275 let mut out = vec![];
276 self.render_conversation_into(messages.iter().copied(), &mut out, config)?;
277 if let Some(last) = messages.last() {
278 if last.author.role == Role::Assistant && last.channel.as_deref() == Some("final") {
279 if let Some(last_token) = out.last_mut() {
280 *last_token =
281 self.render_formatting_token(FormattingToken::EndMessageDoneSampling)?;
282 }
283 }
284 }
285 Ok(out)
286 }
287
288 pub fn render_conversation<'a, I>(
290 &self,
291 conversation: I,
292 config: Option<&RenderConversationConfig>,
293 ) -> anyhow::Result<Vec<Rank>>
294 where
295 I: IntoIterator<Item = &'a Message>,
296 {
297 let mut out = vec![];
298 self.render_conversation_into(conversation, &mut out, config)?;
299 Ok(out)
300 }
301
302 pub fn render(
304 &self,
305 message: &Message,
306 render_options: Option<&RenderOptions>,
307 ) -> anyhow::Result<Vec<Rank>> {
308 let mut out = vec![];
309 Render::<Message>::render(self, message, &mut out, render_options)?;
310 Ok(out)
311 }
312
313 pub fn render_into<B>(
315 &self,
316 message: &Message,
317 into: &mut B,
318 render_options: Option<&RenderOptions>,
319 ) -> anyhow::Result<()>
320 where
321 B: Extend<Rank>,
322 {
323 Render::<Message>::render(self, message, into, render_options)
324 }
325}
326
327impl HarmonyEncoding {
329 fn mapped_format_token(&self, t: FormattingToken) -> Option<&str> {
330 self.format_token_mapping.get(&t).map(|s| s.as_str())
331 }
332
333 fn render_formatting_token(
334 &self,
335 t: FormattingToken,
336 ) -> Result<Rank, RenderFormattingTokenError> {
337 let mapped = self
338 .mapped_format_token(t)
339 .ok_or(RenderFormattingTokenError::UnmappedToken(t))?;
340 let encoded = self.tokenizer.encode_with_special_tokens(mapped);
341 if encoded.len() != 1 {
342 return Err(RenderFormattingTokenError::InvalidEncoding {
343 token: t,
344 encoding: encoded,
345 });
346 }
347 Ok(encoded[0])
348 }
349
350 fn render_formatting_token_into<B>(
351 &self,
352 t: FormattingToken,
353 into: &mut B,
354 ) -> anyhow::Result<()>
355 where
356 B: Extend<Rank>,
357 {
358 let r = self.render_formatting_token(t)?;
359 into.extend(std::iter::once(r));
360 Ok(())
361 }
362
363 fn render_text_into<T, B>(&self, text: T, into: &mut B) -> anyhow::Result<()>
364 where
365 T: AsRef<str>,
366 B: Extend<Rank>,
367 {
368 into.extend(self.tokenizer.encode_ordinary(text.as_ref()));
369 Ok(())
370 }
371
372 pub fn parse_messages_from_completion_tokens<I>(
373 &self,
374 tokens: I,
375 role: Option<Role>,
376 ) -> anyhow::Result<Vec<Message>>
377 where
378 I: IntoIterator<Item = Rank>,
379 {
380 let mut parser = StreamableParser::new(self.clone(), role)?;
381 for token in tokens {
382 parser.process(token)?;
383 }
384 parser.process_eos()?;
385 Ok(parser.into_messages())
386 }
387
388 fn json_schema_to_typescript(schema: &serde_json::Value, indent: &str) -> String {
390 fn is_enum(schema: &serde_json::Value) -> bool {
392 schema
393 .get("enum")
394 .and_then(|e| e.as_array())
395 .is_some_and(|arr| !arr.is_empty())
396 }
397
398 if let Some(one_of) = schema.get("oneOf") {
400 if let Some(arr) = one_of.as_array() {
401 let mut out = String::new();
402 let mut first = true;
403 for variant in arr {
404 if !first {
405 out.push('\n');
406 out.push_str(&format!("{indent} | "));
407 } else {
408 out.push_str(&format!("\n{indent} | "));
409 first = false;
410 }
411 let type_str =
412 Self::json_schema_to_typescript(variant, &format!("{indent} "));
413 let mut type_str = type_str;
414 if variant
415 .get("nullable")
416 .and_then(|n| n.as_bool())
417 .unwrap_or(false)
418 && !type_str.contains("null")
419 {
420 type_str = format!("{type_str} | null");
421 }
422 out.push_str(&type_str);
423 let mut trailing_comments = Vec::new();
425 if let Some(desc) = variant.get("description") {
426 if let Some(desc_str) = desc.as_str() {
427 trailing_comments.push(desc_str.to_string());
428 }
429 }
430 if let Some(default) = variant.get("default") {
431 if default.is_string() && !is_enum(variant) {
432 trailing_comments
433 .push(format!("default: \"{}\"", default.as_str().unwrap()));
434 } else {
435 trailing_comments.push(format!("default: {default}"));
436 }
437 }
438 if !trailing_comments.is_empty() {
439 out.push_str(&format!(" // {}", trailing_comments.join(" ")));
440 }
441 }
442 return out;
443 }
444 }
445 if let Some(types) = schema.get("type").and_then(|v| v.as_array()) {
447 let mut type_strings = Vec::new();
448 for ty in types {
449 if let Some(ty_str) = ty.as_str() {
450 let mapped = match ty_str {
451 "integer" => "number",
452 other => other,
453 };
454 type_strings.push(mapped.to_string());
455 }
456 }
457 if !type_strings.is_empty() {
458 return type_strings.join(" | ");
459 }
460 }
461 if let Some(ty) = schema.get("type").and_then(|v| v.as_str()) {
463 match ty {
464 "object" => {
465 let mut out = String::new();
466 if let Some(desc) = schema.get("description") {
468 if let Some(desc_str) = desc.as_str() {
469 out.push_str(&format!("{indent}// {desc_str}\n"));
470 }
471 }
472 out.push_str("{\n");
473
474 if let Some(props) = schema.get("properties") {
475 if let Some(props_map) = props.as_object() {
476 let mut required = std::collections::HashSet::new();
478 if let Some(req) = schema.get("required") {
479 if let Some(req_arr) = req.as_array() {
480 for r in req_arr {
481 if let Some(s) = r.as_str() {
482 required.insert(s);
483 }
484 }
485 }
486 }
487 for (key, val) in props_map {
488 if let Some(title) = val.get("title") {
490 if let Some(title_str) = title.as_str() {
491 out.push_str(&format!(
492 "{indent}// {title_str}\n{indent}//\n"
493 ));
494 }
495 }
496 if val.get("oneOf").is_none() {
498 if let Some(desc) = val.get("description") {
499 if let Some(desc_str) = desc.as_str() {
500 out.push_str(&format!("{indent}// {desc_str}\n"));
501 }
502 }
503 }
504 if let Some(examples) = val.get("examples") {
505 if let Some(arr) = examples.as_array() {
506 if !arr.is_empty() {
507 out.push_str(&format!("{indent}// Examples:\n"));
508 for ex in arr {
509 if let Some(ex_str) = ex.as_str() {
510 out.push_str(&format!(
511 "{indent}// - \"{ex_str}\"\n"
512 ));
513 }
514 }
515 }
516 }
517 }
518 if let Some(one_of) = val.get("oneOf") {
520 if let Some(arr) = one_of.as_array() {
521 let mut property_desc: Option<&str> = None;
523 if let Some(desc) = val.get("description") {
524 if let Some(desc_str) = desc.as_str() {
525 property_desc = Some(desc_str);
526 }
527 }
528 let mut skip_property_desc = false;
529 if let Some(desc_str) = property_desc {
530 if let Some(first_variant) = arr.first() {
531 if let Some(variant_desc) =
532 first_variant.get("description")
533 {
534 if let Some(variant_desc_str) =
535 variant_desc.as_str()
536 {
537 if desc_str == variant_desc_str {
538 skip_property_desc = true;
539 }
540 }
541 }
542 }
543 }
544 let mut rendered_property_desc_above = false;
546 if !skip_property_desc {
547 if let Some(desc_str) = property_desc {
548 out.push_str(&format!("{indent}// {desc_str}\n"));
549 rendered_property_desc_above = true;
550 }
551 }
552 if let Some(default) = val.get("default") {
553 if default.is_string() && !is_enum(val) {
554 out.push_str(&format!(
555 "{}// default: \"{}\"\n",
556 indent,
557 default.as_str().unwrap()
558 ));
559 } else if default.is_string() {
560 out.push_str(&format!(
561 "{}// default: {}\n",
562 indent,
563 default.as_str().unwrap()
564 ));
565 } else {
566 out.push_str(&format!(
567 "{indent}// default: {default}\n"
568 ));
569 }
570 }
571 out.push_str(&format!(
573 "{}{}{}:\n",
574 indent,
575 key,
576 if required.contains(key.as_str()) {
577 ""
578 } else {
579 "?"
580 }
581 ));
582 for (i, variant) in arr.iter().enumerate() {
584 out.push_str(&format!("{indent} | "));
585 let type_str = Self::json_schema_to_typescript(
586 variant,
587 &format!("{indent} "),
588 );
589 let mut type_str = type_str;
591 if variant
592 .get("nullable")
593 .and_then(|n| n.as_bool())
594 .unwrap_or(false)
595 && !type_str.contains("null")
596 {
597 type_str = format!("{type_str} | null");
598 }
599 out.push_str(&type_str);
600 let mut trailing_comments = Vec::new();
602 if i == 0 && rendered_property_desc_above {
603 } else if let Some(desc) = variant.get("description") {
605 if let Some(desc_str) = desc.as_str() {
606 if Some(desc_str) != property_desc {
608 trailing_comments
609 .push(desc_str.to_string());
610 }
611 }
612 }
613 if let Some(default) = variant.get("default") {
614 if default.is_string() && !is_enum(variant) {
615 trailing_comments.push(format!(
616 "default: \"{}\"",
617 default.as_str().unwrap()
618 ));
619 } else if default.is_string() {
620 trailing_comments.push(format!(
621 "default: {}",
622 default.as_str().unwrap()
623 ));
624 } else {
625 trailing_comments
626 .push(format!("default: {default}"));
627 }
628 }
629 if !trailing_comments.is_empty() {
630 out.push_str(&format!(
631 " // {}",
632 trailing_comments.join(" ")
633 ));
634 }
635 out.push('\n');
636 }
637 out.push_str(&format!("{indent},\n"));
638 continue;
639 }
640 }
641 out.push_str(&format!(
643 "{}{}{}: ",
644 indent,
645 key,
646 if required.contains(key.as_str()) {
647 ""
648 } else {
649 "?"
650 }
651 ));
652 let mut type_str =
654 Self::json_schema_to_typescript(val, &format!("{indent} "));
655 if val
656 .get("nullable")
657 .and_then(|n| n.as_bool())
658 .unwrap_or(false)
659 && !type_str.contains("null")
660 {
661 type_str = format!("{type_str} | null");
662 }
663 out.push_str(&type_str);
664 out.push(',');
665 if val.get("oneOf").is_none() {
667 if let Some(default) = val.get("default") {
668 if default.is_string() && !is_enum(val) {
669 out.push_str(&format!(
670 " // default: \"{}\"",
671 default.as_str().unwrap()
672 ));
673 } else if default.is_string() {
674 out.push_str(&format!(
675 " // default: {}",
676 default.as_str().unwrap()
677 ));
678 } else {
679 out.push_str(&format!(" // default: {default}"));
680 }
681 }
682 }
683 out.push('\n');
684 }
685 }
686 }
687 out.push_str(&format!("{indent}}}"));
688 out
689 }
690 "string" => {
691 if let Some(enum_vals) = schema.get("enum") {
692 if let Some(arr) = enum_vals.as_array() {
693 let enums: Vec<String> = arr
694 .iter()
695 .filter_map(|v| v.as_str().map(|s| format!("\"{s}\"")))
696 .collect();
697 if !enums.is_empty() {
698 return enums.join(" | ");
699 }
700 }
701 }
702 "string".to_string()
703 }
704 "number" => "number".to_string(),
705 "integer" => "number".to_string(),
706 "boolean" => "boolean".to_string(),
707 "array" => {
708 if let Some(items) = schema.get("items") {
709 format!("{}[]", Self::json_schema_to_typescript(items, indent))
710 } else {
711 "Array<any>".to_string()
712 }
713 }
714 _ => "any".to_string(),
715 }
716 } else if let Some(one_of) = schema.get("oneOf") {
717 if let Some(arr) = one_of.as_array() {
719 let mut out = String::new();
720 let mut first = true;
721 for variant in arr {
722 if !first {
723 out.push_str("\n | ");
724 } else {
725 first = false;
726 }
727 out.push_str(&Self::json_schema_to_typescript(variant, indent));
728 }
729 return out;
730 }
731 "any".to_string()
732 } else {
733 "any".to_string()
734 }
735 }
736
737 fn template_tools_section(
739 tools: &std::collections::BTreeMap<String, crate::chat::ToolNamespaceConfig>,
740 ) -> String {
741 let mut tool_sections = Vec::<String>::new();
742 tool_sections.push("# Tools".to_string());
743 for ns_config in tools.values() {
744 let mut tool_section_content = Vec::<String>::new();
745 tool_section_content.push(format!("## {}\n", ns_config.name));
746 if let Some(desc) = &ns_config.description {
747 for line in desc.lines() {
748 if !ns_config.tools.is_empty() {
749 tool_section_content.push(format!("// {line}"));
750 } else {
751 tool_section_content.push(line.to_string());
752 }
753 }
754 }
755 if !ns_config.tools.is_empty() {
756 tool_section_content.push(format!("namespace {} {{\n", ns_config.name));
757 for tool in &ns_config.tools {
758 for line in tool.description.lines() {
759 tool_section_content.push(format!("// {line}"));
760 }
761 if let Some(params) = &tool.parameters {
762 let param_type = Self::json_schema_to_typescript(params, "");
763 tool_section_content.push(format!(
764 "type {} = (_: {}) => any;\n",
765 tool.name, param_type
766 ));
767 } else {
768 tool_section_content.push(format!("type {} = () => any;\n", tool.name));
769 }
770 }
771 tool_section_content.push(format!("}} // namespace {}", ns_config.name));
772 }
773 tool_sections.push(tool_section_content.join("\n"));
774 }
775 tool_sections.join("\n\n")
776 }
777}
778
779#[derive(Clone, Copy, Debug, Default)]
780pub struct RenderOptions {
781 pub conversation_has_function_tools: bool,
782}
783
784trait Render<T: ?Sized> {
785 fn render<B>(
786 &self,
787 item: &T,
788 into: &mut B,
789 render_options: Option<&RenderOptions>,
790 ) -> anyhow::Result<()>
791 where
792 B: Extend<Rank>;
793}
794
795impl Render<Message> for HarmonyEncoding {
796 fn render<B>(
797 &self,
798 message: &Message,
799 into: &mut B,
800 render_options: Option<&RenderOptions>,
801 ) -> anyhow::Result<()>
802 where
803 B: Extend<Rank>,
804 {
805 self.render_formatting_token_into(FormattingToken::Start, into)?;
806
807 if matches!(message.author.role, Role::Tool) {
809 if let Some(name) = &message.author.name {
811 self.render_text_into(name, into)?;
812 } else {
813 anyhow::bail!("Tools should have a name!");
814 }
815 } else {
816 self.render_text_into(message.author.role.as_str(), into)?;
818 if let Some(name) = &message.author.name {
819 self.render_text_into(format!(":{name}"), into)?;
820 }
821 };
822
823 if let Some(recipient) = &message.recipient {
825 if recipient != "all" {
826 self.render_text_into(format!(" to={recipient}"), into)?;
827 }
828 }
829
830 if let Some(channel) = &message.channel {
832 self.render_formatting_token_into(FormattingToken::Channel, into)?;
833 self.render_text_into(channel, into)?;
834 }
835
836 if let Some(content_type) = &message.content_type {
838 self.render_text_into(format!(" {content_type}"), into)?;
839 }
840
841 self.render_formatting_token_into(FormattingToken::Message, into)?;
842 for content in message.content.iter() {
843 if let crate::chat::Content::SystemContent(_) = content {
845 anyhow::ensure!(
846 message.author.role == crate::chat::Role::System,
847 "SystemContent may only appear in system messages, found in {:?}",
848 message.author.role
849 );
850 }
851 if let crate::chat::Content::DeveloperContent(_) = content {
852 anyhow::ensure!(
853 message.author.role == crate::chat::Role::Developer,
854 "DeveloperContent may only appear in developer messages, found in {:?}",
855 message.author.role
856 );
857 }
858 Render::<Content>::render(self, content, into, render_options)?;
859 }
860
861 if message.author.role == crate::chat::Role::Assistant && message.recipient.is_some() {
863 self.render_formatting_token_into(FormattingToken::EndMessageAssistantToTool, into)?;
864 } else {
865 self.render_formatting_token_into(FormattingToken::EndMessage, into)?;
866 }
867 Ok(())
868 }
869}
870
871impl Render<Content> for HarmonyEncoding {
873 fn render<B>(
874 &self,
875 content: &Content,
876 into: &mut B,
877 render_options: Option<&RenderOptions>,
878 ) -> anyhow::Result<()>
879 where
880 B: Extend<Rank>,
881 {
882 match content {
883 Content::Text(text) => Render::<TextContent>::render(self, text, into, render_options),
884 Content::SystemContent(sys) => {
885 Render::<SystemContent>::render(self, sys, into, render_options)
886 }
887 Content::DeveloperContent(dev) => {
888 Render::<crate::chat::DeveloperContent>::render(self, dev, into, render_options)
889 }
890 }
891 }
892}
893
894impl Render<TextContent> for HarmonyEncoding {
896 fn render<B>(
897 &self,
898 text: &TextContent,
899 into: &mut B,
900 _render_options: Option<&RenderOptions>,
901 ) -> anyhow::Result<()>
902 where
903 B: Extend<Rank>,
904 {
905 self.render_text_into(&text.text, into)
906 }
907}
908
909impl Render<SystemContent> for HarmonyEncoding {
911 fn render<B>(
912 &self,
913 sys: &SystemContent,
914 into: &mut B,
915 render_options: Option<&RenderOptions>,
916 ) -> anyhow::Result<()>
917 where
918 B: Extend<Rank>,
919 {
920 let mut sections = Vec::<String>::new();
921
922 let mut top_section = Vec::<String>::new();
923 if let Some(model_id) = &sys.model_identity {
924 top_section.push(model_id.clone());
925 }
926 if let Some(knowledge_cutoff) = &sys.knowledge_cutoff {
927 top_section.push(format!("Knowledge cutoff: {knowledge_cutoff}"));
928 }
929 if let Some(conversation_start_date) = &sys.conversation_start_date {
930 top_section.push(format!("Current date: {conversation_start_date}"));
931 }
932 if !top_section.is_empty() {
933 sections.push(top_section.join("\n"));
934 }
935
936 let mut instructions_and_reasoning = Vec::<String>::new();
937 if let Some(effort) = sys.reasoning_effort {
938 let effort_str = match effort {
939 ReasoningEffort::Low => "low",
940 ReasoningEffort::Medium => "medium",
941 ReasoningEffort::High => "high",
942 };
943 instructions_and_reasoning.push(format!("Reasoning: {effort_str}"));
944 }
945 if !instructions_and_reasoning.is_empty() {
946 sections.push(instructions_and_reasoning.join("\n"));
947 }
948
949 if let Some(tools) = &sys.tools {
950 if !tools.is_empty() {
951 sections.push(Self::template_tools_section(tools));
952 }
953 }
954
955 if let Some(channel_config) = &sys.channel_config {
956 if !channel_config.valid_channels.is_empty() {
957 let channels_str = channel_config.valid_channels.join(", ");
958 let mut channels_header = format!("# Valid channels: {channels_str}.");
959 if channel_config.channel_required {
960 channels_header.push_str(" Channel must be included for every message.");
961 }
962 if render_options.is_some_and(|o| o.conversation_has_function_tools) {
963 channels_header.push('\n');
964 channels_header.push_str(
965 "Calls to these tools must go to the commentary channel: 'functions'.",
966 );
967 }
968 sections.push(channels_header);
969 }
970 }
971 let formatted = sections.join("\n\n");
972 self.render_text_into(&formatted, into)?;
973 Ok(())
974 }
975}
976
977impl Render<crate::chat::DeveloperContent> for HarmonyEncoding {
979 fn render<B>(
980 &self,
981 dev: &crate::chat::DeveloperContent,
982 into: &mut B,
983 _render_options: Option<&RenderOptions>,
984 ) -> anyhow::Result<()>
985 where
986 B: Extend<Rank>,
987 {
988 let mut sections = Vec::<String>::new();
989
990 if let Some(instr) = &dev.instructions {
991 sections.push("# Instructions".to_string());
992 sections.push(instr.clone());
993 }
994
995 if let Some(tools) = &dev.tools {
996 if !tools.is_empty() {
997 sections.push(Self::template_tools_section(tools));
998 }
999 }
1000 let formatted = sections.join("\n\n");
1001 self.render_text_into(&formatted, into)?;
1002 Ok(())
1003 }
1004}
1005
1006pub struct StreamableParser {
1011 encoding: HarmonyEncoding,
1012 next_role: Option<Role>,
1013 tokens: Vec<Rank>,
1014 messages: Vec<Message>,
1015 state: StreamState,
1016 stop_tokens: HashSet<Rank>,
1017 last_content_delta: Option<String>,
1018 undecoded_tokens: Vec<Rank>,
1019}
1020
1021#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
1022pub enum StreamState {
1023 ExpectStart,
1024 Header {
1025 header_tokens: Vec<Rank>,
1026 },
1027 Content {
1028 header: ParsedHeader,
1029 content_tokens: Vec<Rank>,
1030 },
1031}
1032
1033impl StreamableParser {
1034 pub fn new(encoding: HarmonyEncoding, role: Option<Role>) -> anyhow::Result<Self> {
1036 let stop_tokens = encoding.stop_tokens()?;
1037 let (state, next_role) = match role {
1038 Some(role) => (
1039 StreamState::Header {
1040 header_tokens: Vec::new(),
1041 },
1042 Some(role),
1043 ),
1044 None => (StreamState::ExpectStart, None),
1045 };
1046 Ok(Self {
1047 encoding,
1048 next_role,
1049 tokens: Vec::new(),
1050 messages: Vec::new(),
1051 state,
1052 stop_tokens,
1053 last_content_delta: None,
1054 undecoded_tokens: Vec::new(),
1055 })
1056 }
1057
1058 fn process_next(&mut self, token: Option<Rank>) -> anyhow::Result<&mut Self> {
1061 if let Some(token) = token {
1062 self.tokens.push(token);
1063 }
1064 let next_role_clone = self.next_role.clone();
1066 match &mut self.state {
1067 StreamState::ExpectStart => {
1068 let start = self
1069 .encoding
1070 .render_formatting_token(FormattingToken::Start)?;
1071 match token {
1072 Some(token) if token == start => {
1073 self.state = StreamState::Header {
1074 header_tokens: Vec::new(),
1075 };
1076 }
1077 Some(token) => {
1078 anyhow::bail!(
1079 "Unexpected token {} while expecting start token {}",
1080 token,
1081 start
1082 );
1083 }
1084 None => {
1085 }
1089 }
1090 }
1091 StreamState::Header { header_tokens } => {
1092 let msg_tok = self
1093 .encoding
1094 .render_formatting_token(FormattingToken::Message)?;
1095 match token {
1096 Some(token) if token == msg_tok => {
1097 let header_tokens_cloned = header_tokens.clone();
1099 let next_role_cloned = next_role_clone;
1100 self.state = StreamState::ExpectStart;
1102 let header =
1103 self.parse_header_from_tokens(&header_tokens_cloned, next_role_cloned)?;
1104 self.next_role = None;
1105 self.state = StreamState::Content {
1106 header,
1107 content_tokens: Vec::new(),
1108 };
1109 }
1110 Some(token) => {
1111 header_tokens.push(token);
1112 }
1113 None => {
1114 anyhow::bail!(
1115 "Unexpected EOS while waiting for message header to complete"
1116 );
1117 }
1118 }
1119 }
1120 StreamState::Content {
1121 header,
1122 content_tokens,
1123 } => {
1124 let is_eos = if let Some(token) = token {
1125 if self.stop_tokens.contains(&token) {
1126 true
1128 } else {
1129 self.undecoded_tokens.push(token);
1130 match self
1133 .encoding
1134 .tokenizer()
1135 .decode_utf8(&self.undecoded_tokens)
1136 {
1137 Ok(decoded) => {
1138 content_tokens.extend(self.undecoded_tokens.iter().copied());
1139 self.last_content_delta = Some(decoded);
1140 self.undecoded_tokens.clear();
1141 }
1142 Err(_) => {
1143 self.last_content_delta = None;
1144 }
1145 }
1146 false
1148 }
1149 } else {
1150 true
1152 };
1153 if is_eos {
1154 let text = self.encoding.tokenizer().decode_utf8(content_tokens)?;
1155 let message = Message {
1156 author: header.author.clone(),
1157 recipient: header.recipient.clone(),
1158 channel: header.channel.clone(),
1159 content_type: header.content_type.clone(),
1160 content: vec![Content::Text(TextContent { text })],
1161 };
1162 self.messages.push(message);
1163 self.state = StreamState::ExpectStart;
1164 self.last_content_delta = None;
1165 self.undecoded_tokens.clear();
1166 }
1167 }
1168 }
1169 Ok(self)
1170 }
1171
1172 pub fn process(&mut self, token: Rank) -> anyhow::Result<&mut Self> {
1173 self.process_next(Some(token))
1174 }
1175
1176 pub fn process_eos(&mut self) -> anyhow::Result<&mut Self> {
1177 self.process_next(None)?;
1178 Ok(self)
1179 }
1180
1181 fn parse_header_from_tokens(
1182 &self,
1183 header_tokens: &[Rank],
1184 role: Option<Role>,
1185 ) -> anyhow::Result<ParsedHeader> {
1186 let mut header_string = self
1187 .encoding
1188 .tokenizer()
1189 .decode_utf8(header_tokens)
1190 .context("could not decode header")?;
1191
1192 let mut channel: Option<String> = None;
1193 if let Some(channel_marker) = self.encoding.mapped_format_token(FormattingToken::Channel) {
1194 if let Some(idx) = header_string.find(channel_marker) {
1195 let after_marker = &header_string[idx + channel_marker.len()..];
1196 let channel_end = after_marker
1197 .find(|c: char| c.is_whitespace() || c == '<')
1198 .unwrap_or(after_marker.len());
1199 let channel_value = &after_marker[..channel_end];
1200 if channel_value.is_empty() {
1201 anyhow::bail!("channel marker present but no channel value found in header");
1202 }
1203 channel = Some(channel_value.to_string());
1204
1205 let mut new_header = String::new();
1206 new_header.push_str(&header_string[..idx]);
1207 new_header.push_str(&after_marker[channel_end..]);
1208 header_string = new_header;
1209 }
1210 }
1211
1212 header_string = header_string.trim().to_string();
1215
1216 if let Some(constrain_marker) = self
1221 .encoding
1222 .mapped_format_token(FormattingToken::ConstrainedFormat)
1223 {
1224 if header_string.contains(constrain_marker) {
1225 header_string = header_string
1226 .replace(constrain_marker, &format!(" {constrain_marker}"))
1227 .trim()
1228 .to_string();
1229 }
1230 }
1231
1232 let mut parts: Vec<&str> = header_string.split_ascii_whitespace().collect();
1233
1234 let mut role_str_opt: Option<String> = None;
1235 let role = match role {
1236 Some(r) => r,
1237 None => {
1238 let role_str = parts
1239 .first()
1240 .context("message header did not contain a role")?;
1241 role_str_opt = Some((*role_str).to_string());
1242 let parsed_role = Role::try_from(*role_str);
1243 let out = match parsed_role {
1244 Ok(r) => r,
1245 Err(_) => {
1246 if parts.len() > 1 || (parts.len() == 1 && parts[0].starts_with("to=")) {
1248 parts.remove(0); Role::Tool
1250 } else {
1251 return Err(anyhow::anyhow!("Unknown role: {}", role_str));
1252 }
1253 }
1254 };
1255 out
1256 }
1257 };
1258
1259 if let Some(&first) = parts.first() {
1260 if first == role.as_str() {
1261 parts.remove(0);
1262 }
1263 }
1264
1265 let mut recipient: Option<String> = None;
1266 let mut content_type: Option<String> = None;
1267
1268 if !parts.is_empty() {
1269 let num_parts = parts.len();
1272 let last_part = parts.pop().unwrap();
1274
1275 if let Some(stripped) = last_part.strip_prefix("to=") {
1276 recipient = Some(stripped.to_string());
1278 } else if num_parts == 1 {
1279 recipient = Some(last_part.to_string());
1282 } else {
1283 content_type = Some(last_part.to_string());
1285
1286 if let Some(raw_recipient) = parts.pop() {
1288 recipient = if let Some(stripped) = raw_recipient.strip_prefix("to=") {
1289 Some(stripped.to_string())
1290 } else {
1291 Some(raw_recipient.to_string())
1292 };
1293 }
1294 }
1295 }
1296 anyhow::ensure!(
1297 parts.is_empty(),
1298 "unexpected tokens remaining in message header: {:?}",
1299 parts
1300 );
1301
1302 let author = if role == Role::Tool {
1303 let name = role_str_opt;
1304 Author { role, name }
1305 } else {
1306 Author { role, name: None }
1307 };
1308 Ok(ParsedHeader {
1309 author,
1310 recipient,
1311 channel,
1312 content_type,
1313 })
1314 }
1315
1316 pub fn current_content(&self) -> anyhow::Result<String> {
1318 match &self.state {
1319 StreamState::Content { content_tokens, .. } => self
1320 .encoding
1321 .tokenizer()
1322 .decode_utf8(content_tokens)
1323 .map_err(|e| anyhow::anyhow!(e)),
1324 _ => Ok(String::new()),
1325 }
1326 }
1327
1328 pub fn current_role(&self) -> Option<Role> {
1330 match &self.state {
1331 StreamState::Content { header, .. } => Some(header.author.role.clone()),
1332 _ => self.next_role.clone(),
1333 }
1334 }
1335
1336 pub fn current_content_type(&self) -> Option<String> {
1338 match &self.state {
1339 StreamState::Content { header, .. } => header.content_type.clone(),
1340 _ => None,
1341 }
1342 }
1343
1344 pub fn last_content_delta(&self) -> anyhow::Result<Option<String>> {
1346 Ok(self.last_content_delta.clone())
1347 }
1348
1349 pub fn into_messages(self) -> Vec<Message> {
1351 self.messages
1352 }
1353
1354 pub fn messages(&self) -> &[Message] {
1356 &self.messages
1357 }
1358
1359 pub fn tokens(&self) -> &[Rank] {
1361 &self.tokens
1362 }
1363
1364 pub fn state_json(&self) -> anyhow::Result<String> {
1366 #[derive(serde::Serialize)]
1367 #[serde(tag = "state")]
1368 enum SerializableStreamState<'a> {
1369 ExpectStart,
1370 Header {
1371 header_tokens: &'a [Rank],
1372 },
1373 Content {
1374 header: &'a ParsedHeader,
1375 content_tokens: &'a [Rank],
1376 },
1377 }
1378 let serializable = match &self.state {
1379 StreamState::ExpectStart => SerializableStreamState::ExpectStart,
1380 StreamState::Header { header_tokens } => {
1381 SerializableStreamState::Header { header_tokens }
1382 }
1383 StreamState::Content {
1384 header,
1385 content_tokens,
1386 } => SerializableStreamState::Content {
1387 header,
1388 content_tokens,
1389 },
1390 };
1391 Ok(serde_json::to_string(&serializable)?)
1392 }
1393
1394 pub fn current_recipient(&self) -> Option<String> {
1396 match &self.state {
1397 StreamState::Content { header, .. } => header.recipient.clone(),
1398 _ => None,
1399 }
1400 }
1401
1402 pub fn current_channel(&self) -> Option<String> {
1404 match &self.state {
1405 StreamState::Content { header, .. } => header.channel.clone(),
1406 _ => None,
1407 }
1408 }
1409}
1410
1411#[derive(Clone, Debug)]
1413pub struct RenderConversationConfig {
1414 pub auto_drop_analysis: bool,
1415}
1416
1417impl Default for RenderConversationConfig {
1418 fn default() -> Self {
1419 Self {
1420 auto_drop_analysis: true,
1421 }
1422 }
1423}