webmazta commited on
Commit Β·
4b270d1
1
Parent(s): 22d62d7
feat(llm): add 90s cooldown + auto-retry on OpenRouter rate limit (429)
Browse files- app.py +26 -1
- article_rewriter.py +54 -21
- main.py +49 -22
app.py
CHANGED
|
@@ -39,6 +39,27 @@ from dotenv import load_dotenv
|
|
| 39 |
|
| 40 |
load_dotenv()
|
| 41 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
# ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 43 |
# App Setup
|
| 44 |
# ββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -328,7 +349,11 @@ def screen_article(title, summary, criteria):
|
|
| 328 |
if not key: return True
|
| 329 |
prompt = f"Decide if this article matches criteria: {criteria}\nTitle: {title}\nSummary: {summary}\nRespond ONLY with JSON: {{\"publish\": true/false, \"reason\": \"...\"}}"
|
| 330 |
client = OpenAI(base_url="https://openrouter.ai/api/v1", api_key=key)
|
| 331 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 332 |
result = json.loads(resp.choices[0].message.content)
|
| 333 |
print(f"[AGENT] Screening: {result.get('publish')} - {result.get('reason')}")
|
| 334 |
return result.get("publish", True)
|
|
|
|
| 39 |
|
| 40 |
load_dotenv()
|
| 41 |
|
| 42 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 43 |
+
# OpenRouter Retry Helper
|
| 44 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 45 |
+
|
| 46 |
+
def _openrouter_with_retry(api_call_fn, max_retries=1):
|
| 47 |
+
"""
|
| 48 |
+
Retry an OpenRouter API call with 90-second cooldown on rate limit (HTTP 429).
|
| 49 |
+
Falls through to caller's exception handling after max retries exhausted.
|
| 50 |
+
"""
|
| 51 |
+
import time
|
| 52 |
+
for attempt in range(max_retries + 1):
|
| 53 |
+
try:
|
| 54 |
+
return api_call_fn()
|
| 55 |
+
except Exception as e:
|
| 56 |
+
error_str = str(e)
|
| 57 |
+
if "429" in error_str and attempt < max_retries:
|
| 58 |
+
print(f" [RATE LIMIT] OpenRouter rate limited (429). Cooling down for 90 seconds... (retry {attempt + 1}/{max_retries})")
|
| 59 |
+
time.sleep(90)
|
| 60 |
+
else:
|
| 61 |
+
raise
|
| 62 |
+
|
| 63 |
# ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 64 |
# App Setup
|
| 65 |
# ββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 349 |
if not key: return True
|
| 350 |
prompt = f"Decide if this article matches criteria: {criteria}\nTitle: {title}\nSummary: {summary}\nRespond ONLY with JSON: {{\"publish\": true/false, \"reason\": \"...\"}}"
|
| 351 |
client = OpenAI(base_url="https://openrouter.ai/api/v1", api_key=key)
|
| 352 |
+
|
| 353 |
+
def _do_screen():
|
| 354 |
+
return client.chat.completions.create(model="qwen/qwen3.6-plus:free", messages=[{"role":"user","content":prompt}], response_format={"type":"json_object"})
|
| 355 |
+
|
| 356 |
+
resp = _openrouter_with_retry(_do_screen)
|
| 357 |
result = json.loads(resp.choices[0].message.content)
|
| 358 |
print(f"[AGENT] Screening: {result.get('publish')} - {result.get('reason')}")
|
| 359 |
return result.get("publish", True)
|
article_rewriter.py
CHANGED
|
@@ -51,6 +51,32 @@ load_dotenv()
|
|
| 51 |
|
| 52 |
OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY", "")
|
| 53 |
GROQ_API_KEY = os.getenv("GROQ_API_KEY", "")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
WP_URL = os.getenv("WP_URL", "").rstrip("/")
|
| 55 |
WP_USERNAME = os.getenv("WP_USERNAME", "")
|
| 56 |
WP_APP_PASSWORD = os.getenv("WP_APP_PASSWORD", "")
|
|
@@ -590,21 +616,24 @@ def rewrite_article(original_title: str, original_body: str, source_url: str) ->
|
|
| 590 |
try:
|
| 591 |
print(f"βοΈ Rewriting with OpenRouter ({LLM_MODEL})...")
|
| 592 |
client = OpenAI(base_url="https://openrouter.ai/api/v1", api_key=OPENROUTER_API_KEY)
|
| 593 |
-
|
| 594 |
-
|
| 595 |
-
|
| 596 |
-
|
| 597 |
-
|
| 598 |
-
|
| 599 |
-
|
| 600 |
-
|
| 601 |
-
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
"
|
|
|
|
|
|
|
| 605 |
}
|
| 606 |
-
|
| 607 |
-
|
|
|
|
| 608 |
raw_text = completion.choices[0].message.content.strip()
|
| 609 |
|
| 610 |
# Log reasoning tokens (OpenRouter specific)
|
|
@@ -853,13 +882,17 @@ def filter_irrelevant_images(image_urls: list[str], article_title: str, article_
|
|
| 853 |
try:
|
| 854 |
print(f" [AI-FILTER] Trying OpenRouter (qwen/qwen3.6-plus:free)...")
|
| 855 |
client = OpenAI(base_url="https://openrouter.ai/api/v1", api_key=openrouter_key)
|
| 856 |
-
|
| 857 |
-
|
| 858 |
-
|
| 859 |
-
|
| 860 |
-
|
| 861 |
-
|
| 862 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 863 |
raw_text = completion.choices[0].message.content.strip()
|
| 864 |
except Exception as e:
|
| 865 |
print(f" [WARN] OpenRouter image filter failed: {e}")
|
|
|
|
| 51 |
|
| 52 |
OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY", "")
|
| 53 |
GROQ_API_KEY = os.getenv("GROQ_API_KEY", "")
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 57 |
+
# 0b. OpenRouter Retry Helper
|
| 58 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 59 |
+
|
| 60 |
+
def _openrouter_with_retry(api_call_fn, max_retries=1):
|
| 61 |
+
"""
|
| 62 |
+
Retry an OpenRouter API call with 90-second cooldown on rate limit (HTTP 429).
|
| 63 |
+
Falls through to caller's exception handling after max retries exhausted.
|
| 64 |
+
"""
|
| 65 |
+
for attempt in range(max_retries + 1):
|
| 66 |
+
try:
|
| 67 |
+
return api_call_fn()
|
| 68 |
+
except Exception as e:
|
| 69 |
+
error_str = str(e)
|
| 70 |
+
if "429" in error_str and attempt < max_retries:
|
| 71 |
+
print(f" [RATE LIMIT] OpenRouter rate limited (429). Cooling down for 90 seconds... (retry {attempt + 1}/{max_retries})")
|
| 72 |
+
time.sleep(90)
|
| 73 |
+
else:
|
| 74 |
+
raise
|
| 75 |
+
|
| 76 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 77 |
+
# Remaining Configuration
|
| 78 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 79 |
+
|
| 80 |
WP_URL = os.getenv("WP_URL", "").rstrip("/")
|
| 81 |
WP_USERNAME = os.getenv("WP_USERNAME", "")
|
| 82 |
WP_APP_PASSWORD = os.getenv("WP_APP_PASSWORD", "")
|
|
|
|
| 616 |
try:
|
| 617 |
print(f"βοΈ Rewriting with OpenRouter ({LLM_MODEL})...")
|
| 618 |
client = OpenAI(base_url="https://openrouter.ai/api/v1", api_key=OPENROUTER_API_KEY)
|
| 619 |
+
|
| 620 |
+
def _do_rewrite():
|
| 621 |
+
return client.chat.completions.create(
|
| 622 |
+
model=LLM_MODEL,
|
| 623 |
+
messages=[
|
| 624 |
+
{"role": "system", "content": "You are an expert content writer and SEO specialist."},
|
| 625 |
+
{"role": "user", "content": prompt}
|
| 626 |
+
],
|
| 627 |
+
temperature=0.8,
|
| 628 |
+
response_format={"type": "json_object"},
|
| 629 |
+
extra_body={
|
| 630 |
+
"reasoning": {
|
| 631 |
+
"max_tokens": 2048
|
| 632 |
+
}
|
| 633 |
}
|
| 634 |
+
)
|
| 635 |
+
|
| 636 |
+
completion = _openrouter_with_retry(_do_rewrite)
|
| 637 |
raw_text = completion.choices[0].message.content.strip()
|
| 638 |
|
| 639 |
# Log reasoning tokens (OpenRouter specific)
|
|
|
|
| 882 |
try:
|
| 883 |
print(f" [AI-FILTER] Trying OpenRouter (qwen/qwen3.6-plus:free)...")
|
| 884 |
client = OpenAI(base_url="https://openrouter.ai/api/v1", api_key=openrouter_key)
|
| 885 |
+
|
| 886 |
+
def _do_filter():
|
| 887 |
+
return client.chat.completions.create(
|
| 888 |
+
model="qwen/qwen3.6-plus:free",
|
| 889 |
+
messages=[{"role": "user", "content": prompt}],
|
| 890 |
+
temperature=0.1,
|
| 891 |
+
response_format={"type": "json_object"},
|
| 892 |
+
extra_body={"reasoning": {"max_tokens": 512}}
|
| 893 |
+
)
|
| 894 |
+
|
| 895 |
+
completion = _openrouter_with_retry(_do_filter)
|
| 896 |
raw_text = completion.choices[0].message.content.strip()
|
| 897 |
except Exception as e:
|
| 898 |
print(f" [WARN] OpenRouter image filter failed: {e}")
|
main.py
CHANGED
|
@@ -707,6 +707,27 @@ def extract_frames(video_path: Path, count: int = 3) -> list[Image.Image]:
|
|
| 707 |
return frames
|
| 708 |
|
| 709 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 710 |
# ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 711 |
# 3. Video Analysis with Gemini
|
| 712 |
# ββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -765,7 +786,7 @@ def _analyze_frames(frames: list[Image.Image]) -> dict:
|
|
| 765 |
try:
|
| 766 |
print(f" [ENGINE] Trying OpenRouter ({LLM_MODEL})...")
|
| 767 |
client = OpenAI(base_url="https://openrouter.ai/api/v1", api_key=openrouter_key)
|
| 768 |
-
|
| 769 |
messages_content = [{"type": "text", "text": prompt}]
|
| 770 |
for frame in frames[:60]:
|
| 771 |
b64_image = _image_to_base64(frame)
|
|
@@ -774,17 +795,20 @@ def _analyze_frames(frames: list[Image.Image]) -> dict:
|
|
| 774 |
"image_url": {"url": f"data:image/jpeg;base64,{b64_image}"}
|
| 775 |
})
|
| 776 |
|
| 777 |
-
|
| 778 |
-
|
| 779 |
-
|
| 780 |
-
|
| 781 |
-
|
| 782 |
-
|
| 783 |
-
|
| 784 |
-
"
|
|
|
|
|
|
|
| 785 |
}
|
| 786 |
-
|
| 787 |
-
|
|
|
|
| 788 |
raw_text = completion.choices[0].message.content.strip()
|
| 789 |
|
| 790 |
# Log reasoning tokens if available (OpenRouter specific)
|
|
@@ -897,18 +921,21 @@ def generate_content(analysis: any, source_url: str) -> dict:
|
|
| 897 |
try:
|
| 898 |
print(f"[CONTENT] Generating with OpenRouter ({LLM_MODEL})...")
|
| 899 |
client = OpenAI(base_url="https://openrouter.ai/api/v1", api_key=openrouter_key)
|
| 900 |
-
|
| 901 |
-
|
| 902 |
-
|
| 903 |
-
|
| 904 |
-
|
| 905 |
-
|
| 906 |
-
|
| 907 |
-
|
| 908 |
-
"
|
|
|
|
|
|
|
| 909 |
}
|
| 910 |
-
|
| 911 |
-
|
|
|
|
| 912 |
raw_text = completion.choices[0].message.content.strip()
|
| 913 |
|
| 914 |
# Log reasoning tokens (OpenRouter specific)
|
|
|
|
| 707 |
return frames
|
| 708 |
|
| 709 |
|
| 710 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 711 |
+
# 2b. OpenRouter Retry Helper
|
| 712 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 713 |
+
|
| 714 |
+
def _openrouter_with_retry(api_call_fn, max_retries=1):
|
| 715 |
+
"""
|
| 716 |
+
Retry an OpenRouter API call with 90-second cooldown on rate limit (HTTP 429).
|
| 717 |
+
Falls through to caller's exception handling after max retries exhausted.
|
| 718 |
+
"""
|
| 719 |
+
for attempt in range(max_retries + 1):
|
| 720 |
+
try:
|
| 721 |
+
return api_call_fn()
|
| 722 |
+
except Exception as e:
|
| 723 |
+
error_str = str(e)
|
| 724 |
+
if "429" in error_str and attempt < max_retries:
|
| 725 |
+
print(f" [RATE LIMIT] OpenRouter rate limited (429). Cooling down for 90 seconds... (retry {attempt + 1}/{max_retries})")
|
| 726 |
+
time.sleep(90)
|
| 727 |
+
else:
|
| 728 |
+
raise
|
| 729 |
+
|
| 730 |
+
|
| 731 |
# ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 732 |
# 3. Video Analysis with Gemini
|
| 733 |
# ββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 786 |
try:
|
| 787 |
print(f" [ENGINE] Trying OpenRouter ({LLM_MODEL})...")
|
| 788 |
client = OpenAI(base_url="https://openrouter.ai/api/v1", api_key=openrouter_key)
|
| 789 |
+
|
| 790 |
messages_content = [{"type": "text", "text": prompt}]
|
| 791 |
for frame in frames[:60]:
|
| 792 |
b64_image = _image_to_base64(frame)
|
|
|
|
| 795 |
"image_url": {"url": f"data:image/jpeg;base64,{b64_image}"}
|
| 796 |
})
|
| 797 |
|
| 798 |
+
def _do_analyze():
|
| 799 |
+
return client.chat.completions.create(
|
| 800 |
+
model=LLM_MODEL,
|
| 801 |
+
messages=[{"role": "user", "content": messages_content}],
|
| 802 |
+
temperature=0.2,
|
| 803 |
+
response_format={"type": "json_object"},
|
| 804 |
+
extra_body={
|
| 805 |
+
"reasoning": {
|
| 806 |
+
"max_tokens": 2048
|
| 807 |
+
}
|
| 808 |
}
|
| 809 |
+
)
|
| 810 |
+
|
| 811 |
+
completion = _openrouter_with_retry(_do_analyze)
|
| 812 |
raw_text = completion.choices[0].message.content.strip()
|
| 813 |
|
| 814 |
# Log reasoning tokens if available (OpenRouter specific)
|
|
|
|
| 921 |
try:
|
| 922 |
print(f"[CONTENT] Generating with OpenRouter ({LLM_MODEL})...")
|
| 923 |
client = OpenAI(base_url="https://openrouter.ai/api/v1", api_key=openrouter_key)
|
| 924 |
+
|
| 925 |
+
def _do_generate():
|
| 926 |
+
return client.chat.completions.create(
|
| 927 |
+
model=LLM_MODEL,
|
| 928 |
+
messages=[{"role": "user", "content": prompt}],
|
| 929 |
+
temperature=0.7,
|
| 930 |
+
response_format={"type": "json_object"},
|
| 931 |
+
extra_body={
|
| 932 |
+
"reasoning": {
|
| 933 |
+
"max_tokens": 2048
|
| 934 |
+
}
|
| 935 |
}
|
| 936 |
+
)
|
| 937 |
+
|
| 938 |
+
completion = _openrouter_with_retry(_do_generate)
|
| 939 |
raw_text = completion.choices[0].message.content.strip()
|
| 940 |
|
| 941 |
# Log reasoning tokens (OpenRouter specific)
|