utim-cli 2.4.2__tar.gz → 2.5.0__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 (204) hide show
  1. {utim_cli-2.4.2 → utim_cli-2.5.0}/CHANGELOG.md +23 -0
  2. {utim_cli-2.4.2/utim_cli.egg-info → utim_cli-2.5.0}/PKG-INFO +4 -4
  3. {utim_cli-2.4.2 → utim_cli-2.5.0}/pyproject.toml +4 -4
  4. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim/__init__.py +1 -1
  5. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/__init__.py +1 -1
  6. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/_version.py +1 -1
  7. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/architecture_indexer.py +112 -5
  8. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/ask_helper.py +88 -93
  9. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/blender_agent.py +38 -48
  10. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/client_utils.py +270 -32
  11. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/config.py +17 -18
  12. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/context_pruner.py +99 -26
  13. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/dependency_manager.py +3 -3
  14. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/orchestrator.py +41 -3
  15. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/orchestrator_helpers.py +88 -173
  16. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/orchestrator_llm_transport.py +39 -104
  17. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/orchestrator_turn_history.py +54 -39
  18. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/reflection.py +10 -13
  19. utim_cli-2.5.0/utim_cli/server/api_batcher.py +442 -0
  20. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/server/api_models_db.py +20 -4
  21. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/server/audit_log.py +1 -1
  22. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/server/auth.py +17 -9
  23. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/server/cli_auth.py +11 -3
  24. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/server/db.py +396 -1
  25. utim_cli-2.5.0/utim_cli/server/firewall.py +467 -0
  26. utim_cli-2.5.0/utim_cli/server/hobby_throttler.py +329 -0
  27. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/server/key_rotator.py +50 -23
  28. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/server/logging_config.py +82 -79
  29. utim_cli-2.5.0/utim_cli/server/maintenance.py +106 -0
  30. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/server/models.py +264 -11
  31. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/server/plan_integrity.py +18 -0
  32. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/server/platform_billing.py +116 -1
  33. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/server/rate_limit.py +15 -9
  34. utim_cli-2.5.0/utim_cli/server/request_batcher.py +510 -0
  35. utim_cli-2.5.0/utim_cli/server/request_queue.py +450 -0
  36. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/server/router.py +350 -86
  37. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/server/routes/__init__.py +3 -0
  38. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/server/routes/admin_bonus_routes.py +80 -0
  39. utim_cli-2.5.0/utim_cli/server/routes/admin_separated_models_routes.py +719 -0
  40. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/server/routes/api_gateway_extensions_routes.py +542 -544
  41. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/server/routes/api_models_admin_routes.py +8 -0
  42. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/server/routes/api_models_public_routes.py +692 -273
  43. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/server/routes/auth_routes.py +10 -2
  44. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/server/routes/completion_routes.py +285 -206
  45. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/server/routes/credit_routes.py +36 -37
  46. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/server/routes/provider_compat_routes.py +28 -2
  47. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/server/routes/quota_routes.py +11 -1
  48. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/server/routes/security_routes.py +13 -17
  49. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/tools.py +168 -113
  50. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/utim.py +140 -99
  51. {utim_cli-2.4.2 → utim_cli-2.5.0/utim_cli.egg-info}/PKG-INFO +4 -4
  52. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli.egg-info/SOURCES.txt +7 -0
  53. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli.egg-info/requires.txt +3 -3
  54. {utim_cli-2.4.2 → utim_cli-2.5.0}/LICENSE +0 -0
  55. {utim_cli-2.4.2 → utim_cli-2.5.0}/MANIFEST.in +0 -0
  56. {utim_cli-2.4.2 → utim_cli-2.5.0}/README.md +0 -0
  57. {utim_cli-2.4.2 → utim_cli-2.5.0}/setup.cfg +0 -0
  58. {utim_cli-2.4.2 → utim_cli-2.5.0}/setup.py +0 -0
  59. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/_chip_test.py +0 -0
  60. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/_verify_repro.py +0 -0
  61. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/_verify_syntax.py +0 -0
  62. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/_wizard_smoke_test.py +0 -0
  63. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/agent.py +0 -0
  64. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/auth.py +0 -0
  65. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/backup.py +0 -0
  66. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/billing.py +0 -0
  67. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/bootstrap.py +0 -0
  68. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/brain.py +0 -0
  69. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/builtin/skills/utim-guide/SKILL.md +0 -0
  70. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/constants.py +0 -0
  71. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/context_inspector.py +0 -0
  72. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/crawler/__init__.py +0 -0
  73. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/crawler/engine.py +0 -0
  74. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/crawler/markdown_distiller.py +0 -0
  75. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/doctor.py +0 -0
  76. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/evidence_registry.py +0 -0
  77. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/harbor.py +0 -0
  78. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/knowledge_graph.py +0 -0
  79. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/local_db.py +0 -0
  80. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/logger.py +0 -0
  81. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/mcp_clean_wrapper.py +0 -0
  82. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/mcp_client.py +0 -0
  83. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/mcp_registry.json +0 -0
  84. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/models.txt +0 -0
  85. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/orchestrator_tool_exec.py +0 -0
  86. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/personalities.py +0 -0
  87. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/platform_installer.py +0 -0
  88. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/plugins/__init__.py +0 -0
  89. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/plugins/hooks.py +0 -0
  90. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/plugins/loader.py +0 -0
  91. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/plugins/manager.py +0 -0
  92. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/plugins/registry.py +0 -0
  93. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/plugins/runtime.py +0 -0
  94. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/plugins/schema.py +0 -0
  95. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/plugins/tool_loader.py +0 -0
  96. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/report.py +0 -0
  97. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/scrapy_search.py +0 -0
  98. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/sdk/__init__.py +0 -0
  99. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/sdk/config.py +0 -0
  100. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/sdk/context.py +0 -0
  101. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/sdk/events.py +0 -0
  102. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/sdk/handlers.py +0 -0
  103. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/sdk/session.py +0 -0
  104. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/sdk/sidecar.py +0 -0
  105. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/server/__init__.py +0 -0
  106. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/server/admin_auth.py +0 -0
  107. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/server/attribution.py +0 -0
  108. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/server/batch_processor.py +0 -0
  109. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/server/captcha.py +0 -0
  110. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/server/concurrency.py +0 -0
  111. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/server/docs_md/SECRET_PROVISIONING.md +0 -0
  112. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/server/docs_md/about.md +0 -0
  113. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/server/docs_md/changelog.md +0 -0
  114. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/server/docs_md/docs.md +0 -0
  115. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/server/docs_md/features.md +0 -0
  116. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/server/docs_md/license.md +0 -0
  117. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/server/docs_md/pricing.md +0 -0
  118. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/server/docs_md/privacy.md +0 -0
  119. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/server/docs_md/refund.md +0 -0
  120. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/server/docs_md/support.md +0 -0
  121. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/server/docs_md/terms.md +0 -0
  122. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/server/email_utils.py +0 -0
  123. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/server/exchange_rate.py +0 -0
  124. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/server/firebase.py +0 -0
  125. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/server/fix_duplicate_users.py +0 -0
  126. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/server/history.py +0 -0
  127. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/server/local_llm.py +0 -0
  128. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/server/micro_batcher.py +0 -0
  129. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/server/model_agent.py +0 -0
  130. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/server/pricing_updater.py +0 -0
  131. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/server/provider_balance.py +0 -0
  132. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/server/provider_discovery.py +0 -0
  133. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/server/provider_router.py +0 -0
  134. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/server/provision_build.py +0 -0
  135. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/server/rewards_engine.py +0 -0
  136. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/server/routes/admin_db_routes.py +0 -0
  137. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/server/routes/admin_providers_routes.py +0 -0
  138. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/server/routes/api_key_routes.py +0 -0
  139. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/server/routes/billing_routes.py +0 -0
  140. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/server/routes/feedback_routes.py +0 -0
  141. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/server/routes/marketplace_routes.py +0 -0
  142. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/server/routes/quota_share_routes.py +0 -0
  143. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/server/routes/referral_routes.py +0 -0
  144. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/server/routes/rewards_routes.py +0 -0
  145. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/server/routes/session_routes.py +0 -0
  146. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/server/routes/share_routes.py +0 -0
  147. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/server/routes/wallet_routes.py +0 -0
  148. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/server/routes/webhook_routes.py +0 -0
  149. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/server/security_alerts.py +0 -0
  150. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/server/server.py +0 -0
  151. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/server/signup_guard.py +0 -0
  152. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/server/storage_nodes.py +0 -0
  153. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/server/utils.py +0 -0
  154. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/share.py +0 -0
  155. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/share_tui.py +0 -0
  156. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/situational_scoring.py +0 -0
  157. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/state.py +0 -0
  158. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/subagent_manager.py +0 -0
  159. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/syntax_fix.py +0 -0
  160. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/task_dispatcher.py +0 -0
  161. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/task_logger.py +0 -0
  162. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/tools_grep.py +0 -0
  163. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/tui/__init__.py +0 -0
  164. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/tui/compression_dialog.py +0 -0
  165. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/tui/feedback_dialog.py +0 -0
  166. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/tui/history_dialog.py +0 -0
  167. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/tui/marketplace_app_state.py +0 -0
  168. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/tui/marketplace_dialog.py +0 -0
  169. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/tui/marketplace_layout_engine.py +0 -0
  170. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/tui/mcp_dialog.py +0 -0
  171. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/tui/miniagents_dialog.py +0 -0
  172. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/tui/model_dialog.py +0 -0
  173. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/tui/personality_dialog.py +0 -0
  174. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/tui/plugins_dialog.py +0 -0
  175. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/tui/publish_dialog.py +0 -0
  176. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/tui/quota_dialog.py +0 -0
  177. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/tui/quota_redeem_dialog.py +0 -0
  178. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/tui/quota_share_dialog.py +0 -0
  179. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/tui/resume_dialog.py +0 -0
  180. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/tui/rewards_tui.py +0 -0
  181. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/tui/skills_dialog.py +0 -0
  182. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/tui/subagents_dialog.py +0 -0
  183. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/tui/tasks_dialog.py +0 -0
  184. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/tui/thinking_display.py +0 -0
  185. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/tui/tools_dialog.py +0 -0
  186. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/tui/update_dialog.py +0 -0
  187. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/update_checker.py +0 -0
  188. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/utilities.py +0 -0
  189. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/utimmodel.txt +0 -0
  190. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/vector_memory.py +0 -0
  191. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/voice.py +0 -0
  192. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/voice_coding.py +0 -0
  193. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/voice_events.py +0 -0
  194. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/voice_handler.py +0 -0
  195. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/voice_instant.py +0 -0
  196. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/voice_orchestrator.py +0 -0
  197. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/voice_session.py +0 -0
  198. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/voice_stt.py +0 -0
  199. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/voice_tts.py +0 -0
  200. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/wheel.py +0 -0
  201. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli/workspace.py +0 -0
  202. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli.egg-info/dependency_links.txt +0 -0
  203. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli.egg-info/entry_points.txt +0 -0
  204. {utim_cli-2.4.2 → utim_cli-2.5.0}/utim_cli.egg-info/top_level.txt +0 -0
@@ -1,5 +1,28 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.5.0] - 2026-10-18
4
+
5
+ ### ✨ Credit System → Unlimited UX + Internal Throttling
6
+ - **Paid = Unlimited Tokens (Rate-Limits Apply)**: All paid plans now display Unlimited Tokens; caps are enforced server-side as 5-hour refill + weekly fair-use throttling + Quota Bank rollover (never a hard credit wall).
7
+ - **Pricing & Docs Alignment**: Synced landing, docs, and prerender copy — Free stays 3,000 tokens/month.
8
+
9
+ ### 🐛 Fixes
10
+ - **Keyring Startup Spam**: Deduped OS-keyring warning to once-per-process and demoted to `debug` so adding 4 BYOK models no longer floods the terminal.
11
+ - **Firewall & Compression Hardening**: External firewall suites remain isolated; BYOK/custom compression routing fixed.
12
+
13
+ ### 🔧 Internal
14
+ - **BYOK / Custom Provider Compression**: `_resolve_compression_routing` now mirrors live `Orchestrator.model_source` so summarize/compress requests follow the same provider as the main model.
15
+
16
+ ## [2.4.3] - 2026-10-13
17
+
18
+ ### 🛡️ Process Immunity & Self-Kill Protection
19
+ - **Immunity Against Self-Termination**: Protected the UTIM process tree from accidental termination when executing shell commands like `taskkill`, `kill`, or `Stop-Process`. Resolves ancestry PIDs and blocks commands targeting own PID or executable names.
20
+
21
+ ### 🐛 Model Response & Streaming Fixes
22
+ - **Claude / Anthropic Completion Stability**: Fixed an issue where blank finish reasons on clean streaming completions defaulted to `"length"`, triggering spurious truncation warnings.
23
+ - **Removed Active-Turn `[status: pending]` Tagging**: Removed active user message status injection that previously confused models (especially Claude) into premature halts or continuation loops.
24
+ - **Backtick Heuristic Gating**: Gated unclosed code-block detection to only fire on non-clean completions, preventing false truncation triggers on prose responses.
25
+
3
26
  ## [2.4.2] - 2026-10-13
4
27
 
5
28
  ### 🤖 Sub-Agent Swarm Model Configuration & Hardcoded Lock
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: utim-cli
3
- Version: 2.4.2
3
+ Version: 2.5.0
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
@@ -20,12 +20,12 @@ Classifier: Intended Audience :: Developers
20
20
  Requires-Python: >=3.9
21
21
  Description-Content-Type: text/markdown
22
22
  License-File: LICENSE
23
- Requires-Dist: requests==2.31.0
23
+ Requires-Dist: requests>=2.32.3
24
24
  Requires-Dist: aiohttp>=3.9.5
25
25
  Requires-Dist: httpx>=0.25.0
26
26
  Requires-Dist: python-dotenv==1.0.1
27
- Requires-Dist: urllib3==1.26.18
28
- Requires-Dist: charset-normalizer==3.3.2
27
+ Requires-Dist: urllib3>=2.2.3
28
+ Requires-Dist: charset-normalizer>=3.3.2
29
29
  Requires-Dist: chardet==4.0.0
30
30
  Requires-Dist: typer>=0.9.0
31
31
  Requires-Dist: rich>=13.0.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "utim-cli"
7
- version = "2.4.2"
7
+ version = "2.5.0"
8
8
 
9
9
  description = "UTIM – Universal Terminal Intelligence Manager. A powerful agentic AI coding assistant for your terminal."
10
10
  readme = "README.md"
@@ -23,12 +23,12 @@ classifiers = [
23
23
  "Intended Audience :: Developers",
24
24
  ]
25
25
  dependencies = [
26
- "requests==2.31.0",
26
+ "requests>=2.32.3",
27
27
  "aiohttp>=3.9.5",
28
28
  "httpx>=0.25.0",
29
29
  "python-dotenv==1.0.1",
30
- "urllib3==1.26.18",
31
- "charset-normalizer==3.3.2",
30
+ "urllib3>=2.2.3",
31
+ "charset-normalizer>=3.3.2",
32
32
  "chardet==4.0.0",
33
33
  "typer>=0.9.0",
34
34
  "rich>=13.0.0",
@@ -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.4.2"
17
+ __version__ = "2.5.0"
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.4.2"
8
+ __version__ = "2.5.0"
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.4.2"
14
+ VERSION = "2.5.0"
15
15
 
16
16
 
@@ -93,18 +93,115 @@ def index_generic_file(filepath: Path) -> dict:
93
93
  except Exception:
94
94
  return {"imports": [], "symbols": []}
95
95
 
96
+ # ── Session-level in-memory cache: prevents re-indexing within the same process ──
97
+ _ARCH_LOCK_MEM_CACHE: dict = {}
98
+ # Reuse window: if the same project is locked again within this TTL, return the
99
+ # cached result instantly at zero cost. 600 s = 10 min covers an entire session.
100
+ _ARCH_LOCK_COOLDOWN_S = 600
101
+ # Disk cache TTL for the stable arch_lock dir — if the graph file is fresher than
102
+ # this, the project file tree is considered unchanged unless force-refreshed.
103
+ _ARCH_LOCK_DISK_TTL_S = 600
104
+
105
+
106
+ def _quick_file_tree_hash(project_path: str) -> str:
107
+ """Cheap hash of the project's file listing (names + mtimes) for change detection."""
108
+ import time as _t
109
+ h = hashlib.sha256()
110
+ try:
111
+ for root, dirs, files in os.walk(project_path):
112
+ # mirror _ignore_dir filtering so hash is stable
113
+ dirs[:] = [d for d in dirs if not _ignore_dir(d)]
114
+ for f in sorted(files):
115
+ if f.startswith('.'):
116
+ continue
117
+ fp = os.path.join(root, f)
118
+ try:
119
+ st = os.stat(fp)
120
+ h.update(f.encode())
121
+ h.update(str(int(st.st_mtime)).encode())
122
+ h.update(str(st.st_size).encode())
123
+ except Exception:
124
+ h.update(f.encode())
125
+ except Exception:
126
+ pass
127
+ return h.hexdigest()[:16]
128
+
129
+
96
130
  def architecture_lock(project_path: str = ".") -> str:
97
- import uuid
131
+ import time as _time
98
132
  from utim_cli.brain import get_current_resolved_project
99
133
  abs_path = os.path.abspath(project_path)
100
134
  if not os.path.isdir(abs_path):
101
135
  return f"Error: Project path '{abs_path}' does not exist or is not a directory."
102
136
 
103
137
  proj_id, proj_dir = get_current_resolved_project(abs_path)
104
- arch_hash = uuid.uuid4().hex[:8]
105
- arch_dir = proj_dir / f"arch_{arch_hash}"
138
+
139
+ # ── 1. In-memory session cache (zero-IO fast path) ───────────────────────
140
+ _now = _time.time()
141
+ _mem = _ARCH_LOCK_MEM_CACHE.get(proj_id)
142
+ if _mem and (_now - _mem.get("ts", 0) < _ARCH_LOCK_COOLDOWN_S):
143
+ _cached_dir = _mem.get("arch_dir")
144
+ _cached_result = _mem.get("result")
145
+ if _cached_dir and Path(_cached_dir).exists() and _cached_result:
146
+ # Fresh within the session — return cached output instantly.
147
+ return (
148
+ "[CACHED — architecture lock already generated this session; NOT re-indexed]\n"
149
+ + _cached_result
150
+ + "\n\n⚠️ Do NOT call architecture_lock again this session. "
151
+ "Use `read_file` on the component_<domain>.md blueprints listed above."
152
+ )
153
+
154
+ # ── 2. Stable single directory (replaces per-call arch_{uuid} spam) ───────
155
+ arch_dir = proj_dir / "arch_lock"
106
156
  arch_dir.mkdir(parents=True, exist_ok=True)
107
157
  brain_file = arch_dir / "00_architecture_map.json"
158
+ meta_file = arch_dir / ".arch_lock_meta.json"
159
+
160
+ # ── 3. Disk cache: reuse if the graph is fresh AND file tree unchanged ───
161
+ _tree_hash = _quick_file_tree_hash(abs_path)
162
+ try:
163
+ if brain_file.exists() and meta_file.exists():
164
+ _meta = json.loads(meta_file.read_text(encoding="utf-8"))
165
+ _age = _now - float(_meta.get("ts", 0))
166
+ _same_tree = _meta.get("tree_hash") == _tree_hash
167
+ if _same_tree and _age < _ARCH_LOCK_DISK_TTL_S:
168
+ # Rebuild the lightweight return string from the cached dir
169
+ # without re-walking / re-parsing the entire codebase.
170
+ _cached_mds = sorted([p.name for p in arch_dir.glob("component_*.md")])
171
+ if _cached_mds:
172
+ # Count files from cached graph
173
+ try:
174
+ _g = json.loads(brain_file.read_text(encoding="utf-8"))
175
+ _n_files = _g.get("total_files", "?")
176
+ _ptype = ", ".join(_g.get("project_type", [])) or "Unknown"
177
+ except Exception:
178
+ _n_files, _ptype = "?", "Unknown"
179
+ _bullets = "\n".join(
180
+ f"- {m}: [file:///{str((arch_dir / m).absolute()).replace(chr(92), '/')}]"
181
+ for m in _cached_mds
182
+ )
183
+ _result = (
184
+ f"✓ Architecture lock (CACHED — reused, not re-indexed) at: {arch_dir}\n"
185
+ f"Identified stack: {_ptype} | Indexed files: {_n_files} | Age: {int(_age)}s\n\n"
186
+ f"WHAT WAS PROVIDED:\n"
187
+ f"1. Structural Dependency Graph: `00_architecture_map.json`\n"
188
+ f"2. Architecture Conventions: `conventions_summary.txt`\n"
189
+ f"3. Modular Component Blueprints ({len(_cached_mds)} domain modules):\n{_bullets}\n\n"
190
+ f"WHY THIS IS NEEDED & NEXT ACTION:\n"
191
+ f"- Zero-token structural grounding eliminates hallucinations and token-wasting blind exploration.\n"
192
+ f"- NEXT STEP: Use `read_file` on the relevant `component_<domain>.md` blueprint above "
193
+ f"matching your task to inspect exact function/class signatures BEFORE editing.\n"
194
+ f"- Do NOT call architecture_lock again unless the project file tree has changed."
195
+ )
196
+ _ARCH_LOCK_MEM_CACHE[proj_id] = {"ts": _now, "arch_dir": str(arch_dir), "result": _result}
197
+ return (
198
+ "[CACHED — architecture lock reused from disk; NOT re-indexed]\n"
199
+ + _result
200
+ + "\n\n⚠️ Do NOT call architecture_lock again. "
201
+ "Use `read_file` on the component blueprints listed above."
202
+ )
203
+ except Exception:
204
+ pass # any cache failure → fall through to full index
108
205
 
109
206
  inventory = []
110
207
  project_type = []
@@ -250,12 +347,18 @@ def architecture_lock(project_path: str = ".") -> str:
250
347
  with open(brain_file, 'w', encoding='utf-8') as bf:
251
348
  json.dump(graph_data, bf, indent=2)
252
349
 
350
+ # Write meta sidecar for disk-cache invalidation (tree hash + timestamp)
351
+ try:
352
+ meta_file.write_text(json.dumps({"ts": _now, "tree_hash": _tree_hash}, indent=2), encoding="utf-8")
353
+ except Exception:
354
+ pass
355
+
253
356
  component_bullets = "\n".join([
254
357
  f"- {m}: [file:///{str(arch_dir.absolute() / m).replace(chr(92), '/')}]"
255
358
  for m in generated_mds
256
359
  ]) if generated_mds else "- None"
257
360
 
258
- return (
361
+ _result_str = (
259
362
  f"✓ Architecture lock successfully generated at: {arch_dir}\n"
260
363
  f"Identified stack: {', '.join(project_type) or 'Unknown'} | Indexed files: {len(inventory)}\n\n"
261
364
  f"WHAT WAS PROVIDED:\n"
@@ -264,6 +367,10 @@ def architecture_lock(project_path: str = ".") -> str:
264
367
  f"3. Modular Component Blueprints ({len(generated_mds)} domain modules):\n{component_bullets}\n\n"
265
368
  f"WHY THIS IS NEEDED & NEXT ACTION:\n"
266
369
  f"- Zero-token structural grounding eliminates hallucinations and token-wasting blind repo exploration.\n"
267
- f"- NEXT STEP: Use `read_file` on the relevant `component_<domain>.md` blueprint above matching your task to inspect exact function/class signatures and dependencies before reading or editing source code."
370
+ f"- NEXT STEP: Use `read_file` on the relevant `component_<domain>.md` blueprint above matching your task to inspect exact function/class signatures and dependencies before reading or editing source code.\n"
371
+ f"- Do NOT call architecture_lock again this session — reuse the blueprints above."
268
372
  )
373
+ # Populate in-memory cache so a second call this session is instant
374
+ _ARCH_LOCK_MEM_CACHE[proj_id] = {"ts": _now, "arch_dir": str(arch_dir), "result": _result_str}
375
+ return _result_str
269
376
 
@@ -24,14 +24,15 @@ from rich.text import Text
24
24
  from rich.markdown import Markdown
25
25
  from rich import box
26
26
 
27
- # ── Primary & Fallback Free Models ───────────────────────────────────────────
27
+ # ── Sidecar Helper Model ───────────────────────────────────────────
28
28
 
29
- from utim_cli.client_utils import get_free_models_fallback_chain
29
+ PRIMARY_HELPER_MODEL = "nvidia/nemotron-3.5-lightning:free"
30
+
31
+
32
+ def get_free_models_fallback_chain() -> List[str]:
33
+ """Return single primary helper model (fallback chain disabled)."""
34
+ return [PRIMARY_HELPER_MODEL]
30
35
 
31
- # Primary helper model = 'nvidia/nemotron-3.5-lightning:free' (verified free model), then falls
32
- # back through the verified free models from the server DB registry.
33
- _HELPER_FALLBACK_CHAIN = get_free_models_fallback_chain()
34
- PRIMARY_HELPER_MODEL = _HELPER_FALLBACK_CHAIN[0] if _HELPER_FALLBACK_CHAIN else "nvidia/nemotron-3.5-lightning:free"
35
36
 
36
37
 
37
38
 
@@ -339,7 +340,7 @@ def execute_ask_helper(
339
340
  if not console:
340
341
  console = Console(highlight=False, markup=True)
341
342
 
342
- models_to_try = get_free_models_fallback_chain()
343
+ model = PRIMARY_HELPER_MODEL
343
344
 
344
345
  # Pre-extract live session chat history and context
345
346
  chat_summary = read_chat_history(orchestrator, max_turns=12)
@@ -378,95 +379,89 @@ def execute_ask_helper(
378
379
  call_success = False
379
380
  last_error = ""
380
381
 
381
- for model in models_to_try:
382
- try:
383
- from utim_cli.client_utils import proxy_openrouter_request
384
- resp = proxy_openrouter_request(
385
- json_data={
386
- "model": model,
387
- "messages": messages,
388
- "tools": tools,
389
- "max_tokens": 1000,
390
- },
391
- stream=False,
392
- timeout=(15, 300),
393
- is_reflection=False,
394
- )
395
- if resp.status_code == 429:
396
- continue
397
- resp.raise_for_status()
398
-
399
- data = resp.json()
400
- choice = data.get("choices", [{}])[0]
401
- msg = choice.get("message", {})
402
- content = msg.get("content") or ""
403
- tool_calls = msg.get("tool_calls") or []
404
-
405
- # Clean thinking tags
406
- content = re.sub(r"<think(?:ing)?>.*?</think(?:ing)?>", "", content, flags=re.DOTALL).strip()
407
-
408
- if tool_calls:
409
- messages.append({
410
- "role": "assistant",
411
- "content": content,
412
- "tool_calls": tool_calls,
413
- })
414
-
415
- for tc in tool_calls:
416
- fn = tc.get("function", {})
417
- fname = fn.get("name", "")
418
- call_id = tc.get("id", "call_tool")
419
-
420
- if fname == "read_chat":
421
- args_str = fn.get("arguments", "{}")
422
- try:
423
- args = json.loads(args_str) if isinstance(args_str, str) else args_str
424
- except Exception:
425
- args = {}
426
- max_turns = args.get("max_turns", 15)
427
- chat_result = read_chat_history(orchestrator, max_turns=max_turns)
428
- messages.append({
429
- "role": "tool",
430
- "tool_call_id": call_id,
431
- "name": "read_chat",
432
- "content": chat_result,
433
- })
434
- elif fname in ("web_search", "websearch", "search"):
435
- args_str = fn.get("arguments", "{}")
436
- try:
437
- args = json.loads(args_str) if isinstance(args_str, str) else args_str
438
- except Exception:
439
- args = {}
440
- search_q = args.get("query") or args.get("prompt") or args.get("q") or question
441
- search_res = execute_sidecar_websearch(search_q)
442
- messages.append({
443
- "role": "tool",
444
- "tool_call_id": call_id,
445
- "name": fname,
446
- "content": search_res[:12000],
447
- })
448
- else:
449
- messages.append({
450
- "role": "tool",
451
- "tool_call_id": call_id,
452
- "name": fname,
453
- "content": f"Error: Tool '{fname}' is not allowed. Only 'read_chat' and 'web_search' are available.",
454
- })
455
- call_success = True
456
- break
457
-
458
- elif content:
459
- final_response = content
460
- call_success = True
461
- break
462
-
463
- except Exception as e:
464
- last_error = str(e)
465
- continue
382
+ try:
383
+ from utim_cli.client_utils import proxy_openrouter_request
384
+ resp = proxy_openrouter_request(
385
+ json_data={
386
+ "model": model,
387
+ "messages": messages,
388
+ "tools": tools,
389
+ "max_tokens": 1000,
390
+ },
391
+ stream=False,
392
+ timeout=(15, 300),
393
+ is_reflection=False,
394
+ )
395
+ resp.raise_for_status()
396
+
397
+ data = resp.json()
398
+ choice = data.get("choices", [{}])[0]
399
+ msg = choice.get("message", {})
400
+ content = msg.get("content") or ""
401
+ tool_calls = msg.get("tool_calls") or []
402
+
403
+ # Clean thinking tags
404
+ content = re.sub(r"<think(?:ing)?>.*?</think(?:ing)?>", "", content, flags=re.DOTALL).strip()
405
+
406
+ if tool_calls:
407
+ messages.append({
408
+ "role": "assistant",
409
+ "content": content,
410
+ "tool_calls": tool_calls,
411
+ })
412
+
413
+ for tc in tool_calls:
414
+ fn = tc.get("function", {})
415
+ fname = fn.get("name", "")
416
+ call_id = tc.get("id", "call_tool")
417
+
418
+ if fname == "read_chat":
419
+ args_str = fn.get("arguments", "{}")
420
+ try:
421
+ args = json.loads(args_str) if isinstance(args_str, str) else args_str
422
+ except Exception:
423
+ args = {}
424
+ max_turns = args.get("max_turns", 15)
425
+ chat_result = read_chat_history(orchestrator, max_turns=max_turns)
426
+ messages.append({
427
+ "role": "tool",
428
+ "tool_call_id": call_id,
429
+ "name": "read_chat",
430
+ "content": chat_result,
431
+ })
432
+ elif fname in ("web_search", "websearch", "search"):
433
+ args_str = fn.get("arguments", "{}")
434
+ try:
435
+ args = json.loads(args_str) if isinstance(args_str, str) else args_str
436
+ except Exception:
437
+ args = {}
438
+ search_q = args.get("query") or args.get("prompt") or args.get("q") or question
439
+ search_res = execute_sidecar_websearch(search_q)
440
+ messages.append({
441
+ "role": "tool",
442
+ "tool_call_id": call_id,
443
+ "name": fname,
444
+ "content": search_res[:12000],
445
+ })
446
+ else:
447
+ messages.append({
448
+ "role": "tool",
449
+ "tool_call_id": call_id,
450
+ "name": fname,
451
+ "content": f"Error: Tool '{fname}' is not allowed. Only 'read_chat' and 'web_search' are available.",
452
+ })
453
+ call_success = True
454
+
455
+ elif content:
456
+ final_response = content
457
+ call_success = True
458
+
459
+ except Exception as e:
460
+ last_error = str(e)
466
461
 
467
462
  if not call_success:
468
463
  if not final_response:
469
- final_response = f"I'm here to help, but encountered connection errors across free fallback models: {last_error}"
464
+ final_response = f"I'm here to help, but encountered connection errors with model '{model}': {last_error}"
470
465
  break
471
466
 
472
467
  if final_response:
@@ -54,42 +54,32 @@ from utim_cli.constants import DEFAULT_MODEL
54
54
 
55
55
  _OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions"
56
56
 
57
- _VISION_MODELS: List[str] = [
58
- "google/gemma-4-31b-it:free",
59
- "google/gemma-4-26b-a4b-it:free",
60
- "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free",
61
- "nvidia/nemotron-nano-12b-v2-vl:free",
62
- ]
63
-
64
- from utim_cli.client_utils import get_free_models_fallback_chain
65
-
66
- _CODE_MODELS: List[str] = get_free_models_fallback_chain()
67
-
68
-
57
+ _VISION_MODEL: str = "google/gemma-4-31b-it:free"
58
+ _CODE_MODEL: str = "nvidia/nemotron-3.5-lightning:free"
69
59
 
70
60
 
71
61
  def _get_vision_models() -> List[str]:
72
- """Return vision model list, with user override prepended if configured."""
62
+ """Return vision model (single model, no fallbacks)."""
73
63
  try:
74
64
  from utim_cli.config import config
75
65
  override = config.get("subagent_model_blender_vision")
76
66
  if override and override not in ("__non_agent__", "__none__"):
77
- return [override] + [m for m in _VISION_MODELS if m != override]
67
+ return [override]
78
68
  except Exception:
79
69
  pass
80
- return _VISION_MODELS
70
+ return [_VISION_MODEL]
81
71
 
82
72
 
83
73
  def _get_code_models() -> List[str]:
84
- """Return code model list, with user override prepended if configured."""
74
+ """Return code model (single model, no fallbacks)."""
85
75
  try:
86
76
  from utim_cli.config import config
87
77
  override = config.get("subagent_model_blender_code")
88
78
  if override and override not in ("__non_agent__", "__none__"):
89
- return [override] + [m for m in _CODE_MODELS if m != override]
79
+ return [override]
90
80
  except Exception:
91
81
  pass
92
- return _CODE_MODELS
82
+ return [_CODE_MODEL]
93
83
 
94
84
  MAX_RETRIES = 3 # Script fix-and-retry attempts
95
85
 
@@ -552,36 +542,36 @@ def _llm_call(
552
542
  else:
553
543
  user_content = user_text
554
544
 
555
- last_err: Exception = RuntimeError("No models tried.")
556
- for model in models:
557
- for attempt in range(3):
558
- try:
559
- from utim_cli.client_utils import proxy_openrouter_request
560
- resp = proxy_openrouter_request(
561
- json_data={
562
- "model": model,
563
- "messages": [
564
- {"role": "system", "content": system},
565
- {"role": "user", "content": user_content},
566
- ],
567
- "max_tokens": max_tokens,
568
- "stream": False,
569
- },
570
- stream=False,
571
- timeout=timeout,
572
- )
573
- resp.raise_for_status()
574
- data = resp.json()
575
- text = data["choices"][0]["message"]["content"]
576
- return text.strip()
577
- except Exception as exc: # noqa: BLE001
578
- last_err = exc
579
- code = getattr(getattr(exc, "response", None), "status_code", 0)
580
- if code == 429 and attempt < 2:
581
- time.sleep(5 * (attempt + 1))
582
- continue
583
- break # try next model
584
- raise RuntimeError(f"All LLM models failed. Last error: {last_err}") from last_err
545
+ last_err: Exception = RuntimeError("No model specified.")
546
+ model = models[0] if models else _CODE_MODEL
547
+ for attempt in range(3):
548
+ try:
549
+ from utim_cli.client_utils import proxy_openrouter_request
550
+ resp = proxy_openrouter_request(
551
+ json_data={
552
+ "model": model,
553
+ "messages": [
554
+ {"role": "system", "content": system},
555
+ {"role": "user", "content": user_content},
556
+ ],
557
+ "max_tokens": max_tokens,
558
+ "stream": False,
559
+ },
560
+ stream=False,
561
+ timeout=timeout,
562
+ )
563
+ resp.raise_for_status()
564
+ data = resp.json()
565
+ text = data["choices"][0]["message"]["content"]
566
+ return text.strip()
567
+ except Exception as exc: # noqa: BLE001
568
+ last_err = exc
569
+ code = getattr(getattr(exc, "response", None), "status_code", 0)
570
+ if code == 429 and attempt < 2:
571
+ time.sleep(5 * (attempt + 1))
572
+ continue
573
+ break
574
+ raise RuntimeError(f"LLM model '{model}' failed: {last_err}") from last_err
585
575
 
586
576
 
587
577
  # ---------------------------------------------------------------------------