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.
- code_puppy/agents/base_agent.py +74 -93
- code_puppy/cli_runner.py +105 -2
- code_puppy/command_line/add_model_menu.py +15 -0
- code_puppy/command_line/autosave_menu.py +5 -0
- code_puppy/command_line/colors_menu.py +5 -0
- code_puppy/command_line/config_commands.py +24 -1
- code_puppy/command_line/core_commands.py +51 -0
- code_puppy/command_line/diff_menu.py +5 -0
- code_puppy/command_line/mcp/custom_server_form.py +4 -0
- code_puppy/command_line/mcp/install_menu.py +5 -1
- code_puppy/command_line/model_settings_menu.py +5 -0
- code_puppy/command_line/motd.py +13 -7
- code_puppy/command_line/onboarding_slides.py +180 -0
- code_puppy/command_line/onboarding_wizard.py +340 -0
- code_puppy/config.py +3 -2
- code_puppy/http_utils.py +155 -196
- code_puppy/keymap.py +10 -8
- code_puppy/model_factory.py +86 -15
- code_puppy/models.json +2 -2
- code_puppy/plugins/__init__.py +12 -0
- code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
- code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
- code_puppy/plugins/antigravity_oauth/antigravity_model.py +612 -0
- code_puppy/plugins/antigravity_oauth/config.py +42 -0
- code_puppy/plugins/antigravity_oauth/constants.py +136 -0
- code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
- code_puppy/plugins/antigravity_oauth/register_callbacks.py +406 -0
- code_puppy/plugins/antigravity_oauth/storage.py +271 -0
- code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
- code_puppy/plugins/antigravity_oauth/token.py +167 -0
- code_puppy/plugins/antigravity_oauth/transport.py +595 -0
- code_puppy/plugins/antigravity_oauth/utils.py +169 -0
- code_puppy/plugins/chatgpt_oauth/register_callbacks.py +2 -0
- code_puppy/plugins/claude_code_oauth/register_callbacks.py +2 -0
- code_puppy/reopenable_async_client.py +8 -8
- code_puppy/terminal_utils.py +168 -3
- code_puppy/tools/command_runner.py +42 -54
- code_puppy/uvx_detection.py +242 -0
- {code_puppy-0.0.323.data → code_puppy-0.0.335.data}/data/code_puppy/models.json +2 -2
- {code_puppy-0.0.323.dist-info → code_puppy-0.0.335.dist-info}/METADATA +30 -1
- {code_puppy-0.0.323.dist-info → code_puppy-0.0.335.dist-info}/RECORD +45 -30
- {code_puppy-0.0.323.data → code_puppy-0.0.335.data}/data/code_puppy/models_dev_api.json +0 -0
- {code_puppy-0.0.323.dist-info → code_puppy-0.0.335.dist-info}/WHEEL +0 -0
- {code_puppy-0.0.323.dist-info → code_puppy-0.0.335.dist-info}/entry_points.txt +0 -0
- {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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
#
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
#
|
|
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
|
-
#
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
-
#
|
|
332
|
-
|
|
333
|
-
proxy_url
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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
|
|
343
|
-
|
|
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
|
-
|
|
378
|
-
|
|
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
|
|
90
|
-
|
|
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
|
|
code_puppy/model_factory.py
CHANGED
|
@@ -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
|
-
|
|
562
|
-
|
|
563
|
-
|
|
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
|
-
|
|
566
|
-
def base_url(self):
|
|
567
|
-
return url
|
|
636
|
+
return model
|
|
568
637
|
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
return
|
|
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
|
-
|
|
576
|
-
model = GoogleModel(model_name=model_config["name"], provider=
|
|
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"
|
code_puppy/plugins/__init__.py
CHANGED
|
@@ -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"]
|