utim-cli 2.3.2__tar.gz → 2.3.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {utim_cli-2.3.2 → utim_cli-2.3.4}/CHANGELOG.md +12 -0
- {utim_cli-2.3.2/utim_cli.egg-info → utim_cli-2.3.4}/PKG-INFO +1 -1
- {utim_cli-2.3.2 → utim_cli-2.3.4}/pyproject.toml +1 -1
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/__init__.py +1 -1
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/_version.py +1 -1
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/ask_helper.py +7 -54
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/blender_agent.py +4 -5
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/bootstrap.py +4 -1
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/client_utils.py +153 -25
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/config.py +4 -1
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/constants.py +1 -1
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/context_pruner.py +277 -69
- utim_cli-2.3.4/utim_cli/orchestrator.py +1920 -0
- utim_cli-2.3.4/utim_cli/orchestrator_helpers.py +1266 -0
- utim_cli-2.3.4/utim_cli/orchestrator_llm_transport.py +1266 -0
- utim_cli-2.3.4/utim_cli/orchestrator_tool_exec.py +1534 -0
- utim_cli-2.3.4/utim_cli/orchestrator_turn_history.py +1359 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/reflection.py +54 -60
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/db.py +4 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/models.py +11 -1
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/pricing_updater.py +28 -74
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/router.py +12 -18
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/routes/admin_db_routes.py +21 -14
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/routes/auth_routes.py +2 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/routes/completion_routes.py +94 -39
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/task_dispatcher.py +80 -37
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/tools.py +228 -198
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/tui/model_dialog.py +69 -28
- utim_cli-2.3.4/utim_cli/tui/tasks_dialog.py +188 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/utim.py +141 -107
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/wheel.py +3 -8
- {utim_cli-2.3.2 → utim_cli-2.3.4/utim_cli.egg-info}/PKG-INFO +1 -1
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli.egg-info/SOURCES.txt +5 -0
- utim_cli-2.3.2/utim_cli/orchestrator.py +0 -6814
- {utim_cli-2.3.2 → utim_cli-2.3.4}/LICENSE +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/MANIFEST.in +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/README.md +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/setup.cfg +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/setup.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim/__init__.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/agent.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/auth.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/backup.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/billing.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/brain.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/doctor.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/harbor.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/knowledge_graph.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/local_db.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/logger.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/mcp_clean_wrapper.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/mcp_client.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/mcp_registry.json +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/models.txt +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/plugins/__init__.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/plugins/manager.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/plugins/registry.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/plugins/schema.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/report.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/scrapy_search.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/sdk/__init__.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/sdk/config.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/sdk/context.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/sdk/events.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/sdk/handlers.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/sdk/session.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/sdk/sidecar.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/__init__.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/admin_auth.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/attribution.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/audit_log.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/auth.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/batch_processor.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/captcha.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/cli_auth.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/docs_md/SECRET_PROVISIONING.md +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/docs_md/about.md +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/docs_md/changelog.md +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/docs_md/docs.md +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/docs_md/features.md +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/docs_md/license.md +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/docs_md/pricing.md +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/docs_md/privacy.md +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/docs_md/refund.md +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/docs_md/support.md +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/docs_md/terms.md +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/email_utils.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/exchange_rate.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/firebase.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/fix_duplicate_users.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/history.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/local_llm.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/logging_config.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/micro_batcher.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/model_agent.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/provision_build.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/rate_limit.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/rewards_engine.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/routes/__init__.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/routes/billing_routes.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/routes/credit_routes.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/routes/feedback_routes.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/routes/marketplace_routes.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/routes/quota_routes.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/routes/quota_share_routes.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/routes/referral_routes.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/routes/rewards_routes.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/routes/security_routes.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/routes/session_routes.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/routes/share_routes.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/server.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/storage_nodes.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/share.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/share_tui.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/situational_scoring.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/state.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/subagent_manager.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/tui/__init__.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/tui/feedback_dialog.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/tui/history_dialog.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/tui/marketplace_app_state.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/tui/marketplace_dialog.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/tui/marketplace_layout_engine.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/tui/mcp_dialog.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/tui/miniagents_dialog.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/tui/plugins_dialog.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/tui/publish_dialog.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/tui/quota_dialog.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/tui/quota_redeem_dialog.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/tui/quota_share_dialog.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/tui/resume_dialog.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/tui/rewards_tui.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/tui/skills_dialog.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/tui/subagents_dialog.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/tui/thinking_display.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/tui/tools_dialog.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/tui/update_dialog.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/utilities.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/utimmodel.txt +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/vector_memory.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/workspace.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli.egg-info/dependency_links.txt +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli.egg-info/entry_points.txt +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli.egg-info/requires.txt +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli.egg-info/top_level.txt +0 -0
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.3.4] - 2026-08-22
|
|
4
|
+
|
|
5
|
+
### ⚡ Performance & Polish
|
|
6
|
+
- Latency optimizations: Zero-buffer stream reading, asynchronous background experience caching, parallel multi-tool file inspection execution, and micro-batching bypass for interactive sessions.
|
|
7
|
+
- Dynamic fallback model resolution from server model registry with `openrouter/free` priority.
|
|
8
|
+
- Configured reasoning capability toggling across server admin interface and CLI model selector.
|
|
9
|
+
|
|
10
|
+
## [2.3.3] - 2026-08-20
|
|
11
|
+
|
|
12
|
+
### 🚀 Performance & Polish
|
|
13
|
+
- Token usage optimization and minor enhancements
|
|
14
|
+
|
|
3
15
|
## [2.3.2] - 2026-08-20
|
|
4
16
|
|
|
5
17
|
### 🚀 Performance & Fixes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: utim-cli
|
|
3
|
-
Version: 2.3.
|
|
3
|
+
Version: 2.3.4
|
|
4
4
|
Summary: UTIM – Universal Terminal Intelligence Manager. A powerful agentic AI coding assistant for your terminal.
|
|
5
5
|
License: Emend AI Proprietary EULA
|
|
6
6
|
Project-URL: Homepage, https://utim.dev
|
|
@@ -25,47 +25,10 @@ from rich import box
|
|
|
25
25
|
|
|
26
26
|
# ── Primary & Fallback Free Models ───────────────────────────────────────────
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
DEFAULT_FREE_FALLBACKS = [
|
|
31
|
-
"google/gemini-2.5-flash:free",
|
|
32
|
-
"meta-llama/llama-3.3-70b-instruct:free",
|
|
33
|
-
"cohere/north-mini-code:free",
|
|
34
|
-
"poolside/laguna-xs-2.1:free",
|
|
35
|
-
"nvidia/nemotron-3-ultra-550b-a55b:free",
|
|
36
|
-
"inclusionai/ling-3.0-flash:free",
|
|
37
|
-
"deepseek/deepseek-r1:free",
|
|
38
|
-
"openai/gpt-oss-20b:free",
|
|
39
|
-
"google/gemma-4-31b-it:free",
|
|
40
|
-
"google/gemma-4-26b-a4b-it:free",
|
|
41
|
-
"nvidia/nemotron-nano-12b-v2-vl:free",
|
|
42
|
-
]
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
def get_free_models_fallback_chain() -> List[str]:
|
|
46
|
-
"""Returns prioritized list of free models starting with 'openrouter/free'."""
|
|
47
|
-
chain = [PRIMARY_HELPER_MODEL]
|
|
48
|
-
seen = {PRIMARY_HELPER_MODEL}
|
|
49
|
-
|
|
50
|
-
# Add dynamic live OpenRouter free models if available
|
|
51
|
-
try:
|
|
52
|
-
from utim_cli.tui.model_dialog import fetch_active_openrouter_free_models
|
|
53
|
-
live_free = fetch_active_openrouter_free_models()
|
|
54
|
-
if live_free:
|
|
55
|
-
for m in live_free:
|
|
56
|
-
if m not in seen:
|
|
57
|
-
chain.append(m)
|
|
58
|
-
seen.add(m)
|
|
59
|
-
except Exception:
|
|
60
|
-
pass
|
|
28
|
+
from utim_cli.client_utils import get_free_models_fallback_chain
|
|
61
29
|
|
|
62
|
-
|
|
63
|
-
for m in DEFAULT_FREE_FALLBACKS:
|
|
64
|
-
if m not in seen:
|
|
65
|
-
chain.append(m)
|
|
66
|
-
seen.add(m)
|
|
30
|
+
PRIMARY_HELPER_MODEL = "openrouter/free"
|
|
67
31
|
|
|
68
|
-
return chain
|
|
69
32
|
|
|
70
33
|
|
|
71
34
|
HELPER_SYSTEM_PROMPT = """You are the UTIM Sidecar Assistant, an embedded technical companion running alongside the active UTIM CLI session.
|
|
@@ -119,22 +82,12 @@ WEB_SEARCH_TOOL_SCHEMA = {
|
|
|
119
82
|
|
|
120
83
|
def execute_sidecar_websearch(query: str) -> str:
|
|
121
84
|
"""
|
|
122
|
-
Executes a
|
|
123
|
-
Returns direct snippets, URLs, and summaries without spawning subagents.
|
|
85
|
+
Executes a web search using DuckDuckGo search and returns structured markdown research findings from .utim_tmp.
|
|
124
86
|
"""
|
|
125
87
|
try:
|
|
126
|
-
from utim_cli.
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
config.set("subagent_model_web_search", "__non_agent__")
|
|
130
|
-
from utim_cli.tools import web_search
|
|
131
|
-
res = web_search(prompt=query, level="low")
|
|
132
|
-
return res or "No web results found."
|
|
133
|
-
finally:
|
|
134
|
-
if orig is not None:
|
|
135
|
-
config.set("subagent_model_web_search", orig)
|
|
136
|
-
else:
|
|
137
|
-
config.pop("subagent_model_web_search", None)
|
|
88
|
+
from utim_cli.tools import web_search
|
|
89
|
+
res = web_search(prompt=query, level="low")
|
|
90
|
+
return res or "No web results found."
|
|
138
91
|
except Exception as e:
|
|
139
92
|
return f"Web search error: {str(e)}"
|
|
140
93
|
|
|
@@ -427,7 +380,7 @@ def execute_ask_helper(
|
|
|
427
380
|
"max_tokens": 1000,
|
|
428
381
|
},
|
|
429
382
|
stream=False,
|
|
430
|
-
timeout=
|
|
383
|
+
timeout=(15, 300),
|
|
431
384
|
is_reflection=True,
|
|
432
385
|
)
|
|
433
386
|
if resp.status_code == 429:
|
|
@@ -62,11 +62,10 @@ _VISION_MODELS: List[str] = [
|
|
|
62
62
|
"openrouter/free"
|
|
63
63
|
]
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
]
|
|
65
|
+
from utim_cli.client_utils import get_free_models_fallback_chain
|
|
66
|
+
|
|
67
|
+
_CODE_MODELS: List[str] = get_free_models_fallback_chain()
|
|
68
|
+
|
|
70
69
|
|
|
71
70
|
|
|
72
71
|
|
|
@@ -57,12 +57,15 @@ def _sync_global_experience(api_key: str, current_folder: str):
|
|
|
57
57
|
Since .utim is now global (~/.utim), there is no need to copy experience files
|
|
58
58
|
between project folders — they are always available from the shared location.
|
|
59
59
|
"""
|
|
60
|
+
if not api_key or str(api_key).startswith(("sk-or-", "sk-ant-", "sk-proj-", "Bearer ")):
|
|
61
|
+
return
|
|
62
|
+
|
|
60
63
|
import requests
|
|
61
64
|
from utim_cli.auth import SERVER_URL
|
|
62
65
|
|
|
63
66
|
try:
|
|
64
67
|
requests.post(
|
|
65
|
-
f"{SERVER_URL}/auth/last-folder",
|
|
68
|
+
f"{SERVER_URL}/api/auth/last-folder",
|
|
66
69
|
headers={"X-API-Key": api_key},
|
|
67
70
|
json={"folder_path": current_folder},
|
|
68
71
|
timeout=5
|
|
@@ -2,8 +2,10 @@ from __future__ import annotations
|
|
|
2
2
|
import os
|
|
3
3
|
import json
|
|
4
4
|
import math
|
|
5
|
+
import threading
|
|
5
6
|
import requests
|
|
6
7
|
from utim_cli.config import config
|
|
8
|
+
from utim_cli.constants import DEFAULT_MODEL
|
|
7
9
|
|
|
8
10
|
class WrappedResponse:
|
|
9
11
|
"""A compatibility wrapper that mimics requests.Response for client callers."""
|
|
@@ -105,36 +107,152 @@ def get_server_model_catalog(timeout: float = 5.0) -> dict | None:
|
|
|
105
107
|
return _catalog_cache
|
|
106
108
|
except Exception:
|
|
107
109
|
pass
|
|
108
|
-
|
|
110
|
+
_FREE_MODELS_CACHE = []
|
|
111
|
+
_LAST_FREE_MODELS_FETCH = 0.0
|
|
112
|
+
_FREE_MODELS_LOCK = threading.Lock() # single-flight guard for chain building
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def get_free_models_fallback_chain() -> list[str]:
|
|
116
|
+
"""Returns prioritized fallback chain of free models for background processes,
|
|
117
|
+
starting with 'openrouter/free' as the primary model, followed by all currently
|
|
118
|
+
available free models from the server DB model_registry and resilient live backups.
|
|
119
|
+
"""
|
|
120
|
+
global _FREE_MODELS_CACHE, _LAST_FREE_MODELS_FETCH
|
|
121
|
+
import time
|
|
122
|
+
now = time.time()
|
|
123
|
+
if _FREE_MODELS_CACHE and (now - _LAST_FREE_MODELS_FETCH) < 300:
|
|
124
|
+
return list(_FREE_MODELS_CACHE)
|
|
125
|
+
# Single-flight (stampede protection): only one thread builds the chain at a
|
|
126
|
+
# time; concurrent callers receive the current cache instead of duplicating
|
|
127
|
+
# the DB / catalog / OpenRouter lookups.
|
|
128
|
+
if not _FREE_MODELS_LOCK.acquire(blocking=False):
|
|
129
|
+
return list(_FREE_MODELS_CACHE)
|
|
130
|
+
try:
|
|
131
|
+
# Double-check after acquiring — another thread may have populated it.
|
|
132
|
+
now = time.time()
|
|
133
|
+
if _FREE_MODELS_CACHE and (now - _LAST_FREE_MODELS_FETCH) < 300:
|
|
134
|
+
return list(_FREE_MODELS_CACHE)
|
|
135
|
+
return _build_free_models_fallback_chain(now)
|
|
136
|
+
finally:
|
|
137
|
+
_FREE_MODELS_LOCK.release()
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _build_free_models_fallback_chain(now: float) -> list[str]:
|
|
141
|
+
"""Builds the fallback chain dynamically from the server model_registry DB with openrouter/free first."""
|
|
142
|
+
global _FREE_MODELS_CACHE, _LAST_FREE_MODELS_FETCH
|
|
143
|
+
chain = ["openrouter/free"]
|
|
144
|
+
seen = {"openrouter/free"}
|
|
145
|
+
|
|
146
|
+
INVALID_DUMMY_IDS = {
|
|
147
|
+
"free-model:free",
|
|
148
|
+
"inclusionai/ling-2.6-flash:free",
|
|
149
|
+
"muses/muse-spark-1.1:free",
|
|
150
|
+
"z-ai/glm-5.2:free",
|
|
151
|
+
"inclusionai/ling-3.0-flash:free",
|
|
152
|
+
"poolside/laguna-s-2.1:free",
|
|
153
|
+
"poolside/laguna-xs-2.1:free"
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
# 1. Query server DB model_registry table for all active free models
|
|
157
|
+
try:
|
|
158
|
+
from utim_cli.server.db import SessionLocal, ModelDB
|
|
159
|
+
db = SessionLocal()
|
|
160
|
+
try:
|
|
161
|
+
free_rows = db.query(ModelDB).filter(
|
|
162
|
+
ModelDB.is_active == True,
|
|
163
|
+
(ModelDB.is_free == True) | (ModelDB.cost_input_per_1m == 0.0) | (ModelDB.model_id.like("%:free"))
|
|
164
|
+
).order_by(ModelDB.model_id.asc()).all()
|
|
165
|
+
for r in free_rows:
|
|
166
|
+
mid = r.model_id
|
|
167
|
+
if mid and mid not in seen and mid not in INVALID_DUMMY_IDS:
|
|
168
|
+
chain.append(mid)
|
|
169
|
+
seen.add(mid)
|
|
170
|
+
finally:
|
|
171
|
+
db.close()
|
|
172
|
+
except Exception:
|
|
173
|
+
pass
|
|
174
|
+
|
|
175
|
+
# 2. Query server /models/catalog endpoint
|
|
176
|
+
try:
|
|
177
|
+
catalog = fetch_remote_models_catalog(timeout=3.0)
|
|
178
|
+
if catalog and isinstance(catalog, dict):
|
|
179
|
+
for grp_key in ("all_text", "main_agent", "plan_project", "analyze_image"):
|
|
180
|
+
for m in catalog.get(grp_key, []):
|
|
181
|
+
mid = m.get("id") or m.get("model_id")
|
|
182
|
+
if mid and mid not in INVALID_DUMMY_IDS and (m.get("is_free") or mid.endswith(":free") or (m.get("cost_input_per_1k") == 0 and m.get("cost_output_per_1k") == 0)):
|
|
183
|
+
if mid not in seen:
|
|
184
|
+
chain.append(mid)
|
|
185
|
+
seen.add(mid)
|
|
186
|
+
except Exception:
|
|
187
|
+
pass
|
|
188
|
+
|
|
189
|
+
# 3. Query dynamic OpenRouter live free models
|
|
190
|
+
try:
|
|
191
|
+
from utim_cli.tui.model_dialog import fetch_active_openrouter_free_models
|
|
192
|
+
live_free = fetch_active_openrouter_free_models()
|
|
193
|
+
if live_free:
|
|
194
|
+
for mid in live_free:
|
|
195
|
+
if mid and mid not in seen and mid not in INVALID_DUMMY_IDS:
|
|
196
|
+
chain.append(mid)
|
|
197
|
+
seen.add(mid)
|
|
198
|
+
except Exception:
|
|
199
|
+
pass
|
|
200
|
+
|
|
201
|
+
# 4. Standard resilient default free fallbacks
|
|
202
|
+
default_fallbacks = [
|
|
203
|
+
"google/gemini-2.5-flash:free",
|
|
204
|
+
"google/gemini-2.0-flash-exp:free",
|
|
205
|
+
"meta-llama/llama-3.3-70b-instruct:free",
|
|
206
|
+
"deepseek/deepseek-r1:free",
|
|
207
|
+
"qwen/qwen-2.5-coder-32b-instruct:free",
|
|
208
|
+
"mistralai/mistral-7b-instruct:free",
|
|
209
|
+
"openai/gpt-oss-20b:free",
|
|
210
|
+
"cohere/north-mini-code:free",
|
|
211
|
+
]
|
|
212
|
+
for mid in default_fallbacks:
|
|
213
|
+
if mid not in seen and mid not in INVALID_DUMMY_IDS:
|
|
214
|
+
chain.append(mid)
|
|
215
|
+
seen.add(mid)
|
|
216
|
+
|
|
217
|
+
_FREE_MODELS_CACHE = chain
|
|
218
|
+
_LAST_FREE_MODELS_FETCH = now
|
|
219
|
+
return list(chain)
|
|
109
220
|
|
|
110
221
|
|
|
111
222
|
def proxy_openrouter_request(json_data: dict, stream: bool = False, timeout=None, is_reflection: bool = False) -> WrappedResponse:
|
|
112
223
|
"""Routes LLM request to UTIM server /completions, or falls back to direct OpenRouter."""
|
|
113
224
|
if timeout is None:
|
|
114
225
|
timeout = (15, 900)
|
|
226
|
+
elif isinstance(timeout, (int, float)):
|
|
227
|
+
timeout = (min(int(timeout), 15), max(int(timeout), 300))
|
|
228
|
+
elif isinstance(timeout, (list, tuple)) and len(timeout) == 2:
|
|
229
|
+
timeout = (min(int(timeout[0]), 15), max(int(timeout[1]), 300))
|
|
115
230
|
# Scrub payload for NaN/Infinity/non-JSON types BEFORE it leaves the
|
|
116
231
|
# machine. Providers like Parasail do a strict parse and reject the whole
|
|
117
232
|
# request (400 'Expecting value') if even one value is malformed.
|
|
118
233
|
json_data = _sanitize_json(json_data)
|
|
119
|
-
#
|
|
234
|
+
# Load local .env without overwriting already configured environment or auth variables
|
|
120
235
|
_cwd_env = os.path.join(os.getcwd(), ".env")
|
|
121
236
|
if os.path.isfile(_cwd_env):
|
|
122
237
|
try:
|
|
123
238
|
from dotenv import load_dotenv
|
|
124
|
-
load_dotenv(_cwd_env, override=
|
|
239
|
+
load_dotenv(_cwd_env, override=False)
|
|
125
240
|
except Exception:
|
|
126
241
|
pass
|
|
127
242
|
|
|
128
|
-
api_key = config.get("api_key")
|
|
243
|
+
api_key = config.get("api_key") or os.getenv("UTIM_API_KEY")
|
|
129
244
|
server_url = get_server_url()
|
|
245
|
+
main_model_source = config.get("main_model_source")
|
|
130
246
|
llm_key = os.getenv("OPENROUTER_API_KEY")
|
|
131
247
|
|
|
248
|
+
# If the user is logged into UTIM and hasn't explicitly selected direct openrouter source,
|
|
249
|
+
# route via UTIM Server FIRST to prevent ambient system environment variables from hijacking routing.
|
|
250
|
+
is_utim_key = bool(api_key) and not str(api_key).startswith(("sk-or-", "sk-ant-", "sk-proj-", "Bearer "))
|
|
251
|
+
use_utim_server = is_utim_key and main_model_source != "openrouter"
|
|
132
252
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
# analytics. The user-agent and install-id are added by the global
|
|
137
|
-
# session patcher in config.py.
|
|
253
|
+
def _try_direct_openrouter():
|
|
254
|
+
if not llm_key:
|
|
255
|
+
return None
|
|
138
256
|
headers = {
|
|
139
257
|
"Authorization": f"Bearer {llm_key}",
|
|
140
258
|
"Content-Type": "application/json",
|
|
@@ -142,20 +260,26 @@ def proxy_openrouter_request(json_data: dict, stream: bool = False, timeout=None
|
|
|
142
260
|
"X-Title": "UTIM CLI Agent",
|
|
143
261
|
"User-Agent": "UTIM-CLI/2.0 (+https://utim.dev)",
|
|
144
262
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
263
|
+
try:
|
|
264
|
+
return requests.post(
|
|
265
|
+
"https://openrouter.ai/api/v1/chat/completions",
|
|
266
|
+
json=json_data,
|
|
267
|
+
headers=headers,
|
|
268
|
+
stream=stream or json_data.get("stream"),
|
|
269
|
+
timeout=timeout,
|
|
270
|
+
verify=config.verify_ssl,
|
|
271
|
+
)
|
|
272
|
+
except Exception:
|
|
273
|
+
return None
|
|
274
|
+
|
|
275
|
+
if not use_utim_server and llm_key:
|
|
276
|
+
direct_resp = _try_direct_openrouter()
|
|
277
|
+
if direct_resp is not None:
|
|
278
|
+
return direct_resp
|
|
153
279
|
|
|
154
280
|
if api_key:
|
|
155
|
-
# Path via UTIM server
|
|
156
|
-
|
|
157
|
-
# is on) verify the request signature. The session-level UA patch in
|
|
158
|
-
# config.py adds the User-Agent too.
|
|
281
|
+
# Path via UTIM server with CLI identity headers
|
|
282
|
+
|
|
159
283
|
try:
|
|
160
284
|
from utim_cli._version import VERSION as _CLI_VERSION
|
|
161
285
|
except Exception:
|
|
@@ -223,7 +347,7 @@ def proxy_openrouter_request(json_data: dict, stream: bool = False, timeout=None
|
|
|
223
347
|
|
|
224
348
|
if stream or json_data.get("stream"):
|
|
225
349
|
def line_generator():
|
|
226
|
-
for line in resp.iter_lines(decode_unicode=True):
|
|
350
|
+
for line in resp.iter_lines(chunk_size=1, decode_unicode=True):
|
|
227
351
|
if line:
|
|
228
352
|
try:
|
|
229
353
|
data = json.loads(line)
|
|
@@ -234,6 +358,8 @@ def proxy_openrouter_request(json_data: dict, stream: bool = False, timeout=None
|
|
|
234
358
|
}]
|
|
235
359
|
}
|
|
236
360
|
yield "data: " + json.dumps(chunk)
|
|
361
|
+
elif data.get("type") == "ping":
|
|
362
|
+
continue
|
|
237
363
|
elif data.get("type") == "done":
|
|
238
364
|
if data.get("error"):
|
|
239
365
|
yield "data: " + json.dumps({"error": {"message": data["error"]}})
|
|
@@ -242,7 +368,7 @@ def proxy_openrouter_request(json_data: dict, stream: bool = False, timeout=None
|
|
|
242
368
|
if usage:
|
|
243
369
|
in_tokens = usage.get("input_tokens", 0)
|
|
244
370
|
out_tokens = usage.get("output_tokens", 0)
|
|
245
|
-
model_id = json_data.get("model") or
|
|
371
|
+
model_id = json_data.get("model") or DEFAULT_MODEL
|
|
246
372
|
from utim_cli.server.models import estimate_cost
|
|
247
373
|
cost = estimate_cost(model_id, in_tokens, out_tokens)
|
|
248
374
|
update_session_usage("tools", in_tokens, out_tokens, cost)
|
|
@@ -266,12 +392,14 @@ def proxy_openrouter_request(json_data: dict, stream: bool = False, timeout=None
|
|
|
266
392
|
in_tokens = 0
|
|
267
393
|
out_tokens = 0
|
|
268
394
|
server_error = None
|
|
269
|
-
for line in resp.iter_lines(decode_unicode=True):
|
|
395
|
+
for line in resp.iter_lines(chunk_size=1, decode_unicode=True):
|
|
270
396
|
if line:
|
|
271
397
|
try:
|
|
272
398
|
data = json.loads(line)
|
|
273
399
|
if data.get("type") == "content_delta" and data.get("text"):
|
|
274
400
|
content += data["text"]
|
|
401
|
+
elif data.get("type") == "ping":
|
|
402
|
+
continue
|
|
275
403
|
elif data.get("type") == "done":
|
|
276
404
|
if data.get("error"):
|
|
277
405
|
server_error = data["error"]
|
|
@@ -287,7 +415,7 @@ def proxy_openrouter_request(json_data: dict, stream: bool = False, timeout=None
|
|
|
287
415
|
if server_error:
|
|
288
416
|
raise RuntimeError(server_error)
|
|
289
417
|
if in_tokens > 0 or out_tokens > 0:
|
|
290
|
-
model_id = json_data.get("model") or
|
|
418
|
+
model_id = json_data.get("model") or DEFAULT_MODEL
|
|
291
419
|
from utim_cli.server.models import estimate_cost
|
|
292
420
|
cost = estimate_cost(model_id, in_tokens, out_tokens)
|
|
293
421
|
update_session_usage("tools", in_tokens, out_tokens, cost)
|
|
@@ -340,7 +340,9 @@ class Config:
|
|
|
340
340
|
val = self._data.get(key, default)
|
|
341
341
|
if val is None:
|
|
342
342
|
if key == "api_key":
|
|
343
|
-
return os.getenv("UTIM_API_KEY")
|
|
343
|
+
return os.getenv("UTIM_API_KEY")
|
|
344
|
+
elif key == "openrouter_api_key":
|
|
345
|
+
return os.getenv("OPENROUTER_API_KEY")
|
|
344
346
|
elif key == "email":
|
|
345
347
|
return os.getenv("UTIM_EMAIL")
|
|
346
348
|
return val
|
|
@@ -492,6 +494,7 @@ class Config:
|
|
|
492
494
|
if models:
|
|
493
495
|
return [m.strip() for m in models.split(",")]
|
|
494
496
|
return [
|
|
497
|
+
"poolside/laguna-s-2.1:free",
|
|
495
498
|
"cohere/north-mini-code:free",
|
|
496
499
|
"openrouter/free",
|
|
497
500
|
"meta-llama/llama-3.3-70b-instruct:free",
|