gac 1.2.0__py3-none-any.whl → 1.2.1__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 CHANGED
@@ -1,3 +1,3 @@
1
1
  """Version information for gac package."""
2
2
 
3
- __version__ = "1.2.0"
3
+ __version__ = "1.2.1"
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
- raise AIError.authentication_error(f"AI generation failed: {str(e)}") from e
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
- raise AIError.model_error(
228
- f"AI generation failed after {max_retries} attempts: {str(last_exception)}"
229
- ) from last_exception
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.debug(f"Choice structure: {choice}")
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gac
3
- Version: 1.2.0
3
+ Version: 1.2.1
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
@@ -1,13 +1,13 @@
1
1
  gac/__init__.py,sha256=HFWgSVNbTAFhgetCRWI1WrtyE7zC7IHvoBOrfDGUurM,989
2
- gac/__version__.py,sha256=_cfu33DyI3jZln3bUKxhnDn1AeCj66_yLEqy9IJASqs,66
3
- gac/ai.py,sha256=RcZKXiyx9Wll2e-dRx0jNzQPzojVYE7OaSEDclE2MKc,2979
4
- gac/ai_utils.py,sha256=p63lxYp4AqzBgbYkYyNUKLecIszTVvBp4Wkm2hKlMlc,7854
2
+ gac/__version__.py,sha256=mlHECiUCezS8zokbb1QH5XRF5CbT1gnkLsHOUqsJMpQ,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=3vIRMQ2QF3sP9_rPfXAFuu5ZSjIVX4FxM-FAuiR8N-8,7416
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=EPivjTg3TUqynBofnatlIxKzFTpLPP4psVb562Dsx5o,2040
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
23
  gac/providers/openrouter.py,sha256=Vs0MXfv9KCldfEUD2roTwcXqs89tgE3ndNqRKoqdJQs,1473
24
- gac-1.2.0.dist-info/METADATA,sha256=fbK0j24cpsMWVJXnybrXyQ6MzMS791bpdIKu2vvG7-c,8558
25
- gac-1.2.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
26
- gac-1.2.0.dist-info/entry_points.txt,sha256=tdjN-XMmcWfL92swuRAjT62bFLOAwk9bTMRLGP5Z4aI,36
27
- gac-1.2.0.dist-info/licenses/LICENSE,sha256=vOab37NouL1PNs5BswnPayrMCqaN2sqLfMQfqPDrpZg,1103
28
- gac-1.2.0.dist-info/RECORD,,
24
+ gac-1.2.1.dist-info/METADATA,sha256=RWaYfOmCltyXsiO0a3Fuz1Qy-0Msr7tAWySzG2OdXsw,8558
25
+ gac-1.2.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
26
+ gac-1.2.1.dist-info/entry_points.txt,sha256=tdjN-XMmcWfL92swuRAjT62bFLOAwk9bTMRLGP5Z4aI,36
27
+ gac-1.2.1.dist-info/licenses/LICENSE,sha256=vOab37NouL1PNs5BswnPayrMCqaN2sqLfMQfqPDrpZg,1103
28
+ gac-1.2.1.dist-info/RECORD,,
File without changes