commitgpt 0.3.8 → 0.3.10
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.
- checksums.yaml +4 -4
- data/README.md +15 -8
- data/lib/commitgpt/cli.rb +1 -0
- data/lib/commitgpt/config_manager.rb +20 -0
- data/lib/commitgpt/provider_presets.rb +10 -1
- data/lib/commitgpt/setup_wizard.rb +99 -32
- data/lib/commitgpt/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bc50a5ed7cfb5cc59703014b3b6f58370028dfe2c1bc5c7186e3c6d43a755dd7
|
|
4
|
+
data.tar.gz: e9dd5c01f7c07062fc803710c79a31ce40ab9a9625918c2753bf59d25b4f9d4b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2005518993b77ff65ab764075016d6c1a8bb1259efff645017ab3f0a23a83f03e1b18fc7df257f497a8ecf5a705d4a0c91c29a5e044d00e9896437faed7cf675
|
|
7
|
+
data.tar.gz: d290dac27335967df2b7dfdc8309509f7f228ede0d50c7e0b86e99c28cb16b1551d2bb5004ce3d846a183f1fb85beda193d5fec2873ad9e397211fa6e93ceb26
|
data/README.md
CHANGED
|
@@ -121,10 +121,13 @@ $ aicm setup
|
|
|
121
121
|
```
|
|
122
122
|
|
|
123
123
|
You'll be guided to:
|
|
124
|
-
1.
|
|
125
|
-
2.
|
|
126
|
-
3.
|
|
127
|
-
4.
|
|
124
|
+
1. Set the model list fetch timeout in seconds (defaults to 30; raise it if a provider is slow to respond)
|
|
125
|
+
2. Choose an AI provider (Presets: Cerebras, OpenAI, Ollama, Groq, etc.)
|
|
126
|
+
3. Enter your API Key (stored securely in `config.local.yml`)
|
|
127
|
+
4. Select a model interactively
|
|
128
|
+
5. Set maximum diff length
|
|
129
|
+
|
|
130
|
+
The timeout is saved as a top-level `model_fetch_timeout` key in `config.yml` and applies to every provider, including `aicm -p` and `aicm -m`. You can also edit it there directly.
|
|
128
131
|
|
|
129
132
|
**Note:** Please add `~/.config/commitgpt/config.local.yml` to your `.gitignore` if you are syncing your home directory, as it contains your API keys.
|
|
130
133
|
|
|
@@ -258,13 +261,17 @@ Get an API key at [https://developer.amd.com.cn/radeon/tokenfactory](https://dev
|
|
|
258
261
|
|
|
259
262
|
> **Note**: Registration requires **both an email address and a Chinese (+86) mobile phone number**. Without a Chinese phone number you cannot complete sign-up, so this provider is only usable if you have one.
|
|
260
263
|
|
|
264
|
+
> **Note**: The gateway in front of this endpoint always answers `GET /models` with 403, so models cannot be discovered automatically. CommitGPT ships the list below and offers it directly during setup — no detection request is made for this provider.
|
|
265
|
+
|
|
261
266
|
```
|
|
262
|
-
DeepSeek-V4-Flash
|
|
263
|
-
Qwen3.6-35B-A3B
|
|
264
|
-
|
|
265
|
-
|
|
267
|
+
DeepSeek-V4-Flash # Max Context Length: 1,000,000 tokens
|
|
268
|
+
Qwen3.6-35B-A3B # Max Context Length: 262,144 tokens
|
|
269
|
+
MiniCPM-V46 # Max Context Length: 262,144 tokens
|
|
270
|
+
MiniCPM5-1B # Max Context Length: 131,072 tokens
|
|
266
271
|
```
|
|
267
272
|
|
|
273
|
+
Setup reports the selected model's context window and suggests a matching `diff_len`, so you don't have to work the number out yourself.
|
|
274
|
+
|
|
268
275
|
## How It Works
|
|
269
276
|
This CLI tool runs a `git diff` command to grab all staged changes, sends this to OpenAI's GPT API (or compatible endpoint), and returns an AI-generated commit message. The tool uses the `/v1/chat/completions` endpoint with optimized prompts/system instructions for generating conventional commit messages.
|
|
270
277
|
|
data/lib/commitgpt/cli.rb
CHANGED
|
@@ -63,6 +63,7 @@ module CommitGpt
|
|
|
63
63
|
shell.say " Format: #{format.capitalize.yellow}"
|
|
64
64
|
shell.say " Base URL: #{config['base_url']}"
|
|
65
65
|
shell.say " Diff Len: #{config['diff_len']}"
|
|
66
|
+
shell.say " Timeout: #{CommitGpt::ConfigManager.get_model_fetch_timeout}s"
|
|
66
67
|
shell.say ''
|
|
67
68
|
end
|
|
68
69
|
rescue StandardError
|
|
@@ -8,6 +8,9 @@ require_relative 'string'
|
|
|
8
8
|
module CommitGpt
|
|
9
9
|
# Manages configuration files for CommitGPT
|
|
10
10
|
class ConfigManager
|
|
11
|
+
# Seconds to wait when fetching a provider's model list
|
|
12
|
+
DEFAULT_MODEL_FETCH_TIMEOUT = 30
|
|
13
|
+
|
|
11
14
|
class << self
|
|
12
15
|
# Get the config directory path
|
|
13
16
|
def config_dir
|
|
@@ -165,6 +168,23 @@ module CommitGpt
|
|
|
165
168
|
save_main_config(main_config)
|
|
166
169
|
end
|
|
167
170
|
|
|
171
|
+
# Get the model list fetch timeout (seconds)
|
|
172
|
+
def get_model_fetch_timeout
|
|
173
|
+
return DEFAULT_MODEL_FETCH_TIMEOUT unless config_exists?
|
|
174
|
+
|
|
175
|
+
main_config = YAML.load_file(main_config_path)
|
|
176
|
+
timeout = main_config['model_fetch_timeout'].to_i
|
|
177
|
+
timeout.positive? ? timeout : DEFAULT_MODEL_FETCH_TIMEOUT
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Set the model list fetch timeout (seconds)
|
|
181
|
+
def set_model_fetch_timeout(seconds)
|
|
182
|
+
ensure_config_dir
|
|
183
|
+
main_config = config_exists? ? YAML.load_file(main_config_path) : { 'providers' => [], 'active_provider' => '' }
|
|
184
|
+
main_config['model_fetch_timeout'] = seconds
|
|
185
|
+
save_main_config(main_config)
|
|
186
|
+
end
|
|
187
|
+
|
|
168
188
|
private
|
|
169
189
|
|
|
170
190
|
# Merge main config with local config (local overrides main)
|
|
@@ -3,7 +3,16 @@
|
|
|
3
3
|
module CommitGpt
|
|
4
4
|
# Provider presets for common AI providers
|
|
5
5
|
PROVIDER_PRESETS = [
|
|
6
|
-
|
|
6
|
+
# The Azure gateway in front of this endpoint always answers /models with 403,
|
|
7
|
+
# so the model list is built in and no request is made during setup.
|
|
8
|
+
# Each value is that model's context window in tokens.
|
|
9
|
+
{ label: 'AMD Radeon (China)', value: 'amd', base_url: 'https://developer.amd.com.cn/radeon/api/v1',
|
|
10
|
+
models: {
|
|
11
|
+
'DeepSeek-V4-Flash' => 1_000_000,
|
|
12
|
+
'MiniCPM-V46' => 262_144,
|
|
13
|
+
'MiniCPM5-1B' => 131_072,
|
|
14
|
+
'Qwen3.6-35B-A3B' => 262_144
|
|
15
|
+
} },
|
|
7
16
|
{ label: 'Anthropic Claude', value: 'anthropic', base_url: 'https://api.anthropic.com/v1' },
|
|
8
17
|
{ label: 'Cerebras', value: 'cerebras', base_url: 'https://api.cerebras.ai/v1' },
|
|
9
18
|
{ label: 'DeepSeek', value: 'deepseek', base_url: 'https://api.deepseek.com' },
|
|
@@ -10,8 +10,17 @@ require_relative 'string'
|
|
|
10
10
|
module CommitGpt
|
|
11
11
|
# Interactive setup wizard for configuring AI providers
|
|
12
12
|
class SetupWizard
|
|
13
|
-
#
|
|
14
|
-
|
|
13
|
+
# Diff bytes per context token. Code and CJK pack more bytes into a token
|
|
14
|
+
# than English prose, so this stays deliberately conservative.
|
|
15
|
+
DIFF_BYTES_PER_TOKEN = 2
|
|
16
|
+
|
|
17
|
+
# Ceiling for the suggested diff length. Past this a single request gets
|
|
18
|
+
# slow and one commit message stops describing the change usefully, so
|
|
19
|
+
# chunked mode is the better answer than a bigger window.
|
|
20
|
+
MAX_SUGGESTED_DIFF_LEN = 131_072
|
|
21
|
+
|
|
22
|
+
# Used when the model's context window is unknown
|
|
23
|
+
FALLBACK_DIFF_LEN = 32_768
|
|
15
24
|
|
|
16
25
|
def initialize
|
|
17
26
|
@prompt = TTY::Prompt.new
|
|
@@ -22,6 +31,7 @@ module CommitGpt
|
|
|
22
31
|
ConfigManager.ensure_config_dir
|
|
23
32
|
ConfigManager.generate_default_configs unless ConfigManager.config_exists?
|
|
24
33
|
|
|
34
|
+
prompt_model_fetch_timeout
|
|
25
35
|
provider_choice = select_provider
|
|
26
36
|
configure_provider(provider_choice)
|
|
27
37
|
end
|
|
@@ -47,13 +57,13 @@ module CommitGpt
|
|
|
47
57
|
provider = config['providers'].find { |p| p['name'] == selected }
|
|
48
58
|
|
|
49
59
|
# Fetch models and let user select
|
|
50
|
-
models = fetch_models_with_timeout(provider['base_url'], provider['api_key'])
|
|
60
|
+
models = fetch_models_with_timeout(provider['base_url'], provider['api_key'], preset_models(selected))
|
|
51
61
|
return if models.nil?
|
|
52
62
|
|
|
53
63
|
model = select_model(models, provider['model'])
|
|
54
64
|
|
|
55
65
|
# Prompt for diff length
|
|
56
|
-
diff_len = prompt_diff_len(provider['diff_len']
|
|
66
|
+
diff_len = prompt_diff_len(default_diff_len(selected, model, provider['diff_len']))
|
|
57
67
|
|
|
58
68
|
# Update config
|
|
59
69
|
ConfigManager.update_provider(selected, { 'model' => model, 'diff_len' => diff_len })
|
|
@@ -77,7 +87,8 @@ module CommitGpt
|
|
|
77
87
|
end
|
|
78
88
|
|
|
79
89
|
# Fetch models and let user select
|
|
80
|
-
models = fetch_models_with_timeout(provider_config['base_url'], provider_config['api_key']
|
|
90
|
+
models = fetch_models_with_timeout(provider_config['base_url'], provider_config['api_key'],
|
|
91
|
+
preset_models(provider_config['name']))
|
|
81
92
|
return if models.nil?
|
|
82
93
|
|
|
83
94
|
model = select_model(models, provider_config['model'])
|
|
@@ -150,14 +161,14 @@ module CommitGpt
|
|
|
150
161
|
return if api_key.nil? # User cancelled
|
|
151
162
|
|
|
152
163
|
# Fetch models with timeout
|
|
153
|
-
models = fetch_models_with_timeout(base_url, api_key)
|
|
164
|
+
models = fetch_models_with_timeout(base_url, api_key, preset_models(provider_name))
|
|
154
165
|
return if models.nil?
|
|
155
166
|
|
|
156
167
|
# Let user select model
|
|
157
168
|
model = select_model(models, existing_provider&.dig('model'))
|
|
158
169
|
|
|
159
170
|
# Prompt for diff length
|
|
160
|
-
diff_len = prompt_diff_len(existing_provider&.dig('diff_len')
|
|
171
|
+
diff_len = prompt_diff_len(default_diff_len(provider_name, model, existing_provider&.dig('diff_len')))
|
|
161
172
|
|
|
162
173
|
# Save configuration
|
|
163
174
|
ConfigManager.update_provider(
|
|
@@ -183,14 +194,14 @@ module CommitGpt
|
|
|
183
194
|
q.default 'http://localhost:8080/v1'
|
|
184
195
|
end
|
|
185
196
|
|
|
186
|
-
api_key = @prompt.mask('Enter your API key (optional):') { |q| q.echo false }
|
|
197
|
+
api_key = @prompt.mask('Enter your API key (optional):') { |q| q.echo false }.to_s
|
|
187
198
|
|
|
188
199
|
# Fetch models
|
|
189
200
|
models = fetch_models_with_timeout(base_url, api_key)
|
|
190
201
|
return if models.nil?
|
|
191
202
|
|
|
192
203
|
model = select_model(models)
|
|
193
|
-
diff_len = prompt_diff_len(
|
|
204
|
+
diff_len = prompt_diff_len(FALLBACK_DIFF_LEN)
|
|
194
205
|
|
|
195
206
|
# Add to presets dynamically (just for this session)
|
|
196
207
|
# Save to config
|
|
@@ -232,29 +243,89 @@ module CommitGpt
|
|
|
232
243
|
|
|
233
244
|
# Prompt for API key
|
|
234
245
|
def prompt_api_key(_provider_name, existing_key)
|
|
235
|
-
|
|
246
|
+
has_existing = existing_key && !existing_key.empty?
|
|
247
|
+
|
|
248
|
+
message = if has_existing
|
|
236
249
|
'Enter your API key (press Enter to keep existing):'
|
|
237
250
|
else
|
|
238
251
|
'Enter your API key:'
|
|
239
252
|
end
|
|
240
253
|
|
|
241
|
-
|
|
254
|
+
# mask returns nil, not '', when the input is empty
|
|
255
|
+
key = @prompt.mask(message) { |q| q.echo false }.to_s
|
|
242
256
|
|
|
243
257
|
# If user pressed Enter and there's an existing key, use it
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
258
|
+
key.empty? && has_existing ? existing_key : key
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Ask how long to wait when fetching the model list, and persist it
|
|
262
|
+
def prompt_model_fetch_timeout
|
|
263
|
+
current = ConfigManager.get_model_fetch_timeout
|
|
264
|
+
|
|
265
|
+
seconds = @prompt.ask('Set the timeout (seconds) for fetching the model list:') do |q|
|
|
266
|
+
q.default current.to_s
|
|
267
|
+
q.convert :int
|
|
268
|
+
q.validate ->(input) { input.to_i.positive? }
|
|
269
|
+
q.messages[:valid?] = 'Timeout must be a positive number of seconds'
|
|
248
270
|
end
|
|
271
|
+
|
|
272
|
+
seconds = current unless seconds.is_a?(Integer) && seconds.positive?
|
|
273
|
+
ConfigManager.set_model_fetch_timeout(seconds)
|
|
274
|
+
seconds
|
|
249
275
|
end
|
|
250
276
|
|
|
251
|
-
#
|
|
252
|
-
def
|
|
277
|
+
# Built-in models a preset declares, as { model => context tokens }
|
|
278
|
+
def preset_model_table(provider_name)
|
|
279
|
+
models = PROVIDER_PRESETS.find { |p| p[:value] == provider_name }&.dig(:models)
|
|
280
|
+
models unless models.nil? || models.empty?
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
# Built-in model list for a preset, if it declares one
|
|
284
|
+
def preset_models(provider_name)
|
|
285
|
+
preset_model_table(provider_name)&.keys
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
# Context window of a built-in model, in tokens
|
|
289
|
+
def model_context_tokens(provider_name, model)
|
|
290
|
+
preset_model_table(provider_name)&.dig(model)
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# Default diff length for a model, derived from its context window when
|
|
294
|
+
# known, and reported so the number is not a mystery. Numbers are printed
|
|
295
|
+
# undelimited because they get typed straight into the next prompt.
|
|
296
|
+
def default_diff_len(provider_name, model, existing)
|
|
297
|
+
context_tokens = model_context_tokens(provider_name, model)
|
|
298
|
+
return existing || FALLBACK_DIFF_LEN if context_tokens.nil?
|
|
299
|
+
|
|
300
|
+
fits = context_tokens * DIFF_BYTES_PER_TOKEN
|
|
301
|
+
suggested = [fits, MAX_SUGGESTED_DIFF_LEN].min
|
|
302
|
+
capped = ' (capped; larger diffs use chunked mode)' if suggested < fits
|
|
303
|
+
|
|
304
|
+
puts "\n#{model} context window: #{context_tokens} tokens (~#{fits} bytes of diff)".gray
|
|
305
|
+
puts " Suggested max diff length: #{suggested} bytes#{capped}".gray
|
|
306
|
+
|
|
307
|
+
# Only a value the user actually chose outranks the suggestion
|
|
308
|
+
customized = existing && existing != FALLBACK_DIFF_LEN
|
|
309
|
+
customized ? existing : suggested
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# Fetch models from provider with timeout.
|
|
313
|
+
# A preset that declares :models has no usable /models endpoint, so its
|
|
314
|
+
# built-in list is offered directly and no request is made.
|
|
315
|
+
def fetch_models_with_timeout(base_url, api_key, builtin_models = nil)
|
|
316
|
+
if builtin_models
|
|
317
|
+
puts 'Using the built-in model list (this provider does not expose /models).'.gray
|
|
318
|
+
return builtin_models
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
timeout = ConfigManager.get_model_fetch_timeout
|
|
253
322
|
puts 'Fetching available models...'.gray
|
|
254
323
|
|
|
255
324
|
models = nil
|
|
325
|
+
failure = nil
|
|
326
|
+
|
|
256
327
|
begin
|
|
257
|
-
Timeout.timeout(
|
|
328
|
+
Timeout.timeout(timeout) do
|
|
258
329
|
headers = {
|
|
259
330
|
'Content-Type' => 'application/json',
|
|
260
331
|
'User-Agent' => "Ruby/#{RUBY_VERSION}"
|
|
@@ -264,27 +335,23 @@ module CommitGpt
|
|
|
264
335
|
response = HTTParty.get("#{base_url}/models", headers: headers)
|
|
265
336
|
|
|
266
337
|
if response.code == 200
|
|
267
|
-
models = response['data'] || []
|
|
268
|
-
models = models.map { |m| m['id'] }.compact.sort
|
|
338
|
+
models = (response['data'] || []).map { |m| m['id'] }.compact.sort
|
|
269
339
|
else
|
|
270
|
-
|
|
271
|
-
return nil
|
|
340
|
+
failure = "Failed to fetch models: HTTP #{response.code}"
|
|
272
341
|
end
|
|
273
342
|
end
|
|
274
343
|
rescue Timeout::Error
|
|
275
|
-
|
|
276
|
-
exit(0)
|
|
344
|
+
failure = "Connection timeout (#{timeout}s). Please check your network, base_url, and api_key."
|
|
277
345
|
rescue StandardError => e
|
|
278
|
-
|
|
279
|
-
exit(0)
|
|
346
|
+
failure = "Error fetching models: #{e.message}"
|
|
280
347
|
end
|
|
281
348
|
|
|
282
|
-
if models.nil? || models.empty?
|
|
283
|
-
|
|
284
|
-
exit(0)
|
|
285
|
-
end
|
|
349
|
+
failure ||= 'No models found. Please check your configuration.' if models.nil? || models.empty?
|
|
350
|
+
return models if failure.nil?
|
|
286
351
|
|
|
287
|
-
|
|
352
|
+
puts "✖ #{failure}".red
|
|
353
|
+
puts " Run 'aicm setup' again to raise the timeout.".gray if failure.start_with?('Connection timeout')
|
|
354
|
+
exit(0)
|
|
288
355
|
end
|
|
289
356
|
|
|
290
357
|
# Let user select a model
|
|
@@ -312,7 +379,7 @@ module CommitGpt
|
|
|
312
379
|
end
|
|
313
380
|
|
|
314
381
|
# Prompt for diff length
|
|
315
|
-
def prompt_diff_len(default =
|
|
382
|
+
def prompt_diff_len(default = FALLBACK_DIFF_LEN)
|
|
316
383
|
answer = @prompt.ask('Set the maximum diff length (Bytes) for generating commit message:') do |q|
|
|
317
384
|
q.default default.to_s
|
|
318
385
|
q.convert :int
|
data/lib/commitgpt/version.rb
CHANGED