- 1
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] - 2
- 3
use tokio_util::sync::CancellationToken; - 4
- 5
use vak_llm::Provider; - 6
use vak_llm::error::LlmError; - 7
use vak_llm::openai_responses::{OpenAiResponsesConfig, OpenAiResponsesProvider, build_body}; - 8
use vak_llm::stream::StreamEvent; - 9
use vak_llm::types::{ChatRequest, ContentBlock, Message, Role, StopReason, ToolDefinition}; - 10
- 11
fn sample_request() -> ChatRequest { - 12
let mut req = ChatRequest::new("gpt-5.6"); - 13
req.system = Some("You are a coding agent.".into()); - 14
req.messages = vec![ - 15
Message::user_text("list the files"), - 16
Message::assistant(vec![ - 17
ContentBlock::text("Checking."), - 18
ContentBlock::ToolUse { - 19
id: "call_abc".into(), - 20
name: "bash".into(), - 21
input: serde_json::json!({"command": "ls"}), - 22
}, - 23
]), - 24
Message { - 25
role: Role::User, - 26
content: vec![ContentBlock::tool_result("call_abc", "src\nREADME.md")], - 27
}, - 28
]; - 29
req.tools = vec![ToolDefinition::new( - 30
"bash", - 31
"Run a shell command", - 32
serde_json::json!({"type": "object", "properties": {"command": {"type": "string"}}, "required": ["command"]}), - 33
)]; - 34
req - 35
} - 36
- 37
#[test] - 38
fn body_maps_neutral_history_to_responses_shape() { - 39
let body = build_body(&OpenAiResponsesConfig::default(), &sample_request()).unwrap(); - 40
assert_eq!(body["model"], "gpt-5.6"); - 41
assert_eq!(body["stream"], true); - 42
assert_eq!(body["instructions"], "You are a coding agent."); - 43
- 44
let input = body["input"].as_array().unwrap(); - 45
// user msg, function_call item, assistant text item, function_call_output - 46
assert_eq!(input.len(), 4); - 47
assert_eq!(input[0]["role"], "user"); - 48
assert_eq!(input[0]["content"][0]["type"], "input_text"); - 49
- 50
assert_eq!(input[1]["type"], "function_call"); - 51
assert_eq!( - 52
input[1]["call_id"], "call_abc", - 53
"ids survive provider switches" - 54
); - 55
assert_eq!(input[1]["name"], "bash"); - 56
let args = input[1]["arguments"].as_str().unwrap(); - 57
assert!(serde_json::from_str::<serde_json::Value>(args).is_ok()); - 58
- 59
assert_eq!(input[2]["role"], "assistant"); - 60
assert_eq!(input[2]["content"][0]["type"], "output_text"); - 61
- 62
assert_eq!(input[3]["type"], "function_call_output"); - 63
assert_eq!(input[3]["call_id"], "call_abc"); - 64
- 65
let tools = body["tools"].as_array().unwrap(); - 66
assert_eq!(tools[0]["type"], "function"); - 67
assert_eq!(tools[0]["name"], "bash"); - 68
} - 69
- 70
const FIXTURE_STREAM: &str = "\ - 71
event: response.output_text.delta\n\ - 72
data: {\"type\":\"response.output_text.delta\",\"delta\":\"Read\"}\n\ - 73
\n\ - 74
event: response.output_text.delta\n\ - 75
data: {\"type\":\"response.output_text.delta\",\"delta\":\"ing now.\"}\n\ - 76
\n\ - 77
event: response.output_item.added\n\ - 78
data: {\"type\":\"response.output_item.added\",\"item\":{\"type\":\"function_call\",\"id\":\"fc_1\",\"call_id\":\"call_9\",\"name\":\"read\",\"arguments\":\"\"}}\n\ - 79
\n\ - 80
event: response.function_call_arguments.delta\n\ - 81
data: {\"type\":\"response.function_call_arguments.delta\",\"item_id\":\"fc_1\",\"delta\":\"{\\\"path\\\":\"}\n\ - 82
\n\ - 83
event: response.function_call_arguments.delta\n\ - 84
data: {\"type\":\"response.function_call_arguments.delta\",\"item_id\":\"fc_1\",\"delta\":\"\\\"a.txt\\\"}\"}\n\ - 85
\n\ - 86
event: response.completed\n\ - 87
data: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_123\",\"usage\":{\"input_tokens\":33,\"output_tokens\":9,\"input_tokens_details\":{\"cached_tokens\":4}}}}\n\ - 88
\n"; - 89
- 90
#[tokio::test] - 91
async fn full_stream_accumulates_text_and_tool_calls() { - 92
let provider = OpenAiResponsesProvider::new(OpenAiResponsesConfig { - 93
api_key: "k".into(), - 94
base_url: mock_url(FIXTURE_STREAM).await, - 95
..Default::default() - 96
}) - 97
.unwrap(); - 98
- 99
let mut es = provider - 100
.stream(sample_request(), CancellationToken::new()) - 101
.await - 102
.unwrap(); - 103
let mut text = String::new(); - 104
while let Some(ev) = futures::StreamExt::next(&mut es).await { - 105
if let StreamEvent::TextDelta { delta, .. } = ev { - 106
text.push_str(&delta); - 107
} - 108
} - 109
let msg = es.result().await.unwrap(); - 110
- 111
assert_eq!(text, "Reading now."); - 112
// A function_call item means the model wants tools — the loop must - 113
// continue even though response.completed arrived. - 114
assert_eq!(msg.stop_reason, StopReason::ToolUse); - 115
// The fixture's `input_tokens: 33` already includes the 4 cached - 116
// tokens (Responses API semantics), so the normalized, non-cached - 117
// `input_tokens` is 29 — the split, not the raw total. - 118
assert_eq!(msg.usage.input_tokens, 29); - 119
assert_eq!(msg.usage.output_tokens, 9); - 120
assert_eq!(msg.usage.cache_read_input_tokens, Some(4)); - 121
// The provider's original total is reconstructible from the split. - 122
assert_eq!(msg.usage.prompt_tokens(), 33); - 123
assert_eq!(msg.response_id, Some("resp_123".to_string())); - 124
- 125
let calls: Vec<&ContentBlock> = msg - 126
.content - 127
.iter() - 128
.filter(|b| matches!(b, ContentBlock::ToolUse { .. })) - 129
.collect(); - 130
assert_eq!(calls.len(), 1); - 131
if let ContentBlock::ToolUse { id, name, input } = calls[0] { - 132
assert_eq!(id, "call_9"); - 133
assert_eq!(name, "read"); - 134
assert_eq!(input, &serde_json::json!({"path": "a.txt"})); - 135
} else { - 136
unreachable!() - 137
} - 138
} - 139
- 140
const FIXTURE_FULL_CACHE_HIT: &str = "\ - 141
event: response.completed\n\ - 142
data: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_456\",\"usage\":{\"input_tokens\":12000,\"output_tokens\":50,\"input_tokens_details\":{\"cached_tokens\":12000}}}}\n\ - 143
\n"; - 144
- 145
#[tokio::test] - 146
async fn a_full_cache_hit_reports_zero_fresh_input_tokens_not_the_raw_total() { - 147
let provider = OpenAiResponsesProvider::new(OpenAiResponsesConfig { - 148
api_key: "k".into(), - 149
base_url: mock_url(FIXTURE_FULL_CACHE_HIT).await, - 150
..Default::default() - 151
}) - 152
.unwrap(); - 153
let mut req = ChatRequest::new("m"); - 154
req.messages = vec![Message::user_text("hi")]; - 155
let mut es = provider - 156
.stream(req, CancellationToken::new()) - 157
.await - 158
.unwrap(); - 159
while futures::StreamExt::next(&mut es).await.is_some() {} - 160
let msg = es.result().await.unwrap(); - 161
assert_eq!(msg.usage.input_tokens, 0); - 162
assert_eq!(msg.usage.cache_read_input_tokens, Some(12_000)); - 163
assert_eq!(msg.usage.prompt_tokens(), 12_000); - 164
} - 165
- 166
const FIXTURE_INCOMPLETE: &str = "\ - 167
data: {\"type\":\"response.incomplete\",\"response\":{\"usage\":{\"input_tokens\":1,\"output_tokens\":1}}}\n\ - 168
\n"; - 169
- 170
#[tokio::test] - 171
async fn incomplete_maps_to_max_tokens() { - 172
let provider = OpenAiResponsesProvider::new(OpenAiResponsesConfig { - 173
api_key: "k".into(), - 174
base_url: mock_url(FIXTURE_INCOMPLETE).await, - 175
..Default::default() - 176
}) - 177
.unwrap(); - 178
let mut req = ChatRequest::new("m"); - 179
req.messages = vec![Message::user_text("hi")]; - 180
let mut es = provider - 181
.stream(req, CancellationToken::new()) - 182
.await - 183
.unwrap(); - 184
while futures::StreamExt::next(&mut es).await.is_some() {} - 185
let msg = es.result().await.unwrap(); - 186
assert_eq!(msg.stop_reason, StopReason::MaxTokens); - 187
} - 188
- 189
#[tokio::test] - 190
async fn http_error_maps_to_typed_value() { - 191
let url = error_url(401, r#"{"error":{"message":"bad key"}}"#).await; - 192
let provider = OpenAiResponsesProvider::new(OpenAiResponsesConfig { - 193
api_key: "bad".into(), - 194
base_url: url, - 195
..Default::default() - 196
}) - 197
.unwrap(); - 198
let err = provider - 199
.stream(ChatRequest::new("m"), CancellationToken::new()) - 200
.await - 201
.err() - 202
.expect("expected error"); - 203
assert!(matches!(err, LlmError::Auth(_)), "got {err:?}"); - 204
} - 205
- 206
async fn spawn_server(body: Vec<u8>) -> String { - 207
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); - 208
let addr = listener.local_addr().unwrap(); - 209
tokio::spawn(async move { - 210
let (mut sock, _) = listener.accept().await.unwrap(); - 211
drain_headers(&mut sock).await; - 212
use tokio::io::AsyncWriteExt; - 213
let _ = sock.write_all(&body).await; - 214
let _ = sock.flush().await; - 215
let _ = sock.shutdown().await; - 216
}); - 217
format!("http://{addr}") - 218
} - 219
- 220
async fn drain_headers<S>(sock: &mut S) - 221
where - 222
S: tokio::io::AsyncRead + Unpin, - 223
{ - 224
use tokio::io::AsyncReadExt; - 225
let mut buf = Vec::new(); - 226
let mut chunk = [0u8; 1024]; - 227
loop { - 228
let n = sock.read(&mut chunk).await.unwrap_or(0); - 229
if n == 0 { - 230
return; - 231
} - 232
buf.extend_from_slice(&chunk[..n]); - 233
if buf.windows(4).any(|w| w == b"\r\n\r\n") { - 234
return; - 235
} - 236
} - 237
} - 238
- 239
async fn mock_url(sse_body: &str) -> String { - 240
let owned = format!( - 241
"HTTP/1.1 200 OK\r\ncontent-type: text/event-stream\r\nconnection: close\r\n\r\n{sse_body}" - 242
); - 243
spawn_server(owned.into_bytes()).await - 244
} - 245
- 246
async fn error_url(status: u16, json: &str) -> String { - 247
let owned = format!( - 248
"HTTP/1.1 {status} Err\r\ncontent-type: application/json\r\nconnection: close\r\n\r\n{json}" - 249
); - 250
spawn_server(owned.into_bytes()).await - 251
} - 252
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.