utim-cli 1.46.35__tar.gz → 1.46.36__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-1.46.35 → utim_cli-1.46.36}/CHANGELOG.md +5 -0
- {utim_cli-1.46.35/utim_cli.egg-info → utim_cli-1.46.36}/PKG-INFO +15 -3
- {utim_cli-1.46.35 → utim_cli-1.46.36}/README.md +18 -12
- {utim_cli-1.46.35 → utim_cli-1.46.36}/pyproject.toml +11 -3
- {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/__init__.py +1 -1
- {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/_version.py +1 -1
- {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/agent.py +16 -9
- {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/doctor.py +4 -2
- {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/orchestrator.py +66 -34
- {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/tui/model_dialog.py +206 -100
- {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/utim.py +7 -8
- {utim_cli-1.46.35 → utim_cli-1.46.36/utim_cli.egg-info}/PKG-INFO +15 -3
- {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli.egg-info/requires.txt +14 -2
- {utim_cli-1.46.35 → utim_cli-1.46.36}/LICENSE +0 -0
- {utim_cli-1.46.35 → utim_cli-1.46.36}/MANIFEST.in +0 -0
- {utim_cli-1.46.35 → utim_cli-1.46.36}/setup.cfg +0 -0
- {utim_cli-1.46.35 → utim_cli-1.46.36}/setup.py +0 -0
- {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/auth.py +0 -0
- {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/backup.py +0 -0
- {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/blender_agent.py +0 -0
- {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/bootstrap.py +0 -0
- {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/client_utils.py +0 -0
- {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/config.py +0 -0
- {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/constants.py +0 -0
- {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/context_pruner.py +0 -0
- {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/knowledge_graph.py +0 -0
- {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/local_db.py +0 -0
- {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/logger.py +0 -0
- {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/mcp_clean_wrapper.py +0 -0
- {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/mcp_client.py +0 -0
- {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/mcp_registry.json +0 -0
- {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/reflection.py +0 -0
- {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/report.py +0 -0
- {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/scrapy_search.py +0 -0
- {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/server/__init__.py +0 -0
- {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/server/auth.py +0 -0
- {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/server/db.py +0 -0
- {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/server/email_utils.py +0 -0
- {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/server/exchange_rate.py +0 -0
- {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/server/firebase.py +0 -0
- {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/server/history.py +0 -0
- {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/server/logging_config.py +0 -0
- {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/server/models.py +0 -0
- {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/server/pricing_updater.py +0 -0
- {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/server/rate_limit.py +0 -0
- {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/server/router.py +0 -0
- {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/server/routes/__init__.py +0 -0
- {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/server/routes/auth_routes.py +0 -0
- {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/server/routes/completion_routes.py +0 -0
- {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/server/routes/credit_routes.py +0 -0
- {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/server/routes/feedback_routes.py +0 -0
- {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/server/routes/quota_routes.py +0 -0
- {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/server/routes/session_routes.py +0 -0
- {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/server/routes/share_routes.py +0 -0
- {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/server/server.py +0 -0
- {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/share.py +0 -0
- {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/share_tui.py +0 -0
- {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/situational_scoring.py +0 -0
- {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/state.py +0 -0
- {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/tools.py +0 -0
- {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/tui/__init__.py +0 -0
- {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/tui/feedback_dialog.py +0 -0
- {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/tui/history_dialog.py +0 -0
- {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/tui/mcp_dialog.py +0 -0
- {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/tui/resume_dialog.py +0 -0
- {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/tui/tools_dialog.py +0 -0
- {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/tui/update_dialog.py +0 -0
- {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/utilities.py +0 -0
- {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/vector_memory.py +0 -0
- {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/workspace.py +0 -0
- {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli.egg-info/SOURCES.txt +0 -0
- {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli.egg-info/dependency_links.txt +0 -0
- {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli.egg-info/entry_points.txt +0 -0
- {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli.egg-info/top_level.txt +0 -0
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the UTIM CLI Agent will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [1.46.36] - 2026-07-12
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **Reciprocal Server URL Fallbacks**: Integrated automatic fallback routing in the request session patcher to dynamically swap and retry calls between `api.utim.dev` and `utim-cli-production.up.railway.app` in case of connection timeouts or proxy blocking errors.
|
|
9
|
+
|
|
5
10
|
## [1.46.35] - 2026-07-11
|
|
6
11
|
|
|
7
12
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: utim-cli
|
|
3
|
-
Version: 1.46.
|
|
3
|
+
Version: 1.46.36
|
|
4
4
|
Summary: UTIM – Universal Terminal Intelligence Manager. A powerful agentic AI coding assistant for your terminal.
|
|
5
5
|
License: MIT
|
|
6
6
|
Project-URL: Homepage, https://utim.dev
|
|
@@ -20,7 +20,6 @@ Classifier: Intended Audience :: Developers
|
|
|
20
20
|
Requires-Python: >=3.9
|
|
21
21
|
Description-Content-Type: text/markdown
|
|
22
22
|
License-File: LICENSE
|
|
23
|
-
Requires-Dist: openai>=1.30.0
|
|
24
23
|
Requires-Dist: requests==2.31.0
|
|
25
24
|
Requires-Dist: aiohttp>=3.9.5
|
|
26
25
|
Requires-Dist: python-dotenv==1.0.1
|
|
@@ -30,9 +29,10 @@ Requires-Dist: chardet==4.0.0
|
|
|
30
29
|
Requires-Dist: typer>=0.9.0
|
|
31
30
|
Requires-Dist: rich>=13.0.0
|
|
32
31
|
Requires-Dist: prompt_toolkit>=3.0.0
|
|
33
|
-
Requires-Dist: mcp>=0.1.0
|
|
34
32
|
Requires-Dist: nest-asyncio>=1.5.0
|
|
35
33
|
Requires-Dist: sqlalchemy>=2.0.0
|
|
34
|
+
Requires-Dist: openai>=1.30.0; platform_system != "Linux" or (platform_machine != "aarch64" and platform_machine != "arm64")
|
|
35
|
+
Requires-Dist: mcp>=0.1.0; platform_system != "Linux" or (platform_machine != "aarch64" and platform_machine != "arm64")
|
|
36
36
|
Provides-Extra: search
|
|
37
37
|
Requires-Dist: scrapy>=2.11.0; extra == "search"
|
|
38
38
|
Requires-Dist: scrapy-playwright>=0.0.34; extra == "search"
|
|
@@ -44,6 +44,8 @@ Requires-Dist: tree-sitter>=0.21.0; extra == "parsers"
|
|
|
44
44
|
Requires-Dist: tree-sitter-python>=0.21.0; extra == "parsers"
|
|
45
45
|
Requires-Dist: tree-sitter-javascript>=0.21.0; extra == "parsers"
|
|
46
46
|
Requires-Dist: tree-sitter-typescript>=0.21.0; extra == "parsers"
|
|
47
|
+
Provides-Extra: mcp
|
|
48
|
+
Requires-Dist: mcp>=0.1.0; extra == "mcp"
|
|
47
49
|
Provides-Extra: full
|
|
48
50
|
Requires-Dist: scrapy>=2.11.0; extra == "full"
|
|
49
51
|
Requires-Dist: scrapy-playwright>=0.0.34; extra == "full"
|
|
@@ -53,6 +55,10 @@ Requires-Dist: tree-sitter>=0.21.0; extra == "full"
|
|
|
53
55
|
Requires-Dist: tree-sitter-python>=0.21.0; extra == "full"
|
|
54
56
|
Requires-Dist: tree-sitter-javascript>=0.21.0; extra == "full"
|
|
55
57
|
Requires-Dist: tree-sitter-typescript>=0.21.0; extra == "full"
|
|
58
|
+
Requires-Dist: mcp>=0.1.0; (platform_system != "Linux" or (platform_machine != "aarch64" and platform_machine != "arm64")) and extra == "full"
|
|
59
|
+
Requires-Dist: openai>=1.30.0; (platform_system != "Linux" or (platform_machine != "aarch64" and platform_machine != "arm64")) and extra == "full"
|
|
60
|
+
Provides-Extra: legacy-agent
|
|
61
|
+
Requires-Dist: openai>=1.30.0; extra == "legacy-agent"
|
|
56
62
|
|
|
57
63
|
# UTIM CLI Agent: Enterprise Coder Assistant
|
|
58
64
|
|
|
@@ -194,6 +200,12 @@ pip install utim-cli
|
|
|
194
200
|
pip install "utim-cli[full]"
|
|
195
201
|
```
|
|
196
202
|
|
|
203
|
+
Desktop installs include the normal SDK/MCP path automatically. Mobile/Termux
|
|
204
|
+
installs keep those native-heavy pieces optional to avoid `pydantic-core`; use
|
|
205
|
+
`utim-cli[mcp]` or `utim-cli[legacy-agent]` only if you explicitly need them.
|
|
206
|
+
Do not install `utim_cli/server/requirements.txt` unless you are deploying the
|
|
207
|
+
server.
|
|
208
|
+
|
|
197
209
|
### Source install (development)
|
|
198
210
|
```bash
|
|
199
211
|
git clone https://github.com/emendai/utim.git
|
|
@@ -131,18 +131,24 @@ Inside the interactive chat terminal, type these slash commands for direct works
|
|
|
131
131
|
npm install -g @emend-ai/utim
|
|
132
132
|
```
|
|
133
133
|
|
|
134
|
-
### pip (from PyPI)
|
|
135
|
-
```bash
|
|
136
|
-
pip install utim-cli
|
|
137
|
-
# Full install with optional features (web search, images, parsers)
|
|
138
|
-
pip install "utim-cli[full]"
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
134
|
+
### pip (from PyPI)
|
|
135
|
+
```bash
|
|
136
|
+
pip install utim-cli
|
|
137
|
+
# Full install with optional features (web search, images, parsers)
|
|
138
|
+
pip install "utim-cli[full]"
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Desktop installs include the normal SDK/MCP path automatically. Mobile/Termux
|
|
142
|
+
installs keep those native-heavy pieces optional to avoid `pydantic-core`; use
|
|
143
|
+
`utim-cli[mcp]` or `utim-cli[legacy-agent]` only if you explicitly need them.
|
|
144
|
+
Do not install `utim_cli/server/requirements.txt` unless you are deploying the
|
|
145
|
+
server.
|
|
146
|
+
|
|
147
|
+
### Source install (development)
|
|
148
|
+
```bash
|
|
149
|
+
git clone https://github.com/emendai/utim.git
|
|
150
|
+
cd utim
|
|
151
|
+
pip install -e ".[full]"
|
|
146
152
|
```
|
|
147
153
|
|
|
148
154
|
### Smoke-test your install
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "utim-cli"
|
|
7
|
-
version = "1.46.
|
|
7
|
+
version = "1.46.36"
|
|
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"
|
|
@@ -22,7 +22,6 @@ classifiers = [
|
|
|
22
22
|
"Intended Audience :: Developers",
|
|
23
23
|
]
|
|
24
24
|
dependencies = [
|
|
25
|
-
"openai>=1.30.0",
|
|
26
25
|
"requests==2.31.0",
|
|
27
26
|
"aiohttp>=3.9.5",
|
|
28
27
|
"python-dotenv==1.0.1",
|
|
@@ -32,9 +31,10 @@ dependencies = [
|
|
|
32
31
|
"typer>=0.9.0",
|
|
33
32
|
"rich>=13.0.0",
|
|
34
33
|
"prompt_toolkit>=3.0.0",
|
|
35
|
-
"mcp>=0.1.0",
|
|
36
34
|
"nest-asyncio>=1.5.0",
|
|
37
35
|
"sqlalchemy>=2.0.0",
|
|
36
|
+
"openai>=1.30.0; platform_system != 'Linux' or (platform_machine != 'aarch64' and platform_machine != 'arm64')",
|
|
37
|
+
"mcp>=0.1.0; platform_system != 'Linux' or (platform_machine != 'aarch64' and platform_machine != 'arm64')",
|
|
38
38
|
]
|
|
39
39
|
|
|
40
40
|
[project.urls]
|
|
@@ -59,6 +59,9 @@ parsers = [
|
|
|
59
59
|
"tree-sitter-javascript>=0.21.0",
|
|
60
60
|
"tree-sitter-typescript>=0.21.0",
|
|
61
61
|
]
|
|
62
|
+
mcp = [
|
|
63
|
+
"mcp>=0.1.0",
|
|
64
|
+
]
|
|
62
65
|
full = [
|
|
63
66
|
"scrapy>=2.11.0",
|
|
64
67
|
"scrapy-playwright>=0.0.34",
|
|
@@ -68,6 +71,11 @@ full = [
|
|
|
68
71
|
"tree-sitter-python>=0.21.0",
|
|
69
72
|
"tree-sitter-javascript>=0.21.0",
|
|
70
73
|
"tree-sitter-typescript>=0.21.0",
|
|
74
|
+
"mcp>=0.1.0; platform_system != 'Linux' or (platform_machine != 'aarch64' and platform_machine != 'arm64')",
|
|
75
|
+
"openai>=1.30.0; platform_system != 'Linux' or (platform_machine != 'aarch64' and platform_machine != 'arm64')",
|
|
76
|
+
]
|
|
77
|
+
legacy-agent = [
|
|
78
|
+
"openai>=1.30.0",
|
|
71
79
|
]
|
|
72
80
|
|
|
73
81
|
[project.scripts]
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import os
|
|
2
|
-
import json
|
|
3
|
-
import sys
|
|
4
|
-
import time
|
|
5
|
-
from
|
|
6
|
-
from .tools import get_tools
|
|
2
|
+
import json
|
|
3
|
+
import sys
|
|
4
|
+
import time
|
|
5
|
+
from .tools import get_tools
|
|
7
6
|
from rich.console import Console
|
|
8
7
|
from rich.live import Live
|
|
9
8
|
from rich.spinner import Spinner
|
|
@@ -52,10 +51,18 @@ class ReActAgent:
|
|
|
52
51
|
if not api_key:
|
|
53
52
|
self.console.print("[dim]⚠ Warning: OPENROUTER_API_KEY is not set.[/dim]")
|
|
54
53
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
54
|
+
try:
|
|
55
|
+
from openai import OpenAI
|
|
56
|
+
except ImportError as exc:
|
|
57
|
+
raise RuntimeError(
|
|
58
|
+
"The legacy ReActAgent requires the optional OpenAI SDK. "
|
|
59
|
+
"Install it with: pip install 'utim-cli[legacy-agent]'"
|
|
60
|
+
) from exc
|
|
61
|
+
|
|
62
|
+
self.client = OpenAI(
|
|
63
|
+
base_url="https://openrouter.ai/api/v1",
|
|
64
|
+
api_key=api_key or "sk-fake-key-for-init",
|
|
65
|
+
)
|
|
59
66
|
self.messages = [{"role": "system", "content": system_prompt}]
|
|
60
67
|
self.start_time = None
|
|
61
68
|
|
|
@@ -21,8 +21,8 @@ def run_diagnostics(console: Console = None):
|
|
|
21
21
|
# 2. Dependency Check
|
|
22
22
|
console.print("\n [bold #42bcf5]Core Dependencies:[/bold #42bcf5]")
|
|
23
23
|
core_dependencies = [
|
|
24
|
-
"
|
|
25
|
-
"prompt_toolkit", "
|
|
24
|
+
"requests", "aiohttp", "typer", "rich",
|
|
25
|
+
"prompt_toolkit", "nest_asyncio"
|
|
26
26
|
]
|
|
27
27
|
for dep in core_dependencies:
|
|
28
28
|
try:
|
|
@@ -40,6 +40,8 @@ def run_diagnostics(console: Console = None):
|
|
|
40
40
|
"scrapy": "Scrapy search crawler",
|
|
41
41
|
"scrapy-playwright": "Playwright JS crawler",
|
|
42
42
|
"pillow": "Image utilities (generate_image)",
|
|
43
|
+
"openai": "Legacy ReActAgent SDK client",
|
|
44
|
+
"mcp": "Model Context Protocol server integrations",
|
|
43
45
|
}
|
|
44
46
|
for dep, feat in optional_dependencies.items():
|
|
45
47
|
try:
|
|
@@ -750,10 +750,11 @@ class Orchestrator:
|
|
|
750
750
|
f"or .env file (looked in {_cwd_env!r} then .utim/.env).[/bold yellow]\n"
|
|
751
751
|
)
|
|
752
752
|
|
|
753
|
-
self._local_client: bool = bool(self._local_api_key)
|
|
754
|
-
|
|
755
|
-
# OpenRouter base URL (can be overridden per-model for custom providers)
|
|
756
|
-
self._openrouter_base_url = "https://openrouter.ai/api/v1/chat/completions"
|
|
753
|
+
self._local_client: bool = bool(self._local_api_key)
|
|
754
|
+
|
|
755
|
+
# OpenRouter base URL (can be overridden per-model for custom providers)
|
|
756
|
+
self._openrouter_base_url = "https://openrouter.ai/api/v1/chat/completions"
|
|
757
|
+
self.model_source: Optional[str] = config.get("main_model_source")
|
|
757
758
|
|
|
758
759
|
# Cancellation flag
|
|
759
760
|
self.cancel_event = threading.Event()
|
|
@@ -854,13 +855,14 @@ class Orchestrator:
|
|
|
854
855
|
)
|
|
855
856
|
|
|
856
857
|
# Write current active session state to session_state.json
|
|
857
|
-
state_data = {
|
|
858
|
-
"session_id": self.session_id,
|
|
859
|
-
"messages": clean_messages,
|
|
860
|
-
"turn_history": clean_turn_history,
|
|
861
|
-
"redo_history": clean_redo_history,
|
|
862
|
-
"model_id": self.model_id
|
|
863
|
-
|
|
858
|
+
state_data = {
|
|
859
|
+
"session_id": self.session_id,
|
|
860
|
+
"messages": clean_messages,
|
|
861
|
+
"turn_history": clean_turn_history,
|
|
862
|
+
"redo_history": clean_redo_history,
|
|
863
|
+
"model_id": self.model_id,
|
|
864
|
+
"model_source": self.model_source,
|
|
865
|
+
}
|
|
864
866
|
os.makedirs(".utim", exist_ok=True)
|
|
865
867
|
with open(".utim/session_state.json", "w", encoding="utf-8") as f:
|
|
866
868
|
json.dump(state_data, f, ensure_ascii=False, indent=4)
|
|
@@ -880,15 +882,37 @@ class Orchestrator:
|
|
|
880
882
|
)
|
|
881
883
|
|
|
882
884
|
# ── Custom-provider endpoint resolution ─────────────────────────────────
|
|
883
|
-
def
|
|
884
|
-
"""Return
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
885
|
+
def _get_configured_model_source(self, model_id: str, *, is_primary: bool = False) -> Optional[str]:
|
|
886
|
+
"""Return the selected source for a model id when the picker recorded one."""
|
|
887
|
+
if is_primary:
|
|
888
|
+
source = getattr(self, "model_source", None) or config.get("main_model_source")
|
|
889
|
+
if source in ("custom", "utim", "openrouter"):
|
|
890
|
+
return source
|
|
891
|
+
|
|
892
|
+
for key, value in config._data.items():
|
|
893
|
+
if key.startswith("subagent_model_") and not key.endswith("_source") and value == model_id:
|
|
894
|
+
source = config.get(f"{key}_source")
|
|
895
|
+
if source in ("custom", "utim", "openrouter"):
|
|
896
|
+
return source
|
|
897
|
+
return None
|
|
898
|
+
|
|
899
|
+
def _is_custom_model_selection(self, model_id: str, *, is_primary: bool = False) -> bool:
|
|
900
|
+
source = self._get_configured_model_source(model_id, is_primary=is_primary)
|
|
901
|
+
if source in ("utim", "openrouter"):
|
|
902
|
+
return False
|
|
903
|
+
if source == "custom":
|
|
904
|
+
return bool(config.get_custom_model(model_id))
|
|
905
|
+
return bool(config.get_custom_model(model_id))
|
|
906
|
+
|
|
907
|
+
def _resolve_model_endpoint(self, model_id: str, *, is_primary: bool = False) -> tuple:
|
|
908
|
+
"""Return (chat_completions_url, api_key) for *model_id*.
|
|
909
|
+
|
|
910
|
+
Custom models (added via /model add) carry their own base_url and
|
|
911
|
+
api_key; everything else falls back to OpenRouter.
|
|
912
|
+
"""
|
|
913
|
+
custom = config.get_custom_model(model_id) if self._is_custom_model_selection(model_id, is_primary=is_primary) else None
|
|
914
|
+
if custom:
|
|
915
|
+
base = custom.get("base_url", "").rstrip("/")
|
|
892
916
|
# Append /chat/completions if the caller gave us just the base path
|
|
893
917
|
if not base.endswith("/chat/completions"):
|
|
894
918
|
url = base + "/chat/completions"
|
|
@@ -923,7 +947,7 @@ class Orchestrator:
|
|
|
923
947
|
if resp.status_code == 200:
|
|
924
948
|
quota = resp.json()
|
|
925
949
|
chosen_model = override_model if override_model else self.model_id
|
|
926
|
-
is_custom =
|
|
950
|
+
is_custom = self._is_custom_model_selection(chosen_model, is_primary=not override_model)
|
|
927
951
|
|
|
928
952
|
# 1. Check if quota is exhausted (only for non-custom models)
|
|
929
953
|
used = quota.get("credits_used", quota.get("requests_used", 0.0))
|
|
@@ -950,7 +974,7 @@ class Orchestrator:
|
|
|
950
974
|
models_allowed = quota["models_allowed"]
|
|
951
975
|
free_bonus_balance = quota.get("free_bonus_balance", 0.0) or 0.0
|
|
952
976
|
chosen_model = override_model if override_model else self.model_id
|
|
953
|
-
is_custom =
|
|
977
|
+
is_custom = self._is_custom_model_selection(chosen_model, is_primary=not override_model)
|
|
954
978
|
|
|
955
979
|
# If user has active bonus credits, all models are unlocked — skip gating entirely
|
|
956
980
|
if not is_custom and free_bonus_balance <= 0.0 and models_allowed != ["all"] and chosen_model not in models_allowed and not chosen_model.endswith(":free"):
|
|
@@ -960,18 +984,22 @@ class Orchestrator:
|
|
|
960
984
|
self.console.print(" Top up at [bold]utim.dev/pricing[/bold] or run [bold]utim upgrade[/bold].\n")
|
|
961
985
|
fallback_model = DEFAULT_MODEL
|
|
962
986
|
self.console.print(f" Falling back to: [bold]{fallback_model}[/bold]")
|
|
963
|
-
if override_model:
|
|
964
|
-
override_model = fallback_model
|
|
965
|
-
else:
|
|
966
|
-
self.model_id = fallback_model
|
|
967
|
-
|
|
968
|
-
|
|
987
|
+
if override_model:
|
|
988
|
+
override_model = fallback_model
|
|
989
|
+
else:
|
|
990
|
+
self.model_id = fallback_model
|
|
991
|
+
self.model_source = "utim"
|
|
992
|
+
config.set("main_model_source", "utim")
|
|
993
|
+
else:
|
|
994
|
+
fallback_model = DEFAULT_MODEL # default free fallback
|
|
969
995
|
self.console.print(f"\n[bold yellow]⚠ Model '{chosen_model}' is gated under your current '{quota['display_name']}' plan.[/bold yellow]")
|
|
970
996
|
self.console.print(f" Downgrading to default allowed model: '{fallback_model}' for this request.")
|
|
971
|
-
if override_model:
|
|
972
|
-
override_model = fallback_model
|
|
973
|
-
else:
|
|
974
|
-
self.model_id = fallback_model
|
|
997
|
+
if override_model:
|
|
998
|
+
override_model = fallback_model
|
|
999
|
+
else:
|
|
1000
|
+
self.model_id = fallback_model
|
|
1001
|
+
self.model_source = "utim"
|
|
1002
|
+
config.set("main_model_source", "utim")
|
|
975
1003
|
except SystemExit:
|
|
976
1004
|
raise
|
|
977
1005
|
except Exception:
|
|
@@ -994,7 +1022,8 @@ class Orchestrator:
|
|
|
994
1022
|
if self.cancel_event.is_set():
|
|
995
1023
|
break
|
|
996
1024
|
|
|
997
|
-
|
|
1025
|
+
current_is_primary = model_idx == 0
|
|
1026
|
+
current_is_custom = self._is_custom_model_selection(current_model, is_primary=current_is_primary and not override_model)
|
|
998
1027
|
# Check for API key only if we need it for this built-in/OpenRouter model
|
|
999
1028
|
if not current_is_custom and not self._local_api_key and not config.get("api_key"):
|
|
1000
1029
|
continue
|
|
@@ -1061,7 +1090,10 @@ class Orchestrator:
|
|
|
1061
1090
|
"session_id": self.session_id,
|
|
1062
1091
|
}
|
|
1063
1092
|
else:
|
|
1064
|
-
_endpoint_url, _endpoint_key = self._resolve_model_endpoint(
|
|
1093
|
+
_endpoint_url, _endpoint_key = self._resolve_model_endpoint(
|
|
1094
|
+
current_model,
|
|
1095
|
+
is_primary=current_is_primary and not override_model,
|
|
1096
|
+
)
|
|
1065
1097
|
_headers = {
|
|
1066
1098
|
"Authorization": f"Bearer {_endpoint_key}",
|
|
1067
1099
|
"Content-Type": "application/json",
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import json
|
|
3
|
-
import httpx
|
|
4
|
-
import re
|
|
5
|
-
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
import httpx
|
|
4
|
+
import re
|
|
5
|
+
import textwrap
|
|
6
|
+
from utim_cli.config import config
|
|
6
7
|
|
|
7
8
|
def _safe_prompt(prompt_text: str, color: str = None, is_password: bool = False) -> str:
|
|
8
9
|
"""Bulletproof replacement for prompt_toolkit's prompt() when nested inside run_in_terminal context.
|
|
@@ -609,12 +610,12 @@ def _dialog_model_main(orchestrator, target="main"):
|
|
|
609
610
|
if "free" not in tags_val:
|
|
610
611
|
tags_val = list(tags_val) + ["free"]
|
|
611
612
|
|
|
612
|
-
primary_models.append({
|
|
613
|
-
"model_id": mid,
|
|
614
|
-
"desc": desc_val,
|
|
615
|
-
"tags": tags_val,
|
|
616
|
-
"source": "
|
|
617
|
-
})
|
|
613
|
+
primary_models.append({
|
|
614
|
+
"model_id": mid,
|
|
615
|
+
"desc": desc_val,
|
|
616
|
+
"tags": tags_val,
|
|
617
|
+
"source": "utim"
|
|
618
|
+
})
|
|
618
619
|
|
|
619
620
|
# Sort based on cost hierarchy
|
|
620
621
|
primary_models = sorted(primary_models, key=lambda x: (cost_hierarchy.get(x["model_id"], (9, ""))[0], x["model_id"]))
|
|
@@ -661,62 +662,147 @@ def _dialog_model_main(orchestrator, target="main"):
|
|
|
661
662
|
"label": "⬜ None (main agent writes prompt directly)",
|
|
662
663
|
})
|
|
663
664
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
665
|
+
|
|
666
|
+
# Sort based on cost hierarchy
|
|
667
|
+
primary_models = sorted(primary_models, key=lambda x: (cost_hierarchy.get(x["model_id"], (9, ""))[0], x["model_id"]))
|
|
668
|
+
|
|
669
|
+
# ── Prepend user-defined custom models ───────────────────────────────────
|
|
670
|
+
custom_entries = [
|
|
671
|
+
{
|
|
672
|
+
"model_id": m["model_id"],
|
|
673
|
+
"tags": ["custom", m.get("provider_name", "")],
|
|
674
|
+
"source": "custom",
|
|
675
|
+
"desc": f"Custom model via {m.get('provider_name', 'Custom')}."
|
|
676
|
+
}
|
|
677
|
+
for m in config.custom_models
|
|
678
|
+
]
|
|
679
|
+
models = custom_entries + primary_models
|
|
680
|
+
|
|
681
|
+
# ── Inject sentinel options for subagent targets ───────────────────────────
|
|
682
|
+
# "Non-Agent Tool" — disables the LLM loop, tool runs in simple direct mode
|
|
683
|
+
# "None" — only available for generate_image, so main agent writes the prompt
|
|
684
|
+
_BLENDER_TARGETS = ('subagent_blender_vision', 'subagent_blender_code')
|
|
685
|
+
if target.startswith('subagent_') and target not in ('subagent_image_gen',) + _BLENDER_TARGETS:
|
|
686
|
+
non_agent_desc = (
|
|
687
|
+
"Disable the LLM subagent loop. Tool runs in direct/simple mode (e.g. web search = raw results, "
|
|
688
|
+
"codebase investigator = file content only, planner = task list from main agent)."
|
|
689
|
+
)
|
|
690
|
+
sentinel_label = "🚫 Non-Agent Tool (direct mode)"
|
|
691
|
+
if target in ("subagent_generate_image",):
|
|
692
|
+
sentinel_label = "⬜ None (main agent writes the image prompt)"
|
|
693
|
+
non_agent_desc = "Disable the prompt expander entirely — the main agent writes the image prompt directly."
|
|
694
|
+
models.insert(0, {
|
|
695
|
+
"model_id": "__non_agent__",
|
|
696
|
+
"desc": non_agent_desc,
|
|
697
|
+
"tags": ["sentinel"],
|
|
698
|
+
"source": "sentinel",
|
|
699
|
+
"label": sentinel_label,
|
|
700
|
+
})
|
|
701
|
+
if target == 'subagent_generate_image':
|
|
702
|
+
# Also offer a 2nd sentinel: __none__ = no prompt expander at all
|
|
703
|
+
models.insert(0, {
|
|
704
|
+
"model_id": "__none__",
|
|
705
|
+
"desc": "Disable the prompt expander entirely — the main agent writes the image prompt directly.",
|
|
706
|
+
"tags": ["sentinel"],
|
|
707
|
+
"source": "sentinel",
|
|
708
|
+
"label": "⬜ None (main agent writes prompt directly)",
|
|
709
|
+
})
|
|
710
|
+
|
|
711
|
+
# Hoist the currently selected model to the top of the list so it is highlighted by default
|
|
712
|
+
current_model = None
|
|
713
|
+
current_source = None
|
|
714
|
+
if target == 'main':
|
|
715
|
+
current_model = orchestrator.model_id
|
|
716
|
+
current_source = getattr(orchestrator, 'model_source', None) or config.get("main_model_source")
|
|
717
|
+
elif target.startswith('subagent_'):
|
|
718
|
+
subkey = target.split('_', 1)[1]
|
|
719
|
+
current_model = config.get(f"subagent_model_{subkey}")
|
|
720
|
+
current_source = config.get(f"subagent_model_{subkey}_source")
|
|
721
|
+
|
|
722
|
+
def _normalize_source(source: str | None) -> str:
|
|
723
|
+
return "utim" if source in (None, "", "openrouter") else source
|
|
724
|
+
|
|
725
|
+
if current_model in ("__non_agent__", "__none__"):
|
|
726
|
+
current_source = "sentinel"
|
|
727
|
+
else:
|
|
728
|
+
current_source = _normalize_source(current_source)
|
|
729
|
+
|
|
730
|
+
if current_model:
|
|
731
|
+
current_item = None
|
|
732
|
+
for item in models:
|
|
733
|
+
if item["model_id"] == current_model and _normalize_source(item.get("source")) == current_source:
|
|
734
|
+
current_item = item
|
|
735
|
+
break
|
|
736
|
+
if current_item:
|
|
737
|
+
models.remove(current_item)
|
|
738
|
+
models.insert(0, current_item)
|
|
739
|
+
|
|
740
|
+
if not models:
|
|
741
|
+
console.print("\n[red]No models available.[/red]\n")
|
|
742
|
+
return
|
|
743
|
+
|
|
744
|
+
def render_model(i, m, sel):
|
|
745
|
+
bg = 'bg:#a6e3a1 bold #1e1e2e' if sel else ''
|
|
746
|
+
mid = m['model_id']
|
|
747
|
+
source = _normalize_source(m.get('source'))
|
|
748
|
+
|
|
749
|
+
def _compact_desc(text: str, width: int) -> list[str]:
|
|
750
|
+
text = re.sub(r"\s+", " ", str(text or "Fast, efficient model.")).strip()
|
|
751
|
+
text = re.sub(r"\bThis model\b", "It", text, flags=re.IGNORECASE)
|
|
752
|
+
parts = re.split(r"(?<=[.!?])\s+", text)
|
|
753
|
+
compact = parts[0] if parts and parts[0] else text
|
|
754
|
+
if len(compact) < 48 and len(parts) > 1:
|
|
755
|
+
compact = f"{compact} {parts[1]}"
|
|
756
|
+
max_chars = max(width, min(120, width * 2))
|
|
757
|
+
if len(compact) > max_chars:
|
|
758
|
+
compact = compact[:max_chars].rsplit(" ", 1)[0].rstrip(" ,;:")
|
|
759
|
+
compact += "..."
|
|
760
|
+
return textwrap.wrap(
|
|
761
|
+
compact,
|
|
762
|
+
width=width,
|
|
763
|
+
max_lines=2,
|
|
764
|
+
placeholder="...",
|
|
765
|
+
break_long_words=False,
|
|
766
|
+
) or [""]
|
|
767
|
+
|
|
768
|
+
# Sentinel items: Non-Agent Tool / None
|
|
769
|
+
if source == 'sentinel':
|
|
770
|
+
label = m.get('label', mid)
|
|
771
|
+
sentinel_style = 'bg:#f9e2af bold #1e1e2e' if sel else 'bold #f9e2af'
|
|
772
|
+
desc_style = bg or 'fg:#585b70'
|
|
773
|
+
current_mark = ''
|
|
774
|
+
subkey = target.split('_', 1)[1] if target.startswith('subagent_') else ''
|
|
775
|
+
if config.get(f"subagent_model_{subkey}") == mid:
|
|
776
|
+
current_mark = ' ◀ current'
|
|
777
|
+
if is_paid_plan:
|
|
778
|
+
desc_width = max(24, min(54, (console.size.width or 100) - 62))
|
|
779
|
+
desc_lines = _compact_desc(m['desc'], desc_width)
|
|
780
|
+
desc_text = f" {desc_lines[0]}{current_mark}\n"
|
|
781
|
+
for extra in desc_lines[1:]:
|
|
782
|
+
desc_text += f"{'':<55}{extra}\n"
|
|
783
|
+
desc_text += "\n"
|
|
784
|
+
return [
|
|
785
|
+
(sentinel_style, f" {label:<26}"),
|
|
786
|
+
(sentinel_style, f"{'':<12}"),
|
|
787
|
+
(sentinel_style, f"{'':<14}"),
|
|
788
|
+
(desc_style, desc_text),
|
|
789
|
+
]
|
|
790
|
+
else:
|
|
791
|
+
desc_width = max(24, min(72, (console.size.width or 100) - 10))
|
|
792
|
+
desc_lines = _compact_desc(m['desc'], desc_width)
|
|
793
|
+
desc_text = "".join(f" {line}\n" for line in desc_lines) + "\n"
|
|
794
|
+
return [
|
|
795
|
+
(sentinel_style, f" {label}"),
|
|
796
|
+
(sentinel_style, f"{current_mark}\n"),
|
|
797
|
+
(desc_style, desc_text),
|
|
798
|
+
]
|
|
799
|
+
|
|
800
|
+
# Clean display ID (remove provider prefix and ':free' suffix)
|
|
801
|
+
display_id = mid
|
|
802
|
+
if source == 'utim':
|
|
803
|
+
display_id = mid.split('/', 1)[-1]
|
|
804
|
+
if display_id.endswith(':free'):
|
|
805
|
+
display_id = display_id[:-5]
|
|
720
806
|
|
|
721
807
|
if display_id == "gemini-3.1-pro-preview-customtools":
|
|
722
808
|
display_id = "gemini-3.1-pro-preview"
|
|
@@ -727,9 +813,9 @@ def _dialog_model_main(orchestrator, target="main"):
|
|
|
727
813
|
|
|
728
814
|
|
|
729
815
|
current = ''
|
|
730
|
-
if target == 'main' and mid == orchestrator.model_id:
|
|
816
|
+
if target == 'main' and mid == orchestrator.model_id and source == current_source:
|
|
731
817
|
current = ' ◀ current'
|
|
732
|
-
elif target.startswith('subagent_') and mid == config.get(f"subagent_model_{target.split('_', 1)[1]}"):
|
|
818
|
+
elif target.startswith('subagent_') and mid == config.get(f"subagent_model_{target.split('_', 1)[1]}") and source == current_source:
|
|
733
819
|
current = ' ◀ current'
|
|
734
820
|
|
|
735
821
|
desc = m.get("desc", "Fast, efficient model for daily tasks.")
|
|
@@ -759,18 +845,28 @@ def _dialog_model_main(orchestrator, target="main"):
|
|
|
759
845
|
else:
|
|
760
846
|
cost_style = "fg:#f38ba8"
|
|
761
847
|
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
return [
|
|
770
|
-
(style,
|
|
771
|
-
(
|
|
772
|
-
(
|
|
773
|
-
|
|
848
|
+
desc_width = max(24, min(54, (console.size.width or 100) - 62))
|
|
849
|
+
desc_lines = _compact_desc(desc, desc_width)
|
|
850
|
+
desc_text = f" {desc_lines[0]}{current}\n"
|
|
851
|
+
for extra in desc_lines[1:]:
|
|
852
|
+
desc_text += f"{'':<55}{extra}\n"
|
|
853
|
+
desc_text += "\n"
|
|
854
|
+
|
|
855
|
+
return [
|
|
856
|
+
(style, col_model),
|
|
857
|
+
(cost_style, col_cost),
|
|
858
|
+
(rec_style, col_rec),
|
|
859
|
+
(desc_style, desc_text),
|
|
860
|
+
]
|
|
861
|
+
else:
|
|
862
|
+
desc_width = max(24, min(72, (console.size.width or 100) - 10))
|
|
863
|
+
desc_lines = _compact_desc(desc, desc_width)
|
|
864
|
+
desc_text = "".join(f" {line}\n" for line in desc_lines) + "\n"
|
|
865
|
+
return [
|
|
866
|
+
(style, f" {display_id}"),
|
|
867
|
+
(style, f"{current}\n"),
|
|
868
|
+
(desc_style, desc_text),
|
|
869
|
+
]
|
|
774
870
|
|
|
775
871
|
title_str = 'Select Model [dim](a=Add Custom b=BYOK Import d=Delete Custom x=Disconnect Provider q=Cancel)[/dim]'
|
|
776
872
|
if is_paid_plan:
|
|
@@ -785,20 +881,23 @@ def _dialog_model_main(orchestrator, target="main"):
|
|
|
785
881
|
|
|
786
882
|
if action == 'select':
|
|
787
883
|
selected_model = models[idx]['model_id']
|
|
788
|
-
source = models[idx].get('source'
|
|
884
|
+
source = _normalize_source(models[idx].get('source'))
|
|
789
885
|
label = '[yellow](custom)[/yellow] ' if source == 'custom' else ''
|
|
790
886
|
|
|
791
|
-
if target == 'main':
|
|
792
|
-
orchestrator.model_id = selected_model
|
|
887
|
+
if target == 'main':
|
|
888
|
+
orchestrator.model_id = selected_model
|
|
889
|
+
orchestrator.model_source = source
|
|
890
|
+
config.set("main_model_source", source)
|
|
793
891
|
console.print(f"\n[bold #f9e2af]✓ Main Agent model set to {label}{selected_model}[/bold #f9e2af]\n")
|
|
794
892
|
# Update compression threshold for new model
|
|
795
893
|
try:
|
|
796
894
|
orchestrator._update_model_threshold(orchestrator.model_id)
|
|
797
895
|
except Exception:
|
|
798
896
|
pass
|
|
799
|
-
elif target.startswith('subagent_'):
|
|
800
|
-
subkey = target.split('_', 1)[1]
|
|
801
|
-
config.set(f"subagent_model_{subkey}", selected_model)
|
|
897
|
+
elif target.startswith('subagent_'):
|
|
898
|
+
subkey = target.split('_', 1)[1]
|
|
899
|
+
config.set(f"subagent_model_{subkey}", selected_model)
|
|
900
|
+
config.set(f"subagent_model_{subkey}_source", source)
|
|
802
901
|
console.print(f"\n[bold #f9e2af]✓ {subkey} subagent model set to {label}{selected_model}[/bold #f9e2af]\n")
|
|
803
902
|
|
|
804
903
|
elif action == 'add_custom':
|
|
@@ -1111,10 +1210,12 @@ def _dialog_add_custom_model(orchestrator):
|
|
|
1111
1210
|
except (EOFError, KeyboardInterrupt):
|
|
1112
1211
|
switch = "n"
|
|
1113
1212
|
|
|
1114
|
-
if switch in ("", "y", "yes"):
|
|
1115
|
-
orchestrator.model_id = model_id
|
|
1116
|
-
|
|
1117
|
-
|
|
1213
|
+
if switch in ("", "y", "yes"):
|
|
1214
|
+
orchestrator.model_id = model_id
|
|
1215
|
+
orchestrator.model_source = "custom"
|
|
1216
|
+
config.set("main_model_source", "custom")
|
|
1217
|
+
try:
|
|
1218
|
+
orchestrator._update_model_threshold(model_id)
|
|
1118
1219
|
except Exception:
|
|
1119
1220
|
pass
|
|
1120
1221
|
add_console.print(f"\n[bold #a6e3a1]✓ Now using {model_id}[/bold #a6e3a1]\n")
|
|
@@ -1144,10 +1245,12 @@ def _dialog_delete_custom_model(orchestrator, model_id: str):
|
|
|
1144
1245
|
if removed:
|
|
1145
1246
|
del_console.print(f"\n[bold #a6e3a1]✓ Removed {model_id}[/bold #a6e3a1]\n")
|
|
1146
1247
|
# If the deleted model was active, reset to default
|
|
1147
|
-
if orchestrator.model_id == model_id:
|
|
1148
|
-
from utim_cli.constants import DEFAULT_MODEL
|
|
1149
|
-
orchestrator.model_id = DEFAULT_MODEL
|
|
1150
|
-
|
|
1248
|
+
if orchestrator.model_id == model_id and (getattr(orchestrator, "model_source", None) or config.get("main_model_source")) == "custom":
|
|
1249
|
+
from utim_cli.constants import DEFAULT_MODEL
|
|
1250
|
+
orchestrator.model_id = DEFAULT_MODEL
|
|
1251
|
+
orchestrator.model_source = "utim"
|
|
1252
|
+
config.set("main_model_source", "utim")
|
|
1253
|
+
del_console.print(f"[dim]Switched back to {DEFAULT_MODEL}[/dim]\n")
|
|
1151
1254
|
else:
|
|
1152
1255
|
del_console.print("[yellow]Model not found in custom list.[/yellow]\n")
|
|
1153
1256
|
else:
|
|
@@ -1226,17 +1329,20 @@ def _dialog_disconnect_provider(orchestrator):
|
|
|
1226
1329
|
)
|
|
1227
1330
|
# If active model was deleted, reset to DEFAULT_MODEL
|
|
1228
1331
|
active_model = orchestrator.model_id
|
|
1229
|
-
|
|
1230
|
-
|
|
1332
|
+
active_source = getattr(orchestrator, "model_source", None) or config.get("main_model_source")
|
|
1333
|
+
active_still_exists = any(m["model_id"] == active_model for m in config.custom_models)
|
|
1334
|
+
if active_source == "custom" and not active_still_exists and active_model not in [DEFAULT_MODEL, "anthropic/claude-sonnet-4.6"]:
|
|
1231
1335
|
is_custom_removed = True
|
|
1232
1336
|
# Hardcoded check since we cannot import utim_cli.server in production CLI builds
|
|
1233
1337
|
# (as the server module is excluded from the package in pyproject.toml)
|
|
1234
1338
|
if active_model.startswith("openai/") or active_model.startswith("anthropic/") or active_model.startswith("google/") or active_model.startswith("cohere/"):
|
|
1235
1339
|
is_custom_removed = False
|
|
1236
1340
|
|
|
1237
|
-
if is_custom_removed:
|
|
1238
|
-
orchestrator.model_id = DEFAULT_MODEL
|
|
1239
|
-
|
|
1341
|
+
if is_custom_removed:
|
|
1342
|
+
orchestrator.model_id = DEFAULT_MODEL
|
|
1343
|
+
orchestrator.model_source = "utim"
|
|
1344
|
+
config.set("main_model_source", "utim")
|
|
1345
|
+
disc_console.print(f"[dim]Active model removed. Switched back to cohere/north-mini-code:free[/dim]\n")
|
|
1240
1346
|
else:
|
|
1241
1347
|
disc_console.print("\n[dim]Cancelled.[/dim]\n")
|
|
1242
1348
|
|
|
@@ -883,7 +883,7 @@ def _print_animated_banner(animated: bool = True):
|
|
|
883
883
|
else:
|
|
884
884
|
console.print(f"[bold {color}]{BANNER_BIG}[/bold {color}]")
|
|
885
885
|
|
|
886
|
-
console.print(f" [bold white]U Think I Make[/bold white] [dim]v1.46.
|
|
886
|
+
console.print(f" [bold white]U Think I Make[/bold white] [dim]v1.46.36[/dim]")
|
|
887
887
|
user_email = config.email
|
|
888
888
|
user_type = "UTIM Community"
|
|
889
889
|
api_key = config.get("api_key")
|
|
@@ -1783,10 +1783,7 @@ def _handle_command(cmd: str, orchestrator: Orchestrator, app_ref) -> None:
|
|
|
1783
1783
|
# Activate immediately in this session
|
|
1784
1784
|
import os as _os2
|
|
1785
1785
|
_os2.environ["OPENROUTER_API_KEY"] = new_key
|
|
1786
|
-
|
|
1787
|
-
orchestrator._local_client = _OAI(
|
|
1788
|
-
base_url="https://openrouter.ai/api/v1", api_key=new_key
|
|
1789
|
-
)
|
|
1786
|
+
orchestrator._local_client = True
|
|
1790
1787
|
console.print("\n[bold #f9e2af]✓ API key saved and active.[/bold #f9e2af]\n")
|
|
1791
1788
|
|
|
1792
1789
|
else:
|
|
@@ -3143,7 +3140,7 @@ def check_and_update_background():
|
|
|
3143
3140
|
return
|
|
3144
3141
|
|
|
3145
3142
|
latest_ver = resp.json().get("version", "")
|
|
3146
|
-
current_ver = "1.46.
|
|
3143
|
+
current_ver = "1.46.36"
|
|
3147
3144
|
|
|
3148
3145
|
def parse_ver(v):
|
|
3149
3146
|
return [int(x) for x in v.split(".") if x.isdigit()]
|
|
@@ -3264,8 +3261,10 @@ def start_chat():
|
|
|
3264
3261
|
orchestrator.messages = saved.get("messages", [])
|
|
3265
3262
|
orchestrator.turn_history = saved.get("turn_history", [])
|
|
3266
3263
|
orchestrator.redo_history = saved.get("redo_history", [])
|
|
3267
|
-
if "model_id" in saved:
|
|
3268
|
-
orchestrator.model_id = saved["model_id"]
|
|
3264
|
+
if "model_id" in saved:
|
|
3265
|
+
orchestrator.model_id = saved["model_id"]
|
|
3266
|
+
if "model_source" in saved:
|
|
3267
|
+
orchestrator.model_source = saved["model_source"]
|
|
3269
3268
|
|
|
3270
3269
|
STATE["session_id"] = saved.get("session_id")
|
|
3271
3270
|
if "queue" in saved:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: utim-cli
|
|
3
|
-
Version: 1.46.
|
|
3
|
+
Version: 1.46.36
|
|
4
4
|
Summary: UTIM – Universal Terminal Intelligence Manager. A powerful agentic AI coding assistant for your terminal.
|
|
5
5
|
License: MIT
|
|
6
6
|
Project-URL: Homepage, https://utim.dev
|
|
@@ -20,7 +20,6 @@ Classifier: Intended Audience :: Developers
|
|
|
20
20
|
Requires-Python: >=3.9
|
|
21
21
|
Description-Content-Type: text/markdown
|
|
22
22
|
License-File: LICENSE
|
|
23
|
-
Requires-Dist: openai>=1.30.0
|
|
24
23
|
Requires-Dist: requests==2.31.0
|
|
25
24
|
Requires-Dist: aiohttp>=3.9.5
|
|
26
25
|
Requires-Dist: python-dotenv==1.0.1
|
|
@@ -30,9 +29,10 @@ Requires-Dist: chardet==4.0.0
|
|
|
30
29
|
Requires-Dist: typer>=0.9.0
|
|
31
30
|
Requires-Dist: rich>=13.0.0
|
|
32
31
|
Requires-Dist: prompt_toolkit>=3.0.0
|
|
33
|
-
Requires-Dist: mcp>=0.1.0
|
|
34
32
|
Requires-Dist: nest-asyncio>=1.5.0
|
|
35
33
|
Requires-Dist: sqlalchemy>=2.0.0
|
|
34
|
+
Requires-Dist: openai>=1.30.0; platform_system != "Linux" or (platform_machine != "aarch64" and platform_machine != "arm64")
|
|
35
|
+
Requires-Dist: mcp>=0.1.0; platform_system != "Linux" or (platform_machine != "aarch64" and platform_machine != "arm64")
|
|
36
36
|
Provides-Extra: search
|
|
37
37
|
Requires-Dist: scrapy>=2.11.0; extra == "search"
|
|
38
38
|
Requires-Dist: scrapy-playwright>=0.0.34; extra == "search"
|
|
@@ -44,6 +44,8 @@ Requires-Dist: tree-sitter>=0.21.0; extra == "parsers"
|
|
|
44
44
|
Requires-Dist: tree-sitter-python>=0.21.0; extra == "parsers"
|
|
45
45
|
Requires-Dist: tree-sitter-javascript>=0.21.0; extra == "parsers"
|
|
46
46
|
Requires-Dist: tree-sitter-typescript>=0.21.0; extra == "parsers"
|
|
47
|
+
Provides-Extra: mcp
|
|
48
|
+
Requires-Dist: mcp>=0.1.0; extra == "mcp"
|
|
47
49
|
Provides-Extra: full
|
|
48
50
|
Requires-Dist: scrapy>=2.11.0; extra == "full"
|
|
49
51
|
Requires-Dist: scrapy-playwright>=0.0.34; extra == "full"
|
|
@@ -53,6 +55,10 @@ Requires-Dist: tree-sitter>=0.21.0; extra == "full"
|
|
|
53
55
|
Requires-Dist: tree-sitter-python>=0.21.0; extra == "full"
|
|
54
56
|
Requires-Dist: tree-sitter-javascript>=0.21.0; extra == "full"
|
|
55
57
|
Requires-Dist: tree-sitter-typescript>=0.21.0; extra == "full"
|
|
58
|
+
Requires-Dist: mcp>=0.1.0; (platform_system != "Linux" or (platform_machine != "aarch64" and platform_machine != "arm64")) and extra == "full"
|
|
59
|
+
Requires-Dist: openai>=1.30.0; (platform_system != "Linux" or (platform_machine != "aarch64" and platform_machine != "arm64")) and extra == "full"
|
|
60
|
+
Provides-Extra: legacy-agent
|
|
61
|
+
Requires-Dist: openai>=1.30.0; extra == "legacy-agent"
|
|
56
62
|
|
|
57
63
|
# UTIM CLI Agent: Enterprise Coder Assistant
|
|
58
64
|
|
|
@@ -194,6 +200,12 @@ pip install utim-cli
|
|
|
194
200
|
pip install "utim-cli[full]"
|
|
195
201
|
```
|
|
196
202
|
|
|
203
|
+
Desktop installs include the normal SDK/MCP path automatically. Mobile/Termux
|
|
204
|
+
installs keep those native-heavy pieces optional to avoid `pydantic-core`; use
|
|
205
|
+
`utim-cli[mcp]` or `utim-cli[legacy-agent]` only if you explicitly need them.
|
|
206
|
+
Do not install `utim_cli/server/requirements.txt` unless you are deploying the
|
|
207
|
+
server.
|
|
208
|
+
|
|
197
209
|
### Source install (development)
|
|
198
210
|
```bash
|
|
199
211
|
git clone https://github.com/emendai/utim.git
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
openai>=1.30.0
|
|
2
1
|
requests==2.31.0
|
|
3
2
|
aiohttp>=3.9.5
|
|
4
3
|
python-dotenv==1.0.1
|
|
@@ -8,10 +7,13 @@ chardet==4.0.0
|
|
|
8
7
|
typer>=0.9.0
|
|
9
8
|
rich>=13.0.0
|
|
10
9
|
prompt_toolkit>=3.0.0
|
|
11
|
-
mcp>=0.1.0
|
|
12
10
|
nest-asyncio>=1.5.0
|
|
13
11
|
sqlalchemy>=2.0.0
|
|
14
12
|
|
|
13
|
+
[:platform_system != "Linux" or (platform_machine != "aarch64" and platform_machine != "arm64")]
|
|
14
|
+
openai>=1.30.0
|
|
15
|
+
mcp>=0.1.0
|
|
16
|
+
|
|
15
17
|
[full]
|
|
16
18
|
scrapy>=2.11.0
|
|
17
19
|
scrapy-playwright>=0.0.34
|
|
@@ -22,9 +24,19 @@ tree-sitter-python>=0.21.0
|
|
|
22
24
|
tree-sitter-javascript>=0.21.0
|
|
23
25
|
tree-sitter-typescript>=0.21.0
|
|
24
26
|
|
|
27
|
+
[full:platform_system != "Linux" or (platform_machine != "aarch64" and platform_machine != "arm64")]
|
|
28
|
+
mcp>=0.1.0
|
|
29
|
+
openai>=1.30.0
|
|
30
|
+
|
|
25
31
|
[images]
|
|
26
32
|
pillow>=9.0.0
|
|
27
33
|
|
|
34
|
+
[legacy-agent]
|
|
35
|
+
openai>=1.30.0
|
|
36
|
+
|
|
37
|
+
[mcp]
|
|
38
|
+
mcp>=0.1.0
|
|
39
|
+
|
|
28
40
|
[parsers]
|
|
29
41
|
tree-sitter>=0.21.0
|
|
30
42
|
tree-sitter-python>=0.21.0
|
|
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
|