import os import psycopg2 import subprocess from datetime import datetime, timezone # ๐Ÿ”ฑ Imperial Gemini API 2026 Quota Limits (Gemini 2.5 / 3.1 Pro/Flash-Lite / etc.) # 0 means Unlimited (๋ฌด์ œํ•œ) MODEL_LIMITS = { "gemini-3.1-flash-lite": {"rpd": 150000, "category": "ํ…์ŠคํŠธ ์ถœ๋ ฅ ๋ชจ๋ธ", "display_name": "Gemini 3.1 Flash-Lite"}, "gemini-3.1-pro": {"rpd": 250, "category": "ํ…์ŠคํŠธ ์ถœ๋ ฅ ๋ชจ๋ธ", "display_name": "Gemini 3.1 Pro"}, "gemini-3.0-flash": {"rpd": 10000, "category": "ํ…์ŠคํŠธ ์ถœ๋ ฅ ๋ชจ๋ธ", "display_name": "Gemini 3.0 Flash"}, "gemini-2.0-flash": {"rpd": 0, "category": "ํ…์ŠคํŠธ ์ถœ๋ ฅ ๋ชจ๋ธ", "display_name": "Gemini 2.0 Flash"}, "gemini-2.0-flash-lite": {"rpd": 0, "category": "ํ…์ŠคํŠธ ์ถœ๋ ฅ ๋ชจ๋ธ", "display_name": "Gemini 2.0 Flash Lite"}, "gemini-3.1-flash-tts-preview": {"rpd": 100, "category": "๋ฉ€ํ‹ฐ๋ชจ๋‹ฌ ์ƒ์„ฑ ๋ชจ๋ธ", "display_name": "Gemini 3.1 Flash TTS"}, "gemini-3-flash-live": {"rpd": 0, "category": "Live API", "display_name": "Gemini 3 Flash Live"}, "imagen-4.0-generate-001": {"rpd": 70, "category": "๋ฉ€ํ‹ฐ๋ชจ๋‹ฌ ์ƒ์„ฑ ๋ชจ๋ธ", "display_name": "Imagen 4.0 Pro"}, "imagen-4.0-fast-generate-001": {"rpd": 70, "category": "๋ฉ€ํ‹ฐ๋ชจ๋‹ฌ ์ƒ์„ฑ ๋ชจ๋ธ", "display_name": "Imagen 4.0 Fast"}, "gemini-3.1-flash-image-preview": {"rpd": 1000, "category": "๋ฉ€ํ‹ฐ๋ชจ๋‹ฌ ์ƒ์„ฑ ๋ชจ๋ธ", "display_name": "๋‚˜๋…ธ๋ฐ”๋‚˜๋‚˜ 2"}, "gemini-3-pro-image-preview": {"rpd": 250, "category": "๋ฉ€ํ‹ฐ๋ชจ๋‹ฌ ์ƒ์„ฑ ๋ชจ๋ธ", "display_name": "๋‚˜๋…ธ๋ฐ”๋‚˜๋‚˜ Pro"}, "gemini-embedding-2-preview": {"rpd": 0, "category": "๊ธฐํƒ€ ๋ชจ๋ธ", "display_name": "Gemini Embedding 2"}, "gemma-4-26b-it": {"rpd": 14400, "category": "๊ธฐํƒ€ ๋ชจ๋ธ", "display_name": "Gemma 4 26B"}, "gemma-4-31b-it": {"rpd": 14400, "category": "๊ธฐํƒ€ ๋ชจ๋ธ", "display_name": "Gemma 4 31B"}, "google-search-grounding-default": {"rpd": 5000, "category": "๊ฒ€์ƒ‰ ๊ทธ๋ผ์šฐ๋”ฉ", "display_name": "Default"}, "google-search-grounding-gemini2": {"rpd": 5000, "category": "๊ฒ€์ƒ‰ ๊ทธ๋ผ์šฐ๋”ฉ", "display_name": "Gemini 2"}, "google-search-grounding-gemini3": {"rpd": 1500, "category": "๊ฒ€์ƒ‰ ๊ทธ๋ผ์šฐ๋”ฉ", "display_name": "Gemini 3"}, } # ๐Ÿ”ฑ GitHub Copilot Model List with Premium Multipliers # multiplier=0 means FREE (no premium requests consumed) COPILOT_MONTHLY_BUDGET = 300 # GitHub Copilot Pro plan default COPILOT_MODEL_INFO = { "claude-sonnet-4.6": {"display_name": "Claude Sonnet 4.6", "multiplier": 1.0, "category": "Anthropic"}, "claude-sonnet-4.5": {"display_name": "Claude Sonnet 4.5", "multiplier": 1.0, "category": "Anthropic"}, "claude-opus-4.6": {"display_name": "Claude Opus 4.6", "multiplier": 3.0, "category": "Anthropic"}, "claude-opus-4.6-fast": {"display_name": "Claude Opus 4.6 (fast mode)", "multiplier": 30.0, "category": "Anthropic"}, "claude-haiku-4.5": {"display_name": "Claude Haiku 4.5", "multiplier": 0.33, "category": "Anthropic"}, "gpt-4.1": {"display_name": "GPT-4.1", "multiplier": 0.0, "category": "OpenAI"}, "gpt-4o": {"display_name": "GPT-4o", "multiplier": 0.0, "category": "OpenAI"}, "gpt-4.1-mini": {"display_name": "GPT-4.1 mini", "multiplier": 0.0, "category": "OpenAI"}, "gpt-4.1-nano": {"display_name": "GPT-4.1 nano", "multiplier": 0.0, "category": "OpenAI"}, "gpt-5.4": {"display_name": "GPT-5.4", "multiplier": 1.0, "category": "OpenAI"}, "gpt-5.4-mini": {"display_name": "GPT-5.4 mini", "multiplier": 0.33, "category": "OpenAI"}, "gpt-5.1": {"display_name": "GPT-5.1", "multiplier": 1.0, "category": "OpenAI"}, "gpt-5.2": {"display_name": "GPT-5.2", "multiplier": 1.0, "category": "OpenAI"}, "gpt-5.3-codex": {"display_name": "GPT-5.3-Codex", "multiplier": 1.0, "category": "OpenAI"}, "gpt-5.2-codex": {"display_name": "GPT-5.2-Codex", "multiplier": 1.0, "category": "OpenAI"}, "gpt-5.1-codex": {"display_name": "GPT-5.1-Codex", "multiplier": 1.0, "category": "OpenAI"}, "gpt-5.1-codex-max": {"display_name": "GPT-5.1-Codex-Max", "multiplier": 1.0, "category": "OpenAI"}, "gpt-5.1-codex-mini": {"display_name": "GPT-5.1-Codex-Mini", "multiplier": 0.33, "category": "OpenAI"}, "gpt-5-mini": {"display_name": "GPT-5 mini", "multiplier": 0.0, "category": "OpenAI"}, "o1": {"display_name": "o1", "multiplier": 1.0, "category": "OpenAI"}, "o1-mini": {"display_name": "o1-mini", "multiplier": 0.0, "category": "OpenAI"}, "o3-mini": {"display_name": "o3-mini", "multiplier": 0.0, "category": "OpenAI"}, "gemini-2.0-pro": {"display_name": "Gemini 2.0 Pro", "multiplier": 1.0, "category": "Google"}, "gemini-3-flash": {"display_name": "Gemini 3 Flash", "multiplier": 0.33, "category": "Google"}, "gemini-3-pro": {"display_name": "Gemini 3 Pro", "multiplier": 1.0, "category": "Google"}, "gemini-3.1-pro": {"display_name": "Gemini 3.1 Pro", "multiplier": 1.0, "category": "Google"}, "grok-code-fast-1": {"display_name": "Grok Code Fast 1", "multiplier": 0.25, "category": "xAI"}, "raptor-mini": {"display_name": "Raptor mini", "multiplier": 0.0, "category": "๊ธฐํƒ€"}, } class CopilotUsageManager: """GitHub Copilot API ์‚ฌ์šฉ๋Ÿ‰์„ ์›”๊ฐ„ ๊ธฐ์ค€์œผ๋กœ ์ถ”์ ํ•ฉ๋‹ˆ๋‹ค.""" DB_PREFIX = "copilot:" @staticmethod def get_db_conn(): if os.environ.get("LOCAL_AGENT_MODE", "false").lower() == "true": return None return psycopg2.connect(_resolve_db_url(), connect_timeout=5) @staticmethod def _month_start(): now = datetime.now(timezone.utc) return now.replace(day=1).date() @classmethod def track_usage(cls, model_id): db_key = cls.DB_PREFIX + model_id now_utc = datetime.now(timezone.utc) month_start = cls._month_start() conn = None cur = None try: conn = cls.get_db_conn() if not conn: return True cur = conn.cursor() cur.execute( "SELECT last_reset_date FROM taemingames.api_usage_tracker WHERE model_id = %s FOR UPDATE;", (db_key,) ) row = cur.fetchone() if not row: cur.execute( "INSERT INTO taemingames.api_usage_tracker (model_id, last_reset_date, daily_requests, daily_tokens) VALUES (%s, %s, 1, 0) ON CONFLICT DO NOTHING;", (db_key, month_start) ) cur.execute( "UPDATE taemingames.api_usage_tracker SET last_request_at = %s WHERE model_id = %s;", (now_utc, db_key) ) else: last_reset = row[0] if last_reset < month_start: cur.execute( "UPDATE taemingames.api_usage_tracker SET daily_requests=1, last_request_at=%s, last_reset_date=%s WHERE model_id=%s;", (now_utc, month_start, db_key) ) else: cur.execute( "UPDATE taemingames.api_usage_tracker SET daily_requests=daily_requests+1, last_request_at=%s WHERE model_id=%s;", (now_utc, db_key) ) conn.commit() return True except Exception as e: print(f"โŒ [CopilotUsageManager] track failed: {e}") return False finally: if cur: try: cur.close() except: pass if conn: try: conn.close() except: pass @classmethod def get_all_usage(cls, monthly_budget=None): if monthly_budget is None: monthly_budget = COPILOT_MONTHLY_BUDGET results = [] conn = None cur = None try: prefix = cls.DB_PREFIX conn = cls.get_db_conn() if not conn: return {"models": [], "monthly_budget": monthly_budget, "total_premium_used": 0} cur = conn.cursor() cur.execute( "SELECT model_id, daily_requests, last_request_at FROM taemingames.api_usage_tracker WHERE model_id LIKE %s;", (prefix + "%",) ) rows = {r[0].replace(prefix, ""): (r[1], r[2]) for r in cur.fetchall()} total_premium = 0.0 for mid, info in COPILOT_MODEL_INFO.items(): calls, last = rows.get(mid, (0, None)) premium = round(calls * info["multiplier"], 2) total_premium += premium results.append({ "model_id": mid, "display_name": info["display_name"], "category": info["category"], "multiplier": info["multiplier"], "calls_this_month": calls, "premium_used": premium, "last_request_at": last.isoformat() if last else None, }) return { "models": results, "monthly_budget": monthly_budget, "total_premium_used": round(total_premium, 2), } except Exception as e: print(f"โŒ [CopilotUsageManager] get_all_usage failed: {e}") return {"models": [], "monthly_budget": monthly_budget, "total_premium_used": 0} finally: if cur: try: cur.close() except: pass if conn: try: conn.close() except: pass def _sanitize_db_url(url: str) -> str: from urllib.parse import urlparse, urlunparse, urlencode, parse_qs url = url.strip().strip('"').strip("'").strip() parsed = urlparse(url) qs = parse_qs(parsed.query, keep_blank_values=True) qs.pop("schema", None) qs.pop("search_path", None) qs.pop("connection_limit", None) clean_query = urlencode(qs, doseq=True) return urlunparse(parsed._replace(query=clean_query)) def _resolve_db_url(): url = os.environ.get("DATABASE_URL") if url: return _sanitize_db_url(url) try: result = subprocess.run( 'bw get password "Imperial: LOCAL_DATABASE_URL" --raw', capture_output=True, text=True, shell=True, timeout=10, encoding='utf-8', errors='replace' ) val = result.stdout.strip() if val and "postgresql://" in val: val = _sanitize_db_url(val) os.environ["DATABASE_URL"] = val return val except: pass return "postgresql://localhost:5432/postgres" class QuotaExceededError(Exception): """API ์ฟผํ„ฐ(RPD)๊ฐ€ ์ดˆ๊ณผ๋˜์—ˆ์„ ๋•Œ ๋ฐœ์ƒํ•˜๋Š” ์ œ๊ตญ ํ‘œ์ค€ ์˜ˆ์™ธ์ž…๋‹ˆ๋‹ค.""" def __init__(self, model_id, limit): self.model_id = model_id self.limit = limit super().__init__(f"๐Ÿ”ฑ [ํ™ฉ์‹ค ๋ณด์•ˆ ๊ฒฝ๋ณด] '{model_id}' ๋ชจ๋ธ์˜ ์ผ์ผ ์‚ฌ์šฉ ํ•œ๋„({limit} RPD)๋ฅผ ์ดˆ๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค. ๋‚ด์ผ ๋‹ค์‹œ ์ดˆ๊ธฐํ™”๋ฉ๋‹ˆ๋‹ค.") class ImperialQuotaManager: """Gemini API ์‚ฌ์šฉ๋Ÿ‰์„ ์ถ”์ ํ•˜๊ณ  ํ•œ๋„๋ฅผ ๊ฐ•์ œํ•˜๋Š” ์ „๋‹ด ๊ด€๋ฆฌ์ž์ž…๋‹ˆ๋‹ค.""" @staticmethod def get_db_conn(): if os.environ.get("LOCAL_AGENT_MODE", "false").lower() == "true": return None return psycopg2.connect(_resolve_db_url(), connect_timeout=5) @staticmethod def _get_utc_now(): return datetime.now(timezone.utc) @classmethod def validate_quota(cls, model_id): """API ํ˜ธ์ถœ ์ „ RPD ํ•œ๋„๋ฅผ ์ฒดํฌํ•˜๊ณ  ์ดˆ๊ณผ ์‹œ QuotaExceededError๋ฅผ ๋ฐœ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค.""" limit_data = MODEL_LIMITS.get(model_id) if not limit_data: return # ๋ฏธ๋“ฑ๋ก ๋ชจ๋ธ์€ ํ†ต๊ณผ rpd_limit = int(limit_data.get("rpd", 0)) if rpd_limit <= 0: return # ๋ฌด์ œํ•œ(0 or -1) ํ†ต๊ณผ conn = None cur = None try: conn = cls.get_db_conn() if not conn: return cur = conn.cursor() cur.execute( "SELECT daily_requests, last_reset_date FROM taemingames.api_usage_tracker WHERE model_id = %s;", (model_id,) ) row = cur.fetchone() if not row: return # ๊ธฐ๋ก ์—†์œผ๋ฉด ์ฒซ ํ˜ธ์ถœ์ด๋ฏ€๋กœ ํ†ต๊ณผ used_requests, last_reset = row today_utc = cls._get_utc_now().date() # ๋‚ ์งœ๊ฐ€ ์˜ค๋Š˜์ด๋ฉด ์‹ค์ œ ์‚ฌ์šฉ๋Ÿ‰ ์ฒดํฌ if last_reset == today_utc: if used_requests >= rpd_limit: raise QuotaExceededError(model_id, rpd_limit) except QuotaExceededError: raise except Exception as e: print(f"[Quota Guard] Validation failed: {e}") return # DB ์žฅ์•  ์‹œ ๊ฐ€๋™ ์œ ์ง€๋ฅผ ์œ„ํ•ด ํ†ต๊ณผ finally: if cur: try: cur.close() except: pass if conn: try: conn.close() except: pass @classmethod def track_usage(cls, model_id, tokens=0): """API ํ˜ธ์ถœ ์‹œ ์‚ฌ์šฉ๋Ÿ‰์„ ๊ธฐ๋กํ•˜๊ณ  ์ผ์ผ ๋ฆฌ์…‹์„ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.""" now_utc = cls._get_utc_now() today_utc = now_utc.date() conn = None cur = None try: conn = cls.get_db_conn() if not conn: return True cur = conn.cursor() # 1. ํ˜„์žฌ ์ƒํƒœ ์กฐํšŒ ๋ฐ ๋ฆฌ์…‹ ์ฒดํฌ cur.execute( "SELECT last_reset_date FROM taemingames.api_usage_tracker WHERE model_id = %s FOR UPDATE;", (model_id,) ) row = cur.fetchone() if not row: # ๋ชจ๋ธ์ด ์—†์œผ๋ฉด ์ƒ์„ฑ cur.execute( "INSERT INTO taemingames.api_usage_tracker (model_id, last_reset_date, daily_requests, daily_tokens) VALUES (%s, %s, 1, 0) ON CONFLICT DO NOTHING;", (model_id, today_utc) ) last_reset = today_utc else: last_reset = row[0] # 2. ๋‚ ์งœ๊ฐ€ ๋ฐ”๋€Œ์—ˆ์œผ๋ฉด ๋ฆฌ์…‹ (๊ตฌ๊ธ€ ๋ฆฌ์…‹ ์ฃผ๊ธฐ: 00:00 UTC) if last_reset != today_utc: cur.execute( """UPDATE taemingames.api_usage_tracker SET daily_requests = 1, daily_tokens = 0, last_request_at = %s, last_reset_date = %s WHERE model_id = %s;""", (now_utc, today_utc, model_id) ) else: # ์ผ๋ฐ˜ ๋ˆ„์  ์—…๋ฐ์ดํŠธ cur.execute( """UPDATE taemingames.api_usage_tracker SET daily_requests = daily_requests + 1, daily_tokens = 0, last_request_at = %s WHERE model_id = %s;""", (now_utc, model_id) ) conn.commit() return True except Exception as e: print(f"โŒ [QuotaManager] Failed to track usage for {model_id}: {e}") return False finally: if cur: try: cur.close() except: pass if conn: try: conn.close() except: pass @classmethod def get_all_usage_with_limits(cls): """๋ชจ๋“  ๋ชจ๋ธ์˜ ํ˜„์žฌ ์‚ฌ์šฉ๋Ÿ‰๊ณผ ์„ค์ •๋œ ํ•œ๋„๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. (์‚ฌ์šฉ๋Ÿ‰ 0์ธ ๋ชจ๋ธ ํฌํ•จ)""" results = [] conn = None cur = None try: conn = cls.get_db_conn() if not conn: # Return dummy limits when bypassing DB for mid, limit in MODEL_LIMITS.items(): results.append({ "model_id": mid, "rpm": 0, "tpm": 0, "rpd": 0, "last_request_at": None, "limit": limit }) return results cur = conn.cursor() cur.execute("SELECT model_id, daily_requests, daily_tokens, last_request_at FROM taemingames.api_usage_tracker;") rows_data = cur.fetchall() rows_dict = {r[0]: (r[1], r[2], r[3]) for r in rows_data} # MODEL_LIMITS์— ์ •์˜๋œ ๋ชจ๋“  ๋ชจ๋ธ์„ ์ˆœํšŒํ•˜๋ฉฐ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์œผ๋ฉด 0์œผ๋กœ ์ฑ„์›€ for mid, limit in MODEL_LIMITS.items(): req, tok, last = rows_dict.get(mid, (0, 0, None)) results.append({ "model_id": mid, "rpm": 0, # ์‹ค์‹œ๊ฐ„ RPM์€ DB์— ๋ˆ„์ ํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ 0์œผ๋กœ ํ‘œ์‹œ (ํ•œ๋„๋Š” limit์— ํฌํ•จ) "tpm": tok, # Daily Tokens (UI์—์„œ TPM์œผ๋กœ ํ‘œ์‹œ๋  ์ˆ˜ ์žˆ์œผ๋‚˜ ์‹ค์ œ๋ก  ์ผ์ผ ๋ˆ„์ ) "rpd": req, # Daily Requests "last_request_at": last.isoformat() if last else None, "limit": limit }) return results except Exception as e: print(f"[QuotaManager] ERROR: Failed to get usage: {e}") return [] finally: if cur: try: cur.close() except: pass if conn: try: conn.close() except: pass if __name__ == "__main__": # Test ImperialQuotaManager.track_usage("gemini-3.1-flash-lite", tokens=100) print(ImperialQuotaManager.get_all_usage_with_limits())