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.
Files changed (145) hide show
  1. {utim_cli-2.3.2 → utim_cli-2.3.4}/CHANGELOG.md +12 -0
  2. {utim_cli-2.3.2/utim_cli.egg-info → utim_cli-2.3.4}/PKG-INFO +1 -1
  3. {utim_cli-2.3.2 → utim_cli-2.3.4}/pyproject.toml +1 -1
  4. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/__init__.py +1 -1
  5. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/_version.py +1 -1
  6. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/ask_helper.py +7 -54
  7. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/blender_agent.py +4 -5
  8. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/bootstrap.py +4 -1
  9. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/client_utils.py +153 -25
  10. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/config.py +4 -1
  11. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/constants.py +1 -1
  12. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/context_pruner.py +277 -69
  13. utim_cli-2.3.4/utim_cli/orchestrator.py +1920 -0
  14. utim_cli-2.3.4/utim_cli/orchestrator_helpers.py +1266 -0
  15. utim_cli-2.3.4/utim_cli/orchestrator_llm_transport.py +1266 -0
  16. utim_cli-2.3.4/utim_cli/orchestrator_tool_exec.py +1534 -0
  17. utim_cli-2.3.4/utim_cli/orchestrator_turn_history.py +1359 -0
  18. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/reflection.py +54 -60
  19. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/db.py +4 -0
  20. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/models.py +11 -1
  21. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/pricing_updater.py +28 -74
  22. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/router.py +12 -18
  23. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/routes/admin_db_routes.py +21 -14
  24. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/routes/auth_routes.py +2 -0
  25. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/routes/completion_routes.py +94 -39
  26. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/task_dispatcher.py +80 -37
  27. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/tools.py +228 -198
  28. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/tui/model_dialog.py +69 -28
  29. utim_cli-2.3.4/utim_cli/tui/tasks_dialog.py +188 -0
  30. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/utim.py +141 -107
  31. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/wheel.py +3 -8
  32. {utim_cli-2.3.2 → utim_cli-2.3.4/utim_cli.egg-info}/PKG-INFO +1 -1
  33. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli.egg-info/SOURCES.txt +5 -0
  34. utim_cli-2.3.2/utim_cli/orchestrator.py +0 -6814
  35. {utim_cli-2.3.2 → utim_cli-2.3.4}/LICENSE +0 -0
  36. {utim_cli-2.3.2 → utim_cli-2.3.4}/MANIFEST.in +0 -0
  37. {utim_cli-2.3.2 → utim_cli-2.3.4}/README.md +0 -0
  38. {utim_cli-2.3.2 → utim_cli-2.3.4}/setup.cfg +0 -0
  39. {utim_cli-2.3.2 → utim_cli-2.3.4}/setup.py +0 -0
  40. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim/__init__.py +0 -0
  41. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/agent.py +0 -0
  42. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/auth.py +0 -0
  43. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/backup.py +0 -0
  44. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/billing.py +0 -0
  45. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/brain.py +0 -0
  46. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/doctor.py +0 -0
  47. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/harbor.py +0 -0
  48. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/knowledge_graph.py +0 -0
  49. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/local_db.py +0 -0
  50. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/logger.py +0 -0
  51. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/mcp_clean_wrapper.py +0 -0
  52. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/mcp_client.py +0 -0
  53. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/mcp_registry.json +0 -0
  54. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/models.txt +0 -0
  55. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/plugins/__init__.py +0 -0
  56. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/plugins/manager.py +0 -0
  57. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/plugins/registry.py +0 -0
  58. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/plugins/schema.py +0 -0
  59. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/report.py +0 -0
  60. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/scrapy_search.py +0 -0
  61. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/sdk/__init__.py +0 -0
  62. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/sdk/config.py +0 -0
  63. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/sdk/context.py +0 -0
  64. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/sdk/events.py +0 -0
  65. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/sdk/handlers.py +0 -0
  66. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/sdk/session.py +0 -0
  67. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/sdk/sidecar.py +0 -0
  68. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/__init__.py +0 -0
  69. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/admin_auth.py +0 -0
  70. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/attribution.py +0 -0
  71. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/audit_log.py +0 -0
  72. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/auth.py +0 -0
  73. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/batch_processor.py +0 -0
  74. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/captcha.py +0 -0
  75. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/cli_auth.py +0 -0
  76. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/docs_md/SECRET_PROVISIONING.md +0 -0
  77. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/docs_md/about.md +0 -0
  78. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/docs_md/changelog.md +0 -0
  79. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/docs_md/docs.md +0 -0
  80. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/docs_md/features.md +0 -0
  81. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/docs_md/license.md +0 -0
  82. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/docs_md/pricing.md +0 -0
  83. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/docs_md/privacy.md +0 -0
  84. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/docs_md/refund.md +0 -0
  85. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/docs_md/support.md +0 -0
  86. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/docs_md/terms.md +0 -0
  87. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/email_utils.py +0 -0
  88. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/exchange_rate.py +0 -0
  89. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/firebase.py +0 -0
  90. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/fix_duplicate_users.py +0 -0
  91. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/history.py +0 -0
  92. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/local_llm.py +0 -0
  93. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/logging_config.py +0 -0
  94. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/micro_batcher.py +0 -0
  95. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/model_agent.py +0 -0
  96. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/provision_build.py +0 -0
  97. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/rate_limit.py +0 -0
  98. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/rewards_engine.py +0 -0
  99. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/routes/__init__.py +0 -0
  100. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/routes/billing_routes.py +0 -0
  101. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/routes/credit_routes.py +0 -0
  102. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/routes/feedback_routes.py +0 -0
  103. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/routes/marketplace_routes.py +0 -0
  104. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/routes/quota_routes.py +0 -0
  105. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/routes/quota_share_routes.py +0 -0
  106. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/routes/referral_routes.py +0 -0
  107. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/routes/rewards_routes.py +0 -0
  108. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/routes/security_routes.py +0 -0
  109. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/routes/session_routes.py +0 -0
  110. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/routes/share_routes.py +0 -0
  111. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/server.py +0 -0
  112. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/server/storage_nodes.py +0 -0
  113. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/share.py +0 -0
  114. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/share_tui.py +0 -0
  115. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/situational_scoring.py +0 -0
  116. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/state.py +0 -0
  117. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/subagent_manager.py +0 -0
  118. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/tui/__init__.py +0 -0
  119. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/tui/feedback_dialog.py +0 -0
  120. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/tui/history_dialog.py +0 -0
  121. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/tui/marketplace_app_state.py +0 -0
  122. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/tui/marketplace_dialog.py +0 -0
  123. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/tui/marketplace_layout_engine.py +0 -0
  124. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/tui/mcp_dialog.py +0 -0
  125. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/tui/miniagents_dialog.py +0 -0
  126. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/tui/plugins_dialog.py +0 -0
  127. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/tui/publish_dialog.py +0 -0
  128. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/tui/quota_dialog.py +0 -0
  129. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/tui/quota_redeem_dialog.py +0 -0
  130. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/tui/quota_share_dialog.py +0 -0
  131. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/tui/resume_dialog.py +0 -0
  132. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/tui/rewards_tui.py +0 -0
  133. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/tui/skills_dialog.py +0 -0
  134. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/tui/subagents_dialog.py +0 -0
  135. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/tui/thinking_display.py +0 -0
  136. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/tui/tools_dialog.py +0 -0
  137. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/tui/update_dialog.py +0 -0
  138. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/utilities.py +0 -0
  139. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/utimmodel.txt +0 -0
  140. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/vector_memory.py +0 -0
  141. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli/workspace.py +0 -0
  142. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli.egg-info/dependency_links.txt +0 -0
  143. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli.egg-info/entry_points.txt +0 -0
  144. {utim_cli-2.3.2 → utim_cli-2.3.4}/utim_cli.egg-info/requires.txt +0 -0
  145. {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.2
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "utim-cli"
7
- version = "2.3.2"
7
+ version = "2.3.4"
8
8
 
9
9
  description = "UTIM – Universal Terminal Intelligence Manager. A powerful agentic AI coding assistant for your terminal."
10
10
  readme = "README.md"
@@ -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__ = "2.3.2"
8
+ __version__ = "2.3.4"
9
9
 
10
10
 
11
11
  import builtins, os, pathlib, urllib.parse, re
@@ -11,6 +11,6 @@
11
11
  # from here so every surface stays in sync automatically.
12
12
  # ─────────────────────────────────────────────────────────────────────────────
13
13
 
14
- VERSION = "2.3.2"
14
+ VERSION = "2.3.4"
15
15
 
16
16
 
@@ -25,47 +25,10 @@ from rich import box
25
25
 
26
26
  # ── Primary & Fallback Free Models ───────────────────────────────────────────
27
27
 
28
- PRIMARY_HELPER_MODEL = "openrouter/free"
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
- # Add default free fallbacks
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 fast non-agent web search using search API proxies and fallback search engines.
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.config import config
127
- orig = config.get("subagent_model_web_search")
128
- try:
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=30,
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
- _CODE_MODELS: List[str] = [
66
- DEFAULT_MODEL,
67
- "cohere/north-mini-code:free",
68
- "openrouter/free"
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
- return None
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
- # Ensure environment variables are loaded (orchestrator handles this, but tools.py is independent)
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=True)
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
- if llm_key:
134
- # Direct path to OpenRouter — must always carry canonical attribution
135
- # headers so OpenRouter shows "UTIM CLI Agent" (NOT "unknown") in its
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
- return requests.post(
146
- "https://openrouter.ai/api/v1/chat/completions",
147
- json=json_data,
148
- headers=headers,
149
- stream=stream or json_data.get("stream"),
150
- timeout=timeout,
151
- verify=config.verify_ssl,
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. We attach CLI identity headers (version,
156
- # install-id) so the server can correlate calls and (when enforcement
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 "cohere/north-mini-code:free"
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 "cohere/north-mini-code:free"
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") or os.getenv("OPENROUTER_API_KEY") or os.getenv("OPENAI_API_KEY") or os.getenv("ANTHROPIC_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",
@@ -1,6 +1,6 @@
1
1
  import os
2
2
 
3
- DEFAULT_MODEL = "cohere/north-mini-code:free"
3
+ DEFAULT_MODEL = "poolside/laguna-s-2.1:free"
4
4
  SERVER_URL = os.getenv("UTIM_SERVER_URL", "https://api.utim.dev")
5
5
  OPENROUTER_BASE_URL = os.getenv("OPENROUTER_BASE_URL", "https://openrouter.ai/api/v1")
6
6