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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5087467607c5fa8e321e66d9515ac5ebd4a65ef513a1cb0dc9a5819db4e72601
4
- data.tar.gz: 3e33525a45b3892e4811671dde772df60e230d22b1b011019f3906ab4752ff1e
3
+ metadata.gz: bc50a5ed7cfb5cc59703014b3b6f58370028dfe2c1bc5c7186e3c6d43a755dd7
4
+ data.tar.gz: e9dd5c01f7c07062fc803710c79a31ce40ab9a9625918c2753bf59d25b4f9d4b
5
5
  SHA512:
6
- metadata.gz: c78638efe575fd73035cffee4bd8779eace5552315beb8c5400a6223d99410dd4981fef26c755156920fd65206f99b9c4bdb3530b638afdf2dfbb3bd76e5190c
7
- data.tar.gz: f40cc753a3b6849df45d2b8a11b19d008ad20661f4461d5ba61f54a20ce8234592fa6432b64354a7e27bbfd0ffda1dde82fb2f5385bd34dec76fab6bc3012f75
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. Choose an AI provider (Presets: Cerebras, OpenAI, Ollama, Groq, etc.)
125
- 2. Enter your API Key (stored securely in `config.local.yml`)
126
- 3. Select a model interactively
127
- 4. Set maximum diff length
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
- MiniCPM5-1B
265
- MiniCPM-V46
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
- { label: 'AMD Radeon (China)', value: 'amd', base_url: 'https://developer.amd.com.cn/radeon/api/v1' },
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
- # Seconds to wait when fetching the provider's model list
14
- MODEL_FETCH_TIMEOUT = 30
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'] || 32_768)
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') || 32_768)
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(32_768)
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
- message = if existing_key && !existing_key.empty?
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
- key = @prompt.mask(message) { |q| q.echo false }
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
- if key.empty? && existing_key && !existing_key.empty?
245
- existing_key
246
- else
247
- key
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
- # Fetch models from provider with timeout
252
- def fetch_models_with_timeout(base_url, api_key)
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(MODEL_FETCH_TIMEOUT) do
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
- puts "Failed to fetch models: HTTP #{response.code}".red
271
- return nil
340
+ failure = "Failed to fetch models: HTTP #{response.code}"
272
341
  end
273
342
  end
274
343
  rescue Timeout::Error
275
- puts "Connection timeout (#{MODEL_FETCH_TIMEOUT}s). Please check your network, base_url, and api_key.".red
276
- exit(0)
344
+ failure = "Connection timeout (#{timeout}s). Please check your network, base_url, and api_key."
277
345
  rescue StandardError => e
278
- puts "Error fetching models: #{e.message}".red
279
- exit(0)
346
+ failure = "Error fetching models: #{e.message}"
280
347
  end
281
348
 
282
- if models.nil? || models.empty?
283
- puts '✖ No models found. Please check your configuration.'.red
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
- models
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 = 32_768)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CommitGpt
4
- VERSION = '0.3.8'
4
+ VERSION = '0.3.10'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: commitgpt
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.8
4
+ version: 0.3.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peng Zhang