[eric] free-trial: hand the wheel back when a real model is connected mid-trial (key or 9router sub)

This commit is contained in:
ciregenz
2026-06-10 16:32:25 -07:00
parent d242114643
commit a126c42a07
3 changed files with 43 additions and 4 deletions
+1 -1
View File
@@ -1602,7 +1602,7 @@ class AgentManager:
"ANTHROPIC_BASE_URL": proxy_url,
# Pin subagent ids; CLI default 'claude-haiku-4-5-20251001'
# gets rejected by Pro's surface as "No credentials for provider: anthropic".
# (Free-trial down-routes all of these to Haiku server-side.)
# (Free-trial clamps to its allowed Claude set + weights credits server-side.)
"CLAUDE_CODE_SUBAGENT_MODEL": "claude-sonnet-4-6",
"ANTHROPIC_SMALL_FAST_MODEL": "claude-haiku-4-5-20251001",
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "claude-haiku-4-5-20251001",
+17
View File
@@ -143,6 +143,23 @@ async def update_settings(body: AppSettings):
for k in SERVER_OWNED_FIELDS:
setattr(body, k, getattr(old, k, None))
# If the user connects their own model while the free trial is armed, hand
# the wheel back to their provider. Without this, connection_mode (server-
# owned, so the loop above just restored it to "free-trial") would keep them
# pinned to the forced Haiku lane even though they pasted a real key.
if getattr(old, "connection_mode", "own_key") == "free-trial":
from backend.apps.subscription.free_trial import _has_own_model
if _has_own_model(body):
body.connection_mode = "own_key"
body.free_trial_token = None
body.free_trial_remaining = None
try:
import asyncio as _aio
from backend.apps.nine_router import sync_pro_routing as _spr
_aio.create_task(_spr(body)) # drop the now-stale free-trial 9router node
except Exception:
pass
secret_keys = {"anthropic_api_key", "openai_api_key", "google_api_key", "openrouter_api_key",
"claude_subscription_token", "openai_subscription_token", "gemini_subscription_token",
"openswarm_bearer_token", "free_trial_token", "installation_id"}
+25 -3
View File
@@ -74,7 +74,7 @@ def _fingerprint(settings_obj) -> str | None:
def _has_own_model(s) -> bool:
"""True if the user already has any real model path; never shadow it."""
"""True if the user already has any real model path in settings; never shadow it."""
if any(getattr(s, k, None) for k in (
"anthropic_api_key", "openai_api_key", "google_api_key", "openrouter_api_key",
"claude_subscription_token", "openai_subscription_token", "gemini_subscription_token",
@@ -90,6 +90,23 @@ def _has_own_model(s) -> bool:
return False
async def _has_connected_subscription() -> bool:
"""True if 9Router holds a live Claude/ChatGPT/Gemini subscription. Those
connections live in 9Router, not settings, so the sync check above misses
them; this catches a sub connected while the trial was armed."""
try:
from backend.apps.nine_router import is_running as _9r_running, get_providers as _9r_providers
if not _9r_running():
return False
conns = await _9r_providers()
return any(
c.get("isActive") and c.get("provider") in ("claude", "codex", "gemini-cli")
for c in conns
)
except Exception:
return False
def _proxy_base(settings_obj) -> str:
return (getattr(settings_obj, "openswarm_proxy_url", None) or OPENSWARM_DEFAULT_PROXY_URL).rstrip("/")
@@ -117,9 +134,14 @@ async def arm_free_trial(settings_obj) -> dict:
free-trial mode. Guarded: never arms over a real key/subscription."""
if not _enabled():
return {"armed": False, "reason": "disabled"}
if getattr(settings_obj, "connection_mode", "own_key") not in ("own_key", "free-trial"):
mode = getattr(settings_obj, "connection_mode", "own_key")
if mode not in ("own_key", "free-trial"):
return {"armed": False, "reason": "other_mode"}
if _has_own_model(settings_obj):
if _has_own_model(settings_obj) or await _has_connected_subscription():
# A real model exists now (key, custom provider, or a 9Router sub). If we
# were on the free lane, hand the wheel back instead of re-arming.
if mode == "free-trial":
await clear_free_trial(settings_obj)
return {"armed": False, "reason": "has_model"}
fp = _fingerprint(settings_obj)