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.
Files changed (76) hide show
  1. gac/__init__.py +4 -6
  2. gac/__version__.py +1 -1
  3. gac/ai_utils.py +18 -49
  4. gac/cli.py +14 -10
  5. gac/commit_executor.py +59 -0
  6. gac/config.py +28 -3
  7. gac/config_cli.py +19 -7
  8. gac/constants/__init__.py +34 -0
  9. gac/constants/commit.py +63 -0
  10. gac/constants/defaults.py +40 -0
  11. gac/constants/file_patterns.py +110 -0
  12. gac/constants/languages.py +119 -0
  13. gac/diff_cli.py +0 -22
  14. gac/errors.py +8 -2
  15. gac/git.py +6 -6
  16. gac/git_state_validator.py +193 -0
  17. gac/grouped_commit_workflow.py +458 -0
  18. gac/init_cli.py +2 -1
  19. gac/interactive_mode.py +179 -0
  20. gac/language_cli.py +0 -1
  21. gac/main.py +222 -959
  22. gac/model_cli.py +2 -1
  23. gac/model_identifier.py +70 -0
  24. gac/oauth/claude_code.py +2 -2
  25. gac/oauth/qwen_oauth.py +4 -0
  26. gac/oauth/token_store.py +2 -2
  27. gac/oauth_retry.py +161 -0
  28. gac/postprocess.py +155 -0
  29. gac/prompt.py +20 -490
  30. gac/prompt_builder.py +88 -0
  31. gac/providers/README.md +437 -0
  32. gac/providers/__init__.py +70 -81
  33. gac/providers/anthropic.py +12 -56
  34. gac/providers/azure_openai.py +48 -92
  35. gac/providers/base.py +329 -0
  36. gac/providers/cerebras.py +10 -43
  37. gac/providers/chutes.py +16 -72
  38. gac/providers/claude_code.py +64 -97
  39. gac/providers/custom_anthropic.py +51 -85
  40. gac/providers/custom_openai.py +29 -87
  41. gac/providers/deepseek.py +10 -43
  42. gac/providers/error_handler.py +139 -0
  43. gac/providers/fireworks.py +10 -43
  44. gac/providers/gemini.py +66 -73
  45. gac/providers/groq.py +10 -62
  46. gac/providers/kimi_coding.py +19 -59
  47. gac/providers/lmstudio.py +62 -52
  48. gac/providers/minimax.py +10 -43
  49. gac/providers/mistral.py +10 -43
  50. gac/providers/moonshot.py +10 -43
  51. gac/providers/ollama.py +54 -41
  52. gac/providers/openai.py +30 -46
  53. gac/providers/openrouter.py +15 -62
  54. gac/providers/protocol.py +71 -0
  55. gac/providers/qwen.py +55 -67
  56. gac/providers/registry.py +58 -0
  57. gac/providers/replicate.py +137 -91
  58. gac/providers/streamlake.py +26 -56
  59. gac/providers/synthetic.py +35 -47
  60. gac/providers/together.py +10 -43
  61. gac/providers/zai.py +21 -59
  62. gac/py.typed +0 -0
  63. gac/security.py +1 -1
  64. gac/templates/__init__.py +1 -0
  65. gac/templates/question_generation.txt +60 -0
  66. gac/templates/system_prompt.txt +224 -0
  67. gac/templates/user_prompt.txt +28 -0
  68. gac/utils.py +6 -5
  69. gac/workflow_context.py +162 -0
  70. {gac-3.8.1.dist-info → gac-3.10.10.dist-info}/METADATA +1 -1
  71. gac-3.10.10.dist-info/RECORD +79 -0
  72. gac/constants.py +0 -328
  73. gac-3.8.1.dist-info/RECORD +0 -56
  74. {gac-3.8.1.dist-info → gac-3.10.10.dist-info}/WHEEL +0 -0
  75. {gac-3.8.1.dist-info → gac-3.10.10.dist-info}/entry_points.txt +0 -0
  76. {gac-3.8.1.dist-info → gac-3.10.10.dist-info}/licenses/LICENSE +0 -0
@@ -1,112 +1,79 @@
1
- """Claude Code provider implementation.
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 logging
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.utils import get_ssl_verify
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
- This provider uses the Claude Code subscription OAuth token instead of the Anthropic API key.
23
- It authenticates using Bearer token authentication with the special anthropic-beta header.
14
+ class ClaudeCodeProvider(AnthropicCompatibleProvider):
15
+ """Claude Code OAuth provider with special system message requirements."""
24
16
 
25
- Environment variables:
26
- CLAUDE_CODE_ACCESS_TOKEN: OAuth access token from Claude Code authentication
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
- Args:
29
- model: Model name (e.g., 'claude-sonnet-4-5')
30
- messages: List of message dictionaries with 'role' and 'content' keys
31
- temperature: Sampling temperature (0.0-1.0)
32
- max_tokens: Maximum tokens in response
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
- "CLAUDE_CODE_ACCESS_TOKEN not found in environment variables. "
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
- url = "https://api.anthropic.com/v1/messages"
48
- headers = {
49
- "Authorization": f"Bearer {access_token}",
50
- "anthropic-version": "2023-06-01",
51
- "anthropic-beta": "oauth-2025-04-20",
52
- "content-type": "application/json",
53
- }
54
-
55
- # Convert messages to Anthropic format
56
- # IMPORTANT: Claude Code OAuth tokens require the system message to be EXACTLY
57
- # "You are Claude Code, Anthropic's official CLI for Claude." with NO additional content.
58
- # Any other instructions must be moved to the user message.
59
- anthropic_messages = []
60
- system_instructions = ""
61
-
62
- for msg in messages:
63
- if msg["role"] == "system":
64
- system_instructions = msg["content"]
65
- else:
66
- anthropic_messages.append({"role": msg["role"], "content": msg["content"]})
67
-
68
- # Claude Code requires this exact system message, nothing more
69
- system_message = "You are Claude Code, Anthropic's official CLI for Claude."
70
-
71
- # Move any system instructions into the first user message
72
- if system_instructions and anthropic_messages:
73
- # Prepend system instructions to the first user message
74
- first_user_msg = anthropic_messages[0]
75
- first_user_msg["content"] = f"{system_instructions}\n\n{first_user_msg['content']}"
76
-
77
- data = {
78
- "model": model,
79
- "messages": anthropic_messages,
80
- "temperature": temperature,
81
- "max_tokens": max_tokens,
82
- "system": system_message,
83
- }
84
-
85
- logger.debug(f"Calling Claude Code API with model={model}")
86
-
87
- try:
88
- response = httpx.post(
89
- url, headers=headers, json=data, timeout=ProviderDefaults.HTTP_TIMEOUT, verify=get_ssl_verify()
90
- )
91
- response.raise_for_status()
92
- response_data = response.json()
93
- content = response_data["content"][0]["text"]
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.utils import get_ssl_verify
13
+ from gac.providers.base import AnthropicCompatibleProvider, ProviderConfig
16
14
 
17
15
  logger = logging.getLogger(__name__)
18
16
 
19
17
 
20
- def call_custom_anthropic_api(model: str, messages: list[dict], temperature: float, max_tokens: int) -> str:
21
- """Call a custom Anthropic-compatible API endpoint.
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
- Args:
35
- model: The model to use (e.g., 'claude-sonnet-4-5', 'claude-haiku-4-5')
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
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
- Returns:
41
- The generated commit message
27
+ def __init__(self, config: ProviderConfig):
28
+ """Initialize the provider with custom configuration from environment variables.
42
29
 
43
- Raises:
44
- AIError: If authentication fails, API errors occur, or response is invalid
45
- """
46
- api_key = os.getenv("CUSTOM_ANTHROPIC_API_KEY")
47
- if not api_key:
48
- raise AIError.authentication_error("CUSTOM_ANTHROPIC_API_KEY environment variable not set")
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
- url = f"{base_url}/v1/messages"
59
- else:
60
- url = base_url
61
-
62
- headers = {"x-api-key": api_key, "anthropic-version": api_version, "content-type": "application/json"}
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
- anthropic_messages = []
65
- system_message = ""
48
+ # Update config with the custom base URL
49
+ config.base_url = base_url
66
50
 
67
- for msg in messages:
68
- if msg["role"] == "system":
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
- data = {"model": model, "messages": anthropic_messages, "temperature": temperature, "max_tokens": max_tokens}
54
+ super().__init__(config)
74
55
 
75
- if system_message:
76
- data["system"] = system_message
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
- try:
79
- response = httpx.post(
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 = response_data.get("content", [])
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(response_data)}"
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(response_data)}")
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
@@ -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.utils import get_ssl_verify
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
- try:
65
- response = httpx.post(
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
- try:
72
- content = response_data["choices"][0]["message"]["content"]
73
- except (KeyError, IndexError, TypeError) as e:
74
- logger.error(f"Unexpected response format from Custom OpenAI API. Response: {json.dumps(response_data)}")
75
- raise AIError.model_error(
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
- if content is None:
81
- raise AIError.model_error("Custom OpenAI API returned null content")
82
- if content == "":
83
- raise AIError.model_error("Custom OpenAI API returned empty content")
84
- return content
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 status_code == 401:
92
- raise AIError.authentication_error(f"Custom OpenAI API authentication failed: {error_text}") from e
93
- elif status_code == 429:
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
- raise AIError.model_error(f"Custom OpenAI API error: {status_code} - {error_text}") from e
97
- except httpx.TimeoutException as e:
98
- raise AIError.timeout_error(f"Custom OpenAI API request timed out: {str(e)}") from e
99
- except AIError:
100
- raise
101
- except Exception as e:
102
- raise AIError.model_error(f"Error calling Custom OpenAI API: {str(e)}") from e
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 logging
4
- import os
3
+ from gac.providers.base import OpenAICompatibleProvider, ProviderConfig
5
4
 
6
- import httpx
7
5
 
8
- from gac.constants import ProviderDefaults
9
- from gac.errors import AIError
10
- from gac.utils import get_ssl_verify
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
- logger = logging.getLogger(__name__)
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"