utim-cli 2.3.16__tar.gz → 2.3.17__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.16 → utim_cli-2.3.17}/CHANGELOG.md +7 -0
- {utim_cli-2.3.16/utim_cli.egg-info → utim_cli-2.3.17}/PKG-INFO +1 -1
- {utim_cli-2.3.16 → utim_cli-2.3.17}/pyproject.toml +1 -1
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim/__init__.py +1 -1
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/__init__.py +1 -1
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/_version.py +1 -1
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/orchestrator_llm_transport.py +64 -11
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/auth.py +26 -4
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/concurrency.py +2 -2
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/db.py +14 -11
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/key_rotator.py +240 -33
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/models.py +187 -1
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/router.py +68 -1
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/routes/admin_db_routes.py +549 -549
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/routes/completion_routes.py +346 -256
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/routes/security_routes.py +1 -1
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/tui/model_dialog.py +60 -11
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/tui/thinking_display.py +145 -137
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/utim.py +163 -76
- {utim_cli-2.3.16 → utim_cli-2.3.17/utim_cli.egg-info}/PKG-INFO +1 -1
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli.egg-info/SOURCES.txt +0 -2
- utim_cli-2.3.16/utim_cli/models.txt +0 -38350
- utim_cli-2.3.16/utim_cli/utimmodel.txt +0 -133
- {utim_cli-2.3.16 → utim_cli-2.3.17}/LICENSE +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/MANIFEST.in +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/README.md +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/setup.cfg +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/setup.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/_verify_repro.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/_verify_syntax.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/agent.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/ask_helper.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/auth.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/backup.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/billing.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/blender_agent.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/bootstrap.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/brain.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/builtin/skills/utim-guide/SKILL.md +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/client_utils.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/config.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/constants.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/context_pruner.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/crawler/__init__.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/crawler/engine.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/crawler/markdown_distiller.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/dependency_manager.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/doctor.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/evidence_registry.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/harbor.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/knowledge_graph.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/local_db.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/logger.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/mcp_clean_wrapper.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/mcp_client.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/mcp_registry.json +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/orchestrator.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/orchestrator_helpers.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/orchestrator_tool_exec.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/orchestrator_turn_history.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/personalities.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/platform_installer.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/plugins/__init__.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/plugins/hooks.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/plugins/manager.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/plugins/registry.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/plugins/schema.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/plugins/tool_loader.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/reflection.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/report.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/scrapy_search.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/sdk/__init__.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/sdk/config.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/sdk/context.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/sdk/events.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/sdk/handlers.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/sdk/session.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/sdk/sidecar.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/__init__.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/admin_auth.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/attribution.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/audit_log.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/batch_processor.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/captcha.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/cli_auth.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/docs_md/SECRET_PROVISIONING.md +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/docs_md/about.md +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/docs_md/changelog.md +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/docs_md/docs.md +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/docs_md/features.md +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/docs_md/license.md +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/docs_md/pricing.md +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/docs_md/privacy.md +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/docs_md/refund.md +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/docs_md/support.md +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/docs_md/terms.md +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/email_utils.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/exchange_rate.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/firebase.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/fix_duplicate_users.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/history.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/local_llm.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/logging_config.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/micro_batcher.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/model_agent.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/pricing_updater.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/provision_build.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/rate_limit.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/rewards_engine.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/routes/__init__.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/routes/admin_bonus_routes.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/routes/auth_routes.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/routes/billing_routes.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/routes/credit_routes.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/routes/feedback_routes.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/routes/marketplace_routes.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/routes/quota_routes.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/routes/quota_share_routes.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/routes/referral_routes.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/routes/rewards_routes.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/routes/session_routes.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/routes/share_routes.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/server.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/storage_nodes.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/utils.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/share.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/share_tui.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/situational_scoring.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/state.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/subagent_manager.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/task_dispatcher.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/task_logger.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/tools.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/tools_grep.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/tui/__init__.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/tui/feedback_dialog.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/tui/history_dialog.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/tui/marketplace_app_state.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/tui/marketplace_dialog.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/tui/marketplace_layout_engine.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/tui/mcp_dialog.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/tui/miniagents_dialog.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/tui/personality_dialog.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/tui/plugins_dialog.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/tui/publish_dialog.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/tui/quota_dialog.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/tui/quota_redeem_dialog.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/tui/quota_share_dialog.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/tui/resume_dialog.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/tui/rewards_tui.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/tui/skills_dialog.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/tui/subagents_dialog.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/tui/tasks_dialog.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/tui/tools_dialog.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/tui/update_dialog.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/update_checker.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/utilities.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/vector_memory.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/wheel.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/workspace.py +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli.egg-info/dependency_links.txt +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli.egg-info/entry_points.txt +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli.egg-info/requires.txt +0 -0
- {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli.egg-info/top_level.txt +0 -0
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.3.17] - 2026-08-31
|
|
4
|
+
|
|
5
|
+
### ✨ Premium Model Visibility for Bonus-Credit Users
|
|
6
|
+
- Free-plan users with bonus credits can now see premium models in the model catalog (server-side `/models/catalog` and client-side TUI gating)
|
|
7
|
+
- Added bonus-credit detection helper with caching and graceful offline fallback
|
|
8
|
+
- Bonus credits are deducted exclusively when premium completions finish; visibility switches back automatically once the balance is exhausted
|
|
9
|
+
|
|
3
10
|
## [2.3.16] - 2026-08-29
|
|
4
11
|
|
|
5
12
|
### 🛠️ Minor Enhancements and Bug Fixes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: utim-cli
|
|
3
|
-
Version: 2.3.
|
|
3
|
+
Version: 2.3.17
|
|
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
|
|
@@ -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.17"
|
|
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
|
|
@@ -380,15 +380,42 @@ class LLMTransportMixin:
|
|
|
380
380
|
table_buf = []
|
|
381
381
|
code_block_buf = []
|
|
382
382
|
|
|
383
|
+
# ── Stream paint throttling (UI lag fix) ──────────────────────
|
|
384
|
+
# Batch streamed markdown lines and repaint at a capped rate.
|
|
385
|
+
# Previously every line triggered console.print -> patch_stdout
|
|
386
|
+
# -> full prompt_toolkit re-render; during fast streams that
|
|
387
|
+
# meant dozens of full repaints/sec competing with the spinner
|
|
388
|
+
# thread's invalidates (typing stutters, multi-second freezes).
|
|
389
|
+
_pending_md = [] # batched markdown lines awaiting print
|
|
390
|
+
_last_md_flush = [0.0] # monotonic ts of last streamed paint
|
|
391
|
+
_MD_FLUSH_INTERVAL = 0.12 # max ~8 stream repaints/sec
|
|
392
|
+
|
|
393
|
+
def _flush_pending_md(force=False):
|
|
394
|
+
import time as _t
|
|
395
|
+
now = _t.monotonic()
|
|
396
|
+
if _pending_md and (force or (now - _last_md_flush[0]) >= _MD_FLUSH_INTERVAL):
|
|
397
|
+
text = "\n".join(_pending_md)
|
|
398
|
+
_pending_md.clear()
|
|
399
|
+
if text.strip():
|
|
400
|
+
self.console.print(Markdown(text))
|
|
401
|
+
else:
|
|
402
|
+
self.console.print()
|
|
403
|
+
_last_md_flush[0] = now
|
|
404
|
+
try:
|
|
405
|
+
from utim_cli.state import STATE
|
|
406
|
+
STATE["last_stream_paint"] = _t.time()
|
|
407
|
+
except Exception:
|
|
408
|
+
pass
|
|
409
|
+
|
|
383
410
|
def stream_markdown_line(part_text):
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
return
|
|
387
|
-
self.console.print(Markdown(part_text))
|
|
411
|
+
_pending_md.append(part_text)
|
|
412
|
+
_flush_pending_md()
|
|
388
413
|
|
|
389
414
|
def flush_table():
|
|
390
415
|
if not table_buf:
|
|
391
416
|
return
|
|
417
|
+
# Keep ordering: paint any pending prose before the table.
|
|
418
|
+
_flush_pending_md(force=True)
|
|
392
419
|
table_text = "\n".join(table_buf)
|
|
393
420
|
table_buf.clear()
|
|
394
421
|
self.console.print(Markdown(table_text))
|
|
@@ -396,6 +423,8 @@ class LLMTransportMixin:
|
|
|
396
423
|
def flush_code_block():
|
|
397
424
|
if not code_block_buf:
|
|
398
425
|
return
|
|
426
|
+
# Keep ordering: paint any pending prose before the block.
|
|
427
|
+
_flush_pending_md(force=True)
|
|
399
428
|
code_text = "\n".join(code_block_buf)
|
|
400
429
|
code_block_buf.clear()
|
|
401
430
|
self.console.print(Markdown(code_text))
|
|
@@ -585,8 +614,18 @@ class LLMTransportMixin:
|
|
|
585
614
|
# Stream complete for winner
|
|
586
615
|
if is_this_winner:
|
|
587
616
|
flush_table()
|
|
617
|
+
# Force out any batched (throttled) markdown lines so no
|
|
618
|
+
# stream text is left unrendered at end of stream.
|
|
619
|
+
try:
|
|
620
|
+
_flush_pending_md(force=True)
|
|
621
|
+
except Exception:
|
|
622
|
+
pass
|
|
588
623
|
if _line_buf and not silent:
|
|
589
624
|
process_stream_part(_line_buf)
|
|
625
|
+
try:
|
|
626
|
+
_flush_pending_md(force=True)
|
|
627
|
+
except Exception:
|
|
628
|
+
pass
|
|
590
629
|
_line_buf = ""
|
|
591
630
|
|
|
592
631
|
final_content = collapse_repeating_patterns(final_content)
|
|
@@ -714,11 +753,10 @@ class LLMTransportMixin:
|
|
|
714
753
|
|
|
715
754
|
if not is_custom and free_bonus_balance <= 0.0 and models_allowed != ["all"] and chosen_model not in models_allowed and not chosen_model.endswith(":free"):
|
|
716
755
|
if quota.get("plan") == "free" or quota.get("display_name") == "Free":
|
|
717
|
-
self.console.print(f"\n[bold red]✗ Monthly free quota exhausted and no bonus credits remaining.[/bold red]")
|
|
718
|
-
self.console.print(f" Model '{chosen_model}' requires bonus credits or an upgraded plan.")
|
|
719
|
-
self.console.print(" Top up at [bold]utim.dev/pricing[/bold] or run [bold]utim upgrade[/bold].\n")
|
|
720
756
|
fallback_model = DEFAULT_MODEL
|
|
721
|
-
self.console.print(f"
|
|
757
|
+
self.console.print(f"\n[bold yellow]⚠ Bonus credits exhausted — '{chosen_model}' is gated on the Free plan.[/bold yellow]")
|
|
758
|
+
self.console.print(f" Auto-switching to the free model [bold]{fallback_model}[/bold] so you can keep working.")
|
|
759
|
+
self.console.print(" Top up bonus credits at [bold]utim.dev/pricing[/bold] or run [bold]utim upgrade[/bold].\n")
|
|
722
760
|
if override_model:
|
|
723
761
|
override_model = fallback_model
|
|
724
762
|
else:
|
|
@@ -1805,10 +1843,25 @@ class LLMTransportMixin:
|
|
|
1805
1843
|
if _has_bonus:
|
|
1806
1844
|
# Bonus credits still active — server may have stale state, skip to next fallback
|
|
1807
1845
|
break
|
|
1808
|
-
|
|
1846
|
+
# Bonus exhausted (or plan-gated): auto-degrade to the free
|
|
1847
|
+
# default model instead of aborting the user's turn.
|
|
1848
|
+
_free_fallback = DEFAULT_MODEL
|
|
1849
|
+
if current_model != _free_fallback and not current_model.endswith(":free"):
|
|
1850
|
+
self.console.print(f"\n[bold yellow]⚠ Model '{current_model}' needs bonus credits and your bonus balance is empty.[/bold yellow]")
|
|
1851
|
+
self.console.print(f" Auto-switching to the free model [bold]{_free_fallback}[/bold] so you can keep working.")
|
|
1852
|
+
self.console.print(" Top up bonus credits at [bold]utim.dev/pricing[/bold] or run [bold]utim upgrade[/bold].\n")
|
|
1853
|
+
# Splice the free fallback in as the very next model to try.
|
|
1854
|
+
_head = models_to_try[:model_idx + 1]
|
|
1855
|
+
_tail = [m for m in models_to_try[model_idx + 1:] if m != _free_fallback]
|
|
1856
|
+
models_to_try[:] = _head + [_free_fallback] + _tail
|
|
1857
|
+
if not override_model and model_idx == 0:
|
|
1858
|
+
self.model_id = _free_fallback
|
|
1859
|
+
self.model_source = "utim"
|
|
1860
|
+
config.set("main_model_source", "utim")
|
|
1861
|
+
break # attempt loop → outer loop picks up _free_fallback next
|
|
1862
|
+
self.console.print(f"\n[bold red]✗ Model '{current_model}' is unavailable on your current plan.[/bold red]")
|
|
1809
1863
|
self.console.print(" Top up at [bold]utim.dev/pricing[/bold] or run [bold]utim upgrade[/bold].")
|
|
1810
|
-
self.console.print(" [
|
|
1811
|
-
self.console.print(" [yellow] Press Ctrl+M or type /model to switch to a free model, or run 'utim reset'.[/yellow]\n")
|
|
1864
|
+
self.console.print(" [yellow]Tip: free models like 'poolside/laguna-s-2.1:free' always work — press Ctrl+M or type /model to pick one.[/yellow]\n")
|
|
1812
1865
|
raise _FatalClientError(f"Model '{current_model}' requires bonus credits or an upgraded plan.")
|
|
1813
1866
|
if code == 429:
|
|
1814
1867
|
# ── OpenRouter / provider rate-limit hit ───────────────────────
|
|
@@ -677,17 +677,39 @@ def _resolve_user_plan_name(user) -> str:
|
|
|
677
677
|
|
|
678
678
|
Safe to call without a DB session — falls back to ``"free"`` on any error
|
|
679
679
|
so that catalog gating cannot leak paid models to anonymous callers.
|
|
680
|
+
|
|
681
|
+
Expiration-aware: an ``active`` subscription whose ``end_date`` has passed
|
|
682
|
+
(and which was never renewed) is treated as ``free`` — we do not rely on a
|
|
683
|
+
background job to flip the status to ``expired``.
|
|
680
684
|
"""
|
|
681
685
|
try:
|
|
682
686
|
from .db import SessionLocal, UserSubscription
|
|
683
|
-
|
|
687
|
+
from datetime import datetime, timezone
|
|
688
|
+
subs = (
|
|
684
689
|
SessionLocal()
|
|
685
690
|
.query(UserSubscription)
|
|
686
691
|
.filter(UserSubscription.user_id == user.id, UserSubscription.status == "active")
|
|
687
|
-
.
|
|
692
|
+
.all()
|
|
688
693
|
)
|
|
689
|
-
|
|
690
|
-
|
|
694
|
+
now = datetime.now(timezone.utc)
|
|
695
|
+
best = None
|
|
696
|
+
best_end = None
|
|
697
|
+
for sub in subs:
|
|
698
|
+
if not (sub.plan and getattr(sub.plan, "name", None)):
|
|
699
|
+
continue
|
|
700
|
+
end = getattr(sub, "end_date", None)
|
|
701
|
+
# Normalise tz-aware end_date to naive UTC (or vice-versa) so the
|
|
702
|
+
# comparison never raises TypeError on mixed aware/naive values.
|
|
703
|
+
if end is not None and getattr(end, "tzinfo", None) is not None:
|
|
704
|
+
end = end.astimezone(timezone.utc).replace(tzinfo=None)
|
|
705
|
+
# Skip subscriptions that already expired.
|
|
706
|
+
if end is not None and end < now.replace(tzinfo=None):
|
|
707
|
+
continue
|
|
708
|
+
if best is None or (end is not None and (best_end is None or end > best_end)):
|
|
709
|
+
best = sub
|
|
710
|
+
best_end = end
|
|
711
|
+
if best is not None:
|
|
712
|
+
return str(best.plan.name).lower()
|
|
691
713
|
except Exception:
|
|
692
714
|
pass
|
|
693
715
|
return "free"
|
|
@@ -43,8 +43,8 @@ logger = logging.getLogger("utim.concurrency")
|
|
|
43
43
|
|
|
44
44
|
# ── Thread-pool executor for sync DB calls ────────────────────────────────────
|
|
45
45
|
# Size = number of DB connections we'll keep busy in parallel.
|
|
46
|
-
# Must be <= pool_size + max_overflow defined in db.py (currently
|
|
47
|
-
_DB_POOL_WORKERS =
|
|
46
|
+
# Must be <= pool_size + max_overflow defined in db.py (currently 30).
|
|
47
|
+
_DB_POOL_WORKERS = 30
|
|
48
48
|
_db_executor = ThreadPoolExecutor(
|
|
49
49
|
max_workers=_DB_POOL_WORKERS,
|
|
50
50
|
thread_name_prefix="utim-db",
|
|
@@ -45,16 +45,21 @@ if _is_sqlite:
|
|
|
45
45
|
else:
|
|
46
46
|
# Postgres: tune the connection pool for high concurrency across multi-worker setups.
|
|
47
47
|
# pool_size=10 — base connections kept alive permanently per worker
|
|
48
|
-
# max_overflow=
|
|
49
|
-
# pool_timeout=
|
|
48
|
+
# max_overflow=20 — burst up to 30 simultaneous DB connections per worker
|
|
49
|
+
# pool_timeout=30 — wait up to 30 s for a free connection before raising
|
|
50
|
+
# (was 10s — too short for billing bursts under high load)
|
|
50
51
|
# pool_recycle=1800 — recycle connections every 30 min (avoids "server closed connection")
|
|
52
|
+
# pool_use_lifo=True — reuse freshly-returned connections first (keeps pool warm)
|
|
53
|
+
#
|
|
54
|
+
# NOTE: pool_size + max_overflow must be >= concurrency._DB_POOL_WORKERS (currently 25).
|
|
51
55
|
engine = create_engine(
|
|
52
56
|
DATABASE_URL,
|
|
53
57
|
pool_pre_ping=True,
|
|
54
58
|
pool_size=10,
|
|
55
|
-
max_overflow=
|
|
56
|
-
pool_timeout=
|
|
59
|
+
max_overflow=20,
|
|
60
|
+
pool_timeout=30,
|
|
57
61
|
pool_recycle=1800,
|
|
62
|
+
pool_use_lifo=True,
|
|
58
63
|
connect_args=_connect_args,
|
|
59
64
|
)
|
|
60
65
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
|
@@ -986,13 +991,11 @@ def init_db(silent: bool = True) -> None:
|
|
|
986
991
|
except Exception as node_exc:
|
|
987
992
|
_print(f"[DB INIT] Storage node seeding warning: {node_exc}")
|
|
988
993
|
|
|
989
|
-
#
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
except Exception as model_sync_exc:
|
|
995
|
-
_print(f"[DB INIT] Model registry sync warning: {model_sync_exc}")
|
|
994
|
+
# NOTE: Model registry auto-sync on startup is intentionally DISABLED.
|
|
995
|
+
# sync_models_to_db() was previously called here but it overwrote the
|
|
996
|
+
# admin's manually curated model list with the hardcoded MODEL_REGISTRY
|
|
997
|
+
# on every deploy. Model management is now done exclusively via the
|
|
998
|
+
# admin panel — no automatic seeding on startup.
|
|
996
999
|
|
|
997
1000
|
# Seed guest user and guest subscription for persistent rewards preview
|
|
998
1001
|
try:
|