Skip to content

Commit 3dc1479

Browse files
authored
Merge pull request #45 from Muneer320/main
2 parents 64baf24 + 2b056dd commit 3dc1479

File tree

5 files changed

+245
-9
lines changed

5 files changed

+245
-9
lines changed

CONTRIBUTING.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ Improving documentation is always appreciated:
8181
- Git
8282
- API key for either:
8383
- Google Gemini (`GEMINI_API_KEY`)
84+
- Groq (`GROQ_API_KEY`)
8485
- Grok (`GROK_API_KEY`)
8586
- Claude (`CLAUDE_API_KEY`)
8687

@@ -89,9 +90,11 @@ Improving documentation is always appreciated:
8990
1. Set up your environment variables:
9091

9192
```bash
92-
export COMMIT_LLM=gemini # or "grok"
93+
export COMMIT_LLM=gemini # or "groq" / "grok"
9394
export GEMINI_API_KEY=your-api-key-here
9495
# OR
96+
export GROQ_API_KEY=your-api-key-here
97+
# OR
9598
export GROK_API_KEY=your-api-key-here
9699
```
97100

README.md

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ Looking to contribute? Check out:
2828
## Features
2929

3030
**AI-Powered Commit Messages** - Automatically generate meaningful commit messages
31-
🔄 **Multiple LLM Support** - Choose between Google Gemini, Grok, Claude or ChatGPT
31+
🔄 **Multiple LLM Support** - Choose between Google Gemini, Groq, Grok, Claude or ChatGPT
3232
📝 **Context-Aware** - Analyzes staged and unstaged changes
3333
📋 **Auto-Copy to Clipboard** - Generated messages are automatically copied for instant use
3434
📊 **File Statistics Display** - Visual preview of changed files and line counts
@@ -41,13 +41,14 @@ You can use **Google Gemini**, **Grok**, **Claude**, or **ChatGPT** as the LLM t
4141

4242
### Environment Variables
4343

44-
| Variable | Values | Description |
45-
| :--- | :--- | :--- |
46-
| `COMMIT_LLM` | `gemini`, `grok`, `claude`, or `chatgpt` | Choose your LLM provider |
47-
| `GEMINI_API_KEY` | Your API key | Required if using Gemini |
48-
| `GROK_API_KEY` | Your API key | Required if using Grok |
49-
| `CLAUDE_API_KEY` | Your API key | Required if using Claude |
50-
| `OPENAI_API_KEY` | Your API key | Required if using ChatGPT |
44+
| Variable | Values | Description |
45+
| :--------------- | :----------------------------------------------- | :------------------------ |
46+
| `COMMIT_LLM` | `gemini`, `groq`, `grok`, `claude`, or `chatgpt` | Choose your LLM provider |
47+
| `GEMINI_API_KEY` | Your API key | Required if using Gemini |
48+
| `GROQ_API_KEY` | Your API key | Required if using Groq |
49+
| `GROK_API_KEY` | Your API key | Required if using Grok |
50+
| `CLAUDE_API_KEY` | Your API key | Required if using Claude |
51+
| `OPENAI_API_KEY` | Your API key | Required if using ChatGPT |
5152

5253
---
5354

@@ -173,6 +174,13 @@ commit .
173174
2. Generate an API key
174175
3. Set the `GROK_API_KEY` environment variable
175176

177+
**Groq:**
178+
179+
1. Sign up at [Groq Cloud](https://console.groq.com/)
180+
2. Create an API key
181+
3. Set the `GROQ_API_KEY` environment variable
182+
4. _(Optional)_ Set `GROQ_MODEL` or `GROQ_API_URL` to override defaults
183+
176184
**Claude (Anthropic):**
177185

178186
1. Visit the [Anthropic Console](https://console.anthropic.com/)

cmd/commit-msg/main.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"github.com/dfanso/commit-msg/internal/gemini"
1212
"github.com/dfanso/commit-msg/internal/git"
1313
"github.com/dfanso/commit-msg/internal/grok"
14+
"github.com/dfanso/commit-msg/internal/groq"
1415
"github.com/dfanso/commit-msg/internal/ollama"
1516
"github.com/dfanso/commit-msg/internal/stats"
1617
"github.com/dfanso/commit-msg/pkg/types"
@@ -40,6 +41,11 @@ func main() {
4041
if apiKey == "" {
4142
log.Fatalf("GROK_API_KEY is not set")
4243
}
44+
case "groq":
45+
apiKey = os.Getenv("GROQ_API_KEY")
46+
if apiKey == "" {
47+
log.Fatalf("GROQ_API_KEY is not set")
48+
}
4349
case "chatgpt":
4450
apiKey = os.Getenv("OPENAI_API_KEY")
4551
if apiKey == "" {
@@ -139,6 +145,8 @@ func main() {
139145
model = "llama3:latest"
140146
}
141147
commitMsg, err = ollama.GenerateCommitMessage(config, changes, url, model)
148+
case "groq":
149+
commitMsg, err = groq.GenerateCommitMessage(config, changes, apiKey)
142150
default:
143151
commitMsg, err = grok.GenerateCommitMessage(config, changes, apiKey)
144152
}

internal/groq/groq.go

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package groq
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"fmt"
7+
"io"
8+
"net/http"
9+
"os"
10+
"time"
11+
12+
"github.com/dfanso/commit-msg/pkg/types"
13+
)
14+
15+
type chatMessage struct {
16+
Role string `json:"role"`
17+
Content string `json:"content"`
18+
}
19+
20+
type chatRequest struct {
21+
Model string `json:"model"`
22+
Messages []chatMessage `json:"messages"`
23+
Temperature float64 `json:"temperature"`
24+
MaxTokens int `json:"max_tokens"`
25+
}
26+
27+
type chatChoice struct {
28+
Message chatMessage `json:"message"`
29+
}
30+
31+
type chatResponse struct {
32+
Choices []chatChoice `json:"choices"`
33+
}
34+
35+
// defaultModel uses Groq's recommended general-purpose model as of Oct 2025.
36+
// If Groq updates their defaults again, override via GROQ_MODEL.
37+
const defaultModel = "llama-3.3-70b-versatile"
38+
39+
var (
40+
// allow overrides in tests
41+
baseURL = "https://api.groq.com/openai/v1/chat/completions"
42+
httpClient = &http.Client{Timeout: 30 * time.Second}
43+
)
44+
45+
// GenerateCommitMessage calls Groq's OpenAI-compatible chat completions API.
46+
func GenerateCommitMessage(_ *types.Config, changes string, apiKey string) (string, error) {
47+
if changes == "" {
48+
return "", fmt.Errorf("no changes provided for commit message generation")
49+
}
50+
51+
prompt := fmt.Sprintf("%s\n\n%s", types.CommitPrompt, changes)
52+
53+
model := os.Getenv("GROQ_MODEL")
54+
if model == "" {
55+
model = defaultModel
56+
}
57+
58+
payload := chatRequest{
59+
Model: model,
60+
Temperature: 0.2,
61+
MaxTokens: 200,
62+
Messages: []chatMessage{
63+
{Role: "system", Content: "You are an assistant that writes clear, concise git commit messages."},
64+
{Role: "user", Content: prompt},
65+
},
66+
}
67+
68+
body, err := json.Marshal(payload)
69+
if err != nil {
70+
return "", fmt.Errorf("failed to marshal Groq request: %w", err)
71+
}
72+
73+
endpoint := baseURL
74+
if customEndpoint := os.Getenv("GROQ_API_URL"); customEndpoint != "" {
75+
endpoint = customEndpoint
76+
}
77+
78+
req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewBuffer(body))
79+
if err != nil {
80+
return "", fmt.Errorf("failed to create Groq request: %w", err)
81+
}
82+
83+
req.Header.Set("Content-Type", "application/json")
84+
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey))
85+
86+
resp, err := httpClient.Do(req)
87+
if err != nil {
88+
return "", fmt.Errorf("failed to call Groq API: %w", err)
89+
}
90+
defer resp.Body.Close()
91+
92+
responseBody, err := io.ReadAll(resp.Body)
93+
if err != nil {
94+
return "", fmt.Errorf("failed to read Groq response: %w", err)
95+
}
96+
97+
if resp.StatusCode != http.StatusOK {
98+
return "", fmt.Errorf("groq API returned status %d: %s", resp.StatusCode, string(responseBody))
99+
}
100+
101+
var completion chatResponse
102+
if err := json.Unmarshal(responseBody, &completion); err != nil {
103+
return "", fmt.Errorf("failed to decode Groq response: %w", err)
104+
}
105+
106+
if len(completion.Choices) == 0 || completion.Choices[0].Message.Content == "" {
107+
return "", fmt.Errorf("groq API returned empty response")
108+
}
109+
110+
return completion.Choices[0].Message.Content, nil
111+
}

internal/groq/groq_test.go

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package groq
2+
3+
import (
4+
"encoding/json"
5+
"net/http"
6+
"net/http/httptest"
7+
"testing"
8+
9+
"github.com/dfanso/commit-msg/pkg/types"
10+
)
11+
12+
type capturedRequest struct {
13+
Model string `json:"model"`
14+
Messages []chatMessage `json:"messages"`
15+
Temperature float64 `json:"temperature"`
16+
MaxTokens int `json:"max_tokens"`
17+
}
18+
19+
func withTestServer(t *testing.T, handler http.HandlerFunc, fn func()) {
20+
t.Helper()
21+
22+
t.Setenv("GROQ_API_URL", "")
23+
t.Setenv("GROQ_MODEL", "")
24+
25+
srv := httptest.NewServer(handler)
26+
t.Cleanup(srv.Close)
27+
28+
prevURL := baseURL
29+
prevClient := httpClient
30+
31+
baseURL = srv.URL
32+
httpClient = srv.Client()
33+
34+
t.Cleanup(func() {
35+
baseURL = prevURL
36+
httpClient = prevClient
37+
})
38+
39+
fn()
40+
}
41+
42+
func TestGenerateCommitMessageSuccess(t *testing.T) {
43+
withTestServer(t, func(w http.ResponseWriter, r *http.Request) {
44+
if r.Method != http.MethodPost {
45+
t.Fatalf("unexpected method: %s", r.Method)
46+
}
47+
48+
if got := r.Header.Get("Authorization"); got != "Bearer test-key" {
49+
t.Fatalf("unexpected authorization header: %s", got)
50+
}
51+
52+
var payload capturedRequest
53+
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
54+
t.Fatalf("failed to decode request: %v", err)
55+
}
56+
57+
if payload.Model != "llama-3.3-70b-versatile" {
58+
t.Fatalf("unexpected model: %s", payload.Model)
59+
}
60+
61+
if len(payload.Messages) != 2 {
62+
t.Fatalf("expected 2 messages, got %d", len(payload.Messages))
63+
}
64+
65+
resp := chatResponse{
66+
Choices: []chatChoice{
67+
{Message: chatMessage{Role: "assistant", Content: "Feat: add groq provider"}},
68+
},
69+
}
70+
71+
w.Header().Set("Content-Type", "application/json")
72+
if err := json.NewEncoder(w).Encode(resp); err != nil {
73+
t.Fatalf("failed to write response: %v", err)
74+
}
75+
}, func() {
76+
msg, err := GenerateCommitMessage(&types.Config{}, "diff", "test-key")
77+
if err != nil {
78+
t.Fatalf("GenerateCommitMessage returned error: %v", err)
79+
}
80+
81+
expected := "Feat: add groq provider"
82+
if msg != expected {
83+
t.Fatalf("expected %q, got %q", expected, msg)
84+
}
85+
})
86+
}
87+
88+
func TestGenerateCommitMessageNonOK(t *testing.T) {
89+
withTestServer(t, func(w http.ResponseWriter, r *http.Request) {
90+
http.Error(w, `{"error":"bad things"}`, http.StatusBadGateway)
91+
}, func() {
92+
_, err := GenerateCommitMessage(&types.Config{}, "changes", "key")
93+
if err == nil {
94+
t.Fatal("expected error but got nil")
95+
}
96+
})
97+
}
98+
99+
func TestGenerateCommitMessageEmptyChanges(t *testing.T) {
100+
t.Setenv("GROQ_MODEL", "")
101+
t.Setenv("GROQ_API_URL", "")
102+
103+
if _, err := GenerateCommitMessage(&types.Config{}, "", "key"); err == nil {
104+
t.Fatal("expected error for empty changes")
105+
}
106+
}

0 commit comments

Comments
 (0)