codex-helper-core 0.20.1

Core library for codex-helper.
Documentation
use axum::http::{HeaderMap, Method, StatusCode};
use serde_json::{Value, json};

use super::{
    CodexRelayLiveSmokeCase, CodexRelayLiveSmokeResult, classify_compact_live_smoke_response,
    classify_image_live_smoke_response, classify_remote_compaction_v2_live_smoke_response,
};

pub(super) const RESPONSES_WS_BETA_HEADER: &str = "responses_websockets=2026-02-06";
pub(super) const REMOTE_COMPACTION_V2_BETA_FEATURE: &str = "remote_compaction_v2";

#[derive(Debug, Clone, Copy)]
pub(super) struct CodexRelayLiveSmokeCaseDescriptor {
    pub(super) case: CodexRelayLiveSmokeCase,
    pub(super) default_enabled: bool,
    pub(super) acknowledgement_required: bool,
    pub(super) explicit_only_warning: Option<&'static str>,
    pub(super) executor: LiveSmokeExecutor,
}

#[derive(Debug, Clone, Copy)]
pub(super) enum LiveSmokeExecutor {
    Http(LiveSmokeHttpDescriptor),
    WebSocket(LiveSmokeWebSocketDescriptor),
}

#[derive(Debug, Clone, Copy)]
pub(super) struct LiveSmokeHttpDescriptor {
    method: &'static str,
    path: &'static str,
    headers: &'static [(&'static str, &'static str)],
    stream: bool,
    timeout_secs: u64,
    body: fn(&str, Option<&str>) -> Value,
    classify: fn(StatusCode, &HeaderMap, &[u8]) -> CodexRelayLiveSmokeResult,
}

#[derive(Debug, Clone, Copy)]
pub(super) struct LiveSmokeWebSocketDescriptor {
    pub(super) path: &'static str,
    pub(super) beta_header: &'static str,
    pub(super) handshake_timeout_secs: u64,
    pub(super) read_timeout_secs: u64,
    pub(super) body: fn(&str, Option<&str>) -> Value,
}

const CODEX_RELAY_LIVE_SMOKE_CASES: &[CodexRelayLiveSmokeCaseDescriptor] = &[
    CodexRelayLiveSmokeCaseDescriptor {
        case: CodexRelayLiveSmokeCase::ResponsesCompact,
        default_enabled: true,
        acknowledgement_required: true,
        explicit_only_warning: None,
        executor: LiveSmokeExecutor::Http(LiveSmokeHttpDescriptor {
            method: "POST",
            path: "/responses/compact",
            headers: &[],
            stream: false,
            timeout_secs: 30,
            body: compact_live_smoke_body,
            classify: classify_compact_live_smoke_response,
        }),
    },
    CodexRelayLiveSmokeCaseDescriptor {
        case: CodexRelayLiveSmokeCase::RemoteCompactionV2,
        default_enabled: false,
        acknowledgement_required: true,
        explicit_only_warning: Some(
            "remote compaction v2 was not tested because compact v2 smoke is explicit-only",
        ),
        executor: LiveSmokeExecutor::Http(LiveSmokeHttpDescriptor {
            method: "POST",
            path: "/responses",
            headers: &[("x-codex-beta-features", REMOTE_COMPACTION_V2_BETA_FEATURE)],
            stream: true,
            timeout_secs: 60,
            body: remote_compaction_v2_live_smoke_body,
            classify: classify_remote_compaction_v2_live_smoke_response,
        }),
    },
    CodexRelayLiveSmokeCaseDescriptor {
        case: CodexRelayLiveSmokeCase::HostedImageGeneration,
        default_enabled: false,
        acknowledgement_required: true,
        explicit_only_warning: Some(
            "hosted image generation was not tested because image smoke is explicit-only",
        ),
        executor: LiveSmokeExecutor::Http(LiveSmokeHttpDescriptor {
            method: "POST",
            path: "/responses",
            headers: &[],
            stream: true,
            timeout_secs: 60,
            body: image_generation_live_smoke_body,
            classify: classify_image_live_smoke_response,
        }),
    },
    CodexRelayLiveSmokeCaseDescriptor {
        case: CodexRelayLiveSmokeCase::ResponsesWebSocket,
        default_enabled: false,
        acknowledgement_required: true,
        explicit_only_warning: Some(
            "Responses WebSocket was not tested because websocket smoke is explicit-only",
        ),
        executor: LiveSmokeExecutor::WebSocket(LiveSmokeWebSocketDescriptor {
            path: "/responses",
            beta_header: RESPONSES_WS_BETA_HEADER,
            handshake_timeout_secs: 30,
            read_timeout_secs: 60,
            body: responses_websocket_live_smoke_body,
        }),
    },
];

pub(super) fn codex_relay_live_smoke_cases() -> &'static [CodexRelayLiveSmokeCaseDescriptor] {
    CODEX_RELAY_LIVE_SMOKE_CASES
}

pub(super) fn live_smoke_case_descriptor(
    case: CodexRelayLiveSmokeCase,
) -> &'static CodexRelayLiveSmokeCaseDescriptor {
    codex_relay_live_smoke_cases()
        .iter()
        .find(|descriptor| descriptor.case == case)
        .expect("Codex relay live smoke case must be registered")
}

pub(super) struct LiveSmokeSpec {
    pub(super) method: Method,
    pub(super) path: &'static str,
    pub(super) headers: &'static [(&'static str, &'static str)],
    pub(super) body: Value,
    pub(super) stream: bool,
    pub(super) timeout: std::time::Duration,
    pub(super) classify: fn(StatusCode, &HeaderMap, &[u8]) -> CodexRelayLiveSmokeResult,
}

impl LiveSmokeSpec {
    pub(super) fn for_case(
        case: CodexRelayLiveSmokeCase,
        model: &str,
        service_tier: Option<&str>,
    ) -> Option<Self> {
        let descriptor = live_smoke_case_descriptor(case);
        let LiveSmokeExecutor::Http(http) = descriptor.executor else {
            return None;
        };
        Some(Self {
            method: Method::from_bytes(http.method.as_bytes()).unwrap_or(Method::POST),
            path: http.path,
            headers: http.headers,
            body: (http.body)(model, service_tier),
            stream: http.stream,
            timeout: std::time::Duration::from_secs(http.timeout_secs),
            classify: http.classify,
        })
    }
}

fn compact_live_smoke_body(model: &str, service_tier: Option<&str>) -> Value {
    let mut body = json!({
        "model": model,
        "input": [
            {
                "type": "message",
                "role": "user",
                "content": [
                    {
                        "type": "input_text",
                        "text": "Codex relay live smoke: please compact this short diagnostic conversation."
                    }
                ]
            },
            {
                "type": "message",
                "role": "assistant",
                "content": [
                    {
                        "type": "output_text",
                        "text": "Diagnostic reply for live smoke."
                    }
                ]
            }
        ],
        "instructions": "Return a compacted Codex conversation history for this diagnostic request.",
        "tools": [],
        "parallel_tool_calls": false,
        "prompt_cache_key": "codex-helper-live-smoke"
    });
    if let Some(service_tier) = service_tier {
        body["service_tier"] = Value::String(service_tier.to_string());
    }
    body
}

fn remote_compaction_v2_live_smoke_body(model: &str, service_tier: Option<&str>) -> Value {
    let mut body = json!({
        "model": model,
        "instructions": "Return exactly one Codex compaction output item for this diagnostic request.",
        "input": [
            {
                "type": "message",
                "role": "user",
                "content": [
                    {
                        "type": "input_text",
                        "text": "Codex relay remote_compaction_v2 live smoke. Compact this short diagnostic conversation."
                    }
                ]
            },
            {
                "type": "message",
                "role": "assistant",
                "content": [
                    {
                        "type": "output_text",
                        "text": "Diagnostic reply for remote compaction v2 live smoke."
                    }
                ]
            },
            {
                "type": "compaction_trigger"
            }
        ],
        "tools": [],
        "parallel_tool_calls": false,
        "store": false,
        "stream": true,
        "prompt_cache_key": "codex-helper-live-smoke"
    });
    if let Some(service_tier) = service_tier {
        body["service_tier"] = Value::String(service_tier.to_string());
    }
    body
}

fn image_generation_live_smoke_body(model: &str, service_tier: Option<&str>) -> Value {
    let mut body = json!({
        "model": model,
        "instructions": "You are running a Codex relay live smoke diagnostic. If available, use the hosted image_generation tool once.",
        "input": [
            {
                "type": "message",
                "role": "user",
                "content": [
                    {
                        "type": "input_text",
                        "text": "For diagnostics, create a tiny simple blue square PNG. No text."
                    }
                ]
            }
        ],
        "tools": [
            {
                "type": "image_generation",
                "output_format": "png"
            }
        ],
        "tool_choice": "auto",
        "parallel_tool_calls": false,
        "store": false,
        "stream": true,
        "include": [],
        "prompt_cache_key": "codex-helper-live-smoke"
    });
    if let Some(service_tier) = service_tier {
        body["service_tier"] = Value::String(service_tier.to_string());
    }
    body
}

fn responses_websocket_live_smoke_body(model: &str, service_tier: Option<&str>) -> Value {
    let mut body = json!({
        "type": "response.create",
        "model": model,
        "instructions": "You are running a Codex relay Responses WebSocket live smoke diagnostic. Reply with exactly OK.",
        "input": [
            {
                "type": "message",
                "role": "user",
                "content": [
                    {
                        "type": "input_text",
                        "text": "Codex relay Responses WebSocket live smoke. Reply OK."
                    }
                ]
            }
        ],
        "tools": [],
        "parallel_tool_calls": false,
        "store": false,
        "stream": true,
        "prompt_cache_key": "codex-helper-live-smoke"
    });
    if let Some(service_tier) = service_tier {
        body["service_tier"] = Value::String(service_tier.to_string());
    }
    body
}