utim-cli 1.47.1__tar.gz → 1.47.2__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-1.47.1 → utim_cli-1.47.2}/CHANGELOG.md +11 -0
- {utim_cli-1.47.1/utim_cli.egg-info → utim_cli-1.47.2}/PKG-INFO +1 -1
- {utim_cli-1.47.1 → utim_cli-1.47.2}/pyproject.toml +1 -1
- {utim_cli-1.47.1 → utim_cli-1.47.2}/utim_cli/__init__.py +1 -1
- {utim_cli-1.47.1 → utim_cli-1.47.2}/utim_cli/_version.py +1 -1
- {utim_cli-1.47.1 → utim_cli-1.47.2}/utim_cli/orchestrator.py +2 -0
- {utim_cli-1.47.1 → utim_cli-1.47.2}/utim_cli/reflection.py +13 -7
- {utim_cli-1.47.1 → utim_cli-1.47.2}/utim_cli/server/db.py +26 -0
- {utim_cli-1.47.1 → utim_cli-1.47.2}/utim_cli/server/models.py +27 -0
- {utim_cli-1.47.1 → utim_cli-1.47.2}/utim_cli/server/router.py +5 -3
- {utim_cli-1.47.1 → utim_cli-1.47.2}/utim_cli/server/routes/__init__.py +2 -1
- {utim_cli-1.47.1 → utim_cli-1.47.2}/utim_cli/server/routes/quota_routes.py +81 -5
- utim_cli-1.47.2/utim_cli/server/routes/referral_routes.py +248 -0
- {utim_cli-1.47.1 → utim_cli-1.47.2}/utim_cli/tools.py +18 -1
- {utim_cli-1.47.1 → utim_cli-1.47.2}/utim_cli/tui/model_dialog.py +13 -2
- {utim_cli-1.47.1 → utim_cli-1.47.2}/utim_cli/utim.py +4 -3
- {utim_cli-1.47.1 → utim_cli-1.47.2/utim_cli.egg-info}/PKG-INFO +1 -1
- {utim_cli-1.47.1 → utim_cli-1.47.2}/utim_cli.egg-info/SOURCES.txt +1 -0
- {utim_cli-1.47.1 → utim_cli-1.47.2}/LICENSE +0 -0
- {utim_cli-1.47.1 → utim_cli-1.47.2}/MANIFEST.in +0 -0
- {utim_cli-1.47.1 → utim_cli-1.47.2}/README.md +0 -0
- {utim_cli-1.47.1 → utim_cli-1.47.2}/setup.cfg +0 -0
- {utim_cli-1.47.1 → utim_cli-1.47.2}/setup.py +0 -0
- {utim_cli-1.47.1 → utim_cli-1.47.2}/utim_cli/agent.py +0 -0
- {utim_cli-1.47.1 → utim_cli-1.47.2}/utim_cli/auth.py +0 -0
- {utim_cli-1.47.1 → utim_cli-1.47.2}/utim_cli/backup.py +0 -0
- {utim_cli-1.47.1 → utim_cli-1.47.2}/utim_cli/blender_agent.py +0 -0
- {utim_cli-1.47.1 → utim_cli-1.47.2}/utim_cli/bootstrap.py +0 -0
- {utim_cli-1.47.1 → utim_cli-1.47.2}/utim_cli/client_utils.py +0 -0
- {utim_cli-1.47.1 → utim_cli-1.47.2}/utim_cli/config.py +0 -0
- {utim_cli-1.47.1 → utim_cli-1.47.2}/utim_cli/constants.py +0 -0
- {utim_cli-1.47.1 → utim_cli-1.47.2}/utim_cli/context_pruner.py +0 -0
- {utim_cli-1.47.1 → utim_cli-1.47.2}/utim_cli/doctor.py +0 -0
- {utim_cli-1.47.1 → utim_cli-1.47.2}/utim_cli/knowledge_graph.py +0 -0
- {utim_cli-1.47.1 → utim_cli-1.47.2}/utim_cli/local_db.py +0 -0
- {utim_cli-1.47.1 → utim_cli-1.47.2}/utim_cli/logger.py +0 -0
- {utim_cli-1.47.1 → utim_cli-1.47.2}/utim_cli/mcp_clean_wrapper.py +0 -0
- {utim_cli-1.47.1 → utim_cli-1.47.2}/utim_cli/mcp_client.py +0 -0
- {utim_cli-1.47.1 → utim_cli-1.47.2}/utim_cli/mcp_registry.json +0 -0
- {utim_cli-1.47.1 → utim_cli-1.47.2}/utim_cli/report.py +0 -0
- {utim_cli-1.47.1 → utim_cli-1.47.2}/utim_cli/scrapy_search.py +0 -0
- {utim_cli-1.47.1 → utim_cli-1.47.2}/utim_cli/server/__init__.py +0 -0
- {utim_cli-1.47.1 → utim_cli-1.47.2}/utim_cli/server/auth.py +0 -0
- {utim_cli-1.47.1 → utim_cli-1.47.2}/utim_cli/server/email_utils.py +0 -0
- {utim_cli-1.47.1 → utim_cli-1.47.2}/utim_cli/server/exchange_rate.py +0 -0
- {utim_cli-1.47.1 → utim_cli-1.47.2}/utim_cli/server/firebase.py +0 -0
- {utim_cli-1.47.1 → utim_cli-1.47.2}/utim_cli/server/history.py +0 -0
- {utim_cli-1.47.1 → utim_cli-1.47.2}/utim_cli/server/logging_config.py +0 -0
- {utim_cli-1.47.1 → utim_cli-1.47.2}/utim_cli/server/pricing_updater.py +0 -0
- {utim_cli-1.47.1 → utim_cli-1.47.2}/utim_cli/server/rate_limit.py +0 -0
- {utim_cli-1.47.1 → utim_cli-1.47.2}/utim_cli/server/routes/auth_routes.py +0 -0
- {utim_cli-1.47.1 → utim_cli-1.47.2}/utim_cli/server/routes/completion_routes.py +0 -0
- {utim_cli-1.47.1 → utim_cli-1.47.2}/utim_cli/server/routes/credit_routes.py +0 -0
- {utim_cli-1.47.1 → utim_cli-1.47.2}/utim_cli/server/routes/feedback_routes.py +0 -0
- {utim_cli-1.47.1 → utim_cli-1.47.2}/utim_cli/server/routes/session_routes.py +0 -0
- {utim_cli-1.47.1 → utim_cli-1.47.2}/utim_cli/server/routes/share_routes.py +0 -0
- {utim_cli-1.47.1 → utim_cli-1.47.2}/utim_cli/server/server.py +0 -0
- {utim_cli-1.47.1 → utim_cli-1.47.2}/utim_cli/share.py +0 -0
- {utim_cli-1.47.1 → utim_cli-1.47.2}/utim_cli/share_tui.py +0 -0
- {utim_cli-1.47.1 → utim_cli-1.47.2}/utim_cli/situational_scoring.py +0 -0
- {utim_cli-1.47.1 → utim_cli-1.47.2}/utim_cli/state.py +0 -0
- {utim_cli-1.47.1 → utim_cli-1.47.2}/utim_cli/tui/__init__.py +0 -0
- {utim_cli-1.47.1 → utim_cli-1.47.2}/utim_cli/tui/feedback_dialog.py +0 -0
- {utim_cli-1.47.1 → utim_cli-1.47.2}/utim_cli/tui/history_dialog.py +0 -0
- {utim_cli-1.47.1 → utim_cli-1.47.2}/utim_cli/tui/mcp_dialog.py +0 -0
- {utim_cli-1.47.1 → utim_cli-1.47.2}/utim_cli/tui/quota_dialog.py +0 -0
- {utim_cli-1.47.1 → utim_cli-1.47.2}/utim_cli/tui/resume_dialog.py +0 -0
- {utim_cli-1.47.1 → utim_cli-1.47.2}/utim_cli/tui/tools_dialog.py +0 -0
- {utim_cli-1.47.1 → utim_cli-1.47.2}/utim_cli/tui/update_dialog.py +0 -0
- {utim_cli-1.47.1 → utim_cli-1.47.2}/utim_cli/utilities.py +0 -0
- {utim_cli-1.47.1 → utim_cli-1.47.2}/utim_cli/vector_memory.py +0 -0
- {utim_cli-1.47.1 → utim_cli-1.47.2}/utim_cli/workspace.py +0 -0
- {utim_cli-1.47.1 → utim_cli-1.47.2}/utim_cli.egg-info/dependency_links.txt +0 -0
- {utim_cli-1.47.1 → utim_cli-1.47.2}/utim_cli.egg-info/entry_points.txt +0 -0
- {utim_cli-1.47.1 → utim_cli-1.47.2}/utim_cli.egg-info/requires.txt +0 -0
- {utim_cli-1.47.1 → utim_cli-1.47.2}/utim_cli.egg-info/top_level.txt +0 -0
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.47.2] - 2026-07-18
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- **UnboundLocalError in Command Output Renderer**: Pre-initialized stdout and stderr lines lists in `_render_result` to prevent runtime exception on commands yielding empty output.
|
|
7
|
+
- **Chunk-Boundary Unicode Decode Errors**: Upgraded `read_file` tool to slice trailing incomplete bytes during UTF-8 checks, preventing decoding failures on split multi-byte character boundaries.
|
|
8
|
+
- **Reflection Engine Skill Creation Guardrail**: Restructured reflection engine to only create new skill files when at least 3 substantial guidelines (each >= 40 characters) are suggested, preventing clutter from useless "one-liner" skills.
|
|
9
|
+
- **Console Status Spinner Fallback**: Patched the `console.status` spinner in `utim.py` to check for legacy Windows CMD/conhost and non-UTF-8 environments, dynamically falling back to ASCII `line` spinners instead of unicode dots.
|
|
10
|
+
- **Support Chat Model Casing**: Updated frontend support assistant to target `openrouter/free` exclusively to maintain service reliability.
|
|
11
|
+
|
|
3
12
|
## [1.47.1] - 2026-07-18
|
|
4
13
|
|
|
5
14
|
### Added
|
|
15
|
+
- **Referral Program**: Launched a full referral system. Referrers earn a 2% per-plan discount for each referred user who purchases a subscription. Discounts stack per-plan up to 100% (fully free). Every payment renewal by a referee adds another 2%. Referral dashboard available at `/referral` with live discount breakdown, progress bars, copy-ready referral link/code, and a leaderboard.
|
|
6
16
|
- **Instant Server-Side Deletion**: Connected client share deletion to a new `DELETE /shares/delete/{share_id}` server endpoint to destroy shared zip packages and metadata instantly when removed from the CLI dashboard.
|
|
7
17
|
- **Custom Exclude Checklist Option**: Embedded a dynamic `[ Add custom files/folders... ]` checkbox directly into the share wizard to prompt for custom omits only when selected, rather than asking unconditionally.
|
|
8
18
|
- **Exclusion Path Root Validation**: Added validation to reject and display warnings for typed/pasted custom paths that fall outside the current workspace root directory.
|
|
9
19
|
- **Ctrl+D Dashboard Shortcut**: Resolved key collision where pressing `D` got typed in the default-focused search bar by binding `Ctrl+D` globally to delete the active share.
|
|
10
20
|
|
|
11
21
|
### Fixed
|
|
22
|
+
- **Content Security Policy Alignment**: Added `https://ipapi.co` and `https://analytics.google.com` to Netlify's `connect-src` CSP header rules, allowing the website frontend to complete geolocated IP checking and analytics telemetry during startup without throwing console errors or blocking the AI chat component.
|
|
12
23
|
- **Unicode Markdown Rendering Fallbacks**: Patched Rich's markdown rule, table, and list item components to detect legacy Command Prompt terminals and fallback to safe ASCII representations (`*` for bullets, `-` for rules, and `+--+` for table grids) instead of throwing UnicodeEncodeErrors or printing replacement boxes.
|
|
13
24
|
- **Unicode Fallback Spinners**: Re-engineered all loading/thinking spinners and progress bar characters to detect legacy Windows CMD/conhost terminals and non-UTF-8 console environments, falling back to clean ASCII patterns (`['|', '/', '-', '\\']` and `[#---]`) to prevent rendering boxes/question marks.
|
|
14
25
|
- **Graceful Prompt-Toolkit Dialog Exits**: Introduced a safe-exit mechanism to catch and prevent unhandled `Application.exit()` loop exceptions.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "utim-cli"
|
|
7
|
-
version = "1.47.
|
|
7
|
+
version = "1.47.2"
|
|
8
8
|
description = "UTIM – Universal Terminal Intelligence Manager. A powerful agentic AI coding assistant for your terminal."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -969,15 +969,24 @@ def apply_skill_modifications(skill_modifications: Dict[str, List[str]]):
|
|
|
969
969
|
|
|
970
970
|
for skill_path in paths_to_write:
|
|
971
971
|
try:
|
|
972
|
-
|
|
973
|
-
|
|
972
|
+
skill_exists = skill_path.exists()
|
|
973
|
+
|
|
974
|
+
# Filter rules by length and substance based on whether the skill already exists
|
|
975
|
+
min_len = 25 if skill_exists else 40
|
|
976
|
+
valid_rules = [r.strip() for r in new_rules if r and len(r.strip()) >= min_len]
|
|
977
|
+
|
|
978
|
+
# Enforce strict guardrail for new skills: must have at least 3 substantial rules
|
|
979
|
+
if not skill_exists and len(valid_rules) < 3:
|
|
980
|
+
continue # Ignore creation of this new skill entirely
|
|
974
981
|
|
|
975
982
|
content = ""
|
|
976
|
-
if
|
|
983
|
+
if skill_exists:
|
|
977
984
|
with open(skill_path, 'r', encoding='utf-8') as f:
|
|
978
985
|
content = f.read()
|
|
979
986
|
|
|
980
987
|
if not content:
|
|
988
|
+
# Ensure parent directory exists before writing
|
|
989
|
+
skill_path.parent.mkdir(parents=True, exist_ok=True)
|
|
981
990
|
# Initialize a new skill md template
|
|
982
991
|
title_name = skill_name.replace('-', ' ').title()
|
|
983
992
|
content = f"""---
|
|
@@ -992,10 +1001,7 @@ description: Automatically learned guidelines for {title_name}.
|
|
|
992
1001
|
|
|
993
1002
|
# Append new rules avoiding duplicates
|
|
994
1003
|
rules_to_add = []
|
|
995
|
-
for
|
|
996
|
-
rule_clean = rule.strip()
|
|
997
|
-
if not rule_clean:
|
|
998
|
-
continue
|
|
1004
|
+
for rule_clean in valid_rules:
|
|
999
1005
|
# Check if already present in content (case-insensitive substring check)
|
|
1000
1006
|
if rule_clean.lower() not in content.lower():
|
|
1001
1007
|
rules_to_add.append(rule_clean)
|
|
@@ -54,7 +54,10 @@ class User(Base):
|
|
|
54
54
|
created_at = Column(DateTime, default=datetime.datetime.utcnow, nullable=False)
|
|
55
55
|
last_project_folder = Column(Text, nullable=True)
|
|
56
56
|
firebase_uid = Column(String(128), nullable=True, index=True)
|
|
57
|
+
referrer_id = Column(String(36), ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
|
|
58
|
+
referral_code = Column(String(50), unique=True, nullable=False, default=lambda: uuid.uuid4().hex[:8])
|
|
57
59
|
|
|
60
|
+
referrer = relationship("User", remote_side=[id], backref="referees")
|
|
58
61
|
credits = relationship("Credit", back_populates="user", uselist=False, cascade="all, delete-orphan")
|
|
59
62
|
conversations = relationship("Conversation", back_populates="user", cascade="all, delete-orphan")
|
|
60
63
|
transactions = relationship("Transaction", back_populates="user", cascade="all, delete-orphan")
|
|
@@ -472,6 +475,14 @@ def init_db(silent: bool = True) -> None:
|
|
|
472
475
|
# Run manual migrations for missing columns
|
|
473
476
|
# Each ALTER runs in its OWN transaction so one failure doesn't block the rest.
|
|
474
477
|
from sqlalchemy import text, inspect as sa_inspect
|
|
478
|
+
|
|
479
|
+
# Ensure referral_discounts table exists (defined in referral_routes, not imported into Base yet)
|
|
480
|
+
try:
|
|
481
|
+
from .routes.referral_routes import ReferralDiscount
|
|
482
|
+
Base.metadata.create_all(bind=engine, tables=[ReferralDiscount.__table__])
|
|
483
|
+
except Exception as _ref_tbl_exc:
|
|
484
|
+
_print(f"[DB INIT] referral_discounts table init note: {_ref_tbl_exc}")
|
|
485
|
+
|
|
475
486
|
is_postgres = DATABASE_URL.startswith("postgresql")
|
|
476
487
|
|
|
477
488
|
try:
|
|
@@ -512,6 +523,8 @@ def init_db(silent: bool = True) -> None:
|
|
|
512
523
|
_print("[DB INIT] Postgres detected. Running ALTER TABLE migrations if needed.")
|
|
513
524
|
_run_migration("credits", "free_monthly_used", "ALTER TABLE credits ADD COLUMN IF NOT EXISTS free_monthly_used FLOAT DEFAULT 0.0 NOT NULL", "postgres add free_monthly_used")
|
|
514
525
|
_run_migration("users", "firebase_uid", "ALTER TABLE users ADD COLUMN IF NOT EXISTS firebase_uid VARCHAR(128)", "postgres add firebase_uid")
|
|
526
|
+
_run_migration("users", "referrer_id", "ALTER TABLE users ADD COLUMN IF NOT EXISTS referrer_id VARCHAR(36)", "postgres add referrer_id")
|
|
527
|
+
_run_migration("users", "referral_code", "ALTER TABLE users ADD COLUMN IF NOT EXISTS referral_code VARCHAR(50)", "postgres add referral_code")
|
|
515
528
|
_run_migration("feedbacks", "chat_history", "ALTER TABLE feedbacks ADD COLUMN IF NOT EXISTS chat_history TEXT", "postgres add chat_history")
|
|
516
529
|
else:
|
|
517
530
|
# SQLite: no IF NOT EXISTS on ADD COLUMN, swallow duplicate errors
|
|
@@ -526,6 +539,8 @@ def init_db(silent: bool = True) -> None:
|
|
|
526
539
|
_run_migration("email_tracking", "bonus_email_sent_at", "ALTER TABLE email_tracking ADD COLUMN bonus_email_sent_at TIMESTAMP", "sqlite add bonus_email_sent_at")
|
|
527
540
|
_run_migration("users", "last_project_folder", "ALTER TABLE users ADD COLUMN last_project_folder TEXT", "sqlite add last_project_folder")
|
|
528
541
|
_run_migration("users", "firebase_uid", "ALTER TABLE users ADD COLUMN firebase_uid VARCHAR(128)", "sqlite add firebase_uid")
|
|
542
|
+
_run_migration("users", "referrer_id", "ALTER TABLE users ADD COLUMN referrer_id VARCHAR(36)", "sqlite add referrer_id")
|
|
543
|
+
_run_migration("users", "referral_code", "ALTER TABLE users ADD COLUMN referral_code VARCHAR(50)", "sqlite add referral_code")
|
|
529
544
|
_run_migration("feedbacks", "chat_history", "ALTER TABLE feedbacks ADD COLUMN chat_history TEXT", "sqlite add feedbacks chat_history")
|
|
530
545
|
|
|
531
546
|
# Seed the plans if they don't exist
|
|
@@ -654,6 +669,17 @@ def init_db(silent: bool = True) -> None:
|
|
|
654
669
|
ultimate_plan.credits_per_month = 90000
|
|
655
670
|
if ultimate_plan.price_inr != 11000:
|
|
656
671
|
ultimate_plan.price_inr = 11000
|
|
672
|
+
|
|
673
|
+
# Generate referral codes for existing users that don't have one
|
|
674
|
+
try:
|
|
675
|
+
users_without_code = db.query(User).filter(User.referral_code == None).all()
|
|
676
|
+
if users_without_code:
|
|
677
|
+
_print(f"[DB INIT] Generating referral codes for {len(users_without_code)} users...")
|
|
678
|
+
for u in users_without_code:
|
|
679
|
+
u.referral_code = uuid.uuid4().hex[:8]
|
|
680
|
+
except Exception as ref_exc:
|
|
681
|
+
_print(f"[DB INIT] Referral code migration check failed: {ref_exc}")
|
|
682
|
+
|
|
657
683
|
db.commit()
|
|
658
684
|
except Exception as exc:
|
|
659
685
|
db.rollback()
|
|
@@ -1116,6 +1116,33 @@ MODEL_REGISTRY: dict[MODEL_ID, ModelEntry] = {
|
|
|
1116
1116
|
capabilities=["chat", "code"],
|
|
1117
1117
|
tags=["premium"],
|
|
1118
1118
|
),
|
|
1119
|
+
"thinkingmachines/inkling": ModelEntry(
|
|
1120
|
+
model_id="thinkingmachines/inkling",
|
|
1121
|
+
provider="openrouter",
|
|
1122
|
+
cost_input_per_1k=0.00105,
|
|
1123
|
+
cost_output_per_1k=0.0042525,
|
|
1124
|
+
context_window=1_048_576,
|
|
1125
|
+
capabilities=["chat", "code"],
|
|
1126
|
+
tags=["premium"],
|
|
1127
|
+
),
|
|
1128
|
+
"moonshotai/kimi-k3": ModelEntry(
|
|
1129
|
+
model_id="moonshotai/kimi-k3",
|
|
1130
|
+
provider="openrouter",
|
|
1131
|
+
cost_input_per_1k=0.00315,
|
|
1132
|
+
cost_output_per_1k=0.01575,
|
|
1133
|
+
context_window=1_000_000,
|
|
1134
|
+
capabilities=["chat", "reasoning"],
|
|
1135
|
+
tags=["premium", "reasoning"],
|
|
1136
|
+
),
|
|
1137
|
+
"meta/muse-spark-1.1": ModelEntry(
|
|
1138
|
+
model_id="meta/muse-spark-1.1",
|
|
1139
|
+
provider="openrouter",
|
|
1140
|
+
cost_input_per_1k=0.0013125,
|
|
1141
|
+
cost_output_per_1k=0.0044625,
|
|
1142
|
+
context_window=1_048_576,
|
|
1143
|
+
capabilities=["chat", "code", "multimodal"],
|
|
1144
|
+
tags=["premium", "multimodal"],
|
|
1145
|
+
),
|
|
1119
1146
|
}
|
|
1120
1147
|
|
|
1121
1148
|
# Default model used when the user does not specify one
|
|
@@ -26,7 +26,7 @@ from .logging_config import configure_logging, RequestLoggingMiddleware
|
|
|
26
26
|
from .rate_limit import limiter
|
|
27
27
|
from slowapi import _rate_limit_exceeded_handler
|
|
28
28
|
from slowapi.errors import RateLimitExceeded
|
|
29
|
-
from .routes import auth_router, credit_router, session_router, completion_router, quota_router, share_router, feedback_router
|
|
29
|
+
from .routes import auth_router, credit_router, session_router, completion_router, quota_router, share_router, feedback_router, referral_router
|
|
30
30
|
from .models import list_models
|
|
31
31
|
from .auth import get_admin_user
|
|
32
32
|
|
|
@@ -112,6 +112,7 @@ app.include_router(completion_router)
|
|
|
112
112
|
app.include_router(quota_router)
|
|
113
113
|
app.include_router(share_router)
|
|
114
114
|
app.include_router(feedback_router)
|
|
115
|
+
app.include_router(referral_router)
|
|
115
116
|
|
|
116
117
|
# ── Support Chatbot Endpoint ──────────────────────────────────────────────────
|
|
117
118
|
|
|
@@ -189,8 +190,9 @@ async def support_chat(request: Request, req: SupportChatRequest):
|
|
|
189
190
|
import json
|
|
190
191
|
ALLOWED_FREE_MODELS = {
|
|
191
192
|
"poolside/laguna-xs.2:free",
|
|
192
|
-
"google/gemma-2-9b-it:free",
|
|
193
193
|
"cohere/north-mini-code:free",
|
|
194
|
+
"google/gemma-4-31b-it:free",
|
|
195
|
+
"google/gemma-4-26b-a4b-it:free",
|
|
194
196
|
"openrouter/free"
|
|
195
197
|
}
|
|
196
198
|
|
|
@@ -210,7 +212,7 @@ async def support_chat(request: Request, req: SupportChatRequest):
|
|
|
210
212
|
|
|
211
213
|
# Generate candidate models starting with the requested one
|
|
212
214
|
candidate_models = [model_name]
|
|
213
|
-
for fallback in ["openrouter/free", "
|
|
215
|
+
for fallback in ["openrouter/free", "cohere/north-mini-code:free", "google/gemma-4-31b-it:free", "poolside/laguna-xs.2:free"]:
|
|
214
216
|
if fallback not in candidate_models:
|
|
215
217
|
candidate_models.append(fallback)
|
|
216
218
|
|
|
@@ -5,6 +5,7 @@ from .completion_routes import router as completion_router
|
|
|
5
5
|
from .quota_routes import router as quota_router
|
|
6
6
|
from .share_routes import router as share_router
|
|
7
7
|
from .feedback_routes import router as feedback_router
|
|
8
|
+
from .referral_routes import router as referral_router
|
|
8
9
|
|
|
9
|
-
__all__ = ["auth_router", "credit_router", "session_router", "completion_router", "quota_router", "share_router", "feedback_router"]
|
|
10
|
+
__all__ = ["auth_router", "credit_router", "session_router", "completion_router", "quota_router", "share_router", "feedback_router", "referral_router"]
|
|
10
11
|
|
|
@@ -1196,6 +1196,23 @@ def api_subscription_create(
|
|
|
1196
1196
|
|
|
1197
1197
|
currency = (req.currency or "INR").upper().strip()
|
|
1198
1198
|
|
|
1199
|
+
# Fetch user's active referral discounts
|
|
1200
|
+
from .referral_routes import get_referrer_discounts
|
|
1201
|
+
try:
|
|
1202
|
+
discounts = get_referrer_discounts(db, user.id)
|
|
1203
|
+
except Exception:
|
|
1204
|
+
discounts = {}
|
|
1205
|
+
|
|
1206
|
+
discount_pct = min(100.0, discounts.get(db_plan_id, 0.0))
|
|
1207
|
+
|
|
1208
|
+
# If the user has a 100% discount, bypass Razorpay payment entirely
|
|
1209
|
+
if discount_pct >= 100.0:
|
|
1210
|
+
return {
|
|
1211
|
+
"success": True,
|
|
1212
|
+
"subscriptionId": f"free_referral_100pct_{db_plan_id}_monthly",
|
|
1213
|
+
"keyId": "free"
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1199
1216
|
if not key_id or key_id == "mock_key_id":
|
|
1200
1217
|
return {
|
|
1201
1218
|
"success": True,
|
|
@@ -1205,7 +1222,48 @@ def api_subscription_create(
|
|
|
1205
1222
|
|
|
1206
1223
|
import requests
|
|
1207
1224
|
|
|
1208
|
-
|
|
1225
|
+
# Try to load discounted plan from environment variables or JSON mapping
|
|
1226
|
+
env_plan_id = None
|
|
1227
|
+
if 0.0 < discount_pct < 100.0:
|
|
1228
|
+
# Map to closest step of 2 (2%, 4%, ..., 98%)
|
|
1229
|
+
pct_step = int(round(discount_pct / 2) * 2)
|
|
1230
|
+
pct_step = max(2, min(98, pct_step))
|
|
1231
|
+
|
|
1232
|
+
# Determine variable keys checking aliases
|
|
1233
|
+
plan_env_name = db_plan_id.upper()
|
|
1234
|
+
if plan_env_name == "PRO":
|
|
1235
|
+
plan_env_name = "STARTER"
|
|
1236
|
+
elif plan_env_name == "MAX":
|
|
1237
|
+
plan_env_name = "PROFESSIONAL"
|
|
1238
|
+
|
|
1239
|
+
env_key = f"RAZORPAY_PLAN_{plan_env_name}_{pct_step}"
|
|
1240
|
+
env_plan_id = os.environ.get(env_key)
|
|
1241
|
+
|
|
1242
|
+
# Fallback check for direct DB plan name
|
|
1243
|
+
if not env_plan_id:
|
|
1244
|
+
env_key_direct = f"RAZORPAY_PLAN_{db_plan_id.upper()}_{pct_step}"
|
|
1245
|
+
env_plan_id = os.environ.get(env_key_direct)
|
|
1246
|
+
|
|
1247
|
+
if env_plan_id:
|
|
1248
|
+
logger.info(f"Dynamic discounted plan resolved from environment: {env_plan_id} ({env_key})")
|
|
1249
|
+
else:
|
|
1250
|
+
# Fallback to local JSON mapping file
|
|
1251
|
+
import json
|
|
1252
|
+
json_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "razorpay_discounted_plans.json")
|
|
1253
|
+
if os.path.exists(json_path):
|
|
1254
|
+
try:
|
|
1255
|
+
with open(json_path, "r", encoding="utf-8") as f:
|
|
1256
|
+
plans_map = json.load(f)
|
|
1257
|
+
plan_key = f"{db_plan_id}_{pct_step}"
|
|
1258
|
+
env_plan_id = plans_map.get(plan_key)
|
|
1259
|
+
if env_plan_id:
|
|
1260
|
+
logger.info(f"Dynamic discounted plan resolved from JSON: {env_plan_id} for user {user.id} ({plan_key})")
|
|
1261
|
+
except Exception as json_err:
|
|
1262
|
+
logger.error(f"Failed to read discounted plans JSON mapping: {json_err}")
|
|
1263
|
+
|
|
1264
|
+
if not env_plan_id:
|
|
1265
|
+
env_plan_id = get_razorpay_plan_id(db_plan_id, interval, currency)
|
|
1266
|
+
|
|
1209
1267
|
if not env_plan_id:
|
|
1210
1268
|
raise HTTPException(
|
|
1211
1269
|
status_code=400,
|
|
@@ -1254,15 +1312,26 @@ def api_subscription_verify(
|
|
|
1254
1312
|
key_id = os.environ.get("RAZORPAY_KEY_ID", "")
|
|
1255
1313
|
key_secret = os.environ.get("RAZORPAY_KEY_SECRET", "")
|
|
1256
1314
|
|
|
1257
|
-
if req.razorpay_subscription_id.startswith("sub_mock") or not key_id or key_id == "mock_key_id":
|
|
1315
|
+
if req.razorpay_subscription_id.startswith("free_referral_100pct") or req.razorpay_subscription_id.startswith("sub_mock") or not key_id or key_id == "mock_key_id":
|
|
1258
1316
|
plan_id = "hobby"
|
|
1259
1317
|
interval = "monthly"
|
|
1260
1318
|
parts = req.razorpay_subscription_id.split("_")
|
|
1261
|
-
|
|
1319
|
+
# free_referral_100pct_{plan_id}_monthly
|
|
1320
|
+
if len(parts) >= 4 and parts[3] in ("hobby", "pro", "max", "ultimate"):
|
|
1321
|
+
plan_id = parts[3]
|
|
1322
|
+
elif len(parts) >= 3 and parts[2] in ("hobby", "pro", "max", "ultimate"):
|
|
1262
1323
|
plan_id = parts[2]
|
|
1263
|
-
if len(parts) >=
|
|
1324
|
+
if len(parts) >= 5 and parts[4] in ("monthly", "yearly"):
|
|
1325
|
+
interval = parts[4]
|
|
1326
|
+
elif len(parts) >= 4 and parts[3] in ("monthly", "yearly"):
|
|
1264
1327
|
interval = parts[3]
|
|
1265
1328
|
activate_subscription(db, user_id=user.id, plan_id=plan_id, razorpay_sub_id=req.razorpay_subscription_id, interval=interval)
|
|
1329
|
+
# Award referral discount to referrer if applicable
|
|
1330
|
+
try:
|
|
1331
|
+
from .referral_routes import apply_referral_purchase
|
|
1332
|
+
apply_referral_purchase(db, referee_id=user.id, plan_id=plan_id)
|
|
1333
|
+
except Exception as _ref_err:
|
|
1334
|
+
logger.warning(f"referral_purchase_hook_failed: {_ref_err}")
|
|
1266
1335
|
return {"success": True}
|
|
1267
1336
|
|
|
1268
1337
|
import hmac
|
|
@@ -1304,7 +1373,14 @@ def api_subscription_verify(
|
|
|
1304
1373
|
razorpay_cust_id=f"cust_{user.id[:8]}",
|
|
1305
1374
|
interval=interval
|
|
1306
1375
|
)
|
|
1307
|
-
|
|
1376
|
+
|
|
1377
|
+
# Award referral discount to referrer if applicable
|
|
1378
|
+
try:
|
|
1379
|
+
from .referral_routes import apply_referral_purchase
|
|
1380
|
+
apply_referral_purchase(db, referee_id=user.id, plan_id=plan_id)
|
|
1381
|
+
except Exception as _ref_err:
|
|
1382
|
+
logger.warning(f"referral_purchase_hook_failed: {_ref_err}")
|
|
1383
|
+
|
|
1308
1384
|
return {"success": True}
|
|
1309
1385
|
|
|
1310
1386
|
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Referral System API routes.
|
|
3
|
+
|
|
4
|
+
Logic:
|
|
5
|
+
- Each user has a unique referral_code (8 hex chars stored on the User model).
|
|
6
|
+
- When a new user signs up with ?ref=<code>, the server stores the referrer's ID on their account.
|
|
7
|
+
- Every time a referred user's subscription payment is verified, the referrer earns a 2% discount
|
|
8
|
+
on that exact plan (e.g. if B buys hobby, referrer gets 2% off hobby).
|
|
9
|
+
- Discounts stack per-plan up to 100%. If the same referred user renews, the discount
|
|
10
|
+
increments again.
|
|
11
|
+
- The discount is stored in a separate ReferralDiscount table so it's queryable per plan.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import uuid
|
|
15
|
+
import datetime
|
|
16
|
+
import logging
|
|
17
|
+
|
|
18
|
+
from fastapi import APIRouter, Depends, HTTPException
|
|
19
|
+
from pydantic import BaseModel
|
|
20
|
+
from sqlalchemy import Column, String, Integer, Float, DateTime, ForeignKey, UniqueConstraint
|
|
21
|
+
from sqlalchemy.orm import Session, relationship
|
|
22
|
+
|
|
23
|
+
from ..db import Base, get_db, SessionLocal
|
|
24
|
+
from ..auth import get_current_firebase_user
|
|
25
|
+
from ..db import User
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger("utim.referrals")
|
|
28
|
+
|
|
29
|
+
router = APIRouter()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# ── ORM Model ─────────────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
class ReferralDiscount(Base):
|
|
35
|
+
"""Tracks per-plan discount percentage earned by a referrer."""
|
|
36
|
+
__tablename__ = "referral_discounts"
|
|
37
|
+
__table_args__ = (
|
|
38
|
+
UniqueConstraint("referrer_id", "plan_id", name="uq_referral_discount_referrer_plan"),
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
|
42
|
+
referrer_id = Column(String(36), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True)
|
|
43
|
+
plan_id = Column(String(50), nullable=False) # e.g. "hobby", "pro", "max"
|
|
44
|
+
discount_pct = Column(Float, default=0.0, nullable=False) # 0-100
|
|
45
|
+
referee_count = Column(Integer, default=0, nullable=False) # how many distinct referral purchases stacked this
|
|
46
|
+
updated_at = Column(DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow)
|
|
47
|
+
|
|
48
|
+
referrer = relationship("User", foreign_keys=[referrer_id], backref="referral_discounts")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# ── Pydantic Schemas ───────────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
class RegisterReferralRequest(BaseModel):
|
|
54
|
+
referral_code: str # code of the referrer
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# ── Helpers ────────────────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
MAX_DISCOUNT_PCT = 100.0
|
|
60
|
+
DISCOUNT_PER_REFERRAL = 2.0
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def apply_referral_purchase(db: Session, referee_id: str, plan_id: str) -> bool:
|
|
64
|
+
"""
|
|
65
|
+
Called after a successful payment by `referee_id`.
|
|
66
|
+
Finds their referrer and increments the plan-specific discount by 2%.
|
|
67
|
+
Returns True if a referrer was found and updated.
|
|
68
|
+
"""
|
|
69
|
+
referee = db.query(User).filter(User.id == referee_id).first()
|
|
70
|
+
if not referee or not referee.referrer_id:
|
|
71
|
+
return False
|
|
72
|
+
|
|
73
|
+
referrer_id = referee.referrer_id
|
|
74
|
+
|
|
75
|
+
# Normalise plan alias
|
|
76
|
+
normalised = _normalise_plan(plan_id)
|
|
77
|
+
|
|
78
|
+
discount_row = (
|
|
79
|
+
db.query(ReferralDiscount)
|
|
80
|
+
.filter(
|
|
81
|
+
ReferralDiscount.referrer_id == referrer_id,
|
|
82
|
+
ReferralDiscount.plan_id == normalised,
|
|
83
|
+
)
|
|
84
|
+
.first()
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
if not discount_row:
|
|
88
|
+
discount_row = ReferralDiscount(
|
|
89
|
+
referrer_id=referrer_id,
|
|
90
|
+
plan_id=normalised,
|
|
91
|
+
discount_pct=0.0,
|
|
92
|
+
referee_count=0,
|
|
93
|
+
)
|
|
94
|
+
db.add(discount_row)
|
|
95
|
+
|
|
96
|
+
new_pct = min(MAX_DISCOUNT_PCT, discount_row.discount_pct + DISCOUNT_PER_REFERRAL)
|
|
97
|
+
discount_row.discount_pct = new_pct
|
|
98
|
+
discount_row.referee_count += 1
|
|
99
|
+
discount_row.updated_at = datetime.datetime.utcnow()
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
db.commit()
|
|
103
|
+
except Exception as e:
|
|
104
|
+
db.rollback()
|
|
105
|
+
logger.error(f"referral_discount_commit_error: {e}")
|
|
106
|
+
return False
|
|
107
|
+
|
|
108
|
+
logger.info(
|
|
109
|
+
f"referral_discount_updated referrer={referrer_id} plan={normalised} "
|
|
110
|
+
f"new_pct={new_pct} referee_count={discount_row.referee_count}"
|
|
111
|
+
)
|
|
112
|
+
return True
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _normalise_plan(plan_id: str) -> str:
|
|
116
|
+
"""Map frontend plan aliases to canonical DB plan ids."""
|
|
117
|
+
mapping = {"starter": "pro", "professional": "max", "ultimate": "ultimate"}
|
|
118
|
+
pid = plan_id.lower().strip()
|
|
119
|
+
return mapping.get(pid, pid)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def get_referrer_discounts(db: Session, referrer_id: str) -> dict:
|
|
123
|
+
"""Return a dict of plan_id -> discount_pct for a given referrer."""
|
|
124
|
+
rows = db.query(ReferralDiscount).filter(ReferralDiscount.referrer_id == referrer_id).all()
|
|
125
|
+
return {r.plan_id: round(r.discount_pct, 2) for r in rows}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
# ── Routes ─────────────────────────────────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
@router.post("/api/referrals/register")
|
|
131
|
+
def register_referral(
|
|
132
|
+
req: RegisterReferralRequest,
|
|
133
|
+
db: Session = Depends(get_db),
|
|
134
|
+
user: User = Depends(get_current_firebase_user),
|
|
135
|
+
):
|
|
136
|
+
"""
|
|
137
|
+
Link the current user to a referrer by their referral_code.
|
|
138
|
+
Can only be called once per user (if they don't already have a referrer).
|
|
139
|
+
"""
|
|
140
|
+
if user.referrer_id:
|
|
141
|
+
raise HTTPException(status_code=400, detail="You have already been linked to a referrer.")
|
|
142
|
+
|
|
143
|
+
referrer = db.query(User).filter(User.referral_code == req.referral_code.strip()).first()
|
|
144
|
+
if not referrer:
|
|
145
|
+
raise HTTPException(status_code=404, detail="Referral code not found.")
|
|
146
|
+
|
|
147
|
+
if referrer.id == user.id:
|
|
148
|
+
raise HTTPException(status_code=400, detail="You cannot use your own referral code.")
|
|
149
|
+
|
|
150
|
+
user.referrer_id = referrer.id
|
|
151
|
+
try:
|
|
152
|
+
db.commit()
|
|
153
|
+
except Exception as e:
|
|
154
|
+
db.rollback()
|
|
155
|
+
raise HTTPException(status_code=500, detail=f"Failed to link referral: {e}")
|
|
156
|
+
|
|
157
|
+
logger.info(f"referral_linked referee={user.id} referrer={referrer.id}")
|
|
158
|
+
return {"success": True, "message": f"Successfully linked to referrer."}
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@router.get("/api/referrals/info")
|
|
162
|
+
def get_referral_info(
|
|
163
|
+
db: Session = Depends(get_db),
|
|
164
|
+
user: User = Depends(get_current_firebase_user),
|
|
165
|
+
):
|
|
166
|
+
"""
|
|
167
|
+
Returns the current user's referral code, stats, and any discounts they have earned
|
|
168
|
+
as a referrer.
|
|
169
|
+
"""
|
|
170
|
+
# Ensure user has a referral code (backfill for old accounts)
|
|
171
|
+
if not user.referral_code:
|
|
172
|
+
user.referral_code = uuid.uuid4().hex[:8]
|
|
173
|
+
try:
|
|
174
|
+
db.commit()
|
|
175
|
+
except Exception:
|
|
176
|
+
db.rollback()
|
|
177
|
+
|
|
178
|
+
# Count how many users they've referred
|
|
179
|
+
try:
|
|
180
|
+
referee_count = db.query(User).filter(User.referrer_id == user.id).count()
|
|
181
|
+
except Exception:
|
|
182
|
+
referee_count = 0
|
|
183
|
+
|
|
184
|
+
# Get their earned discounts
|
|
185
|
+
try:
|
|
186
|
+
discounts = get_referrer_discounts(db, user.id)
|
|
187
|
+
except Exception:
|
|
188
|
+
discounts = {}
|
|
189
|
+
|
|
190
|
+
# Did they sign up via a referral?
|
|
191
|
+
referred_by = None
|
|
192
|
+
if user.referrer_id:
|
|
193
|
+
try:
|
|
194
|
+
referrer = db.query(User).filter(User.id == user.referrer_id).first()
|
|
195
|
+
if referrer:
|
|
196
|
+
referred_by = {
|
|
197
|
+
"display_name": referrer.display_name or referrer.email.split("@")[0],
|
|
198
|
+
"email_hint": referrer.email[:3] + "***"
|
|
199
|
+
}
|
|
200
|
+
except Exception:
|
|
201
|
+
pass
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
"referral_code": user.referral_code,
|
|
205
|
+
"referral_url": f"https://utim.dev/auth?ref={user.referral_code}",
|
|
206
|
+
"referee_count": referee_count,
|
|
207
|
+
"discounts": discounts,
|
|
208
|
+
"referred_by": referred_by,
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
@router.get("/api/referrals/leaderboard")
|
|
213
|
+
def referral_leaderboard(
|
|
214
|
+
db: Session = Depends(get_db),
|
|
215
|
+
user: User = Depends(get_current_firebase_user),
|
|
216
|
+
):
|
|
217
|
+
"""
|
|
218
|
+
Returns the top referrers by total referee count (anonymized).
|
|
219
|
+
Also returns current user's rank.
|
|
220
|
+
"""
|
|
221
|
+
from sqlalchemy import text
|
|
222
|
+
try:
|
|
223
|
+
result = db.execute(
|
|
224
|
+
text("""
|
|
225
|
+
SELECT u.id, u.display_name, COUNT(r.id) as cnt
|
|
226
|
+
FROM users u
|
|
227
|
+
LEFT JOIN users r ON r.referrer_id = u.id
|
|
228
|
+
GROUP BY u.id, u.display_name
|
|
229
|
+
HAVING COUNT(r.id) > 0
|
|
230
|
+
ORDER BY cnt DESC
|
|
231
|
+
LIMIT 10
|
|
232
|
+
""")
|
|
233
|
+
).fetchall()
|
|
234
|
+
|
|
235
|
+
board = [
|
|
236
|
+
{
|
|
237
|
+
"rank": i + 1,
|
|
238
|
+
"name": (row[1] or "Anonymous")[:15],
|
|
239
|
+
"referrals": row[2],
|
|
240
|
+
"is_me": row[0] == user.id,
|
|
241
|
+
}
|
|
242
|
+
for i, row in enumerate(result)
|
|
243
|
+
]
|
|
244
|
+
except Exception as e:
|
|
245
|
+
logger.error(f"leaderboard_query_error: {e}")
|
|
246
|
+
board = []
|
|
247
|
+
|
|
248
|
+
return {"leaderboard": board}
|
|
@@ -102,11 +102,28 @@ def read_file(filepath: str, start_line: int = None, end_line: int = None) -> st
|
|
|
102
102
|
chunk = f.read(2048)
|
|
103
103
|
if b"\x00" in chunk:
|
|
104
104
|
return f"Error: '{filepath}' appears to be a binary file and cannot be read as text."
|
|
105
|
+
|
|
105
106
|
# Test decode
|
|
107
|
+
decode_success = False
|
|
106
108
|
try:
|
|
107
109
|
chunk.decode("utf-8")
|
|
110
|
+
decode_success = True
|
|
108
111
|
except UnicodeDecodeError:
|
|
109
|
-
|
|
112
|
+
# If it's a split multi-byte character at the end of the chunk, check smaller chunks
|
|
113
|
+
for i in range(1, min(5, len(chunk))):
|
|
114
|
+
try:
|
|
115
|
+
chunk[:-i].decode("utf-8")
|
|
116
|
+
decode_success = True
|
|
117
|
+
break
|
|
118
|
+
except UnicodeDecodeError:
|
|
119
|
+
continue
|
|
120
|
+
|
|
121
|
+
if not decode_success:
|
|
122
|
+
# Bypass check for known text extensions
|
|
123
|
+
ext = _os.path.splitext(filepath)[1].lower()
|
|
124
|
+
text_extensions = {'.txt', '.md', '.py', '.js', '.ts', '.jsx', '.tsx', '.json', '.yaml', '.yml', '.ini', '.cfg', '.conf', '.sh', '.bat', '.ps1', '.html', '.css', '.csv', '.xml', '.toml'}
|
|
125
|
+
if ext not in text_extensions:
|
|
126
|
+
return f"Error: '{filepath}' uses an unsupported encoding and cannot be read as text."
|
|
110
127
|
|
|
111
128
|
with open(filepath, "r", encoding="utf-8", errors="replace") as f:
|
|
112
129
|
all_lines = f.readlines()
|
|
@@ -214,6 +214,9 @@ def _dialog_model_main(orchestrator, target="main"):
|
|
|
214
214
|
"google/gemini-3.5-flash",
|
|
215
215
|
"qwen/qwen3.7-max",
|
|
216
216
|
"stepfun/step-3.7-flash",
|
|
217
|
+
"thinkingmachines/inkling",
|
|
218
|
+
"moonshotai/kimi-k3",
|
|
219
|
+
"meta/muse-spark-1.1",
|
|
217
220
|
|
|
218
221
|
"anthropic/claude-sonnet-4.5",
|
|
219
222
|
"anthropic/claude-sonnet-4.6",
|
|
@@ -284,11 +287,13 @@ def _dialog_model_main(orchestrator, target="main"):
|
|
|
284
287
|
"qwen/qwen3.7-max",
|
|
285
288
|
"qwen/qwen3.6-plus",
|
|
286
289
|
"openai/gpt-5.4-mini",
|
|
287
|
-
"minimax/minimax-m2.5",
|
|
288
290
|
"x-ai/grok-4.20",
|
|
289
291
|
"x-ai/grok-build-0.1",
|
|
290
292
|
"moonshotai/kimi-k2.5",
|
|
291
|
-
|
|
293
|
+
"thinkingmachines/inkling",
|
|
294
|
+
"moonshotai/kimi-k3",
|
|
295
|
+
"meta/muse-spark-1.1",
|
|
296
|
+
})
|
|
292
297
|
elif target == "subagent_image_gen":
|
|
293
298
|
# subagent_image_gen
|
|
294
299
|
approved_set = {
|
|
@@ -407,6 +412,9 @@ def _dialog_model_main(orchestrator, target="main"):
|
|
|
407
412
|
"google/gemini-3.5-flash": (3, "High"),
|
|
408
413
|
"qwen/qwen3.7-max": (3, "High"),
|
|
409
414
|
"stepfun/step-3.7-flash": (1, "Low"),
|
|
415
|
+
"thinkingmachines/inkling": (0, "Very low"),
|
|
416
|
+
"moonshotai/kimi-k3": (1, "Low"),
|
|
417
|
+
"meta/muse-spark-1.1": (0, "Very low"),
|
|
410
418
|
|
|
411
419
|
# Free NVIDIA models:
|
|
412
420
|
|
|
@@ -500,6 +508,9 @@ def _dialog_model_main(orchestrator, target="main"):
|
|
|
500
508
|
"google/gemini-3.5-flash": ("Fast, multimodal and agentic model by Google.", []),
|
|
501
509
|
"qwen/qwen3.7-max": ("Flagship agentic and reasoning model by Qwen.", []),
|
|
502
510
|
"stepfun/step-3.7-flash": ("Cost-effective multimodal assistant by StepFun.", []),
|
|
511
|
+
"thinkingmachines/inkling": ("Large-context, code-optimized reasoning model by Thinking Machines Lab.", ["premium"]),
|
|
512
|
+
"moonshotai/kimi-k3": ("Frontier-class reasoning MoE model by Moonshot AI.", ["premium", "reasoning"]),
|
|
513
|
+
"meta/muse-spark-1.1": ("Multimodal reasoning and agentic model by Meta Superintelligence Labs.", ["premium", "multimodal"]),
|
|
503
514
|
|
|
504
515
|
# Free NVIDIA models:
|
|
505
516
|
|
|
@@ -939,7 +939,7 @@ def _print_animated_banner(animated: bool = True):
|
|
|
939
939
|
else:
|
|
940
940
|
console.print(f"[bold {color}]{BANNER_BIG}[/bold {color}]")
|
|
941
941
|
|
|
942
|
-
console.print(f" [bold white]U Think I Make[/bold white] [dim]v1.47.
|
|
942
|
+
console.print(f" [bold white]U Think I Make[/bold white] [dim]v1.47.2[/dim]")
|
|
943
943
|
user_email = config.email
|
|
944
944
|
user_type = "UTIM Community"
|
|
945
945
|
api_key = config.get("api_key")
|
|
@@ -3521,7 +3521,7 @@ def check_and_update_background():
|
|
|
3521
3521
|
return
|
|
3522
3522
|
|
|
3523
3523
|
latest_ver = resp.json().get("version", "")
|
|
3524
|
-
current_ver = "1.47.
|
|
3524
|
+
current_ver = "1.47.2"
|
|
3525
3525
|
|
|
3526
3526
|
def parse_ver(v):
|
|
3527
3527
|
return [int(x) for x in v.split(".") if x.isdigit()]
|
|
@@ -4154,7 +4154,8 @@ def upgrade():
|
|
|
4154
4154
|
console.print(" [yellow]Upgrade cancelled.[/yellow]\n")
|
|
4155
4155
|
return
|
|
4156
4156
|
|
|
4157
|
-
|
|
4157
|
+
spinner_type = "line" if (_IS_LEGACY_WIN or getattr(sys.stdout, 'encoding', '').lower() not in ('utf-8', 'utf8', 'cp65001')) else "dots"
|
|
4158
|
+
with console.status("[bold green]Creating subscription link...", spinner=spinner_type):
|
|
4158
4159
|
try:
|
|
4159
4160
|
resp = requests.post(
|
|
4160
4161
|
f"{SERVER_URL}/subscribe",
|
|
@@ -60,6 +60,7 @@ utim_cli/server/routes/completion_routes.py
|
|
|
60
60
|
utim_cli/server/routes/credit_routes.py
|
|
61
61
|
utim_cli/server/routes/feedback_routes.py
|
|
62
62
|
utim_cli/server/routes/quota_routes.py
|
|
63
|
+
utim_cli/server/routes/referral_routes.py
|
|
63
64
|
utim_cli/server/routes/session_routes.py
|
|
64
65
|
utim_cli/server/routes/share_routes.py
|
|
65
66
|
utim_cli/tui/__init__.py
|
|
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
|