git-copilot-commit 0.5.7__py3-none-any.whl → 0.6.1__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.
git_copilot_commit/cli.py CHANGED
@@ -9,12 +9,10 @@ import re
9
9
  import sys
10
10
  from typing import Annotated, Sequence
11
11
 
12
- import rich
13
- import typer
12
+ import cyclopts
14
13
  from rich.console import Console
15
14
  from rich.panel import Panel
16
- from rich.prompt import Confirm
17
- from typer.main import get_command
15
+ from rich.prompt import Confirm, Prompt
18
16
 
19
17
  from .git import GitRepository, GitError, GitStatus, NotAGitRepositoryError
20
18
  from .split_commits import (
@@ -29,10 +27,12 @@ from .split_commits import (
29
27
  )
30
28
  from .settings import Settings
31
29
  from .version import __version__
32
- from . import github_copilot
30
+ from .llms import copilot
31
+ from .llms import core as llm
32
+ from .llms import providers
33
33
 
34
34
  console = Console()
35
- app = typer.Typer(help=__doc__, add_completion=False)
35
+ app = cyclopts.App(help=__doc__, version=lambda: f"git-copilot-commit {__version__}")
36
36
 
37
37
  COMMIT_MESSAGE_PROMPT_FILENAME = "commit-message-generator-prompt.md"
38
38
  SPLIT_COMMIT_PLANNER_PROMPT_FILENAME = "split-commit-planner-prompt.md"
@@ -55,22 +55,50 @@ NATIVE_TLS_HELP = (
55
55
 
56
56
  CaBundleOption = Annotated[
57
57
  str | None,
58
- typer.Option("--ca-bundle", metavar="PATH", help=CA_BUNDLE_HELP),
58
+ cyclopts.Parameter(name="--ca-bundle", help=CA_BUNDLE_HELP),
59
59
  ]
60
60
  InsecureOption = Annotated[
61
61
  bool,
62
- typer.Option("--insecure", help="Disable SSL certificate verification."),
62
+ cyclopts.Parameter(name="--insecure", help="Disable SSL certificate verification."),
63
63
  ]
64
64
  NativeTlsOption = Annotated[
65
65
  bool,
66
- typer.Option("--native-tls/--no-native-tls", help=NATIVE_TLS_HELP),
66
+ cyclopts.Parameter(
67
+ name="--native-tls",
68
+ negative="--no-native-tls",
69
+ help=NATIVE_TLS_HELP,
70
+ ),
71
+ ]
72
+ ProviderOption = Annotated[
73
+ str | None,
74
+ cyclopts.Parameter(
75
+ name="--provider",
76
+ help="LLM provider to use: copilot or openai.",
77
+ ),
78
+ ]
79
+ BaseUrlOption = Annotated[
80
+ str | None,
81
+ cyclopts.Parameter(
82
+ name="--base-url",
83
+ help=(
84
+ "Base URL for an OpenAI-compatible provider, for example "
85
+ "http://127.0.0.1:11434/v1."
86
+ ),
87
+ ),
88
+ ]
89
+ ApiKeyOption = Annotated[
90
+ str | None,
91
+ cyclopts.Parameter(
92
+ name="--api-key",
93
+ help="API key for an OpenAI-compatible provider. Omit when the server does not require one.",
94
+ ),
67
95
  ]
68
96
 
69
97
 
70
98
  SplitOption = Annotated[
71
99
  bool,
72
- typer.Option(
73
- "--split",
100
+ cyclopts.Parameter(
101
+ name="--split",
74
102
  help=(
75
103
  "Split staged hunks into multiple commits automatically. Pass "
76
104
  "`--split=N` to express a preference for N commits."
@@ -79,10 +107,10 @@ SplitOption = Annotated[
79
107
  ]
80
108
  SplitCountOption = Annotated[
81
109
  int | None,
82
- typer.Option(
83
- "--split-count",
84
- hidden=True,
85
- min=1,
110
+ cyclopts.Parameter(
111
+ name="--split-count",
112
+ show=False,
113
+ validator=cyclopts.validators.Number(gte=1),
86
114
  ),
87
115
  ]
88
116
 
@@ -198,39 +226,13 @@ def order_prepared_split_commits(
198
226
  def run(args: Sequence[str] | None = None) -> None:
199
227
  """Run the CLI entrypoint with argument normalization."""
200
228
  raw_args = list(args) if args is not None else sys.argv[1:]
201
- command = get_command(app)
202
- command.main(
203
- args=preprocess_cli_args(raw_args),
204
- prog_name=Path(sys.argv[0]).name,
205
- )
229
+ app(preprocess_cli_args(raw_args))
206
230
 
207
231
 
208
- def version_callback(value: bool):
209
- if value:
210
- rich.print(f"git-copilot-commit [bold yellow]{__version__}[/]")
211
- raise typer.Exit()
212
-
213
-
214
- @app.callback(invoke_without_command=True)
215
- def main(
216
- ctx: typer.Context,
217
- _: bool = typer.Option(
218
- False, "--version", callback=version_callback, help="Show version and exit"
219
- ),
220
- ):
221
- """
222
- Automatically commit changes in the current git repository.
223
- """
224
- if ctx.invoked_subcommand is None:
225
- # Show help when no command is provided
226
- console.print(ctx.get_help())
227
- raise typer.Exit()
228
- else:
229
- # Don't show version for print command to avoid interfering with pipes
230
- if ctx.invoked_subcommand != "echo":
231
- console.print(
232
- f"[bold]{(__package__ or 'git_copilot_commit').replace('_', '-')}[/] - [bold green]v{__version__}[/]\n"
233
- )
232
+ def print_cli_banner() -> None:
233
+ """Render the CLI banner before command output."""
234
+ package_name = (__package__ or "git_copilot_commit").replace("_", "-")
235
+ console.print(f"[bold]{package_name}[/] - [bold green]v{__version__}[/]\n")
234
236
 
235
237
 
236
238
  def get_prompt_locations(filename: str):
@@ -253,7 +255,7 @@ def resolve_prompt_file() -> Path | None:
253
255
  console.print(
254
256
  f"[red]Configured default prompt file in {settings.config_file} is invalid.[/red]"
255
257
  )
256
- raise typer.Exit(1)
258
+ raise SystemExit(1)
257
259
 
258
260
  if configured_prompt_file is None:
259
261
  return None
@@ -271,7 +273,7 @@ def load_system_prompt() -> str:
271
273
  console.print(
272
274
  f"[red]Error reading prompt file {resolved_prompt_file}: {exc}[/red]"
273
275
  )
274
- raise typer.Exit(1)
276
+ raise SystemExit(1)
275
277
 
276
278
  return load_named_prompt(COMMIT_MESSAGE_PROMPT_FILENAME)
277
279
 
@@ -285,7 +287,7 @@ def load_named_prompt(filename: str) -> str:
285
287
  continue
286
288
 
287
289
  console.print(f"[red]Error: Prompt file {filename} not found in any location[/red]")
288
- raise typer.Exit(1)
290
+ raise SystemExit(1)
289
291
 
290
292
 
291
293
  def build_http_client_config(
@@ -293,29 +295,29 @@ def build_http_client_config(
293
295
  ca_bundle: str | None,
294
296
  insecure: bool,
295
297
  native_tls: bool,
296
- ) -> github_copilot.HttpClientConfig:
298
+ ) -> llm.HttpClientConfig:
297
299
  if ca_bundle is not None:
298
300
  ca_bundle = os.path.expanduser(ca_bundle)
299
- return github_copilot.HttpClientConfig(
301
+ return llm.HttpClientConfig(
300
302
  native_tls=native_tls,
301
303
  insecure=insecure,
302
304
  ca_bundle=ca_bundle,
303
305
  )
304
306
 
305
307
 
306
- def print_copilot_error(message: str, exc: github_copilot.CopilotError) -> None:
307
- """Render Copilot errors, with rich formatting for model selection issues."""
308
- if isinstance(exc, github_copilot.ModelSelectionError):
308
+ def print_llm_error(message: str, exc: llm.LLMError) -> None:
309
+ """Render LLM errors, with rich formatting for model selection issues."""
310
+ if isinstance(exc, llm.ModelSelectionError):
309
311
  console.print(f"[red]{message}[/red]")
310
- github_copilot.print_model_selection_error(exc)
312
+ llm.print_model_selection_error(exc)
311
313
  return
312
314
 
313
315
  console.print(f"[red]{message}: {exc}[/red]")
314
316
 
315
317
 
316
- def display_selected_model(model: github_copilot.CopilotModel) -> None:
317
- """Show the resolved Copilot model for the current command."""
318
- details = [github_copilot.infer_api_surface(model)]
318
+ def display_selected_model(model: llm.Model) -> None:
319
+ """Show the resolved model for the current command."""
320
+ details = [llm.infer_api_surface(model)]
319
321
  if model.vendor:
320
322
  details.insert(0, model.vendor)
321
323
  console.print(f"[green]Using model:[/green] {model.id} ({', '.join(details)})")
@@ -330,7 +332,7 @@ def build_commit_message_prompt(
330
332
  """Build the prompt used to generate a commit message."""
331
333
  if not status.has_staged_changes:
332
334
  console.print("[red]No staged changes to commit.[/red]")
333
- raise typer.Exit()
335
+ raise SystemExit()
334
336
 
335
337
  prompt_parts = [
336
338
  "`git status`:\n",
@@ -352,20 +354,26 @@ def build_commit_message_prompt(
352
354
 
353
355
 
354
356
  def normalize_model_name(model: str | None) -> str | None:
355
- """Normalize model names accepted by the CLI to Copilot API model ids."""
356
- if model is not None and model.startswith("github_copilot/"):
357
- return model.replace("github_copilot/", "", 1)
357
+ """Normalize model names accepted by the CLI to provider model ids."""
358
+ if model is not None:
359
+ for prefix in (
360
+ "copilot/",
361
+ "openai-compatible/",
362
+ ):
363
+ if model.startswith(prefix):
364
+ return model.replace(prefix, "", 1)
358
365
  return model
359
366
 
360
367
 
361
- def ask_copilot_with_system_prompt(
368
+ def ask_llm_with_system_prompt(
362
369
  system_prompt: str,
363
370
  prompt: str,
364
371
  model: str | None = None,
365
- http_client_config: github_copilot.HttpClientConfig | None = None,
372
+ provider_config: providers.ProviderConfig | None = None,
373
+ http_client_config: llm.HttpClientConfig | None = None,
366
374
  ) -> str:
367
- """Send a prepared prompt to Copilot using the provided system prompt."""
368
- return github_copilot.ask(
375
+ """Send a prepared prompt to the selected LLM provider."""
376
+ return providers.ask(
369
377
  f"""
370
378
  # System Prompt
371
379
 
@@ -375,6 +383,7 @@ def ask_copilot_with_system_prompt(
375
383
 
376
384
  {prompt}
377
385
  """,
386
+ provider_config=provider_config,
378
387
  model=normalize_model_name(model),
379
388
  http_client_config=http_client_config,
380
389
  )
@@ -383,20 +392,22 @@ def ask_copilot_with_system_prompt(
383
392
  def generate_commit_message_for_prompt(
384
393
  prompt: str,
385
394
  model: str | None = None,
386
- http_client_config: github_copilot.HttpClientConfig | None = None,
395
+ provider_config: providers.ProviderConfig | None = None,
396
+ http_client_config: llm.HttpClientConfig | None = None,
387
397
  ) -> str:
388
398
  """Generate a conventional commit message from a prepared prompt."""
389
- return ask_copilot_with_system_prompt(
399
+ return ask_llm_with_system_prompt(
390
400
  load_system_prompt(),
391
401
  prompt,
392
402
  model=model,
403
+ provider_config=provider_config,
393
404
  http_client_config=http_client_config,
394
405
  )
395
406
 
396
407
 
397
- def should_retry_with_compact_prompt(exc: github_copilot.CopilotError) -> bool:
408
+ def should_retry_with_compact_prompt(exc: llm.LLMError) -> bool:
398
409
  message_parts = [str(exc)]
399
- if isinstance(exc, github_copilot.CopilotHttpError) and exc.detail:
410
+ if isinstance(exc, llm.LLMHttpError) and exc.detail:
400
411
  message_parts.append(exc.detail)
401
412
 
402
413
  haystack = " ".join(part.strip() for part in message_parts if part).lower()
@@ -422,7 +433,8 @@ def generate_commit_message_for_status(
422
433
  status: GitStatus,
423
434
  model: str | None = None,
424
435
  context: str = "",
425
- http_client_config: github_copilot.HttpClientConfig | None = None,
436
+ provider_config: providers.ProviderConfig | None = None,
437
+ http_client_config: llm.HttpClientConfig | None = None,
426
438
  ) -> str:
427
439
  """Generate a commit message for a staged status snapshot."""
428
440
  full_prompt = build_commit_message_prompt(status, context=context)
@@ -430,9 +442,10 @@ def generate_commit_message_for_status(
430
442
  return generate_commit_message_for_prompt(
431
443
  full_prompt,
432
444
  model=model,
445
+ provider_config=provider_config,
433
446
  http_client_config=http_client_config,
434
447
  )
435
- except github_copilot.CopilotError as exc:
448
+ except llm.LLMError as exc:
436
449
  if not should_retry_with_compact_prompt(exc):
437
450
  raise
438
451
 
@@ -447,6 +460,7 @@ def generate_commit_message_for_status(
447
460
  return generate_commit_message_for_prompt(
448
461
  fallback_prompt,
449
462
  model=model,
463
+ provider_config=provider_config,
450
464
  http_client_config=http_client_config,
451
465
  )
452
466
 
@@ -466,35 +480,35 @@ def commit_with_retry_no_verify(
466
480
  "Retry commit with [bold]`-n`[/] (skip hooks) using the same commit message?",
467
481
  default=True,
468
482
  ):
469
- raise typer.Exit(1)
483
+ raise SystemExit(1)
470
484
 
471
485
  try:
472
486
  return repo.commit(message, use_editor=use_editor, no_verify=True, env=env)
473
487
  except GitError as retry_error:
474
488
  console.print(f"[red]Commit with -n failed: {retry_error}[/red]")
475
- raise typer.Exit(1)
489
+ raise SystemExit(1)
476
490
 
477
491
 
478
492
  def ensure_copilot_authentication(
479
- http_client_config: github_copilot.HttpClientConfig,
493
+ http_client_config: llm.HttpClientConfig,
480
494
  ) -> None:
481
495
  """Authenticate if no cached Copilot credentials are available."""
482
496
  try:
483
- existing_credentials = github_copilot.load_credentials()
484
- except github_copilot.CopilotError:
497
+ existing_credentials = copilot.load_credentials()
498
+ except copilot.LLMError:
485
499
  existing_credentials = None
486
500
 
487
501
  if existing_credentials is not None:
488
502
  return
489
503
 
490
504
  try:
491
- github_copilot.login(
505
+ copilot.login(
492
506
  force=True,
493
507
  http_client_config=http_client_config,
494
508
  )
495
- except github_copilot.CopilotError as exc:
496
- print_copilot_error("Authentication failed", exc)
497
- raise typer.Exit(1)
509
+ except copilot.LLMError as exc:
510
+ print_llm_error("Authentication failed", exc)
511
+ raise SystemExit(1)
498
512
 
499
513
 
500
514
  def stage_changes_for_commit(
@@ -533,7 +547,8 @@ def request_commit_message(
533
547
  status: GitStatus,
534
548
  model: str | None = None,
535
549
  context: str = "",
536
- http_client_config: github_copilot.HttpClientConfig | None = None,
550
+ provider_config: providers.ProviderConfig | None = None,
551
+ http_client_config: llm.HttpClientConfig | None = None,
537
552
  ) -> str:
538
553
  """Request a commit message for the provided staged state."""
539
554
  try:
@@ -544,11 +559,12 @@ def request_commit_message(
544
559
  status,
545
560
  model=model,
546
561
  context=context,
562
+ provider_config=provider_config,
547
563
  http_client_config=http_client_config,
548
564
  )
549
- except github_copilot.CopilotError as exc:
550
- print_copilot_error("Could not generate a commit message", exc)
551
- raise typer.Exit(1)
565
+ except llm.LLMError as exc:
566
+ print_llm_error("Could not generate a commit message", exc)
567
+ raise SystemExit(1)
552
568
 
553
569
 
554
570
  def request_split_commit_plan(
@@ -558,7 +574,8 @@ def request_split_commit_plan(
558
574
  preferred_commits: int | None = None,
559
575
  model: str | None = None,
560
576
  context: str = "",
561
- http_client_config: github_copilot.HttpClientConfig | None = None,
577
+ provider_config: providers.ProviderConfig | None = None,
578
+ http_client_config: llm.HttpClientConfig | None = None,
562
579
  ) -> SplitCommitPlan:
563
580
  """Request and validate a split-commit plan for the staged patch units."""
564
581
  planner_system_prompt = load_named_prompt(SPLIT_COMMIT_PLANNER_PROMPT_FILENAME)
@@ -573,16 +590,17 @@ def request_split_commit_plan(
573
590
  with console.status(
574
591
  "[yellow]Planning split commits from [bold]staged hunks[/] ...[/yellow]"
575
592
  ):
576
- response = ask_copilot_with_system_prompt(
593
+ response = ask_llm_with_system_prompt(
577
594
  planner_system_prompt,
578
595
  planner_prompt,
579
596
  model=model,
597
+ provider_config=provider_config,
580
598
  http_client_config=http_client_config,
581
599
  )
582
- except github_copilot.CopilotError as exc:
600
+ except llm.LLMError as exc:
583
601
  if not should_retry_with_compact_prompt(exc):
584
- print_copilot_error("Could not generate a split commit plan", exc)
585
- raise typer.Exit(1)
602
+ print_llm_error("Could not generate a split commit plan", exc)
603
+ raise SystemExit(1)
586
604
 
587
605
  console.print(
588
606
  "[yellow]Staged patch units exceeded the model context window; retrying split planning with summaries only.[/yellow]"
@@ -605,15 +623,16 @@ def request_split_commit_plan(
605
623
  with console.status(
606
624
  "[yellow]Planning split commits from [bold]patch summaries[/] ...[/yellow]"
607
625
  ):
608
- response = ask_copilot_with_system_prompt(
626
+ response = ask_llm_with_system_prompt(
609
627
  planner_system_prompt,
610
628
  compact_planner_prompt,
611
629
  model=model,
630
+ provider_config=provider_config,
612
631
  http_client_config=http_client_config,
613
632
  )
614
- except github_copilot.CopilotError as exc:
615
- print_copilot_error("Could not generate a split commit plan", exc)
616
- raise typer.Exit(1)
633
+ except llm.LLMError as exc:
634
+ print_llm_error("Could not generate a split commit plan", exc)
635
+ raise SystemExit(1)
617
636
 
618
637
  return parse_split_plan_response(
619
638
  response,
@@ -627,7 +646,8 @@ def request_split_commit_messages(
627
646
  *,
628
647
  model: str | None = None,
629
648
  context: str = "",
630
- http_client_config: github_copilot.HttpClientConfig | None = None,
649
+ provider_config: providers.ProviderConfig | None = None,
650
+ http_client_config: llm.HttpClientConfig | None = None,
631
651
  ) -> list[PreparedSplitCommit]:
632
652
  """Generate commit messages for each planned split-commit group."""
633
653
  try:
@@ -643,6 +663,7 @@ def request_split_commit_messages(
643
663
  build_status_for_patch_units(unit_group),
644
664
  model=model,
645
665
  context=context,
666
+ provider_config=provider_config,
646
667
  http_client_config=http_client_config,
647
668
  )
648
669
 
@@ -651,9 +672,9 @@ def request_split_commit_messages(
651
672
  )
652
673
 
653
674
  return prepared_commits
654
- except github_copilot.CopilotError as exc:
655
- print_copilot_error("Could not generate split commit messages", exc)
656
- raise typer.Exit(1)
675
+ except llm.LLMError as exc:
676
+ print_llm_error("Could not generate split commit messages", exc)
677
+ raise SystemExit(1)
657
678
 
658
679
 
659
680
  def confirm_split_commit_count(
@@ -683,7 +704,7 @@ def confirm_split_commit_count(
683
704
  return plan
684
705
 
685
706
  console.print("Split commit plan cancelled.")
686
- raise typer.Exit()
707
+ raise SystemExit()
687
708
 
688
709
 
689
710
  def display_commit_message(commit_message: str) -> None:
@@ -722,7 +743,7 @@ def execute_commit_action(
722
743
  if yes:
723
744
  return commit_with_retry_no_verify(repo, commit_message)
724
745
 
725
- choice = typer.prompt(
746
+ choice = Prompt.ask(
726
747
  "Choose action: (c)ommit, (e)dit message, (q)uit",
727
748
  default="c",
728
749
  show_default=True,
@@ -730,7 +751,7 @@ def execute_commit_action(
730
751
 
731
752
  if choice == "q":
732
753
  console.print("Commit cancelled.")
733
- raise typer.Exit()
754
+ raise SystemExit()
734
755
  if choice == "e":
735
756
  console.print("[cyan]Opening git editor...[/cyan]")
736
757
  return commit_with_retry_no_verify(repo, commit_message, use_editor=True)
@@ -738,7 +759,7 @@ def execute_commit_action(
738
759
  return commit_with_retry_no_verify(repo, commit_message)
739
760
 
740
761
  console.print("Invalid choice. Commit cancelled.")
741
- raise typer.Exit()
762
+ raise SystemExit()
742
763
 
743
764
 
744
765
  def execute_split_commit_plan(
@@ -750,7 +771,7 @@ def execute_split_commit_plan(
750
771
  """Run the split-commit plan against temporary alternate indexes."""
751
772
  use_editor = False
752
773
  if not yes:
753
- choice = typer.prompt(
774
+ choice = Prompt.ask(
754
775
  "Choose action: (c)ommit all, (e)dit each message, (q)uit",
755
776
  default="c",
756
777
  show_default=True,
@@ -758,12 +779,12 @@ def execute_split_commit_plan(
758
779
 
759
780
  if choice == "q":
760
781
  console.print("Commit cancelled.")
761
- raise typer.Exit()
782
+ raise SystemExit()
762
783
  if choice == "e":
763
784
  use_editor = True
764
785
  elif choice != "c":
765
786
  console.print("Invalid choice. Commit cancelled.")
766
- raise typer.Exit()
787
+ raise SystemExit()
767
788
 
768
789
  execution_state = SplitCommitExecutionState(
769
790
  original_head_sha=repo.get_head_sha() if repo.has_commit("HEAD") else None,
@@ -793,16 +814,19 @@ def execute_split_commit_plan(
793
814
  console.print(
794
815
  f"[red]Failed to apply the planned changes for commit {index}: {exc}[/red]"
795
816
  )
796
- raise typer.Exit(1)
797
-
798
- commit_shas.append(
799
- commit_with_retry_no_verify(
800
- repo,
801
- prepared_commit.message,
802
- use_editor=use_editor,
803
- env=alternate_index.env,
817
+ raise SystemExit(1)
818
+
819
+ try:
820
+ commit_shas.append(
821
+ repo.create_commit_from_index(
822
+ prepared_commit.message,
823
+ index=alternate_index,
824
+ use_editor=use_editor,
825
+ )
804
826
  )
805
- )
827
+ except GitError as exc:
828
+ console.print(f"[red]Failed to create commit {index}: {exc}[/red]")
829
+ raise SystemExit(1)
806
830
  except BaseException:
807
831
  try:
808
832
  if execution_state.original_head_sha is not None:
@@ -832,13 +856,15 @@ def handle_single_commit_flow(
832
856
  model: str | None = None,
833
857
  yes: bool = False,
834
858
  context: str = "",
835
- http_client_config: github_copilot.HttpClientConfig | None = None,
859
+ provider_config: providers.ProviderConfig | None = None,
860
+ http_client_config: llm.HttpClientConfig | None = None,
836
861
  ) -> None:
837
862
  """Generate, display, and execute the single-commit flow."""
838
863
  commit_message = request_commit_message(
839
864
  status,
840
865
  model=model,
841
866
  context=context,
867
+ provider_config=provider_config,
842
868
  http_client_config=http_client_config,
843
869
  )
844
870
  display_commit_message(commit_message)
@@ -855,7 +881,8 @@ def handle_split_commit_flow(
855
881
  model: str | None = None,
856
882
  yes: bool = False,
857
883
  context: str = "",
858
- http_client_config: github_copilot.HttpClientConfig | None = None,
884
+ provider_config: providers.ProviderConfig | None = None,
885
+ http_client_config: llm.HttpClientConfig | None = None,
859
886
  ) -> None:
860
887
  """Generate, display, and execute the split-commit flow."""
861
888
  patch_units = tuple(
@@ -872,6 +899,7 @@ def handle_split_commit_flow(
872
899
  model=model,
873
900
  yes=yes,
874
901
  context=context,
902
+ provider_config=provider_config,
875
903
  http_client_config=http_client_config,
876
904
  )
877
905
  return
@@ -886,6 +914,7 @@ def handle_split_commit_flow(
886
914
  model=model,
887
915
  yes=yes,
888
916
  context=context,
917
+ provider_config=provider_config,
889
918
  http_client_config=http_client_config,
890
919
  )
891
920
  return
@@ -907,6 +936,7 @@ def handle_split_commit_flow(
907
936
  preferred_commits=preferred_commits,
908
937
  model=model,
909
938
  context=context,
939
+ provider_config=provider_config,
910
940
  http_client_config=http_client_config,
911
941
  )
912
942
  except SplitPlanningError as exc:
@@ -920,6 +950,7 @@ def handle_split_commit_flow(
920
950
  model=model,
921
951
  yes=yes,
922
952
  context=context,
953
+ provider_config=provider_config,
923
954
  http_client_config=http_client_config,
924
955
  )
925
956
  return
@@ -936,6 +967,7 @@ def handle_split_commit_flow(
936
967
  patch_units,
937
968
  model=model,
938
969
  context=context,
970
+ provider_config=provider_config,
939
971
  http_client_config=http_client_config,
940
972
  )
941
973
  prepared_commits = order_prepared_split_commits(prepared_commits)
@@ -957,69 +989,94 @@ def handle_split_commit_flow(
957
989
  console.print(f"[green]{commit_sha[:8]}[/green] {prepared_commit.message}")
958
990
 
959
991
 
960
- @app.command("authenticate")
961
- @app.command("login", hidden=True)
992
+ @app.command(name="authenticate")
993
+ @app.command(name="login", show=False)
962
994
  def authenticate(
963
- enterprise_domain: str | None = typer.Option(
964
- None,
965
- "--enterprise-domain",
966
- help="GitHub Enterprise hostname. Omit for github.com.",
967
- ),
968
- force: bool = typer.Option(
969
- False, "--force", help="Replace cached GitHub Copilot credentials"
970
- ),
995
+ enterprise_domain: Annotated[
996
+ str | None,
997
+ cyclopts.Parameter(
998
+ name="--enterprise-domain",
999
+ help="GitHub Enterprise hostname. Omit for github.com.",
1000
+ ),
1001
+ ] = None,
1002
+ force: Annotated[
1003
+ bool,
1004
+ cyclopts.Parameter(
1005
+ name="--force",
1006
+ help="Replace cached GitHub Copilot credentials",
1007
+ ),
1008
+ ] = False,
971
1009
  ca_bundle: CaBundleOption = None,
972
1010
  insecure: InsecureOption = False,
973
1011
  native_tls: NativeTlsOption = False,
974
1012
  ):
975
1013
  """Authenticate with GitHub Copilot and cache credentials locally."""
1014
+ print_cli_banner()
976
1015
  http_client_config = build_http_client_config(
977
1016
  ca_bundle=ca_bundle,
978
1017
  insecure=insecure,
979
1018
  native_tls=native_tls,
980
1019
  )
981
1020
  try:
982
- github_copilot.login(
1021
+ copilot.login(
983
1022
  enterprise_domain=enterprise_domain,
984
1023
  force=force,
985
1024
  http_client_config=http_client_config,
986
1025
  )
987
- except github_copilot.CopilotError as exc:
988
- print_copilot_error("Authentication failed", exc)
989
- raise typer.Exit(1)
1026
+ except copilot.LLMError as exc:
1027
+ print_llm_error("Authentication failed", exc)
1028
+ raise SystemExit(1)
990
1029
 
991
1030
 
992
- @app.command("summary")
1031
+ @app.command(name="summary")
993
1032
  def summary(
1033
+ provider: ProviderOption = None,
1034
+ base_url: BaseUrlOption = None,
1035
+ api_key: ApiKeyOption = None,
994
1036
  ca_bundle: CaBundleOption = None,
995
1037
  insecure: InsecureOption = False,
996
1038
  native_tls: NativeTlsOption = False,
997
1039
  ):
998
- """Show the current cached GitHub Copilot login summary."""
1040
+ """Show the configured LLM provider summary."""
1041
+ print_cli_banner()
999
1042
  http_client_config = build_http_client_config(
1000
1043
  ca_bundle=ca_bundle,
1001
1044
  insecure=insecure,
1002
1045
  native_tls=native_tls,
1003
1046
  )
1004
1047
  try:
1005
- github_copilot.show_login_summary(http_client_config=http_client_config)
1006
- except github_copilot.CopilotError as exc:
1007
- print_copilot_error("Could not load login summary", exc)
1008
- raise typer.Exit(1)
1048
+ provider_config = providers.resolve_provider_config(
1049
+ provider=provider,
1050
+ base_url=base_url,
1051
+ api_key=api_key,
1052
+ )
1053
+ providers.show_summary(
1054
+ provider_config=provider_config,
1055
+ http_client_config=http_client_config,
1056
+ )
1057
+ except llm.LLMError as exc:
1058
+ print_llm_error("Could not load provider summary", exc)
1059
+ raise SystemExit(1)
1009
1060
 
1010
1061
 
1011
- @app.command("models")
1062
+ @app.command(name="models")
1012
1063
  def models_command(
1013
- vendor: str | None = typer.Option(
1014
- None,
1015
- "--vendor",
1016
- help="Filter listed models by vendor: anthropic, gemini/google, or openai.",
1017
- ),
1064
+ provider: ProviderOption = None,
1065
+ base_url: BaseUrlOption = None,
1066
+ api_key: ApiKeyOption = None,
1067
+ vendor: Annotated[
1068
+ str | None,
1069
+ cyclopts.Parameter(
1070
+ name="--vendor",
1071
+ help="Filter listed models by vendor: anthropic, gemini/google, or openai.",
1072
+ ),
1073
+ ] = None,
1018
1074
  ca_bundle: CaBundleOption = None,
1019
1075
  insecure: InsecureOption = False,
1020
1076
  native_tls: NativeTlsOption = False,
1021
1077
  ):
1022
- """List available Copilot models for the current account."""
1078
+ """List available models for the configured LLM provider."""
1079
+ print_cli_banner()
1023
1080
  http_client_config = build_http_client_config(
1024
1081
  ca_bundle=ca_bundle,
1025
1082
  insecure=insecure,
@@ -1027,42 +1084,64 @@ def models_command(
1027
1084
  )
1028
1085
 
1029
1086
  try:
1030
- credentials, models = github_copilot.get_available_models(
1087
+ provider_config = providers.resolve_provider_config(
1088
+ provider=provider,
1089
+ base_url=base_url,
1090
+ api_key=api_key,
1091
+ )
1092
+ inventory = providers.get_available_models(
1093
+ provider_config=provider_config,
1031
1094
  vendor=vendor,
1032
1095
  http_client_config=http_client_config,
1033
1096
  )
1034
1097
 
1035
- console.print(f"[green]Copilot base URL:[/green] {credentials.base_url()}")
1036
- console.print(f"[green]Model count:[/green] {len(models)}")
1037
- github_copilot.print_model_table(models)
1038
- except github_copilot.CopilotError as exc:
1039
- print_copilot_error("Could not load models", exc)
1040
- raise typer.Exit(1)
1098
+ console.print(f"[green]LLM provider:[/green] {provider_config.display_name}")
1099
+ console.print(f"[green]Base URL:[/green] {inventory.base_url}")
1100
+ console.print(f"[green]Model count:[/green] {len(inventory.models)}")
1101
+ llm.print_model_table(
1102
+ inventory.models,
1103
+ title=f"Available {provider_config.display_name} Models",
1104
+ )
1105
+ except llm.LLMError as exc:
1106
+ print_llm_error("Could not load models", exc)
1107
+ raise SystemExit(1)
1041
1108
 
1042
1109
 
1043
- @app.command()
1110
+ @app.command
1044
1111
  def commit(
1045
- all_files: bool = typer.Option(
1046
- False, "--all", "-a", help="Stage all files before committing"
1047
- ),
1112
+ all_files: Annotated[
1113
+ bool,
1114
+ cyclopts.Parameter(
1115
+ name=("--all", "-a"),
1116
+ help="Stage all files before committing",
1117
+ ),
1118
+ ] = False,
1048
1119
  split: SplitOption = False,
1049
1120
  split_count: SplitCountOption = None,
1050
- model: str | None = typer.Option(
1051
- None,
1052
- "--model",
1053
- "-m",
1054
- metavar="MODEL_ID",
1055
- help="Model to use for generating commit message",
1056
- ),
1057
- yes: bool = typer.Option(
1058
- False, "--yes", "-y", help="Automatically accept the generated commit message"
1059
- ),
1060
- context: str = typer.Option(
1061
- "",
1062
- "--context",
1063
- "-c",
1064
- help="Optional user-provided context to guide commit message",
1065
- ),
1121
+ model: Annotated[
1122
+ str | None,
1123
+ cyclopts.Parameter(
1124
+ name=("--model", "-m"),
1125
+ help="Model to use for generating commit message",
1126
+ ),
1127
+ ] = None,
1128
+ yes: Annotated[
1129
+ bool,
1130
+ cyclopts.Parameter(
1131
+ name=("--yes", "-y"),
1132
+ help="Automatically accept the generated commit message",
1133
+ ),
1134
+ ] = False,
1135
+ context: Annotated[
1136
+ str,
1137
+ cyclopts.Parameter(
1138
+ name=("--context", "-c"),
1139
+ help="Optional user-provided context to guide commit message",
1140
+ ),
1141
+ ] = "",
1142
+ provider: ProviderOption = None,
1143
+ base_url: BaseUrlOption = None,
1144
+ api_key: ApiKeyOption = None,
1066
1145
  ca_bundle: CaBundleOption = None,
1067
1146
  insecure: InsecureOption = False,
1068
1147
  native_tls: NativeTlsOption = False,
@@ -1070,25 +1149,37 @@ def commit(
1070
1149
  """
1071
1150
  Generate commit message based on changes in the current git repository and commit them.
1072
1151
  """
1152
+ print_cli_banner()
1073
1153
  try:
1074
1154
  repo = GitRepository()
1075
1155
  except NotAGitRepositoryError:
1076
1156
  console.print("[red]Error: Not in a git repository[/red]")
1077
- raise typer.Exit(1)
1157
+ raise SystemExit(1)
1078
1158
 
1079
1159
  http_client_config = build_http_client_config(
1080
1160
  ca_bundle=ca_bundle,
1081
1161
  insecure=insecure,
1082
1162
  native_tls=native_tls,
1083
1163
  )
1084
- ensure_copilot_authentication(http_client_config)
1164
+ try:
1165
+ provider_config = providers.resolve_provider_config(
1166
+ provider=provider,
1167
+ base_url=base_url,
1168
+ api_key=api_key,
1169
+ )
1170
+ except llm.LLMError as exc:
1171
+ print_llm_error("Could not resolve the LLM provider", exc)
1172
+ raise SystemExit(1)
1173
+
1174
+ if provider_config.provider == "copilot":
1175
+ ensure_copilot_authentication(http_client_config)
1085
1176
 
1086
1177
  # Get initial status
1087
1178
  status = repo.get_status()
1088
1179
 
1089
1180
  if not status.files:
1090
1181
  console.print("[yellow]No changes to commit.[/yellow]")
1091
- raise typer.Exit()
1182
+ raise SystemExit()
1092
1183
 
1093
1184
  status = stage_changes_for_commit(repo, status, all_files=all_files)
1094
1185
 
@@ -1099,13 +1190,14 @@ def commit(
1099
1190
 
1100
1191
  normalized_model = normalize_model_name(model)
1101
1192
  try:
1102
- selected_model = github_copilot.ensure_auth_ready(
1193
+ selected_model = providers.ensure_model_ready(
1194
+ provider_config=provider_config,
1103
1195
  model=normalized_model,
1104
1196
  http_client_config=http_client_config,
1105
1197
  )
1106
- except github_copilot.CopilotError as exc:
1107
- print_copilot_error("Could not select a model", exc)
1108
- raise typer.Exit(1)
1198
+ except llm.LLMError as exc:
1199
+ print_llm_error("Could not select a model", exc)
1200
+ raise SystemExit(1)
1109
1201
 
1110
1202
  display_selected_model(selected_model)
1111
1203
  model = selected_model.id
@@ -1118,6 +1210,7 @@ def commit(
1118
1210
  model=model,
1119
1211
  yes=yes,
1120
1212
  context=context,
1213
+ provider_config=provider_config,
1121
1214
  http_client_config=http_client_config,
1122
1215
  )
1123
1216
  return
@@ -1128,6 +1221,7 @@ def commit(
1128
1221
  model=model,
1129
1222
  yes=yes,
1130
1223
  context=context,
1224
+ provider_config=provider_config,
1131
1225
  http_client_config=http_client_config,
1132
1226
  )
1133
1227