Skip to content

Add SSE keepalives during long image generation calls#122

Draft
adambalogh wants to merge 1 commit into
mainfrom
claude/gpt2-image-gen-timeout-aq1d84
Draft

Add SSE keepalives during long image generation calls#122
adambalogh wants to merge 1 commit into
mainfrom
claude/gpt2-image-gen-timeout-aq1d84

Conversation

@adambalogh

Copy link
Copy Markdown
Contributor

Summary

Image generation requests to providers take 60-120 seconds with no bytes on the wire. Idle-timeout proxies between the enclave and browser reset such silent streams at ~60s, surfacing as ERR_HTTP2_PROTOCOL_ERROR on the client. This change emits SSE comment frames ("keepalives") at regular intervals while generation runs to keep bytes flowing and prevent proxy timeouts.

Key Changes

  • New run_with_sse_keepalives() generator (image_generation.py): Runs a callable on a daemon worker thread, yielding SSE comment frames (: keepalive\n\n) at configurable intervals while the work executes. Returns the callable's result or re-raises any exception. Designed for use with yield from in streaming response generators.

  • Updated image generation timeout (image_generation.py): Added _IMAGE_GENERATION_TIMEOUT (300s read, 15s connect) to both /images/generations POST calls (form-based edits and JSON generations). Image generation is far slower than chat and needs its own generous timeout instead of the shared client default.

  • Integrated keepalives into streaming response (image_generation.py): Modified create_image_generation_streaming_response() to wrap _run_image_generation() with run_with_sse_keepalives(), ensuring keepalive frames flow while the provider call executes. Final signed frame and error semantics unchanged.

  • Extended keepalives to Gemini image generation (chat_controller.py): Applied the same keepalive pattern to the Gemini image generation path in generate() where model.invoke() is called for image-output models, protecting that long-running call as well.

  • Comprehensive test coverage (test/test_image_generation.py): Added TestStreamingKeepalives class with four test cases:

    • test_keepalives_flow_while_generation_runs: Verifies keepalives are emitted during slow generation and final signed frame is intact
    • test_generation_error_still_yields_error_frame: Confirms error frames are properly emitted even with keepalives
    • test_fast_generation_emits_no_keepalives: Ensures no unnecessary keepalives for quick operations
    • test_run_with_sse_keepalives_returns_value_and_raises: Unit tests the keepalive generator's return value and exception propagation

Implementation Details

  • Keepalive interval is configurable via _KEEPALIVE_INTERVAL_SECONDS (default 10s, patched to 0.01s in tests for speed)
  • Worker thread is daemon-threaded; if the client disconnects mid-generation, the generator closes but the provider call continues on the daemon
  • OHTTP encrypter downstream holds each chunk until the next arrives (one-chunk look-ahead), so wire sees bytes at most every 2x the interval—still far under the ~60s idle timeouts
  • Comment frames are SSE-compliant and ignored by parsers (the app's stream parser only reads data: lines)

https://claude.ai/code/session_017bwXiKz9YQsuhTyRcwEnKz

…on't kill the stream

gpt-image-2 generations regularly take 60-120s, and the streaming responder
ran the whole provider call before yielding a single byte. With nothing on
the wire, an idle-timeout proxy between the enclave and the browser resets
the HTTP/2 stream at ~60s, surfacing client-side as
ERR_HTTP2_PROTOCOL_ERROR.

Run the provider call on a worker thread and yield SSE comment frames
(": keepalive") every 10s while it executes. The frames are sealed as
ordinary chunked-OHTTP chunks, so bytes keep flowing across every hop
(enclave -> relay -> browser), and SSE parsers ignore comment lines so the
client contract is unchanged. Applied to both long-silent paths: the
endpoint-based image models (gpt-image-2, grok-2-image, Seedream, GLM-Image)
and the Gemini inline-image invoke in the chat controller.

Also give the provider image call its own 300s read timeout instead of the
shared chat client default, since image generation is far slower than chat.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017bwXiKz9YQsuhTyRcwEnKz
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants