utim-cli 2.1.1__tar.gz → 2.1.2__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 (117) hide show
  1. {utim_cli-2.1.1 → utim_cli-2.1.2}/CHANGELOG.md +9 -0
  2. utim_cli-2.1.2/LICENSE +26 -0
  3. {utim_cli-2.1.1/utim_cli.egg-info → utim_cli-2.1.2}/PKG-INFO +2 -1
  4. {utim_cli-2.1.1 → utim_cli-2.1.2}/pyproject.toml +4 -1
  5. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/__init__.py +1 -1
  6. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/_version.py +1 -1
  7. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/blender_agent.py +2 -1
  8. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/client_utils.py +20 -3
  9. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/config.py +4 -2
  10. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/constants.py +2 -0
  11. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/context_pruner.py +7 -3
  12. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/orchestrator.py +9 -8
  13. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/reflection.py +2 -1
  14. utim_cli-2.1.2/utim_cli/server/batch_processor.py +119 -0
  15. utim_cli-2.1.2/utim_cli/server/micro_batcher.py +95 -0
  16. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/models.py +6 -10
  17. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/router.py +5 -0
  18. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/routes/__init__.py +1 -0
  19. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/routes/completion_routes.py +67 -3
  20. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/server.py +4 -0
  21. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/situational_scoring.py +5 -2
  22. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/tools.py +83 -13
  23. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/tui/feedback_dialog.py +433 -419
  24. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/tui/model_dialog.py +11 -4
  25. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/utim.py +13 -4
  26. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/vector_memory.py +10 -1
  27. {utim_cli-2.1.1 → utim_cli-2.1.2/utim_cli.egg-info}/PKG-INFO +2 -1
  28. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli.egg-info/SOURCES.txt +2 -0
  29. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli.egg-info/entry_points.txt +1 -0
  30. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli.egg-info/requires.txt +1 -0
  31. utim_cli-2.1.1/LICENSE +0 -21
  32. {utim_cli-2.1.1 → utim_cli-2.1.2}/MANIFEST.in +0 -0
  33. {utim_cli-2.1.1 → utim_cli-2.1.2}/README.md +0 -0
  34. {utim_cli-2.1.1 → utim_cli-2.1.2}/setup.cfg +0 -0
  35. {utim_cli-2.1.1 → utim_cli-2.1.2}/setup.py +0 -0
  36. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/_check_obj.py +0 -0
  37. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/_find_mcp.py +0 -0
  38. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/_find_plan.py +0 -0
  39. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/_find_tool.py +0 -0
  40. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/_patch_multimodal.py +0 -0
  41. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/_test_regex.py +0 -0
  42. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/agent.py +0 -0
  43. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/auth.py +0 -0
  44. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/backup.py +0 -0
  45. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/billing.py +0 -0
  46. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/bootstrap.py +0 -0
  47. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/doctor.py +0 -0
  48. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/harbor.py +0 -0
  49. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/knowledge_graph.py +0 -0
  50. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/local_db.py +0 -0
  51. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/logger.py +0 -0
  52. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/mcp_clean_wrapper.py +0 -0
  53. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/mcp_client.py +0 -0
  54. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/mcp_registry.json +0 -0
  55. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/report.py +0 -0
  56. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/scrapy_search.py +0 -0
  57. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/__init__.py +0 -0
  58. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/attribution.py +0 -0
  59. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/audit_log.py +0 -0
  60. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/auth.py +0 -0
  61. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/captcha.py +0 -0
  62. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/cli_auth.py +0 -0
  63. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/db.py +0 -0
  64. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/docs_md/SECRET_PROVISIONING.md +0 -0
  65. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/docs_md/about.md +0 -0
  66. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/docs_md/changelog.md +0 -0
  67. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/docs_md/docs.md +0 -0
  68. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/docs_md/features.md +0 -0
  69. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/docs_md/license.md +0 -0
  70. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/docs_md/pricing.md +0 -0
  71. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/docs_md/privacy.md +0 -0
  72. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/docs_md/refund.md +0 -0
  73. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/docs_md/support.md +0 -0
  74. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/docs_md/terms.md +0 -0
  75. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/email_utils.py +0 -0
  76. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/exchange_rate.py +0 -0
  77. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/firebase.py +0 -0
  78. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/fix_duplicate_users.py +0 -0
  79. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/history.py +0 -0
  80. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/logging_config.py +0 -0
  81. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/pricing_updater.py +0 -0
  82. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/provision_build.py +0 -0
  83. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/rate_limit.py +0 -0
  84. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/routes/auth_routes.py +0 -0
  85. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/routes/credit_routes.py +0 -0
  86. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/routes/feedback_routes.py +0 -0
  87. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/routes/marketplace_routes.py +0 -0
  88. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/routes/quota_routes.py +0 -0
  89. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/routes/quota_share_routes.py +0 -0
  90. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/routes/referral_routes.py +0 -0
  91. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/routes/security_routes.py +0 -0
  92. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/routes/session_routes.py +0 -0
  93. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/routes/share_routes.py +0 -0
  94. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/storage_nodes.py +0 -0
  95. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/share.py +0 -0
  96. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/share_tui.py +0 -0
  97. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/state.py +0 -0
  98. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/tui/__init__.py +0 -0
  99. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/tui/history_dialog.py +0 -0
  100. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/tui/marketplace_app_state.py +0 -0
  101. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/tui/marketplace_dialog.py +0 -0
  102. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/tui/marketplace_layout_engine.py +0 -0
  103. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/tui/mcp_dialog.py +0 -0
  104. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/tui/miniagents_dialog.py +0 -0
  105. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/tui/publish_dialog.py +0 -0
  106. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/tui/quota_dialog.py +0 -0
  107. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/tui/quota_redeem_dialog.py +0 -0
  108. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/tui/quota_share_dialog.py +0 -0
  109. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/tui/resume_dialog.py +0 -0
  110. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/tui/skills_dialog.py +0 -0
  111. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/tui/subagents_dialog.py +0 -0
  112. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/tui/tools_dialog.py +0 -0
  113. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/tui/update_dialog.py +0 -0
  114. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/utilities.py +0 -0
  115. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/workspace.py +0 -0
  116. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli.egg-info/dependency_links.txt +0 -0
  117. {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli.egg-info/top_level.txt +0 -0
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.1.2] - 2026-08-10
4
+
5
+ ### 🛠️ Fixes
6
+ - **Fixed `grep_search` auto-regex promotion bug**: Queries containing regex metacharacters (`|`, `\`, `*`, etc.) were silently auto-promoted to regex mode even when `is_regex=False`, causing literal searches to fail with "No matches found." Now `is_regex=False` always means literal search, matching the documented "Literal-first semantics."
7
+ - **Made ripgrep an automatic dependency**: Added `ripgrep>=0.13.0` to core dependencies in `pyproject.toml` and `requirements.txt`. The `_grep_find_ripgrep()` function now falls back to the `ripgrep` PyPI package (which bundles a prebuilt binary) when ripgrep isn't found on the system PATH, ensuring fast searches work out of the box.
8
+ - **Increased context compression interval from 25 to 35 iterations**: Reduced compression frequency to conserve context resources and improve performance during long agent sessions.
9
+ - **Reduced batch poll interval from 5s to 0.3s**: Improved responsiveness of batch status checks in `BatchAPIProcessor.poll_and_retrieve_results()`.
10
+ - **Cleaned up network error messages in feedback dialog**: Stripped verbose `HTTPSConnectionPool` technical details from error messages, now showing clean messages like "Connection timed out. Please check your network and try again."
11
+
3
12
  ## [2.1.1] - 2026-08-06
4
13
 
5
14
  ### 🛠️ Fixes
utim_cli-2.1.2/LICENSE ADDED
@@ -0,0 +1,26 @@
1
+ UTIM PROPRIETARY SOFTWARE END USER LICENSE AGREEMENT (EULA)
2
+
3
+ Copyright (c) 2026 Emend AI. All Rights Reserved.
4
+
5
+ IMPORTANT: READ THIS LICENSE AGREEMENT CAREFULLY BEFORE DOWNLOADING, INSTALLING, OR USING THE UTIM CLI SOFTWARE.
6
+
7
+ 1. PROPRIETARY SOFTWARE
8
+ UTIM CLI ("Software") is proprietary commercial software developed and owned exclusively by Emend AI ("Licensor"). The Software is licensed, not sold, to you ("Licensee") under the terms and conditions of this Agreement.
9
+
10
+ 2. LICENSE GRANT
11
+ Subject to compliance with this Agreement and payment of applicable subscription or usage fees:
12
+ - Free Tier License: Licensor grants Licensee a limited, non-exclusive, non-transferable, revocable license to install and use the Software for personal, open-source learning, and non-commercial evaluation purposes.
13
+ - Commercial Tier License: Paid subscribers (Hobby, Pro, Max, Ultimate Tiers) or users with active Bring Your Own Key (BYOK) configurations are granted a non-exclusive, non-transferable license to use the Software for commercial software engineering, corporate repositories, and professional client work.
14
+
15
+ 3. RESTRICTIONS
16
+ Licensee shall NOT:
17
+ - Reverse engineer, decompile, disassemble, modify, or attempt to discover the source code or underlying client orchestrator mechanisms of compiled Software binaries.
18
+ - Circumvent rate limits, credit accounting controls, or 5-hour quota refill mechanisms.
19
+ - Sublicense, resell, rent, lease, or distribute the Software to third parties without prior written authorization.
20
+ - Create duplicate or automated accounts to harvest free promotional credit allocations.
21
+
22
+ 4. INTELLECTUAL PROPERTY
23
+ All title, ownership, brand trademarks, domain adaptation engines, AST parsing logic, and intellectual property rights in and to the Software remain the exclusive property of Emend AI. Licensee retains 100% full ownership of all user prompts submitted and code files generated in their local workspaces.
24
+
25
+ 5. NO WARRANTY & LIMITATION OF LIABILITY
26
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED. IN NO EVENT SHALL EMEND AI OR ITS SUPPLIERS BE LIABLE FOR ANY CLAIM, DIRECT, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES (INCLUDING DATA LOSS, REPOSITORY CORRUPTION, OR API BILLING COSTS) ARISING OUT OF THE USE OF OR INABILITY TO USE THE SOFTWARE.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: utim-cli
3
- Version: 2.1.1
3
+ Version: 2.1.2
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
@@ -31,6 +31,7 @@ Requires-Dist: rich>=13.0.0
31
31
  Requires-Dist: prompt_toolkit>=3.0.0
32
32
  Requires-Dist: nest-asyncio>=1.5.0
33
33
  Requires-Dist: sqlalchemy>=2.0.0
34
+ Requires-Dist: ripgrep>=0.13.0
34
35
  Provides-Extra: search
35
36
  Requires-Dist: scrapy>=2.11.0; extra == "search"
36
37
  Requires-Dist: scrapy-playwright>=0.0.34; extra == "search"
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "utim-cli"
7
- version = "2.1.1"
7
+ version = "2.1.2"
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"
@@ -33,6 +33,7 @@ dependencies = [
33
33
  "prompt_toolkit>=3.0.0",
34
34
  "nest-asyncio>=1.5.0",
35
35
  "sqlalchemy>=2.0.0",
36
+ "ripgrep>=0.13.0",
36
37
  ]
37
38
 
38
39
  [project.urls]
@@ -88,6 +89,8 @@ memory = [
88
89
 
89
90
  [project.scripts]
90
91
  utim = "utim_cli.utim:main_cli_entry"
92
+ utimlite = "utim_cli.utim:main_cli_entry"
93
+
91
94
 
92
95
  [tool.setuptools.packages.find]
93
96
  include = ["utim_cli*"]
@@ -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.1.1"
8
+ __version__ = "2.1.2"
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 = "2.1.1"
14
+ VERSION = "2.1.2"
@@ -63,12 +63,13 @@ _VISION_MODELS: List[str] = [
63
63
  ]
64
64
 
65
65
  _CODE_MODELS: List[str] = [
66
- "poolside/laguna-m.1:free",
67
66
  DEFAULT_MODEL,
67
+ "cohere/north-mini-code:free",
68
68
  "openrouter/free"
69
69
  ]
70
70
 
71
71
 
72
+
72
73
  def _get_vision_models() -> List[str]:
73
74
  """Return vision model list, with user override prepended if configured."""
74
75
  try:
@@ -128,6 +128,7 @@ def proxy_openrouter_request(json_data: dict, stream: bool = False, timeout=None
128
128
  server_url = get_server_url()
129
129
  llm_key = os.getenv("OPENROUTER_API_KEY")
130
130
 
131
+
131
132
  if llm_key:
132
133
  # Direct path to OpenRouter — must always carry canonical attribution
133
134
  # headers so OpenRouter shows "UTIM CLI Agent" (NOT "unknown") in its
@@ -199,9 +200,25 @@ def proxy_openrouter_request(json_data: dict, stream: bool = False, timeout=None
199
200
  if "reasoning" in json_data:
200
201
  payload["reasoning"] = json_data["reasoning"]
201
202
 
202
- # UTIM server completions is a streaming endpoint
203
- resp = requests.post(f"{server_url}/completions", json=payload, headers=headers, stream=True, timeout=timeout, verify=config.verify_ssl)
204
- resp.raise_for_status()
203
+ # UTIM server completions is a streaming endpoint with retry & connection protection
204
+ import time as _timemod
205
+ max_http_retries = 2
206
+ last_http_err = None
207
+ resp = None
208
+
209
+ for attempt in range(max_http_retries + 1):
210
+ try:
211
+ resp = requests.post(f"{server_url}/completions", json=payload, headers=headers, stream=True, timeout=timeout, verify=config.verify_ssl)
212
+ resp.raise_for_status()
213
+ last_http_err = None
214
+ break
215
+ except (requests.exceptions.ConnectionError, requests.exceptions.ChunkedEncodingError, requests.exceptions.Timeout) as _conn_err:
216
+ last_http_err = _conn_err
217
+ if attempt < max_http_retries:
218
+ _timemod.sleep(1.0 * (attempt + 1))
219
+ continue
220
+ else:
221
+ raise RuntimeError(f"Connection to UTIM server failed: {str(_conn_err)}")
205
222
 
206
223
  if stream or json_data.get("stream"):
207
224
  def line_generator():
@@ -492,10 +492,12 @@ class Config:
492
492
  if models:
493
493
  return [m.strip() for m in models.split(",")]
494
494
  return [
495
- "poolside/laguna-m.1:free",
496
- "openrouter/free"
495
+ "cohere/north-mini-code:free",
496
+ "openrouter/free",
497
+ "inclusionai/ling-3.0-flash:free",
497
498
  ]
498
499
 
500
+
499
501
  @property
500
502
  def keep_full_turns(self) -> int:
501
503
  # Number of recent turns to retain in memory
@@ -10,6 +10,8 @@ SUBAGENT_DEFAULTS = {
10
10
  "generate_image": "nvidia/nemotron-3-nano-30b-a3b:free",
11
11
  }
12
12
 
13
+
14
+
13
15
  import sys
14
16
  _IS_LEGACY_WIN = (sys.platform == "win32" and "WT_SESSION" not in os.environ)
15
17
  PROMPT_SYMBOL = ">" if _IS_LEGACY_WIN else "❯"
@@ -16,11 +16,14 @@ from utim_cli.constants import DEFAULT_MODEL
16
16
  # Fallback models for compression operations - ordered by reliability and quality.
17
17
  # "openrouter/free" is the first fallback tried after the primary model.
18
18
  COMPRESSION_FALLBACK_MODELS = [
19
- "openrouter/free",
20
- "poolside/laguna-xs-2.1:free",
21
19
  "cohere/north-mini-code:free",
20
+ "openrouter/free",
21
+ "inclusionai/ling-3.0-flash:free",
22
22
  ]
23
23
 
24
+
25
+
26
+
24
27
  # Track what content has already been summarized to prevent duplicate summarization
25
28
  _summarized_content_hashes: Set[str] = set()
26
29
 
@@ -340,9 +343,10 @@ def _call_compression_model_with_fallback(messages: List[Dict], llm_key: str, ma
340
343
  if content_hash in _summarized_content_hashes:
341
344
  return "[Content previously summarized - skipped]"
342
345
  _summarized_content_hashes.add(content_hash)
343
-
346
+
344
347
  # Prioritize primary model if provided, followed by fallback models
345
348
  models_to_try = list(COMPRESSION_FALLBACK_MODELS)
349
+
346
350
  if primary_model:
347
351
  if primary_model in models_to_try:
348
352
  models_to_try.remove(primary_model)
@@ -172,8 +172,8 @@ def _get_compression_threshold(model_id: str, context_window: int) -> int:
172
172
 
173
173
 
174
174
  def _get_compression_interval(context_window: int = 0) -> int:
175
- """Return the iteration gap for model context compression (every 25 iterations)."""
176
- return 25
175
+ """Return the iteration gap for model context compression (every 35 iterations)."""
176
+ return 35
177
177
 
178
178
 
179
179
  from utim_cli.config import get_utim_dir
@@ -3769,8 +3769,8 @@ class Orchestrator:
3769
3769
  return 65_000
3770
3770
 
3771
3771
  def _get_dynamic_interval(self) -> int:
3772
- """Get iteration context compression interval (every 25 iterations)."""
3773
- return 25
3772
+ """Get iteration context compression interval (every 35 iterations)."""
3773
+ return 35
3774
3774
 
3775
3775
 
3776
3776
  def _get_iteration_budget(self) -> int:
@@ -5182,9 +5182,10 @@ class Orchestrator:
5182
5182
  t_tool_start = time.time()
5183
5183
 
5184
5184
  # ── Iteration-based auto-compression ─────────────────────────────
5185
- # Compress every 45 iterations to keep context lean regardless of
5186
- # model context window size.
5187
- COMPRESS_INTERVAL = 45
5185
+ # Compress every N iterations (default 35) to keep context lean.
5186
+ # The interval is dynamically determined by _get_dynamic_interval(),
5187
+ # which returns 35 by default but can scale with the model's context window.
5188
+ COMPRESS_INTERVAL = self._get_dynamic_interval()
5188
5189
  compression_instruction = "" # kept for _compress_intra_turn API compat
5189
5190
 
5190
5191
  if iteration > 0 and iteration % COMPRESS_INTERVAL == 0:
@@ -5643,7 +5644,7 @@ class Orchestrator:
5643
5644
  if hasattr(self, "_tool_name_failure_counts"):
5644
5645
  self._tool_name_failure_counts.pop(func_name, None)
5645
5646
 
5646
- # Periodic iteration context compression (every 25 iterations)
5647
+ # Periodic iteration context compression (every 35 iterations)
5647
5648
  interval = getattr(self, "_compression_interval", None) or self._get_dynamic_interval()
5648
5649
  if turn_iteration > 0 and turn_iteration % interval == 0:
5649
5650
  try:
@@ -12,10 +12,11 @@ from utim_cli.constants import DEFAULT_MODEL
12
12
  # These are free-tier models optimized for structured JSON output tasks.
13
13
  # Primary is tried first; fallbacks are used on rate-limit or error.
14
14
  REFLECTION_MODELS = [
15
- "poolside/laguna-xs-2.1:free",
16
15
  "cohere/north-mini-code:free",
17
16
  "openrouter/free",
17
+ "inclusionai/ling-3.0-flash:free",
18
18
  ]
19
+
19
20
  REFLECTION_PRIMARY_MODEL = REFLECTION_MODELS[0]
20
21
  REFLECTION_MAX_TOKENS = 5000
21
22
 
@@ -0,0 +1,119 @@
1
+ """
2
+ Offline / Background Batch API Processor for OpenAI / DeepInfra / OpenRouter /v1/batches.
3
+
4
+ Handles:
5
+ 1. Packaging multiple JSON requests into a .jsonl batch file.
6
+ 2. Uploading the batch file to POST /v1/files.
7
+ 3. Creating a batch job via POST /v1/batches.
8
+ 4. Polling until completion and retrieving final output results.
9
+ """
10
+
11
+ import os
12
+ import json
13
+ import time
14
+ import requests
15
+ import logging
16
+ from typing import List, Dict, Any, Optional
17
+
18
+ logger = logging.getLogger("utim.batch_processor")
19
+
20
+ class BatchAPIProcessor:
21
+ """Manages true offline /v1/batches file jobs."""
22
+
23
+ def __init__(self, api_key: str, base_url: str = "https://api.deepinfra.com/v1/openai"):
24
+ self.api_key = api_key
25
+ self.base_url = base_url.rstrip("/")
26
+ self.headers = {
27
+ "Authorization": f"Bearer {api_key}",
28
+ "Content-Type": "application/json"
29
+ }
30
+
31
+ def create_batch_job(self, requests_list: List[Dict[str, Any]], model_id: str) -> Optional[Dict[str, Any]]:
32
+ """Package requests into a .jsonl batch payload and submit to POST /v1/batches."""
33
+ jsonl_lines = []
34
+ for idx, req_payload in enumerate(requests_list):
35
+ item = {
36
+ "custom_id": f"request-{idx}-{int(time.time())}",
37
+ "method": "POST",
38
+ "url": "/v1/chat/completions",
39
+ "body": {
40
+ "model": model_id,
41
+ "messages": req_payload.get("messages", []),
42
+ "max_tokens": req_payload.get("max_tokens", 1500),
43
+ "temperature": req_payload.get("temperature", 0.2)
44
+ }
45
+ }
46
+ jsonl_lines.append(json.dumps(item))
47
+
48
+ jsonl_content = "\n".join(jsonl_lines)
49
+ print(f"📦 [BATCH FILE API] Packaging {len(requests_list)} requests into .jsonl payload for model '{model_id}'...")
50
+
51
+ # Step 1: Upload file to /v1/files
52
+ try:
53
+ files_url = f"{self.base_url}/files"
54
+ file_response = requests.post(
55
+ files_url,
56
+ headers={"Authorization": f"Bearer {self.api_key}"},
57
+ files={"file": ("batch_input.jsonl", jsonl_content.encode("utf-8"), "application/jsonl")},
58
+ data={"purpose": "batch"},
59
+ timeout=30
60
+ )
61
+ if file_response.status_code != 200:
62
+ print(f"❌ [BATCH FILE API ERROR] Failed to upload batch file: {file_response.text}")
63
+ return None
64
+
65
+ file_id = file_response.json().get("id")
66
+ print(f"✓ [BATCH FILE UPLOADED] File ID: {file_id}")
67
+
68
+ # Step 2: Create batch job via /v1/batches
69
+ batches_url = f"{self.base_url}/batches"
70
+ batch_payload = {
71
+ "input_file_id": file_id,
72
+ "endpoint": "/v1/chat/completions",
73
+ "completion_window": "24h"
74
+ }
75
+ batch_resp = requests.post(batches_url, json=batch_payload, headers=self.headers, timeout=30)
76
+ if batch_resp.status_code == 200:
77
+ batch_job = batch_resp.json()
78
+ print(f"🚀 [BATCH JOB CREATED] Job ID: {batch_job.get('id')} | Status: {batch_job.get('status')}")
79
+ return batch_job
80
+ else:
81
+ print(f"❌ [BATCH JOB CREATED ERROR]: {batch_resp.text}")
82
+ return None
83
+
84
+ except Exception as exc:
85
+ print(f"❌ [BATCH PROCESSOR EXCEPTION]: {exc}")
86
+ return None
87
+
88
+ def poll_and_retrieve_results(self, batch_id: str, poll_interval: float = 0.3, timeout: int = 300) -> Optional[List[Dict[str, Any]]]:
89
+ """Poll batch job until status is completed and download output file."""
90
+ start_time = time.time()
91
+ batch_url = f"{self.base_url}/batches/{batch_id}"
92
+
93
+ while time.time() - start_time < timeout:
94
+ resp = requests.get(batch_url, headers=self.headers, timeout=15)
95
+ if resp.status_code == 200:
96
+ job_data = resp.json()
97
+ status = job_data.get("status")
98
+ print(f"⏳ [BATCH POLLING] Job ID '{batch_id}' | Status: {status}")
99
+
100
+ if status == "completed":
101
+ output_file_id = job_data.get("output_file_id")
102
+ if output_file_id:
103
+ file_content_url = f"{self.base_url}/files/{output_file_id}/content"
104
+ content_resp = requests.get(file_content_url, headers=self.headers, timeout=30)
105
+ if content_resp.status_code == 200:
106
+ results = []
107
+ for line in content_resp.text.strip().splitlines():
108
+ if line:
109
+ results.append(json.loads(line))
110
+ print(f"✓ [BATCH COMPLETED] Downloaded {len(results)} completed results!")
111
+ return results
112
+ elif status in ["failed", "cancelled", "expired"]:
113
+ print(f"❌ [BATCH JOB FAILED] Job ended with status: {status}")
114
+ return None
115
+
116
+ time.sleep(poll_interval)
117
+
118
+ print(f"⚠️ [BATCH TIMEOUT] Reached timeout of {timeout}s waiting for job '{batch_id}'")
119
+ return None
@@ -0,0 +1,95 @@
1
+ """
2
+ Model-Specific Micro-Batcher for UTIM Server.
3
+
4
+ Batches requests specifically grouped by (model_id, is_reflection).
5
+ Flushes when:
6
+ 1. Queue for specific model reaches max_batch_size (e.g. 4)
7
+ 2. Max wait timeout (e.g. 10.0s for testing) expires.
8
+ """
9
+
10
+ import asyncio
11
+ import time
12
+ import logging
13
+ from typing import Dict, Any, List, Optional, Tuple, Callable
14
+
15
+ logger = logging.getLogger("utim.micro_batcher")
16
+
17
+ class ModelSpecificMicroBatcher:
18
+ """Manages separate batch queues grouped by (model_id, is_reflection)."""
19
+
20
+ def __init__(self, max_batch_size: int = 4, max_wait_seconds: float = 10.0):
21
+ self.max_batch_size = max_batch_size
22
+ self.max_wait_seconds = max_wait_seconds
23
+ # Key: (model_id, is_reflection) -> List[(payload, future)]
24
+ self.queues: Dict[Tuple[str, bool], List[Tuple[Any, asyncio.Future]]] = {}
25
+ self.flush_tasks: Dict[Tuple[str, bool], asyncio.Task] = {}
26
+ self.lock = asyncio.Lock()
27
+
28
+ async def submit(self, model_id: str, is_reflection: bool, payload: Any, processor_func: Callable) -> Any:
29
+ loop = asyncio.get_running_loop()
30
+ future = loop.create_future()
31
+ queue_key = (model_id, is_reflection)
32
+
33
+ async with self.lock:
34
+ if queue_key not in self.queues:
35
+ self.queues[queue_key] = []
36
+
37
+ self.queues[queue_key].append((payload, future))
38
+ current_size = len(self.queues[queue_key])
39
+
40
+ print(f"📦 [MICRO-BATCH QUEUE] Model: '{model_id}' | Queue Size: {current_size}/{self.max_batch_size}")
41
+
42
+ if current_size >= self.max_batch_size:
43
+ # Trigger A: Batch size limit reached!
44
+ print(f"⚡ [MICRO-BATCH TRIGGER A] Batch size {self.max_batch_size} reached for '{model_id}'! Processing batch immediately...")
45
+ if queue_key in self.flush_tasks and not self.flush_tasks[queue_key].done():
46
+ self.flush_tasks[queue_key].cancel()
47
+ self.flush_tasks[queue_key] = asyncio.create_task(self._flush(queue_key, processor_func))
48
+ elif current_size == 1:
49
+ # Trigger B: First item, start max_wait_seconds timer
50
+ print(f"⏱️ [MICRO-BATCH TRIGGER B] First request in queue for '{model_id}'. Waiting up to {self.max_wait_seconds}s for batch to fill...")
51
+ self.flush_tasks[queue_key] = asyncio.create_task(self._wait_and_flush(queue_key, processor_func))
52
+
53
+ return await future
54
+
55
+ async def _wait_and_flush(self, queue_key: Tuple[str, bool], processor_func: Callable):
56
+ try:
57
+ await asyncio.sleep(self.max_wait_seconds)
58
+ async with self.lock:
59
+ if queue_key in self.queues and self.queues[queue_key]:
60
+ print(f"⏰ [MICRO-BATCH TIMEOUT] {self.max_wait_seconds}s timeout expired for '{queue_key[0]}'. Processing batch of {len(self.queues[queue_key])} requests...")
61
+ await self._flush(queue_key, processor_func)
62
+ except asyncio.CancelledError:
63
+ pass
64
+
65
+ async def _flush(self, queue_key: Tuple[str, bool], processor_func: Callable):
66
+ batch = self.queues.get(queue_key, [])[:self.max_batch_size]
67
+ if queue_key in self.queues:
68
+ self.queues[queue_key] = self.queues[queue_key][self.max_batch_size:]
69
+ if not self.queues[queue_key]:
70
+ del self.queues[queue_key]
71
+
72
+ if not batch:
73
+ return
74
+
75
+ model_id = queue_key[0]
76
+ payloads = [item[0] for item in batch]
77
+ futures = [item[1] for item in batch]
78
+
79
+ print(f"🚀 [MICRO-BATCH EXECUTING] Processing batch of {len(payloads)} requests together for model '{model_id}'!")
80
+
81
+ try:
82
+ results = await processor_func(model_id, payloads)
83
+ for fut, res in zip(futures, results):
84
+ if not fut.done():
85
+ fut.set_result(res)
86
+ except Exception as exc:
87
+ print(f"❌ [MICRO-BATCH ERROR] Failed to process batch for '{model_id}': {exc}")
88
+ for fut in futures:
89
+ if not fut.done():
90
+ fut.set_exception(exc)
91
+
92
+
93
+ # Global singleton instance (Max batch: 4, Wait timeout: 0.3s for instant flushing)
94
+ global_micro_batcher = ModelSpecificMicroBatcher(max_batch_size=4, max_wait_seconds=0.3)
95
+
@@ -87,6 +87,7 @@ def get_max_output_tokens(model_id: MODEL_ID, fallback: int = 128_000) -> int:
87
87
 
88
88
  MODEL_REGISTRY: dict[MODEL_ID, ModelEntry] = {
89
89
  "inclusionai/ling-3.0-flash:free": ModelEntry(
90
+
90
91
  model_id="inclusionai/ling-3.0-flash:free",
91
92
  provider="openrouter",
92
93
  cost_input_per_1k=0.000000,
@@ -97,6 +98,7 @@ MODEL_REGISTRY: dict[MODEL_ID, ModelEntry] = {
97
98
  max_output_tokens=32_768,
98
99
  description="InclusionAI Ling 3.0 Flash — ultra-fast free model for lightweight miniagents (<100KB).",
99
100
  ),
101
+
100
102
  "nvidia/nemotron-3-ultra-550b-a55b:free": ModelEntry(
101
103
  model_id="nvidia/nemotron-3-ultra-550b-a55b:free",
102
104
  provider="openrouter",
@@ -1502,16 +1504,8 @@ def estimate_cost(
1502
1504
  is_free = True
1503
1505
 
1504
1506
  if is_free:
1505
- if is_upgraded:
1506
- # Upgraded pricing: $0.002 per 1M input, $0.003 per 1M output
1507
- # Mapped to credits (1 USD = 1,000 credits):
1508
- # 0.002 credits per 1,000 input tokens, 0.003 credits per 1,000 output tokens
1509
- return (input_tokens / 1_000) * 0.002 + (output_tokens / 1_000) * 0.003
1510
- else:
1511
- # Free models pricing: $0.2 per 1M input, $0.3 per 1M output
1512
- # Mapped to credits (1 USD = 1,000 credits):
1513
- # 0.2 credits per 1,000 input tokens, 0.3 credits per 1,000 output tokens
1514
- return (input_tokens / 1_000) * 0.2 + (output_tokens / 1_000) * 0.3
1507
+ return 0.0
1508
+
1515
1509
 
1516
1510
  m = get_model(model_id)
1517
1511
  return (input_tokens / 1_000) * m.cost_input_per_1k + (
@@ -1519,6 +1513,8 @@ def estimate_cost(
1519
1513
  ) * m.cost_output_per_1k
1520
1514
 
1521
1515
 
1516
+
1517
+
1522
1518
  def route_model(task_description: str) -> MODEL_ID:
1523
1519
  """
1524
1520
  Simple heuristic router — can be swapped out for model-based routing later.
@@ -30,10 +30,12 @@ from .attribution import attach_openrouter_headers
30
30
  from slowapi import _rate_limit_exceeded_handler
31
31
  from slowapi.errors import RateLimitExceeded
32
32
  from .routes import auth_router, credit_router, session_router, completion_router, quota_router, share_router, feedback_router, referral_router, quota_share_router, marketplace_router
33
+
33
34
  from .routes.security_routes import router as security_router
34
35
  from .models import list_models
35
36
  from .auth import get_admin_user
36
37
 
38
+
37
39
  # ── Logging ───────────────────────────────────────────────────────────────────
38
40
 
39
41
  configure_logging(level=os.environ.get("LOG_LEVEL", "INFO"))
@@ -158,6 +160,9 @@ app.include_router(quota_share_router)
158
160
  app.include_router(marketplace_router, prefix="/marketplace")
159
161
  app.include_router(security_router)
160
162
 
163
+
164
+
165
+
161
166
  # ── Support Chatbot Endpoint ──────────────────────────────────────────────────
162
167
 
163
168
  class SupportChatRequest(BaseModel):
@@ -11,3 +11,4 @@ from .marketplace_routes import router as marketplace_router
11
11
 
12
12
  __all__ = ["auth_router", "credit_router", "session_router", "completion_router", "quota_router", "share_router", "feedback_router", "referral_router", "quota_share_router", "marketplace_router"]
13
13
 
14
+