code-puppy 0.0.169__py3-none-any.whl → 0.0.366__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/__init__.py +7 -1
- code_puppy/agents/__init__.py +8 -8
- code_puppy/agents/agent_c_reviewer.py +155 -0
- code_puppy/agents/agent_code_puppy.py +9 -2
- code_puppy/agents/agent_code_reviewer.py +90 -0
- code_puppy/agents/agent_cpp_reviewer.py +132 -0
- code_puppy/agents/agent_creator_agent.py +48 -9
- code_puppy/agents/agent_golang_reviewer.py +151 -0
- code_puppy/agents/agent_javascript_reviewer.py +160 -0
- code_puppy/agents/agent_manager.py +146 -199
- code_puppy/agents/agent_pack_leader.py +383 -0
- code_puppy/agents/agent_planning.py +163 -0
- code_puppy/agents/agent_python_programmer.py +165 -0
- code_puppy/agents/agent_python_reviewer.py +90 -0
- code_puppy/agents/agent_qa_expert.py +163 -0
- code_puppy/agents/agent_qa_kitten.py +208 -0
- code_puppy/agents/agent_security_auditor.py +181 -0
- code_puppy/agents/agent_terminal_qa.py +323 -0
- code_puppy/agents/agent_typescript_reviewer.py +166 -0
- code_puppy/agents/base_agent.py +1713 -1
- code_puppy/agents/event_stream_handler.py +350 -0
- code_puppy/agents/json_agent.py +12 -1
- code_puppy/agents/pack/__init__.py +34 -0
- code_puppy/agents/pack/bloodhound.py +304 -0
- code_puppy/agents/pack/husky.py +321 -0
- code_puppy/agents/pack/retriever.py +393 -0
- code_puppy/agents/pack/shepherd.py +348 -0
- code_puppy/agents/pack/terrier.py +287 -0
- code_puppy/agents/pack/watchdog.py +367 -0
- code_puppy/agents/prompt_reviewer.py +145 -0
- code_puppy/agents/subagent_stream_handler.py +276 -0
- code_puppy/api/__init__.py +13 -0
- code_puppy/api/app.py +169 -0
- code_puppy/api/main.py +21 -0
- code_puppy/api/pty_manager.py +446 -0
- code_puppy/api/routers/__init__.py +12 -0
- code_puppy/api/routers/agents.py +36 -0
- code_puppy/api/routers/commands.py +217 -0
- code_puppy/api/routers/config.py +74 -0
- code_puppy/api/routers/sessions.py +232 -0
- code_puppy/api/templates/terminal.html +361 -0
- code_puppy/api/websocket.py +154 -0
- code_puppy/callbacks.py +174 -4
- code_puppy/chatgpt_codex_client.py +283 -0
- code_puppy/claude_cache_client.py +586 -0
- code_puppy/cli_runner.py +916 -0
- code_puppy/command_line/add_model_menu.py +1079 -0
- code_puppy/command_line/agent_menu.py +395 -0
- code_puppy/command_line/attachments.py +395 -0
- code_puppy/command_line/autosave_menu.py +605 -0
- code_puppy/command_line/clipboard.py +527 -0
- code_puppy/command_line/colors_menu.py +520 -0
- code_puppy/command_line/command_handler.py +233 -627
- code_puppy/command_line/command_registry.py +150 -0
- code_puppy/command_line/config_commands.py +715 -0
- code_puppy/command_line/core_commands.py +792 -0
- code_puppy/command_line/diff_menu.py +863 -0
- code_puppy/command_line/load_context_completion.py +15 -22
- code_puppy/command_line/mcp/base.py +1 -4
- code_puppy/command_line/mcp/catalog_server_installer.py +175 -0
- code_puppy/command_line/mcp/custom_server_form.py +688 -0
- code_puppy/command_line/mcp/custom_server_installer.py +195 -0
- code_puppy/command_line/mcp/edit_command.py +148 -0
- code_puppy/command_line/mcp/handler.py +9 -4
- code_puppy/command_line/mcp/help_command.py +6 -5
- code_puppy/command_line/mcp/install_command.py +16 -27
- code_puppy/command_line/mcp/install_menu.py +685 -0
- code_puppy/command_line/mcp/list_command.py +3 -3
- code_puppy/command_line/mcp/logs_command.py +174 -65
- code_puppy/command_line/mcp/remove_command.py +2 -2
- code_puppy/command_line/mcp/restart_command.py +12 -4
- code_puppy/command_line/mcp/search_command.py +17 -11
- code_puppy/command_line/mcp/start_all_command.py +22 -13
- code_puppy/command_line/mcp/start_command.py +50 -31
- code_puppy/command_line/mcp/status_command.py +6 -7
- code_puppy/command_line/mcp/stop_all_command.py +11 -8
- code_puppy/command_line/mcp/stop_command.py +11 -10
- code_puppy/command_line/mcp/test_command.py +2 -2
- code_puppy/command_line/mcp/utils.py +1 -1
- code_puppy/command_line/mcp/wizard_utils.py +22 -18
- code_puppy/command_line/mcp_completion.py +174 -0
- code_puppy/command_line/model_picker_completion.py +89 -30
- code_puppy/command_line/model_settings_menu.py +884 -0
- code_puppy/command_line/motd.py +14 -8
- code_puppy/command_line/onboarding_slides.py +179 -0
- code_puppy/command_line/onboarding_wizard.py +340 -0
- code_puppy/command_line/pin_command_completion.py +329 -0
- code_puppy/command_line/prompt_toolkit_completion.py +626 -75
- code_puppy/command_line/session_commands.py +296 -0
- code_puppy/command_line/utils.py +54 -0
- code_puppy/config.py +1181 -51
- code_puppy/error_logging.py +118 -0
- code_puppy/gemini_code_assist.py +385 -0
- code_puppy/gemini_model.py +602 -0
- code_puppy/http_utils.py +220 -104
- code_puppy/keymap.py +128 -0
- code_puppy/main.py +5 -594
- code_puppy/{mcp → mcp_}/__init__.py +17 -0
- code_puppy/{mcp → mcp_}/async_lifecycle.py +35 -4
- code_puppy/{mcp → mcp_}/blocking_startup.py +70 -43
- code_puppy/{mcp → mcp_}/captured_stdio_server.py +2 -2
- code_puppy/{mcp → mcp_}/config_wizard.py +5 -5
- code_puppy/{mcp → mcp_}/dashboard.py +15 -6
- code_puppy/{mcp → mcp_}/examples/retry_example.py +4 -1
- code_puppy/{mcp → mcp_}/managed_server.py +66 -39
- code_puppy/{mcp → mcp_}/manager.py +146 -52
- code_puppy/mcp_/mcp_logs.py +224 -0
- code_puppy/{mcp → mcp_}/registry.py +6 -6
- code_puppy/{mcp → mcp_}/server_registry_catalog.py +25 -8
- code_puppy/messaging/__init__.py +199 -2
- code_puppy/messaging/bus.py +610 -0
- code_puppy/messaging/commands.py +167 -0
- code_puppy/messaging/markdown_patches.py +57 -0
- code_puppy/messaging/message_queue.py +17 -48
- code_puppy/messaging/messages.py +500 -0
- code_puppy/messaging/queue_console.py +1 -24
- code_puppy/messaging/renderers.py +43 -146
- code_puppy/messaging/rich_renderer.py +1027 -0
- code_puppy/messaging/spinner/__init__.py +33 -5
- code_puppy/messaging/spinner/console_spinner.py +92 -52
- code_puppy/messaging/spinner/spinner_base.py +29 -0
- code_puppy/messaging/subagent_console.py +461 -0
- code_puppy/model_factory.py +686 -80
- code_puppy/model_utils.py +167 -0
- code_puppy/models.json +86 -104
- code_puppy/models_dev_api.json +1 -0
- code_puppy/models_dev_parser.py +592 -0
- code_puppy/plugins/__init__.py +164 -10
- 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 +704 -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 +767 -0
- code_puppy/plugins/antigravity_oauth/utils.py +169 -0
- code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
- code_puppy/plugins/chatgpt_oauth/config.py +52 -0
- code_puppy/plugins/chatgpt_oauth/oauth_flow.py +328 -0
- code_puppy/plugins/chatgpt_oauth/register_callbacks.py +94 -0
- code_puppy/plugins/chatgpt_oauth/test_plugin.py +293 -0
- code_puppy/plugins/chatgpt_oauth/utils.py +489 -0
- code_puppy/plugins/claude_code_oauth/README.md +167 -0
- code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
- code_puppy/plugins/claude_code_oauth/__init__.py +6 -0
- code_puppy/plugins/claude_code_oauth/config.py +50 -0
- code_puppy/plugins/claude_code_oauth/register_callbacks.py +308 -0
- code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
- code_puppy/plugins/claude_code_oauth/utils.py +518 -0
- code_puppy/plugins/customizable_commands/__init__.py +0 -0
- code_puppy/plugins/customizable_commands/register_callbacks.py +169 -0
- code_puppy/plugins/example_custom_command/README.md +280 -0
- code_puppy/plugins/example_custom_command/register_callbacks.py +51 -0
- code_puppy/plugins/file_permission_handler/__init__.py +4 -0
- code_puppy/plugins/file_permission_handler/register_callbacks.py +523 -0
- code_puppy/plugins/frontend_emitter/__init__.py +25 -0
- code_puppy/plugins/frontend_emitter/emitter.py +121 -0
- code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
- code_puppy/plugins/oauth_puppy_html.py +228 -0
- code_puppy/plugins/shell_safety/__init__.py +6 -0
- code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
- code_puppy/plugins/shell_safety/command_cache.py +156 -0
- code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
- code_puppy/prompts/antigravity_system_prompt.md +1 -0
- 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/round_robin_model.py +10 -15
- code_puppy/session_storage.py +294 -0
- code_puppy/status_display.py +21 -4
- code_puppy/summarization_agent.py +52 -14
- code_puppy/terminal_utils.py +418 -0
- code_puppy/tools/__init__.py +139 -6
- code_puppy/tools/agent_tools.py +548 -49
- code_puppy/tools/browser/__init__.py +37 -0
- code_puppy/tools/browser/browser_control.py +289 -0
- code_puppy/tools/browser/browser_interactions.py +545 -0
- code_puppy/tools/browser/browser_locators.py +640 -0
- code_puppy/tools/browser/browser_manager.py +316 -0
- code_puppy/tools/browser/browser_navigation.py +251 -0
- code_puppy/tools/browser/browser_screenshot.py +179 -0
- code_puppy/tools/browser/browser_scripts.py +462 -0
- code_puppy/tools/browser/browser_workflows.py +221 -0
- code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
- code_puppy/tools/browser/terminal_command_tools.py +521 -0
- code_puppy/tools/browser/terminal_screenshot_tools.py +556 -0
- code_puppy/tools/browser/terminal_tools.py +525 -0
- code_puppy/tools/command_runner.py +941 -153
- code_puppy/tools/common.py +1146 -6
- code_puppy/tools/display.py +84 -0
- code_puppy/tools/file_modifications.py +288 -89
- code_puppy/tools/file_operations.py +352 -266
- code_puppy/tools/subagent_context.py +158 -0
- code_puppy/uvx_detection.py +242 -0
- code_puppy/version_checker.py +30 -11
- code_puppy-0.0.366.data/data/code_puppy/models.json +110 -0
- code_puppy-0.0.366.data/data/code_puppy/models_dev_api.json +1 -0
- {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/METADATA +184 -67
- code_puppy-0.0.366.dist-info/RECORD +217 -0
- {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/WHEEL +1 -1
- {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/entry_points.txt +1 -0
- code_puppy/agent.py +0 -231
- code_puppy/agents/agent_orchestrator.json +0 -26
- code_puppy/agents/runtime_manager.py +0 -272
- code_puppy/command_line/mcp/add_command.py +0 -183
- code_puppy/command_line/meta_command_handler.py +0 -153
- code_puppy/message_history_processor.py +0 -490
- code_puppy/messaging/spinner/textual_spinner.py +0 -101
- code_puppy/state_management.py +0 -200
- code_puppy/tui/__init__.py +0 -10
- code_puppy/tui/app.py +0 -986
- code_puppy/tui/components/__init__.py +0 -21
- code_puppy/tui/components/chat_view.py +0 -550
- code_puppy/tui/components/command_history_modal.py +0 -218
- code_puppy/tui/components/copy_button.py +0 -139
- code_puppy/tui/components/custom_widgets.py +0 -63
- code_puppy/tui/components/human_input_modal.py +0 -175
- code_puppy/tui/components/input_area.py +0 -167
- code_puppy/tui/components/sidebar.py +0 -309
- code_puppy/tui/components/status_bar.py +0 -182
- code_puppy/tui/messages.py +0 -27
- code_puppy/tui/models/__init__.py +0 -8
- code_puppy/tui/models/chat_message.py +0 -25
- code_puppy/tui/models/command_history.py +0 -89
- code_puppy/tui/models/enums.py +0 -24
- code_puppy/tui/screens/__init__.py +0 -15
- code_puppy/tui/screens/help.py +0 -130
- code_puppy/tui/screens/mcp_install_wizard.py +0 -803
- code_puppy/tui/screens/settings.py +0 -290
- code_puppy/tui/screens/tools.py +0 -74
- code_puppy-0.0.169.data/data/code_puppy/models.json +0 -128
- code_puppy-0.0.169.dist-info/RECORD +0 -112
- /code_puppy/{mcp → mcp_}/circuit_breaker.py +0 -0
- /code_puppy/{mcp → mcp_}/error_isolation.py +0 -0
- /code_puppy/{mcp → mcp_}/health_monitor.py +0 -0
- /code_puppy/{mcp → mcp_}/retry_manager.py +0 -0
- /code_puppy/{mcp → mcp_}/status_tracker.py +0 -0
- /code_puppy/{mcp → mcp_}/system_tools.py +0 -0
- {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,592 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Models development API parser for Code Puppy.
|
|
3
|
+
|
|
4
|
+
This module provides functionality to parse and work with the models.dev API,
|
|
5
|
+
including provider and model information, search capabilities, and conversion to Code Puppy
|
|
6
|
+
configuration format.
|
|
7
|
+
|
|
8
|
+
The parser fetches data from the live models.dev API first, falling back to a bundled
|
|
9
|
+
JSON file if the API is unavailable.
|
|
10
|
+
|
|
11
|
+
The parser supports filtering by cost, context length, capabilities, and provides
|
|
12
|
+
comprehensive type safety throughout the implementation.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any, Dict, List, Optional
|
|
21
|
+
|
|
22
|
+
import httpx
|
|
23
|
+
|
|
24
|
+
from code_puppy.messaging import emit_error, emit_info, emit_warning
|
|
25
|
+
|
|
26
|
+
# Live API endpoint for models.dev
|
|
27
|
+
MODELS_DEV_API_URL = "https://models.dev/api.json"
|
|
28
|
+
|
|
29
|
+
# Bundled fallback JSON file (relative to this module)
|
|
30
|
+
BUNDLED_JSON_FILENAME = "models_dev_api.json"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(slots=True)
|
|
34
|
+
class ProviderInfo:
|
|
35
|
+
"""Information about a model provider."""
|
|
36
|
+
|
|
37
|
+
id: str
|
|
38
|
+
name: str
|
|
39
|
+
env: List[str]
|
|
40
|
+
api: str
|
|
41
|
+
npm: Optional[str] = None
|
|
42
|
+
doc: Optional[str] = None
|
|
43
|
+
models: Dict[str, Dict[str, Any]] = field(default_factory=dict)
|
|
44
|
+
|
|
45
|
+
def __post_init__(self) -> None:
|
|
46
|
+
"""Validate provider data after initialization."""
|
|
47
|
+
if not self.id:
|
|
48
|
+
raise ValueError("Provider ID cannot be empty")
|
|
49
|
+
if not self.name:
|
|
50
|
+
raise ValueError("Provider name cannot be empty")
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def model_count(self) -> int:
|
|
54
|
+
"""Get the number of models for this provider."""
|
|
55
|
+
return len(self.models)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(slots=True)
|
|
59
|
+
class ModelInfo:
|
|
60
|
+
"""Information about a specific model."""
|
|
61
|
+
|
|
62
|
+
provider_id: str
|
|
63
|
+
model_id: str
|
|
64
|
+
name: str
|
|
65
|
+
attachment: bool = False
|
|
66
|
+
reasoning: bool = False
|
|
67
|
+
tool_call: bool = False
|
|
68
|
+
temperature: bool = False
|
|
69
|
+
structured_output: bool = False
|
|
70
|
+
cost_input: Optional[float] = None
|
|
71
|
+
cost_output: Optional[float] = None
|
|
72
|
+
cost_cache_read: Optional[float] = None
|
|
73
|
+
context_length: int = 0
|
|
74
|
+
max_output: int = 0
|
|
75
|
+
input_modalities: List[str] = field(default_factory=list)
|
|
76
|
+
output_modalities: List[str] = field(default_factory=list)
|
|
77
|
+
knowledge: Optional[str] = None
|
|
78
|
+
release_date: Optional[str] = None
|
|
79
|
+
last_updated: Optional[str] = None
|
|
80
|
+
open_weights: bool = False
|
|
81
|
+
|
|
82
|
+
def __post_init__(self) -> None:
|
|
83
|
+
"""Validate model data after initialization."""
|
|
84
|
+
if not self.provider_id:
|
|
85
|
+
raise ValueError("Provider ID cannot be empty")
|
|
86
|
+
if not self.model_id:
|
|
87
|
+
raise ValueError("Model ID cannot be empty")
|
|
88
|
+
if not self.name:
|
|
89
|
+
raise ValueError("Model name cannot be empty")
|
|
90
|
+
if self.context_length < 0:
|
|
91
|
+
raise ValueError("Context length cannot be negative")
|
|
92
|
+
if self.max_output < 0:
|
|
93
|
+
raise ValueError("Max output cannot be negative")
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def full_id(self) -> str:
|
|
97
|
+
"""Get the full identifier: provider_id::model_id."""
|
|
98
|
+
return f"{self.provider_id}::{self.model_id}"
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def has_vision(self) -> bool:
|
|
102
|
+
"""Check if the model supports vision capabilities."""
|
|
103
|
+
return "image" in self.input_modalities
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def is_multimodal(self) -> bool:
|
|
107
|
+
"""Check if the model supports multiple modalities."""
|
|
108
|
+
return len(self.input_modalities) > 1 or len(self.output_modalities) > 1
|
|
109
|
+
|
|
110
|
+
def supports_capability(self, capability: str) -> bool:
|
|
111
|
+
"""Check if model supports a specific capability."""
|
|
112
|
+
return getattr(self, capability, False) is True
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class ModelsDevRegistry:
|
|
116
|
+
"""Registry for managing models and providers from models.dev API.
|
|
117
|
+
|
|
118
|
+
Fetches data from the live models.dev API first, falling back to a bundled
|
|
119
|
+
JSON file if the API is unavailable.
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
def __init__(self, json_path: str | Path | None = None) -> None:
|
|
123
|
+
"""
|
|
124
|
+
Initialize the registry by fetching from models.dev API or loading bundled JSON.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
json_path: Optional path to a local JSON file (for testing/offline use).
|
|
128
|
+
If None, will try live API first, then bundled fallback.
|
|
129
|
+
|
|
130
|
+
Raises:
|
|
131
|
+
FileNotFoundError: If no data source is available
|
|
132
|
+
json.JSONDecodeError: If the data contains invalid JSON
|
|
133
|
+
ValueError: If required fields are missing or malformed
|
|
134
|
+
"""
|
|
135
|
+
self.json_path = Path(json_path) if json_path else None
|
|
136
|
+
self.providers: Dict[str, ProviderInfo] = {}
|
|
137
|
+
self.models: Dict[str, ModelInfo] = {}
|
|
138
|
+
self.provider_models: Dict[
|
|
139
|
+
str, List[str]
|
|
140
|
+
] = {} # Maps provider_id to list of model IDs
|
|
141
|
+
self.data_source: str = "unknown" # Track where data came from
|
|
142
|
+
self._load_data()
|
|
143
|
+
|
|
144
|
+
def _fetch_from_api(self) -> Optional[Dict[str, Any]]:
|
|
145
|
+
"""Fetch data from the live models.dev API.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Parsed JSON data if successful, None otherwise.
|
|
149
|
+
"""
|
|
150
|
+
try:
|
|
151
|
+
with httpx.Client(timeout=10.0) as client:
|
|
152
|
+
response = client.get(MODELS_DEV_API_URL)
|
|
153
|
+
response.raise_for_status()
|
|
154
|
+
data = response.json()
|
|
155
|
+
if isinstance(data, dict) and len(data) > 0:
|
|
156
|
+
return data
|
|
157
|
+
return None
|
|
158
|
+
except httpx.TimeoutException:
|
|
159
|
+
emit_warning("models.dev API timed out, using bundled fallback")
|
|
160
|
+
return None
|
|
161
|
+
except httpx.HTTPStatusError as e:
|
|
162
|
+
emit_warning(
|
|
163
|
+
f"models.dev API returned {e.response.status_code}, using bundled fallback"
|
|
164
|
+
)
|
|
165
|
+
return None
|
|
166
|
+
except Exception as e:
|
|
167
|
+
emit_warning(
|
|
168
|
+
f"Failed to fetch from models.dev API: {e}, using bundled fallback"
|
|
169
|
+
)
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
def _get_bundled_json_path(self) -> Path:
|
|
173
|
+
"""Get the path to the bundled JSON file."""
|
|
174
|
+
return Path(__file__).parent / BUNDLED_JSON_FILENAME
|
|
175
|
+
|
|
176
|
+
def _load_data(self) -> None:
|
|
177
|
+
"""Load data from API or fallback sources, populating internal data structures."""
|
|
178
|
+
data: Optional[Dict[str, Any]] = None
|
|
179
|
+
|
|
180
|
+
# If explicit json_path provided, use that directly (for testing)
|
|
181
|
+
if self.json_path:
|
|
182
|
+
if not self.json_path.exists():
|
|
183
|
+
raise FileNotFoundError(f"Models API file not found: {self.json_path}")
|
|
184
|
+
try:
|
|
185
|
+
with open(self.json_path, "r", encoding="utf-8") as f:
|
|
186
|
+
data = json.load(f)
|
|
187
|
+
self.data_source = f"file:{self.json_path}"
|
|
188
|
+
except json.JSONDecodeError as e:
|
|
189
|
+
emit_error(f"Invalid JSON in {self.json_path}: {e}")
|
|
190
|
+
raise
|
|
191
|
+
else:
|
|
192
|
+
# Try live API first
|
|
193
|
+
data = self._fetch_from_api()
|
|
194
|
+
if data:
|
|
195
|
+
self.data_source = "live:models.dev"
|
|
196
|
+
emit_info("📡 Fetched latest models from models.dev")
|
|
197
|
+
else:
|
|
198
|
+
# Fall back to bundled JSON
|
|
199
|
+
bundled_path = self._get_bundled_json_path()
|
|
200
|
+
if bundled_path.exists():
|
|
201
|
+
try:
|
|
202
|
+
with open(bundled_path, "r", encoding="utf-8") as f:
|
|
203
|
+
data = json.load(f)
|
|
204
|
+
self.data_source = f"bundled:{bundled_path.name}"
|
|
205
|
+
emit_info(
|
|
206
|
+
"📦 Using bundled models database (models.dev unavailable)"
|
|
207
|
+
)
|
|
208
|
+
except json.JSONDecodeError as e:
|
|
209
|
+
emit_error(f"Invalid JSON in bundled file {bundled_path}: {e}")
|
|
210
|
+
raise
|
|
211
|
+
else:
|
|
212
|
+
raise FileNotFoundError(
|
|
213
|
+
f"No data source available: models.dev API failed and bundled file not found at {bundled_path}"
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
if not isinstance(data, dict):
|
|
217
|
+
raise ValueError("Top-level JSON must be an object")
|
|
218
|
+
|
|
219
|
+
# Parse flat structure: {provider_id: {id, name, env, api, npm, doc, models: {model_id: {...}}}}
|
|
220
|
+
for provider_id, provider_data in data.items():
|
|
221
|
+
try:
|
|
222
|
+
provider = self._parse_provider(provider_id, provider_data)
|
|
223
|
+
self.providers[provider_id] = provider
|
|
224
|
+
self.provider_models[provider_id] = []
|
|
225
|
+
|
|
226
|
+
# Parse models nested under the provider
|
|
227
|
+
models_data = provider_data.get("models", {})
|
|
228
|
+
if isinstance(models_data, dict):
|
|
229
|
+
for model_id, model_data in models_data.items():
|
|
230
|
+
try:
|
|
231
|
+
model = self._parse_model(provider_id, model_id, model_data)
|
|
232
|
+
model_key = model.full_id
|
|
233
|
+
self.models[model_key] = model
|
|
234
|
+
self.provider_models[provider_id].append(model_id)
|
|
235
|
+
except Exception as e:
|
|
236
|
+
emit_warning(
|
|
237
|
+
f"Skipping malformed model {provider_id}::{model_id}: {e}"
|
|
238
|
+
)
|
|
239
|
+
continue
|
|
240
|
+
|
|
241
|
+
except Exception as e:
|
|
242
|
+
emit_warning(f"Skipping malformed provider {provider_id}: {e}")
|
|
243
|
+
continue
|
|
244
|
+
|
|
245
|
+
emit_info(
|
|
246
|
+
f"Loaded {len(self.providers)} providers and {len(self.models)} models"
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
def _parse_provider(self, provider_id: str, data: Dict[str, Any]) -> ProviderInfo:
|
|
250
|
+
"""Parse provider data from JSON."""
|
|
251
|
+
# Only name and env are truly required - api is optional for SDK-based providers
|
|
252
|
+
# like Anthropic, OpenAI, Azure that don't need a custom API URL
|
|
253
|
+
required_fields = ["name", "env"]
|
|
254
|
+
missing_fields = [f for f in required_fields if f not in data]
|
|
255
|
+
if missing_fields:
|
|
256
|
+
raise ValueError(f"Missing required fields: {missing_fields}")
|
|
257
|
+
|
|
258
|
+
return ProviderInfo(
|
|
259
|
+
id=provider_id,
|
|
260
|
+
name=data["name"],
|
|
261
|
+
env=data["env"],
|
|
262
|
+
api=data.get("api", ""), # Optional - empty string for SDK-based providers
|
|
263
|
+
npm=data.get("npm"),
|
|
264
|
+
doc=data.get("doc"),
|
|
265
|
+
models=data.get("models", {}),
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
def _parse_model(
|
|
269
|
+
self, provider_id: str, model_id: str, data: Dict[str, Any]
|
|
270
|
+
) -> ModelInfo:
|
|
271
|
+
"""Parse model data from JSON."""
|
|
272
|
+
if not data.get("name"):
|
|
273
|
+
raise ValueError("Missing required field: name")
|
|
274
|
+
|
|
275
|
+
# Extract cost data from nested dict
|
|
276
|
+
cost_data = data.get("cost", {})
|
|
277
|
+
cost_input = cost_data.get("input")
|
|
278
|
+
cost_output = cost_data.get("output")
|
|
279
|
+
cost_cache_read = cost_data.get("cache_read")
|
|
280
|
+
|
|
281
|
+
# Extract limit data from nested dict
|
|
282
|
+
limit_data = data.get("limit", {})
|
|
283
|
+
context_length = limit_data.get("context", 0)
|
|
284
|
+
max_output = limit_data.get("output", 0)
|
|
285
|
+
|
|
286
|
+
# Extract modalities from nested dict
|
|
287
|
+
modalities = data.get("modalities", {})
|
|
288
|
+
input_mods = modalities.get("input", [])
|
|
289
|
+
output_mods = modalities.get("output", [])
|
|
290
|
+
|
|
291
|
+
return ModelInfo(
|
|
292
|
+
provider_id=provider_id,
|
|
293
|
+
model_id=model_id,
|
|
294
|
+
name=data["name"],
|
|
295
|
+
attachment=data.get("attachment", False),
|
|
296
|
+
reasoning=data.get("reasoning", False),
|
|
297
|
+
tool_call=data.get("tool_call", False),
|
|
298
|
+
temperature=data.get("temperature", True),
|
|
299
|
+
structured_output=data.get("structured_output", False),
|
|
300
|
+
cost_input=cost_input,
|
|
301
|
+
cost_output=cost_output,
|
|
302
|
+
cost_cache_read=cost_cache_read,
|
|
303
|
+
context_length=context_length,
|
|
304
|
+
max_output=max_output,
|
|
305
|
+
input_modalities=input_mods,
|
|
306
|
+
output_modalities=output_mods,
|
|
307
|
+
knowledge=data.get("knowledge"),
|
|
308
|
+
release_date=data.get("release_date"),
|
|
309
|
+
last_updated=data.get("last_updated"),
|
|
310
|
+
open_weights=data.get("open_weights", False),
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
def get_providers(self) -> List[ProviderInfo]:
|
|
314
|
+
"""
|
|
315
|
+
Get all providers, sorted by name.
|
|
316
|
+
|
|
317
|
+
Returns:
|
|
318
|
+
List of ProviderInfo objects sorted by name
|
|
319
|
+
"""
|
|
320
|
+
return sorted(self.providers.values(), key=lambda p: p.name.lower())
|
|
321
|
+
|
|
322
|
+
def get_provider(self, provider_id: str) -> Optional[ProviderInfo]:
|
|
323
|
+
"""
|
|
324
|
+
Get a specific provider by ID.
|
|
325
|
+
|
|
326
|
+
Args:
|
|
327
|
+
provider_id: The provider identifier
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
ProviderInfo if found, None otherwise
|
|
331
|
+
"""
|
|
332
|
+
return self.providers.get(provider_id)
|
|
333
|
+
|
|
334
|
+
def get_models(self, provider_id: Optional[str] = None) -> List[ModelInfo]:
|
|
335
|
+
"""
|
|
336
|
+
Get models, optionally filtered by provider.
|
|
337
|
+
|
|
338
|
+
Args:
|
|
339
|
+
provider_id: Optional provider ID to filter by
|
|
340
|
+
|
|
341
|
+
Returns:
|
|
342
|
+
List of ModelInfo objects sorted by name
|
|
343
|
+
"""
|
|
344
|
+
if provider_id:
|
|
345
|
+
model_ids = self.provider_models.get(provider_id, [])
|
|
346
|
+
models = [
|
|
347
|
+
self.models[f"{provider_id}::{model_id}"]
|
|
348
|
+
for model_id in model_ids
|
|
349
|
+
if f"{provider_id}::{model_id}" in self.models
|
|
350
|
+
]
|
|
351
|
+
else:
|
|
352
|
+
models = list(self.models.values())
|
|
353
|
+
|
|
354
|
+
return sorted(models, key=lambda m: m.name.lower())
|
|
355
|
+
|
|
356
|
+
def get_model(self, provider_id: str, model_id: str) -> Optional[ModelInfo]:
|
|
357
|
+
"""
|
|
358
|
+
Get a specific model.
|
|
359
|
+
|
|
360
|
+
Args:
|
|
361
|
+
provider_id: The provider identifier
|
|
362
|
+
model_id: The model identifier
|
|
363
|
+
|
|
364
|
+
Returns:
|
|
365
|
+
ModelInfo if found, None otherwise
|
|
366
|
+
"""
|
|
367
|
+
full_id = f"{provider_id}::{model_id}"
|
|
368
|
+
return self.models.get(full_id)
|
|
369
|
+
|
|
370
|
+
def search_models(
|
|
371
|
+
self,
|
|
372
|
+
query: Optional[str] = None,
|
|
373
|
+
capability_filters: Optional[Dict[str, Any]] = None,
|
|
374
|
+
) -> List[ModelInfo]:
|
|
375
|
+
"""
|
|
376
|
+
Search models by name/query and filter by capabilities.
|
|
377
|
+
|
|
378
|
+
Args:
|
|
379
|
+
query: Optional search string (case-insensitive)
|
|
380
|
+
capability_filters: Optional capability filters (e.g., {"vision": True})
|
|
381
|
+
|
|
382
|
+
Returns:
|
|
383
|
+
List of matching ModelInfo objects
|
|
384
|
+
"""
|
|
385
|
+
models = list(self.models.values())
|
|
386
|
+
|
|
387
|
+
# Filter by query
|
|
388
|
+
if query:
|
|
389
|
+
query_lower = query.lower()
|
|
390
|
+
models = [
|
|
391
|
+
m
|
|
392
|
+
for m in models
|
|
393
|
+
if query_lower in m.name.lower() or query_lower in m.model_id.lower()
|
|
394
|
+
]
|
|
395
|
+
|
|
396
|
+
# Filter by capabilities
|
|
397
|
+
if capability_filters:
|
|
398
|
+
for capability, required in capability_filters.items():
|
|
399
|
+
if isinstance(required, bool):
|
|
400
|
+
models = [
|
|
401
|
+
m
|
|
402
|
+
for m in models
|
|
403
|
+
if m.supports_capability(capability) == required
|
|
404
|
+
]
|
|
405
|
+
else:
|
|
406
|
+
# Handle other capability filter types if needed
|
|
407
|
+
models = [
|
|
408
|
+
m for m in models if getattr(m, capability, None) == required
|
|
409
|
+
]
|
|
410
|
+
|
|
411
|
+
return sorted(models, key=lambda m: m.name.lower())
|
|
412
|
+
|
|
413
|
+
def filter_by_cost(
|
|
414
|
+
self,
|
|
415
|
+
models: List[ModelInfo],
|
|
416
|
+
max_input_cost: Optional[float] = None,
|
|
417
|
+
max_output_cost: Optional[float] = None,
|
|
418
|
+
) -> List[ModelInfo]:
|
|
419
|
+
"""
|
|
420
|
+
Filter models by cost constraints.
|
|
421
|
+
|
|
422
|
+
Args:
|
|
423
|
+
models: List of models to filter
|
|
424
|
+
max_input_cost: Maximum input cost per token (optional)
|
|
425
|
+
max_output_cost: Maximum output cost per token (optional)
|
|
426
|
+
|
|
427
|
+
Returns:
|
|
428
|
+
Filtered list of models within cost constraints
|
|
429
|
+
"""
|
|
430
|
+
filtered_models = models
|
|
431
|
+
|
|
432
|
+
if max_input_cost is not None:
|
|
433
|
+
filtered_models = [
|
|
434
|
+
m
|
|
435
|
+
for m in filtered_models
|
|
436
|
+
if m.cost_input is not None and m.cost_input <= max_input_cost
|
|
437
|
+
]
|
|
438
|
+
|
|
439
|
+
if max_output_cost is not None:
|
|
440
|
+
filtered_models = [
|
|
441
|
+
m
|
|
442
|
+
for m in filtered_models
|
|
443
|
+
if m.cost_output is not None and m.cost_output <= max_output_cost
|
|
444
|
+
]
|
|
445
|
+
|
|
446
|
+
return filtered_models
|
|
447
|
+
|
|
448
|
+
def filter_by_context(
|
|
449
|
+
self, models: List[ModelInfo], min_context_length: int
|
|
450
|
+
) -> List[ModelInfo]:
|
|
451
|
+
"""
|
|
452
|
+
Filter models by minimum context length.
|
|
453
|
+
|
|
454
|
+
Args:
|
|
455
|
+
models: List of models to filter
|
|
456
|
+
min_context_length: Minimum context length requirement
|
|
457
|
+
|
|
458
|
+
Returns:
|
|
459
|
+
Filtered list of models meeting context requirement
|
|
460
|
+
"""
|
|
461
|
+
return [m for m in models if m.context_length >= min_context_length]
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
# Provider type mapping for Code Puppy configuration
|
|
465
|
+
PROVIDER_TYPE_MAP = {
|
|
466
|
+
"anthropic": "anthropic",
|
|
467
|
+
"openai": "openai",
|
|
468
|
+
"google": "gemini",
|
|
469
|
+
"deepseek": "deepseek",
|
|
470
|
+
"ollama": "ollama",
|
|
471
|
+
"groq": "groq",
|
|
472
|
+
"cohere": "cohere",
|
|
473
|
+
"mistral": "mistral",
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def convert_to_code_puppy_config(
|
|
478
|
+
model: ModelInfo, provider: ProviderInfo
|
|
479
|
+
) -> Dict[str, Any]:
|
|
480
|
+
"""
|
|
481
|
+
Convert a model and provider to Code Puppy configuration format.
|
|
482
|
+
|
|
483
|
+
Args:
|
|
484
|
+
model: ModelInfo object
|
|
485
|
+
provider: ProviderInfo object
|
|
486
|
+
|
|
487
|
+
Returns:
|
|
488
|
+
Dictionary in Code Puppy configuration format
|
|
489
|
+
|
|
490
|
+
Raises:
|
|
491
|
+
ValueError: If required configuration fields are missing
|
|
492
|
+
"""
|
|
493
|
+
# Determine provider type
|
|
494
|
+
provider_type = PROVIDER_TYPE_MAP.get(provider.id, provider.id)
|
|
495
|
+
|
|
496
|
+
# Basic configuration
|
|
497
|
+
config = {
|
|
498
|
+
"type": provider_type,
|
|
499
|
+
"model": model.model_id,
|
|
500
|
+
"enabled": True,
|
|
501
|
+
"provider_id": provider.id,
|
|
502
|
+
"env_vars": provider.env,
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
# Add optional fields if available
|
|
506
|
+
if provider.api:
|
|
507
|
+
config["api_url"] = provider.api
|
|
508
|
+
if provider.npm:
|
|
509
|
+
config["npm_package"] = provider.npm
|
|
510
|
+
|
|
511
|
+
# Add cost information
|
|
512
|
+
if model.cost_input is not None:
|
|
513
|
+
config["input_cost_per_token"] = model.cost_input
|
|
514
|
+
if model.cost_output is not None:
|
|
515
|
+
config["output_cost_per_token"] = model.cost_output
|
|
516
|
+
if model.cost_cache_read is not None:
|
|
517
|
+
config["cache_read_cost_per_token"] = model.cost_cache_read
|
|
518
|
+
|
|
519
|
+
# Add limits
|
|
520
|
+
if model.context_length > 0:
|
|
521
|
+
config["max_tokens"] = model.context_length
|
|
522
|
+
if model.max_output > 0:
|
|
523
|
+
config["max_output_tokens"] = model.max_output
|
|
524
|
+
|
|
525
|
+
# Add capabilities
|
|
526
|
+
capabilities = {
|
|
527
|
+
"attachment": model.attachment,
|
|
528
|
+
"reasoning": model.reasoning,
|
|
529
|
+
"tool_call": model.tool_call,
|
|
530
|
+
"temperature": model.temperature,
|
|
531
|
+
"structured_output": model.structured_output,
|
|
532
|
+
}
|
|
533
|
+
config["capabilities"] = capabilities
|
|
534
|
+
|
|
535
|
+
# Add modalities
|
|
536
|
+
if model.input_modalities:
|
|
537
|
+
config["input_modalities"] = model.input_modalities
|
|
538
|
+
if model.output_modalities:
|
|
539
|
+
config["output_modalities"] = model.output_modalities
|
|
540
|
+
|
|
541
|
+
# Add metadata
|
|
542
|
+
metadata = {}
|
|
543
|
+
if model.knowledge:
|
|
544
|
+
metadata["knowledge"] = model.knowledge
|
|
545
|
+
if model.release_date:
|
|
546
|
+
metadata["release_date"] = model.release_date
|
|
547
|
+
if model.last_updated:
|
|
548
|
+
metadata["last_updated"] = model.last_updated
|
|
549
|
+
metadata["open_weights"] = model.open_weights
|
|
550
|
+
|
|
551
|
+
if metadata:
|
|
552
|
+
config["metadata"] = metadata
|
|
553
|
+
|
|
554
|
+
return config
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
# Example usage
|
|
558
|
+
if __name__ == "__main__":
|
|
559
|
+
# This is for testing purposes
|
|
560
|
+
try:
|
|
561
|
+
registry = ModelsDevRegistry()
|
|
562
|
+
|
|
563
|
+
# Example: Get all providers
|
|
564
|
+
providers = registry.get_providers()
|
|
565
|
+
emit_info(f"Loaded {len(providers)} providers")
|
|
566
|
+
|
|
567
|
+
# Example: Search for vision models
|
|
568
|
+
vision_models = registry.search_models()
|
|
569
|
+
vision_models = [m for m in vision_models if m.has_vision]
|
|
570
|
+
emit_info(f"Found {len(vision_models)} vision models")
|
|
571
|
+
|
|
572
|
+
# Example: Filter by cost
|
|
573
|
+
affordable_models = registry.filter_by_cost(
|
|
574
|
+
registry.get_models(), max_input_cost=0.001
|
|
575
|
+
)
|
|
576
|
+
emit_info(f"Found {len(affordable_models)} affordable models")
|
|
577
|
+
|
|
578
|
+
# Example: Convert to Code Puppy config
|
|
579
|
+
if providers and registry.get_models():
|
|
580
|
+
provider = providers[0]
|
|
581
|
+
models = registry.get_models(provider.id)
|
|
582
|
+
if models:
|
|
583
|
+
config = convert_to_code_puppy_config(models[0], provider)
|
|
584
|
+
emit_info(f"Example config created for {models[0].name}")
|
|
585
|
+
|
|
586
|
+
# Show data source
|
|
587
|
+
emit_info(f"Data source: {registry.data_source}")
|
|
588
|
+
|
|
589
|
+
except FileNotFoundError as e:
|
|
590
|
+
emit_error(f"No data source available: {e}")
|
|
591
|
+
except Exception as e:
|
|
592
|
+
emit_error(f"Error loading models: {e}")
|