codex-helper-core 0.17.0

Core library for codex-helper.
Documentation
use super::*;
use crate::proxy::tests::harness::{
    post_images_generations_json, spawn_test_proxy, spawn_test_upstream,
};
use std::sync::{Arc, Mutex};

#[tokio::test]
async fn openai_images_generation_endpoint_translates_request_and_response() {
    let seen_body = Arc::new(Mutex::new(None::<serde_json::Value>));
    let seen_body_for_route = seen_body.clone();
    let upstream = axum::Router::new().route(
        "/v1/responses",
        post(move |Json(body): Json<serde_json::Value>| {
            let seen_body_for_route = seen_body_for_route.clone();
            async move {
                *seen_body_for_route.lock().expect("lock") = Some(body);
                (
                    StatusCode::OK,
                    Json(serde_json::json!({
                        "id": "resp_image",
                        "created": 42,
                        "output": [
                            {
                                "type": "image_generation_call",
                                "id": "ig_1",
                                "status": "completed",
                                "result": "Zm9v",
                                "revised_prompt": "A neon rain cat"
                            }
                        ]
                    })),
                )
            }
        }),
    );
    let upstream = spawn_test_upstream(upstream);
    let cfg = make_proxy_config(vec![upstream.upstream_config()], RetryConfig::default());
    let proxy = spawn_test_proxy(cfg);
    let client = Client::new();

    let response = post_images_generations_json(
        &client,
        &proxy,
        r#"{"model":"gpt-image-2","prompt":"一只猫在雨夜的霓虹灯下","size":"3840x2160","output_format":"png","quality":"high"}"#,
    )
    .await
    .error_for_status()
    .expect("images status")
    .json::<serde_json::Value>()
    .await
    .expect("images json");

    assert_eq!(response["created"], 42);
    assert_eq!(response["data"][0]["b64_json"], "Zm9v");
    assert_eq!(response["data"][0]["revised_prompt"], "A neon rain cat");

    let seen = seen_body
        .lock()
        .expect("lock")
        .clone()
        .expect("upstream body");
    assert_eq!(seen["model"], "gpt-image-2");
    assert_eq!(seen["input"], "一只猫在雨夜的霓虹灯下");
    assert_eq!(seen["tools"][0]["type"], "image_generation");
    assert_eq!(seen["tools"][0]["size"], "3840x2160");
    assert_eq!(seen["tools"][0]["output_format"], "png");
    assert_eq!(seen["tools"][0]["quality"], "high");
}

#[tokio::test]
async fn openai_images_generation_endpoint_strips_client_user_agent_before_upstream() {
    let seen_user_agent = Arc::new(Mutex::new(None::<Option<String>>));
    let seen_user_agent_for_route = seen_user_agent.clone();
    let upstream = axum::Router::new().route(
        "/v1/responses",
        post(move |headers: HeaderMap| {
            let seen_user_agent_for_route = seen_user_agent_for_route.clone();
            async move {
                let seen = headers
                    .get(axum::http::header::USER_AGENT)
                    .and_then(|value| value.to_str().ok())
                    .map(|value| value.to_string());
                *seen_user_agent_for_route.lock().expect("lock") = Some(seen);
                (
                    StatusCode::OK,
                    Json(serde_json::json!({
                        "id": "resp_image",
                        "created": 42,
                        "output": [
                            {
                                "type": "image_generation_call",
                                "id": "ig_1",
                                "status": "completed",
                                "result": "Zm9v",
                                "revised_prompt": "A neon rain cat"
                            }
                        ]
                    })),
                )
            }
        }),
    );
    let upstream = spawn_test_upstream(upstream);
    let cfg = make_proxy_config(vec![upstream.upstream_config()], RetryConfig::default());
    let proxy = spawn_test_proxy(cfg);
    let client = Client::new();

    let response = client
        .post(proxy.images_generations_url())
        .header("content-type", "application/json")
        .header(axum::http::header::USER_AGENT, "Python-urllib/3.13")
        .body(r#"{"model":"gpt-image-2","prompt":"cat"}"#)
        .send()
        .await
        .expect("send images request");

    let response = response.error_for_status().expect("images status");
    response
        .json::<serde_json::Value>()
        .await
        .expect("images json");

    let seen = seen_user_agent
        .lock()
        .expect("lock")
        .clone()
        .expect("upstream user-agent");
    assert_ne!(seen.as_deref(), Some("Python-urllib/3.13"));
}

#[tokio::test]
async fn openai_images_generation_endpoint_rejects_n_greater_than_one_before_upstream() {
    let hit_count = Arc::new(std::sync::atomic::AtomicUsize::new(0));
    let hit_count_for_route = hit_count.clone();
    let upstream = axum::Router::new().route(
        "/v1/responses",
        post(move || {
            let hit_count_for_route = hit_count_for_route.clone();
            async move {
                hit_count_for_route.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
                (
                    StatusCode::OK,
                    Json(serde_json::json!({"id": "resp_unused", "output": []})),
                )
            }
        }),
    );
    let upstream = spawn_test_upstream(upstream);
    let cfg = make_proxy_config(vec![upstream.upstream_config()], RetryConfig::default());
    let proxy = spawn_test_proxy(cfg);
    let client = Client::new();

    let response = post_images_generations_json(
        &client,
        &proxy,
        r#"{"model":"gpt-image-2","prompt":"cat","n":2}"#,
    )
    .await;
    let status = response.status();
    let text = response.text().await.expect("body");

    assert_eq!(status, StatusCode::BAD_REQUEST);
    assert!(text.contains("n=1"));
    assert_eq!(hit_count.load(std::sync::atomic::Ordering::SeqCst), 0);
}

#[tokio::test]
async fn openai_images_generation_endpoint_passes_upstream_errors_through() {
    let upstream = axum::Router::new().route(
        "/v1/responses",
        post(|| async {
            (
                StatusCode::BAD_REQUEST,
                Json(serde_json::json!({
                    "error": {
                        "message": "image generation is not enabled"
                    }
                })),
            )
        }),
    );
    let upstream = spawn_test_upstream(upstream);
    let cfg = make_proxy_config(vec![upstream.upstream_config()], RetryConfig::default());
    let proxy = spawn_test_proxy(cfg);
    let client = Client::new();

    let response =
        post_images_generations_json(&client, &proxy, r#"{"model":"gpt-image-2","prompt":"cat"}"#)
            .await;
    let status = response.status();
    let body = response.json::<serde_json::Value>().await.expect("json");

    assert_eq!(status, StatusCode::BAD_REQUEST);
    assert_eq!(body["error"]["message"], "image generation is not enabled");
}

#[tokio::test]
async fn openai_images_generation_endpoint_reports_missing_image_result() {
    let upstream = axum::Router::new().route(
        "/v1/responses",
        post(|| async {
            (
                StatusCode::OK,
                Json(serde_json::json!({
                    "id": "resp_no_image",
                    "output": [
                        {"type": "message", "content": []}
                    ]
                })),
            )
        }),
    );
    let upstream = spawn_test_upstream(upstream);
    let cfg = make_proxy_config(vec![upstream.upstream_config()], RetryConfig::default());
    let proxy = spawn_test_proxy(cfg);
    let client = Client::new();

    let response =
        post_images_generations_json(&client, &proxy, r#"{"model":"gpt-image-2","prompt":"cat"}"#)
            .await;
    let status = response.status();
    let text = response.text().await.expect("body");

    assert_eq!(status, StatusCode::BAD_GATEWAY);
    assert!(text.contains("image_generation_call"));
}