code-puppy 0.0.323__py3-none-any.whl → 0.0.335__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 (45) hide show
  1. code_puppy/agents/base_agent.py +74 -93
  2. code_puppy/cli_runner.py +105 -2
  3. code_puppy/command_line/add_model_menu.py +15 -0
  4. code_puppy/command_line/autosave_menu.py +5 -0
  5. code_puppy/command_line/colors_menu.py +5 -0
  6. code_puppy/command_line/config_commands.py +24 -1
  7. code_puppy/command_line/core_commands.py +51 -0
  8. code_puppy/command_line/diff_menu.py +5 -0
  9. code_puppy/command_line/mcp/custom_server_form.py +4 -0
  10. code_puppy/command_line/mcp/install_menu.py +5 -1
  11. code_puppy/command_line/model_settings_menu.py +5 -0
  12. code_puppy/command_line/motd.py +13 -7
  13. code_puppy/command_line/onboarding_slides.py +180 -0
  14. code_puppy/command_line/onboarding_wizard.py +340 -0
  15. code_puppy/config.py +3 -2
  16. code_puppy/http_utils.py +155 -196
  17. code_puppy/keymap.py +10 -8
  18. code_puppy/model_factory.py +86 -15
  19. code_puppy/models.json +2 -2
  20. code_puppy/plugins/__init__.py +12 -0
  21. code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
  22. code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
  23. code_puppy/plugins/antigravity_oauth/antigravity_model.py +612 -0
  24. code_puppy/plugins/antigravity_oauth/config.py +42 -0
  25. code_puppy/plugins/antigravity_oauth/constants.py +136 -0
  26. code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
  27. code_puppy/plugins/antigravity_oauth/register_callbacks.py +406 -0
  28. code_puppy/plugins/antigravity_oauth/storage.py +271 -0
  29. code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
  30. code_puppy/plugins/antigravity_oauth/token.py +167 -0
  31. code_puppy/plugins/antigravity_oauth/transport.py +595 -0
  32. code_puppy/plugins/antigravity_oauth/utils.py +169 -0
  33. code_puppy/plugins/chatgpt_oauth/register_callbacks.py +2 -0
  34. code_puppy/plugins/claude_code_oauth/register_callbacks.py +2 -0
  35. code_puppy/reopenable_async_client.py +8 -8
  36. code_puppy/terminal_utils.py +168 -3
  37. code_puppy/tools/command_runner.py +42 -54
  38. code_puppy/uvx_detection.py +242 -0
  39. {code_puppy-0.0.323.data → code_puppy-0.0.335.data}/data/code_puppy/models.json +2 -2
  40. {code_puppy-0.0.323.dist-info → code_puppy-0.0.335.dist-info}/METADATA +30 -1
  41. {code_puppy-0.0.323.dist-info → code_puppy-0.0.335.dist-info}/RECORD +45 -30
  42. {code_puppy-0.0.323.data → code_puppy-0.0.335.data}/data/code_puppy/models_dev_api.json +0 -0
  43. {code_puppy-0.0.323.dist-info → code_puppy-0.0.335.dist-info}/WHEEL +0 -0
  44. {code_puppy-0.0.323.dist-info → code_puppy-0.0.335.dist-info}/entry_points.txt +0 -0
  45. {code_puppy-0.0.323.dist-info → code_puppy-0.0.335.dist-info}/licenses/LICENSE +0 -0
code_puppy/http_utils.py CHANGED
@@ -4,29 +4,19 @@ HTTP utilities module for code-puppy.
4
4
  This module provides functions for creating properly configured HTTP clients.
5
5
  """
6
6
 
7
+ import asyncio
8
+ import logging
7
9
  import os
8
10
  import socket
9
- from typing import Dict, Optional, Union
11
+ import time
12
+ from typing import Any, Dict, Optional, Union
10
13
 
11
14
  import httpx
12
15
  import requests
13
- from tenacity import stop_after_attempt, wait_exponential
14
16
 
15
17
  from code_puppy.config import get_http2
16
18
 
17
- try:
18
- from pydantic_ai.retries import (
19
- AsyncTenacityTransport,
20
- RetryConfig,
21
- TenacityTransport,
22
- wait_retry_after,
23
- )
24
- except ImportError:
25
- # Fallback if pydantic_ai.retries is not available
26
- AsyncTenacityTransport = None
27
- RetryConfig = None
28
- TenacityTransport = None
29
- wait_retry_after = None
19
+ logger = logging.getLogger(__name__)
30
20
 
31
21
  try:
32
22
  from .reopenable_async_client import ReopenableAsyncClient
@@ -34,12 +24,109 @@ except ImportError:
34
24
  ReopenableAsyncClient = None
35
25
 
36
26
  try:
37
- from .messaging import emit_info
27
+ from .messaging import emit_info, emit_warning
38
28
  except ImportError:
39
29
  # Fallback if messaging system is not available
40
30
  def emit_info(content: str, **metadata):
41
31
  pass # No-op if messaging system is not available
42
32
 
33
+ def emit_warning(content: str, **metadata):
34
+ pass
35
+
36
+
37
+ class RetryingAsyncClient(httpx.AsyncClient):
38
+ """AsyncClient with built-in rate limit handling (429) and retries.
39
+
40
+ This replaces the Tenacity transport with a more direct subclass implementation,
41
+ which plays nicer with proxies and custom transports (like Antigravity).
42
+ """
43
+
44
+ def __init__(
45
+ self,
46
+ retry_status_codes: tuple = (429, 502, 503, 504),
47
+ max_retries: int = 5,
48
+ **kwargs,
49
+ ):
50
+ super().__init__(**kwargs)
51
+ self.retry_status_codes = retry_status_codes
52
+ self.max_retries = max_retries
53
+
54
+ async def send(self, request: httpx.Request, **kwargs: Any) -> httpx.Response:
55
+ """Send request with automatic retries for rate limits and server errors."""
56
+ last_response = None
57
+ last_exception = None
58
+
59
+ for attempt in range(self.max_retries + 1):
60
+ try:
61
+ # Clone request for retry (streams might be consumed)
62
+ # But only if it's not the first attempt
63
+ req_to_send = request
64
+ if attempt > 0:
65
+ # httpx requests are reusable, but we need to be careful with streams
66
+ pass
67
+
68
+ response = await super().send(req_to_send, **kwargs)
69
+ last_response = response
70
+
71
+ # Check for retryable status
72
+ if response.status_code not in self.retry_status_codes:
73
+ return response
74
+
75
+ # Close response if we're going to retry
76
+ await response.aclose()
77
+
78
+ # Determine wait time
79
+ wait_time = 1.0 * (
80
+ 2**attempt
81
+ ) # Default exponential backoff: 1s, 2s, 4s...
82
+
83
+ # Check Retry-After header
84
+ retry_after = response.headers.get("Retry-After")
85
+ if retry_after:
86
+ try:
87
+ wait_time = float(retry_after)
88
+ except ValueError:
89
+ # Try parsing http-date
90
+ from email.utils import parsedate_to_datetime
91
+
92
+ try:
93
+ date = parsedate_to_datetime(retry_after)
94
+ wait_time = date.timestamp() - time.time()
95
+ except Exception:
96
+ pass
97
+
98
+ # Cap wait time
99
+ wait_time = max(0.5, min(wait_time, 60.0))
100
+
101
+ if attempt < self.max_retries:
102
+ emit_info(
103
+ f"HTTP retry: {response.status_code} received. Waiting {wait_time:.1f}s (attempt {attempt + 1}/{self.max_retries})"
104
+ )
105
+ await asyncio.sleep(wait_time)
106
+
107
+ except (httpx.ConnectError, httpx.ReadTimeout, httpx.PoolTimeout) as e:
108
+ last_exception = e
109
+ wait_time = 1.0 * (2**attempt)
110
+ if attempt < self.max_retries:
111
+ emit_warning(
112
+ f"HTTP connection error: {e}. Retrying in {wait_time}s..."
113
+ )
114
+ await asyncio.sleep(wait_time)
115
+ else:
116
+ raise
117
+ except Exception:
118
+ raise
119
+
120
+ # Return last response (even if it's an error status)
121
+ if last_response:
122
+ return last_response
123
+
124
+ # Should catch this in loop, but just in case
125
+ if last_exception:
126
+ raise last_exception
127
+
128
+ return last_response
129
+
43
130
 
44
131
  def get_cert_bundle_path() -> str:
45
132
  # First check if SSL_CERT_FILE environment variable is set
@@ -60,53 +147,15 @@ def create_client(
60
147
  # Check if HTTP/2 is enabled in config
61
148
  http2_enabled = get_http2()
62
149
 
63
- # Check if custom retry transport should be disabled (e.g., for integration tests with proxies)
64
- disable_retry_transport = os.environ.get(
65
- "CODE_PUPPY_DISABLE_RETRY_TRANSPORT", ""
66
- ).lower() in ("1", "true", "yes")
67
-
68
150
  # If retry components are available, create a client with retry transport
69
- if (
70
- TenacityTransport
71
- and RetryConfig
72
- and wait_retry_after
73
- and not disable_retry_transport
74
- ):
75
-
76
- def should_retry_status(response):
77
- """Raise exceptions for retryable HTTP status codes."""
78
- if response.status_code in retry_status_codes:
79
- emit_info(
80
- f"HTTP retry: Retrying request due to status code {response.status_code}"
81
- )
82
- return True
83
-
84
- transport = TenacityTransport(
85
- config=RetryConfig(
86
- retry=lambda e: isinstance(e, httpx.HTTPStatusError)
87
- and e.response.status_code in retry_status_codes,
88
- wait=wait_retry_after(
89
- fallback_strategy=wait_exponential(multiplier=1, max=60),
90
- max_wait=300,
91
- ),
92
- stop=stop_after_attempt(10),
93
- reraise=True,
94
- ),
95
- validate_response=should_retry_status,
96
- )
97
-
98
- return httpx.Client(
99
- transport=transport,
100
- verify=verify,
101
- headers=headers or {},
102
- timeout=timeout,
103
- http2=http2_enabled,
104
- )
105
- else:
106
- # Fallback to regular client if retry components are not available
107
- return httpx.Client(
108
- verify=verify, headers=headers or {}, timeout=timeout, http2=http2_enabled
109
- )
151
+ # Note: TenacityTransport was removed. For now we just return a standard client.
152
+ # Future TODO: Implement RetryingClient(httpx.Client) if needed.
153
+ return httpx.Client(
154
+ verify=verify,
155
+ headers=headers or {},
156
+ timeout=timeout,
157
+ http2=http2_enabled,
158
+ )
110
159
 
111
160
 
112
161
  def create_async_client(
@@ -145,52 +194,21 @@ def create_async_client(
145
194
  else:
146
195
  trust_env = False
147
196
 
148
- # If retry components are available, create a client with retry transport
149
- # BUT: disable retry transport when proxies are detected because custom transports
150
- # don't play nicely with proxy configuration
151
- if (
152
- AsyncTenacityTransport
153
- and RetryConfig
154
- and wait_retry_after
155
- and not disable_retry_transport
156
- and not has_proxy
157
- ):
158
-
159
- def should_retry_status(response):
160
- """Raise exceptions for retryable HTTP status codes."""
161
- if response.status_code in retry_status_codes:
162
- emit_info(
163
- f"HTTP retry: Retrying request due to status code {response.status_code}"
164
- )
165
- return True
166
-
167
- # Create transport (with or without proxy base)
168
- if has_proxy:
169
- # Extract proxy URL from environment
170
- proxy_url = (
171
- os.environ.get("HTTPS_PROXY")
172
- or os.environ.get("https_proxy")
173
- or os.environ.get("HTTP_PROXY")
174
- or os.environ.get("http_proxy")
175
- )
176
- else:
177
- proxy_url = None
178
-
179
- # Create retry transport wrapper
180
- transport = AsyncTenacityTransport(
181
- config=RetryConfig(
182
- retry=lambda e: isinstance(e, httpx.HTTPStatusError)
183
- and e.response.status_code in retry_status_codes,
184
- wait=wait_retry_after(10),
185
- stop=stop_after_attempt(10),
186
- reraise=True,
187
- ),
188
- validate_response=should_retry_status,
197
+ # Extract proxy URL if needed
198
+ proxy_url = None
199
+ if has_proxy:
200
+ proxy_url = (
201
+ os.environ.get("HTTPS_PROXY")
202
+ or os.environ.get("https_proxy")
203
+ or os.environ.get("HTTP_PROXY")
204
+ or os.environ.get("http_proxy")
189
205
  )
190
206
 
191
- return httpx.AsyncClient(
192
- transport=transport,
193
- proxy=proxy_url, # Pass proxy to client, not transport
207
+ # Use RetryingAsyncClient if retries are enabled
208
+ if not disable_retry_transport:
209
+ return RetryingAsyncClient(
210
+ retry_status_codes=retry_status_codes,
211
+ proxy=proxy_url,
194
212
  verify=verify,
195
213
  headers=headers or {},
196
214
  timeout=timeout,
@@ -198,19 +216,7 @@ def create_async_client(
198
216
  trust_env=trust_env,
199
217
  )
200
218
  else:
201
- # Fallback to regular client if retry components are not available,
202
- # when retry transport is explicitly disabled, or when proxies are detected
203
- # Extract proxy URL if needed
204
- if has_proxy:
205
- proxy_url = (
206
- os.environ.get("HTTPS_PROXY")
207
- or os.environ.get("https_proxy")
208
- or os.environ.get("HTTP_PROXY")
209
- or os.environ.get("http_proxy")
210
- )
211
- else:
212
- proxy_url = None
213
-
219
+ # Regular client for testing
214
220
  return httpx.AsyncClient(
215
221
  proxy=proxy_url,
216
222
  verify=verify,
@@ -295,87 +301,41 @@ def create_reopenable_async_client(
295
301
  else:
296
302
  trust_env = False
297
303
 
298
- # If retry components are available, create a client with retry transport
299
- # BUT: disable retry transport when proxies are detected because custom transports
300
- # don't play nicely with proxy configuration
301
- if (
302
- AsyncTenacityTransport
303
- and RetryConfig
304
- and wait_retry_after
305
- and not disable_retry_transport
306
- and not has_proxy
307
- ):
304
+ # Extract proxy URL if needed
305
+ proxy_url = None
306
+ if has_proxy:
307
+ proxy_url = (
308
+ os.environ.get("HTTPS_PROXY")
309
+ or os.environ.get("https_proxy")
310
+ or os.environ.get("HTTP_PROXY")
311
+ or os.environ.get("http_proxy")
312
+ )
308
313
 
309
- def should_retry_status(response):
310
- """Raise exceptions for retryable HTTP status codes."""
311
- if response.status_code in retry_status_codes:
312
- emit_info(
313
- f"HTTP retry: Retrying request due to status code {response.status_code}"
314
- )
315
- return True
316
-
317
- transport = AsyncTenacityTransport(
318
- config=RetryConfig(
319
- retry=lambda e: isinstance(e, httpx.HTTPStatusError)
320
- and e.response.status_code in retry_status_codes,
321
- wait=wait_retry_after(
322
- fallback_strategy=wait_exponential(multiplier=1, max=60),
323
- max_wait=300,
324
- ),
325
- stop=stop_after_attempt(10),
326
- reraise=True,
327
- ),
328
- validate_response=should_retry_status,
314
+ if ReopenableAsyncClient is not None:
315
+ # Use RetryingAsyncClient if retries are enabled
316
+ client_class = (
317
+ RetryingAsyncClient if not disable_retry_transport else httpx.AsyncClient
329
318
  )
330
319
 
331
- # Extract proxy URL if needed
332
- if has_proxy:
333
- proxy_url = (
334
- os.environ.get("HTTPS_PROXY")
335
- or os.environ.get("https_proxy")
336
- or os.environ.get("HTTP_PROXY")
337
- or os.environ.get("http_proxy")
338
- )
339
- else:
340
- proxy_url = None
320
+ # Pass retry config only if using RetryingAsyncClient
321
+ kwargs = {
322
+ "proxy": proxy_url,
323
+ "verify": verify,
324
+ "headers": headers or {},
325
+ "timeout": timeout,
326
+ "http2": http2_enabled,
327
+ "trust_env": trust_env,
328
+ }
341
329
 
342
- if ReopenableAsyncClient is not None:
343
- return ReopenableAsyncClient(
344
- transport=transport,
345
- proxy=proxy_url,
346
- verify=verify,
347
- headers=headers or {},
348
- timeout=timeout,
349
- http2=http2_enabled,
350
- trust_env=trust_env,
351
- )
352
- else:
353
- # Fallback to regular AsyncClient if ReopenableAsyncClient is not available
354
- return httpx.AsyncClient(
355
- transport=transport,
356
- proxy=proxy_url,
357
- verify=verify,
358
- headers=headers or {},
359
- timeout=timeout,
360
- http2=http2_enabled,
361
- trust_env=trust_env,
362
- )
363
- else:
364
- # Fallback to regular clients if retry components are not available
365
- # or when proxies are detected
366
- # Extract proxy URL if needed
367
- if has_proxy:
368
- proxy_url = (
369
- os.environ.get("HTTPS_PROXY")
370
- or os.environ.get("https_proxy")
371
- or os.environ.get("HTTP_PROXY")
372
- or os.environ.get("http_proxy")
373
- )
374
- else:
375
- proxy_url = None
330
+ if not disable_retry_transport:
331
+ kwargs["retry_status_codes"] = retry_status_codes
376
332
 
377
- if ReopenableAsyncClient is not None:
378
- return ReopenableAsyncClient(
333
+ return ReopenableAsyncClient(client_class=client_class, **kwargs)
334
+ else:
335
+ # Fallback to RetryingAsyncClient
336
+ if not disable_retry_transport:
337
+ return RetryingAsyncClient(
338
+ retry_status_codes=retry_status_codes,
379
339
  proxy=proxy_url,
380
340
  verify=verify,
381
341
  headers=headers or {},
@@ -384,7 +344,6 @@ def create_reopenable_async_client(
384
344
  trust_env=trust_env,
385
345
  )
386
346
  else:
387
- # Fallback to regular AsyncClient if ReopenableAsyncClient is not available
388
347
  return httpx.AsyncClient(
389
348
  proxy=proxy_url,
390
349
  verify=verify,
code_puppy/keymap.py CHANGED
@@ -55,11 +55,19 @@ class KeymapError(Exception):
55
55
  def get_cancel_agent_key() -> str:
56
56
  """Get the configured cancel agent key from config.
57
57
 
58
+ On Windows when launched via uvx, this automatically returns "ctrl+k"
59
+ to work around uvx capturing Ctrl+C before it reaches Python.
60
+
58
61
  Returns:
59
62
  The key name (e.g., "ctrl+c", "ctrl+k") from config,
60
63
  or the default if not configured.
61
64
  """
62
65
  from code_puppy.config import get_value
66
+ from code_puppy.uvx_detection import should_use_alternate_cancel_key
67
+
68
+ # On Windows + uvx, force ctrl+k to bypass uvx's SIGINT capture
69
+ if should_use_alternate_cancel_key():
70
+ return "ctrl+k"
63
71
 
64
72
  key = get_value("cancel_agent_key")
65
73
  if key is None or key.strip() == "":
@@ -86,15 +94,9 @@ def cancel_agent_uses_signal() -> bool:
86
94
  """Check if the cancel agent key uses SIGINT (Ctrl+C).
87
95
 
88
96
  Returns:
89
- True if the cancel key is ctrl+c AND we're not on Windows
90
- (uses SIGINT handler), False if it uses keyboard listener approach.
97
+ True if the cancel key is ctrl+c (uses SIGINT handler),
98
+ False if it uses keyboard listener approach.
91
99
  """
92
- import sys
93
-
94
- # On Windows, always use keyboard listener - SIGINT is unreliable
95
- if sys.platform == "win32":
96
- return False
97
-
98
100
  return get_cancel_agent_key() == "ctrl+c"
99
101
 
100
102
 
@@ -4,7 +4,6 @@ import os
4
4
  import pathlib
5
5
  from typing import Any, Dict
6
6
 
7
- import httpx
8
7
  from anthropic import AsyncAnthropic
9
8
  from openai import AsyncAzureOpenAI
10
9
  from pydantic_ai.models.anthropic import AnthropicModel, AnthropicModelSettings
@@ -212,6 +211,7 @@ class ModelFactory:
212
211
 
213
212
  # Import OAuth model file paths from main config
214
213
  from code_puppy.config import (
214
+ ANTIGRAVITY_MODELS_FILE,
215
215
  CHATGPT_MODELS_FILE,
216
216
  CLAUDE_MODELS_FILE,
217
217
  GEMINI_MODELS_FILE,
@@ -223,6 +223,7 @@ class ModelFactory:
223
223
  (pathlib.Path(CHATGPT_MODELS_FILE), "ChatGPT OAuth models", False),
224
224
  (pathlib.Path(CLAUDE_MODELS_FILE), "Claude Code OAuth models", True),
225
225
  (pathlib.Path(GEMINI_MODELS_FILE), "Gemini OAuth models", False),
226
+ (pathlib.Path(ANTIGRAVITY_MODELS_FILE), "Antigravity OAuth models", False),
226
227
  ]
227
228
 
228
229
  for source_path, label, use_filtered in extra_sources:
@@ -556,24 +557,94 @@ class ModelFactory:
556
557
  f"API key is not set for custom Gemini endpoint; skipping model '{model_config.get('name')}'."
557
558
  )
558
559
  return None
559
- os.environ["GEMINI_API_KEY"] = api_key
560
560
 
561
- class CustomGoogleGLAProvider(GoogleProvider):
562
- def __init__(self, *args, **kwargs):
563
- super().__init__(*args, **kwargs)
561
+ # Check if this is an Antigravity model
562
+ if model_config.get("antigravity"):
563
+ try:
564
+ from code_puppy.plugins.antigravity_oauth.token import (
565
+ is_token_expired,
566
+ refresh_access_token,
567
+ )
568
+ from code_puppy.plugins.antigravity_oauth.transport import (
569
+ create_antigravity_client,
570
+ )
571
+ from code_puppy.plugins.antigravity_oauth.utils import (
572
+ load_stored_tokens,
573
+ save_tokens,
574
+ )
575
+
576
+ # Try to import custom model for thinking signatures
577
+ try:
578
+ from code_puppy.plugins.antigravity_oauth.antigravity_model import (
579
+ AntigravityModel,
580
+ )
581
+ except ImportError:
582
+ AntigravityModel = None
583
+
584
+ # Get fresh access token (refresh if needed)
585
+ tokens = load_stored_tokens()
586
+ if not tokens:
587
+ emit_warning(
588
+ "Antigravity tokens not found; run /antigravity-auth first."
589
+ )
590
+ return None
591
+
592
+ access_token = tokens.get("access_token", "")
593
+ refresh_token = tokens.get("refresh_token", "")
594
+ expires_at = tokens.get("expires_at")
595
+
596
+ # Refresh if expired or about to expire
597
+ if is_token_expired(expires_at):
598
+ new_tokens = refresh_access_token(refresh_token)
599
+ if new_tokens:
600
+ access_token = new_tokens.access_token
601
+ tokens["access_token"] = new_tokens.access_token
602
+ tokens["refresh_token"] = new_tokens.refresh_token
603
+ tokens["expires_at"] = new_tokens.expires_at
604
+ save_tokens(tokens)
605
+ else:
606
+ emit_warning(
607
+ "Failed to refresh Antigravity token; run /antigravity-auth again."
608
+ )
609
+ return None
610
+
611
+ project_id = tokens.get(
612
+ "project_id", model_config.get("project_id", "")
613
+ )
614
+ client = create_antigravity_client(
615
+ access_token=access_token,
616
+ project_id=project_id,
617
+ model_name=model_config["name"],
618
+ base_url=url,
619
+ headers=headers,
620
+ )
621
+
622
+ provider = GoogleProvider(
623
+ api_key=api_key, base_url=url, http_client=client
624
+ )
625
+
626
+ # Use custom model if available to preserve thinking signatures
627
+ if AntigravityModel:
628
+ model = AntigravityModel(
629
+ model_name=model_config["name"], provider=provider
630
+ )
631
+ else:
632
+ model = GoogleModel(
633
+ model_name=model_config["name"], provider=provider
634
+ )
564
635
 
565
- @property
566
- def base_url(self):
567
- return url
636
+ return model
568
637
 
569
- @property
570
- def client(self) -> httpx.AsyncClient:
571
- _client = create_async_client(headers=headers, verify=verify)
572
- _client.base_url = self.base_url
573
- return _client
638
+ except ImportError:
639
+ emit_warning(
640
+ f"Antigravity transport not available; skipping model '{model_config.get('name')}'."
641
+ )
642
+ return None
643
+ else:
644
+ client = create_async_client(headers=headers, verify=verify)
574
645
 
575
- google_gla = CustomGoogleGLAProvider(api_key=api_key)
576
- model = GoogleModel(model_name=model_config["name"], provider=google_gla)
646
+ provider = GoogleProvider(api_key=api_key, base_url=url, http_client=client)
647
+ model = GoogleModel(model_name=model_config["name"], provider=provider)
577
648
  return model
578
649
  elif model_type == "cerebras":
579
650
 
code_puppy/models.json CHANGED
@@ -9,9 +9,9 @@
9
9
  "context_length": 200000,
10
10
  "supported_settings": ["temperature", "seed"]
11
11
  },
12
- "synthetic-MiniMax-M2": {
12
+ "synthetic-MiniMax-M2.1": {
13
13
  "type": "custom_openai",
14
- "name": "hf:MiniMaxAI/MiniMax-M2",
14
+ "name": "hf:MiniMaxAI/MiniMax-M2.1",
15
15
  "custom_endpoint": {
16
16
  "url": "https://api.synthetic.new/openai/v1/",
17
17
  "api_key": "$SYN_API_KEY"
@@ -18,6 +18,9 @@ def _load_builtin_plugins(plugins_dir: Path) -> list[str]:
18
18
 
19
19
  Returns list of successfully loaded plugin names.
20
20
  """
21
+ # Import safety permission check for shell_safety plugin
22
+ from code_puppy.config import get_safety_permission_level
23
+
21
24
  loaded = []
22
25
 
23
26
  for item in plugins_dir.iterdir():
@@ -26,6 +29,15 @@ def _load_builtin_plugins(plugins_dir: Path) -> list[str]:
26
29
  callbacks_file = item / "register_callbacks.py"
27
30
 
28
31
  if callbacks_file.exists():
32
+ # Skip shell_safety plugin unless safety_permission_level is "low" or "none"
33
+ if plugin_name == "shell_safety":
34
+ safety_level = get_safety_permission_level()
35
+ if safety_level not in ("none", "low"):
36
+ logger.debug(
37
+ f"Skipping shell_safety plugin - safety_permission_level is '{safety_level}' (needs 'low' or 'none')"
38
+ )
39
+ continue
40
+
29
41
  try:
30
42
  module_name = f"code_puppy.plugins.{plugin_name}.register_callbacks"
31
43
  importlib.import_module(module_name)
@@ -0,0 +1,10 @@
1
+ """Antigravity OAuth Plugin for Code Puppy.
2
+
3
+ Enables authentication with Google/Antigravity APIs to access Gemini and Claude models
4
+ via Google credentials. Supports multi-account load balancing and automatic failover.
5
+ """
6
+
7
+ from .config import ANTIGRAVITY_OAUTH_CONFIG
8
+ from .register_callbacks import * # noqa: F401, F403
9
+
10
+ __all__ = ["ANTIGRAVITY_OAUTH_CONFIG"]