gac 3.6.0__py3-none-any.whl → 3.8.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.
Files changed (47) hide show
  1. gac/__version__.py +1 -1
  2. gac/ai_utils.py +47 -0
  3. gac/auth_cli.py +181 -36
  4. gac/cli.py +13 -0
  5. gac/config.py +54 -0
  6. gac/constants.py +7 -0
  7. gac/main.py +53 -11
  8. gac/model_cli.py +65 -10
  9. gac/oauth/__init__.py +26 -0
  10. gac/oauth/claude_code.py +87 -20
  11. gac/oauth/qwen_oauth.py +323 -0
  12. gac/oauth/token_store.py +81 -0
  13. gac/prompt.py +16 -4
  14. gac/providers/__init__.py +3 -0
  15. gac/providers/anthropic.py +11 -1
  16. gac/providers/azure_openai.py +5 -1
  17. gac/providers/cerebras.py +11 -1
  18. gac/providers/chutes.py +11 -1
  19. gac/providers/claude_code.py +11 -1
  20. gac/providers/custom_anthropic.py +5 -1
  21. gac/providers/custom_openai.py +5 -1
  22. gac/providers/deepseek.py +11 -1
  23. gac/providers/fireworks.py +11 -1
  24. gac/providers/gemini.py +11 -1
  25. gac/providers/groq.py +5 -1
  26. gac/providers/kimi_coding.py +5 -1
  27. gac/providers/lmstudio.py +12 -1
  28. gac/providers/minimax.py +11 -1
  29. gac/providers/mistral.py +11 -1
  30. gac/providers/moonshot.py +11 -1
  31. gac/providers/ollama.py +11 -1
  32. gac/providers/openai.py +11 -1
  33. gac/providers/openrouter.py +11 -1
  34. gac/providers/qwen.py +76 -0
  35. gac/providers/replicate.py +14 -2
  36. gac/providers/streamlake.py +11 -1
  37. gac/providers/synthetic.py +11 -1
  38. gac/providers/together.py +11 -1
  39. gac/providers/zai.py +11 -1
  40. gac/utils.py +30 -1
  41. gac/workflow_utils.py +3 -8
  42. {gac-3.6.0.dist-info → gac-3.8.1.dist-info}/METADATA +6 -4
  43. gac-3.8.1.dist-info/RECORD +56 -0
  44. gac-3.6.0.dist-info/RECORD +0 -53
  45. {gac-3.6.0.dist-info → gac-3.8.1.dist-info}/WHEEL +0 -0
  46. {gac-3.6.0.dist-info → gac-3.8.1.dist-info}/entry_points.txt +0 -0
  47. {gac-3.6.0.dist-info → gac-3.8.1.dist-info}/licenses/LICENSE +0 -0
gac/__version__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  """Version information for gac package."""
2
2
 
3
- __version__ = "3.6.0"
3
+ __version__ = "3.8.1"
gac/ai_utils.py CHANGED
@@ -124,6 +124,53 @@ def generate_with_retries(
124
124
  if not messages:
125
125
  raise AIError.model_error("No messages provided for AI generation")
126
126
 
127
+ # Load Claude Code token from TokenStore if needed
128
+ if provider == "claude-code":
129
+ from gac.oauth import refresh_token_if_expired
130
+ from gac.oauth.token_store import TokenStore
131
+
132
+ # Check token expiry and refresh if needed
133
+ if not refresh_token_if_expired(quiet=True):
134
+ raise AIError.authentication_error(
135
+ "Claude Code token not found or expired. Please authenticate with 'gac auth claude-code login'."
136
+ )
137
+
138
+ # Load the (possibly refreshed) token
139
+ token_store = TokenStore()
140
+ token_data = token_store.get_token("claude-code")
141
+ if token_data and "access_token" in token_data:
142
+ os.environ["CLAUDE_CODE_ACCESS_TOKEN"] = token_data["access_token"]
143
+ else:
144
+ raise AIError.authentication_error(
145
+ "Claude Code token not found. Please authenticate with 'gac auth claude-code login'."
146
+ )
147
+
148
+ # Check Qwen OAuth token expiry and refresh if needed
149
+ if provider == "qwen":
150
+ from gac.oauth import QwenOAuthProvider, TokenStore
151
+
152
+ oauth_provider = QwenOAuthProvider(TokenStore())
153
+ token = oauth_provider.get_token()
154
+ if not token:
155
+ if not quiet:
156
+ console.print("[yellow]⚠ Qwen authentication not found or expired[/yellow]")
157
+ console.print("[cyan]🔐 Starting automatic authentication...[/cyan]")
158
+ try:
159
+ oauth_provider.initiate_auth(open_browser=True)
160
+ token = oauth_provider.get_token()
161
+ if not token:
162
+ raise AIError.authentication_error(
163
+ "Qwen authentication failed. Run 'gac auth qwen login' to authenticate manually."
164
+ )
165
+ if not quiet:
166
+ console.print("[green]✓ Authentication successful![/green]\n")
167
+ except AIError:
168
+ raise
169
+ except Exception as e:
170
+ raise AIError.authentication_error(
171
+ f"Qwen authentication failed: {e}. Run 'gac auth qwen login' to authenticate manually."
172
+ ) from e
173
+
127
174
  # Set up spinner
128
175
  if is_group:
129
176
  message_type = f"grouped {task_description}s"
gac/auth_cli.py CHANGED
@@ -1,54 +1,95 @@
1
- """CLI for authenticating Claude Code OAuth tokens.
1
+ """CLI for OAuth authentication with various providers.
2
2
 
3
- Provides a command to authenticate and re-authenticate Claude Code subscriptions.
3
+ Provides commands to authenticate and manage OAuth tokens for supported providers.
4
4
  """
5
5
 
6
6
  import logging
7
7
 
8
8
  import click
9
9
 
10
- from gac.oauth.claude_code import authenticate_and_save, load_stored_token
10
+ from gac.oauth import (
11
+ QwenOAuthProvider,
12
+ TokenStore,
13
+ authenticate_and_save,
14
+ remove_token,
15
+ )
11
16
  from gac.utils import setup_logging
12
17
 
13
18
  logger = logging.getLogger(__name__)
14
19
 
15
20
 
16
- @click.command()
17
- @click.option(
18
- "--quiet",
19
- "-q",
20
- is_flag=True,
21
- help="Suppress non-error output",
22
- )
23
- @click.option(
24
- "--log-level",
25
- default="INFO",
26
- type=click.Choice(["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"], case_sensitive=False),
27
- help="Set log level (default: INFO)",
28
- )
29
- def auth(quiet: bool = False, log_level: str = "INFO") -> None:
30
- """Authenticate Claude Code OAuth token.
21
+ @click.group(invoke_without_command=True)
22
+ @click.pass_context
23
+ def auth(ctx: click.Context) -> None:
24
+ """Manage OAuth authentication for AI providers.
31
25
 
32
- This command allows you to authenticate or re-authenticate your
33
- Claude Code OAuth token when it expires or you want to refresh it.
34
- It opens a browser window for the OAuth flow and saves the token
35
- to ~/.gac.env.
26
+ Supports authentication for:
27
+ - claude-code: Claude Code subscription OAuth
28
+ - qwen: Qwen AI OAuth (device flow)
36
29
 
37
- The token is used by the Claude Code provider to access your
38
- Claude Code subscription instead of requiring an Anthropic API key.
30
+ Examples:
31
+ gac auth # Show authentication status
32
+ gac auth claude-code login # Login to Claude Code
33
+ gac auth claude-code logout # Logout from Claude Code
34
+ gac auth claude-code status # Check Claude Code auth status
35
+ gac auth qwen login # Login to Qwen
36
+ gac auth qwen logout # Logout from Qwen
37
+ gac auth qwen status # Check Qwen auth status
39
38
  """
40
- # Setup logging
41
- if quiet:
42
- effective_log_level = "ERROR"
39
+ if ctx.invoked_subcommand is None:
40
+ _show_auth_status()
41
+
42
+
43
+ def _show_auth_status() -> None:
44
+ """Show authentication status for all providers."""
45
+ click.echo("OAuth Authentication Status")
46
+ click.echo("-" * 40)
47
+
48
+ token_store = TokenStore()
49
+
50
+ claude_token = token_store.get_token("claude-code")
51
+ if claude_token:
52
+ click.echo("Claude Code: ✓ Authenticated")
43
53
  else:
44
- effective_log_level = log_level
45
- setup_logging(effective_log_level)
54
+ click.echo("Claude Code: ✗ Not authenticated")
55
+ click.echo(" Run 'gac auth claude-code login' to login")
56
+
57
+ qwen_token = token_store.get_token("qwen")
58
+ if qwen_token:
59
+ click.echo("Qwen: ✓ Authenticated")
60
+ else:
61
+ click.echo("Qwen: ✗ Not authenticated")
62
+ click.echo(" Run 'gac auth qwen login' to login")
63
+
64
+
65
+ # Claude Code commands
66
+ @auth.group("claude-code")
67
+ def claude_code() -> None:
68
+ """Manage Claude Code OAuth authentication.
69
+
70
+ Use browser-based authentication to log in to Claude Code.
71
+ """
72
+ pass
46
73
 
47
- # Check if there's an existing token
48
- existing_token = load_stored_token()
49
- if existing_token and not quiet:
50
- click.echo("✓ Found existing Claude Code access token.")
51
- click.echo()
74
+
75
+ @claude_code.command("login")
76
+ @click.option("--quiet", "-q", is_flag=True, help="Suppress non-error output")
77
+ def claude_code_login(quiet: bool = False) -> None:
78
+ """Login to Claude Code using OAuth.
79
+
80
+ Opens a browser to authenticate with Claude Code. The token is stored
81
+ securely in ~/.gac/oauth/claude-code.json.
82
+ """
83
+ if not quiet:
84
+ setup_logging("INFO")
85
+
86
+ token_store = TokenStore()
87
+ existing_token = token_store.get_token("claude-code")
88
+ if existing_token:
89
+ if not quiet:
90
+ click.echo("✓ Already authenticated with Claude Code.")
91
+ if not click.confirm("Re-authenticate?"):
92
+ return
52
93
 
53
94
  if not quiet:
54
95
  click.echo("🔐 Starting Claude Code OAuth authentication...")
@@ -56,14 +97,118 @@ def auth(quiet: bool = False, log_level: str = "INFO") -> None:
56
97
  click.echo(" (Waiting up to 3 minutes for callback)")
57
98
  click.echo()
58
99
 
59
- # Perform OAuth authentication
60
100
  success = authenticate_and_save(quiet=quiet)
61
101
 
62
102
  if success:
63
103
  if not quiet:
104
+ click.echo()
64
105
  click.echo("✅ Claude Code authentication completed successfully!")
65
- click.echo(" Your new token has been saved and is ready to use.")
66
106
  else:
67
107
  click.echo("❌ Claude Code authentication failed.")
68
108
  click.echo(" Please try again or check your network connection.")
69
109
  raise click.ClickException("Claude Code authentication failed")
110
+
111
+
112
+ @claude_code.command("logout")
113
+ @click.option("--quiet", "-q", is_flag=True, help="Suppress non-error output")
114
+ def claude_code_logout(quiet: bool = False) -> None:
115
+ """Logout from Claude Code and remove stored tokens."""
116
+ token_store = TokenStore()
117
+ existing_token = token_store.get_token("claude-code")
118
+
119
+ if not existing_token:
120
+ if not quiet:
121
+ click.echo("Not currently authenticated with Claude Code.")
122
+ return
123
+
124
+ try:
125
+ remove_token()
126
+ if not quiet:
127
+ click.echo("✅ Successfully logged out from Claude Code.")
128
+ except Exception as e:
129
+ click.echo("❌ Failed to remove Claude Code token.")
130
+ raise click.ClickException("Claude Code logout failed") from e
131
+
132
+
133
+ @claude_code.command("status")
134
+ def claude_code_status() -> None:
135
+ """Check Claude Code authentication status."""
136
+ token_store = TokenStore()
137
+ token = token_store.get_token("claude-code")
138
+
139
+ if token:
140
+ click.echo("Claude Code Authentication Status: ✓ Authenticated")
141
+ else:
142
+ click.echo("Claude Code Authentication Status: ✗ Not authenticated")
143
+ click.echo("Run 'gac auth claude-code login' to authenticate.")
144
+
145
+
146
+ # Qwen commands
147
+ @auth.group()
148
+ def qwen() -> None:
149
+ """Manage Qwen OAuth authentication.
150
+
151
+ Use device flow authentication to log in to Qwen AI.
152
+ """
153
+ pass
154
+
155
+
156
+ @qwen.command("login")
157
+ @click.option("--no-browser", is_flag=True, help="Don't automatically open browser")
158
+ @click.option("--quiet", "-q", is_flag=True, help="Suppress non-error output")
159
+ def qwen_login(no_browser: bool = False, quiet: bool = False) -> None:
160
+ """Login to Qwen using OAuth device flow.
161
+
162
+ Opens a browser to authenticate with Qwen. The token is stored
163
+ securely in ~/.gac/oauth/qwen.json.
164
+ """
165
+ if not quiet:
166
+ setup_logging("INFO")
167
+
168
+ provider = QwenOAuthProvider()
169
+
170
+ if provider.is_authenticated():
171
+ if not quiet:
172
+ click.echo("✓ Already authenticated with Qwen.")
173
+ if not click.confirm("Re-authenticate?"):
174
+ return
175
+
176
+ try:
177
+ provider.initiate_auth(open_browser=not no_browser)
178
+ if not quiet:
179
+ click.echo()
180
+ click.echo("✅ Qwen authentication completed successfully!")
181
+ except Exception as e:
182
+ click.echo(f"❌ Qwen authentication failed: {e}")
183
+ raise click.ClickException("Qwen authentication failed") from None
184
+
185
+
186
+ @qwen.command("logout")
187
+ @click.option("--quiet", "-q", is_flag=True, help="Suppress non-error output")
188
+ def qwen_logout(quiet: bool = False) -> None:
189
+ """Logout from Qwen and remove stored tokens."""
190
+ provider = QwenOAuthProvider()
191
+
192
+ if not provider.is_authenticated():
193
+ if not quiet:
194
+ click.echo("Not currently authenticated with Qwen.")
195
+ return
196
+
197
+ provider.logout()
198
+ if not quiet:
199
+ click.echo("✅ Successfully logged out from Qwen.")
200
+
201
+
202
+ @qwen.command("status")
203
+ def qwen_status() -> None:
204
+ """Check Qwen authentication status."""
205
+ provider = QwenOAuthProvider()
206
+ token = provider.get_token()
207
+
208
+ if token:
209
+ click.echo("Qwen Authentication Status: ✓ Authenticated")
210
+ if token.get("resource_url"):
211
+ click.echo(f"API Endpoint: {token['resource_url']}")
212
+ else:
213
+ click.echo("Qwen Authentication Status: ✗ Not authenticated")
214
+ click.echo("Run 'gac auth qwen login' to authenticate.")
gac/cli.py CHANGED
@@ -6,6 +6,7 @@ Defines the Click-based command-line interface and delegates execution to the ma
6
6
  """
7
7
 
8
8
  import logging
9
+ import os
9
10
  import sys
10
11
 
11
12
  import click
@@ -73,6 +74,11 @@ console = Console()
73
74
  # Advanced options
74
75
  @click.option("--no-verify", is_flag=True, help="Skip pre-commit and lefthook hooks when committing")
75
76
  @click.option("--skip-secret-scan", is_flag=True, help="Skip security scan for secrets in staged changes")
77
+ @click.option(
78
+ "--no-verify-ssl",
79
+ is_flag=True,
80
+ help="Skip SSL certificate verification (useful for corporate proxies)",
81
+ )
76
82
  @click.option(
77
83
  "--hook-timeout",
78
84
  type=int,
@@ -103,6 +109,7 @@ def cli(
103
109
  verbose: bool = False,
104
110
  no_verify: bool = False,
105
111
  skip_secret_scan: bool = False,
112
+ no_verify_ssl: bool = False,
106
113
  hook_timeout: int = 0,
107
114
  ) -> None:
108
115
  """Git Auto Commit - Generate commit messages with AI."""
@@ -116,6 +123,11 @@ def cli(
116
123
  setup_logging(effective_log_level)
117
124
  logger.info("Starting gac")
118
125
 
126
+ # Set SSL verification environment variable if flag is used or config is set
127
+ if no_verify_ssl or config.get("no_verify_ssl", False):
128
+ os.environ["GAC_NO_VERIFY_SSL"] = "true"
129
+ logger.info("SSL certificate verification disabled")
130
+
119
131
  # Validate incompatible flag combinations
120
132
  if message_only and group:
121
133
  console.print("[red]Error: --message-only and --group options are mutually exclusive[/red]")
@@ -179,6 +191,7 @@ def cli(
179
191
  "verbose": verbose,
180
192
  "no_verify": no_verify,
181
193
  "skip_secret_scan": skip_secret_scan,
194
+ "no_verify_ssl": no_verify_ssl,
182
195
  "hook_timeout": hook_timeout,
183
196
  }
184
197
 
gac/config.py CHANGED
@@ -9,6 +9,57 @@ from pathlib import Path
9
9
  from dotenv import load_dotenv
10
10
 
11
11
  from gac.constants import EnvDefaults, Logging
12
+ from gac.errors import ConfigError
13
+
14
+
15
+ def validate_config(config: dict[str, str | int | float | bool | None]) -> None:
16
+ """Validate configuration values at load time.
17
+
18
+ Args:
19
+ config: Configuration dictionary to validate
20
+
21
+ Raises:
22
+ ConfigError: If any configuration value is invalid
23
+ """
24
+ # Validate temperature (0.0 to 2.0)
25
+ if config.get("temperature") is not None:
26
+ temp = config["temperature"]
27
+ if not isinstance(temp, (int, float)):
28
+ raise ConfigError(f"temperature must be a number, got {type(temp).__name__}")
29
+ if not 0.0 <= temp <= 2.0:
30
+ raise ConfigError(f"temperature must be between 0.0 and 2.0, got {temp}")
31
+
32
+ # Validate max_output_tokens (1 to 100000)
33
+ if config.get("max_output_tokens") is not None:
34
+ tokens = config["max_output_tokens"]
35
+ if not isinstance(tokens, int):
36
+ raise ConfigError(f"max_output_tokens must be an integer, got {type(tokens).__name__}")
37
+ if tokens < 1 or tokens > 100000:
38
+ raise ConfigError(f"max_output_tokens must be between 1 and 100000, got {tokens}")
39
+
40
+ # Validate max_retries (1 to 10)
41
+ if config.get("max_retries") is not None:
42
+ retries = config["max_retries"]
43
+ if not isinstance(retries, int):
44
+ raise ConfigError(f"max_retries must be an integer, got {type(retries).__name__}")
45
+ if retries < 1 or retries > 10:
46
+ raise ConfigError(f"max_retries must be between 1 and 10, got {retries}")
47
+
48
+ # Validate warning_limit_tokens (must be positive)
49
+ if config.get("warning_limit_tokens") is not None:
50
+ warning_limit = config["warning_limit_tokens"]
51
+ if not isinstance(warning_limit, int):
52
+ raise ConfigError(f"warning_limit_tokens must be an integer, got {type(warning_limit).__name__}")
53
+ if warning_limit < 1:
54
+ raise ConfigError(f"warning_limit_tokens must be positive, got {warning_limit}")
55
+
56
+ # Validate hook_timeout (must be positive)
57
+ if config.get("hook_timeout") is not None:
58
+ hook_timeout = config["hook_timeout"]
59
+ if not isinstance(hook_timeout, int):
60
+ raise ConfigError(f"hook_timeout must be an integer, got {type(hook_timeout).__name__}")
61
+ if hook_timeout < 1:
62
+ raise ConfigError(f"hook_timeout must be positive, got {hook_timeout}")
12
63
 
13
64
 
14
65
  def load_config() -> dict[str, str | int | float | bool | None]:
@@ -35,6 +86,8 @@ def load_config() -> dict[str, str | int | float | bool | None]:
35
86
  "skip_secret_scan": os.getenv("GAC_SKIP_SECRET_SCAN", str(EnvDefaults.SKIP_SECRET_SCAN)).lower()
36
87
  in ("true", "1", "yes", "on"),
37
88
  "no_tiktoken": os.getenv("GAC_NO_TIKTOKEN", str(EnvDefaults.NO_TIKTOKEN)).lower() in ("true", "1", "yes", "on"),
89
+ "no_verify_ssl": os.getenv("GAC_NO_VERIFY_SSL", str(EnvDefaults.NO_VERIFY_SSL)).lower()
90
+ in ("true", "1", "yes", "on"),
38
91
  "verbose": os.getenv("GAC_VERBOSE", str(EnvDefaults.VERBOSE)).lower() in ("true", "1", "yes", "on"),
39
92
  "system_prompt_path": os.getenv("GAC_SYSTEM_PROMPT_PATH"),
40
93
  "language": os.getenv("GAC_LANGUAGE"),
@@ -43,4 +96,5 @@ def load_config() -> dict[str, str | int | float | bool | None]:
43
96
  "hook_timeout": int(os.getenv("GAC_HOOK_TIMEOUT", EnvDefaults.HOOK_TIMEOUT)),
44
97
  }
45
98
 
99
+ validate_config(config)
46
100
  return config
gac/constants.py CHANGED
@@ -26,9 +26,16 @@ class EnvDefaults:
26
26
  SKIP_SECRET_SCAN: bool = False
27
27
  VERBOSE: bool = False
28
28
  NO_TIKTOKEN: bool = False
29
+ NO_VERIFY_SSL: bool = False # Skip SSL certificate verification (for corporate proxies)
29
30
  HOOK_TIMEOUT: int = 120 # Timeout for pre-commit and lefthook hooks in seconds
30
31
 
31
32
 
33
+ class ProviderDefaults:
34
+ """Default values for provider configurations."""
35
+
36
+ HTTP_TIMEOUT: int = 120 # seconds - timeout for HTTP requests to LLM providers
37
+
38
+
32
39
  class Logging:
33
40
  """Logging configuration constants."""
34
41
 
gac/main.py CHANGED
@@ -14,7 +14,7 @@ from gac.ai import generate_commit_message
14
14
  from gac.ai_utils import count_tokens
15
15
  from gac.config import load_config
16
16
  from gac.constants import EnvDefaults, Utility
17
- from gac.errors import AIError, GitError, handle_error
17
+ from gac.errors import AIError, ConfigError, GitError, handle_error
18
18
  from gac.git import (
19
19
  detect_rename_mappings,
20
20
  get_staged_files,
@@ -233,7 +233,8 @@ def execute_grouped_commits_workflow(
233
233
 
234
234
  if first_iteration:
235
235
  warning_limit_val = config.get("warning_limit_tokens", EnvDefaults.WARNING_LIMIT_TOKENS)
236
- assert warning_limit_val is not None
236
+ if warning_limit_val is None:
237
+ raise ConfigError("warning_limit_tokens configuration missing")
237
238
  warning_limit = int(warning_limit_val)
238
239
  if not check_token_warning(prompt_tokens, warning_limit, require_confirmation):
239
240
  sys.exit(0)
@@ -343,7 +344,7 @@ def execute_grouped_commits_workflow(
343
344
  files = commit["files"]
344
345
  files_display = ", ".join(files)
345
346
  console.print(f"[dim]{files_display}[/dim]")
346
- commit_msg = commit["message"]
347
+ commit_msg = commit["message"].strip()
347
348
  console.print(Panel(commit_msg, title=f"Commit Message {idx}/{num_commits}", border_style="cyan"))
348
349
  console.print()
349
350
 
@@ -396,7 +397,7 @@ def execute_grouped_commits_workflow(
396
397
  for idx, commit in enumerate(grouped_result["commits"], 1):
397
398
  console.print(f"\n[cyan]Commit {idx}/{num_commits}:[/cyan]")
398
399
  console.print(f" Files: {', '.join(commit['files'])}")
399
- console.print(f" Message: {commit['message'][:50]}...")
400
+ console.print(f" Message: {commit['message'].strip()[:50]}...")
400
401
  else:
401
402
  original_staged_files = get_staged_files(existing_only=False)
402
403
  original_staged_diff = run_git_command(["diff", "--cached", "--binary"], silent=True)
@@ -418,7 +419,7 @@ def execute_grouped_commits_workflow(
418
419
  run_git_command(["add", "-A", file_path])
419
420
  else:
420
421
  run_git_command(["add", "-A", file_path])
421
- execute_commit(commit["message"], no_verify, hook_timeout)
422
+ execute_commit(commit["message"].strip(), no_verify, hook_timeout)
422
423
  console.print(f"[green]✓ Commit {idx}/{num_commits} created[/green]")
423
424
  except Exception as e:
424
425
  console.print(f"[red]✗ Failed at commit {idx}/{num_commits}: {e}[/red]")
@@ -550,7 +551,8 @@ def execute_single_commit_workflow(
550
551
  prompt_tokens = count_tokens(conversation_messages, model)
551
552
  if first_iteration:
552
553
  warning_limit_val = config.get("warning_limit_tokens", EnvDefaults.WARNING_LIMIT_TOKENS)
553
- assert warning_limit_val is not None
554
+ if warning_limit_val is None:
555
+ raise ConfigError("warning_limit_tokens configuration missing")
554
556
  warning_limit = int(warning_limit_val)
555
557
  if not check_token_warning(prompt_tokens, warning_limit, require_confirmation):
556
558
  sys.exit(0)
@@ -773,15 +775,18 @@ def main(
773
775
  model = str(model_from_config)
774
776
 
775
777
  temperature_val = config["temperature"]
776
- assert temperature_val is not None
778
+ if temperature_val is None:
779
+ raise ConfigError("temperature configuration missing")
777
780
  temperature = float(temperature_val)
778
781
 
779
782
  max_tokens_val = config["max_output_tokens"]
780
- assert max_tokens_val is not None
783
+ if max_tokens_val is None:
784
+ raise ConfigError("max_output_tokens configuration missing")
781
785
  max_output_tokens = int(max_tokens_val)
782
786
 
783
787
  max_retries_val = config["max_retries"]
784
- assert max_retries_val is not None
788
+ if max_retries_val is None:
789
+ raise ConfigError("max_retries configuration missing")
785
790
  max_retries = int(max_retries_val)
786
791
 
787
792
  if stage_all and (not dry_run):
@@ -882,7 +887,8 @@ def main(
882
887
  logger.info("No secrets detected in staged changes")
883
888
 
884
889
  logger.debug(f"Preprocessing diff ({len(diff)} characters)")
885
- assert model is not None
890
+ if model is None:
891
+ raise ConfigError("Model must be specified via GAC_MODEL environment variable or --model flag")
886
892
  processed_diff = preprocess_diff(diff, token_limit=Utility.DEFAULT_DIFF_TOKEN_LIMIT, model=model)
887
893
  logger.debug(f"Processed diff ({len(processed_diff)} characters)")
888
894
 
@@ -1012,8 +1018,44 @@ def main(
1012
1018
  console.print(f"[red]Re-authentication error: {auth_error}[/red]")
1013
1019
  console.print("[yellow]Run 'gac model' to re-authenticate manually.[/yellow]")
1014
1020
  sys.exit(1)
1021
+ # Check if this is a Qwen OAuth token expiration
1022
+ elif e.error_type == "authentication" and model.startswith("qwen:"):
1023
+ logger.error(str(e))
1024
+ console.print("[yellow]⚠ Qwen authentication failed[/yellow]")
1025
+ console.print("[cyan]🔐 Starting automatic re-authentication...[/cyan]")
1026
+
1027
+ try:
1028
+ from gac.oauth import QwenOAuthProvider, TokenStore
1029
+
1030
+ oauth_provider = QwenOAuthProvider(TokenStore())
1031
+ oauth_provider.initiate_auth(open_browser=True)
1032
+ console.print("[green]✓ Re-authentication successful![/green]")
1033
+ console.print("[cyan]Retrying commit...[/cyan]\n")
1034
+
1035
+ # Retry the commit workflow
1036
+ execute_single_commit_workflow(
1037
+ system_prompt=system_prompt,
1038
+ user_prompt=user_prompt,
1039
+ model=model,
1040
+ temperature=temperature,
1041
+ max_output_tokens=max_output_tokens,
1042
+ max_retries=max_retries,
1043
+ require_confirmation=require_confirmation,
1044
+ quiet=quiet,
1045
+ no_verify=no_verify,
1046
+ dry_run=dry_run,
1047
+ message_only=message_only,
1048
+ push=push,
1049
+ show_prompt=show_prompt,
1050
+ hook_timeout=hook_timeout,
1051
+ interactive=interactive,
1052
+ )
1053
+ except Exception as auth_error:
1054
+ console.print(f"[red]Re-authentication error: {auth_error}[/red]")
1055
+ console.print("[yellow]Run 'gac auth qwen login' to re-authenticate manually.[/yellow]")
1056
+ sys.exit(1)
1015
1057
  else:
1016
- # Non-Claude Code error or non-auth error
1058
+ # Non-Claude Code/Qwen error or non-auth error
1017
1059
  logger.error(str(e))
1018
1060
  console.print(f"[red]Failed to generate commit message: {str(e)}[/red]")
1019
1061
  sys.exit(1)