Skip to content

fix(anthropic): dynamic image downscaling on append busts prompt prefix cache #4532

Closed

Description

@jiaoyun286

Client or integration

Codex App

Area

Provider adapter

Summary

In OpenCodex 2.53.0, the Anthropic adapter dynamically calculates image downscaling tiers based on relative recency (initialPosition in anthropic-image-normalize.ts).

When a session appends a new image, earlier images in conversation history are pushed into lower resolution tiers (e.g. dropping from 2000px to 1024px) and re-encoded. This mutates past request bytes and completely invalidates Anthropic's prompt prefix cache, causing substantial token waste and cache misses on subsequent turns.

Expected behavior: Once an image is normalized and sent within a conversation, its encoded bytes/tier should remain immutable across future turns.

Reproduction

  1. Send an initial turn with 6 images through the Anthropic adapter. Note the encoded base64 hash/size of Image 1.
  2. In the next turn, append a 7th image to the session.
  3. Inspect the outgoing Anthropic payload:
    • Observed: Image 1 dropped from Tier 1 (2000px) to Tier 2 (1024px); its base64 payload mutated, breaking the prefix cache.
    • Expected: Image 1 retains its initial resolution and base64 encoding; prefix cache remains intact.

Version

2.53.0

Operating system

macOS 15.2

Provider and model

anthropic / claude-5-sonnet, openAi / gpt-6,googel / gemini-3.8-flash

Logs or error output

Affected files:
- src/adapters/anthropic-image-codec.ts (tier configs: 6 @ 2000px, 14 @ 1024px, older @ 700px)
- src/adapters/anthropic-image-normalize.ts (line 132 recalculating initialPosition dynamically)
- src/adapters/anthropic.ts (calling normalization per request)

Screenshots and supporting files

No response

Redacted configuration

Checks

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

Activity

  1. added
    providerProvider adapters, OpenAI-compat presets, upstream API quirks
    on Sep 13, 2026
  2. lidge-jun commented on Sep 13, 2026

    @lidge-jun
    Owner

    리뷰 · 우선순위 66 / 80

    설명
    이 이슈는 Anthropic 어댑터의 나이 티어 이미지 정규화가 매 요청마다 newestFirstIndex로 시작 해상도를 다시 매겨서, 세션에 이미지를 추가할수록 이미 보냈던 옛 이미지 바이트가 바뀌고 Anthropic prompt prefix cache가 깨지는 문제입니다. 지금 dev HEAD df7dc1be5(2.54.0)에도 그대로입니다. 보고 버전은 2.53.0이지만 해당 코드 경로는 tip과 같습니다.

    초등학생 말로 풀면 이렇습니다. Anthropic으로 보내는 요청에는 이미지가 여러 장 들어갈 수 있는데, OpenCodex는 “너무 크면 안 되니” 새 그림은 크게, 오래된 그림은 작게 JPEG로 다시 만듭니다. 문제는 “오래됨”을 이번 요청에서 몇 번째로 새롭냐로만 센다는 점입니다. 처음에 그림 6장만 있으면 첫 그림이 큰 티어(약 2000px)입니다. 다음에 7번째 그림을 붙이면 첫 그림은 한 칸 밀려 작은 티어(약 1024px)로 다시 인코딩됩니다. 모델 입장에서는 앞부분 토큰이 통째로 달라진 것처럼 보여서, 비싼 prefix cache를 버리고 다시 읽습니다. 긴 비전 세션일수록 토큰 낭비가 커집니다.

    코드 근거는 분명합니다. src/adapters/anthropic-image-normalize.ts의 initialPosition(newestFirstIndex, bias)는 “최신 6장은 tier0, 다음 14장은 tier1, 나머지 tier2”입니다. normalizeImageTargets는 매 호출마다 newestFirstIndex = n - 1 - i로 다시 계산합니다. src/adapters/anthropic-image-codec.ts의 processAt 캐시 키는 ${hash}:${mediaType}:${pos}라서, 같은 원본이라도 pos가 바뀌면 다른 인코딩이 나갑니다. 프로세스 내부 LRU는 “같은 티어 재계산”을 줄일 뿐, Anthropic 쪽 prefix cache를 지켜 주지 않습니다. src/adapters/anthropic.ts가 요청마다 정규화를 돌리는 구조와도 맞습니다.

    이건 “예산 초과 시 오래된 그림을 더 줄이는 demotion”과 다른 축입니다. demotion은 합이 클 때 필요하지만, 예산이 넉넉한데도 새 그림 한 장 때문에 옛 그림 바이트가 바뀌는 것이 버그입니다. 기대 동작은 “한 대화에서 이미 내보낸 이미지의 인코딩(또는 최소 시작 티어)을 고정”하는 쪽에 가깝습니다. types.ts/config.ts 분리와 무관하고, #4502/#4512(오디오 status)나 Devin 이미지 이슈들과도 겹치지 않는 Anthropic-only 캐시 안정성 버그입니다.

    src/adapters/anthropic-image-normalize.ts initialPosition / normalizeImageTargets - 매 요청 newestFirstIndex로 시작 pos를 다시 매겨, append만으로 과거 이미지 티어가 내려간다
    src/adapters/anthropic-image-codec.ts processAt 캐시 키 hash:mediaType:pos - pos가 바뀌면 새 JPEG가 나와 Anthropic prefix bytes가 달라진다
    src/adapters/anthropic.ts 요청 빌드 경로 - 매 요청 정규화를 다시 돌려 세션 sticky 인코딩이 없다
    경로/심볼 - 프로세스 내부 normalize LRU - 같은 pos 재인코딩만 줄일 뿐, 업스트림 prompt cache 안정성과는 무관하다
    경로/심볼 - aggregate demotion 루프 - 예산 압박 시의 추가 축소는 남을 수 있지만, “여유 있을 때 append로 티어가 흔들리는 것”과 정책을 분리해야 한다

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

    • sticky 단위를 “원본 hash → 첫 성공 인코딩 바이트”로 할지, “원본 hash → 첫 시작 pos(티어)”로 할지
    • 예산 demotion이 필요할 때 sticky를 깨고 줄이는 것을 허용할지, demotion 시에도 한 번 정한 와이어 바이트만 더 낮은 고정 티어로 내리는 one-way로 할지
    • sticky 상태를 프로세스 메모리만으로 둘지, 대화/턴 키로 범위 지을지(멀티 워커·재시작 시 캐시 미스가 다시 날 수 있음)
    • 재현 테스트를 “7장 append 후 1번 이미지 base64 불변”으로 고정할지

    너의 추천
    재현이 명확하니 bug/provider 라벨 유지하고, 원본 콘텐츠 hash에 sticky 인코딩(또는 sticky 시작 pos) 를 넣는 작은 PR을 받으세요. 예산 demotion은 one-way로만 sticky를 갱신하게 하세요. “매 요청 전체 재티어링”은 제거하는 편이 맞습니다. 이 이슈를 닫지 말고 PR이 위 불변식을 테스트로 잠그면 닫으세요.

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

  3. lidge-jun commented on Sep 14, 2026

    @lidge-jun
    Owner

    Fixed on dev by 44027ae.

    Appending an image no longer rewrites the encoding of images already in the history, so the prompt prefix survives instead of being invalidated on every append.

    This one took two attempts and the detail is worth recording: the first implementation kept an emitted-position store keyed only by content hash and media type, which collapsed duplicate images in a conversation onto a single ladder slot and broke age-tier pass-through for histories containing repeated images. Hosted CI caught it, and the merged version keys positions so duplicates keep their own slots.

    Thanks to @jiaoyun286, whose PR #4549 is carried into this change with a Co-authored-by trailer.

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 workingproviderProvider adapters, OpenAI-compat presets, upstream API quirks

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions