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.
Files changed (74) hide show
  1. {utim_cli-1.46.34 → utim_cli-1.46.36}/CHANGELOG.md +13 -0
  2. {utim_cli-1.46.34/utim_cli.egg-info → utim_cli-1.46.36}/PKG-INFO +15 -3
  3. {utim_cli-1.46.34 → utim_cli-1.46.36}/README.md +18 -12
  4. {utim_cli-1.46.34 → utim_cli-1.46.36}/pyproject.toml +11 -3
  5. {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/__init__.py +1 -1
  6. {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/_version.py +1 -1
  7. {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/agent.py +19 -12
  8. {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/config.py +63 -16
  9. {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/doctor.py +4 -2
  10. {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/orchestrator.py +66 -34
  11. {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/tui/model_dialog.py +206 -100
  12. {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/utim.py +25 -20
  13. {utim_cli-1.46.34 → utim_cli-1.46.36/utim_cli.egg-info}/PKG-INFO +15 -3
  14. {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli.egg-info/requires.txt +14 -2
  15. {utim_cli-1.46.34 → utim_cli-1.46.36}/LICENSE +0 -0
  16. {utim_cli-1.46.34 → utim_cli-1.46.36}/MANIFEST.in +0 -0
  17. {utim_cli-1.46.34 → utim_cli-1.46.36}/setup.cfg +0 -0
  18. {utim_cli-1.46.34 → utim_cli-1.46.36}/setup.py +0 -0
  19. {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/auth.py +0 -0
  20. {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/backup.py +0 -0
  21. {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/blender_agent.py +0 -0
  22. {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/bootstrap.py +0 -0
  23. {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/client_utils.py +0 -0
  24. {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/constants.py +0 -0
  25. {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/context_pruner.py +0 -0
  26. {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/knowledge_graph.py +0 -0
  27. {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/local_db.py +0 -0
  28. {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/logger.py +0 -0
  29. {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/mcp_clean_wrapper.py +0 -0
  30. {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/mcp_client.py +0 -0
  31. {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/mcp_registry.json +0 -0
  32. {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/reflection.py +0 -0
  33. {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/report.py +0 -0
  34. {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/scrapy_search.py +0 -0
  35. {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/server/__init__.py +0 -0
  36. {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/server/auth.py +0 -0
  37. {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/server/db.py +0 -0
  38. {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/server/email_utils.py +0 -0
  39. {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/server/exchange_rate.py +0 -0
  40. {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/server/firebase.py +0 -0
  41. {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/server/history.py +0 -0
  42. {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/server/logging_config.py +0 -0
  43. {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/server/models.py +0 -0
  44. {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/server/pricing_updater.py +0 -0
  45. {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/server/rate_limit.py +0 -0
  46. {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/server/router.py +0 -0
  47. {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/server/routes/__init__.py +0 -0
  48. {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/server/routes/auth_routes.py +0 -0
  49. {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/server/routes/completion_routes.py +0 -0
  50. {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/server/routes/credit_routes.py +0 -0
  51. {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/server/routes/feedback_routes.py +0 -0
  52. {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/server/routes/quota_routes.py +0 -0
  53. {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/server/routes/session_routes.py +0 -0
  54. {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/server/routes/share_routes.py +0 -0
  55. {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/server/server.py +0 -0
  56. {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/share.py +0 -0
  57. {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/share_tui.py +0 -0
  58. {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/situational_scoring.py +0 -0
  59. {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/state.py +0 -0
  60. {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/tools.py +0 -0
  61. {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/tui/__init__.py +0 -0
  62. {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/tui/feedback_dialog.py +0 -0
  63. {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/tui/history_dialog.py +0 -0
  64. {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/tui/mcp_dialog.py +0 -0
  65. {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/tui/resume_dialog.py +0 -0
  66. {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/tui/tools_dialog.py +0 -0
  67. {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/tui/update_dialog.py +0 -0
  68. {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/utilities.py +0 -0
  69. {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/vector_memory.py +0 -0
  70. {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli/workspace.py +0 -0
  71. {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli.egg-info/SOURCES.txt +0 -0
  72. {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli.egg-info/dependency_links.txt +0 -0
  73. {utim_cli-1.46.34 → utim_cli-1.46.36}/utim_cli.egg-info/entry_points.txt +0 -0
  74. {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.34
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
- ### Source install (development)
142
- ```bash
143
- git clone https://github.com/emendai/utim.git
144
- cd utim
145
- pip install -e ".[full]"
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.34"
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]
@@ -5,7 +5,7 @@ try:
5
5
  from utim_cli._version import VERSION as __version__
6
6
  except ImportError:
7
7
  # Fallback during first-install / editable-install edge cases
8
- __version__ = "1.46.34"
8
+ __version__ = "1.46.36"
9
9
 
10
10
  import builtins, os, pathlib, urllib.parse, re
11
11
 
@@ -11,4 +11,4 @@
11
11
  # from here so every surface stays in sync automatically.
12
12
  # ─────────────────────────────────────────────────────────────────────────────
13
13
 
14
- VERSION = "1.46.34"
14
+ VERSION = "1.46.36"
@@ -1,9 +1,8 @@
1
1
  import os
2
- import json
3
- import sys
4
- import time
5
- from openai import OpenAI
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: Copy the last response to your clipboard with /copy",
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 balance with /balance",
38
- "Tip: List available tools with /tools",
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
- self.client = OpenAI(
56
- base_url="https://openrouter.ai/api/v1",
57
- api_key=api_key or "sk-fake-key-for-init",
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
- # Suppress warnings and patch requests if SSL verification is disabled
54
- if not self.verify_ssl:
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
- "openai", "requests", "aiohttp", "typer", "rich",
25
- "prompt_toolkit", "mcp", "nest_asyncio"
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 _resolve_model_endpoint(self, model_id: str) -> tuple:
884
- """Return (chat_completions_url, api_key) for *model_id*.
885
-
886
- Custom models (added via /model add) carry their own base_url and
887
- api_key; everything else falls back to OpenRouter.
888
- """
889
- custom = config.get_custom_model(model_id)
890
- if custom:
891
- base = custom.get("base_url", "").rstrip("/")
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 = bool(config.get_custom_model(chosen_model))
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 = bool(config.get_custom_model(chosen_model))
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
- else:
968
- fallback_model = DEFAULT_MODEL # default free fallback
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
- current_is_custom = bool(config.get_custom_model(current_model))
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(current_model)
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",