Skip to content

Commit ff4e0f9

Browse files
committed
feat: no login experience and prem tokens
1 parent 87452bb commit ff4e0f9

68 files changed

Lines changed: 5914 additions & 121 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ node_modules/
66
.venv
77
.pnpm-store
88
.DS_Store
9-
deepagents/
9+
deepagents/
10+
debug.log

docker/.env.example

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,11 @@ STRIPE_PAGE_BUYING_ENABLED=FALSE
158158
# STRIPE_RECONCILIATION_LOOKBACK_MINUTES=10
159159
# STRIPE_RECONCILIATION_BATCH_SIZE=100
160160

161+
# Premium token purchases ($1 per 1M tokens for premium-tier models)
162+
# STRIPE_TOKEN_BUYING_ENABLED=FALSE
163+
# STRIPE_PREMIUM_TOKEN_PRICE_ID=price_...
164+
# STRIPE_TOKENS_PER_UNIT=1000000
165+
161166
# ------------------------------------------------------------------------------
162167
# TTS & STT (Text-to-Speech / Speech-to-Text)
163168
# ------------------------------------------------------------------------------
@@ -309,6 +314,26 @@ STT_SERVICE=local/base
309314
# Pages limit per user for ETL (default: unlimited)
310315
# PAGES_LIMIT=500
311316

317+
# Premium token quota per registered user (default: 5M)
318+
# Only applies to models with billing_tier=premium in global_llm_config.yaml
319+
# PREMIUM_TOKEN_LIMIT=5000000
320+
321+
# No-login (anonymous) mode — public users can chat without an account
322+
# Set TRUE to enable /free pages and anonymous chat API
323+
NOLOGIN_MODE_ENABLED=FALSE
324+
# ANON_TOKEN_LIMIT=1000000
325+
# ANON_TOKEN_WARNING_THRESHOLD=800000
326+
# ANON_TOKEN_QUOTA_TTL_DAYS=30
327+
# ANON_MAX_UPLOAD_SIZE_MB=5
328+
# QUOTA_MAX_RESERVE_PER_CALL=8000
329+
# Abuse prevention: max concurrent anonymous streams per IP
330+
# ANON_MAX_CONCURRENT_STREAMS=2
331+
# Number of chat requests per IP before Turnstile CAPTCHA is required
332+
# ANON_CAPTCHA_REQUEST_THRESHOLD=5
333+
# Cloudflare Turnstile CAPTCHA (https://dash.cloudflare.com/ -> Turnstile)
334+
# TURNSTILE_ENABLED=FALSE
335+
# TURNSTILE_SECRET_KEY=
336+
312337
# Connector indexing lock TTL in seconds (default: 28800 = 8 hours)
313338
# CONNECTOR_INDEXING_LOCK_TTL_SECONDS=28800
314339

surfsense_backend/.env.example

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,13 @@ STRIPE_PRICE_ID=price_...
5353
STRIPE_PAGES_PER_UNIT=1000
5454
# Set FALSE to disable new checkout session creation temporarily
5555
STRIPE_PAGE_BUYING_ENABLED=TRUE
56+
57+
# Premium token purchases via Stripe (for premium-tier model usage)
58+
# Set TRUE to allow users to buy premium token packs ($1 per 1M tokens)
59+
STRIPE_TOKEN_BUYING_ENABLED=FALSE
60+
STRIPE_PREMIUM_TOKEN_PRICE_ID=price_...
61+
STRIPE_TOKENS_PER_UNIT=1000000
62+
5663
# Periodic Stripe safety net for purchases left in PENDING (minutes old)
5764
STRIPE_RECONCILIATION_LOOKBACK_MINUTES=10
5865
# Max pending purchases to check per reconciliation run
@@ -177,6 +184,34 @@ VIDEO_PRESENTATION_DEFAULT_DURATION_IN_FRAMES=300
177184
# (Optional) Maximum pages limit per user for ETL services (default: `999999999` for unlimited in OSS version)
178185
PAGES_LIMIT=500
179186

187+
# Premium token quota per registered user (default: 5,000,000)
188+
# Applies only to models with billing_tier=premium in global_llm_config.yaml
189+
PREMIUM_TOKEN_LIMIT=5000000
190+
191+
# No-login (anonymous) mode — allows public users to chat without an account
192+
# Set TRUE to enable /free pages and anonymous chat API
193+
NOLOGIN_MODE_ENABLED=FALSE
194+
# Total tokens allowed per anonymous session before requiring account creation
195+
ANON_TOKEN_LIMIT=1000000
196+
# Token count at which the UI shows a soft warning
197+
ANON_TOKEN_WARNING_THRESHOLD=800000
198+
# Days before anonymous quota tracking expires in Redis
199+
ANON_TOKEN_QUOTA_TTL_DAYS=30
200+
# Max document upload size for anonymous users (MB)
201+
ANON_MAX_UPLOAD_SIZE_MB=5
202+
# Maximum tokens to reserve per LLM call for quota enforcement (safety cap)
203+
QUOTA_MAX_RESERVE_PER_CALL=8000
204+
205+
# Abuse prevention: max concurrent anonymous streams per IP (default: 2)
206+
ANON_MAX_CONCURRENT_STREAMS=2
207+
# Number of chat requests per IP before Turnstile CAPTCHA is required (default: 5)
208+
ANON_CAPTCHA_REQUEST_THRESHOLD=5
209+
210+
# Cloudflare Turnstile CAPTCHA (https://dash.cloudflare.com/ -> Turnstile)
211+
# Set TURNSTILE_ENABLED=TRUE and provide keys to activate CAPTCHA for anonymous chat
212+
TURNSTILE_ENABLED=FALSE
213+
TURNSTILE_SECRET_KEY=
214+
180215

181216
# Residential Proxy Configuration (anonymous-proxies.net)
182217
# Used for web crawling, link previews, and YouTube transcript fetching to avoid IP bans.
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
"""add premium token quota columns and purchase table
2+
3+
Revision ID: 126
4+
Revises: 125
5+
Create Date: 2026-04-15
6+
7+
Adds premium_tokens_limit, premium_tokens_used, premium_tokens_reserved
8+
to the user table and creates the premium_token_purchases table.
9+
"""
10+
11+
from __future__ import annotations
12+
13+
import os
14+
from collections.abc import Sequence
15+
16+
import sqlalchemy as sa
17+
from sqlalchemy.dialects import postgresql
18+
19+
from alembic import op
20+
21+
revision: str = "126"
22+
down_revision: str | None = "125"
23+
branch_labels: str | Sequence[str] | None = None
24+
depends_on: str | Sequence[str] | None = None
25+
26+
PREMIUM_TOKEN_LIMIT_DEFAULT = os.getenv("PREMIUM_TOKEN_LIMIT", "5000000")
27+
28+
29+
def upgrade() -> None:
30+
conn = op.get_bind()
31+
32+
# --- User table: add premium token columns if missing ---
33+
inspector = sa.inspect(conn)
34+
user_columns = {c["name"] for c in inspector.get_columns("user")}
35+
36+
if "premium_tokens_limit" not in user_columns:
37+
op.add_column(
38+
"user",
39+
sa.Column(
40+
"premium_tokens_limit",
41+
sa.BigInteger(),
42+
nullable=False,
43+
server_default=PREMIUM_TOKEN_LIMIT_DEFAULT,
44+
),
45+
)
46+
if "premium_tokens_used" not in user_columns:
47+
op.add_column(
48+
"user",
49+
sa.Column(
50+
"premium_tokens_used",
51+
sa.BigInteger(),
52+
nullable=False,
53+
server_default="0",
54+
),
55+
)
56+
if "premium_tokens_reserved" not in user_columns:
57+
op.add_column(
58+
"user",
59+
sa.Column(
60+
"premium_tokens_reserved",
61+
sa.BigInteger(),
62+
nullable=False,
63+
server_default="0",
64+
),
65+
)
66+
67+
# --- PremiumTokenPurchase enum + table ---
68+
enum_exists = conn.execute(
69+
sa.text("SELECT 1 FROM pg_type WHERE typname = 'premiumtokenpurchasestatus'")
70+
).fetchone()
71+
if not enum_exists:
72+
purchase_status_enum = postgresql.ENUM(
73+
"PENDING",
74+
"COMPLETED",
75+
"FAILED",
76+
name="premiumtokenpurchasestatus",
77+
create_type=False,
78+
)
79+
purchase_status_enum.create(conn, checkfirst=True)
80+
81+
if not inspector.has_table("premium_token_purchases"):
82+
op.create_table(
83+
"premium_token_purchases",
84+
sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False),
85+
sa.Column("user_id", postgresql.UUID(as_uuid=True), nullable=False),
86+
sa.Column(
87+
"stripe_checkout_session_id",
88+
sa.String(length=255),
89+
nullable=False,
90+
),
91+
sa.Column(
92+
"stripe_payment_intent_id",
93+
sa.String(length=255),
94+
nullable=True,
95+
),
96+
sa.Column("quantity", sa.Integer(), nullable=False),
97+
sa.Column("tokens_granted", sa.BigInteger(), nullable=False),
98+
sa.Column("amount_total", sa.Integer(), nullable=True),
99+
sa.Column("currency", sa.String(length=10), nullable=True),
100+
sa.Column(
101+
"status",
102+
postgresql.ENUM(
103+
"PENDING",
104+
"COMPLETED",
105+
"FAILED",
106+
name="premiumtokenpurchasestatus",
107+
create_type=False,
108+
),
109+
nullable=False,
110+
server_default=sa.text("'PENDING'::premiumtokenpurchasestatus"),
111+
),
112+
sa.Column("completed_at", sa.TIMESTAMP(timezone=True), nullable=True),
113+
sa.Column(
114+
"created_at",
115+
sa.TIMESTAMP(timezone=True),
116+
server_default=sa.text("now()"),
117+
nullable=False,
118+
),
119+
sa.Column(
120+
"updated_at",
121+
sa.TIMESTAMP(timezone=True),
122+
server_default=sa.text("now()"),
123+
nullable=False,
124+
),
125+
sa.ForeignKeyConstraint(
126+
["user_id"],
127+
["user.id"],
128+
ondelete="CASCADE",
129+
),
130+
sa.PrimaryKeyConstraint("id"),
131+
sa.UniqueConstraint(
132+
"stripe_checkout_session_id",
133+
name="uq_premium_token_purchases_stripe_checkout_session_id",
134+
),
135+
)
136+
137+
op.execute(
138+
"CREATE INDEX IF NOT EXISTS ix_premium_token_purchases_user_id "
139+
"ON premium_token_purchases (user_id)"
140+
)
141+
op.execute(
142+
"CREATE UNIQUE INDEX IF NOT EXISTS ix_premium_token_purchases_stripe_session "
143+
"ON premium_token_purchases (stripe_checkout_session_id)"
144+
)
145+
op.execute(
146+
"CREATE INDEX IF NOT EXISTS ix_premium_token_purchases_payment_intent "
147+
"ON premium_token_purchases (stripe_payment_intent_id)"
148+
)
149+
op.execute(
150+
"CREATE INDEX IF NOT EXISTS ix_premium_token_purchases_status "
151+
"ON premium_token_purchases (status)"
152+
)
153+
154+
155+
def downgrade() -> None:
156+
op.execute("DROP INDEX IF EXISTS ix_premium_token_purchases_status")
157+
op.execute("DROP INDEX IF EXISTS ix_premium_token_purchases_payment_intent")
158+
op.execute("DROP INDEX IF EXISTS ix_premium_token_purchases_stripe_session")
159+
op.execute("DROP INDEX IF EXISTS ix_premium_token_purchases_user_id")
160+
op.execute("DROP TABLE IF EXISTS premium_token_purchases")
161+
postgresql.ENUM(name="premiumtokenpurchasestatus").drop(
162+
op.get_bind(), checkfirst=True
163+
)
164+
op.drop_column("user", "premium_tokens_reserved")
165+
op.drop_column("user", "premium_tokens_used")
166+
op.drop_column("user", "premium_tokens_limit")

surfsense_backend/app/agents/new_chat/chat_deepagent.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ async def create_surfsense_deep_agent(
161161
firecrawl_api_key: str | None = None,
162162
thread_visibility: ChatVisibility | None = None,
163163
mentioned_document_ids: list[int] | None = None,
164+
anon_session_id: str | None = None,
164165
):
165166
"""
166167
Create a SurfSense deep agent with configurable tools and prompts.
@@ -463,6 +464,7 @@ async def create_surfsense_deep_agent(
463464
available_connectors=available_connectors,
464465
available_document_types=available_document_types,
465466
mentioned_document_ids=mentioned_document_ids,
467+
anon_session_id=anon_session_id,
466468
),
467469
SurfSenseFilesystemMiddleware(
468470
search_space_id=search_space_id,

surfsense_backend/app/agents/new_chat/llm_config.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,12 @@ class AgentConfig:
109109
# Auto mode flag
110110
is_auto_mode: bool = False
111111

112+
# Token quota and policy
113+
billing_tier: str = "free"
114+
is_premium: bool = False
115+
anonymous_enabled: bool = False
116+
quota_reserve_tokens: int | None = None
117+
112118
@classmethod
113119
def from_auto_mode(cls) -> "AgentConfig":
114120
"""
@@ -130,6 +136,10 @@ def from_auto_mode(cls) -> "AgentConfig":
130136
config_id=AUTO_MODE_ID,
131137
config_name="Auto (Fastest)",
132138
is_auto_mode=True,
139+
billing_tier="free",
140+
is_premium=False,
141+
anonymous_enabled=False,
142+
quota_reserve_tokens=None,
133143
)
134144

135145
@classmethod
@@ -158,6 +168,10 @@ def from_new_llm_config(cls, config) -> "AgentConfig":
158168
config_id=config.id,
159169
config_name=config.name,
160170
is_auto_mode=False,
171+
billing_tier="free",
172+
is_premium=False,
173+
anonymous_enabled=False,
174+
quota_reserve_tokens=None,
161175
)
162176

163177
@classmethod
@@ -195,6 +209,10 @@ def from_yaml_config(cls, yaml_config: dict) -> "AgentConfig":
195209
config_id=yaml_config.get("id"),
196210
config_name=yaml_config.get("name"),
197211
is_auto_mode=False,
212+
billing_tier=yaml_config.get("billing_tier", "free"),
213+
is_premium=yaml_config.get("billing_tier", "free") == "premium",
214+
anonymous_enabled=yaml_config.get("anonymous_enabled", False),
215+
quota_reserve_tokens=yaml_config.get("quota_reserve_tokens"),
198216
)
199217

200218

0 commit comments

Comments
 (0)