devcommit 0.1.5.5__tar.gz → 0.1.5.7__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devcommit
3
- Version: 0.1.5.5
3
+ Version: 0.1.5.7
4
4
  Summary: AI-powered git commit message generator
5
5
  License: GNU GENERAL PUBLIC LICENSE
6
6
  Version 3, 29 June 2007
@@ -782,6 +782,7 @@ A command-line AI tool for autocommits.
782
782
  # GEMINI_MODEL / OPENAI_MODEL / GROQ_MODEL / OPENROUTER_MODEL / ANTHROPIC_MODEL / OLLAMA_MODEL / CUSTOM_MODEL
783
783
  MODEL_NAME = gemini-2.5-flash
784
784
  COMMIT_MODE = auto
785
+ CHANGELOG_MODE = timestamped
785
786
  EOF
786
787
  ```
787
788
 
@@ -796,6 +797,7 @@ A command-line AI tool for autocommits.
796
797
  COMMIT_TYPE = conventional
797
798
  MODEL_NAME = gemini-2.0-flash-exp
798
799
  COMMIT_MODE = auto
800
+ CHANGELOG_MODE = timestamped
799
801
  EOF
800
802
  ```
801
803
 
@@ -1150,8 +1152,11 @@ devcommit --stageAll --changelog --files src/
1150
1152
 
1151
1153
  - **With `--stageAll`**: Changelog is generated from unstaged changes **before** staging
1152
1154
  - **Without `--stageAll`**: Changelog is generated from the last commit **after** committing
1153
- - Changelogs are saved as markdown files inside `branch/year/month/day` folders with time-based names (e.g., `changelogs/staging/2026/01/28/00-55-30.md`)
1154
- - Default base directory: `changelogs/` (configurable via `CHANGELOG_DIR` in `.dcommit`)
1155
+ - Storage mode is controlled by `CHANGELOG_MODE`:
1156
+ - `timestamped` (default): new file per run in `changelogs/<branch>/<year>/<month>/<day>/<time>.md`
1157
+ - `branch`: one file per branch, updated in place at `changelogs/<branch>.md`
1158
+ - Default base directory is `changelogs/` (configurable via `CHANGELOG_DIR` in `.dcommit`)
1159
+ - Empty sections are automatically removed (including empty `## [Unreleased]`)
1155
1160
  - Uses Keep a Changelog format with AI-generated content
1156
1161
 
1157
1162
  **Example workflow:**
@@ -1163,7 +1168,9 @@ devcommit --stageAll --changelog --files src/
1163
1168
  # Stage all changes and generate changelog before committing
1164
1169
  devcommit --stageAll --changelog
1165
1170
 
1166
- # The changelog file is created in changelogs/<branch>/<year>/<month>/<day>/ directory
1171
+ # The changelog file is created based on CHANGELOG_MODE
1172
+ # timestamped: changelogs/<branch>/<year>/<month>/<day>/<time>.md
1173
+ # branch: changelogs/<branch>.md
1167
1174
  # Then changes are staged and committed
1168
1175
  ```
1169
1176
 
@@ -1177,6 +1184,8 @@ devcommit --stageAll --changelog
1177
1184
  - `--files` or `-f`: Stage and commit specific files or folders (can specify multiple)
1178
1185
  - `--push` or `-p`: Push commits to remote after committing
1179
1186
  - `--changelog` or `-c`: Generate changelog file from changes
1187
+ - `--models`: Fetch and list models from the configured `AI_PROVIDER`
1188
+ - `--free` / `--paid`: With `--models`, limit OpenRouter results to free or paid models
1180
1189
 
1181
1190
  ### Examples
1182
1191
 
@@ -1255,6 +1264,19 @@ export OPENROUTER_MODEL='meta-llama/llama-3.3-70b-instruct:free'
1255
1264
  devcommit
1256
1265
  ```
1257
1266
 
1267
+ To fetch the live model catalogue for your configured provider without opening a browser:
1268
+
1269
+ ```bash
1270
+ AI_PROVIDER=openrouter devcommit --models
1271
+
1272
+ # Only free OpenRouter models
1273
+ AI_PROVIDER=openrouter devcommit --models --free
1274
+ ```
1275
+
1276
+ For OpenRouter, the table identifies free and paid models and shows their live token prices. For other providers, it lists the models accessible with that provider's configured API key; Ollama lists models installed locally. Copy an ID into the matching provider model setting, such as `OPENROUTER_MODEL`.
1277
+
1278
+ Set `REASONING_EFFORT=none` to disable reasoning where the selected model supports it and favor faster commit messages. The default is `auto`; `low`, `medium`, `high`, `xhigh`, and `max` are also accepted where supported.
1279
+
1258
1280
  **Popular free models on OpenRouter (add `:free` suffix):**
1259
1281
 
1260
1282
  **Recommended Models:**
@@ -1356,9 +1378,11 @@ All configuration can be set via **environment variables** or **`.dcommit` file*
1356
1378
  | `MAX_NO` | Number of commit message suggestions | `1` | Any positive integer |
1357
1379
  | `COMMIT_TYPE` | Style of commit messages | `general` | `general`, `conventional`, etc. |
1358
1380
  | `COMMIT_MODE` | Default commit strategy | `auto` | `auto`, `directory`, `global`, `related` |
1359
- | `EXCLUDE_FILES` | Files to exclude from diff | `package-lock.json, pnpm-lock.yaml, yarn.lock, *.lock` | Comma-separated file patterns |
1381
+ | `REASONING_EFFORT` | Reasoning effort for supported models | `auto` | `auto`, `none`, `low`, `medium`, `high`, `xhigh`, `max` |
1382
+ | `EXCLUDE_FILES` | Files to exclude from diff | `package-lock.json, pnpm-lock.yaml, yarn.lock, *.lock` | Comma-separated patterns or absolute paths within one repo |
1360
1383
  | `MAX_TOKENS` | Maximum tokens for AI response | `8192` | Any positive integer |
1361
1384
  | `CHANGELOG_DIR` | Directory for changelog files | `changelogs` | Any directory path |
1385
+ | `CHANGELOG_MODE` | Changelog file strategy | `timestamped` | `timestamped`, `branch` |
1362
1386
 
1363
1387
  ### Configuration Priority
1364
1388
 
@@ -77,6 +77,7 @@ A command-line AI tool for autocommits.
77
77
  # GEMINI_MODEL / OPENAI_MODEL / GROQ_MODEL / OPENROUTER_MODEL / ANTHROPIC_MODEL / OLLAMA_MODEL / CUSTOM_MODEL
78
78
  MODEL_NAME = gemini-2.5-flash
79
79
  COMMIT_MODE = auto
80
+ CHANGELOG_MODE = timestamped
80
81
  EOF
81
82
  ```
82
83
 
@@ -91,6 +92,7 @@ A command-line AI tool for autocommits.
91
92
  COMMIT_TYPE = conventional
92
93
  MODEL_NAME = gemini-2.0-flash-exp
93
94
  COMMIT_MODE = auto
95
+ CHANGELOG_MODE = timestamped
94
96
  EOF
95
97
  ```
96
98
 
@@ -445,8 +447,11 @@ devcommit --stageAll --changelog --files src/
445
447
 
446
448
  - **With `--stageAll`**: Changelog is generated from unstaged changes **before** staging
447
449
  - **Without `--stageAll`**: Changelog is generated from the last commit **after** committing
448
- - Changelogs are saved as markdown files inside `branch/year/month/day` folders with time-based names (e.g., `changelogs/staging/2026/01/28/00-55-30.md`)
449
- - Default base directory: `changelogs/` (configurable via `CHANGELOG_DIR` in `.dcommit`)
450
+ - Storage mode is controlled by `CHANGELOG_MODE`:
451
+ - `timestamped` (default): new file per run in `changelogs/<branch>/<year>/<month>/<day>/<time>.md`
452
+ - `branch`: one file per branch, updated in place at `changelogs/<branch>.md`
453
+ - Default base directory is `changelogs/` (configurable via `CHANGELOG_DIR` in `.dcommit`)
454
+ - Empty sections are automatically removed (including empty `## [Unreleased]`)
450
455
  - Uses Keep a Changelog format with AI-generated content
451
456
 
452
457
  **Example workflow:**
@@ -458,7 +463,9 @@ devcommit --stageAll --changelog --files src/
458
463
  # Stage all changes and generate changelog before committing
459
464
  devcommit --stageAll --changelog
460
465
 
461
- # The changelog file is created in changelogs/<branch>/<year>/<month>/<day>/ directory
466
+ # The changelog file is created based on CHANGELOG_MODE
467
+ # timestamped: changelogs/<branch>/<year>/<month>/<day>/<time>.md
468
+ # branch: changelogs/<branch>.md
462
469
  # Then changes are staged and committed
463
470
  ```
464
471
 
@@ -472,6 +479,8 @@ devcommit --stageAll --changelog
472
479
  - `--files` or `-f`: Stage and commit specific files or folders (can specify multiple)
473
480
  - `--push` or `-p`: Push commits to remote after committing
474
481
  - `--changelog` or `-c`: Generate changelog file from changes
482
+ - `--models`: Fetch and list models from the configured `AI_PROVIDER`
483
+ - `--free` / `--paid`: With `--models`, limit OpenRouter results to free or paid models
475
484
 
476
485
  ### Examples
477
486
 
@@ -550,6 +559,19 @@ export OPENROUTER_MODEL='meta-llama/llama-3.3-70b-instruct:free'
550
559
  devcommit
551
560
  ```
552
561
 
562
+ To fetch the live model catalogue for your configured provider without opening a browser:
563
+
564
+ ```bash
565
+ AI_PROVIDER=openrouter devcommit --models
566
+
567
+ # Only free OpenRouter models
568
+ AI_PROVIDER=openrouter devcommit --models --free
569
+ ```
570
+
571
+ For OpenRouter, the table identifies free and paid models and shows their live token prices. For other providers, it lists the models accessible with that provider's configured API key; Ollama lists models installed locally. Copy an ID into the matching provider model setting, such as `OPENROUTER_MODEL`.
572
+
573
+ Set `REASONING_EFFORT=none` to disable reasoning where the selected model supports it and favor faster commit messages. The default is `auto`; `low`, `medium`, `high`, `xhigh`, and `max` are also accepted where supported.
574
+
553
575
  **Popular free models on OpenRouter (add `:free` suffix):**
554
576
 
555
577
  **Recommended Models:**
@@ -651,9 +673,11 @@ All configuration can be set via **environment variables** or **`.dcommit` file*
651
673
  | `MAX_NO` | Number of commit message suggestions | `1` | Any positive integer |
652
674
  | `COMMIT_TYPE` | Style of commit messages | `general` | `general`, `conventional`, etc. |
653
675
  | `COMMIT_MODE` | Default commit strategy | `auto` | `auto`, `directory`, `global`, `related` |
654
- | `EXCLUDE_FILES` | Files to exclude from diff | `package-lock.json, pnpm-lock.yaml, yarn.lock, *.lock` | Comma-separated file patterns |
676
+ | `REASONING_EFFORT` | Reasoning effort for supported models | `auto` | `auto`, `none`, `low`, `medium`, `high`, `xhigh`, `max` |
677
+ | `EXCLUDE_FILES` | Files to exclude from diff | `package-lock.json, pnpm-lock.yaml, yarn.lock, *.lock` | Comma-separated patterns or absolute paths within one repo |
655
678
  | `MAX_TOKENS` | Maximum tokens for AI response | `8192` | Any positive integer |
656
679
  | `CHANGELOG_DIR` | Directory for changelog files | `changelogs` | Any directory path |
680
+ | `CHANGELOG_MODE` | Changelog file strategy | `timestamped` | `timestamped`, `branch` |
657
681
 
658
682
  ### Configuration Priority
659
683
 
@@ -34,6 +34,19 @@ except ImportError:
34
34
  sys.stderr = _stderr
35
35
  _devnull.close()
36
36
 
37
+ REASONING_EFFORTS = {"auto", "none", "low", "medium", "high", "xhigh", "max"}
38
+
39
+
40
+ def normalize_reasoning_effort(value: str) -> str:
41
+ effort = value.lower()
42
+ if effort not in REASONING_EFFORTS:
43
+ raise ValueError("REASONING_EFFORT must be auto, none, low, medium, high, xhigh, or max")
44
+ return effort
45
+
46
+
47
+ def reasoning_extra_body(effort: str) -> dict:
48
+ return {} if effort == "auto" else {"reasoning_effort": effort}
49
+
37
50
 
38
51
  class AIProvider(ABC):
39
52
  """Base class for AI providers"""
@@ -106,11 +119,12 @@ class GeminiProvider(AIProvider):
106
119
  class OpenAIProvider(AIProvider):
107
120
  """OpenAI GPT provider"""
108
121
 
109
- def __init__(self, api_key: str, model: str = "gpt-4o-mini"):
122
+ def __init__(self, api_key: str, model: str = "gpt-4o-mini", reasoning_effort: str = "auto"):
110
123
  if not openai:
111
124
  raise ImportError("openai not installed. Run: pip install openai")
112
125
  self.client = openai.OpenAI(api_key=api_key)
113
126
  self.model = model
127
+ self.reasoning_effort = normalize_reasoning_effort(reasoning_effort)
114
128
 
115
129
  def generate_commit_message(
116
130
  self, diff: str, prompt: str, max_tokens: int
@@ -123,6 +137,7 @@ class OpenAIProvider(AIProvider):
123
137
  ],
124
138
  max_tokens=max_tokens,
125
139
  temperature=0.7,
140
+ extra_body=reasoning_extra_body(self.reasoning_effort),
126
141
  )
127
142
  if not response or not response.choices:
128
143
  raise ValueError("OpenAI returned empty response (no choices).")
@@ -132,13 +147,14 @@ class OpenAIProvider(AIProvider):
132
147
  class GroqProvider(AIProvider):
133
148
  """Groq AI provider (OpenAI-compatible)"""
134
149
 
135
- def __init__(self, api_key: str, model: str = "llama-3.3-70b-versatile"):
150
+ def __init__(self, api_key: str, model: str = "llama-3.3-70b-versatile", reasoning_effort: str = "auto"):
136
151
  if not openai:
137
152
  raise ImportError("openai not installed. Run: pip install openai")
138
153
  self.client = openai.OpenAI(
139
154
  api_key=api_key, base_url="https://api.groq.com/openai/v1"
140
155
  )
141
156
  self.model = model
157
+ self.reasoning_effort = normalize_reasoning_effort(reasoning_effort)
142
158
 
143
159
  def generate_commit_message(
144
160
  self, diff: str, prompt: str, max_tokens: int
@@ -151,6 +167,7 @@ class GroqProvider(AIProvider):
151
167
  ],
152
168
  max_tokens=max_tokens,
153
169
  temperature=0.7,
170
+ extra_body=reasoning_extra_body(self.reasoning_effort),
154
171
  )
155
172
  if not response or not response.choices:
156
173
  raise ValueError("Groq returned empty response (no choices).")
@@ -164,6 +181,7 @@ class OpenRouterProvider(AIProvider):
164
181
  self,
165
182
  api_key: str,
166
183
  model: str = "meta-llama/llama-3.3-70b-instruct:free",
184
+ reasoning_effort: str = "auto",
167
185
  ):
168
186
  if not openai:
169
187
  raise ImportError("openai not installed. Run: pip install openai")
@@ -176,6 +194,7 @@ class OpenRouterProvider(AIProvider):
176
194
  },
177
195
  )
178
196
  self.model = model
197
+ self.reasoning_effort = normalize_reasoning_effort(reasoning_effort)
179
198
 
180
199
  def generate_commit_message(
181
200
  self, diff: str, prompt: str, max_tokens: int
@@ -188,7 +207,7 @@ class OpenRouterProvider(AIProvider):
188
207
  ],
189
208
  max_tokens=max_tokens,
190
209
  temperature=0.7,
191
- extra_body={"transforms": ["middle-out"]},
210
+ extra_body={"transforms": ["middle-out"], **reasoning_extra_body(self.reasoning_effort)},
192
211
  extra_headers={
193
212
  "HTTP-Referer": "https://github.com/hordunlarmy/DevCommit",
194
213
  "X-Title": "DevCommit",
@@ -207,23 +226,30 @@ class OpenRouterProvider(AIProvider):
207
226
  class AnthropicProvider(AIProvider):
208
227
  """Anthropic Claude provider"""
209
228
 
210
- def __init__(self, api_key: str, model: str = "claude-3-haiku-20240307"):
229
+ def __init__(self, api_key: str, model: str = "claude-3-haiku-20240307", reasoning_effort: str = "auto"):
211
230
  if not anthropic:
212
231
  raise ImportError(
213
232
  "anthropic not installed. Run: pip install anthropic"
214
233
  )
215
234
  self.client = anthropic.Anthropic(api_key=api_key)
216
235
  self.model = model
236
+ self.reasoning_effort = normalize_reasoning_effort(reasoning_effort)
217
237
 
218
238
  def generate_commit_message(
219
239
  self, diff: str, prompt: str, max_tokens: int
220
240
  ) -> str:
221
- message = self.client.messages.create(
241
+ request = dict(
222
242
  model=self.model,
223
243
  max_tokens=max_tokens,
224
244
  system=prompt,
225
245
  messages=[{"role": "user", "content": diff}],
226
246
  )
247
+ if self.reasoning_effort == "none":
248
+ request["thinking"] = {"type": "disabled"}
249
+ elif self.reasoning_effort != "auto":
250
+ request["thinking"] = {"type": "adaptive"}
251
+ request["output_config"] = {"effort": self.reasoning_effort}
252
+ message = self.client.messages.create(**request)
227
253
  return message.content[0].text.strip()
228
254
 
229
255
 
@@ -231,7 +257,7 @@ class OllamaProvider(AIProvider):
231
257
  """Ollama local model provider"""
232
258
 
233
259
  def __init__(
234
- self, base_url: str = "http://localhost:11434", model: str = "llama3"
260
+ self, base_url: str = "http://localhost:11434", model: str = "llama3", reasoning_effort: str = "auto"
235
261
  ):
236
262
  if not requests:
237
263
  raise ImportError(
@@ -239,6 +265,7 @@ class OllamaProvider(AIProvider):
239
265
  )
240
266
  self.base_url = base_url.rstrip("/")
241
267
  self.model = model
268
+ self.reasoning_effort = normalize_reasoning_effort(reasoning_effort)
242
269
 
243
270
  def generate_commit_message(
244
271
  self, diff: str, prompt: str, max_tokens: int
@@ -250,6 +277,8 @@ class OllamaProvider(AIProvider):
250
277
  "stream": False,
251
278
  "options": {"temperature": 0.7, "num_predict": max_tokens},
252
279
  }
280
+ if self.reasoning_effort != "auto":
281
+ data["think"] = False if self.reasoning_effort == "none" else self.reasoning_effort
253
282
 
254
283
  response = requests.post(url, json=data, timeout=60)
255
284
  response.raise_for_status()
@@ -267,6 +296,7 @@ class CustomProvider(AIProvider):
267
296
  api_url: str,
268
297
  api_key: Optional[str] = None,
269
298
  model: str = "default",
299
+ reasoning_effort: str = "auto",
270
300
  ):
271
301
  if not openai:
272
302
  raise ImportError("openai not installed. Run: pip install openai")
@@ -282,6 +312,7 @@ class CustomProvider(AIProvider):
282
312
  api_key=api_key or "dummy-key", base_url=base_url
283
313
  )
284
314
  self.model = model
315
+ self.reasoning_effort = normalize_reasoning_effort(reasoning_effort)
285
316
 
286
317
  def generate_commit_message(
287
318
  self, diff: str, prompt: str, max_tokens: int
@@ -294,6 +325,7 @@ class CustomProvider(AIProvider):
294
325
  ],
295
326
  max_tokens=max_tokens,
296
327
  temperature=0.7,
328
+ extra_body=reasoning_extra_body(self.reasoning_effort),
297
329
  )
298
330
  if not response or not response.choices:
299
331
  raise ValueError("Custom API returned empty response (no choices).")
@@ -304,6 +336,7 @@ def get_ai_provider(config) -> AIProvider:
304
336
  """Factory function to get the appropriate AI provider based on config"""
305
337
 
306
338
  provider_name = config("AI_PROVIDER", default="gemini").lower()
339
+ reasoning_effort = config("REASONING_EFFORT", default="auto")
307
340
 
308
341
  if provider_name == "gemini":
309
342
  api_key = config("GEMINI_API_KEY", default=None)
@@ -313,6 +346,8 @@ def get_ai_provider(config) -> AIProvider:
313
346
  model = config("GEMINI_MODEL", default=None) or config(
314
347
  "MODEL_NAME", default="gemini-2.0-flash-exp"
315
348
  )
349
+ if normalize_reasoning_effort(reasoning_effort) != "auto":
350
+ raise ValueError("REASONING_EFFORT is unsupported by the legacy Gemini SDK; use auto")
316
351
  return GeminiProvider(api_key, model)
317
352
 
318
353
  elif provider_name == "openai":
@@ -322,7 +357,7 @@ def get_ai_provider(config) -> AIProvider:
322
357
  model = config("OPENAI_MODEL", default=None) or config(
323
358
  "MODEL_NAME", default="gpt-4o-mini"
324
359
  )
325
- return OpenAIProvider(api_key, model)
360
+ return OpenAIProvider(api_key, model, reasoning_effort)
326
361
 
327
362
  elif provider_name == "groq":
328
363
  api_key = config("GROQ_API_KEY", default=None)
@@ -331,7 +366,7 @@ def get_ai_provider(config) -> AIProvider:
331
366
  model = config("GROQ_MODEL", default=None) or config(
332
367
  "MODEL_NAME", default="llama-3.3-70b-versatile"
333
368
  )
334
- return GroqProvider(api_key, model)
369
+ return GroqProvider(api_key, model, reasoning_effort)
335
370
 
336
371
  elif provider_name == "openrouter":
337
372
  api_key = config("OPENROUTER_API_KEY", default=None)
@@ -340,7 +375,7 @@ def get_ai_provider(config) -> AIProvider:
340
375
  model = config("OPENROUTER_MODEL", default=None) or config(
341
376
  "MODEL_NAME", default="meta-llama/llama-3.3-70b-instruct:free"
342
377
  )
343
- return OpenRouterProvider(api_key, model)
378
+ return OpenRouterProvider(api_key, model, reasoning_effort)
344
379
 
345
380
  elif provider_name == "anthropic":
346
381
  api_key = config("ANTHROPIC_API_KEY", default=None)
@@ -349,14 +384,14 @@ def get_ai_provider(config) -> AIProvider:
349
384
  model = config("ANTHROPIC_MODEL", default=None) or config(
350
385
  "MODEL_NAME", default="claude-3-haiku-20240307"
351
386
  )
352
- return AnthropicProvider(api_key, model)
387
+ return AnthropicProvider(api_key, model, reasoning_effort)
353
388
 
354
389
  elif provider_name == "ollama":
355
390
  base_url = config("OLLAMA_BASE_URL", default="http://localhost:11434")
356
391
  model = config("OLLAMA_MODEL", default=None) or config(
357
392
  "MODEL_NAME", default="llama3"
358
393
  )
359
- return OllamaProvider(base_url, model)
394
+ return OllamaProvider(base_url, model, reasoning_effort)
360
395
 
361
396
  elif provider_name == "custom":
362
397
  api_url = config("CUSTOM_API_URL", default=None)
@@ -366,7 +401,7 @@ def get_ai_provider(config) -> AIProvider:
366
401
  model = config("CUSTOM_MODEL", default=None) or config(
367
402
  "MODEL_NAME", default="default"
368
403
  )
369
- return CustomProvider(api_url, api_key, model)
404
+ return CustomProvider(api_url, api_key, model, reasoning_effort)
370
405
 
371
406
  else:
372
407
  raise ValueError(