blob: 37369205f6e452338e82a3b62251b4dc46e2d6c0 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
pub use maka_agent_facts::model::assembly::StepBuilder;
#[cfg(test)]
mod tests {
use super::*;
use maka_agent_facts::model::ModelEvent;
use maka_agent_facts::model::ModelFinishReason;
use maka_agent_facts::model::ModelPart;
use maka_agent_facts::model::ModelUsage;
use maka_agent_facts::model::TextKind;
use serde_json::json;
#[test]
fn interleaved_parts_preserve_metadata_and_require_complete_streams() {
let events = [
ModelEvent::PartStarted {
id: "r".into(),
text_kind: TextKind::Thinking,
provider_options: Some(json!({"anthropic":{"signature":"initial","other":1}})),
},
ModelEvent::PartStarted {
id: "t".into(),
text_kind: TextKind::Text,
provider_options: None,
},
ModelEvent::PartDelta {
id: "r".into(),
text: "think".into(),
provider_options: None,
},
ModelEvent::PartDelta {
id: "t".into(),
text: "answer".into(),
provider_options: None,
},
ModelEvent::PartFinished {
id: "r".into(),
provider_options: Some(json!({"anthropic":{"signature":"signed"}})),
},
ModelEvent::PartFinished {
id: "t".into(),
provider_options: Some(json!({"openai":{"itemId":"item"}})),
},
ModelEvent::Finished {
reason: ModelFinishReason::Stop,
usage: ModelUsage {
input_tokens: Some(3),
output_tokens: Some(4),
reasoning_tokens: Some(2),
..Default::default()
},
provider_options: None,
},
];
for length in [4, 6, 7] {
let mut builder = StepBuilder::default();
for event in &events[..length] {
builder.push(event.clone()).unwrap();
}
let result = builder.finish();
assert_eq!(result.is_ok(), length == 7);
if let Ok(step) = result {
assert_eq!(
step.parts,
vec![
ModelPart::Text {
text_kind: TextKind::Thinking,
text: "think".into(),
provider_options: Some(
json!({"anthropic":{"signature":"signed","other":1}})
)
},
ModelPart::Text {
text_kind: TextKind::Text,
text: "answer".into(),
provider_options: Some(json!({"openai":{"itemId":"item"}}))
},
]
);
assert_eq!(step.usage.reasoning_tokens, Some(2));
}
}
for already_finished in [false, true] {
let mut builder = StepBuilder::default();
let finish = ModelEvent::Finished {
reason: ModelFinishReason::Stop,
usage: ModelUsage::default(),
provider_options: None,
};
if already_finished {
builder.push(finish.clone()).unwrap();
}
assert!(
builder
.push(ModelEvent::PartFinished {
id: "unknown".into(),
provider_options: None,
})
.is_err()
);
assert!(builder.push(finish).is_err());
assert!(builder.finish().is_err());
}
for reason in ["content-filter", "error", "unknown"] {
assert!(serde_json::from_value::<ModelFinishReason>(json!(reason)).is_err());
}
for (reason, wire) in [
(ModelFinishReason::Stop, "stop"),
(ModelFinishReason::ToolCalls, "tool-calls"),
(ModelFinishReason::Length, "length"),
] {
assert_eq!(serde_json::to_value(reason).unwrap(), json!(wire));
assert_eq!(
serde_json::from_value::<ModelFinishReason>(json!(wire)).unwrap(),
reason
);
let finish = ModelEvent::Finished {
reason,
usage: ModelUsage {
input_tokens: Some(3),
output_tokens: Some(8),
..Default::default()
},
provider_options: None,
};
let mut builder = StepBuilder::default();
builder.push(finish).unwrap();
let step = builder.finish().unwrap();
assert_eq!(step.finish_reason, reason);
assert_eq!(step.usage.output_tokens, Some(8));
}
}
}