Skip to content
Open
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
68 changes: 48 additions & 20 deletions src/adapters/anthropic-image-codec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,16 +116,32 @@ let encodeCalls = 0;
* rank by one and can push it across a tier boundary — re-encoding it to different
* bytes and busting Anthropic's prompt prefix cache for the whole history. Pinning the
* start position to the image's own identity keeps already-emitted bytes stable across
* appends. Keys are the encode cache's identity minus the position suffix
* (`${hash}:${mediaType}`, see processAt). Entry-count cap with LRU eviction: a
* value is one small number, so a count bound is a byte bound (~4096 * ~50B worst
* case, far under the app-owned memory budget's headroom).
* appends. Keys are fixed-size digests of the bytes and canonical media type, so
* caller-controlled metadata cannot make the retained identity arbitrarily large.
* The store participates in the shared retained-memory budget as well as its own
* entry-count cap.
*/
const POSITION_STORE_MAX_ENTRIES = 4_096;
const emittedPositions = new Map<string, number>();
const MAX_CANONICAL_MEDIA_TYPE_LENGTH = 127;
const MEDIA_TYPE_PATTERN = /^[a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+-]+$/;
interface PositionEntry { position: number; sizeBytes: number; storedAt: number }
const emittedPositions = new Map<string, PositionEntry>();
let positionBytes = 0;

function positionKey(b64: string, mediaType: string): string {
return `${Bun.hash(b64).toString(36)}:${mediaType}`;
const normalized = mediaType.trim().toLowerCase();
const canonical = normalized.length <= MAX_CANONICAL_MEDIA_TYPE_LENGTH && MEDIA_TYPE_PATTERN.test(normalized)
? normalized
: "application/octet-stream";
return new Bun.CryptoHasher("sha256").update(b64).update("\0").update(canonical).digest("hex");
}

function deletePositionEntry(key: string): number {
const entry = emittedPositions.get(key);
if (!entry) return 0;
emittedPositions.delete(key);
positionBytes -= entry.sizeBytes;
return entry.sizeBytes;
}

/**
Expand All @@ -134,12 +150,13 @@ function positionKey(b64: string, mediaType: string): string {
*/
export function recordedEmittedPosition(b64: string, mediaType: string): number | undefined {
const key = positionKey(b64, mediaType);
const pos = emittedPositions.get(key);
if (pos !== undefined) {
const entry = emittedPositions.get(key);
if (entry !== undefined) {
emittedPositions.delete(key);
emittedPositions.set(key, pos);
entry.storedAt = Date.now();
emittedPositions.set(key, entry);
}
return pos;
return entry?.position;
}

/**
Expand All @@ -153,15 +170,17 @@ export function recordEmittedPosition(b64: string, mediaType: string, pos: numbe
const key = positionKey(b64, mediaType);
const existing = emittedPositions.get(key);
if (existing !== undefined) {
emittedPositions.delete(key);
pos = Math.max(existing, pos);
deletePositionEntry(key);
pos = Math.max(existing.position, pos);
}
while (emittedPositions.size + 1 > POSITION_STORE_MAX_ENTRIES) {
const oldest = emittedPositions.keys().next().value;
if (oldest === undefined) break;
emittedPositions.delete(oldest);
deletePositionEntry(oldest);
}
emittedPositions.set(key, pos);
const sizeBytes = cacheEncoder.encode(key).byteLength + 16;
emittedPositions.set(key, { position: pos, sizeBytes, storedAt: Date.now() });
positionBytes += sizeBytes;
enforceAppOwnedMemoryBudget();
}

Expand Down Expand Up @@ -241,6 +260,7 @@ export function getNormalizeStatsForTests(): {
export function resetNormalizeStateForTests(): void {
cache.clear();
emittedPositions.clear();
positionBytes = 0;
cacheBytes = 0;
cacheMetadataBytes = 0;
cacheSentinelEntries = 0;
Expand All @@ -259,18 +279,26 @@ export function anthropicImageNormalizeRetainedStoreSnapshot(): {
pinnedBytes: number;
oldestAt: number | null;
} {
const oldestAt = Math.min(
cache.values().next().value?.storedAt ?? Infinity,
emittedPositions.values().next().value?.storedAt ?? Infinity,
);
return {
count: cache.size,
bytes: cacheBytes,
evictableBytes: cacheBytes,
count: cache.size + emittedPositions.size,
bytes: cacheBytes + positionBytes,
evictableBytes: cacheBytes + positionBytes,
pinnedBytes: 0,
oldestAt: cache.values().next().value?.storedAt ?? null,
oldestAt: Number.isFinite(oldestAt) ? oldestAt : null,
};
}

export function evictOldestAnthropicImageNormalizeForBudget(): number {
const oldest = cache.keys().next().value;
return oldest === undefined ? 0 : deleteCacheEntry(oldest);
const cacheOldest = cache.entries().next().value as [string, CacheEntry] | undefined;
const positionOldest = emittedPositions.entries().next().value as [string, PositionEntry] | undefined;
if (!positionOldest || (cacheOldest && cacheOldest[1].storedAt <= positionOldest[1].storedAt)) {
return cacheOldest ? deleteCacheEntry(cacheOldest[0]) : 0;
}
return deletePositionEntry(positionOldest[0]);
}

/** Default encoder: Bun.Image resize-to-fit + JPEG at the given quality. */
Expand Down
2 changes: 1 addition & 1 deletion structure/transports/inventory.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ surface is listed here so a maintainer can find the owner without grepping:
| Meta Muse Responses tool names | `src/responses/muse-tool-name-alias.ts`, `src/adapters/openai-responses.ts` | `api.meta.ai` only: function names over 64 characters or containing characters outside `[a-zA-Z0-9_-]` become collision-safe wire aliases and are restored before the client sees them. |
| Google / Vertex / Antigravity | `src/adapters/google.ts`, `src/adapters/google-http.ts`, `src/adapters/google-wire-compiler.ts`, `src/adapters/google-tool-schema.ts`, `src/adapters/google-truncation.ts`, `src/adapters/google-errors.ts`, `src/adapters/google-antigravity-wire.ts`, `src/adapters/google-antigravity-replay.ts`, `src/adapters/google-wire-shape.ts` | Vertex and Antigravity install a Google-family `fetchResponse` and so own their retry policy, while AI Studio Gemini leaves it undefined and uses the default server fetch path. The Google-family wrapper reuses shared abort/deadline helpers, upstream error normalization, and policy-aware wire-body repair: strict initial schema loss sends nothing, while strict repair withholding returns the original 400 without a changed send. The final compiler produces the [content-free tool-schema loss contract](../providers/google.md#google-tool-schema-loss-reporting). `google-wire-shape.ts` remains diagnostic-only. |
| Mimo Free | `src/adapters/mimo-free.ts` | Client identity and JWT handling are transport-local; the per-install client id lives in the opencodex state root. |
| Anthropic image ingress | `src/adapters/anthropic-image-guard.ts`, `src/adapters/anthropic-image-normalize.ts`, `src/adapters/anthropic-image-codec.ts` | Oversized or unsupported images are normalized or rejected before reaching upstream. An image's ladder position is pinned to its own identity (content hash + media type) rather than recomputed from recency each request (#4532); appending a newer image therefore cannot demote and re-encode older images and bust Anthropic's prompt prefix cache. Unseen images still take the age-tier pyramid's first position, the total byte budget still binds, and a 413 `tierBias` retry still applies. Recorded positions only move down the ladder, so the store is monotonic. |
| Anthropic image ingress | `src/adapters/anthropic-image-guard.ts`, `src/adapters/anthropic-image-normalize.ts`, `src/adapters/anthropic-image-codec.ts` | Oversized or unsupported images are normalized or rejected before reaching upstream. An image's ladder position is pinned to its own fixed-size digest of content and canonical media type rather than recomputed from recency each request (#4532); appending a newer image therefore cannot demote and re-encode older images and bust Anthropic's prompt prefix cache. The position store is byte-accounted with the normalization cache. Unseen images still take the age-tier pyramid's first position, the total byte budget still binds, and a 413 `tierBias` retry still applies. Recorded positions only move down the ladder, so the store is monotonic. |
| Adapter execution support | `src/adapters/run-turn-queue.ts`, `src/adapters/tool-catalog-nudge.ts`, `src/adapters/identity.ts`, `src/adapters/image.ts`, `src/adapters/upstream-http-error.ts` | Shared machinery: turn ordering, tool-catalog nudging, client fingerprinting, image conversion, upstream error normalization. |
| Cursor (beyond the sections above) | `src/adapters/cursor/live-transport.ts`, `src/adapters/cursor/http1-bidi.ts`, `src/adapters/cursor/live-models.ts`, `src/adapters/cursor/transport-retry.ts`, `src/adapters/cursor/mcp-manager.ts`, `src/adapters/cursor/thread-continuity.ts`, `src/adapters/cursor/checkpoint-store.ts` | Thread continuity is the point: a retry must not start a new Cursor thread, and a validated checkpoint must not rebuild the full root history. HTTP/2 remains the default; an explicit `http1.1`/`h1` pin maps the bidi run onto Cursor's `RunSSE` receive stream plus sequenced `BidiAppend` sends, and applies to live discovery too. |
| Claude Messages | `src/server/claude-messages.ts` | Routed translation, a native Anthropic passthrough branch, and `count_tokens`. |
Expand Down
20 changes: 18 additions & 2 deletions tests/adapters/anthropic/anthropic-image-normalize.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ import {
sniffImageDimensions,
TOTAL_IMAGE_BASE64_BUDGET,
} from "../../../src/adapters/anthropic-image-guard";
import { TIER0_COUNT } from "../../../src/adapters/anthropic-image-codec";
import {
recordEmittedPosition,
TIER0_COUNT,
} from "../../../src/adapters/anthropic-image-codec";

/** 1x1 red PNG — the smallest real, fully-decodable fixture. */
const ONE_PX_PNG =
Expand Down Expand Up @@ -127,7 +130,7 @@ describe("bounded normalization cache accounting", () => {
await Promise.all([first, second]);
const stats = getNormalizeStatsForTests();
expect(stats.cacheEntries).toBe(1);
expect(stats.cacheBytes).toBe(anthropicImageNormalizeRetainedStoreSnapshot().bytes);
expect(anthropicImageNormalizeRetainedStoreSnapshot().bytes).toBeGreaterThan(stats.cacheBytes);
expect(stats.cacheBytes).toBeGreaterThan(1_000);
});

Expand Down Expand Up @@ -165,6 +168,19 @@ describe("bounded normalization cache accounting", () => {
expect(released).toBeGreaterThanOrEqual(getNormalizeStatsForTests().metadataBytes / Math.max(1, before.count));
expect(anthropicImageNormalizeRetainedStoreSnapshot().bytes).toBe(before.bytes - released);
});

test("position identities retain a fixed amount for caller-controlled media types", () => {
const hugeMediaType = `image/${"x".repeat(1024 * 1024)}`;
recordEmittedPosition(ONE_PX_PNG, hugeMediaType, 1);
const first = anthropicImageNormalizeRetainedStoreSnapshot();
expect(first.count).toBe(1);
expect(first.bytes).toBeLessThan(256);

recordEmittedPosition(ONE_PX_PNG, `${hugeMediaType}y`, 2);
const second = anthropicImageNormalizeRetainedStoreSnapshot();
expect(second.count).toBe(first.count);
expect(second.bytes).toBe(first.bytes);
});
});

describe("normalizeAnthropicImages — real Bun.Image path", () => {
Expand Down
4 changes: 4 additions & 0 deletions tests/adapters/openai/openai-chat-dangling-toolcalls.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ const provider: OcxProviderConfig = {
baseUrl: "https://example.test/v1",
apiKey: "sk-test",
authMode: "key",
// The wire role folds to `system` unless a destination is recorded as accepting
// `developer`; this suite is about tool-result repair ordering, so it declares the
// destination rather than asserting the default.
foldDeveloperRoleToSystem: false,
};

interface ChatMsg {
Expand Down
4 changes: 4 additions & 0 deletions tests/responses/chat-inline-document-bytes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ const chatProvider: OcxProviderConfig = {
adapter: "openai-chat",
baseUrl: "https://gateway.example.internal/v1",
apiKey: "k",
// The wire role folds to `system` unless a destination is recorded as accepting
// `developer`; the document test asserts the role a turn keeps, so it declares the
// destination rather than asserting the default.
foldDeveloperRoleToSystem: false,
};
const anthropicProvider = {
adapter: "anthropic",
Expand Down
Loading