dotandru commited on
Commit
137ff4e
·
1 Parent(s): aa0d35c

feat: token pipeline and device fingerprinting

Browse files
Files changed (3) hide show
  1. cost_tracker.py +12 -1
  2. main.py +10 -2
  3. quota_system_v2.py +52 -8
cost_tracker.py CHANGED
@@ -52,8 +52,13 @@ def get_log_file_path():
52
  LOG_FILE = get_log_file_path()
53
  LOG_DIR = os.path.dirname(LOG_FILE)
54
 
 
 
 
 
 
55
  def log_api_usage(usage_metadata, source="unknown"):
56
- """Log API usage to proper location"""
57
  if not usage_metadata:
58
  return
59
 
@@ -73,6 +78,12 @@ def log_api_usage(usage_metadata, source="unknown"):
73
  with open(LOG_FILE, "a", encoding="utf-8") as f:
74
  f.write(json.dumps(entry) + "\n")
75
 
 
 
 
 
 
 
76
  # Also print to stdout for immediate visibility in console logs
77
  print(f"💰 [USAGE] {source}: In={entry['input_tokens']}, Out={entry['output_tokens']}")
78
 
 
52
  LOG_FILE = get_log_file_path()
53
  LOG_DIR = os.path.dirname(LOG_FILE)
54
 
55
+ import contextvars
56
+
57
+ # Stores the total tokens used in the current solve request
58
+ current_request_tokens = contextvars.ContextVar('current_request_tokens', default=0)
59
+
60
  def log_api_usage(usage_metadata, source="unknown"):
61
+ """Log API usage to proper location and add to current ContextVar"""
62
  if not usage_metadata:
63
  return
64
 
 
78
  with open(LOG_FILE, "a", encoding="utf-8") as f:
79
  f.write(json.dumps(entry) + "\n")
80
 
81
+ # Update the ContextVar for the current request
82
+ try:
83
+ current_request_tokens.set(current_request_tokens.get() + entry["total_tokens"])
84
+ except Exception:
85
+ pass
86
+
87
  # Also print to stdout for immediate visibility in console logs
88
  print(f"💰 [USAGE] {source}: In={entry['input_tokens']}, Out={entry['output_tokens']}")
89
 
main.py CHANGED
@@ -246,9 +246,10 @@ async def solve_stream_v2(
246
  uid = decoded_token.get('uid')
247
  final_student_name = student_name or user or "תלמיד"
248
  print(f"🚀 🟢 [V2] Received request from UID: {uid} ({final_student_name}). Grade: {grade}")
 
249
 
250
  # V2 Quota Check (Firestore)
251
- is_allowed, msg, current_usage, limit = quota_manager_v2.check_limit(uid)
252
  if not is_allowed:
253
  response_content = build_standard_response(
254
  final_answer=f"הגעת למכסה היומית ({limit} שאלות)",
@@ -289,6 +290,8 @@ async def solve_stream_v2(
289
  print("🚀 [TRACE-V2] Initiating streaming orchestrator.solve_problem with multiple images...")
290
 
291
  async def event_generator():
 
 
292
  try:
293
  # orchestrator handles the rest using student_name for pedagogical stuff, but quota is already handled.
294
  async for event in orchestrator.solve_problem(
@@ -313,6 +316,11 @@ async def solve_stream_v2(
313
  "event": "error",
314
  "data": json.dumps({"error": str(e)})
315
  }
 
 
 
 
 
316
 
317
  return EventSourceResponse(event_generator())
318
 
@@ -380,7 +388,7 @@ async def upgrade_success(req: UpgradeRequest):
380
  user_ref.set({
381
  "tier": "parent_premium",
382
  "parent_email": req.parent_email,
383
- "quota_limit": 999
384
  }, merge=True)
385
 
386
  logger.info(f"🎉 UPGRADED USER {req.uid} to parent_premium")
 
246
  uid = decoded_token.get('uid')
247
  final_student_name = student_name or user or "תלמיד"
248
  print(f"🚀 🟢 [V2] Received request from UID: {uid} ({final_student_name}). Grade: {grade}")
249
+ device_id = request.headers.get('Device-ID')
250
 
251
  # V2 Quota Check (Firestore)
252
+ is_allowed, msg, current_usage, limit = quota_manager_v2.check_limit(uid, device_id=device_id)
253
  if not is_allowed:
254
  response_content = build_standard_response(
255
  final_answer=f"הגעת למכסה היומית ({limit} שאלות)",
 
290
  print("🚀 [TRACE-V2] Initiating streaming orchestrator.solve_problem with multiple images...")
291
 
292
  async def event_generator():
293
+ import cost_tracker
294
+ cost_tracker.current_request_tokens.set(0)
295
  try:
296
  # orchestrator handles the rest using student_name for pedagogical stuff, but quota is already handled.
297
  async for event in orchestrator.solve_problem(
 
316
  "event": "error",
317
  "data": json.dumps({"error": str(e)})
318
  }
319
+ finally:
320
+ total_tokens = cost_tracker.current_request_tokens.get()
321
+ if total_tokens > 0:
322
+ quota_manager_v2.increment_usage(uid, increment_questions=0, tokens_used=total_tokens)
323
+ print(f"🪙 [V2-QUOTA] Deducted {total_tokens} tokens for UID: {uid}")
324
 
325
  return EventSourceResponse(event_generator())
326
 
 
388
  user_ref.set({
389
  "tier": "parent_premium",
390
  "parent_email": req.parent_email,
391
+ "monthly_token_budget": 2800000
392
  }, merge=True)
393
 
394
  logger.info(f"🎉 UPGRADED USER {req.uid} to parent_premium")
quota_system_v2.py CHANGED
@@ -22,8 +22,11 @@ class QuotaManagerV2:
22
 
23
  def get_today_key(self):
24
  return datetime.date.today().isoformat()
 
 
 
25
 
26
- def check_limit(self, uid: str):
27
  """
28
  Returns:
29
  (allowed: bool, message: str, current_usage: int, limit: int)
@@ -54,16 +57,35 @@ class QuotaManagerV2:
54
  if data.get("role") == "admin" or data.get("status") == "admin":
55
  return True, "Unlimited (Admin)", 0, -1
56
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  limit = data.get("quota_limit", self.default_limit)
 
58
 
59
  # Special limits
60
  if limit < 0:
61
  return True, "Unlimited", 0, -1
62
 
63
  today = self.get_today_key()
 
 
64
  last_usage_date = data.get("last_usage_date", "")
 
65
 
66
- # If it's a new day, usage is effectively 0
67
  if last_usage_date != today:
68
  current_count = 0
69
  else:
@@ -71,6 +93,16 @@ class QuotaManagerV2:
71
 
72
  if current_count >= limit:
73
  return False, f"Daily limit reached ({limit})", current_count, limit
 
 
 
 
 
 
 
 
 
 
74
 
75
  return True, "Allowed", current_count, limit
76
 
@@ -79,7 +111,7 @@ class QuotaManagerV2:
79
  # Fail closed or open? Let's fail open temporarily so we don't block users if DB hiccups
80
  return True, "Error - Fallback Allow", 0, self.default_limit
81
 
82
- def increment_usage(self, uid: str):
83
  if not uid:
84
  return
85
 
@@ -88,6 +120,7 @@ class QuotaManagerV2:
88
  return
89
 
90
  today = self.get_today_key()
 
91
  doc_ref = db.collection(self.collection_name).document(uid)
92
 
93
  try:
@@ -95,32 +128,43 @@ class QuotaManagerV2:
95
  def update_in_transaction(transaction, doc_ref):
96
  snapshot = doc_ref.get(transaction=transaction)
97
  if not snapshot.exists:
98
- # Create document if it doesn't exist (though migration should handle this)
99
  transaction.set(doc_ref, {
100
  "quota_limit": self.default_limit,
101
- "used_today": 1,
 
102
  "last_usage_date": today,
 
103
  "last_seen": firestore.SERVER_TIMESTAMP
104
  })
105
  return
106
 
107
  data = snapshot.to_dict()
108
  last_date = data.get("last_usage_date", "")
 
109
 
 
110
  if last_date != today:
111
- new_usage = 1
 
 
 
 
 
 
112
  else:
113
- new_usage = data.get("used_today", 0) + 1
114
 
115
  transaction.update(doc_ref, {
116
  "used_today": new_usage,
 
117
  "last_usage_date": today,
 
118
  "last_seen": firestore.SERVER_TIMESTAMP
119
  })
120
 
121
  transaction = db.transaction()
122
  update_in_transaction(transaction, doc_ref)
123
- logger.info(f"📊 [QUOTA_V2] Incremented usage for {uid}. Date: {today}")
124
 
125
  except Exception as e:
126
  logger.error(f"❌ [QUOTA_V2] Failed to increment usage for {uid}: {e}")
 
22
 
23
  def get_today_key(self):
24
  return datetime.date.today().isoformat()
25
+
26
+ def get_this_month_key(self):
27
+ return datetime.date.today().strftime("%Y-%m")
28
 
29
+ def check_limit(self, uid: str, device_id: str = None):
30
  """
31
  Returns:
32
  (allowed: bool, message: str, current_usage: int, limit: int)
 
57
  if data.get("role") == "admin" or data.get("status") == "admin":
58
  return True, "Unlimited (Admin)", 0, -1
59
 
60
+ # --- Device ID Enforcement ---
61
+ if device_id:
62
+ tier = data.get("tier", "student_basic")
63
+ allowed_devices = data.get("allowed_devices", [])
64
+
65
+ max_devices = 2 if tier == "parent_premium" else 1
66
+
67
+ if device_id not in allowed_devices:
68
+ if len(allowed_devices) >= max_devices:
69
+ return False, f"Device Limit Exceeded ({max_devices})", 0, 0
70
+ else:
71
+ # Add device atomically
72
+ doc_ref.update({"allowed_devices": firestore.ArrayUnion([device_id])})
73
+ # -----------------------------
74
+
75
  limit = data.get("quota_limit", self.default_limit)
76
+ monthly_token_budget = data.get("monthly_token_budget")
77
 
78
  # Special limits
79
  if limit < 0:
80
  return True, "Unlimited", 0, -1
81
 
82
  today = self.get_today_key()
83
+ this_month = self.get_this_month_key()
84
+
85
  last_usage_date = data.get("last_usage_date", "")
86
+ last_usage_month = data.get("last_usage_month", "")
87
 
88
+ # Daily Question Quota
89
  if last_usage_date != today:
90
  current_count = 0
91
  else:
 
93
 
94
  if current_count >= limit:
95
  return False, f"Daily limit reached ({limit})", current_count, limit
96
+
97
+ # Monthly Token Quota (if defined)
98
+ if monthly_token_budget is not None:
99
+ if last_usage_month != this_month:
100
+ used_tokens = 0
101
+ else:
102
+ used_tokens = data.get("used_tokens_this_month", 0)
103
+
104
+ if used_tokens >= monthly_token_budget:
105
+ return False, f"Monthly token budget reached", current_count, limit
106
 
107
  return True, "Allowed", current_count, limit
108
 
 
111
  # Fail closed or open? Let's fail open temporarily so we don't block users if DB hiccups
112
  return True, "Error - Fallback Allow", 0, self.default_limit
113
 
114
+ def increment_usage(self, uid: str, increment_questions: int = 1, tokens_used: int = 0):
115
  if not uid:
116
  return
117
 
 
120
  return
121
 
122
  today = self.get_today_key()
123
+ this_month = self.get_this_month_key()
124
  doc_ref = db.collection(self.collection_name).document(uid)
125
 
126
  try:
 
128
  def update_in_transaction(transaction, doc_ref):
129
  snapshot = doc_ref.get(transaction=transaction)
130
  if not snapshot.exists:
 
131
  transaction.set(doc_ref, {
132
  "quota_limit": self.default_limit,
133
+ "used_today": increment_questions,
134
+ "used_tokens_this_month": tokens_used,
135
  "last_usage_date": today,
136
+ "last_usage_month": this_month,
137
  "last_seen": firestore.SERVER_TIMESTAMP
138
  })
139
  return
140
 
141
  data = snapshot.to_dict()
142
  last_date = data.get("last_usage_date", "")
143
+ last_month = data.get("last_usage_month", "")
144
 
145
+ # Daily logic
146
  if last_date != today:
147
+ new_usage = increment_questions
148
+ else:
149
+ new_usage = data.get("used_today", 0) + increment_questions
150
+
151
+ # Monthly Token logic
152
+ if last_month != this_month:
153
+ new_tokens = tokens_used
154
  else:
155
+ new_tokens = data.get("used_tokens_this_month", 0) + tokens_used
156
 
157
  transaction.update(doc_ref, {
158
  "used_today": new_usage,
159
+ "used_tokens_this_month": new_tokens,
160
  "last_usage_date": today,
161
+ "last_usage_month": this_month,
162
  "last_seen": firestore.SERVER_TIMESTAMP
163
  })
164
 
165
  transaction = db.transaction()
166
  update_in_transaction(transaction, doc_ref)
167
+ logger.info(f"📊 [QUOTA_V2] Incremented usage for {uid}. Date: {today}, Tokens: {tokens_used}")
168
 
169
  except Exception as e:
170
  logger.error(f"❌ [QUOTA_V2] Failed to increment usage for {uid}: {e}")