use serde_json::{Value, json};
use crate::ir::IrStreamEvent;
use crate::map::MapError;
use super::{MAX_CONTENT_BLOCK_INDEX, check_index};
pub(super) fn decode(value: &Value) -> Result<Option<IrStreamEvent>, MapError> {
if let Some(delta) = value.pointer("/contentBlockDelta/delta") {
if let Some(text) = delta.get("text").and_then(Value::as_str) {
return Ok(Some(IrStreamEvent::TextDelta {
text: text.to_string(),
}));
}
if let Some(text) = delta
.pointer("/reasoningContent/text")
.and_then(Value::as_str)
{
return Ok(Some(IrStreamEvent::ReasoningDelta {
text: text.to_string(),
}));
}
if let Some(signature) = delta
.pointer("/reasoningContent/signature")
.and_then(Value::as_str)
{
return Ok(Some(IrStreamEvent::ReasoningSignature {
signature: signature.to_string(),
}));
}
if let Some(blob) = delta
.pointer("/reasoningContent/redactedContent")
.and_then(Value::as_str)
.filter(|blob| !blob.is_empty())
{
return Ok(Some(IrStreamEvent::Protocol {
item_type: "converse_redacted_content".into(),
payload: json!(blob),
}));
}
if let Some(input) = delta.pointer("/toolUse/input").and_then(Value::as_str) {
let block = value.get("contentBlockDelta").unwrap_or(value);
return Ok(Some(IrStreamEvent::ToolCallArgDelta {
delta: input.to_string(),
index: content_block_index(block)?,
}));
}
if let Some(citation) = delta.get("citation") {
if let Some(annotation) = annotation_from_converse_citation(citation) {
return Ok(Some(IrStreamEvent::AnnotationAdded { annotation }));
}
return Ok(Some(IrStreamEvent::Protocol {
item_type: "converse_frame".into(),
payload: value.clone(),
}));
}
}
if let Some(start) = value.get("contentBlockStart")
&& let Some(tool) = start.pointer("/start/toolUse")
{
let id = tool
.get("toolUseId")
.and_then(Value::as_str)
.unwrap_or("")
.to_string();
let name = tool
.get("name")
.and_then(Value::as_str)
.unwrap_or("")
.to_string();
return Ok(Some(IrStreamEvent::ToolCallStart {
id,
name,
thought_signature: None,
index: content_block_index(start)?,
}));
}
if let Some(start) = value.get("contentBlockStart")
&& let Some(image) = start.pointer("/start/image")
{
return Ok(image_delta_from_converse(image));
}
if let Some(start) = value.get("contentBlockStart")
&& let Some(audio) = start.pointer("/start/audio")
{
return Ok(audio_delta_from_block(audio));
}
if value.get("contentBlockStop").is_some() {
return Ok(None);
}
if let Some(stop) = value
.pointer("/messageStop/stopReason")
.and_then(Value::as_str)
{
return Ok(Some(IrStreamEvent::FinishReason {
reason: stop.to_string(),
vendor: None,
}));
}
if let Some(usage) = value.pointer("/metadata/usage") {
return Ok(Some(usage_from_converse(usage)));
}
if let Some(tier) = service_tier_from(value) {
return Ok(Some(IrStreamEvent::ServiceTier { tier }));
}
if value.get("messageStart").is_some() {
return Ok(None);
}
Ok(Some(IrStreamEvent::Unknown {
event: "converse".into(),
raw: value.clone(),
}))
}
pub(super) fn encode(ev: &IrStreamEvent) -> Result<Value, MapError> {
match ev {
IrStreamEvent::TextDelta { text }
| IrStreamEvent::RefusalDelta { text }
| IrStreamEvent::AudioTranscriptDelta { text } => Ok(json!({
"contentBlockDelta": { "delta": { "text": text } }
})),
IrStreamEvent::ReasoningDelta { text } => Ok(json!({
"contentBlockDelta": { "delta": { "reasoningContent": { "text": text } } }
})),
IrStreamEvent::ReasoningSignature { signature } => Ok(json!({
"contentBlockDelta": { "delta": { "reasoningContent": { "signature": signature } } }
})),
IrStreamEvent::ToolCallStart { id, name, .. } => Ok(json!({
"contentBlockStart": {
"start": { "toolUse": { "toolUseId": id, "name": name } }
}
})),
IrStreamEvent::CustomToolCallStart { id, name, .. } => Ok(json!({
"contentBlockStart": {
"start": { "toolUse": { "toolUseId": id, "name": name } }
}
})),
IrStreamEvent::AnnotationAdded { annotation } => Ok(json!({
"contentBlockDelta": { "delta": { "citation": citation_from_annotation(annotation) } }
})),
IrStreamEvent::ImageDelta { media_type, data } => {
let Some(format) = crate::map::converse_image_format(media_type) else {
return Err(MapError::Invalid(format!(
"converse image format is not supported: {media_type}"
)));
};
Ok(json!({
"contentBlockStart": {
"start": {
"image": {
"format": format,
"source": { "bytes": data }
}
}
}
}))
}
IrStreamEvent::AudioDelta { data } => Ok(json!({
"contentBlockStart": {
"start": {
"audio": {
"format": "mp3",
"source": { "bytes": data }
}
}
}
})),
IrStreamEvent::Logprobs { .. }
| IrStreamEvent::Created { .. }
| IrStreamEvent::Metadata { .. }
| IrStreamEvent::Moderation { .. } => Ok(json!({
"contentBlockDelta": { "delta": { "text": "" } }
})),
IrStreamEvent::SearchEntryPoint { .. } => Ok(json!({})),
IrStreamEvent::ServiceTier { tier } => {
let mut metadata = serde_json::Map::new();
if let Some((mapped, _)) = crate::map::converse_service_tier(tier) {
metadata.insert("serviceTier".into(), json!({ "type": mapped }));
}
Ok(json!({ "metadata": metadata }))
}
IrStreamEvent::ToolCallArgDelta { delta, .. } => Ok(json!({
"contentBlockDelta": { "delta": { "toolUse": { "input": delta } } }
})),
IrStreamEvent::CustomToolCallInputDelta { delta, .. } => {
let text = crate::map::response_custom_tool_input(delta).to_string();
Ok(json!({
"contentBlockDelta": { "delta": { "toolUse": { "input": text } } }
}))
}
IrStreamEvent::ToolCallEnd => Ok(json!({ "contentBlockStop": {} })),
IrStreamEvent::FinishReason { reason, .. } => Ok(json!({
"messageStop": { "stopReason": finish_reason(reason) }
})),
IrStreamEvent::Usage {
prompt_tokens,
completion_tokens,
cache_read_tokens,
cache_write_tokens,
..
} => Ok(json!({
"metadata": {
"usage": encode_converse_usage(
*prompt_tokens,
*completion_tokens,
*cache_read_tokens,
*cache_write_tokens,
)
}
})),
IrStreamEvent::Done => Ok(json!({ "messageStop": { "stopReason": "end_turn" } })),
other => Err(MapError::Invalid(format!(
"converse stream cannot encode {other:?}"
))),
}
}
pub(super) fn encode_complete(events: &[IrStreamEvent]) -> Result<Value, MapError> {
let mut text = String::new();
let mut content = Vec::new();
let mut current_tool: Option<(String, String, String, bool)> = None;
let mut citations = Vec::new();
let mut reasoning = String::new();
let mut reasoning_signature = None;
let mut redacted_reasoning = Vec::new();
let mut audio_data = String::new();
let mut stop = "end_turn";
let mut usage = None;
let mut service_tier = None;
let mut passthrough = Vec::new();
for ev in events {
match ev {
IrStreamEvent::TextDelta { text: delta }
| IrStreamEvent::AudioTranscriptDelta { text: delta }
| IrStreamEvent::RefusalDelta { text: delta } => text.push_str(delta),
IrStreamEvent::ReasoningDelta { text: delta } => reasoning.push_str(delta),
IrStreamEvent::ReasoningSignature { signature } => {
reasoning_signature = Some(signature.clone());
}
IrStreamEvent::Protocol { item_type, payload }
if item_type == "converse_redacted_content" =>
{
if let Some(blob) = payload.as_str().filter(|blob| !blob.is_empty()) {
redacted_reasoning.push(blob.to_string());
}
}
IrStreamEvent::AudioDelta { data } => audio_data.push_str(data),
IrStreamEvent::ImageDelta { media_type, data } => {
let Some(format) = crate::map::converse_image_format(media_type) else {
return Err(MapError::Invalid(format!(
"converse image format is not supported: {media_type}"
)));
};
flush_text(&mut text, &mut content);
content.push(json!({
"image": {
"format": format,
"source": { "bytes": data }
}
}));
}
IrStreamEvent::AnnotationAdded { annotation } => {
citations.push(citation_from_annotation(annotation));
}
IrStreamEvent::ToolCallStart { id, name, .. }
| IrStreamEvent::CustomToolCallStart { id, name, .. } => {
let custom = matches!(ev, IrStreamEvent::CustomToolCallStart { .. });
flush_text(&mut text, &mut content);
if let Some((id, name, args, custom)) = current_tool.take() {
content.push(tool_use(&id, &name, &args, custom));
}
current_tool = Some((id.clone(), name.clone(), String::new(), custom));
}
IrStreamEvent::ToolCallArgDelta { delta, .. }
| IrStreamEvent::CustomToolCallInputDelta { delta, .. } => {
if let Some((_, _, args, _)) = current_tool.as_mut() {
args.push_str(delta);
}
}
IrStreamEvent::ToolCallEnd => {
if let Some((id, name, args, custom)) = current_tool.take() {
content.push(tool_use(&id, &name, &args, custom));
stop = "tool_use";
}
}
IrStreamEvent::FinishReason { reason, .. } => stop = finish_reason(reason),
IrStreamEvent::ServiceTier { tier } => {
if let Some((mapped, _)) = crate::map::converse_service_tier(tier) {
service_tier = Some(mapped);
}
}
IrStreamEvent::Usage {
prompt_tokens,
completion_tokens,
cache_read_tokens,
cache_write_tokens,
..
} => {
usage = Some((
*prompt_tokens,
*completion_tokens,
*cache_read_tokens,
*cache_write_tokens,
));
}
IrStreamEvent::Protocol { item_type, payload }
if item_type == "converse_citation" && payload.is_object() =>
{
citations.push(payload.clone());
}
IrStreamEvent::Protocol { item_type, payload }
if payload.is_object()
&& let Some(key) = match item_type.as_str() {
"converse_guard_content" => Some("guardContent"),
"converse_document" => Some("document"),
"converse_video" => Some("video"),
"converse_image" => Some("image"),
"converse_audio" => Some("audio"),
_ => None,
} =>
{
flush_text(&mut text, &mut content);
content.push(json!({ key: payload }));
}
IrStreamEvent::Protocol { item_type, payload }
if converse_passthrough(item_type) && !payload.is_null() =>
{
passthrough.push((item_type.clone(), payload.clone()));
}
_ => {}
}
}
let mut prefix = Vec::new();
for blob in &redacted_reasoning {
prefix.push(json!({
"reasoningContent": { "redactedContent": blob }
}));
}
if !reasoning.is_empty()
|| reasoning_signature
.as_deref()
.is_some_and(|s| !s.is_empty())
{
let mut reasoning_text = json!({ "text": reasoning });
if let Some(signature) = reasoning_signature.as_deref().filter(|s| !s.is_empty()) {
reasoning_text["signature"] = json!(signature);
}
prefix.push(json!({
"reasoningContent": { "reasoningText": reasoning_text }
}));
}
if !audio_data.is_empty() {
prefix.push(json!({
"audio": {
"format": "mp3",
"source": { "bytes": audio_data }
}
}));
}
prefix.append(&mut content);
content = prefix;
if !citations.is_empty() {
if !text.is_empty() {
content.push(json!({ "text": text }));
text.clear();
}
let mut kept = Vec::new();
let mut run = Vec::new();
let mut cited = false;
let flush_run =
|kept: &mut Vec<Value>, run: &mut Vec<Value>, cited: &mut bool, citations: &[Value]| {
if run.is_empty() {
return;
}
if !*cited {
kept.push(json!({
"citationsContent": {
"content": run.clone(),
"citations": citations
}
}));
*cited = true;
run.clear();
} else {
kept.append(run);
}
};
for block in content.drain(..) {
if block.get("text").is_some() {
run.push(block);
} else {
flush_run(&mut kept, &mut run, &mut cited, &citations);
kept.push(block);
}
}
flush_run(&mut kept, &mut run, &mut cited, &citations);
if !cited {
kept.push(json!({
"citationsContent": {
"content": [],
"citations": citations
}
}));
}
content = kept;
}
flush_text(&mut text, &mut content);
if let Some((id, name, args, custom)) = current_tool.take() {
content.push(tool_use(&id, &name, &args, custom));
if stop == "end_turn" {
stop = "tool_use";
}
}
let mut body = json!({
"output": { "message": { "role": "assistant", "content": content } },
"stopReason": stop
});
if let Some(tier) = service_tier {
body["serviceTier"] = json!({ "type": tier });
}
if let Some((input, output, cache_read, cache_write)) = usage {
body["usage"] = encode_converse_usage(input, output, cache_read, cache_write);
}
for (key, fields) in passthrough {
body[key] = fields;
}
Ok(body)
}
pub(super) fn decode_complete(value: &Value) -> Result<Vec<IrStreamEvent>, MapError> {
let mut out = Vec::new();
let blocks = value
.pointer("/output/message/content")
.and_then(Value::as_array)
.cloned()
.unwrap_or_default();
let mut tool_index = 0u32;
for block in &blocks {
let sibling = block.get("text").and_then(Value::as_str).unwrap_or("");
let generated = citations_content_text(block);
let text = if sibling.is_empty() {
generated
} else if generated.is_empty() || sibling == generated {
sibling.to_string()
} else {
format!("{sibling}{generated}")
};
if !text.is_empty() {
out.push(IrStreamEvent::TextDelta { text });
}
if let Some(text) = block
.pointer("/reasoningContent/reasoningText/text")
.and_then(Value::as_str)
.filter(|s| !s.is_empty())
{
out.push(IrStreamEvent::ReasoningDelta {
text: text.to_string(),
});
}
if let Some(signature) = block
.pointer("/reasoningContent/reasoningText/signature")
.and_then(Value::as_str)
.filter(|s| !s.is_empty())
{
out.push(IrStreamEvent::ReasoningSignature {
signature: signature.to_string(),
});
}
if let Some(blob) = block
.pointer("/reasoningContent/redactedContent")
.and_then(Value::as_str)
.filter(|blob| !blob.is_empty())
{
out.push(IrStreamEvent::Protocol {
item_type: "converse_redacted_content".into(),
payload: json!(blob),
});
}
if let Some(audio) = block.get("audio") {
if let Some(ev) = audio_delta_from_block(audio) {
out.push(ev);
} else if audio.is_object() {
out.push(IrStreamEvent::Protocol {
item_type: "converse_audio".into(),
payload: audio.clone(),
});
}
}
if let Some(image) = block.get("image") {
if let Some(ev) = image_delta_from_converse(image) {
out.push(ev);
} else if image.is_object() {
out.push(IrStreamEvent::Protocol {
item_type: "converse_image".into(),
payload: image.clone(),
});
}
}
if let Some(guard) = block.get("guardContent").filter(|guard| guard.is_object()) {
out.push(IrStreamEvent::Protocol {
item_type: "converse_guard_content".into(),
payload: guard.clone(),
});
}
for (key, item_type) in [
("document", "converse_document"),
("video", "converse_video"),
] {
if let Some(value) = block.get(key).filter(|value| value.is_object()) {
out.push(IrStreamEvent::Protocol {
item_type: item_type.into(),
payload: value.clone(),
});
}
}
if let Some(citations) = block
.pointer("/citationsContent/citations")
.and_then(Value::as_array)
{
for citation in citations {
if let Some(annotation) = annotation_from_converse_citation(citation) {
out.push(IrStreamEvent::AnnotationAdded { annotation });
} else if citation.is_object() {
out.push(IrStreamEvent::Protocol {
item_type: "converse_citation".into(),
payload: citation.clone(),
});
}
}
}
if let Some(tool) = block.get("toolUse") {
let id = tool
.get("toolUseId")
.and_then(Value::as_str)
.unwrap_or("")
.to_string();
let name = tool
.get("name")
.and_then(Value::as_str)
.unwrap_or("")
.to_string();
let args = tool
.get("input")
.map(|v| v.to_string())
.unwrap_or_else(|| "{}".into());
let index = tool_index;
tool_index = tool_index.saturating_add(1);
out.push(IrStreamEvent::ToolCallStart {
id,
name,
thought_signature: None,
index,
});
out.push(IrStreamEvent::ToolCallArgDelta { delta: args, index });
out.push(IrStreamEvent::ToolCallEnd);
}
}
if let Some(reason) = value.get("stopReason").and_then(Value::as_str) {
out.push(IrStreamEvent::FinishReason {
reason: reason.to_string(),
vendor: None,
});
}
if let Some(tier) = service_tier_from(value) {
out.push(IrStreamEvent::ServiceTier { tier });
}
if let Some(usage) = value.get("usage") {
out.push(usage_from_converse(usage));
}
for key in [
"additionalModelResponseFields",
"metrics",
"trace",
"performanceConfig",
] {
if let Some(fields) = value.get(key).filter(|fields| !fields.is_null()) {
out.push(IrStreamEvent::Protocol {
item_type: key.into(),
payload: fields.clone(),
});
}
}
out.push(IrStreamEvent::Done);
Ok(out)
}
pub(super) fn annotation_from_converse_citation(citation: &Value) -> Option<Value> {
let url = citation
.pointer("/location/web/url")
.or_else(|| citation.get("source"))
.and_then(Value::as_str)
.filter(|s| !s.is_empty())?;
let mut out = json!({ "type": "url_citation", "url": url });
if let Some(title) = citation
.get("title")
.and_then(Value::as_str)
.filter(|s| !s.is_empty())
{
out["title"] = json!(title);
}
Some(out)
}
pub(super) fn citation_from_annotation(annotation: &Value) -> Value {
let url = super::chat::annotation_url(annotation).unwrap_or("");
let mut citation = json!({
"source": url,
"location": { "web": { "url": url } }
});
if let Some(title) = super::chat::annotation_title(annotation) {
citation["title"] = json!(title);
}
citation
}
pub(super) fn decode_metadata_events(value: &Value) -> Vec<IrStreamEvent> {
let mut out = Vec::new();
if let Some(tier) = service_tier_from(value) {
out.push(IrStreamEvent::ServiceTier { tier });
}
let source = value.get("metadata").unwrap_or(value);
for key in [
"additionalModelResponseFields",
"metrics",
"trace",
"performanceConfig",
] {
if let Some(fields) = source.get(key).filter(|fields| !fields.is_null()) {
out.push(IrStreamEvent::Protocol {
item_type: key.into(),
payload: fields.clone(),
});
}
}
if let Some(usage) = value.pointer("/metadata/usage") {
out.push(usage_from_converse(usage));
}
out
}
fn converse_passthrough(item_type: &str) -> bool {
matches!(
item_type,
"additionalModelResponseFields" | "metrics" | "trace" | "performanceConfig"
)
}
fn service_tier_from(value: &Value) -> Option<String> {
value
.pointer("/metadata/serviceTier/type")
.or_else(|| value.pointer("/serviceTier/type"))
.and_then(Value::as_str)
.filter(|s| !s.is_empty())
.map(str::to_string)
}
fn usage_from_converse(usage: &Value) -> IrStreamEvent {
IrStreamEvent::Usage {
prompt_tokens: usage_u32(usage, "inputTokens"),
completion_tokens: usage_u32(usage, "outputTokens"),
cache_read_tokens: usage_u32(usage, "cacheReadInputTokens"),
cache_write_tokens: usage_u32(usage, "cacheWriteInputTokens"),
reasoning_tokens: 0,
audio_tokens: 0,
completion_audio_tokens: 0,
inference_geo: None,
}
}
fn encode_converse_usage(
prompt_tokens: u32,
completion_tokens: u32,
cache_read_tokens: u32,
cache_write_tokens: u32,
) -> Value {
let mut usage = json!({
"inputTokens": prompt_tokens,
"outputTokens": completion_tokens,
});
if cache_read_tokens > 0 {
usage["cacheReadInputTokens"] = json!(cache_read_tokens);
}
if cache_write_tokens > 0 {
usage["cacheWriteInputTokens"] = json!(cache_write_tokens);
}
let input_tokens = usage
.get("inputTokens")
.and_then(Value::as_u64)
.unwrap_or(0);
let output_tokens = usage
.get("outputTokens")
.and_then(Value::as_u64)
.unwrap_or(0);
usage["totalTokens"] = json!(input_tokens.saturating_add(output_tokens));
usage
}
fn citations_content_text(block: &Value) -> String {
let Some(parts) = block
.pointer("/citationsContent/content")
.and_then(Value::as_array)
else {
return String::new();
};
let mut out = String::new();
for part in parts {
if let Some(text) = part.get("text").and_then(Value::as_str) {
out.push_str(text);
}
}
out
}
fn usage_u32(usage: &Value, key: &str) -> u32 {
usage.get(key).and_then(Value::as_u64).unwrap_or(0) as u32
}
fn flush_text(text: &mut String, content: &mut Vec<Value>) {
if !text.is_empty() {
content.push(json!({ "text": text.as_str() }));
text.clear();
}
}
fn tool_use(id: &str, name: &str, args: &str, custom: bool) -> Value {
let input = if custom {
crate::map::response_custom_tool_input(args)
} else {
serde_json::from_str(args).unwrap_or_else(|_| json!(args))
};
json!({
"toolUse": {
"toolUseId": id,
"name": name,
"input": input
}
})
}
fn audio_delta_from_block(audio: &Value) -> Option<IrStreamEvent> {
let data = audio
.pointer("/source/bytes")
.and_then(Value::as_str)
.filter(|s| !s.is_empty())?;
Some(IrStreamEvent::AudioDelta {
data: data.to_string(),
})
}
fn image_delta_from_converse(image: &Value) -> Option<IrStreamEvent> {
let data = image
.pointer("/source/bytes")
.and_then(Value::as_str)
.filter(|s| !s.is_empty())?;
let format = image
.get("format")
.and_then(Value::as_str)
.filter(|s| !s.is_empty())?;
Some(IrStreamEvent::ImageDelta {
media_type: converse_image_media_type(format),
data: data.to_string(),
})
}
fn converse_image_media_type(format: &str) -> String {
match format.to_ascii_lowercase().as_str() {
"png" => "image/png".into(),
"jpeg" | "jpg" => "image/jpeg".into(),
"gif" => "image/gif".into(),
"webp" => "image/webp".into(),
_ => format!("image/{format}"),
}
}
fn content_block_index(block: &Value) -> Result<u32, MapError> {
check_index(
block,
"contentBlockIndex",
MAX_CONTENT_BLOCK_INDEX,
"content block",
)?;
Ok(block
.get("contentBlockIndex")
.and_then(Value::as_u64)
.and_then(|n| u32::try_from(n).ok())
.unwrap_or(0))
}
fn finish_reason(reason: &str) -> &'static str {
match reason {
"tool_use" | "tool-use" | "tool_calls" => "tool_use",
"max_tokens" | "length" => "max_tokens",
"content_filtered" | "content_filter" | "content-filter" => "content_filtered",
"stop_sequence" => "stop_sequence",
"guardrail_intervened" => "guardrail_intervened",
_ => "end_turn",
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn converse_encode_finish_maps_chat_stop_and_tool_calls() {
let stop = encode(&IrStreamEvent::FinishReason {
reason: "stop".into(),
vendor: None,
})
.expect("encode stop");
assert_eq!(
stop.pointer("/messageStop/stopReason")
.and_then(Value::as_str),
Some("end_turn"),
"Chat stop must become AWS end_turn, got {stop}"
);
let tools = encode(&IrStreamEvent::FinishReason {
reason: "tool_calls".into(),
vendor: None,
})
.expect("encode tool_calls");
assert_eq!(
tools
.pointer("/messageStop/stopReason")
.and_then(Value::as_str),
Some("tool_use"),
"Chat tool_calls must become AWS tool_use, got {tools}"
);
let done = encode(&IrStreamEvent::Done).expect("encode done");
assert_eq!(
done.pointer("/messageStop/stopReason")
.and_then(Value::as_str),
Some("end_turn"),
"Done stays end_turn, got {done}"
);
}
#[test]
fn converse_encode_finish_maps_content_filter() {
let stop = encode(&IrStreamEvent::FinishReason {
reason: "content_filter".into(),
vendor: None,
})
.expect("encode content_filter");
assert_eq!(
stop.pointer("/messageStop/stopReason")
.and_then(Value::as_str),
Some("content_filtered"),
"IR content_filter must become AWS content_filtered, got {stop}"
);
let complete = encode_complete(&[IrStreamEvent::FinishReason {
reason: "content_filter".into(),
vendor: None,
}])
.expect("complete");
assert_eq!(
complete.get("stopReason").and_then(Value::as_str),
Some("content_filtered"),
"complete JSON must map content_filter, got {complete}"
);
}
#[test]
fn converse_encode_reasoning_signature() {
let value = encode(&IrStreamEvent::ReasoningSignature {
signature: "sig-1".into(),
})
.expect("encode signature");
assert_eq!(
value
.pointer("/contentBlockDelta/delta/reasoningContent/signature")
.and_then(Value::as_str),
Some("sig-1"),
"dest Converse must emit reasoningContent.signature, got {value}"
);
}
#[test]
fn converse_decode_reasoning_signature() {
let value = json!({
"contentBlockDelta": {
"delta": { "reasoningContent": { "signature": "sig-1" } }
}
});
let ev = decode(&value)
.expect("decode dest Converse STREAM reasoningContent.signature")
.expect("event");
assert!(
matches!(ev, IrStreamEvent::ReasoningSignature { ref signature } if signature == "sig-1"),
"dest Converse STREAM reasoningContent.signature must decode ReasoningSignature, got {ev:?}"
);
}
}