utim-cli 2.4.2__tar.gz → 2.4.3__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 (197) hide show
  1. {utim_cli-2.4.2 → utim_cli-2.4.3}/CHANGELOG.md +10 -0
  2. {utim_cli-2.4.2/utim_cli.egg-info → utim_cli-2.4.3}/PKG-INFO +1 -1
  3. {utim_cli-2.4.2 → utim_cli-2.4.3}/pyproject.toml +1 -1
  4. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim/__init__.py +1 -1
  5. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/__init__.py +1 -1
  6. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/_version.py +1 -1
  7. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/orchestrator.py +11 -3
  8. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/orchestrator_llm_transport.py +12 -3
  9. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/orchestrator_turn_history.py +6 -8
  10. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/api_models_db.py +0 -3
  11. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/audit_log.py +1 -1
  12. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/router.py +214 -63
  13. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/routes/api_models_public_routes.py +6 -52
  14. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/tools.py +137 -0
  15. {utim_cli-2.4.2 → utim_cli-2.4.3/utim_cli.egg-info}/PKG-INFO +1 -1
  16. {utim_cli-2.4.2 → utim_cli-2.4.3}/LICENSE +0 -0
  17. {utim_cli-2.4.2 → utim_cli-2.4.3}/MANIFEST.in +0 -0
  18. {utim_cli-2.4.2 → utim_cli-2.4.3}/README.md +0 -0
  19. {utim_cli-2.4.2 → utim_cli-2.4.3}/setup.cfg +0 -0
  20. {utim_cli-2.4.2 → utim_cli-2.4.3}/setup.py +0 -0
  21. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/_chip_test.py +0 -0
  22. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/_verify_repro.py +0 -0
  23. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/_verify_syntax.py +0 -0
  24. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/_wizard_smoke_test.py +0 -0
  25. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/agent.py +0 -0
  26. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/architecture_indexer.py +0 -0
  27. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/ask_helper.py +0 -0
  28. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/auth.py +0 -0
  29. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/backup.py +0 -0
  30. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/billing.py +0 -0
  31. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/blender_agent.py +0 -0
  32. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/bootstrap.py +0 -0
  33. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/brain.py +0 -0
  34. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/builtin/skills/utim-guide/SKILL.md +0 -0
  35. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/client_utils.py +0 -0
  36. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/config.py +0 -0
  37. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/constants.py +0 -0
  38. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/context_inspector.py +0 -0
  39. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/context_pruner.py +0 -0
  40. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/crawler/__init__.py +0 -0
  41. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/crawler/engine.py +0 -0
  42. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/crawler/markdown_distiller.py +0 -0
  43. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/dependency_manager.py +0 -0
  44. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/doctor.py +0 -0
  45. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/evidence_registry.py +0 -0
  46. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/harbor.py +0 -0
  47. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/knowledge_graph.py +0 -0
  48. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/local_db.py +0 -0
  49. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/logger.py +0 -0
  50. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/mcp_clean_wrapper.py +0 -0
  51. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/mcp_client.py +0 -0
  52. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/mcp_registry.json +0 -0
  53. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/models.txt +0 -0
  54. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/orchestrator_helpers.py +0 -0
  55. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/orchestrator_tool_exec.py +0 -0
  56. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/personalities.py +0 -0
  57. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/platform_installer.py +0 -0
  58. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/plugins/__init__.py +0 -0
  59. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/plugins/hooks.py +0 -0
  60. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/plugins/loader.py +0 -0
  61. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/plugins/manager.py +0 -0
  62. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/plugins/registry.py +0 -0
  63. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/plugins/runtime.py +0 -0
  64. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/plugins/schema.py +0 -0
  65. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/plugins/tool_loader.py +0 -0
  66. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/reflection.py +0 -0
  67. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/report.py +0 -0
  68. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/scrapy_search.py +0 -0
  69. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/sdk/__init__.py +0 -0
  70. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/sdk/config.py +0 -0
  71. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/sdk/context.py +0 -0
  72. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/sdk/events.py +0 -0
  73. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/sdk/handlers.py +0 -0
  74. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/sdk/session.py +0 -0
  75. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/sdk/sidecar.py +0 -0
  76. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/__init__.py +0 -0
  77. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/admin_auth.py +0 -0
  78. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/attribution.py +0 -0
  79. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/auth.py +0 -0
  80. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/batch_processor.py +0 -0
  81. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/captcha.py +0 -0
  82. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/cli_auth.py +0 -0
  83. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/concurrency.py +0 -0
  84. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/db.py +0 -0
  85. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/docs_md/SECRET_PROVISIONING.md +0 -0
  86. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/docs_md/about.md +0 -0
  87. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/docs_md/changelog.md +0 -0
  88. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/docs_md/docs.md +0 -0
  89. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/docs_md/features.md +0 -0
  90. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/docs_md/license.md +0 -0
  91. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/docs_md/pricing.md +0 -0
  92. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/docs_md/privacy.md +0 -0
  93. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/docs_md/refund.md +0 -0
  94. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/docs_md/support.md +0 -0
  95. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/docs_md/terms.md +0 -0
  96. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/email_utils.py +0 -0
  97. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/exchange_rate.py +0 -0
  98. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/firebase.py +0 -0
  99. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/fix_duplicate_users.py +0 -0
  100. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/history.py +0 -0
  101. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/key_rotator.py +0 -0
  102. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/local_llm.py +0 -0
  103. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/logging_config.py +0 -0
  104. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/micro_batcher.py +0 -0
  105. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/model_agent.py +0 -0
  106. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/models.py +0 -0
  107. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/plan_integrity.py +0 -0
  108. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/platform_billing.py +0 -0
  109. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/pricing_updater.py +0 -0
  110. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/provider_balance.py +0 -0
  111. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/provider_discovery.py +0 -0
  112. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/provider_router.py +0 -0
  113. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/provision_build.py +0 -0
  114. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/rate_limit.py +0 -0
  115. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/rewards_engine.py +0 -0
  116. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/routes/__init__.py +0 -0
  117. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/routes/admin_bonus_routes.py +0 -0
  118. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/routes/admin_db_routes.py +0 -0
  119. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/routes/admin_providers_routes.py +0 -0
  120. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/routes/api_gateway_extensions_routes.py +0 -0
  121. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/routes/api_key_routes.py +0 -0
  122. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/routes/api_models_admin_routes.py +0 -0
  123. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/routes/auth_routes.py +0 -0
  124. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/routes/billing_routes.py +0 -0
  125. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/routes/completion_routes.py +0 -0
  126. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/routes/credit_routes.py +0 -0
  127. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/routes/feedback_routes.py +0 -0
  128. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/routes/marketplace_routes.py +0 -0
  129. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/routes/provider_compat_routes.py +0 -0
  130. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/routes/quota_routes.py +0 -0
  131. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/routes/quota_share_routes.py +0 -0
  132. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/routes/referral_routes.py +0 -0
  133. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/routes/rewards_routes.py +0 -0
  134. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/routes/security_routes.py +0 -0
  135. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/routes/session_routes.py +0 -0
  136. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/routes/share_routes.py +0 -0
  137. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/routes/wallet_routes.py +0 -0
  138. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/routes/webhook_routes.py +0 -0
  139. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/security_alerts.py +0 -0
  140. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/server.py +0 -0
  141. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/signup_guard.py +0 -0
  142. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/storage_nodes.py +0 -0
  143. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/utils.py +0 -0
  144. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/share.py +0 -0
  145. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/share_tui.py +0 -0
  146. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/situational_scoring.py +0 -0
  147. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/state.py +0 -0
  148. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/subagent_manager.py +0 -0
  149. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/syntax_fix.py +0 -0
  150. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/task_dispatcher.py +0 -0
  151. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/task_logger.py +0 -0
  152. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/tools_grep.py +0 -0
  153. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/tui/__init__.py +0 -0
  154. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/tui/compression_dialog.py +0 -0
  155. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/tui/feedback_dialog.py +0 -0
  156. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/tui/history_dialog.py +0 -0
  157. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/tui/marketplace_app_state.py +0 -0
  158. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/tui/marketplace_dialog.py +0 -0
  159. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/tui/marketplace_layout_engine.py +0 -0
  160. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/tui/mcp_dialog.py +0 -0
  161. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/tui/miniagents_dialog.py +0 -0
  162. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/tui/model_dialog.py +0 -0
  163. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/tui/personality_dialog.py +0 -0
  164. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/tui/plugins_dialog.py +0 -0
  165. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/tui/publish_dialog.py +0 -0
  166. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/tui/quota_dialog.py +0 -0
  167. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/tui/quota_redeem_dialog.py +0 -0
  168. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/tui/quota_share_dialog.py +0 -0
  169. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/tui/resume_dialog.py +0 -0
  170. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/tui/rewards_tui.py +0 -0
  171. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/tui/skills_dialog.py +0 -0
  172. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/tui/subagents_dialog.py +0 -0
  173. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/tui/tasks_dialog.py +0 -0
  174. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/tui/thinking_display.py +0 -0
  175. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/tui/tools_dialog.py +0 -0
  176. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/tui/update_dialog.py +0 -0
  177. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/update_checker.py +0 -0
  178. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/utilities.py +0 -0
  179. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/utim.py +0 -0
  180. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/utimmodel.txt +0 -0
  181. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/vector_memory.py +0 -0
  182. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/voice.py +0 -0
  183. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/voice_coding.py +0 -0
  184. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/voice_events.py +0 -0
  185. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/voice_handler.py +0 -0
  186. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/voice_instant.py +0 -0
  187. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/voice_orchestrator.py +0 -0
  188. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/voice_session.py +0 -0
  189. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/voice_stt.py +0 -0
  190. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/voice_tts.py +0 -0
  191. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/wheel.py +0 -0
  192. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/workspace.py +0 -0
  193. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli.egg-info/SOURCES.txt +0 -0
  194. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli.egg-info/dependency_links.txt +0 -0
  195. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli.egg-info/entry_points.txt +0 -0
  196. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli.egg-info/requires.txt +0 -0
  197. {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli.egg-info/top_level.txt +0 -0
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.4.3] - 2026-10-13
4
+
5
+ ### 🛡️ Process Immunity & Self-Kill Protection
6
+ - **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.
7
+
8
+ ### 🐛 Model Response & Streaming Fixes
9
+ - **Claude / Anthropic Completion Stability**: Fixed an issue where blank finish reasons on clean streaming completions defaulted to `"length"`, triggering spurious truncation warnings.
10
+ - **Removed Active-Turn `[status: pending]` Tagging**: Removed active user message status injection that previously confused models (especially Claude) into premature halts or continuation loops.
11
+ - **Backtick Heuristic Gating**: Gated unclosed code-block detection to only fire on non-clean completions, preventing false truncation triggers on prose responses.
12
+
3
13
  ## [2.4.2] - 2026-10-13
4
14
 
5
15
  ### 🤖 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.4.3
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.4.2"
7
+ version = "2.4.3"
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.4.2"
17
+ __version__ = "2.4.3"
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.4.3"
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.4.3"
15
15
 
16
16
 
@@ -1039,7 +1039,15 @@ class Orchestrator(LLMTransportMixin, TurnHistoryMixin, ToolExecutionMixin):
1039
1039
  # If the model response was cut off mid-turn due to length/token limits, nudge it to continue
1040
1040
  # ONLY trigger when the response was genuinely truncated by token limits (never when finish_reason is "stop" or "end_turn")
1041
1041
  _fr = msg.get("finish_reason") or "stop"
1042
- _has_unclosed_code = bool(content and content.count("```") % 2 != 0)
1042
+ # Unclosed code-block detection: only meaningful as a truncation signal when the
1043
+ # model didn't explicitly say "stop" / "end_turn". If the model cleanly ended its
1044
+ # response, odd backtick count is simply normal content (e.g. mentioning ``` in prose).
1045
+ _clean_stop = _fr in ("stop", "end_turn")
1046
+ _has_unclosed_code = bool(
1047
+ content
1048
+ and not _clean_stop
1049
+ and content.count("```") % 2 != 0
1050
+ )
1043
1051
  _is_truly_token_truncated = (
1044
1052
  not msg.get("aborted")
1045
1053
  and not tool_calls
@@ -1050,8 +1058,8 @@ class Orchestrator(LLMTransportMixin, TurnHistoryMixin, ToolExecutionMixin):
1050
1058
  or _has_unclosed_code
1051
1059
  )
1052
1060
  )
1053
- # If it explicitly said stop, only override if it's obviously cut off (unclosed code) or hit limits
1054
- if _fr in ("stop", "end_turn") and not _has_unclosed_code and not bool(msg.get("was_truncated_by_limit")):
1061
+ # If it explicitly said stop/end_turn, never treat as truncated unless token limit was hit
1062
+ if _clean_stop and not bool(msg.get("was_truncated_by_limit")):
1055
1063
  _is_truly_token_truncated = False
1056
1064
  if _is_truly_token_truncated:
1057
1065
  self.console.print("\n[bold yellow]Response truncated by token limits. Continuing response...[/bold yellow]\n")
@@ -2143,10 +2143,19 @@ class LLMTransportMixin:
2143
2143
  if _stream_finish_reason in ("stop", "end_turn") and not _was_truncated_by_limit:
2144
2144
  was_cut_off = False
2145
2145
 
2146
- # If the stream was severed abruptly (no finish_reason ever arrived),
2147
- # default to 'length' so the orchestrator auto-continues the cutoff text.
2146
+ # If the stream ended without an explicit finish_reason, decide whether
2147
+ # it was a clean completion or an abrupt disconnect:
2148
+ # - If content or tool calls were produced → the model finished naturally
2149
+ # (common with Anthropic Claude which often omits finish_reason on
2150
+ # clean stop). Default to "stop" so we don't falsely trigger
2151
+ # "Response truncated" and inject an unwanted continuation nudge.
2152
+ # - If NOTHING was produced → truly abrupt disconnect; default to
2153
+ # "length" so the orchestrator will retry / continue.
2148
2154
  if not _stream_finish_reason:
2149
- _stream_finish_reason = "length"
2155
+ if clean_content or final_tool_calls:
2156
+ _stream_finish_reason = "stop"
2157
+ else:
2158
+ _stream_finish_reason = "length"
2150
2159
 
2151
2160
  _not_avail_str = "This model isn't currently available on UTIM in free plan"
2152
2161
  if clean_content and _not_avail_str in clean_content:
@@ -1076,14 +1076,12 @@ class TurnHistoryMixin:
1076
1076
  tagged_past.append(m_copy)
1077
1077
  past_slice = tagged_past
1078
1078
 
1079
- if cur_msgs:
1080
- tagged_cur = []
1081
- for m in cur_msgs:
1082
- m_copy = dict(m)
1083
- if m.get("role") == "user" and not is_compression_artifact_message(m) and not is_system_notification_message(m):
1084
- m_copy["content"] = _tag_content(m.get("content"), "pending")
1085
- tagged_cur.append(m_copy)
1086
- cur_msgs = tagged_cur
1079
+ # NOTE: We intentionally do NOT tag current-turn user messages with [status: pending].
1080
+ # Past-turn messages use [status: done/aborted] to help the model understand history,
1081
+ # but tagging the ACTIVE user message with [status: pending] confuses some models
1082
+ # (especially Claude) — they misread it as a meta-directive meaning the task is
1083
+ # incomplete, which triggers premature stopping or unnecessary looping.
1084
+ # The current user message is already "live" by position; no status tag is needed.
1087
1085
 
1088
1086
  def _quick_token_est(msgs):
1089
1087
  try:
@@ -1275,9 +1275,6 @@ def init_api_models_db():
1275
1275
  logger.error(f"Error initializing utim_models db: {exc}")
1276
1276
 
1277
1277
 
1278
- init_api_models_db()
1279
-
1280
-
1281
1278
  def get_api_db():
1282
1279
  """FastAPI dependency for obtaining a Session to the utim_models database."""
1283
1280
  db = ApiSessionLocal()
@@ -30,7 +30,7 @@ import os
30
30
  import re
31
31
  from typing import Any, Optional
32
32
 
33
- from .db import SessionLocal, Base, Column, String, DateTime, Text, BigInteger, create_engine, DATABindError # type: ignore # noqa: F401
33
+ from .db import SessionLocal, Base, Column, String, DateTime, Text, BigInteger, create_engine
34
34
 
35
35
  logger = logging.getLogger("utim.audit")
36
36
 
@@ -53,24 +53,32 @@ from .auth import get_admin_user
53
53
  configure_logging(level=os.environ.get("LOG_LEVEL", "INFO"))
54
54
  logger = logging.getLogger("utim.server")
55
55
 
56
- # ── Database init ─────────────────────────────────────────────────────────────
56
+ # ── Maintenance Mode Switch ─────────────────────────────────────────────────
57
+ # Set UTIM_MAINTENANCE_MODE=1 (or true/on/yes) in Railway env to put the API
58
+ # into maintenance mode without redeploying code. When enabled:
59
+ # • /health still returns 200 (so Railway doesn't kill the instance) but
60
+ # reports {"status":"maintenance"}
61
+ # • every other route returns 503 with Retry-After, except admin bypass
62
+ # paths and requests carrying X-UTIM-Maintenance-Bypass == UTIM_MAINTENANCE_BYPASS_TOKEN
63
+ # You can also toggle at runtime via POST /admin/maintenance (admin auth) which
64
+ # overrides the env var in-memory until the next restart.
65
+
66
+ _maintenance_override: bool | None = None
67
+
68
+ def _is_maintenance_mode() -> bool:
69
+ if _maintenance_override is not None:
70
+ return _maintenance_override
71
+ return os.environ.get("UTIM_MAINTENANCE_MODE", "0").strip().lower() in ("1", "true", "on", "yes")
72
+
73
+ # ── Database init & startup sweeps ──────────────────────────────────────────
74
+ # Heavy DB work (init_db, auto-degrade, reconciliation) MUST NOT run at import
75
+ # time — it blocks uvicorn from binding to $PORT, so Railway health checks
76
+ # see "service unavailable" until the DB job finishes (or forever if Postgres
77
+ # is cold). All sweeps are deferred to the lifespan startup hook below, where
78
+ # they run after the port is already listening.
57
79
 
58
- init_db()
59
-
60
-
61
- # ── Auto-degrade sweep on startup ─────────────────────────────────────────────
62
- # Sweep all active paid subscriptions whose `current_period_end` has passed
63
- # and downgrade them to the Free plan. Without this, a user who never makes
64
- # a request after their plan expires would stay on the paid plan in the DB
65
- # forever, because the per-request auto-degrade in `process_pending_refills`
66
- # only fires when the user actually calls /quota or /v1/chat/completions.
67
80
  def _auto_degrade_sweep() -> int:
68
- """One-shot sweep: downgrade any user whose paid period has ended.
69
-
70
- Returns the number of users downgraded. Safe to call repeatedly; only
71
- touches users who are still flagged as ``status='active'`` on a paid plan
72
- but whose ``current_period_end`` is in the past.
73
- """
81
+ """One-shot sweep: downgrade any user whose paid period has ended."""
74
82
  try:
75
83
  import datetime
76
84
  from .db import UserSubscription
@@ -110,16 +118,6 @@ def _auto_degrade_sweep() -> int:
110
118
  return 0
111
119
 
112
120
 
113
- # Fire the sweep once at import time. Wrapped in try/except so a transient
114
- # DB hiccup at boot never prevents the server from coming up.
115
- try:
116
- _auto_degraded = _auto_degrade_sweep()
117
- if _auto_degraded:
118
- logger.info("startup_auto_degrade_done count=%s", _auto_degraded)
119
- except Exception as _ex:
120
- logger.error("startup_auto_degrade_init_failed err=%s", _ex)
121
-
122
-
123
121
  def _subscription_reconcile_sweep() -> int:
124
122
  """Audit and reconcile active subscriptions on boot: downgrades unpaid paid tiers to free."""
125
123
  try:
@@ -144,14 +142,6 @@ def _subscription_reconcile_sweep() -> int:
144
142
  logger.error("subscription_reconcile_sweep_failed err=%s", _ex)
145
143
  return 0
146
144
 
147
-
148
- try:
149
- _reconciled = _subscription_reconcile_sweep()
150
- if _reconciled:
151
- logger.info("startup_subscription_reconcile_done count=%s", _reconciled)
152
- except Exception as _ex:
153
- logger.error("startup_subscription_reconcile_init_failed err=%s", _ex)
154
-
155
145
  # ── App Lifespan — startup / shutdown hooks ───────────────────────────────────
156
146
 
157
147
  from contextlib import asynccontextmanager
@@ -160,28 +150,40 @@ import asyncio as _asyncio
160
150
 
161
151
  @asynccontextmanager
162
152
  async def _lifespan(app):
163
- """Startup: ensure key rotator background tasks are running.
164
- A watchdog coroutine restarts the credit poller and queue watcher if they die.
153
+ """Startup: DB init + background tasks. The port is already bound when this
154
+ runs (uvicorn lifespan), so heavy DB work here no longer blocks health checks.
165
155
  """
156
+ import threading as _threading
157
+
158
+ def _bg_db_bootstrap():
159
+ try:
160
+ from .db import init_db as _init_db
161
+ _init_db()
162
+ logger.info("lifespan_db_init_done")
163
+ except Exception as _e:
164
+ logger.warning("lifespan_db_init_error err=%s", _e)
165
+ for _fn, _label in ((_auto_degrade_sweep, "auto_degrade"), (_subscription_reconcile_sweep, "reconcile")):
166
+ try:
167
+ n = _fn()
168
+ if n:
169
+ logger.info("lifespan_%s_done count=%s", _label, n)
170
+ except Exception as _e:
171
+ logger.warning("lifespan_%s_error err=%s", _label, _e)
172
+ try:
173
+ from .api_models_db import init_api_models_db as _init_api
174
+ _init_api()
175
+ except Exception as _e:
176
+ logger.debug("lifespan_api_models_init_notice err=%s", _e)
177
+
178
+ _threading.Thread(target=_bg_db_bootstrap, daemon=True, name="utim-startup-bootstrap").start()
179
+
166
180
  from .key_rotator import global_key_rotator
167
181
 
168
- # Ensure noisy external HTTP loggers remain quiet
169
182
  for quiet_logger in ("httpx", "httpcore", "uvicorn.access", "sqlalchemy.engine"):
170
183
  logging.getLogger(quiet_logger).setLevel(logging.WARNING)
171
184
 
172
- # Ensure credit poller is started (idempotent)
173
185
  global_key_rotator._ensure_credit_poller()
174
186
 
175
- # Reconcile subscriptions on startup
176
- _subscription_reconcile_sweep()
177
-
178
- # Ensure canonical utim_models & utim_model_providers tables exist
179
- try:
180
- from .api_models_db import init_api_models_db
181
- init_api_models_db()
182
- except Exception as _e:
183
- logger.debug("startup_api_models_init_notice err=%s", _e)
184
-
185
187
  async def _watchdog():
186
188
  """Restarts key-rotator background tasks if they die unexpectedly."""
187
189
  while True:
@@ -296,9 +298,52 @@ class SecurityHeadersMiddleware(BaseHTTPMiddleware):
296
298
  return response
297
299
 
298
300
 
301
+ class MaintenanceMiddleware(BaseHTTPMiddleware):
302
+ """When maintenance mode is on, short-circuit most requests with 503.
303
+
304
+ Whitelisted paths (health, maintenance toggle itself, docs) stay available.
305
+ Admins can bypass with header X-UTIM-Maintenance-Bypass == UTIM_MAINTENANCE_BYPASS_TOKEN.
306
+ /health intentionally stays 200 so Railway health checks don't kill the instance.
307
+ """
308
+
309
+ _BYPASS_PREFIXES = (
310
+ "/health",
311
+ "/admin/maintenance",
312
+ "/docs",
313
+ "/redoc",
314
+ "/openapi.json",
315
+ )
316
+
317
+ async def dispatch(self, request, call_next):
318
+ if _is_maintenance_mode():
319
+ path = request.url.path
320
+ # Header bypass for admin automation
321
+ bypass_token = os.environ.get("UTIM_MAINTENANCE_BYPASS_TOKEN", "")
322
+ header_bypass = request.headers.get("x-utim-maintenance-bypass", "")
323
+ if bypass_token and header_bypass and header_bypass == bypass_token:
324
+ return await call_next(request)
325
+ # Whitelisted paths stay reachable
326
+ if any(path == p or path.startswith(p.rstrip("/")) for p in self._BYPASS_PREFIXES):
327
+ if path == "/health" or path.startswith("/health"):
328
+ return await call_next(request)
329
+ # allow toggle/docs through
330
+ return await call_next(request)
331
+ return JSONResponse(
332
+ status_code=503,
333
+ content={
334
+ "status": "maintenance",
335
+ "detail": "API is in maintenance mode. Please retry shortly.",
336
+ "retry_after": 60,
337
+ },
338
+ headers={"Retry-After": "60"},
339
+ )
340
+ return await call_next(request)
341
+
342
+
299
343
  # Inner middlewares
300
344
  app.add_middleware(LimitUploadSize)
301
345
  app.add_middleware(SecurityHeadersMiddleware)
346
+ app.add_middleware(MaintenanceMiddleware)
302
347
 
303
348
  # CLI signature verification — rejects requests that don't carry a valid
304
349
  # X-UTIM-CLI-Signature for protected routes when UTIM_REQUIRE_CLI_SIGNATURE=1.
@@ -651,23 +696,73 @@ def _find_changelog_path():
651
696
 
652
697
  @app.get("/health", tags=["system"], summary="Health check")
653
698
  async def health():
654
- """Always returns 200 so Railway health checks pass.
655
- DB status is reported in the body for diagnostics.
699
+ """Always returns 200 so Railway health checks pass instantly.
700
+
701
+ The check is fully synchronous and does not touch the DB thread-pool —
702
+ the DB status is reported opportunistically via a short-timeout probe so
703
+ a slow Postgres never makes /health itself time out. In maintenance mode
704
+ still returns 200 (so Railway doesn't kill the pod) but with status=maintenance.
656
705
  """
657
- from .concurrency import run_in_db
658
- def _check_db():
659
- db = SessionLocal()
706
+ # Maintenance-aware fast path — report it before touching the DB
707
+ if _is_maintenance_mode():
660
708
  try:
661
- db.execute(text("SELECT 1"))
662
- return "ok", None
663
- except Exception as exc:
664
- return "error", str(exc)
709
+ import re as _re2
710
+ _cl_path2 = _find_changelog_path()
711
+ _version2 = "1.47.16"
712
+ if _cl_path2:
713
+ with open(_cl_path2, "r", encoding="utf-8") as _f2:
714
+ for _line2 in _f2:
715
+ _m2 = _re2.match(r"^##\s+\[?([0-9a-zA-Z\.\-]+)\]?\s*-", _line2.strip())
716
+ if _m2:
717
+ _version2 = _m2.group(1)
718
+ break
719
+ except Exception:
720
+ _version2 = "1.47.16"
721
+ return JSONResponse(
722
+ content={"status": "maintenance", "db": "ok", "version": _version2, "maintenance": True},
723
+ status_code=200,
724
+ )
725
+
726
+ import time as _t
727
+ from sqlalchemy import text as _text
728
+
729
+ db_status: str = "ok"
730
+ db_error: str | None = None
731
+
732
+ # Probe the DB with an aggressive statement timeout so /health itself
733
+ # can never be held up by a saturated pool or a cold Postgres.
734
+ _deadline = _t.monotonic() + 1.5 # hard wall-clock deadline for /health
735
+ try:
736
+ _db = SessionLocal()
737
+ try:
738
+ # Short statement timeout for the probe only (does not leak).
739
+ try:
740
+ _db.execute(_text("SET LOCAL statement_timeout = '1000'"))
741
+ except Exception:
742
+ pass
743
+ _db.execute(_text("SELECT 1"))
744
+ db_status = "ok"
745
+ except Exception as _exc:
746
+ db_status = "error"
747
+ db_error = str(_exc)
748
+ try:
749
+ _db.rollback()
750
+ except Exception:
751
+ pass
752
+ logger.error("health_db_error: %s", _exc)
665
753
  finally:
666
- db.close()
754
+ try:
755
+ _db.close()
756
+ except Exception:
757
+ pass
758
+ except Exception as _exc: # SessionLocal() itself failed (e.g. bad DSN)
759
+ db_status = "error"
760
+ db_error = str(_exc)
667
761
 
668
- db_status, db_error = await run_in_db(_check_db)
669
- if db_error:
670
- logger.error(f"health_db_error: {db_error}")
762
+ if _t.monotonic() > _deadline:
763
+ # We exceeded the deadline only if DB was painfully slow; still
764
+ # return a body — the status code must stay 200 anyway.
765
+ pass
671
766
 
672
767
  # Derive version dynamically from CHANGELOG.md so a deploy auto-updates it
673
768
  try:
@@ -690,6 +785,63 @@ async def health():
690
785
  return JSONResponse(content=body, status_code=200)
691
786
 
692
787
 
788
+ # ── Maintenance Mode Admin Endpoints ──────────────────────────────────────────
789
+
790
+ @app.get("/admin/maintenance", tags=["admin"], summary="Get maintenance mode status")
791
+ async def get_maintenance_status(request: Request):
792
+ """Check whether maintenance mode is active. Requires admin auth."""
793
+ from .admin_auth import require_admin_auth
794
+ require_admin_auth(request)
795
+ return JSONResponse(content={
796
+ "maintenance": _is_maintenance_mode(),
797
+ "override": _maintenance_override,
798
+ "env": os.environ.get("UTIM_MAINTENANCE_MODE", "0"),
799
+ })
800
+
801
+
802
+ class MaintenanceToggleRequest(BaseModel):
803
+ enabled: bool
804
+ # optional bypass token to set at same time
805
+ bypass_token: Optional[str] = None
806
+
807
+
808
+ @app.post("/admin/maintenance", tags=["admin"], summary="Toggle maintenance mode at runtime")
809
+ async def set_maintenance_mode(request: Request, body: MaintenanceToggleRequest):
810
+ """Toggle maintenance mode without redeploy. Requires admin auth (x-admin-key or Authorization: Bearer <UTIM_MASTER_KEY>).
811
+
812
+ POST body: {"enabled": true/false, "bypass_token": "optional-secret"}
813
+ When enabled=true, all non-whitelisted routes return 503. /health stays 200 with status=maintenance.
814
+ When enabled=false, normal operation resumes.
815
+ The in-memory override takes precedence over the UTIM_MAINTENANCE_MODE env var until next restart.
816
+ """
817
+ from .admin_auth import require_admin_auth
818
+ require_admin_auth(request)
819
+ global _maintenance_override
820
+ _maintenance_override = bool(body.enabled)
821
+ if body.bypass_token is not None:
822
+ if body.bypass_token:
823
+ os.environ["UTIM_MAINTENANCE_BYPASS_TOKEN"] = body.bypass_token
824
+ elif "UTIM_MAINTENANCE_BYPASS_TOKEN" in os.environ:
825
+ del os.environ["UTIM_MAINTENANCE_BYPASS_TOKEN"]
826
+ logger.warning("maintenance_mode_toggled enabled=%s by_admin=%s", _maintenance_override, request.client.host if request.client else "unknown")
827
+ return JSONResponse(content={
828
+ "status": "maintenance" if _maintenance_override else "ok",
829
+ "maintenance": _maintenance_override,
830
+ "message": "Maintenance mode enabled — API returns 503 (except /health, /docs, /admin/maintenance)" if _maintenance_override else "Maintenance mode disabled — API back to normal",
831
+ })
832
+
833
+
834
+ @app.delete("/admin/maintenance", tags=["admin"], summary="Clear runtime override and fallback to env var")
835
+ async def clear_maintenance_override(request: Request):
836
+ """Clear the in-memory override so UTIM_MAINTENANCE_MODE env var takes effect again."""
837
+ from .admin_auth import require_admin_auth
838
+ require_admin_auth(request)
839
+ global _maintenance_override
840
+ _maintenance_override = None
841
+ logger.info("maintenance_mode_override_cleared")
842
+ return JSONResponse(content={"maintenance": _is_maintenance_mode(), "override": None, "message": "Override cleared, now following UTIM_MAINTENANCE_MODE env var"})
843
+
844
+
693
845
  @app.get("/api/version/latest", tags=["system"], summary="Latest CLI version (source of truth for update banners)")
694
846
  async def api_version_latest():
695
847
  """Single source of truth for CLI update prompts.
@@ -713,8 +865,7 @@ async def api_version_latest():
713
865
  return JSONResponse(content={"latest": _version}, status_code=200)
714
866
  except Exception:
715
867
  pass
716
- except Exception:
717
- return JSONResponse(content={"latest": None}, status_code=200)
868
+ return JSONResponse(content={"latest": None}, status_code=200)
718
869
 
719
870
 
720
871
  @app.get("/models", tags=["system"], summary="List available LLM models from ModelDB")
@@ -1243,30 +1243,10 @@ async def _do_dispatch_chat_completion(
1243
1243
  ] if model_obj else []
1244
1244
 
1245
1245
  if not model_obj or not model_obj.is_active or not active_providers:
1246
- # Check if model exists in primary platform registry (MODEL_REGISTRY / ModelDB)
1247
- from ..models import is_model_in_registry
1248
- if is_model_in_registry(req.model, db=user_db):
1249
- from .provider_compat_routes import (
1250
- v1_chat_completions,
1251
- ChatCompletionRequest as CompatChatCompletionRequest,
1252
- )
1253
- compat_req = CompatChatCompletionRequest(
1254
- model=req.model,
1255
- messages=req.messages,
1256
- stream=req.stream,
1257
- temperature=req.temperature,
1258
- max_tokens=req.max_tokens,
1259
- tools=req.tools,
1260
- )
1261
- res = await v1_chat_completions(
1262
- request=request,
1263
- req=compat_req,
1264
- db=user_db,
1265
- user=user,
1266
- concurrency_acquired=True,
1267
- )
1268
- return res, True
1269
-
1246
+ # ── Strict api_models isolation: NEVER fall back to OpenRouter / platform registry.
1247
+ # Requests hitting /v1/chat/completions (api_models router) must stay on the
1248
+ # api_models endpoint so a provider failure surfaces as 404/503 from this
1249
+ # endpoint instead of silently proxying to OpenRouter.
1270
1250
  if not model_obj or not model_obj.is_active:
1271
1251
  raise HTTPException(
1272
1252
  status_code=404,
@@ -1486,34 +1466,8 @@ async def _do_dispatch_chat_completion(
1486
1466
  if status_code not in (401, 403, 429) or key_idx == len(candidate_keys) - 1:
1487
1467
  break
1488
1468
 
1489
- # If all upstream providers in api_models failed:
1490
- # Check if primary platform registry has this model to serve as fallback before giving up with 502!
1491
- from ..models import is_model_in_registry
1492
- if is_model_in_registry(req.model, db=user_db):
1493
- logger.warning(
1494
- f"All api_models providers failed for '{req.model}'. Falling back to platform registry..."
1495
- )
1496
- from .provider_compat_routes import (
1497
- v1_chat_completions,
1498
- ChatCompletionRequest as CompatChatCompletionRequest,
1499
- )
1500
- compat_req = CompatChatCompletionRequest(
1501
- model=req.model,
1502
- messages=req.messages,
1503
- stream=req.stream,
1504
- temperature=req.temperature,
1505
- max_tokens=req.max_tokens,
1506
- tools=req.tools,
1507
- )
1508
- res = await v1_chat_completions(
1509
- request=request,
1510
- req=compat_req,
1511
- db=user_db,
1512
- user=user,
1513
- concurrency_acquired=True,
1514
- )
1515
- return res, True
1516
-
1469
+ # ── Strict api_models isolation: upstream failures must NOT fall back to OpenRouter.
1470
+ # Surface the real upstream error so callers see the api_models failure directly.
1517
1471
  raise HTTPException(status_code=502, detail={"error": {"message": f"All upstream providers failed: {last_err}", "type": "bad_gateway"}})
1518
1472
 
1519
1473