utim-cli 2.0.1__tar.gz → 2.0.2__tar.gz
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.
- {utim_cli-2.0.1 → utim_cli-2.0.2}/CHANGELOG.md +5 -0
- {utim_cli-2.0.1/utim_cli.egg-info → utim_cli-2.0.2}/PKG-INFO +1 -1
- {utim_cli-2.0.1 → utim_cli-2.0.2}/pyproject.toml +1 -1
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/__init__.py +1 -1
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/_version.py +1 -1
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/orchestrator.py +44 -187
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/tools.py +170 -28
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/tui/model_dialog.py +40 -36
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/utim.py +3 -3
- {utim_cli-2.0.1 → utim_cli-2.0.2/utim_cli.egg-info}/PKG-INFO +1 -1
- {utim_cli-2.0.1 → utim_cli-2.0.2}/LICENSE +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/MANIFEST.in +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/README.md +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/setup.cfg +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/setup.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/_check_obj.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/_find_mcp.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/_find_plan.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/_find_tool.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/_patch_multimodal.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/_test_regex.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/agent.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/auth.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/backup.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/billing.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/blender_agent.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/bootstrap.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/client_utils.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/config.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/constants.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/context_pruner.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/doctor.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/harbor.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/knowledge_graph.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/local_db.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/logger.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/mcp_clean_wrapper.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/mcp_client.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/mcp_registry.json +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/reflection.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/report.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/scrapy_search.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/server/__init__.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/server/auth.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/server/db.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/server/docs_md/about.md +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/server/docs_md/changelog.md +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/server/docs_md/docs.md +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/server/docs_md/features.md +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/server/docs_md/license.md +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/server/docs_md/pricing.md +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/server/docs_md/privacy.md +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/server/docs_md/refund.md +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/server/docs_md/support.md +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/server/docs_md/terms.md +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/server/email_utils.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/server/exchange_rate.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/server/firebase.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/server/history.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/server/logging_config.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/server/models.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/server/pricing_updater.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/server/rate_limit.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/server/router.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/server/routes/__init__.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/server/routes/auth_routes.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/server/routes/completion_routes.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/server/routes/credit_routes.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/server/routes/feedback_routes.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/server/routes/marketplace_routes.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/server/routes/quota_routes.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/server/routes/quota_share_routes.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/server/routes/referral_routes.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/server/routes/security_routes.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/server/routes/session_routes.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/server/routes/share_routes.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/server/server.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/server/storage_nodes.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/share.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/share_tui.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/situational_scoring.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/state.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/tui/__init__.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/tui/feedback_dialog.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/tui/history_dialog.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/tui/marketplace_app_state.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/tui/marketplace_dialog.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/tui/marketplace_layout_engine.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/tui/mcp_dialog.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/tui/miniagents_dialog.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/tui/publish_dialog.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/tui/quota_dialog.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/tui/quota_redeem_dialog.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/tui/quota_share_dialog.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/tui/resume_dialog.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/tui/skills_dialog.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/tui/subagents_dialog.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/tui/tools_dialog.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/tui/update_dialog.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/utilities.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/vector_memory.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/workspace.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli.egg-info/SOURCES.txt +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli.egg-info/dependency_links.txt +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli.egg-info/entry_points.txt +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli.egg-info/requires.txt +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli.egg-info/top_level.txt +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "utim-cli"
|
|
7
|
-
version = "2.0.
|
|
7
|
+
version = "2.0.2"
|
|
8
8
|
description = "UTIM – Universal Terminal Intelligence Manager. A powerful agentic AI coding assistant for your terminal."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -3429,6 +3429,7 @@ class Orchestrator:
|
|
|
3429
3429
|
results[orig_idx] = (tc, "[Aborted by user]")
|
|
3430
3430
|
continue
|
|
3431
3431
|
_tools_module._cancel_event = self.cancel_event
|
|
3432
|
+
_tools_module._active_model_id = self.model_id
|
|
3432
3433
|
result = self._execute_tool_timed(tc)
|
|
3433
3434
|
self._current_line_len = 0
|
|
3434
3435
|
results[orig_idx] = (tc, result)
|
|
@@ -3684,81 +3685,6 @@ class Orchestrator:
|
|
|
3684
3685
|
self._persist_messages()
|
|
3685
3686
|
return res
|
|
3686
3687
|
|
|
3687
|
-
def _filter_active_tools(self, all_tools: List[Dict], messages: List[Dict]) -> List[Dict]:
|
|
3688
|
-
"""Semantic Intent Tool Masking with Self-Healing Task Re-hydration.
|
|
3689
|
-
|
|
3690
|
-
Core coding tools and MCP tools are ALWAYS active. Specialized tools
|
|
3691
|
-
(analyze_image, generate_image, plan_project, create_skill, background_tasks)
|
|
3692
|
-
are activated via broad semantic intent detection (synonyms, implicit requests,
|
|
3693
|
-
file paths) and persistently retained once activated during the task session.
|
|
3694
|
-
"""
|
|
3695
|
-
if not all_tools:
|
|
3696
|
-
return all_tools
|
|
3697
|
-
|
|
3698
|
-
if not hasattr(self, "_activated_specialized_tools"):
|
|
3699
|
-
self._activated_specialized_tools = set()
|
|
3700
|
-
|
|
3701
|
-
CORE_TOOLS = {
|
|
3702
|
-
"read_file", "write_file", "edit_file", "move_file", "delete_file",
|
|
3703
|
-
"run_command", "list_directory", "grep_search", "search", "search_codebase",
|
|
3704
|
-
"recall_experience", "store_experience", "manage_memory"
|
|
3705
|
-
}
|
|
3706
|
-
|
|
3707
|
-
# Broad semantic intent patterns (handles slang, implicit desires, multi-language phrasing)
|
|
3708
|
-
SEMANTIC_INTENT_PATTERNS = {
|
|
3709
|
-
"analyze_image": [
|
|
3710
|
-
r"\.(png|jpg|jpeg|webp|gif|bmp|svg|tiff|ico)\b",
|
|
3711
|
-
r"(?i)\b(image|img|screenshot|photo|picture|diagram|fig|figure|ui|mockup|wireframe|visual|inspect|look at|see|show|crop|icon|logo|asset)\b"
|
|
3712
|
-
],
|
|
3713
|
-
"generate_image": [
|
|
3714
|
-
r"(?i)\b(generate|draw|create|paint|render|make|design|build)\b.*\b(image|picture|photo|illustration|graphic|logo|banner|art|ui|mockup|icon|asset|cover|poster|avatar)\b",
|
|
3715
|
-
r"(?i)\b(text to image|image gen|sd|flux|dall-e|midjourney|prompt expander)\b"
|
|
3716
|
-
],
|
|
3717
|
-
"plan_project": [
|
|
3718
|
-
r"(?i)\b(plan|architect|roadmap|milestone|phases|breakdown|strategy|design system|structure repo|project structure|todo list|task list|multi-step|workflow|steps)\b",
|
|
3719
|
-
r"(?i)\b(how should (i|we)|what is the best (way|architecture|structure)|organize this)\b"
|
|
3720
|
-
],
|
|
3721
|
-
"create_skill": [
|
|
3722
|
-
r"(?i)\b(skill|custom tool|reusable prompt|workflow template|save routine|workspace rule|automation|agent skill)\b"
|
|
3723
|
-
],
|
|
3724
|
-
"background_tasks": [
|
|
3725
|
-
r"(?i)\b(background|task|bg|daemon|long running|process|job|async|monitor|poll|kill task)\b"
|
|
3726
|
-
]
|
|
3727
|
-
}
|
|
3728
|
-
|
|
3729
|
-
# Collect text across recent messages (user + assistant reasoning + tool outputs)
|
|
3730
|
-
recent_text_parts = []
|
|
3731
|
-
for m in (messages[-8:] if messages else []):
|
|
3732
|
-
content = m.get("content")
|
|
3733
|
-
if isinstance(content, str):
|
|
3734
|
-
recent_text_parts.append(content)
|
|
3735
|
-
elif isinstance(content, list):
|
|
3736
|
-
for part in content:
|
|
3737
|
-
if isinstance(part, dict) and part.get("type") == "text":
|
|
3738
|
-
recent_text_parts.append(part.get("text", ""))
|
|
3739
|
-
|
|
3740
|
-
combined_text = "\n".join(recent_text_parts)
|
|
3741
|
-
|
|
3742
|
-
# 1. Semantic intent detection
|
|
3743
|
-
for tool_name, patterns in SEMANTIC_INTENT_PATTERNS.items():
|
|
3744
|
-
for pat in patterns:
|
|
3745
|
-
if re.search(pat, combined_text):
|
|
3746
|
-
self._activated_specialized_tools.add(tool_name)
|
|
3747
|
-
break
|
|
3748
|
-
|
|
3749
|
-
# 2. Re-hydrate if iteration count >= 3 (complex multi-step task automatically gets plan_project)
|
|
3750
|
-
if getattr(self, "current_iteration", 0) >= 3:
|
|
3751
|
-
self._activated_specialized_tools.add("plan_project")
|
|
3752
|
-
|
|
3753
|
-
# 3. Filter all_tools
|
|
3754
|
-
filtered = []
|
|
3755
|
-
for t in all_tools:
|
|
3756
|
-
name = t.get("function", {}).get("name", "")
|
|
3757
|
-
if name in CORE_TOOLS or name not in SEMANTIC_INTENT_PATTERNS or name in self._activated_specialized_tools:
|
|
3758
|
-
filtered.append(t)
|
|
3759
|
-
|
|
3760
|
-
return filtered
|
|
3761
|
-
|
|
3762
3688
|
# ── Context compression ──────────────────────────────────────────────────
|
|
3763
3689
|
|
|
3764
3690
|
@staticmethod
|
|
@@ -4909,30 +4835,6 @@ class Orchestrator:
|
|
|
4909
4835
|
# No extra newline here - let the next block handle it
|
|
4910
4836
|
pass
|
|
4911
4837
|
|
|
4912
|
-
# ── Self-Healing Re-Activation Gate ──────────────────────────────
|
|
4913
|
-
# If native tool_calls are empty and the model expresses inability to find a tool,
|
|
4914
|
-
# or mentions a specialized tool by name, immediately unmask ALL specialized tools
|
|
4915
|
-
# and re-prompt the model in 0ms so it NEVER gets stuck or stops.
|
|
4916
|
-
SPECIALIZED_TOOLS_ALL = {"analyze_image", "generate_image", "plan_project", "create_skill", "background_tasks"}
|
|
4917
|
-
if not tool_calls and content and not _attempted_tool_call:
|
|
4918
|
-
content_lower = content.lower()
|
|
4919
|
-
mentioned_specialized = {t for t in SPECIALIZED_TOOLS_ALL if t in content_lower}
|
|
4920
|
-
lacks_tool_signal = any(phrase in content_lower for phrase in [
|
|
4921
|
-
"don't have", "do not have", "cannot access", "no tool", "tool is missing",
|
|
4922
|
-
"unable to generate", "unable to analyze", "not available", "don't see"
|
|
4923
|
-
])
|
|
4924
|
-
if mentioned_specialized or lacks_tool_signal:
|
|
4925
|
-
if not hasattr(self, "_activated_specialized_tools"):
|
|
4926
|
-
self._activated_specialized_tools = set()
|
|
4927
|
-
self._activated_specialized_tools.update(SPECIALIZED_TOOLS_ALL)
|
|
4928
|
-
|
|
4929
|
-
self.messages.append({"role": "assistant", "content": content})
|
|
4930
|
-
self.messages.append({
|
|
4931
|
-
"role": "user",
|
|
4932
|
-
"content": "[SYSTEM DIRECTIVE] All specialized tools (analyze_image, generate_image, plan_project, create_skill, background_tasks) have been fully unmasked and loaded into your active tool schema. Please re-run immediately using the required tool."
|
|
4933
|
-
})
|
|
4934
|
-
continue
|
|
4935
|
-
|
|
4936
4838
|
# Parse text-based tool calls fallback if native tool calls are empty
|
|
4937
4839
|
if not tool_calls and content:
|
|
4938
4840
|
parsed_calls = []
|
|
@@ -5520,6 +5422,11 @@ class Orchestrator:
|
|
|
5520
5422
|
+ result[-tail_chars:]
|
|
5521
5423
|
+ (f"\n\n{_line_hint}" if _line_hint else "")
|
|
5522
5424
|
)
|
|
5425
|
+
|
|
5426
|
+
# Clear any stale vision images from a previous iteration before
|
|
5427
|
+
# this batch of tools runs (defensive — shouldn't normally be set).
|
|
5428
|
+
_tools_module._pending_vision_images.clear()
|
|
5429
|
+
|
|
5523
5430
|
# Execute tools in parallel when beneficial
|
|
5524
5431
|
if len(tool_calls) > 1:
|
|
5525
5432
|
# Use parallel execution for multiple tools
|
|
@@ -5536,50 +5443,14 @@ class Orchestrator:
|
|
|
5536
5443
|
func_name = tc.get("function", {}).get("name", "")
|
|
5537
5444
|
result = _cap_tool_result(func_name, result)
|
|
5538
5445
|
|
|
5539
|
-
|
|
5540
|
-
|
|
5541
|
-
|
|
5542
|
-
|
|
5543
|
-
|
|
5544
|
-
|
|
5545
|
-
|
|
5546
|
-
|
|
5547
|
-
_multimodal = _tool_result_to_multimodal(result, _img_path)
|
|
5548
|
-
self.messages.append(
|
|
5549
|
-
{
|
|
5550
|
-
"role": "tool",
|
|
5551
|
-
"tool_call_id": tc_id,
|
|
5552
|
-
"name": func_name,
|
|
5553
|
-
"content": _multimodal,
|
|
5554
|
-
}
|
|
5555
|
-
)
|
|
5556
|
-
else:
|
|
5557
|
-
self.messages.append(
|
|
5558
|
-
{
|
|
5559
|
-
"role": "tool",
|
|
5560
|
-
"tool_call_id": tc_id,
|
|
5561
|
-
"name": func_name,
|
|
5562
|
-
"content": result,
|
|
5563
|
-
}
|
|
5564
|
-
)
|
|
5565
|
-
except Exception:
|
|
5566
|
-
self.messages.append(
|
|
5567
|
-
{
|
|
5568
|
-
"role": "tool",
|
|
5569
|
-
"tool_call_id": tc_id,
|
|
5570
|
-
"name": func_name,
|
|
5571
|
-
"content": result,
|
|
5572
|
-
}
|
|
5573
|
-
)
|
|
5574
|
-
else:
|
|
5575
|
-
self.messages.append(
|
|
5576
|
-
{
|
|
5577
|
-
"role": "tool",
|
|
5578
|
-
"tool_call_id": tc_id,
|
|
5579
|
-
"name": func_name,
|
|
5580
|
-
"content": result,
|
|
5581
|
-
}
|
|
5582
|
-
)
|
|
5446
|
+
self.messages.append(
|
|
5447
|
+
{
|
|
5448
|
+
"role": "tool",
|
|
5449
|
+
"tool_call_id": tc_id,
|
|
5450
|
+
"name": func_name,
|
|
5451
|
+
"content": result,
|
|
5452
|
+
}
|
|
5453
|
+
)
|
|
5583
5454
|
else:
|
|
5584
5455
|
# Single tool - execute directly
|
|
5585
5456
|
for tc in tool_calls:
|
|
@@ -5588,55 +5459,41 @@ class Orchestrator:
|
|
|
5588
5459
|
|
|
5589
5460
|
func_name = tc.get("function", {}).get("name", "")
|
|
5590
5461
|
_tools_module._cancel_event = self.cancel_event
|
|
5462
|
+
_tools_module._active_model_id = self.model_id
|
|
5591
5463
|
result = self._execute_tool_timed(tc)
|
|
5592
5464
|
self._current_line_len = 0
|
|
5593
5465
|
tc_id = tc.get("id") or str(tc.get("index", "0"))
|
|
5594
5466
|
result = _cap_tool_result(func_name, result)
|
|
5595
5467
|
|
|
5596
|
-
|
|
5597
|
-
|
|
5598
|
-
|
|
5599
|
-
|
|
5600
|
-
|
|
5601
|
-
|
|
5602
|
-
|
|
5603
|
-
|
|
5604
|
-
|
|
5605
|
-
|
|
5606
|
-
|
|
5607
|
-
|
|
5608
|
-
|
|
5609
|
-
|
|
5610
|
-
|
|
5611
|
-
|
|
5612
|
-
|
|
5613
|
-
|
|
5614
|
-
|
|
5615
|
-
|
|
5616
|
-
|
|
5617
|
-
|
|
5618
|
-
|
|
5619
|
-
|
|
5620
|
-
|
|
5621
|
-
|
|
5622
|
-
|
|
5623
|
-
|
|
5624
|
-
|
|
5625
|
-
"role": "tool",
|
|
5626
|
-
"tool_call_id": tc_id,
|
|
5627
|
-
"name": func_name,
|
|
5628
|
-
"content": result,
|
|
5629
|
-
}
|
|
5630
|
-
)
|
|
5631
|
-
else:
|
|
5632
|
-
self.messages.append(
|
|
5633
|
-
{
|
|
5634
|
-
"role": "tool",
|
|
5635
|
-
"tool_call_id": tc_id,
|
|
5636
|
-
"name": func_name,
|
|
5637
|
-
"content": result,
|
|
5638
|
-
}
|
|
5639
|
-
)
|
|
5468
|
+
self.messages.append(
|
|
5469
|
+
{
|
|
5470
|
+
"role": "tool",
|
|
5471
|
+
"tool_call_id": tc_id,
|
|
5472
|
+
"name": func_name,
|
|
5473
|
+
"content": result,
|
|
5474
|
+
}
|
|
5475
|
+
)
|
|
5476
|
+
|
|
5477
|
+
# ── Vision image injection ────────────────────────────────────────
|
|
5478
|
+
# read_file encodes image bytes and pushes them to
|
|
5479
|
+
# _tools_module._pending_vision_images so they bypass the tool-result
|
|
5480
|
+
# string pipeline entirely. Drain the queue here (after ALL tools in
|
|
5481
|
+
# this iteration have run) and inject each image as a "user" message
|
|
5482
|
+
# — the only role that providers universally accept image_url content in.
|
|
5483
|
+
_vision_queue = getattr(_tools_module, "_pending_vision_images", [])
|
|
5484
|
+
if _vision_queue:
|
|
5485
|
+
for _vimg in list(_vision_queue):
|
|
5486
|
+
self.messages.append(
|
|
5487
|
+
{
|
|
5488
|
+
"role": "user",
|
|
5489
|
+
"content": [
|
|
5490
|
+
{"type": "text", "text": f"[Image from read_file: {_vimg['meta']}]"},
|
|
5491
|
+
{"type": "image_url", "image_url": {"url": f"data:{_vimg['mime']};base64,{_vimg['b64']}"}},
|
|
5492
|
+
],
|
|
5493
|
+
}
|
|
5494
|
+
)
|
|
5495
|
+
_vision_queue.clear()
|
|
5496
|
+
|
|
5640
5497
|
# --- Repeated Tool Failure Loop Detection ---
|
|
5641
5498
|
if not hasattr(self, "_tool_failure_counts"):
|
|
5642
5499
|
self._tool_failure_counts = {}
|
|
@@ -159,27 +159,36 @@ def resolve_project_path(filepath: str) -> str:
|
|
|
159
159
|
_VISION_MODELS_CACHE = None
|
|
160
160
|
|
|
161
161
|
def is_model_vision_capable(model_id: str = None) -> bool:
|
|
162
|
-
"""Determine if a model is vision-capable based on models.txt
|
|
162
|
+
"""Determine if a model is vision-capable (Vision: True/False) based on MODEL_REGISTRY and models.txt input_modalities."""
|
|
163
163
|
global _VISION_MODELS_CACHE
|
|
164
164
|
if not model_id:
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
165
|
+
# Prefer the slot injected by the orchestrator; fall back to config as last resort.
|
|
166
|
+
model_id = _active_model_id or ""
|
|
167
|
+
if not model_id:
|
|
168
|
+
try:
|
|
169
|
+
from utim_cli.config import config
|
|
170
|
+
model_id = config.get("main_model") or config.get("model") or ""
|
|
171
|
+
except Exception:
|
|
172
|
+
model_id = ""
|
|
170
173
|
|
|
171
|
-
model_id_str = str(model_id).
|
|
174
|
+
model_id_str = str(model_id).strip()
|
|
175
|
+
if not model_id_str:
|
|
176
|
+
return False # unknown model → treat as non-vision (safe default)
|
|
172
177
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
178
|
+
model_id_lower = model_id_str.lower()
|
|
179
|
+
|
|
180
|
+
# 1. Check server MODEL_REGISTRY first
|
|
181
|
+
try:
|
|
182
|
+
from utim_cli.server.models import MODEL_REGISTRY
|
|
183
|
+
if model_id_str in MODEL_REGISTRY:
|
|
184
|
+
return MODEL_REGISTRY[model_id_str].vision
|
|
185
|
+
for k, entry in MODEL_REGISTRY.items():
|
|
186
|
+
if k.lower() == model_id_lower:
|
|
187
|
+
return entry.vision
|
|
188
|
+
except Exception:
|
|
189
|
+
pass
|
|
181
190
|
|
|
182
|
-
#
|
|
191
|
+
# 2. Build / query models.txt cache (authoritative list from OpenRouter metadata)
|
|
183
192
|
if _VISION_MODELS_CACHE is None:
|
|
184
193
|
_VISION_MODELS_CACHE = {}
|
|
185
194
|
try:
|
|
@@ -190,15 +199,27 @@ def is_model_vision_capable(model_id: str = None) -> bool:
|
|
|
190
199
|
models_txt_path = "models.txt"
|
|
191
200
|
if os.path.exists(models_txt_path):
|
|
192
201
|
with open(models_txt_path, "r", encoding="utf-8") as f:
|
|
193
|
-
data = json.load(f)
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
202
|
+
data = json.load(f)
|
|
203
|
+
items = data.get("data", []) if isinstance(data, dict) else data
|
|
204
|
+
for m in items:
|
|
205
|
+
mid = m.get("id", "")
|
|
206
|
+
if not mid:
|
|
207
|
+
continue
|
|
208
|
+
in_mods = m.get("architecture", {}).get("input_modalities", []) or []
|
|
209
|
+
_VISION_MODELS_CACHE[mid.lower()] = ("image" in in_mods)
|
|
198
210
|
except Exception:
|
|
199
211
|
_VISION_MODELS_CACHE = {}
|
|
200
212
|
|
|
201
|
-
|
|
213
|
+
if model_id_lower in _VISION_MODELS_CACHE:
|
|
214
|
+
return _VISION_MODELS_CACHE[model_id_lower]
|
|
215
|
+
|
|
216
|
+
# 3. Fallback for custom / unlisted model names
|
|
217
|
+
vision_keywords = ["vision", "-vl", "llava", "pixtral", "image", "multimodal", "gpt-4o", "gemini", "claude-3", "claude-4", "claude-5", "qwen3"]
|
|
218
|
+
if any(kw in model_id_lower for kw in vision_keywords):
|
|
219
|
+
if not any(non_v in model_id_lower for non_v in ["text-only", "coder-air", "kat-coder"]):
|
|
220
|
+
return True
|
|
221
|
+
|
|
222
|
+
return False
|
|
202
223
|
|
|
203
224
|
|
|
204
225
|
def _extract_file_symbols(filepath: str, file_content: str) -> list:
|
|
@@ -305,15 +326,125 @@ def read_file(filepath: str, start_line: int = None, end_line: int = None, symbo
|
|
|
305
326
|
ext = _os.path.splitext(filepath)[1].lower()
|
|
306
327
|
image_extensions = {'.png', '.jpg', '.jpeg', '.webp', '.gif', '.bmp'}
|
|
307
328
|
if ext in image_extensions:
|
|
308
|
-
#
|
|
309
|
-
# description that clutters the tool indicator. Non-vision models still
|
|
310
|
-
# need the description as their only way to "see" the image.
|
|
329
|
+
# Collect image metadata (size, dimensions) for the text portion of the response.
|
|
311
330
|
try:
|
|
312
|
-
|
|
313
|
-
|
|
331
|
+
file_size = _os.path.getsize(filepath)
|
|
332
|
+
size_kb = file_size / 1024.0
|
|
333
|
+
size_str = f"{size_kb:.1f} KB" if size_kb < 1024 else f"{size_kb/1024:.2f} MB"
|
|
334
|
+
except Exception:
|
|
335
|
+
size_str = "unknown size"
|
|
336
|
+
|
|
337
|
+
meta = f"[Image: {_os.path.basename(filepath)} | {ext.lstrip('.').upper()} | {size_str} | URI: {_make_file_uri(filepath)}]"
|
|
338
|
+
|
|
339
|
+
# Try to also read image dimensions (pure stdlib where possible, else Pillow)
|
|
340
|
+
dim_str = ""
|
|
341
|
+
try:
|
|
342
|
+
with open(filepath, "rb") as _f:
|
|
343
|
+
_head = _f.read(64)
|
|
344
|
+
# PNG: width@16, height@20 (big-endian uint32)
|
|
345
|
+
if _head.startswith(b"\x89PNG\r\n\x1a\n") and len(_head) >= 24:
|
|
346
|
+
import struct
|
|
347
|
+
w, h = struct.unpack(">II", _head[16:24])
|
|
348
|
+
dim_str = f" | {w}x{h}px"
|
|
349
|
+
# GIF: width@6, height@8 (little-endian uint16)
|
|
350
|
+
elif _head.startswith(b"GIF8") and len(_head) >= 10:
|
|
351
|
+
import struct
|
|
352
|
+
w, h = struct.unpack("<HH", _head[6:10])
|
|
353
|
+
dim_str = f" | {w}x{h}px"
|
|
354
|
+
# JPEG: parse SOF marker to find dimensions
|
|
355
|
+
elif _head.startswith(b"\xff\xd8\xff"):
|
|
356
|
+
import struct
|
|
357
|
+
_f2 = open(filepath, "rb")
|
|
358
|
+
try:
|
|
359
|
+
soi = _f2.read(2)
|
|
360
|
+
while True:
|
|
361
|
+
marker = _f2.read(2)
|
|
362
|
+
if len(marker) < 2:
|
|
363
|
+
break
|
|
364
|
+
if marker[0] != 0xFF:
|
|
365
|
+
break
|
|
366
|
+
# Skip SOI / EOI / RST markers
|
|
367
|
+
if marker[1] in (0xD8, 0xD9) or 0xD0 <= marker[1] <= 0xD7:
|
|
368
|
+
continue
|
|
369
|
+
seg_len_bytes = _f2.read(2)
|
|
370
|
+
if len(seg_len_bytes) < 2:
|
|
371
|
+
break
|
|
372
|
+
seg_len = struct.unpack(">H", seg_len_bytes)[0]
|
|
373
|
+
# SOFn markers (Start of Frame): 0xC0-0xCF except 0xC4,0xC8,0xCC
|
|
374
|
+
if 0xC0 <= marker[1] <= 0xCF and marker[1] not in (0xC4, 0xC8, 0xCC):
|
|
375
|
+
_f2.read(1) # precision
|
|
376
|
+
h, w = struct.unpack(">HH", _f2.read(4))
|
|
377
|
+
dim_str = f" | {w}x{h}px"
|
|
378
|
+
break
|
|
379
|
+
else:
|
|
380
|
+
_f2.read(seg_len - 2)
|
|
381
|
+
finally:
|
|
382
|
+
_f2.close()
|
|
383
|
+
# WebP: 'RIFF....WEBP' then VP8 / VP8L / VP8X chunk
|
|
384
|
+
elif _head.startswith(b"RIFF") and _head[8:12] == b"WEBP":
|
|
385
|
+
with open(filepath, "rb") as _f3:
|
|
386
|
+
_f3.read(12)
|
|
387
|
+
chunk_hdr = _f3.read(4)
|
|
388
|
+
if chunk_hdr == b"VP8 ":
|
|
389
|
+
_f3.read(10) # frame tag + start code
|
|
390
|
+
w = int.from_bytes(_f3.read(2), "little") & 0x3FFF
|
|
391
|
+
h = int.from_bytes(_f3.read(2), "little") & 0x3FFF
|
|
392
|
+
dim_str = f" | {w}x{h}px"
|
|
393
|
+
elif chunk_hdr == b"VP8L":
|
|
394
|
+
_f3.read(1) # signature byte
|
|
395
|
+
b1, b2, b3, b4 = _f3.read(4)
|
|
396
|
+
w = ((b2 & 0x3F) << 8 | b1) + 1
|
|
397
|
+
h = (((b4 & 0x0F) << 10) | (b3 << 2) | ((b2 & 0xC0) >> 6)) + 1
|
|
398
|
+
dim_str = f" | {w}x{h}px"
|
|
399
|
+
elif chunk_hdr == b"VP8X":
|
|
400
|
+
_f3.read(8)
|
|
401
|
+
w = int.from_bytes(_f3.read(3), "little") + 1
|
|
402
|
+
h = int.from_bytes(_f3.read(3), "little") + 1
|
|
403
|
+
dim_str = f" | {w}x{h}px"
|
|
314
404
|
except Exception:
|
|
315
405
|
pass
|
|
316
|
-
|
|
406
|
+
|
|
407
|
+
meta += dim_str
|
|
408
|
+
|
|
409
|
+
# Route image to model based on vision capability:
|
|
410
|
+
#
|
|
411
|
+
# Vision-capable model → encode bytes here, push to _pending_vision_images.
|
|
412
|
+
# The orchestrator drains this queue after all tools complete and injects a
|
|
413
|
+
# "user" message with the raw base64 image so the model truly sees it.
|
|
414
|
+
# (image_url blocks are NOT supported inside "tool" role messages by most
|
|
415
|
+
# providers — they must live in a "user" message.)
|
|
416
|
+
#
|
|
417
|
+
# Non-vision model → call analyze_image() in the background and return its
|
|
418
|
+
# text description so the model has useful context.
|
|
419
|
+
if is_model_vision_capable():
|
|
420
|
+
import base64 as _b64mod, mimetypes as _mtmod
|
|
421
|
+
_mime_v, _ = _mtmod.guess_type(filepath)
|
|
422
|
+
_ext_v = _os.path.splitext(filepath)[1].lower()
|
|
423
|
+
if not _mime_v or not _mime_v.startswith("image/"):
|
|
424
|
+
_mime_v = f"image/{_ext_v[1:]}"
|
|
425
|
+
if _ext_v == ".jpg":
|
|
426
|
+
_mime_v = "image/jpeg"
|
|
427
|
+
try:
|
|
428
|
+
with open(filepath, "rb") as _fv:
|
|
429
|
+
_b64_v = _b64mod.b64encode(_fv.read()).decode("utf-8")
|
|
430
|
+
# Push to queue — orchestrator injects the user message after tools complete
|
|
431
|
+
_pending_vision_images.append({"meta": meta, "b64": _b64_v, "mime": _mime_v})
|
|
432
|
+
return meta # return clean metadata text only; no sentinel needed
|
|
433
|
+
except Exception as _venc_err:
|
|
434
|
+
# Encoding failed — fall through to analyze_image text fallback below
|
|
435
|
+
pass
|
|
436
|
+
|
|
437
|
+
# Non-vision model (or encoding failed): describe image via external vision sub-model.
|
|
438
|
+
try:
|
|
439
|
+
description = analyze_image(
|
|
440
|
+
filepath,
|
|
441
|
+
"Identify and describe this image in detail, listing any people, actors, characters, "
|
|
442
|
+
"text, UI elements, colors, visual layout, code or terminal output shown, and any "
|
|
443
|
+
"visible error messages or stack traces verbatim."
|
|
444
|
+
)
|
|
445
|
+
return f"{meta}\n{description}"
|
|
446
|
+
except Exception as _e2:
|
|
447
|
+
return f"{meta}\n(Vision description unavailable: {_e2})"
|
|
317
448
|
|
|
318
449
|
try:
|
|
319
450
|
with open(filepath, "rb") as f:
|
|
@@ -967,6 +1098,17 @@ def edit_file(filepath: str, old_str: str = None, new_str: str = None, replaceme
|
|
|
967
1098
|
# threading.Event here before each tool call so run_command can be aborted.
|
|
968
1099
|
_cancel_event = None # type: threading.Event | None
|
|
969
1100
|
|
|
1101
|
+
# Active model ID slot — injected by the orchestrator before each tool call
|
|
1102
|
+
# (same pattern as _cancel_event). read_file reads this to decide whether to
|
|
1103
|
+
# push image bytes to _pending_vision_images or call analyze_image instead.
|
|
1104
|
+
_active_model_id: str = ""
|
|
1105
|
+
|
|
1106
|
+
# Queue for vision images: read_file pushes here when the active model is
|
|
1107
|
+
# vision-capable; the orchestrator drains it after each tool-call batch and
|
|
1108
|
+
# injects a "user" message containing the raw image bytes so the model can
|
|
1109
|
+
# actually see the image (most providers reject image_url in "tool" messages).
|
|
1110
|
+
_pending_vision_images: list = [] # list of {"meta": str, "b64": str, "mime": str}
|
|
1111
|
+
|
|
970
1112
|
# ─── Intelligent Sandbox mode ──────────────────────────────────────────────────
|
|
971
1113
|
# Set to True via --sandbox CLI flag. When active, run_command uses static analysis
|
|
972
1114
|
# to check commands for safety. Risky commands are blocked unless approved.
|
|
@@ -621,55 +621,53 @@ def _dialog_model_main(orchestrator, target="main"):
|
|
|
621
621
|
if is_paid_plan:
|
|
622
622
|
approved_set.update({
|
|
623
623
|
"anthropic/claude-sonnet-4.6",
|
|
624
|
+
"anthropic/claude-sonnet-4.5",
|
|
625
|
+
"anthropic/claude-sonnet-5",
|
|
626
|
+
"anthropic/claude-opus-4.5",
|
|
627
|
+
"anthropic/claude-opus-4.6",
|
|
628
|
+
"anthropic/claude-opus-4.7",
|
|
629
|
+
"anthropic/claude-opus-4.8",
|
|
630
|
+
"anthropic/claude-fable-5",
|
|
624
631
|
"inclusionai/ling-2.6-flash",
|
|
632
|
+
"inclusionai/ling-2.6-1t",
|
|
625
633
|
"xiaomi/mimo-v2.5",
|
|
626
634
|
"xiaomi/mimo-v2.5-pro",
|
|
627
635
|
"deepseek/deepseek-v4-flash",
|
|
636
|
+
"deepseek/deepseek-v4-flash-0731",
|
|
628
637
|
"deepseek/deepseek-v4-pro",
|
|
638
|
+
"deepseek/deepseek-r1",
|
|
629
639
|
"openai/gpt-5.5",
|
|
630
|
-
"
|
|
631
|
-
"
|
|
640
|
+
"openai/gpt-5.4",
|
|
641
|
+
"openai/gpt-5.4-mini",
|
|
632
642
|
"openai/gpt-5.3-codex",
|
|
643
|
+
"moonshotai/kimi-k2.6",
|
|
644
|
+
"moonshotai/kimi-k2.7-code",
|
|
645
|
+
"moonshotai/kimi-k2.5",
|
|
646
|
+
"moonshotai/kimi-k3",
|
|
633
647
|
"google/gemini-3.1-pro-preview-customtools",
|
|
634
|
-
"
|
|
648
|
+
"google/gemini-3.5-flash",
|
|
649
|
+
"google/gemini-3.6-flash",
|
|
635
650
|
"minimax/minimax-m2.7",
|
|
651
|
+
"minimax/minimax-m2.5",
|
|
652
|
+
"minimax/minimax-m3",
|
|
636
653
|
"kwaipilot/kat-coder-pro-v2",
|
|
637
654
|
"z-ai/glm-5.1",
|
|
638
|
-
"anthropic/claude-fable-5",
|
|
639
|
-
"nex-agi/nex-n2-pro",
|
|
640
|
-
"minimax/minimax-m3",
|
|
641
|
-
"moonshotai/kimi-k2.7-code",
|
|
642
|
-
"deepseek/deepseek-r1",
|
|
643
|
-
"x-ai/grok-4.3",
|
|
644
|
-
"google/gemini-3.5-flash",
|
|
645
|
-
"qwen/qwen3.7-max",
|
|
646
|
-
"stepfun/step-3.7-flash",
|
|
647
|
-
"thinkingmachines/inkling",
|
|
648
|
-
"moonshotai/kimi-k3",
|
|
649
|
-
"meta/muse-spark-1.1",
|
|
650
|
-
|
|
651
|
-
"anthropic/claude-sonnet-4.5",
|
|
652
|
-
"anthropic/claude-sonnet-4.6",
|
|
653
|
-
"anthropic/claude-opus-4.5",
|
|
654
|
-
"anthropic/claude-opus-4.6",
|
|
655
|
-
"anthropic/claude-opus-4.7",
|
|
656
|
-
"anthropic/claude-opus-4.8",
|
|
657
|
-
"anthropic/claude-sonnet-5",
|
|
658
655
|
"z-ai/glm-5-turbo",
|
|
659
656
|
"z-ai/glm-4.7",
|
|
660
657
|
"z-ai/glm-5",
|
|
661
658
|
"z-ai/glm-5.2",
|
|
662
|
-
"
|
|
663
|
-
"
|
|
664
|
-
"qwen/qwen3.6-plus",
|
|
665
|
-
"openai/gpt-5.4-mini",
|
|
666
|
-
"minimax/minimax-m2.5",
|
|
659
|
+
"nex-agi/nex-n2-pro",
|
|
660
|
+
"x-ai/grok-4.3",
|
|
667
661
|
"x-ai/grok-4.20",
|
|
668
662
|
"x-ai/grok-build-0.1",
|
|
669
|
-
"
|
|
670
|
-
"
|
|
663
|
+
"qwen/qwen3.7-max",
|
|
664
|
+
"qwen/qwen3.7-plus",
|
|
665
|
+
"qwen/qwen3.6-plus",
|
|
666
|
+
"stepfun/step-3.7-flash",
|
|
667
|
+
"thinkingmachines/inkling",
|
|
668
|
+
"meta/muse-spark-1.1",
|
|
671
669
|
"poolside/laguna-s-2.1:free",
|
|
672
|
-
})
|
|
670
|
+
})
|
|
673
671
|
elif target.startswith('subagent_') and target not in ('subagent_image_gen', 'subagent_blender_vision', 'subagent_analyze_image', 'subagent_blender_code'):
|
|
674
672
|
approved_set = {
|
|
675
673
|
DEFAULT_MODEL,
|
|
@@ -818,6 +816,7 @@ def _dialog_model_main(orchestrator, target="main"):
|
|
|
818
816
|
"xiaomi/mimo-v2.5": (0, "Very low"),
|
|
819
817
|
"xiaomi/mimo-v2.5-pro": (2, "Medium"),
|
|
820
818
|
"deepseek/deepseek-v4-flash": (0, "Very low"),
|
|
819
|
+
"deepseek/deepseek-v4-flash-0731": (0, "Very low"),
|
|
821
820
|
"deepseek/deepseek-v4-pro": (1, "Low"),
|
|
822
821
|
"openai/gpt-5.5": (4, "Very high"),
|
|
823
822
|
"inclusionai/ling-2.6-1t": (1, "Low"),
|
|
@@ -907,6 +906,7 @@ def _dialog_model_main(orchestrator, target="main"):
|
|
|
907
906
|
"xiaomi/mimo-v2.5": ("Highly capable multimodal model by Xiaomi.", []),
|
|
908
907
|
"xiaomi/mimo-v2.5-pro": ("Xiaomi flagship multimodal and reasoning model.", []),
|
|
909
908
|
"deepseek/deepseek-v4-flash": ("Ultra-fast, cost-effective model by DeepSeek.", []),
|
|
909
|
+
"deepseek/deepseek-v4-flash-0731": ("DeepSeek V4 Flash 0731 — Ultra-fast MoE coding and reasoning model by DeepSeek.", []),
|
|
910
910
|
"deepseek/deepseek-v4-pro": ("DeepSeek flagship MoE and reasoning model.", []),
|
|
911
911
|
"openai/gpt-5.5": ("Next-gen frontier reasoning model by OpenAI.", []),
|
|
912
912
|
"inclusionai/ling-2.6-1t": ("Large context agent model by InclusionAI.", []),
|
|
@@ -953,9 +953,7 @@ def _dialog_model_main(orchestrator, target="main"):
|
|
|
953
953
|
"google/gemini-3.1-flash-image": ("Google stable image and multimodal vision model.", []),
|
|
954
954
|
"openai/gpt-5-image-mini": ("OpenAI high-speed multimodal and image model.", []),
|
|
955
955
|
"openai/gpt-image-2": ("OpenAI advanced image synthesis and editing model.", []),
|
|
956
|
-
|
|
957
956
|
"anthropic/claude-sonnet-4.5": ("Anthropic: Claude Sonnet 4.5.", []),
|
|
958
|
-
"anthropic/claude-sonnet-4.6": ("Anthropic: Claude Sonnet 4.6.", []),
|
|
959
957
|
"anthropic/claude-opus-4.5": ("Anthropic: Claude Opus 4.5.", []),
|
|
960
958
|
"anthropic/claude-opus-4.6": ("Anthropic: Claude Opus 4.6.", []),
|
|
961
959
|
"anthropic/claude-opus-4.7": ("Anthropic: Claude Opus 4.7.", []),
|
|
@@ -966,7 +964,6 @@ def _dialog_model_main(orchestrator, target="main"):
|
|
|
966
964
|
"z-ai/glm-5": ("Z.ai: GLM 5.", []),
|
|
967
965
|
"z-ai/glm-5.2": ("Z.ai: GLM 5.2.", []),
|
|
968
966
|
"qwen/qwen3.7-plus": ("Qwen: Qwen3.7 Plus.", []),
|
|
969
|
-
"qwen/qwen3.7-max": ("Qwen: Qwen3.7 Max.", []),
|
|
970
967
|
"qwen/qwen3.6-plus": ("Qwen: Qwen3.6 Plus.", []),
|
|
971
968
|
"openai/gpt-5.4-mini": ("OpenAI: GPT-5.4 Mini.", []),
|
|
972
969
|
"minimax/minimax-m2.5": ("MiniMax: MiniMax M2.5.", []),
|
|
@@ -1334,6 +1331,8 @@ def _dialog_model_main(orchestrator, target="main"):
|
|
|
1334
1331
|
display_id = "Krea 2 image"
|
|
1335
1332
|
elif display_id == "gemini-3.1-pro-preview-customtools":
|
|
1336
1333
|
display_id = "gemini-3.1-pro-preview"
|
|
1334
|
+
elif "qwen3-next-80b" in display_id.lower() or "qwen3-next-80b" in mid.lower():
|
|
1335
|
+
display_id = "Qwen3 Next 80B"
|
|
1337
1336
|
else:
|
|
1338
1337
|
display_id = display_id.replace('-', ' ')
|
|
1339
1338
|
if display_id:
|
|
@@ -1348,6 +1347,11 @@ def _dialog_model_main(orchestrator, target="main"):
|
|
|
1348
1347
|
|
|
1349
1348
|
desc = m.get("desc", "Fast, efficient model for daily tasks.")
|
|
1350
1349
|
|
|
1350
|
+
# Determine explicit vision capability from models.txt / MODEL_REGISTRY
|
|
1351
|
+
from utim_cli.tools import is_model_vision_capable
|
|
1352
|
+
vis_tag = "[Vision: True]" if is_model_vision_capable(mid) else "[Vision: False]"
|
|
1353
|
+
full_desc = f"{vis_tag} {desc}"
|
|
1354
|
+
|
|
1351
1355
|
style = bg or ('fg:#f9e2af' if source == 'custom' else 'fg:#cdd6f4')
|
|
1352
1356
|
desc_style = bg or 'fg:#585b70'
|
|
1353
1357
|
|
|
@@ -1374,7 +1378,7 @@ def _dialog_model_main(orchestrator, target="main"):
|
|
|
1374
1378
|
cost_style = "fg:#f38ba8"
|
|
1375
1379
|
|
|
1376
1380
|
desc_width = max(24, min(54, (console.size.width or 100) - 62))
|
|
1377
|
-
desc_lines = _compact_desc(
|
|
1381
|
+
desc_lines = _compact_desc(full_desc, desc_width)
|
|
1378
1382
|
desc_text = f" {desc_lines[0]}{current}\n"
|
|
1379
1383
|
for extra in desc_lines[1:]:
|
|
1380
1384
|
desc_text += f"{'':<55}{extra}\n"
|
|
@@ -1388,7 +1392,7 @@ def _dialog_model_main(orchestrator, target="main"):
|
|
|
1388
1392
|
]
|
|
1389
1393
|
else:
|
|
1390
1394
|
desc_width = max(24, min(72, (console.size.width or 100) - 10))
|
|
1391
|
-
desc_lines = _compact_desc(
|
|
1395
|
+
desc_lines = _compact_desc(full_desc, desc_width)
|
|
1392
1396
|
desc_text = "".join(f" {line}\n" for line in desc_lines) + "\n"
|
|
1393
1397
|
return [
|
|
1394
1398
|
(style, f" {display_id}"),
|
|
@@ -1196,7 +1196,7 @@ def _print_animated_banner(animated: bool = True):
|
|
|
1196
1196
|
else:
|
|
1197
1197
|
console.print(f"[bold {color}]{BANNER_BIG}[/bold {color}]")
|
|
1198
1198
|
|
|
1199
|
-
console.print(f" [bold white]U Think I Make[/bold white] [bold #cba6f7]v2.0.
|
|
1199
|
+
console.print(f" [bold white]U Think I Make[/bold white] [bold #cba6f7]v2.0.2[/bold #cba6f7]")
|
|
1200
1200
|
user_email = config.email
|
|
1201
1201
|
is_logged_in = bool(config.token and user_email and user_email.upper() != "GUEST")
|
|
1202
1202
|
user_type = "Guest Mode" if not is_logged_in else "UTIM Community"
|
|
@@ -2003,7 +2003,7 @@ def _run_mcp_search_list_dialog(rows, render_row_fn, title="", legend="", search
|
|
|
2003
2003
|
return [
|
|
2004
2004
|
('', '\n'),
|
|
2005
2005
|
('bold #f38ba8', ' ⚠️ No matching MCP servers found.\n'),
|
|
2006
|
-
('dim', ' Try searching for different keywords (e.g. database, search, maps, slack).\n')
|
|
2006
|
+
('class:dim', ' Try searching for different keywords (e.g. database, search, maps, slack).\n')
|
|
2007
2007
|
]
|
|
2008
2008
|
|
|
2009
2009
|
matching_heights = [row_heights[orig_idx] for orig_idx, _ in filtered_rows]
|
|
@@ -4686,7 +4686,7 @@ def check_and_update_background():
|
|
|
4686
4686
|
return
|
|
4687
4687
|
|
|
4688
4688
|
latest_ver = resp.json().get("version", "")
|
|
4689
|
-
current_ver = "2.0.
|
|
4689
|
+
current_ver = "2.0.2"
|
|
4690
4690
|
|
|
4691
4691
|
def parse_ver(v):
|
|
4692
4692
|
return [int(x) for x in v.split(".") if x.isdigit()]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|