@@ -37,14 +37,17 @@ const (
3737 colorError = "\033 [38;5;203m"
3838 colorStrong = "\033 [1m"
3939
40- maxAIContextChars = 16000
41- maxGroupingContextChars = 14000
42- maxDiffExcerptChars = 3500
43- minDiffExcerptChars = 600
44- diffHeadLines = 24
45- diffTailLines = 14
46- maxSemanticItems = 12
47- maxSampleLines = 8
40+ maxAIContextChars = 16000
41+ maxCommitRequestTokens = 1400
42+ maxGroupingRequestTokens = 1200
43+ maxDiffExcerptChars = 3500
44+ minDiffExcerptChars = 600
45+ diffHeadLines = 24
46+ diffTailLines = 14
47+ maxSemanticItems = 12
48+ maxSampleLines = 8
49+ estimatedCharsPerToken = 4
50+ maxGroqAttempts = 4
4851)
4952
5053type FileChange struct {
@@ -754,11 +757,11 @@ func generateCommitGroups(apiKey, model string, changes []FileChange) (string, e
754757 payload := chatCompletionRequest {
755758 Model : model ,
756759 Temperature : 0.1 ,
757- MaxTokens : 900 ,
760+ MaxTokens : 500 ,
758761 Messages : []chatMessage {
759762 {
760763 Role : "system" ,
761- Content : "You group changed files into logical git commits. Return JSON only. " +
764+ Content : "You group changed file paths into logical git commits. Use only path, status, and area metadata; do not assume diff contents . Return JSON only. " +
762765 `Use this schema: [{"label":"short category label","files":["path1","path2"]}]. ` +
763766 "Each file must appear at most once." ,
764767 },
@@ -769,18 +772,60 @@ func generateCommitGroups(apiKey, model string, changes []FileChange) (string, e
769772 },
770773 }
771774
775+ payload = limitChatPayload (payload , maxGroupingRequestTokens )
772776 return sendGroqChat (apiKey , payload )
773777}
774778
775779func buildGroupingPrompt (changes []FileChange ) string {
776780 var builder strings.Builder
777- builder .WriteString ("Group these changed files into logical commits.\n " )
778- builder .WriteString ("Prefer a small number of clear categories . Keep unrelated files separate.\n \n " )
779- builder .WriteString (buildManagedChangeContext (changes , maxGroupingContextChars ))
781+ builder .WriteString ("Group these changed files into logical commits from the file graph only .\n " )
782+ builder .WriteString ("Use directory structure, path names, extensions, status, and inferred area . Keep unrelated paths separate.\n \n " )
783+ builder .WriteString (buildChangedFilesGraph (changes ))
780784
781785 return builder .String ()
782786}
783787
788+ func buildChangedFilesGraph (changes []FileChange ) string {
789+ var builder strings.Builder
790+ builder .WriteString ("Changed file graph:\n " )
791+ for _ , change := range changes {
792+ builder .WriteString (formatGraphPath (change .FileName ))
793+ builder .WriteString (" [" )
794+ builder .WriteString (change .Status )
795+ builder .WriteString (", " )
796+ builder .WriteString (classifyFileArea (change .FileName ))
797+ builder .WriteString ("]" )
798+ builder .WriteString ("\n " )
799+ }
800+ return builder .String ()
801+ }
802+
803+ func formatGraphPath (path string ) string {
804+ parts := strings .Split (filepath .ToSlash (path ), "/" )
805+ if len (parts ) == 0 {
806+ return "- " + path
807+ }
808+
809+ var builder strings.Builder
810+ for index , part := range parts {
811+ if part == "" {
812+ continue
813+ }
814+ if builder .Len () == 0 {
815+ builder .WriteString ("- " )
816+ } else {
817+ builder .WriteString (" > " )
818+ }
819+ builder .WriteString (part )
820+ if index == len (parts )- 1 {
821+ builder .WriteString (" (" )
822+ builder .WriteString (path )
823+ builder .WriteString (")" )
824+ }
825+ }
826+ return builder .String ()
827+ }
828+
784829func parseCommitGroups (content string ) ([]commitGroupProposal , error ) {
785830 cleaned := strings .TrimSpace (content )
786831 cleaned = strings .TrimPrefix (cleaned , "```json" )
@@ -875,6 +920,7 @@ func generateCommitMessage(apiKey, model string, changes []FileChange) (string,
875920 },
876921 }
877922
923+ payload = limitChatPayload (payload , maxCommitRequestTokens )
878924 message , err := sendGroqChat (apiKey , payload )
879925 if err != nil {
880926 return "" , err
@@ -888,6 +934,7 @@ func generateCommitMessage(apiKey, model string, changes []FileChange) (string,
888934 chatMessage {Role : "assistant" , Content : message },
889935 chatMessage {Role : "user" , Content : "That subject is too vague or malformed. Generate one specific conventional commit subject using the file summaries and changed symbols. Do not include null, generic filler, or explanations." },
890936 )
937+ payload = limitChatPayload (payload , maxCommitRequestTokens )
891938 message , err = sendGroqChat (apiKey , payload )
892939 if err != nil {
893940 return "" , err
@@ -1507,7 +1554,141 @@ func truncateContext(text string, limit int) string {
15071554 return text [:limit - len (marker )] + marker
15081555}
15091556
1557+ func limitChatPayload (payload chatCompletionRequest , requestTokenBudget int ) chatCompletionRequest {
1558+ if requestTokenBudget <= 0 {
1559+ return payload
1560+ }
1561+
1562+ availableInputTokens := requestTokenBudget - payload .MaxTokens - 80
1563+ if availableInputTokens < 300 {
1564+ availableInputTokens = 300
1565+ }
1566+
1567+ for estimateChatInputTokens (payload .Messages ) > availableInputTokens {
1568+ userIndex := largestUserMessageIndex (payload .Messages )
1569+ if userIndex < 0 {
1570+ break
1571+ }
1572+
1573+ current := payload .Messages [userIndex ].Content
1574+ overTokens := estimateChatInputTokens (payload .Messages ) - availableInputTokens
1575+ cutChars := overTokens * estimatedCharsPerToken
1576+ target := len (current ) - cutChars
1577+ if target < 500 {
1578+ target = 500
1579+ }
1580+ if target >= len (current ) {
1581+ target = len (current ) - estimatedCharsPerToken
1582+ }
1583+ if target <= 0 {
1584+ break
1585+ }
1586+
1587+ payload .Messages [userIndex ].Content = truncateContext (current , target )
1588+ }
1589+
1590+ return payload
1591+ }
1592+
1593+ func largestUserMessageIndex (messages []chatMessage ) int {
1594+ largestIndex := - 1
1595+ largestSize := 0
1596+ for index , message := range messages {
1597+ if message .Role == "user" && len (message .Content ) > largestSize {
1598+ largestIndex = index
1599+ largestSize = len (message .Content )
1600+ }
1601+ }
1602+ return largestIndex
1603+ }
1604+
1605+ func estimateChatInputTokens (messages []chatMessage ) int {
1606+ total := 0
1607+ for _ , message := range messages {
1608+ total += estimateTokens (message .Role )
1609+ total += estimateTokens (message .Content )
1610+ total += 4
1611+ }
1612+ return total + 8
1613+ }
1614+
1615+ func estimateTokens (text string ) int {
1616+ if text == "" {
1617+ return 0
1618+ }
1619+
1620+ tokens := len (text ) / estimatedCharsPerToken
1621+ if len (text )% estimatedCharsPerToken != 0 {
1622+ tokens ++
1623+ }
1624+ return tokens
1625+ }
1626+
15101627func sendGroqChat (apiKey string , payload chatCompletionRequest ) (string , error ) {
1628+ var lastErr error
1629+ for attempt := 1 ; attempt <= maxGroqAttempts ; attempt ++ {
1630+ content , err := sendGroqChatOnce (apiKey , payload )
1631+ if err == nil {
1632+ return content , nil
1633+ }
1634+
1635+ lastErr = err
1636+ wait , ok := groqRetryDelay (err )
1637+ if ! ok || attempt == maxGroqAttempts {
1638+ return "" , err
1639+ }
1640+
1641+ if wait < 500 * time .Millisecond {
1642+ wait = 500 * time .Millisecond
1643+ }
1644+ if wait > 30 * time .Second {
1645+ wait = 30 * time .Second
1646+ }
1647+ time .Sleep (wait )
1648+ }
1649+
1650+ return "" , lastErr
1651+ }
1652+
1653+ type groqHTTPError struct {
1654+ statusCode int
1655+ status string
1656+ message string
1657+ }
1658+
1659+ func (err groqHTTPError ) Error () string {
1660+ if err .message != "" {
1661+ return err .message
1662+ }
1663+ return "groq request failed with status " + err .status
1664+ }
1665+
1666+ func groqRetryDelay (err error ) (time.Duration , bool ) {
1667+ var httpErr groqHTTPError
1668+ if ! errors .As (err , & httpErr ) {
1669+ return 0 , false
1670+ }
1671+ if httpErr .statusCode != http .StatusTooManyRequests && ! strings .Contains (strings .ToLower (httpErr .message ), "rate limit" ) {
1672+ return 0 , false
1673+ }
1674+
1675+ lower := strings .ToLower (httpErr .message )
1676+ marker := "try again in "
1677+ if index := strings .Index (lower , marker ); index >= 0 {
1678+ remainder := httpErr .message [index + len (marker ):]
1679+ fields := strings .Fields (remainder )
1680+ if len (fields ) > 0 {
1681+ rawDuration := strings .TrimRight (fields [0 ], ".,;" )
1682+ if delay , parseErr := time .ParseDuration (rawDuration ); parseErr == nil {
1683+ return delay + 250 * time .Millisecond , true
1684+ }
1685+ }
1686+ }
1687+
1688+ return 5 * time .Second , true
1689+ }
1690+
1691+ func sendGroqChatOnce (apiKey string , payload chatCompletionRequest ) (string , error ) {
15111692 body , err := json .Marshal (payload )
15121693 if err != nil {
15131694 return "" , err
@@ -1540,9 +1721,9 @@ func sendGroqChat(apiKey string, payload chatCompletionRequest) (string, error)
15401721
15411722 if resp .StatusCode >= 400 {
15421723 if completion .Error != nil && completion .Error .Message != "" {
1543- return "" , errors . New ( completion .Error .Message )
1724+ return "" , groqHTTPError { statusCode : resp . StatusCode , status : resp . Status , message : completion .Error .Message }
15441725 }
1545- return "" , fmt . Errorf ( "groq request failed with status %s" , resp .Status )
1726+ return "" , groqHTTPError { statusCode : resp . StatusCode , status : resp .Status }
15461727 }
15471728
15481729 if len (completion .Choices ) == 0 {
0 commit comments