diff --git a/scripts/sync-litellm-models.py b/scripts/sync-litellm-models.py index 52758d1..f25af21 100755 --- a/scripts/sync-litellm-models.py +++ b/scripts/sync-litellm-models.py @@ -88,6 +88,14 @@ def fetch_json(url: str, api_key: str | None): return json.loads(resp.read().decode("utf-8", errors="replace")) +def fetch_model_detail(base_root: str, model_id: str, api_key: str | None): + from urllib.parse import quote + try: + return fetch_json(f"{base_root}/models/{quote(model_id, safe='')}", api_key) + except Exception: + return None + + def fetch_models_and_info(base_url: str, api_key: str | None): url = normalize_base(base_url) if not url: @@ -115,12 +123,19 @@ def fetch_models_and_info(base_url: str, api_key: str | None): model_ids.append(mid) model_rows[mid] = row - model_info = None + model_info = {} info_error = None - try: - model_info = fetch_json(f"{normalize_base(base_url)}/model/info", api_key) - except Exception as e: # best effort - info_error = str(e) + base_root = normalize_base(base_url).removesuffix('/v1') + detail_errors = 0 + for mid in model_ids: + detail = fetch_model_detail(base_root, mid, api_key) + if isinstance(detail, dict): + model_info[mid] = detail + else: + detail_errors += 1 + + if detail_errors: + info_error = f"model detail unavailable for {detail_errors}/{len(model_ids)} models" return model_ids, model_rows, model_info, info_error @@ -182,6 +197,39 @@ def metadata_from_litellm(model_id: str, model_rows: dict[str, dict[str, Any]], return out +def official_alias_metadata(model_id: str, official_models: dict[str, Any]) -> dict[str, Any]: + direct = official_models.get(model_id) + if isinstance(direct, dict): + return direct + + if model_id.startswith('copilot-'): + base = model_id[len('copilot-'):] + base_meta = official_models.get(base) + if isinstance(base_meta, dict): + out = dict(base_meta) + out['source'] = f"alias:{base_meta.get('source', 'official')}" + return out + + alias_map = { + 'gemini-flash-latest': 'gemini-2.5-flash', + 'gemini-flash-lite-latest': 'gemini-2.5-flash-lite', + 'gemini-pro-latest': 'gemini-2.5-pro', + 'gemini-3-flash-preview': 'gemini-2.5-flash', + 'gemini-3-pro-preview': 'gemini-2.5-pro', + 'gemini-3.1-pro-preview': 'gemini-2.5-pro', + 'gpt-5.1-codex-max': 'gpt-5.1-codex', + } + base = alias_map.get(model_id) + if base: + base_meta = official_models.get(base) + if isinstance(base_meta, dict): + out = dict(base_meta) + out['source'] = f"alias:{base_meta.get('source', 'official')}" + return out + + return {} + + def merge_metadata(existing: dict[str, Any], official: dict[str, Any], litellm_meta: dict[str, Any], model_id: str) -> tuple[dict[str, Any], str]: merged = dict(existing) merged["id"] = model_id @@ -214,13 +262,15 @@ def merge_metadata(existing: dict[str, Any], official: dict[str, Any], litellm_m def build_sync_report(models: list[dict[str, Any]], official_meta: dict[str, Any]): fallback = [m["id"] for m in models if m.get("metadataSource") == "fallback-default"] from_official = [m["id"] for m in models if str(m.get("metadataSource", "")).startswith("official-")] - missing_official = [m["id"] for m in models if m["id"] not in official_meta] + alias_derived = [m["id"] for m in models if str(m.get("metadataSource", "")).startswith("alias:")] + unresolved = [m["id"] for m in models if m.get("metadataSource") == "fallback-default"] return { "total": len(models), "officialCount": len(from_official), + "aliasDerivedCount": len(alias_derived), "fallbackCount": len(fallback), "fallbackModels": fallback, - "missingOfficialMetadata": missing_official, + "missingOfficialMetadata": unresolved, } @@ -260,7 +310,7 @@ def main(): new_models = [] for mid in model_ids: existing = dict(existing_by_id.get(mid, {})) - official = official_models.get(mid, {}) if isinstance(official_models.get(mid), dict) else {} + official = official_alias_metadata(mid, official_models) litellm_meta = metadata_from_litellm(mid, model_rows, model_info) merged, _ = merge_metadata(existing, official, litellm_meta, mid) new_models.append(merged)