superqode 0.1.5__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 (288) hide show
  1. superqode/__init__.py +33 -0
  2. superqode/acp/__init__.py +23 -0
  3. superqode/acp/client.py +913 -0
  4. superqode/acp/permission_screen.py +457 -0
  5. superqode/acp/types.py +480 -0
  6. superqode/acp_discovery.py +856 -0
  7. superqode/agent/__init__.py +22 -0
  8. superqode/agent/edit_strategies.py +334 -0
  9. superqode/agent/loop.py +892 -0
  10. superqode/agent/qe_report_templates.py +39 -0
  11. superqode/agent/system_prompts.py +353 -0
  12. superqode/agent_output.py +721 -0
  13. superqode/agent_stream.py +953 -0
  14. superqode/agents/__init__.py +59 -0
  15. superqode/agents/acp_registry.py +305 -0
  16. superqode/agents/client.py +249 -0
  17. superqode/agents/data/augmentcode.com.toml +51 -0
  18. superqode/agents/data/cagent.dev.toml +51 -0
  19. superqode/agents/data/claude.com.toml +60 -0
  20. superqode/agents/data/codeassistant.dev.toml +51 -0
  21. superqode/agents/data/codex.openai.com.toml +57 -0
  22. superqode/agents/data/fastagent.ai.toml +66 -0
  23. superqode/agents/data/geminicli.com.toml +77 -0
  24. superqode/agents/data/goose.block.xyz.toml +54 -0
  25. superqode/agents/data/junie.jetbrains.com.toml +56 -0
  26. superqode/agents/data/kimi.moonshot.cn.toml +57 -0
  27. superqode/agents/data/llmlingagent.dev.toml +51 -0
  28. superqode/agents/data/molt.bot.toml +49 -0
  29. superqode/agents/data/opencode.ai.toml +60 -0
  30. superqode/agents/data/stakpak.dev.toml +51 -0
  31. superqode/agents/data/vtcode.dev.toml +51 -0
  32. superqode/agents/discovery.py +266 -0
  33. superqode/agents/messaging.py +160 -0
  34. superqode/agents/persona.py +166 -0
  35. superqode/agents/registry.py +421 -0
  36. superqode/agents/schema.py +72 -0
  37. superqode/agents/unified.py +367 -0
  38. superqode/app/__init__.py +111 -0
  39. superqode/app/constants.py +314 -0
  40. superqode/app/css.py +366 -0
  41. superqode/app/models.py +118 -0
  42. superqode/app/suggester.py +125 -0
  43. superqode/app/widgets.py +1591 -0
  44. superqode/app_enhanced.py +399 -0
  45. superqode/app_main.py +17187 -0
  46. superqode/approval.py +312 -0
  47. superqode/atomic.py +296 -0
  48. superqode/commands/__init__.py +1 -0
  49. superqode/commands/acp.py +965 -0
  50. superqode/commands/agents.py +180 -0
  51. superqode/commands/auth.py +278 -0
  52. superqode/commands/config.py +374 -0
  53. superqode/commands/init.py +826 -0
  54. superqode/commands/providers.py +819 -0
  55. superqode/commands/qe.py +1145 -0
  56. superqode/commands/roles.py +380 -0
  57. superqode/commands/serve.py +172 -0
  58. superqode/commands/suggestions.py +127 -0
  59. superqode/commands/superqe.py +460 -0
  60. superqode/config/__init__.py +51 -0
  61. superqode/config/loader.py +812 -0
  62. superqode/config/schema.py +498 -0
  63. superqode/core/__init__.py +111 -0
  64. superqode/core/roles.py +281 -0
  65. superqode/danger.py +386 -0
  66. superqode/data/superqode-template.yaml +1522 -0
  67. superqode/design_system.py +1080 -0
  68. superqode/dialogs/__init__.py +6 -0
  69. superqode/dialogs/base.py +39 -0
  70. superqode/dialogs/model.py +130 -0
  71. superqode/dialogs/provider.py +870 -0
  72. superqode/diff_view.py +919 -0
  73. superqode/enterprise.py +21 -0
  74. superqode/evaluation/__init__.py +25 -0
  75. superqode/evaluation/adapters.py +93 -0
  76. superqode/evaluation/behaviors.py +89 -0
  77. superqode/evaluation/engine.py +209 -0
  78. superqode/evaluation/scenarios.py +96 -0
  79. superqode/execution/__init__.py +36 -0
  80. superqode/execution/linter.py +538 -0
  81. superqode/execution/modes.py +347 -0
  82. superqode/execution/resolver.py +283 -0
  83. superqode/execution/runner.py +642 -0
  84. superqode/file_explorer.py +811 -0
  85. superqode/file_viewer.py +471 -0
  86. superqode/flash.py +183 -0
  87. superqode/guidance/__init__.py +58 -0
  88. superqode/guidance/config.py +203 -0
  89. superqode/guidance/prompts.py +71 -0
  90. superqode/harness/__init__.py +54 -0
  91. superqode/harness/accelerator.py +291 -0
  92. superqode/harness/config.py +319 -0
  93. superqode/harness/validator.py +147 -0
  94. superqode/history.py +279 -0
  95. superqode/integrations/superopt_runner.py +124 -0
  96. superqode/logging/__init__.py +49 -0
  97. superqode/logging/adapters.py +219 -0
  98. superqode/logging/formatter.py +923 -0
  99. superqode/logging/integration.py +341 -0
  100. superqode/logging/sinks.py +170 -0
  101. superqode/logging/unified_log.py +417 -0
  102. superqode/lsp/__init__.py +26 -0
  103. superqode/lsp/client.py +544 -0
  104. superqode/main.py +1069 -0
  105. superqode/mcp/__init__.py +89 -0
  106. superqode/mcp/auth_storage.py +380 -0
  107. superqode/mcp/client.py +1236 -0
  108. superqode/mcp/config.py +319 -0
  109. superqode/mcp/integration.py +337 -0
  110. superqode/mcp/oauth.py +436 -0
  111. superqode/mcp/oauth_callback.py +385 -0
  112. superqode/mcp/types.py +290 -0
  113. superqode/memory/__init__.py +31 -0
  114. superqode/memory/feedback.py +342 -0
  115. superqode/memory/store.py +522 -0
  116. superqode/notifications.py +369 -0
  117. superqode/optimization/__init__.py +5 -0
  118. superqode/optimization/config.py +33 -0
  119. superqode/permissions/__init__.py +25 -0
  120. superqode/permissions/rules.py +488 -0
  121. superqode/plan.py +323 -0
  122. superqode/providers/__init__.py +33 -0
  123. superqode/providers/gateway/__init__.py +165 -0
  124. superqode/providers/gateway/base.py +228 -0
  125. superqode/providers/gateway/litellm_gateway.py +1170 -0
  126. superqode/providers/gateway/openresponses_gateway.py +436 -0
  127. superqode/providers/health.py +297 -0
  128. superqode/providers/huggingface/__init__.py +74 -0
  129. superqode/providers/huggingface/downloader.py +472 -0
  130. superqode/providers/huggingface/endpoints.py +442 -0
  131. superqode/providers/huggingface/hub.py +531 -0
  132. superqode/providers/huggingface/inference.py +394 -0
  133. superqode/providers/huggingface/transformers_runner.py +516 -0
  134. superqode/providers/local/__init__.py +100 -0
  135. superqode/providers/local/base.py +438 -0
  136. superqode/providers/local/discovery.py +418 -0
  137. superqode/providers/local/lmstudio.py +256 -0
  138. superqode/providers/local/mlx.py +457 -0
  139. superqode/providers/local/ollama.py +486 -0
  140. superqode/providers/local/sglang.py +268 -0
  141. superqode/providers/local/tgi.py +260 -0
  142. superqode/providers/local/tool_support.py +477 -0
  143. superqode/providers/local/vllm.py +258 -0
  144. superqode/providers/manager.py +1338 -0
  145. superqode/providers/models.py +1016 -0
  146. superqode/providers/models_dev.py +578 -0
  147. superqode/providers/openresponses/__init__.py +87 -0
  148. superqode/providers/openresponses/converters/__init__.py +17 -0
  149. superqode/providers/openresponses/converters/messages.py +343 -0
  150. superqode/providers/openresponses/converters/tools.py +268 -0
  151. superqode/providers/openresponses/schema/__init__.py +56 -0
  152. superqode/providers/openresponses/schema/models.py +585 -0
  153. superqode/providers/openresponses/streaming/__init__.py +5 -0
  154. superqode/providers/openresponses/streaming/parser.py +338 -0
  155. superqode/providers/openresponses/tools/__init__.py +21 -0
  156. superqode/providers/openresponses/tools/apply_patch.py +352 -0
  157. superqode/providers/openresponses/tools/code_interpreter.py +290 -0
  158. superqode/providers/openresponses/tools/file_search.py +333 -0
  159. superqode/providers/openresponses/tools/mcp_adapter.py +252 -0
  160. superqode/providers/registry.py +716 -0
  161. superqode/providers/usage.py +332 -0
  162. superqode/pure_mode.py +384 -0
  163. superqode/qr/__init__.py +23 -0
  164. superqode/qr/dashboard.py +781 -0
  165. superqode/qr/generator.py +1018 -0
  166. superqode/qr/templates.py +135 -0
  167. superqode/safety/__init__.py +41 -0
  168. superqode/safety/sandbox.py +413 -0
  169. superqode/safety/warnings.py +256 -0
  170. superqode/server/__init__.py +33 -0
  171. superqode/server/lsp_server.py +775 -0
  172. superqode/server/web.py +250 -0
  173. superqode/session/__init__.py +25 -0
  174. superqode/session/persistence.py +580 -0
  175. superqode/session/sharing.py +477 -0
  176. superqode/session.py +475 -0
  177. superqode/sidebar.py +2991 -0
  178. superqode/stream_view.py +648 -0
  179. superqode/styles/__init__.py +3 -0
  180. superqode/superqe/__init__.py +184 -0
  181. superqode/superqe/acp_runner.py +1064 -0
  182. superqode/superqe/constitution/__init__.py +62 -0
  183. superqode/superqe/constitution/evaluator.py +308 -0
  184. superqode/superqe/constitution/loader.py +432 -0
  185. superqode/superqe/constitution/schema.py +250 -0
  186. superqode/superqe/events.py +591 -0
  187. superqode/superqe/frameworks/__init__.py +65 -0
  188. superqode/superqe/frameworks/base.py +234 -0
  189. superqode/superqe/frameworks/e2e.py +263 -0
  190. superqode/superqe/frameworks/executor.py +237 -0
  191. superqode/superqe/frameworks/javascript.py +409 -0
  192. superqode/superqe/frameworks/python.py +373 -0
  193. superqode/superqe/frameworks/registry.py +92 -0
  194. superqode/superqe/mcp_tools/__init__.py +47 -0
  195. superqode/superqe/mcp_tools/core_tools.py +418 -0
  196. superqode/superqe/mcp_tools/registry.py +230 -0
  197. superqode/superqe/mcp_tools/testing_tools.py +167 -0
  198. superqode/superqe/noise.py +89 -0
  199. superqode/superqe/orchestrator.py +778 -0
  200. superqode/superqe/roles.py +609 -0
  201. superqode/superqe/session.py +713 -0
  202. superqode/superqe/skills/__init__.py +57 -0
  203. superqode/superqe/skills/base.py +106 -0
  204. superqode/superqe/skills/core_skills.py +899 -0
  205. superqode/superqe/skills/registry.py +90 -0
  206. superqode/superqe/verifier.py +101 -0
  207. superqode/superqe_cli.py +76 -0
  208. superqode/tool_call.py +358 -0
  209. superqode/tools/__init__.py +93 -0
  210. superqode/tools/agent_tools.py +496 -0
  211. superqode/tools/base.py +324 -0
  212. superqode/tools/batch_tool.py +133 -0
  213. superqode/tools/diagnostics.py +311 -0
  214. superqode/tools/edit_tools.py +653 -0
  215. superqode/tools/enhanced_base.py +515 -0
  216. superqode/tools/file_tools.py +269 -0
  217. superqode/tools/file_tracking.py +45 -0
  218. superqode/tools/lsp_tools.py +610 -0
  219. superqode/tools/network_tools.py +350 -0
  220. superqode/tools/permissions.py +400 -0
  221. superqode/tools/question_tool.py +324 -0
  222. superqode/tools/search_tools.py +598 -0
  223. superqode/tools/shell_tools.py +259 -0
  224. superqode/tools/todo_tools.py +121 -0
  225. superqode/tools/validation.py +80 -0
  226. superqode/tools/web_tools.py +639 -0
  227. superqode/tui.py +1152 -0
  228. superqode/tui_integration.py +875 -0
  229. superqode/tui_widgets/__init__.py +27 -0
  230. superqode/tui_widgets/widgets/__init__.py +18 -0
  231. superqode/tui_widgets/widgets/progress.py +185 -0
  232. superqode/tui_widgets/widgets/tool_display.py +188 -0
  233. superqode/undo_manager.py +574 -0
  234. superqode/utils/__init__.py +5 -0
  235. superqode/utils/error_handling.py +323 -0
  236. superqode/utils/fuzzy.py +257 -0
  237. superqode/widgets/__init__.py +477 -0
  238. superqode/widgets/agent_collab.py +390 -0
  239. superqode/widgets/agent_store.py +936 -0
  240. superqode/widgets/agent_switcher.py +395 -0
  241. superqode/widgets/animation_manager.py +284 -0
  242. superqode/widgets/code_context.py +356 -0
  243. superqode/widgets/command_palette.py +412 -0
  244. superqode/widgets/connection_status.py +537 -0
  245. superqode/widgets/conversation_history.py +470 -0
  246. superqode/widgets/diff_indicator.py +155 -0
  247. superqode/widgets/enhanced_status_bar.py +385 -0
  248. superqode/widgets/enhanced_toast.py +476 -0
  249. superqode/widgets/file_browser.py +809 -0
  250. superqode/widgets/file_reference.py +585 -0
  251. superqode/widgets/issue_timeline.py +340 -0
  252. superqode/widgets/leader_key.py +264 -0
  253. superqode/widgets/mode_switcher.py +445 -0
  254. superqode/widgets/model_picker.py +234 -0
  255. superqode/widgets/permission_preview.py +1205 -0
  256. superqode/widgets/prompt.py +358 -0
  257. superqode/widgets/provider_connect.py +725 -0
  258. superqode/widgets/pty_shell.py +587 -0
  259. superqode/widgets/qe_dashboard.py +321 -0
  260. superqode/widgets/resizable_sidebar.py +377 -0
  261. superqode/widgets/response_changes.py +218 -0
  262. superqode/widgets/response_display.py +528 -0
  263. superqode/widgets/rich_tool_display.py +613 -0
  264. superqode/widgets/sidebar_panels.py +1180 -0
  265. superqode/widgets/slash_complete.py +356 -0
  266. superqode/widgets/split_view.py +612 -0
  267. superqode/widgets/status_bar.py +273 -0
  268. superqode/widgets/superqode_display.py +786 -0
  269. superqode/widgets/thinking_display.py +815 -0
  270. superqode/widgets/throbber.py +87 -0
  271. superqode/widgets/toast.py +206 -0
  272. superqode/widgets/unified_output.py +1073 -0
  273. superqode/workspace/__init__.py +75 -0
  274. superqode/workspace/artifacts.py +472 -0
  275. superqode/workspace/coordinator.py +353 -0
  276. superqode/workspace/diff_tracker.py +429 -0
  277. superqode/workspace/git_guard.py +373 -0
  278. superqode/workspace/git_snapshot.py +526 -0
  279. superqode/workspace/manager.py +750 -0
  280. superqode/workspace/snapshot.py +357 -0
  281. superqode/workspace/watcher.py +535 -0
  282. superqode/workspace/worktree.py +440 -0
  283. superqode-0.1.5.dist-info/METADATA +204 -0
  284. superqode-0.1.5.dist-info/RECORD +288 -0
  285. superqode-0.1.5.dist-info/WHEEL +5 -0
  286. superqode-0.1.5.dist-info/entry_points.txt +3 -0
  287. superqode-0.1.5.dist-info/licenses/LICENSE +648 -0
  288. superqode-0.1.5.dist-info/top_level.txt +1 -0
@@ -0,0 +1,578 @@
1
+ """
2
+ Models.dev Integration - Fetch latest AI model data from models.dev
3
+
4
+ Provides real-time model information including:
5
+ - Pricing (input/output per 1M tokens)
6
+ - Context window and output limits
7
+ - Capabilities (tools, vision, reasoning, etc.)
8
+ - Provider metadata
9
+
10
+ The data is cached locally with a configurable TTL to reduce API calls.
11
+
12
+ Usage:
13
+ from superqode.providers.models_dev import ModelsDev
14
+
15
+ client = ModelsDev()
16
+ await client.refresh() # Fetch latest data
17
+ models = client.get_models_for_provider("anthropic")
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import asyncio
23
+ import json
24
+ import logging
25
+ from dataclasses import dataclass, field
26
+ from datetime import datetime, timedelta
27
+ from pathlib import Path
28
+ from typing import Any, Dict, List, Optional, Set
29
+
30
+ try:
31
+ import aiohttp
32
+
33
+ HAS_AIOHTTP = True
34
+ except ImportError:
35
+ HAS_AIOHTTP = False
36
+
37
+ try:
38
+ import httpx
39
+
40
+ HAS_HTTPX = True
41
+ except ImportError:
42
+ HAS_HTTPX = False
43
+
44
+ from .models import ModelInfo, ModelCapability
45
+
46
+ logger = logging.getLogger(__name__)
47
+
48
+ # ============================================================================
49
+ # CONSTANTS
50
+ # ============================================================================
51
+
52
+ MODELS_DEV_API_URL = "https://models.dev/api.json"
53
+ CACHE_FILE = Path.home() / ".superqode" / "models_cache.json"
54
+ DEFAULT_CACHE_TTL = timedelta(hours=24)
55
+
56
+ # Providers we actively support (others available via OpenRouter)
57
+ SUPPORTED_PROVIDERS = {
58
+ "anthropic",
59
+ "openai",
60
+ "google",
61
+ "deepseek",
62
+ "groq",
63
+ "openrouter",
64
+ "xai",
65
+ "mistral",
66
+ "cohere",
67
+ "together",
68
+ "fireworks",
69
+ "perplexity",
70
+ }
71
+
72
+ # Provider ID mappings (models.dev ID -> our ID)
73
+ PROVIDER_ID_MAP = {
74
+ "google-ai-studio": "google",
75
+ "google-vertex": "google",
76
+ "google-vertex-anthropic": "google-vertex-anthropic",
77
+ "x-ai": "xai",
78
+ }
79
+
80
+
81
+ # ============================================================================
82
+ # DATA CLASSES
83
+ # ============================================================================
84
+
85
+
86
+ @dataclass
87
+ class ProviderInfo:
88
+ """Provider metadata from models.dev."""
89
+
90
+ id: str
91
+ name: str
92
+ env_vars: List[str] = field(default_factory=list)
93
+ doc_url: str = ""
94
+ api_url: str = ""
95
+
96
+
97
+ @dataclass
98
+ class CacheMetadata:
99
+ """Metadata for the cached models data."""
100
+
101
+ fetched_at: str
102
+ ttl_hours: int = 24
103
+ provider_count: int = 0
104
+ model_count: int = 0
105
+
106
+ @property
107
+ def is_expired(self) -> bool:
108
+ """Check if the cache has expired."""
109
+ try:
110
+ fetched = datetime.fromisoformat(self.fetched_at)
111
+ return datetime.now() - fetched > timedelta(hours=self.ttl_hours)
112
+ except (ValueError, TypeError):
113
+ return True
114
+
115
+
116
+ # ============================================================================
117
+ # MODELS.DEV CLIENT
118
+ # ============================================================================
119
+
120
+
121
+ class ModelsDev:
122
+ """
123
+ Client for fetching and caching model data from models.dev.
124
+
125
+ Features:
126
+ - Async HTTP fetching with aiohttp/httpx fallback
127
+ - Local JSON cache with TTL
128
+ - Transforms models.dev format to ModelInfo
129
+ - Provider filtering and mapping
130
+ """
131
+
132
+ def __init__(self, cache_ttl: timedelta = DEFAULT_CACHE_TTL):
133
+ self.cache_ttl = cache_ttl
134
+ self._providers: Dict[str, ProviderInfo] = {}
135
+ self._models: Dict[str, Dict[str, ModelInfo]] = {}
136
+ self._raw_data: Dict[str, Any] = {}
137
+ self._metadata: Optional[CacheMetadata] = None
138
+ self._loaded = False
139
+
140
+ # ========================================================================
141
+ # PUBLIC API
142
+ # ========================================================================
143
+
144
+ async def ensure_loaded(self) -> bool:
145
+ """
146
+ Ensure model data is loaded, fetching if needed.
147
+
148
+ Returns True if data is available (cached or fetched).
149
+ """
150
+ if self._loaded and self._metadata and not self._metadata.is_expired:
151
+ return True
152
+
153
+ # Try loading from cache first
154
+ if self._load_cache():
155
+ self._loaded = True
156
+ if not self._metadata.is_expired:
157
+ logger.debug("Using cached models.dev data")
158
+ return True
159
+ logger.debug("Cache expired, will refresh in background")
160
+
161
+ # Fetch fresh data
162
+ success = await self.refresh()
163
+ return success or self._loaded
164
+
165
+ async def refresh(self, force: bool = False) -> bool:
166
+ """
167
+ Fetch fresh data from models.dev API.
168
+
169
+ Args:
170
+ force: If True, fetch even if cache is valid
171
+
172
+ Returns:
173
+ True if fetch succeeded
174
+ """
175
+ if not force and self._metadata and not self._metadata.is_expired:
176
+ return True
177
+
178
+ logger.info("Fetching models from models.dev...")
179
+
180
+ try:
181
+ raw_data = await self._fetch_api()
182
+ if raw_data:
183
+ self._raw_data = raw_data
184
+ self._parse_data(raw_data)
185
+ self._save_cache(raw_data)
186
+ self._loaded = True
187
+ logger.info(
188
+ f"Loaded {len(self._models)} providers, {sum(len(m) for m in self._models.values())} models"
189
+ )
190
+ return True
191
+ except Exception as e:
192
+ logger.error(f"Failed to fetch from models.dev: {e}")
193
+
194
+ return False
195
+
196
+ def get_providers(self) -> Dict[str, ProviderInfo]:
197
+ """Get all available providers."""
198
+ return self._providers.copy()
199
+
200
+ def get_supported_providers(self) -> Dict[str, ProviderInfo]:
201
+ """Get only the providers we actively support."""
202
+ return {pid: info for pid, info in self._providers.items() if pid in SUPPORTED_PROVIDERS}
203
+
204
+ def get_provider(self, provider_id: str) -> Optional[ProviderInfo]:
205
+ """Get a specific provider's info."""
206
+ # Check direct match first
207
+ if provider_id in self._providers:
208
+ return self._providers[provider_id]
209
+ # Check mapped IDs
210
+ mapped_id = PROVIDER_ID_MAP.get(provider_id)
211
+ if mapped_id and mapped_id in self._providers:
212
+ return self._providers[mapped_id]
213
+ return None
214
+
215
+ def get_models_for_provider(self, provider_id: str) -> Dict[str, ModelInfo]:
216
+ """Get all models for a provider."""
217
+ # Check direct match
218
+ if provider_id in self._models:
219
+ return self._models[provider_id].copy()
220
+ # Check mapped IDs
221
+ mapped_id = PROVIDER_ID_MAP.get(provider_id)
222
+ if mapped_id and mapped_id in self._models:
223
+ return self._models[mapped_id].copy()
224
+ return {}
225
+
226
+ def get_model(self, provider_id: str, model_id: str) -> Optional[ModelInfo]:
227
+ """Get a specific model's info."""
228
+ models = self.get_models_for_provider(provider_id)
229
+ return models.get(model_id)
230
+
231
+ def get_all_models(self) -> List[ModelInfo]:
232
+ """Get all models across all providers."""
233
+ all_models = []
234
+ for provider_models in self._models.values():
235
+ all_models.extend(provider_models.values())
236
+ return all_models
237
+
238
+ def search_models(self, query: str, limit: int = 20) -> List[ModelInfo]:
239
+ """
240
+ Search models by name or ID.
241
+
242
+ Args:
243
+ query: Search string (case-insensitive)
244
+ limit: Maximum results to return
245
+ """
246
+ query_lower = query.lower()
247
+ results = []
248
+
249
+ for model in self.get_all_models():
250
+ score = 0
251
+ # Exact ID match
252
+ if query_lower == model.id.lower():
253
+ score = 100
254
+ # ID contains query
255
+ elif query_lower in model.id.lower():
256
+ score = 80
257
+ # Name contains query
258
+ elif query_lower in model.name.lower():
259
+ score = 60
260
+ # Provider contains query
261
+ elif query_lower in model.provider.lower():
262
+ score = 40
263
+ # Description contains query
264
+ elif query_lower in model.description.lower():
265
+ score = 20
266
+
267
+ if score > 0:
268
+ results.append((score, model))
269
+
270
+ # Sort by score descending, then by name
271
+ results.sort(key=lambda x: (-x[0], x[1].name))
272
+ return [model for _, model in results[:limit]]
273
+
274
+ def get_cache_info(self) -> Dict[str, Any]:
275
+ """Get information about the cache status."""
276
+ return {
277
+ "loaded": self._loaded,
278
+ "provider_count": len(self._providers),
279
+ "model_count": sum(len(m) for m in self._models.values()),
280
+ "cache_file": str(CACHE_FILE),
281
+ "cache_exists": CACHE_FILE.exists(),
282
+ "fetched_at": self._metadata.fetched_at if self._metadata else None,
283
+ "is_expired": self._metadata.is_expired if self._metadata else True,
284
+ }
285
+
286
+ # ========================================================================
287
+ # PRIVATE METHODS
288
+ # ========================================================================
289
+
290
+ async def _fetch_api(self) -> Optional[Dict[str, Any]]:
291
+ """Fetch data from the models.dev API."""
292
+ # Try aiohttp first (more common in async contexts)
293
+ if HAS_AIOHTTP:
294
+ try:
295
+ async with aiohttp.ClientSession() as session:
296
+ async with session.get(
297
+ MODELS_DEV_API_URL, timeout=aiohttp.ClientTimeout(total=30)
298
+ ) as resp:
299
+ if resp.status == 200:
300
+ return await resp.json()
301
+ logger.error(f"models.dev API returned {resp.status}")
302
+ except Exception as e:
303
+ logger.warning(f"aiohttp fetch failed: {e}")
304
+
305
+ # Fallback to httpx
306
+ if HAS_HTTPX:
307
+ try:
308
+ async with httpx.AsyncClient(timeout=30) as client:
309
+ resp = await client.get(MODELS_DEV_API_URL)
310
+ if resp.status_code == 200:
311
+ return resp.json()
312
+ logger.error(f"models.dev API returned {resp.status_code}")
313
+ except Exception as e:
314
+ logger.warning(f"httpx fetch failed: {e}")
315
+
316
+ # Last resort: sync request in thread
317
+ try:
318
+ import urllib.request
319
+
320
+ loop = asyncio.get_event_loop()
321
+ data = await loop.run_in_executor(None, self._sync_fetch)
322
+ return data
323
+ except Exception as e:
324
+ logger.error(f"All fetch methods failed: {e}")
325
+
326
+ return None
327
+
328
+ def _sync_fetch(self) -> Optional[Dict[str, Any]]:
329
+ """Synchronous fallback fetch using urllib."""
330
+ import urllib.request
331
+ import ssl
332
+
333
+ ctx = ssl.create_default_context()
334
+ req = urllib.request.Request(MODELS_DEV_API_URL, headers={"User-Agent": "SuperQode/1.0"})
335
+
336
+ with urllib.request.urlopen(req, timeout=30, context=ctx) as response:
337
+ return json.loads(response.read().decode("utf-8"))
338
+
339
+ def _parse_data(self, raw_data: Dict[str, Any]) -> None:
340
+ """Parse raw models.dev data into our format."""
341
+ self._providers.clear()
342
+ self._models.clear()
343
+
344
+ for provider_id, provider_data in raw_data.items():
345
+ if not isinstance(provider_data, dict):
346
+ continue
347
+
348
+ # Normalize provider ID
349
+ normalized_id = PROVIDER_ID_MAP.get(provider_id, provider_id)
350
+
351
+ # Parse provider info
352
+ provider_info = ProviderInfo(
353
+ id=normalized_id,
354
+ name=provider_data.get("name", provider_id),
355
+ env_vars=provider_data.get("env", []),
356
+ doc_url=provider_data.get("doc", ""),
357
+ api_url=provider_data.get("api", ""),
358
+ )
359
+
360
+ # Merge if provider already exists (e.g., google-ai-studio + google-vertex)
361
+ if normalized_id in self._providers:
362
+ existing = self._providers[normalized_id]
363
+ # Merge env vars
364
+ existing.env_vars = list(set(existing.env_vars + provider_info.env_vars))
365
+ else:
366
+ self._providers[normalized_id] = provider_info
367
+
368
+ # Parse models
369
+ models_data = provider_data.get("models", {})
370
+ if normalized_id not in self._models:
371
+ self._models[normalized_id] = {}
372
+
373
+ for model_id, model_data in models_data.items():
374
+ if not isinstance(model_data, dict):
375
+ continue
376
+
377
+ model_info = self._parse_model(normalized_id, model_id, model_data)
378
+ if model_info:
379
+ self._models[normalized_id][model_id] = model_info
380
+
381
+ # Update metadata
382
+ self._metadata = CacheMetadata(
383
+ fetched_at=datetime.now().isoformat(),
384
+ ttl_hours=int(self.cache_ttl.total_seconds() / 3600),
385
+ provider_count=len(self._providers),
386
+ model_count=sum(len(m) for m in self._models.values()),
387
+ )
388
+
389
+ def _parse_model(
390
+ self, provider_id: str, model_id: str, data: Dict[str, Any]
391
+ ) -> Optional[ModelInfo]:
392
+ """Parse a single model's data."""
393
+ try:
394
+ # Extract cost info
395
+ cost = data.get("cost", {})
396
+ input_price = cost.get("input", 0) if isinstance(cost, dict) else 0
397
+ output_price = cost.get("output", 0) if isinstance(cost, dict) else 0
398
+
399
+ # Extract limits
400
+ limits = data.get("limit", {})
401
+ context_window = limits.get("context", 128000) if isinstance(limits, dict) else 128000
402
+ max_output = limits.get("output", 4096) if isinstance(limits, dict) else 4096
403
+
404
+ # Build capabilities list
405
+ capabilities = []
406
+ if data.get("tool_call"):
407
+ capabilities.append(ModelCapability.TOOLS)
408
+ if data.get("reasoning"):
409
+ capabilities.append(ModelCapability.REASONING)
410
+
411
+ # Check modalities for vision
412
+ modalities = data.get("modalities", {})
413
+ input_modalities = modalities.get("input", []) if isinstance(modalities, dict) else []
414
+ if "image" in input_modalities or "video" in input_modalities:
415
+ capabilities.append(ModelCapability.VISION)
416
+
417
+ # Assume streaming for most models
418
+ capabilities.append(ModelCapability.STREAMING)
419
+
420
+ # JSON mode if tools supported
421
+ if data.get("tool_call"):
422
+ capabilities.append(ModelCapability.JSON_MODE)
423
+
424
+ # Long context flag
425
+ if context_window >= 100000:
426
+ capabilities.append(ModelCapability.LONG_CONTEXT)
427
+
428
+ # Code optimization (heuristic based on name/family)
429
+ name_lower = data.get("name", "").lower()
430
+ family_lower = data.get("family", "").lower()
431
+ if any(kw in name_lower or kw in family_lower for kw in ["code", "coder", "codex"]):
432
+ capabilities.append(ModelCapability.CODE)
433
+
434
+ # Build description
435
+ description = ""
436
+ if data.get("reasoning"):
437
+ description = "Advanced reasoning model"
438
+ elif "flash" in name_lower or "mini" in name_lower or "haiku" in name_lower:
439
+ description = "Fast and cost-effective"
440
+ elif "opus" in name_lower or "pro" in name_lower:
441
+ description = "Most capable variant"
442
+ elif "sonnet" in name_lower:
443
+ description = "Balanced performance and cost"
444
+
445
+ # Recommended for
446
+ recommended = []
447
+ if ModelCapability.CODE in capabilities:
448
+ recommended.append("coding")
449
+ if ModelCapability.REASONING in capabilities:
450
+ recommended.append("complex reasoning")
451
+ if ModelCapability.VISION in capabilities:
452
+ recommended.append("vision")
453
+ if input_price == 0 and output_price == 0:
454
+ recommended.append("free")
455
+ if input_price < 1:
456
+ recommended.append("budget")
457
+ if not recommended:
458
+ recommended.append("general")
459
+
460
+ return ModelInfo(
461
+ id=model_id,
462
+ name=data.get("name", model_id),
463
+ provider=provider_id,
464
+ input_price=float(input_price),
465
+ output_price=float(output_price),
466
+ context_window=int(context_window),
467
+ max_output=int(max_output),
468
+ capabilities=capabilities,
469
+ description=description,
470
+ recommended_for=recommended,
471
+ released=data.get("release_date", ""),
472
+ )
473
+ except Exception as e:
474
+ logger.warning(f"Failed to parse model {provider_id}/{model_id}: {e}")
475
+ return None
476
+
477
+ def _load_cache(self) -> bool:
478
+ """Load data from local cache file."""
479
+ if not CACHE_FILE.exists():
480
+ return False
481
+
482
+ try:
483
+ with open(CACHE_FILE, "r") as f:
484
+ cache_data = json.load(f)
485
+
486
+ # Parse metadata
487
+ meta = cache_data.get("_metadata", {})
488
+ self._metadata = CacheMetadata(
489
+ fetched_at=meta.get("fetched_at", ""),
490
+ ttl_hours=meta.get("ttl_hours", 24),
491
+ provider_count=meta.get("provider_count", 0),
492
+ model_count=meta.get("model_count", 0),
493
+ )
494
+
495
+ # Parse the actual data
496
+ raw_data = {k: v for k, v in cache_data.items() if k != "_metadata"}
497
+ if raw_data:
498
+ self._raw_data = raw_data
499
+ self._parse_data(raw_data)
500
+ return True
501
+
502
+ except Exception as e:
503
+ logger.warning(f"Failed to load cache: {e}")
504
+
505
+ return False
506
+
507
+ def _save_cache(self, raw_data: Dict[str, Any]) -> bool:
508
+ """Save data to local cache file."""
509
+ try:
510
+ CACHE_FILE.parent.mkdir(parents=True, exist_ok=True)
511
+
512
+ cache_data = raw_data.copy()
513
+ cache_data["_metadata"] = {
514
+ "fetched_at": datetime.now().isoformat(),
515
+ "ttl_hours": int(self.cache_ttl.total_seconds() / 3600),
516
+ "provider_count": len(self._providers),
517
+ "model_count": sum(len(m) for m in self._models.values()),
518
+ }
519
+
520
+ with open(CACHE_FILE, "w") as f:
521
+ json.dump(cache_data, f)
522
+
523
+ logger.debug(f"Saved models cache to {CACHE_FILE}")
524
+ return True
525
+ except Exception as e:
526
+ logger.warning(f"Failed to save cache: {e}")
527
+ return False
528
+
529
+
530
+ # ============================================================================
531
+ # SINGLETON INSTANCE
532
+ # ============================================================================
533
+
534
+ _instance: Optional[ModelsDev] = None
535
+
536
+
537
+ def get_models_dev() -> ModelsDev:
538
+ """Get the singleton ModelsDev instance."""
539
+ global _instance
540
+ if _instance is None:
541
+ _instance = ModelsDev()
542
+ return _instance
543
+
544
+
545
+ async def get_model_info_live(provider_id: str, model_id: str) -> Optional[ModelInfo]:
546
+ """
547
+ Get model info, fetching from models.dev if needed.
548
+
549
+ This is a convenience function that ensures data is loaded.
550
+ """
551
+ client = get_models_dev()
552
+ await client.ensure_loaded()
553
+ return client.get_model(provider_id, model_id)
554
+
555
+
556
+ async def get_models_for_provider_live(provider_id: str) -> Dict[str, ModelInfo]:
557
+ """
558
+ Get all models for a provider, fetching from models.dev if needed.
559
+ """
560
+ client = get_models_dev()
561
+ await client.ensure_loaded()
562
+ return client.get_models_for_provider(provider_id)
563
+
564
+
565
+ # ============================================================================
566
+ # EXPORTS
567
+ # ============================================================================
568
+
569
+ __all__ = [
570
+ "ModelsDev",
571
+ "ProviderInfo",
572
+ "CacheMetadata",
573
+ "get_models_dev",
574
+ "get_model_info_live",
575
+ "get_models_for_provider_live",
576
+ "SUPPORTED_PROVIDERS",
577
+ "MODELS_DEV_API_URL",
578
+ ]
@@ -0,0 +1,87 @@
1
+ """
2
+ Open Responses Provider - Unified API Integration.
3
+
4
+ Implements the Open Responses specification for a consistent API across
5
+ multiple AI providers. Supports:
6
+ - Streaming with 45+ event types
7
+ - Reasoning/thinking content
8
+ - Built-in tools (apply_patch, code_interpreter, file_search)
9
+ - Message ↔ Item conversion
10
+
11
+ Usage:
12
+ from superqode.providers.openresponses import OpenResponsesGateway
13
+
14
+ gateway = OpenResponsesGateway(base_url="http://localhost:8080")
15
+ response = await gateway.chat_completion(messages, model="qwen3:8b")
16
+ """
17
+
18
+ from .schema.models import (
19
+ # Request/Response types
20
+ ResponseRequest,
21
+ Response,
22
+ ResponseUsage,
23
+ # Item types
24
+ ItemParam,
25
+ UserMessageItemParam,
26
+ AssistantMessageItemParam,
27
+ SystemMessageItemParam,
28
+ FunctionCallItemParam,
29
+ FunctionCallOutputItemParam,
30
+ # Content types
31
+ TextContentParam,
32
+ ImageContentParam,
33
+ # Tool types
34
+ FunctionToolParam,
35
+ CodeInterpreterToolParam,
36
+ FileSearchToolParam,
37
+ ApplyPatchToolParam,
38
+ # Streaming events
39
+ StreamingEvent,
40
+ ResponseCreatedEvent,
41
+ ResponseInProgressEvent,
42
+ ResponseCompletedEvent,
43
+ ResponseOutputTextDeltaEvent,
44
+ ResponseReasoningDeltaEvent,
45
+ ResponseFunctionCallArgumentsDeltaEvent,
46
+ )
47
+
48
+ from .converters.messages import (
49
+ messages_to_items,
50
+ items_to_messages,
51
+ )
52
+
53
+ from .converters.tools import (
54
+ convert_tools_to_openresponses,
55
+ convert_tools_from_openresponses,
56
+ )
57
+
58
+ __all__ = [
59
+ # Schema types
60
+ "ResponseRequest",
61
+ "Response",
62
+ "ResponseUsage",
63
+ "ItemParam",
64
+ "UserMessageItemParam",
65
+ "AssistantMessageItemParam",
66
+ "SystemMessageItemParam",
67
+ "FunctionCallItemParam",
68
+ "FunctionCallOutputItemParam",
69
+ "TextContentParam",
70
+ "ImageContentParam",
71
+ "FunctionToolParam",
72
+ "CodeInterpreterToolParam",
73
+ "FileSearchToolParam",
74
+ "ApplyPatchToolParam",
75
+ "StreamingEvent",
76
+ "ResponseCreatedEvent",
77
+ "ResponseInProgressEvent",
78
+ "ResponseCompletedEvent",
79
+ "ResponseOutputTextDeltaEvent",
80
+ "ResponseReasoningDeltaEvent",
81
+ "ResponseFunctionCallArgumentsDeltaEvent",
82
+ # Converters
83
+ "messages_to_items",
84
+ "items_to_messages",
85
+ "convert_tools_to_openresponses",
86
+ "convert_tools_from_openresponses",
87
+ ]
@@ -0,0 +1,17 @@
1
+ """
2
+ Open Responses Format Converters.
3
+
4
+ Provides bidirectional conversion between:
5
+ - OpenAI-style messages and Open Responses items
6
+ - Gateway tool definitions and Open Responses tools
7
+ """
8
+
9
+ from .messages import messages_to_items, items_to_messages
10
+ from .tools import convert_tools_to_openresponses, convert_tools_from_openresponses
11
+
12
+ __all__ = [
13
+ "messages_to_items",
14
+ "items_to_messages",
15
+ "convert_tools_to_openresponses",
16
+ "convert_tools_from_openresponses",
17
+ ]