utim-cli 1.46.34__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.34 → utim_cli-1.46.36}/CHANGELOG.md +13 -0
- {utim_cli-1.46.34/utim_cli.egg-info → utim_cli-1.46.36}/PKG-INFO +15 -3
- {utim_cli-1.46.34 → utim_cli-1.46.36}/README.md +18 -12
- {utim_cli-1.46.34 → utim_cli-1.46.36}/pyproject.toml +11 -3
- {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/__init__.py +1 -1
- {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/_version.py +1 -1
- {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/agent.py +19 -12
- {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/config.py +63 -16
- {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/doctor.py +4 -2
- {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/orchestrator.py +66 -34
- {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/tui/model_dialog.py +206 -100
- {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/utim.py +25 -20
- {utim_cli-1.46.34 → utim_cli-1.46.36/utim_cli.egg-info}/PKG-INFO +15 -3
- {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli.egg-info/requires.txt +14 -2
- {utim_cli-1.46.34 → utim_cli-1.46.36}/LICENSE +0 -0
- {utim_cli-1.46.34 → utim_cli-1.46.36}/MANIFEST.in +0 -0
- {utim_cli-1.46.34 → utim_cli-1.46.36}/setup.cfg +0 -0
- {utim_cli-1.46.34 → utim_cli-1.46.36}/setup.py +0 -0
- {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/auth.py +0 -0
- {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/backup.py +0 -0
- {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/blender_agent.py +0 -0
- {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/bootstrap.py +0 -0
- {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/client_utils.py +0 -0
- {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/constants.py +0 -0
- {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/context_pruner.py +0 -0
- {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/knowledge_graph.py +0 -0
- {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/local_db.py +0 -0
- {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/logger.py +0 -0
- {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/mcp_clean_wrapper.py +0 -0
- {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/mcp_client.py +0 -0
- {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/mcp_registry.json +0 -0
- {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/reflection.py +0 -0
- {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/report.py +0 -0
- {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/scrapy_search.py +0 -0
- {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/server/__init__.py +0 -0
- {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/server/auth.py +0 -0
- {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/server/db.py +0 -0
- {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/server/email_utils.py +0 -0
- {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/server/exchange_rate.py +0 -0
- {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/server/firebase.py +0 -0
- {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/server/history.py +0 -0
- {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/server/logging_config.py +0 -0
- {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/server/models.py +0 -0
- {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/server/pricing_updater.py +0 -0
- {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/server/rate_limit.py +0 -0
- {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/server/router.py +0 -0
- {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/server/routes/__init__.py +0 -0
- {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/server/routes/auth_routes.py +0 -0
- {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/server/routes/completion_routes.py +0 -0
- {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/server/routes/credit_routes.py +0 -0
- {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/server/routes/feedback_routes.py +0 -0
- {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/server/routes/quota_routes.py +0 -0
- {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/server/routes/session_routes.py +0 -0
- {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/server/routes/share_routes.py +0 -0
- {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/server/server.py +0 -0
- {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/share.py +0 -0
- {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/share_tui.py +0 -0
- {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/situational_scoring.py +0 -0
- {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/state.py +0 -0
- {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/tools.py +0 -0
- {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/tui/__init__.py +0 -0
- {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/tui/feedback_dialog.py +0 -0
- {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/tui/history_dialog.py +0 -0
- {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/tui/mcp_dialog.py +0 -0
- {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/tui/resume_dialog.py +0 -0
- {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/tui/tools_dialog.py +0 -0
- {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/tui/update_dialog.py +0 -0
- {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/utilities.py +0 -0
- {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/vector_memory.py +0 -0
- {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/workspace.py +0 -0
- {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli.egg-info/SOURCES.txt +0 -0
- {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli.egg-info/dependency_links.txt +0 -0
- {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli.egg-info/entry_points.txt +0 -0
- {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli.egg-info/top_level.txt +0 -0
|
@@ -2,6 +2,19 @@
|
|
|
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
|
+
|
|
10
|
+
## [1.46.35] - 2026-07-11
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Instant SSL Verification Toggle**: Upgraded the `/sslverify` session patcher to dynamically apply and remove the `verify=False` wrapper live on the running `requests.Session` class, eliminating the need to restart the CLI when toggling proxy security.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- **CLI Tips Correctness**: Refined all CLI and ReAct Agent usage tips to only feature verified, public commands and actual hotkeys, removing any references to internal or hidden commands.
|
|
17
|
+
|
|
5
18
|
## [1.46.34] - 2026-07-11
|
|
6
19
|
|
|
7
20
|
### 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
|
|
@@ -31,11 +30,11 @@ TOOL_META = {
|
|
|
31
30
|
}
|
|
32
31
|
|
|
33
32
|
TIPS = [
|
|
34
|
-
"Tip:
|
|
33
|
+
"Tip: Use /help to see all available commands and the help panel",
|
|
35
34
|
"Tip: Save your current conversation with /resume save <tag>",
|
|
36
35
|
"Tip: Clear conversation history with /clear",
|
|
37
|
-
"Tip: Check your credit
|
|
38
|
-
"Tip:
|
|
36
|
+
"Tip: Check your quota limits and credit usage with /usage",
|
|
37
|
+
"Tip: Enable or disable individual tools with /tools",
|
|
39
38
|
"Tip: Switch models with /model",
|
|
40
39
|
]
|
|
41
40
|
|
|
@@ -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
|
|
|
@@ -7,6 +7,48 @@ from typing import Any, Dict, Optional
|
|
|
7
7
|
|
|
8
8
|
logger = logging.getLogger("utim.config")
|
|
9
9
|
|
|
10
|
+
# ── SSL session patch ─────────────────────────────────────────────────────────
|
|
11
|
+
# Keeps a reference to the true original so toggling works correctly in both
|
|
12
|
+
# directions without chaining patches on top of each other.
|
|
13
|
+
_ORIGINAL_SESSION_REQUEST = None
|
|
14
|
+
_SSL_PATCH_ACTIVE = False
|
|
15
|
+
|
|
16
|
+
def _apply_ssl_session_patch(disable: bool) -> None:
|
|
17
|
+
"""Apply or remove the verify=False monkey-patch on requests.Session.request.
|
|
18
|
+
|
|
19
|
+
Safe to call multiple times. Does nothing when the requested state already
|
|
20
|
+
matches the current state.
|
|
21
|
+
"""
|
|
22
|
+
global _ORIGINAL_SESSION_REQUEST, _SSL_PATCH_ACTIVE
|
|
23
|
+
try:
|
|
24
|
+
import requests as _req_mod
|
|
25
|
+
import urllib3
|
|
26
|
+
|
|
27
|
+
# Capture the true original exactly once.
|
|
28
|
+
if _ORIGINAL_SESSION_REQUEST is None:
|
|
29
|
+
_ORIGINAL_SESSION_REQUEST = _req_mod.Session.request
|
|
30
|
+
|
|
31
|
+
if disable and not _SSL_PATCH_ACTIVE:
|
|
32
|
+
# Suppress noisy InsecureRequestWarning lines.
|
|
33
|
+
try:
|
|
34
|
+
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
|
35
|
+
except Exception:
|
|
36
|
+
pass
|
|
37
|
+
_orig = _ORIGINAL_SESSION_REQUEST
|
|
38
|
+
def _ssl_off(self, method, url, *args, **kwargs):
|
|
39
|
+
kwargs.setdefault('verify', False)
|
|
40
|
+
return _orig(self, method, url, *args, **kwargs)
|
|
41
|
+
_req_mod.Session.request = _ssl_off
|
|
42
|
+
_SSL_PATCH_ACTIVE = True
|
|
43
|
+
|
|
44
|
+
elif not disable and _SSL_PATCH_ACTIVE:
|
|
45
|
+
# Restore the true original so verify defaults to True again.
|
|
46
|
+
_req_mod.Session.request = _ORIGINAL_SESSION_REQUEST
|
|
47
|
+
_SSL_PATCH_ACTIVE = False
|
|
48
|
+
|
|
49
|
+
except Exception:
|
|
50
|
+
pass
|
|
51
|
+
|
|
10
52
|
class Config:
|
|
11
53
|
def __init__(self):
|
|
12
54
|
import sys
|
|
@@ -44,28 +86,33 @@ class Config:
|
|
|
44
86
|
hdrs = dict(hdrs)
|
|
45
87
|
hdrs.setdefault("User-Agent", _BROWSER_UA)
|
|
46
88
|
kwargs["headers"] = hdrs
|
|
89
|
+
|
|
90
|
+
primary_host = "api.utim.dev"
|
|
91
|
+
fallback_host = "utim-cli-production.up.railway.app"
|
|
92
|
+
|
|
93
|
+
if primary_host in url:
|
|
94
|
+
try:
|
|
95
|
+
return _original_request(self, method, url, *args, **kwargs)
|
|
96
|
+
except _req_mod.exceptions.RequestException as e:
|
|
97
|
+
fallback_url = url.replace(primary_host, fallback_host)
|
|
98
|
+
logger.warning(f"Connection to {primary_host} failed, trying fallback {fallback_host}: {e}")
|
|
99
|
+
return _original_request(self, method, fallback_url, *args, **kwargs)
|
|
100
|
+
elif fallback_host in url:
|
|
101
|
+
try:
|
|
102
|
+
return _original_request(self, method, url, *args, **kwargs)
|
|
103
|
+
except _req_mod.exceptions.RequestException as e:
|
|
104
|
+
fallback_url = url.replace(fallback_host, primary_host)
|
|
105
|
+
logger.warning(f"Connection to {fallback_host} failed, trying fallback {primary_host}: {e}")
|
|
106
|
+
return _original_request(self, method, fallback_url, *args, **kwargs)
|
|
107
|
+
|
|
47
108
|
return _original_request(self, method, url, *args, **kwargs)
|
|
48
109
|
|
|
49
110
|
_req_mod.Session.request = _ua_patched_request
|
|
50
111
|
except Exception:
|
|
51
112
|
pass
|
|
52
113
|
|
|
53
|
-
#
|
|
54
|
-
|
|
55
|
-
try:
|
|
56
|
-
import urllib3
|
|
57
|
-
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
|
58
|
-
except Exception:
|
|
59
|
-
pass
|
|
60
|
-
try:
|
|
61
|
-
import requests as _req_mod2
|
|
62
|
-
_original_request2 = _req_mod2.Session.request
|
|
63
|
-
def _ssl_patched_request(self, method, url, *args, **kwargs):
|
|
64
|
-
kwargs.setdefault('verify', False)
|
|
65
|
-
return _original_request2(self, method, url, *args, **kwargs)
|
|
66
|
-
_req_mod2.Session.request = _ssl_patched_request
|
|
67
|
-
except Exception:
|
|
68
|
-
pass
|
|
114
|
+
# Apply SSL patch immediately if already disabled in config.
|
|
115
|
+
_apply_ssl_session_patch(disable=not self.verify_ssl)
|
|
69
116
|
|
|
70
117
|
def _load(self):
|
|
71
118
|
# Load global config
|
|
@@ -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",
|