gac 3.8.1__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 +18 -49
- gac/cli.py +14 -10
- gac/commit_executor.py +59 -0
- gac/config.py +28 -3
- 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 +222 -959
- gac/model_cli.py +2 -1
- gac/model_identifier.py +70 -0
- gac/oauth/claude_code.py +2 -2
- gac/oauth/qwen_oauth.py +4 -0
- gac/oauth/token_store.py +2 -2
- gac/oauth_retry.py +161 -0
- gac/postprocess.py +155 -0
- gac/prompt.py +20 -490
- gac/prompt_builder.py +88 -0
- gac/providers/README.md +437 -0
- gac/providers/__init__.py +70 -81
- gac/providers/anthropic.py +12 -56
- gac/providers/azure_openai.py +48 -92
- gac/providers/base.py +329 -0
- gac/providers/cerebras.py +10 -43
- gac/providers/chutes.py +16 -72
- gac/providers/claude_code.py +64 -97
- gac/providers/custom_anthropic.py +51 -85
- gac/providers/custom_openai.py +29 -87
- gac/providers/deepseek.py +10 -43
- gac/providers/error_handler.py +139 -0
- gac/providers/fireworks.py +10 -43
- gac/providers/gemini.py +66 -73
- gac/providers/groq.py +10 -62
- gac/providers/kimi_coding.py +19 -59
- gac/providers/lmstudio.py +62 -52
- gac/providers/minimax.py +10 -43
- gac/providers/mistral.py +10 -43
- gac/providers/moonshot.py +10 -43
- gac/providers/ollama.py +54 -41
- gac/providers/openai.py +30 -46
- gac/providers/openrouter.py +15 -62
- gac/providers/protocol.py +71 -0
- gac/providers/qwen.py +55 -67
- gac/providers/registry.py +58 -0
- gac/providers/replicate.py +137 -91
- gac/providers/streamlake.py +26 -56
- gac/providers/synthetic.py +35 -47
- gac/providers/together.py +10 -43
- gac/providers/zai.py +21 -59
- 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 +6 -5
- gac/workflow_context.py +162 -0
- {gac-3.8.1.dist-info → gac-3.10.10.dist-info}/METADATA +1 -1
- gac-3.10.10.dist-info/RECORD +79 -0
- gac/constants.py +0 -328
- gac-3.8.1.dist-info/RECORD +0 -56
- {gac-3.8.1.dist-info → gac-3.10.10.dist-info}/WHEEL +0 -0
- {gac-3.8.1.dist-info → gac-3.10.10.dist-info}/entry_points.txt +0 -0
- {gac-3.8.1.dist-info → gac-3.10.10.dist-info}/licenses/LICENSE +0 -0
gac/providers/claude_code.py
CHANGED
|
@@ -1,112 +1,79 @@
|
|
|
1
|
-
"""Claude Code provider
|
|
1
|
+
"""Claude Code API provider for gac.
|
|
2
2
|
|
|
3
3
|
This provider allows users with Claude Code subscriptions to use their OAuth tokens
|
|
4
4
|
instead of paying for the expensive Anthropic API.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
import
|
|
8
|
-
import os
|
|
7
|
+
from typing import Any
|
|
9
8
|
|
|
10
|
-
import httpx
|
|
11
|
-
|
|
12
|
-
from gac.constants import ProviderDefaults
|
|
13
9
|
from gac.errors import AIError
|
|
14
|
-
from gac.
|
|
15
|
-
|
|
16
|
-
logger = logging.getLogger(__name__)
|
|
17
|
-
|
|
10
|
+
from gac.oauth.claude_code import load_stored_token
|
|
11
|
+
from gac.providers.base import AnthropicCompatibleProvider, ProviderConfig
|
|
18
12
|
|
|
19
|
-
def call_claude_code_api(model: str, messages: list[dict], temperature: float, max_tokens: int) -> str:
|
|
20
|
-
"""Call Claude Code API using OAuth token.
|
|
21
13
|
|
|
22
|
-
|
|
23
|
-
|
|
14
|
+
class ClaudeCodeProvider(AnthropicCompatibleProvider):
|
|
15
|
+
"""Claude Code OAuth provider with special system message requirements."""
|
|
24
16
|
|
|
25
|
-
|
|
26
|
-
|
|
17
|
+
config = ProviderConfig(
|
|
18
|
+
name="Claude Code",
|
|
19
|
+
api_key_env="CLAUDE_CODE_ACCESS_TOKEN",
|
|
20
|
+
base_url="https://api.anthropic.com/v1/messages",
|
|
21
|
+
)
|
|
27
22
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
23
|
+
def _get_api_key(self) -> str:
|
|
24
|
+
"""Get OAuth token from token store."""
|
|
25
|
+
token = load_stored_token()
|
|
26
|
+
if token:
|
|
27
|
+
return token
|
|
33
28
|
|
|
34
|
-
Returns:
|
|
35
|
-
Generated text response
|
|
36
|
-
|
|
37
|
-
Raises:
|
|
38
|
-
AIError: If authentication fails or API call fails
|
|
39
|
-
"""
|
|
40
|
-
access_token = os.getenv("CLAUDE_CODE_ACCESS_TOKEN")
|
|
41
|
-
if not access_token:
|
|
42
29
|
raise AIError.authentication_error(
|
|
43
|
-
"
|
|
44
|
-
"Please authenticate with Claude Code and set this token."
|
|
30
|
+
"Claude Code authentication not found. Run 'gac auth claude-code login' to authenticate."
|
|
45
31
|
)
|
|
46
32
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
if content is None:
|
|
95
|
-
raise AIError.model_error("Claude Code API returned null content")
|
|
96
|
-
if content == "":
|
|
97
|
-
raise AIError.model_error("Claude Code API returned empty content")
|
|
98
|
-
logger.debug("Claude Code API response received successfully")
|
|
99
|
-
return content
|
|
100
|
-
except httpx.HTTPStatusError as e:
|
|
101
|
-
if e.response.status_code == 401:
|
|
102
|
-
raise AIError.authentication_error(
|
|
103
|
-
f"Claude Code authentication failed: {e.response.text}. "
|
|
104
|
-
"Your token may have expired. Please re-authenticate."
|
|
105
|
-
) from e
|
|
106
|
-
if e.response.status_code == 429:
|
|
107
|
-
raise AIError.rate_limit_error(f"Claude Code API rate limit exceeded: {e.response.text}") from e
|
|
108
|
-
raise AIError.model_error(f"Claude Code API error: {e.response.status_code} - {e.response.text}") from e
|
|
109
|
-
except httpx.TimeoutException as e:
|
|
110
|
-
raise AIError.timeout_error(f"Claude Code API request timed out: {str(e)}") from e
|
|
111
|
-
except Exception as e:
|
|
112
|
-
raise AIError.model_error(f"Error calling Claude Code API: {str(e)}") from e
|
|
33
|
+
def _build_headers(self) -> dict[str, str]:
|
|
34
|
+
"""Build headers with OAuth token and special anthropic-beta."""
|
|
35
|
+
headers = super()._build_headers()
|
|
36
|
+
# Replace x-api-key with Bearer token
|
|
37
|
+
if "x-api-key" in headers:
|
|
38
|
+
del headers["x-api-key"]
|
|
39
|
+
headers["Authorization"] = f"Bearer {self.api_key}"
|
|
40
|
+
# Add special OAuth beta header
|
|
41
|
+
headers["anthropic-beta"] = "oauth-2025-04-20"
|
|
42
|
+
return headers
|
|
43
|
+
|
|
44
|
+
def _build_request_body(
|
|
45
|
+
self, messages: list[dict[str, Any]], temperature: float, max_tokens: int, model: str, **kwargs: Any
|
|
46
|
+
) -> dict[str, Any]:
|
|
47
|
+
"""Build Anthropic-style request with fixed system message.
|
|
48
|
+
|
|
49
|
+
IMPORTANT: Claude Code OAuth tokens require the system message to be EXACTLY
|
|
50
|
+
"You are Claude Code, Anthropic's official CLI for Claude." with NO additional content.
|
|
51
|
+
Any other instructions must be moved to the first user message.
|
|
52
|
+
"""
|
|
53
|
+
# Extract and process messages
|
|
54
|
+
anthropic_messages = []
|
|
55
|
+
system_instructions = ""
|
|
56
|
+
|
|
57
|
+
for msg in messages:
|
|
58
|
+
if msg["role"] == "system":
|
|
59
|
+
system_instructions = msg["content"]
|
|
60
|
+
else:
|
|
61
|
+
anthropic_messages.append({"role": msg["role"], "content": msg["content"]})
|
|
62
|
+
|
|
63
|
+
# Move any system instructions into the first user message
|
|
64
|
+
if system_instructions and anthropic_messages:
|
|
65
|
+
first_user_msg = anthropic_messages[0]
|
|
66
|
+
first_user_msg["content"] = f"{system_instructions}\n\n{first_user_msg['content']}"
|
|
67
|
+
|
|
68
|
+
# Claude Code requires this exact system message
|
|
69
|
+
system_message = "You are Claude Code, Anthropic's official CLI for Claude."
|
|
70
|
+
|
|
71
|
+
body = {
|
|
72
|
+
"messages": anthropic_messages,
|
|
73
|
+
"temperature": temperature,
|
|
74
|
+
"max_tokens": max_tokens,
|
|
75
|
+
"system": system_message,
|
|
76
|
+
**kwargs,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return body
|
|
@@ -7,83 +7,67 @@ while using the same model capabilities as the standard Anthropic provider.
|
|
|
7
7
|
import json
|
|
8
8
|
import logging
|
|
9
9
|
import os
|
|
10
|
+
from typing import Any
|
|
10
11
|
|
|
11
|
-
import httpx
|
|
12
|
-
|
|
13
|
-
from gac.constants import ProviderDefaults
|
|
14
12
|
from gac.errors import AIError
|
|
15
|
-
from gac.
|
|
13
|
+
from gac.providers.base import AnthropicCompatibleProvider, ProviderConfig
|
|
16
14
|
|
|
17
15
|
logger = logging.getLogger(__name__)
|
|
18
16
|
|
|
19
17
|
|
|
20
|
-
|
|
21
|
-
"""
|
|
22
|
-
|
|
23
|
-
This provider is useful for:
|
|
24
|
-
- Anthropic-compatible proxies or gateways
|
|
25
|
-
- Self-hosted Anthropic-compatible services
|
|
26
|
-
- Other services implementing the Anthropic Messages API
|
|
27
|
-
|
|
28
|
-
Environment variables:
|
|
29
|
-
CUSTOM_ANTHROPIC_API_KEY: API key for authentication (required)
|
|
30
|
-
CUSTOM_ANTHROPIC_BASE_URL: Base URL for the API endpoint (required)
|
|
31
|
-
Example: https://your-proxy.example.com
|
|
32
|
-
CUSTOM_ANTHROPIC_VERSION: API version header (optional, defaults to '2023-06-01')
|
|
18
|
+
class CustomAnthropicProvider(AnthropicCompatibleProvider):
|
|
19
|
+
"""Custom Anthropic-compatible provider with configurable endpoint and version."""
|
|
33
20
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
21
|
+
config = ProviderConfig(
|
|
22
|
+
name="Custom Anthropic",
|
|
23
|
+
api_key_env="CUSTOM_ANTHROPIC_API_KEY",
|
|
24
|
+
base_url="", # Will be set in __init__ from environment
|
|
25
|
+
)
|
|
39
26
|
|
|
40
|
-
|
|
41
|
-
|
|
27
|
+
def __init__(self, config: ProviderConfig):
|
|
28
|
+
"""Initialize the provider with custom configuration from environment variables.
|
|
42
29
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
30
|
+
Environment variables:
|
|
31
|
+
CUSTOM_ANTHROPIC_API_KEY: API key for authentication (required)
|
|
32
|
+
CUSTOM_ANTHROPIC_BASE_URL: Base URL for the API endpoint (required)
|
|
33
|
+
CUSTOM_ANTHROPIC_VERSION: API version header (optional, defaults to '2023-06-01')
|
|
34
|
+
"""
|
|
35
|
+
# Get base_url from environment and normalize it
|
|
36
|
+
base_url = os.getenv("CUSTOM_ANTHROPIC_BASE_URL")
|
|
37
|
+
if not base_url:
|
|
38
|
+
raise AIError.model_error("CUSTOM_ANTHROPIC_BASE_URL environment variable not set")
|
|
49
39
|
|
|
50
|
-
base_url = os.getenv("CUSTOM_ANTHROPIC_BASE_URL")
|
|
51
|
-
if not base_url:
|
|
52
|
-
raise AIError.model_error("CUSTOM_ANTHROPIC_BASE_URL environment variable not set")
|
|
53
|
-
|
|
54
|
-
api_version = os.getenv("CUSTOM_ANTHROPIC_VERSION", "2023-06-01")
|
|
55
|
-
|
|
56
|
-
if "/v1/messages" not in base_url:
|
|
57
40
|
base_url = base_url.rstrip("/")
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
41
|
+
if base_url.endswith("/messages"):
|
|
42
|
+
pass # Already a complete endpoint URL
|
|
43
|
+
elif base_url.endswith("/v1"):
|
|
44
|
+
base_url = f"{base_url}/messages"
|
|
45
|
+
else:
|
|
46
|
+
base_url = f"{base_url}/v1/messages"
|
|
63
47
|
|
|
64
|
-
|
|
65
|
-
|
|
48
|
+
# Update config with the custom base URL
|
|
49
|
+
config.base_url = base_url
|
|
66
50
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
system_message = msg["content"]
|
|
70
|
-
else:
|
|
71
|
-
anthropic_messages.append({"role": msg["role"], "content": msg["content"]})
|
|
51
|
+
# Store the custom version for use in headers
|
|
52
|
+
self.custom_version = os.getenv("CUSTOM_ANTHROPIC_VERSION", "2023-06-01")
|
|
72
53
|
|
|
73
|
-
|
|
54
|
+
super().__init__(config)
|
|
74
55
|
|
|
75
|
-
|
|
76
|
-
|
|
56
|
+
def _build_headers(self) -> dict[str, str]:
|
|
57
|
+
"""Build headers with custom Anthropic version."""
|
|
58
|
+
headers = super()._build_headers()
|
|
59
|
+
headers["anthropic-version"] = self.custom_version
|
|
60
|
+
return headers
|
|
77
61
|
|
|
78
|
-
|
|
79
|
-
response
|
|
80
|
-
url, headers=headers, json=data, timeout=ProviderDefaults.HTTP_TIMEOUT, verify=get_ssl_verify()
|
|
81
|
-
)
|
|
82
|
-
response.raise_for_status()
|
|
83
|
-
response_data = response.json()
|
|
62
|
+
def _parse_response(self, response: dict[str, Any]) -> str:
|
|
63
|
+
"""Parse response with support for extended format (e.g., MiniMax with thinking).
|
|
84
64
|
|
|
65
|
+
Handles both:
|
|
66
|
+
- Standard Anthropic format: content[0].text
|
|
67
|
+
- Extended format: first item with type="text"
|
|
68
|
+
"""
|
|
85
69
|
try:
|
|
86
|
-
content_list =
|
|
70
|
+
content_list = response.get("content", [])
|
|
87
71
|
if not content_list:
|
|
88
72
|
raise AIError.model_error("Custom Anthropic API returned empty content array")
|
|
89
73
|
|
|
@@ -97,41 +81,23 @@ def call_custom_anthropic_api(model: str, messages: list[dict], temperature: flo
|
|
|
97
81
|
content = text_item["text"]
|
|
98
82
|
else:
|
|
99
83
|
logger.error(
|
|
100
|
-
f"Unexpected response format from Custom Anthropic API. Response: {json.dumps(
|
|
84
|
+
f"Unexpected response format from Custom Anthropic API. Response: {json.dumps(response)}"
|
|
101
85
|
)
|
|
102
86
|
raise AIError.model_error(
|
|
103
87
|
"Custom Anthropic API returned unexpected format. Expected 'text' field in content array."
|
|
104
88
|
)
|
|
89
|
+
|
|
90
|
+
if content is None:
|
|
91
|
+
raise AIError.model_error("Custom Anthropic API returned null content")
|
|
92
|
+
if content == "":
|
|
93
|
+
raise AIError.model_error("Custom Anthropic API returned empty content")
|
|
94
|
+
return content
|
|
105
95
|
except AIError:
|
|
106
96
|
raise
|
|
107
97
|
except (KeyError, IndexError, TypeError, StopIteration) as e:
|
|
108
|
-
logger.error(f"Unexpected response format from Custom Anthropic API. Response: {json.dumps(
|
|
98
|
+
logger.error(f"Unexpected response format from Custom Anthropic API. Response: {json.dumps(response)}")
|
|
109
99
|
raise AIError.model_error(
|
|
110
100
|
f"Custom Anthropic API returned unexpected format. Expected Anthropic-compatible response with "
|
|
111
101
|
f"'content[0].text' or items with type='text', but got: {type(e).__name__}. "
|
|
112
102
|
f"Check logs for full response structure."
|
|
113
103
|
) from e
|
|
114
|
-
|
|
115
|
-
if content is None:
|
|
116
|
-
raise AIError.model_error("Custom Anthropic API returned null content")
|
|
117
|
-
if content == "":
|
|
118
|
-
raise AIError.model_error("Custom Anthropic API returned empty content")
|
|
119
|
-
return content
|
|
120
|
-
except httpx.ConnectError as e:
|
|
121
|
-
raise AIError.connection_error(f"Custom Anthropic API connection failed: {str(e)}") from e
|
|
122
|
-
except httpx.HTTPStatusError as e:
|
|
123
|
-
status_code = e.response.status_code
|
|
124
|
-
error_text = e.response.text
|
|
125
|
-
|
|
126
|
-
if status_code == 401:
|
|
127
|
-
raise AIError.authentication_error(f"Custom Anthropic API authentication failed: {error_text}") from e
|
|
128
|
-
elif status_code == 429:
|
|
129
|
-
raise AIError.rate_limit_error(f"Custom Anthropic API rate limit exceeded: {error_text}") from e
|
|
130
|
-
else:
|
|
131
|
-
raise AIError.model_error(f"Custom Anthropic API error: {status_code} - {error_text}") from e
|
|
132
|
-
except httpx.TimeoutException as e:
|
|
133
|
-
raise AIError.timeout_error(f"Custom Anthropic API request timed out: {str(e)}") from e
|
|
134
|
-
except AIError:
|
|
135
|
-
raise
|
|
136
|
-
except Exception as e:
|
|
137
|
-
raise AIError.model_error(f"Error calling Custom Anthropic API: {str(e)}") from e
|
gac/providers/custom_openai.py
CHANGED
|
@@ -4,99 +4,41 @@ This provider allows users to specify a custom OpenAI-compatible endpoint
|
|
|
4
4
|
while using the same model capabilities as the standard OpenAI provider.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
import json
|
|
8
|
-
import logging
|
|
9
7
|
import os
|
|
8
|
+
from typing import Any
|
|
10
9
|
|
|
11
|
-
import httpx
|
|
12
|
-
|
|
13
|
-
from gac.constants import ProviderDefaults
|
|
14
10
|
from gac.errors import AIError
|
|
15
|
-
from gac.
|
|
16
|
-
|
|
17
|
-
logger = logging.getLogger(__name__)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def call_custom_openai_api(model: str, messages: list[dict], temperature: float, max_tokens: int) -> str:
|
|
21
|
-
"""Call a custom OpenAI-compatible API endpoint.
|
|
22
|
-
|
|
23
|
-
This provider is useful for:
|
|
24
|
-
- OpenAI-compatible proxies or gateways
|
|
25
|
-
- Self-hosted OpenAI-compatible services
|
|
26
|
-
- Other services implementing the OpenAI Chat Completions API
|
|
27
|
-
|
|
28
|
-
Environment variables:
|
|
29
|
-
CUSTOM_OPENAI_API_KEY: API key for authentication (required)
|
|
30
|
-
CUSTOM_OPENAI_BASE_URL: Base URL for the API endpoint (required)
|
|
31
|
-
Example: https://your-proxy.example.com/v1
|
|
32
|
-
Example: https://your-custom-endpoint.com
|
|
33
|
-
|
|
34
|
-
Args:
|
|
35
|
-
model: The model to use (e.g., 'gpt-4', 'gpt-3.5-turbo')
|
|
36
|
-
messages: List of message dictionaries with 'role' and 'content' keys
|
|
37
|
-
temperature: Controls randomness (0.0-1.0)
|
|
38
|
-
max_tokens: Maximum tokens in the response
|
|
39
|
-
|
|
40
|
-
Returns:
|
|
41
|
-
The generated commit message
|
|
42
|
-
|
|
43
|
-
Raises:
|
|
44
|
-
AIError: If authentication fails, API errors occur, or response is invalid
|
|
45
|
-
"""
|
|
46
|
-
api_key = os.getenv("CUSTOM_OPENAI_API_KEY")
|
|
47
|
-
if not api_key:
|
|
48
|
-
raise AIError.authentication_error("CUSTOM_OPENAI_API_KEY environment variable not set")
|
|
49
|
-
|
|
50
|
-
base_url = os.getenv("CUSTOM_OPENAI_BASE_URL")
|
|
51
|
-
if not base_url:
|
|
52
|
-
raise AIError.model_error("CUSTOM_OPENAI_BASE_URL environment variable not set")
|
|
53
|
-
|
|
54
|
-
if "/chat/completions" not in base_url:
|
|
55
|
-
base_url = base_url.rstrip("/")
|
|
56
|
-
url = f"{base_url}/chat/completions"
|
|
57
|
-
else:
|
|
58
|
-
url = base_url
|
|
59
|
-
|
|
60
|
-
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
|
|
11
|
+
from gac.providers.base import OpenAICompatibleProvider, ProviderConfig
|
|
61
12
|
|
|
62
|
-
data = {"model": model, "messages": messages, "temperature": temperature, "max_completion_tokens": max_tokens}
|
|
63
13
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
url, headers=headers, json=data, timeout=ProviderDefaults.HTTP_TIMEOUT, verify=get_ssl_verify()
|
|
67
|
-
)
|
|
68
|
-
response.raise_for_status()
|
|
69
|
-
response_data = response.json()
|
|
14
|
+
class CustomOpenAIProvider(OpenAICompatibleProvider):
|
|
15
|
+
"""Custom OpenAI-compatible provider with configurable base URL."""
|
|
70
16
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
f"Custom OpenAI API returned unexpected format. Expected OpenAI-compatible response with "
|
|
77
|
-
f"'choices[0].message.content', but got: {type(e).__name__}. Check logs for full response structure."
|
|
78
|
-
) from e
|
|
17
|
+
config = ProviderConfig(
|
|
18
|
+
name="Custom OpenAI",
|
|
19
|
+
api_key_env="CUSTOM_OPENAI_API_KEY",
|
|
20
|
+
base_url="", # Will be set in __init__
|
|
21
|
+
)
|
|
79
22
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
except httpx.ConnectError as e:
|
|
86
|
-
raise AIError.connection_error(f"Custom OpenAI API connection failed: {str(e)}") from e
|
|
87
|
-
except httpx.HTTPStatusError as e:
|
|
88
|
-
status_code = e.response.status_code
|
|
89
|
-
error_text = e.response.text
|
|
23
|
+
def __init__(self, config: ProviderConfig):
|
|
24
|
+
"""Initialize with base URL from environment."""
|
|
25
|
+
base_url = os.getenv("CUSTOM_OPENAI_BASE_URL")
|
|
26
|
+
if not base_url:
|
|
27
|
+
raise AIError.model_error("CUSTOM_OPENAI_BASE_URL environment variable not set")
|
|
90
28
|
|
|
91
|
-
if
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
raise AIError.rate_limit_error(f"Custom OpenAI API rate limit exceeded: {error_text}") from e
|
|
29
|
+
if "/chat/completions" not in base_url:
|
|
30
|
+
base_url = base_url.rstrip("/")
|
|
31
|
+
url = f"{base_url}/chat/completions"
|
|
95
32
|
else:
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
33
|
+
url = base_url
|
|
34
|
+
|
|
35
|
+
config.base_url = url
|
|
36
|
+
super().__init__(config)
|
|
37
|
+
|
|
38
|
+
def _build_request_body(
|
|
39
|
+
self, messages: list[dict[str, Any]], temperature: float, max_tokens: int, model: str, **kwargs: Any
|
|
40
|
+
) -> dict[str, Any]:
|
|
41
|
+
"""Build request body with max_completion_tokens instead of max_tokens."""
|
|
42
|
+
data = super()._build_request_body(messages, temperature, max_tokens, model, **kwargs)
|
|
43
|
+
data["max_completion_tokens"] = data.pop("max_tokens")
|
|
44
|
+
return data
|
gac/providers/deepseek.py
CHANGED
|
@@ -1,48 +1,15 @@
|
|
|
1
1
|
"""DeepSeek 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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
class DeepSeekProvider(OpenAICompatibleProvider):
|
|
7
|
+
config = ProviderConfig(
|
|
8
|
+
name="DeepSeek",
|
|
9
|
+
api_key_env="DEEPSEEK_API_KEY",
|
|
10
|
+
base_url="https://api.deepseek.com/v1",
|
|
11
|
+
)
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def call_deepseek_api(model: str, messages: list[dict], temperature: float, max_tokens: int) -> str:
|
|
16
|
-
"""Call DeepSeek API directly."""
|
|
17
|
-
api_key = os.getenv("DEEPSEEK_API_KEY")
|
|
18
|
-
if not api_key:
|
|
19
|
-
raise AIError.authentication_error("DEEPSEEK_API_KEY not found in environment variables")
|
|
20
|
-
|
|
21
|
-
url = "https://api.deepseek.com/v1/chat/completions"
|
|
22
|
-
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
|
|
23
|
-
|
|
24
|
-
data = {"model": model, "messages": messages, "temperature": temperature, "max_tokens": max_tokens}
|
|
25
|
-
|
|
26
|
-
logger.debug(f"Calling DeepSeek API with model={model}")
|
|
27
|
-
|
|
28
|
-
try:
|
|
29
|
-
response = httpx.post(
|
|
30
|
-
url, headers=headers, json=data, timeout=ProviderDefaults.HTTP_TIMEOUT, verify=get_ssl_verify()
|
|
31
|
-
)
|
|
32
|
-
response.raise_for_status()
|
|
33
|
-
response_data = response.json()
|
|
34
|
-
content = response_data["choices"][0]["message"]["content"]
|
|
35
|
-
if content is None:
|
|
36
|
-
raise AIError.model_error("DeepSeek API returned null content")
|
|
37
|
-
if content == "":
|
|
38
|
-
raise AIError.model_error("DeepSeek API returned empty content")
|
|
39
|
-
logger.debug("DeepSeek API response received successfully")
|
|
40
|
-
return content
|
|
41
|
-
except httpx.HTTPStatusError as e:
|
|
42
|
-
if e.response.status_code == 429:
|
|
43
|
-
raise AIError.rate_limit_error(f"DeepSeek API rate limit exceeded: {e.response.text}") from e
|
|
44
|
-
raise AIError.model_error(f"DeepSeek API error: {e.response.status_code} - {e.response.text}") from e
|
|
45
|
-
except httpx.TimeoutException as e:
|
|
46
|
-
raise AIError.timeout_error(f"DeepSeek API request timed out: {str(e)}") from e
|
|
47
|
-
except Exception as e:
|
|
48
|
-
raise AIError.model_error(f"Error calling DeepSeek API: {str(e)}") from e
|
|
13
|
+
def _get_api_url(self, model: str | None = None) -> str:
|
|
14
|
+
"""Get DeepSeek API URL with /chat/completions endpoint."""
|
|
15
|
+
return f"{self.config.base_url}/chat/completions"
|