1use serde::{Deserialize, Serialize};
7
8use crate::{FerrumError, FinishReason, Result};
9
10const START: &str = "<|start|>";
11const END: &str = "<|end|>";
12const MESSAGE: &str = "<|message|>";
13const CHANNEL: &str = "<|channel|>";
14const CONSTRAIN: &str = "<|constrain|>";
15const CALL: &str = "<|call|>";
16const RETURN: &str = "<|return|>";
17const FUNCTION_RECIPIENT_PREFIX: &str = "functions.";
18
19#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
21pub struct HarmonyToolCall {
22 pub name: String,
23 pub arguments_json: String,
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
29pub struct ParsedHarmonyResponse {
30 pub reasoning_content: Option<String>,
31 pub content: String,
32 pub tool_call: Option<HarmonyToolCall>,
33}
34
35pub fn parse_harmony_response(output: &str) -> Result<ParsedHarmonyResponse> {
47 parse_harmony_response_internal(output, false)
48}
49
50pub fn parse_length_truncated_harmony_response(output: &str) -> Result<ParsedHarmonyResponse> {
56 parse_harmony_response_internal(output, true)
57}
58
59pub fn parse_harmony_response_for_finish_reason(
67 output: &str,
68 finish_reason: Option<FinishReason>,
69) -> Result<ParsedHarmonyResponse> {
70 match finish_reason {
71 Some(FinishReason::Stop | FinishReason::Length) => {
72 parse_length_truncated_harmony_response(output)
73 }
74 _ => parse_harmony_response(output),
75 }
76}
77
78fn parse_harmony_response_internal(
79 output: &str,
80 allow_missing_text_terminal: bool,
81) -> Result<ParsedHarmonyResponse> {
82 let first = parse_message(output, true, allow_missing_text_terminal)?;
83 match first.channel {
84 HarmonyChannel::Final => {
85 validate_plain_message(&first, "final")?;
86 require_text_terminal(&first, HarmonyTerminal::Return, allow_missing_text_terminal)?;
87 require_no_trailing_output(&first)?;
88 Ok(ParsedHarmonyResponse {
89 reasoning_content: None,
90 content: first.payload.to_string(),
91 tool_call: None,
92 })
93 }
94 HarmonyChannel::Analysis => {
95 validate_plain_message(&first, "analysis")?;
96 if first.terminal.is_none() && allow_missing_text_terminal {
97 return Ok(ParsedHarmonyResponse {
98 reasoning_content: Some(first.payload.to_string()),
99 content: String::new(),
100 tool_call: None,
101 });
102 }
103 require_terminal(&first, HarmonyTerminal::End)?;
104 if first.remaining.is_empty() {
105 if allow_missing_text_terminal {
106 return Ok(ParsedHarmonyResponse {
107 reasoning_content: Some(first.payload.to_string()),
108 content: String::new(),
109 tool_call: None,
110 });
111 }
112 return Err(invalid_harmony(
113 "analysis message was not followed by a terminal final answer or tool call",
114 ));
115 }
116
117 if allow_missing_text_terminal && is_length_truncated_followup_envelope(first.remaining)
118 {
119 return Ok(ParsedHarmonyResponse {
120 reasoning_content: Some(first.payload.to_string()),
121 content: String::new(),
122 tool_call: None,
123 });
124 }
125
126 let second = parse_message(first.remaining, false, allow_missing_text_terminal)?;
127 match second.channel {
128 HarmonyChannel::Final => {
129 validate_plain_message(&second, "final")?;
130 require_text_terminal(
131 &second,
132 HarmonyTerminal::Return,
133 allow_missing_text_terminal,
134 )?;
135 require_no_trailing_output(&second)?;
136 Ok(ParsedHarmonyResponse {
137 reasoning_content: Some(first.payload.to_string()),
138 content: second.payload.to_string(),
139 tool_call: None,
140 })
141 }
142 HarmonyChannel::Commentary => {
143 parse_terminal_tool_call(&second, Some(first.payload))
144 }
145 HarmonyChannel::Analysis => Err(invalid_harmony(
146 "only one analysis message is supported before the terminal response",
147 )),
148 }
149 }
150 HarmonyChannel::Commentary => parse_terminal_tool_call(&first, None),
151 }
152}
153
154fn parse_terminal_tool_call(
155 message: &ParsedMessage<'_>,
156 reasoning_content: Option<&str>,
157) -> Result<ParsedHarmonyResponse> {
158 let tool_call = parse_tool_call(message)?;
159 require_terminal(message, HarmonyTerminal::Call)?;
160 require_no_trailing_output(message)?;
161 Ok(ParsedHarmonyResponse {
162 reasoning_content: reasoning_content.map(str::to_string),
163 content: String::new(),
164 tool_call: Some(tool_call),
165 })
166}
167
168#[derive(Debug, Clone, Copy, PartialEq, Eq)]
169enum HarmonyChannel {
170 Analysis,
171 Commentary,
172 Final,
173}
174
175impl HarmonyChannel {
176 fn parse(value: &str) -> Result<Self> {
177 match value {
178 "analysis" => Ok(Self::Analysis),
179 "commentary" => Ok(Self::Commentary),
180 "final" => Ok(Self::Final),
181 _ => Err(invalid_harmony(format!(
182 "unsupported Harmony channel {value:?}"
183 ))),
184 }
185 }
186}
187
188#[derive(Debug, Clone, Copy, PartialEq, Eq)]
189enum HarmonyTerminal {
190 End,
191 Call,
192 Return,
193}
194
195impl HarmonyTerminal {
196 const fn text(self) -> &'static str {
197 match self {
198 Self::End => END,
199 Self::Call => CALL,
200 Self::Return => RETURN,
201 }
202 }
203}
204
205#[derive(Debug)]
206struct ParsedMessage<'a> {
207 channel: HarmonyChannel,
208 recipient: Option<&'a str>,
209 content_type: Option<&'a str>,
210 payload: &'a str,
211 terminal: Option<HarmonyTerminal>,
212 remaining: &'a str,
213}
214
215fn parse_message(
216 output: &str,
217 first: bool,
218 allow_missing_terminal: bool,
219) -> Result<ParsedMessage<'_>> {
220 if output.is_empty() {
221 return Err(invalid_harmony("Harmony output is empty"));
222 }
223
224 let (role_recipient, after_channel_marker) =
225 if let Some(after_start) = output.strip_prefix(START) {
226 let channel_offset = after_start.find(CHANNEL).ok_or_else(|| {
227 invalid_harmony("message start was not followed by a channel marker")
228 })?;
229 let role_header = &after_start[..channel_offset];
230 reject_raw_marker(role_header, "assistant role header")?;
231 let recipient = parse_role_header(role_header)?;
232 (recipient, &after_start[channel_offset + CHANNEL.len()..])
233 } else if first {
234 let after_channel = output
235 .strip_prefix(CHANNEL)
236 .ok_or_else(|| invalid_harmony("first message must begin with a channel marker"))?;
237 (None, after_channel)
238 } else {
239 return Err(invalid_harmony(
240 "a message after analysis must begin with <|start|>assistant",
241 ));
242 };
243
244 let message_offset = after_channel_marker.find(MESSAGE);
245 let channel_header = message_offset
246 .map(|offset| &after_channel_marker[..offset])
247 .unwrap_or(after_channel_marker);
248 let (channel, channel_recipient, content_type) = parse_channel_header(channel_header)?;
249 let recipient = match (role_recipient, channel_recipient) {
250 (Some(_), Some(_)) => {
251 return Err(invalid_harmony(
252 "recipient was repeated in both role and channel headers",
253 ));
254 }
255 (Some(recipient), None) | (None, Some(recipient)) => Some(recipient),
256 (None, None) => None,
257 };
258
259 let Some(message_offset) = message_offset else {
260 if allow_missing_terminal
261 && matches!(channel, HarmonyChannel::Analysis | HarmonyChannel::Final)
262 && recipient.is_none()
263 && content_type.is_none()
264 {
265 return Ok(ParsedMessage {
266 channel,
267 recipient,
268 content_type,
269 payload: "",
270 terminal: None,
271 remaining: "",
272 });
273 }
274 return Err(invalid_harmony(
275 "channel header was not followed by a message marker",
276 ));
277 };
278
279 let after_message = &after_channel_marker[message_offset + MESSAGE.len()..];
280 let Some(terminal_offset) = after_message.find("<|") else {
281 reject_raw_marker(after_message, "message payload")?;
282 if !allow_missing_terminal {
283 return Err(invalid_harmony(
284 "message is missing a terminal control token",
285 ));
286 }
287 return Ok(ParsedMessage {
288 channel,
289 recipient,
290 content_type,
291 payload: after_message,
292 terminal: None,
293 remaining: "",
294 });
295 };
296 let payload = &after_message[..terminal_offset];
297 reject_raw_marker(payload, "message payload")?;
298 let terminal_and_remaining = &after_message[terminal_offset..];
299 let (terminal, remaining) = if let Some(remaining) = terminal_and_remaining.strip_prefix(END) {
300 (Some(HarmonyTerminal::End), remaining)
301 } else if let Some(remaining) = terminal_and_remaining.strip_prefix(CALL) {
302 (Some(HarmonyTerminal::Call), remaining)
303 } else if let Some(remaining) = terminal_and_remaining.strip_prefix(RETURN) {
304 (Some(HarmonyTerminal::Return), remaining)
305 } else {
306 return Err(invalid_harmony(
307 "message payload contains an unknown or misplaced raw control marker",
308 ));
309 };
310
311 Ok(ParsedMessage {
312 channel,
313 recipient,
314 content_type,
315 payload,
316 terminal,
317 remaining,
318 })
319}
320
321fn is_length_truncated_followup_envelope(output: &str) -> bool {
322 output == START
323 || output == concat!("<|start|>", "assistant")
324 || output == concat!("<|start|>", "assistant", "<|channel|>")
325}
326
327fn parse_role_header(header: &str) -> Result<Option<&str>> {
328 let mut parts = header.split_ascii_whitespace();
329 if parts.next() != Some("assistant") {
330 return Err(invalid_harmony(
331 "generated Harmony messages must have the assistant role",
332 ));
333 }
334 let recipient = parts.next().map(parse_recipient_token).transpose()?;
335 if parts.next().is_some() {
336 return Err(invalid_harmony(
337 "assistant role header contains unsupported metadata",
338 ));
339 }
340 Ok(recipient)
341}
342
343fn parse_channel_header(header: &str) -> Result<(HarmonyChannel, Option<&str>, Option<&str>)> {
344 let mut constrain_parts = header.split(CONSTRAIN);
345 let channel_part = constrain_parts.next().unwrap_or_default();
346 let content_type = constrain_parts.next().map(str::trim);
347 if constrain_parts.next().is_some() {
348 return Err(invalid_harmony(
349 "channel header contains repeated constrain markers",
350 ));
351 }
352 reject_raw_marker(channel_part, "channel header")?;
353 if let Some(content_type) = content_type {
354 reject_raw_marker(content_type, "content type")?;
355 if content_type.is_empty() {
356 return Err(invalid_harmony("constrain marker has no content type"));
357 }
358 }
359
360 let mut parts = channel_part.split_ascii_whitespace();
361 let channel = parts
362 .next()
363 .ok_or_else(|| invalid_harmony("channel marker has no channel value"))?;
364 let channel = HarmonyChannel::parse(channel)?;
365 let recipient = parts.next().map(parse_recipient_token).transpose()?;
366 if parts.next().is_some() {
367 return Err(invalid_harmony(
368 "channel header contains unsupported metadata",
369 ));
370 }
371 Ok((channel, recipient, content_type))
372}
373
374fn parse_recipient_token(token: &str) -> Result<&str> {
375 let recipient = token
376 .strip_prefix("to=")
377 .ok_or_else(|| invalid_harmony("recipient metadata must use the to= form"))?;
378 if recipient.is_empty() {
379 return Err(invalid_harmony("recipient must not be empty"));
380 }
381 Ok(recipient)
382}
383
384fn parse_tool_call(message: &ParsedMessage<'_>) -> Result<HarmonyToolCall> {
385 let recipient = message
386 .recipient
387 .ok_or_else(|| invalid_harmony("commentary tool call has no recipient"))?;
388 let name = recipient
389 .strip_prefix(FUNCTION_RECIPIENT_PREFIX)
390 .ok_or_else(|| invalid_harmony(format!("unknown tool recipient {recipient:?}")))?;
391 if name.is_empty() {
392 return Err(invalid_harmony("tool name must not be empty"));
393 }
394 if let Some(content_type) = message.content_type {
395 if content_type != "json" {
396 return Err(invalid_harmony(format!(
397 "tool arguments must use the json content type, got {content_type:?}"
398 )));
399 }
400 }
401
402 let arguments_json = message.payload.trim();
403 let arguments: serde_json::Value = serde_json::from_str(arguments_json)
404 .map_err(|error| invalid_harmony(format!("tool arguments are not valid JSON: {error}")))?;
405 if !arguments.is_object() {
406 return Err(invalid_harmony("tool arguments must be a JSON object"));
407 }
408
409 Ok(HarmonyToolCall {
410 name: name.to_string(),
411 arguments_json: arguments_json.to_string(),
412 })
413}
414
415fn validate_plain_message(message: &ParsedMessage<'_>, channel: &str) -> Result<()> {
416 if message.recipient.is_some() {
417 return Err(invalid_harmony(format!(
418 "{channel} channel must not contain a recipient"
419 )));
420 }
421 if message.content_type.is_some() {
422 return Err(invalid_harmony(format!(
423 "{channel} channel must not contain a constrain marker"
424 )));
425 }
426 Ok(())
427}
428
429fn require_terminal(message: &ParsedMessage<'_>, expected: HarmonyTerminal) -> Result<()> {
430 if message.terminal != Some(expected) {
431 return Err(invalid_harmony(format!(
432 "{} channel must end with {}, got {}",
433 match message.channel {
434 HarmonyChannel::Analysis => "analysis",
435 HarmonyChannel::Commentary => "commentary",
436 HarmonyChannel::Final => "final",
437 },
438 expected.text(),
439 message
440 .terminal
441 .map(HarmonyTerminal::text)
442 .unwrap_or("no terminal token"),
443 )));
444 }
445 Ok(())
446}
447
448fn require_text_terminal(
449 message: &ParsedMessage<'_>,
450 expected: HarmonyTerminal,
451 allow_missing: bool,
452) -> Result<()> {
453 if allow_missing && message.terminal.is_none() {
454 return Ok(());
455 }
456 require_terminal(message, expected)
457}
458
459fn require_no_trailing_output(message: &ParsedMessage<'_>) -> Result<()> {
460 if !message.remaining.is_empty() {
461 return Err(invalid_harmony(
462 "terminal control token was followed by duplicate terminal data or trailing garbage",
463 ));
464 }
465 Ok(())
466}
467
468fn reject_raw_marker(value: &str, location: &str) -> Result<()> {
469 if value.contains("<|") || value.contains("|>") {
470 return Err(invalid_harmony(format!(
471 "{location} contains a raw or incomplete control marker"
472 )));
473 }
474 Ok(())
475}
476
477fn invalid_harmony(message: impl Into<String>) -> FerrumError {
478 FerrumError::invalid_format(format!(
479 "invalid GPT-OSS Harmony output: {}",
480 message.into()
481 ))
482}
483
484#[cfg(test)]
485mod tests {
486 use super::*;
487
488 #[test]
489 fn parses_direct_final_response() {
490 let parsed =
491 parse_harmony_response("<|channel|>final<|message|>The capital is Paris.<|return|>")
492 .unwrap();
493
494 assert_eq!(
495 parsed,
496 ParsedHarmonyResponse {
497 reasoning_content: None,
498 content: "The capital is Paris.".to_string(),
499 tool_call: None,
500 }
501 );
502 }
503
504 #[test]
505 fn parses_analysis_then_final_response() {
506 let parsed = parse_harmony_response(
507 "<|channel|>analysis<|message|>Need the capital.<|end|>\
508 <|start|>assistant<|channel|>final<|message|>Paris.<|return|>",
509 )
510 .unwrap();
511
512 assert_eq!(
513 parsed.reasoning_content.as_deref(),
514 Some("Need the capital.")
515 );
516 assert_eq!(parsed.content, "Paris.");
517 assert_eq!(parsed.tool_call, None);
518 }
519
520 #[test]
521 fn parses_user_or_length_truncated_text_without_weakening_strict_parser() {
522 for (output, reasoning, content) in [
523 ("<|channel|>final", None, ""),
524 ("<|channel|>analysis", Some(""), ""),
525 (
526 "<|channel|>final<|message|>Partial answer",
527 None,
528 "Partial answer",
529 ),
530 (
531 "<|channel|>analysis<|message|>Partial reasoning",
532 Some("Partial reasoning"),
533 "",
534 ),
535 (
536 "<|channel|>analysis<|message|>Reason.<|end|>",
537 Some("Reason."),
538 "",
539 ),
540 (
541 "<|channel|>analysis<|message|>Reason.<|end|>\
542 <|start|>assistant<|channel|>final",
543 Some("Reason."),
544 "",
545 ),
546 (
547 "<|channel|>analysis<|message|>Reason.<|end|>\
548 <|start|>assistant<|channel|>final<|message|>Partial answer",
549 Some("Reason."),
550 "Partial answer",
551 ),
552 ] {
553 assert!(parse_harmony_response(output).is_err());
554 let parsed = parse_length_truncated_harmony_response(output).unwrap();
555 assert_eq!(parsed.reasoning_content.as_deref(), reasoning);
556 assert_eq!(parsed.content, content);
557 assert!(parsed.tool_call.is_none());
558 for finish_reason in [FinishReason::Stop, FinishReason::Length] {
559 assert_eq!(
560 parse_harmony_response_for_finish_reason(output, Some(finish_reason)).unwrap(),
561 parsed
562 );
563 }
564 for finish_reason in [
565 None,
566 Some(FinishReason::EOS),
567 Some(FinishReason::Cancelled),
568 Some(FinishReason::Error),
569 Some(FinishReason::ContentFilter),
570 ] {
571 assert!(
572 parse_harmony_response_for_finish_reason(output, finish_reason).is_err(),
573 "accepted incomplete output {output:?} for {finish_reason:?}"
574 );
575 }
576 }
577 }
578
579 #[test]
580 fn parses_length_truncation_between_followup_envelope_markers() {
581 for output in [
582 "<|channel|>analysis<|message|>Reason.<|end|>\
583 <|start|>",
584 "<|channel|>analysis<|message|>Reason.<|end|>\
585 <|start|>assistant",
586 "<|channel|>analysis<|message|>Reason.<|end|>\
587 <|start|>assistant<|channel|>",
588 ] {
589 assert!(parse_harmony_response(output).is_err());
590 let parsed = parse_length_truncated_harmony_response(output).unwrap();
591 assert_eq!(parsed.reasoning_content.as_deref(), Some("Reason."));
592 assert!(parsed.content.is_empty());
593 assert!(parsed.tool_call.is_none());
594 assert_eq!(
595 parse_harmony_response_for_finish_reason(output, Some(FinishReason::Stop)).unwrap(),
596 parsed
597 );
598 assert!(
599 parse_harmony_response_for_finish_reason(output, Some(FinishReason::EOS)).is_err()
600 );
601 }
602 }
603
604 #[test]
605 fn user_or_length_truncation_keeps_tool_calls_and_control_markers_fail_closed() {
606 for output in [
607 "<|channel|>analysis<|message|>Reason.<|end|>\
608 <|start|>assistant<|channel|>commentary to=functions.weather\
609 <|constrain|>json<|message|>{\"city\":\"Paris\"}",
610 "<|channel|>analysis<|message|>Reason.<|end|>\
611 <|start|>assistant<|channel|>commentary to=functions.weather\
612 <|constrain|>json<|message|>{\"city\":",
613 "<|channel|>final<|message|>leak <|bogus|>",
614 "<|channel|>final<|message|>incomplete <|",
615 "<|channel|>analysis<|message|>Reason.<|end|>\
616 <|start|>assistant<|channel|>commentary to=functions.weather\
617 <|constrain|>json",
618 "<|channel|>analysis<|message|>Reason.<|end|>\
619 <|start|>assistant<|channel|>final<|mess",
620 "<|channel|>analysis<|message|>Reason.<|end|>\
621 <|start|>assistant<|channel|>fina",
622 "<|channel|>analysis<|message|>Reason.<|end|>\
623 <|start|>assistant to=functions.weather<|channel|>final",
624 ] {
625 assert!(
626 parse_length_truncated_harmony_response(output).is_err(),
627 "accepted {output:?}"
628 );
629 for finish_reason in [FinishReason::Stop, FinishReason::Length] {
630 assert!(
631 parse_harmony_response_for_finish_reason(output, Some(finish_reason)).is_err(),
632 "accepted {output:?} for {finish_reason:?}"
633 );
634 }
635 }
636 }
637
638 #[test]
639 fn parses_analysis_then_function_tool_call_in_both_header_orders() {
640 for output in [
641 "<|channel|>analysis<|message|>Need weather.<|end|>\
642 <|start|>assistant<|channel|>commentary to=functions.weather<|constrain|>json\
643 <|message|>{\"city\":\"Paris\"}<|call|>",
644 "<|channel|>analysis<|message|>Need weather.<|end|>\
645 <|start|>assistant to=functions.weather<|channel|>commentary<|constrain|>json\
646 <|message|>{\"city\":\"Paris\"}<|call|>",
647 ] {
648 let parsed = parse_harmony_response(output).unwrap();
649 for finish_reason in [FinishReason::Stop, FinishReason::Length, FinishReason::EOS] {
650 assert_eq!(
651 parse_harmony_response_for_finish_reason(output, Some(finish_reason)).unwrap(),
652 parsed
653 );
654 }
655 assert_eq!(parsed.reasoning_content.as_deref(), Some("Need weather."));
656 assert!(parsed.content.is_empty());
657 assert_eq!(
658 parsed.tool_call,
659 Some(HarmonyToolCall {
660 name: "weather".to_string(),
661 arguments_json: "{\"city\":\"Paris\"}".to_string(),
662 })
663 );
664 }
665 }
666
667 #[test]
668 fn parses_direct_function_tool_call_with_optional_prefix_and_content_type() {
669 for header in [
670 "<|channel|>commentary to=functions.weather",
671 "<|start|>assistant<|channel|>commentary to=functions.weather",
672 "<|start|>assistant to=functions.weather<|channel|>commentary",
673 ] {
674 for content_type in ["", "<|constrain|>json"] {
675 let output =
676 format!("{header}{content_type}<|message|> {{\"city\": \"Paris\"}} \n<|call|>");
677 let expected = ParsedHarmonyResponse {
678 reasoning_content: None,
679 content: String::new(),
680 tool_call: Some(HarmonyToolCall {
681 name: "weather".to_string(),
682 arguments_json: "{\"city\": \"Paris\"}".to_string(),
683 }),
684 };
685 assert_eq!(parse_harmony_response(&output).unwrap(), expected);
686 for finish_reason in [FinishReason::Stop, FinishReason::Length, FinishReason::EOS] {
687 assert_eq!(
688 parse_harmony_response_for_finish_reason(&output, Some(finish_reason))
689 .unwrap(),
690 expected
691 );
692 }
693 }
694 }
695 }
696
697 #[test]
698 fn rejects_direct_tool_calls_with_invalid_recipient_or_content_type() {
699 for header in [
700 "<|channel|>commentary",
701 "<|channel|>commentary to=",
702 "<|channel|>commentary to=functions.",
703 "<|channel|>commentary to=browser.search",
704 "<|channel|>commentary to=python",
705 "<|start|>assistant to=functions.weather<|channel|>commentary to=functions.weather",
706 "<|channel|>commentary to=functions.weather<|constrain|>text",
707 ] {
708 let output = format!("{header}<|message|>{{}}<|call|>");
709 assert!(
710 parse_harmony_response(&output).is_err(),
711 "accepted {output:?}"
712 );
713 assert!(
714 parse_length_truncated_harmony_response(&output).is_err(),
715 "accepted {output:?}"
716 );
717 }
718 }
719
720 #[test]
721 fn direct_tool_calls_require_complete_call_terminal_even_when_truncated() {
722 for suffix in [
723 "",
724 "<|message|>{\"city\":",
725 "<|message|>{\"city\":\"Paris\"}",
726 "<|message|>{}<|cal",
727 "<|message|>{}<|end|>",
728 "<|message|>{}<|return|>",
729 "<|message|>{}<|call|><|call|>",
730 "<|message|>{}<|call|>garbage",
731 "<|message|>{}<|call|><|start|>assistant<|channel|>final<|message|>done<|return|>",
732 ] {
733 let output =
734 format!("<|channel|>commentary to=functions.weather<|constrain|>json{suffix}");
735 assert!(
736 parse_harmony_response(&output).is_err(),
737 "accepted {output:?}"
738 );
739 for finish_reason in [FinishReason::Stop, FinishReason::Length, FinishReason::EOS] {
740 assert!(
741 parse_harmony_response_for_finish_reason(&output, Some(finish_reason)).is_err(),
742 "accepted {output:?} for {finish_reason:?}"
743 );
744 }
745 }
746 }
747
748 #[test]
749 fn rejects_invalid_or_non_object_tool_json() {
750 for arguments in ["{", "[]", "null", "\"Paris\""] {
751 for prefix in [
752 "",
753 "<|channel|>analysis<|message|>Need weather.<|end|><|start|>assistant",
754 ] {
755 let output = format!(
756 "{prefix}<|channel|>commentary to=functions.weather<|constrain|>json\
757 <|message|>{arguments}<|call|>"
758 );
759 assert!(
760 parse_harmony_response(&output).is_err(),
761 "accepted {output:?}"
762 );
763 }
764 }
765 }
766
767 #[test]
768 fn rejects_raw_marker_leakage_and_incomplete_envelopes() {
769 for output in [
770 "<|channel|>final<|message|>leak <|bogus|> marker<|return|>",
771 "<|channel|>final answer<|return|>",
772 "<|channel|>final<|message|>incomplete <| marker",
773 ] {
774 assert!(
775 parse_harmony_response(output).is_err(),
776 "accepted {output:?}"
777 );
778 }
779 }
780
781 #[test]
782 fn rejects_wrong_missing_or_repeated_terminal_tokens() {
783 for output in [
784 "<|channel|>final<|message|>Paris<|end|>",
785 "<|channel|>final<|message|>Paris<|call|>",
786 "<|channel|>final<|message|>Paris",
787 "<|channel|>final<|message|>Paris<|return|><|return|>",
788 "<|channel|>final<|message|>Paris<|return|>garbage",
789 "<|channel|>analysis<|message|>Need weather.<|end|>\
790 <|start|>assistant<|channel|>commentary to=functions.weather<|message|>{}<|return|>",
791 ] {
792 assert!(
793 parse_harmony_response(output).is_err(),
794 "accepted {output:?}"
795 );
796 }
797 }
798
799 #[test]
800 fn rejects_unknown_or_empty_tool_recipients() {
801 for recipient in ["browser.search", "python", "functions."] {
802 let output = format!(
803 "<|channel|>analysis<|message|>Need a tool.<|end|>\
804 <|start|>assistant<|channel|>commentary to={recipient}<|constrain|>json\
805 <|message|>{{}}<|call|>"
806 );
807 assert!(
808 parse_harmony_response(&output).is_err(),
809 "accepted {recipient:?}"
810 );
811 }
812 }
813
814 #[test]
815 fn rejects_invalid_channel_or_extra_message() {
816 for output in [
817 "<|channel|>developer<|message|>no<|return|>",
818 "<|channel|>analysis<|message|>one<|end|>\
819 <|start|>assistant<|channel|>analysis<|message|>two<|end|>\
820 <|start|>assistant<|channel|>final<|message|>answer<|return|>",
821 ] {
822 assert!(
823 parse_harmony_response(output).is_err(),
824 "accepted {output:?}"
825 );
826 }
827 }
828}