feat(snapcompact): implemented squad context compression benchmark

- Added a new SQuAD-based context-compression benchmark script for evaluating recall conditions.
- Added model and shape command-line arguments along with pricing and shape configurations.
- Updated default model variants and added an unknown billing family to shape resolution.
- Updated shape resolution and model resolution tests to verify the new defaults.
This commit is contained in:
can1357
2026-08-04 02:20:03 +02:00
parent e8f890fb16
commit ee335ce9c2
6 changed files with 457 additions and 35 deletions
+55 -15
View File
@@ -2,16 +2,20 @@
# requires-python = ">=3.10"
# dependencies = ["pillow"]
# ///
"""Kimi K2.6 chunked benchmark runner (KimiK26Bench scratch file).
"""Kimi-line chunked benchmark runner over PRODUCTION-rendered frames.
mono_prod protocol (same flow/questions/seed) but every request carries <=8
frames: OpenRouter silently drops images after the first 8, and kimi itself
dilutes >8 genuine frames. Chunk plan = windows of 8 frames, overlap >=1,
evenly spread (reproduces the diag [(0,8),(6,14),(13,21)] plan for 21 frames
so the .973 anchor is a cache hit). Questions are routed to the chunk where
their answer position is most interior.
so the k2.6 .973 anchor is a cache hit). Questions are routed to the chunk
where their answer position is most interior.
uv run --with pillow python bench_kimi.py --shape-json '<Shape>' --name <label>
uv run bench_kimi.py --shape 8on16-bw # k2.6 anchor
uv run bench_kimi.py --model moonshotai/kimi-k3 --shape 8on22-bw
--shape takes a settings variant name from mono_prod.SHAPES (the production
SHAPE_VARIANTS payloads); --shape-json + --name still accept a raw Shape.
"""
import argparse
@@ -26,10 +30,11 @@ sys.path.insert(0, str(HERE))
import squad # noqa: E402
from final import MODELS, cached # noqa: E402
from mono_prod import SHAPES # noqa: E402
from providers import _png_b64, _post, load_env_key, llm_complete # noqa: E402
from run import CACHE, RESULTS, load_prompt, sha8 # noqa: E402
OR_MODEL = "moonshotai/kimi-k2.6"
DEFAULT_MODEL = "moonshotai/kimi-k2.6"
FW_MODEL = "accounts/fireworks/models/kimi-k2p6"
FW_URL = "https://api.fireworks.ai/inference/v1/chat/completions"
FW_PRICE = (0.95, 4.00) # $/M in, out — fireworks.ai/models/fireworks/kimi-k2p6
@@ -94,8 +99,14 @@ def plan_chunks(n: int) -> list[tuple[int, int]]:
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--shape-json", required=True)
ap.add_argument("--name", required=True)
ap.add_argument("--model", default=DEFAULT_MODEL)
ap.add_argument(
"--shape", choices=sorted(SHAPES), help="named shape from mono_prod.SHAPES"
)
ap.add_argument(
"--shape-json", help="raw production Shape JSON (alternative to --shape)"
)
ap.add_argument("--name", help="condition label; required with --shape-json")
ap.add_argument(
"--route", default="openrouter", choices=["openrouter", "fireworks"]
)
@@ -112,7 +123,18 @@ def main() -> None:
ap.add_argument("--fresh", action="store_true")
args = ap.parse_args()
shape, label = json.loads(args.shape_json), args.name
if args.shape_json:
if not args.name:
ap.error("--name is required with --shape-json")
shape, label = json.loads(args.shape_json), args.name
elif args.shape:
shape, label = SHAPES[args.shape], args.shape
else:
ap.error("pass --shape or --shape-json")
if args.route == "fireworks" and args.model != DEFAULT_MODEL:
ap.error("the fireworks route is wired to kimi-k2.6 only")
if args.route == "openrouter" and args.model not in MODELS:
ap.error(f"model {args.model} not in final.MODELS; add its prices there")
keys = {
"openrouter": load_env_key("OPENROUTER_API_KEY"),
"anthropic": "",
@@ -144,8 +166,13 @@ def main() -> None:
pngs = sorted(frame_dir.glob("page-*.png"))
n_frames = len(pngs)
size = shape["frameSize"]
cols = size // shape["cellWidth"]
rows = size // shape["cellHeight"]
repeat = shape.get("lineRepeat", 1)
cols = (
(size // shape["cellWidth"] - 3) // 2
if shape.get("columns") == 2
else size // shape["cellWidth"]
)
rows = size // shape["cellHeight"] // repeat
chunks = plan_chunks(n_frames)
print(f"{label}: {n_frames} frames @{size}px, chunks={chunks}")
@@ -170,6 +197,17 @@ def main() -> None:
preamble = load_prompt("qa-image-multi.md").format(
k=len(chunk_pngs), cols=cols, rows=rows
)
if shape.get("columns") == 2:
preamble += (
"\nNote: each image lays text out as two word-wrapped newspaper columns separated by a gutter; "
"read the left column top to bottom, then the right column."
)
if repeat > 1:
preamble += (
f"\nNote: every text line is rendered {repeat} times consecutively - first on the plain "
"background, then repeated on a pale highlight band. The copies show identical characters; "
"cross-check between them when a glyph is hard to read, and do not treat copies as separate text."
)
ctx = [
{"text": preamble},
*({"image_path": p} for p in chunk_pngs),
@@ -187,7 +225,7 @@ def main() -> None:
zip(
("text", "usage", "stop"),
llm_complete(
keys, OR_MODEL, m, max_tokens=args.max_tokens, effort=None
keys, args.model, m, max_tokens=args.max_tokens, effort=None
),
)
)
@@ -196,7 +234,7 @@ def main() -> None:
zip(("text", "usage", "stop"), fireworks_complete(m, args.max_tokens))
)
qa = cached(
OR_MODEL, tag, {"messages": messages, "effort": None}, fn, args.fresh
args.model, tag, {"messages": messages, "effort": None}, fn, args.fresh
)
for q, a in zip(qs, squad.parse_numbered(qa["text"], len(qs))):
answers_by_q[q["q"]] = a
@@ -208,7 +246,7 @@ def main() -> None:
rows_out = [
{
"model": OR_MODEL,
"model": args.model,
"cond": f"bench-{label}",
"pos_rel": q["pos_rel"],
"q": q["q"],
@@ -224,7 +262,9 @@ def main() -> None:
k: sum(x[k] for x in usages)
for k in ("in", "out", "cache_w", "cache_r", "reasoning")
}
price_in, price_out = MODELS[OR_MODEL] if args.route == "openrouter" else FW_PRICE
price_in, price_out = (
MODELS[args.model] if args.route == "openrouter" else FW_PRICE
)
cost = u["in"] / 1e6 * price_in + u["out"] / 1e6 * price_out
quart = []
for lo, hi in ((0, 0.25), (0.25, 0.5), (0.5, 0.75), (0.75, 1.01)):
@@ -258,7 +298,7 @@ def main() -> None:
"q3": quart[2],
"q4": quart[3],
}
out_dir = RESULTS / f"bench-kimi-{label}"
out_dir = RESULTS / f"bench-{args.model.split('/')[-1]}-{label}"
out_dir.mkdir(parents=True, exist_ok=True)
(out_dir / "records.jsonl").write_text("\n".join(json.dumps(r) for r in rows_out))
(out_dir / "summary.json").write_text(json.dumps([summary], indent=1))
+1
View File
@@ -59,6 +59,7 @@ MODELS = {
"gpt-5.4-mini": (0.75, 4.5),
"google/gemini-3.5-flash": (0.6, 4.0),
"moonshotai/kimi-k2.6": (0.68, 3.41),
"moonshotai/kimi-k3": (3.00, 15.00),
"z-ai/glm-4.6v": (0.30, 0.90),
}
LENGTHS = (50, 150, 250)
@@ -75,6 +75,26 @@ SHAPES = {
"frameSize": SIZE,
"frameTokenEstimate": 2900,
},
"8on22-bw": {
"font": "8x13",
"cellWidth": 8,
"cellHeight": 22,
"stretch": False,
"variant": "bw",
"lineRepeat": 1,
"frameSize": SIZE,
"frameTokenEstimate": 2900,
},
"11on16-bw": {
"font": "8x13",
"cellWidth": 11,
"cellHeight": 16,
"stretch": False,
"variant": "bw",
"lineRepeat": 1,
"frameSize": SIZE,
"frameTokenEstimate": 2900,
},
"6x12-dim": {
"font": "6x12",
"cellWidth": 6,
+353
View File
@@ -0,0 +1,353 @@
# /// script
# requires-python = ">=3.10"
# dependencies = ["pillow"]
# ///
"""snapcompact: QA recall per context-compression strategy, over the full SQuAD dev set.
The corpus (all SQuAD v1.1 dev passages, space-joined; ~1.5M chars) is processed
per condition in chunks sized to that condition's carrying capacity:
text plain text, fixed 40,716-char chunks (= img-6x10 capacity)
compact agent compaction summary of each text chunk, QA on the summary
handoff agent handoff document of each text chunk, QA on the summary
img-<font>-<v> one 1568x1568 image per chunk; font in {8x13,6x10,5x8,5x7,
4x6tt,4x5tt}, render variant v in {color,zebra,bw}
Per chunk, up to --qpc questions are sampled (seeded, evenly spread across the
chunk so answers land at every image row band; pos_rel is recorded for position
analysis). Scoring is official SQuAD EM/F1. Responses are cached by payload
hash, so interrupted runs resume for free.
Usage examples:
uv run run.py # default condition set, full corpus
uv run run.py --limit-chars 200000 # quick pass on a corpus prefix
uv run run.py --conditions img-6x10-bw # one condition
uv run run.py --report # re-print tables from cache, no API
Key: ANTHROPIC_API_KEY from ~/.env (last assignment wins).
"""
import argparse
import hashlib
import json
import re
import sys
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
HERE = Path(__file__).resolve().parent
def find_agent_prompts() -> Path:
for parent in HERE.parents:
for candidate in (
parent / "packages" / "agent" / "src" / "compaction" / "prompts",
parent / "agent" / "src" / "compaction" / "prompts",
):
if candidate.exists():
return candidate
raise FileNotFoundError("Could not find agent compaction prompts")
sys.path.insert(0, str(HERE))
import squad # noqa: E402
from anthropic_api import complete, image_block, load_api_key # noqa: E402
from bdf import VARIANTS, FontCfg, capacity, render # noqa: E402
AGENT_PROMPTS = find_agent_prompts()
CACHE = HERE / ".cache"
QA_CACHE = CACHE / "qa"
RESULTS = HERE / "results"
FONTS = {
"8x13": FontCfg("8x13", "8x13", 8, 13),
"7x13": FontCfg("7x13", "7x13", 7, 13),
"6x12": FontCfg("6x12", "6x12", 6, 12),
"6x10": FontCfg("6x10", "6x10", 6, 10),
"6x9": FontCfg("6x9", "6x9", 6, 9),
# Anisotropic squashes: same glyphs, tighter grid. Crop flavor (6x6/6x8/3x10):
# next row's band paints over the vertical overhang, halved advance fuses ink.
# Stretch flavor (*s): rasterize native 6x10, Lanczos-resize to the target cell.
"6x8s": FontCfg("6x8s", "6x10", 6, 8, native=(6, 10)),
"6x6s": FontCfg("6x6s", "6x10", 6, 6, native=(6, 10)),
"3x10s": FontCfg("3x10s", "6x10", 3, 10, native=(6, 10)),
# Human-designed square cell: unscii-8 native, and stretched to a 6x6 cell.
"8x8u": FontCfg("8x8u", "unscii-8", 8, 8),
"6x6u": FontCfg("6x6u", "unscii-8", 6, 6, native=(8, 8)),
"6x8": FontCfg("6x8", "6x9", 6, 8, ascent=7),
"6x6": FontCfg("6x6", "6x9", 6, 6, ascent=6),
"3x10": FontCfg("3x10", "6x10", 3, 10),
# Redundancy coding: every line rendered twice, duplicate on a pale highlight.
"8x8r": FontCfg("8x8r", "unscii-8", 8, 8, repeat=2),
# Downsample-survivors: unscii-8 upscaled so gateway resizes leave legible glyphs.
"12x12u": FontCfg("12x12u", "unscii-8", 12, 12, native=(8, 8)),
"16x16u": FontCfg("16x16u", "unscii-8", 16, 16, native=(8, 8)),
"5x8": FontCfg("5x8", "5x8", 5, 8),
"5x7": FontCfg("5x7", "5x7", 5, 7),
"4x6tt": FontCfg("4x6tt", "tom-thumb", 4, 6, ascent=5),
"4x5tt": FontCfg("4x5tt", "tom-thumb", 4, 5, ascent=5),
}
TEXT_CHUNK = 40716 # = img-6x10 capacity; keeps text/summary chunks comparable
DEFAULT_CONDITIONS = (
"text,compact,handoff,"
"img-8x13-color,img-6x10-color,img-5x8-color,img-5x7-color,"
"img-6x10-zebra,img-5x8-zebra,img-6x10-bw,img-5x8-bw"
)
def sha8(*parts: str) -> str:
return hashlib.sha1("\x00".join(parts).encode()).hexdigest()[:8]
def load_prompt(name: str) -> str:
return (HERE / "prompts" / name).read_text()
def agent_prompt(name: str) -> str:
text = (AGENT_PROMPTS / name).read_text()
# Drop unused Handlebars conditionals (no custom focus in this eval).
return re.sub(r"\{\{#if .*?\{\{/if\}\}\n?", "", text, flags=re.DOTALL)
def cached_complete(api_key: str, model: str, messages: list[dict], fresh: bool, **kw) -> tuple[str, dict]:
"""complete() with response caching keyed on the full request payload.
Truncated responses (stop_reason == max_tokens) are never cached and never
served from cache, so re-runs with a larger budget repair them.
"""
key = sha8(model, kw.get("effort") or "", json.dumps(messages, sort_keys=True))
path = QA_CACHE / f"{key}.json"
if path.exists() and not fresh:
hit = json.loads(path.read_text())
if hit.get("stop") != "max_tokens" and hit["text"]:
return hit["text"], hit["usage"]
text, usage, stop = complete(api_key, model, messages, **kw)
if stop == "max_tokens":
print(f" WARN truncated response (stop=max_tokens), not cached: {key}")
else:
path.write_text(json.dumps({"text": text, "usage": usage, "stop": stop}))
return text, usage
def parse_condition(name: str) -> dict:
if name in ("text", "compact", "handoff"):
return {"name": name, "kind": name}
m = re.fullmatch(r"img-([a-z0-9]+)-([a-z-]+)", name)
if not m or m.group(1) not in FONTS or m.group(2) not in VARIANTS:
raise SystemExit(f"bad condition {name!r}; expected text|compact|handoff|img-<font>-<variant>")
return {"name": name, "kind": "image", "font": FONTS[m.group(1)], "variant": m.group(2)}
def run_chunk(cond: dict, start: int, end: int, ctx_args: dict) -> list[dict]:
"""Execute one (condition, chunk) task; returns per-question records."""
args, flow, paras, offsets, api_key = (
ctx_args["args"],
ctx_args["flow"],
ctx_args["paras"],
ctx_args["offsets"],
ctx_args["api_key"],
)
questions = squad.sample_chunk_questions(paras, offsets, start, end, args.qpc, args.seed)
if not questions:
return []
chunk_text = flow[start:end]
usage_rows: list[tuple[str, dict]] = []
png = cols = rows = None
context = chunk_text
if cond["kind"] == "image":
salt = ("dimv2",) if cond["variant"] == "dim" else () # cache-bust pre-fix sticky-fg dim renders
png = CACHE / f"img-{cond['font'].name}-{cond['variant']}-{sha8(chunk_text, str(args.size), *salt)}.png"
if not png.exists():
render(chunk_text, cond["font"], CACHE, args.size, cond["variant"]).save(png)
cols, rows, _ = capacity(cond["font"], args.size)
elif cond["kind"] in ("compact", "handoff"):
prompt_file = {"compact": "compaction-summary.md", "handoff": "handoff-document.md"}[cond["kind"]]
gen_messages = [
{"role": "user", "content": load_prompt("session-frame.md").format(context=chunk_text)},
{"role": "assistant", "content": "Noted. I have read the passages and will keep them in mind."},
{"role": "user", "content": agent_prompt(prompt_file)},
]
context, gen_usage = cached_complete(
api_key, args.model, gen_messages, args.fresh,
system=agent_prompt("summarization-system.md"), max_tokens=4096,
)
usage_rows.append(("summarize", gen_usage))
use_cache = args.cache == "on" or (args.cache == "auto" and args.qpb > 0)
batch_size = args.qpb or len(questions)
answers: list[str] = []
for b in range(0, len(questions), batch_size):
batch = questions[b : b + batch_size]
q_block = "\n".join(f"{i + 1}. {q['q']}" for i, q in enumerate(batch))
if cond["kind"] == "image":
carrier = image_block(png)
preamble = {"type": "text", "text": load_prompt("qa-image.md").format(cols=cols, rows=rows)}
else:
carrier = {"type": "text", "text": load_prompt("qa-text.md").format(context=context)}
preamble = None
if use_cache:
carrier["cache_control"] = {"type": "ephemeral"}
content = ([preamble] if preamble else []) + [carrier, {"type": "text", "text": q_block}]
messages = [{"role": "user", "content": content}]
text, usage = cached_complete(
api_key, args.model, messages, args.fresh, max_tokens=args.max_tokens, effort=args.effort
)
usage_rows.append(("qa", usage))
answers.extend(squad.parse_numbered(text, len(batch)))
records = []
for q, a in zip(questions, answers):
records.append(
{
"cond": cond["name"],
"chunk": start,
"pos_rel": q["pos_rel"],
"q": q["q"],
"answer": a,
"golds": q["golds"],
"em": squad.exact_match(a, q["golds"]),
"f1": squad.f1(a, q["golds"]),
"abstained": "unreadable" in a.lower(),
}
)
# Attach token usage to the first record of the chunk (avoids double counting).
records[0]["usage"] = [
{
"phase": phase,
"in": u.get("input_tokens", 0),
"out": u.get("output_tokens", 0),
"cache_w": u.get("cache_creation_input_tokens", 0),
"cache_r": u.get("cache_read_input_tokens", 0),
}
for phase, u in usage_rows
]
return records
def aggregate(name: str, records: list[dict], price_in: float, price_out: float) -> dict:
n = len(records)
f1s = [r["f1"] for r in records]
mean_f1 = sum(f1s) / n
se = (sum((x - mean_f1) ** 2 for x in f1s) / (n * (n - 1))) ** 0.5 if n > 1 else 0.0
usages = [u for r in records if "usage" in r for u in r["usage"]]
tok_in = sum(u["in"] for u in usages)
tok_out = sum(u["out"] for u in usages)
cache_w = sum(u.get("cache_w", 0) for u in usages)
cache_r = sum(u.get("cache_r", 0) for u in usages)
quart = []
for lo, hi in ((0.0, 0.25), (0.25, 0.5), (0.5, 0.75), (0.75, 1.01)):
qs = [r["f1"] for r in records if lo <= r["pos_rel"] < hi]
quart.append(sum(qs) / len(qs) if qs else None)
return {
"name": name,
"n": n,
"em": sum(r["em"] for r in records) / n,
"f1": mean_f1,
"f1_se": se,
"abstained": sum(r["abstained"] for r in records),
"tokens_in": tok_in,
"tokens_out": tok_out,
"cache_w": cache_w,
"cache_r": cache_r,
# Anthropic pricing: cache write 1.25x input, cache read 0.1x input (5m TTL).
"cost_usd": (tok_in + 1.25 * cache_w + 0.1 * cache_r) / 1e6 * price_in + tok_out / 1e6 * price_out,
"f1_by_quartile": quart,
}
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--model", default="claude-fable-5")
ap.add_argument("--conditions", default=DEFAULT_CONDITIONS)
ap.add_argument("--qpc", type=int, default=30, help="questions sampled per chunk")
ap.add_argument("--qpb", type=int, default=0, help="questions per API call (batches the chunk); 0 = all at once")
ap.add_argument("--cache", choices=["auto", "on", "off"], default="auto",
help="prompt-cache the carrier block; auto = on when --qpb is set")
ap.add_argument("--max-tokens", type=int, default=8192, help="output budget per QA call (incl. thinking)")
ap.add_argument("--effort", choices=["low", "medium", "high", "xhigh", "max"], default=None,
help="adaptive-thinking effort for QA calls; default = provider default")
ap.add_argument("--seed", type=int, default=42)
ap.add_argument("--size", type=int, default=1568)
ap.add_argument("--workers", type=int, default=4)
ap.add_argument("--limit-chars", type=int, default=0, help="cap corpus size; 0 = full dev set")
ap.add_argument("--limit-paras", type=int, default=0, help="cap corpus to first N passages; 0 = all")
ap.add_argument("--fresh", action="store_true", help="ignore cached responses")
ap.add_argument("--report", action="store_true", help="aggregate cached records only; no API calls")
ap.add_argument("--price-in", type=float, default=10.0, help="$ per 1M input tokens")
ap.add_argument("--price-out", type=float, default=50.0, help="$ per 1M output tokens")
ap.add_argument("--env", default="~/.env")
args = ap.parse_args()
CACHE.mkdir(exist_ok=True)
QA_CACHE.mkdir(exist_ok=True)
scope = f"p{args.limit_paras}" if args.limit_paras else (args.limit_chars or "full")
tag = "".join(
[
f"-qpb{args.qpb}" if args.qpb else "",
f"-s{args.size}" if args.size != 1568 else "",
f"-e{args.effort}" if args.effort else "",
]
)
run_dir = RESULTS / f"{args.model}-seed{args.seed}-qpc{args.qpc}-{scope}{tag}"
run_dir.mkdir(parents=True, exist_ok=True)
paras = squad.load_paragraphs(CACHE)
if args.limit_paras:
paras = paras[: args.limit_paras]
flow, offsets = squad.build_flow(paras, args.limit_chars or None)
conditions = [parse_condition(c.strip()) for c in args.conditions.split(",") if c.strip()]
tasks: list[tuple[dict, int, int]] = []
for cond in conditions:
budget = capacity(cond["font"], args.size)[2] if cond["kind"] == "image" else TEXT_CHUNK
for start in range(0, len(flow), budget):
tasks.append((cond, start, min(start + budget, len(flow))))
calls = len(tasks) + sum(1 for c, *_ in tasks if c["kind"] in ("compact", "handoff"))
print(
f"corpus={len(flow):,} chars ({len(offsets):,} passages), {len(conditions)} conditions, "
f"{len(tasks)} chunks, <= {calls} API calls, qpc={args.qpc}, model={args.model}"
)
api_key = "" if args.report else load_api_key(args.env)
ctx_args = {"args": args, "flow": flow, "paras": paras, "offsets": offsets, "api_key": api_key}
records: list[dict] = []
done = 0
with ThreadPoolExecutor(args.workers) as pool:
futures = [pool.submit(run_chunk, cond, start, end, ctx_args) for cond, start, end in tasks]
for fut in futures:
records.extend(fut.result())
done += 1
if done % 20 == 0:
print(f" {done}/{len(tasks)} chunks", flush=True)
with (run_dir / "records.jsonl").open("w") as fh:
for r in records:
fh.write(json.dumps(r) + "\n")
rows = [
aggregate(cond["name"], [r for r in records if r["cond"] == cond["name"]], args.price_in, args.price_out)
for cond in conditions
if any(r["cond"] == cond["name"] for r in records)
]
rows.sort(key=lambda r: -r["f1"])
(run_dir / "summary.json").write_text(json.dumps({"args": vars(args), "rows": rows}, indent=1))
hdr = (
f"{'condition':<15}{'n':>6}{'EM':>7}{'F1':>7}{'±se':>6}{'abst':>6}"
f"{'in tok':>10}{'cache w':>9}{'cache r':>9}{'out tok':>9}{'$':>7}"
)
print("\n" + hdr + "\n" + "-" * len(hdr))
for r in rows:
print(
f"{r['name']:<15}{r['n']:>6}{r['em']:>7.3f}{r['f1']:>7.3f}{r['f1_se']:>6.3f}{r['abstained']:>6}"
f"{r['tokens_in']:>10,}{r['cache_w']:>9,}{r['cache_r']:>9,}{r['tokens_out']:>9,}{r['cost_usd']:>7.2f}"
)
print(f"\n{'condition':<15} F1 by position quartile (Q1..Q4)")
for r in rows:
cells = " ".join(" - " if q is None else f"{q:.3f}" for q in r["f1_by_quartile"])
print(f"{r['name']:<15} {cells}")
print(f"\nresults -> {run_dir}/")
if __name__ == "__main__":
main()
+18 -12
View File
@@ -31,11 +31,12 @@
* billing (32px × 1.2, 10k-patch budget at `detail: "original"`) is
* area-proportional, so resolution cannot improve chars/$ — 1568 stays.
* `detail: "high"` would downgrade (2,500-patch cap); `original` is sent.
* - **Unknown providers** default to the Anthropic shape. `providerImageBudget`
* still caps per-request images per provider so inline imaging cannot flood a
* request with attachments, but the old OpenRouter-specific 8-image cap is
* gone; routers now use the same permissive budget as direct Anthropic/Claude
* lines unless configured otherwise upstream.
* - **Unknown providers** default to `8on22-bw` with Anthropic-style
* visual-token area billing. `providerImageBudget` still caps per-request
* images per provider so inline imaging cannot flood a request with
* attachments, but the old OpenRouter-specific 8-image cap is gone; routers
* now use the same permissive budget as direct Anthropic/Claude lines unless
* configured otherwise upstream.
*
* The whole pass is local and deterministic — no LLM call, no API key, no
* latency beyond rendering. Rasterization and PNG encoding happen in native
@@ -201,10 +202,13 @@ export function isShapeVariantName(value: unknown): value is ShapeVariantName {
}
/** Provider families with distinct image billing. */
type BillingFamily = "anthropic" | "google" | "openai";
type BillingFamily = "anthropic" | "google" | "openai" | "unknown";
function billingFamily(api?: Api): BillingFamily {
switch (api) {
case "anthropic-messages":
case "bedrock-converse-stream":
return "anthropic";
case "openai-completions":
case "openai-responses":
case "openai-codex-responses":
@@ -215,9 +219,8 @@ function billingFamily(api?: Api): BillingFamily {
case "google-vertex":
return "google";
default:
// anthropic-messages, bedrock-converse-stream, and anything unknown
// share Anthropic's pixel-area pricing as the safe ceiling.
return "anthropic";
// Unknown APIs share Anthropic's pixel-area pricing as the safe ceiling.
return "unknown";
}
}
@@ -305,6 +308,7 @@ const FAMILY_VARIANT: Record<BillingFamily, ShapeVariantName> = {
anthropic: "11on16-bw",
google: "8on22-bw",
openai: "8on22-bw",
unknown: "8on22-bw",
};
/** Denser companion variant per family for the foveated archive middle: same
@@ -315,12 +319,14 @@ const FAMILY_VARIANT_LOW: Record<BillingFamily, ShapeVariantName> = {
anthropic: "8on16-bw",
google: "8on16-bw",
openai: "8on16-bw",
unknown: "8on16-bw",
};
const FAMILY_SHAPE: Record<BillingFamily, Shape> = {
anthropic: SHAPES.anthropic,
google: SHAPES.google,
openai: SHAPES.openai,
unknown: priceShape(SHAPE_VARIANTS["8on22-bw"], "unknown"),
};
/** One model line's ideal format: variant plus an optional frame-size
@@ -348,9 +354,9 @@ const MODEL_VARIANTS: readonly (readonly [RegExp, IdealShape])[] = [
[/gemini/i, { variant: "8on22-bw", frameSize: 2048 }],
// gpt-5.5 patch billing is area-proportional; 1568 is already optimal.
[/gpt|codex/i, { variant: "8on22-bw" }],
// kimi's image processor downscales past 1792px (64×64 28px patches);
// 1568 wins on chars/$ and reads at f1 .973 (≤8 frames per request).
[/kimi/i, { variant: "8on16-bw" }],
// kimi-k3 chunked bench: `8on22-bw` scored f1 .915 @ $0.66 vs .813 on `8on16-bw` ($0.70);
// 1568 wins on chars/$ (image processor downscales past 1792px).
[/kimi/i, { variant: "8on22-bw" }],
// glm-4.6v .780 mono via direct vendor routing.
[/glm/i, { variant: "8on16-bw" }],
];
+10 -8
View File
@@ -267,9 +267,11 @@ describe("shape resolution", () => {
expect(snapcompact.resolveShape({ api: "openai-responses" })).toBe(snapcompact.SHAPES.openai);
expect(snapcompact.resolveShape({ api: "azure-openai-responses" })).toBe(snapcompact.SHAPES.openai);
expect(snapcompact.resolveShape({ api: "google-generative-ai" })).toBe(snapcompact.SHAPES.google);
// Unknown and absent APIs fall back to the Anthropic family default.
expect(snapcompact.resolveShape({ api: "some-future-api" })).toBe(snapcompact.SHAPES.anthropic);
expect(snapcompact.resolveShape(undefined)).toBe(snapcompact.SHAPES.anthropic);
// Unknown and absent APIs fall back to the unknown family default (8on22-bw with Anthropic token billing).
const unknownFallback = snapcompact.resolveShape({ api: "some-future-api" });
expect(unknownFallback.cellHeight).toBe(22);
expect(unknownFallback.variant).toBe("bw");
expect(snapcompact.resolveShape(undefined)).toEqual(unknownFallback);
});
it("detects the ideal shape from the model id across gateways", () => {
@@ -309,11 +311,11 @@ describe("shape resolution", () => {
expect(gemini.cellHeight).toBe(22); // extra leading
expect(gemini.frameTokenEstimate).toBe(1120);
// Measured openai-compat readers keep their own validated `8on16-bw`
// geometry (not the family's leading default), at the gateway's billing.
const kimiShape = snapcompact.resolveShape({ api: "openai-completions" }, "8on16-bw");
// Kimi models resolve to 8on22-bw; GLM keeps 8on16-bw.
const kimiShape = snapcompact.resolveShape({ api: "openai-completions" }, "8on22-bw");
const glmShape = snapcompact.resolveShape({ api: "openai-completions" }, "8on16-bw");
expect(snapcompact.resolveShape({ api: "openai-completions", id: "moonshotai/kimi-k2.6" })).toEqual(kimiShape);
expect(snapcompact.resolveShape({ api: "openai-completions", id: "z-ai/glm-4.6v" })).toEqual(kimiShape);
expect(snapcompact.resolveShape({ api: "openai-completions", id: "z-ai/glm-4.6v" })).toEqual(glmShape);
// Unmeasured model ids fall back to the API family default object.
expect(snapcompact.resolveShape({ api: "openai-completions", id: "qwen/qwen3-vl" })).toBe(
@@ -914,7 +916,7 @@ describe("compact", () => {
const hugeText = `HEAD sentinel. ${"Important fact number one. ".repeat(1000)}TAIL sentinel.`;
const result = await snapcompact.compact(
makePreparation({ messagesToSummarize: [createUserMessage(hugeText)] }),
{ frameSize: TEST_FRAME_SIZE, maxFrames: 7 },
{ model: { api: "anthropic-messages" }, frameSize: TEST_FRAME_SIZE, maxFrames: 7 },
);
const archive = snapcompact.getPreservedArchive(result.preserveData);
expect(archive?.frames).toHaveLength(7);