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,477 @@
1
+ """Tool-calling capability detection and testing for local models.
2
+
3
+ This module provides utilities for detecting and testing whether local
4
+ models support function/tool calling, which is critical for coding assistants.
5
+ """
6
+
7
+ import asyncio
8
+ import json
9
+ import time
10
+ from dataclasses import dataclass, field
11
+ from typing import Any, Dict, List, Optional, Set
12
+
13
+ from superqode.providers.local.base import (
14
+ LocalModel,
15
+ ToolTestResult,
16
+ detect_model_family,
17
+ )
18
+
19
+
20
+ # Models known to support tool calling well (by family and version)
21
+ TOOL_CAPABLE_MODELS: Dict[str, Dict[str, Any]] = {
22
+ # Llama family
23
+ "llama3.1": {
24
+ "supports_tools": True,
25
+ "parallel_tools": True,
26
+ "tool_choice": ["auto", "required", "none"],
27
+ "notes": "Native tool support in Llama 3.1+",
28
+ },
29
+ "llama3.2": {
30
+ "supports_tools": True,
31
+ "parallel_tools": True,
32
+ "tool_choice": ["auto", "required", "none"],
33
+ "notes": "Native tool support",
34
+ },
35
+ "llama3.3": {
36
+ "supports_tools": True,
37
+ "parallel_tools": True,
38
+ "tool_choice": ["auto", "required", "none"],
39
+ "notes": "Latest Llama with improved tool support",
40
+ },
41
+ # Qwen family
42
+ "qwen2.5": {
43
+ "supports_tools": True,
44
+ "parallel_tools": True,
45
+ "tool_choice": ["auto", "required", "none"],
46
+ "notes": "Excellent tool support in Qwen 2.5",
47
+ "recommended_params": {"num_ctx": 16384},
48
+ },
49
+ "qwen2.5-coder": {
50
+ "supports_tools": True,
51
+ "parallel_tools": True,
52
+ "tool_choice": ["auto", "required", "none"],
53
+ "notes": "Optimized for code, great tool support",
54
+ "recommended_params": {"num_ctx": 32768},
55
+ },
56
+ # Mistral family
57
+ "mistral": {
58
+ "supports_tools": True,
59
+ "parallel_tools": False,
60
+ "tool_choice": ["auto", "none"],
61
+ "notes": "Good tool support",
62
+ },
63
+ "mixtral": {
64
+ "supports_tools": True,
65
+ "parallel_tools": True,
66
+ "tool_choice": ["auto", "required", "none"],
67
+ "notes": "MoE with tool support",
68
+ },
69
+ # DeepSeek
70
+ "deepseek-coder": {
71
+ "supports_tools": True,
72
+ "parallel_tools": False,
73
+ "tool_choice": ["auto"],
74
+ "notes": "Code-focused with tool support",
75
+ },
76
+ "deepseek-coder-v2": {
77
+ "supports_tools": True,
78
+ "parallel_tools": True,
79
+ "tool_choice": ["auto", "required"],
80
+ "notes": "Improved tool support in v2",
81
+ },
82
+ # Command-R
83
+ "command-r": {
84
+ "supports_tools": True,
85
+ "parallel_tools": True,
86
+ "tool_choice": ["auto", "required", "none"],
87
+ "notes": "Cohere's tool-focused model",
88
+ },
89
+ # Hermes (fine-tuned for tools)
90
+ "hermes": {
91
+ "supports_tools": True,
92
+ "parallel_tools": True,
93
+ "tool_choice": ["auto", "required"],
94
+ "notes": "Fine-tuned specifically for function calling",
95
+ },
96
+ "nous-hermes": {
97
+ "supports_tools": True,
98
+ "parallel_tools": True,
99
+ "tool_choice": ["auto", "required"],
100
+ "notes": "NousResearch fine-tune for tools",
101
+ },
102
+ # Functionary (specialized for function calling)
103
+ "functionary": {
104
+ "supports_tools": True,
105
+ "parallel_tools": True,
106
+ "tool_choice": ["auto", "required", "none"],
107
+ "notes": "Specialized for OpenAI-compatible function calling",
108
+ },
109
+ }
110
+
111
+ # Models that need special handling or have quirks
112
+ TOOL_QUIRKS: Dict[str, Dict[str, Any]] = {
113
+ "qwen2.5-coder": {
114
+ "needs_num_ctx": 16384, # Needs larger context for tools
115
+ "json_mode_helps": True,
116
+ },
117
+ "llama3.1": {
118
+ "supports_parallel_tools": True,
119
+ "native_tool_format": True,
120
+ },
121
+ "mistral": {
122
+ "tool_use_special_tokens": True,
123
+ "max_tools_per_call": 1, # Single tool per response
124
+ },
125
+ }
126
+
127
+ # Models that definitely do NOT support tools
128
+ NO_TOOL_SUPPORT: Set[str] = {
129
+ "tinyllama",
130
+ "phi-2",
131
+ "gemma", # Base Gemma doesn't support tools
132
+ "stablelm",
133
+ "falcon",
134
+ "mpt",
135
+ "dolly",
136
+ "vicuna",
137
+ "alpaca",
138
+ }
139
+
140
+ # Test tool definition for capability testing
141
+ TEST_TOOL = {
142
+ "type": "function",
143
+ "function": {
144
+ "name": "get_weather",
145
+ "description": "Get the current weather for a city",
146
+ "parameters": {
147
+ "type": "object",
148
+ "properties": {
149
+ "city": {"type": "string", "description": "The city name"},
150
+ "unit": {
151
+ "type": "string",
152
+ "enum": ["celsius", "fahrenheit"],
153
+ "description": "Temperature unit",
154
+ },
155
+ },
156
+ "required": ["city"],
157
+ },
158
+ },
159
+ }
160
+
161
+ # Test message that should trigger tool use
162
+ TEST_MESSAGE = {"role": "user", "content": "What's the weather like in Paris?"}
163
+
164
+
165
+ @dataclass
166
+ class ToolCapabilityInfo:
167
+ """Detailed tool capability information for a model.
168
+
169
+ Attributes:
170
+ model_id: Model identifier
171
+ supports_tools: Whether tool calling is supported
172
+ parallel_tools: Whether parallel tool calls are supported
173
+ tool_choice_modes: Supported tool_choice modes
174
+ recommended_params: Recommended parameters for tool use
175
+ quirks: Known quirks and workarounds
176
+ verified: Whether capability was verified by testing
177
+ confidence: Confidence level (heuristic, tested, confirmed)
178
+ notes: Additional notes
179
+ """
180
+
181
+ model_id: str
182
+ supports_tools: bool = False
183
+ parallel_tools: bool = False
184
+ tool_choice_modes: List[str] = field(default_factory=list)
185
+ recommended_params: Dict[str, Any] = field(default_factory=dict)
186
+ quirks: Dict[str, Any] = field(default_factory=dict)
187
+ verified: bool = False
188
+ confidence: str = "unknown" # unknown, heuristic, tested, confirmed
189
+ notes: str = ""
190
+
191
+
192
+ def get_tool_capability_info(model_id: str) -> ToolCapabilityInfo:
193
+ """Get tool capability information for a model based on heuristics.
194
+
195
+ This provides a quick assessment without actually testing the model.
196
+
197
+ Args:
198
+ model_id: Model identifier (e.g., "llama3.2:8b-instruct-q4_K_M")
199
+
200
+ Returns:
201
+ ToolCapabilityInfo with heuristic-based assessment.
202
+ """
203
+ model_lower = model_id.lower()
204
+
205
+ # Check if definitely not supported
206
+ for pattern in NO_TOOL_SUPPORT:
207
+ if pattern in model_lower:
208
+ return ToolCapabilityInfo(
209
+ model_id=model_id,
210
+ supports_tools=False,
211
+ confidence="heuristic",
212
+ notes=f"Model family '{pattern}' does not support tools",
213
+ )
214
+
215
+ # Check known capable models
216
+ for pattern, info in TOOL_CAPABLE_MODELS.items():
217
+ if pattern in model_lower:
218
+ quirks = TOOL_QUIRKS.get(pattern, {})
219
+ return ToolCapabilityInfo(
220
+ model_id=model_id,
221
+ supports_tools=info.get("supports_tools", False),
222
+ parallel_tools=info.get("parallel_tools", False),
223
+ tool_choice_modes=info.get("tool_choice", []),
224
+ recommended_params=info.get("recommended_params", {}),
225
+ quirks=quirks,
226
+ confidence="heuristic",
227
+ notes=info.get("notes", ""),
228
+ )
229
+
230
+ # Unknown - check for instruct variant which might support tools
231
+ if any(x in model_lower for x in ["instruct", "chat", "assistant"]):
232
+ return ToolCapabilityInfo(
233
+ model_id=model_id,
234
+ supports_tools=False, # Assume no until tested
235
+ confidence="unknown",
236
+ notes="Instruct model, may support tools. Test to confirm.",
237
+ )
238
+
239
+ return ToolCapabilityInfo(
240
+ model_id=model_id,
241
+ supports_tools=False,
242
+ confidence="unknown",
243
+ notes="Unknown model, tool support uncertain",
244
+ )
245
+
246
+
247
+ async def test_tool_calling(
248
+ model_id: str, provider_host: str = "http://localhost:11434", timeout: float = 60.0
249
+ ) -> ToolTestResult:
250
+ """Test if a model can execute tool calls.
251
+
252
+ Performs an actual API call to test tool calling capability.
253
+
254
+ Args:
255
+ model_id: Model identifier
256
+ provider_host: Provider host URL (default: Ollama)
257
+ timeout: Request timeout in seconds
258
+
259
+ Returns:
260
+ ToolTestResult with actual test results.
261
+ """
262
+ from urllib.request import Request, urlopen
263
+ from urllib.error import URLError, HTTPError
264
+
265
+ start_time = time.time()
266
+
267
+ # First check heuristics
268
+ info = get_tool_capability_info(model_id)
269
+ if info.confidence == "heuristic" and not info.supports_tools:
270
+ return ToolTestResult(model_id=model_id, supports_tools=False, notes=info.notes)
271
+
272
+ # Prepare test request
273
+ endpoint = f"{provider_host}/api/chat"
274
+
275
+ payload = {
276
+ "model": model_id,
277
+ "messages": [TEST_MESSAGE],
278
+ "tools": [TEST_TOOL],
279
+ "stream": False,
280
+ }
281
+
282
+ # Add recommended params if any
283
+ if info.recommended_params:
284
+ payload["options"] = info.recommended_params
285
+
286
+ loop = asyncio.get_event_loop()
287
+
288
+ def do_test():
289
+ try:
290
+ headers = {"Content-Type": "application/json"}
291
+ body = json.dumps(payload).encode("utf-8")
292
+ request = Request(endpoint, data=body, headers=headers, method="POST")
293
+
294
+ with urlopen(request, timeout=timeout) as response:
295
+ return json.loads(response.read().decode("utf-8"))
296
+
297
+ except HTTPError as e:
298
+ if e.code == 400:
299
+ # Model might not support tools parameter
300
+ return {"error": "tools_not_supported", "code": 400}
301
+ return {"error": str(e), "code": e.code}
302
+ except URLError as e:
303
+ return {"error": str(e.reason)}
304
+ except Exception as e:
305
+ return {"error": str(e)}
306
+
307
+ try:
308
+ response = await loop.run_in_executor(None, do_test)
309
+ latency = (time.time() - start_time) * 1000
310
+
311
+ # Check for errors
312
+ if "error" in response:
313
+ error_msg = response.get("error", "")
314
+ if "tools_not_supported" in str(error_msg) or response.get("code") == 400:
315
+ return ToolTestResult(
316
+ model_id=model_id,
317
+ supports_tools=False,
318
+ latency_ms=latency,
319
+ notes="Model returned error for tools parameter",
320
+ )
321
+ return ToolTestResult(
322
+ model_id=model_id,
323
+ supports_tools=False,
324
+ error=str(error_msg),
325
+ latency_ms=latency,
326
+ )
327
+
328
+ # Check for tool calls in response
329
+ message = response.get("message", {})
330
+ tool_calls = message.get("tool_calls", [])
331
+
332
+ if tool_calls:
333
+ # Verify tool call structure
334
+ valid_call = False
335
+ parallel = len(tool_calls) > 1
336
+
337
+ for call in tool_calls:
338
+ func = call.get("function", {})
339
+ if func.get("name") == "get_weather":
340
+ valid_call = True
341
+ break
342
+
343
+ return ToolTestResult(
344
+ model_id=model_id,
345
+ supports_tools=valid_call,
346
+ parallel_tools=parallel,
347
+ tool_choice=info.tool_choice_modes or ["auto"],
348
+ latency_ms=latency,
349
+ notes="Tool calling verified by test"
350
+ if valid_call
351
+ else "Response had tool_calls but wrong function",
352
+ )
353
+ else:
354
+ # Model responded but didn't use tools
355
+ content = message.get("content", "")
356
+ if content:
357
+ return ToolTestResult(
358
+ model_id=model_id,
359
+ supports_tools=False,
360
+ latency_ms=latency,
361
+ notes="Model responded with text instead of tool call",
362
+ )
363
+
364
+ return ToolTestResult(
365
+ model_id=model_id,
366
+ supports_tools=False,
367
+ latency_ms=latency,
368
+ notes="Empty response, tool calling may not be supported",
369
+ )
370
+
371
+ except Exception as e:
372
+ return ToolTestResult(
373
+ model_id=model_id,
374
+ supports_tools=False,
375
+ error=str(e),
376
+ notes="Test failed with exception",
377
+ )
378
+
379
+
380
+ def get_recommended_coding_models() -> List[Dict[str, Any]]:
381
+ """Get list of models recommended for coding with tool support.
382
+
383
+ Returns:
384
+ List of model recommendations with capability info.
385
+ """
386
+ recommendations = [
387
+ {
388
+ "model": "qwen2.5-coder:32b",
389
+ "family": "qwen",
390
+ "params": "32B",
391
+ "tool_support": "excellent",
392
+ "coding_quality": "excellent",
393
+ "context": "32K",
394
+ "notes": "Best for coding with tools, requires ~20GB VRAM",
395
+ },
396
+ {
397
+ "model": "qwen2.5-coder:7b",
398
+ "family": "qwen",
399
+ "params": "7B",
400
+ "tool_support": "excellent",
401
+ "coding_quality": "very good",
402
+ "context": "32K",
403
+ "notes": "Good balance of quality and resources",
404
+ },
405
+ {
406
+ "model": "llama3.3:70b",
407
+ "family": "llama",
408
+ "params": "70B",
409
+ "tool_support": "excellent",
410
+ "coding_quality": "excellent",
411
+ "context": "128K",
412
+ "notes": "Latest Llama, excellent overall",
413
+ },
414
+ {
415
+ "model": "llama3.2:8b",
416
+ "family": "llama",
417
+ "params": "8B",
418
+ "tool_support": "excellent",
419
+ "coding_quality": "good",
420
+ "context": "128K",
421
+ "notes": "Efficient with native tool support",
422
+ },
423
+ {
424
+ "model": "deepseek-coder-v2:16b",
425
+ "family": "deepseek",
426
+ "params": "16B",
427
+ "tool_support": "good",
428
+ "coding_quality": "excellent",
429
+ "context": "128K",
430
+ "notes": "Specialized for code generation",
431
+ },
432
+ {
433
+ "model": "mistral:7b",
434
+ "family": "mistral",
435
+ "params": "7B",
436
+ "tool_support": "good",
437
+ "coding_quality": "good",
438
+ "context": "32K",
439
+ "notes": "Reliable tool support, efficient",
440
+ },
441
+ {
442
+ "model": "functionary:latest",
443
+ "family": "functionary",
444
+ "params": "varies",
445
+ "tool_support": "excellent",
446
+ "coding_quality": "good",
447
+ "context": "8K",
448
+ "notes": "Specialized for function calling",
449
+ },
450
+ ]
451
+
452
+ return recommendations
453
+
454
+
455
+ def estimate_tool_support(model: LocalModel) -> str:
456
+ """Estimate tool support level for a model.
457
+
458
+ Args:
459
+ model: LocalModel instance
460
+
461
+ Returns:
462
+ Support level: "excellent", "good", "limited", "none", "unknown"
463
+ """
464
+ info = get_tool_capability_info(model.id)
465
+
466
+ if not info.supports_tools:
467
+ if info.confidence == "heuristic":
468
+ return "none"
469
+ return "unknown"
470
+
471
+ if info.parallel_tools and len(info.tool_choice_modes) >= 3:
472
+ return "excellent"
473
+
474
+ if info.supports_tools:
475
+ return "good"
476
+
477
+ return "limited"