code-puppy 0.0.373__py3-none-any.whl → 0.0.375__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/agent_creator_agent.py +49 -1
- code_puppy/agents/agent_helios.py +122 -0
- code_puppy/agents/agent_manager.py +60 -4
- code_puppy/agents/base_agent.py +61 -4
- code_puppy/agents/json_agent.py +30 -7
- code_puppy/callbacks.py +125 -0
- code_puppy/command_line/colors_menu.py +2 -0
- code_puppy/command_line/command_handler.py +1 -0
- code_puppy/command_line/config_commands.py +3 -1
- code_puppy/command_line/uc_menu.py +890 -0
- code_puppy/config.py +29 -0
- code_puppy/messaging/messages.py +18 -0
- code_puppy/messaging/rich_renderer.py +48 -7
- code_puppy/messaging/subagent_console.py +0 -1
- code_puppy/model_factory.py +63 -258
- code_puppy/model_utils.py +33 -1
- code_puppy/plugins/antigravity_oauth/register_callbacks.py +106 -1
- code_puppy/plugins/antigravity_oauth/utils.py +2 -3
- code_puppy/plugins/chatgpt_oauth/register_callbacks.py +85 -3
- code_puppy/plugins/claude_code_oauth/register_callbacks.py +88 -0
- code_puppy/plugins/ralph/__init__.py +13 -0
- code_puppy/plugins/ralph/agents.py +433 -0
- code_puppy/plugins/ralph/commands.py +208 -0
- code_puppy/plugins/ralph/loop_controller.py +285 -0
- code_puppy/plugins/ralph/models.py +125 -0
- code_puppy/plugins/ralph/register_callbacks.py +133 -0
- code_puppy/plugins/ralph/state_manager.py +322 -0
- code_puppy/plugins/ralph/tools.py +451 -0
- code_puppy/plugins/universal_constructor/__init__.py +13 -0
- code_puppy/plugins/universal_constructor/models.py +138 -0
- code_puppy/plugins/universal_constructor/register_callbacks.py +47 -0
- code_puppy/plugins/universal_constructor/registry.py +304 -0
- code_puppy/plugins/universal_constructor/sandbox.py +584 -0
- code_puppy/tools/__init__.py +169 -1
- code_puppy/tools/agent_tools.py +1 -1
- code_puppy/tools/command_runner.py +23 -9
- code_puppy/tools/universal_constructor.py +889 -0
- {code_puppy-0.0.373.dist-info → code_puppy-0.0.375.dist-info}/METADATA +1 -1
- {code_puppy-0.0.373.dist-info → code_puppy-0.0.375.dist-info}/RECORD +44 -28
- {code_puppy-0.0.373.data → code_puppy-0.0.375.data}/data/code_puppy/models.json +0 -0
- {code_puppy-0.0.373.data → code_puppy-0.0.375.data}/data/code_puppy/models_dev_api.json +0 -0
- {code_puppy-0.0.373.dist-info → code_puppy-0.0.375.dist-info}/WHEEL +0 -0
- {code_puppy-0.0.373.dist-info → code_puppy-0.0.375.dist-info}/entry_points.txt +0 -0
- {code_puppy-0.0.373.dist-info → code_puppy-0.0.375.dist-info}/licenses/LICENSE +0 -0
code_puppy/config.py
CHANGED
|
@@ -101,6 +101,9 @@ PACK_AGENT_NAMES = frozenset(
|
|
|
101
101
|
]
|
|
102
102
|
)
|
|
103
103
|
|
|
104
|
+
# Agents that require Universal Constructor to be enabled
|
|
105
|
+
UC_AGENT_NAMES = frozenset(["helios"])
|
|
106
|
+
|
|
104
107
|
|
|
105
108
|
def get_pack_agents_enabled() -> bool:
|
|
106
109
|
"""Return True if pack agents are enabled (default False).
|
|
@@ -117,6 +120,30 @@ def get_pack_agents_enabled() -> bool:
|
|
|
117
120
|
return str(cfg_val).strip().lower() in {"1", "true", "yes", "on"}
|
|
118
121
|
|
|
119
122
|
|
|
123
|
+
def get_universal_constructor_enabled() -> bool:
|
|
124
|
+
"""Return True if the Universal Constructor is enabled (default True).
|
|
125
|
+
|
|
126
|
+
The Universal Constructor allows agents to dynamically create, manage,
|
|
127
|
+
and execute custom tools at runtime. When enabled, agents can extend
|
|
128
|
+
their capabilities by writing Python code that becomes callable tools.
|
|
129
|
+
|
|
130
|
+
When False, the universal_constructor tool is not registered with agents.
|
|
131
|
+
"""
|
|
132
|
+
cfg_val = get_value("enable_universal_constructor")
|
|
133
|
+
if cfg_val is None:
|
|
134
|
+
return True # Enabled by default
|
|
135
|
+
return str(cfg_val).strip().lower() in {"1", "true", "yes", "on"}
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def set_universal_constructor_enabled(enabled: bool) -> None:
|
|
139
|
+
"""Enable or disable the Universal Constructor.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
enabled: True to enable, False to disable
|
|
143
|
+
"""
|
|
144
|
+
set_value("enable_universal_constructor", "true" if enabled else "false")
|
|
145
|
+
|
|
146
|
+
|
|
120
147
|
DEFAULT_SECTION = "puppy"
|
|
121
148
|
REQUIRED_KEYS = ["puppy_name", "owner_name"]
|
|
122
149
|
|
|
@@ -260,6 +287,8 @@ def get_config_keys():
|
|
|
260
287
|
default_keys.append("enable_dbos")
|
|
261
288
|
# Add pack agents control key
|
|
262
289
|
default_keys.append("enable_pack_agents")
|
|
290
|
+
# Add universal constructor control key
|
|
291
|
+
default_keys.append("enable_universal_constructor")
|
|
263
292
|
# Add cancel agent key configuration
|
|
264
293
|
default_keys.append("cancel_agent_key")
|
|
265
294
|
# Add banner color keys
|
code_puppy/messaging/messages.py
CHANGED
|
@@ -317,6 +317,21 @@ class SubAgentStatusMessage(BaseMessage):
|
|
|
317
317
|
)
|
|
318
318
|
|
|
319
319
|
|
|
320
|
+
class UniversalConstructorMessage(BaseMessage):
|
|
321
|
+
"""Result of a universal_constructor operation."""
|
|
322
|
+
|
|
323
|
+
category: MessageCategory = MessageCategory.TOOL_OUTPUT
|
|
324
|
+
action: str = Field(
|
|
325
|
+
description="The UC action performed (list/call/create/update/info)"
|
|
326
|
+
)
|
|
327
|
+
tool_name: Optional[str] = Field(
|
|
328
|
+
default=None, description="Tool name if applicable"
|
|
329
|
+
)
|
|
330
|
+
success: bool = Field(description="Whether the operation succeeded")
|
|
331
|
+
summary: str = Field(description="Brief summary of the result")
|
|
332
|
+
details: Optional[str] = Field(default=None, description="Additional details")
|
|
333
|
+
|
|
334
|
+
|
|
320
335
|
# =============================================================================
|
|
321
336
|
# User Interaction Messages (Agent → User)
|
|
322
337
|
# =============================================================================
|
|
@@ -443,6 +458,7 @@ AnyMessage = Union[
|
|
|
443
458
|
SubAgentInvocationMessage,
|
|
444
459
|
SubAgentResponseMessage,
|
|
445
460
|
SubAgentStatusMessage,
|
|
461
|
+
UniversalConstructorMessage,
|
|
446
462
|
UserInputRequest,
|
|
447
463
|
ConfirmationRequest,
|
|
448
464
|
SelectionRequest,
|
|
@@ -485,6 +501,8 @@ __all__ = [
|
|
|
485
501
|
"SubAgentInvocationMessage",
|
|
486
502
|
"SubAgentResponseMessage",
|
|
487
503
|
"SubAgentStatusMessage",
|
|
504
|
+
# Universal Constructor
|
|
505
|
+
"UniversalConstructorMessage",
|
|
488
506
|
# User interaction
|
|
489
507
|
"UserInputRequest",
|
|
490
508
|
"ConfirmationRequest",
|
|
@@ -48,6 +48,7 @@ from .messages import (
|
|
|
48
48
|
SubAgentInvocationMessage,
|
|
49
49
|
SubAgentResponseMessage,
|
|
50
50
|
TextMessage,
|
|
51
|
+
UniversalConstructorMessage,
|
|
51
52
|
UserInputRequest,
|
|
52
53
|
VersionCheckMessage,
|
|
53
54
|
)
|
|
@@ -287,6 +288,8 @@ class RichConsoleRenderer:
|
|
|
287
288
|
elif isinstance(message, SubAgentResponseMessage):
|
|
288
289
|
# Skip rendering - we now display sub-agent responses via display_non_streamed_result
|
|
289
290
|
pass
|
|
291
|
+
elif isinstance(message, UniversalConstructorMessage):
|
|
292
|
+
self._render_universal_constructor(message)
|
|
290
293
|
elif isinstance(message, UserInputRequest):
|
|
291
294
|
# Can't handle async user input in sync context - skip
|
|
292
295
|
self._console.print("[dim]User input requested (requires async)[/dim]")
|
|
@@ -672,15 +675,21 @@ class RichConsoleRenderer:
|
|
|
672
675
|
self._console.print(f"[dim]⏱ Timeout: {msg.timeout}s[/dim]")
|
|
673
676
|
|
|
674
677
|
def _render_shell_line(self, msg: ShellLineMessage) -> None:
|
|
675
|
-
"""Render shell output line preserving ANSI codes."""
|
|
676
|
-
|
|
678
|
+
"""Render shell output line preserving ANSI codes and carriage returns."""
|
|
679
|
+
import sys
|
|
677
680
|
|
|
678
|
-
|
|
679
|
-
# This preserves colors while still being safe
|
|
680
|
-
text = Text.from_ansi(msg.line)
|
|
681
|
+
from rich.text import Text
|
|
681
682
|
|
|
682
|
-
#
|
|
683
|
-
|
|
683
|
+
# Check if line contains carriage return (progress bar style output)
|
|
684
|
+
if "\r" in msg.line:
|
|
685
|
+
# Bypass Rich entirely - write directly to stdout so terminal interprets \r
|
|
686
|
+
# Apply dim styling manually via ANSI codes
|
|
687
|
+
sys.stdout.write(f"\033[2m{msg.line}\033[0m")
|
|
688
|
+
sys.stdout.flush()
|
|
689
|
+
else:
|
|
690
|
+
# Normal line: use Rich for nice formatting
|
|
691
|
+
text = Text.from_ansi(msg.line)
|
|
692
|
+
self._console.print(text, style="dim")
|
|
684
693
|
|
|
685
694
|
def _render_shell_output(self, msg: ShellOutputMessage) -> None:
|
|
686
695
|
"""Render shell command output - just a trailing newline for spinner separation.
|
|
@@ -775,6 +784,38 @@ class RichConsoleRenderer:
|
|
|
775
784
|
f"({msg.message_count} messages)[/dim]"
|
|
776
785
|
)
|
|
777
786
|
|
|
787
|
+
def _render_universal_constructor(self, msg: UniversalConstructorMessage) -> None:
|
|
788
|
+
"""Render universal_constructor tool output with banner."""
|
|
789
|
+
# Skip for sub-agents unless verbose mode
|
|
790
|
+
if self._should_suppress_subagent_output():
|
|
791
|
+
return
|
|
792
|
+
|
|
793
|
+
# Format banner
|
|
794
|
+
banner = self._format_banner("universal_constructor", "UNIVERSAL CONSTRUCTOR")
|
|
795
|
+
|
|
796
|
+
# Build the header line with action and optional tool name
|
|
797
|
+
# Escape user-controlled strings to prevent Rich markup injection
|
|
798
|
+
header_parts = [f"\n{banner} 🔧 [bold cyan]{msg.action.upper()}[/bold cyan]"]
|
|
799
|
+
if msg.tool_name:
|
|
800
|
+
safe_tool_name = escape_rich_markup(msg.tool_name)
|
|
801
|
+
header_parts.append(f" [dim]tool=[/dim][bold]{safe_tool_name}[/bold]")
|
|
802
|
+
self._console.print("".join(header_parts))
|
|
803
|
+
|
|
804
|
+
# Status indicator
|
|
805
|
+
safe_summary = escape_rich_markup(msg.summary) if msg.summary else ""
|
|
806
|
+
if msg.success:
|
|
807
|
+
self._console.print(f"[green]✓[/green] {safe_summary}")
|
|
808
|
+
else:
|
|
809
|
+
self._console.print(f"[red]✗[/red] {safe_summary}")
|
|
810
|
+
|
|
811
|
+
# Show details if present
|
|
812
|
+
if msg.details:
|
|
813
|
+
safe_details = escape_rich_markup(msg.details)
|
|
814
|
+
self._console.print(f"[dim]{safe_details}[/dim]")
|
|
815
|
+
|
|
816
|
+
# Trailing newline for spinner separation
|
|
817
|
+
self._console.print()
|
|
818
|
+
|
|
778
819
|
# =========================================================================
|
|
779
820
|
# User Interaction
|
|
780
821
|
# =========================================================================
|
|
@@ -24,7 +24,6 @@ from rich.text import Text
|
|
|
24
24
|
|
|
25
25
|
from code_puppy.messaging.messages import SubAgentStatusMessage
|
|
26
26
|
|
|
27
|
-
|
|
28
27
|
# =============================================================================
|
|
29
28
|
# Status Configuration
|
|
30
29
|
# =============================================================================
|
code_puppy/model_factory.py
CHANGED
|
@@ -393,78 +393,9 @@ class ModelFactory:
|
|
|
393
393
|
|
|
394
394
|
provider = AnthropicProvider(anthropic_client=anthropic_client)
|
|
395
395
|
return AnthropicModel(model_name=model_config["name"], provider=provider)
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
if model_config.get("oauth_source") == "claude-code-plugin":
|
|
399
|
-
try:
|
|
400
|
-
from code_puppy.plugins.claude_code_oauth.utils import (
|
|
401
|
-
get_valid_access_token,
|
|
402
|
-
)
|
|
403
|
-
|
|
404
|
-
refreshed_token = get_valid_access_token()
|
|
405
|
-
if refreshed_token:
|
|
406
|
-
api_key = refreshed_token
|
|
407
|
-
custom_endpoint = model_config.get("custom_endpoint")
|
|
408
|
-
if isinstance(custom_endpoint, dict):
|
|
409
|
-
custom_endpoint["api_key"] = refreshed_token
|
|
410
|
-
except ImportError:
|
|
411
|
-
pass
|
|
412
|
-
if not api_key:
|
|
413
|
-
emit_warning(
|
|
414
|
-
f"API key is not set for Claude Code endpoint; skipping model '{model_config.get('name')}'."
|
|
415
|
-
)
|
|
416
|
-
return None
|
|
417
|
-
|
|
418
|
-
# Check if interleaved thinking is enabled (defaults to True for OAuth models)
|
|
419
|
-
from code_puppy.config import get_effective_model_settings
|
|
420
|
-
|
|
421
|
-
effective_settings = get_effective_model_settings(model_name)
|
|
422
|
-
interleaved_thinking = effective_settings.get("interleaved_thinking", True)
|
|
423
|
-
|
|
424
|
-
# Handle anthropic-beta header based on interleaved_thinking setting
|
|
425
|
-
if "anthropic-beta" in headers:
|
|
426
|
-
beta_parts = [p.strip() for p in headers["anthropic-beta"].split(",")]
|
|
427
|
-
if interleaved_thinking:
|
|
428
|
-
# Ensure interleaved-thinking is in the header
|
|
429
|
-
if "interleaved-thinking-2025-05-14" not in beta_parts:
|
|
430
|
-
beta_parts.append("interleaved-thinking-2025-05-14")
|
|
431
|
-
else:
|
|
432
|
-
# Remove interleaved-thinking from the header
|
|
433
|
-
beta_parts = [
|
|
434
|
-
p for p in beta_parts if "interleaved-thinking" not in p
|
|
435
|
-
]
|
|
436
|
-
headers["anthropic-beta"] = ",".join(beta_parts) if beta_parts else None
|
|
437
|
-
if headers.get("anthropic-beta") is None:
|
|
438
|
-
del headers["anthropic-beta"]
|
|
439
|
-
elif interleaved_thinking:
|
|
440
|
-
# No existing beta header, add one for interleaved thinking
|
|
441
|
-
headers["anthropic-beta"] = "interleaved-thinking-2025-05-14"
|
|
442
|
-
|
|
443
|
-
# Use a dedicated client wrapper that injects cache_control on /v1/messages
|
|
444
|
-
if verify is None:
|
|
445
|
-
verify = get_cert_bundle_path()
|
|
446
|
-
|
|
447
|
-
http2_enabled = get_http2()
|
|
448
|
-
|
|
449
|
-
client = ClaudeCacheAsyncClient(
|
|
450
|
-
headers=headers,
|
|
451
|
-
verify=verify,
|
|
452
|
-
timeout=180,
|
|
453
|
-
http2=http2_enabled,
|
|
454
|
-
)
|
|
396
|
+
# NOTE: 'claude_code' model type is now handled by the claude_code_oauth plugin
|
|
397
|
+
# via the register_model_type callback. See plugins/claude_code_oauth/register_callbacks.py
|
|
455
398
|
|
|
456
|
-
anthropic_client = AsyncAnthropic(
|
|
457
|
-
base_url=url,
|
|
458
|
-
http_client=client,
|
|
459
|
-
auth_token=api_key,
|
|
460
|
-
)
|
|
461
|
-
# Ensure cache_control is injected at the Anthropic SDK layer too
|
|
462
|
-
# so we don't depend solely on httpx internals.
|
|
463
|
-
patch_anthropic_client_messages(anthropic_client)
|
|
464
|
-
anthropic_client.api_key = None
|
|
465
|
-
anthropic_client.auth_token = api_key
|
|
466
|
-
provider = AnthropicProvider(anthropic_client=anthropic_client)
|
|
467
|
-
return AnthropicModel(model_name=model_config["name"], provider=provider)
|
|
468
399
|
elif model_type == "azure_openai":
|
|
469
400
|
azure_endpoint_config = model_config.get("azure_endpoint")
|
|
470
401
|
if not azure_endpoint_config:
|
|
@@ -571,7 +502,42 @@ class ModelFactory:
|
|
|
571
502
|
)
|
|
572
503
|
setattr(zai_model, "provider", provider)
|
|
573
504
|
return zai_model
|
|
505
|
+
# NOTE: 'antigravity' model type is now handled by the antigravity_oauth plugin
|
|
506
|
+
# via the register_model_type callback. See plugins/antigravity_oauth/register_callbacks.py
|
|
507
|
+
|
|
574
508
|
elif model_type == "custom_gemini":
|
|
509
|
+
# Backwards compatibility: delegate to antigravity plugin if antigravity flag is set
|
|
510
|
+
# New configs use type="antigravity" directly, but old configs may have
|
|
511
|
+
# type="custom_gemini" with antigravity=True
|
|
512
|
+
if model_config.get("antigravity"):
|
|
513
|
+
# Find and call the antigravity handler from the plugin
|
|
514
|
+
registered_handlers = callbacks.on_register_model_types()
|
|
515
|
+
for handler_info in registered_handlers:
|
|
516
|
+
handlers = (
|
|
517
|
+
handler_info
|
|
518
|
+
if isinstance(handler_info, list)
|
|
519
|
+
else [handler_info]
|
|
520
|
+
if handler_info
|
|
521
|
+
else []
|
|
522
|
+
)
|
|
523
|
+
for handler_entry in handlers:
|
|
524
|
+
if (
|
|
525
|
+
isinstance(handler_entry, dict)
|
|
526
|
+
and handler_entry.get("type") == "antigravity"
|
|
527
|
+
):
|
|
528
|
+
handler = handler_entry.get("handler")
|
|
529
|
+
if callable(handler):
|
|
530
|
+
try:
|
|
531
|
+
return handler(model_name, model_config, config)
|
|
532
|
+
except Exception as e:
|
|
533
|
+
logger.error(f"Antigravity handler failed: {e}")
|
|
534
|
+
return None
|
|
535
|
+
# If no antigravity handler found, warn and fall through
|
|
536
|
+
emit_warning(
|
|
537
|
+
f"Model '{model_config.get('name')}' has antigravity=True but antigravity plugin not loaded."
|
|
538
|
+
)
|
|
539
|
+
return None
|
|
540
|
+
|
|
575
541
|
url, headers, verify, api_key = get_custom_config(model_config)
|
|
576
542
|
if not api_key:
|
|
577
543
|
emit_warning(
|
|
@@ -579,114 +545,7 @@ class ModelFactory:
|
|
|
579
545
|
)
|
|
580
546
|
return None
|
|
581
547
|
|
|
582
|
-
|
|
583
|
-
if model_config.get("antigravity"):
|
|
584
|
-
try:
|
|
585
|
-
from code_puppy.plugins.antigravity_oauth.token import (
|
|
586
|
-
is_token_expired,
|
|
587
|
-
refresh_access_token,
|
|
588
|
-
)
|
|
589
|
-
from code_puppy.plugins.antigravity_oauth.transport import (
|
|
590
|
-
create_antigravity_client,
|
|
591
|
-
)
|
|
592
|
-
from code_puppy.plugins.antigravity_oauth.utils import (
|
|
593
|
-
load_stored_tokens,
|
|
594
|
-
save_tokens,
|
|
595
|
-
)
|
|
596
|
-
|
|
597
|
-
# Try to import custom model for thinking signatures
|
|
598
|
-
try:
|
|
599
|
-
from code_puppy.plugins.antigravity_oauth.antigravity_model import (
|
|
600
|
-
AntigravityModel,
|
|
601
|
-
)
|
|
602
|
-
except ImportError:
|
|
603
|
-
AntigravityModel = None
|
|
604
|
-
|
|
605
|
-
# Get fresh access token (refresh if needed)
|
|
606
|
-
tokens = load_stored_tokens()
|
|
607
|
-
if not tokens:
|
|
608
|
-
emit_warning(
|
|
609
|
-
"Antigravity tokens not found; run /antigravity-auth first."
|
|
610
|
-
)
|
|
611
|
-
return None
|
|
612
|
-
|
|
613
|
-
access_token = tokens.get("access_token", "")
|
|
614
|
-
refresh_token = tokens.get("refresh_token", "")
|
|
615
|
-
expires_at = tokens.get("expires_at")
|
|
616
|
-
|
|
617
|
-
# Refresh if expired or about to expire (initial check)
|
|
618
|
-
if is_token_expired(expires_at):
|
|
619
|
-
new_tokens = refresh_access_token(refresh_token)
|
|
620
|
-
if new_tokens:
|
|
621
|
-
access_token = new_tokens.access_token
|
|
622
|
-
refresh_token = new_tokens.refresh_token
|
|
623
|
-
expires_at = new_tokens.expires_at
|
|
624
|
-
tokens["access_token"] = new_tokens.access_token
|
|
625
|
-
tokens["refresh_token"] = new_tokens.refresh_token
|
|
626
|
-
tokens["expires_at"] = new_tokens.expires_at
|
|
627
|
-
save_tokens(tokens)
|
|
628
|
-
else:
|
|
629
|
-
emit_warning(
|
|
630
|
-
"Failed to refresh Antigravity token; run /antigravity-auth again."
|
|
631
|
-
)
|
|
632
|
-
return None
|
|
633
|
-
|
|
634
|
-
# Callback to persist tokens when proactively refreshed during session
|
|
635
|
-
def on_token_refreshed(new_tokens):
|
|
636
|
-
"""Persist new tokens when proactively refreshed."""
|
|
637
|
-
try:
|
|
638
|
-
updated_tokens = load_stored_tokens() or {}
|
|
639
|
-
updated_tokens["access_token"] = new_tokens.access_token
|
|
640
|
-
updated_tokens["refresh_token"] = new_tokens.refresh_token
|
|
641
|
-
updated_tokens["expires_at"] = new_tokens.expires_at
|
|
642
|
-
save_tokens(updated_tokens)
|
|
643
|
-
logger.debug(
|
|
644
|
-
"Persisted proactively refreshed Antigravity tokens"
|
|
645
|
-
)
|
|
646
|
-
except Exception as e:
|
|
647
|
-
logger.warning("Failed to persist refreshed tokens: %s", e)
|
|
648
|
-
|
|
649
|
-
project_id = tokens.get(
|
|
650
|
-
"project_id", model_config.get("project_id", "")
|
|
651
|
-
)
|
|
652
|
-
client = create_antigravity_client(
|
|
653
|
-
access_token=access_token,
|
|
654
|
-
project_id=project_id,
|
|
655
|
-
model_name=model_config["name"],
|
|
656
|
-
base_url=url,
|
|
657
|
-
headers=headers,
|
|
658
|
-
refresh_token=refresh_token,
|
|
659
|
-
expires_at=expires_at,
|
|
660
|
-
on_token_refreshed=on_token_refreshed,
|
|
661
|
-
)
|
|
662
|
-
|
|
663
|
-
# Use custom model with direct httpx client
|
|
664
|
-
if AntigravityModel:
|
|
665
|
-
model = AntigravityModel(
|
|
666
|
-
model_name=model_config["name"],
|
|
667
|
-
api_key=api_key
|
|
668
|
-
or "", # Antigravity uses OAuth, key may be empty
|
|
669
|
-
base_url=url,
|
|
670
|
-
http_client=client,
|
|
671
|
-
)
|
|
672
|
-
else:
|
|
673
|
-
model = GeminiModel(
|
|
674
|
-
model_name=model_config["name"],
|
|
675
|
-
api_key=api_key or "",
|
|
676
|
-
base_url=url,
|
|
677
|
-
http_client=client,
|
|
678
|
-
)
|
|
679
|
-
|
|
680
|
-
return model
|
|
681
|
-
|
|
682
|
-
except ImportError:
|
|
683
|
-
emit_warning(
|
|
684
|
-
f"Antigravity transport not available; skipping model '{model_config.get('name')}'."
|
|
685
|
-
)
|
|
686
|
-
return None
|
|
687
|
-
else:
|
|
688
|
-
client = create_async_client(headers=headers, verify=verify)
|
|
689
|
-
|
|
548
|
+
client = create_async_client(headers=headers, verify=verify)
|
|
690
549
|
model = GeminiModel(
|
|
691
550
|
model_name=model_config["name"],
|
|
692
551
|
api_key=api_key,
|
|
@@ -814,85 +673,8 @@ class ModelFactory:
|
|
|
814
673
|
)
|
|
815
674
|
return model
|
|
816
675
|
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
try:
|
|
820
|
-
try:
|
|
821
|
-
from chatgpt_oauth.config import CHATGPT_OAUTH_CONFIG
|
|
822
|
-
from chatgpt_oauth.utils import (
|
|
823
|
-
get_valid_access_token,
|
|
824
|
-
load_stored_tokens,
|
|
825
|
-
)
|
|
826
|
-
except ImportError:
|
|
827
|
-
from code_puppy.plugins.chatgpt_oauth.config import (
|
|
828
|
-
CHATGPT_OAUTH_CONFIG,
|
|
829
|
-
)
|
|
830
|
-
from code_puppy.plugins.chatgpt_oauth.utils import (
|
|
831
|
-
get_valid_access_token,
|
|
832
|
-
load_stored_tokens,
|
|
833
|
-
)
|
|
834
|
-
except ImportError as exc:
|
|
835
|
-
emit_warning(
|
|
836
|
-
f"ChatGPT OAuth plugin not available; skipping model '{model_config.get('name')}'. "
|
|
837
|
-
f"Error: {exc}"
|
|
838
|
-
)
|
|
839
|
-
return None
|
|
840
|
-
|
|
841
|
-
# Get a valid access token (refreshing if needed)
|
|
842
|
-
access_token = get_valid_access_token()
|
|
843
|
-
if not access_token:
|
|
844
|
-
emit_warning(
|
|
845
|
-
f"Failed to get valid ChatGPT OAuth token; skipping model '{model_config.get('name')}'. "
|
|
846
|
-
"Run /chatgpt-auth to authenticate."
|
|
847
|
-
)
|
|
848
|
-
return None
|
|
849
|
-
|
|
850
|
-
# Get account_id from stored tokens (required for ChatGPT-Account-Id header)
|
|
851
|
-
tokens = load_stored_tokens()
|
|
852
|
-
account_id = tokens.get("account_id", "") if tokens else ""
|
|
853
|
-
if not account_id:
|
|
854
|
-
emit_warning(
|
|
855
|
-
f"No account_id found in ChatGPT OAuth tokens; skipping model '{model_config.get('name')}'. "
|
|
856
|
-
"Run /chatgpt-auth to re-authenticate."
|
|
857
|
-
)
|
|
858
|
-
return None
|
|
859
|
-
|
|
860
|
-
# Build headers for ChatGPT Codex API
|
|
861
|
-
originator = CHATGPT_OAUTH_CONFIG.get("originator", "codex_cli_rs")
|
|
862
|
-
client_version = CHATGPT_OAUTH_CONFIG.get("client_version", "0.72.0")
|
|
863
|
-
|
|
864
|
-
headers = {
|
|
865
|
-
"ChatGPT-Account-Id": account_id,
|
|
866
|
-
"originator": originator,
|
|
867
|
-
"User-Agent": f"{originator}/{client_version}",
|
|
868
|
-
}
|
|
869
|
-
# Merge with any headers from model config
|
|
870
|
-
config_headers = model_config.get("custom_endpoint", {}).get("headers", {})
|
|
871
|
-
headers.update(config_headers)
|
|
872
|
-
|
|
873
|
-
# Get base URL - Codex API uses chatgpt.com, not api.openai.com
|
|
874
|
-
base_url = model_config.get("custom_endpoint", {}).get(
|
|
875
|
-
"url", CHATGPT_OAUTH_CONFIG["api_base_url"]
|
|
876
|
-
)
|
|
877
|
-
|
|
878
|
-
# Create HTTP client with Codex interceptor for store=false injection
|
|
879
|
-
from code_puppy.chatgpt_codex_client import create_codex_async_client
|
|
880
|
-
|
|
881
|
-
verify = get_cert_bundle_path()
|
|
882
|
-
client = create_codex_async_client(headers=headers, verify=verify)
|
|
883
|
-
|
|
884
|
-
provider = OpenAIProvider(
|
|
885
|
-
api_key=access_token,
|
|
886
|
-
base_url=base_url,
|
|
887
|
-
http_client=client,
|
|
888
|
-
)
|
|
889
|
-
|
|
890
|
-
# ChatGPT Codex API only supports Responses format
|
|
891
|
-
model = OpenAIResponsesModel(
|
|
892
|
-
model_name=model_config["name"], provider=provider
|
|
893
|
-
)
|
|
894
|
-
setattr(model, "provider", provider)
|
|
895
|
-
return model
|
|
676
|
+
# NOTE: 'chatgpt_oauth' model type is now handled by the chatgpt_oauth plugin
|
|
677
|
+
# via the register_model_type callback. See plugins/chatgpt_oauth/register_callbacks.py
|
|
896
678
|
|
|
897
679
|
elif model_type == "round_robin":
|
|
898
680
|
# Get the list of model names to use in the round-robin
|
|
@@ -916,4 +698,27 @@ class ModelFactory:
|
|
|
916
698
|
return RoundRobinModel(*models, rotate_every=rotate_every)
|
|
917
699
|
|
|
918
700
|
else:
|
|
701
|
+
# Check for plugin-registered model type handlers
|
|
702
|
+
registered_handlers = callbacks.on_register_model_types()
|
|
703
|
+
for handler_info in registered_handlers:
|
|
704
|
+
# Handler info can be a list of dicts or a single dict
|
|
705
|
+
if isinstance(handler_info, list):
|
|
706
|
+
handlers = handler_info
|
|
707
|
+
else:
|
|
708
|
+
handlers = [handler_info] if handler_info else []
|
|
709
|
+
|
|
710
|
+
for handler_entry in handlers:
|
|
711
|
+
if not isinstance(handler_entry, dict):
|
|
712
|
+
continue
|
|
713
|
+
if handler_entry.get("type") == model_type:
|
|
714
|
+
handler = handler_entry.get("handler")
|
|
715
|
+
if callable(handler):
|
|
716
|
+
try:
|
|
717
|
+
return handler(model_name, model_config, config)
|
|
718
|
+
except Exception as e:
|
|
719
|
+
logger.error(
|
|
720
|
+
f"Plugin handler for model type '{model_type}' failed: {e}"
|
|
721
|
+
)
|
|
722
|
+
return None
|
|
723
|
+
|
|
919
724
|
raise ValueError(f"Unsupported model type: {model_type}")
|
code_puppy/model_utils.py
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
This module centralizes logic for handling model-specific behaviors,
|
|
4
4
|
particularly for claude-code and antigravity models which require special prompt handling.
|
|
5
|
+
|
|
6
|
+
Plugins can register custom system prompt handlers via the 'get_model_system_prompt'
|
|
7
|
+
callback to extend support for additional model types.
|
|
5
8
|
"""
|
|
6
9
|
|
|
7
10
|
import pathlib
|
|
@@ -68,7 +71,36 @@ def prepare_prompt_for_model(
|
|
|
68
71
|
user_prompt: str,
|
|
69
72
|
prepend_system_to_user: bool = True,
|
|
70
73
|
) -> PreparedPrompt:
|
|
71
|
-
"""Prepare instructions and prompt for a specific model.
|
|
74
|
+
"""Prepare instructions and prompt for a specific model.
|
|
75
|
+
|
|
76
|
+
This function handles model-specific system prompt requirements. Plugins can
|
|
77
|
+
register custom handlers via the 'get_model_system_prompt' callback to extend
|
|
78
|
+
support for additional model types.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
model_name: The name of the model being used
|
|
82
|
+
system_prompt: The default system prompt from the agent
|
|
83
|
+
user_prompt: The user's prompt/message
|
|
84
|
+
prepend_system_to_user: Whether to prepend system prompt to user prompt
|
|
85
|
+
for models that require it (default: True)
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
PreparedPrompt with instructions and user_prompt ready for the model.
|
|
89
|
+
"""
|
|
90
|
+
# Check for plugin-registered system prompt handlers first
|
|
91
|
+
from code_puppy import callbacks
|
|
92
|
+
|
|
93
|
+
results = callbacks.on_get_model_system_prompt(
|
|
94
|
+
model_name, system_prompt, user_prompt
|
|
95
|
+
)
|
|
96
|
+
for result in results:
|
|
97
|
+
if result and isinstance(result, dict) and result.get("handled"):
|
|
98
|
+
return PreparedPrompt(
|
|
99
|
+
instructions=result.get("instructions", system_prompt),
|
|
100
|
+
user_prompt=result.get("user_prompt", user_prompt),
|
|
101
|
+
is_claude_code=result.get("is_claude_code", False),
|
|
102
|
+
)
|
|
103
|
+
|
|
72
104
|
# Handle Claude Code models
|
|
73
105
|
if is_claude_code_model(model_name):
|
|
74
106
|
modified_prompt = user_prompt
|