utim-cli 2.3.23__tar.gz → 2.3.25__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 (173) hide show
  1. {utim_cli-2.3.23 → utim_cli-2.3.25}/CHANGELOG.md +10 -0
  2. {utim_cli-2.3.23/utim_cli.egg-info → utim_cli-2.3.25}/PKG-INFO +1 -1
  3. {utim_cli-2.3.23 → utim_cli-2.3.25}/pyproject.toml +1 -1
  4. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim/__init__.py +2 -2
  5. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/__init__.py +1 -1
  6. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/_version.py +1 -1
  7. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/orchestrator.py +1 -1
  8. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/orchestrator_llm_transport.py +6 -41
  9. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/orchestrator_tool_exec.py +7 -0
  10. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/server/key_rotator.py +83 -56
  11. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/server/models.py +4 -1
  12. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/server/routes/completion_routes.py +39 -16
  13. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/state.py +1 -0
  14. utim_cli-2.3.25/utim_cli/tui/compression_dialog.py +188 -0
  15. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/utim.py +151 -166
  16. {utim_cli-2.3.23 → utim_cli-2.3.25/utim_cli.egg-info}/PKG-INFO +1 -1
  17. utim_cli-2.3.23/utim_cli/tui/compression_dialog.py +0 -230
  18. {utim_cli-2.3.23 → utim_cli-2.3.25}/LICENSE +0 -0
  19. {utim_cli-2.3.23 → utim_cli-2.3.25}/MANIFEST.in +0 -0
  20. {utim_cli-2.3.23 → utim_cli-2.3.25}/README.md +0 -0
  21. {utim_cli-2.3.23 → utim_cli-2.3.25}/setup.cfg +0 -0
  22. {utim_cli-2.3.23 → utim_cli-2.3.25}/setup.py +0 -0
  23. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/_chip_test.py +0 -0
  24. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/_verify_repro.py +0 -0
  25. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/_verify_syntax.py +0 -0
  26. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/_wizard_smoke_test.py +0 -0
  27. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/agent.py +0 -0
  28. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/ask_helper.py +0 -0
  29. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/auth.py +0 -0
  30. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/backup.py +0 -0
  31. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/billing.py +0 -0
  32. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/blender_agent.py +0 -0
  33. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/bootstrap.py +0 -0
  34. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/brain.py +0 -0
  35. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/builtin/skills/utim-guide/SKILL.md +0 -0
  36. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/client_utils.py +0 -0
  37. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/config.py +0 -0
  38. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/constants.py +0 -0
  39. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/context_pruner.py +0 -0
  40. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/crawler/__init__.py +0 -0
  41. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/crawler/engine.py +0 -0
  42. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/crawler/markdown_distiller.py +0 -0
  43. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/dependency_manager.py +0 -0
  44. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/doctor.py +0 -0
  45. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/evidence_registry.py +0 -0
  46. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/harbor.py +0 -0
  47. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/knowledge_graph.py +0 -0
  48. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/local_db.py +0 -0
  49. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/logger.py +0 -0
  50. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/mcp_clean_wrapper.py +0 -0
  51. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/mcp_client.py +0 -0
  52. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/mcp_registry.json +0 -0
  53. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/models.txt +0 -0
  54. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/orchestrator_helpers.py +0 -0
  55. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/orchestrator_turn_history.py +0 -0
  56. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/personalities.py +0 -0
  57. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/platform_installer.py +0 -0
  58. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/plugins/__init__.py +0 -0
  59. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/plugins/hooks.py +0 -0
  60. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/plugins/loader.py +0 -0
  61. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/plugins/manager.py +0 -0
  62. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/plugins/registry.py +0 -0
  63. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/plugins/runtime.py +0 -0
  64. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/plugins/schema.py +0 -0
  65. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/plugins/tool_loader.py +0 -0
  66. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/reflection.py +0 -0
  67. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/report.py +0 -0
  68. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/scrapy_search.py +0 -0
  69. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/sdk/__init__.py +0 -0
  70. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/sdk/config.py +0 -0
  71. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/sdk/context.py +0 -0
  72. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/sdk/events.py +0 -0
  73. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/sdk/handlers.py +0 -0
  74. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/sdk/session.py +0 -0
  75. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/sdk/sidecar.py +0 -0
  76. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/server/__init__.py +0 -0
  77. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/server/admin_auth.py +0 -0
  78. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/server/attribution.py +0 -0
  79. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/server/audit_log.py +0 -0
  80. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/server/auth.py +0 -0
  81. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/server/batch_processor.py +0 -0
  82. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/server/captcha.py +0 -0
  83. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/server/cli_auth.py +0 -0
  84. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/server/concurrency.py +0 -0
  85. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/server/db.py +0 -0
  86. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/server/docs_md/SECRET_PROVISIONING.md +0 -0
  87. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/server/docs_md/about.md +0 -0
  88. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/server/docs_md/changelog.md +0 -0
  89. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/server/docs_md/docs.md +0 -0
  90. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/server/docs_md/features.md +0 -0
  91. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/server/docs_md/license.md +0 -0
  92. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/server/docs_md/pricing.md +0 -0
  93. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/server/docs_md/privacy.md +0 -0
  94. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/server/docs_md/refund.md +0 -0
  95. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/server/docs_md/support.md +0 -0
  96. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/server/docs_md/terms.md +0 -0
  97. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/server/email_utils.py +0 -0
  98. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/server/exchange_rate.py +0 -0
  99. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/server/firebase.py +0 -0
  100. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/server/fix_duplicate_users.py +0 -0
  101. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/server/history.py +0 -0
  102. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/server/local_llm.py +0 -0
  103. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/server/logging_config.py +0 -0
  104. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/server/micro_batcher.py +0 -0
  105. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/server/model_agent.py +0 -0
  106. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/server/plan_integrity.py +0 -0
  107. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/server/pricing_updater.py +0 -0
  108. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/server/provision_build.py +0 -0
  109. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/server/rate_limit.py +0 -0
  110. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/server/rewards_engine.py +0 -0
  111. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/server/router.py +0 -0
  112. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/server/routes/__init__.py +0 -0
  113. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/server/routes/admin_bonus_routes.py +0 -0
  114. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/server/routes/admin_db_routes.py +0 -0
  115. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/server/routes/auth_routes.py +0 -0
  116. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/server/routes/billing_routes.py +0 -0
  117. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/server/routes/credit_routes.py +0 -0
  118. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/server/routes/feedback_routes.py +0 -0
  119. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/server/routes/marketplace_routes.py +0 -0
  120. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/server/routes/quota_routes.py +0 -0
  121. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/server/routes/quota_share_routes.py +0 -0
  122. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/server/routes/referral_routes.py +0 -0
  123. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/server/routes/rewards_routes.py +0 -0
  124. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/server/routes/security_routes.py +0 -0
  125. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/server/routes/session_routes.py +0 -0
  126. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/server/routes/share_routes.py +0 -0
  127. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/server/routes/webhook_routes.py +0 -0
  128. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/server/security_alerts.py +0 -0
  129. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/server/server.py +0 -0
  130. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/server/storage_nodes.py +0 -0
  131. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/server/utils.py +0 -0
  132. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/share.py +0 -0
  133. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/share_tui.py +0 -0
  134. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/situational_scoring.py +0 -0
  135. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/subagent_manager.py +0 -0
  136. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/task_dispatcher.py +0 -0
  137. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/task_logger.py +0 -0
  138. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/tools.py +0 -0
  139. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/tools_grep.py +0 -0
  140. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/tui/__init__.py +0 -0
  141. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/tui/feedback_dialog.py +0 -0
  142. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/tui/history_dialog.py +0 -0
  143. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/tui/marketplace_app_state.py +0 -0
  144. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/tui/marketplace_dialog.py +0 -0
  145. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/tui/marketplace_layout_engine.py +0 -0
  146. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/tui/mcp_dialog.py +0 -0
  147. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/tui/miniagents_dialog.py +0 -0
  148. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/tui/model_dialog.py +0 -0
  149. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/tui/personality_dialog.py +0 -0
  150. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/tui/plugins_dialog.py +0 -0
  151. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/tui/publish_dialog.py +0 -0
  152. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/tui/quota_dialog.py +0 -0
  153. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/tui/quota_redeem_dialog.py +0 -0
  154. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/tui/quota_share_dialog.py +0 -0
  155. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/tui/resume_dialog.py +0 -0
  156. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/tui/rewards_tui.py +0 -0
  157. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/tui/skills_dialog.py +0 -0
  158. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/tui/subagents_dialog.py +0 -0
  159. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/tui/tasks_dialog.py +0 -0
  160. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/tui/thinking_display.py +0 -0
  161. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/tui/tools_dialog.py +0 -0
  162. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/tui/update_dialog.py +0 -0
  163. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/update_checker.py +0 -0
  164. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/utilities.py +0 -0
  165. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/utimmodel.txt +0 -0
  166. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/vector_memory.py +0 -0
  167. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/wheel.py +0 -0
  168. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli/workspace.py +0 -0
  169. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli.egg-info/SOURCES.txt +0 -0
  170. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli.egg-info/dependency_links.txt +0 -0
  171. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli.egg-info/entry_points.txt +0 -0
  172. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli.egg-info/requires.txt +0 -0
  173. {utim_cli-2.3.23 → utim_cli-2.3.25}/utim_cli.egg-info/top_level.txt +0 -0
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.3.25] - 2026-09-07
4
+
5
+ ### 🛠️ Bug Fixes
6
+ - Fixed usage dialog glitches
7
+
8
+ ## [2.3.24] - 2026-09-07
9
+
10
+ ### ⚡ Performance
11
+ - Fixed latency issues
12
+
3
13
  ## [2.3.23] - 2026-09-07
4
14
 
5
15
  ### 🛠️ Minor Bug Fixes and Enhancement
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: utim-cli
3
- Version: 2.3.23
3
+ Version: 2.3.25
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.23"
7
+ version = "2.3.25"
8
8
 
9
9
  description = "UTIM – Universal Terminal Intelligence Manager. A powerful agentic AI coding assistant for your terminal."
10
10
  readme = "README.md"
@@ -1,4 +1,4 @@
1
- """
1
+ """
2
2
  UTIM Developer SDK
3
3
  ------------------
4
4
  Unified autonomous coding agent SDK.
@@ -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.22"
17
+ __version__ = "2.3.25"
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.23"
8
+ __version__ = "2.3.25"
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.23"
14
+ VERSION = "2.3.25"
15
15
 
16
16
 
@@ -848,7 +848,7 @@ class Orchestrator(LLMTransportMixin, TurnHistoryMixin, ToolExecutionMixin):
848
848
  msg, was_streamed = self._call_llm(
849
849
  send_msgs,
850
850
  override_tools=override_tools or getattr(self, "_subagent_tool_override", None),
851
- silent=True # Suppress "⏳ Model" rate-limit messages during fallback flows
851
+ silent=False,
852
852
  )
853
853
  reasoning_duration = time.time() - t_llm_start
854
854
 
@@ -268,11 +268,6 @@ class LLMTransportMixin:
268
268
  if not race_candidates:
269
269
  return None
270
270
 
271
- if not silent:
272
- self.console.print(
273
- f"\n[dim yellow]↪ Primary model unavailable — racing {len(race_candidates)} fallback models for lowest latency response…[/dim yellow]\n"
274
- )
275
-
276
271
  winner_lock = threading.Lock()
277
272
  winner_model = [None]
278
273
  winner_result = [None]
@@ -1135,10 +1130,6 @@ class LLMTransportMixin:
1135
1130
  _exhausted_key = _api_key
1136
1131
 
1137
1132
  if _exhausted_key:
1138
- remaining = config.get_exhaustion_countdown(_exhausted_key)
1139
- remaining_str = f"{remaining // 3600}h {remaining % 3600 // 60}m" if remaining else "24h"
1140
- if not silent:
1141
- self.console.print(f"[dim]Skipping exhausted API key {_exhausted_key[:12]}... (cooldown: {remaining_str})[/dim]")
1142
1133
  raise _ServerUnavailableError(f"API key exhausted: {_exhausted_key[:12]}...")
1143
1134
 
1144
1135
  # Build auth headers or query params depending on provider
@@ -1610,17 +1601,8 @@ class LLMTransportMixin:
1610
1601
  _paid_slug = current_model.rsplit(":free", 1)[0]
1611
1602
  _remaining = models_to_try[model_idx + 1:]
1612
1603
  if _paid_slug not in _remaining:
1613
- if not silent:
1614
- self.console.print(
1615
- f"\n[dim yellow]↪ Free model rate-limited — trying paid version '{_paid_slug}'…[/dim yellow]"
1616
- )
1617
1604
  # Insert paid slug as the very next model, then break
1618
1605
  models_to_try[model_idx + 1:model_idx + 1] = [_paid_slug]
1619
- else:
1620
- if not silent:
1621
- self.console.print(
1622
- f"\n[dim yellow]↪ Free model rate-limited — paid version already in queue, skipping…[/dim yellow]"
1623
- )
1624
1606
  break # inner attempt loop → outer loop picks up injected paid slug
1625
1607
  # Non-free model: nothing to fall back to — show message and return
1626
1608
  rate_limit_display = (
@@ -1641,21 +1623,9 @@ class LLMTransportMixin:
1641
1623
  "tool_calls": None,
1642
1624
  }, False
1643
1625
  elif _is_provider_upstream:
1644
- if not silent:
1645
- self.console.print(
1646
- f"\n[dim yellow]↪ {_friendly_name}'s provider is having trouble — "
1647
- f"switching to the next available model…[/dim yellow]"
1648
- )
1649
1626
  raise RuntimeError(f"__PROVIDER_ERROR__:{current_model}")
1650
1627
  else:
1651
- # Unknown error: show a concise message, not the raw blob
1652
- _short = err_msg.split("{'error'")[0].strip() or err_msg[:120]
1653
- if not silent:
1654
- self.console.print(
1655
- f"\n[bold red]Model error on {_friendly_name}: {_short}[/bold red]\n"
1656
- f"[dim]Trying the next available model…[/dim]"
1657
- )
1658
- raise RuntimeError(f"Server completion error ({current_model}): {err_msg}")
1628
+ raise RuntimeError(f"Server completion error ({current_model}): {err_msg}")
1659
1629
  if "finish_reason" in chunk and chunk["finish_reason"]:
1660
1630
  _stream_finish_reason = chunk["finish_reason"]
1661
1631
  elif not _stream_finish_reason:
@@ -2188,19 +2158,14 @@ class LLMTransportMixin:
2188
2158
  _paid_slug = current_model.rsplit(":free", 1)[0]
2189
2159
  _remaining = models_to_try[model_idx + 1:]
2190
2160
  if _paid_slug not in _remaining:
2191
- if not silent:
2192
- self.console.print(
2193
- f"\n[dim yellow]↪ Free model rate-limited — trying paid version '{_paid_slug}'…[/dim yellow]"
2194
- )
2195
2161
  # Insert paid slug as the very next model, then break
2196
2162
  models_to_try[model_idx + 1:model_idx + 1] = [_paid_slug]
2197
- else:
2198
- if not silent:
2199
- self.console.print(
2200
- f"\n[dim yellow]↪ Free model rate-limited — paid version already in queue, skipping…[/dim yellow]"
2201
- )
2202
2163
  break # inner attempt loop → outer loop picks up injected paid slug
2203
- # Non-free model: nothing to fall back to show message and return
2164
+ # If primary model is rate-limited and fallback models exist, race them
2165
+ if current_is_primary and fallback_list:
2166
+ raise _ServerUnavailableError(f"Rate-limited on model '{current_model}'")
2167
+
2168
+ # Non-free model with no fallback: show message and return
2204
2169
  rate_limit_display = (
2205
2170
  f"⏳ **Model Rate Limited**: `{current_model}` is currently experiencing high demand and is temporarily rate-limited by the provider.\n\n"
2206
2171
  f"- **What to do:** Please wait a moment before retrying, or switch to a different model with `/model` or `Ctrl+M`.\n"
@@ -659,6 +659,13 @@ class ToolExecutionMixin:
659
659
  self.console.print(bl)
660
660
  rendered_text = capture.get()
661
661
  if print_to_console and getattr(self, "_subagent_depth", 0) == 0:
662
+ try:
663
+ from utim_cli.state import STATE
664
+ if STATE.get("dialog_active"):
665
+ self.console.print(rendered_text, end="", markup=False, highlight=False)
666
+ return rendered_text
667
+ except Exception:
668
+ pass
662
669
  if getattr(self.console, "file", None) is not sys.stdout and getattr(self.console, "file", None) is not sys.__stdout__:
663
670
  # IMPORTANT: rendered_text is a *plain string* that was already
664
671
  # produced by Rich (via capture.get()). It can legitimately
@@ -1891,12 +1891,7 @@ class ApinexKeyRotator:
1891
1891
  return max(self.TOTAL_RPM_PER_KEY, len(self._keys) * self.TOTAL_RPM_PER_KEY)
1892
1892
 
1893
1893
  def _get_active_key_locked(self, now: float) -> Optional[KeyEntry]:
1894
- """Find the current active unexhausted APINEX key.
1895
-
1896
- Pins to the current key for all requests until it returns 402.
1897
- When 402 occurs, that key is marked exhausted until 00:00 UTC (5:30 AM IST)
1898
- and rotation advances to the next key until all keys are exhausted.
1899
- """
1894
+ """Find the next active unexhausted APINEX key using round-robin rotation."""
1900
1895
  if not self._keys:
1901
1896
  return None
1902
1897
  n = len(self._keys)
@@ -1909,7 +1904,7 @@ class ApinexKeyRotator:
1909
1904
  continue
1910
1905
  if k.is_in_cooldown(now):
1911
1906
  continue
1912
- self._current_key_idx = idx
1907
+ self._current_key_idx = (idx + 1) % n
1913
1908
  return k
1914
1909
 
1915
1910
  # Fallback: if all non-exhausted keys are currently in cooldown, return the first unexhausted key
@@ -1918,13 +1913,17 @@ class ApinexKeyRotator:
1918
1913
  k = self._keys[idx]
1919
1914
  if (k.exhausted_until > 0 and now < k.exhausted_until) or k.credits_exhausted or k.paid_exhausted:
1920
1915
  continue
1916
+ self._current_key_idx = (idx + 1) % n
1921
1917
  return k
1922
1918
  return None
1923
1919
 
1924
1920
  def has_available_keys(self) -> bool:
1925
1921
  with self._lock:
1926
1922
  now = time.time()
1927
- return self._get_active_key_locked(now) is not None
1923
+ return any(
1924
+ not ((k.exhausted_until > 0 and now < k.exhausted_until) or k.credits_exhausted or k.paid_exhausted)
1925
+ for k in self._keys
1926
+ )
1928
1927
 
1929
1928
  def get_all_unexhausted_keys(self, now: Optional[float] = None) -> List[KeyEntry]:
1930
1929
  """Return all APINEX keys that are not 402 daily quota exhausted."""
@@ -1940,6 +1939,7 @@ class ApinexKeyRotator:
1940
1939
  return unexhausted
1941
1940
 
1942
1941
  def acquire_key(self, purpose: str = "user") -> KeyEntry:
1942
+ """Acquire the next available APINEX key using round-robin rotation and load balancing."""
1943
1943
  with self._lock:
1944
1944
  if not self._keys:
1945
1945
  self._refresh_keys()
@@ -1953,36 +1953,46 @@ class ApinexKeyRotator:
1953
1953
  for k in self._keys:
1954
1954
  k.prune_expired(now)
1955
1955
 
1956
- active_key = self._get_active_key_locked(now)
1957
- if active_key is None:
1958
- raise HTTPException(
1959
- status_code=503,
1960
- detail=(
1961
- f"All {len(self._keys)} APINEX API keys have exhausted their daily quota (402) "
1962
- f"until 00:00 UTC (5:30 AM IST). Service will resume at 00:00 UTC."
1963
- ),
1956
+ candidates = [
1957
+ k for k in self._keys
1958
+ if k.has_capacity(
1959
+ purpose=purpose,
1960
+ now=now,
1961
+ user_limit=self.USER_RPM_PER_KEY,
1962
+ reflection_limit=self.REFLECTION_RPM_PER_KEY,
1963
+ total_limit=self.TOTAL_RPM_PER_KEY,
1964
+ daily_limit=self.DAILY_RPD_PER_KEY,
1964
1965
  )
1966
+ ]
1965
1967
 
1966
- if not active_key.has_capacity(
1967
- purpose=purpose,
1968
- now=now,
1969
- user_limit=self.USER_RPM_PER_KEY,
1970
- reflection_limit=self.REFLECTION_RPM_PER_KEY,
1971
- total_limit=self.TOTAL_RPM_PER_KEY,
1972
- daily_limit=self.DAILY_RPD_PER_KEY,
1973
- ):
1974
- total_rpm = active_key.current_total_rpm(now)
1968
+ if not candidates:
1969
+ if all((k.exhausted_until > 0 and now < k.exhausted_until) or k.credits_exhausted or k.paid_exhausted for k in self._keys):
1970
+ raise HTTPException(
1971
+ status_code=503,
1972
+ detail=(
1973
+ f"All {len(self._keys)} APINEX API keys have exhausted their daily quota (402) "
1974
+ f"until 00:00 UTC (5:30 AM IST). Service will resume at 00:00 UTC."
1975
+ ),
1976
+ )
1977
+ total_rpm = sum(k.current_total_rpm(now) for k in self._keys)
1975
1978
  raise HTTPException(
1976
1979
  status_code=429,
1977
1980
  detail=(
1978
- f"APINEX capacity limit reached on current key ({total_rpm}/{self.TOTAL_RPM_PER_KEY} RPM, "
1979
- f"max 15 RPM/key). Please wait a few seconds before retrying."
1981
+ f"APINEX capacity limit reached ({total_rpm}/{self.max_total_rpm} RPM across "
1982
+ f"{len(self._keys)} keys, max 15 RPM/key). Please wait a few seconds before retrying."
1980
1983
  ),
1981
1984
  headers={"Retry-After": "5"},
1982
1985
  )
1983
1986
 
1984
- active_key.record_request(purpose=purpose, now=now)
1985
- return active_key
1987
+ start_idx = self._rr_idx % len(self._keys)
1988
+ ordered = sorted(
1989
+ candidates,
1990
+ key=lambda k: (k.active_requests, (self._keys.index(k) - start_idx) % len(self._keys)),
1991
+ )
1992
+ chosen = ordered[0]
1993
+ self._rr_idx = (self._keys.index(chosen) + 1) % len(self._keys)
1994
+ chosen.record_request(purpose=purpose, now=now)
1995
+ return chosen
1986
1996
 
1987
1997
  def release_key(
1988
1998
  self,
@@ -2016,7 +2026,6 @@ class ApinexKeyRotator:
2016
2026
  if self._keys and key_entry in self._keys:
2017
2027
  curr_idx = self._keys.index(key_entry)
2018
2028
  self._current_key_idx = (curr_idx + 1) % len(self._keys)
2019
- self._get_active_key_locked(now)
2020
2029
 
2021
2030
  self._try_dequeue_locked(now)
2022
2031
 
@@ -2067,25 +2076,32 @@ class ApinexKeyRotator:
2067
2076
  for k in self._keys:
2068
2077
  k.prune_expired(now)
2069
2078
 
2070
- active_key = self._get_active_key_locked(now)
2071
- if active_key is None:
2072
- return False
2073
-
2074
2079
  dequeued_any = False
2075
2080
  remaining: List[QueuedRequest] = []
2076
2081
  for req in self._waiting_queue:
2077
2082
  if req.is_cancelled:
2078
2083
  continue
2079
- if active_key.has_capacity(
2080
- purpose=req.purpose,
2081
- now=now,
2082
- user_limit=self.USER_RPM_PER_KEY,
2083
- reflection_limit=self.REFLECTION_RPM_PER_KEY,
2084
- total_limit=self.TOTAL_RPM_PER_KEY,
2085
- daily_limit=self.DAILY_RPD_PER_KEY,
2086
- ):
2087
- active_key.record_request(purpose=req.purpose, now=now)
2088
- req.chosen_key = active_key
2084
+ candidates = [
2085
+ k for k in self._keys
2086
+ if k.has_capacity(
2087
+ purpose=req.purpose,
2088
+ now=now,
2089
+ user_limit=self.USER_RPM_PER_KEY,
2090
+ reflection_limit=self.REFLECTION_RPM_PER_KEY,
2091
+ total_limit=self.TOTAL_RPM_PER_KEY,
2092
+ daily_limit=self.DAILY_RPD_PER_KEY,
2093
+ )
2094
+ ]
2095
+ if candidates:
2096
+ start_idx = self._rr_idx % len(self._keys)
2097
+ ordered = sorted(
2098
+ candidates,
2099
+ key=lambda k: (k.active_requests, (self._keys.index(k) - start_idx) % len(self._keys)),
2100
+ )
2101
+ chosen = ordered[0]
2102
+ self._rr_idx = (self._keys.index(chosen) + 1) % len(self._keys)
2103
+ chosen.record_request(purpose=req.purpose, now=now)
2104
+ req.chosen_key = chosen
2089
2105
  req.event.set()
2090
2106
  req.notify_event.set()
2091
2107
  dequeued_any = True
@@ -2175,18 +2191,29 @@ class ApinexKeyRotator:
2175
2191
  )
2176
2192
 
2177
2193
  if not self._waiting_queue:
2178
- if active_key.has_capacity(
2179
- purpose=purpose,
2180
- now=now,
2181
- user_limit=self.USER_RPM_PER_KEY,
2182
- reflection_limit=self.REFLECTION_RPM_PER_KEY,
2183
- total_limit=self.TOTAL_RPM_PER_KEY,
2184
- daily_limit=self.DAILY_RPD_PER_KEY,
2185
- ):
2186
- active_key.record_request(purpose=purpose, now=now)
2187
- if active_key.async_client is None:
2188
- self._init_clients(active_key)
2189
- ready_item = {"status": "ready", "client": active_key.async_client, "key_entry": active_key}
2194
+ candidates = [
2195
+ k for k in self._keys
2196
+ if k.has_capacity(
2197
+ purpose=purpose,
2198
+ now=now,
2199
+ user_limit=self.USER_RPM_PER_KEY,
2200
+ reflection_limit=self.REFLECTION_RPM_PER_KEY,
2201
+ total_limit=self.TOTAL_RPM_PER_KEY,
2202
+ daily_limit=self.DAILY_RPD_PER_KEY,
2203
+ )
2204
+ ]
2205
+ if candidates:
2206
+ start_idx = self._rr_idx % len(self._keys)
2207
+ ordered = sorted(
2208
+ candidates,
2209
+ key=lambda k: (k.active_requests, (self._keys.index(k) - start_idx) % len(self._keys)),
2210
+ )
2211
+ chosen = ordered[0]
2212
+ self._rr_idx = (self._keys.index(chosen) + 1) % len(self._keys)
2213
+ chosen.record_request(purpose=purpose, now=now)
2214
+ if chosen.async_client is None:
2215
+ self._init_clients(chosen)
2216
+ ready_item = {"status": "ready", "client": chosen.async_client, "key_entry": chosen}
2190
2217
 
2191
2218
  if ready_item is not None:
2192
2219
  yield ready_item
@@ -3217,7 +3217,10 @@ def get_model_catalog(user_tier: str = "all") -> dict:
3217
3217
  payload[key] = [it for it in payload[key] if _is_free_item(it)]
3218
3218
  else:
3219
3219
  # Non-free users:
3220
- # APINEX models are free gateway models (zero cost)they remain available on paid plans too.
3220
+ # APINEX models are free gateway models for free tier users exclude them from paid tier catalogs
3221
+ if not is_all_user:
3222
+ for key in ("main_agent", "plan_project", "subagent_text", "analyze_image", "image_gen", "all_text"):
3223
+ payload[key] = [it for it in payload[key] if str(it.get("provider", "")).lower() != "apinex"]
3221
3224
 
3222
3225
  # Paid plan: look up the Plan row's `allowed_models`
3223
3226
  try:
@@ -93,9 +93,9 @@ def is_model_allowed(model_id: str, allowed_models_str: str, bonus_balance: floa
93
93
  or (entry and getattr(entry, "provider", "").lower() == "apinex")
94
94
  )
95
95
 
96
- # 1. APINEX models: free gateway models allowed on ALL plans (free + paid)
96
+ # 1. APINEX models: free gateway models allowed on free plan (or with bonus credits/all)
97
97
  if is_apinex_model:
98
- return True
98
+ return is_free_plan or bonus_balance > 0.0 or allowed_models_str == "all"
99
99
 
100
100
  if bonus_balance > 0.0:
101
101
  return True
@@ -1407,9 +1407,8 @@ async def completions(
1407
1407
  detail="All OpenRouter API keys have exhausted paid credits (402) until 00:00 UTC (5:30 AM IST). Free models may still be available.",
1408
1408
  )
1409
1409
 
1410
- # ── 4. Non-free paid model 429: handle upstream vs daily quota ──
1410
+ # ── 4. Non-free paid model 429: rotate across remaining paid keys in the pool ──
1411
1411
  elif stream is None and key_entry and hasattr(key_entry, "key_id") and key_entry.key_id != "nvidia" and _is_429:
1412
- mark_model_rate_limited(req.model_id, cooldown_seconds=25.0)
1413
1412
  if _is_upstream_429:
1414
1413
  _purpose_str = "reflection" if is_refl else "user"
1415
1414
  global_key_rotator.unrecord_key(key_entry, purpose=_purpose_str)
@@ -1419,23 +1418,47 @@ async def completions(
1419
1418
  status_code=429,
1420
1419
  upstream_provider_429=True,
1421
1420
  )
1422
- key_entry = None
1423
- raise HTTPException(
1424
- status_code=429,
1425
- detail=(
1426
- f"Model '{req.model_id}' is temporarily rate-limited by the provider. "
1427
- f"Please wait ~25s before retrying, or switch to a different model with /model."
1428
- ),
1429
- headers={"Retry-After": "25"},
1430
- )
1431
1421
  else:
1432
1422
  global_key_rotator.demote_key_to_last(key_entry)
1433
1423
  global_key_rotator.release_key(key_entry, success=False, status_code=429, is_daily_quota_429=False)
1434
- key_entry = None
1424
+ key_entry = None
1425
+
1426
+ # Rotate across remaining available paid keys before giving up
1427
+ cand_paid_keys = [k for k in global_key_rotator._keys if not k.is_paid_exhausted(time.time())]
1428
+ for p_key in cand_paid_keys:
1429
+ if p_key.is_in_cooldown(time.time()):
1430
+ continue
1431
+ try:
1432
+ p_key.record_request(purpose=purpose, now=time.time())
1433
+ stream = await p_key.async_client.chat.completions.create(**kwargs)
1434
+ if stream:
1435
+ client = p_key.async_client
1436
+ key_entry = p_key
1437
+ logger.info(f"Paid model '{req.model_id}' succeeded after rotating to key {p_key.env_var}")
1438
+ break
1439
+ except Exception as p_err:
1440
+ p_st = getattr(p_err, "status_code", None)
1441
+ p_s = str(p_err).lower()
1442
+ if p_st == 402 or "402" in p_s or "payment required" in p_s:
1443
+ global_key_rotator.release_key(p_key, success=False, status_code=402)
1444
+ continue
1445
+ elif p_st == 429 or "429" in p_s or "rate" in p_s:
1446
+ global_key_rotator.demote_key_to_last(p_key)
1447
+ global_key_rotator.release_key(p_key, success=False, status_code=429)
1448
+ continue
1449
+ else:
1450
+ global_key_rotator.release_key(p_key, success=False)
1451
+ break
1452
+
1453
+ if stream is None:
1454
+ mark_model_rate_limited(req.model_id, cooldown_seconds=20.0)
1435
1455
  raise HTTPException(
1436
1456
  status_code=429,
1437
- detail="Provider temporary rate limit reached. Please wait a moment and retry your request.",
1438
- headers={"Retry-After": "15"},
1457
+ detail=(
1458
+ f"Model '{req.model_id}' is temporarily rate-limited across all provider keys. "
1459
+ f"Please wait ~20s before retrying, or switch to a different model with /model."
1460
+ ),
1461
+ headers={"Retry-After": "20"},
1439
1462
  )
1440
1463
 
1441
1464
  # 2. Sequential fallback attempts for 400 Bad Request / parameter incompatibilities
@@ -13,4 +13,5 @@ STATE = {
13
13
  "tools_expanded": False, # Toggle global tool results inline expand/collapse
14
14
  "thinking_topic": "", # Dynamic topic for the spinner
15
15
  "is_verified": False, # True if user has entered the secret code in this session
16
+ "dialog_active": False, # True when any modal/dialog is full screen or running in terminal
16
17
  }