Skip to content

Feature: opt-in per-provider request byte budget that downscales then prunes inline images #2511

Open

Description

@NotWizard

Area

Proxy and routing

What are you trying to accomplish?

I want image-heavy Codex sessions to survive against providers whose gateway enforces a hard request-body size limit, instead of the thread becoming permanently unusable.

Concretely: I attach screenshots and let Codex read rendered PDF/PPT pages while working. After enough images accumulate in the thread, every subsequent turn fails with a provider-side body-size rejection, and the thread can never recover — including its own compaction attempt, because that request also carries the history.

What prevents this today?

Three things combine:

  1. Codex sends images at original resolution. A Retina screenshot is 2–4 MB of base64. There is no client-side byte budget.
  2. OpenCodex replays the full history. expandPreviousResponseInput (src/responses/state.ts) materializes the stored previous_response_id state back into input, so every turn re-sends every image ever attached. This is correct for cross-provider routing, but it means the payload only grows.
  3. Codex auto-compaction cannot save the thread. Compaction triggers on tokens, while the gateway limit is bytes. Images are byte-heavy and token-light (a 1 MB screenshot is ~1.4 MB base64 but only ~1–2.5k vision tokens), so the byte ceiling is reached long before the token threshold. Nothing in the pipeline watches serialized body size.

Measured limits on Alibaba Cloud Bailian (Model Studio), OpenAI-compatible endpoints:

Endpoint Body cap Error
public dashscope.aliyuncs.com/compatible-mode/v1 6 MB BadRequest.TooLarge: Exceeded limit on max bytes to request body : 6291456
dedicated Bailian host (*.maas.aliyuncs.com) 10 MB RequestEntityTooLarge: Request body size exceeds maximum allowed size: 10485760 bytes

Bailian's Responses API also has no files/file-id upload mechanism, and its input_image field is documented as taking a public URL — but Codex only produces inline base64, so that escape hatch is unavailable.

What should OpenCodex do?

Add an opt-in, per-provider request byte budget in the responses pipeline. When the serialized body exceeds the budget, slim it in two ordered passes before forwarding:

  1. Downscale oversized inline images (re-encode to a bounded longest-edge JPEG). Cheap, lossy only in resolution, keeps every image present.
  2. Prune oldest-first if still over budget: replace the oldest input_image parts with an explicit input_text marker (e.g. [image removed: request size limit]) so both the model and the user know the image was dropped rather than silently ignored. The newest turn's images survive longest.

Requirements that matter for correctness:

  • Must walk the whole payload, not just input[].content[] — Codex also embeds images inside tool-output items (function_call_output). Scanning only the obvious path silently no-ops.
  • Must serialize compactly when measuring; a pretty-printed re-serialization can make the body larger.
  • Disabled by default, so providers with generous limits are unaffected.

Example usage or interface

// ~/.opencodex/config.json
{
  "providers": {
    "bailian": {
      "adapter": "openai-responses",
      "maxRequestBytes": 9000000,      // unset = today's behaviour (no slimming)
      "imageDownscaleMaxEdge": 1600    // optional; omit to skip pass 1
    }
  }
}

Log line when it engages:

[opencodex] slimmed body 38729496 -> 5887546 bytes (downscaled=2, pruned=0)

Alternatives or workarounds

I ran a ~230-line stdlib-only local proxy implementing exactly this design in front of Bailian, to check the approach holds end to end:

  • Synthetic 38.7 MB two-image request → slimmed to 5.9 MB → upstream 200 with a normal completion. The identical body sent directly upstream returns 413.
  • Real Codex turn viewing four 2400×2400 images (66 MB on disk) → completed normally, no error.
  • Regressions checked: plain-text turn fine, SSE streaming still incremental, previous_response_id continuation intact.

Workarounds that do not work:

  • Switching endpoints only trades 10 MB for 6 MB; the cap is a gateway property, not a wire-format one.
  • Instructing the agent (via AGENTS.md) to downscale before viewing helps but is best-effort, and cannot cover images the user pastes directly.
  • /compact requires a successful model call, which is exactly what is failing.

Additional context

Checks

  • I searched existing issues and documentation.
  • This request describes a concrete OpenCodex workflow rather than merely naming a desired technology.
  • I removed secrets and personal data.

Activity

  1. added
    enhancementNew feature or request
    providerProvider adapters, OpenAI-compat presets, upstream API quirks
    proxyHTTP proxy, routing, reverse-proxy / management auth
    on Aug 25, 2026
  2. Ingwannu commented on Aug 25, 2026

    @Ingwannu
    Owner

    This is a valid failure mode and the opt-in per-provider boundary is valuable, but I do not think the full proposed implementation should be merged as one first step.

    Current OpenCodex can expand stored continuation input back into a full request body, so inline image history can exceed a provider byte cap before token-based compaction helps. The first implementation should establish a deterministic serialized-byte budget at the final provider request boundary, disabled by default, and cover expanded previous-response input plus images nested in tool outputs.

    Please split policy from image transformation:

    1. add maxRequestBytes with exact compact-byte measurement, a clear fail-closed error when the budget is exceeded, and regressions for direct input, expanded history, tool-output nesting, streaming, and an oversized newest turn;
    2. add pruning as an explicit provider policy, preserving newest-turn content and replacing removed images with a visible marker;
    3. treat downscaling as a separate optional capability because it adds codec/runtime/platform behavior and image-quality policy.

    The server should never silently mutate requests when the option is unset, and logs must contain counts/byte totals only, not image data. Before implementation, document whether the newest user turn may ever be pruned; my recommendation is no—if historical pruning cannot meet the cap, return a clear local error instead. I am leaving this open for that scoped design/implementation rather than closing it as unsupported.

  3. lidge-jun commented on Aug 25, 2026

    @lidge-jun
    Owner

    리뷰 · 우선순위 55 / 80

    설명: 이 이슈는 그림이 쌓인 코덱스 스레드가 제공자 게이트의 요청 바이트 한도에 걸려 영구히 죽는 문제를 적는다. 지금은 구현 풀이 없다. 지금 CURRENT dev HEAD 는 bb89eaf 이다. origin/dev 는 이번 시간에 안 움직였다. src/config.ts 는 3238줄이다. 작성 제안은 제공자마다 켜는 요청 바이트 예산이다. 넘치면 큰 그림을 줄이고, 그래도 넘치면 오래된 그림을 글 표식으로 바꾼다.

    세 가지가 겹친다. 코덱스는 원본 해상도 그림을 보낸다. 레티나 스크린샷은 베이스64 로 2-4MB 다. OpenCodex 는 이전 응답 아이디를 풀어 히스토리를 다시 넣는다. src/responses/state.ts 1230 줄 expandPreviousResponseInput 이 그 길이다. src/server/responses/core.ts 1746 과 2189 에서 부른다. 크로스 제공자 라우팅에는 맞다. 그래서 페이로드는 턴마다 커진다. 코덱스 압축은 토큰을 본다. 게이트 한도는 바이트다. 그림은 바이트는 크고 토큰은 작다. 토큰 압축이 뜨기 전에 바이트 천장이 먼저 온다.

    작성자가 잰 알리바바 백련 한도는 공개 호스트 6MB, 전용 호스트 10MB 다. Responses API 에 파일 아이디 업로드가 없고, 그림 칸은 공개 URL 을 받도록 적혀 있다. 코덱스는 인라인 베이스64 만 만든다. 그래서 그 우회는 못 쓴다. 작성자는 약 230줄 로컬 프록시로 같은 설계를 앞에 두고 확인했다. 38.7MB 두 그림이 5.9MB 로 줄고 업스트림 200 이 났다. 같은 본문을 직접 보내면 413 이다.

    요구는 맞다. 본문 전체를 걸어야 한다. input 의 content 만 보면 function_call_output 안의 그림은 남는다. 측정은 예쁘게 직렬화하면 안 된다. 기본은 꺼야 한다. 한도가 큰 제공자를 건드리면 안 된다. 이 이슈는 987 을 다시 연 것이다. 987 은 템플릿 봇이 API 개설을 닫았다. 메인테이너 답은 없었다. 그래서 이 이슈를 987 중복으로 닫지 말 것. 987 은 이미 닫혀 있고 내용이 같다. 이 이슈를 살아 있는 트래커로 둔다. 2473 의 WS 502 게이트와는 다르다. 저것은 코덱스 WS 업스트림 프레임 크기다. 여기는 라우트된 Responses 본문 바이트다. 2426 을 다시 열지 말 것.

    src/responses/state.ts 1230 expandPreviousResponseInput - 이전 응답 아이디를 풀어 그림을 매 턴 다시 넣는다
    src/server/responses/core.ts 1746 / 2189 - 그 확장이 본문 입구에서 돌아간다
    제안 maxRequestBytes / imageDownscaleMaxEdge - HEAD 설정 스키마에 없다. 구현 풀이 오면 스키마 자리부터 검토한다
    #987 - 같은 제안이 템플릿 봇에 닫혔다. 이 이슈를 중복 닫지 말 것
    #2426 / #2473 - WS 프레임 게이트와 다른 구멍이다. 2426 을 다시 열지 말 것

    메인테이너의 판단이 필요한 지점

    • 제공자별 옵트인 바이트 예산을 받을지. 받는 편이 맞다. 기본 꺼짐이어야 한다
    • 줄이기와 잘라내기를 프록시가 할지, 클라이언트에 미룰지. 압축이 바이트를 못 구하니 프록시가 맞다
    • 987 을 다시 열지. 열지 말 것. 이 이슈를 트래커로 둔다
    • 구현 없이 이 이슈를 닫을지. 닫지 말 것

    너의 추천
    열어 둔다. 설계는 작고 옵트인으로 받는다. 본문 전체 순회와 기본 꺼짐을 조건으로 둔다. 구현 PR 이 오면 그 PR 로 닫는다. 987 과 2426 은 다시 열지 말 것. 라벨은 그대로 둔다.

    이 댓글은 grok-bot이 작성했습니다

  4. lidge-jun commented on Sep 1, 2026

    @lidge-jun
    Owner

    Backlog-pass status: #3196 (52d941640) landed the refusal half — maxUpstreamBodyBytes, opt-in and default off, with the serialized-byte measurement helper, the inline-image walk, and the rebuild-site inventory. It deliberately does not close this issue: downscale-then-prune mutates request content and is the remediation this issue asks for, and it can now be built on those helpers without a second measurement path. Staying open as the tracking item for that slice.

  5. added
    priority: P3Low: new provider/client integration, large or experimental feature (>2000 LOC or >50 files), RFC/ro
    on Sep 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestpriority: P3Low: new provider/client integration, large or experimental feature (>2000 LOC or >50 files), RFC/roproviderProvider adapters, OpenAI-compat presets, upstream API quirksproxyHTTP proxy, routing, reverse-proxy / management auth

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions