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.
Files changed (243) hide show
  1. code_puppy/__init__.py +7 -1
  2. code_puppy/agents/__init__.py +8 -8
  3. code_puppy/agents/agent_c_reviewer.py +155 -0
  4. code_puppy/agents/agent_code_puppy.py +9 -2
  5. code_puppy/agents/agent_code_reviewer.py +90 -0
  6. code_puppy/agents/agent_cpp_reviewer.py +132 -0
  7. code_puppy/agents/agent_creator_agent.py +48 -9
  8. code_puppy/agents/agent_golang_reviewer.py +151 -0
  9. code_puppy/agents/agent_javascript_reviewer.py +160 -0
  10. code_puppy/agents/agent_manager.py +146 -199
  11. code_puppy/agents/agent_pack_leader.py +383 -0
  12. code_puppy/agents/agent_planning.py +163 -0
  13. code_puppy/agents/agent_python_programmer.py +165 -0
  14. code_puppy/agents/agent_python_reviewer.py +90 -0
  15. code_puppy/agents/agent_qa_expert.py +163 -0
  16. code_puppy/agents/agent_qa_kitten.py +208 -0
  17. code_puppy/agents/agent_security_auditor.py +181 -0
  18. code_puppy/agents/agent_terminal_qa.py +323 -0
  19. code_puppy/agents/agent_typescript_reviewer.py +166 -0
  20. code_puppy/agents/base_agent.py +1713 -1
  21. code_puppy/agents/event_stream_handler.py +350 -0
  22. code_puppy/agents/json_agent.py +12 -1
  23. code_puppy/agents/pack/__init__.py +34 -0
  24. code_puppy/agents/pack/bloodhound.py +304 -0
  25. code_puppy/agents/pack/husky.py +321 -0
  26. code_puppy/agents/pack/retriever.py +393 -0
  27. code_puppy/agents/pack/shepherd.py +348 -0
  28. code_puppy/agents/pack/terrier.py +287 -0
  29. code_puppy/agents/pack/watchdog.py +367 -0
  30. code_puppy/agents/prompt_reviewer.py +145 -0
  31. code_puppy/agents/subagent_stream_handler.py +276 -0
  32. code_puppy/api/__init__.py +13 -0
  33. code_puppy/api/app.py +169 -0
  34. code_puppy/api/main.py +21 -0
  35. code_puppy/api/pty_manager.py +446 -0
  36. code_puppy/api/routers/__init__.py +12 -0
  37. code_puppy/api/routers/agents.py +36 -0
  38. code_puppy/api/routers/commands.py +217 -0
  39. code_puppy/api/routers/config.py +74 -0
  40. code_puppy/api/routers/sessions.py +232 -0
  41. code_puppy/api/templates/terminal.html +361 -0
  42. code_puppy/api/websocket.py +154 -0
  43. code_puppy/callbacks.py +174 -4
  44. code_puppy/chatgpt_codex_client.py +283 -0
  45. code_puppy/claude_cache_client.py +586 -0
  46. code_puppy/cli_runner.py +916 -0
  47. code_puppy/command_line/add_model_menu.py +1079 -0
  48. code_puppy/command_line/agent_menu.py +395 -0
  49. code_puppy/command_line/attachments.py +395 -0
  50. code_puppy/command_line/autosave_menu.py +605 -0
  51. code_puppy/command_line/clipboard.py +527 -0
  52. code_puppy/command_line/colors_menu.py +520 -0
  53. code_puppy/command_line/command_handler.py +233 -627
  54. code_puppy/command_line/command_registry.py +150 -0
  55. code_puppy/command_line/config_commands.py +715 -0
  56. code_puppy/command_line/core_commands.py +792 -0
  57. code_puppy/command_line/diff_menu.py +863 -0
  58. code_puppy/command_line/load_context_completion.py +15 -22
  59. code_puppy/command_line/mcp/base.py +1 -4
  60. code_puppy/command_line/mcp/catalog_server_installer.py +175 -0
  61. code_puppy/command_line/mcp/custom_server_form.py +688 -0
  62. code_puppy/command_line/mcp/custom_server_installer.py +195 -0
  63. code_puppy/command_line/mcp/edit_command.py +148 -0
  64. code_puppy/command_line/mcp/handler.py +9 -4
  65. code_puppy/command_line/mcp/help_command.py +6 -5
  66. code_puppy/command_line/mcp/install_command.py +16 -27
  67. code_puppy/command_line/mcp/install_menu.py +685 -0
  68. code_puppy/command_line/mcp/list_command.py +3 -3
  69. code_puppy/command_line/mcp/logs_command.py +174 -65
  70. code_puppy/command_line/mcp/remove_command.py +2 -2
  71. code_puppy/command_line/mcp/restart_command.py +12 -4
  72. code_puppy/command_line/mcp/search_command.py +17 -11
  73. code_puppy/command_line/mcp/start_all_command.py +22 -13
  74. code_puppy/command_line/mcp/start_command.py +50 -31
  75. code_puppy/command_line/mcp/status_command.py +6 -7
  76. code_puppy/command_line/mcp/stop_all_command.py +11 -8
  77. code_puppy/command_line/mcp/stop_command.py +11 -10
  78. code_puppy/command_line/mcp/test_command.py +2 -2
  79. code_puppy/command_line/mcp/utils.py +1 -1
  80. code_puppy/command_line/mcp/wizard_utils.py +22 -18
  81. code_puppy/command_line/mcp_completion.py +174 -0
  82. code_puppy/command_line/model_picker_completion.py +89 -30
  83. code_puppy/command_line/model_settings_menu.py +884 -0
  84. code_puppy/command_line/motd.py +14 -8
  85. code_puppy/command_line/onboarding_slides.py +179 -0
  86. code_puppy/command_line/onboarding_wizard.py +340 -0
  87. code_puppy/command_line/pin_command_completion.py +329 -0
  88. code_puppy/command_line/prompt_toolkit_completion.py +626 -75
  89. code_puppy/command_line/session_commands.py +296 -0
  90. code_puppy/command_line/utils.py +54 -0
  91. code_puppy/config.py +1181 -51
  92. code_puppy/error_logging.py +118 -0
  93. code_puppy/gemini_code_assist.py +385 -0
  94. code_puppy/gemini_model.py +602 -0
  95. code_puppy/http_utils.py +220 -104
  96. code_puppy/keymap.py +128 -0
  97. code_puppy/main.py +5 -594
  98. code_puppy/{mcp → mcp_}/__init__.py +17 -0
  99. code_puppy/{mcp → mcp_}/async_lifecycle.py +35 -4
  100. code_puppy/{mcp → mcp_}/blocking_startup.py +70 -43
  101. code_puppy/{mcp → mcp_}/captured_stdio_server.py +2 -2
  102. code_puppy/{mcp → mcp_}/config_wizard.py +5 -5
  103. code_puppy/{mcp → mcp_}/dashboard.py +15 -6
  104. code_puppy/{mcp → mcp_}/examples/retry_example.py +4 -1
  105. code_puppy/{mcp → mcp_}/managed_server.py +66 -39
  106. code_puppy/{mcp → mcp_}/manager.py +146 -52
  107. code_puppy/mcp_/mcp_logs.py +224 -0
  108. code_puppy/{mcp → mcp_}/registry.py +6 -6
  109. code_puppy/{mcp → mcp_}/server_registry_catalog.py +25 -8
  110. code_puppy/messaging/__init__.py +199 -2
  111. code_puppy/messaging/bus.py +610 -0
  112. code_puppy/messaging/commands.py +167 -0
  113. code_puppy/messaging/markdown_patches.py +57 -0
  114. code_puppy/messaging/message_queue.py +17 -48
  115. code_puppy/messaging/messages.py +500 -0
  116. code_puppy/messaging/queue_console.py +1 -24
  117. code_puppy/messaging/renderers.py +43 -146
  118. code_puppy/messaging/rich_renderer.py +1027 -0
  119. code_puppy/messaging/spinner/__init__.py +33 -5
  120. code_puppy/messaging/spinner/console_spinner.py +92 -52
  121. code_puppy/messaging/spinner/spinner_base.py +29 -0
  122. code_puppy/messaging/subagent_console.py +461 -0
  123. code_puppy/model_factory.py +686 -80
  124. code_puppy/model_utils.py +167 -0
  125. code_puppy/models.json +86 -104
  126. code_puppy/models_dev_api.json +1 -0
  127. code_puppy/models_dev_parser.py +592 -0
  128. code_puppy/plugins/__init__.py +164 -10
  129. code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
  130. code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
  131. code_puppy/plugins/antigravity_oauth/antigravity_model.py +704 -0
  132. code_puppy/plugins/antigravity_oauth/config.py +42 -0
  133. code_puppy/plugins/antigravity_oauth/constants.py +136 -0
  134. code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
  135. code_puppy/plugins/antigravity_oauth/register_callbacks.py +406 -0
  136. code_puppy/plugins/antigravity_oauth/storage.py +271 -0
  137. code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
  138. code_puppy/plugins/antigravity_oauth/token.py +167 -0
  139. code_puppy/plugins/antigravity_oauth/transport.py +767 -0
  140. code_puppy/plugins/antigravity_oauth/utils.py +169 -0
  141. code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
  142. code_puppy/plugins/chatgpt_oauth/config.py +52 -0
  143. code_puppy/plugins/chatgpt_oauth/oauth_flow.py +328 -0
  144. code_puppy/plugins/chatgpt_oauth/register_callbacks.py +94 -0
  145. code_puppy/plugins/chatgpt_oauth/test_plugin.py +293 -0
  146. code_puppy/plugins/chatgpt_oauth/utils.py +489 -0
  147. code_puppy/plugins/claude_code_oauth/README.md +167 -0
  148. code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
  149. code_puppy/plugins/claude_code_oauth/__init__.py +6 -0
  150. code_puppy/plugins/claude_code_oauth/config.py +50 -0
  151. code_puppy/plugins/claude_code_oauth/register_callbacks.py +308 -0
  152. code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
  153. code_puppy/plugins/claude_code_oauth/utils.py +518 -0
  154. code_puppy/plugins/customizable_commands/__init__.py +0 -0
  155. code_puppy/plugins/customizable_commands/register_callbacks.py +169 -0
  156. code_puppy/plugins/example_custom_command/README.md +280 -0
  157. code_puppy/plugins/example_custom_command/register_callbacks.py +51 -0
  158. code_puppy/plugins/file_permission_handler/__init__.py +4 -0
  159. code_puppy/plugins/file_permission_handler/register_callbacks.py +523 -0
  160. code_puppy/plugins/frontend_emitter/__init__.py +25 -0
  161. code_puppy/plugins/frontend_emitter/emitter.py +121 -0
  162. code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
  163. code_puppy/plugins/oauth_puppy_html.py +228 -0
  164. code_puppy/plugins/shell_safety/__init__.py +6 -0
  165. code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
  166. code_puppy/plugins/shell_safety/command_cache.py +156 -0
  167. code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
  168. code_puppy/prompts/antigravity_system_prompt.md +1 -0
  169. code_puppy/prompts/codex_system_prompt.md +310 -0
  170. code_puppy/pydantic_patches.py +131 -0
  171. code_puppy/reopenable_async_client.py +8 -8
  172. code_puppy/round_robin_model.py +10 -15
  173. code_puppy/session_storage.py +294 -0
  174. code_puppy/status_display.py +21 -4
  175. code_puppy/summarization_agent.py +52 -14
  176. code_puppy/terminal_utils.py +418 -0
  177. code_puppy/tools/__init__.py +139 -6
  178. code_puppy/tools/agent_tools.py +548 -49
  179. code_puppy/tools/browser/__init__.py +37 -0
  180. code_puppy/tools/browser/browser_control.py +289 -0
  181. code_puppy/tools/browser/browser_interactions.py +545 -0
  182. code_puppy/tools/browser/browser_locators.py +640 -0
  183. code_puppy/tools/browser/browser_manager.py +316 -0
  184. code_puppy/tools/browser/browser_navigation.py +251 -0
  185. code_puppy/tools/browser/browser_screenshot.py +179 -0
  186. code_puppy/tools/browser/browser_scripts.py +462 -0
  187. code_puppy/tools/browser/browser_workflows.py +221 -0
  188. code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
  189. code_puppy/tools/browser/terminal_command_tools.py +521 -0
  190. code_puppy/tools/browser/terminal_screenshot_tools.py +556 -0
  191. code_puppy/tools/browser/terminal_tools.py +525 -0
  192. code_puppy/tools/command_runner.py +941 -153
  193. code_puppy/tools/common.py +1146 -6
  194. code_puppy/tools/display.py +84 -0
  195. code_puppy/tools/file_modifications.py +288 -89
  196. code_puppy/tools/file_operations.py +352 -266
  197. code_puppy/tools/subagent_context.py +158 -0
  198. code_puppy/uvx_detection.py +242 -0
  199. code_puppy/version_checker.py +30 -11
  200. code_puppy-0.0.366.data/data/code_puppy/models.json +110 -0
  201. code_puppy-0.0.366.data/data/code_puppy/models_dev_api.json +1 -0
  202. {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/METADATA +184 -67
  203. code_puppy-0.0.366.dist-info/RECORD +217 -0
  204. {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/WHEEL +1 -1
  205. {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/entry_points.txt +1 -0
  206. code_puppy/agent.py +0 -231
  207. code_puppy/agents/agent_orchestrator.json +0 -26
  208. code_puppy/agents/runtime_manager.py +0 -272
  209. code_puppy/command_line/mcp/add_command.py +0 -183
  210. code_puppy/command_line/meta_command_handler.py +0 -153
  211. code_puppy/message_history_processor.py +0 -490
  212. code_puppy/messaging/spinner/textual_spinner.py +0 -101
  213. code_puppy/state_management.py +0 -200
  214. code_puppy/tui/__init__.py +0 -10
  215. code_puppy/tui/app.py +0 -986
  216. code_puppy/tui/components/__init__.py +0 -21
  217. code_puppy/tui/components/chat_view.py +0 -550
  218. code_puppy/tui/components/command_history_modal.py +0 -218
  219. code_puppy/tui/components/copy_button.py +0 -139
  220. code_puppy/tui/components/custom_widgets.py +0 -63
  221. code_puppy/tui/components/human_input_modal.py +0 -175
  222. code_puppy/tui/components/input_area.py +0 -167
  223. code_puppy/tui/components/sidebar.py +0 -309
  224. code_puppy/tui/components/status_bar.py +0 -182
  225. code_puppy/tui/messages.py +0 -27
  226. code_puppy/tui/models/__init__.py +0 -8
  227. code_puppy/tui/models/chat_message.py +0 -25
  228. code_puppy/tui/models/command_history.py +0 -89
  229. code_puppy/tui/models/enums.py +0 -24
  230. code_puppy/tui/screens/__init__.py +0 -15
  231. code_puppy/tui/screens/help.py +0 -130
  232. code_puppy/tui/screens/mcp_install_wizard.py +0 -803
  233. code_puppy/tui/screens/settings.py +0 -290
  234. code_puppy/tui/screens/tools.py +0 -74
  235. code_puppy-0.0.169.data/data/code_puppy/models.json +0 -128
  236. code_puppy-0.0.169.dist-info/RECORD +0 -112
  237. /code_puppy/{mcp → mcp_}/circuit_breaker.py +0 -0
  238. /code_puppy/{mcp → mcp_}/error_isolation.py +0 -0
  239. /code_puppy/{mcp → mcp_}/health_monitor.py +0 -0
  240. /code_puppy/{mcp → mcp_}/retry_manager.py +0 -0
  241. /code_puppy/{mcp → mcp_}/status_tracker.py +0 -0
  242. /code_puppy/{mcp → mcp_}/system_tools.py +0 -0
  243. {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}")