git-commit-message 0.5.1__tar.gz → 0.7.0__tar.gz
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.
- {git_commit_message-0.5.1 → git_commit_message-0.7.0}/PKG-INFO +51 -4
- {git_commit_message-0.5.1 → git_commit_message-0.7.0}/README.md +47 -2
- {git_commit_message-0.5.1 → git_commit_message-0.7.0}/pyproject.toml +4 -2
- {git_commit_message-0.5.1 → git_commit_message-0.7.0}/src/git_commit_message/__init__.py +1 -3
- {git_commit_message-0.5.1 → git_commit_message-0.7.0}/src/git_commit_message/_cli.py +91 -34
- git_commit_message-0.7.0/src/git_commit_message/_gemini.py +122 -0
- {git_commit_message-0.5.1 → git_commit_message-0.7.0}/src/git_commit_message/_git.py +13 -13
- git_commit_message-0.7.0/src/git_commit_message/_gpt.py +90 -0
- git_commit_message-0.7.0/src/git_commit_message/_llm.py +594 -0
- {git_commit_message-0.5.1 → git_commit_message-0.7.0}/src/git_commit_message.egg-info/PKG-INFO +51 -4
- {git_commit_message-0.5.1 → git_commit_message-0.7.0}/src/git_commit_message.egg-info/SOURCES.txt +2 -0
- git_commit_message-0.7.0/src/git_commit_message.egg-info/requires.txt +4 -0
- git_commit_message-0.5.1/src/git_commit_message/_gpt.py +0 -312
- git_commit_message-0.5.1/src/git_commit_message.egg-info/requires.txt +0 -2
- {git_commit_message-0.5.1 → git_commit_message-0.7.0}/UNLICENSE +0 -0
- {git_commit_message-0.5.1 → git_commit_message-0.7.0}/setup.cfg +0 -0
- {git_commit_message-0.5.1 → git_commit_message-0.7.0}/src/git_commit_message/__main__.py +0 -0
- {git_commit_message-0.5.1 → git_commit_message-0.7.0}/src/git_commit_message.egg-info/dependency_links.txt +0 -0
- {git_commit_message-0.5.1 → git_commit_message-0.7.0}/src/git_commit_message.egg-info/entry_points.txt +0 -0
- {git_commit_message-0.5.1 → git_commit_message-0.7.0}/src/git_commit_message.egg-info/top_level.txt +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: git-commit-message
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: Generate Git commit messages from staged changes using
|
|
3
|
+
Version: 0.7.0
|
|
4
|
+
Summary: Generate Git commit messages from staged changes using LLM
|
|
5
5
|
Maintainer-email: Mina Her <minacle@live.com>
|
|
6
6
|
License: This is free and unencumbered software released into the public domain.
|
|
7
7
|
|
|
@@ -44,7 +44,9 @@ Classifier: Topic :: Software Development :: Version Control :: Git
|
|
|
44
44
|
Requires-Python: >=3.13
|
|
45
45
|
Description-Content-Type: text/markdown
|
|
46
46
|
Requires-Dist: babel>=2.17.0
|
|
47
|
+
Requires-Dist: google-genai>=1.56.0
|
|
47
48
|
Requires-Dist: openai>=2.6.1
|
|
49
|
+
Requires-Dist: tiktoken>=0.12.0
|
|
48
50
|
|
|
49
51
|
# git-commit-message
|
|
50
52
|
|
|
@@ -82,6 +84,12 @@ Set your API key (POSIX sh):
|
|
|
82
84
|
export OPENAI_API_KEY="sk-..."
|
|
83
85
|
```
|
|
84
86
|
|
|
87
|
+
Or for the Google provider:
|
|
88
|
+
|
|
89
|
+
```sh
|
|
90
|
+
export GOOGLE_API_KEY="..."
|
|
91
|
+
```
|
|
92
|
+
|
|
85
93
|
Note (fish): In fish, set it as follows.
|
|
86
94
|
|
|
87
95
|
```fish
|
|
@@ -109,18 +117,49 @@ git-commit-message "optional extra context about the change"
|
|
|
109
117
|
git-commit-message --one-line "optional context"
|
|
110
118
|
```
|
|
111
119
|
|
|
120
|
+
- Select provider (default: openai):
|
|
121
|
+
|
|
122
|
+
```sh
|
|
123
|
+
git-commit-message --provider openai "optional context"
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
- Select provider (Google Gemini via google-genai):
|
|
127
|
+
|
|
128
|
+
```sh
|
|
129
|
+
git-commit-message --provider google "optional context"
|
|
130
|
+
```
|
|
131
|
+
|
|
112
132
|
- Limit subject length (default 72):
|
|
113
133
|
|
|
114
134
|
```sh
|
|
115
135
|
git-commit-message --one-line --max-length 50 "optional context"
|
|
116
136
|
```
|
|
117
137
|
|
|
138
|
+
- Chunk long diffs by token budget (0 = single chunk + summary, -1 = disable chunking):
|
|
139
|
+
|
|
140
|
+
```sh
|
|
141
|
+
# force a single summary pass over the whole diff (default)
|
|
142
|
+
git-commit-message --chunk-tokens 0 "optional context"
|
|
143
|
+
|
|
144
|
+
# chunk the diff into ~4000-token pieces before summarising
|
|
145
|
+
git-commit-message --chunk-tokens 4000 "optional context"
|
|
146
|
+
|
|
147
|
+
# disable summarisation and use the legacy one-shot prompt
|
|
148
|
+
git-commit-message --chunk-tokens -1 "optional context"
|
|
149
|
+
```
|
|
150
|
+
|
|
118
151
|
- Commit immediately with editor:
|
|
119
152
|
|
|
120
153
|
```sh
|
|
121
154
|
git-commit-message --commit --edit "refactor parser for speed"
|
|
122
155
|
```
|
|
123
156
|
|
|
157
|
+
- Print debug info (prompt/response + token usage):
|
|
158
|
+
|
|
159
|
+
```sh
|
|
160
|
+
git-commit-message --debug "optional context"
|
|
161
|
+
```
|
|
162
|
+
|
|
124
163
|
- Select output language/locale (default: en-GB):
|
|
125
164
|
|
|
126
165
|
```sh
|
|
@@ -141,9 +180,17 @@ Notes:
|
|
|
141
180
|
|
|
142
181
|
Environment:
|
|
143
182
|
|
|
144
|
-
- `OPENAI_API_KEY`: required
|
|
145
|
-
- `
|
|
183
|
+
- `OPENAI_API_KEY`: required when provider is `openai`
|
|
184
|
+
- `GOOGLE_API_KEY`: required when provider is `google`
|
|
185
|
+
- `GIT_COMMIT_MESSAGE_PROVIDER`: optional (default: `openai`). `--provider` overrides this value.
|
|
186
|
+
- `GIT_COMMIT_MESSAGE_MODEL`: optional model override (defaults: `openai` -> `gpt-5-mini`, `google` -> `gemini-2.5-flash`)
|
|
187
|
+
- `OPENAI_MODEL`: optional OpenAI-only model override
|
|
146
188
|
- `GIT_COMMIT_MESSAGE_LANGUAGE`: optional (default: `en-GB`)
|
|
189
|
+
- `GIT_COMMIT_MESSAGE_CHUNK_TOKENS`: optional token budget per diff chunk (default: 0 = single chunk + summary; -1 disables summarisation)
|
|
190
|
+
|
|
191
|
+
Notes:
|
|
192
|
+
|
|
193
|
+
- If token counting fails for your provider while chunking, try `--chunk-tokens 0` (default) or `--chunk-tokens -1`.
|
|
147
194
|
|
|
148
195
|
## AI‑generated code notice
|
|
149
196
|
|
|
@@ -34,6 +34,12 @@ Set your API key (POSIX sh):
|
|
|
34
34
|
export OPENAI_API_KEY="sk-..."
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
+
Or for the Google provider:
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
export GOOGLE_API_KEY="..."
|
|
41
|
+
```
|
|
42
|
+
|
|
37
43
|
Note (fish): In fish, set it as follows.
|
|
38
44
|
|
|
39
45
|
```fish
|
|
@@ -61,18 +67,49 @@ git-commit-message "optional extra context about the change"
|
|
|
61
67
|
git-commit-message --one-line "optional context"
|
|
62
68
|
```
|
|
63
69
|
|
|
70
|
+
- Select provider (default: openai):
|
|
71
|
+
|
|
72
|
+
```sh
|
|
73
|
+
git-commit-message --provider openai "optional context"
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
- Select provider (Google Gemini via google-genai):
|
|
77
|
+
|
|
78
|
+
```sh
|
|
79
|
+
git-commit-message --provider google "optional context"
|
|
80
|
+
```
|
|
81
|
+
|
|
64
82
|
- Limit subject length (default 72):
|
|
65
83
|
|
|
66
84
|
```sh
|
|
67
85
|
git-commit-message --one-line --max-length 50 "optional context"
|
|
68
86
|
```
|
|
69
87
|
|
|
88
|
+
- Chunk long diffs by token budget (0 = single chunk + summary, -1 = disable chunking):
|
|
89
|
+
|
|
90
|
+
```sh
|
|
91
|
+
# force a single summary pass over the whole diff (default)
|
|
92
|
+
git-commit-message --chunk-tokens 0 "optional context"
|
|
93
|
+
|
|
94
|
+
# chunk the diff into ~4000-token pieces before summarising
|
|
95
|
+
git-commit-message --chunk-tokens 4000 "optional context"
|
|
96
|
+
|
|
97
|
+
# disable summarisation and use the legacy one-shot prompt
|
|
98
|
+
git-commit-message --chunk-tokens -1 "optional context"
|
|
99
|
+
```
|
|
100
|
+
|
|
70
101
|
- Commit immediately with editor:
|
|
71
102
|
|
|
72
103
|
```sh
|
|
73
104
|
git-commit-message --commit --edit "refactor parser for speed"
|
|
74
105
|
```
|
|
75
106
|
|
|
107
|
+
- Print debug info (prompt/response + token usage):
|
|
108
|
+
|
|
109
|
+
```sh
|
|
110
|
+
git-commit-message --debug "optional context"
|
|
111
|
+
```
|
|
112
|
+
|
|
76
113
|
- Select output language/locale (default: en-GB):
|
|
77
114
|
|
|
78
115
|
```sh
|
|
@@ -93,9 +130,17 @@ Notes:
|
|
|
93
130
|
|
|
94
131
|
Environment:
|
|
95
132
|
|
|
96
|
-
- `OPENAI_API_KEY`: required
|
|
97
|
-
- `
|
|
133
|
+
- `OPENAI_API_KEY`: required when provider is `openai`
|
|
134
|
+
- `GOOGLE_API_KEY`: required when provider is `google`
|
|
135
|
+
- `GIT_COMMIT_MESSAGE_PROVIDER`: optional (default: `openai`). `--provider` overrides this value.
|
|
136
|
+
- `GIT_COMMIT_MESSAGE_MODEL`: optional model override (defaults: `openai` -> `gpt-5-mini`, `google` -> `gemini-2.5-flash`)
|
|
137
|
+
- `OPENAI_MODEL`: optional OpenAI-only model override
|
|
98
138
|
- `GIT_COMMIT_MESSAGE_LANGUAGE`: optional (default: `en-GB`)
|
|
139
|
+
- `GIT_COMMIT_MESSAGE_CHUNK_TOKENS`: optional token budget per diff chunk (default: 0 = single chunk + summary; -1 disables summarisation)
|
|
140
|
+
|
|
141
|
+
Notes:
|
|
142
|
+
|
|
143
|
+
- If token counting fails for your provider while chunking, try `--chunk-tokens 0` (default) or `--chunk-tokens -1`.
|
|
99
144
|
|
|
100
145
|
## AI‑generated code notice
|
|
101
146
|
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "git-commit-message"
|
|
3
|
-
version = "0.
|
|
4
|
-
description = "Generate Git commit messages from staged changes using
|
|
3
|
+
version = "0.7.0"
|
|
4
|
+
description = "Generate Git commit messages from staged changes using LLM"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.13"
|
|
7
7
|
dependencies = [
|
|
8
8
|
"babel>=2.17.0",
|
|
9
|
+
"google-genai>=1.56.0",
|
|
9
10
|
"openai>=2.6.1",
|
|
11
|
+
"tiktoken>=0.12.0",
|
|
10
12
|
]
|
|
11
13
|
maintainers = [{ name = "Mina Her", email = "minacle@live.com" }]
|
|
12
14
|
license = { file = "UNLICENSE" }
|
|
@@ -1,24 +1,44 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
1
|
"""Command-line interface entry point.
|
|
4
2
|
|
|
5
|
-
Collect staged changes from the repository and call an
|
|
3
|
+
Collect staged changes from the repository and call an LLM provider
|
|
6
4
|
to generate a commit message, or create a commit straight away.
|
|
7
5
|
"""
|
|
8
6
|
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
9
|
from argparse import ArgumentParser, Namespace
|
|
10
|
+
from os import environ
|
|
10
11
|
from pathlib import Path
|
|
11
|
-
import
|
|
12
|
+
from sys import exit as sys_exit
|
|
13
|
+
from sys import stderr
|
|
12
14
|
from typing import Final
|
|
13
15
|
|
|
14
|
-
from ._git import
|
|
15
|
-
|
|
16
|
+
from ._git import (
|
|
17
|
+
commit_with_message,
|
|
18
|
+
get_repo_root,
|
|
19
|
+
get_staged_diff,
|
|
20
|
+
has_staged_changes,
|
|
21
|
+
)
|
|
22
|
+
from ._llm import (
|
|
23
|
+
CommitMessageResult,
|
|
24
|
+
UnsupportedProviderError,
|
|
16
25
|
generate_commit_message,
|
|
17
26
|
generate_commit_message_with_info,
|
|
18
|
-
CommitMessageResult,
|
|
19
27
|
)
|
|
20
28
|
|
|
21
29
|
|
|
30
|
+
def _env_chunk_tokens_default() -> int | None:
|
|
31
|
+
"""Return chunk token default from env if valid, else None."""
|
|
32
|
+
|
|
33
|
+
raw: str | None = environ.get("GIT_COMMIT_MESSAGE_CHUNK_TOKENS")
|
|
34
|
+
if raw is None:
|
|
35
|
+
return None
|
|
36
|
+
try:
|
|
37
|
+
return int(raw)
|
|
38
|
+
except ValueError:
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
|
|
22
42
|
def _build_parser() -> ArgumentParser:
|
|
23
43
|
"""Create the CLI argument parser.
|
|
24
44
|
|
|
@@ -31,7 +51,7 @@ def _build_parser() -> ArgumentParser:
|
|
|
31
51
|
parser: ArgumentParser = ArgumentParser(
|
|
32
52
|
prog="git-commit-message",
|
|
33
53
|
description=(
|
|
34
|
-
"Generate a commit message
|
|
54
|
+
"Generate a commit message based on the staged changes."
|
|
35
55
|
),
|
|
36
56
|
)
|
|
37
57
|
|
|
@@ -53,11 +73,21 @@ def _build_parser() -> ArgumentParser:
|
|
|
53
73
|
help="Open an editor to amend the message before committing. Use with '--commit'.",
|
|
54
74
|
)
|
|
55
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
|
+
|
|
56
86
|
parser.add_argument(
|
|
57
87
|
"--model",
|
|
58
88
|
default=None,
|
|
59
89
|
help=(
|
|
60
|
-
"
|
|
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)."
|
|
61
91
|
),
|
|
62
92
|
)
|
|
63
93
|
|
|
@@ -92,12 +122,24 @@ def _build_parser() -> ArgumentParser:
|
|
|
92
122
|
help="Maximum subject (first line) length (default: 72).",
|
|
93
123
|
)
|
|
94
124
|
|
|
125
|
+
parser.add_argument(
|
|
126
|
+
"--chunk-tokens",
|
|
127
|
+
dest="chunk_tokens",
|
|
128
|
+
type=int,
|
|
129
|
+
default=None,
|
|
130
|
+
help=(
|
|
131
|
+
"Target token budget per diff chunk. "
|
|
132
|
+
"0 forces a single chunk with summarisation; -1 disables summarisation (legacy one-shot). "
|
|
133
|
+
"If omitted, uses GIT_COMMIT_MESSAGE_CHUNK_TOKENS when set (default: 0)."
|
|
134
|
+
),
|
|
135
|
+
)
|
|
136
|
+
|
|
95
137
|
return parser
|
|
96
138
|
|
|
97
139
|
|
|
98
140
|
def _run(
|
|
99
|
-
*,
|
|
100
141
|
args: Namespace,
|
|
142
|
+
/,
|
|
101
143
|
) -> int:
|
|
102
144
|
"""Main execution logic.
|
|
103
145
|
|
|
@@ -114,37 +156,50 @@ def _run(
|
|
|
114
156
|
|
|
115
157
|
repo_root: Path = get_repo_root()
|
|
116
158
|
|
|
117
|
-
if not has_staged_changes(
|
|
118
|
-
print("No staged changes. Run 'git add' and try again.", file=
|
|
159
|
+
if not has_staged_changes(repo_root):
|
|
160
|
+
print("No staged changes. Run 'git add' and try again.", file=stderr)
|
|
119
161
|
return 2
|
|
120
162
|
|
|
121
|
-
diff_text: str = get_staged_diff(
|
|
163
|
+
diff_text: str = get_staged_diff(repo_root)
|
|
122
164
|
|
|
123
165
|
hint: str | None = args.description if isinstance(args.description, str) else None
|
|
124
166
|
|
|
167
|
+
chunk_tokens: int | None = args.chunk_tokens
|
|
168
|
+
if chunk_tokens is None:
|
|
169
|
+
chunk_tokens = _env_chunk_tokens_default()
|
|
170
|
+
if chunk_tokens is None:
|
|
171
|
+
chunk_tokens = 0
|
|
172
|
+
|
|
125
173
|
result: CommitMessageResult | None = None
|
|
126
174
|
try:
|
|
127
175
|
if args.debug:
|
|
128
176
|
result = generate_commit_message_with_info(
|
|
129
|
-
|
|
130
|
-
hint
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
177
|
+
diff_text,
|
|
178
|
+
hint,
|
|
179
|
+
args.model,
|
|
180
|
+
getattr(args, "one_line", False),
|
|
181
|
+
getattr(args, "max_length", None),
|
|
182
|
+
getattr(args, "language", None),
|
|
183
|
+
chunk_tokens,
|
|
184
|
+
getattr(args, "provider", None),
|
|
135
185
|
)
|
|
136
186
|
message = result.message
|
|
137
187
|
else:
|
|
138
188
|
message = generate_commit_message(
|
|
139
|
-
|
|
140
|
-
hint
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
189
|
+
diff_text,
|
|
190
|
+
hint,
|
|
191
|
+
args.model,
|
|
192
|
+
getattr(args, "one_line", False),
|
|
193
|
+
getattr(args, "max_length", None),
|
|
194
|
+
getattr(args, "language", None),
|
|
195
|
+
chunk_tokens,
|
|
196
|
+
getattr(args, "provider", None),
|
|
145
197
|
)
|
|
198
|
+
except UnsupportedProviderError as exc:
|
|
199
|
+
print(str(exc), file=stderr)
|
|
200
|
+
return 3
|
|
146
201
|
except Exception as exc: # noqa: BLE001 - to preserve standard output messaging
|
|
147
|
-
print(f"Failed to generate commit message: {exc}", file=
|
|
202
|
+
print(f"Failed to generate commit message: {exc}", file=stderr)
|
|
148
203
|
return 3
|
|
149
204
|
|
|
150
205
|
# Option: force single-line message
|
|
@@ -160,7 +215,8 @@ def _run(
|
|
|
160
215
|
if not args.commit:
|
|
161
216
|
if args.debug and result is not None:
|
|
162
217
|
# Print debug information
|
|
163
|
-
print("====
|
|
218
|
+
print(f"==== {result.provider} Usage ====")
|
|
219
|
+
print(f"provider: {result.provider}")
|
|
164
220
|
print(f"model: {result.model}")
|
|
165
221
|
print(f"response_id: {getattr(result, 'response_id', '(n/a)')}")
|
|
166
222
|
if result.total_tokens is not None:
|
|
@@ -181,7 +237,8 @@ def _run(
|
|
|
181
237
|
|
|
182
238
|
if args.debug and result is not None:
|
|
183
239
|
# Also print debug info before commit
|
|
184
|
-
print("====
|
|
240
|
+
print(f"==== {result.provider} Usage ====")
|
|
241
|
+
print(f"provider: {result.provider}")
|
|
185
242
|
print(f"model: {result.model}")
|
|
186
243
|
print(f"response_id: {getattr(result, 'response_id', '(n/a)')}")
|
|
187
244
|
if result.total_tokens is not None:
|
|
@@ -198,9 +255,9 @@ def _run(
|
|
|
198
255
|
print(message)
|
|
199
256
|
|
|
200
257
|
if args.edit:
|
|
201
|
-
rc: int = commit_with_message(message
|
|
258
|
+
rc: int = commit_with_message(message, True, repo_root)
|
|
202
259
|
else:
|
|
203
|
-
rc = commit_with_message(message
|
|
260
|
+
rc = commit_with_message(message, False, repo_root)
|
|
204
261
|
|
|
205
262
|
return rc
|
|
206
263
|
|
|
@@ -215,8 +272,8 @@ def main() -> None:
|
|
|
215
272
|
args: Namespace = parser.parse_args()
|
|
216
273
|
|
|
217
274
|
if args.edit and not args.commit:
|
|
218
|
-
print("'--edit' must be used together with '--commit'.", file=
|
|
219
|
-
|
|
275
|
+
print("'--edit' must be used together with '--commit'.", file=stderr)
|
|
276
|
+
sys_exit(2)
|
|
220
277
|
|
|
221
|
-
code: int = _run(args
|
|
222
|
-
|
|
278
|
+
code: int = _run(args)
|
|
279
|
+
sys_exit(code)
|
|
@@ -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
|
+
)
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
1
|
"""Git-related helper functions.
|
|
4
2
|
|
|
5
3
|
Provides repository root discovery, extraction of staged changes, and
|
|
6
4
|
creating commits from a message.
|
|
7
5
|
"""
|
|
8
6
|
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
9
|
from pathlib import Path
|
|
10
|
-
import
|
|
10
|
+
from subprocess import CalledProcessError, check_call, check_output, run
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
def get_repo_root(
|
|
14
|
-
*,
|
|
15
14
|
cwd: Path | None = None,
|
|
15
|
+
/,
|
|
16
16
|
) -> Path:
|
|
17
17
|
"""Find the repository root from the current working directory.
|
|
18
18
|
|
|
@@ -29,7 +29,7 @@ def get_repo_root(
|
|
|
29
29
|
|
|
30
30
|
start: Path = cwd or Path.cwd()
|
|
31
31
|
try:
|
|
32
|
-
out: bytes =
|
|
32
|
+
out: bytes = check_output(
|
|
33
33
|
[
|
|
34
34
|
"git",
|
|
35
35
|
"rev-parse",
|
|
@@ -37,7 +37,7 @@ def get_repo_root(
|
|
|
37
37
|
],
|
|
38
38
|
cwd=str(start),
|
|
39
39
|
)
|
|
40
|
-
except
|
|
40
|
+
except CalledProcessError as exc: # noqa: TRY003
|
|
41
41
|
raise RuntimeError("Not a Git repository.") from exc
|
|
42
42
|
|
|
43
43
|
root = Path(out.decode().strip())
|
|
@@ -45,28 +45,28 @@ def get_repo_root(
|
|
|
45
45
|
|
|
46
46
|
|
|
47
47
|
def has_staged_changes(
|
|
48
|
-
*,
|
|
49
48
|
cwd: Path,
|
|
49
|
+
/,
|
|
50
50
|
) -> bool:
|
|
51
51
|
"""Check whether there are staged changes."""
|
|
52
52
|
|
|
53
53
|
try:
|
|
54
|
-
|
|
54
|
+
check_call(
|
|
55
55
|
["git", "diff", "--cached", "--quiet", "--exit-code"],
|
|
56
56
|
cwd=str(cwd),
|
|
57
57
|
)
|
|
58
58
|
return False
|
|
59
|
-
except
|
|
59
|
+
except CalledProcessError:
|
|
60
60
|
return True
|
|
61
61
|
|
|
62
62
|
|
|
63
63
|
def get_staged_diff(
|
|
64
|
-
*,
|
|
65
64
|
cwd: Path,
|
|
65
|
+
/,
|
|
66
66
|
) -> str:
|
|
67
67
|
"""Return the staged changes as diff text."""
|
|
68
68
|
|
|
69
|
-
out: bytes =
|
|
69
|
+
out: bytes = check_output(
|
|
70
70
|
[
|
|
71
71
|
"git",
|
|
72
72
|
"diff",
|
|
@@ -81,10 +81,10 @@ def get_staged_diff(
|
|
|
81
81
|
|
|
82
82
|
|
|
83
83
|
def commit_with_message(
|
|
84
|
-
*,
|
|
85
84
|
message: str,
|
|
86
85
|
edit: bool,
|
|
87
86
|
cwd: Path,
|
|
87
|
+
/,
|
|
88
88
|
) -> int:
|
|
89
89
|
"""Create a commit with the given message.
|
|
90
90
|
|
|
@@ -108,7 +108,7 @@ def commit_with_message(
|
|
|
108
108
|
cmd.append("--edit")
|
|
109
109
|
|
|
110
110
|
try:
|
|
111
|
-
completed =
|
|
111
|
+
completed = run(cmd, cwd=str(cwd), check=False)
|
|
112
112
|
return int(completed.returncode)
|
|
113
113
|
except OSError as exc: # e.g., editor launch failure, etc.
|
|
114
114
|
raise RuntimeError(f"Failed to run 'git commit': {exc}") from exc
|