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,38 +1,15 @@
1
1
  """Fireworks 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 FireworksProvider(OpenAICompatibleProvider):
7
+ config = ProviderConfig(
8
+ name="Fireworks",
9
+ api_key_env="FIREWORKS_API_KEY",
10
+ base_url="https://api.fireworks.ai/inference/v1",
11
+ )
8
12
 
9
-
10
- def call_fireworks_api(model: str, messages: list[dict], temperature: float, max_tokens: int) -> str:
11
- """Call Fireworks AI API directly."""
12
- api_key = os.getenv("FIREWORKS_API_KEY")
13
- if not api_key:
14
- raise AIError.authentication_error("FIREWORKS_API_KEY not found in environment variables")
15
-
16
- url = "https://api.fireworks.ai/inference/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("Fireworks AI API returned null content")
28
- if content == "":
29
- raise AIError.model_error("Fireworks 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"Fireworks AI API rate limit exceeded: {e.response.text}") from e
34
- raise AIError.model_error(f"Fireworks AI API error: {e.response.status_code} - {e.response.text}") from e
35
- except httpx.TimeoutException as e:
36
- raise AIError.timeout_error(f"Fireworks AI API request timed out: {str(e)}") from e
37
- except Exception as e:
38
- raise AIError.model_error(f"Error calling Fireworks AI API: {str(e)}") from e
13
+ def _get_api_url(self, model: str | None = None) -> str:
14
+ """Get Fireworks API URL with /chat/completions endpoint."""
15
+ return f"{self.config.base_url}/chat/completions"
gac/providers/gemini.py CHANGED
@@ -1,61 +1,74 @@
1
1
  """Gemini AI provider implementation."""
2
2
 
3
- import os
4
3
  from typing import Any
5
4
 
6
- import httpx
7
-
8
5
  from gac.errors import AIError
9
-
10
-
11
- def call_gemini_api(model: str, messages: list[dict[str, Any]], temperature: float, max_tokens: int) -> str:
12
- """Call Gemini API directly."""
13
- api_key = os.getenv("GEMINI_API_KEY")
14
- if not api_key:
15
- raise AIError.authentication_error("GEMINI_API_KEY not found in environment variables")
16
-
17
- url = f"https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent"
18
-
19
- # Build Gemini request payload, converting roles to supported values.
20
- contents: list[dict[str, Any]] = []
21
- system_instruction_parts: list[dict[str, str]] = []
22
-
23
- for msg in messages:
24
- role = msg.get("role")
25
- content_value = msg.get("content")
26
- content = "" if content_value is None else str(content_value)
27
-
28
- if role == "system":
29
- if content.strip():
30
- system_instruction_parts.append({"text": content})
31
- continue
32
-
33
- if role == "assistant":
34
- gemini_role = "model"
35
- elif role == "user":
36
- gemini_role = "user"
37
- else:
38
- raise AIError.model_error(f"Unsupported message role for Gemini API: {role}")
39
-
40
- contents.append({"role": gemini_role, "parts": [{"text": content}]})
41
-
42
- payload: dict[str, Any] = {
43
- "contents": contents,
44
- "generationConfig": {"temperature": temperature, "maxOutputTokens": max_tokens},
45
- }
46
-
47
- if system_instruction_parts:
48
- payload["systemInstruction"] = {"role": "system", "parts": system_instruction_parts}
49
-
50
- headers = {"x-goog-api-key": api_key, "Content-Type": "application/json"}
51
-
52
- try:
53
- response = httpx.post(url, headers=headers, json=payload, timeout=120)
54
- response.raise_for_status()
55
- response_data = response.json()
56
-
57
- # Check for candidates and proper response structure
58
- candidates = response_data.get("candidates")
6
+ from gac.providers.base import GenericHTTPProvider, ProviderConfig
7
+
8
+
9
+ class GeminiProvider(GenericHTTPProvider):
10
+ """Google Gemini provider with custom format and role conversion."""
11
+
12
+ config = ProviderConfig(
13
+ name="Gemini",
14
+ api_key_env="GEMINI_API_KEY",
15
+ base_url="https://generativelanguage.googleapis.com/v1beta",
16
+ )
17
+
18
+ def _get_api_url(self, model: str | None = None) -> str:
19
+ """Build Gemini URL with model in path."""
20
+ if model is None:
21
+ return super()._get_api_url(model)
22
+ return f"{self.config.base_url}/models/{model}:generateContent"
23
+
24
+ def _build_headers(self) -> dict[str, str]:
25
+ """Build headers with Google API key."""
26
+ headers = super()._build_headers()
27
+ # Remove any Authorization header
28
+ if "Authorization" in headers:
29
+ del headers["Authorization"]
30
+ headers["x-goog-api-key"] = self.api_key
31
+ return headers
32
+
33
+ def _build_request_body(
34
+ self, messages: list[dict[str, Any]], temperature: float, max_tokens: int, model: str, **kwargs: Any
35
+ ) -> dict[str, Any]:
36
+ """Build Gemini-format request with role conversion and system instruction extraction."""
37
+ contents: list[dict[str, Any]] = []
38
+ system_instruction_parts: list[dict[str, str]] = []
39
+
40
+ for msg in messages:
41
+ role = msg.get("role")
42
+ content_value = msg.get("content")
43
+ content = "" if content_value is None else str(content_value)
44
+
45
+ if role == "system":
46
+ if content.strip():
47
+ system_instruction_parts.append({"text": content})
48
+ continue
49
+
50
+ if role == "assistant":
51
+ gemini_role = "model"
52
+ elif role == "user":
53
+ gemini_role = "user"
54
+ else:
55
+ raise AIError.model_error(f"Unsupported message role for Gemini API: {role}")
56
+
57
+ contents.append({"role": gemini_role, "parts": [{"text": content}]})
58
+
59
+ body: dict[str, Any] = {
60
+ "contents": contents,
61
+ "generationConfig": {"temperature": temperature, "maxOutputTokens": max_tokens},
62
+ }
63
+
64
+ if system_instruction_parts:
65
+ body["systemInstruction"] = {"role": "system", "parts": system_instruction_parts}
66
+
67
+ return body
68
+
69
+ def _parse_response(self, response: dict[str, Any]) -> str:
70
+ """Parse Gemini response format: candidates[0].content.parts[0].text."""
71
+ candidates = response.get("candidates")
59
72
  if not candidates:
60
73
  raise AIError.model_error("Gemini API response missing candidates")
61
74
 
@@ -75,13 +88,3 @@ def call_gemini_api(model: str, messages: list[dict[str, Any]], temperature: flo
75
88
  raise AIError.model_error("Gemini API response missing text content")
76
89
 
77
90
  return content_text
78
- except AIError:
79
- raise
80
- except httpx.HTTPStatusError as e:
81
- if e.response.status_code == 429:
82
- raise AIError.rate_limit_error(f"Gemini API rate limit exceeded: {e.response.text}") from e
83
- raise AIError.model_error(f"Gemini API error: {e.response.status_code} - {e.response.text}") from e
84
- except httpx.TimeoutException as e:
85
- raise AIError.timeout_error(f"Gemini API request timed out: {str(e)}") from e
86
- except Exception as e:
87
- raise AIError.model_error(f"Error calling Gemini API: {str(e)}") from e
gac/providers/groq.py CHANGED
@@ -1,63 +1,15 @@
1
1
  """Groq API provider for gac."""
2
2
 
3
- import logging
4
- import os
3
+ from gac.providers.base import OpenAICompatibleProvider, ProviderConfig
5
4
 
6
- import httpx
7
5
 
8
- from gac.errors import AIError
6
+ class GroqProvider(OpenAICompatibleProvider):
7
+ config = ProviderConfig(
8
+ name="Groq",
9
+ api_key_env="GROQ_API_KEY",
10
+ base_url="https://api.groq.com/openai/v1",
11
+ )
9
12
 
10
- logger = logging.getLogger(__name__)
11
-
12
-
13
- def call_groq_api(model: str, messages: list[dict], temperature: float, max_tokens: int) -> str:
14
- """Call Groq API directly."""
15
- api_key = os.getenv("GROQ_API_KEY")
16
- if not api_key:
17
- raise AIError.authentication_error("GROQ_API_KEY not found in environment variables")
18
-
19
- url = "https://api.groq.com/openai/v1/chat/completions"
20
- headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
21
-
22
- data = {"model": model, "messages": messages, "temperature": temperature, "max_tokens": max_tokens}
23
-
24
- try:
25
- response = httpx.post(url, headers=headers, json=data, timeout=120)
26
- response.raise_for_status()
27
- response_data = response.json()
28
-
29
- # Debug logging to understand response structure
30
- logger.debug(f"Groq API response: {response_data}")
31
-
32
- # Handle different response formats
33
- if "choices" in response_data and len(response_data["choices"]) > 0:
34
- choice = response_data["choices"][0]
35
- if "message" in choice and "content" in choice["message"]:
36
- content = choice["message"]["content"]
37
- logger.debug(f"Found content in message.content: {repr(content)}")
38
- if content is None:
39
- raise AIError.model_error("Groq API returned null content")
40
- if content == "":
41
- raise AIError.model_error("Groq API returned empty content")
42
- return content
43
- elif "text" in choice:
44
- content = choice["text"]
45
- logger.debug(f"Found content in choice.text: {repr(content)}")
46
- if content is None:
47
- logger.warning("Groq API returned None content in choice.text")
48
- return ""
49
- return content
50
- else:
51
- logger.warning(f"Unexpected choice structure: {choice}")
52
-
53
- # If we can't find content in the expected places, raise an error
54
- logger.error(f"Unexpected response format from Groq API: {response_data}")
55
- raise AIError.model_error(f"Unexpected response format from Groq API: {response_data}")
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
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
62
- except Exception as e:
63
- raise AIError.model_error(f"Error calling Groq API: {str(e)}") from e
13
+ def _get_api_url(self, model: str | None = None) -> str:
14
+ """Get Groq API URL with /chat/completions endpoint."""
15
+ return f"{self.config.base_url}/chat/completions"
@@ -1,63 +1,27 @@
1
1
  """Kimi Coding AI provider implementation."""
2
2
 
3
- import json
4
- import logging
5
- import os
3
+ from typing import Any
6
4
 
7
- import httpx
5
+ from gac.providers.base import OpenAICompatibleProvider, ProviderConfig
8
6
 
9
- from gac.errors import AIError
10
7
 
11
- logger = logging.getLogger(__name__)
8
+ class KimiCodingProvider(OpenAICompatibleProvider):
9
+ """Kimi Coding API provider using OpenAI-compatible format."""
12
10
 
11
+ config = ProviderConfig(
12
+ name="Kimi Coding",
13
+ api_key_env="KIMI_CODING_API_KEY",
14
+ base_url="https://api.kimi.com/coding/v1",
15
+ )
13
16
 
14
- def call_kimi_coding_api(model: str, messages: list[dict], temperature: float, max_tokens: int) -> str:
15
- """Call Kimi Coding API using OpenAI-compatible endpoint."""
16
- api_key = os.getenv("KIMI_CODING_API_KEY")
17
- if not api_key:
18
- raise AIError.authentication_error("KIMI_CODING_API_KEY not found in environment variables")
17
+ def _get_api_url(self, model: str | None = None) -> str:
18
+ """Get Kimi Coding API URL with /chat/completions endpoint."""
19
+ return f"{self.config.base_url}/chat/completions"
19
20
 
20
- base_url = "https://api.kimi.com/coding/v1"
21
- url = f"{base_url}/chat/completions"
22
- headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
23
-
24
- # Use standard OpenAI format - no message conversion needed
25
- data = {"model": model, "messages": messages, "temperature": temperature, "max_completion_tokens": max_tokens}
26
-
27
- try:
28
- response = httpx.post(url, headers=headers, json=data, timeout=120)
29
- response.raise_for_status()
30
- response_data = response.json()
31
-
32
- try:
33
- content = response_data["choices"][0]["message"]["content"]
34
- except (KeyError, IndexError, TypeError) as e:
35
- logger.error(f"Unexpected response format from Kimi Coding API. Response: {json.dumps(response_data)}")
36
- raise AIError.model_error(
37
- f"Kimi Coding API returned unexpected format. Expected OpenAI-compatible response with "
38
- f"'choices[0].message.content', but got: {type(e).__name__}. Check logs for full response structure."
39
- ) from e
40
-
41
- if content is None:
42
- raise AIError.model_error("Kimi Coding API returned null content")
43
- if content == "":
44
- raise AIError.model_error("Kimi Coding API returned empty content")
45
- return content
46
- except httpx.ConnectError as e:
47
- raise AIError.connection_error(f"Kimi Coding API connection failed: {str(e)}") from e
48
- except httpx.HTTPStatusError as e:
49
- status_code = e.response.status_code
50
- error_text = e.response.text
51
-
52
- if status_code == 401:
53
- raise AIError.authentication_error(f"Kimi Coding API authentication failed: {error_text}") from e
54
- elif status_code == 429:
55
- raise AIError.rate_limit_error(f"Kimi Coding API rate limit exceeded: {error_text}") from e
56
- else:
57
- raise AIError.model_error(f"Kimi Coding API error: {status_code} - {error_text}") from e
58
- except httpx.TimeoutException as e:
59
- raise AIError.timeout_error(f"Kimi Coding API request timed out: {str(e)}") from e
60
- except AIError:
61
- raise
62
- except Exception as e:
63
- raise AIError.model_error(f"Error calling Kimi Coding API: {str(e)}") from e
21
+ def _build_request_body(
22
+ self, messages: list[dict[str, Any]], temperature: float, max_tokens: int, model: str, **kwargs: Any
23
+ ) -> dict[str, Any]:
24
+ """Build request body with max_completion_tokens instead of max_tokens."""
25
+ data = super()._build_request_body(messages, temperature, max_tokens, model, **kwargs)
26
+ data["max_completion_tokens"] = data.pop("max_tokens")
27
+ return data
gac/providers/lmstudio.py CHANGED
@@ -1,59 +1,80 @@
1
- """LM Studio AI provider implementation."""
1
+ """LM Studio API provider for gac."""
2
2
 
3
3
  import os
4
4
  from typing import Any
5
5
 
6
- import httpx
6
+ from gac.providers.base import OpenAICompatibleProvider, ProviderConfig
7
7
 
8
- from gac.errors import AIError
9
8
 
9
+ class LMStudioProvider(OpenAICompatibleProvider):
10
+ """LM Studio provider for local OpenAI-compatible models."""
10
11
 
11
- def call_lmstudio_api(model: str, messages: list[dict[str, Any]], temperature: float, max_tokens: int) -> str:
12
- """Call LM Studio's OpenAI-compatible API."""
13
- api_url = os.getenv("LMSTUDIO_API_URL", "http://localhost:1234")
14
- api_url = api_url.rstrip("/")
12
+ config = ProviderConfig(
13
+ name="LM Studio",
14
+ api_key_env="LMSTUDIO_API_KEY",
15
+ base_url="http://localhost:1234/v1",
16
+ )
15
17
 
16
- url = f"{api_url}/v1/chat/completions"
18
+ def __init__(self, config: ProviderConfig):
19
+ """Initialize with configurable URL from environment."""
20
+ super().__init__(config)
21
+ # Allow URL override via environment variable
22
+ api_url = os.getenv("LMSTUDIO_API_URL", "http://localhost:1234")
23
+ api_url = api_url.rstrip("/")
24
+ self.config.base_url = f"{api_url}/v1"
17
25
 
18
- headers = {"Content-Type": "application/json"}
19
- api_key = os.getenv("LMSTUDIO_API_KEY")
20
- if api_key:
21
- headers["Authorization"] = f"Bearer {api_key}"
26
+ def _get_api_key(self) -> str:
27
+ """Get optional API key for LM Studio."""
28
+ api_key = os.getenv(self.config.api_key_env)
29
+ if not api_key:
30
+ return "" # Optional API key
31
+ return api_key
22
32
 
23
- payload: dict[str, Any] = {
24
- "model": model,
25
- "messages": messages,
26
- "temperature": temperature,
27
- "max_tokens": max_tokens,
28
- "stream": False,
29
- }
33
+ def _build_headers(self) -> dict[str, str]:
34
+ """Build headers with optional API key."""
35
+ headers = super()._build_headers()
36
+ # Remove Bearer token from parent if it was added
37
+ if "Authorization" in headers:
38
+ del headers["Authorization"]
39
+ # Add optional Authorization
40
+ api_key = os.getenv("LMSTUDIO_API_KEY")
41
+ if api_key:
42
+ headers["Authorization"] = f"Bearer {api_key}"
43
+ return headers
30
44
 
31
- try:
32
- response = httpx.post(url, headers=headers, json=payload, timeout=120)
33
- response.raise_for_status()
34
- response_data = response.json()
35
- choices = response_data.get("choices") or []
36
- if not choices:
37
- raise AIError.model_error("LM Studio API response missing choices")
45
+ def _get_api_url(self, model: str | None = None) -> str:
46
+ """Get LM Studio API URL with /chat/completions endpoint."""
47
+ return f"{self.config.base_url}/chat/completions"
38
48
 
39
- message = choices[0].get("message") or {}
40
- content = message.get("content")
41
- if content:
49
+ def _build_request_body(
50
+ self, messages: list[dict[str, Any]], temperature: float, max_tokens: int, model: str, **kwargs: Any
51
+ ) -> dict[str, Any]:
52
+ """Build OpenAI-compatible request body with stream disabled."""
53
+ body = super()._build_request_body(messages, temperature, max_tokens, model, **kwargs)
54
+ body["stream"] = False
55
+ return body
56
+
57
+ def _parse_response(self, response: dict[str, Any]) -> str:
58
+ """Parse OpenAI-compatible response with text field fallback."""
59
+ from gac.errors import AIError
60
+
61
+ choices = response.get("choices")
62
+ if not choices or not isinstance(choices, list):
63
+ raise AIError.model_error("Invalid response: missing choices")
64
+
65
+ # First try message.content (standard OpenAI format)
66
+ choice = choices[0]
67
+ content = choice.get("message", {}).get("content")
68
+ if content is not None:
69
+ if content == "":
70
+ raise AIError.model_error("Invalid response: empty content")
42
71
  return content
43
72
 
44
- # Some OpenAI-compatible servers return text field directly
45
- content = choices[0].get("text")
46
- if content:
73
+ # Fallback to text field (some OpenAI-compatible servers use this)
74
+ content = choice.get("text")
75
+ if content is not None:
76
+ if content == "":
77
+ raise AIError.model_error("Invalid response: empty content")
47
78
  return content
48
79
 
49
- raise AIError.model_error("LM Studio API response missing content")
50
- except httpx.ConnectError as e:
51
- raise AIError.connection_error(f"LM Studio connection failed: {str(e)}") from e
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
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
58
- except Exception as e:
59
- raise AIError.model_error(f"Error calling LM Studio API: {str(e)}") from e
80
+ raise AIError.model_error("Invalid response: missing content")
gac/providers/minimax.py CHANGED
@@ -1,38 +1,15 @@
1
1
  """MiniMax 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 MinimaxProvider(OpenAICompatibleProvider):
7
+ config = ProviderConfig(
8
+ name="MiniMax",
9
+ api_key_env="MINIMAX_API_KEY",
10
+ base_url="https://api.minimaxi.com/v1",
11
+ )
8
12
 
9
-
10
- def call_minimax_api(model: str, messages: list[dict], temperature: float, max_tokens: int) -> str:
11
- """Call MiniMax API directly."""
12
- api_key = os.getenv("MINIMAX_API_KEY")
13
- if not api_key:
14
- raise AIError.authentication_error("MINIMAX_API_KEY not found in environment variables")
15
-
16
- url = "https://api.minimax.io/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("MiniMax API returned null content")
28
- if content == "":
29
- raise AIError.model_error("MiniMax 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"MiniMax API rate limit exceeded: {e.response.text}") from e
34
- raise AIError.model_error(f"MiniMax API error: {e.response.status_code} - {e.response.text}") from e
35
- except httpx.TimeoutException as e:
36
- raise AIError.timeout_error(f"MiniMax API request timed out: {str(e)}") from e
37
- except Exception as e:
38
- raise AIError.model_error(f"Error calling MiniMax API: {str(e)}") from e
13
+ def _get_api_url(self, model: str | None = None) -> str:
14
+ """Get MiniMax API URL with /chat/completions endpoint."""
15
+ return f"{self.config.base_url}/chat/completions"
gac/providers/mistral.py CHANGED
@@ -1,38 +1,15 @@
1
1
  """Mistral 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 MistralProvider(OpenAICompatibleProvider):
7
+ config = ProviderConfig(
8
+ name="Mistral",
9
+ api_key_env="MISTRAL_API_KEY",
10
+ base_url="https://api.mistral.ai/v1",
11
+ )
8
12
 
9
-
10
- def call_mistral_api(model: str, messages: list[dict], temperature: float, max_tokens: int) -> str:
11
- """Call Mistral API directly."""
12
- api_key = os.getenv("MISTRAL_API_KEY")
13
- if not api_key:
14
- raise AIError.authentication_error("MISTRAL_API_KEY not found in environment variables")
15
-
16
- url = "https://api.mistral.ai/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("Mistral API returned null content")
28
- if content == "":
29
- raise AIError.model_error("Mistral 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"Mistral API rate limit exceeded: {e.response.text}") from e
34
- raise AIError.model_error(f"Mistral API error: {e.response.status_code} - {e.response.text}") from e
35
- except httpx.TimeoutException as e:
36
- raise AIError.timeout_error(f"Mistral API request timed out: {str(e)}") from e
37
- except Exception as e:
38
- raise AIError.model_error(f"Error calling Mistral API: {str(e)}") from e
13
+ def _get_api_url(self, model: str | None = None) -> str:
14
+ """Get Mistral API URL with /chat/completions endpoint."""
15
+ return f"{self.config.base_url}/chat/completions"
gac/providers/moonshot.py CHANGED
@@ -1,38 +1,15 @@
1
1
  """Moonshot AI 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 MoonshotProvider(OpenAICompatibleProvider):
7
+ config = ProviderConfig(
8
+ name="Moonshot",
9
+ api_key_env="MOONSHOT_API_KEY",
10
+ base_url="https://api.moonshot.cn/v1",
11
+ )
8
12
 
9
-
10
- def call_moonshot_api(model: str, messages: list[dict], temperature: float, max_tokens: int) -> str:
11
- """Call Moonshot AI API directly."""
12
- api_key = os.getenv("MOONSHOT_API_KEY")
13
- if not api_key:
14
- raise AIError.authentication_error("MOONSHOT_API_KEY not found in environment variables")
15
-
16
- url = "https://api.moonshot.ai/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("Moonshot AI API returned null content")
28
- if content == "":
29
- raise AIError.model_error("Moonshot 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"Moonshot AI API rate limit exceeded: {e.response.text}") from e
34
- raise AIError.model_error(f"Moonshot AI API error: {e.response.status_code} - {e.response.text}") from e
35
- except httpx.TimeoutException as e:
36
- raise AIError.timeout_error(f"Moonshot AI API request timed out: {str(e)}") from e
37
- except Exception as e:
38
- raise AIError.model_error(f"Error calling Moonshot AI API: {str(e)}") from e
13
+ def _get_api_url(self, model: str | None = None) -> str:
14
+ """Get Moonshot API URL with /chat/completions endpoint."""
15
+ return f"{self.config.base_url}/chat/completions"