gac 1.2.0__py3-none-any.whl → 1.2.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of gac might be problematic. Click here for more details.
- gac/__version__.py +1 -1
- gac/ai.py +3 -0
- gac/ai_utils.py +25 -69
- gac/errors.py +5 -0
- gac/providers/groq.py +10 -3
- gac/providers/openrouter.py +0 -9
- {gac-1.2.0.dist-info → gac-1.2.2.dist-info}/METADATA +1 -3
- {gac-1.2.0.dist-info → gac-1.2.2.dist-info}/RECORD +11 -11
- {gac-1.2.0.dist-info → gac-1.2.2.dist-info}/WHEEL +0 -0
- {gac-1.2.0.dist-info → gac-1.2.2.dist-info}/entry_points.txt +0 -0
- {gac-1.2.0.dist-info → gac-1.2.2.dist-info}/licenses/LICENSE +0 -0
gac/__version__.py
CHANGED
gac/ai.py
CHANGED
|
@@ -81,6 +81,9 @@ def generate_commit_message(
|
|
|
81
81
|
max_retries=max_retries,
|
|
82
82
|
quiet=quiet,
|
|
83
83
|
)
|
|
84
|
+
except AIError:
|
|
85
|
+
# Re-raise AIError exceptions as-is to preserve error classification
|
|
86
|
+
raise
|
|
84
87
|
except Exception as e:
|
|
85
88
|
logger.error(f"Failed to generate commit message: {e}")
|
|
86
89
|
raise AIError.model_error(f"Failed to generate commit message: {e}") from e
|
gac/ai_utils.py
CHANGED
|
@@ -4,12 +4,10 @@ This module provides utility functions that support the AI provider implementati
|
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
import logging
|
|
7
|
-
import os
|
|
8
7
|
import time
|
|
9
8
|
from functools import lru_cache
|
|
10
9
|
from typing import Any
|
|
11
10
|
|
|
12
|
-
import httpx
|
|
13
11
|
import tiktoken
|
|
14
12
|
from halo import Halo
|
|
15
13
|
|
|
@@ -25,12 +23,6 @@ def count_tokens(content: str | list[dict[str, str]] | dict[str, Any], model: st
|
|
|
25
23
|
if not text:
|
|
26
24
|
return 0
|
|
27
25
|
|
|
28
|
-
if model.startswith("anthropic"):
|
|
29
|
-
anthropic_tokens = anthropic_count_tokens(text, model)
|
|
30
|
-
if anthropic_tokens is not None:
|
|
31
|
-
return anthropic_tokens
|
|
32
|
-
return len(text) // 4
|
|
33
|
-
|
|
34
26
|
try:
|
|
35
27
|
encoding = get_encoding(model)
|
|
36
28
|
return len(encoding.encode(text))
|
|
@@ -39,60 +31,6 @@ def count_tokens(content: str | list[dict[str, str]] | dict[str, Any], model: st
|
|
|
39
31
|
return len(text) // 4
|
|
40
32
|
|
|
41
33
|
|
|
42
|
-
def anthropic_count_tokens(text: str, model: str) -> int | None:
|
|
43
|
-
"""Call Anthropic's token count endpoint and return the token usage.
|
|
44
|
-
|
|
45
|
-
Returns the token count when successful, otherwise ``None`` so callers can
|
|
46
|
-
fall back to a heuristic estimate.
|
|
47
|
-
"""
|
|
48
|
-
api_key = os.getenv("ANTHROPIC_API_KEY")
|
|
49
|
-
if not api_key:
|
|
50
|
-
logger.debug("ANTHROPIC_API_KEY not set; using heuristic token estimation for Anthropic model")
|
|
51
|
-
return None
|
|
52
|
-
|
|
53
|
-
model_name = model.split(":", 1)[1] if ":" in model else "claude-3-5-haiku-latest"
|
|
54
|
-
headers = {
|
|
55
|
-
"Content-Type": "application/json",
|
|
56
|
-
"x-api-key": api_key,
|
|
57
|
-
"anthropic-version": "2023-06-01",
|
|
58
|
-
}
|
|
59
|
-
payload = {
|
|
60
|
-
"model": model_name,
|
|
61
|
-
"messages": [
|
|
62
|
-
{
|
|
63
|
-
"role": "user",
|
|
64
|
-
"content": [
|
|
65
|
-
{
|
|
66
|
-
"type": "text",
|
|
67
|
-
"text": text,
|
|
68
|
-
}
|
|
69
|
-
],
|
|
70
|
-
}
|
|
71
|
-
],
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
try:
|
|
75
|
-
response = httpx.post(
|
|
76
|
-
"https://api.anthropic.com/v1/messages/count_tokens",
|
|
77
|
-
headers=headers,
|
|
78
|
-
json=payload,
|
|
79
|
-
timeout=30.0,
|
|
80
|
-
)
|
|
81
|
-
response.raise_for_status()
|
|
82
|
-
data = response.json()
|
|
83
|
-
|
|
84
|
-
if "input_tokens" in data:
|
|
85
|
-
return data["input_tokens"]
|
|
86
|
-
if "usage" in data and "input_tokens" in data["usage"]:
|
|
87
|
-
return data["usage"]["input_tokens"]
|
|
88
|
-
|
|
89
|
-
logger.warning("Unexpected response format from Anthropic token count API: %s", data)
|
|
90
|
-
except Exception as exc:
|
|
91
|
-
logger.warning("Failed to retrieve Anthropic token count via HTTP: %s", exc)
|
|
92
|
-
|
|
93
|
-
return None
|
|
94
|
-
|
|
95
|
-
|
|
96
34
|
def extract_text_content(content: str | list[dict[str, str]] | dict[str, Any]) -> str:
|
|
97
35
|
"""Extract text content from various input formats."""
|
|
98
36
|
if isinstance(content, str):
|
|
@@ -172,6 +110,7 @@ def generate_with_retries(
|
|
|
172
110
|
spinner.start()
|
|
173
111
|
|
|
174
112
|
last_exception = None
|
|
113
|
+
last_error_type = "unknown"
|
|
175
114
|
|
|
176
115
|
for attempt in range(max_retries):
|
|
177
116
|
try:
|
|
@@ -190,20 +129,27 @@ def generate_with_retries(
|
|
|
190
129
|
if spinner:
|
|
191
130
|
spinner.succeed(f"Generated commit message with {provider} {model_name}")
|
|
192
131
|
|
|
193
|
-
if content:
|
|
132
|
+
if content is not None and content.strip():
|
|
194
133
|
return content.strip()
|
|
195
134
|
else:
|
|
135
|
+
logger.warning(f"Empty or None content received from {provider} {model_name}: {repr(content)}")
|
|
196
136
|
raise AIError.model_error("Empty response from AI model")
|
|
197
137
|
|
|
198
138
|
except Exception as e:
|
|
199
139
|
last_exception = e
|
|
200
140
|
error_type = _classify_error(str(e))
|
|
141
|
+
last_error_type = error_type
|
|
201
142
|
|
|
143
|
+
# For authentication and model errors, don't retry
|
|
202
144
|
if error_type in ["authentication", "model"]:
|
|
203
|
-
# Don't retry these errors
|
|
204
145
|
if spinner:
|
|
205
146
|
spinner.fail(f"Failed to generate commit message with {provider} {model_name}")
|
|
206
|
-
|
|
147
|
+
|
|
148
|
+
# Create the appropriate error type based on classification
|
|
149
|
+
if error_type == "authentication":
|
|
150
|
+
raise AIError.authentication_error(f"AI generation failed: {str(e)}") from e
|
|
151
|
+
elif error_type == "model":
|
|
152
|
+
raise AIError.model_error(f"AI generation failed: {str(e)}") from e
|
|
207
153
|
|
|
208
154
|
if attempt < max_retries - 1:
|
|
209
155
|
# Exponential backoff
|
|
@@ -223,7 +169,17 @@ def generate_with_retries(
|
|
|
223
169
|
if spinner:
|
|
224
170
|
spinner.fail(f"Failed to generate commit message with {provider} {model_name}")
|
|
225
171
|
|
|
226
|
-
# If we get here, all retries failed
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
172
|
+
# If we get here, all retries failed - use the last classified error type
|
|
173
|
+
error_message = f"Failed to generate commit message after {max_retries} attempts"
|
|
174
|
+
if last_error_type == "authentication":
|
|
175
|
+
raise AIError.authentication_error(error_message) from last_exception
|
|
176
|
+
elif last_error_type == "rate_limit":
|
|
177
|
+
raise AIError.rate_limit_error(error_message) from last_exception
|
|
178
|
+
elif last_error_type == "timeout":
|
|
179
|
+
raise AIError.timeout_error(error_message) from last_exception
|
|
180
|
+
elif last_error_type == "connection":
|
|
181
|
+
raise AIError.connection_error(error_message) from last_exception
|
|
182
|
+
elif last_error_type == "model":
|
|
183
|
+
raise AIError.model_error(error_message) from last_exception
|
|
184
|
+
else:
|
|
185
|
+
raise AIError.unknown_error(error_message) from last_exception
|
gac/errors.py
CHANGED
|
@@ -95,6 +95,11 @@ class AIError(GacError):
|
|
|
95
95
|
"""Create a model error."""
|
|
96
96
|
return cls(message, error_type="model")
|
|
97
97
|
|
|
98
|
+
@classmethod
|
|
99
|
+
def unknown_error(cls, message: str) -> "AIError":
|
|
100
|
+
"""Create an unknown error."""
|
|
101
|
+
return cls(message, error_type="unknown")
|
|
102
|
+
|
|
98
103
|
|
|
99
104
|
class FormattingError(GacError):
|
|
100
105
|
"""Error related to code formatting."""
|
gac/providers/groq.py
CHANGED
|
@@ -34,16 +34,23 @@ def call_groq_api(model: str, messages: list[dict], temperature: float, max_toke
|
|
|
34
34
|
choice = response_data["choices"][0]
|
|
35
35
|
if "message" in choice and "content" in choice["message"]:
|
|
36
36
|
content = choice["message"]["content"]
|
|
37
|
-
logger.debug(f"Found content in message.content: {content}")
|
|
37
|
+
logger.debug(f"Found content in message.content: {repr(content)}")
|
|
38
|
+
if content is None:
|
|
39
|
+
logger.warning("Groq API returned None content in message.content")
|
|
40
|
+
return ""
|
|
38
41
|
return content
|
|
39
42
|
elif "text" in choice:
|
|
40
43
|
content = choice["text"]
|
|
41
|
-
logger.debug(f"Found content in choice.text: {content}")
|
|
44
|
+
logger.debug(f"Found content in choice.text: {repr(content)}")
|
|
45
|
+
if content is None:
|
|
46
|
+
logger.warning("Groq API returned None content in choice.text")
|
|
47
|
+
return ""
|
|
42
48
|
return content
|
|
43
49
|
else:
|
|
44
|
-
logger.
|
|
50
|
+
logger.warning(f"Unexpected choice structure: {choice}")
|
|
45
51
|
|
|
46
52
|
# If we can't find content in the expected places, raise an error
|
|
53
|
+
logger.error(f"Unexpected response format from Groq API: {response_data}")
|
|
47
54
|
raise AIError.model_error(f"Unexpected response format from Groq API: {response_data}")
|
|
48
55
|
except httpx.HTTPStatusError as e:
|
|
49
56
|
raise AIError.model_error(f"Groq API error: {e.response.status_code} - {e.response.text}") from e
|
gac/providers/openrouter.py
CHANGED
|
@@ -19,15 +19,6 @@ def call_openrouter_api(model: str, messages: list[dict], temperature: float, ma
|
|
|
19
19
|
"Authorization": f"Bearer {api_key}",
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
# Add optional headers if environment variables are set
|
|
23
|
-
site_url = os.getenv("OPENROUTER_SITE_URL")
|
|
24
|
-
if site_url:
|
|
25
|
-
headers["HTTP-Referer"] = site_url
|
|
26
|
-
|
|
27
|
-
site_name = os.getenv("OPENROUTER_SITE_NAME")
|
|
28
|
-
if site_name:
|
|
29
|
-
headers["X-Title"] = site_name
|
|
30
|
-
|
|
31
22
|
data = {
|
|
32
23
|
"model": model,
|
|
33
24
|
"messages": messages,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gac
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.2
|
|
4
4
|
Summary: AI-powered Git commit message generator with multi-provider support
|
|
5
5
|
Project-URL: Homepage, https://github.com/cellwebb/gac
|
|
6
6
|
Project-URL: Documentation, https://github.com/cellwebb/gac#readme
|
|
@@ -139,8 +139,6 @@ ANTHROPIC_API_KEY=your_anthropic_key_here
|
|
|
139
139
|
# Optional: configure OpenRouter
|
|
140
140
|
# GAC_MODEL=openrouter:openrouter/auto
|
|
141
141
|
# OPENROUTER_API_KEY=your_openrouter_key_here
|
|
142
|
-
# OPENROUTER_SITE_URL=https://example.com
|
|
143
|
-
# OPENROUTER_SITE_NAME=Example App
|
|
144
142
|
```
|
|
145
143
|
|
|
146
144
|
Alternatively, you can configure `gac` using environment variables or by manually creating/editing the configuration file.
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
gac/__init__.py,sha256=HFWgSVNbTAFhgetCRWI1WrtyE7zC7IHvoBOrfDGUurM,989
|
|
2
|
-
gac/__version__.py,sha256=
|
|
3
|
-
gac/ai.py,sha256=
|
|
4
|
-
gac/ai_utils.py,sha256=
|
|
2
|
+
gac/__version__.py,sha256=iNS1DVqJb74bO6_PxrsUIrOdhcYnsH1F80ZnErn3N7A,66
|
|
3
|
+
gac/ai.py,sha256=iW7DqVoWGaHJeUWhkcYgFxJZHEkvNMcKgnEFQiBT_Dg,3090
|
|
4
|
+
gac/ai_utils.py,sha256=4qr1Jpm89ND5avqWQoQIjyc-zS-CPLoODjlhI44l8M8,7079
|
|
5
5
|
gac/cli.py,sha256=eQS8S7v6p0CfN9wtr239ujYGTi9rKl-KV7STX2U-C3w,4581
|
|
6
6
|
gac/config.py,sha256=wSgEDjtis7Vk1pv5VPvYmJyD9-tymDS6GiUHjnCMbIM,1486
|
|
7
7
|
gac/config_cli.py,sha256=v9nFHZO1RvK9fzHyuUS6SG-BCLHMsdOMDwWamBhVVh4,1608
|
|
8
8
|
gac/constants.py,sha256=MAxdASGncfZY1TdKGdhJZ0wvTBEU3gTN6KEdw8n3Bd8,4844
|
|
9
9
|
gac/diff_cli.py,sha256=wnVQ9OFGnM0d2Pj9WVjWbo0jxqIuRHVAwmb8wU9Pa3E,5676
|
|
10
|
-
gac/errors.py,sha256=
|
|
10
|
+
gac/errors.py,sha256=kwKxBhBpdgESRftUdN8RBiGDKE7E_H7nFN5-JVNyZE4,7578
|
|
11
11
|
gac/git.py,sha256=MS2m4fv8h4mau1djFG1aje9NXTmkGsjPO9w18LqNGX0,6031
|
|
12
12
|
gac/init_cli.py,sha256=e4z9-4NhoUn2DUyApIru8JR-W7HuNq2VeeXkR9aXHLo,1868
|
|
13
13
|
gac/main.py,sha256=POay7l6ihm3oF9ajGWx9cA40Pu-NVz5x_OzQOYPDoX8,12011
|
|
@@ -17,12 +17,12 @@ gac/utils.py,sha256=W3ladtmsH01MNLdckQYTzYrYbTGEdzCKI36he9C-y_E,3945
|
|
|
17
17
|
gac/providers/__init__.py,sha256=iGwZmV-cFqL3AeFE0vc6KpHwm-RLWcVSU17c7IvJg2s,456
|
|
18
18
|
gac/providers/anthropic.py,sha256=esf6pq6nRdqD0mpKz_IQNXmXe5WnkoSA2b1isrrRB4o,1514
|
|
19
19
|
gac/providers/cerebras.py,sha256=eE9lAjEzrATIo941vv97I2DSmpnXYBCJ9HkVIb-6Whg,1130
|
|
20
|
-
gac/providers/groq.py,sha256=
|
|
20
|
+
gac/providers/groq.py,sha256=Z-j-RKrRGV7evSWxTyKKnPff1Mn5YmYZxitqWVlwadE,2452
|
|
21
21
|
gac/providers/ollama.py,sha256=Bp94DvortQssDhekuNdJ7fKLeWpWASYXSssJNCuGszg,1383
|
|
22
22
|
gac/providers/openai.py,sha256=1l-Wu7ETXXaJ7cNB3OD5ivf4_72iIEP9bPFMQst8JWI,1109
|
|
23
|
-
gac/providers/openrouter.py,sha256=
|
|
24
|
-
gac-1.2.
|
|
25
|
-
gac-1.2.
|
|
26
|
-
gac-1.2.
|
|
27
|
-
gac-1.2.
|
|
28
|
-
gac-1.2.
|
|
23
|
+
gac/providers/openrouter.py,sha256=x9jGhvHHbApCT859bSAUZrkpgQKfQTHUIFSlWYPvlfE,1196
|
|
24
|
+
gac-1.2.2.dist-info/METADATA,sha256=4vc5SEzjFTwQsR0NZjMzEHaoVGNWhShTkttWnrqsgTc,8481
|
|
25
|
+
gac-1.2.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
26
|
+
gac-1.2.2.dist-info/entry_points.txt,sha256=tdjN-XMmcWfL92swuRAjT62bFLOAwk9bTMRLGP5Z4aI,36
|
|
27
|
+
gac-1.2.2.dist-info/licenses/LICENSE,sha256=vOab37NouL1PNs5BswnPayrMCqaN2sqLfMQfqPDrpZg,1103
|
|
28
|
+
gac-1.2.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|