gac 1.5.1__py3-none-any.whl → 1.6.0__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/__init__.py +0 -12
- gac/__version__.py +1 -1
- gac/ai.py +8 -4
- gac/ai_utils.py +2 -0
- gac/init_cli.py +70 -8
- gac/main.py +1 -1
- gac/providers/__init__.py +4 -0
- gac/providers/anthropic.py +5 -1
- gac/providers/cerebras.py +5 -1
- gac/providers/gemini.py +5 -1
- gac/providers/groq.py +5 -1
- gac/providers/lmstudio.py +4 -0
- gac/providers/ollama.py +4 -0
- gac/providers/openai.py +5 -1
- gac/providers/openrouter.py +3 -1
- gac/providers/streamlake.py +51 -0
- gac/providers/synthetic.py +38 -0
- gac/providers/zai.py +5 -1
- {gac-1.5.1.dist-info → gac-1.6.0.dist-info}/METADATA +9 -8
- gac-1.6.0.dist-info/RECORD +34 -0
- gac-1.5.1.dist-info/RECORD +0 -32
- {gac-1.5.1.dist-info → gac-1.6.0.dist-info}/WHEEL +0 -0
- {gac-1.5.1.dist-info → gac-1.6.0.dist-info}/entry_points.txt +0 -0
- {gac-1.5.1.dist-info → gac-1.6.0.dist-info}/licenses/LICENSE +0 -0
gac/__init__.py
CHANGED
|
@@ -4,12 +4,6 @@ from gac.__version__ import __version__
|
|
|
4
4
|
from gac.ai import generate_commit_message
|
|
5
5
|
from gac.git import get_staged_files, push_changes
|
|
6
6
|
from gac.prompt import build_prompt, clean_commit_message
|
|
7
|
-
from gac.providers.anthropic import call_anthropic_api as anthropic_generate
|
|
8
|
-
from gac.providers.cerebras import call_cerebras_api as cerebras_generate
|
|
9
|
-
from gac.providers.groq import call_groq_api as groq_generate
|
|
10
|
-
from gac.providers.ollama import call_ollama_api as ollama_generate
|
|
11
|
-
from gac.providers.openai import call_openai_api as openai_generate
|
|
12
|
-
from gac.providers.openrouter import call_openrouter_api as openrouter_generate
|
|
13
7
|
|
|
14
8
|
__all__ = [
|
|
15
9
|
"__version__",
|
|
@@ -18,10 +12,4 @@ __all__ = [
|
|
|
18
12
|
"clean_commit_message",
|
|
19
13
|
"get_staged_files",
|
|
20
14
|
"push_changes",
|
|
21
|
-
"anthropic_generate",
|
|
22
|
-
"cerebras_generate",
|
|
23
|
-
"groq_generate",
|
|
24
|
-
"ollama_generate",
|
|
25
|
-
"openai_generate",
|
|
26
|
-
"openrouter_generate",
|
|
27
15
|
]
|
gac/__version__.py
CHANGED
gac/ai.py
CHANGED
|
@@ -18,6 +18,8 @@ from gac.providers import (
|
|
|
18
18
|
call_ollama_api,
|
|
19
19
|
call_openai_api,
|
|
20
20
|
call_openrouter_api,
|
|
21
|
+
call_streamlake_api,
|
|
22
|
+
call_synthetic_api,
|
|
21
23
|
call_zai_api,
|
|
22
24
|
call_zai_coding_api,
|
|
23
25
|
)
|
|
@@ -66,15 +68,17 @@ def generate_commit_message(
|
|
|
66
68
|
# Provider functions mapping
|
|
67
69
|
provider_funcs = {
|
|
68
70
|
"anthropic": call_anthropic_api,
|
|
69
|
-
"openai": call_openai_api,
|
|
70
|
-
"groq": call_groq_api,
|
|
71
71
|
"cerebras": call_cerebras_api,
|
|
72
|
+
"gemini": call_gemini_api,
|
|
73
|
+
"groq": call_groq_api,
|
|
74
|
+
"lmstudio": call_lmstudio_api,
|
|
72
75
|
"ollama": call_ollama_api,
|
|
76
|
+
"openai": call_openai_api,
|
|
73
77
|
"openrouter": call_openrouter_api,
|
|
78
|
+
"streamlake": call_streamlake_api,
|
|
79
|
+
"synthetic": call_synthetic_api,
|
|
74
80
|
"zai": call_zai_api,
|
|
75
81
|
"zai-coding": call_zai_coding_api,
|
|
76
|
-
"gemini": call_gemini_api,
|
|
77
|
-
"lmstudio": call_lmstudio_api,
|
|
78
82
|
}
|
|
79
83
|
|
|
80
84
|
# Generate the commit message using centralized retry logic
|
gac/ai_utils.py
CHANGED
gac/init_cli.py
CHANGED
|
@@ -9,6 +9,18 @@ from dotenv import set_key
|
|
|
9
9
|
GAC_ENV_PATH = Path.home() / ".gac.env"
|
|
10
10
|
|
|
11
11
|
|
|
12
|
+
def _prompt_required_text(prompt: str) -> str | None:
|
|
13
|
+
"""Prompt until a non-empty string is provided or the user cancels."""
|
|
14
|
+
while True:
|
|
15
|
+
response = questionary.text(prompt).ask()
|
|
16
|
+
if response is None:
|
|
17
|
+
return None
|
|
18
|
+
value = response.strip()
|
|
19
|
+
if value:
|
|
20
|
+
return value
|
|
21
|
+
click.echo("A value is required. Please try again.")
|
|
22
|
+
|
|
23
|
+
|
|
12
24
|
@click.command()
|
|
13
25
|
def init() -> None:
|
|
14
26
|
"""Interactively set up $HOME/.gac.env for gac."""
|
|
@@ -24,10 +36,12 @@ def init() -> None:
|
|
|
24
36
|
("Cerebras", "qwen-3-coder-480b"),
|
|
25
37
|
("Gemini", "gemini-2.5-flash"),
|
|
26
38
|
("Groq", "meta-llama/llama-4-maverick-17b-128e-instruct"),
|
|
27
|
-
("LM Studio", "
|
|
39
|
+
("LM Studio", "gemma3"),
|
|
28
40
|
("Ollama", "gemma3"),
|
|
29
41
|
("OpenAI", "gpt-4.1-mini"),
|
|
30
42
|
("OpenRouter", "openrouter/auto"),
|
|
43
|
+
("Streamlake", ""),
|
|
44
|
+
("Synthetic", "hf:zai-org/GLM-4.6"),
|
|
31
45
|
("Z.AI", "glm-4.5-air"),
|
|
32
46
|
("Z.AI Coding", "glm-4.6"),
|
|
33
47
|
]
|
|
@@ -36,18 +50,66 @@ def init() -> None:
|
|
|
36
50
|
if not provider:
|
|
37
51
|
click.echo("Provider selection cancelled. Exiting.")
|
|
38
52
|
return
|
|
39
|
-
provider_key = provider.lower().replace(".", "").replace(" ", "-")
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
53
|
+
provider_key = provider.lower().replace(".", "").replace(" ", "-").replace("syntheticnew", "synthetic")
|
|
54
|
+
|
|
55
|
+
is_ollama = provider_key == "ollama"
|
|
56
|
+
is_lmstudio = provider_key == "lm-studio"
|
|
57
|
+
is_streamlake = provider_key == "streamlake"
|
|
58
|
+
is_zai = provider_key in ("zai", "zai-coding")
|
|
59
|
+
|
|
60
|
+
if is_streamlake:
|
|
61
|
+
endpoint_id = _prompt_required_text("Enter the Streamlake inference endpoint ID (required):")
|
|
62
|
+
if endpoint_id is None:
|
|
63
|
+
click.echo("Streamlake configuration cancelled. Exiting.")
|
|
64
|
+
return
|
|
65
|
+
model_to_save = endpoint_id
|
|
66
|
+
else:
|
|
67
|
+
model_suggestion = dict(providers)[provider]
|
|
68
|
+
model_prompt = f"Enter the model (default: {model_suggestion}):"
|
|
69
|
+
model = questionary.text(model_prompt, default=model_suggestion).ask()
|
|
70
|
+
if model is None:
|
|
71
|
+
click.echo("Model entry cancelled. Exiting.")
|
|
72
|
+
return
|
|
73
|
+
model_to_save = model.strip() if model.strip() else model_suggestion
|
|
74
|
+
|
|
43
75
|
set_key(str(GAC_ENV_PATH), "GAC_MODEL", f"{provider_key}:{model_to_save}")
|
|
44
76
|
click.echo(f"Set GAC_MODEL={provider_key}:{model_to_save}")
|
|
45
77
|
|
|
46
|
-
|
|
78
|
+
if is_ollama:
|
|
79
|
+
url_default = "http://localhost:11434"
|
|
80
|
+
url = questionary.text(f"Enter the Ollama API URL (default: {url_default}):", default=url_default).ask()
|
|
81
|
+
if url is None:
|
|
82
|
+
click.echo("Ollama URL entry cancelled. Exiting.")
|
|
83
|
+
return
|
|
84
|
+
url_to_save = url.strip() if url.strip() else url_default
|
|
85
|
+
set_key(str(GAC_ENV_PATH), "OLLAMA_API_URL", url_to_save)
|
|
86
|
+
click.echo(f"Set OLLAMA_API_URL={url_to_save}")
|
|
87
|
+
elif is_lmstudio:
|
|
88
|
+
url_default = "http://localhost:1234"
|
|
89
|
+
url = questionary.text(f"Enter the LM Studio API URL (default: {url_default}):", default=url_default).ask()
|
|
90
|
+
if url is None:
|
|
91
|
+
click.echo("LM Studio URL entry cancelled. Exiting.")
|
|
92
|
+
return
|
|
93
|
+
url_to_save = url.strip() if url.strip() else url_default
|
|
94
|
+
set_key(str(GAC_ENV_PATH), "LMSTUDIO_API_URL", url_to_save)
|
|
95
|
+
click.echo(f"Set LMSTUDIO_API_URL={url_to_save}")
|
|
96
|
+
|
|
97
|
+
api_key_prompt = "Enter your API key (input hidden, can be set later):"
|
|
98
|
+
if is_ollama or is_lmstudio:
|
|
99
|
+
click.echo(
|
|
100
|
+
"This provider typically runs locally. API keys are optional unless your instance requires authentication."
|
|
101
|
+
)
|
|
102
|
+
api_key_prompt = "Enter your API key (optional, press Enter to skip):"
|
|
103
|
+
|
|
104
|
+
api_key = questionary.password(api_key_prompt).ask()
|
|
47
105
|
if api_key:
|
|
48
|
-
|
|
49
|
-
|
|
106
|
+
if is_zai:
|
|
107
|
+
api_key_name = "ZAI_API_KEY"
|
|
108
|
+
else:
|
|
109
|
+
api_key_name = f"{provider_key.upper()}_API_KEY"
|
|
50
110
|
set_key(str(GAC_ENV_PATH), api_key_name, api_key)
|
|
51
111
|
click.echo(f"Set {api_key_name} (hidden)")
|
|
112
|
+
elif is_ollama or is_lmstudio:
|
|
113
|
+
click.echo("Skipping API key. You can add one later if needed.")
|
|
52
114
|
|
|
53
115
|
click.echo(f"\ngac environment setup complete. You can edit {GAC_ENV_PATH} to update values later.")
|
gac/main.py
CHANGED
|
@@ -59,7 +59,7 @@ def main(
|
|
|
59
59
|
if model is None:
|
|
60
60
|
handle_error(
|
|
61
61
|
AIError.model_error(
|
|
62
|
-
"
|
|
62
|
+
"gac init hasn't been run yet. Please run 'gac init' to set up your configuration, then try again."
|
|
63
63
|
),
|
|
64
64
|
exit_program=True,
|
|
65
65
|
)
|
gac/providers/__init__.py
CHANGED
|
@@ -8,6 +8,8 @@ from .lmstudio import call_lmstudio_api
|
|
|
8
8
|
from .ollama import call_ollama_api
|
|
9
9
|
from .openai import call_openai_api
|
|
10
10
|
from .openrouter import call_openrouter_api
|
|
11
|
+
from .streamlake import call_streamlake_api
|
|
12
|
+
from .synthetic import call_synthetic_api
|
|
11
13
|
from .zai import call_zai_api, call_zai_coding_api
|
|
12
14
|
|
|
13
15
|
__all__ = [
|
|
@@ -19,6 +21,8 @@ __all__ = [
|
|
|
19
21
|
"call_ollama_api",
|
|
20
22
|
"call_openai_api",
|
|
21
23
|
"call_openrouter_api",
|
|
24
|
+
"call_streamlake_api",
|
|
25
|
+
"call_synthetic_api",
|
|
22
26
|
"call_zai_api",
|
|
23
27
|
"call_zai_coding_api",
|
|
24
28
|
]
|
gac/providers/anthropic.py
CHANGED
|
@@ -11,7 +11,7 @@ def call_anthropic_api(model: str, messages: list[dict], temperature: float, max
|
|
|
11
11
|
"""Call Anthropic API directly."""
|
|
12
12
|
api_key = os.getenv("ANTHROPIC_API_KEY")
|
|
13
13
|
if not api_key:
|
|
14
|
-
raise AIError.
|
|
14
|
+
raise AIError.authentication_error("ANTHROPIC_API_KEY not found in environment variables")
|
|
15
15
|
|
|
16
16
|
url = "https://api.anthropic.com/v1/messages"
|
|
17
17
|
headers = {"x-api-key": api_key, "anthropic-version": "2023-06-01", "content-type": "application/json"}
|
|
@@ -42,6 +42,10 @@ def call_anthropic_api(model: str, messages: list[dict], temperature: float, max
|
|
|
42
42
|
raise AIError.model_error("Anthropic API returned empty content")
|
|
43
43
|
return content
|
|
44
44
|
except httpx.HTTPStatusError as e:
|
|
45
|
+
if e.response.status_code == 429:
|
|
46
|
+
raise AIError.rate_limit_error(f"Anthropic API rate limit exceeded: {e.response.text}") from e
|
|
45
47
|
raise AIError.model_error(f"Anthropic API error: {e.response.status_code} - {e.response.text}") from e
|
|
48
|
+
except httpx.TimeoutException as e:
|
|
49
|
+
raise AIError.timeout_error(f"Anthropic API request timed out: {str(e)}") from e
|
|
46
50
|
except Exception as e:
|
|
47
51
|
raise AIError.model_error(f"Error calling Anthropic API: {str(e)}") from e
|
gac/providers/cerebras.py
CHANGED
|
@@ -11,7 +11,7 @@ def call_cerebras_api(model: str, messages: list[dict], temperature: float, max_
|
|
|
11
11
|
"""Call Cerebras API directly."""
|
|
12
12
|
api_key = os.getenv("CEREBRAS_API_KEY")
|
|
13
13
|
if not api_key:
|
|
14
|
-
raise AIError.
|
|
14
|
+
raise AIError.authentication_error("CEREBRAS_API_KEY not found in environment variables")
|
|
15
15
|
|
|
16
16
|
url = "https://api.cerebras.ai/v1/chat/completions"
|
|
17
17
|
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
|
|
@@ -29,6 +29,10 @@ def call_cerebras_api(model: str, messages: list[dict], temperature: float, max_
|
|
|
29
29
|
raise AIError.model_error("Cerebras API returned empty content")
|
|
30
30
|
return content
|
|
31
31
|
except httpx.HTTPStatusError as e:
|
|
32
|
+
if e.response.status_code == 429:
|
|
33
|
+
raise AIError.rate_limit_error(f"Cerebras API rate limit exceeded: {e.response.text}") from e
|
|
32
34
|
raise AIError.model_error(f"Cerebras API error: {e.response.status_code} - {e.response.text}") from e
|
|
35
|
+
except httpx.TimeoutException as e:
|
|
36
|
+
raise AIError.timeout_error(f"Cerebras API request timed out: {str(e)}") from e
|
|
33
37
|
except Exception as e:
|
|
34
38
|
raise AIError.model_error(f"Error calling Cerebras API: {str(e)}") from e
|
gac/providers/gemini.py
CHANGED
|
@@ -12,7 +12,7 @@ def call_gemini_api(model: str, messages: list[dict[str, Any]], temperature: flo
|
|
|
12
12
|
"""Call Gemini API directly."""
|
|
13
13
|
api_key = os.getenv("GEMINI_API_KEY")
|
|
14
14
|
if not api_key:
|
|
15
|
-
raise AIError.
|
|
15
|
+
raise AIError.authentication_error("GEMINI_API_KEY not found in environment variables")
|
|
16
16
|
|
|
17
17
|
url = f"https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent"
|
|
18
18
|
|
|
@@ -65,6 +65,10 @@ def call_gemini_api(model: str, messages: list[dict[str, Any]], temperature: flo
|
|
|
65
65
|
except AIError:
|
|
66
66
|
raise
|
|
67
67
|
except httpx.HTTPStatusError as e:
|
|
68
|
+
if e.response.status_code == 429:
|
|
69
|
+
raise AIError.rate_limit_error(f"Gemini API rate limit exceeded: {e.response.text}") from e
|
|
68
70
|
raise AIError.model_error(f"Gemini API error: {e.response.status_code} - {e.response.text}") from e
|
|
71
|
+
except httpx.TimeoutException as e:
|
|
72
|
+
raise AIError.timeout_error(f"Gemini API request timed out: {str(e)}") from e
|
|
69
73
|
except Exception as e:
|
|
70
74
|
raise AIError.model_error(f"Error calling Gemini API: {str(e)}") from e
|
gac/providers/groq.py
CHANGED
|
@@ -14,7 +14,7 @@ def call_groq_api(model: str, messages: list[dict], temperature: float, max_toke
|
|
|
14
14
|
"""Call Groq API directly."""
|
|
15
15
|
api_key = os.getenv("GROQ_API_KEY")
|
|
16
16
|
if not api_key:
|
|
17
|
-
raise AIError.
|
|
17
|
+
raise AIError.authentication_error("GROQ_API_KEY not found in environment variables")
|
|
18
18
|
|
|
19
19
|
url = "https://api.groq.com/openai/v1/chat/completions"
|
|
20
20
|
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
|
|
@@ -54,6 +54,10 @@ def call_groq_api(model: str, messages: list[dict], temperature: float, max_toke
|
|
|
54
54
|
logger.error(f"Unexpected response format from Groq API: {response_data}")
|
|
55
55
|
raise AIError.model_error(f"Unexpected response format from Groq API: {response_data}")
|
|
56
56
|
except httpx.HTTPStatusError as e:
|
|
57
|
+
if e.response.status_code == 429:
|
|
58
|
+
raise AIError.rate_limit_error(f"Groq API rate limit exceeded: {e.response.text}") from e
|
|
57
59
|
raise AIError.model_error(f"Groq API error: {e.response.status_code} - {e.response.text}") from e
|
|
60
|
+
except httpx.TimeoutException as e:
|
|
61
|
+
raise AIError.timeout_error(f"Groq API request timed out: {str(e)}") from e
|
|
58
62
|
except Exception as e:
|
|
59
63
|
raise AIError.model_error(f"Error calling Groq API: {str(e)}") from e
|
gac/providers/lmstudio.py
CHANGED
|
@@ -50,6 +50,10 @@ def call_lmstudio_api(model: str, messages: list[dict[str, Any]], temperature: f
|
|
|
50
50
|
except httpx.ConnectError as e:
|
|
51
51
|
raise AIError.connection_error(f"LM Studio connection failed: {str(e)}") from e
|
|
52
52
|
except httpx.HTTPStatusError as e:
|
|
53
|
+
if e.response.status_code == 429:
|
|
54
|
+
raise AIError.rate_limit_error(f"LM Studio API rate limit exceeded: {e.response.text}") from e
|
|
53
55
|
raise AIError.model_error(f"LM Studio API error: {e.response.status_code} - {e.response.text}") from e
|
|
56
|
+
except httpx.TimeoutException as e:
|
|
57
|
+
raise AIError.timeout_error(f"LM Studio API request timed out: {str(e)}") from e
|
|
54
58
|
except Exception as e:
|
|
55
59
|
raise AIError.model_error(f"Error calling LM Studio API: {str(e)}") from e
|
gac/providers/ollama.py
CHANGED
|
@@ -41,6 +41,10 @@ def call_ollama_api(model: str, messages: list[dict], temperature: float, max_to
|
|
|
41
41
|
except httpx.ConnectError as e:
|
|
42
42
|
raise AIError.connection_error(f"Ollama connection failed. Make sure Ollama is running: {str(e)}") from e
|
|
43
43
|
except httpx.HTTPStatusError as e:
|
|
44
|
+
if e.response.status_code == 429:
|
|
45
|
+
raise AIError.rate_limit_error(f"Ollama API rate limit exceeded: {e.response.text}") from e
|
|
44
46
|
raise AIError.model_error(f"Ollama API error: {e.response.status_code} - {e.response.text}") from e
|
|
47
|
+
except httpx.TimeoutException as e:
|
|
48
|
+
raise AIError.timeout_error(f"Ollama API request timed out: {str(e)}") from e
|
|
45
49
|
except Exception as e:
|
|
46
50
|
raise AIError.model_error(f"Error calling Ollama API: {str(e)}") from e
|
gac/providers/openai.py
CHANGED
|
@@ -11,7 +11,7 @@ def call_openai_api(model: str, messages: list[dict], temperature: float, max_to
|
|
|
11
11
|
"""Call OpenAI API directly."""
|
|
12
12
|
api_key = os.getenv("OPENAI_API_KEY")
|
|
13
13
|
if not api_key:
|
|
14
|
-
raise AIError.
|
|
14
|
+
raise AIError.authentication_error("OPENAI_API_KEY not found in environment variables")
|
|
15
15
|
|
|
16
16
|
url = "https://api.openai.com/v1/chat/completions"
|
|
17
17
|
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
|
|
@@ -29,6 +29,10 @@ def call_openai_api(model: str, messages: list[dict], temperature: float, max_to
|
|
|
29
29
|
raise AIError.model_error("OpenAI API returned empty content")
|
|
30
30
|
return content
|
|
31
31
|
except httpx.HTTPStatusError as e:
|
|
32
|
+
if e.response.status_code == 429:
|
|
33
|
+
raise AIError.rate_limit_error(f"OpenAI API rate limit exceeded: {e.response.text}") from e
|
|
32
34
|
raise AIError.model_error(f"OpenAI API error: {e.response.status_code} - {e.response.text}") from e
|
|
35
|
+
except httpx.TimeoutException as e:
|
|
36
|
+
raise AIError.timeout_error(f"OpenAI API request timed out: {str(e)}") from e
|
|
33
37
|
except Exception as e:
|
|
34
38
|
raise AIError.model_error(f"Error calling OpenAI API: {str(e)}") from e
|
gac/providers/openrouter.py
CHANGED
|
@@ -11,7 +11,7 @@ def call_openrouter_api(model: str, messages: list[dict], temperature: float, ma
|
|
|
11
11
|
"""Call OpenRouter API directly."""
|
|
12
12
|
api_key = os.getenv("OPENROUTER_API_KEY")
|
|
13
13
|
if not api_key:
|
|
14
|
-
raise AIError.
|
|
14
|
+
raise AIError.authentication_error("OPENROUTER_API_KEY environment variable not set")
|
|
15
15
|
|
|
16
16
|
url = "https://openrouter.ai/api/v1/chat/completions"
|
|
17
17
|
headers = {
|
|
@@ -52,5 +52,7 @@ def call_openrouter_api(model: str, messages: list[dict], temperature: float, ma
|
|
|
52
52
|
raise AIError.model_error(f"OpenRouter API error: {status_code} - {error_text}") from e
|
|
53
53
|
except httpx.ConnectError as e:
|
|
54
54
|
raise AIError.connection_error(f"OpenRouter API connection error: {str(e)}") from e
|
|
55
|
+
except httpx.TimeoutException as e:
|
|
56
|
+
raise AIError.timeout_error(f"OpenRouter API request timed out: {str(e)}") from e
|
|
55
57
|
except Exception as e:
|
|
56
58
|
raise AIError.model_error(f"Error calling OpenRouter API: {str(e)}") from e
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""StreamLake (Vanchin) API provider for gac."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
from gac.errors import AIError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def call_streamlake_api(model: str, messages: list[dict], temperature: float, max_tokens: int) -> str:
|
|
11
|
+
"""Call StreamLake (Vanchin) chat completions API."""
|
|
12
|
+
api_key = os.getenv("STREAMLAKE_API_KEY") or os.getenv("VC_API_KEY")
|
|
13
|
+
if not api_key:
|
|
14
|
+
raise AIError.authentication_error(
|
|
15
|
+
"STREAMLAKE_API_KEY not found in environment variables (VC_API_KEY alias also not set)"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
url = "https://vanchin.streamlake.ai/api/gateway/v1/endpoints/chat/completions"
|
|
19
|
+
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
|
|
20
|
+
|
|
21
|
+
data = {
|
|
22
|
+
"model": model,
|
|
23
|
+
"messages": messages,
|
|
24
|
+
"temperature": temperature,
|
|
25
|
+
"max_tokens": max_tokens,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
response = httpx.post(url, headers=headers, json=data, timeout=120)
|
|
30
|
+
response.raise_for_status()
|
|
31
|
+
response_data = response.json()
|
|
32
|
+
choices = response_data.get("choices")
|
|
33
|
+
if not choices:
|
|
34
|
+
raise AIError.model_error("StreamLake API returned no choices")
|
|
35
|
+
|
|
36
|
+
message = choices[0].get("message", {})
|
|
37
|
+
content = message.get("content")
|
|
38
|
+
if content is None:
|
|
39
|
+
raise AIError.model_error("StreamLake API returned null content")
|
|
40
|
+
if content == "":
|
|
41
|
+
raise AIError.model_error("StreamLake API returned empty content")
|
|
42
|
+
|
|
43
|
+
return content
|
|
44
|
+
except httpx.HTTPStatusError as e:
|
|
45
|
+
if e.response.status_code == 429:
|
|
46
|
+
raise AIError.rate_limit_error(f"StreamLake API rate limit exceeded: {e.response.text}") from e
|
|
47
|
+
raise AIError.model_error(f"StreamLake API error: {e.response.status_code} - {e.response.text}") from e
|
|
48
|
+
except httpx.TimeoutException as e:
|
|
49
|
+
raise AIError.timeout_error(f"StreamLake API request timed out: {str(e)}") from e
|
|
50
|
+
except Exception as e: # noqa: BLE001 - convert to AIError
|
|
51
|
+
raise AIError.model_error(f"Error calling StreamLake API: {str(e)}") from e
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Synthetic.new API provider for gac."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
from gac.errors import AIError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def call_synthetic_api(model: str, messages: list[dict], temperature: float, max_tokens: int) -> str:
|
|
11
|
+
"""Call Synthetic API directly."""
|
|
12
|
+
api_key = os.getenv("SYNTHETIC_API_KEY") or os.getenv("SYN_API_KEY")
|
|
13
|
+
if not api_key:
|
|
14
|
+
raise AIError.authentication_error("SYNTHETIC_API_KEY or SYN_API_KEY not found in environment variables")
|
|
15
|
+
|
|
16
|
+
url = "https://api.synthetic.new/openai/v1/chat/completions"
|
|
17
|
+
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
|
|
18
|
+
|
|
19
|
+
data = {"model": model, "messages": messages, "temperature": temperature, "max_completion_tokens": max_tokens}
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
response = httpx.post(url, headers=headers, json=data, timeout=120)
|
|
23
|
+
response.raise_for_status()
|
|
24
|
+
response_data = response.json()
|
|
25
|
+
content = response_data["choices"][0]["message"]["content"]
|
|
26
|
+
if content is None:
|
|
27
|
+
raise AIError.model_error("Synthetic.new API returned null content")
|
|
28
|
+
if content == "":
|
|
29
|
+
raise AIError.model_error("Synthetic.new API returned empty content")
|
|
30
|
+
return content
|
|
31
|
+
except httpx.HTTPStatusError as e:
|
|
32
|
+
if e.response.status_code == 429:
|
|
33
|
+
raise AIError.rate_limit_error(f"Synthetic.new API rate limit exceeded: {e.response.text}") from e
|
|
34
|
+
raise AIError.model_error(f"Synthetic.new API error: {e.response.status_code} - {e.response.text}") from e
|
|
35
|
+
except httpx.TimeoutException as e:
|
|
36
|
+
raise AIError.timeout_error(f"Synthetic.new API request timed out: {str(e)}") from e
|
|
37
|
+
except Exception as e:
|
|
38
|
+
raise AIError.model_error(f"Error calling Synthetic.new API: {str(e)}") from e
|
gac/providers/zai.py
CHANGED
|
@@ -13,7 +13,7 @@ def _call_zai_api_impl(
|
|
|
13
13
|
"""Internal implementation for Z.AI API calls."""
|
|
14
14
|
api_key = os.getenv("ZAI_API_KEY")
|
|
15
15
|
if not api_key:
|
|
16
|
-
raise AIError.
|
|
16
|
+
raise AIError.authentication_error("ZAI_API_KEY not found in environment variables")
|
|
17
17
|
|
|
18
18
|
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
|
|
19
19
|
data = {"model": model, "messages": messages, "temperature": temperature, "max_tokens": max_tokens}
|
|
@@ -38,7 +38,11 @@ def _call_zai_api_impl(
|
|
|
38
38
|
else:
|
|
39
39
|
raise AIError.model_error(f"{api_name} API unexpected response structure: {response_data}")
|
|
40
40
|
except httpx.HTTPStatusError as e:
|
|
41
|
+
if e.response.status_code == 429:
|
|
42
|
+
raise AIError.rate_limit_error(f"{api_name} API rate limit exceeded: {e.response.text}") from e
|
|
41
43
|
raise AIError.model_error(f"{api_name} API error: {e.response.status_code} - {e.response.text}") from e
|
|
44
|
+
except httpx.TimeoutException as e:
|
|
45
|
+
raise AIError.timeout_error(f"{api_name} API request timed out: {str(e)}") from e
|
|
42
46
|
except Exception as e:
|
|
43
47
|
raise AIError.model_error(f"Error calling {api_name} API: {str(e)}") from e
|
|
44
48
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gac
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.6.0
|
|
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
|
|
@@ -17,6 +17,7 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.11
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.12
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
21
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
21
22
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
22
23
|
Requires-Python: >=3.10
|
|
@@ -24,12 +25,12 @@ Requires-Dist: anthropic>=0.68.0
|
|
|
24
25
|
Requires-Dist: click>=8.3.0
|
|
25
26
|
Requires-Dist: halo
|
|
26
27
|
Requires-Dist: httpx>=0.28.0
|
|
27
|
-
Requires-Dist: pydantic>=2.
|
|
28
|
+
Requires-Dist: pydantic>=2.12.0
|
|
28
29
|
Requires-Dist: python-dotenv>=1.1.1
|
|
29
30
|
Requires-Dist: questionary
|
|
30
31
|
Requires-Dist: rich>=14.1.0
|
|
31
32
|
Requires-Dist: sumy
|
|
32
|
-
Requires-Dist: tiktoken>=0.
|
|
33
|
+
Requires-Dist: tiktoken>=0.12.0
|
|
33
34
|
Provides-Extra: dev
|
|
34
35
|
Requires-Dist: build; extra == 'dev'
|
|
35
36
|
Requires-Dist: bump-my-version; extra == 'dev'
|
|
@@ -45,7 +46,7 @@ Description-Content-Type: text/markdown
|
|
|
45
46
|
# Git Auto Commit (gac)
|
|
46
47
|
|
|
47
48
|
[](https://pypi.org/project/gac/)
|
|
48
|
-
[](https://www.python.org/downloads/)
|
|
49
|
+
[](https://www.python.org/downloads/)
|
|
49
50
|
[](https://github.com/cellwebb/gac/actions)
|
|
50
51
|
[](https://app.codecov.io/gh/cellwebb/gac)
|
|
51
52
|
[](https://github.com/psf/black)
|
|
@@ -56,7 +57,7 @@ Description-Content-Type: text/markdown
|
|
|
56
57
|
|
|
57
58
|
- **LLM-Powered Commit Messages:** Automatically generates clear, concise, and context-aware commit messages using large language models.
|
|
58
59
|
- **Deep Contextual Analysis:** Understands your code by analyzing staged changes, repository structure, and recent commit history to provide highly relevant suggestions.
|
|
59
|
-
- **Multi-Provider & Model Support:** Flexibly works with leading AI providers (Anthropic, Cerebras,
|
|
60
|
+
- **Multi-Provider & Model Support:** Flexibly works with leading AI providers (Anthropic, Cerebras, Gemini, Groq, OpenAI, OpenRouter, Streamlake/Vanchin, Z.AI) and local providers (LM Studio, Ollama), easily configured through an interactive setup or environment variables.
|
|
60
61
|
- **Seamless Git Workflow:** Integrates smoothly into your existing Git routine as a simple drop-in replacement for `git commit`.
|
|
61
62
|
- **Extensive Customization:** Tailor commit messages to your needs with a rich set of flags, including one-liners (`-o`), AI hints (`-h`), scope inference (`-s`), and specific model selection (`-m`).
|
|
62
63
|
- **Streamlined Workflow Commands:** Boost your productivity with convenient options to stage all changes (`-a`), auto-confirm commits (`-y`), and push to your remote repository (`-p`) in a single step.
|
|
@@ -137,9 +138,6 @@ Example `$HOME/.gac.env` output:
|
|
|
137
138
|
```env
|
|
138
139
|
GAC_MODEL=anthropic:claude-3-5-haiku-latest
|
|
139
140
|
ANTHROPIC_API_KEY=your_anthropic_key_here
|
|
140
|
-
# Optional: configure OpenRouter
|
|
141
|
-
# GAC_MODEL=openrouter:openrouter/auto
|
|
142
|
-
# OPENROUTER_API_KEY=your_openrouter_key_here
|
|
143
141
|
```
|
|
144
142
|
|
|
145
143
|
Alternatively, you can configure `gac` using environment variables or by manually creating/editing the configuration file.
|
|
@@ -148,6 +146,9 @@ Alternatively, you can configure `gac` using environment variables or by manuall
|
|
|
148
146
|
|
|
149
147
|
You can manage settings in your `$HOME/.gac.env` file using `gac config` commands:
|
|
150
148
|
|
|
149
|
+
- Streamlake uses inference endpoint IDs instead of model names. When prompted, paste the exact endpoint ID from the Streamlake console.
|
|
150
|
+
- For local providers like Ollama and LM Studio, gac will ask for the base API URL. API keys are optional for these providers unless your instance requires authentication.
|
|
151
|
+
|
|
151
152
|
- Show config: `gac config show`
|
|
152
153
|
- Set a value: `gac config set GAC_MODEL groq:meta-llama/llama-4-scout-17b-16e-instruct`
|
|
153
154
|
- Get a value: `gac config get GAC_MODEL`
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
gac/__init__.py,sha256=z9yGInqtycFIT3g1ca24r-A3699hKVaRqGUI79wsmMc,415
|
|
2
|
+
gac/__version__.py,sha256=Roc4mrxthdXl5rPRtyhrtsoqIlxTWC0IrZ5NqP60A_s,66
|
|
3
|
+
gac/ai.py,sha256=zl9-rmESKTHP2KNstS4sBqMGvGNC-KYcEsPPCpshaUE,3456
|
|
4
|
+
gac/ai_utils.py,sha256=6iSz2q8MohNf2M3CNmF_9eNRDuACLB2kuh_Q9bRrvdE,7252
|
|
5
|
+
gac/cli.py,sha256=nvz6l-wctfo3SMpC-zqtXyHMg8rtdzxw9cllbVMXJ0w,4872
|
|
6
|
+
gac/config.py,sha256=N62phuLUyVj54eLDiDL6VN8-2_Zt6yB5zsnimFavU3I,1630
|
|
7
|
+
gac/config_cli.py,sha256=v9nFHZO1RvK9fzHyuUS6SG-BCLHMsdOMDwWamBhVVh4,1608
|
|
8
|
+
gac/constants.py,sha256=hGzmLGhVDB2KPIqwtl6tHMNuSwHj-2P1RK0cGm4pyNA,4962
|
|
9
|
+
gac/diff_cli.py,sha256=wnVQ9OFGnM0d2Pj9WVjWbo0jxqIuRHVAwmb8wU9Pa3E,5676
|
|
10
|
+
gac/errors.py,sha256=ysDIVRCd0YQVTOW3Q6YzdolxCdtkoQCAFf3_jrqbjUY,7916
|
|
11
|
+
gac/git.py,sha256=MS2m4fv8h4mau1djFG1aje9NXTmkGsjPO9w18LqNGX0,6031
|
|
12
|
+
gac/init_cli.py,sha256=_0FAdJCgrdV61bUVmQm5ykX5TlCsOUcyKKlRY6zav5A,4638
|
|
13
|
+
gac/main.py,sha256=5yL5bgTzfps3b3Nx9Obie8dyxa0VYlQ3Vot5Uw-dmr4,15289
|
|
14
|
+
gac/preprocess.py,sha256=krrLPHsccYMdn_YAtUrppBJIoRgevxGWusDwhE40LEo,15366
|
|
15
|
+
gac/prompt.py,sha256=K6r9q2cAlyPu1fud6-jJsZ4zeweEo3yt6_WeYv8a_SQ,17087
|
|
16
|
+
gac/security.py,sha256=M1MZm6BLOeKl6rH_-UdXsSKol39FnA5fIP3YP394yZE,9898
|
|
17
|
+
gac/utils.py,sha256=W3ladtmsH01MNLdckQYTzYrYbTGEdzCKI36he9C-y_E,3945
|
|
18
|
+
gac/providers/__init__.py,sha256=MB0c9utV3grWKRX4mtPLRwxC138m2W_N4up12pZMZi0,817
|
|
19
|
+
gac/providers/anthropic.py,sha256=VK5d1s1PmBNDwh_x7illQ2CIZIHNIYU28btVfizwQPs,2036
|
|
20
|
+
gac/providers/cerebras.py,sha256=Ik8lhlsliGJVkgDgqlThfpra9tqbdYQZkaC4eNxRd9w,1648
|
|
21
|
+
gac/providers/gemini.py,sha256=GZQz6Y9fd5-xk-U4pXn9bXLeBowxDXOYDyWyrtjFurM,2909
|
|
22
|
+
gac/providers/groq.py,sha256=9v2fAjDa_iRNHFptiUBN8Vt7ZDKkW_JOmIBeYvycD1M,2806
|
|
23
|
+
gac/providers/lmstudio.py,sha256=R82-f0tWdFfGQxLT6o3Q2tfvYguF7ESUg9DEUHNyrDk,2146
|
|
24
|
+
gac/providers/ollama.py,sha256=hPkagbhEiAoH9RTET4EQe9-lTL0YmMRCbQ5dVbRQw6Q,2095
|
|
25
|
+
gac/providers/openai.py,sha256=iHVD6bHf57W-QmW7u1Ee5vOpev7XZ-K75NcolLfebOk,1630
|
|
26
|
+
gac/providers/openrouter.py,sha256=H3ce8JcRUYq1I30lOjGESdX7jfoPkW3mKAYnc2aYfBw,2204
|
|
27
|
+
gac/providers/streamlake.py,sha256=KAA2ZnpuEI5imzvdWVWUhEBHSP0BMnprKXte6CbwBWY,2047
|
|
28
|
+
gac/providers/synthetic.py,sha256=Te_bj5Q7foLnTch89-hCGwt8V9O_6BzJZ0eMESArpLM,1744
|
|
29
|
+
gac/providers/zai.py,sha256=kywhhrCfPBu0rElZyb-iENxQxxpVGykvePuL4xrXlaU,2739
|
|
30
|
+
gac-1.6.0.dist-info/METADATA,sha256=WXIPxP8Pcx9yVu4c-0BgRYy8DTY1pLFLXq-17t9Fx_A,9821
|
|
31
|
+
gac-1.6.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
32
|
+
gac-1.6.0.dist-info/entry_points.txt,sha256=tdjN-XMmcWfL92swuRAjT62bFLOAwk9bTMRLGP5Z4aI,36
|
|
33
|
+
gac-1.6.0.dist-info/licenses/LICENSE,sha256=vOab37NouL1PNs5BswnPayrMCqaN2sqLfMQfqPDrpZg,1103
|
|
34
|
+
gac-1.6.0.dist-info/RECORD,,
|
gac-1.5.1.dist-info/RECORD
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
gac/__init__.py,sha256=HFWgSVNbTAFhgetCRWI1WrtyE7zC7IHvoBOrfDGUurM,989
|
|
2
|
-
gac/__version__.py,sha256=jqo2PBOQzwZYj1cBEdd7cR5I7BpjI5t9dhuiXIVR5hA,66
|
|
3
|
-
gac/ai.py,sha256=iOxHt1HHmTfut6pQ5Iy0jr0LnxOlaDVKKDVt3_1Yhg0,3323
|
|
4
|
-
gac/ai_utils.py,sha256=8PczY1uq9N9LnvLPjLeMsWEFDfBsZQkPUpmcBxJdifs,7209
|
|
5
|
-
gac/cli.py,sha256=nvz6l-wctfo3SMpC-zqtXyHMg8rtdzxw9cllbVMXJ0w,4872
|
|
6
|
-
gac/config.py,sha256=N62phuLUyVj54eLDiDL6VN8-2_Zt6yB5zsnimFavU3I,1630
|
|
7
|
-
gac/config_cli.py,sha256=v9nFHZO1RvK9fzHyuUS6SG-BCLHMsdOMDwWamBhVVh4,1608
|
|
8
|
-
gac/constants.py,sha256=hGzmLGhVDB2KPIqwtl6tHMNuSwHj-2P1RK0cGm4pyNA,4962
|
|
9
|
-
gac/diff_cli.py,sha256=wnVQ9OFGnM0d2Pj9WVjWbo0jxqIuRHVAwmb8wU9Pa3E,5676
|
|
10
|
-
gac/errors.py,sha256=ysDIVRCd0YQVTOW3Q6YzdolxCdtkoQCAFf3_jrqbjUY,7916
|
|
11
|
-
gac/git.py,sha256=MS2m4fv8h4mau1djFG1aje9NXTmkGsjPO9w18LqNGX0,6031
|
|
12
|
-
gac/init_cli.py,sha256=d2wTJLXF4czVZEL7H38CcveXi6KO96L4aZQn8vqdqks,2203
|
|
13
|
-
gac/main.py,sha256=igiUnkdDG5akjcPHa2iCfqstziYifGmyGegP2k6g_c4,15273
|
|
14
|
-
gac/preprocess.py,sha256=krrLPHsccYMdn_YAtUrppBJIoRgevxGWusDwhE40LEo,15366
|
|
15
|
-
gac/prompt.py,sha256=K6r9q2cAlyPu1fud6-jJsZ4zeweEo3yt6_WeYv8a_SQ,17087
|
|
16
|
-
gac/security.py,sha256=M1MZm6BLOeKl6rH_-UdXsSKol39FnA5fIP3YP394yZE,9898
|
|
17
|
-
gac/utils.py,sha256=W3ladtmsH01MNLdckQYTzYrYbTGEdzCKI36he9C-y_E,3945
|
|
18
|
-
gac/providers/__init__.py,sha256=3mFyeXIfNWTRYr7QbMv-L5PNBWRzQRfHXdXVM1GJY3s,678
|
|
19
|
-
gac/providers/anthropic.py,sha256=U9gz1Qy7uH1FwG4zSGHSYhVQzL2NFCbkmJM8NmupSkw,1749
|
|
20
|
-
gac/providers/cerebras.py,sha256=XrpgVYzkmQXnK4Jjct_HeXa906m61we45oSiI5l7idw,1363
|
|
21
|
-
gac/providers/gemini.py,sha256=FgXU_ne8G0DobZPkfLTLmr-qexvsSFVee4ZILPjw_RY,2628
|
|
22
|
-
gac/providers/groq.py,sha256=ILe1Qo8tK0mZ_b-fCPq25j76HfI9KcPnUi2Dginw0Ys,2529
|
|
23
|
-
gac/providers/lmstudio.py,sha256=TwAMFmz5b4zMzZb-trAOhHiLUWRZJl9XS9Rv9x9v7jI,1868
|
|
24
|
-
gac/providers/ollama.py,sha256=E89fY_jj8TBoRdWniCXcXrm0ZURnL-Gz4vuQI1mpE9U,1823
|
|
25
|
-
gac/providers/openai.py,sha256=p4Mox372Ketm3s4Iik_WXxW6wbTS28CC4G7Mtojfo8U,1349
|
|
26
|
-
gac/providers/openrouter.py,sha256=3ZsxL1n6Xi1Ylu8DH3z4JI9FBM4RvMg-asQ-xPciWtQ,2065
|
|
27
|
-
gac/providers/zai.py,sha256=J9SogoU-K-Gl3jgyWWJtVqqoUDXMUNodolf7Qrx3CBY,2450
|
|
28
|
-
gac-1.5.1.dist-info/METADATA,sha256=8WnjncUwyO4pllItkiAjjzcIiEfqoytwKEFiGlpcT4A,9518
|
|
29
|
-
gac-1.5.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
30
|
-
gac-1.5.1.dist-info/entry_points.txt,sha256=tdjN-XMmcWfL92swuRAjT62bFLOAwk9bTMRLGP5Z4aI,36
|
|
31
|
-
gac-1.5.1.dist-info/licenses/LICENSE,sha256=vOab37NouL1PNs5BswnPayrMCqaN2sqLfMQfqPDrpZg,1103
|
|
32
|
-
gac-1.5.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|