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.
- {utim_cli-2.4.2 → utim_cli-2.4.3}/CHANGELOG.md +10 -0
- {utim_cli-2.4.2/utim_cli.egg-info → utim_cli-2.4.3}/PKG-INFO +1 -1
- {utim_cli-2.4.2 → utim_cli-2.4.3}/pyproject.toml +1 -1
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim/__init__.py +1 -1
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/__init__.py +1 -1
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/_version.py +1 -1
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/orchestrator.py +11 -3
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/orchestrator_llm_transport.py +12 -3
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/orchestrator_turn_history.py +6 -8
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/api_models_db.py +0 -3
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/audit_log.py +1 -1
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/router.py +214 -63
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/routes/api_models_public_routes.py +6 -52
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/tools.py +137 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3/utim_cli.egg-info}/PKG-INFO +1 -1
- {utim_cli-2.4.2 → utim_cli-2.4.3}/LICENSE +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/MANIFEST.in +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/README.md +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/setup.cfg +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/setup.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/_chip_test.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/_verify_repro.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/_verify_syntax.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/_wizard_smoke_test.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/agent.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/architecture_indexer.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/ask_helper.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/auth.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/backup.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/billing.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/blender_agent.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/bootstrap.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/brain.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/builtin/skills/utim-guide/SKILL.md +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/client_utils.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/config.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/constants.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/context_inspector.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/context_pruner.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/crawler/__init__.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/crawler/engine.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/crawler/markdown_distiller.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/dependency_manager.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/doctor.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/evidence_registry.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/harbor.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/knowledge_graph.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/local_db.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/logger.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/mcp_clean_wrapper.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/mcp_client.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/mcp_registry.json +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/models.txt +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/orchestrator_helpers.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/orchestrator_tool_exec.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/personalities.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/platform_installer.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/plugins/__init__.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/plugins/hooks.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/plugins/loader.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/plugins/manager.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/plugins/registry.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/plugins/runtime.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/plugins/schema.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/plugins/tool_loader.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/reflection.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/report.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/scrapy_search.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/sdk/__init__.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/sdk/config.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/sdk/context.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/sdk/events.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/sdk/handlers.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/sdk/session.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/sdk/sidecar.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/__init__.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/admin_auth.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/attribution.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/auth.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/batch_processor.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/captcha.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/cli_auth.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/concurrency.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/db.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/docs_md/SECRET_PROVISIONING.md +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/docs_md/about.md +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/docs_md/changelog.md +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/docs_md/docs.md +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/docs_md/features.md +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/docs_md/license.md +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/docs_md/pricing.md +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/docs_md/privacy.md +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/docs_md/refund.md +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/docs_md/support.md +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/docs_md/terms.md +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/email_utils.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/exchange_rate.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/firebase.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/fix_duplicate_users.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/history.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/key_rotator.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/local_llm.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/logging_config.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/micro_batcher.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/model_agent.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/models.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/plan_integrity.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/platform_billing.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/pricing_updater.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/provider_balance.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/provider_discovery.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/provider_router.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/provision_build.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/rate_limit.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/rewards_engine.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/routes/__init__.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/routes/admin_bonus_routes.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/routes/admin_db_routes.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/routes/admin_providers_routes.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/routes/api_gateway_extensions_routes.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/routes/api_key_routes.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/routes/api_models_admin_routes.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/routes/auth_routes.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/routes/billing_routes.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/routes/completion_routes.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/routes/credit_routes.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/routes/feedback_routes.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/routes/marketplace_routes.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/routes/provider_compat_routes.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/routes/quota_routes.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/routes/quota_share_routes.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/routes/referral_routes.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/routes/rewards_routes.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/routes/security_routes.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/routes/session_routes.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/routes/share_routes.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/routes/wallet_routes.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/routes/webhook_routes.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/security_alerts.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/server.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/signup_guard.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/storage_nodes.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/server/utils.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/share.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/share_tui.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/situational_scoring.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/state.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/subagent_manager.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/syntax_fix.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/task_dispatcher.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/task_logger.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/tools_grep.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/tui/__init__.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/tui/compression_dialog.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/tui/feedback_dialog.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/tui/history_dialog.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/tui/marketplace_app_state.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/tui/marketplace_dialog.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/tui/marketplace_layout_engine.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/tui/mcp_dialog.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/tui/miniagents_dialog.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/tui/model_dialog.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/tui/personality_dialog.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/tui/plugins_dialog.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/tui/publish_dialog.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/tui/quota_dialog.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/tui/quota_redeem_dialog.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/tui/quota_share_dialog.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/tui/resume_dialog.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/tui/rewards_tui.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/tui/skills_dialog.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/tui/subagents_dialog.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/tui/tasks_dialog.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/tui/thinking_display.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/tui/tools_dialog.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/tui/update_dialog.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/update_checker.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/utilities.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/utim.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/utimmodel.txt +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/vector_memory.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/voice.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/voice_coding.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/voice_events.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/voice_handler.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/voice_instant.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/voice_orchestrator.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/voice_session.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/voice_stt.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/voice_tts.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/wheel.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli/workspace.py +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli.egg-info/SOURCES.txt +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli.egg-info/dependency_links.txt +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli.egg-info/entry_points.txt +0 -0
- {utim_cli-2.4.2 → utim_cli-2.4.3}/utim_cli.egg-info/requires.txt +0 -0
- {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.
|
|
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
|
|
@@ -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.
|
|
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
|
|
@@ -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
|
-
|
|
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,
|
|
1054
|
-
if
|
|
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
|
|
2147
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
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
|
|
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
|
-
# ──
|
|
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:
|
|
164
|
-
|
|
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
|
-
|
|
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
|
-
|
|
658
|
-
|
|
659
|
-
db = SessionLocal()
|
|
706
|
+
# Maintenance-aware fast path — report it before touching the DB
|
|
707
|
+
if _is_maintenance_mode():
|
|
660
708
|
try:
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
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
|
-
|
|
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
|
-
|
|
669
|
-
|
|
670
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
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
|
-
#
|
|
1490
|
-
#
|
|
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
|
|