GgaammGo commited on
Commit
3924d4e
Β·
verified Β·
1 Parent(s): 96698e9

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +51 -25
app.py CHANGED
@@ -9,24 +9,39 @@ from openai import OpenAI, APIError
9
  MODEL_ID = "openrouter/free"
10
  OPENROUTER_API_KEY = os.environ.get("OPENROUTER_API_KEY", None)
11
 
 
 
 
 
 
 
12
  # λ„€νŠΈμ›Œν¬ 였λ₯˜ λ“±μœΌλ‘œ μ‹€μ‹œκ°„ μ‘°νšŒκ°€ μ™„μ „νžˆ μ‹€νŒ¨ν–ˆμ„ λ•Œλ§Œ μ‚¬μš©ν•˜λŠ”
13
  # μ΅œν›„μ˜ μ•ˆμ „λ§(fallback) λͺ©λ‘μž…λ‹ˆλ‹€. 2026-07-02 κΈ°μ€€μœΌλ‘œ ν™•μΈλœ κ°’μž…λ‹ˆλ‹€.
 
 
 
 
14
  _SAFETY_NET_VISION_MODELS = [
15
  "google/gemma-4-31b-it:free",
16
  "nvidia/nemotron-nano-12b-v2-vl:free",
17
  ]
18
 
19
- # ── 이미지 지원 무료 λͺ¨λΈ λͺ©λ‘ μΊμ‹œ ──────────────────────────────────────
20
  # OpenRouter 곡식 λ¬Έμ„œ(openrouter.ai/docs/guides/overview/models)에 λ”°λ₯΄λ©΄
21
  # /api/v1/models 응닡은 OpenRouter μͺ½μ—μ„œλ„ edge에 μΊμ‹±λ˜μ–΄ μ œκ³΅λ˜λ―€λ‘œ,
22
  # 우리 μͺ½μ—μ„œλ„ λ§€ μš”μ²­λ§ˆλ‹€ μ‘°νšŒν•˜μ§€ μ•Šκ³  일정 μ‹œκ°„(TTL) λ™μ•ˆ μž¬μ‚¬μš©ν•©λ‹ˆλ‹€.
23
- _vision_models_cache: dict = {"models": [], "fetched_at": 0.0}
 
 
 
24
  _CACHE_TTL_SECONDS = 60 * 60 # 1μ‹œκ°„
25
 
26
 
27
- def _fetch_vision_free_models() -> list:
28
  """OpenRouter 곡식 API(/api/v1/models)λ₯Ό ν˜ΈμΆœν•΄, μ§€κΈˆ 이 μˆœκ°„
29
- 이미지 μž…λ ₯을 μ§€μ›ν•˜λŠ” 무료(:free) λͺ¨λΈ λͺ©λ‘μ„ κ°€μ Έμ˜΅λ‹ˆλ‹€.
 
 
30
  """
31
  resp = requests.get("https://openrouter.ai/api/v1/models", timeout=10)
32
  resp.raise_for_status()
@@ -37,32 +52,39 @@ def _fetch_vision_free_models() -> list:
37
  model_id = m.get("id", "")
38
  if not model_id.endswith(":free"):
39
  continue
 
 
40
  modalities = m.get("architecture", {}).get("input_modalities", [])
41
- if "image" in modalities:
42
- result.append(model_id)
 
43
  return result
44
 
45
 
46
- def get_vision_capable_free_models(force_refresh: bool = False) -> list:
47
- """μΊμ‹œλœ 이미지 지원 무료 λͺ¨λΈ λͺ©λ‘μ„ λ°˜ν™˜ν•©λ‹ˆλ‹€.
48
  μΊμ‹œκ°€ μ—†κ±°λ‚˜(졜초 호좜), TTL(1μ‹œκ°„)이 μ§€λ‚¬κ±°λ‚˜, force_refresh=True이면
49
  OpenRouter APIλ₯Ό λ‹€μ‹œ ν˜ΈμΆœν•΄ κ°±μ‹ ν•©λ‹ˆλ‹€.
50
  """
 
 
 
 
51
  now = time.time()
52
- cache_expired = (now - _vision_models_cache["fetched_at"]) > _CACHE_TTL_SECONDS
53
 
54
- if force_refresh or cache_expired or not _vision_models_cache["models"]:
55
  try:
56
- fresh_models = _fetch_vision_free_models()
57
  if fresh_models:
58
- _vision_models_cache["models"] = fresh_models
59
- _vision_models_cache["fetched_at"] = now
60
  except Exception:
61
  # 쑰회 μžμ²΄κ°€ μ‹€νŒ¨ν•˜λ©΄(λ„€νŠΈμ›Œν¬ 였λ₯˜ λ“±), κΈ°μ‘΄ μΊμ‹œκ°€ 있으면
62
  # κ·ΈλŒ€λ‘œ μ“°κ³ , μΊμ‹œμ‘°μ°¨ μ—†μœΌλ©΄ μ΅œν›„μ˜ μ•ˆμ „λ§ λͺ©λ‘μ„ μ‚¬μš©ν•©λ‹ˆλ‹€.
63
  pass
64
 
65
- return _vision_models_cache["models"] or _SAFETY_NET_VISION_MODELS
66
 
67
 
68
  # ── OpenRouter ν΄λΌμ΄μ–ΈνŠΈ μ΄ˆκΈ°ν™” ─────────────────────────────────────────
@@ -198,13 +220,17 @@ def predict_chat(message, history: list):
198
  has_image = bool(files)
199
 
200
  def _resolve_request_target(force_refresh: bool = False):
201
- if not has_image:
202
- return MODEL_ID, {}
203
- vision_models = get_vision_capable_free_models(force_refresh=force_refresh)
 
 
 
 
204
  # OpenRouter의 fallback κΈ°λŠ₯은 'models' 배열에 μ΅œλŒ€ 3κ°œκΉŒμ§€λ§Œ ν—ˆμš©ν•©λ‹ˆλ‹€.
205
  # (OpenRouter μ„œλ²„ 응닡 κΈ°μ€€: "'models' array must have 3 items or fewer.")
206
- vision_models = vision_models[:3]
207
- return vision_models[0], {"models": vision_models}
208
 
209
  request_model, extra_body = _resolve_request_target()
210
 
@@ -212,10 +238,9 @@ def predict_chat(message, history: list):
212
  try:
213
  stream = _call_openrouter_stream(request_model, messages, extra_body)
214
  except APIError as e:
215
- # 이미지 μš”μ²­μΈλ° "지원 λͺ¨λΈμ„ λͺ» 찾음(404)" 였λ₯˜κ°€ λ‚˜λ©΄,
216
- # μΊμ‹œκ°€ λ‚‘μ•˜μ„ κ°€λŠ₯성이 μžˆμœΌλ―€λ‘œ κ°•μ œλ‘œ ν•œ 번 더 μƒˆλ‘œ μ‘°νšŒν•΄μ„œ
217
- # λ”± ν•œ 번만 μž¬μ‹œλ„ν•©λ‹ˆλ‹€.
218
- if has_image and getattr(e, "status_code", None) == 404:
219
  request_model, extra_body = _resolve_request_target(force_refresh=True)
220
  stream = _call_openrouter_stream(request_model, messages, extra_body)
221
  else:
@@ -253,9 +278,10 @@ def predict_chat(message, history: list):
253
  # ── UI λΉŒλ“œ 및 API κ°œμ„€ ──────────────────────────────────────────────────────
254
  with gr.Blocks() as demo:
255
  gr.Markdown(
256
- f"# πŸ€– GS-AI API Server (OpenRouter Mode)\nModel: `{MODEL_ID}`\n\n"
 
257
  "ν…μŠ€νŠΈμ™€ ν•¨κ»˜ 이미지λ₯Ό μ²¨λΆ€ν•΄μ„œ μ§ˆλ¬Έν•˜μ‹€ 수 μžˆμŠ΅λ‹ˆλ‹€. "
258
- "(단, κ·Έ μ‹œμ μ— 이미지 인식 κ°€λŠ₯ν•œ 무료 λͺ¨λΈμ΄ μ—†μœΌλ©΄ 였λ₯˜κ°€ λ‚  수 μžˆμŠ΅λ‹ˆλ‹€.)"
259
  )
260
 
261
  chatbot_ui = gr.Chatbot(height=480)
 
9
  MODEL_ID = "openrouter/free"
10
  OPENROUTER_API_KEY = os.environ.get("OPENROUTER_API_KEY", None)
11
 
12
+ # μ±„νŒ… λͺ©μ μ— λ§žμ§€ μ•ŠλŠ” λͺ¨λΈ(κ²€μ—΄/νŒμ • μ „μš©, μž¬λž­ν‚Ή, μž„λ² λ”© λ“±)을 κ±ΈλŸ¬λ‚΄κΈ°
13
+ # μœ„ν•œ ν‚€μ›Œλ“œμž…λ‹ˆλ‹€. 이런 λͺ¨λΈμ€ ':free'이고 ν…μŠ€νŠΈ/이미지 μž…λ ₯을 μ§€μ›ν•˜λ”λΌλ„
14
+ # μ‹€μ œ λŒ€ν™” 응닡 λŒ€μ‹  νŒμ • 라벨 λ“± μ—‰λš±ν•œ 좜λ ₯을 내놓기 λ•Œλ¬Έμ— μ œμ™Έν•©λ‹ˆλ‹€.
15
+ # (예: nvidia/nemotron-3.5-content-safety:free β€” μ‹€μ œλ‘œ κ²ͺμœΌμ‹  사둀)
16
+ _NON_CHAT_MODEL_KEYWORDS = ["content-safety", "guard", "rerank", "embed", "moderation"]
17
+
18
  # λ„€νŠΈμ›Œν¬ 였λ₯˜ λ“±μœΌλ‘œ μ‹€μ‹œκ°„ μ‘°νšŒκ°€ μ™„μ „νžˆ μ‹€νŒ¨ν–ˆμ„ λ•Œλ§Œ μ‚¬μš©ν•˜λŠ”
19
  # μ΅œν›„μ˜ μ•ˆμ „λ§(fallback) λͺ©λ‘μž…λ‹ˆλ‹€. 2026-07-02 κΈ°μ€€μœΌλ‘œ ν™•μΈλœ κ°’μž…λ‹ˆλ‹€.
20
+ _SAFETY_NET_TEXT_MODELS = [
21
+ "meta-llama/llama-4-scout:free",
22
+ "deepseek/deepseek-chat-v3.1:free",
23
+ ]
24
  _SAFETY_NET_VISION_MODELS = [
25
  "google/gemma-4-31b-it:free",
26
  "nvidia/nemotron-nano-12b-v2-vl:free",
27
  ]
28
 
29
+ # ── μ±„νŒ…μš© 무료 λͺ¨λΈ λͺ©λ‘ μΊμ‹œ (ν…μŠ€νŠΈ μ „μš© / 이미지 지원 각각) ────────────
30
  # OpenRouter 곡식 λ¬Έμ„œ(openrouter.ai/docs/guides/overview/models)에 λ”°λ₯΄λ©΄
31
  # /api/v1/models 응닡은 OpenRouter μͺ½μ—μ„œλ„ edge에 μΊμ‹±λ˜μ–΄ μ œκ³΅λ˜λ―€λ‘œ,
32
  # 우리 μͺ½μ—μ„œλ„ λ§€ μš”μ²­λ§ˆλ‹€ μ‘°νšŒν•˜μ§€ μ•Šκ³  일정 μ‹œκ°„(TTL) λ™μ•ˆ μž¬μ‚¬μš©ν•©λ‹ˆλ‹€.
33
+ _models_cache: dict = {
34
+ "text": {"models": [], "fetched_at": 0.0},
35
+ "vision": {"models": [], "fetched_at": 0.0},
36
+ }
37
  _CACHE_TTL_SECONDS = 60 * 60 # 1μ‹œκ°„
38
 
39
 
40
+ def _fetch_chat_capable_free_models(require_image: bool) -> list:
41
  """OpenRouter 곡식 API(/api/v1/models)λ₯Ό ν˜ΈμΆœν•΄, μ§€κΈˆ 이 μˆœκ°„
42
+ μ‹€μ œ 'λŒ€ν™” 응닡'을 μƒμ„±ν•˜λŠ” 무료(:free) λͺ¨λΈ λͺ©λ‘μ„ κ°€μ Έμ˜΅λ‹ˆλ‹€.
43
+ κ²€μ—΄/μž¬λž­ν‚Ή/μž„λ² λ”© λ“± λΉ„μ±„νŒ… λͺ©μ  λͺ¨λΈμ€ 항상 μ œμ™Έν•˜λ©°,
44
+ require_image=True이면 이미지 μž…λ ₯κΉŒμ§€ μ§€μ›ν•˜λŠ” λͺ¨λΈλ§Œ λ‚¨κΉλ‹ˆλ‹€.
45
  """
46
  resp = requests.get("https://openrouter.ai/api/v1/models", timeout=10)
47
  resp.raise_for_status()
 
52
  model_id = m.get("id", "")
53
  if not model_id.endswith(":free"):
54
  continue
55
+ if any(kw in model_id.lower() for kw in _NON_CHAT_MODEL_KEYWORDS):
56
+ continue
57
  modalities = m.get("architecture", {}).get("input_modalities", [])
58
+ if require_image and "image" not in modalities:
59
+ continue
60
+ result.append(model_id)
61
  return result
62
 
63
 
64
+ def get_chat_capable_free_models(require_image: bool = False, force_refresh: bool = False) -> list:
65
+ """μΊμ‹œλœ 'μ‹€μ œ λŒ€ν™”μš©' 무료 λͺ¨λΈ λͺ©λ‘μ„ λ°˜ν™˜ν•©λ‹ˆλ‹€.
66
  μΊμ‹œκ°€ μ—†κ±°λ‚˜(졜초 호좜), TTL(1μ‹œκ°„)이 μ§€λ‚¬κ±°λ‚˜, force_refresh=True이면
67
  OpenRouter APIλ₯Ό λ‹€μ‹œ ν˜ΈμΆœν•΄ κ°±μ‹ ν•©λ‹ˆλ‹€.
68
  """
69
+ cache_key = "vision" if require_image else "text"
70
+ cache = _models_cache[cache_key]
71
+ safety_net = _SAFETY_NET_VISION_MODELS if require_image else _SAFETY_NET_TEXT_MODELS
72
+
73
  now = time.time()
74
+ cache_expired = (now - cache["fetched_at"]) > _CACHE_TTL_SECONDS
75
 
76
+ if force_refresh or cache_expired or not cache["models"]:
77
  try:
78
+ fresh_models = _fetch_chat_capable_free_models(require_image)
79
  if fresh_models:
80
+ cache["models"] = fresh_models
81
+ cache["fetched_at"] = now
82
  except Exception:
83
  # 쑰회 μžμ²΄κ°€ μ‹€νŒ¨ν•˜λ©΄(λ„€νŠΈμ›Œν¬ 였λ₯˜ λ“±), κΈ°μ‘΄ μΊμ‹œκ°€ 있으면
84
  # κ·ΈλŒ€λ‘œ μ“°κ³ , μΊμ‹œμ‘°μ°¨ μ—†μœΌλ©΄ μ΅œν›„μ˜ μ•ˆμ „λ§ λͺ©λ‘μ„ μ‚¬μš©ν•©λ‹ˆλ‹€.
85
  pass
86
 
87
+ return cache["models"] or safety_net
88
 
89
 
90
  # ── OpenRouter ν΄λΌμ΄μ–ΈνŠΈ μ΄ˆκΈ°ν™” ─────────────────────────────────────────
 
220
  has_image = bool(files)
221
 
222
  def _resolve_request_target(force_refresh: bool = False):
223
+ # ν…μŠ€νŠΈ μ „μš©μ΄λ“  이미지 첨뢀든, "κ²€μ—΄/νŒμ • μ „μš© λͺ¨λΈ"이 μ‹€μ œ λŒ€ν™”
224
+ # 응닡 μžλ¦¬μ— λ½‘νžˆλŠ” 문제λ₯Ό 막기 μœ„ν•΄ 항상 ν•„ν„°λ§λœ μΊμ‹œ λͺ©λ‘μ„
225
+ # μ‚¬μš©ν•©λ‹ˆλ‹€. (openrouter/freeλ₯Ό κ·ΈλŒ€λ‘œ μ“°λ©΄, μ±„νŒ… λͺ©μ μ— λ§žμ§€ μ•ŠλŠ”
226
+ # λͺ¨λΈμ΄ λ¬΄μž‘μœ„λ‘œ 선택될 수 μžˆλ‹€λŠ” 것을 μ‹€μ œλ‘œ ν™•μΈν–ˆμŠ΅λ‹ˆλ‹€.)
227
+ candidate_models = get_chat_capable_free_models(
228
+ require_image=has_image, force_refresh=force_refresh
229
+ )
230
  # OpenRouter의 fallback κΈ°λŠ₯은 'models' 배열에 μ΅œλŒ€ 3κ°œκΉŒμ§€λ§Œ ν—ˆμš©ν•©λ‹ˆλ‹€.
231
  # (OpenRouter μ„œλ²„ 응닡 κΈ°μ€€: "'models' array must have 3 items or fewer.")
232
+ candidate_models = candidate_models[:3]
233
+ return candidate_models[0], {"models": candidate_models}
234
 
235
  request_model, extra_body = _resolve_request_target()
236
 
 
238
  try:
239
  stream = _call_openrouter_stream(request_model, messages, extra_body)
240
  except APIError as e:
241
+ # "지원 λͺ¨λΈμ„ λͺ» 찾음(404)" 였λ₯˜κ°€ λ‚˜λ©΄, μΊμ‹œκ°€ λ‚‘μ•˜μ„ κ°€λŠ₯성이
242
+ # μžˆμœΌλ―€λ‘œ κ°•μ œλ‘œ ν•œ 번 더 μƒˆλ‘œ μ‘°νšŒν•΄μ„œ λ”± ν•œ 번만 μž¬μ‹œλ„ν•©λ‹ˆλ‹€.
243
+ if getattr(e, "status_code", None) == 404:
 
244
  request_model, extra_body = _resolve_request_target(force_refresh=True)
245
  stream = _call_openrouter_stream(request_model, messages, extra_body)
246
  else:
 
278
  # ── UI λΉŒλ“œ 및 API κ°œμ„€ ──────────────────────────────────────────────────────
279
  with gr.Blocks() as demo:
280
  gr.Markdown(
281
+ "# πŸ€– GS-AI API Server (OpenRouter Mode)\n"
282
+ "무료 λͺ¨λΈ 쀑 μ±„νŒ…μ— μ ν•©ν•œ λͺ¨λΈμ„ μžλ™μœΌλ‘œ 골라 μ‘λ‹΅ν•©λ‹ˆλ‹€.\n\n"
283
  "ν…μŠ€νŠΈμ™€ ν•¨κ»˜ 이미지λ₯Ό μ²¨λΆ€ν•΄μ„œ μ§ˆλ¬Έν•˜μ‹€ 수 μžˆμŠ΅λ‹ˆλ‹€. "
284
+ "(단, κ·Έ μ‹œμ μ— 이용 κ°€λŠ₯ν•œ 무료 λͺ¨λΈμ΄ μ—†μœΌλ©΄ 였λ₯˜κ°€ λ‚  수 μžˆμŠ΅λ‹ˆλ‹€.)"
285
  )
286
 
287
  chatbot_ui = gr.Chatbot(height=480)