Update app.py
Browse files
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 |
-
|
|
|
|
|
|
|
|
|
|
| 24 |
_CACHE_TTL_SECONDS = 60 * 60 # 1μκ°
|
| 25 |
|
| 26 |
|
| 27 |
-
def
|
| 28 |
"""OpenRouter 곡μ API(/api/v1/models)λ₯Ό νΈμΆν΄, μ§κΈ μ΄ μκ°
|
| 29 |
-
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
| 43 |
return result
|
| 44 |
|
| 45 |
|
| 46 |
-
def
|
| 47 |
-
"""μΊμλ
|
| 48 |
μΊμκ° μκ±°λ(μ΅μ΄ νΈμΆ), TTL(1μκ°)μ΄ μ§λ¬κ±°λ, force_refresh=Trueμ΄λ©΄
|
| 49 |
OpenRouter APIλ₯Ό λ€μ νΈμΆν΄ κ°±μ ν©λλ€.
|
| 50 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
now = time.time()
|
| 52 |
-
cache_expired = (now -
|
| 53 |
|
| 54 |
-
if force_refresh or cache_expired or not
|
| 55 |
try:
|
| 56 |
-
fresh_models =
|
| 57 |
if fresh_models:
|
| 58 |
-
|
| 59 |
-
|
| 60 |
except Exception:
|
| 61 |
# μ‘°ν μμ²΄κ° μ€ν¨νλ©΄(λ€νΈμν¬ μ€λ₯ λ±), κΈ°μ‘΄ μΊμκ° μμΌλ©΄
|
| 62 |
# κ·Έλλ‘ μ°κ³ , μΊμμ‘°μ°¨ μμΌλ©΄ μ΅νμ μμ λ§ λͺ©λ‘μ μ¬μ©ν©λλ€.
|
| 63 |
pass
|
| 64 |
|
| 65 |
-
return
|
| 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 |
-
|
| 202 |
-
|
| 203 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 204 |
# OpenRouterμ fallback κΈ°λ₯μ 'models' λ°°μ΄μ μ΅λ 3κ°κΉμ§λ§ νμ©ν©λλ€.
|
| 205 |
# (OpenRouter μλ² μλ΅ κΈ°μ€: "'models' array must have 3 items or fewer.")
|
| 206 |
-
|
| 207 |
-
return
|
| 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 |
-
#
|
| 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 |
-
|
|
|
|
| 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)
|