Skip to content

Commit dac7a36

Browse files
committed
Add token limit handling to prevent 8k token overflow
Problem: - GitHub Models API has an 8k token limit for entire requests - Large git diffs can exceed this limit, causing API failures - Users experience failures when staging large changes Solution: - Added token estimation using character-based heuristic (1 token ≈ 4 chars) - Implemented truncation logic that preserves UTF-8 boundaries - Added intelligent content prioritization when over limit Implementation Details: - estimateTokens(): Approximates tokens for any text content - truncateToTokenLimit(): Safely truncates text with ellipsis indicator - Modified GenerateCommitMessage() to: * Estimate tokens for prompt templates + changes + examples * Reserve tokens for templates (with buffer) * Prioritize examples (20% of remaining tokens) when present * Truncate changes to fit remaining budget * Display warning when truncation occurs Benefits: - Prevents API failures from token overflow - Maintains functionality by preserving maximum content - User-friendly with clear truncation warnings - No external dependencies, follows existing code style - Gracefully handles both changes-only and changes+examples scenarios
1 parent d5f0b8c commit dac7a36

File tree

1 file changed

+105
-3
lines changed

1 file changed

+105
-3
lines changed

internal/llm/client.go

Lines changed: 105 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,47 @@ import (
1818
//go:embed commitmsg.prompt.yml
1919
var commitmsgPromptYAML []byte
2020

21+
const (
22+
maxTokens = 8000
23+
tokensPerChar = 0.25 // 1 token ≈ 4 characters
24+
)
25+
26+
// estimateTokens approximates the number of tokens in text using a simple character-based heuristic.
27+
func estimateTokens(text string) int {
28+
return int(float64(len(text)) * tokensPerChar)
29+
}
30+
31+
// truncateToTokenLimit truncates text to fit within the specified token limit.
32+
// Preserves UTF-8 boundaries and adds ellipsis when truncation occurs.
33+
func truncateToTokenLimit(text string, maxTokens int) string {
34+
if estimateTokens(text) <= maxTokens {
35+
return text
36+
}
37+
38+
// Convert maxTokens to approximate character limit
39+
maxChars := int(float64(maxTokens) / tokensPerChar)
40+
41+
// Ensure we don't exceed the string length
42+
if maxChars >= len(text) {
43+
return text
44+
}
45+
46+
// Find a safe UTF-8 boundary near the limit
47+
runes := []rune(text)
48+
targetLen := maxChars
49+
if targetLen > len(runes) {
50+
targetLen = len(runes)
51+
}
52+
53+
// Reserve space for ellipsis if we're truncating
54+
if targetLen > 3 {
55+
targetLen -= 3
56+
}
57+
58+
truncated := string(runes[:targetLen]) + "..."
59+
return truncated
60+
}
61+
2162
// PromptConfig represents the structure of the prompt configuration file.
2263
type PromptConfig struct {
2364
Name string `yaml:"name"`
@@ -103,17 +144,78 @@ func (c *Client) GenerateCommitMessage(
103144
selectedModel := model
104145
selectedLanguage := language
105146

147+
// Estimate tokens and truncate if necessary to stay under 8k limit
148+
truncatedChanges := changesSummary
149+
truncatedExamples := examples
150+
151+
// Estimate tokens for the prompt template (without placeholders)
152+
promptTemplateTokens := 0
153+
for _, msg := range promptConfig.Messages {
154+
content := msg.Content
155+
content = strings.ReplaceAll(content, "{{changes}}", "")
156+
content = strings.ReplaceAll(content, "{{language}}", selectedLanguage)
157+
content = strings.ReplaceAll(content, "{{examples}}", "")
158+
promptTemplateTokens += estimateTokens(content)
159+
}
160+
161+
// Estimate tokens for changes and examples
162+
changesTokens := estimateTokens(changesSummary)
163+
examplesTokens := 0
164+
if examples != "" {
165+
examplesTokens = estimateTokens(createExamplesString(examples))
166+
}
167+
168+
totalTokens := promptTemplateTokens + changesTokens + examplesTokens
169+
170+
if totalTokens > maxTokens {
171+
fmt.Println(" Warning: Content truncated to fit token limit")
172+
173+
// Reserve tokens for prompt templates (add some buffer)
174+
reservedTokens := promptTemplateTokens + 500 // buffer for template processing
175+
176+
remainingTokens := maxTokens - reservedTokens
177+
if remainingTokens < 0 {
178+
remainingTokens = 0
179+
}
180+
181+
// Prioritize examples if present, otherwise use all remaining for changes
182+
if examplesTokens > 0 {
183+
// Reserve some tokens for examples (up to 20% of remaining)
184+
examplesReserved := int(float64(remainingTokens) * 0.2)
185+
if examplesReserved > examplesTokens {
186+
examplesReserved = examplesTokens
187+
}
188+
189+
remainingTokens -= examplesReserved
190+
191+
// Truncate examples if needed
192+
if examplesReserved < examplesTokens {
193+
truncatedExamples = truncateToTokenLimit(examples, examplesReserved)
194+
}
195+
196+
// Truncate changes with remaining tokens
197+
if remainingTokens < changesTokens {
198+
truncatedChanges = truncateToTokenLimit(changesSummary, remainingTokens)
199+
}
200+
} else {
201+
// No examples, use all remaining tokens for changes
202+
if remainingTokens < changesTokens {
203+
truncatedChanges = truncateToTokenLimit(changesSummary, remainingTokens)
204+
}
205+
}
206+
}
207+
106208
// Build messages from the prompt config, replacing template variables
107209
messages := make([]Message, len(promptConfig.Messages))
108210
for i, msg := range promptConfig.Messages {
109211
content := msg.Content
110212
// Replace the template variables
111-
content = strings.ReplaceAll(content, "{{changes}}", changesSummary)
213+
content = strings.ReplaceAll(content, "{{changes}}", truncatedChanges)
112214
content = strings.ReplaceAll(content, "{{language}}", selectedLanguage)
113215

114-
if examples != "" && strings.Contains(content, "{{examples}}") {
216+
if truncatedExamples != "" && strings.Contains(content, "{{examples}}") {
115217
// If examples are provided, replace the {{examples}} placeholder
116-
content = strings.ReplaceAll(content, "{{examples}}", createExamplesString(examples))
218+
content = strings.ReplaceAll(content, "{{examples}}", createExamplesString(truncatedExamples))
117219
} else {
118220
// If no examples are provided, remove the {{examples}} placeholder
119221
content = strings.ReplaceAll(content, "{{examples}}", "")

0 commit comments

Comments
 (0)