Skip to content

Vision sidecar silently truncates valid captions at the 64 KiB SSE limit #4727

Closed

Description

@parkgogogo

Client or integration

Codex App

Area

Streaming

Summary

A normal vision-sidecar response is silently cut off before its terminal response.completed event because parseSidecarSSE limits total raw SSE bytes to 64 KiB. describeImage accepts the partial deltas as a successful description, and the text-only target model receives an incomplete caption without any truncation indication. In the observed session this led the target model to crop a screenshot into three parts and retry reading it.

Expected: ordinary completed captions should survive SSE framing overhead while parsing remains bounded. When a safety limit really is reached, the vision path should explicitly identify incomplete output rather than cache/render it as a complete successful description.

This is distinct from DESC_MAX_CHARS = 2000: the captured complete caption was only 1488 characters, but the parser returned 725. The raw-stream limit was introduced in #1376. Existing configuration-parity issue #1201 does not cover this data-loss behavior.

Reproduction

Deterministic offline reproduction (no credentials, images, or network calls required): save the following as repro-sidecar.ts at the repository root and run bun repro-sidecar.ts. Tested against the installed 2.56.0 parser.

import { parseSidecarSSE } from "./src/web-search/parse";
const text = "Readable screenshot text. ".repeat(60);
const frame = (data: unknown) => `data: ${JSON.stringify(data)}\n\n`;
let wire = "";
for (let i = 0; i < text.length; i += 3) {
  wire += frame({type: "response.output_text.delta", item_id: "msg_synthetic", output_index: 0, content_index: 0, sequence_number: i / 3, delta: text.slice(i, i + 3)});
}
wire += frame({type: "response.completed", response: {status: "completed", output: [{type: "message", content: [{type: "output_text", text}]}]}});
const result = await parseSidecarSSE(new Response(wire));
console.log({wireBytes: Buffer.byteLength(wire), expectedChars: text.length, actualChars: result.text.length, error: result.error ?? null});

Actual: 75436 wire bytes, expected 1560 characters, actual 1386 characters, error null.

Original observation: enable visionSidecar with gpt-5.6-luna and route an image-bearing turn to a text-only deepseek-v4.1-flash model. A tall screenshot containing readable text triggered the problem. Capturing the complete upstream response separately and replaying the same bytes twice through the installed parser reproduced exactly the same 725-character partial result each time. Live model output varies; the synthetic reproduction above is deterministic.

Version

@bitkyc08/opencodex 2.56.0

Operating system

macOS 27.0 (26A5425a), arm64

Provider and model

Vision: OpenAI Responses / gpt-5.6-luna (low); target: OpenCode Go / deepseek-v4.1-flash

Logs or error output

Synthetic reproduction:
wireBytes=75436 expectedChars=1560 actualChars=1386 error=null

Aggregate metrics from the separately captured live response:
wireBytes=138693
response.output_text.delta events=467
terminal event=response.completed; status=completed
complete description chars=1488
parseSidecarSSE result chars=725; error=null
replay 1 chars=725; replay 2 chars=725
2000-character rendering clamp did not activate

Screenshots and supporting files

The credential-free synthetic reproduction above replaces the original screenshot and captured stream, which contain private conversation content. No private image or raw response is attached.

Relevant code: src/web-search/parse.ts (MAX_SIDECAR_RESPONSE_BYTES and parseSidecarSSE), src/vision/describe.ts (accepts partial parsed text), src/vision/index.ts (renders/caches it).

Redacted configuration

{
  "visionSidecar": {
    "enabled": true,
    "model": "gpt-5.6-luna",
    "reasoning": "low",
    "maxDescriptionsPerTurn": 8,
    "timeoutMs": 45000
  }
}

Checks

  • I searched existing issues and documentation.
  • I removed secrets, tokens, account details, request credentials, and personal data.

Activity

  1. added
    streamingSSE, WebSocket, terminal stream frames
    toolstool_calls, MCP, web-search / sidecar tools
    on Sep 15, 2026
  2. lidge-jun commented on Sep 15, 2026

    @lidge-jun
    Owner

    리뷰 · 우선순위 76 / 80

    이슈 #4727은 vision sidecar가 SSE를 읽을 때 원문 바이트 상한 64 KiB에 걸리면, 완성된 캡션인데도 중간에서 잘린 텍스트를 성공 설명으로 받아들이는 문제를 보고한다. 제보자 재현은 자격증명·이미지·네트워크 없이 parseSidecarSSE만 돌린다. 짧은 델타를 많이 보내면 와이어는 약 75 KiB인데 파서가 에러 없이 일부 글자만 돌려준다. 실제 세션에서도 완성 캡션 1488자인데 파서 결과가 725자로 고정 재현됐고, 텍스트 전용 타깃 모델이 잘린 설명을 보고 스크린샷을 잘라 재시도하는 부작용까지 났다.

    지금 dev HEAD(45cfb04e9, package 2.57.0)에서 경로는 이렇다. src/web-search/parse.ts 36행 MAX_SIDECAR_RESPONSE_BYTES = 64 * 1024. parseSidecarSSE(약 213행)는 청크를 받을 때마다 남은 바이트만 받아 디코드하고, 283–288행에서 한도에 닿으면 리더를 취소한 뒤 break한다. 주석은 완전한 이벤트는 남기고 미완성 줄은 버린다고 하지만, 반환부(약 295–314행)는 response.completed / done / delta 중 있는 텍스트를 고르고 acc.error가 있을 때만 error를 붙인다. 바이트 한도 자체는 error로 남지 않는다. 그래서 한도 앞에서 모은 delta만으로 부분 텍스트가 정상처럼 나간다.

    src/vision/describe.ts 약 121–127행은 parseSidecarSSE 결과를 받고, 텍스트가 비어 있고 parsed.error가 있을 때만 실패로 돌린다. 부분 텍스트에 에러가 없으면 성공으로 반환한다. src/vision/index.ts 49행 DESC_MAX_CHARS = 2000과 약 347행 successfulText 캐시 경로도 outcome.error가 없으면 그 텍스트를 완성 설명으로 캐시·렌더한다. 이슈가 말하듯 이 버그는 2000자 표시 클램프와 다르다. 완성본이 1488자여도 SSE 프레임 오버헤드 때문에 원문 64 KiB 한도에 먼저 걸린다. #1376에서 넣은 안전 한도가, 평범한 완료 캡션까지 조용히 자르는 쪽으로 쓰이고 있다.

    참고로 Anthropic vision 경로(src/vision/anthropic-describe.ts)는 같은 64 KiB 한도를 쓰지만, 스트림 이상 종료 시 부분 텍스트를 버리고 error outcome을 돌려 불완전 설명을 캐시하지 않는다는 주석(review F1)이 있다. 공유 parseSidecarSSE(OpenAI Responses sidecar / web-search와 공용)만 한도 hit를 침묵 성공으로 남긴다. 설정 패리티 이슈 #1201과도 별개다. 타입/config 분할에 무효화될 자리 아니다. 데이터 손실이 조용해서 우선순위는 높다.

    라인 36 (src/web-search/parse.ts MAX_SIDECAR_RESPONSE_BYTES) - 원문 SSE 64 KiB 상한이 프레임 오버헤드를 포함한 값이라, 짧은 완료 캡션도 잘릴 수 있다
    라인 283–288 (parseSidecarSSE) - 한도 도달 시 리더만 끊고 acc.error를 세우지 않는다
    라인 295–314 (parseSidecarSSE 반환) - 부분 delta를 성공 텍스트로 돌려 truncation 표시가 없다
    라인 121–127 (src/vision/describe.ts) - 부분 텍스트와 에러 없음을 성공 설명으로 수용한다
    라인 347–352 (src/vision/index.ts) - outcome.error가 없으면 잘린 텍스트를 캐시·렌더한다
    DESC_MAX_CHARS (index.ts 49) - 표시 클램프와 별개이며, 이번 손실의 원인이 아니다

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

    • 한도를 올려 평범한 완료 캡션과 프레임이 통과하게 할지, 한도는 유지하고 hit 시 반드시 error/incomplete로 표시할지(또는 둘 다)
    • 고칠 자리를 공유 parseSidecarSSE로 둘지(web-search에도 영향), vision describe.ts에서만 불완전을 거절할지
    • Anthropic 경로처럼 한도 hit면 캐시 금지를 OpenAI sidecar에도 같은 정책으로 맞출지

    너의 추천
    이슈는 유효하고 재현이 결정적이다. 첫째, parseSidecarSSE가 바이트 한도로 끊으면 error 또는 incomplete 표시를 반드시 세운다. 둘째, vision은 그 경우 부분 텍스트를 성공·캐시하지 않는다. 셋째, 필요하면 한도를 현실적인 완료 캡션과 SSE 오버헤드에 맞게 올리되 상한은 남긴다. 제보자 synthetic 스크립트를 테스트로 고정하면 좋다. #1201과 묶지 말고 이 데이터 손실만 좁게 고치는 PR을 받는 쪽을 권한다. close-don't-rebase 해당 없음.

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

  3. added a commit that references this issue on Sep 16, 2026
  4. lidge-jun commented on Sep 16, 2026

    @lidge-jun
    Owner

    Landed via #4752 at 112d2ea

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

    bugSomething isn't workinglanded-via-maintainerOriginal PR closed after landing via a maintainer merge trainstreamingSSE, WebSocket, terminal stream framestoolstool_calls, MCP, web-search / sidecar tools

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions