git-commit-message 0.6.0__py3-none-any.whl → 0.7.0__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.
@@ -1,6 +1,6 @@
1
1
  """Command-line interface entry point.
2
2
 
3
- Collect staged changes from the repository and call an OpenAI GPT model
3
+ Collect staged changes from the repository and call an LLM provider
4
4
  to generate a commit message, or create a commit straight away.
5
5
  """
6
6
 
@@ -19,10 +19,11 @@ from ._git import (
19
19
  get_staged_diff,
20
20
  has_staged_changes,
21
21
  )
22
- from ._gpt import (
22
+ from ._llm import (
23
+ CommitMessageResult,
24
+ UnsupportedProviderError,
23
25
  generate_commit_message,
24
26
  generate_commit_message_with_info,
25
- CommitMessageResult,
26
27
  )
27
28
 
28
29
 
@@ -50,7 +51,7 @@ def _build_parser() -> ArgumentParser:
50
51
  parser: ArgumentParser = ArgumentParser(
51
52
  prog="git-commit-message",
52
53
  description=(
53
- "Generate a commit message with OpenAI GPT based on the staged changes."
54
+ "Generate a commit message based on the staged changes."
54
55
  ),
55
56
  )
56
57
 
@@ -72,11 +73,21 @@ def _build_parser() -> ArgumentParser:
72
73
  help="Open an editor to amend the message before committing. Use with '--commit'.",
73
74
  )
74
75
 
76
+ parser.add_argument(
77
+ "--provider",
78
+ default=None,
79
+ help=(
80
+ "LLM provider to use (default: openai). "
81
+ "You may also set GIT_COMMIT_MESSAGE_PROVIDER. "
82
+ "The CLI flag overrides the environment variable."
83
+ ),
84
+ )
85
+
75
86
  parser.add_argument(
76
87
  "--model",
77
88
  default=None,
78
89
  help=(
79
- "OpenAI model name to use. If unspecified, uses the environment variables (GIT_COMMIT_MESSAGE_MODEL, OPENAI_MODEL) or 'gpt-5-mini'."
90
+ "Model name to use. If unspecified, uses GIT_COMMIT_MESSAGE_MODEL or a provider-specific default (openai: gpt-5-mini; google: gemini-2.5-flash)."
80
91
  ),
81
92
  )
82
93
 
@@ -170,6 +181,7 @@ def _run(
170
181
  getattr(args, "max_length", None),
171
182
  getattr(args, "language", None),
172
183
  chunk_tokens,
184
+ getattr(args, "provider", None),
173
185
  )
174
186
  message = result.message
175
187
  else:
@@ -181,7 +193,11 @@ def _run(
181
193
  getattr(args, "max_length", None),
182
194
  getattr(args, "language", None),
183
195
  chunk_tokens,
196
+ getattr(args, "provider", None),
184
197
  )
198
+ except UnsupportedProviderError as exc:
199
+ print(str(exc), file=stderr)
200
+ return 3
185
201
  except Exception as exc: # noqa: BLE001 - to preserve standard output messaging
186
202
  print(f"Failed to generate commit message: {exc}", file=stderr)
187
203
  return 3
@@ -199,7 +215,8 @@ def _run(
199
215
  if not args.commit:
200
216
  if args.debug and result is not None:
201
217
  # Print debug information
202
- print("==== OpenAI Usage ====")
218
+ print(f"==== {result.provider} Usage ====")
219
+ print(f"provider: {result.provider}")
203
220
  print(f"model: {result.model}")
204
221
  print(f"response_id: {getattr(result, 'response_id', '(n/a)')}")
205
222
  if result.total_tokens is not None:
@@ -220,7 +237,8 @@ def _run(
220
237
 
221
238
  if args.debug and result is not None:
222
239
  # Also print debug info before commit
223
- print("==== OpenAI Usage ====")
240
+ print(f"==== {result.provider} Usage ====")
241
+ print(f"provider: {result.provider}")
224
242
  print(f"model: {result.model}")
225
243
  print(f"response_id: {getattr(result, 'response_id', '(n/a)')}")
226
244
  if result.total_tokens is not None:
@@ -0,0 +1,122 @@
1
+ """Google (Gemini) provider implementation.
2
+
3
+ This module contains only Google GenAI-specific API calls and token counting.
4
+ Provider-agnostic orchestration/prompt logic lives in `_llm.py`.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from os import environ
10
+
11
+ from google import genai
12
+ from google.genai import types
13
+
14
+ from ._llm import LLMTextResult, LLMUsage
15
+
16
+
17
+ class GoogleGenAIProvider:
18
+ name = "google"
19
+
20
+ def __init__(
21
+ self,
22
+ /,
23
+ *,
24
+ api_key: str | None = None,
25
+ ) -> None:
26
+ key = api_key or environ.get("GOOGLE_API_KEY")
27
+ if not key:
28
+ raise RuntimeError("The GOOGLE_API_KEY environment variable is required.")
29
+ self._client = genai.Client(api_key=key)
30
+
31
+ def count_tokens(
32
+ self,
33
+ /,
34
+ *,
35
+ model: str,
36
+ text: str,
37
+ ) -> int:
38
+ try:
39
+ resp = self._client.models.count_tokens(
40
+ model=model,
41
+ contents=text,
42
+ )
43
+ except Exception as exc:
44
+ raise RuntimeError(
45
+ "Token counting failed for the Google provider. "
46
+ "Try `--chunk-tokens 0` (default) or `--chunk-tokens -1` to disable summarisation."
47
+ ) from exc
48
+
49
+ total = getattr(resp, "total_tokens", None)
50
+ if not isinstance(total, int):
51
+ raise RuntimeError(
52
+ "Token counting returned an unexpected response from the Google provider. "
53
+ "Try `--chunk-tokens 0` (default) or `--chunk-tokens -1` to disable summarisation."
54
+ )
55
+
56
+ return total
57
+
58
+ def generate_text(
59
+ self,
60
+ /,
61
+ *,
62
+ model: str,
63
+ instructions: str,
64
+ user_text: str,
65
+ ) -> LLMTextResult:
66
+ config = types.GenerateContentConfig(
67
+ system_instruction=instructions,
68
+ )
69
+
70
+ resp = self._client.models.generate_content(
71
+ model=model,
72
+ contents=user_text,
73
+ config=config,
74
+ )
75
+
76
+ text = self._extract_text(resp)
77
+ if not text:
78
+ raise RuntimeError("An empty response text was generated by the provider.")
79
+
80
+ usage = self._extract_usage(resp)
81
+
82
+ return LLMTextResult(
83
+ text=text,
84
+ response_id=getattr(resp, "response_id", None),
85
+ usage=usage,
86
+ )
87
+
88
+ @staticmethod
89
+ def _extract_text(
90
+ resp: types.GenerateContentResponse,
91
+ /,
92
+ ) -> str:
93
+ candidates = getattr(resp, "candidates", None)
94
+ if not candidates:
95
+ return ""
96
+
97
+ parts = getattr(candidates[0].content, "parts", None) if candidates[0].content else None
98
+ if not parts:
99
+ return ""
100
+
101
+ texts: list[str] = []
102
+ for part in parts:
103
+ t = getattr(part, "text", None)
104
+ if isinstance(t, str) and t.strip():
105
+ texts.append(t)
106
+
107
+ return "\n".join(texts).strip()
108
+
109
+ @staticmethod
110
+ def _extract_usage(
111
+ resp: types.GenerateContentResponse,
112
+ /,
113
+ ) -> LLMUsage | None:
114
+ metadata = getattr(resp, "usage_metadata", None)
115
+ if metadata is None:
116
+ return None
117
+
118
+ return LLMUsage(
119
+ prompt_tokens=getattr(metadata, "prompt_token_count", None),
120
+ completion_tokens=getattr(metadata, "candidates_token_count", None),
121
+ total_tokens=getattr(metadata, "total_token_count", None),
122
+ )