1"""AI Model Price API — Apify Actor エントリポイント.
2
3キーワード/プロバイダで LLM 価格を検索し、Dataset に出力する.
4データは OpenRouter 公式 API + LiteLLM OSS から run 時に取得し、
5KV store に 6 時間キャッシュする.
6"""
7from __future__ import annotations
8
9import time
10from typing import Any
11
12import httpx
13from apify import Actor
14
15LITELLM_URL = (
16 "https://raw.githubusercontent.com/BerriAI/litellm/main/"
17 "model_prices_and_context_window.json"
18)
19OPENROUTER_URL = "https://openrouter.ai/api/v1/models"
20CACHE_KEY = "all-models"
21CACHE_TTL_SECONDS = 6 * 3600
22
23
24async def _fetch_all(client: httpx.AsyncClient) -> list[dict[str, Any]]:
25 """OpenRouter + LiteLLM から全モデル価格を取得し、正規化して返す."""
26 models: dict[str, dict[str, Any]] = {}
27 now = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
28
29
30 try:
31 resp = await client.get(LITELLM_URL, timeout=30)
32 resp.raise_for_status()
33 for model_id, info in resp.json().items():
34 if not isinstance(info, dict):
35 continue
36 provider = model_id.split("/")[0] if "/" in model_id else "unknown"
37 in_cost = info.get("input_cost_per_token")
38 out_cost = info.get("output_cost_per_token")
39 cached_cost = info.get("cache_read_input_token_cost")
40 models[model_id] = {
41 "modelId": model_id,
42 "displayName": info.get("display_name") or model_id,
43 "provider": provider,
44 "contextLength": (
45 str(info["max_input_tokens"]) if info.get("max_input_tokens") else ""
46 ),
47 "inputPrice": str(round(in_cost * 1_000_000, 6)) if in_cost else "",
48 "outputPrice": str(round(out_cost * 1_000_000, 6)) if out_cost else "",
49 "cachedInputPrice": (
50 str(round(cached_cost * 1_000_000, 6)) if cached_cost else ""
51 ),
52 "source": "litellm",
53 "lastUpdated": now,
54 }
55 Actor.log.info(f"LiteLLM: {len(models)} models")
56 except Exception as exc:
57 Actor.log.warning(f"LiteLLM fetch failed: {exc}")
58
59
60 try:
61 resp = await client.get(OPENROUTER_URL, timeout=30)
62 resp.raise_for_status()
63 added = 0
64 for m in resp.json().get("data", []):
65 mid = m.get("id") or ""
66 if not mid or mid in models:
67 continue
68 pricing = m.get("pricing") or {}
69 provider = mid.split("/")[0] if "/" in mid else "unknown"
70 try:
71 prompt = float(pricing["prompt"]) if pricing.get("prompt") else 0.0
72 completion = float(pricing["completion"]) if pricing.get("completion") else 0.0
73 except (ValueError, TypeError):
74 prompt = completion = 0.0
75 models[mid] = {
76 "modelId": mid,
77 "displayName": m.get("name") or mid,
78 "provider": provider,
79 "contextLength": (
80 str(m["context_length"]) if m.get("context_length") else ""
81 ),
82 "inputPrice": str(round(prompt * 1_000_000, 6)) if prompt else "",
83 "outputPrice": str(round(completion * 1_000_000, 6)) if completion else "",
84 "cachedInputPrice": "",
85 "source": "openrouter",
86 "lastUpdated": now,
87 }
88 added += 1
89 Actor.log.info(f"OpenRouter: +{added} models (total {len(models)})")
90 except Exception as exc:
91 Actor.log.warning(f"OpenRouter fetch failed: {exc}")
92
93 return list(models.values())
94
95
96async def _get_models_cached() -> list[dict[str, Any]]:
97 """KV store に 6 時間キャッシュしつつ全モデル取得."""
98 cache = await Actor.get_value(CACHE_KEY)
99 if isinstance(cache, dict) and (time.time() - cache.get("fetched_at", 0)) < CACHE_TTL_SECONDS:
100 Actor.log.info("Cache hit")
101 return cache["models"]
102 async with httpx.AsyncClient() as client:
103 models = await _fetch_all(client)
104 await Actor.set_value(CACHE_KEY, {"fetched_at": time.time(), "models": models})
105 Actor.log.info(f"Fetched fresh: {len(models)} models")
106 return models
107
108
109async def main() -> None:
110 async with Actor:
111 run_input = await Actor.get_input() or {}
112 keyword = (run_input.get("keyword") or "").lower().strip()
113 provider = (run_input.get("provider") or "").lower().strip()
114 limit = int(run_input.get("limit") or 100)
115 limit = max(1, min(limit, 10000))
116
117 models = await _get_models_cached()
118
119 if keyword:
120 models = [
121 m for m in models
122 if keyword in m["modelId"].lower() or keyword in m["displayName"].lower()
123 ]
124 if provider:
125 models = [m for m in models if provider in m["provider"].lower()]
126
127 models.sort(key=lambda m: m["modelId"])
128 Actor.log.info(f"Output: {len(models)} models (keyword={keyword!r}, provider={provider!r})")
129
130 await Actor.push_data(models[:limit])
131
132
133if __name__ == "__main__":
134 import asyncio
135 asyncio.run(main())