utim-cli 2.3.23__tar.gz → 2.3.24__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.3.23 → utim_cli-2.3.24}/CHANGELOG.md +5 -0
- {utim_cli-2.3.23/utim_cli.egg-info → utim_cli-2.3.24}/PKG-INFO +1 -1
- {utim_cli-2.3.23 → utim_cli-2.3.24}/pyproject.toml +1 -1
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim/__init__.py +2 -2
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/__init__.py +1 -1
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/_version.py +1 -1
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/orchestrator.py +1 -1
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/orchestrator_llm_transport.py +9 -1
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/server/key_rotator.py +83 -56
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/server/models.py +4 -1
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/server/routes/completion_routes.py +39 -16
- {utim_cli-2.3.23 → utim_cli-2.3.24/utim_cli.egg-info}/PKG-INFO +1 -1
- {utim_cli-2.3.23 → utim_cli-2.3.24}/LICENSE +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/MANIFEST.in +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/README.md +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/setup.cfg +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/setup.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/_chip_test.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/_verify_repro.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/_verify_syntax.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/_wizard_smoke_test.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/agent.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/ask_helper.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/auth.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/backup.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/billing.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/blender_agent.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/bootstrap.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/brain.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/builtin/skills/utim-guide/SKILL.md +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/client_utils.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/config.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/constants.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/context_pruner.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/crawler/__init__.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/crawler/engine.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/crawler/markdown_distiller.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/dependency_manager.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/doctor.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/evidence_registry.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/harbor.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/knowledge_graph.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/local_db.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/logger.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/mcp_clean_wrapper.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/mcp_client.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/mcp_registry.json +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/models.txt +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/orchestrator_helpers.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/orchestrator_tool_exec.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/orchestrator_turn_history.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/personalities.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/platform_installer.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/plugins/__init__.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/plugins/hooks.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/plugins/loader.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/plugins/manager.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/plugins/registry.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/plugins/runtime.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/plugins/schema.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/plugins/tool_loader.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/reflection.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/report.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/scrapy_search.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/sdk/__init__.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/sdk/config.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/sdk/context.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/sdk/events.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/sdk/handlers.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/sdk/session.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/sdk/sidecar.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/server/__init__.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/server/admin_auth.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/server/attribution.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/server/audit_log.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/server/auth.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/server/batch_processor.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/server/captcha.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/server/cli_auth.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/server/concurrency.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/server/db.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/server/docs_md/SECRET_PROVISIONING.md +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/server/docs_md/about.md +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/server/docs_md/changelog.md +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/server/docs_md/docs.md +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/server/docs_md/features.md +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/server/docs_md/license.md +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/server/docs_md/pricing.md +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/server/docs_md/privacy.md +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/server/docs_md/refund.md +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/server/docs_md/support.md +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/server/docs_md/terms.md +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/server/email_utils.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/server/exchange_rate.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/server/firebase.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/server/fix_duplicate_users.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/server/history.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/server/local_llm.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/server/logging_config.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/server/micro_batcher.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/server/model_agent.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/server/plan_integrity.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/server/pricing_updater.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/server/provision_build.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/server/rate_limit.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/server/rewards_engine.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/server/router.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/server/routes/__init__.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/server/routes/admin_bonus_routes.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/server/routes/admin_db_routes.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/server/routes/auth_routes.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/server/routes/billing_routes.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/server/routes/credit_routes.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/server/routes/feedback_routes.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/server/routes/marketplace_routes.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/server/routes/quota_routes.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/server/routes/quota_share_routes.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/server/routes/referral_routes.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/server/routes/rewards_routes.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/server/routes/security_routes.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/server/routes/session_routes.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/server/routes/share_routes.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/server/routes/webhook_routes.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/server/security_alerts.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/server/server.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/server/storage_nodes.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/server/utils.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/share.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/share_tui.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/situational_scoring.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/state.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/subagent_manager.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/task_dispatcher.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/task_logger.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/tools.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/tools_grep.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/tui/__init__.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/tui/compression_dialog.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/tui/feedback_dialog.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/tui/history_dialog.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/tui/marketplace_app_state.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/tui/marketplace_dialog.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/tui/marketplace_layout_engine.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/tui/mcp_dialog.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/tui/miniagents_dialog.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/tui/model_dialog.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/tui/personality_dialog.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/tui/plugins_dialog.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/tui/publish_dialog.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/tui/quota_dialog.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/tui/quota_redeem_dialog.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/tui/quota_share_dialog.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/tui/resume_dialog.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/tui/rewards_tui.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/tui/skills_dialog.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/tui/subagents_dialog.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/tui/tasks_dialog.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/tui/thinking_display.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/tui/tools_dialog.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/tui/update_dialog.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/update_checker.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/utilities.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/utim.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/utimmodel.txt +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/vector_memory.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/wheel.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli/workspace.py +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli.egg-info/SOURCES.txt +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli.egg-info/dependency_links.txt +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli.egg-info/entry_points.txt +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli.egg-info/requires.txt +0 -0
- {utim_cli-2.3.23 → utim_cli-2.3.24}/utim_cli.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: utim-cli
|
|
3
|
-
Version: 2.3.
|
|
3
|
+
Version: 2.3.24
|
|
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
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
"""
|
|
2
2
|
UTIM Developer SDK
|
|
3
3
|
------------------
|
|
4
4
|
Unified autonomous coding agent SDK.
|
|
@@ -14,7 +14,7 @@ Quickstart:
|
|
|
14
14
|
try:
|
|
15
15
|
from utim_cli._version import VERSION as __version__
|
|
16
16
|
except ImportError:
|
|
17
|
-
__version__ = "2.3.
|
|
17
|
+
__version__ = "2.3.24"
|
|
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
|
|
@@ -848,7 +848,7 @@ class Orchestrator(LLMTransportMixin, TurnHistoryMixin, ToolExecutionMixin):
|
|
|
848
848
|
msg, was_streamed = self._call_llm(
|
|
849
849
|
send_msgs,
|
|
850
850
|
override_tools=override_tools or getattr(self, "_subagent_tool_override", None),
|
|
851
|
-
silent=
|
|
851
|
+
silent=False,
|
|
852
852
|
)
|
|
853
853
|
reasoning_duration = time.time() - t_llm_start
|
|
854
854
|
|
|
@@ -2200,7 +2200,15 @@ class LLMTransportMixin:
|
|
|
2200
2200
|
f"\n[dim yellow]↪ Free model rate-limited — paid version already in queue, skipping…[/dim yellow]"
|
|
2201
2201
|
)
|
|
2202
2202
|
break # inner attempt loop → outer loop picks up injected paid slug
|
|
2203
|
-
#
|
|
2203
|
+
# If primary model is rate-limited and fallback models exist, race them
|
|
2204
|
+
if current_is_primary and fallback_list:
|
|
2205
|
+
if not silent:
|
|
2206
|
+
self.console.print(
|
|
2207
|
+
f"\n[dim yellow]⏳ Model '{current_model}' is temporarily rate-limited by the provider — racing fallback models…[/dim yellow]\n"
|
|
2208
|
+
)
|
|
2209
|
+
raise _ServerUnavailableError(f"Rate-limited on model '{current_model}'")
|
|
2210
|
+
|
|
2211
|
+
# Non-free model with no fallback: show message and return
|
|
2204
2212
|
rate_limit_display = (
|
|
2205
2213
|
f"⏳ **Model Rate Limited**: `{current_model}` is currently experiencing high demand and is temporarily rate-limited by the provider.\n\n"
|
|
2206
2214
|
f"- **What to do:** Please wait a moment before retrying, or switch to a different model with `/model` or `Ctrl+M`.\n"
|
|
@@ -1891,12 +1891,7 @@ class ApinexKeyRotator:
|
|
|
1891
1891
|
return max(self.TOTAL_RPM_PER_KEY, len(self._keys) * self.TOTAL_RPM_PER_KEY)
|
|
1892
1892
|
|
|
1893
1893
|
def _get_active_key_locked(self, now: float) -> Optional[KeyEntry]:
|
|
1894
|
-
"""Find the
|
|
1895
|
-
|
|
1896
|
-
Pins to the current key for all requests until it returns 402.
|
|
1897
|
-
When 402 occurs, that key is marked exhausted until 00:00 UTC (5:30 AM IST)
|
|
1898
|
-
and rotation advances to the next key until all keys are exhausted.
|
|
1899
|
-
"""
|
|
1894
|
+
"""Find the next active unexhausted APINEX key using round-robin rotation."""
|
|
1900
1895
|
if not self._keys:
|
|
1901
1896
|
return None
|
|
1902
1897
|
n = len(self._keys)
|
|
@@ -1909,7 +1904,7 @@ class ApinexKeyRotator:
|
|
|
1909
1904
|
continue
|
|
1910
1905
|
if k.is_in_cooldown(now):
|
|
1911
1906
|
continue
|
|
1912
|
-
self._current_key_idx = idx
|
|
1907
|
+
self._current_key_idx = (idx + 1) % n
|
|
1913
1908
|
return k
|
|
1914
1909
|
|
|
1915
1910
|
# Fallback: if all non-exhausted keys are currently in cooldown, return the first unexhausted key
|
|
@@ -1918,13 +1913,17 @@ class ApinexKeyRotator:
|
|
|
1918
1913
|
k = self._keys[idx]
|
|
1919
1914
|
if (k.exhausted_until > 0 and now < k.exhausted_until) or k.credits_exhausted or k.paid_exhausted:
|
|
1920
1915
|
continue
|
|
1916
|
+
self._current_key_idx = (idx + 1) % n
|
|
1921
1917
|
return k
|
|
1922
1918
|
return None
|
|
1923
1919
|
|
|
1924
1920
|
def has_available_keys(self) -> bool:
|
|
1925
1921
|
with self._lock:
|
|
1926
1922
|
now = time.time()
|
|
1927
|
-
return
|
|
1923
|
+
return any(
|
|
1924
|
+
not ((k.exhausted_until > 0 and now < k.exhausted_until) or k.credits_exhausted or k.paid_exhausted)
|
|
1925
|
+
for k in self._keys
|
|
1926
|
+
)
|
|
1928
1927
|
|
|
1929
1928
|
def get_all_unexhausted_keys(self, now: Optional[float] = None) -> List[KeyEntry]:
|
|
1930
1929
|
"""Return all APINEX keys that are not 402 daily quota exhausted."""
|
|
@@ -1940,6 +1939,7 @@ class ApinexKeyRotator:
|
|
|
1940
1939
|
return unexhausted
|
|
1941
1940
|
|
|
1942
1941
|
def acquire_key(self, purpose: str = "user") -> KeyEntry:
|
|
1942
|
+
"""Acquire the next available APINEX key using round-robin rotation and load balancing."""
|
|
1943
1943
|
with self._lock:
|
|
1944
1944
|
if not self._keys:
|
|
1945
1945
|
self._refresh_keys()
|
|
@@ -1953,36 +1953,46 @@ class ApinexKeyRotator:
|
|
|
1953
1953
|
for k in self._keys:
|
|
1954
1954
|
k.prune_expired(now)
|
|
1955
1955
|
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1956
|
+
candidates = [
|
|
1957
|
+
k for k in self._keys
|
|
1958
|
+
if k.has_capacity(
|
|
1959
|
+
purpose=purpose,
|
|
1960
|
+
now=now,
|
|
1961
|
+
user_limit=self.USER_RPM_PER_KEY,
|
|
1962
|
+
reflection_limit=self.REFLECTION_RPM_PER_KEY,
|
|
1963
|
+
total_limit=self.TOTAL_RPM_PER_KEY,
|
|
1964
|
+
daily_limit=self.DAILY_RPD_PER_KEY,
|
|
1964
1965
|
)
|
|
1966
|
+
]
|
|
1965
1967
|
|
|
1966
|
-
if not
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1968
|
+
if not candidates:
|
|
1969
|
+
if all((k.exhausted_until > 0 and now < k.exhausted_until) or k.credits_exhausted or k.paid_exhausted for k in self._keys):
|
|
1970
|
+
raise HTTPException(
|
|
1971
|
+
status_code=503,
|
|
1972
|
+
detail=(
|
|
1973
|
+
f"All {len(self._keys)} APINEX API keys have exhausted their daily quota (402) "
|
|
1974
|
+
f"until 00:00 UTC (5:30 AM IST). Service will resume at 00:00 UTC."
|
|
1975
|
+
),
|
|
1976
|
+
)
|
|
1977
|
+
total_rpm = sum(k.current_total_rpm(now) for k in self._keys)
|
|
1975
1978
|
raise HTTPException(
|
|
1976
1979
|
status_code=429,
|
|
1977
1980
|
detail=(
|
|
1978
|
-
f"APINEX capacity limit reached
|
|
1979
|
-
f"max 15 RPM/key). Please wait a few seconds before retrying."
|
|
1981
|
+
f"APINEX capacity limit reached ({total_rpm}/{self.max_total_rpm} RPM across "
|
|
1982
|
+
f"{len(self._keys)} keys, max 15 RPM/key). Please wait a few seconds before retrying."
|
|
1980
1983
|
),
|
|
1981
1984
|
headers={"Retry-After": "5"},
|
|
1982
1985
|
)
|
|
1983
1986
|
|
|
1984
|
-
|
|
1985
|
-
|
|
1987
|
+
start_idx = self._rr_idx % len(self._keys)
|
|
1988
|
+
ordered = sorted(
|
|
1989
|
+
candidates,
|
|
1990
|
+
key=lambda k: (k.active_requests, (self._keys.index(k) - start_idx) % len(self._keys)),
|
|
1991
|
+
)
|
|
1992
|
+
chosen = ordered[0]
|
|
1993
|
+
self._rr_idx = (self._keys.index(chosen) + 1) % len(self._keys)
|
|
1994
|
+
chosen.record_request(purpose=purpose, now=now)
|
|
1995
|
+
return chosen
|
|
1986
1996
|
|
|
1987
1997
|
def release_key(
|
|
1988
1998
|
self,
|
|
@@ -2016,7 +2026,6 @@ class ApinexKeyRotator:
|
|
|
2016
2026
|
if self._keys and key_entry in self._keys:
|
|
2017
2027
|
curr_idx = self._keys.index(key_entry)
|
|
2018
2028
|
self._current_key_idx = (curr_idx + 1) % len(self._keys)
|
|
2019
|
-
self._get_active_key_locked(now)
|
|
2020
2029
|
|
|
2021
2030
|
self._try_dequeue_locked(now)
|
|
2022
2031
|
|
|
@@ -2067,25 +2076,32 @@ class ApinexKeyRotator:
|
|
|
2067
2076
|
for k in self._keys:
|
|
2068
2077
|
k.prune_expired(now)
|
|
2069
2078
|
|
|
2070
|
-
active_key = self._get_active_key_locked(now)
|
|
2071
|
-
if active_key is None:
|
|
2072
|
-
return False
|
|
2073
|
-
|
|
2074
2079
|
dequeued_any = False
|
|
2075
2080
|
remaining: List[QueuedRequest] = []
|
|
2076
2081
|
for req in self._waiting_queue:
|
|
2077
2082
|
if req.is_cancelled:
|
|
2078
2083
|
continue
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2084
|
+
candidates = [
|
|
2085
|
+
k for k in self._keys
|
|
2086
|
+
if k.has_capacity(
|
|
2087
|
+
purpose=req.purpose,
|
|
2088
|
+
now=now,
|
|
2089
|
+
user_limit=self.USER_RPM_PER_KEY,
|
|
2090
|
+
reflection_limit=self.REFLECTION_RPM_PER_KEY,
|
|
2091
|
+
total_limit=self.TOTAL_RPM_PER_KEY,
|
|
2092
|
+
daily_limit=self.DAILY_RPD_PER_KEY,
|
|
2093
|
+
)
|
|
2094
|
+
]
|
|
2095
|
+
if candidates:
|
|
2096
|
+
start_idx = self._rr_idx % len(self._keys)
|
|
2097
|
+
ordered = sorted(
|
|
2098
|
+
candidates,
|
|
2099
|
+
key=lambda k: (k.active_requests, (self._keys.index(k) - start_idx) % len(self._keys)),
|
|
2100
|
+
)
|
|
2101
|
+
chosen = ordered[0]
|
|
2102
|
+
self._rr_idx = (self._keys.index(chosen) + 1) % len(self._keys)
|
|
2103
|
+
chosen.record_request(purpose=req.purpose, now=now)
|
|
2104
|
+
req.chosen_key = chosen
|
|
2089
2105
|
req.event.set()
|
|
2090
2106
|
req.notify_event.set()
|
|
2091
2107
|
dequeued_any = True
|
|
@@ -2175,18 +2191,29 @@ class ApinexKeyRotator:
|
|
|
2175
2191
|
)
|
|
2176
2192
|
|
|
2177
2193
|
if not self._waiting_queue:
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2194
|
+
candidates = [
|
|
2195
|
+
k for k in self._keys
|
|
2196
|
+
if k.has_capacity(
|
|
2197
|
+
purpose=purpose,
|
|
2198
|
+
now=now,
|
|
2199
|
+
user_limit=self.USER_RPM_PER_KEY,
|
|
2200
|
+
reflection_limit=self.REFLECTION_RPM_PER_KEY,
|
|
2201
|
+
total_limit=self.TOTAL_RPM_PER_KEY,
|
|
2202
|
+
daily_limit=self.DAILY_RPD_PER_KEY,
|
|
2203
|
+
)
|
|
2204
|
+
]
|
|
2205
|
+
if candidates:
|
|
2206
|
+
start_idx = self._rr_idx % len(self._keys)
|
|
2207
|
+
ordered = sorted(
|
|
2208
|
+
candidates,
|
|
2209
|
+
key=lambda k: (k.active_requests, (self._keys.index(k) - start_idx) % len(self._keys)),
|
|
2210
|
+
)
|
|
2211
|
+
chosen = ordered[0]
|
|
2212
|
+
self._rr_idx = (self._keys.index(chosen) + 1) % len(self._keys)
|
|
2213
|
+
chosen.record_request(purpose=purpose, now=now)
|
|
2214
|
+
if chosen.async_client is None:
|
|
2215
|
+
self._init_clients(chosen)
|
|
2216
|
+
ready_item = {"status": "ready", "client": chosen.async_client, "key_entry": chosen}
|
|
2190
2217
|
|
|
2191
2218
|
if ready_item is not None:
|
|
2192
2219
|
yield ready_item
|
|
@@ -3217,7 +3217,10 @@ def get_model_catalog(user_tier: str = "all") -> dict:
|
|
|
3217
3217
|
payload[key] = [it for it in payload[key] if _is_free_item(it)]
|
|
3218
3218
|
else:
|
|
3219
3219
|
# Non-free users:
|
|
3220
|
-
# APINEX models are free gateway models
|
|
3220
|
+
# APINEX models are free gateway models for free tier users — exclude them from paid tier catalogs
|
|
3221
|
+
if not is_all_user:
|
|
3222
|
+
for key in ("main_agent", "plan_project", "subagent_text", "analyze_image", "image_gen", "all_text"):
|
|
3223
|
+
payload[key] = [it for it in payload[key] if str(it.get("provider", "")).lower() != "apinex"]
|
|
3221
3224
|
|
|
3222
3225
|
# Paid plan: look up the Plan row's `allowed_models`
|
|
3223
3226
|
try:
|
|
@@ -93,9 +93,9 @@ def is_model_allowed(model_id: str, allowed_models_str: str, bonus_balance: floa
|
|
|
93
93
|
or (entry and getattr(entry, "provider", "").lower() == "apinex")
|
|
94
94
|
)
|
|
95
95
|
|
|
96
|
-
# 1. APINEX models: free gateway models
|
|
96
|
+
# 1. APINEX models: free gateway models allowed on free plan (or with bonus credits/all)
|
|
97
97
|
if is_apinex_model:
|
|
98
|
-
return
|
|
98
|
+
return is_free_plan or bonus_balance > 0.0 or allowed_models_str == "all"
|
|
99
99
|
|
|
100
100
|
if bonus_balance > 0.0:
|
|
101
101
|
return True
|
|
@@ -1407,9 +1407,8 @@ async def completions(
|
|
|
1407
1407
|
detail="All OpenRouter API keys have exhausted paid credits (402) until 00:00 UTC (5:30 AM IST). Free models may still be available.",
|
|
1408
1408
|
)
|
|
1409
1409
|
|
|
1410
|
-
# ── 4. Non-free paid model 429:
|
|
1410
|
+
# ── 4. Non-free paid model 429: rotate across remaining paid keys in the pool ──
|
|
1411
1411
|
elif stream is None and key_entry and hasattr(key_entry, "key_id") and key_entry.key_id != "nvidia" and _is_429:
|
|
1412
|
-
mark_model_rate_limited(req.model_id, cooldown_seconds=25.0)
|
|
1413
1412
|
if _is_upstream_429:
|
|
1414
1413
|
_purpose_str = "reflection" if is_refl else "user"
|
|
1415
1414
|
global_key_rotator.unrecord_key(key_entry, purpose=_purpose_str)
|
|
@@ -1419,23 +1418,47 @@ async def completions(
|
|
|
1419
1418
|
status_code=429,
|
|
1420
1419
|
upstream_provider_429=True,
|
|
1421
1420
|
)
|
|
1422
|
-
key_entry = None
|
|
1423
|
-
raise HTTPException(
|
|
1424
|
-
status_code=429,
|
|
1425
|
-
detail=(
|
|
1426
|
-
f"Model '{req.model_id}' is temporarily rate-limited by the provider. "
|
|
1427
|
-
f"Please wait ~25s before retrying, or switch to a different model with /model."
|
|
1428
|
-
),
|
|
1429
|
-
headers={"Retry-After": "25"},
|
|
1430
|
-
)
|
|
1431
1421
|
else:
|
|
1432
1422
|
global_key_rotator.demote_key_to_last(key_entry)
|
|
1433
1423
|
global_key_rotator.release_key(key_entry, success=False, status_code=429, is_daily_quota_429=False)
|
|
1434
|
-
|
|
1424
|
+
key_entry = None
|
|
1425
|
+
|
|
1426
|
+
# Rotate across remaining available paid keys before giving up
|
|
1427
|
+
cand_paid_keys = [k for k in global_key_rotator._keys if not k.is_paid_exhausted(time.time())]
|
|
1428
|
+
for p_key in cand_paid_keys:
|
|
1429
|
+
if p_key.is_in_cooldown(time.time()):
|
|
1430
|
+
continue
|
|
1431
|
+
try:
|
|
1432
|
+
p_key.record_request(purpose=purpose, now=time.time())
|
|
1433
|
+
stream = await p_key.async_client.chat.completions.create(**kwargs)
|
|
1434
|
+
if stream:
|
|
1435
|
+
client = p_key.async_client
|
|
1436
|
+
key_entry = p_key
|
|
1437
|
+
logger.info(f"Paid model '{req.model_id}' succeeded after rotating to key {p_key.env_var}")
|
|
1438
|
+
break
|
|
1439
|
+
except Exception as p_err:
|
|
1440
|
+
p_st = getattr(p_err, "status_code", None)
|
|
1441
|
+
p_s = str(p_err).lower()
|
|
1442
|
+
if p_st == 402 or "402" in p_s or "payment required" in p_s:
|
|
1443
|
+
global_key_rotator.release_key(p_key, success=False, status_code=402)
|
|
1444
|
+
continue
|
|
1445
|
+
elif p_st == 429 or "429" in p_s or "rate" in p_s:
|
|
1446
|
+
global_key_rotator.demote_key_to_last(p_key)
|
|
1447
|
+
global_key_rotator.release_key(p_key, success=False, status_code=429)
|
|
1448
|
+
continue
|
|
1449
|
+
else:
|
|
1450
|
+
global_key_rotator.release_key(p_key, success=False)
|
|
1451
|
+
break
|
|
1452
|
+
|
|
1453
|
+
if stream is None:
|
|
1454
|
+
mark_model_rate_limited(req.model_id, cooldown_seconds=20.0)
|
|
1435
1455
|
raise HTTPException(
|
|
1436
1456
|
status_code=429,
|
|
1437
|
-
detail=
|
|
1438
|
-
|
|
1457
|
+
detail=(
|
|
1458
|
+
f"Model '{req.model_id}' is temporarily rate-limited across all provider keys. "
|
|
1459
|
+
f"Please wait ~20s before retrying, or switch to a different model with /model."
|
|
1460
|
+
),
|
|
1461
|
+
headers={"Retry-After": "20"},
|
|
1439
1462
|
)
|
|
1440
1463
|
|
|
1441
1464
|
# 2. Sequential fallback attempts for 400 Bad Request / parameter incompatibilities
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: utim-cli
|
|
3
|
-
Version: 2.3.
|
|
3
|
+
Version: 2.3.24
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|