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.
Files changed (79) hide show
  1. gac/__init__.py +4 -6
  2. gac/__version__.py +1 -1
  3. gac/ai_utils.py +59 -43
  4. gac/auth_cli.py +181 -36
  5. gac/cli.py +26 -9
  6. gac/commit_executor.py +59 -0
  7. gac/config.py +81 -2
  8. gac/config_cli.py +19 -7
  9. gac/constants/__init__.py +34 -0
  10. gac/constants/commit.py +63 -0
  11. gac/constants/defaults.py +40 -0
  12. gac/constants/file_patterns.py +110 -0
  13. gac/constants/languages.py +119 -0
  14. gac/diff_cli.py +0 -22
  15. gac/errors.py +8 -2
  16. gac/git.py +6 -6
  17. gac/git_state_validator.py +193 -0
  18. gac/grouped_commit_workflow.py +458 -0
  19. gac/init_cli.py +2 -1
  20. gac/interactive_mode.py +179 -0
  21. gac/language_cli.py +0 -1
  22. gac/main.py +231 -926
  23. gac/model_cli.py +67 -11
  24. gac/model_identifier.py +70 -0
  25. gac/oauth/__init__.py +26 -0
  26. gac/oauth/claude_code.py +89 -22
  27. gac/oauth/qwen_oauth.py +327 -0
  28. gac/oauth/token_store.py +81 -0
  29. gac/oauth_retry.py +161 -0
  30. gac/postprocess.py +155 -0
  31. gac/prompt.py +21 -479
  32. gac/prompt_builder.py +88 -0
  33. gac/providers/README.md +437 -0
  34. gac/providers/__init__.py +70 -78
  35. gac/providers/anthropic.py +12 -46
  36. gac/providers/azure_openai.py +48 -88
  37. gac/providers/base.py +329 -0
  38. gac/providers/cerebras.py +10 -33
  39. gac/providers/chutes.py +16 -62
  40. gac/providers/claude_code.py +64 -87
  41. gac/providers/custom_anthropic.py +51 -81
  42. gac/providers/custom_openai.py +29 -83
  43. gac/providers/deepseek.py +10 -33
  44. gac/providers/error_handler.py +139 -0
  45. gac/providers/fireworks.py +10 -33
  46. gac/providers/gemini.py +66 -63
  47. gac/providers/groq.py +10 -58
  48. gac/providers/kimi_coding.py +19 -55
  49. gac/providers/lmstudio.py +64 -43
  50. gac/providers/minimax.py +10 -33
  51. gac/providers/mistral.py +10 -33
  52. gac/providers/moonshot.py +10 -33
  53. gac/providers/ollama.py +56 -33
  54. gac/providers/openai.py +30 -36
  55. gac/providers/openrouter.py +15 -52
  56. gac/providers/protocol.py +71 -0
  57. gac/providers/qwen.py +64 -0
  58. gac/providers/registry.py +58 -0
  59. gac/providers/replicate.py +140 -82
  60. gac/providers/streamlake.py +26 -46
  61. gac/providers/synthetic.py +35 -37
  62. gac/providers/together.py +10 -33
  63. gac/providers/zai.py +29 -57
  64. gac/py.typed +0 -0
  65. gac/security.py +1 -1
  66. gac/templates/__init__.py +1 -0
  67. gac/templates/question_generation.txt +60 -0
  68. gac/templates/system_prompt.txt +224 -0
  69. gac/templates/user_prompt.txt +28 -0
  70. gac/utils.py +36 -6
  71. gac/workflow_context.py +162 -0
  72. gac/workflow_utils.py +3 -8
  73. {gac-3.6.0.dist-info → gac-3.10.10.dist-info}/METADATA +6 -4
  74. gac-3.10.10.dist-info/RECORD +79 -0
  75. gac/constants.py +0 -321
  76. gac-3.6.0.dist-info/RECORD +0 -53
  77. {gac-3.6.0.dist-info → gac-3.10.10.dist-info}/WHEEL +0 -0
  78. {gac-3.6.0.dist-info → gac-3.10.10.dist-info}/entry_points.txt +0 -0
  79. {gac-3.6.0.dist-info → gac-3.10.10.dist-info}/licenses/LICENSE +0 -0
@@ -1,98 +1,156 @@
1
1
  """Replicate API provider for gac."""
2
2
 
3
- import os
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
- response = httpx.post(url, headers=headers, json=data, timeout=120)
56
- response.raise_for_status()
57
- prediction_data = response.json()
58
-
59
- # Get the prediction URL to check status
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
- get_response = httpx.get(get_url, headers=headers, timeout=120)
69
- get_response.raise_for_status()
70
- status_data = get_response.json()
71
-
72
- if status_data["status"] == "succeeded":
73
- content = status_data["output"]
74
- if not content:
75
- raise AIError.model_error("Replicate API returned empty content")
76
- return content
77
- elif status_data["status"] == "failed":
78
- raise AIError.model_error(f"Replicate prediction failed: {status_data.get('error', 'Unknown error')}")
79
- elif status_data["status"] in ["starting", "processing"]:
80
- import time
81
-
82
- time.sleep(wait_interval)
83
- elapsed_time += wait_interval
84
- else:
85
- raise AIError.model_error(f"Replicate API returned unknown status: {status_data['status']}")
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
@@ -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
- 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
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
@@ -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
- def call_synthetic_api(model: str, messages: list[dict], temperature: float, max_tokens: int) -> str:
11
- """Call Synthetic API directly."""
12
- # Handle model names without hf: prefix
13
- if not model.startswith("hf:"):
14
- model = f"hf:{model}"
15
-
16
- api_key = os.getenv("SYNTHETIC_API_KEY") or os.getenv("SYN_API_KEY")
17
- if not api_key:
18
- raise AIError.authentication_error("SYNTHETIC_API_KEY or SYN_API_KEY not found in environment variables")
19
-
20
- url = "https://api.synthetic.new/openai/v1/chat/completions"
21
- headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
22
-
23
- data = {"model": model, "messages": messages, "temperature": temperature, "max_completion_tokens": max_tokens}
24
-
25
- try:
26
- response = httpx.post(url, headers=headers, json=data, timeout=120)
27
- response.raise_for_status()
28
- response_data = response.json()
29
- content = response_data["choices"][0]["message"]["content"]
30
- if content is None:
31
- raise AIError.model_error("Synthetic.new API returned null content")
32
- if content == "":
33
- raise AIError.model_error("Synthetic.new API returned empty content")
34
- return content
35
- except httpx.HTTPStatusError as e:
36
- if e.response.status_code == 429:
37
- raise AIError.rate_limit_error(f"Synthetic.new API rate limit exceeded: {e.response.text}") from e
38
- raise AIError.model_error(f"Synthetic.new API error: {e.response.status_code} - {e.response.text}") from e
39
- except httpx.TimeoutException as e:
40
- raise AIError.timeout_error(f"Synthetic.new API request timed out: {str(e)}") from e
41
- except Exception as e:
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 os
3
+ from gac.providers.base import OpenAICompatibleProvider, ProviderConfig
4
4
 
5
- import httpx
6
5
 
7
- from gac.errors import AIError
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
- def call_together_api(model: str, messages: list[dict], temperature: float, max_tokens: int) -> str:
11
- """Call Together AI API directly."""
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 os
4
-
5
- import httpx
6
-
7
- from gac.errors import AIError
8
-
9
-
10
- def _call_zai_api_impl(
11
- url: str, api_name: str, model: str, messages: list[dict], temperature: float, max_tokens: int
12
- ) -> str:
13
- """Internal implementation for Z.AI API calls."""
14
- api_key = os.getenv("ZAI_API_KEY")
15
- if not api_key:
16
- raise AIError.authentication_error("ZAI_API_KEY not found in environment variables")
17
-
18
- headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
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
-
26
- # Handle different possible response structures
27
- if "choices" in response_data and len(response_data["choices"]) > 0:
28
- choice = response_data["choices"][0]
29
- if "message" in choice and "content" in choice["message"]:
30
- content = choice["message"]["content"]
31
- if content is None:
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
@@ -93,7 +93,7 @@ class SecretPatterns:
93
93
  ]
94
94
 
95
95
  @classmethod
96
- def get_all_patterns(cls) -> dict[str, re.Pattern]:
96
+ def get_all_patterns(cls) -> dict[str, re.Pattern[str]]:
97
97
  """Get all secret detection patterns.
98
98
 
99
99
  Returns:
@@ -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>