v0.2.52 4694534

anthropic_image.go RawHistoryFolder

1package provider
2
3import (
4	"bytes"
5	"image"
6	"image/jpeg"
7	"image/png"
8	"strings"
9
10	xdraw "golang.org/x/image/draw"
11
12	// Register decoders for the formats we want to accept on input.
13	_ "image/gif"
14	_ "image/jpeg"
15	_ "image/png"
16)
17
18// anthMaxImageSide is Anthropic's per-image dimension cap when a
19// request contains more than one image. Single-image requests are
20// allowed up to 8000 px, but zot conversations routinely include
21// multiple images (a screenshot here, a tool result image there)
22// so we always normalise down to the stricter limit. 2000 px on the
23// longest side is well below their cap and still readable for the
24// kinds of screenshots / charts the model usually consumes.
25const anthMaxImageSide = 2000
26
27// anthSniffImageMIME returns the media type implied by an image's
28// leading magic bytes, independent of any declared type or stdlib
29// decoder registration. Returns "" when the signature is unrecognized,
30// leaving the caller's declared MIME untouched.
31func anthSniffImageMIME(data []byte) string {
32	switch {
33	case len(data) >= 8 && bytes.Equal(data[:8], []byte{0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A}):
34		return "image/png"
35	case len(data) >= 3 && data[0] == 0xFF && data[1] == 0xD8 && data[2] == 0xFF:
36		return "image/jpeg"
37	case len(data) >= 6 && (bytes.Equal(data[:6], []byte("GIF87a")) || bytes.Equal(data[:6], []byte("GIF89a"))):
38		return "image/gif"
39	case len(data) >= 12 && bytes.Equal(data[:4], []byte("RIFF")) && bytes.Equal(data[8:12], []byte("WEBP")):
40		return "image/webp"
41	}
42	return ""
43}
44
45// anthShrinkImageBytesIfTooBig returns data unchanged when the image
46// already fits within Anthropic's per-image dimension cap. When it
47// doesn't, the image is decoded, resampled with Catmull-Rom (a good
48// balance of quality and speed for downscaling), and re-encoded in
49// the same format. The MIME type may be rewritten when re-encoding
50// requires a format change (e.g. an image originally tagged image/gif
51// is shipped as image/png after resize, since stdlib only encodes
52// gif at the package's own narrow API which is awkward for a single
53// frame).
54//
55// Errors during decode/encode return the original bytes untouched so
56// the caller's existing flow continues to work; Anthropic will then
57// emit its own clearer error than we could synthesise. We log no
58// failures here because this runs on every outbound request and is
59// best-effort.
60func anthShrinkImageBytesIfTooBig(data []byte, mime string) ([]byte, string) {
61	if len(data) == 0 {
62		return data, mime
63	}
64	// Reconcile the declared media type with the actual bytes before
65	// anything else. Callers can mislabel images (a .png that is really
66	// JPEG, an extension that hardcodes a type), and so can already-
67	// persisted session transcripts created before this fix existed.
68	// Anthropic rejects the whole request when the declared type
69	// disagrees with the bytes it sniffs, which would make such a
70	// session impossible to continue. Sniffing the magic bytes here
71	// (independent of stdlib decoder registration, and resilient to a
72	// DecodeConfig failure below) makes the mismatch impossible to ship
73	// and lets a previously-broken session resume cleanly.
74	if real := anthSniffImageMIME(data); real != "" {
75		mime = real
76	}
77	cfg, format, err := image.DecodeConfig(bytes.NewReader(data))
78	if err != nil {
79		return data, mime
80	}
81	if cfg.Width <= anthMaxImageSide && cfg.Height <= anthMaxImageSide {
82		return data, mime
83	}
84	// Compute target dimensions preserving aspect ratio, longest side
85	// clamped to anthMaxImageSide.
86	tw, th := cfg.Width, cfg.Height
87	if tw >= th {
88		th = th * anthMaxImageSide / tw
89		tw = anthMaxImageSide
90	} else {
91		tw = tw * anthMaxImageSide / th
92		th = anthMaxImageSide
93	}
94	if tw < 1 {
95		tw = 1
96	}
97	if th < 1 {
98		th = 1
99	}
100
101	src, _, err := image.Decode(bytes.NewReader(data))
102	if err != nil {
103		return data, mime
104	}
105	dst := image.NewRGBA(image.Rect(0, 0, tw, th))
106	xdraw.CatmullRom.Scale(dst, dst.Bounds(), src, src.Bounds(), xdraw.Over, nil)
107
108	var buf bytes.Buffer
109	switch strings.ToLower(format) {
110	case "jpeg":
111		if err := jpeg.Encode(&buf, dst, &jpeg.Options{Quality: 85}); err != nil {
112			return data, mime
113		}
114		return buf.Bytes(), "image/jpeg"
115	case "png", "gif":
116		// gifs are re-encoded as png so the call site stays simple
117		// (avoiding a single-frame Encode helper). Anthropic accepts
118		// image/png happily; the visual result is the same for the
119		// model's vision pipeline.
120		if err := png.Encode(&buf, dst); err != nil {
121			return data, mime
122		}
123		nextMime := mime
124		if strings.ToLower(format) == "gif" {
125			nextMime = "image/png"
126		}
127		return buf.Bytes(), nextMime
128	default:
129		return data, mime
130	}
131}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close