Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs-site/src/content/docs/fr/reference/adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ Avec l’authentification `key`, [`retryOn429`](/fr/reference/configuration/) s

## `anthropic`

**Stabilité des images historiques :** chaque image intégrée est normalisée indépendamment, à partir d’un bord maximal de 2000px et d’une cible base64 de 2MiB. L’ajout d’images ne réencode plus les précédentes selon leur ancienneté. Au-delà de 100 images, de 20MiB de base64 d’images ou de 32 000 000 octets UTF-8 pour la requête complète, le proxy renvoie HTTP 413 sans supprimer ni dégrader l’historique. Avec plus de 20 images, les dimensions doivent être connues et ne pas dépasser 2000px par côté. Messages natif et `count_tokens` suivent la même politique ; un 413 amont ne déclenche plus de réessai à qualité réduite. Réduisez l’entrée, compactez la conversation côté client ou ouvrez une nouvelle session. La stabilité suppose les mêmes sources, codec et version de politique, sans garantie de cache amont. Kiro et OpenAI Chat conservent leurs politiques.

**Cibles :** l’API **Messages** d’Anthropic (`/v1/messages`).
**Authentification :** `key` (`x-api-key` par défaut, ou `Authorization: Bearer` avec `apiKeyTransport: "bearer"`) ou `oauth` (Bearer + `anthropic-beta`, pour Claude Pro/Max).

Expand Down
2 changes: 2 additions & 0 deletions docs-site/src/content/docs/ja/reference/adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ HTTP リトライ ループの対象外です。

## `anthropic`

**画像履歴の安定性:** インライン画像は長辺2000px、base64の目標上限2MiBから個別に処理します。画像を追加しても相対的な順番によって過去の画像を再エンコードしません。100枚、画像base64合計20MiB、またはリクエスト全体32,000,000 UTF-8バイトを超える場合、履歴を削除・低画質化せずHTTP 413を返します。20枚を超える場合は全画像の寸法を確認でき、各辺2000px以下である必要があります。ネイティブMessagesと`count_tokens`も同じ方針です。上流413後の画像低画質化リトライはありません。クライアントで入力を減らす、会話を圧縮する、または新しいセッションを開始してください。同一の原本・コーデック・方針バージョンが前提で、キャッシュヒットは保証しません。KiroとOpenAI Chatの方針は変わりません。

**対象:** Anthropic **Messages**(`/v1/messages`)。
**認証:** `key`(デフォルトは `x-api-key`、または `apiKeyTransport: "bearer"` による `Authorization: Bearer`)または `oauth`(Bearer + `anthropic-beta`、Claude Pro/Max 用)。

Expand Down
2 changes: 2 additions & 0 deletions docs-site/src/content/docs/ko/reference/adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ Ollama 공급자에 `adapter: "ollama-native"`로 설정할 수도 있습니다.

## `anthropic`

**이미지 이력 안정성:** 인라인 이미지는 긴 변 2000px, base64 목표 2MiB에서 개별 처리합니다. 새 이미지를 추가해도 상대적 순서 때문에 이전 이미지를 다시 인코딩하지 않습니다. 100장, 이미지 base64 합계 20MiB 또는 전체 요청 32,000,000 UTF-8 바이트를 초과하면 이력을 삭제하거나 화질을 낮추지 않고 HTTP 413으로 거절합니다. 20장을 넘으면 모든 이미지의 크기가 확인 가능하고 각 변이 2000px 이하여야 합니다. 네이티브 Messages와 `count_tokens`에도 적용되며, 업스트림 413 이후 화질을 낮춘 재시도는 없습니다. 클라이언트에서 입력을 줄이거나 대화를 압축하거나 새 세션을 시작하세요. 바이트 안정성은 동일한 원본·코덱·정책 버전을 전제로 하며 캐시 적중을 보장하지 않습니다. Kiro와 OpenAI Chat 정책은 유지됩니다.

**대상:** Anthropic **Messages**(`/v1/messages`).
**인증:** `key`(기본 `x-api-key`, 또는 `apiKeyTransport: "bearer"` 설정 시 `Authorization: Bearer`) 또는 `oauth`(Bearer + `anthropic-beta`, Claude Pro/Max용).

Expand Down
6 changes: 6 additions & 0 deletions docs-site/src/content/docs/reference/adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,12 @@ header and does not guarantee a provider cache hit.

## `anthropic`

**Stable image history:** Inline images are normalized independently, starting at a 2000px maximum edge and a 2MiB base64 target; only the image's own size can select a lower encoding step. Adding screenshots no longer changes older images because of their age or the total request size. Full decode validation, per-image safety checks, bounded concurrency and the bounded encoding cache remain enabled.

Requests exceeding 100 images or 20MiB of image base64 are rejected, not repaired by deleting or degrading history. More than 20 images require known dimensions of at most 2000px per side. The complete serialized Anthropic request, including text and tools, must fit 32,000,000 UTF-8 bytes. Local failures return HTTP 413 with an `anthropic_image_*` or `anthropic_request_body_too_large` code before upstream dispatch. An upstream 413 never triggers a lower-quality image retry; existing client-specific context-overflow error mapping still applies.

Native Anthropic Messages and `count_tokens` use the same image policy. Compact/reduce the conversation or start a new session after a budget refusal; the proxy does not do that automatically. Byte stability assumes unchanged original bytes and codec/policy versions, and does not guarantee upstream cache hits or stable contents at remote image URLs. Kiro and OpenAI Chat retain their separate adaptive image policies.

**Targets:** Anthropic **Messages** (`/v1/messages`).
**Auth:** `key` (`x-api-key` by default, or `Authorization: Bearer` with `apiKeyTransport: "bearer"`) or `oauth` (Bearer + `anthropic-beta`, for Claude Pro/Max).

Expand Down
2 changes: 2 additions & 0 deletions docs-site/src/content/docs/ru/reference/adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,8 @@ interface ProviderAdapter {

## `anthropic`

**Стабильность истории изображений:** встроенные изображения обрабатываются независимо, начиная с максимальной стороны 2000px и целевого размера base64 2MiB. Добавление новых изображений не меняет кодирование старых из-за их относительного возраста. При превышении 100 изображений, 20MiB base64 изображений или 32 000 000 байт UTF-8 полного запроса возвращается HTTP 413 без удаления или ухудшения истории. Для более чем 20 изображений размеры должны быть известны и не превышать 2000px по каждой стороне. Нативные Messages и `count_tokens` используют ту же политику; ответ 413 провайдера не вызывает повтор с пониженным качеством. Уменьшите ввод, сожмите историю средствами клиента или начните новый сеанс. Стабильность предполагает неизменные исходные данные, кодек и версию политики, но не гарантирует попадание в кэш провайдера. Политики Kiro и OpenAI Chat не меняются.

**Назначение:** Anthropic **Messages** (`/v1/messages`).
**Аутентификация:** `key` (по умолчанию `x-api-key`, либо `Authorization: Bearer` при `apiKeyTransport: "bearer"`) или `oauth` (Bearer + `anthropic-beta`, для Claude Pro/Max).

Expand Down
2 changes: 2 additions & 0 deletions docs-site/src/content/docs/tr/reference/adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ döngüsünün bir parçası değildir.

## `anthropic`

**Görsel geçmişinin kararlılığı:** Satır içi görseller, en uzun kenar 2000px ve base64 hedefi 2MiB ile başlayarak bağımsız işlenir. Yeni görsel eklemek, eski görselleri göreli yaşlarına göre yeniden kodlamaz. 100 görsel, toplam 20MiB görsel base64 verisi veya tüm istek için 32.000.000 UTF-8 bayt sınırı aşılırsa geçmiş silinmeden ya da kalitesi düşürülmeden HTTP 413 döner. 20’den fazla görselde boyutlar doğrulanabilmeli ve her kenar en fazla 2000px olmalıdır. Yerel Messages ve `count_tokens` aynı politikayı kullanır; sağlayıcının 413 yanıtı düşük kaliteli yeniden denemeyi tetiklemez. Girdiyi azaltın, istemcide konuşmayı sıkıştırın veya yeni oturum açın. Bayt kararlılığı aynı kaynak, kodlayıcı ve politika sürümünü gerektirir; sağlayıcı önbellek isabetini garanti etmez. Kiro ve OpenAI Chat politikaları değişmez.

**Hedefler:** Anthropic **Messages** (`/v1/messages`).
**Kimlik Doğrulama:** `key` (varsayılan olarak `x-api-key` veya
`apiKeyTransport: "bearer"` ile `Authorization: Bearer`) veya `oauth` (Claude
Expand Down
6 changes: 6 additions & 0 deletions docs-site/src/content/docs/zh-cn/reference/adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,12 @@ interface ProviderAdapter {

## `anthropic`

**历史图片稳定性:** 内嵌图片独立正规化,从最大边 2000px、base64 目标上限 2MiB 的档位开始;只有图片自身大小可以触发进一步压缩。追加截图不再因新旧位置或总量改变旧图编码,解码安全检查、有限并发和有界缓存仍保留。

超过 100 张图片、20MiB 图片 base64 总量,或完整请求体超过 32,000,000 UTF-8 字节时,本地返回 HTTP 413 与 `anthropic_image_*`/`anthropic_request_body_too_large` 错误码,不再删旧图或降档凑预算。超过 20 张图片还要求每张尺寸可确认且不超过 2000px。上游 413 不再触发低画质重试;已有的客户端上下文超限错误映射不变。

原生 Anthropic Messages 与 `count_tokens` 使用相同策略。超限后需由宿主压缩、缩减输入或开启新会话,代理不会自动执行。字节稳定性以原图、编码器和策略版本不变为前提,不保证上游缓存命中或远程图片 URL 内容不变。Kiro、OpenAI Chat 保留各自的自适应图片策略。

**目标:** Anthropic **Messages**(`/v1/messages`)。
**认证:** `key`(默认 `x-api-key`,或设置 `apiKeyTransport: "bearer"` 后使用 `Authorization: Bearer`)或 `oauth`(Bearer + `anthropic-beta`,用于 Claude Pro/Max)。

Expand Down
2 changes: 2 additions & 0 deletions docs-site/src/content/docs/zh-tw/reference/adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ interface ProviderAdapter {

## `anthropic`

**歷史圖片穩定性:** 內嵌圖片獨立正規化,從最大邊 2000px、base64 目標上限 2MiB 開始;追加圖片不再依新舊位置改寫舊圖。超過 100 張、20MiB 圖片 base64,或完整請求超過 32,000,000 UTF-8 位元組時回傳 HTTP 413,不再刪除或降級歷史圖片。超過 20 張須確認每張尺寸不超過 2000px。原生 Messages 與 `count_tokens` 使用相同策略;上游 413 不觸發圖片降級重試。請由宿主壓縮、縮減輸入或開新會話。編碼器與策略版本變更仍可能影響位元組,並不保證上游快取命中;Kiro、OpenAI Chat 策略不變。

**目標:** Anthropic **Messages**(`/v1/messages`)。
**認證:** `key`(`x-api-key`)或 `oauth`(Bearer + `anthropic-beta`,用於 Claude Pro/Max)。

Expand Down
2 changes: 1 addition & 1 deletion src/adapters/anthropic-image-codec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export const MAX_INPUT_PIXELS = 100_000_000;
const PASSTHROUGH_MEDIA = new Set(["image/jpeg", "image/png", "image/gif", "image/webp"]);

export interface NormalizeOptions {
/** Shift every image's starting ladder position down (413 retry tightening; 030). */
/** Shift the adaptive Kiro/Chat image ladder; Anthropic never applies a bias. */
tierBias?: number;
/** Test seam: replaces the Bun.Image encode path (audit round 1, blocker 6). */
encode?: EncodeFn;
Expand Down
81 changes: 29 additions & 52 deletions src/adapters/anthropic-image-guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,8 @@
* "At least one of the image dimensions exceed max allowed size for many-image requests: 2000 pixels"
* - Hard cap: 100 images per request.
*
* Codex threads accumulate screenshots in history, so long sessions cross 20 images
* easily and any single retina capture (>2000px wide) kills every later turn. The
* PRIMARY layer is now anthropic-image-normalize.ts (Bun.Image resize/re-encode with an
* age-tier pyramid — devlog/260714_image_normalization_pipeline/020), which runs before
* this guard; these rules remain the deterministic BACKSTOP for whatever normalization
* could not shrink (undecodable passthroughs, all-terminal overflow). When this guard
* must drop, it textifies the OLDEST image blocks — newest screenshots are the ones the
* model needs.
* Image-local normalization runs first. This guard textifies only individually unsafe
* images; request-level limits reject the request instead of rewriting its history.
*/

export const MANY_IMAGE_THRESHOLD = 20;
Expand All @@ -34,20 +28,30 @@ export const MAX_IMAGE_BASE64_LENGTH = 5 * 1024 * 1024;
export const MAX_IMAGE_FILE_BYTES = MAX_IMAGE_BASE64_LENGTH;

/**
* Anthropic rejects raw HTTP bodies over ~32MB with 413 request_too_large, and base64
* image data dominates image-heavy histories (base64 is single-byte ASCII, so base64
* chars ≈ serialized body bytes for the image share). The guard runs inside buildRequest
* BEFORE system/tools attach, so it cannot measure the final body; instead we bound the
* image share to 20MiB, leaving ≥11MB headroom even against a decimal 32,000,000-byte
* cap — realistic non-image share (context-capped text history + tool schemas) stays
* well under that. Residual: a request dominated by non-image content can still 413.
* Image share admission is separate from the final serialized UTF-8 body check:
* system text, tool schemas and framing also consume the upstream request budget.
*/
export const TOTAL_IMAGE_BASE64_BUDGET = 20 * 1024 * 1024;

const OMITTED_TEXT = "[image omitted: Anthropic request exceeded the 20-image limit for large images; older screenshots were dropped]";
const OVERSIZED_TEXT = "[image omitted: exceeds Anthropic's 8000px per-side limit]";
const PER_IMAGE_TOO_LARGE_TEXT = "[image omitted: exceeds Anthropic's 5MB per-image limit]";
const BYTE_BUDGET_TEXT = "[image omitted: total image payload exceeded Anthropic's 32MB request limit; older screenshots were dropped]";
export const MAX_ANTHROPIC_REQUEST_BYTES = 32_000_000;

/** A caller must compact or reduce its input; retrying with degraded history is unsafe. */
export class AnthropicImageLimitError extends Error {
readonly status = 413;

constructor(readonly code: string, message: string) {
super(`${message} Compact the conversation or start a new session before retrying.`);
this.name = "AnthropicImageLimitError";
}
}

export function assertAnthropicRequestBodySize(body: string): void {
if (Buffer.byteLength(body, "utf8") > MAX_ANTHROPIC_REQUEST_BYTES) {
throw new AnthropicImageLimitError("anthropic_request_body_too_large", "The serialized Anthropic request exceeds 32 MB.");
}
}

interface ImageDimensions { width: number; height: number }

Expand Down Expand Up @@ -175,16 +179,15 @@ function textify(ref: ImageBlockRef, text: string): void {
}

/**
* Enforce Anthropic image limits on already-built wire messages (mutates in place).
* Policy: unconditionally textify >8000px images; when the request would be a
* many-image request (>20) with at least one image over 2000px, textify oldest
* images until <=20 so the 8000px allowance applies; always cap at 100 images;
* textify images over the 5MB per-image cap; and drop oldest base64 images until
* the total base64 payload fits the request-size budget.
* Textify only individually invalid images, independently of other messages.
* Reject request-level overflow without dropping or degrading any valid history.
*/
export function enforceAnthropicImageLimits(messages: unknown[]): void {
const refs = collectImageRefs(messages);
if (refs.length === 0) return;
if (refs.length > MAX_IMAGES_PER_REQUEST) {
throw new AnthropicImageLimitError("anthropic_image_count_exceeded", `Anthropic accepts at most ${MAX_IMAGES_PER_REQUEST} images per request.`);
}

const dims = refs.map(r => (r.base64 ? sniffImageDimensions(r.base64) : null));
const live = new Set<number>(refs.keys());
Expand All @@ -208,44 +211,18 @@ export function enforceAnthropicImageLimits(messages: unknown[]): void {
}
}

// Rule 2: many-image requests cap each image at 2000px. Keep the request at <=20
// images (dropping oldest first) whenever a surviving image exceeds that cap OR has
// unknown dimensions (URL sources and unsniffable formats): one unverifiable offender
// 400s the whole request upstream, so unknown counts as risky, not as safe.
// Unknown URL dimensions cannot prove admission for a many-image request.
const hasRiskyForMany = [...live].some(i => {
const d = dims[i];
return d === null || d.width > MANY_IMAGE_MAX_DIMENSION || d.height > MANY_IMAGE_MAX_DIMENSION;
});
if (hasRiskyForMany && live.size > MANY_IMAGE_THRESHOLD) {
for (const i of [...live]) {
if (live.size <= MANY_IMAGE_THRESHOLD) break;
textify(refs[i], OMITTED_TEXT);
live.delete(i);
}
}

// Rule 3: hard cap of 100 images per request regardless of size.
if (live.size > MAX_IMAGES_PER_REQUEST) {
for (const i of [...live]) {
if (live.size <= MAX_IMAGES_PER_REQUEST) break;
textify(refs[i], OMITTED_TEXT);
live.delete(i);
}
throw new AnthropicImageLimitError("anthropic_image_dimensions_exceeded", "Requests with more than 20 images require known dimensions of at most 2000px per side.");
}

// Rule 4: bound the total base64 payload (see TOTAL_IMAGE_BASE64_BUDGET rationale).
// Oldest base64 images are dropped first — newest screenshots are the ones the model
// needs. URL-source images carry no base64 weight and are never evicted here.
let base64Sum = 0;
for (const i of live) base64Sum += refs[i].base64?.length ?? 0;
if (base64Sum > TOTAL_IMAGE_BASE64_BUDGET) {
for (const i of [...live]) {
if (base64Sum <= TOTAL_IMAGE_BASE64_BUDGET) break;
const b64 = refs[i].base64;
if (!b64) continue;
textify(refs[i], BYTE_BUDGET_TEXT);
live.delete(i);
base64Sum -= b64.length;
}
throw new AnthropicImageLimitError("anthropic_image_budget_exceeded", "The total Anthropic image payload exceeds 20 MiB of base64 data.");
}
}
Loading
Loading