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.
- superqode/__init__.py +33 -0
- superqode/acp/__init__.py +23 -0
- superqode/acp/client.py +913 -0
- superqode/acp/permission_screen.py +457 -0
- superqode/acp/types.py +480 -0
- superqode/acp_discovery.py +856 -0
- superqode/agent/__init__.py +22 -0
- superqode/agent/edit_strategies.py +334 -0
- superqode/agent/loop.py +892 -0
- superqode/agent/qe_report_templates.py +39 -0
- superqode/agent/system_prompts.py +353 -0
- superqode/agent_output.py +721 -0
- superqode/agent_stream.py +953 -0
- superqode/agents/__init__.py +59 -0
- superqode/agents/acp_registry.py +305 -0
- superqode/agents/client.py +249 -0
- superqode/agents/data/augmentcode.com.toml +51 -0
- superqode/agents/data/cagent.dev.toml +51 -0
- superqode/agents/data/claude.com.toml +60 -0
- superqode/agents/data/codeassistant.dev.toml +51 -0
- superqode/agents/data/codex.openai.com.toml +57 -0
- superqode/agents/data/fastagent.ai.toml +66 -0
- superqode/agents/data/geminicli.com.toml +77 -0
- superqode/agents/data/goose.block.xyz.toml +54 -0
- superqode/agents/data/junie.jetbrains.com.toml +56 -0
- superqode/agents/data/kimi.moonshot.cn.toml +57 -0
- superqode/agents/data/llmlingagent.dev.toml +51 -0
- superqode/agents/data/molt.bot.toml +49 -0
- superqode/agents/data/opencode.ai.toml +60 -0
- superqode/agents/data/stakpak.dev.toml +51 -0
- superqode/agents/data/vtcode.dev.toml +51 -0
- superqode/agents/discovery.py +266 -0
- superqode/agents/messaging.py +160 -0
- superqode/agents/persona.py +166 -0
- superqode/agents/registry.py +421 -0
- superqode/agents/schema.py +72 -0
- superqode/agents/unified.py +367 -0
- superqode/app/__init__.py +111 -0
- superqode/app/constants.py +314 -0
- superqode/app/css.py +366 -0
- superqode/app/models.py +118 -0
- superqode/app/suggester.py +125 -0
- superqode/app/widgets.py +1591 -0
- superqode/app_enhanced.py +399 -0
- superqode/app_main.py +17187 -0
- superqode/approval.py +312 -0
- superqode/atomic.py +296 -0
- superqode/commands/__init__.py +1 -0
- superqode/commands/acp.py +965 -0
- superqode/commands/agents.py +180 -0
- superqode/commands/auth.py +278 -0
- superqode/commands/config.py +374 -0
- superqode/commands/init.py +826 -0
- superqode/commands/providers.py +819 -0
- superqode/commands/qe.py +1145 -0
- superqode/commands/roles.py +380 -0
- superqode/commands/serve.py +172 -0
- superqode/commands/suggestions.py +127 -0
- superqode/commands/superqe.py +460 -0
- superqode/config/__init__.py +51 -0
- superqode/config/loader.py +812 -0
- superqode/config/schema.py +498 -0
- superqode/core/__init__.py +111 -0
- superqode/core/roles.py +281 -0
- superqode/danger.py +386 -0
- superqode/data/superqode-template.yaml +1522 -0
- superqode/design_system.py +1080 -0
- superqode/dialogs/__init__.py +6 -0
- superqode/dialogs/base.py +39 -0
- superqode/dialogs/model.py +130 -0
- superqode/dialogs/provider.py +870 -0
- superqode/diff_view.py +919 -0
- superqode/enterprise.py +21 -0
- superqode/evaluation/__init__.py +25 -0
- superqode/evaluation/adapters.py +93 -0
- superqode/evaluation/behaviors.py +89 -0
- superqode/evaluation/engine.py +209 -0
- superqode/evaluation/scenarios.py +96 -0
- superqode/execution/__init__.py +36 -0
- superqode/execution/linter.py +538 -0
- superqode/execution/modes.py +347 -0
- superqode/execution/resolver.py +283 -0
- superqode/execution/runner.py +642 -0
- superqode/file_explorer.py +811 -0
- superqode/file_viewer.py +471 -0
- superqode/flash.py +183 -0
- superqode/guidance/__init__.py +58 -0
- superqode/guidance/config.py +203 -0
- superqode/guidance/prompts.py +71 -0
- superqode/harness/__init__.py +54 -0
- superqode/harness/accelerator.py +291 -0
- superqode/harness/config.py +319 -0
- superqode/harness/validator.py +147 -0
- superqode/history.py +279 -0
- superqode/integrations/superopt_runner.py +124 -0
- superqode/logging/__init__.py +49 -0
- superqode/logging/adapters.py +219 -0
- superqode/logging/formatter.py +923 -0
- superqode/logging/integration.py +341 -0
- superqode/logging/sinks.py +170 -0
- superqode/logging/unified_log.py +417 -0
- superqode/lsp/__init__.py +26 -0
- superqode/lsp/client.py +544 -0
- superqode/main.py +1069 -0
- superqode/mcp/__init__.py +89 -0
- superqode/mcp/auth_storage.py +380 -0
- superqode/mcp/client.py +1236 -0
- superqode/mcp/config.py +319 -0
- superqode/mcp/integration.py +337 -0
- superqode/mcp/oauth.py +436 -0
- superqode/mcp/oauth_callback.py +385 -0
- superqode/mcp/types.py +290 -0
- superqode/memory/__init__.py +31 -0
- superqode/memory/feedback.py +342 -0
- superqode/memory/store.py +522 -0
- superqode/notifications.py +369 -0
- superqode/optimization/__init__.py +5 -0
- superqode/optimization/config.py +33 -0
- superqode/permissions/__init__.py +25 -0
- superqode/permissions/rules.py +488 -0
- superqode/plan.py +323 -0
- superqode/providers/__init__.py +33 -0
- superqode/providers/gateway/__init__.py +165 -0
- superqode/providers/gateway/base.py +228 -0
- superqode/providers/gateway/litellm_gateway.py +1170 -0
- superqode/providers/gateway/openresponses_gateway.py +436 -0
- superqode/providers/health.py +297 -0
- superqode/providers/huggingface/__init__.py +74 -0
- superqode/providers/huggingface/downloader.py +472 -0
- superqode/providers/huggingface/endpoints.py +442 -0
- superqode/providers/huggingface/hub.py +531 -0
- superqode/providers/huggingface/inference.py +394 -0
- superqode/providers/huggingface/transformers_runner.py +516 -0
- superqode/providers/local/__init__.py +100 -0
- superqode/providers/local/base.py +438 -0
- superqode/providers/local/discovery.py +418 -0
- superqode/providers/local/lmstudio.py +256 -0
- superqode/providers/local/mlx.py +457 -0
- superqode/providers/local/ollama.py +486 -0
- superqode/providers/local/sglang.py +268 -0
- superqode/providers/local/tgi.py +260 -0
- superqode/providers/local/tool_support.py +477 -0
- superqode/providers/local/vllm.py +258 -0
- superqode/providers/manager.py +1338 -0
- superqode/providers/models.py +1016 -0
- superqode/providers/models_dev.py +578 -0
- superqode/providers/openresponses/__init__.py +87 -0
- superqode/providers/openresponses/converters/__init__.py +17 -0
- superqode/providers/openresponses/converters/messages.py +343 -0
- superqode/providers/openresponses/converters/tools.py +268 -0
- superqode/providers/openresponses/schema/__init__.py +56 -0
- superqode/providers/openresponses/schema/models.py +585 -0
- superqode/providers/openresponses/streaming/__init__.py +5 -0
- superqode/providers/openresponses/streaming/parser.py +338 -0
- superqode/providers/openresponses/tools/__init__.py +21 -0
- superqode/providers/openresponses/tools/apply_patch.py +352 -0
- superqode/providers/openresponses/tools/code_interpreter.py +290 -0
- superqode/providers/openresponses/tools/file_search.py +333 -0
- superqode/providers/openresponses/tools/mcp_adapter.py +252 -0
- superqode/providers/registry.py +716 -0
- superqode/providers/usage.py +332 -0
- superqode/pure_mode.py +384 -0
- superqode/qr/__init__.py +23 -0
- superqode/qr/dashboard.py +781 -0
- superqode/qr/generator.py +1018 -0
- superqode/qr/templates.py +135 -0
- superqode/safety/__init__.py +41 -0
- superqode/safety/sandbox.py +413 -0
- superqode/safety/warnings.py +256 -0
- superqode/server/__init__.py +33 -0
- superqode/server/lsp_server.py +775 -0
- superqode/server/web.py +250 -0
- superqode/session/__init__.py +25 -0
- superqode/session/persistence.py +580 -0
- superqode/session/sharing.py +477 -0
- superqode/session.py +475 -0
- superqode/sidebar.py +2991 -0
- superqode/stream_view.py +648 -0
- superqode/styles/__init__.py +3 -0
- superqode/superqe/__init__.py +184 -0
- superqode/superqe/acp_runner.py +1064 -0
- superqode/superqe/constitution/__init__.py +62 -0
- superqode/superqe/constitution/evaluator.py +308 -0
- superqode/superqe/constitution/loader.py +432 -0
- superqode/superqe/constitution/schema.py +250 -0
- superqode/superqe/events.py +591 -0
- superqode/superqe/frameworks/__init__.py +65 -0
- superqode/superqe/frameworks/base.py +234 -0
- superqode/superqe/frameworks/e2e.py +263 -0
- superqode/superqe/frameworks/executor.py +237 -0
- superqode/superqe/frameworks/javascript.py +409 -0
- superqode/superqe/frameworks/python.py +373 -0
- superqode/superqe/frameworks/registry.py +92 -0
- superqode/superqe/mcp_tools/__init__.py +47 -0
- superqode/superqe/mcp_tools/core_tools.py +418 -0
- superqode/superqe/mcp_tools/registry.py +230 -0
- superqode/superqe/mcp_tools/testing_tools.py +167 -0
- superqode/superqe/noise.py +89 -0
- superqode/superqe/orchestrator.py +778 -0
- superqode/superqe/roles.py +609 -0
- superqode/superqe/session.py +713 -0
- superqode/superqe/skills/__init__.py +57 -0
- superqode/superqe/skills/base.py +106 -0
- superqode/superqe/skills/core_skills.py +899 -0
- superqode/superqe/skills/registry.py +90 -0
- superqode/superqe/verifier.py +101 -0
- superqode/superqe_cli.py +76 -0
- superqode/tool_call.py +358 -0
- superqode/tools/__init__.py +93 -0
- superqode/tools/agent_tools.py +496 -0
- superqode/tools/base.py +324 -0
- superqode/tools/batch_tool.py +133 -0
- superqode/tools/diagnostics.py +311 -0
- superqode/tools/edit_tools.py +653 -0
- superqode/tools/enhanced_base.py +515 -0
- superqode/tools/file_tools.py +269 -0
- superqode/tools/file_tracking.py +45 -0
- superqode/tools/lsp_tools.py +610 -0
- superqode/tools/network_tools.py +350 -0
- superqode/tools/permissions.py +400 -0
- superqode/tools/question_tool.py +324 -0
- superqode/tools/search_tools.py +598 -0
- superqode/tools/shell_tools.py +259 -0
- superqode/tools/todo_tools.py +121 -0
- superqode/tools/validation.py +80 -0
- superqode/tools/web_tools.py +639 -0
- superqode/tui.py +1152 -0
- superqode/tui_integration.py +875 -0
- superqode/tui_widgets/__init__.py +27 -0
- superqode/tui_widgets/widgets/__init__.py +18 -0
- superqode/tui_widgets/widgets/progress.py +185 -0
- superqode/tui_widgets/widgets/tool_display.py +188 -0
- superqode/undo_manager.py +574 -0
- superqode/utils/__init__.py +5 -0
- superqode/utils/error_handling.py +323 -0
- superqode/utils/fuzzy.py +257 -0
- superqode/widgets/__init__.py +477 -0
- superqode/widgets/agent_collab.py +390 -0
- superqode/widgets/agent_store.py +936 -0
- superqode/widgets/agent_switcher.py +395 -0
- superqode/widgets/animation_manager.py +284 -0
- superqode/widgets/code_context.py +356 -0
- superqode/widgets/command_palette.py +412 -0
- superqode/widgets/connection_status.py +537 -0
- superqode/widgets/conversation_history.py +470 -0
- superqode/widgets/diff_indicator.py +155 -0
- superqode/widgets/enhanced_status_bar.py +385 -0
- superqode/widgets/enhanced_toast.py +476 -0
- superqode/widgets/file_browser.py +809 -0
- superqode/widgets/file_reference.py +585 -0
- superqode/widgets/issue_timeline.py +340 -0
- superqode/widgets/leader_key.py +264 -0
- superqode/widgets/mode_switcher.py +445 -0
- superqode/widgets/model_picker.py +234 -0
- superqode/widgets/permission_preview.py +1205 -0
- superqode/widgets/prompt.py +358 -0
- superqode/widgets/provider_connect.py +725 -0
- superqode/widgets/pty_shell.py +587 -0
- superqode/widgets/qe_dashboard.py +321 -0
- superqode/widgets/resizable_sidebar.py +377 -0
- superqode/widgets/response_changes.py +218 -0
- superqode/widgets/response_display.py +528 -0
- superqode/widgets/rich_tool_display.py +613 -0
- superqode/widgets/sidebar_panels.py +1180 -0
- superqode/widgets/slash_complete.py +356 -0
- superqode/widgets/split_view.py +612 -0
- superqode/widgets/status_bar.py +273 -0
- superqode/widgets/superqode_display.py +786 -0
- superqode/widgets/thinking_display.py +815 -0
- superqode/widgets/throbber.py +87 -0
- superqode/widgets/toast.py +206 -0
- superqode/widgets/unified_output.py +1073 -0
- superqode/workspace/__init__.py +75 -0
- superqode/workspace/artifacts.py +472 -0
- superqode/workspace/coordinator.py +353 -0
- superqode/workspace/diff_tracker.py +429 -0
- superqode/workspace/git_guard.py +373 -0
- superqode/workspace/git_snapshot.py +526 -0
- superqode/workspace/manager.py +750 -0
- superqode/workspace/snapshot.py +357 -0
- superqode/workspace/watcher.py +535 -0
- superqode/workspace/worktree.py +440 -0
- superqode-0.1.5.dist-info/METADATA +204 -0
- superqode-0.1.5.dist-info/RECORD +288 -0
- superqode-0.1.5.dist-info/WHEEL +5 -0
- superqode-0.1.5.dist-info/entry_points.txt +3 -0
- superqode-0.1.5.dist-info/licenses/LICENSE +648 -0
- 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
|
+
]
|