gproxy-protocol 4.1.14

GPROXY v4 connection modelling, operation declarations, vendor wire types and pairwise conversion
Documentation
use gproxy_protocol::openai::chat::GenerateContentRequestBody;
use gproxy_protocol::openai::chat::stream::{
    ChatCompletionChunk, ChunkObject, Delta, DeltaRole, DeltaToolCallType,
};

#[test]
fn chat_stream_real_chunk_sequence_preserves_partial_text_and_tool_calls() {
    let chunks = [
        serde_json::json!({"id":"chatcmpl-123","object":"chat.completion.chunk","created":1694268190,"model":"gpt-4o-mini","system_fingerprint":"fp","choices":[{"index":0,"delta":{"role":"assistant","content":""},"logprobs":null,"finish_reason":null}]}),
        serde_json::json!({"id":"chatcmpl-123","object":"chat.completion.chunk","created":1694268190,"model":"gpt-4o-mini","choices":[{"index":0,"delta":{"content":"Hello"},"logprobs":null,"finish_reason":null}]}),
        serde_json::json!({"id":"chatcmpl-123","object":"chat.completion.chunk","created":1694268190,"model":"gpt-4o-mini","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"id":"call_1","type":"function","function":{"name":"lookup","arguments":"{\"q\":"}}]},"logprobs":null,"finish_reason":null}],"future_chunk":true}),
        serde_json::json!({"id":"chatcmpl-123","object":"chat.completion.chunk","created":1694268190,"model":"gpt-4o-mini","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\"x\"}"}}]},"logprobs":null,"finish_reason":"tool_calls"}]}),
        serde_json::json!({"id":"chatcmpl-123","object":"chat.completion.chunk","created":1694268190,"model":"gpt-4o-mini","choices":[],"usage":{"prompt_tokens":5,"completion_tokens":3,"total_tokens":8}}),
    ];
    for value in &chunks {
        let parsed: ChatCompletionChunk = serde_json::from_value(value.clone()).unwrap();
        assert_eq!(serde_json::to_value(parsed).unwrap(), *value);
    }
    let first: ChatCompletionChunk = serde_json::from_value(chunks[0].clone()).unwrap();
    assert!(matches!(first.object, ChunkObject::ChatCompletionChunk));
    assert!(matches!(
        first.choices[0].delta.role,
        Some(Some(DeltaRole::Assistant))
    ));
    assert_eq!(first.choices[0].delta.content, Some(Some("".to_owned())));
    let tool: ChatCompletionChunk = serde_json::from_value(chunks[2].clone()).unwrap();
    let call = &tool.choices[0]
        .delta
        .tool_calls
        .as_ref()
        .unwrap()
        .as_ref()
        .unwrap()[0];
    assert_eq!(call.index, 0);
    assert!(matches!(
        call.type_,
        Some(Some(DeltaToolCallType::Function))
    ));
    assert_eq!(
        call.function
            .as_ref()
            .unwrap()
            .as_ref()
            .unwrap()
            .name
            .as_ref()
            .unwrap()
            .as_deref(),
        Some("lookup")
    );
    assert!(tool.rest.contains_key("future_chunk"));
    let final_chunk: ChatCompletionChunk = serde_json::from_value(chunks[4].clone()).unwrap();
    assert!(final_chunk.choices.is_empty());
    assert_eq!(final_chunk.usage.unwrap().unwrap().total_tokens, 8);
}

#[test]
fn chat_stream_request_keeps_stream_controls_in_normal_request_body() {
    let value = serde_json::json!({
        "messages":[{"role":"user","content":"hello"}],"model":"gpt-4o-mini",
        "stream":true,"stream_options":{"include_usage":true,"include_obfuscation":false},"future":1
    });
    let parsed: GenerateContentRequestBody = serde_json::from_value(value.clone()).unwrap();
    assert_eq!(parsed.stream, Some(Some(true)));
    assert!(parsed.stream_options.is_some());
    assert!(parsed.rest.contains_key("future"));
    assert_eq!(serde_json::to_value(parsed).unwrap(), value);
}

#[test]
fn chat_stream_nullable_fields_and_builder_are_typed() {
    let value = serde_json::json!({
        "id":"x","object":"chat.completion.chunk","created":1,"model":"m",
        "choices":[{"index":0,"delta":{"content":null,"refusal":"no","audio":{"data":"AA=="}},"logprobs":null,"finish_reason":null}],"usage":null
    });
    let parsed: ChatCompletionChunk = serde_json::from_value(value.clone()).unwrap();
    assert_eq!(parsed.choices[0].delta.content, Some(None));
    assert_eq!(parsed.choices[0].finish_reason, Some(None));
    assert_eq!(parsed.usage, Some(None));
    assert_eq!(serde_json::to_value(parsed).unwrap(), value);
    assert!(serde_json::from_value::<ChatCompletionChunk>(serde_json::json!({"id":"x","object":"chat.completion","created":1,"model":"m","choices":[]})).is_err());
    let delta = Delta::builder().build();
    assert_eq!(serde_json::to_value(delta).unwrap(), serde_json::json!({}));
}

#[test]
fn sdk_optional_fields_keep_missing_null_and_partial_values() {
    let value = serde_json::json!({
        "id":"c","object":"chat.completion.chunk","created":1,"model":"m",
        "choices":[{"index":0,"delta":{"role":"developer","content":"","refusal":"r","function_call":{"arguments":"{","name":"f"},"tool_calls":[{"index":0,"id":"t","type":"function","function":{"name":"f","arguments":"{"}}]},"finish_reason":"stop","logprobs":{"content":[{"token":"x","logprob":-1.0,"top_logprobs":[{"token":"y","logprob":-2.0,"bytes":null}]}],"refusal":null}}],
        "usage":{"prompt_tokens":1,"completion_tokens":2,"total_tokens":3,"prompt_tokens_details":{"audio_tokens":0,"cache_write_tokens":0,"cached_tokens":0,"image_tokens":0,"text_tokens":1},"completion_tokens_details":{"accepted_prediction_tokens":0,"rejected_prediction_tokens":0,"audio_tokens":0,"reasoning_tokens":0,"text_tokens":2}},
        "moderation":{"input":{"type":"error","code":"x","message":"x"},"output":{"type":"moderation_results","model":"m","results":[{"categories":{},"category_applied_input_types":{},"category_scores":{},"flagged":false,"model":"m","type":"moderation_result"}]}},"obfuscation":"x","service_tier":"priority","system_fingerprint":"f"
    });
    let parsed: ChatCompletionChunk = serde_json::from_value(value.clone()).unwrap();
    assert!(
        format!("{parsed:?}")
            .split("rest: ")
            .skip(1)
            .all(|rest| rest.starts_with("{}"))
    );
    assert_eq!(serde_json::to_value(parsed).unwrap(), value);
    for path in [
        "/moderation",
        "/obfuscation",
        "/service_tier",
        "/system_fingerprint",
        "/usage",
        "/choices/0/logprobs",
        "/choices/0/finish_reason",
        "/choices/0/delta/role",
        "/choices/0/delta/content",
        "/choices/0/delta/refusal",
        "/choices/0/delta/function_call",
        "/choices/0/delta/function_call/name",
        "/choices/0/delta/function_call/arguments",
        "/choices/0/delta/tool_calls",
        "/choices/0/delta/tool_calls/0/id",
        "/choices/0/delta/tool_calls/0/type",
        "/choices/0/delta/tool_calls/0/function",
        "/choices/0/delta/tool_calls/0/function/name",
        "/choices/0/delta/tool_calls/0/function/arguments",
        "/choices/0/logprobs/content",
        "/choices/0/logprobs/refusal",
        "/usage/completion_tokens_details",
        "/usage/prompt_tokens_details",
        "/usage/prompt_tokens_details/image_tokens",
        "/usage/completion_tokens_details/text_tokens",
    ] {
        let (parent, key) = path.rsplit_once('/').unwrap();
        let mut missing = value.clone();
        missing
            .pointer_mut(parent)
            .unwrap()
            .as_object_mut()
            .unwrap()
            .remove(key);
        assert_eq!(
            serde_json::to_value(
                serde_json::from_value::<ChatCompletionChunk>(missing.clone()).unwrap()
            )
            .unwrap(),
            missing,
            "missing {path}"
        );
        let mut null = value.clone();
        *null.pointer_mut(path).unwrap() = serde_json::Value::Null;
        assert_eq!(
            serde_json::to_value(
                serde_json::from_value::<ChatCompletionChunk>(null.clone()).unwrap()
            )
            .unwrap(),
            null,
            "null {path}"
        );
    }
}

#[test]
fn sdk_roles_function_only_fragments_and_required_fields_are_exact() {
    use gproxy_protocol::openai::chat::stream::{ChunkLogprobs, DeltaToolCall, StreamChoice};
    for role in ["developer", "system", "user", "assistant", "tool"] {
        let value = serde_json::json!({"role":role});
        assert_eq!(
            serde_json::to_value(serde_json::from_value::<Delta>(value.clone()).unwrap()).unwrap(),
            value
        );
    }
    assert!(serde_json::from_value::<Delta>(serde_json::json!({"role":"function"})).is_err());
    assert!(
        serde_json::from_value::<DeltaToolCall>(serde_json::json!({"index":0,"type":"custom"}))
            .is_err()
    );
    assert!(
        serde_json::from_value::<DeltaToolCall>(serde_json::json!({"function":{"arguments":"x"}}))
            .is_err()
    );
    assert!(serde_json::from_value::<StreamChoice>(serde_json::json!({"delta":{}})).is_err());
    for value in [
        serde_json::json!({}),
        serde_json::json!({"content":null}),
        serde_json::json!({"content":[{"token":"a","logprob":0.0,"top_logprobs":[]}],"refusal":null}),
    ] {
        assert_eq!(
            serde_json::to_value(serde_json::from_value::<ChunkLogprobs>(value.clone()).unwrap())
                .unwrap(),
            value
        );
    }
    assert!(serde_json::from_str::<ChatCompletionChunk>("[DONE]").is_err());
    let delta: Delta = serde_json::from_value(
        serde_json::json!({"audio":{"data":"extension"},"custom":{"input":"extension"}}),
    )
    .unwrap();
    assert_eq!(delta.rest.len(), 2);
}

#[test]
fn stream_http_response_keeps_raw_bytes_separate_from_typed_chunks() {
    use gproxy_protocol::openai::chat::stream::{StreamChoice, StreamRequest, StreamResponse};
    use gproxy_protocol::{WireRequest, WireResponse, connection::ByteStream};
    struct Empty;
    impl futures_core::Stream for Empty {
        type Item = Result<bytes::Bytes, gproxy_protocol::connection::TransportError>;
        fn poll_next(
            self: std::pin::Pin<&mut Self>,
            _: &mut std::task::Context<'_>,
        ) -> std::task::Poll<Option<Self::Item>> {
            std::task::Poll::Ready(None)
        }
    }
    let request: StreamRequest = WireRequest {
        method: http::Method::POST,
        path: "/v1/chat/completions".into(),
        query: None,
        headers: http::HeaderMap::new(),
        body: GenerateContentRequestBody::builder(vec![], "m".into())
            .stream(Some(true))
            .build(),
    };
    assert_eq!(request.method, http::Method::POST);
    assert_eq!(request.path, "/v1/chat/completions");
    assert!(request.query.is_none());
    assert!(request.headers.is_empty());
    assert_eq!(
        serde_json::to_value(request.body).unwrap(),
        serde_json::json!({"messages":[],"model":"m","stream":true})
    );
    let raw: ByteStream = Box::pin(Empty);
    let mut headers = http::HeaderMap::new();
    headers.insert("content-type", "text/event-stream".parse().unwrap());
    let response: StreamResponse = WireResponse {
        status: http::StatusCode::OK,
        headers: headers.clone(),
        body: raw,
    };
    assert_eq!(response.status, http::StatusCode::OK);
    assert_eq!(response.headers, headers);
    let chunk = ChatCompletionChunk::builder(
        "c".into(),
        vec![StreamChoice::builder(0, Delta::builder().build()).build()],
        1,
        "m".into(),
        ChunkObject::ChatCompletionChunk,
    )
    .build();
    assert_eq!(
        serde_json::to_value(chunk).unwrap(),
        serde_json::json!({"id":"c","choices":[{"index":0,"delta":{}}],"created":1,"model":"m","object":"chat.completion.chunk"})
    );
}