|
3 | 3 | from .utils import get_summary_from_url, translate_to_korean, categorize_summary
|
4 | 4 | import readtime
|
5 | 5 | import os
|
6 |
| -from datetime import timedelta |
| 6 | +from datetime import timedelta, datetime |
| 7 | +from django.utils import timezone |
| 8 | +import pytz |
7 | 9 |
|
8 | 10 |
|
9 | 11 | def rss_item_upload_path(instance, filename):
|
@@ -434,14 +436,148 @@ class LLMService(models.Model):
|
434 | 436 | def __str__(self):
|
435 | 437 | return f"{self.get_provider_display()} (Priority: {self.priority})"
|
436 | 438 |
|
| 439 | + @classmethod |
| 440 | + def get_llm_provider_model(cls): |
| 441 | + """ |
| 442 | + Calculate today's usage and return available provider and model based on priority. |
| 443 | + Returns tuple: (provider, model_name) or (None, None) if no models available. |
| 444 | + """ |
| 445 | + # Model configurations with daily limits |
| 446 | + MODEL_CONFIGS = { |
| 447 | + 'google-gla:gemini-2.5-pro-preview-06-05': { |
| 448 | + 'daily_requests': 25, |
| 449 | + 'rpm': 5, |
| 450 | + 'tpm': 250000, |
| 451 | + 'daily_tokens': 1000000, |
| 452 | + 'provider': 'gemini' |
| 453 | + }, |
| 454 | + 'google-gla:gemini-2.5-flash-preview-05-20': { |
| 455 | + 'daily_requests': 500, |
| 456 | + 'rpm': 10, |
| 457 | + 'tpm': 250000, |
| 458 | + 'daily_tokens': None, # Not specified |
| 459 | + 'provider': 'gemini' |
| 460 | + }, |
| 461 | + 'openai:gpt-4.5-preview-2025-02-27': { |
| 462 | + 'daily_tokens': 250000, # Combined with gpt-4.1-2025-04-14 |
| 463 | + 'provider': 'openai', |
| 464 | + 'combined_with': ['openai:gpt-4.1-2025-04-14'] |
| 465 | + }, |
| 466 | + 'openai:gpt-4.1-2025-04-14': { |
| 467 | + 'daily_tokens': 250000, # Combined with gpt-4.5-preview-2025-02-27 |
| 468 | + 'provider': 'openai', |
| 469 | + 'combined_with': ['openai:gpt-4.5-preview-2025-02-27'] |
| 470 | + }, |
| 471 | + 'openai:gpt-4.1-mini-2025-04-14': { |
| 472 | + 'daily_tokens': 2500000, |
| 473 | + 'provider': 'openai' |
| 474 | + }, |
| 475 | + 'anthropic:claude-3-5-haiku-latest': { |
| 476 | + 'provider': 'claude' |
| 477 | + }, |
| 478 | + 'anthropic:claude-sonnet-4-0': { |
| 479 | + 'provider': 'claude' |
| 480 | + } |
| 481 | + } |
| 482 | + |
| 483 | + # Get active services ordered by priority |
| 484 | + active_services = cls.objects.filter(is_active=True).order_by('priority') |
| 485 | + |
| 486 | + for service in active_services: |
| 487 | + available_models = cls._get_available_models_for_provider(service.provider, MODEL_CONFIGS) |
| 488 | + if available_models: |
| 489 | + return service.provider, available_models[0] |
| 490 | + |
| 491 | + return None, None |
| 492 | + |
| 493 | + @classmethod |
| 494 | + def _get_available_models_for_provider(cls, provider, model_configs): |
| 495 | + """Get available models for a specific provider based on usage limits.""" |
| 496 | + available_models = [] |
| 497 | + |
| 498 | + if provider == 'gemini': |
| 499 | + # Check Google Gemini models with Pacific Time reset |
| 500 | + pacific_tz = pytz.timezone('US/Pacific') |
| 501 | + now_pacific = timezone.now().astimezone(pacific_tz) |
| 502 | + start_of_day_pacific = now_pacific.replace(hour=0, minute=0, second=0, microsecond=0) |
| 503 | + start_of_day_utc = start_of_day_pacific.astimezone(pytz.UTC) |
| 504 | + |
| 505 | + for model_key, config in model_configs.items(): |
| 506 | + if not model_key.startswith('google-gla:'): |
| 507 | + continue |
| 508 | + |
| 509 | + model_name = model_key.split(':', 1)[1] |
| 510 | + today_usage = LLMUsage.objects.filter( |
| 511 | + model_name=model_key, |
| 512 | + date__gte=start_of_day_utc |
| 513 | + ).aggregate( |
| 514 | + total_tokens=models.Sum(models.F('input_tokens') + models.F('output_tokens')), |
| 515 | + total_requests=models.Count('id') |
| 516 | + ) |
| 517 | + |
| 518 | + total_tokens = today_usage['total_tokens'] or 0 |
| 519 | + total_requests = today_usage['total_requests'] or 0 |
| 520 | + |
| 521 | + # Check daily limits |
| 522 | + if config.get('daily_requests') and total_requests >= config['daily_requests']: |
| 523 | + continue |
| 524 | + if config.get('daily_tokens') and total_tokens >= config['daily_tokens']: |
| 525 | + continue |
| 526 | + |
| 527 | + available_models.append(model_name) |
| 528 | + |
| 529 | + elif provider == 'openai': |
| 530 | + # Check OpenAI models with UTC midnight reset |
| 531 | + today_start = timezone.now().replace(hour=0, minute=0, second=0, microsecond=0) |
| 532 | + |
| 533 | + # Handle combined quota models (gpt-4.5 and gpt-4.1) |
| 534 | + combined_models = ['openai:gpt-4.5-preview-2025-02-27', 'openai:gpt-4.1-2025-04-14'] |
| 535 | + combined_usage = LLMUsage.objects.filter( |
| 536 | + model_name__in=combined_models, |
| 537 | + date__gte=today_start |
| 538 | + ).aggregate( |
| 539 | + total_tokens=models.Sum(models.F('input_tokens') + models.F('output_tokens')) |
| 540 | + ) |
| 541 | + combined_tokens = combined_usage['total_tokens'] or 0 |
| 542 | + |
| 543 | + # Check individual models |
| 544 | + for model_key, config in model_configs.items(): |
| 545 | + if not model_key.startswith('openai:'): |
| 546 | + continue |
| 547 | + |
| 548 | + model_name = model_key.split(':', 1)[1] |
| 549 | + |
| 550 | + if model_key in combined_models: |
| 551 | + # Check combined quota |
| 552 | + if combined_tokens < 250000: |
| 553 | + available_models.append(model_name) |
| 554 | + else: |
| 555 | + # Check individual quota (gpt-4.1-mini) |
| 556 | + today_usage = LLMUsage.objects.filter( |
| 557 | + model_name=model_key, |
| 558 | + date__gte=today_start |
| 559 | + ).aggregate( |
| 560 | + total_tokens=models.Sum(models.F('input_tokens') + models.F('output_tokens')) |
| 561 | + ) |
| 562 | + total_tokens = today_usage['total_tokens'] or 0 |
| 563 | + |
| 564 | + if total_tokens < config['daily_tokens']: |
| 565 | + available_models.append(model_name) |
| 566 | + |
| 567 | + elif provider == 'claude': |
| 568 | + # Claude as fallback - assume always available |
| 569 | + available_models = ['claude-sonnet-4-0'] |
| 570 | + |
| 571 | + return available_models |
| 572 | + |
437 | 573 | class Meta:
|
438 | 574 | verbose_name = "LLM Service"
|
439 | 575 | verbose_name_plural = "LLM Services"
|
440 | 576 | ordering = ['priority']
|
441 | 577 |
|
442 | 578 |
|
443 | 579 | class LLMUsage(models.Model):
|
444 |
| - date = models.DateField( |
| 580 | + date = models.DateTimeField( |
445 | 581 | auto_now_add=True,
|
446 | 582 | help_text="사용 날짜"
|
447 | 583 | )
|
|
0 commit comments