gac 3.6.0__py3-none-any.whl → 3.8.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.
Files changed (47) hide show
  1. gac/__version__.py +1 -1
  2. gac/ai_utils.py +47 -0
  3. gac/auth_cli.py +181 -36
  4. gac/cli.py +13 -0
  5. gac/config.py +54 -0
  6. gac/constants.py +7 -0
  7. gac/main.py +53 -11
  8. gac/model_cli.py +65 -10
  9. gac/oauth/__init__.py +26 -0
  10. gac/oauth/claude_code.py +87 -20
  11. gac/oauth/qwen_oauth.py +323 -0
  12. gac/oauth/token_store.py +81 -0
  13. gac/prompt.py +16 -4
  14. gac/providers/__init__.py +3 -0
  15. gac/providers/anthropic.py +11 -1
  16. gac/providers/azure_openai.py +5 -1
  17. gac/providers/cerebras.py +11 -1
  18. gac/providers/chutes.py +11 -1
  19. gac/providers/claude_code.py +11 -1
  20. gac/providers/custom_anthropic.py +5 -1
  21. gac/providers/custom_openai.py +5 -1
  22. gac/providers/deepseek.py +11 -1
  23. gac/providers/fireworks.py +11 -1
  24. gac/providers/gemini.py +11 -1
  25. gac/providers/groq.py +5 -1
  26. gac/providers/kimi_coding.py +5 -1
  27. gac/providers/lmstudio.py +12 -1
  28. gac/providers/minimax.py +11 -1
  29. gac/providers/mistral.py +11 -1
  30. gac/providers/moonshot.py +11 -1
  31. gac/providers/ollama.py +11 -1
  32. gac/providers/openai.py +11 -1
  33. gac/providers/openrouter.py +11 -1
  34. gac/providers/qwen.py +76 -0
  35. gac/providers/replicate.py +14 -2
  36. gac/providers/streamlake.py +11 -1
  37. gac/providers/synthetic.py +11 -1
  38. gac/providers/together.py +11 -1
  39. gac/providers/zai.py +11 -1
  40. gac/utils.py +30 -1
  41. gac/workflow_utils.py +3 -8
  42. {gac-3.6.0.dist-info → gac-3.8.1.dist-info}/METADATA +6 -4
  43. gac-3.8.1.dist-info/RECORD +56 -0
  44. gac-3.6.0.dist-info/RECORD +0 -53
  45. {gac-3.6.0.dist-info → gac-3.8.1.dist-info}/WHEEL +0 -0
  46. {gac-3.6.0.dist-info → gac-3.8.1.dist-info}/entry_points.txt +0 -0
  47. {gac-3.6.0.dist-info → gac-3.8.1.dist-info}/licenses/LICENSE +0 -0
gac/providers/moonshot.py CHANGED
@@ -1,10 +1,15 @@
1
1
  """Moonshot AI provider for gac."""
2
2
 
3
+ import logging
3
4
  import os
4
5
 
5
6
  import httpx
6
7
 
8
+ from gac.constants import ProviderDefaults
7
9
  from gac.errors import AIError
10
+ from gac.utils import get_ssl_verify
11
+
12
+ logger = logging.getLogger(__name__)
8
13
 
9
14
 
10
15
  def call_moonshot_api(model: str, messages: list[dict], temperature: float, max_tokens: int) -> str:
@@ -18,8 +23,12 @@ def call_moonshot_api(model: str, messages: list[dict], temperature: float, max_
18
23
 
19
24
  data = {"model": model, "messages": messages, "temperature": temperature, "max_tokens": max_tokens}
20
25
 
26
+ logger.debug(f"Calling Moonshot AI API with model={model}")
27
+
21
28
  try:
22
- response = httpx.post(url, headers=headers, json=data, timeout=120)
29
+ response = httpx.post(
30
+ url, headers=headers, json=data, timeout=ProviderDefaults.HTTP_TIMEOUT, verify=get_ssl_verify()
31
+ )
23
32
  response.raise_for_status()
24
33
  response_data = response.json()
25
34
  content = response_data["choices"][0]["message"]["content"]
@@ -27,6 +36,7 @@ def call_moonshot_api(model: str, messages: list[dict], temperature: float, max_
27
36
  raise AIError.model_error("Moonshot AI API returned null content")
28
37
  if content == "":
29
38
  raise AIError.model_error("Moonshot AI API returned empty content")
39
+ logger.debug("Moonshot AI API response received successfully")
30
40
  return content
31
41
  except httpx.HTTPStatusError as e:
32
42
  if e.response.status_code == 429:
gac/providers/ollama.py CHANGED
@@ -1,10 +1,15 @@
1
1
  """Ollama AI provider implementation."""
2
2
 
3
+ import logging
3
4
  import os
4
5
 
5
6
  import httpx
6
7
 
8
+ from gac.constants import ProviderDefaults
7
9
  from gac.errors import AIError
10
+ from gac.utils import get_ssl_verify
11
+
12
+ logger = logging.getLogger(__name__)
8
13
 
9
14
 
10
15
  def call_ollama_api(model: str, messages: list[dict], temperature: float, max_tokens: int) -> str:
@@ -18,8 +23,12 @@ def call_ollama_api(model: str, messages: list[dict], temperature: float, max_to
18
23
  if api_key:
19
24
  headers["Authorization"] = f"Bearer {api_key}"
20
25
 
26
+ logger.debug(f"Calling Ollama API with model={model}")
27
+
21
28
  try:
22
- response = httpx.post(url, headers=headers, json=data, timeout=120)
29
+ response = httpx.post(
30
+ url, headers=headers, json=data, timeout=ProviderDefaults.HTTP_TIMEOUT, verify=get_ssl_verify()
31
+ )
23
32
  response.raise_for_status()
24
33
  response_data = response.json()
25
34
 
@@ -37,6 +46,7 @@ def call_ollama_api(model: str, messages: list[dict], temperature: float, max_to
37
46
  raise AIError.model_error("Ollama API returned null content")
38
47
  if content == "":
39
48
  raise AIError.model_error("Ollama API returned empty content")
49
+ logger.debug("Ollama API response received successfully")
40
50
  return content
41
51
  except httpx.ConnectError as e:
42
52
  raise AIError.connection_error(f"Ollama connection failed. Make sure Ollama is running: {str(e)}") from e
gac/providers/openai.py CHANGED
@@ -1,10 +1,15 @@
1
1
  """OpenAI API provider for gac."""
2
2
 
3
+ import logging
3
4
  import os
4
5
 
5
6
  import httpx
6
7
 
8
+ from gac.constants import ProviderDefaults
7
9
  from gac.errors import AIError
10
+ from gac.utils import get_ssl_verify
11
+
12
+ logger = logging.getLogger(__name__)
8
13
 
9
14
 
10
15
  def call_openai_api(model: str, messages: list[dict], temperature: float, max_tokens: int) -> str:
@@ -18,8 +23,12 @@ def call_openai_api(model: str, messages: list[dict], temperature: float, max_to
18
23
 
19
24
  data = {"model": model, "messages": messages, "temperature": temperature, "max_completion_tokens": max_tokens}
20
25
 
26
+ logger.debug(f"Calling OpenAI API with model={model}")
27
+
21
28
  try:
22
- response = httpx.post(url, headers=headers, json=data, timeout=120)
29
+ response = httpx.post(
30
+ url, headers=headers, json=data, timeout=ProviderDefaults.HTTP_TIMEOUT, verify=get_ssl_verify()
31
+ )
23
32
  response.raise_for_status()
24
33
  response_data = response.json()
25
34
  content = response_data["choices"][0]["message"]["content"]
@@ -27,6 +36,7 @@ def call_openai_api(model: str, messages: list[dict], temperature: float, max_to
27
36
  raise AIError.model_error("OpenAI API returned null content")
28
37
  if content == "":
29
38
  raise AIError.model_error("OpenAI API returned empty content")
39
+ logger.debug("OpenAI API response received successfully")
30
40
  return content
31
41
  except httpx.HTTPStatusError as e:
32
42
  if e.response.status_code == 429:
@@ -1,10 +1,15 @@
1
1
  """OpenRouter API provider for gac."""
2
2
 
3
+ import logging
3
4
  import os
4
5
 
5
6
  import httpx
6
7
 
8
+ from gac.constants import ProviderDefaults
7
9
  from gac.errors import AIError
10
+ from gac.utils import get_ssl_verify
11
+
12
+ logger = logging.getLogger(__name__)
8
13
 
9
14
 
10
15
  def call_openrouter_api(model: str, messages: list[dict], temperature: float, max_tokens: int) -> str:
@@ -26,8 +31,12 @@ def call_openrouter_api(model: str, messages: list[dict], temperature: float, ma
26
31
  "max_tokens": max_tokens,
27
32
  }
28
33
 
34
+ logger.debug(f"Calling OpenRouter API with model={model}")
35
+
29
36
  try:
30
- response = httpx.post(url, headers=headers, json=data, timeout=120)
37
+ response = httpx.post(
38
+ url, headers=headers, json=data, timeout=ProviderDefaults.HTTP_TIMEOUT, verify=get_ssl_verify()
39
+ )
31
40
  response.raise_for_status()
32
41
  response_data = response.json()
33
42
  content = response_data["choices"][0]["message"]["content"]
@@ -35,6 +44,7 @@ def call_openrouter_api(model: str, messages: list[dict], temperature: float, ma
35
44
  raise AIError.model_error("OpenRouter API returned null content")
36
45
  if content == "":
37
46
  raise AIError.model_error("OpenRouter API returned empty content")
47
+ logger.debug("OpenRouter API response received successfully")
38
48
  return content
39
49
  except httpx.HTTPStatusError as e:
40
50
  # Handle specific HTTP status codes
gac/providers/qwen.py ADDED
@@ -0,0 +1,76 @@
1
+ """Qwen API provider for gac with OAuth support."""
2
+
3
+ import logging
4
+ import os
5
+
6
+ import httpx
7
+
8
+ from gac.constants import ProviderDefaults
9
+ from gac.errors import AIError
10
+ from gac.oauth import QwenOAuthProvider, TokenStore
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ QWEN_API_URL = "https://chat.qwen.ai/api/v1/chat/completions"
15
+
16
+
17
+ def get_qwen_auth() -> tuple[str, str]:
18
+ """Get Qwen authentication (API key or OAuth token).
19
+
20
+ Returns:
21
+ Tuple of (token, api_url) for authentication.
22
+ """
23
+ api_key = os.getenv("QWEN_API_KEY")
24
+ if api_key:
25
+ return api_key, QWEN_API_URL
26
+
27
+ oauth_provider = QwenOAuthProvider(TokenStore())
28
+ token = oauth_provider.get_token()
29
+ if token:
30
+ resource_url = token.get("resource_url")
31
+ if resource_url:
32
+ if not resource_url.startswith(("http://", "https://")):
33
+ resource_url = f"https://{resource_url}"
34
+ if not resource_url.endswith("/chat/completions"):
35
+ resource_url = resource_url.rstrip("/") + "/v1/chat/completions"
36
+ api_url = resource_url
37
+ else:
38
+ api_url = QWEN_API_URL
39
+ return token["access_token"], api_url
40
+
41
+ raise AIError.authentication_error(
42
+ "Qwen authentication not found. Set QWEN_API_KEY or run 'gac auth qwen login' for OAuth."
43
+ )
44
+
45
+
46
+ def call_qwen_api(model: str, messages: list[dict], temperature: float, max_tokens: int) -> str:
47
+ """Call Qwen API with OAuth or API key authentication."""
48
+ auth_token, api_url = get_qwen_auth()
49
+
50
+ headers = {"Authorization": f"Bearer {auth_token}", "Content-Type": "application/json"}
51
+
52
+ data = {"model": model, "messages": messages, "temperature": temperature, "max_tokens": max_tokens}
53
+
54
+ logger.debug(f"Calling Qwen API with model={model}")
55
+
56
+ try:
57
+ response = httpx.post(api_url, headers=headers, json=data, timeout=ProviderDefaults.HTTP_TIMEOUT)
58
+ response.raise_for_status()
59
+ response_data = response.json()
60
+ content = response_data["choices"][0]["message"]["content"]
61
+ if content is None:
62
+ raise AIError.model_error("Qwen API returned null content")
63
+ if content == "":
64
+ raise AIError.model_error("Qwen API returned empty content")
65
+ logger.debug("Qwen API response received successfully")
66
+ return content
67
+ except httpx.HTTPStatusError as e:
68
+ if e.response.status_code == 401:
69
+ raise AIError.authentication_error(f"Qwen authentication failed: {e.response.text}") from e
70
+ if e.response.status_code == 429:
71
+ raise AIError.rate_limit_error(f"Qwen API rate limit exceeded: {e.response.text}") from e
72
+ raise AIError.model_error(f"Qwen API error: {e.response.status_code} - {e.response.text}") from e
73
+ except httpx.TimeoutException as e:
74
+ raise AIError.timeout_error(f"Qwen API request timed out: {str(e)}") from e
75
+ except Exception as e:
76
+ raise AIError.model_error(f"Error calling Qwen API: {str(e)}") from e
@@ -1,10 +1,15 @@
1
1
  """Replicate API provider for gac."""
2
2
 
3
+ import logging
3
4
  import os
4
5
 
5
6
  import httpx
6
7
 
8
+ from gac.constants import ProviderDefaults
7
9
  from gac.errors import AIError
10
+ from gac.utils import get_ssl_verify
11
+
12
+ logger = logging.getLogger(__name__)
8
13
 
9
14
 
10
15
  def call_replicate_api(model: str, messages: list[dict], temperature: float, max_tokens: int) -> str:
@@ -50,9 +55,13 @@ def call_replicate_api(model: str, messages: list[dict], temperature: float, max
50
55
  },
51
56
  }
52
57
 
58
+ logger.debug(f"Calling Replicate API with model={model}")
59
+
53
60
  try:
54
61
  # Create prediction
55
- response = httpx.post(url, headers=headers, json=data, timeout=120)
62
+ response = httpx.post(
63
+ url, headers=headers, json=data, timeout=ProviderDefaults.HTTP_TIMEOUT, verify=get_ssl_verify()
64
+ )
56
65
  response.raise_for_status()
57
66
  prediction_data = response.json()
58
67
 
@@ -65,7 +74,9 @@ def call_replicate_api(model: str, messages: list[dict], temperature: float, max
65
74
  elapsed_time = 0
66
75
 
67
76
  while elapsed_time < max_wait_time:
68
- get_response = httpx.get(get_url, headers=headers, timeout=120)
77
+ get_response = httpx.get(
78
+ get_url, headers=headers, timeout=ProviderDefaults.HTTP_TIMEOUT, verify=get_ssl_verify()
79
+ )
69
80
  get_response.raise_for_status()
70
81
  status_data = get_response.json()
71
82
 
@@ -73,6 +84,7 @@ def call_replicate_api(model: str, messages: list[dict], temperature: float, max
73
84
  content = status_data["output"]
74
85
  if not content:
75
86
  raise AIError.model_error("Replicate API returned empty content")
87
+ logger.debug("Replicate API response received successfully")
76
88
  return content
77
89
  elif status_data["status"] == "failed":
78
90
  raise AIError.model_error(f"Replicate prediction failed: {status_data.get('error', 'Unknown error')}")
@@ -1,10 +1,15 @@
1
1
  """StreamLake (Vanchin) API provider for gac."""
2
2
 
3
+ import logging
3
4
  import os
4
5
 
5
6
  import httpx
6
7
 
8
+ from gac.constants import ProviderDefaults
7
9
  from gac.errors import AIError
10
+ from gac.utils import get_ssl_verify
11
+
12
+ logger = logging.getLogger(__name__)
8
13
 
9
14
 
10
15
  def call_streamlake_api(model: str, messages: list[dict], temperature: float, max_tokens: int) -> str:
@@ -25,8 +30,12 @@ def call_streamlake_api(model: str, messages: list[dict], temperature: float, ma
25
30
  "max_tokens": max_tokens,
26
31
  }
27
32
 
33
+ logger.debug(f"Calling StreamLake API with model={model}")
34
+
28
35
  try:
29
- response = httpx.post(url, headers=headers, json=data, timeout=120)
36
+ response = httpx.post(
37
+ url, headers=headers, json=data, timeout=ProviderDefaults.HTTP_TIMEOUT, verify=get_ssl_verify()
38
+ )
30
39
  response.raise_for_status()
31
40
  response_data = response.json()
32
41
  choices = response_data.get("choices")
@@ -40,6 +49,7 @@ def call_streamlake_api(model: str, messages: list[dict], temperature: float, ma
40
49
  if content == "":
41
50
  raise AIError.model_error("StreamLake API returned empty content")
42
51
 
52
+ logger.debug("StreamLake API response received successfully")
43
53
  return content
44
54
  except httpx.HTTPStatusError as e:
45
55
  if e.response.status_code == 429:
@@ -1,10 +1,15 @@
1
1
  """Synthetic.new API provider for gac."""
2
2
 
3
+ import logging
3
4
  import os
4
5
 
5
6
  import httpx
6
7
 
8
+ from gac.constants import ProviderDefaults
7
9
  from gac.errors import AIError
10
+ from gac.utils import get_ssl_verify
11
+
12
+ logger = logging.getLogger(__name__)
8
13
 
9
14
 
10
15
  def call_synthetic_api(model: str, messages: list[dict], temperature: float, max_tokens: int) -> str:
@@ -22,8 +27,12 @@ def call_synthetic_api(model: str, messages: list[dict], temperature: float, max
22
27
 
23
28
  data = {"model": model, "messages": messages, "temperature": temperature, "max_completion_tokens": max_tokens}
24
29
 
30
+ logger.debug(f"Calling Synthetic.new API with model={model}")
31
+
25
32
  try:
26
- response = httpx.post(url, headers=headers, json=data, timeout=120)
33
+ response = httpx.post(
34
+ url, headers=headers, json=data, timeout=ProviderDefaults.HTTP_TIMEOUT, verify=get_ssl_verify()
35
+ )
27
36
  response.raise_for_status()
28
37
  response_data = response.json()
29
38
  content = response_data["choices"][0]["message"]["content"]
@@ -31,6 +40,7 @@ def call_synthetic_api(model: str, messages: list[dict], temperature: float, max
31
40
  raise AIError.model_error("Synthetic.new API returned null content")
32
41
  if content == "":
33
42
  raise AIError.model_error("Synthetic.new API returned empty content")
43
+ logger.debug("Synthetic.new API response received successfully")
34
44
  return content
35
45
  except httpx.HTTPStatusError as e:
36
46
  if e.response.status_code == 429:
gac/providers/together.py CHANGED
@@ -1,10 +1,15 @@
1
1
  """Together AI API provider for gac."""
2
2
 
3
+ import logging
3
4
  import os
4
5
 
5
6
  import httpx
6
7
 
8
+ from gac.constants import ProviderDefaults
7
9
  from gac.errors import AIError
10
+ from gac.utils import get_ssl_verify
11
+
12
+ logger = logging.getLogger(__name__)
8
13
 
9
14
 
10
15
  def call_together_api(model: str, messages: list[dict], temperature: float, max_tokens: int) -> str:
@@ -18,8 +23,12 @@ def call_together_api(model: str, messages: list[dict], temperature: float, max_
18
23
 
19
24
  data = {"model": model, "messages": messages, "temperature": temperature, "max_tokens": max_tokens}
20
25
 
26
+ logger.debug(f"Calling Together AI API with model={model}")
27
+
21
28
  try:
22
- response = httpx.post(url, headers=headers, json=data, timeout=120)
29
+ response = httpx.post(
30
+ url, headers=headers, json=data, timeout=ProviderDefaults.HTTP_TIMEOUT, verify=get_ssl_verify()
31
+ )
23
32
  response.raise_for_status()
24
33
  response_data = response.json()
25
34
  content = response_data["choices"][0]["message"]["content"]
@@ -27,6 +36,7 @@ def call_together_api(model: str, messages: list[dict], temperature: float, max_
27
36
  raise AIError.model_error("Together AI API returned null content")
28
37
  if content == "":
29
38
  raise AIError.model_error("Together AI API returned empty content")
39
+ logger.debug("Together AI API response received successfully")
30
40
  return content
31
41
  except httpx.HTTPStatusError as e:
32
42
  if e.response.status_code == 429:
gac/providers/zai.py CHANGED
@@ -1,10 +1,15 @@
1
1
  """Z.AI API provider for gac."""
2
2
 
3
+ import logging
3
4
  import os
4
5
 
5
6
  import httpx
6
7
 
8
+ from gac.constants import ProviderDefaults
7
9
  from gac.errors import AIError
10
+ from gac.utils import get_ssl_verify
11
+
12
+ logger = logging.getLogger(__name__)
8
13
 
9
14
 
10
15
  def _call_zai_api_impl(
@@ -18,8 +23,12 @@ def _call_zai_api_impl(
18
23
  headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
19
24
  data = {"model": model, "messages": messages, "temperature": temperature, "max_tokens": max_tokens}
20
25
 
26
+ logger.debug(f"Calling {api_name} API with model={model}")
27
+
21
28
  try:
22
- response = httpx.post(url, headers=headers, json=data, timeout=120)
29
+ response = httpx.post(
30
+ url, headers=headers, json=data, timeout=ProviderDefaults.HTTP_TIMEOUT, verify=get_ssl_verify()
31
+ )
23
32
  response.raise_for_status()
24
33
  response_data = response.json()
25
34
 
@@ -32,6 +41,7 @@ def _call_zai_api_impl(
32
41
  raise AIError.model_error(f"{api_name} API returned null content")
33
42
  if content == "":
34
43
  raise AIError.model_error(f"{api_name} API returned empty content")
44
+ logger.debug(f"{api_name} API response received successfully")
35
45
  return content
36
46
  else:
37
47
  raise AIError.model_error(f"{api_name} API response missing content: {response_data}")
gac/utils.py CHANGED
@@ -2,16 +2,45 @@
2
2
 
3
3
  import locale
4
4
  import logging
5
+ import os
5
6
  import subprocess
6
7
  import sys
8
+ from functools import lru_cache
7
9
 
8
10
  from rich.console import Console
9
11
  from rich.theme import Theme
10
12
 
11
- from gac.constants import Logging
13
+ from gac.constants import EnvDefaults, Logging
12
14
  from gac.errors import GacError
13
15
 
14
16
 
17
+ @lru_cache(maxsize=1)
18
+ def should_skip_ssl_verification() -> bool:
19
+ """Return True when SSL certificate verification should be skipped.
20
+
21
+ This is useful for corporate environments with proxy servers that
22
+ intercept SSL traffic and cause certificate verification failures.
23
+
24
+ Can be enabled via:
25
+ - GAC_NO_VERIFY_SSL=true environment variable
26
+ - --no-verify-ssl CLI flag (which sets the env var)
27
+
28
+ Returns:
29
+ True if SSL verification should be skipped, False otherwise.
30
+ """
31
+ value = os.getenv("GAC_NO_VERIFY_SSL", str(EnvDefaults.NO_VERIFY_SSL))
32
+ return value.lower() in ("true", "1", "yes", "on")
33
+
34
+
35
+ def get_ssl_verify() -> bool:
36
+ """Get the SSL verification setting for httpx requests.
37
+
38
+ Returns:
39
+ True to verify SSL certificates (default), False to skip verification.
40
+ """
41
+ return not should_skip_ssl_verification()
42
+
43
+
15
44
  def setup_logging(
16
45
  log_level: int | str = Logging.DEFAULT_LEVEL,
17
46
  quiet: bool = False,
gac/workflow_utils.py CHANGED
@@ -3,6 +3,7 @@ import tempfile
3
3
  from pathlib import Path
4
4
 
5
5
  import click
6
+ from prompt_toolkit import prompt
6
7
  from rich.console import Console
7
8
  from rich.panel import Panel
8
9
 
@@ -157,13 +158,7 @@ def collect_interactive_answers(questions: list[str]) -> dict[str, str] | None:
157
158
  console.print(f"[bold blue]Question {i}:[/bold blue] {question}")
158
159
 
159
160
  try:
160
- answer = click.prompt(
161
- "Your answer",
162
- type=str,
163
- default="", # Allow empty input to skip
164
- show_default=False,
165
- prompt_suffix=": ",
166
- ).strip()
161
+ answer = prompt("Your answer: ").strip()
167
162
 
168
163
  # Handle special commands
169
164
  answer_lower = answer.lower()
@@ -183,7 +178,7 @@ def collect_interactive_answers(questions: list[str]) -> dict[str, str] | None:
183
178
  answers[question] = answer
184
179
  console.print("[dim]↳ Got it![/dim]")
185
180
 
186
- except click.Abort:
181
+ except KeyboardInterrupt:
187
182
  # User pressed Ctrl+C
188
183
  console.print("\n[yellow]⚠️ Interactive mode aborted by user[/yellow]")
189
184
  return None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gac
3
- Version: 3.6.0
3
+ Version: 3.8.1
4
4
  Summary: LLM-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
@@ -47,7 +47,7 @@ Description-Content-Type: text/markdown
47
47
  # 🚀 Git Auto Commit (gac)
48
48
 
49
49
  [![PyPI version](https://img.shields.io/pypi/v/gac.svg)](https://pypi.org/project/gac/)
50
- [![Python](https://img.shields.io/badge/python-3.10%20|%203.11%20|%203.12%20|%203.13%20|%203.14-blue.svg)](https://www.python.org/downloads/)
50
+ [![Python](https://img.shields.io/badge/python-3.10--3.14-blue.svg)](https://www.python.org/downloads/)
51
51
  [![Build Status](https://github.com/cellwebb/gac/actions/workflows/ci.yml/badge.svg)](https://github.com/cellwebb/gac/actions)
52
52
  [![codecov](https://codecov.io/gh/cellwebb/gac/branch/main/graph/badge.svg)](https://app.codecov.io/gh/cellwebb/gac)
53
53
  [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
@@ -109,8 +109,8 @@ uv tool upgrade gac
109
109
  - **Anthropic** • **Azure OpenAI** • **Cerebras** • **Chutes.ai** • **Claude Code (OAuth)**
110
110
  - **DeepSeek** • **Fireworks** • **Gemini** • **Groq** • **Kimi for Coding** • **LM Studio**
111
111
  - **MiniMax.io** • **Mistral AI** • **Moonshot AI** • **Ollama** • **OpenAI** • **OpenRouter**
112
- - **Replicate** • **Streamlake** • **Synthetic.new** • **Together AI** • **Z.AI** • **Z.AI Coding**
113
- - **Custom Endpoints (Anthropic/OpenAI)**
112
+ - **Qwen.ai (OAuth)** • **Replicate** • **Streamlake** • **Synthetic.new** • **Together AI**
113
+ - **Z.AI** • **Z.AI Coding** • **Custom Endpoints (Anthropic/OpenAI)**
114
114
 
115
115
  ### 🧠 **Smart LLM Analysis**
116
116
 
@@ -262,6 +262,8 @@ Track real-time installation metrics and package download statistics.
262
262
  ## Getting Help
263
263
 
264
264
  - **Full documentation**: [docs/USAGE.md](docs/en/USAGE.md) - Complete CLI reference
265
+ - **Claude Code OAuth**: [docs/CLAUDE_CODE.md](docs/en/CLAUDE_CODE.md) - Claude Code setup and authentication
266
+ - **Qwen.ai OAuth**: [docs/QWEN.md](docs/en/QWEN.md) - Qwen.ai setup and authentication
265
267
  - **Custom prompts**: [docs/CUSTOM_SYSTEM_PROMPTS.md](docs/en/CUSTOM_SYSTEM_PROMPTS.md) - Customize commit message style
266
268
  - **Troubleshooting**: [docs/TROUBLESHOOTING.md](docs/en/TROUBLESHOOTING.md) - Common issues and solutions
267
269
  - **Contributing**: [docs/CONTRIBUTING.md](docs/en/CONTRIBUTING.md) - Development setup and guidelines
@@ -0,0 +1,56 @@
1
+ gac/__init__.py,sha256=z9yGInqtycFIT3g1ca24r-A3699hKVaRqGUI79wsmMc,415
2
+ gac/__version__.py,sha256=KrwCaC6wbdQ2R2fD_kzJyUu5mtDSBRAe_FPDHQiGA4I,66
3
+ gac/ai.py,sha256=HnXmRFmUJin5k755iBqSLgKYssjShjKXz9SwICEpMag,3835
4
+ gac/ai_utils.py,sha256=FHN-cbU6_ofICqJOdw1TmN2z1lcW1KqVC8xXFF38HwA,11081
5
+ gac/auth_cli.py,sha256=D1UcpOdyLqnKNJQTBq2lHCHBaxFZsRbM4Krvg-1-NfA,6822
6
+ gac/cli.py,sha256=MgzcRwBrgq10uczRpWh3rIVTvBGCBdvldZloqEwWQRQ,7940
7
+ gac/config.py,sha256=dycwZfB7i0XyNA8qySyxH1UpV0Dkw_FsCluB9SqUcZQ,4667
8
+ gac/config_cli.py,sha256=o8UEEoWwZJ7xmDUbNPZEnbdANd1nLb4HMpmqMbcbbaY,2376
9
+ gac/constants.py,sha256=ckzm2yM2-DA_98TRpetX5mzQ28TqJPGtm0cEZnRj_A8,9958
10
+ gac/diff_cli.py,sha256=wnVQ9OFGnM0d2Pj9WVjWbo0jxqIuRHVAwmb8wU9Pa3E,5676
11
+ gac/errors.py,sha256=ysDIVRCd0YQVTOW3Q6YzdolxCdtkoQCAFf3_jrqbjUY,7916
12
+ gac/git.py,sha256=_NRkOyb6u8SiPrG-t-7GspjdSp7yptmNj1gT8VexmcY,12913
13
+ gac/init_cli.py,sha256=UbldjcEjypHHpAn49tMddzaFQtwwAjlf8ZBQVPoz9YQ,2299
14
+ gac/language_cli.py,sha256=NHg8Q2cAjt4-VOaIYdU5FL_ISvSyu5rbgs232CqfhzM,12929
15
+ gac/main.py,sha256=W9OafYQ5_QP87RWZTBEvdEu-aCf8MPRhF5XnnZ28lCo,44193
16
+ gac/model_cli.py,sha256=p5bkPxJrXd7vyN2vPP91I-TPyqRdvO3X1P-nw_95BAI,18342
17
+ gac/preprocess.py,sha256=hk2p2X4-xVDvuy-T1VMzMa9k5fTUbhlWDyw89DCf81Q,15379
18
+ gac/prompt.py,sha256=3pLc6Sfe6VVc3WBNg-Y7yaHcfhwn4aEi01Ro32CZqtQ,36262
19
+ gac/security.py,sha256=QT91mBEo2Y7la-aXvKuF2zhWuoOSXb6PWKLJ93kSy2k,9926
20
+ gac/utils.py,sha256=CiUSiGasX2myqjMaZPgrM_WZlCFhFogvdxhNTc3kn4w,12637
21
+ gac/workflow_utils.py,sha256=rGN7PqTqeUFLn260gm3hKyzF1PDVVuEsV1i2QHu2EO4,8118
22
+ gac/oauth/__init__.py,sha256=wwfeIMGgpIAb8-ptLcSDyK_sM72HI1bkULyJm7BwVs8,614
23
+ gac/oauth/claude_code.py,sha256=UqHrB2VbJsKeB2dq2T7qcScUPxCE1PaYCD5S3VwsLfU,14101
24
+ gac/oauth/qwen_oauth.py,sha256=Gs2L7VM8FopAE9-wzWyC64MNPmsBu8GrDI2vncqE0GI,10995
25
+ gac/oauth/token_store.py,sha256=d-MfgB3_hfqmn90hR73ajqNRgrPnoQLJN_Bra00Bfsw,2523
26
+ gac/providers/__init__.py,sha256=xUtR2-MZRrRVC0A51M6BsiN2cdth52BZVIEbDkH6Wdc,2969
27
+ gac/providers/anthropic.py,sha256=lqOmnHbHde6pifOogclPL9Wvb-4YPIB9wQDvI346dC0,2374
28
+ gac/providers/azure_openai.py,sha256=paH6LJL_mA-812QutfMbKmoPpenQMirolhQ7x70vtiE,4072
29
+ gac/providers/cerebras.py,sha256=WsqT5pTE-j7psOshddyIBAGdXD3Twxqtdym-gaM_-vQ,1984
30
+ gac/providers/chutes.py,sha256=xBoOcawwCKjqVk26W-yYfgewn-sCbBEet36U9PSYyNw,2955
31
+ gac/providers/claude_code.py,sha256=RwxOT7ARgRlUazPgU4mfzYpSW6P12NxttK-WfJJc6pw,4411
32
+ gac/providers/custom_anthropic.py,sha256=XdHv_OXeIi2SkIUCYeEV_q3InOvrYOHBZJ0P-rTJohM,5682
33
+ gac/providers/custom_openai.py,sha256=ImwWBcda5Gr51gPtBdUL2ApLHx3eEHV8e-9SfYxMaao,4095
34
+ gac/providers/deepseek.py,sha256=r3V5jPm0zPba8XIcZwswM1wXw8V6JWe5JVogmA6XsY8,1979
35
+ gac/providers/fireworks.py,sha256=v2qPfE3cSRQOIPrSiWRcD0aS-p-Cu82IdHXXOk9kIrQ,2032
36
+ gac/providers/gemini.py,sha256=GN6AsUbaidrM6blVD65TAE2hmACbOQBbIIW849jM0hI,3680
37
+ gac/providers/groq.py,sha256=r780wFe40niJ1ObxbYEU3SbR0Nsewrfxwmk51MFWTsk,2959
38
+ gac/providers/kimi_coding.py,sha256=PaCuqtBB3PWDYb36-tMfQUJpoLoqoMti5bMRhRYvVoU,2880
39
+ gac/providers/lmstudio.py,sha256=NTHig1naUm01R2GriGVAxozoPJulQZT2AK_kRlWEzSU,2561
40
+ gac/providers/minimax.py,sha256=-_mS5FajMLgXcz-K6-dhIo6Obk5ML4sMLduEQsvmGSs,1964
41
+ gac/providers/mistral.py,sha256=pUJ0mDbxK8WXH5AUJ8d2g5eoyI2FBkdnqbv4ijpP4Vw,1964
42
+ gac/providers/moonshot.py,sha256=UEEfgNhd2I3cDw2IwlC19u0ufLEM4AWgp0WW0PS2wmE,2004
43
+ gac/providers/ollama.py,sha256=OFKDnzFyzG7_OGPDgiK367-7FKuXmr84LFU9kL1iJjI,2427
44
+ gac/providers/openai.py,sha256=nsiOXljNfB_eeaRu09OIwHZpGuhF5WoXWCx99Hz7Kdo,1962
45
+ gac/providers/openrouter.py,sha256=tFmC9KfLcNAEFiiBxEuX5pzgVuXa6cdtUKmnKLtadpk,2544
46
+ gac/providers/qwen.py,sha256=xw0N2Tx0_FVFi_RPFxZgPcGlvexhpHSp7XVh7ej2Za4,2983
47
+ gac/providers/replicate.py,sha256=ScSz-Bzin77VG3XZowc-mIHA2Nj9B2NTeMPHmFZOFdY,4212
48
+ gac/providers/streamlake.py,sha256=_YAcEqmn6YFUzIN1NjKavGaqF5j1DyyrB5MlwB6omgY,2387
49
+ gac/providers/synthetic.py,sha256=U7XGtReX7extGOp_qvVrAcFcq4hdfcdAF2y7oURevBc,2201
50
+ gac/providers/together.py,sha256=Bg1cb4DCSjgdZoa2RDyt9hC9NYTcXZJ_b5MZ4vWlFp8,2009
51
+ gac/providers/zai.py,sha256=RC_wWOrs1OtPlkOVsBqQ0BqYF7DSymqHq6gZznhCEbk,3088
52
+ gac-3.8.1.dist-info/METADATA,sha256=A22DCTy3KBqB2BNifXjpFszAnov2TuRo0WgKF4uRpQg,11438
53
+ gac-3.8.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
54
+ gac-3.8.1.dist-info/entry_points.txt,sha256=tdjN-XMmcWfL92swuRAjT62bFLOAwk9bTMRLGP5Z4aI,36
55
+ gac-3.8.1.dist-info/licenses/LICENSE,sha256=vOab37NouL1PNs5BswnPayrMCqaN2sqLfMQfqPDrpZg,1103
56
+ gac-3.8.1.dist-info/RECORD,,
@@ -1,53 +0,0 @@
1
- gac/__init__.py,sha256=z9yGInqtycFIT3g1ca24r-A3699hKVaRqGUI79wsmMc,415
2
- gac/__version__.py,sha256=QjkK28WCwoefBMwiKqFYy76cOVTGklvGTI3wsZ1eIDk,66
3
- gac/ai.py,sha256=HnXmRFmUJin5k755iBqSLgKYssjShjKXz9SwICEpMag,3835
4
- gac/ai_utils.py,sha256=YpMEXCXkY1KqLIoYOmMGwVzTxUq-jof_v8VYbsgFMl0,8940
5
- gac/auth_cli.py,sha256=Bd2TSjoVruOwzcPjDvyjfuKtlBr_wEScf8AWsddKfU0,2224
6
- gac/cli.py,sha256=ZbUGyKHuxfW1p6zaiTl8FU_tIYaxPN36tCxLYBinDFk,7450
7
- gac/config.py,sha256=_EWAtHL9FMrvO4wD0ReZQEstKzhF1f2rbSm9dWCT350,2155
8
- gac/config_cli.py,sha256=o8UEEoWwZJ7xmDUbNPZEnbdANd1nLb4HMpmqMbcbbaY,2376
9
- gac/constants.py,sha256=nd8aiNCAsvP1OM4VWwXjonqA7RAtPwTheA__GrVuP5o,9700
10
- gac/diff_cli.py,sha256=wnVQ9OFGnM0d2Pj9WVjWbo0jxqIuRHVAwmb8wU9Pa3E,5676
11
- gac/errors.py,sha256=ysDIVRCd0YQVTOW3Q6YzdolxCdtkoQCAFf3_jrqbjUY,7916
12
- gac/git.py,sha256=_NRkOyb6u8SiPrG-t-7GspjdSp7yptmNj1gT8VexmcY,12913
13
- gac/init_cli.py,sha256=UbldjcEjypHHpAn49tMddzaFQtwwAjlf8ZBQVPoz9YQ,2299
14
- gac/language_cli.py,sha256=NHg8Q2cAjt4-VOaIYdU5FL_ISvSyu5rbgs232CqfhzM,12929
15
- gac/main.py,sha256=jL26ti3t3-3ykV20tJaOdNsDFPKJoqKZX1fg20uYLGM,41872
16
- gac/model_cli.py,sha256=DGI8V6QWyI07KeVYJMzXTMxqQg4crhnSfuOUYiZYygE,16013
17
- gac/preprocess.py,sha256=hk2p2X4-xVDvuy-T1VMzMa9k5fTUbhlWDyw89DCf81Q,15379
18
- gac/prompt.py,sha256=MzW-OzixAOltVcIVaRp6_hm8HscxRcmxj9Jh94P6Rvo,35454
19
- gac/security.py,sha256=QT91mBEo2Y7la-aXvKuF2zhWuoOSXb6PWKLJ93kSy2k,9926
20
- gac/utils.py,sha256=gTpc9zLzy3-3L5k-V5uSeFI-NnSPabA1GGVwSgpeMSk,11709
21
- gac/workflow_utils.py,sha256=-O4QRfY1bbv2lh1tpTaAEd_N0OE4eHV6E_vRvzBCFtk,8268
22
- gac/oauth/__init__.py,sha256=hPSGHlt-BFZXSodg-n6S405-5YZComyV_iEtzkPumdQ,46
23
- gac/oauth/claude_code.py,sha256=S4XsamRKOJoBwXo3GyhhcP5ypj3RE2akoBSo2yojtS8,12120
24
- gac/providers/__init__.py,sha256=th44JWjSQl8YgbLUmbRHXZS8jDXDFuBLeKpXDhL979w,2889
25
- gac/providers/anthropic.py,sha256=VK5d1s1PmBNDwh_x7illQ2CIZIHNIYU28btVfizwQPs,2036
26
- gac/providers/azure_openai.py,sha256=X1T1jtJDQmTw2Qm_I2onzEUDpfRQ49mj1WDGBZ3pXlI,3919
27
- gac/providers/cerebras.py,sha256=Ik8lhlsliGJVkgDgqlThfpra9tqbdYQZkaC4eNxRd9w,1648
28
- gac/providers/chutes.py,sha256=cclJOLuGVIiiaF-9Bs1kH6SSOhEmduGB2zZ86KIaXKw,2617
29
- gac/providers/claude_code.py,sha256=e_j0n0flN6m1UsY_v1lzwcy1miC9pynPY-1Fbq2akyw,4069
30
- gac/providers/custom_anthropic.py,sha256=aqtMdLnbwFjXkQzo7-pKfhIHktFuePVsnfDJOFJvDeM,5529
31
- gac/providers/custom_openai.py,sha256=kLRfClY6kdN8FGsFMYJVFCL82d34tnHZ9IPSZ2VPK2Q,3942
32
- gac/providers/deepseek.py,sha256=leT2S4_CE6JzwF3skDd4umBsu2rkJOJ66AfOdSL5wGc,1643
33
- gac/providers/fireworks.py,sha256=zsWhf6LMVdtsD9keXRFwgn9lCQigz6VmrDl6vqIVkdI,1688
34
- gac/providers/gemini.py,sha256=kl9WKdPm_ANYk0hsrUyMdACzR0cm8Eui9M1IwObYW-4,3348
35
- gac/providers/groq.py,sha256=9v2fAjDa_iRNHFptiUBN8Vt7ZDKkW_JOmIBeYvycD1M,2806
36
- gac/providers/kimi_coding.py,sha256=Nj8iojesCvCbwEEH3F8xNchIE_fmA1S2SMjGhD9arek,2727
37
- gac/providers/lmstudio.py,sha256=R82-f0tWdFfGQxLT6o3Q2tfvYguF7ESUg9DEUHNyrDk,2146
38
- gac/providers/minimax.py,sha256=oI5rEVlkcYenNUNH53zS00X8NqpcZ1gMsTGzQCsmes4,1630
39
- gac/providers/mistral.py,sha256=b2Du1nJutKjmJXmsXz4Ne43Yn52OSS0q6BKGoZnfH8Q,1630
40
- gac/providers/moonshot.py,sha256=Wy0RdYlKCkeRBv40jAJkybjXh420eb2btrTPTLioTbQ,1662
41
- gac/providers/ollama.py,sha256=hPkagbhEiAoH9RTET4EQe9-lTL0YmMRCbQ5dVbRQw6Q,2095
42
- gac/providers/openai.py,sha256=iHVD6bHf57W-QmW7u1Ee5vOpev7XZ-K75NcolLfebOk,1630
43
- gac/providers/openrouter.py,sha256=H3ce8JcRUYq1I30lOjGESdX7jfoPkW3mKAYnc2aYfBw,2204
44
- gac/providers/replicate.py,sha256=TjWZOIZBCq226zr_w0j0YpBxgtp0VY33yryd8q9akuo,3785
45
- gac/providers/streamlake.py,sha256=KAA2ZnpuEI5imzvdWVWUhEBHSP0BMnprKXte6CbwBWY,2047
46
- gac/providers/synthetic.py,sha256=sRMIJTS9LpcXd9A7qp_ZjZxdqtTKRn9fl1W4YwJZP4c,1855
47
- gac/providers/together.py,sha256=1bUIVHfYzcEDw4hQPE8qV6hjc2JNHPv_khVgpk2IJxI,1667
48
- gac/providers/zai.py,sha256=kywhhrCfPBu0rElZyb-iENxQxxpVGykvePuL4xrXlaU,2739
49
- gac-3.6.0.dist-info/METADATA,sha256=aPkvY8fwgU4gtKZojVzp3nbsfCpeWVLNWwM4lAbeQ7g,11254
50
- gac-3.6.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
51
- gac-3.6.0.dist-info/entry_points.txt,sha256=tdjN-XMmcWfL92swuRAjT62bFLOAwk9bTMRLGP5Z4aI,36
52
- gac-3.6.0.dist-info/licenses/LICENSE,sha256=vOab37NouL1PNs5BswnPayrMCqaN2sqLfMQfqPDrpZg,1103
53
- gac-3.6.0.dist-info/RECORD,,
File without changes