#[path = "support/client_tools.rs"]
mod client_tools_base;
#[path = "adapt_generate_stream/host.rs"]
mod host;
use gproxy_protocol::{
Dialect,
adapt::generate::{
chat_claude::ChatViaClaude,
stream::{event::EventLimits, reader::SourceFraming, *},
*,
},
codec::CodecLimits,
transform::{
TransformErrorKind,
generate::claude_chat::stream::ClaudeToChatContext,
identity::{IdNamespace, IdSyntax, IdentityTarget},
},
wire::openai::chat as h,
};
use host::*;
use serde_json::{Value, json};
use std::{
future::Future,
sync::{Arc, atomic::Ordering},
task::{Context, Waker},
time::{Duration, UNIX_EPOCH},
};
fn state(store: &Store) -> GenerationStateAccess<'_, Store> {
GenerationStateAccess {
store,
scope: &(),
target: IdentityTarget::new("selected", Dialect::Claude)
.unwrap()
.with_origin("origin")
.unwrap(),
conversation_key: "conversation".into(),
now: UNIX_EPOCH,
expires_at: UNIX_EPOCH + Duration::from_secs(1000),
}
}
fn selected() -> StreamTarget {
let mut identities = GenerationIdentity::new(
IdNamespace([61; 16]),
IdNamespace([62; 16]),
Dialect::OpenAiChat,
Dialect::Claude,
)
.unwrap();
identities.response_policy = identities
.response_policy
.with_syntax(IdSyntax::AsciiIdentifier);
StreamTarget {
endpoint: Endpoint::new("/messages").unwrap(),
identities,
}
}
fn settings() -> StreamSettings {
StreamSettings {
codec: CodecLimits {
max_buffer_bytes: 65536,
max_value_bytes: 65536,
max_body_bytes: 262144,
max_line_bytes: 65536,
max_part_bytes: 65536,
max_parts: 64,
},
events: EventLimits {
max_bytes: 262144,
max_pending_bytes: 65536,
},
source_framing: SourceFraming::Sse,
client_framing: SourceFraming::Sse,
}
}
fn request(include_usage: bool) -> h::GenerateContentRequestBody {
serde_json::from_value(json!({"model":"client-alias","stream":true,"stream_options":{"include_usage":include_usage},"max_completion_tokens":64,"messages":[{"role":"user","content":"hi","nested_sentinel":"DROP"}],"top_sentinel":"DROP"})).unwrap()
}
fn event(feed: &Feed, value: Value) {
feed.push(format!(
"event: {}\ndata: {}\n\n",
value["type"].as_str().unwrap(),
value
));
}
fn prefix(feed: &Feed) {
event(
feed,
json!({"type":"message_start","message":{"type":"message","id":"message:original","model":"selected","role":"assistant","content":[],"usage":{"input_tokens":3,"output_tokens":0,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens_details":{"thinking_tokens":0}},"nested_sentinel":"DROP"},"top_sentinel":"DROP"}),
);
event(
feed,
json!({"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}),
);
event(
feed,
json!({"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"hello"}}),
);
event(feed, json!({"type":"content_block_stop","index":0}));
}
fn terminal(feed: &Feed) {
event(
feed,
json!({"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":2,"output_tokens_details":{"thinking_tokens":0}}}),
);
event(feed, json!({"type":"message_stop"}));
feed.close();
}
fn prepared(
store: &Store,
usage: bool,
) -> StreamInvocation<gproxy_protocol::transform::generate::claude_chat::stream::ClaudeToChatStream>
{
ready(ChatViaClaude::prepare_stream(
request(usage),
selected(),
ClaudeToChatContext { created: 7 },
settings(),
&state(store),
))
.unwrap()
}
#[test]
fn actual_post_streams_before_eof_without_any_state_io() {
let store = Arc::new(Store::default());
let feed = Feed::default();
prefix(&feed);
let host = Host::stream(store.clone(), feed.clone());
let access = state(&store);
let mut call = prepared(&store, true);
assert!(matches!(
ready(call.start(&host, &(), &access)).unwrap(),
StreamStart::Streaming(_)
));
let sent: Value = serde_json::from_slice(&host.sent.lock().unwrap()[0].body).unwrap();
assert_eq!(sent["stream"], true);
assert_eq!(sent["model"], "selected");
assert!(!sent.to_string().contains("sentinel"));
let mut bytes = Vec::new();
let mut saw_text = false;
for _ in 0..5 {
let chunk = ready(call.next(&access)).unwrap().unwrap();
if let Some(event) = &chunk.event {
assert_ne!(event.id, "message:original");
}
assert_eq!(store.gets.load(Ordering::SeqCst), 0);
assert_eq!(store.serial.load(Ordering::SeqCst), 0);
bytes.extend_from_slice(&chunk.bytes);
if String::from_utf8_lossy(&bytes).contains("hello") {
saw_text = true;
break;
}
}
assert!(saw_text);
assert!(call.client_result().is_none());
terminal(&feed);
while let Some(chunk) = ready(call.next(&access)).unwrap() {
bytes.extend_from_slice(&chunk.bytes);
}
assert!(bytes.ends_with(b"data: [DONE]\n\n"));
assert_eq!(store.gets.load(Ordering::SeqCst), 0);
assert!(store.entries.lock().unwrap().is_empty());
assert_eq!(
call.client_result()
.unwrap()
.usage
.as_ref()
.unwrap()
.total_tokens,
5
);
assert!(!String::from_utf8_lossy(&bytes).contains("sentinel"));
assert_eq!(host.sent.lock().unwrap().len(), 1);
assert_eq!(
ready(call.start(&host, &(), &access)).unwrap_err().kind(),
TransformErrorKind::Conflict
);
}
#[test]
fn repeated_upstream_ids_across_streams_in_one_conversation_both_succeed() {
let store = Arc::new(Store::default());
let access = state(&store);
for turn in 0..2 {
let feed = Feed::default();
event(
&feed,
json!({"type":"message_start","message":{"type":"message","id":"msg_mock","model":"selected","role":"assistant","content":[],"usage":{"input_tokens":3,"output_tokens":0,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens_details":{"thinking_tokens":0}}}}),
);
for (index, id) in ["toolu_mock", "toolu.mock"].into_iter().enumerate() {
event(
&feed,
json!({"type":"content_block_start","index":index,"content_block":{"type":"tool_use","id":id,"name":"f","input":{}}}),
);
event(&feed, json!({"type":"content_block_stop","index":index}));
}
event(
&feed,
json!({"type":"message_delta","delta":{"stop_reason":"tool_use","stop_sequence":null},"usage":{"output_tokens":2,"output_tokens_details":{"thinking_tokens":0}}}),
);
event(&feed, json!({"type":"message_stop"}));
feed.close();
let host = Host::stream(store.clone(), feed);
let mut call = prepared(&store, true);
ready(call.start(&host, &(), &access)).unwrap();
let GenerationOutcome::Success { response, .. } = ready(call.collect(&access)).unwrap()
else {
panic!("turn {turn} rejected")
};
let calls = response.body.choices[0]
.message
.tool_calls
.clone()
.unwrap()
.into_iter()
.map(|call| serde_json::to_value(call).unwrap()["id"].clone())
.collect::<Vec<_>>();
assert_eq!(response.body.id, "msg_mock");
assert_eq!(calls[0], "toolu_mock");
assert_eq!(calls[1], "call_gpe_toolu_2emock");
assert!(store.entries.lock().unwrap().is_empty());
assert_eq!(store.gets.load(Ordering::SeqCst), 0);
}
}
#[test]
fn client_usage_flag_is_honored_and_incomplete_source_never_emits_terminal_success() {
for complete in [true, false] {
let store = Arc::new(Store::default());
let feed = Feed::default();
prefix(&feed);
if complete {
terminal(&feed);
} else {
feed.close();
}
let host = Host::stream(store.clone(), feed);
let access = state(&store);
let mut call = prepared(&store, false);
ready(call.start(&host, &(), &access)).unwrap();
let mut output = Vec::new();
let mut failed = false;
loop {
match ready(call.next(&access)) {
Ok(Some(chunk)) => output.extend_from_slice(&chunk.bytes),
Ok(None) => break,
Err(_) => {
failed = true;
break;
}
}
}
assert_eq!(failed, !complete);
let text = String::from_utf8(output).unwrap();
assert_eq!(text.contains("[DONE]"), complete);
if complete {
assert!(call.client_result().unwrap().usage.is_none());
} else {
assert!(!text.contains("\"finish_reason\":\"stop\""));
}
}
}
#[path = "adapt_generate_stream/all_pairs.rs"]
mod all_pairs;
#[path = "adapt_generate_stream/gemini_thinking.rs"]
mod gemini_thinking;
#[path = "adapt_generate_stream/framing.rs"]
mod framing;
#[path = "adapt_generate_stream/parity.rs"]
mod parity;
#[path = "adapt_generate_stream/synthesis.rs"]
mod synthesis;
#[path = "adapt_generate_stream/websocket.rs"]
mod websocket;
#[path = "adapt_generate_stream/fanout.rs"]
mod fanout;
#[path = "adapt_generate_stream/instructions.rs"]
mod instructions;
#[path = "adapt_generate_stream/tool_reference.rs"]
mod tool_reference;
#[path = "adapt_generate_stream/custom_tools.rs"]
mod custom_tools;
#[path = "adapt_generate_stream/client_tools.rs"]
mod client_tools;
#[path = "adapt_generate_stream/client_tools_backends.rs"]
mod client_tools_backends;
#[path = "adapt_generate_stream/responses_history.rs"]
mod responses_history;