code-puppy 0.0.302__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 +343 -35
- code_puppy/chatgpt_codex_client.py +283 -0
- code_puppy/cli_runner.py +898 -0
- code_puppy/command_line/add_model_menu.py +23 -1
- code_puppy/command_line/autosave_menu.py +271 -35
- code_puppy/command_line/colors_menu.py +520 -0
- code_puppy/command_line/command_handler.py +8 -2
- code_puppy/command_line/config_commands.py +82 -10
- code_puppy/command_line/core_commands.py +70 -7
- 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/edit_command.py +3 -1
- code_puppy/command_line/mcp/handler.py +7 -2
- code_puppy/command_line/mcp/install_command.py +8 -3
- code_puppy/command_line/mcp/install_menu.py +5 -1
- code_puppy/command_line/mcp/logs_command.py +173 -64
- code_puppy/command_line/mcp/restart_command.py +7 -2
- code_puppy/command_line/mcp/search_command.py +10 -4
- code_puppy/command_line/mcp/start_all_command.py +16 -6
- code_puppy/command_line/mcp/start_command.py +3 -1
- code_puppy/command_line/mcp/status_command.py +2 -1
- code_puppy/command_line/mcp/stop_all_command.py +5 -1
- code_puppy/command_line/mcp/stop_command.py +3 -1
- code_puppy/command_line/mcp/wizard_utils.py +10 -4
- code_puppy/command_line/model_settings_menu.py +58 -7
- 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/command_line/prompt_toolkit_completion.py +16 -2
- code_puppy/command_line/session_commands.py +11 -4
- code_puppy/config.py +106 -17
- code_puppy/http_utils.py +155 -196
- code_puppy/keymap.py +8 -0
- code_puppy/main.py +5 -828
- code_puppy/mcp_/__init__.py +17 -0
- code_puppy/mcp_/blocking_startup.py +61 -32
- code_puppy/mcp_/config_wizard.py +5 -1
- code_puppy/mcp_/managed_server.py +23 -3
- code_puppy/mcp_/manager.py +65 -0
- code_puppy/mcp_/mcp_logs.py +224 -0
- code_puppy/messaging/__init__.py +20 -4
- code_puppy/messaging/bus.py +64 -0
- code_puppy/messaging/markdown_patches.py +57 -0
- code_puppy/messaging/messages.py +16 -0
- code_puppy/messaging/renderers.py +21 -9
- code_puppy/messaging/rich_renderer.py +113 -67
- code_puppy/messaging/spinner/console_spinner.py +34 -0
- code_puppy/model_factory.py +271 -45
- code_puppy/model_utils.py +57 -48
- code_puppy/models.json +21 -7
- 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/config.py +5 -1
- code_puppy/plugins/chatgpt_oauth/oauth_flow.py +5 -6
- code_puppy/plugins/chatgpt_oauth/register_callbacks.py +5 -3
- code_puppy/plugins/chatgpt_oauth/test_plugin.py +26 -11
- code_puppy/plugins/chatgpt_oauth/utils.py +180 -65
- code_puppy/plugins/claude_code_oauth/register_callbacks.py +30 -0
- code_puppy/plugins/claude_code_oauth/utils.py +1 -0
- code_puppy/plugins/shell_safety/agent_shell_safety.py +1 -118
- code_puppy/plugins/shell_safety/register_callbacks.py +44 -3
- code_puppy/prompts/codex_system_prompt.md +310 -0
- code_puppy/pydantic_patches.py +131 -0
- code_puppy/reopenable_async_client.py +8 -8
- code_puppy/terminal_utils.py +291 -0
- code_puppy/tools/agent_tools.py +34 -9
- code_puppy/tools/command_runner.py +344 -27
- code_puppy/tools/file_operations.py +33 -45
- code_puppy/uvx_detection.py +242 -0
- {code_puppy-0.0.302.data → code_puppy-0.0.335.data}/data/code_puppy/models.json +21 -7
- {code_puppy-0.0.302.dist-info → code_puppy-0.0.335.dist-info}/METADATA +30 -1
- {code_puppy-0.0.302.dist-info → code_puppy-0.0.335.dist-info}/RECORD +87 -64
- {code_puppy-0.0.302.data → code_puppy-0.0.335.data}/data/code_puppy/models_dev_api.json +0 -0
- {code_puppy-0.0.302.dist-info → code_puppy-0.0.335.dist-info}/WHEEL +0 -0
- {code_puppy-0.0.302.dist-info → code_puppy-0.0.335.dist-info}/entry_points.txt +0 -0
- {code_puppy-0.0.302.dist-info → code_puppy-0.0.335.dist-info}/licenses/LICENSE +0 -0
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Console spinner implementation for CLI mode using Rich's Live Display.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
+
import platform
|
|
5
6
|
import threading
|
|
6
7
|
import time
|
|
7
8
|
|
|
@@ -43,6 +44,9 @@ class ConsoleSpinner(SpinnerBase):
|
|
|
43
44
|
if self._thread and self._thread.is_alive():
|
|
44
45
|
return
|
|
45
46
|
|
|
47
|
+
# Print blank line before spinner for visual separation from content
|
|
48
|
+
self.console.print()
|
|
49
|
+
|
|
46
50
|
# Create a Live display for the spinner
|
|
47
51
|
self._live = Live(
|
|
48
52
|
self._generate_spinner_panel(),
|
|
@@ -75,6 +79,33 @@ class ConsoleSpinner(SpinnerBase):
|
|
|
75
79
|
|
|
76
80
|
self._thread = None
|
|
77
81
|
|
|
82
|
+
# Windows-specific cleanup: Rich's Live display can leave terminal in corrupted state
|
|
83
|
+
if platform.system() == "Windows":
|
|
84
|
+
import sys
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
# Reset ANSI formatting for both stdout and stderr
|
|
88
|
+
sys.stdout.write("\x1b[0m") # Reset all attributes
|
|
89
|
+
sys.stdout.flush()
|
|
90
|
+
sys.stderr.write("\x1b[0m")
|
|
91
|
+
sys.stderr.flush()
|
|
92
|
+
|
|
93
|
+
# Clear the line and reposition cursor
|
|
94
|
+
sys.stdout.write("\r") # Return to start of line
|
|
95
|
+
sys.stdout.write("\x1b[K") # Clear to end of line
|
|
96
|
+
sys.stdout.flush()
|
|
97
|
+
|
|
98
|
+
# Flush keyboard input buffer to clear any stuck keys
|
|
99
|
+
try:
|
|
100
|
+
import msvcrt
|
|
101
|
+
|
|
102
|
+
while msvcrt.kbhit():
|
|
103
|
+
msvcrt.getch()
|
|
104
|
+
except ImportError:
|
|
105
|
+
pass # msvcrt not available (not Windows or different Python impl)
|
|
106
|
+
except Exception:
|
|
107
|
+
pass # Fail silently if cleanup doesn't work
|
|
108
|
+
|
|
78
109
|
# Unregister this spinner from global management
|
|
79
110
|
from . import unregister_spinner
|
|
80
111
|
|
|
@@ -171,6 +202,9 @@ class ConsoleSpinner(SpinnerBase):
|
|
|
171
202
|
sys.stdout.write("\x1b[K") # Clear to end of line
|
|
172
203
|
sys.stdout.flush()
|
|
173
204
|
|
|
205
|
+
# Print blank line before spinner for visual separation
|
|
206
|
+
self.console.print()
|
|
207
|
+
|
|
174
208
|
self._live = Live(
|
|
175
209
|
self._generate_spinner_panel(),
|
|
176
210
|
console=self.console,
|
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
|
|
@@ -26,11 +25,32 @@ from code_puppy.messaging import emit_warning
|
|
|
26
25
|
|
|
27
26
|
from . import callbacks
|
|
28
27
|
from .claude_cache_client import ClaudeCacheAsyncClient, patch_anthropic_client_messages
|
|
29
|
-
from .config import EXTRA_MODELS_FILE
|
|
28
|
+
from .config import EXTRA_MODELS_FILE, get_value
|
|
30
29
|
from .http_utils import create_async_client, get_cert_bundle_path, get_http2
|
|
31
30
|
from .round_robin_model import RoundRobinModel
|
|
32
31
|
|
|
33
32
|
|
|
33
|
+
def get_api_key(env_var_name: str) -> str | None:
|
|
34
|
+
"""Get an API key from config first, then fall back to environment variable.
|
|
35
|
+
|
|
36
|
+
This allows users to set API keys via `/set KIMI_API_KEY=xxx` in addition to
|
|
37
|
+
setting them as environment variables.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
env_var_name: The name of the environment variable (e.g., "OPENAI_API_KEY")
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
The API key value, or None if not found in either config or environment.
|
|
44
|
+
"""
|
|
45
|
+
# First check config (case-insensitive key lookup)
|
|
46
|
+
config_value = get_value(env_var_name.lower())
|
|
47
|
+
if config_value:
|
|
48
|
+
return config_value
|
|
49
|
+
|
|
50
|
+
# Fall back to environment variable
|
|
51
|
+
return os.environ.get(env_var_name)
|
|
52
|
+
|
|
53
|
+
|
|
34
54
|
def make_model_settings(
|
|
35
55
|
model_name: str, max_tokens: int | None = None
|
|
36
56
|
) -> ModelSettings:
|
|
@@ -87,8 +107,14 @@ def make_model_settings(
|
|
|
87
107
|
# Handle Anthropic extended thinking settings
|
|
88
108
|
# Remove top_p as Anthropic doesn't support it with extended thinking
|
|
89
109
|
model_settings_dict.pop("top_p", None)
|
|
90
|
-
|
|
91
|
-
|
|
110
|
+
|
|
111
|
+
# Claude extended thinking requires temperature=1.0 (API restriction)
|
|
112
|
+
# Default to 1.0 if not explicitly set by user
|
|
113
|
+
if model_settings_dict.get("temperature") is None:
|
|
114
|
+
model_settings_dict["temperature"] = 1.0
|
|
115
|
+
|
|
116
|
+
extended_thinking = effective_settings.get("extended_thinking", True)
|
|
117
|
+
budget_tokens = effective_settings.get("budget_tokens", 10000)
|
|
92
118
|
if extended_thinking and budget_tokens:
|
|
93
119
|
model_settings_dict["anthropic_thinking"] = {
|
|
94
120
|
"type": "enabled",
|
|
@@ -118,10 +144,10 @@ def get_custom_config(model_config):
|
|
|
118
144
|
for key, value in custom_config.get("headers", {}).items():
|
|
119
145
|
if value.startswith("$"):
|
|
120
146
|
env_var_name = value[1:]
|
|
121
|
-
resolved_value =
|
|
147
|
+
resolved_value = get_api_key(env_var_name)
|
|
122
148
|
if resolved_value is None:
|
|
123
149
|
emit_warning(
|
|
124
|
-
f"
|
|
150
|
+
f"'{env_var_name}' is not set (check config or environment) for custom endpoint header '{key}'. Proceeding with empty value."
|
|
125
151
|
)
|
|
126
152
|
resolved_value = ""
|
|
127
153
|
value = resolved_value
|
|
@@ -131,10 +157,10 @@ def get_custom_config(model_config):
|
|
|
131
157
|
for token in tokens:
|
|
132
158
|
if token.startswith("$"):
|
|
133
159
|
env_var = token[1:]
|
|
134
|
-
resolved_value =
|
|
160
|
+
resolved_value = get_api_key(env_var)
|
|
135
161
|
if resolved_value is None:
|
|
136
162
|
emit_warning(
|
|
137
|
-
f"
|
|
163
|
+
f"'{env_var}' is not set (check config or environment) for custom endpoint header '{key}'. Proceeding with empty value."
|
|
138
164
|
)
|
|
139
165
|
resolved_values.append("")
|
|
140
166
|
else:
|
|
@@ -147,10 +173,10 @@ def get_custom_config(model_config):
|
|
|
147
173
|
if "api_key" in custom_config:
|
|
148
174
|
if custom_config["api_key"].startswith("$"):
|
|
149
175
|
env_var_name = custom_config["api_key"][1:]
|
|
150
|
-
api_key =
|
|
176
|
+
api_key = get_api_key(env_var_name)
|
|
151
177
|
if api_key is None:
|
|
152
178
|
emit_warning(
|
|
153
|
-
f"
|
|
179
|
+
f"API key '{env_var_name}' is not set (checked config and environment); proceeding without API key."
|
|
154
180
|
)
|
|
155
181
|
else:
|
|
156
182
|
api_key = custom_config["api_key"]
|
|
@@ -185,6 +211,7 @@ class ModelFactory:
|
|
|
185
211
|
|
|
186
212
|
# Import OAuth model file paths from main config
|
|
187
213
|
from code_puppy.config import (
|
|
214
|
+
ANTIGRAVITY_MODELS_FILE,
|
|
188
215
|
CHATGPT_MODELS_FILE,
|
|
189
216
|
CLAUDE_MODELS_FILE,
|
|
190
217
|
GEMINI_MODELS_FILE,
|
|
@@ -196,6 +223,7 @@ class ModelFactory:
|
|
|
196
223
|
(pathlib.Path(CHATGPT_MODELS_FILE), "ChatGPT OAuth models", False),
|
|
197
224
|
(pathlib.Path(CLAUDE_MODELS_FILE), "Claude Code OAuth models", True),
|
|
198
225
|
(pathlib.Path(GEMINI_MODELS_FILE), "Gemini OAuth models", False),
|
|
226
|
+
(pathlib.Path(ANTIGRAVITY_MODELS_FILE), "Antigravity OAuth models", False),
|
|
199
227
|
]
|
|
200
228
|
|
|
201
229
|
for source_path, label, use_filtered in extra_sources:
|
|
@@ -245,10 +273,10 @@ class ModelFactory:
|
|
|
245
273
|
model_type = model_config.get("type")
|
|
246
274
|
|
|
247
275
|
if model_type == "gemini":
|
|
248
|
-
api_key =
|
|
276
|
+
api_key = get_api_key("GEMINI_API_KEY")
|
|
249
277
|
if not api_key:
|
|
250
278
|
emit_warning(
|
|
251
|
-
f"GEMINI_API_KEY is not set; skipping Gemini model '{model_config.get('name')}'."
|
|
279
|
+
f"GEMINI_API_KEY is not set (check config or environment); skipping Gemini model '{model_config.get('name')}'."
|
|
252
280
|
)
|
|
253
281
|
return None
|
|
254
282
|
|
|
@@ -258,10 +286,10 @@ class ModelFactory:
|
|
|
258
286
|
return model
|
|
259
287
|
|
|
260
288
|
elif model_type == "openai":
|
|
261
|
-
api_key =
|
|
289
|
+
api_key = get_api_key("OPENAI_API_KEY")
|
|
262
290
|
if not api_key:
|
|
263
291
|
emit_warning(
|
|
264
|
-
f"OPENAI_API_KEY is not set; skipping OpenAI model '{model_config.get('name')}'."
|
|
292
|
+
f"OPENAI_API_KEY is not set (check config or environment); skipping OpenAI model '{model_config.get('name')}'."
|
|
265
293
|
)
|
|
266
294
|
return None
|
|
267
295
|
|
|
@@ -275,10 +303,10 @@ class ModelFactory:
|
|
|
275
303
|
return model
|
|
276
304
|
|
|
277
305
|
elif model_type == "anthropic":
|
|
278
|
-
api_key =
|
|
306
|
+
api_key = get_api_key("ANTHROPIC_API_KEY")
|
|
279
307
|
if not api_key:
|
|
280
308
|
emit_warning(
|
|
281
|
-
f"ANTHROPIC_API_KEY is not set; skipping Anthropic model '{model_config.get('name')}'."
|
|
309
|
+
f"ANTHROPIC_API_KEY is not set (check config or environment); skipping Anthropic model '{model_config.get('name')}'."
|
|
282
310
|
)
|
|
283
311
|
return None
|
|
284
312
|
|
|
@@ -292,9 +320,21 @@ class ModelFactory:
|
|
|
292
320
|
http2=http2_enabled,
|
|
293
321
|
)
|
|
294
322
|
|
|
323
|
+
# Check if interleaved thinking is enabled for this model
|
|
324
|
+
# Only applies to Claude 4 models (Opus 4.5, Opus 4.1, Opus 4, Sonnet 4)
|
|
325
|
+
from code_puppy.config import get_effective_model_settings
|
|
326
|
+
|
|
327
|
+
effective_settings = get_effective_model_settings(model_name)
|
|
328
|
+
interleaved_thinking = effective_settings.get("interleaved_thinking", False)
|
|
329
|
+
|
|
330
|
+
default_headers = {}
|
|
331
|
+
if interleaved_thinking:
|
|
332
|
+
default_headers["anthropic-beta"] = "interleaved-thinking-2025-05-14"
|
|
333
|
+
|
|
295
334
|
anthropic_client = AsyncAnthropic(
|
|
296
335
|
api_key=api_key,
|
|
297
336
|
http_client=client,
|
|
337
|
+
default_headers=default_headers if default_headers else None,
|
|
298
338
|
)
|
|
299
339
|
|
|
300
340
|
# Ensure cache_control is injected at the Anthropic SDK layer
|
|
@@ -324,10 +364,21 @@ class ModelFactory:
|
|
|
324
364
|
http2=http2_enabled,
|
|
325
365
|
)
|
|
326
366
|
|
|
367
|
+
# Check if interleaved thinking is enabled for this model
|
|
368
|
+
from code_puppy.config import get_effective_model_settings
|
|
369
|
+
|
|
370
|
+
effective_settings = get_effective_model_settings(model_name)
|
|
371
|
+
interleaved_thinking = effective_settings.get("interleaved_thinking", False)
|
|
372
|
+
|
|
373
|
+
default_headers = {}
|
|
374
|
+
if interleaved_thinking:
|
|
375
|
+
default_headers["anthropic-beta"] = "interleaved-thinking-2025-05-14"
|
|
376
|
+
|
|
327
377
|
anthropic_client = AsyncAnthropic(
|
|
328
378
|
base_url=url,
|
|
329
379
|
http_client=client,
|
|
330
380
|
api_key=api_key,
|
|
381
|
+
default_headers=default_headers if default_headers else None,
|
|
331
382
|
)
|
|
332
383
|
|
|
333
384
|
# Ensure cache_control is injected at the Anthropic SDK layer
|
|
@@ -343,6 +394,31 @@ class ModelFactory:
|
|
|
343
394
|
)
|
|
344
395
|
return None
|
|
345
396
|
|
|
397
|
+
# Check if interleaved thinking is enabled (defaults to True for OAuth models)
|
|
398
|
+
from code_puppy.config import get_effective_model_settings
|
|
399
|
+
|
|
400
|
+
effective_settings = get_effective_model_settings(model_name)
|
|
401
|
+
interleaved_thinking = effective_settings.get("interleaved_thinking", True)
|
|
402
|
+
|
|
403
|
+
# Handle anthropic-beta header based on interleaved_thinking setting
|
|
404
|
+
if "anthropic-beta" in headers:
|
|
405
|
+
beta_parts = [p.strip() for p in headers["anthropic-beta"].split(",")]
|
|
406
|
+
if interleaved_thinking:
|
|
407
|
+
# Ensure interleaved-thinking is in the header
|
|
408
|
+
if "interleaved-thinking-2025-05-14" not in beta_parts:
|
|
409
|
+
beta_parts.append("interleaved-thinking-2025-05-14")
|
|
410
|
+
else:
|
|
411
|
+
# Remove interleaved-thinking from the header
|
|
412
|
+
beta_parts = [
|
|
413
|
+
p for p in beta_parts if "interleaved-thinking" not in p
|
|
414
|
+
]
|
|
415
|
+
headers["anthropic-beta"] = ",".join(beta_parts) if beta_parts else None
|
|
416
|
+
if headers.get("anthropic-beta") is None:
|
|
417
|
+
del headers["anthropic-beta"]
|
|
418
|
+
elif interleaved_thinking:
|
|
419
|
+
# No existing beta header, add one for interleaved thinking
|
|
420
|
+
headers["anthropic-beta"] = "interleaved-thinking-2025-05-14"
|
|
421
|
+
|
|
346
422
|
# Use a dedicated client wrapper that injects cache_control on /v1/messages
|
|
347
423
|
if verify is None:
|
|
348
424
|
verify = get_cert_bundle_path()
|
|
@@ -376,10 +452,10 @@ class ModelFactory:
|
|
|
376
452
|
)
|
|
377
453
|
azure_endpoint = azure_endpoint_config
|
|
378
454
|
if azure_endpoint_config.startswith("$"):
|
|
379
|
-
azure_endpoint =
|
|
455
|
+
azure_endpoint = get_api_key(azure_endpoint_config[1:])
|
|
380
456
|
if not azure_endpoint:
|
|
381
457
|
emit_warning(
|
|
382
|
-
f"Azure OpenAI endpoint
|
|
458
|
+
f"Azure OpenAI endpoint '{azure_endpoint_config[1:] if azure_endpoint_config.startswith('$') else azure_endpoint_config}' not found (check config or environment); skipping model '{model_config.get('name')}'."
|
|
383
459
|
)
|
|
384
460
|
return None
|
|
385
461
|
|
|
@@ -390,10 +466,10 @@ class ModelFactory:
|
|
|
390
466
|
)
|
|
391
467
|
api_version = api_version_config
|
|
392
468
|
if api_version_config.startswith("$"):
|
|
393
|
-
api_version =
|
|
469
|
+
api_version = get_api_key(api_version_config[1:])
|
|
394
470
|
if not api_version:
|
|
395
471
|
emit_warning(
|
|
396
|
-
f"Azure OpenAI API version
|
|
472
|
+
f"Azure OpenAI API version '{api_version_config[1:] if api_version_config.startswith('$') else api_version_config}' not found (check config or environment); skipping model '{model_config.get('name')}'."
|
|
397
473
|
)
|
|
398
474
|
return None
|
|
399
475
|
|
|
@@ -404,10 +480,10 @@ class ModelFactory:
|
|
|
404
480
|
)
|
|
405
481
|
api_key = api_key_config
|
|
406
482
|
if api_key_config.startswith("$"):
|
|
407
|
-
api_key =
|
|
483
|
+
api_key = get_api_key(api_key_config[1:])
|
|
408
484
|
if not api_key:
|
|
409
485
|
emit_warning(
|
|
410
|
-
f"Azure OpenAI API key
|
|
486
|
+
f"Azure OpenAI API key '{api_key_config[1:] if api_key_config.startswith('$') else api_key_config}' not found (check config or environment); skipping model '{model_config.get('name')}'."
|
|
411
487
|
)
|
|
412
488
|
return None
|
|
413
489
|
|
|
@@ -441,10 +517,10 @@ class ModelFactory:
|
|
|
441
517
|
setattr(model, "provider", provider)
|
|
442
518
|
return model
|
|
443
519
|
elif model_type == "zai_coding":
|
|
444
|
-
api_key =
|
|
520
|
+
api_key = get_api_key("ZAI_API_KEY")
|
|
445
521
|
if not api_key:
|
|
446
522
|
emit_warning(
|
|
447
|
-
f"ZAI_API_KEY is not set; skipping ZAI coding model '{model_config.get('name')}'."
|
|
523
|
+
f"ZAI_API_KEY is not set (check config or environment); skipping ZAI coding model '{model_config.get('name')}'."
|
|
448
524
|
)
|
|
449
525
|
return None
|
|
450
526
|
provider = OpenAIProvider(
|
|
@@ -458,10 +534,10 @@ class ModelFactory:
|
|
|
458
534
|
setattr(zai_model, "provider", provider)
|
|
459
535
|
return zai_model
|
|
460
536
|
elif model_type == "zai_api":
|
|
461
|
-
api_key =
|
|
537
|
+
api_key = get_api_key("ZAI_API_KEY")
|
|
462
538
|
if not api_key:
|
|
463
539
|
emit_warning(
|
|
464
|
-
f"ZAI_API_KEY is not set; skipping ZAI API model '{model_config.get('name')}'."
|
|
540
|
+
f"ZAI_API_KEY is not set (check config or environment); skipping ZAI API model '{model_config.get('name')}'."
|
|
465
541
|
)
|
|
466
542
|
return None
|
|
467
543
|
provider = OpenAIProvider(
|
|
@@ -481,24 +557,94 @@ class ModelFactory:
|
|
|
481
557
|
f"API key is not set for custom Gemini endpoint; skipping model '{model_config.get('name')}'."
|
|
482
558
|
)
|
|
483
559
|
return None
|
|
484
|
-
os.environ["GEMINI_API_KEY"] = api_key
|
|
485
560
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
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
|
+
)
|
|
489
621
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
622
|
+
provider = GoogleProvider(
|
|
623
|
+
api_key=api_key, base_url=url, http_client=client
|
|
624
|
+
)
|
|
493
625
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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
|
+
)
|
|
499
635
|
|
|
500
|
-
|
|
501
|
-
|
|
636
|
+
return model
|
|
637
|
+
|
|
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)
|
|
645
|
+
|
|
646
|
+
provider = GoogleProvider(api_key=api_key, base_url=url, http_client=client)
|
|
647
|
+
model = GoogleModel(model_name=model_config["name"], provider=provider)
|
|
502
648
|
return model
|
|
503
649
|
elif model_type == "cerebras":
|
|
504
650
|
|
|
@@ -537,21 +683,21 @@ class ModelFactory:
|
|
|
537
683
|
if api_key_config.startswith("$"):
|
|
538
684
|
# It's an environment variable reference
|
|
539
685
|
env_var_name = api_key_config[1:] # Remove the $ prefix
|
|
540
|
-
api_key =
|
|
686
|
+
api_key = get_api_key(env_var_name)
|
|
541
687
|
if api_key is None:
|
|
542
688
|
emit_warning(
|
|
543
|
-
f"OpenRouter API key
|
|
689
|
+
f"OpenRouter API key '{env_var_name}' not found (check config or environment); skipping model '{model_config.get('name')}'."
|
|
544
690
|
)
|
|
545
691
|
return None
|
|
546
692
|
else:
|
|
547
693
|
# It's a raw API key value
|
|
548
694
|
api_key = api_key_config
|
|
549
695
|
else:
|
|
550
|
-
# No API key in config, try to get it from the default environment variable
|
|
551
|
-
api_key =
|
|
696
|
+
# No API key in config, try to get it from config or the default environment variable
|
|
697
|
+
api_key = get_api_key("OPENROUTER_API_KEY")
|
|
552
698
|
if api_key is None:
|
|
553
699
|
emit_warning(
|
|
554
|
-
f"OPENROUTER_API_KEY is not set; skipping OpenRouter model '{model_config.get('name')}'."
|
|
700
|
+
f"OPENROUTER_API_KEY is not set (check config or environment); skipping OpenRouter model '{model_config.get('name')}'."
|
|
555
701
|
)
|
|
556
702
|
return None
|
|
557
703
|
|
|
@@ -618,6 +764,86 @@ class ModelFactory:
|
|
|
618
764
|
)
|
|
619
765
|
return model
|
|
620
766
|
|
|
767
|
+
elif model_type == "chatgpt_oauth":
|
|
768
|
+
# ChatGPT OAuth models use the Codex API at chatgpt.com
|
|
769
|
+
try:
|
|
770
|
+
try:
|
|
771
|
+
from chatgpt_oauth.config import CHATGPT_OAUTH_CONFIG
|
|
772
|
+
from chatgpt_oauth.utils import (
|
|
773
|
+
get_valid_access_token,
|
|
774
|
+
load_stored_tokens,
|
|
775
|
+
)
|
|
776
|
+
except ImportError:
|
|
777
|
+
from code_puppy.plugins.chatgpt_oauth.config import (
|
|
778
|
+
CHATGPT_OAUTH_CONFIG,
|
|
779
|
+
)
|
|
780
|
+
from code_puppy.plugins.chatgpt_oauth.utils import (
|
|
781
|
+
get_valid_access_token,
|
|
782
|
+
load_stored_tokens,
|
|
783
|
+
)
|
|
784
|
+
except ImportError as exc:
|
|
785
|
+
emit_warning(
|
|
786
|
+
f"ChatGPT OAuth plugin not available; skipping model '{model_config.get('name')}'. "
|
|
787
|
+
f"Error: {exc}"
|
|
788
|
+
)
|
|
789
|
+
return None
|
|
790
|
+
|
|
791
|
+
# Get a valid access token (refreshing if needed)
|
|
792
|
+
access_token = get_valid_access_token()
|
|
793
|
+
if not access_token:
|
|
794
|
+
emit_warning(
|
|
795
|
+
f"Failed to get valid ChatGPT OAuth token; skipping model '{model_config.get('name')}'. "
|
|
796
|
+
"Run /chatgpt-auth to authenticate."
|
|
797
|
+
)
|
|
798
|
+
return None
|
|
799
|
+
|
|
800
|
+
# Get account_id from stored tokens (required for ChatGPT-Account-Id header)
|
|
801
|
+
tokens = load_stored_tokens()
|
|
802
|
+
account_id = tokens.get("account_id", "") if tokens else ""
|
|
803
|
+
if not account_id:
|
|
804
|
+
emit_warning(
|
|
805
|
+
f"No account_id found in ChatGPT OAuth tokens; skipping model '{model_config.get('name')}'. "
|
|
806
|
+
"Run /chatgpt-auth to re-authenticate."
|
|
807
|
+
)
|
|
808
|
+
return None
|
|
809
|
+
|
|
810
|
+
# Build headers for ChatGPT Codex API
|
|
811
|
+
originator = CHATGPT_OAUTH_CONFIG.get("originator", "codex_cli_rs")
|
|
812
|
+
client_version = CHATGPT_OAUTH_CONFIG.get("client_version", "0.72.0")
|
|
813
|
+
|
|
814
|
+
headers = {
|
|
815
|
+
"ChatGPT-Account-Id": account_id,
|
|
816
|
+
"originator": originator,
|
|
817
|
+
"User-Agent": f"{originator}/{client_version}",
|
|
818
|
+
}
|
|
819
|
+
# Merge with any headers from model config
|
|
820
|
+
config_headers = model_config.get("custom_endpoint", {}).get("headers", {})
|
|
821
|
+
headers.update(config_headers)
|
|
822
|
+
|
|
823
|
+
# Get base URL - Codex API uses chatgpt.com, not api.openai.com
|
|
824
|
+
base_url = model_config.get("custom_endpoint", {}).get(
|
|
825
|
+
"url", CHATGPT_OAUTH_CONFIG["api_base_url"]
|
|
826
|
+
)
|
|
827
|
+
|
|
828
|
+
# Create HTTP client with Codex interceptor for store=false injection
|
|
829
|
+
from code_puppy.chatgpt_codex_client import create_codex_async_client
|
|
830
|
+
|
|
831
|
+
verify = get_cert_bundle_path()
|
|
832
|
+
client = create_codex_async_client(headers=headers, verify=verify)
|
|
833
|
+
|
|
834
|
+
provider = OpenAIProvider(
|
|
835
|
+
api_key=access_token,
|
|
836
|
+
base_url=base_url,
|
|
837
|
+
http_client=client,
|
|
838
|
+
)
|
|
839
|
+
|
|
840
|
+
# ChatGPT Codex API only supports Responses format
|
|
841
|
+
model = OpenAIResponsesModel(
|
|
842
|
+
model_name=model_config["name"], provider=provider
|
|
843
|
+
)
|
|
844
|
+
setattr(model, "provider", provider)
|
|
845
|
+
return model
|
|
846
|
+
|
|
621
847
|
elif model_type == "round_robin":
|
|
622
848
|
# Get the list of model names to use in the round-robin
|
|
623
849
|
model_names = model_config.get("models")
|