use super::*;
use crate::ai::sse::SseParser;
use crate::ai::types::ToolSpec;
fn decode(stream: &str) -> Result<Vec<Event>, AiError> {
let (mut parser, mut events, mut out) = (SseParser::default(), Vec::new(), Vec::new());
let mut decoder = AnthropicDecoder::default();
parser.push(stream.as_bytes(), &mut events);
for event in &events {
decoder.feed(event, &mut out)?;
}
decoder.finish(&mut out);
Ok(out)
}
const TEXT: &str = "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"usage\":{\"input_tokens\":12}}}\n\n\
event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"delta\":{\"type\":\"text_delta\",\"text\":\"Hola\"}}\n\n\
event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"delta\":{\"type\":\"text_delta\",\"text\":\" mundo\"}}\n\n\
event: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\"},\"usage\":{\"output_tokens\":5}}\n\n\
event: message_stop\ndata: {\"type\":\"message_stop\"}\n\n";
#[test]
fn texto_en_streaming_con_uso_y_fin() {
let events = decode(TEXT).unwrap();
assert_eq!(
events,
[
Event::Text("Hola".into()),
Event::Text(" mundo".into()),
Event::Usage(Usage { input_tokens: 12, output_tokens: 5 }),
Event::Done(StopReason::EndTurn),
]
);
}
#[test]
fn el_uso_incluye_los_tokens_de_cache() {
let stream = "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"usage\":{\"input_tokens\":5,\"cache_read_input_tokens\":1000,\"cache_creation_input_tokens\":20}}}\n\n\
event: message_stop\ndata: {\"type\":\"message_stop\"}\n\n";
assert!(decode(stream).unwrap().contains(&Event::Usage(Usage { input_tokens: 1025, output_tokens: 0 })));
}
#[test]
fn herramienta_con_argumentos_en_trozos() {
let stream = "event: content_block_start\ndata: {\"type\":\"content_block_start\",\"content_block\":{\"type\":\"tool_use\",\"id\":\"t1\",\"name\":\"read_file\"}}\n\n\
event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"{\\\"path\\\":\"}}\n\n\
event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\\\"a.rs\\\"}\"}}\n\n\
event: content_block_stop\ndata: {\"type\":\"content_block_stop\"}\n\n\
event: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"tool_use\"},\"usage\":{\"output_tokens\":9}}\n\n\
event: message_stop\ndata: {\"type\":\"message_stop\"}\n\n";
let events = decode(stream).unwrap();
assert_eq!(events[0], Event::ToolUse { id: "t1".into(), name: "read_file".into(), input: json!({"path": "a.rs"}) });
assert_eq!(events.last(), Some(&Event::Done(StopReason::ToolUse)));
}
#[test]
fn error_del_proveedor_dentro_del_stream() {
let error =
decode("event: error\ndata: {\"type\":\"error\",\"error\":{\"message\":\"saturado\"}}\n\n").unwrap_err();
assert!(matches!(error, AiError::Api(message) if message == "saturado"));
}
#[test]
fn la_suscripcion_antepone_la_identidad_y_usa_token_bearer() {
let request = Request {
model: "m".into(),
system: Some("Mi sistema".into()),
messages: vec![Message::user("hola")],
tools: Vec::new(),
max_tokens: 10,
};
let body = subscription_body(&request);
assert_eq!(body["system"][0]["text"], SUBSCRIPTION_IDENTITY);
assert_eq!(body["system"][1]["text"], "Mi sistema");
let headers = subscription_headers("tok");
assert!(headers.contains(&("authorization".into(), "Bearer tok".into())));
assert!(headers.iter().any(|(name, value)| name == "anthropic-beta" && value.contains("oauth-2025-04-20")));
}
#[test]
fn las_imagenes_van_como_bloques_base64() {
let request = Request {
model: "claude".into(),
system: None,
messages: vec![Message {
role: Role::User,
blocks: vec![
Block::Image { media_type: "image/png".into(), data_base64: "AAA".into(), name: None },
Block::Text("¿qué ves?".into()),
],
}],
tools: Vec::new(),
max_tokens: 10,
};
let body = body(&request);
assert_eq!(body["messages"][0]["content"][0]["type"], "image");
assert_eq!(body["messages"][0]["content"][0]["source"]["media_type"], "image/png");
assert_eq!(body["messages"][0]["content"][0]["source"]["data"], "AAA");
}
#[test]
fn cuerpo_incluye_sistema_herramientas_y_resultados() {
let request = Request {
model: "claude-sonnet-5-5".into(),
system: Some("Eres un editor".into()),
messages: vec![
Message::user("hola"),
Message::assistant(vec![Block::ToolUse { id: "t".into(), name: "ls".into(), input: json!({}) }]),
Message {
role: Role::User,
blocks: vec![Block::ToolResult { id: "t".into(), content: "a".into(), is_error: false }],
},
],
tools: vec![ToolSpec { name: "ls".into(), description: "lista".into(), schema: json!({"type": "object"}) }],
max_tokens: 100,
};
let body = body(&request);
assert_eq!(body["system"][0]["text"], "Eres un editor");
assert_eq!(body["system"][0]["cache_control"]["type"], "ephemeral");
assert_eq!(body["tools"][0]["cache_control"]["type"], "ephemeral");
assert_eq!(body["messages"][2]["content"][0]["cache_control"]["type"], "ephemeral");
assert!(body["messages"][0]["content"][0].get("cache_control").is_none());
assert_eq!(body["stream"], true);
assert_eq!(body["tools"][0]["input_schema"]["type"], "object");
assert_eq!(body["messages"][2]["content"][0]["tool_use_id"], "t");
assert_eq!(body["messages"][1]["role"], "assistant");
}