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.
- {utim_cli-2.1.1 → utim_cli-2.1.2}/CHANGELOG.md +9 -0
- utim_cli-2.1.2/LICENSE +26 -0
- {utim_cli-2.1.1/utim_cli.egg-info → utim_cli-2.1.2}/PKG-INFO +2 -1
- {utim_cli-2.1.1 → utim_cli-2.1.2}/pyproject.toml +4 -1
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/__init__.py +1 -1
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/_version.py +1 -1
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/blender_agent.py +2 -1
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/client_utils.py +20 -3
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/config.py +4 -2
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/constants.py +2 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/context_pruner.py +7 -3
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/orchestrator.py +9 -8
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/reflection.py +2 -1
- utim_cli-2.1.2/utim_cli/server/batch_processor.py +119 -0
- utim_cli-2.1.2/utim_cli/server/micro_batcher.py +95 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/models.py +6 -10
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/router.py +5 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/routes/__init__.py +1 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/routes/completion_routes.py +67 -3
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/server.py +4 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/situational_scoring.py +5 -2
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/tools.py +83 -13
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/tui/feedback_dialog.py +433 -419
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/tui/model_dialog.py +11 -4
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/utim.py +13 -4
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/vector_memory.py +10 -1
- {utim_cli-2.1.1 → utim_cli-2.1.2/utim_cli.egg-info}/PKG-INFO +2 -1
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli.egg-info/SOURCES.txt +2 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli.egg-info/entry_points.txt +1 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli.egg-info/requires.txt +1 -0
- utim_cli-2.1.1/LICENSE +0 -21
- {utim_cli-2.1.1 → utim_cli-2.1.2}/MANIFEST.in +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/README.md +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/setup.cfg +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/setup.py +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/_check_obj.py +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/_find_mcp.py +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/_find_plan.py +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/_find_tool.py +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/_patch_multimodal.py +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/_test_regex.py +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/agent.py +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/auth.py +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/backup.py +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/billing.py +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/bootstrap.py +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/doctor.py +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/harbor.py +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/knowledge_graph.py +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/local_db.py +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/logger.py +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/mcp_clean_wrapper.py +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/mcp_client.py +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/mcp_registry.json +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/report.py +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/scrapy_search.py +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/__init__.py +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/attribution.py +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/audit_log.py +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/auth.py +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/captcha.py +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/cli_auth.py +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/db.py +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/docs_md/SECRET_PROVISIONING.md +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/docs_md/about.md +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/docs_md/changelog.md +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/docs_md/docs.md +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/docs_md/features.md +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/docs_md/license.md +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/docs_md/pricing.md +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/docs_md/privacy.md +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/docs_md/refund.md +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/docs_md/support.md +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/docs_md/terms.md +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/email_utils.py +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/exchange_rate.py +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/firebase.py +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/fix_duplicate_users.py +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/history.py +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/logging_config.py +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/pricing_updater.py +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/provision_build.py +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/rate_limit.py +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/routes/auth_routes.py +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/routes/credit_routes.py +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/routes/feedback_routes.py +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/routes/marketplace_routes.py +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/routes/quota_routes.py +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/routes/quota_share_routes.py +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/routes/referral_routes.py +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/routes/security_routes.py +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/routes/session_routes.py +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/routes/share_routes.py +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/server/storage_nodes.py +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/share.py +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/share_tui.py +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/state.py +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/tui/__init__.py +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/tui/history_dialog.py +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/tui/marketplace_app_state.py +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/tui/marketplace_dialog.py +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/tui/marketplace_layout_engine.py +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/tui/mcp_dialog.py +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/tui/miniagents_dialog.py +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/tui/publish_dialog.py +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/tui/quota_dialog.py +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/tui/quota_redeem_dialog.py +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/tui/quota_share_dialog.py +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/tui/resume_dialog.py +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/tui/skills_dialog.py +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/tui/subagents_dialog.py +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/tui/tools_dialog.py +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/tui/update_dialog.py +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/utilities.py +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli/workspace.py +0 -0
- {utim_cli-2.1.1 → utim_cli-2.1.2}/utim_cli.egg-info/dependency_links.txt +0 -0
- {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.
|
|
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.
|
|
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*"]
|
|
@@ -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
|
-
|
|
204
|
-
|
|
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
|
-
"
|
|
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
|
|
@@ -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
|
|
176
|
-
return
|
|
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
|
|
3773
|
-
return
|
|
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
|
|
5186
|
-
#
|
|
5187
|
-
|
|
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
|
|
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
|
-
|
|
1506
|
-
|
|
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):
|