compressImagesForVision downscales/re-encodes inline image bytes before they reach a vision model — cutting upload bytes, latency and (above the edge cap) the billed vision tokens. Keyless and pure-Go. Controlled by: CHATCLI_VISION_COMPRESS on|off (default on) CHATCLI_VISION_MAX_EDGE
(images []models.ImageContent)
| 243 | // URL-only images (no inline bytes) are passed through untouched. The function |
| 244 | // is conservative: imgcompress never inflates a payload and never drops alpha. |
| 245 | func (cli *ChatCLI) compressImagesForVision(images []models.ImageContent) []models.ImageContent { |
| 246 | if strings.EqualFold(strings.TrimSpace(utils.GetEnvOrDefault("CHATCLI_VISION_COMPRESS", "on")), "off") { |
| 247 | return images |
| 248 | } |
| 249 | opts := imgcompress.DefaultOptions() |
| 250 | if v := strings.TrimSpace(os.Getenv("CHATCLI_VISION_MAX_EDGE")); v != "" { |
| 251 | if n, err := strconv.Atoi(v); err == nil && n > 0 { |
| 252 | opts.MaxEdge = n |
| 253 | } |
| 254 | } |
| 255 | if v := strings.TrimSpace(os.Getenv("CHATCLI_VISION_JPEG_QUALITY")); v != "" { |
| 256 | if n, err := strconv.Atoi(v); err == nil && n >= 1 && n <= 100 { |
| 257 | opts.JPEGQuality = n |
| 258 | } |
| 259 | } |
| 260 | |
| 261 | totalSaved := 0 |
| 262 | out := make([]models.ImageContent, len(images)) |
| 263 | copy(out, images) |
| 264 | for i := range out { |
| 265 | if len(out[i].Data) == 0 { |
| 266 | continue // URL-only; nothing to recompress |
| 267 | } |
| 268 | data, res := imgcompress.Compress(out[i].Data, out[i].MediaType, opts) |
| 269 | if !res.Changed { |
| 270 | continue |
| 271 | } |
| 272 | out[i].Data = data |
| 273 | out[i].MediaType = res.OutMediaType |
| 274 | totalSaved += res.SavedBytes() |
| 275 | cli.logger.Debug("vision: image recompressed", |
| 276 | zap.String("file", out[i].FileName), |
| 277 | zap.Int("orig_bytes", res.OrigBytes), zap.Int("new_bytes", res.NewBytes), |
| 278 | zap.String("orig_dim", fmt.Sprintf("%dx%d", res.OrigW, res.OrigH)), |
| 279 | zap.String("new_dim", fmt.Sprintf("%dx%d", res.NewW, res.NewH)), |
| 280 | zap.String("out_type", res.OutMediaType)) |
| 281 | } |
| 282 | if totalSaved > 0 { |
| 283 | cli.logger.Info("vision: image compression saved bytes", |
| 284 | zap.Int("images", len(out)), zap.Int("bytes_saved", totalSaved)) |
| 285 | } |
| 286 | return out |
| 287 | } |
| 288 | |
| 289 | // imageFileExtensions are the extensions we treat as vision attachments. |
| 290 | var imageFileExtensions = map[string]bool{ |
no test coverage detected