Skip to main content

ferrum_types/
harmony.rs

1//! Strict terminal-output parsing for the GPT-OSS Harmony wire protocol.
2//!
3//! This intentionally supports a final answer or a function call on the
4//! commentary channel, optionally preceded by one analysis message.
5
6use 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/// A validated function call emitted through the Harmony commentary channel.
20#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
21pub struct HarmonyToolCall {
22    pub name: String,
23    /// The original JSON object text, with only surrounding whitespace removed.
24    pub arguments_json: String,
25}
26
27/// Product-facing terminal result of a complete Harmony model output.
28#[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
35/// Parse one complete decoded GPT-OSS Harmony response.
36///
37/// Accepted shapes are deliberately narrow:
38///
39/// - `final<|return|>`
40/// - `commentary to=functions.NAME ... <|call|>`
41/// - `analysis<|end|> -> final<|return|>`
42/// - `analysis<|end|> -> commentary to=functions.NAME ... <|call|>`
43///
44/// The first message may omit `<|start|>assistant` because that prefix is
45/// normally already present at the end of the rendered generation prompt.
46pub fn parse_harmony_response(output: &str) -> Result<ParsedHarmonyResponse> {
47    parse_harmony_response_internal(output, false)
48}
49
50/// Parse a Harmony response that the engine stopped at its token limit.
51///
52/// A length stop may legitimately omit the terminal token from an analysis or
53/// final text message. Tool calls remain fail-closed and still require a
54/// complete `<|call|>` envelope.
55pub fn parse_length_truncated_harmony_response(output: &str) -> Result<ParsedHarmonyResponse> {
56    parse_harmony_response_internal(output, true)
57}
58
59/// Parse decoded Harmony output using the engine's actual completion reason.
60///
61/// A caller-configured stop or token limit can cut an analysis or final text
62/// message before its terminal token. Model EOS and unknown completion reasons
63/// still require a complete envelope. Pass the engine reason before converting
64/// it to a protocol's wire finish reason, which may map EOS to `stop`.
65/// Tool calls and raw control markers retain the strict parser's validation.
66pub 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}