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/fireworks.py
CHANGED
|
@@ -1,38 +1,15 @@
|
|
|
1
1
|
"""Fireworks 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 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
|
-
|
|
11
|
-
|
|
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
|
-
|
|
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
|
-
contents
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
4
|
-
import os
|
|
3
|
+
from gac.providers.base import OpenAICompatibleProvider, ProviderConfig
|
|
5
4
|
|
|
6
|
-
import httpx
|
|
7
5
|
|
|
8
|
-
|
|
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
|
-
|
|
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"
|
gac/providers/kimi_coding.py
CHANGED
|
@@ -1,63 +1,27 @@
|
|
|
1
1
|
"""Kimi Coding AI provider implementation."""
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import logging
|
|
5
|
-
import os
|
|
3
|
+
from typing import Any
|
|
6
4
|
|
|
7
|
-
import
|
|
5
|
+
from gac.providers.base import OpenAICompatibleProvider, ProviderConfig
|
|
8
6
|
|
|
9
|
-
from gac.errors import AIError
|
|
10
7
|
|
|
11
|
-
|
|
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
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
|
1
|
+
"""LM Studio API provider for gac."""
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
4
|
from typing import Any
|
|
5
5
|
|
|
6
|
-
import
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
24
|
-
"
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
"
|
|
28
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
#
|
|
45
|
-
content =
|
|
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("
|
|
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
|
|
3
|
+
from gac.providers.base import OpenAICompatibleProvider, ProviderConfig
|
|
4
4
|
|
|
5
|
-
import httpx
|
|
6
5
|
|
|
7
|
-
|
|
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
|
-
|
|
11
|
-
|
|
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
|
|
3
|
+
from gac.providers.base import OpenAICompatibleProvider, ProviderConfig
|
|
4
4
|
|
|
5
|
-
import httpx
|
|
6
5
|
|
|
7
|
-
|
|
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
|
-
|
|
11
|
-
|
|
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
|
|
3
|
+
from gac.providers.base import OpenAICompatibleProvider, ProviderConfig
|
|
4
4
|
|
|
5
|
-
import httpx
|
|
6
5
|
|
|
7
|
-
|
|
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
|
-
|
|
11
|
-
|
|
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"
|