utim-cli 2.3.18__tar.gz → 2.3.19b0__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.18 → utim_cli-2.3.19b0}/CHANGELOG.md +15 -0
- {utim_cli-2.3.18/utim_cli.egg-info → utim_cli-2.3.19b0}/PKG-INFO +1 -1
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/pyproject.toml +1 -1
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim/__init__.py +1 -1
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/__init__.py +1 -1
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/_version.py +5 -1
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/auth.py +15 -1
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/client_utils.py +51 -4
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/orchestrator.py +7 -2
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/orchestrator_helpers.py +12 -1
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/orchestrator_llm_transport.py +6 -14
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/plugins/manager.py +27 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/plugins/tool_loader.py +8 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/server/auth.py +38 -27
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/server/models.py +7 -10
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/server/routes/auth_routes.py +4 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/server/routes/completion_routes.py +9 -9
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/server/routes/quota_routes.py +42 -8
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/server/routes/rewards_routes.py +14 -5
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/tools.py +45 -41
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/tui/model_dialog.py +109 -66
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/tui/plugins_dialog.py +24 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/utim.py +35 -22
- {utim_cli-2.3.18 → utim_cli-2.3.19b0/utim_cli.egg-info}/PKG-INFO +1 -1
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/LICENSE +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/MANIFEST.in +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/README.md +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/setup.cfg +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/setup.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/_verify_repro.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/_verify_syntax.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/_wizard_smoke_test.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/agent.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/ask_helper.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/backup.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/billing.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/blender_agent.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/bootstrap.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/brain.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/builtin/skills/utim-guide/SKILL.md +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/config.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/constants.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/context_pruner.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/crawler/__init__.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/crawler/engine.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/crawler/markdown_distiller.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/dependency_manager.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/doctor.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/evidence_registry.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/harbor.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/knowledge_graph.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/local_db.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/logger.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/mcp_clean_wrapper.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/mcp_client.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/mcp_registry.json +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/models.txt +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/orchestrator_tool_exec.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/orchestrator_turn_history.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/personalities.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/platform_installer.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/plugins/__init__.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/plugins/hooks.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/plugins/loader.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/plugins/registry.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/plugins/runtime.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/plugins/schema.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/reflection.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/report.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/scrapy_search.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/sdk/__init__.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/sdk/config.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/sdk/context.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/sdk/events.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/sdk/handlers.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/sdk/session.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/sdk/sidecar.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/server/__init__.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/server/admin_auth.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/server/attribution.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/server/audit_log.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/server/batch_processor.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/server/captcha.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/server/cli_auth.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/server/concurrency.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/server/db.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/server/docs_md/SECRET_PROVISIONING.md +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/server/docs_md/about.md +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/server/docs_md/changelog.md +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/server/docs_md/docs.md +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/server/docs_md/features.md +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/server/docs_md/license.md +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/server/docs_md/pricing.md +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/server/docs_md/privacy.md +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/server/docs_md/refund.md +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/server/docs_md/support.md +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/server/docs_md/terms.md +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/server/email_utils.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/server/exchange_rate.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/server/firebase.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/server/fix_duplicate_users.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/server/history.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/server/key_rotator.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/server/local_llm.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/server/logging_config.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/server/micro_batcher.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/server/model_agent.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/server/pricing_updater.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/server/provision_build.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/server/rate_limit.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/server/rewards_engine.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/server/router.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/server/routes/__init__.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/server/routes/admin_bonus_routes.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/server/routes/admin_db_routes.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/server/routes/billing_routes.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/server/routes/credit_routes.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/server/routes/feedback_routes.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/server/routes/marketplace_routes.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/server/routes/quota_share_routes.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/server/routes/referral_routes.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/server/routes/security_routes.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/server/routes/session_routes.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/server/routes/share_routes.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/server/server.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/server/storage_nodes.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/server/utils.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/share.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/share_tui.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/situational_scoring.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/state.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/subagent_manager.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/task_dispatcher.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/task_logger.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/tools_grep.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/tui/__init__.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/tui/feedback_dialog.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/tui/history_dialog.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/tui/marketplace_app_state.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/tui/marketplace_dialog.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/tui/marketplace_layout_engine.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/tui/mcp_dialog.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/tui/miniagents_dialog.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/tui/personality_dialog.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/tui/publish_dialog.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/tui/quota_dialog.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/tui/quota_redeem_dialog.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/tui/quota_share_dialog.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/tui/resume_dialog.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/tui/rewards_tui.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/tui/skills_dialog.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/tui/subagents_dialog.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/tui/tasks_dialog.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/tui/thinking_display.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/tui/tools_dialog.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/tui/update_dialog.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/update_checker.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/utilities.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/utimmodel.txt +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/vector_memory.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/wheel.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli/workspace.py +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli.egg-info/SOURCES.txt +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli.egg-info/dependency_links.txt +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli.egg-info/entry_points.txt +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli.egg-info/requires.txt +0 -0
- {utim_cli-2.3.18 → utim_cli-2.3.19b0}/utim_cli.egg-info/top_level.txt +0 -0
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.3.19-beta] - 2026-09-03
|
|
4
|
+
|
|
5
|
+
### 🚀 Beta Release: Tier Enforcement & Model Access Fixes
|
|
6
|
+
- **Websearch tier gate**: agentic websearch (LLM query synthesis + synthesis) is now paid-plan only; free-tier users always get the fast non-agentic path
|
|
7
|
+
- **Tool registry cleanup**: removed `analyzeimage` from callable tools — vision models receive image bytes directly via `readfile`, non-vision models get internal auto-descriptions
|
|
8
|
+
- **APINEX models ungated**: APINEX models are now available on every plan (paid included) as free models
|
|
9
|
+
- Version tagged as **beta** (`2.3.19-beta`) for beta-channel upload
|
|
10
|
+
|
|
11
|
+
## [2.3.19] - 2026-09-02
|
|
12
|
+
|
|
13
|
+
### 🛠️ Model Dialog Tier Sync & Free Model Preservation
|
|
14
|
+
- Synchronized user plan resolution on account switch to prevent paid models leaking on free accounts
|
|
15
|
+
- Fixed free models disappearing when bonus quota is active
|
|
16
|
+
- Removed default selected model on fresh startup and added configuration prompts
|
|
17
|
+
|
|
3
18
|
## [2.3.18] - 2026-09-02
|
|
4
19
|
|
|
5
20
|
### 🎁 New Free Models Added & Tool Sanitization Fixes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: utim-cli
|
|
3
|
-
Version: 2.3.
|
|
3
|
+
Version: 2.3.19b0
|
|
4
4
|
Summary: UTIM – Universal Terminal Intelligence Manager. A powerful agentic AI coding assistant for your terminal.
|
|
5
5
|
License: Emend AI Proprietary EULA
|
|
6
6
|
Project-URL: Homepage, https://utim.dev
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "utim-cli"
|
|
7
|
-
version = "2.3.
|
|
7
|
+
version = "2.3.19-beta"
|
|
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.3.
|
|
17
|
+
__version__ = "2.3.19-beta"
|
|
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
|
|
@@ -11,6 +11,10 @@
|
|
|
11
11
|
# from here so every surface stays in sync automatically.
|
|
12
12
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
# Beta release: "-beta" is the machine-valid spelling of "2.3.19 beta"
|
|
15
|
+
# (npm semver prerelease tag + PEP 440 pre-release segment; a space would
|
|
16
|
+
# break npm publish and pip). parse_version() strips the suffix so update
|
|
17
|
+
# checks still compare on the numeric core 2.3.19.
|
|
18
|
+
VERSION = "2.3.19-beta"
|
|
15
19
|
|
|
16
20
|
|
|
@@ -495,8 +495,22 @@ def login(restart: bool = False) -> None:
|
|
|
495
495
|
config.set("token", "device_flow")
|
|
496
496
|
config.set("email", body.get("email", ""))
|
|
497
497
|
config.set("uid", body.get("user_id", ""))
|
|
498
|
-
config.set("firebase_uid", body.get("firebase_uid", ""))
|
|
499
498
|
config.set("name", body.get("display_name") or (body.get("email", "") or "").split("@")[0])
|
|
499
|
+
plan_name = body.get("plan")
|
|
500
|
+
if (not plan_name or plan_name == "free") and body.get("api_key"):
|
|
501
|
+
try:
|
|
502
|
+
_vr = requests.get(
|
|
503
|
+
f"{SERVER_URL}/api/user-plan",
|
|
504
|
+
headers={"X-API-Key": body["api_key"]},
|
|
505
|
+
timeout=3.0,
|
|
506
|
+
)
|
|
507
|
+
if _vr.status_code == 200:
|
|
508
|
+
_pdata = _vr.json() or {}
|
|
509
|
+
plan_name = _pdata.get("plan") or _pdata.get("plan_name")
|
|
510
|
+
except Exception:
|
|
511
|
+
pass
|
|
512
|
+
clean_plan = str(plan_name or "free").lower().removesuffix(" plan").strip()
|
|
513
|
+
config.set("user_plan", clean_plan)
|
|
500
514
|
if body.get("api_key"):
|
|
501
515
|
config.set("api_key", body["api_key"])
|
|
502
516
|
raw_print("")
|
|
@@ -64,6 +64,44 @@ def get_server_url() -> str:
|
|
|
64
64
|
return "https://api.utim.dev"
|
|
65
65
|
|
|
66
66
|
|
|
67
|
+
def is_paid_plan(plan_name: Optional[str]) -> bool:
|
|
68
|
+
"""Return True if the given plan name represents a paid subscription tier."""
|
|
69
|
+
if not plan_name:
|
|
70
|
+
return False
|
|
71
|
+
p = str(plan_name).lower().removesuffix(" plan").strip()
|
|
72
|
+
return p not in ("free", "free tier", "", "none", "null", "guest", "undefined")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def is_user_paid(force_refresh: bool = False) -> bool:
|
|
76
|
+
"""Helper to check if the current user has an active paid subscription tier."""
|
|
77
|
+
try:
|
|
78
|
+
from utim_cli.config import config
|
|
79
|
+
api_key = config.get("api_key")
|
|
80
|
+
cached_plan = config.get("user_plan")
|
|
81
|
+
|
|
82
|
+
if not force_refresh and cached_plan and is_paid_plan(cached_plan):
|
|
83
|
+
return True
|
|
84
|
+
|
|
85
|
+
if not api_key:
|
|
86
|
+
return is_paid_plan(cached_plan)
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
from utim_cli.orchestrator_helpers import get_cached_quota
|
|
90
|
+
quota = get_cached_quota(api_key, force_refresh=force_refresh)
|
|
91
|
+
if quota:
|
|
92
|
+
plan = quota.get("plan") or quota.get("plan_name")
|
|
93
|
+
if plan:
|
|
94
|
+
clean_plan = str(plan).lower().removesuffix(" plan").strip()
|
|
95
|
+
config.set("user_plan", clean_plan)
|
|
96
|
+
return is_paid_plan(clean_plan)
|
|
97
|
+
except Exception:
|
|
98
|
+
pass
|
|
99
|
+
|
|
100
|
+
return is_paid_plan(cached_plan)
|
|
101
|
+
except Exception:
|
|
102
|
+
return False
|
|
103
|
+
|
|
104
|
+
|
|
67
105
|
def _sanitize_json(obj):
|
|
68
106
|
"""Recursively scrub a payload for values that break strict JSON parsers.
|
|
69
107
|
|
|
@@ -181,10 +219,10 @@ def fetch_remote_models_catalog(timeout: float = 3.0) -> dict:
|
|
|
181
219
|
params = {}
|
|
182
220
|
try:
|
|
183
221
|
from utim_cli.config import config
|
|
184
|
-
_plan = (config.get("user_plan") or "
|
|
185
|
-
params["tier"] =
|
|
222
|
+
_plan = str(config.get("user_plan") or "free").lower().strip()
|
|
223
|
+
params["tier"] = _plan
|
|
186
224
|
except Exception:
|
|
187
|
-
params["tier"] = "
|
|
225
|
+
params["tier"] = "free"
|
|
188
226
|
resp = requests.get(url, params=params, headers=get_auth_headers(), timeout=timeout)
|
|
189
227
|
if resp.status_code == 200:
|
|
190
228
|
data = resp.json()
|
|
@@ -1026,10 +1064,19 @@ def repair_and_normalize_tool_call(tc: dict) -> dict:
|
|
|
1026
1064
|
raw_name = str(fn.get("name", "")).strip()
|
|
1027
1065
|
raw_args = fn.get("arguments", "{}")
|
|
1028
1066
|
|
|
1029
|
-
|
|
1067
|
+
raw_name_orig = raw_name
|
|
1030
1068
|
for known in KNOWN_TOOLS:
|
|
1031
1069
|
if raw_name.startswith(known):
|
|
1070
|
+
remainder = raw_name[len(known):].strip()
|
|
1032
1071
|
raw_name = known
|
|
1072
|
+
if remainder and (not str(raw_args).strip() or str(raw_args).strip() == "{}"):
|
|
1073
|
+
attrs = re.findall(r'(\w+)\s*=\s*(?:"((?:[^"\\]|\\.)*)"|\'((?:[^\'\\]|\\.)*)\'|(\S+))', remainder)
|
|
1074
|
+
if attrs:
|
|
1075
|
+
parsed_attrs = {}
|
|
1076
|
+
for _k, _v1, _v2, _v3 in attrs:
|
|
1077
|
+
_v = _v1 or _v2 or _v3 or ""
|
|
1078
|
+
parsed_attrs[_k] = _v
|
|
1079
|
+
raw_args = parsed_attrs
|
|
1033
1080
|
break
|
|
1034
1081
|
if raw_name == known + known:
|
|
1035
1082
|
raw_name = known
|
|
@@ -139,9 +139,9 @@ class Orchestrator(LLMTransportMixin, TurnHistoryMixin, ToolExecutionMixin):
|
|
|
139
139
|
pass
|
|
140
140
|
self.server_url = "https://api.utim.dev"
|
|
141
141
|
self.session_id: Optional[str] = None
|
|
142
|
-
# Primary model — load from config
|
|
142
|
+
# Primary model — load from config (no default model selected on first startup)
|
|
143
143
|
saved_main_model = config.get("main_model") or config.get("model")
|
|
144
|
-
self.model_id: str = saved_main_model if saved_main_model else
|
|
144
|
+
self.model_id: Optional[str] = saved_main_model if saved_main_model else None
|
|
145
145
|
self._current_line_len = 0
|
|
146
146
|
self.tool_results: List[Dict[str, Any]] = []
|
|
147
147
|
self.turn_step_timings: List[Dict[str, Any]] = []
|
|
@@ -501,6 +501,11 @@ class Orchestrator(LLMTransportMixin, TurnHistoryMixin, ToolExecutionMixin):
|
|
|
501
501
|
self._subagent_tool_override = override_tools
|
|
502
502
|
max_iterations = 500 # Continuous execution mode with milestone nudges instead of hard pauses
|
|
503
503
|
|
|
504
|
+
# If no model is configured for the main agent, prompt the user to select one
|
|
505
|
+
if not self.model_id or not str(self.model_id).strip():
|
|
506
|
+
self.console.print('\nPlease select a model by running "/model Then main agent"\n')
|
|
507
|
+
return
|
|
508
|
+
|
|
504
509
|
self.turn_step_timings = []
|
|
505
510
|
self._tool_failure_counts = {}
|
|
506
511
|
self._tool_name_failure_counts = {}
|
|
@@ -244,7 +244,13 @@ def get_cached_quota(api_key: str, force_refresh: bool = False) -> Optional[Dict
|
|
|
244
244
|
session = get_http_session()
|
|
245
245
|
resp = session.get(f"{SERVER_URL}/quota", headers={"X-API-Key": api_key}, timeout=2.0)
|
|
246
246
|
if resp.status_code == 200:
|
|
247
|
-
|
|
247
|
+
data = resp.json()
|
|
248
|
+
_QUOTA_CACHE.update({"data": data, "timestamp": time.time(), "api_key": api_key})
|
|
249
|
+
plan = data.get("plan") or data.get("plan_name")
|
|
250
|
+
if plan:
|
|
251
|
+
from utim_cli.config import config
|
|
252
|
+
clean_plan = str(plan).lower().removesuffix(" plan").strip()
|
|
253
|
+
config.set("user_plan", clean_plan)
|
|
248
254
|
except Exception:
|
|
249
255
|
pass
|
|
250
256
|
threading.Thread(target=_bg_quota_refresh, daemon=True).start()
|
|
@@ -263,6 +269,11 @@ def get_cached_quota(api_key: str, force_refresh: bool = False) -> Optional[Dict
|
|
|
263
269
|
data = resp.json()
|
|
264
270
|
_QUOTA_CACHE.clear()
|
|
265
271
|
_QUOTA_CACHE.update({"data": data, "timestamp": now, "api_key": api_key})
|
|
272
|
+
plan = data.get("plan") or data.get("plan_name")
|
|
273
|
+
if plan:
|
|
274
|
+
from utim_cli.config import config
|
|
275
|
+
clean_plan = str(plan).lower().removesuffix(" plan").strip()
|
|
276
|
+
config.set("user_plan", clean_plan)
|
|
266
277
|
return data
|
|
267
278
|
except Exception:
|
|
268
279
|
pass
|
|
@@ -802,18 +802,8 @@ class LLMTransportMixin:
|
|
|
802
802
|
self.model_source = "utim"
|
|
803
803
|
config.set("main_model_source", "utim")
|
|
804
804
|
else:
|
|
805
|
-
# Paid plan
|
|
806
|
-
if is_apinex:
|
|
807
|
-
fallback_model = "anthropic/claude-3-5-sonnet-20241022"
|
|
808
|
-
self.console.print(f"\n[bold yellow]APINEX model '{chosen_model}' is exclusively for the Free plan.[/bold yellow]")
|
|
809
|
-
self.console.print(f" Switching to your plan's model: '{fallback_model}'.\n")
|
|
810
|
-
if override_model:
|
|
811
|
-
override_model = fallback_model
|
|
812
|
-
else:
|
|
813
|
-
self.model_id = fallback_model
|
|
814
|
-
self.model_source = "utim"
|
|
815
|
-
config.set("main_model_source", "utim")
|
|
816
|
-
elif models_allowed != ["all"] and chosen_model not in models_allowed and free_bonus_balance <= 0.0:
|
|
805
|
+
# Paid plan: APINEX models are free gateway models — allowed on every plan
|
|
806
|
+
if models_allowed != ["all"] and chosen_model not in models_allowed and not is_apinex and free_bonus_balance <= 0.0:
|
|
817
807
|
fallback_model = DEFAULT_MODEL
|
|
818
808
|
self.console.print(f"\n[bold yellow]Model '{chosen_model}' is gated under your current '{quota.get('display_name', 'Free')}' plan.[/bold yellow]")
|
|
819
809
|
self.console.print(f" Downgrading to default allowed model: '{fallback_model}' for this request.")
|
|
@@ -1314,8 +1304,10 @@ class LLMTransportMixin:
|
|
|
1314
1304
|
# NOTE: last_content_time is updated further below, only when
|
|
1315
1305
|
# actual content or tool-call data is parsed from the chunk.
|
|
1316
1306
|
|
|
1317
|
-
|
|
1318
|
-
|
|
1307
|
+
if isinstance(raw_line, bytes):
|
|
1308
|
+
line_str = raw_line.decode("utf-8", errors="ignore").strip()
|
|
1309
|
+
else:
|
|
1310
|
+
line_str = str(raw_line).strip()
|
|
1319
1311
|
if not line_str:
|
|
1320
1312
|
continue
|
|
1321
1313
|
|
|
@@ -21,6 +21,7 @@ from pathlib import Path
|
|
|
21
21
|
from typing import Any, Dict, List, Optional, Set
|
|
22
22
|
|
|
23
23
|
from utim_cli.config import get_utim_dir, config
|
|
24
|
+
from utim_cli.client_utils import is_user_paid
|
|
24
25
|
from utim_cli.plugins.schema import Plugin, PluginManifest
|
|
25
26
|
from utim_cli.plugins.registry import get_registry_plugins, find_registry_plugin
|
|
26
27
|
from utim_cli.plugins import loader as staged_loader
|
|
@@ -149,6 +150,8 @@ class PluginManager:
|
|
|
149
150
|
# ── Enable / Disable / Toggle ────────────────────────────────────────────
|
|
150
151
|
|
|
151
152
|
def enable_plugin(self, name: str) -> bool:
|
|
153
|
+
if not is_user_paid():
|
|
154
|
+
return False
|
|
152
155
|
plugin = self.get_plugin(name)
|
|
153
156
|
if not plugin:
|
|
154
157
|
return False
|
|
@@ -171,6 +174,8 @@ class PluginManager:
|
|
|
171
174
|
return True
|
|
172
175
|
|
|
173
176
|
def toggle_plugin(self, name: str) -> bool:
|
|
177
|
+
if not is_user_paid():
|
|
178
|
+
return False
|
|
174
179
|
return self.disable_plugin(name) if self.is_enabled(name) else self.enable_plugin(name)
|
|
175
180
|
|
|
176
181
|
# ── Installation ─────────────────────────────────────────────────────────
|
|
@@ -191,6 +196,9 @@ class PluginManager:
|
|
|
191
196
|
def install_plugin(self, identifier: str, force: bool = False, plugin_type: str = "auto") -> Dict[str, Any]:
|
|
192
197
|
"""Install a plugin from a registry name, git URL, or local path."""
|
|
193
198
|
result: Dict[str, Any] = {"ok": False, "plugin": identifier, "message": ""}
|
|
199
|
+
if not is_user_paid():
|
|
200
|
+
result["message"] = "Plugins are only available on paid plans (Hobby plan or higher). Please upgrade your subscription at https://utim.dev/upgrade"
|
|
201
|
+
return result
|
|
194
202
|
meta = find_registry_plugin(identifier)
|
|
195
203
|
git_url = self._parse_git_url(identifier)
|
|
196
204
|
|
|
@@ -280,6 +288,12 @@ class PluginManager:
|
|
|
280
288
|
• Any other page -> knowledge plugin (scraped markdown becomes
|
|
281
289
|
a SKILL.md the model gets injected with)
|
|
282
290
|
"""
|
|
291
|
+
if not is_user_paid():
|
|
292
|
+
return {
|
|
293
|
+
"ok": False,
|
|
294
|
+
"message": "Plugins are only available on paid plans (Hobby plan or higher). Please upgrade your subscription at https://utim.dev/upgrade",
|
|
295
|
+
}
|
|
296
|
+
|
|
283
297
|
def _say(msg: str) -> None:
|
|
284
298
|
if progress_cb:
|
|
285
299
|
progress_cb(msg)
|
|
@@ -392,6 +406,9 @@ class PluginManager:
|
|
|
392
406
|
|
|
393
407
|
Returns (ok: bool, message: str, path: Optional[Path]).
|
|
394
408
|
"""
|
|
409
|
+
if not is_user_paid():
|
|
410
|
+
return False, "Plugins are only available on paid plans (Hobby plan or higher). Please upgrade your subscription at https://utim.dev/upgrade", None
|
|
411
|
+
|
|
395
412
|
clean = re.sub(r"[^a-zA-Z0-9_\-]", "-", name.strip().lower()).strip("-")
|
|
396
413
|
if not clean:
|
|
397
414
|
return False, "Invalid plugin name", None
|
|
@@ -469,6 +486,8 @@ class PluginManager:
|
|
|
469
486
|
# ── Aggregation (effectors) ──────────────────────────────────────────────
|
|
470
487
|
|
|
471
488
|
def get_active_skills(self) -> Dict[str, Dict[str, Any]]:
|
|
489
|
+
if not is_user_paid():
|
|
490
|
+
return {}
|
|
472
491
|
skills: Dict[str, Dict[str, Any]] = {}
|
|
473
492
|
for p in self.list_plugins():
|
|
474
493
|
if p.enabled and p.is_valid:
|
|
@@ -480,6 +499,8 @@ class PluginManager:
|
|
|
480
499
|
return skills
|
|
481
500
|
|
|
482
501
|
def get_active_rules(self) -> List[Dict[str, Any]]:
|
|
502
|
+
if not is_user_paid():
|
|
503
|
+
return []
|
|
483
504
|
rules: List[Dict[str, Any]] = []
|
|
484
505
|
for p in self.list_plugins():
|
|
485
506
|
if p.enabled and p.is_valid:
|
|
@@ -487,6 +508,8 @@ class PluginManager:
|
|
|
487
508
|
return rules
|
|
488
509
|
|
|
489
510
|
def get_active_mcp_servers(self) -> Dict[str, Dict[str, Any]]:
|
|
511
|
+
if not is_user_paid():
|
|
512
|
+
return {}
|
|
490
513
|
servers: Dict[str, Dict[str, Any]] = {}
|
|
491
514
|
for p in self.list_plugins():
|
|
492
515
|
if p.enabled and p.is_valid:
|
|
@@ -494,6 +517,8 @@ class PluginManager:
|
|
|
494
517
|
return servers
|
|
495
518
|
|
|
496
519
|
def get_active_tools(self) -> List[Dict[str, Any]]:
|
|
520
|
+
if not is_user_paid():
|
|
521
|
+
return []
|
|
497
522
|
tools: List[Dict[str, Any]] = []
|
|
498
523
|
for p in self.list_plugins():
|
|
499
524
|
if p.enabled and p.is_valid:
|
|
@@ -511,6 +536,8 @@ class PluginManager:
|
|
|
511
536
|
(string/dict commands from manifests) go through hooks.py in
|
|
512
537
|
parallel with per-hook timeouts.
|
|
513
538
|
"""
|
|
539
|
+
if not is_user_paid():
|
|
540
|
+
return []
|
|
514
541
|
from utim_cli.plugins import hooks as hook_engine
|
|
515
542
|
payload = payload or {}
|
|
516
543
|
output: Dict[str, Any] = {}
|
|
@@ -53,6 +53,14 @@ def load_plugin_tools(force_refresh: bool = False) -> Tuple[List[Dict], Dict[str
|
|
|
53
53
|
if not force_refresh and _TOOL_CACHE is not None and (now - _TOOL_CACHE[2]) < _TOOL_CACHE_TTL:
|
|
54
54
|
return _TOOL_CACHE[0], _TOOL_CACHE[1]
|
|
55
55
|
|
|
56
|
+
try:
|
|
57
|
+
from utim_cli.client_utils import is_user_paid
|
|
58
|
+
if not is_user_paid():
|
|
59
|
+
_TOOL_CACHE = ([], {}, now)
|
|
60
|
+
return [], {}
|
|
61
|
+
except Exception:
|
|
62
|
+
pass
|
|
63
|
+
|
|
56
64
|
from utim_cli.plugins.manager import get_plugin_manager
|
|
57
65
|
|
|
58
66
|
schemas: List[Dict] = []
|
|
@@ -678,38 +678,49 @@ def _resolve_user_plan_name(user) -> str:
|
|
|
678
678
|
Safe to call without a DB session — falls back to ``"free"`` on any error
|
|
679
679
|
so that catalog gating cannot leak paid models to anonymous callers.
|
|
680
680
|
|
|
681
|
-
Expiration-aware: an ``active`` subscription whose ``
|
|
681
|
+
Expiration-aware: an ``active`` subscription whose ``current_period_end`` has passed
|
|
682
682
|
(and which was never renewed) is treated as ``free`` — we do not rely on a
|
|
683
683
|
background job to flip the status to ``expired``.
|
|
684
684
|
"""
|
|
685
|
+
if not user:
|
|
686
|
+
return "free"
|
|
685
687
|
try:
|
|
686
|
-
from .db import SessionLocal, UserSubscription
|
|
688
|
+
from .db import SessionLocal, UserSubscription, Plan
|
|
687
689
|
from datetime import datetime, timezone
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
690
|
+
db = SessionLocal()
|
|
691
|
+
try:
|
|
692
|
+
subs = (
|
|
693
|
+
db.query(UserSubscription)
|
|
694
|
+
.filter(UserSubscription.user_id == user.id, UserSubscription.status == "active")
|
|
695
|
+
.order_by(UserSubscription.updated_at.desc(), UserSubscription.created_at.desc())
|
|
696
|
+
.all()
|
|
697
|
+
)
|
|
698
|
+
now = datetime.now(timezone.utc)
|
|
699
|
+
best = None
|
|
700
|
+
best_end = None
|
|
701
|
+
for sub in subs:
|
|
702
|
+
plan = sub.plan
|
|
703
|
+
if not plan and sub.plan_id:
|
|
704
|
+
plan = db.query(Plan).filter((Plan.id == sub.plan_id) | (Plan.name == sub.plan_id)).first()
|
|
705
|
+
if not plan or not getattr(plan, "name", None):
|
|
706
|
+
continue
|
|
707
|
+
end = getattr(sub, "current_period_end", None) or getattr(sub, "end_date", None)
|
|
708
|
+
# Normalise tz-aware end date to naive UTC so the comparison never raises TypeError.
|
|
709
|
+
if end is not None and getattr(end, "tzinfo", None) is not None:
|
|
710
|
+
end = end.astimezone(timezone.utc).replace(tzinfo=None)
|
|
711
|
+
# Skip subscriptions that already expired.
|
|
712
|
+
if end is not None and end < now.replace(tzinfo=None):
|
|
713
|
+
continue
|
|
714
|
+
plan_name = str(plan.name).lower().strip()
|
|
715
|
+
if plan_name != "free":
|
|
716
|
+
return plan_name
|
|
717
|
+
if best is None or (end is not None and (best_end is None or end > best_end)):
|
|
718
|
+
best = plan_name
|
|
719
|
+
best_end = end
|
|
720
|
+
if best is not None:
|
|
721
|
+
return best
|
|
722
|
+
finally:
|
|
723
|
+
db.close()
|
|
713
724
|
except Exception:
|
|
714
725
|
pass
|
|
715
726
|
return "free"
|
|
@@ -2919,12 +2919,12 @@ def _tier_derived_allowlist(plan_name: str) -> set:
|
|
|
2919
2919
|
return set()
|
|
2920
2920
|
out = set()
|
|
2921
2921
|
for mid, entry in (MODEL_REGISTRY or {}).items():
|
|
2922
|
-
# APINEX models are
|
|
2923
|
-
if norm_name != "free" and (getattr(entry, "provider", "").lower() == "apinex" or mid in APINEX_MODEL_IDS):
|
|
2924
|
-
continue
|
|
2922
|
+
# APINEX models are free gateway models — included on every plan
|
|
2925
2923
|
rank = _MODEL_TIER_RANK.get(_model_tier_of(entry), 2) # default mid
|
|
2926
2924
|
if rank in keep:
|
|
2927
2925
|
out.add(mid)
|
|
2926
|
+
# Always append APINEX gateway models (zero-cost, free on all plans)
|
|
2927
|
+
out.update(APINEX_MODEL_IDS)
|
|
2928
2928
|
return out
|
|
2929
2929
|
|
|
2930
2930
|
|
|
@@ -3141,8 +3141,8 @@ def get_model_catalog(user_tier: str = "all") -> dict:
|
|
|
3141
3141
|
# ── Per-tier gating ────────────────────────────────────────────────────
|
|
3142
3142
|
# Each plan has a `Plan.allowed_models` column ("free" | "all" | comma-list).
|
|
3143
3143
|
# We use it to:
|
|
3144
|
-
# 1) Free users → see `is_free` items and all APINEX models (
|
|
3145
|
-
# 2) Paid users (hobby, pro, max, ultimate, etc.) →
|
|
3144
|
+
# 1) Free users → see `is_free` items and all APINEX models (free gateway models)
|
|
3145
|
+
# 2) Paid users (hobby, pro, max, ultimate, etc.) → paid models shown; APINEX models also available
|
|
3146
3146
|
# 3) "all" sentinel / admin view → all models returned
|
|
3147
3147
|
normalized_tier = (user_tier or "all").strip().lower()
|
|
3148
3148
|
is_free_user = normalized_tier in ("free", "free_tier", "default")
|
|
@@ -3158,7 +3158,7 @@ def get_model_catalog(user_tier: str = "all") -> dict:
|
|
|
3158
3158
|
if is_free_user:
|
|
3159
3159
|
def _is_free_item(it: dict) -> bool:
|
|
3160
3160
|
# Free users see:
|
|
3161
|
-
# 1) items with provider == "apinex" (
|
|
3161
|
+
# 1) items with provider == "apinex" (free gateway models, zero cost)
|
|
3162
3162
|
# 2) standard free models (is_free or tier == "free" or ending with ":free")
|
|
3163
3163
|
is_apx = (str(it.get("provider", "")).lower() == "apinex")
|
|
3164
3164
|
return is_apx or bool(it.get("is_free")) or (it.get("tier") == "free") or str(it.get("model_id", "")).endswith(":free")
|
|
@@ -3167,10 +3167,7 @@ def get_model_catalog(user_tier: str = "all") -> dict:
|
|
|
3167
3167
|
payload[key] = [it for it in payload[key] if _is_free_item(it)]
|
|
3168
3168
|
else:
|
|
3169
3169
|
# Non-free users:
|
|
3170
|
-
#
|
|
3171
|
-
if not is_all_user:
|
|
3172
|
-
for key in ("main_agent", "plan_project", "subagent_text", "analyze_image", "image_gen", "all_text"):
|
|
3173
|
-
payload[key] = [it for it in payload[key] if str(it.get("provider", "")).lower() != "apinex"]
|
|
3170
|
+
# APINEX models are free gateway models (zero cost) — they remain available on paid plans too.
|
|
3174
3171
|
|
|
3175
3172
|
# Paid plan: look up the Plan row's `allowed_models`
|
|
3176
3173
|
try:
|
|
@@ -287,6 +287,8 @@ async def device_poll(device_code: str, request: Request, db: Session = Depends(
|
|
|
287
287
|
if code.status == "authorized":
|
|
288
288
|
# Return the api_key and burn the code immediately (one-time use)
|
|
289
289
|
user = db.query(User).filter(User.id == code.user_id).first()
|
|
290
|
+
from ..auth import _resolve_user_plan_name
|
|
291
|
+
user_plan = _resolve_user_plan_name(user) if user else "free"
|
|
290
292
|
result = {
|
|
291
293
|
"status": "authorized",
|
|
292
294
|
"api_key": code.api_key,
|
|
@@ -295,6 +297,8 @@ async def device_poll(device_code: str, request: Request, db: Session = Depends(
|
|
|
295
297
|
"user_id": code.user_id,
|
|
296
298
|
"firebase_uid": user.firebase_uid if user else "",
|
|
297
299
|
"credits": user.credits.balance if user and user.credits else 0,
|
|
300
|
+
"plan": user_plan,
|
|
301
|
+
"is_subscribed": user_plan != "free",
|
|
298
302
|
}
|
|
299
303
|
# Invalidate: delete the row so this device_code can never be reused
|
|
300
304
|
db.delete(code)
|
|
@@ -80,7 +80,7 @@ def is_model_allowed(model_id: str, allowed_models_str: str, bonus_balance: floa
|
|
|
80
80
|
"""Check if a model is allowed for the user's plan.
|
|
81
81
|
|
|
82
82
|
If bonus_balance > 0 on any plan (including free), all models are unlocked.
|
|
83
|
-
APINEX models are
|
|
83
|
+
APINEX models are free gateway models (zero cost) and are allowed on every plan.
|
|
84
84
|
"""
|
|
85
85
|
allowed_tokens = [t.strip() for t in allowed_models_str.split(",")]
|
|
86
86
|
is_free_plan = "free" in allowed_tokens and len(allowed_tokens) == 1
|
|
@@ -91,12 +91,9 @@ def is_model_allowed(model_id: str, allowed_models_str: str, bonus_balance: floa
|
|
|
91
91
|
or (entry and getattr(entry, "provider", "").lower() == "apinex")
|
|
92
92
|
)
|
|
93
93
|
|
|
94
|
-
# 1. APINEX models: allowed on
|
|
94
|
+
# 1. APINEX models: free gateway models — allowed on ALL plans (free + paid)
|
|
95
95
|
if is_apinex_model:
|
|
96
|
-
|
|
97
|
-
return True
|
|
98
|
-
# Paid plan: APINEX models are gated for paid users
|
|
99
|
-
return False
|
|
96
|
+
return True
|
|
100
97
|
|
|
101
98
|
if bonus_balance > 0.0:
|
|
102
99
|
return True
|
|
@@ -204,12 +201,14 @@ def _effective_allowed_models(plan_name: str, plan_allowed_models: str) -> str:
|
|
|
204
201
|
keep = ranks_for_plan.get(name, {0, 1, 2, 3, 4})
|
|
205
202
|
out = []
|
|
206
203
|
for mid, entry in (MODEL_REGISTRY or {}).items():
|
|
207
|
-
# APINEX models are
|
|
208
|
-
if name != "free" and (getattr(entry, "provider", "").lower() == "apinex" or mid in APINEX_MODEL_IDS):
|
|
209
|
-
continue
|
|
204
|
+
# APINEX models are free gateway models — included on every plan
|
|
210
205
|
entry_rank = _MODEL_TIER_RANK.get(_model_tier_of(entry), 2) # default mid
|
|
211
206
|
if entry_rank in keep:
|
|
212
207
|
out.append(mid)
|
|
208
|
+
# Always append APINEX gateway models (zero-cost, free on all plans)
|
|
209
|
+
for ap_id in APINEX_MODEL_IDS:
|
|
210
|
+
if ap_id not in out:
|
|
211
|
+
out.append(ap_id)
|
|
213
212
|
if out:
|
|
214
213
|
return ",".join(out)
|
|
215
214
|
except Exception:
|
|
@@ -1412,6 +1411,7 @@ def deduct_credits_unallocated_first(db: Session, user: User, credits_cost: floa
|
|
|
1412
1411
|
if unallocated > 0.0:
|
|
1413
1412
|
take = min(cost_to_charge, unallocated)
|
|
1414
1413
|
sub_row.unallocated_deducted = unallocated_deducted + take
|
|
1414
|
+
charged_to_monthly += take
|
|
1415
1415
|
cost_to_charge -= take
|
|
1416
1416
|
|
|
1417
1417
|
# 3. credit.balance (Quota Bank / Rollover)
|