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.
Files changed (164) hide show
  1. {utim_cli-2.3.16 → utim_cli-2.3.17}/CHANGELOG.md +7 -0
  2. {utim_cli-2.3.16/utim_cli.egg-info → utim_cli-2.3.17}/PKG-INFO +1 -1
  3. {utim_cli-2.3.16 → utim_cli-2.3.17}/pyproject.toml +1 -1
  4. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim/__init__.py +1 -1
  5. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/__init__.py +1 -1
  6. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/_version.py +1 -1
  7. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/orchestrator_llm_transport.py +64 -11
  8. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/auth.py +26 -4
  9. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/concurrency.py +2 -2
  10. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/db.py +14 -11
  11. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/key_rotator.py +240 -33
  12. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/models.py +187 -1
  13. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/router.py +68 -1
  14. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/routes/admin_db_routes.py +549 -549
  15. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/routes/completion_routes.py +346 -256
  16. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/routes/security_routes.py +1 -1
  17. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/tui/model_dialog.py +60 -11
  18. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/tui/thinking_display.py +145 -137
  19. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/utim.py +163 -76
  20. {utim_cli-2.3.16 → utim_cli-2.3.17/utim_cli.egg-info}/PKG-INFO +1 -1
  21. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli.egg-info/SOURCES.txt +0 -2
  22. utim_cli-2.3.16/utim_cli/models.txt +0 -38350
  23. utim_cli-2.3.16/utim_cli/utimmodel.txt +0 -133
  24. {utim_cli-2.3.16 → utim_cli-2.3.17}/LICENSE +0 -0
  25. {utim_cli-2.3.16 → utim_cli-2.3.17}/MANIFEST.in +0 -0
  26. {utim_cli-2.3.16 → utim_cli-2.3.17}/README.md +0 -0
  27. {utim_cli-2.3.16 → utim_cli-2.3.17}/setup.cfg +0 -0
  28. {utim_cli-2.3.16 → utim_cli-2.3.17}/setup.py +0 -0
  29. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/_verify_repro.py +0 -0
  30. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/_verify_syntax.py +0 -0
  31. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/agent.py +0 -0
  32. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/ask_helper.py +0 -0
  33. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/auth.py +0 -0
  34. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/backup.py +0 -0
  35. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/billing.py +0 -0
  36. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/blender_agent.py +0 -0
  37. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/bootstrap.py +0 -0
  38. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/brain.py +0 -0
  39. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/builtin/skills/utim-guide/SKILL.md +0 -0
  40. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/client_utils.py +0 -0
  41. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/config.py +0 -0
  42. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/constants.py +0 -0
  43. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/context_pruner.py +0 -0
  44. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/crawler/__init__.py +0 -0
  45. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/crawler/engine.py +0 -0
  46. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/crawler/markdown_distiller.py +0 -0
  47. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/dependency_manager.py +0 -0
  48. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/doctor.py +0 -0
  49. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/evidence_registry.py +0 -0
  50. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/harbor.py +0 -0
  51. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/knowledge_graph.py +0 -0
  52. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/local_db.py +0 -0
  53. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/logger.py +0 -0
  54. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/mcp_clean_wrapper.py +0 -0
  55. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/mcp_client.py +0 -0
  56. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/mcp_registry.json +0 -0
  57. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/orchestrator.py +0 -0
  58. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/orchestrator_helpers.py +0 -0
  59. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/orchestrator_tool_exec.py +0 -0
  60. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/orchestrator_turn_history.py +0 -0
  61. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/personalities.py +0 -0
  62. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/platform_installer.py +0 -0
  63. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/plugins/__init__.py +0 -0
  64. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/plugins/hooks.py +0 -0
  65. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/plugins/manager.py +0 -0
  66. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/plugins/registry.py +0 -0
  67. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/plugins/schema.py +0 -0
  68. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/plugins/tool_loader.py +0 -0
  69. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/reflection.py +0 -0
  70. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/report.py +0 -0
  71. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/scrapy_search.py +0 -0
  72. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/sdk/__init__.py +0 -0
  73. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/sdk/config.py +0 -0
  74. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/sdk/context.py +0 -0
  75. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/sdk/events.py +0 -0
  76. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/sdk/handlers.py +0 -0
  77. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/sdk/session.py +0 -0
  78. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/sdk/sidecar.py +0 -0
  79. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/__init__.py +0 -0
  80. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/admin_auth.py +0 -0
  81. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/attribution.py +0 -0
  82. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/audit_log.py +0 -0
  83. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/batch_processor.py +0 -0
  84. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/captcha.py +0 -0
  85. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/cli_auth.py +0 -0
  86. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/docs_md/SECRET_PROVISIONING.md +0 -0
  87. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/docs_md/about.md +0 -0
  88. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/docs_md/changelog.md +0 -0
  89. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/docs_md/docs.md +0 -0
  90. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/docs_md/features.md +0 -0
  91. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/docs_md/license.md +0 -0
  92. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/docs_md/pricing.md +0 -0
  93. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/docs_md/privacy.md +0 -0
  94. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/docs_md/refund.md +0 -0
  95. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/docs_md/support.md +0 -0
  96. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/docs_md/terms.md +0 -0
  97. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/email_utils.py +0 -0
  98. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/exchange_rate.py +0 -0
  99. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/firebase.py +0 -0
  100. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/fix_duplicate_users.py +0 -0
  101. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/history.py +0 -0
  102. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/local_llm.py +0 -0
  103. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/logging_config.py +0 -0
  104. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/micro_batcher.py +0 -0
  105. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/model_agent.py +0 -0
  106. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/pricing_updater.py +0 -0
  107. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/provision_build.py +0 -0
  108. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/rate_limit.py +0 -0
  109. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/rewards_engine.py +0 -0
  110. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/routes/__init__.py +0 -0
  111. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/routes/admin_bonus_routes.py +0 -0
  112. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/routes/auth_routes.py +0 -0
  113. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/routes/billing_routes.py +0 -0
  114. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/routes/credit_routes.py +0 -0
  115. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/routes/feedback_routes.py +0 -0
  116. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/routes/marketplace_routes.py +0 -0
  117. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/routes/quota_routes.py +0 -0
  118. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/routes/quota_share_routes.py +0 -0
  119. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/routes/referral_routes.py +0 -0
  120. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/routes/rewards_routes.py +0 -0
  121. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/routes/session_routes.py +0 -0
  122. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/routes/share_routes.py +0 -0
  123. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/server.py +0 -0
  124. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/storage_nodes.py +0 -0
  125. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/server/utils.py +0 -0
  126. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/share.py +0 -0
  127. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/share_tui.py +0 -0
  128. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/situational_scoring.py +0 -0
  129. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/state.py +0 -0
  130. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/subagent_manager.py +0 -0
  131. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/task_dispatcher.py +0 -0
  132. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/task_logger.py +0 -0
  133. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/tools.py +0 -0
  134. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/tools_grep.py +0 -0
  135. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/tui/__init__.py +0 -0
  136. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/tui/feedback_dialog.py +0 -0
  137. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/tui/history_dialog.py +0 -0
  138. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/tui/marketplace_app_state.py +0 -0
  139. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/tui/marketplace_dialog.py +0 -0
  140. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/tui/marketplace_layout_engine.py +0 -0
  141. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/tui/mcp_dialog.py +0 -0
  142. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/tui/miniagents_dialog.py +0 -0
  143. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/tui/personality_dialog.py +0 -0
  144. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/tui/plugins_dialog.py +0 -0
  145. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/tui/publish_dialog.py +0 -0
  146. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/tui/quota_dialog.py +0 -0
  147. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/tui/quota_redeem_dialog.py +0 -0
  148. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/tui/quota_share_dialog.py +0 -0
  149. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/tui/resume_dialog.py +0 -0
  150. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/tui/rewards_tui.py +0 -0
  151. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/tui/skills_dialog.py +0 -0
  152. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/tui/subagents_dialog.py +0 -0
  153. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/tui/tasks_dialog.py +0 -0
  154. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/tui/tools_dialog.py +0 -0
  155. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/tui/update_dialog.py +0 -0
  156. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/update_checker.py +0 -0
  157. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/utilities.py +0 -0
  158. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/vector_memory.py +0 -0
  159. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/wheel.py +0 -0
  160. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli/workspace.py +0 -0
  161. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli.egg-info/dependency_links.txt +0 -0
  162. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli.egg-info/entry_points.txt +0 -0
  163. {utim_cli-2.3.16 → utim_cli-2.3.17}/utim_cli.egg-info/requires.txt +0 -0
  164. {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.16
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "utim-cli"
7
- version = "2.3.16"
7
+ version = "2.3.17"
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.16"
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
@@ -5,7 +5,7 @@ try:
5
5
  from utim_cli._version import VERSION as __version__
6
6
  except ImportError:
7
7
  # Fallback during first-install / editable-install edge cases
8
- __version__ = "2.3.16"
8
+ __version__ = "2.3.17"
9
9
 
10
10
 
11
11
  import builtins, os, pathlib, urllib.parse, re
@@ -11,6 +11,6 @@
11
11
  # from here so every surface stays in sync automatically.
12
12
  # ─────────────────────────────────────────────────────────────────────────────
13
13
 
14
- VERSION = "2.3.16"
14
+ VERSION = "2.3.17"
15
15
 
16
16
 
@@ -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
- if not part_text.strip():
385
- self.console.print()
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" Falling back to: [bold]{fallback_model}[/bold]")
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
- self.console.print(f"\n[bold red]✗ Model '{current_model}' requires bonus credits or an upgraded plan.[/bold red]")
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(" [bold yellow]Tip: Your bonus credits are exhausted. You can still access all free models! [/bold yellow]")
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
- sub = (
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
- .first()
692
+ .all()
688
693
  )
689
- if sub and sub.plan and getattr(sub.plan, "name", None):
690
- return str(sub.plan.name).lower()
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 25).
47
- _DB_POOL_WORKERS = 25
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=15 — burst up to 25 simultaneous DB connections per worker
49
- # pool_timeout=10 — wait up to 10 s for a free connection before raising
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=15,
56
- pool_timeout=10,
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
- # Seed and sync all models into model_registry table
990
- try:
991
- from .models import sync_models_to_db
992
- sync_models_to_db()
993
- _print("[DB INIT] Model registry sync completed.")
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: