From 4dd5cfb2e0e2e6fc484ae318e6df8d0791bfae3c Mon Sep 17 00:00:00 2001 From: Matt Van Horn Date: Thu, 30 Apr 2026 02:45:52 -0700 Subject: [PATCH] feat: add MaxAttachments to Model struct and Gemini/Vertex configs (#258) Adds a per-model hard cap on image attachments so consumers (Crush and others) can stop maintaining their own per-provider lookup tables for image limits. Changes: - pkg/catwalk/provider.go Model gets a new MaxAttachments int64 field with json tag max_attachments,omitempty. Zero (the default) means no provider-enforced cap and the consumer should fall back to context-window sizing. A positive value is a hard cap the consumer must respect. - internal/providers/configs/gemini.json - internal/providers/configs/vertexai.json All Gemini models (6 in gemini.json, 11 in vertexai.json) get max_attachments: 10, matching Google's documented per-request limit. Anthropic / OpenAI / others stay unchanged so they keep the no-hard-limit zero default. The omitempty tag means existing JSON consumers that don't know about the field stay unaffected. New consumers can opt into the field as it suits them. Closes #258 --- internal/providers/configs/gemini.json | 18 ++++++++----- internal/providers/configs/vertexai.json | 33 ++++++++++++++++-------- pkg/catwalk/provider.go | 7 ++++- 3 files changed, 40 insertions(+), 18 deletions(-) diff --git a/internal/providers/configs/gemini.json b/internal/providers/configs/gemini.json index c8a34d7b..57dcebd9 100644 --- a/internal/providers/configs/gemini.json +++ b/internal/providers/configs/gemini.json @@ -19,7 +19,8 @@ "can_reason": true, "reasoning_levels": ["low", "medium", "high"], "default_reasoning_effort": "medium", - "supports_attachments": true + "supports_attachments": true, + "max_attachments": 10 }, { "id": "gemini-3.1-pro-preview-customtools", @@ -33,7 +34,8 @@ "can_reason": true, "reasoning_levels": ["low", "medium", "high"], "default_reasoning_effort": "medium", - "supports_attachments": true + "supports_attachments": true, + "max_attachments": 10 }, { "id": "gemini-3-pro-preview", @@ -47,7 +49,8 @@ "can_reason": true, "reasoning_levels": ["low", "high"], "default_reasoning_effort": "high", - "supports_attachments": true + "supports_attachments": true, + "max_attachments": 10 }, { "id": "gemini-3-flash-preview", @@ -61,7 +64,8 @@ "can_reason": true, "reasoning_levels": ["minimal", "low", "medium", "high"], "default_reasoning_effort": "minimal", - "supports_attachments": true + "supports_attachments": true, + "max_attachments": 10 }, { "id": "gemini-2.5-pro", @@ -73,7 +77,8 @@ "context_window": 1048576, "default_max_tokens": 50000, "can_reason": true, - "supports_attachments": true + "supports_attachments": true, + "max_attachments": 10 }, { "id": "gemini-2.5-flash", @@ -85,7 +90,8 @@ "context_window": 1048576, "default_max_tokens": 50000, "can_reason": true, - "supports_attachments": true + "supports_attachments": true, + "max_attachments": 10 } ] } diff --git a/internal/providers/configs/vertexai.json b/internal/providers/configs/vertexai.json index b81b77e9..08a9887d 100644 --- a/internal/providers/configs/vertexai.json +++ b/internal/providers/configs/vertexai.json @@ -19,7 +19,8 @@ "can_reason": true, "reasoning_levels": ["low", "medium", "high"], "default_reasoning_effort": "medium", - "supports_attachments": true + "supports_attachments": true, + "max_attachments": 10 }, { "id": "gemini-3.1-pro-preview-customtools", @@ -33,7 +34,8 @@ "can_reason": true, "reasoning_levels": ["low", "medium", "high"], "default_reasoning_effort": "medium", - "supports_attachments": true + "supports_attachments": true, + "max_attachments": 10 }, { "id": "gemini-3-pro-preview", @@ -47,7 +49,8 @@ "can_reason": true, "reasoning_levels": ["low", "high"], "default_reasoning_effort": "high", - "supports_attachments": true + "supports_attachments": true, + "max_attachments": 10 }, { "id": "gemini-3-flash-preview", @@ -61,7 +64,8 @@ "can_reason": true, "reasoning_levels": ["minimal", "low", "medium", "high"], "default_reasoning_effort": "minimal", - "supports_attachments": true + "supports_attachments": true, + "max_attachments": 10 }, { "id": "gemini-2.5-pro", @@ -73,7 +77,8 @@ "context_window": 1048576, "default_max_tokens": 50000, "can_reason": true, - "supports_attachments": true + "supports_attachments": true, + "max_attachments": 10 }, { "id": "gemini-2.5-flash", @@ -85,7 +90,8 @@ "context_window": 1048576, "default_max_tokens": 50000, "can_reason": true, - "supports_attachments": true + "supports_attachments": true, + "max_attachments": 10 }, { "id": "claude-sonnet-4-6", @@ -99,7 +105,8 @@ "can_reason": true, "reasoning_levels": ["low", "medium", "high", "max"], "default_reasoning_effort": "medium", - "supports_attachments": true + "supports_attachments": true, + "max_attachments": 10 }, { "id": "claude-sonnet-4-5-20250929", @@ -111,7 +118,8 @@ "context_window": 200000, "default_max_tokens": 50000, "can_reason": true, - "supports_attachments": true + "supports_attachments": true, + "max_attachments": 10 }, { "id": "claude-opus-4-6", @@ -125,7 +133,8 @@ "can_reason": true, "reasoning_levels": ["low", "medium", "high", "max"], "default_reasoning_effort": "medium", - "supports_attachments": true + "supports_attachments": true, + "max_attachments": 10 }, { "id": "claude-opus-4-5-20251101", @@ -137,7 +146,8 @@ "context_window": 200000, "default_max_tokens": 50000, "can_reason": true, - "supports_attachments": true + "supports_attachments": true, + "max_attachments": 10 }, { "id": "claude-haiku-4-5-20251001", @@ -150,7 +160,8 @@ "default_max_tokens": 32000, "can_reason": true, "has_reasoning_efforts": false, - "supports_attachments": true + "supports_attachments": true, + "max_attachments": 10 } ] } diff --git a/pkg/catwalk/provider.go b/pkg/catwalk/provider.go index f107df35..7fa01738 100644 --- a/pkg/catwalk/provider.go +++ b/pkg/catwalk/provider.go @@ -91,7 +91,12 @@ type Model struct { ReasoningLevels []string `json:"reasoning_levels,omitempty"` DefaultReasoningEffort string `json:"default_reasoning_effort,omitempty"` SupportsImages bool `json:"supports_attachments"` - Options ModelOptions `json:"options,omitzero"` + // MaxAttachments is the per-request hard cap on image attachments for this + // model. Zero (the default) means no provider-enforced limit and consumers + // should fall back to context-window sizing. A positive value is a hard + // cap that consumers must respect (e.g. Gemini caps at 10). + MaxAttachments int64 `json:"max_attachments,omitempty"` + Options ModelOptions `json:"options,omitzero"` } // KnownProviders returns all the known inference providers.