diff --git a/bun.lock b/bun.lock index 0bf24c397..c691aea45 100644 --- a/bun.lock +++ b/bun.lock @@ -228,6 +228,7 @@ "version": "17.2.14", "dependencies": { "@oh-my-pi/pi-ai": "catalog:", + "@oh-my-pi/pi-catalog": "catalog:", "@oh-my-pi/pi-natives": "catalog:", "@oh-my-pi/pi-utils": "catalog:", "@oh-my-pi/pi-wire": "catalog:", diff --git a/packages/snapcompact/CHANGELOG.md b/packages/snapcompact/CHANGELOG.md index b6fce27a2..f694be1d7 100644 --- a/packages/snapcompact/CHANGELOG.md +++ b/packages/snapcompact/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed `claude-opus-5` (and later Opus lines) falling back to the 1568px frame instead of the 1932px high-res tier, so snapcompact archives for the current flagship Opus now retain ~33% more history at the same per-frame bill ([#8256](https://github.com/can1357/oh-my-pi/issues/8256)). + ## [17.1.5] - 2026-07-27 ### Fixed diff --git a/packages/snapcompact/package.json b/packages/snapcompact/package.json index a41026457..6e0f8025a 100644 --- a/packages/snapcompact/package.json +++ b/packages/snapcompact/package.json @@ -32,6 +32,7 @@ }, "dependencies": { "@oh-my-pi/pi-ai": "catalog:", + "@oh-my-pi/pi-catalog": "catalog:", "@oh-my-pi/pi-natives": "catalog:", "@oh-my-pi/pi-utils": "catalog:", "@oh-my-pi/pi-wire": "catalog:" diff --git a/packages/snapcompact/src/snapcompact.ts b/packages/snapcompact/src/snapcompact.ts index dce081240..a07c8ef0e 100644 --- a/packages/snapcompact/src/snapcompact.ts +++ b/packages/snapcompact/src/snapcompact.ts @@ -46,6 +46,7 @@ */ import type { Api, ImageContent, Message, TextContent } from "@oh-my-pi/pi-ai"; +import { isFableOrMythos, parseAnthropicModel, semverGte } from "@oh-my-pi/pi-catalog/identity"; import { renderSnapcompactPng, snapcompactSupportedChars } from "@oh-my-pi/pi-natives"; import { formatGroupedPaths, prompt } from "@oh-my-pi/pi-utils"; import { INTENT_FIELD } from "@oh-my-pi/pi-wire"; @@ -336,17 +337,17 @@ export interface IdealShape { frameSize?: number; } -/** Eval-winning format per model line, matched against the model id. The - * wire API only identifies the gateway — a Claude served through Vertex or - * OpenRouter still reads best with its own shape. Patterns cover the model - * lines the mono evals measured; everything else falls back to the API - * family's winner at the standard 1568px frame. First match wins. */ +/** Eval-winning format per model line. The wire API only identifies the + * gateway — a Claude served through Vertex or OpenRouter still reads best + * with its own shape. Classified Anthropic models use the shared catalog + * identity parser; remaining model lines use first-match regex rules and fall + * back to the API family's winner at the standard 1568px frame. */ +const HIGH_RES_ANTHROPIC_VARIANT = { variant: "11on16-bw", frameSize: 1932 } as const satisfies IdealShape; const MODEL_VARIANTS: readonly (readonly [RegExp, IdealShape])[] = [ - // Opus 4.7+ and Fable/Mythos read high-res natively (2576px edge under a - // 4,784 visual-token cap → 1932px square sweet spot): same recall and - // cost as 1568, a third fewer frames. - [/claude.*(fable|mythos)/i, { variant: "11on16-bw", frameSize: 1932 }], - [/claude-?opus-?4[.-][7-9]/i, { variant: "11on16-bw", frameSize: 1932 }], + // Versionless Fable/Mythos aliases (e.g. `claude-fable-latest`) never parse + // a numeric version, so keep them on the high-res tier by name — every + // Fable/Mythos line reads it natively. + [/claude.*(fable|mythos)/i, HIGH_RES_ANTHROPIC_VARIANT], // Older Claude lines downscale past 1568px — keep the safe size. [/claude/i, { variant: "11on16-bw" }], // Gemini 3.x bills a fixed 1,120-token budget per image regardless of @@ -363,6 +364,18 @@ const MODEL_VARIANTS: readonly (readonly [RegExp, IdealShape])[] = [ /** Eval-ideal format for a model id, or undefined when unmeasured. */ export function idealShapeVariant(modelId: string): IdealShape | undefined { + const anthropic = parseAnthropicModel(modelId); + if ( + anthropic && + (isFableOrMythos(anthropic.kind) || (anthropic.kind === "opus" && semverGte(anthropic.version, "4.7"))) + ) { + // Opus 4.7+ and Fable/Mythos read high-res natively: same recall and + // cost as 1568, a third fewer frames. 1932 is the largest *square* not + // downscaled under Anthropic's 4,784 visual-token cap ((1932/28)² = + // 69² = 4,761 ≤ 4,784 28px patches), and staying below 2000px clears + // the stricter ≤2000px limit for requests with more than 20 images. + return HIGH_RES_ANTHROPIC_VARIANT; + } return MODEL_VARIANTS.find(([pattern]) => pattern.test(modelId))?.[1]; } diff --git a/packages/snapcompact/test/snapcompact.test.ts b/packages/snapcompact/test/snapcompact.test.ts index fd6734bd4..c785b8ee1 100644 --- a/packages/snapcompact/test/snapcompact.test.ts +++ b/packages/snapcompact/test/snapcompact.test.ts @@ -299,6 +299,26 @@ describe("shape resolution", () => { // High-res frames are reserved for the lines that read them natively; // older Claude lines keep the safe 1568px family default. expect(snapcompact.resolveShape({ api: "anthropic-messages", id: "claude-opus-4-8" }).frameSize).toBe(1932); + expect(snapcompact.resolveShape({ api: "anthropic-messages", id: "anthropic--claude-4.8-opus" }).frameSize).toBe( + 1932, + ); + expect(snapcompact.resolveShape({ api: "anthropic-messages", id: "claude-opus-4-10" }).frameSize).toBe(1932); + // Versionless Fable/Mythos aliases (bundled `claude-fable-latest`) never + // parse a numeric version but still read the high-res tier by name (#8257). + expect( + snapcompact.resolveShape({ api: "openai-completions", id: "anthropic/claude-fable-latest" }).frameSize, + ).toBe(1932); + expect( + snapcompact.resolveShape({ api: "openai-completions", id: "~anthropic/claude-fable-latest" }).frameSize, + ).toBe(1932); + // Opus 5+ shares the Anthropic visual-token cap, so it stays on the + // high-res tier rather than falling back to the 1568px default (#8256). + expect(snapcompact.resolveShape({ api: "anthropic-messages", id: "claude-opus-5" }).frameSize).toBe(1932); + expect(snapcompact.resolveShape({ api: "anthropic-messages", id: "claude-opus-6" }).frameSize).toBe(1932); + // Opus lines below 4.7 downscale, so they keep the safe family default. + expect(snapcompact.resolveShape({ api: "anthropic-messages", id: "claude-opus-4-6" })).toBe( + snapcompact.SHAPES.anthropic, + ); expect(snapcompact.resolveShape({ api: "anthropic-messages", id: "claude-3-5-sonnet" })).toBe( snapcompact.SHAPES.anthropic, );