utim-cli 2.3.19b0__tar.gz → 2.3.20__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/CHANGELOG.md +3 -6
- {utim_cli-2.3.19b0/utim_cli.egg-info → utim_cli-2.3.20}/PKG-INFO +1 -1
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/pyproject.toml +1 -1
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim/__init__.py +1 -1
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/__init__.py +1 -1
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/_version.py +1 -5
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/orchestrator.py +46 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/orchestrator_llm_transport.py +45 -122
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/orchestrator_turn_history.py +5 -1
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/plugins/hooks.py +11 -3
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/admin_auth.py +194 -163
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/auth.py +106 -5
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/db.py +71 -3
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/key_rotator.py +264 -53
- utim_cli-2.3.20/utim_cli/server/plan_integrity.py +1064 -0
- utim_cli-2.3.20/utim_cli/server/rate_limit.py +38 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/router.py +66 -9
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/routes/admin_bonus_routes.py +8 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/routes/admin_db_routes.py +422 -4
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/routes/auth_routes.py +47 -13
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/routes/completion_routes.py +114 -29
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/routes/credit_routes.py +399 -357
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/routes/quota_routes.py +158 -48
- utim_cli-2.3.20/utim_cli/server/routes/webhook_routes.py +99 -0
- utim_cli-2.3.20/utim_cli/server/security_alerts.py +82 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/share_tui.py +1 -1
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/tui/quota_share_dialog.py +458 -446
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/utim.py +28 -3
- {utim_cli-2.3.19b0 → utim_cli-2.3.20/utim_cli.egg-info}/PKG-INFO +1 -1
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli.egg-info/SOURCES.txt +3 -0
- utim_cli-2.3.19b0/utim_cli/server/rate_limit.py +0 -27
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/LICENSE +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/MANIFEST.in +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/README.md +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/setup.cfg +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/setup.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/_verify_repro.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/_verify_syntax.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/_wizard_smoke_test.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/agent.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/ask_helper.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/auth.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/backup.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/billing.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/blender_agent.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/bootstrap.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/brain.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/builtin/skills/utim-guide/SKILL.md +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/client_utils.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/config.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/constants.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/context_pruner.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/crawler/__init__.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/crawler/engine.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/crawler/markdown_distiller.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/dependency_manager.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/doctor.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/evidence_registry.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/harbor.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/knowledge_graph.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/local_db.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/logger.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/mcp_clean_wrapper.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/mcp_client.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/mcp_registry.json +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/models.txt +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/orchestrator_helpers.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/orchestrator_tool_exec.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/personalities.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/platform_installer.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/plugins/__init__.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/plugins/loader.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/plugins/manager.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/plugins/registry.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/plugins/runtime.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/plugins/schema.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/plugins/tool_loader.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/reflection.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/report.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/scrapy_search.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/sdk/__init__.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/sdk/config.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/sdk/context.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/sdk/events.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/sdk/handlers.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/sdk/session.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/sdk/sidecar.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/__init__.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/attribution.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/audit_log.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/batch_processor.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/captcha.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/cli_auth.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/concurrency.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/docs_md/SECRET_PROVISIONING.md +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/docs_md/about.md +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/docs_md/changelog.md +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/docs_md/docs.md +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/docs_md/features.md +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/docs_md/license.md +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/docs_md/pricing.md +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/docs_md/privacy.md +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/docs_md/refund.md +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/docs_md/support.md +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/docs_md/terms.md +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/email_utils.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/exchange_rate.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/firebase.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/fix_duplicate_users.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/history.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/local_llm.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/logging_config.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/micro_batcher.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/model_agent.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/models.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/pricing_updater.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/provision_build.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/rewards_engine.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/routes/__init__.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/routes/billing_routes.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/routes/feedback_routes.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/routes/marketplace_routes.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/routes/quota_share_routes.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/routes/referral_routes.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/routes/rewards_routes.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/routes/security_routes.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/routes/session_routes.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/routes/share_routes.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/server.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/storage_nodes.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/utils.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/share.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/situational_scoring.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/state.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/subagent_manager.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/task_dispatcher.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/task_logger.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/tools.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/tools_grep.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/tui/__init__.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/tui/feedback_dialog.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/tui/history_dialog.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/tui/marketplace_app_state.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/tui/marketplace_dialog.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/tui/marketplace_layout_engine.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/tui/mcp_dialog.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/tui/miniagents_dialog.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/tui/model_dialog.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/tui/personality_dialog.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/tui/plugins_dialog.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/tui/publish_dialog.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/tui/quota_dialog.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/tui/quota_redeem_dialog.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/tui/resume_dialog.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/tui/rewards_tui.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/tui/skills_dialog.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/tui/subagents_dialog.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/tui/tasks_dialog.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/tui/thinking_display.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/tui/tools_dialog.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/tui/update_dialog.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/update_checker.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/utilities.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/utimmodel.txt +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/vector_memory.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/wheel.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/workspace.py +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli.egg-info/dependency_links.txt +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli.egg-info/entry_points.txt +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli.egg-info/requires.txt +0 -0
- {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli.egg-info/top_level.txt +0 -0
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## [2.3.
|
|
3
|
+
## [2.3.20] - 2026-09-04
|
|
4
4
|
|
|
5
|
-
###
|
|
6
|
-
-
|
|
7
|
-
- **Tool registry cleanup**: removed `analyzeimage` from callable tools — vision models receive image bytes directly via `readfile`, non-vision models get internal auto-descriptions
|
|
8
|
-
- **APINEX models ungated**: APINEX models are now available on every plan (paid included) as free models
|
|
9
|
-
- Version tagged as **beta** (`2.3.19-beta`) for beta-channel upload
|
|
5
|
+
### 🛠️ Minor Fixes and Enhancement
|
|
6
|
+
- Minor fixes and enhancement
|
|
10
7
|
|
|
11
8
|
## [2.3.19] - 2026-09-02
|
|
12
9
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: utim-cli
|
|
3
|
-
Version: 2.3.
|
|
3
|
+
Version: 2.3.20
|
|
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.
|
|
7
|
+
version = "2.3.20"
|
|
8
8
|
|
|
9
9
|
description = "UTIM – Universal Terminal Intelligence Manager. A powerful agentic AI coding assistant for your terminal."
|
|
10
10
|
readme = "README.md"
|
|
@@ -14,7 +14,7 @@ Quickstart:
|
|
|
14
14
|
try:
|
|
15
15
|
from utim_cli._version import VERSION as __version__
|
|
16
16
|
except ImportError:
|
|
17
|
-
__version__ = "2.3.
|
|
17
|
+
__version__ = "2.3.20"
|
|
18
18
|
|
|
19
19
|
# Developer-friendly core classes (similar to `import genai` / `import openai`)
|
|
20
20
|
from utim_cli.sdk.session import UtimSession as Session
|
|
@@ -11,10 +11,6 @@
|
|
|
11
11
|
# from here so every surface stays in sync automatically.
|
|
12
12
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
# (npm semver prerelease tag + PEP 440 pre-release segment; a space would
|
|
16
|
-
# break npm publish and pip). parse_version() strips the suffix so update
|
|
17
|
-
# checks still compare on the numeric core 2.3.19.
|
|
18
|
-
VERSION = "2.3.19-beta"
|
|
14
|
+
VERSION = "2.3.20"
|
|
19
15
|
|
|
20
16
|
|
|
@@ -594,6 +594,30 @@ class Orchestrator(LLMTransportMixin, TurnHistoryMixin, ToolExecutionMixin):
|
|
|
594
594
|
except Exception:
|
|
595
595
|
pass
|
|
596
596
|
|
|
597
|
+
# ── Sanitize: strip base64-encoded binary blobs ─────────────────
|
|
598
|
+
# On Termux, copying an image file can place raw base64 data into the
|
|
599
|
+
# clipboard. If any paste-chip expansion path leaks this binary noise
|
|
600
|
+
# into the user message, we detect and truncate it here before it
|
|
601
|
+
# reaches the model context.
|
|
602
|
+
_B64_RUN_RE = re.compile(r'[A-Za-z0-9+/=]{400,}')
|
|
603
|
+
if _B64_RUN_RE.search(user_message):
|
|
604
|
+
# Count total base64-characters vs meaningful content
|
|
605
|
+
b64_chars = sum(1 for c in user_message if 'A' <= c <= 'Z' or 'a' <= c <= 'z' or '0' <= c <= '9' or c in '+/=')
|
|
606
|
+
total_chars = max(len(user_message), 1)
|
|
607
|
+
if total_chars > 600 and (b64_chars / total_chars) > 0.85:
|
|
608
|
+
# The message is dominated by binary noise — keep only
|
|
609
|
+
# non-base64 lines and any embedded file paths.
|
|
610
|
+
clean_lines = []
|
|
611
|
+
for line in user_message.split('\n'):
|
|
612
|
+
line_stripped = line.strip()
|
|
613
|
+
b64_ratio = sum(1 for c in line_stripped if 'A' <= c <= 'Z' or 'a' <= c <= 'z' or '0' <= c <= '9' or c in '+/=') / max(len(line_stripped), 1)
|
|
614
|
+
# Keep lines that are paths or have low base64 ratio
|
|
615
|
+
if b64_ratio < 0.70 or re.search(r'[A-Za-z]:[\\/]|/[^\s]+\.\w{2,4}', line_stripped):
|
|
616
|
+
clean_lines.append(line)
|
|
617
|
+
if clean_lines:
|
|
618
|
+
user_message = '\n'.join(clean_lines)
|
|
619
|
+
user_message += f"\n\n[Pasted binary clipboard data stripped — {total_chars} chars of base64 noise removed]"
|
|
620
|
+
|
|
597
621
|
# Auto-detect image paths in user_message and send direct multimodal vision payload
|
|
598
622
|
try:
|
|
599
623
|
img_matches = re.findall(r'(?:[A-Za-z]:[\\/][^\s"\']+\.(?:png|jpg|jpeg|webp|gif)|/[^\s"\']+\.(?:png|jpg|jpeg|webp|gif))', user_message, re.IGNORECASE)
|
|
@@ -829,6 +853,17 @@ class Orchestrator(LLMTransportMixin, TurnHistoryMixin, ToolExecutionMixin):
|
|
|
829
853
|
|
|
830
854
|
break # success
|
|
831
855
|
except _ServerUnavailableError as exc:
|
|
856
|
+
_err_text = str(exc).lower()
|
|
857
|
+
if "rate limit" in _err_text or "rate-limit" in _err_text or "429" in _err_text or "too many requests" in _err_text:
|
|
858
|
+
self.console.print(f"\n[bold yellow]⏳ Model is temporarily rate-limited by the provider.[/bold yellow]")
|
|
859
|
+
self.console.print(" [dim]Please wait a moment before retrying, or switch models using [bold]/model[/bold] (Ctrl+M).[/dim]\n")
|
|
860
|
+
if len(self.messages) <= turn_msg_start + 1:
|
|
861
|
+
self.messages.append({
|
|
862
|
+
"role": "assistant",
|
|
863
|
+
"content": "⏳ **Model Rate Limited**: The model is temporarily rate-limited by the provider. Please wait a short moment and retry, or use `/model` to switch models."
|
|
864
|
+
})
|
|
865
|
+
self._persist_messages()
|
|
866
|
+
return
|
|
832
867
|
if self.cancel_event.is_set():
|
|
833
868
|
del self.messages[turn_msg_start + 1:]
|
|
834
869
|
self._persist_messages()
|
|
@@ -875,6 +910,17 @@ class Orchestrator(LLMTransportMixin, TurnHistoryMixin, ToolExecutionMixin):
|
|
|
875
910
|
self.messages.append({"role": "assistant", "content": "[Aborted by user]"})
|
|
876
911
|
break
|
|
877
912
|
except Exception as exc:
|
|
913
|
+
_err_text = str(exc).lower()
|
|
914
|
+
if "rate limit" in _err_text or "rate-limit" in _err_text or "429" in _err_text or "too many requests" in _err_text:
|
|
915
|
+
self.console.print(f"\n[bold yellow]⏳ Model is temporarily rate-limited by the provider.[/bold yellow]")
|
|
916
|
+
self.console.print(" [dim]Please wait a moment before retrying, or switch models using [bold]/model[/bold] (Ctrl+M).[/dim]\n")
|
|
917
|
+
if len(self.messages) <= turn_msg_start + 1:
|
|
918
|
+
self.messages.append({
|
|
919
|
+
"role": "assistant",
|
|
920
|
+
"content": "⏳ **Model Rate Limited**: The model is temporarily rate-limited by the provider. Please wait a short moment and retry, or use `/model` to switch models."
|
|
921
|
+
})
|
|
922
|
+
self._persist_messages()
|
|
923
|
+
return
|
|
878
924
|
if self.cancel_event.is_set():
|
|
879
925
|
if len(self.messages) <= turn_msg_start + 1:
|
|
880
926
|
self.messages.append({"role": "assistant", "content": "[Aborted by user]"})
|
|
@@ -133,6 +133,11 @@ Contains: _get_configured_model_source, _is_custom_model_selection,
|
|
|
133
133
|
_resolve_model_endpoint, _call_llm. Behavior-identical; Orchestrator inherits.
|
|
134
134
|
"""
|
|
135
135
|
class LLMTransportMixin:
|
|
136
|
+
_PRE_THINK_PATTERNS = re.compile(
|
|
137
|
+
r"^\s*(\*\s*Thinking\.\.\.?|\.\.\.(\s*thinking)?|thinking\.\.\.?)\s*$",
|
|
138
|
+
re.IGNORECASE | re.MULTILINE,
|
|
139
|
+
)
|
|
140
|
+
|
|
136
141
|
def _get_configured_model_source(self, model_id: str, *, is_primary: bool = False) -> Optional[str]:
|
|
137
142
|
"""Return the selected source for a model id when the picker recorded one."""
|
|
138
143
|
if is_primary:
|
|
@@ -736,7 +741,9 @@ class LLMTransportMixin:
|
|
|
736
741
|
# 1. Check if quota is exhausted (only for non-custom models)
|
|
737
742
|
is_5h_exhausted = bool(quota.get("five_hour_quota_exhausted", False))
|
|
738
743
|
is_monthly_exhausted = bool(quota.get("is_exhausted", False))
|
|
739
|
-
|
|
744
|
+
bonus_bal = float(quota.get("bonus_balance", quota.get("free_bonus_balance", 0.0)) or 0.0)
|
|
745
|
+
# If the user has available bonus credits, inference is funded by bonus quota
|
|
746
|
+
is_exhausted = (is_5h_exhausted or is_monthly_exhausted) and bonus_bal <= 0.0
|
|
740
747
|
|
|
741
748
|
if not is_custom and is_exhausted:
|
|
742
749
|
reset_raw = quota.get("five_hour_reset_at") if is_5h_exhausted else (quota.get("reset_at") or quota.get("five_hour_reset_at", ""))
|
|
@@ -771,7 +778,7 @@ class LLMTransportMixin:
|
|
|
771
778
|
|
|
772
779
|
# 2. Check if chosen model is allowed — skip entirely for BYOK/custom models
|
|
773
780
|
models_allowed = quota.get("models_allowed", [])
|
|
774
|
-
free_bonus_balance = quota.get("free_bonus_balance", 0.0) or 0.0
|
|
781
|
+
free_bonus_balance = float(quota.get("bonus_balance", quota.get("free_bonus_balance", 0.0)) or 0.0)
|
|
775
782
|
chosen_model = override_model if override_model else self.model_id
|
|
776
783
|
is_custom = self._is_custom_model_selection(chosen_model, is_primary=not override_model)
|
|
777
784
|
is_free_user = (quota.get("plan") == "free" or str(quota.get("display_name", "")).lower() == "free")
|
|
@@ -1348,10 +1355,15 @@ class LLMTransportMixin:
|
|
|
1348
1355
|
if t == "queue_status":
|
|
1349
1356
|
pos = chunk.get("position", 1)
|
|
1350
1357
|
status = chunk.get("status", "queued")
|
|
1358
|
+
tier = chunk.get("tier", "")
|
|
1359
|
+
prio = chunk.get("priority", 0)
|
|
1351
1360
|
if status == "queued":
|
|
1352
1361
|
try:
|
|
1353
1362
|
from utim_cli.state import STATE
|
|
1354
|
-
|
|
1363
|
+
if prio > 0 and tier:
|
|
1364
|
+
STATE["thinking_topic"] = f"Request Queued Position {pos} ({str(tier).title()} Priority)"
|
|
1365
|
+
else:
|
|
1366
|
+
STATE["thinking_topic"] = f"Request Queued Position {pos}"
|
|
1355
1367
|
except Exception:
|
|
1356
1368
|
pass
|
|
1357
1369
|
elif status == "processing":
|
|
@@ -1514,12 +1526,23 @@ class LLMTransportMixin:
|
|
|
1514
1526
|
"tool_calls": None,
|
|
1515
1527
|
}, False
|
|
1516
1528
|
elif _is_rate_limit:
|
|
1529
|
+
rate_limit_display = (
|
|
1530
|
+
f"⏳ **Model Rate Limited**: `{current_model}` is currently experiencing high demand and is temporarily rate-limited by the provider.\n\n"
|
|
1531
|
+
f"- **What to do:** Please wait a moment before retrying, or switch to a different model with `/model` or `Ctrl+M`.\n"
|
|
1532
|
+
f"- **Retry:** You can resend your prompt anytime."
|
|
1533
|
+
)
|
|
1517
1534
|
if not silent:
|
|
1518
1535
|
self.console.print(
|
|
1519
|
-
f"\n[
|
|
1520
|
-
|
|
1536
|
+
f"\n[bold yellow]⏳ Model '{current_model}' is temporarily rate-limited by the provider.[/bold yellow]"
|
|
1537
|
+
)
|
|
1538
|
+
self.console.print(
|
|
1539
|
+
" [dim]Please wait a moment before retrying, or switch models using [bold]/model[/bold] (Ctrl+M).[/dim]\n"
|
|
1521
1540
|
)
|
|
1522
|
-
|
|
1541
|
+
return {
|
|
1542
|
+
"role": "assistant",
|
|
1543
|
+
"content": rate_limit_display,
|
|
1544
|
+
"tool_calls": None,
|
|
1545
|
+
}, False
|
|
1523
1546
|
elif _is_provider_upstream:
|
|
1524
1547
|
if not silent:
|
|
1525
1548
|
self.console.print(
|
|
@@ -1940,7 +1963,8 @@ class LLMTransportMixin:
|
|
|
1940
1963
|
timeout=5,
|
|
1941
1964
|
)
|
|
1942
1965
|
if _qr.status_code == 200:
|
|
1943
|
-
|
|
1966
|
+
_data = _qr.json()
|
|
1967
|
+
_has_bonus = (float(_data.get("bonus_balance", _data.get("free_bonus_balance", 0.0)) or 0.0) > 0.0)
|
|
1944
1968
|
except Exception:
|
|
1945
1969
|
pass
|
|
1946
1970
|
if _has_bonus:
|
|
@@ -2059,124 +2083,23 @@ class LLMTransportMixin:
|
|
|
2059
2083
|
"tool_calls": None,
|
|
2060
2084
|
}, False
|
|
2061
2085
|
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
# Are there more models in the fallback chain to try?
|
|
2070
|
-
_has_more_models = model_idx < len(models_to_try) - 1
|
|
2071
|
-
|
|
2072
|
-
if _has_more_models:
|
|
2073
|
-
# Soft, dim notice — let the next model in the chain take over.
|
|
2074
|
-
# The user doesn't need to (and shouldn't) see provider jargon.
|
|
2075
|
-
try:
|
|
2076
|
-
self.console.print(
|
|
2077
|
-
f"\n[dim yellow]↪ {current_model} is busy right now — "
|
|
2078
|
-
f"switching to the next available model…[/dim yellow]"
|
|
2079
|
-
)
|
|
2080
|
-
except Exception:
|
|
2081
|
-
pass
|
|
2082
|
-
# Break the inner retry loop → outer loop will pick the next model
|
|
2083
|
-
last_exc = exc
|
|
2084
|
-
break
|
|
2085
|
-
else:
|
|
2086
|
-
# Last model in chain also rate-limited — show a CLEAN, friendly
|
|
2087
|
-
# banner. Mask raw OpenRouter jargon. If the model name looks
|
|
2088
|
-
# like a free/OpenRouter endpoint, frame it as "all free
|
|
2089
|
-
# models are momentarily busy" instead of a paid-upgrade CTA.
|
|
2090
|
-
_is_free_tier_model = (
|
|
2091
|
-
":free" in current_model
|
|
2092
|
-
or "/free" in current_model
|
|
2093
|
-
or "openrouter" in current_model.lower()
|
|
2094
|
-
)
|
|
2095
|
-
_display_msg = _rate_limit_detail
|
|
2096
|
-
# Strip provider-specific keywords that leak through to users
|
|
2097
|
-
import re as _re
|
|
2098
|
-
for _jargon in (
|
|
2099
|
-
"tokens per minute", "requests per minute",
|
|
2100
|
-
"free model", "free tier", "provider",
|
|
2101
|
-
):
|
|
2102
|
-
_display_msg = _re.sub(
|
|
2103
|
-
_re.escape(_jargon),
|
|
2104
|
-
"",
|
|
2105
|
-
_display_msg,
|
|
2106
|
-
flags=_re.IGNORECASE,
|
|
2107
|
-
)
|
|
2108
|
-
# Word-boundary stripping for short tokens (tpm, rpm)
|
|
2109
|
-
for _jargon in ("tpm", "rpm"):
|
|
2110
|
-
_display_msg = _re.sub(
|
|
2111
|
-
rf"\b{_jargon}\b",
|
|
2112
|
-
"",
|
|
2113
|
-
_display_msg,
|
|
2114
|
-
flags=_re.IGNORECASE,
|
|
2115
|
-
)
|
|
2116
|
-
# Collapse leftover whitespace + dangling punctuation
|
|
2117
|
-
_display_msg = _re.sub(r"\s+", " ", _display_msg).strip(" .,;:—-")
|
|
2118
|
-
# Drop the generic "returned error:" prefix — it's provider
|
|
2119
|
-
# boilerplate that adds no value to a human reader.
|
|
2120
|
-
_display_msg = _re.sub(
|
|
2121
|
-
r"^(?:returned error|error)\s*[:\-—]?\s*",
|
|
2122
|
-
"",
|
|
2123
|
-
_display_msg,
|
|
2124
|
-
flags=_re.IGNORECASE,
|
|
2125
|
-
).strip(" .,;:—-")
|
|
2126
|
-
# If the sanitized message is too short or doesn't really say
|
|
2127
|
-
# anything useful, swap in a polished default.
|
|
2128
|
-
_low = _display_msg.lower()
|
|
2129
|
-
# Count substantive words (>= 3 chars, alpha) to detect
|
|
2130
|
-
# sentences that were gutted by jargon stripping.
|
|
2131
|
-
_substantive = sum(
|
|
2132
|
-
1 for _w in _re.findall(r"[A-Za-z]{3,}", _display_msg)
|
|
2133
|
-
)
|
|
2134
|
-
# Detect a "stranded period" artifact: a period that's now
|
|
2135
|
-
# surrounded by whitespace because its surrounding words
|
|
2136
|
-
# were stripped. This is a strong signal that the message
|
|
2137
|
-
# was gutted by jargon removal and is no longer readable.
|
|
2138
|
-
_has_stranded_period = bool(_re.search(r"\s\.\s", _display_msg))
|
|
2139
|
-
_is_uninformative = (
|
|
2140
|
-
len(_display_msg) < 12
|
|
2141
|
-
or _substantive < 3
|
|
2142
|
-
or _has_stranded_period
|
|
2143
|
-
or _low in ("returned error", "error", "rate limit", "limit")
|
|
2144
|
-
or _low in ("", ".", "..", "...")
|
|
2145
|
-
)
|
|
2146
|
-
if _is_uninformative:
|
|
2147
|
-
if _is_free_tier_model:
|
|
2148
|
-
_display_msg = (
|
|
2149
|
-
"All free models are momentarily busy. "
|
|
2150
|
-
"This clears automatically within a minute or two."
|
|
2151
|
-
)
|
|
2152
|
-
else:
|
|
2153
|
-
_display_msg = "Rate limit reached for this model."
|
|
2154
|
-
|
|
2086
|
+
rate_limit_display = (
|
|
2087
|
+
f"⏳ **Model Rate Limited**: `{current_model}` is currently experiencing high demand and is temporarily rate-limited by the provider.\n\n"
|
|
2088
|
+
f"- **What to do:** Please wait a moment before retrying, or switch to a different model with `/model` or `Ctrl+M`.\n"
|
|
2089
|
+
f"- **Retry:** You can resend your prompt anytime."
|
|
2090
|
+
)
|
|
2091
|
+
if not silent:
|
|
2155
2092
|
self.console.print(
|
|
2156
|
-
f"\n[bold yellow]⏳ {
|
|
2093
|
+
f"\n[bold yellow]⏳ Model '{current_model}' is temporarily rate-limited by the provider.[/bold yellow]"
|
|
2157
2094
|
)
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
self.console.print(
|
|
2161
|
-
" [dim]Free models rotate through a shared capacity pool — "
|
|
2162
|
-
"this usually clears in under a minute.[/dim]"
|
|
2163
|
-
)
|
|
2164
|
-
self.console.print(
|
|
2165
|
-
" [dim]You can also pick a different model with "
|
|
2166
|
-
"[bold]/model[/bold] or press Ctrl+M.[/dim]\n"
|
|
2167
|
-
)
|
|
2168
|
-
else:
|
|
2169
|
-
if not upgrade_url:
|
|
2170
|
-
upgrade_url = "https://utim.dev/pricing"
|
|
2171
|
-
self.console.print(
|
|
2172
|
-
f" [dim]Top up at [bold]{upgrade_url}[/bold] or run "
|
|
2173
|
-
f"[bold]/balance[/bold] to check your remaining quota.[/dim]\n"
|
|
2174
|
-
)
|
|
2175
|
-
|
|
2176
|
-
raise _FatalClientError(
|
|
2177
|
-
f"All models in the fallback chain are rate-limited: "
|
|
2178
|
-
f"{_rate_limit_detail}"
|
|
2095
|
+
self.console.print(
|
|
2096
|
+
" [dim]Please wait a moment before retrying, or switch models using [bold]/model[/bold] (Ctrl+M).[/dim]\n"
|
|
2179
2097
|
)
|
|
2098
|
+
return {
|
|
2099
|
+
"role": "assistant",
|
|
2100
|
+
"content": rate_limit_display,
|
|
2101
|
+
"tool_calls": None,
|
|
2102
|
+
}, False
|
|
2180
2103
|
if attempt < model_retries:
|
|
2181
2104
|
delay = 3 * (attempt + 1)
|
|
2182
2105
|
# Interruptible wait — so Ctrl+C can abort immediately
|
|
@@ -1381,7 +1381,11 @@ class TurnHistoryMixin:
|
|
|
1381
1381
|
with open("package.json", "r", encoding="utf-8") as f:
|
|
1382
1382
|
pkg = json.load(f)
|
|
1383
1383
|
if "scripts" in pkg and "test" in pkg["scripts"]:
|
|
1384
|
-
|
|
1384
|
+
from shutil import which as _which_npm
|
|
1385
|
+
_npm = _which_npm("npm")
|
|
1386
|
+
if _npm is None:
|
|
1387
|
+
return "npm not found on PATH"
|
|
1388
|
+
res = subprocess.run([_npm, "test"], capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=60)
|
|
1385
1389
|
if res.returncode != 0:
|
|
1386
1390
|
return _compact_test_err("npm test", res.stdout, res.stderr)
|
|
1387
1391
|
return None
|
|
@@ -107,11 +107,19 @@ def run_single_hook(
|
|
|
107
107
|
|
|
108
108
|
argv = _resolve_interpreter(command, Path.cwd())
|
|
109
109
|
stdin_data = json.dumps(payload)
|
|
110
|
+
# Clamp per-hook timeout: a misconfigured plugin manifest must not be
|
|
111
|
+
# able to hold the dispatch thread pool hostage (DoS guard).
|
|
112
|
+
h_timeout = min(max(float(h_timeout), 0.1), 60.0)
|
|
110
113
|
if argv is None:
|
|
111
|
-
# Shell mode
|
|
112
|
-
|
|
114
|
+
# Shell mode — run through an explicit shell argv instead of
|
|
115
|
+
# shell=True, which is injection-prone and resolves slower. On
|
|
116
|
+
# Windows, /d also skips AutoRun registry scripts (hijack guard).
|
|
117
|
+
if os.name == "nt":
|
|
118
|
+
shell_argv = ["cmd.exe", "/d", "/s", "/c", command]
|
|
119
|
+
else:
|
|
120
|
+
shell_argv = ["/bin/sh", "-c", command]
|
|
113
121
|
result = subprocess.run(
|
|
114
|
-
|
|
122
|
+
shell_argv, shell=False, input=stdin_data, capture_output=True,
|
|
115
123
|
text=True, timeout=h_timeout, env=_build_env(plugin_name, event, payload),
|
|
116
124
|
)
|
|
117
125
|
else:
|