gac 3.6.0__py3-none-any.whl → 3.10.10__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.
- gac/__init__.py +4 -6
- gac/__version__.py +1 -1
- gac/ai_utils.py +59 -43
- gac/auth_cli.py +181 -36
- gac/cli.py +26 -9
- gac/commit_executor.py +59 -0
- gac/config.py +81 -2
- gac/config_cli.py +19 -7
- gac/constants/__init__.py +34 -0
- gac/constants/commit.py +63 -0
- gac/constants/defaults.py +40 -0
- gac/constants/file_patterns.py +110 -0
- gac/constants/languages.py +119 -0
- gac/diff_cli.py +0 -22
- gac/errors.py +8 -2
- gac/git.py +6 -6
- gac/git_state_validator.py +193 -0
- gac/grouped_commit_workflow.py +458 -0
- gac/init_cli.py +2 -1
- gac/interactive_mode.py +179 -0
- gac/language_cli.py +0 -1
- gac/main.py +231 -926
- gac/model_cli.py +67 -11
- gac/model_identifier.py +70 -0
- gac/oauth/__init__.py +26 -0
- gac/oauth/claude_code.py +89 -22
- gac/oauth/qwen_oauth.py +327 -0
- gac/oauth/token_store.py +81 -0
- gac/oauth_retry.py +161 -0
- gac/postprocess.py +155 -0
- gac/prompt.py +21 -479
- gac/prompt_builder.py +88 -0
- gac/providers/README.md +437 -0
- gac/providers/__init__.py +70 -78
- gac/providers/anthropic.py +12 -46
- gac/providers/azure_openai.py +48 -88
- gac/providers/base.py +329 -0
- gac/providers/cerebras.py +10 -33
- gac/providers/chutes.py +16 -62
- gac/providers/claude_code.py +64 -87
- gac/providers/custom_anthropic.py +51 -81
- gac/providers/custom_openai.py +29 -83
- gac/providers/deepseek.py +10 -33
- gac/providers/error_handler.py +139 -0
- gac/providers/fireworks.py +10 -33
- gac/providers/gemini.py +66 -63
- gac/providers/groq.py +10 -58
- gac/providers/kimi_coding.py +19 -55
- gac/providers/lmstudio.py +64 -43
- gac/providers/minimax.py +10 -33
- gac/providers/mistral.py +10 -33
- gac/providers/moonshot.py +10 -33
- gac/providers/ollama.py +56 -33
- gac/providers/openai.py +30 -36
- gac/providers/openrouter.py +15 -52
- gac/providers/protocol.py +71 -0
- gac/providers/qwen.py +64 -0
- gac/providers/registry.py +58 -0
- gac/providers/replicate.py +140 -82
- gac/providers/streamlake.py +26 -46
- gac/providers/synthetic.py +35 -37
- gac/providers/together.py +10 -33
- gac/providers/zai.py +29 -57
- gac/py.typed +0 -0
- gac/security.py +1 -1
- gac/templates/__init__.py +1 -0
- gac/templates/question_generation.txt +60 -0
- gac/templates/system_prompt.txt +224 -0
- gac/templates/user_prompt.txt +28 -0
- gac/utils.py +36 -6
- gac/workflow_context.py +162 -0
- gac/workflow_utils.py +3 -8
- {gac-3.6.0.dist-info → gac-3.10.10.dist-info}/METADATA +6 -4
- gac-3.10.10.dist-info/RECORD +79 -0
- gac/constants.py +0 -321
- gac-3.6.0.dist-info/RECORD +0 -53
- {gac-3.6.0.dist-info → gac-3.10.10.dist-info}/WHEEL +0 -0
- {gac-3.6.0.dist-info → gac-3.10.10.dist-info}/entry_points.txt +0 -0
- {gac-3.6.0.dist-info → gac-3.10.10.dist-info}/licenses/LICENSE +0 -0
gac/providers/replicate.py
CHANGED
|
@@ -1,98 +1,156 @@
|
|
|
1
1
|
"""Replicate API provider for gac."""
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import time
|
|
4
|
+
from typing import Any
|
|
4
5
|
|
|
5
6
|
import httpx
|
|
6
7
|
|
|
7
8
|
from gac.errors import AIError
|
|
9
|
+
from gac.providers.base import GenericHTTPProvider, ProviderConfig
|
|
10
|
+
from gac.utils import get_ssl_verify
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ReplicateProvider(GenericHTTPProvider):
|
|
14
|
+
"""Replicate API provider with async prediction polling."""
|
|
15
|
+
|
|
16
|
+
config = ProviderConfig(
|
|
17
|
+
name="Replicate",
|
|
18
|
+
api_key_env="REPLICATE_API_TOKEN",
|
|
19
|
+
base_url="https://api.replicate.com/v1",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
def _get_api_url(self, model: str | None = None) -> str:
|
|
23
|
+
"""Get Replicate API URL with /predictions endpoint."""
|
|
24
|
+
return f"{self.config.base_url}/predictions"
|
|
25
|
+
|
|
26
|
+
def _build_headers(self) -> dict[str, str]:
|
|
27
|
+
"""Build headers with Token-based authorization."""
|
|
28
|
+
headers = super()._build_headers()
|
|
29
|
+
# Replace Bearer token with Token format
|
|
30
|
+
if "Authorization" in headers:
|
|
31
|
+
del headers["Authorization"]
|
|
32
|
+
headers["Authorization"] = f"Token {self.api_key}"
|
|
33
|
+
return headers
|
|
34
|
+
|
|
35
|
+
def _build_request_body(
|
|
36
|
+
self, messages: list[dict[str, Any]], temperature: float, max_tokens: int, model: str, **kwargs: Any
|
|
37
|
+
) -> dict[str, Any]:
|
|
38
|
+
"""Build Replicate prediction payload with message-to-prompt conversion."""
|
|
39
|
+
# Convert messages to a single prompt for Replicate
|
|
40
|
+
prompt_parts = []
|
|
41
|
+
system_message = None
|
|
42
|
+
|
|
43
|
+
for message in messages:
|
|
44
|
+
role = message.get("role")
|
|
45
|
+
content = message.get("content", "")
|
|
46
|
+
|
|
47
|
+
if role == "system":
|
|
48
|
+
system_message = content
|
|
49
|
+
elif role == "user":
|
|
50
|
+
prompt_parts.append(f"Human: {content}")
|
|
51
|
+
elif role == "assistant":
|
|
52
|
+
prompt_parts.append(f"Assistant: {content}")
|
|
53
|
+
|
|
54
|
+
# Add system message at the beginning if present
|
|
55
|
+
if system_message:
|
|
56
|
+
prompt_parts.insert(0, f"System: {system_message}")
|
|
57
|
+
|
|
58
|
+
# Add final assistant prompt
|
|
59
|
+
prompt_parts.append("Assistant:")
|
|
60
|
+
full_prompt = "\n\n".join(prompt_parts)
|
|
61
|
+
|
|
62
|
+
# Replicate prediction payload
|
|
63
|
+
return {
|
|
64
|
+
"version": model, # Replicate uses version string as model identifier
|
|
65
|
+
"input": {
|
|
66
|
+
"prompt": full_prompt,
|
|
67
|
+
"temperature": temperature,
|
|
68
|
+
"max_tokens": max_tokens,
|
|
69
|
+
},
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
def generate(
|
|
73
|
+
self,
|
|
74
|
+
model: str,
|
|
75
|
+
messages: list[dict[str, Any]],
|
|
76
|
+
temperature: float = 0.7,
|
|
77
|
+
max_tokens: int = 1024,
|
|
78
|
+
**kwargs: Any,
|
|
79
|
+
) -> str:
|
|
80
|
+
"""Override generate to handle Replicate's async polling mechanism."""
|
|
81
|
+
# Build request components
|
|
82
|
+
try:
|
|
83
|
+
url = self._get_api_url(model)
|
|
84
|
+
except AIError:
|
|
85
|
+
raise
|
|
86
|
+
except Exception as e:
|
|
87
|
+
raise AIError.model_error(f"Error calling {self.config.name} AI API: {e!s}") from e
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
headers = self._build_headers()
|
|
91
|
+
except AIError:
|
|
92
|
+
raise
|
|
93
|
+
except Exception as e:
|
|
94
|
+
raise AIError.model_error(f"Error calling {self.config.name} AI API: {e!s}") from e
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
body = self._build_request_body(messages, temperature, max_tokens, model, **kwargs)
|
|
98
|
+
except AIError:
|
|
99
|
+
raise
|
|
100
|
+
except Exception as e:
|
|
101
|
+
raise AIError.model_error(f"Error calling {self.config.name} AI API: {e!s}") from e
|
|
8
102
|
|
|
9
|
-
|
|
10
|
-
def call_replicate_api(model: str, messages: list[dict], temperature: float, max_tokens: int) -> str:
|
|
11
|
-
"""Call Replicate API directly."""
|
|
12
|
-
api_key = os.getenv("REPLICATE_API_TOKEN")
|
|
13
|
-
if not api_key:
|
|
14
|
-
raise AIError.authentication_error("REPLICATE_API_TOKEN not found in environment variables")
|
|
15
|
-
|
|
16
|
-
# Replicate uses a different endpoint for language models
|
|
17
|
-
url = "https://api.replicate.com/v1/predictions"
|
|
18
|
-
headers = {"Authorization": f"Token {api_key}", "Content-Type": "application/json"}
|
|
19
|
-
|
|
20
|
-
# Convert messages to a single prompt for Replicate
|
|
21
|
-
prompt_parts = []
|
|
22
|
-
system_message = None
|
|
23
|
-
|
|
24
|
-
for message in messages:
|
|
25
|
-
role = message.get("role")
|
|
26
|
-
content = message.get("content", "")
|
|
27
|
-
|
|
28
|
-
if role == "system":
|
|
29
|
-
system_message = content
|
|
30
|
-
elif role == "user":
|
|
31
|
-
prompt_parts.append(f"Human: {content}")
|
|
32
|
-
elif role == "assistant":
|
|
33
|
-
prompt_parts.append(f"Assistant: {content}")
|
|
34
|
-
|
|
35
|
-
# Add system message at the beginning if present
|
|
36
|
-
if system_message:
|
|
37
|
-
prompt_parts.insert(0, f"System: {system_message}")
|
|
38
|
-
|
|
39
|
-
# Add final assistant prompt
|
|
40
|
-
prompt_parts.append("Assistant:")
|
|
41
|
-
full_prompt = "\n\n".join(prompt_parts)
|
|
42
|
-
|
|
43
|
-
# Replicate prediction payload
|
|
44
|
-
data = {
|
|
45
|
-
"version": model, # Replicate uses version string as model identifier
|
|
46
|
-
"input": {
|
|
47
|
-
"prompt": full_prompt,
|
|
48
|
-
"temperature": temperature,
|
|
49
|
-
"max_tokens": max_tokens,
|
|
50
|
-
},
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
try:
|
|
54
103
|
# Create prediction
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
104
|
+
try:
|
|
105
|
+
response = httpx.post(url, json=body, headers=headers, timeout=self.config.timeout, verify=get_ssl_verify())
|
|
106
|
+
response.raise_for_status()
|
|
107
|
+
prediction_data = response.json()
|
|
108
|
+
except httpx.HTTPStatusError as e:
|
|
109
|
+
if e.response.status_code == 429:
|
|
110
|
+
raise AIError.rate_limit_error(f"Replicate API rate limit exceeded: {e.response.text}") from e
|
|
111
|
+
elif e.response.status_code == 401:
|
|
112
|
+
raise AIError.authentication_error(f"Replicate API authentication failed: {e.response.text}") from e
|
|
113
|
+
raise AIError.model_error(f"Replicate API error: {e.response.status_code} - {e.response.text}") from e
|
|
114
|
+
except httpx.TimeoutException as e:
|
|
115
|
+
raise AIError.timeout_error(f"Replicate API request timed out: {str(e)}") from e
|
|
116
|
+
except Exception as e:
|
|
117
|
+
raise AIError.model_error(f"Error calling Replicate API: {str(e)}") from e
|
|
118
|
+
|
|
119
|
+
# Poll for completion
|
|
60
120
|
get_url = f"https://api.replicate.com/v1/predictions/{prediction_data['id']}"
|
|
61
|
-
|
|
62
|
-
# Poll for completion (Replicate predictions are async)
|
|
63
121
|
max_wait_time = 120
|
|
64
122
|
wait_interval = 2
|
|
65
123
|
elapsed_time = 0
|
|
66
124
|
|
|
67
125
|
while elapsed_time < max_wait_time:
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
126
|
+
try:
|
|
127
|
+
get_response = httpx.get(get_url, headers=headers, timeout=self.config.timeout, verify=get_ssl_verify())
|
|
128
|
+
get_response.raise_for_status()
|
|
129
|
+
status_data = get_response.json()
|
|
130
|
+
|
|
131
|
+
if status_data["status"] == "succeeded":
|
|
132
|
+
content = status_data["output"]
|
|
133
|
+
if not content:
|
|
134
|
+
raise AIError.model_error("Replicate API returned empty content")
|
|
135
|
+
return content
|
|
136
|
+
elif status_data["status"] == "failed":
|
|
137
|
+
raise AIError.model_error(
|
|
138
|
+
f"Replicate prediction failed: {status_data.get('error', 'Unknown error')}"
|
|
139
|
+
)
|
|
140
|
+
elif status_data["status"] in ["starting", "processing"]:
|
|
141
|
+
time.sleep(wait_interval)
|
|
142
|
+
elapsed_time += wait_interval
|
|
143
|
+
else:
|
|
144
|
+
raise AIError.model_error(f"Replicate API returned unknown status: {status_data['status']}")
|
|
145
|
+
except httpx.HTTPStatusError as e:
|
|
146
|
+
if e.response.status_code == 429:
|
|
147
|
+
raise AIError.rate_limit_error(f"Replicate API rate limit exceeded: {e.response.text}") from e
|
|
148
|
+
raise AIError.model_error(f"Replicate API error: {e.response.status_code} - {e.response.text}") from e
|
|
149
|
+
except httpx.TimeoutException as e:
|
|
150
|
+
raise AIError.timeout_error(f"Replicate API request timed out: {str(e)}") from e
|
|
151
|
+
except AIError:
|
|
152
|
+
raise
|
|
153
|
+
except Exception as e:
|
|
154
|
+
raise AIError.model_error(f"Error polling Replicate API: {str(e)}") from e
|
|
86
155
|
|
|
87
156
|
raise AIError.timeout_error("Replicate API prediction timed out")
|
|
88
|
-
|
|
89
|
-
except httpx.HTTPStatusError as e:
|
|
90
|
-
if e.response.status_code == 429:
|
|
91
|
-
raise AIError.rate_limit_error(f"Replicate API rate limit exceeded: {e.response.text}") from e
|
|
92
|
-
elif e.response.status_code == 401:
|
|
93
|
-
raise AIError.authentication_error(f"Replicate API authentication failed: {e.response.text}") from e
|
|
94
|
-
raise AIError.model_error(f"Replicate API error: {e.response.status_code} - {e.response.text}") from e
|
|
95
|
-
except httpx.TimeoutException as e:
|
|
96
|
-
raise AIError.timeout_error(f"Replicate API request timed out: {str(e)}") from e
|
|
97
|
-
except Exception as e:
|
|
98
|
-
raise AIError.model_error(f"Error calling Replicate API: {str(e)}") from e
|
gac/providers/streamlake.py
CHANGED
|
@@ -2,50 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
4
|
|
|
5
|
-
import httpx
|
|
6
|
-
|
|
7
5
|
from gac.errors import AIError
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
"
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
"
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
6
|
+
from gac.providers.base import OpenAICompatibleProvider, ProviderConfig
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class StreamlakeProvider(OpenAICompatibleProvider):
|
|
10
|
+
"""StreamLake (Vanchin) OpenAI-compatible provider with alternative env vars."""
|
|
11
|
+
|
|
12
|
+
config = ProviderConfig(
|
|
13
|
+
name="StreamLake",
|
|
14
|
+
api_key_env="STREAMLAKE_API_KEY",
|
|
15
|
+
base_url="https://vanchin.streamlake.ai/api/gateway/v1/endpoints",
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
def _get_api_url(self, model: str | None = None) -> str:
|
|
19
|
+
"""Get StreamLake API URL with /chat/completions endpoint."""
|
|
20
|
+
return f"{self.config.base_url}/chat/completions"
|
|
21
|
+
|
|
22
|
+
def _get_api_key(self) -> str:
|
|
23
|
+
"""Get API key from environment with fallback to VC_API_KEY."""
|
|
24
|
+
api_key = os.getenv(self.config.api_key_env)
|
|
25
|
+
if not api_key:
|
|
26
|
+
api_key = os.getenv("VC_API_KEY")
|
|
27
|
+
if not api_key:
|
|
28
|
+
raise AIError.authentication_error(
|
|
29
|
+
"STREAMLAKE_API_KEY not found in environment variables (VC_API_KEY alias also not set)"
|
|
30
|
+
)
|
|
31
|
+
return api_key
|
gac/providers/synthetic.py
CHANGED
|
@@ -1,42 +1,40 @@
|
|
|
1
1
|
"""Synthetic.new API provider for gac."""
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
|
-
|
|
5
|
-
import httpx
|
|
4
|
+
from typing import Any
|
|
6
5
|
|
|
7
6
|
from gac.errors import AIError
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
raise AIError.model_error(f"Error calling Synthetic.new API: {str(e)}") from e
|
|
7
|
+
from gac.providers.base import OpenAICompatibleProvider, ProviderConfig
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SyntheticProvider(OpenAICompatibleProvider):
|
|
11
|
+
"""Synthetic.new OpenAI-compatible provider with alternative env vars and model preprocessing."""
|
|
12
|
+
|
|
13
|
+
config = ProviderConfig(
|
|
14
|
+
name="Synthetic",
|
|
15
|
+
api_key_env="SYNTHETIC_API_KEY",
|
|
16
|
+
base_url="https://api.synthetic.new/openai/v1/chat/completions",
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
def _get_api_key(self) -> str:
|
|
20
|
+
"""Get API key from environment with fallback to SYN_API_KEY."""
|
|
21
|
+
api_key = os.getenv(self.config.api_key_env)
|
|
22
|
+
if not api_key:
|
|
23
|
+
api_key = os.getenv("SYN_API_KEY")
|
|
24
|
+
if not api_key:
|
|
25
|
+
raise AIError.authentication_error("SYNTHETIC_API_KEY or SYN_API_KEY not found in environment variables")
|
|
26
|
+
return api_key
|
|
27
|
+
|
|
28
|
+
def _build_request_body(
|
|
29
|
+
self, messages: list[dict[str, Any]], temperature: float, max_tokens: int, model: str, **kwargs: Any
|
|
30
|
+
) -> dict[str, Any]:
|
|
31
|
+
"""Build request body with model name preprocessing and max_completion_tokens."""
|
|
32
|
+
# Auto-add hf: prefix if not present
|
|
33
|
+
if not model.startswith("hf:"):
|
|
34
|
+
model = f"hf:{model}"
|
|
35
|
+
|
|
36
|
+
data = super()._build_request_body(messages, temperature, max_tokens, model, **kwargs)
|
|
37
|
+
data["max_completion_tokens"] = data.pop("max_tokens")
|
|
38
|
+
# Ensure the prefixed model is used
|
|
39
|
+
data["model"] = model
|
|
40
|
+
return data
|
gac/providers/together.py
CHANGED
|
@@ -1,38 +1,15 @@
|
|
|
1
1
|
"""Together AI API provider for gac."""
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
from gac.providers.base import OpenAICompatibleProvider, ProviderConfig
|
|
4
4
|
|
|
5
|
-
import httpx
|
|
6
5
|
|
|
7
|
-
|
|
6
|
+
class TogetherProvider(OpenAICompatibleProvider):
|
|
7
|
+
config = ProviderConfig(
|
|
8
|
+
name="Together",
|
|
9
|
+
api_key_env="TOGETHER_API_KEY",
|
|
10
|
+
base_url="https://api.together.xyz/v1",
|
|
11
|
+
)
|
|
8
12
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
api_key = os.getenv("TOGETHER_API_KEY")
|
|
13
|
-
if not api_key:
|
|
14
|
-
raise AIError.authentication_error("TOGETHER_API_KEY not found in environment variables")
|
|
15
|
-
|
|
16
|
-
url = "https://api.together.xyz/v1/chat/completions"
|
|
17
|
-
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
|
|
18
|
-
|
|
19
|
-
data = {"model": model, "messages": messages, "temperature": temperature, "max_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("Together AI API returned null content")
|
|
28
|
-
if content == "":
|
|
29
|
-
raise AIError.model_error("Together AI 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"Together AI API rate limit exceeded: {e.response.text}") from e
|
|
34
|
-
raise AIError.model_error(f"Together AI API error: {e.response.status_code} - {e.response.text}") from e
|
|
35
|
-
except httpx.TimeoutException as e:
|
|
36
|
-
raise AIError.timeout_error(f"Together AI API request timed out: {str(e)}") from e
|
|
37
|
-
except Exception as e:
|
|
38
|
-
raise AIError.model_error(f"Error calling Together AI API: {str(e)}") from e
|
|
13
|
+
def _get_api_url(self, model: str | None = None) -> str:
|
|
14
|
+
"""Get Together API URL with /chat/completions endpoint."""
|
|
15
|
+
return f"{self.config.base_url}/chat/completions"
|
gac/providers/zai.py
CHANGED
|
@@ -1,59 +1,31 @@
|
|
|
1
1
|
"""Z.AI API provider for gac."""
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
raise AIError.model_error(f"{api_name} API returned null content")
|
|
33
|
-
if content == "":
|
|
34
|
-
raise AIError.model_error(f"{api_name} API returned empty content")
|
|
35
|
-
return content
|
|
36
|
-
else:
|
|
37
|
-
raise AIError.model_error(f"{api_name} API response missing content: {response_data}")
|
|
38
|
-
else:
|
|
39
|
-
raise AIError.model_error(f"{api_name} API unexpected response structure: {response_data}")
|
|
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
|
|
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
|
|
46
|
-
except Exception as e:
|
|
47
|
-
raise AIError.model_error(f"Error calling {api_name} API: {str(e)}") from e
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def call_zai_api(model: str, messages: list[dict], temperature: float, max_tokens: int) -> str:
|
|
51
|
-
"""Call Z.AI regular API directly."""
|
|
52
|
-
url = "https://api.z.ai/api/paas/v4/chat/completions"
|
|
53
|
-
return _call_zai_api_impl(url, "Z.AI", model, messages, temperature, max_tokens)
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def call_zai_coding_api(model: str, messages: list[dict], temperature: float, max_tokens: int) -> str:
|
|
57
|
-
"""Call Z.AI coding API directly."""
|
|
58
|
-
url = "https://api.z.ai/api/coding/paas/v4/chat/completions"
|
|
59
|
-
return _call_zai_api_impl(url, "Z.AI coding", model, messages, temperature, max_tokens)
|
|
3
|
+
from gac.providers.base import OpenAICompatibleProvider, ProviderConfig
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ZAIProvider(OpenAICompatibleProvider):
|
|
7
|
+
"""Z.AI regular API provider with OpenAI-compatible format."""
|
|
8
|
+
|
|
9
|
+
config = ProviderConfig(
|
|
10
|
+
name="Z.AI",
|
|
11
|
+
api_key_env="ZAI_API_KEY",
|
|
12
|
+
base_url="https://api.z.ai/api/paas/v4",
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
def _get_api_url(self, model: str | None = None) -> str:
|
|
16
|
+
"""Get Z.AI API URL with /chat/completions endpoint."""
|
|
17
|
+
return f"{self.config.base_url}/chat/completions"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ZAICodingProvider(OpenAICompatibleProvider):
|
|
21
|
+
"""Z.AI coding API provider with OpenAI-compatible format."""
|
|
22
|
+
|
|
23
|
+
config = ProviderConfig(
|
|
24
|
+
name="Z.AI Coding",
|
|
25
|
+
api_key_env="ZAI_API_KEY",
|
|
26
|
+
base_url="https://api.z.ai/api/coding/paas/v4",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
def _get_api_url(self, model: str | None = None) -> str:
|
|
30
|
+
"""Get Z.AI Coding API URL with /chat/completions endpoint."""
|
|
31
|
+
return f"{self.config.base_url}/chat/completions"
|
gac/py.typed
ADDED
|
File without changes
|
gac/security.py
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# This package contains prompt templates for gac.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
<role>
|
|
2
|
+
You are an expert code reviewer specializing in identifying missing context and intent in code changes. Your task is to analyze git diffs and generate focused questions that clarify the "why" behind the changes.
|
|
3
|
+
</role>
|
|
4
|
+
|
|
5
|
+
<focus>
|
|
6
|
+
Analyze the git diff and determine the appropriate number of questions based on change complexity. Generate 1-5 focused questions to clarify intent, motivation, and impact. Your questions should help the developer provide the essential context needed for a meaningful commit message.
|
|
7
|
+
</focus>
|
|
8
|
+
|
|
9
|
+
<adaptive_guidelines>
|
|
10
|
+
- For very small changes (single file, <10 lines): Ask 1-2 essential questions about core purpose
|
|
11
|
+
- For small changes (few files, <50 lines): Ask 1-3 questions covering intent and impact
|
|
12
|
+
- For medium changes (multiple files, <200 lines): Ask 2-4 questions covering scope, intent, and impact
|
|
13
|
+
- For large changes (many files or substantial modifications): Ask 3-5 questions covering all aspects
|
|
14
|
+
- Always prioritize questions that would most help generate an informative commit message
|
|
15
|
+
- Lean toward fewer questions for straightforward changes
|
|
16
|
+
</adaptive_guidelines>
|
|
17
|
+
|
|
18
|
+
<guidelines>
|
|
19
|
+
- Focus on WHY the changes were made, not just WHAT was changed
|
|
20
|
+
- Ask about the intent, motivation, or business purpose behind the changes
|
|
21
|
+
- Consider what future developers need to understand about this change
|
|
22
|
+
- Ask about the broader impact or consequences of the changes
|
|
23
|
+
- Target areas where technical implementation doesn't reveal the underlying purpose
|
|
24
|
+
- Keep questions concise and specific
|
|
25
|
+
- Format as a clean list for easy parsing
|
|
26
|
+
</guidelines>
|
|
27
|
+
|
|
28
|
+
<rules>
|
|
29
|
+
NEVER write or rewrite the commit message; only ask questions.
|
|
30
|
+
DO NOT suggest specific commit message formats or wording.
|
|
31
|
+
DO NOT ask about implementation details that are already clear from the diff.
|
|
32
|
+
DO NOT include any explanations or preamble with your response.
|
|
33
|
+
</rules>
|
|
34
|
+
|
|
35
|
+
<output_format>
|
|
36
|
+
Respond with ONLY a numbered list of questions, one per line:
|
|
37
|
+
1. First focused question?
|
|
38
|
+
2. Second focused question?
|
|
39
|
+
3. Third focused question?
|
|
40
|
+
4. [etc...]
|
|
41
|
+
</output_format>
|
|
42
|
+
|
|
43
|
+
<examples>
|
|
44
|
+
Good example questions for small changes:
|
|
45
|
+
1. What problem does this fix?
|
|
46
|
+
2. Why was this approach chosen?
|
|
47
|
+
|
|
48
|
+
Good example questions for larger changes:
|
|
49
|
+
1. What problem or user need does this change address?
|
|
50
|
+
2. Why was this particular approach chosen over alternatives?
|
|
51
|
+
3. What impact will this have on existing functionality?
|
|
52
|
+
4. What motivated the addition of these new error cases?
|
|
53
|
+
5. Why are these validation rules being added now?
|
|
54
|
+
|
|
55
|
+
Bad examples (violates rules):
|
|
56
|
+
feat: add user authentication - This is a commit message, not a question
|
|
57
|
+
Should I use "feat" or "fix" for this change? - This asks about formatting, not context
|
|
58
|
+
Why did you rename the variable from x to y? - Too implementation-specific
|
|
59
|
+
You should reformat this as "fix: resolve authentication issue" - This rewrites the message
|
|
60
|
+
</examples>
|