Spaces:
Running
Running
| import argparse | |
| import json | |
| import os | |
| import re | |
| import sys | |
| import urllib.request | |
| from pathlib import Path | |
| from typing import Any | |
| USABLE_MODELS_DATASET = "alchoholpad/litellm-usable-models" | |
| USABLE_MODEL_FILES = [ | |
| ("usable-models.json", "chat"), | |
| ("usable-image-models.json", "image"), | |
| ("usable-vision-models.json", "vision"), | |
| ] | |
| ENV_REF_RE = re.compile(r"os\.environ/([A-Za-z0-9_]+)") | |
| NUMBERED_ENV_SLOT_RE = re.compile(r"_\d+$") | |
| OPTIONAL_ENV_REFS = { | |
| "CLOUDFLARE_ACCOUNT_ID", | |
| "DATABASE_URL", | |
| "LITELLM_MASTER_KEY", | |
| "MODAL_API_BASE", | |
| } | |
| API_PROVIDER_ENVS = { | |
| "AGENTROUTER_API_KEY", | |
| "AIMLAPI_API_KEY", | |
| "ASSEMBLYAI_API_KEY", | |
| "BYTEZ_API_KEY", | |
| "CEREBRAS_API_KEY", | |
| "CHINAWHAPI_API_KEY", | |
| "CLOUDFLARE_API_TOKEN", | |
| "COHERE_API_KEY", | |
| "CONSOLECLOUD_API_KEY", | |
| "CREATEANYTHING_API_KEY", | |
| "DEEPGRAM_API_KEY", | |
| "DEEPSEEK_API_KEY", | |
| "DISCORD_TOKEN", | |
| "EDENAI_API_KEY", | |
| "ELECTRONHUB_API_KEY", | |
| "ELEVENLABS_API_KEY", | |
| "EXA_API_KEY", | |
| "GEMINI_API_KEY", | |
| "GENLABS_API_KEY", | |
| "GITHUB_API_KEY", | |
| "GROQ_API_KEY", | |
| "HUGGINGFACE_API_KEY", | |
| "HUGGINGFACE_API_KEY_1", | |
| "HUGGINGFACE_API_KEY_2", | |
| "IMAGEAPI_API_KEY", | |
| "INFERENCE_SH_API_KEY", | |
| "JINA_AI_API_KEY", | |
| "MAPLEFLOW_API_KEY", | |
| "MISTRAL_API_KEY", | |
| "MODAL_API_KEY", | |
| "MODELSLAB_API_KEY", | |
| "OLLAMA_API_KEY", | |
| "OPENAI_API_KEY", | |
| "OPENROUTER_API_KEY", | |
| "OPENROUTER_API_KEY_1", | |
| "OPENROUTER_API_KEY_2", | |
| "OPENROUTER_API_KEY_3", | |
| "OPENROUTER_API_KEY_4", | |
| "PLATFORMCOMFY_API_KEY", | |
| "POLLINATIONS_API_KEY", | |
| "POLLINATIONS_API_KEY_1", | |
| "PUTER_API_KEY", | |
| "SAMBANOVA_API_KEY", | |
| "STABLEHORDE_API_KEY", | |
| "STABLEHORDE_API_KEY_1", | |
| "TAVILY_API_KEY", | |
| "TWELVELABS_API_KEY", | |
| "VERCEL_AI_GATEWAY_API_KEY", | |
| "VOIDAI_API_KEY", | |
| "WORLDLABS_API_KEY", | |
| "XIAOMI_MIMO_API_KEY", | |
| "YOU_API_KEY", | |
| } | |
| PASS_THROUGH_ENDPOINTS: list[dict[str, Any]] = [ | |
| { | |
| "path": "/genlabs", | |
| "target": "https://api.genlabs.dev/deca/v1", | |
| "include_subpath": True, | |
| "headers": {"Authorization": "Bearer os.environ/GENLABS_API_KEY"}, | |
| }, | |
| { | |
| "path": "/inference-sh", | |
| "target": "https://api.inference.sh", | |
| "include_subpath": True, | |
| "headers": {"Authorization": "Bearer os.environ/INFERENCE_SH_API_KEY"}, | |
| }, | |
| { | |
| "path": "/tavily", | |
| "target": "https://api.tavily.com", | |
| "include_subpath": True, | |
| "headers": {"Authorization": "Bearer os.environ/TAVILY_API_KEY"}, | |
| }, | |
| { | |
| "path": "/exa", | |
| "target": "https://api.exa.ai", | |
| "include_subpath": True, | |
| "headers": {"x-api-key": "os.environ/EXA_API_KEY"}, | |
| }, | |
| { | |
| "path": "/discord", | |
| "target": "https://api.zukijourney.com/v1", | |
| "include_subpath": True, | |
| "headers": {"Authorization": "Bearer os.environ/DISCORD_TOKEN"}, | |
| }, | |
| { | |
| "path": "/worldlabs", | |
| "target": "https://api.worldlabs.ai", | |
| "include_subpath": True, | |
| "headers": {"WLT-Api-Key": "os.environ/WORLDLABS_API_KEY"}, | |
| }, | |
| { | |
| "path": "/twelvelabs", | |
| "target": "https://api.twelvelabs.io", | |
| "include_subpath": True, | |
| "headers": {"x-api-key": "os.environ/TWELVELABS_API_KEY"}, | |
| }, | |
| { | |
| "path": "/stablehorde", | |
| "target": "https://aihorde.net/api", | |
| "include_subpath": True, | |
| "headers": {"apikey": "os.environ/STABLEHORDE_API_KEY"}, | |
| }, | |
| { | |
| "path": "/you", | |
| "target": "https://ydc-index.io", | |
| "include_subpath": True, | |
| "headers": {"X-API-Key": "os.environ/YOU_API_KEY"}, | |
| }, | |
| { | |
| "path": "/modal", | |
| "target": "os.environ/MODAL_API_BASE", | |
| "include_subpath": True, | |
| "headers": {"Authorization": "Bearer os.environ/MODAL_API_KEY"}, | |
| }, | |
| ] | |
| def env(name: str) -> str: | |
| return os.environ.get(name, "").strip() | |
| def unique(values: list[str]) -> list[str]: | |
| seen: set[str] = set() | |
| out: list[str] = [] | |
| for value in values: | |
| if value in seen: | |
| continue | |
| seen.add(value) | |
| out.append(value) | |
| return out | |
| def env_variant_names( | |
| base: str, | |
| *, | |
| include_numbered: bool = True, | |
| include_named: bool = True, | |
| ) -> list[str]: | |
| names: list[str] = [] | |
| if env(base): | |
| names.append(base) | |
| if include_numbered: | |
| for index in range(1, 11): | |
| name = f"{base}_{index}" | |
| if env(name): | |
| names.append(name) | |
| if include_named and not NUMBERED_ENV_SLOT_RE.search(base): | |
| prefix = f"{base}_" | |
| for name in sorted(os.environ): | |
| if not name.startswith(prefix): | |
| continue | |
| suffix = name[len(prefix) :] | |
| if not suffix: | |
| continue | |
| if suffix.isdigit() and not include_numbered: | |
| continue | |
| if env(name): | |
| names.append(name) | |
| return unique(names) | |
| def env_names(base: str) -> list[str]: | |
| return env_variant_names(base) | |
| SECRET_TO_ENV = { | |
| "openrouter_ai": "OPENROUTER_API_KEY", | |
| "aistudio_google_com": "GEMINI_API_KEY", | |
| "huggingface_co": "HUGGINGFACE_API_KEY", | |
| "pollinations_ai": "POLLINATIONS_API_KEY", | |
| "discord_com": "DISCORD_TOKEN", | |
| "admin_mistral_ai": "MISTRAL_API_KEY", | |
| "aimlapi_com": "AIMLAPI_API_KEY", | |
| "api_stablehorde_net": "STABLEHORDE_API_KEY", | |
| "app_edenai_run": "EDENAI_API_KEY", | |
| "app_genlabs_dev": "GENLABS_API_KEY", | |
| "assemblyai_com": "ASSEMBLYAI_API_KEY", | |
| "chinawhapi_com": "CHINAWHAPI_API_KEY", | |
| "cloud_cerebras_ai": "CEREBRAS_API_KEY", | |
| "cloud_sambanova_ai": "SAMBANOVA_API_KEY", | |
| "console_deepgram_com": "DEEPGRAM_API_KEY", | |
| "console_groq_com": "GROQ_API_KEY", | |
| "dash_cloudflare_com": "CLOUDFLARE_API_TOKEN", | |
| "dashboard_cohere_com": "COHERE_API_KEY", | |
| "elevenlabs_io": "ELEVENLABS_API_KEY", | |
| "github_com": "GITHUB_API_KEY", | |
| "jina_ai": "JINA_AI_API_KEY", | |
| "imageapi_org": "IMAGEAPI_API_KEY", | |
| "mapleflow_io": "MAPLEFLOW_API_KEY", | |
| "modal_com": "MODAL_API_KEY", | |
| "modelslab_com": "MODELSLAB_API_KEY", | |
| "ollama_com": "OLLAMA_API_KEY", | |
| "pawan_krd": "Pawan_Krd", | |
| "platform_deepseek_com": "DEEPSEEK_API_KEY", | |
| "platform_openai_com": "OPENAI_API_KEY", | |
| "playground_electronhub_ai": "ELECTRONHUB_API_KEY", | |
| "stablehorde_net": "STABLEHORDE_API_KEY", | |
| "vercel_com": "VERCEL_AI_GATEWAY_API_KEY", | |
| "voidai_app": "VOIDAI_API_KEY", | |
| "xiaomimimo_com": "XIAOMI_MIMO_API_KEY", | |
| } | |
| def _load_domain_email_secrets(): | |
| """Map service_domain_email HF secrets to expected env var names.""" | |
| for name, value in list(os.environ.items()): | |
| if not value or name.startswith("HF_"): | |
| continue | |
| for prefix, env_var in SECRET_TO_ENV.items(): | |
| if name.startswith(prefix + "_") and name != env_var: | |
| if not os.environ.get(env_var): | |
| os.environ[env_var] = value | |
| break | |
| def catalog_env_names(api_key_env: str) -> list[str]: | |
| if NUMBERED_ENV_SLOT_RE.search(api_key_env): | |
| return [api_key_env] if env(api_key_env) else [] | |
| names = env_variant_names(api_key_env, include_numbered=False) | |
| # Also find domain_email suffixed vars (e.g. aistudio_google_com_fahadbinhussain001_gmail_com) | |
| prefix = api_key_env + "_" | |
| for name in sorted(os.environ): | |
| if name.startswith(prefix) and name != api_key_env and env(name): | |
| if name not in names: | |
| names.append(name) | |
| return unique(names) | |
| def download_usable_models() -> set[str]: | |
| """Download usable model IDs from the HF dataset.""" | |
| hf_token = os.environ.get("HF_TOKEN", "").strip() | |
| usable_ids: set[str] = set() | |
| for filename, label in USABLE_MODEL_FILES: | |
| url = f"https://huggingface.co/datasets/{USABLE_MODELS_DATASET}/resolve/main/{filename}" | |
| headers = {"User-Agent": "litellm-render-config/1.0"} | |
| if hf_token: | |
| headers["Authorization"] = f"Bearer {hf_token}" | |
| try: | |
| req = urllib.request.Request(url, headers=headers) | |
| with urllib.request.urlopen(req, timeout=30) as resp: | |
| data = json.loads(resp.read().decode()) | |
| ids = data.get("usable_model_ids", []) | |
| usable_ids.update(ids) | |
| print(f" Loaded {len(ids)} usable {label} models from dataset", file=sys.stderr) | |
| except Exception as e: | |
| print(f" Warning: could not fetch {filename}: {e}", file=sys.stderr) | |
| return usable_ids | |
| def load_usable_models(path: Path | None) -> set[str]: | |
| """Load usable model IDs from a local file or HF dataset.""" | |
| if path and path.exists(): | |
| data = json.loads(path.read_text(encoding="utf-8")) | |
| ids = data.get("usable_model_ids", []) | |
| if isinstance(ids, str): | |
| ids = ids.split() | |
| print(f"Loaded {len(ids)} usable models from {path}", file=sys.stderr) | |
| return set(ids) | |
| print("No local usable models file, fetching from HF dataset...", file=sys.stderr) | |
| return download_usable_models() | |
| def load_secrets(path: Path) -> int: | |
| payload = json.loads(path.read_text(encoding="utf-8-sig")) | |
| if not isinstance(payload, dict): | |
| raise ValueError(f"{path} must contain a JSON object.") | |
| loaded = 0 | |
| for name, value in payload.items(): | |
| if not isinstance(name, str) or not isinstance(value, str) or not value: | |
| continue | |
| os.environ[name] = value | |
| loaded += 1 | |
| return loaded | |
| def yaml_quote(value: str) -> str: | |
| escaped = value.replace("\\", "\\\\").replace('"', '\\"') | |
| return f'"{escaped}"' | |
| def render_scalar(value: Any) -> str: | |
| if isinstance(value, bool): | |
| return "true" if value else "false" | |
| if isinstance(value, (int, float)): | |
| return str(value) | |
| if value is None: | |
| return "null" | |
| return yaml_quote(str(value)) | |
| def render_yaml(value: Any, indent: int = 0) -> list[str]: | |
| prefix = " " * indent | |
| if isinstance(value, dict): | |
| if not value: | |
| return [f"{prefix}{{}}"] | |
| lines: list[str] = [] | |
| for key, child in value.items(): | |
| if isinstance(child, (dict, list)): | |
| lines.append(f"{prefix}{key}:") | |
| lines.extend(render_yaml(child, indent + 2)) | |
| else: | |
| lines.append(f"{prefix}{key}: {render_scalar(child)}") | |
| return lines | |
| if isinstance(value, list): | |
| if not value: | |
| return [f"{prefix}[]"] | |
| lines = [] | |
| for item in value: | |
| if isinstance(item, dict): | |
| if not item: | |
| lines.append(f"{prefix}- {{}}") | |
| continue | |
| first = True | |
| for key, child in item.items(): | |
| marker = "- " if first else " " | |
| if isinstance(child, (dict, list)): | |
| lines.append(f"{prefix}{marker}{key}:") | |
| lines.extend(render_yaml(child, indent + 4)) | |
| else: | |
| lines.append(f"{prefix}{marker}{key}: {render_scalar(child)}") | |
| first = False | |
| elif isinstance(item, list): | |
| lines.append(f"{prefix}-") | |
| lines.extend(render_yaml(item, indent + 2)) | |
| else: | |
| lines.append(f"{prefix}- {render_scalar(item)}") | |
| return lines | |
| return [f"{prefix}{render_scalar(value)}"] | |
| def add_model( | |
| models: list[dict[str, Any]], | |
| alias: str, | |
| model: str, | |
| model_info: dict[str, Any] | None = None, | |
| **params: str, | |
| ) -> None: | |
| litellm_params = {"model": model} | |
| litellm_params.update({key: value for key, value in params.items() if value is not None}) | |
| entry: dict[str, Any] = {"model_name": alias, "litellm_params": litellm_params} | |
| if model_info: | |
| entry["model_info"] = model_info | |
| models.append(entry) | |
| def suffixed(alias: str, index: int, total: int) -> str: | |
| return alias if total == 1 else f"{alias}-{index}" | |
| def build_legacy_models() -> list[dict[str, Any]]: | |
| models: list[dict[str, Any]] = [] | |
| gemini_keys = env_names("GEMINI_API_KEY") + env_names("CONSOLECLOUD_API_KEY") | |
| for index, key_name in enumerate(gemini_keys, start=1): | |
| suffix_total = len(gemini_keys) | |
| add_model( | |
| models, | |
| suffixed("gemini-flash", index, suffix_total), | |
| "gemini/gemini-2.5-flash", | |
| api_key=f"os.environ/{key_name}", | |
| ) | |
| add_model( | |
| models, | |
| suffixed("gemini-flash-lite", index, suffix_total), | |
| "gemini/gemini-2.5-flash-lite", | |
| api_key=f"os.environ/{key_name}", | |
| ) | |
| add_model( | |
| models, | |
| suffixed("gemini-pro", index, suffix_total), | |
| "gemini/gemini-2.5-pro", | |
| api_key=f"os.environ/{key_name}", | |
| ) | |
| openrouter_base = env("OPENROUTER_API_BASE_URL") or "https://openrouter.ai/api/v1" | |
| openrouter_keys = env_names("OPENROUTER_API_KEY") | |
| for index, key_name in enumerate(openrouter_keys, start=1): | |
| add_model( | |
| models, | |
| suffixed("openrouter-auto", index, len(openrouter_keys)), | |
| "openrouter/auto", | |
| api_key=f"os.environ/{key_name}", | |
| api_base=openrouter_base, | |
| ) | |
| openai_keys = env_names("OPENAI_API_KEY") | |
| for index, key_name in enumerate(openai_keys, start=1): | |
| add_model( | |
| models, | |
| suffixed("openai-fast", index, len(openai_keys)), | |
| "openai/gpt-4o-mini", | |
| api_key=f"os.environ/{key_name}", | |
| ) | |
| anthropic_keys = env_names("ANTHROPIC_API_KEY") | |
| for index, key_name in enumerate(anthropic_keys, start=1): | |
| add_model( | |
| models, | |
| suffixed("claude-haiku", index, len(anthropic_keys)), | |
| "anthropic/claude-3-5-haiku-latest", | |
| api_key=f"os.environ/{key_name}", | |
| ) | |
| custom_base = env("CUSTOM_OPENAI_API_BASE") | |
| if custom_base: | |
| custom_alias = env("CUSTOM_OPENAI_ALIAS") or "custom-openai" | |
| custom_model = env("CUSTOM_OPENAI_MODEL") or "gpt-4o-mini" | |
| params = {"api_base": custom_base} | |
| if env("CUSTOM_OPENAI_API_KEY"): | |
| params["api_key"] = "os.environ/CUSTOM_OPENAI_API_KEY" | |
| add_model(models, custom_alias, f"openai/{custom_model}", **params) | |
| return models | |
| def default_catalog_path(template_path: Path) -> Path: | |
| return template_path.resolve().parent / "model-catalog.json" | |
| def load_model_catalog(path: Path, usable_ids: set[str] | None = None) -> list[dict[str, Any]]: | |
| catalog = json.loads(path.read_text(encoding="utf-8")) | |
| if catalog.get("version") != 1: | |
| raise ValueError(f"Unsupported model catalog version in {path}") | |
| # Build a set of usable suffixes for matching (strip provider prefix from usable IDs) | |
| usable_suffixes: set[str] | None = None | |
| if usable_ids is not None: | |
| usable_suffixes = set() | |
| for uid in usable_ids: | |
| if "/" in uid: | |
| usable_suffixes.add(uid.split("/", 1)[1]) | |
| else: | |
| usable_suffixes.add(uid) | |
| usable_suffixes.update(usable_ids) | |
| models: list[dict[str, Any]] = [] | |
| seen_models: set[str] = set() # Track added model IDs to deduplicate | |
| for group in catalog.get("groups", []): | |
| api_key_env = group.get("api_key_env") | |
| literal_api_key = group.get("literal_api_key") | |
| env_slots: list[str | None] | |
| if api_key_env: | |
| env_slots = catalog_env_names(str(api_key_env)) | |
| if not env_slots: | |
| continue | |
| else: | |
| env_slots = [None] | |
| for env_slot in env_slots: | |
| params = dict(group.get("params") or {}) | |
| if env_slot: | |
| params["api_key"] = f"os.environ/{env_slot}" | |
| elif literal_api_key is not None: | |
| params["api_key"] = literal_api_key | |
| for suffix in group.get("suffixes", []): | |
| if isinstance(suffix, dict): | |
| alias_suffix = suffix["alias"] | |
| model_suffix = suffix["model"] | |
| else: | |
| alias_suffix = str(suffix) | |
| model_suffix = str(suffix) | |
| # Filter by usable models if list is provided | |
| if usable_suffixes is not None: | |
| full_model_id = f"{group['model_prefix']}/{model_suffix}" | |
| if (full_model_id not in usable_suffixes and | |
| model_suffix not in usable_suffixes): | |
| continue | |
| # Deduplicate: skip if this model ID was already added | |
| model_id = f"{group['model_prefix']}/{model_suffix}" | |
| if model_id in seen_models: | |
| continue | |
| seen_models.add(model_id) | |
| add_model( | |
| models, | |
| f"{group['alias_prefix']}/{alias_suffix}", | |
| f"{group['model_prefix']}/{model_suffix}", | |
| model_info=group.get("model_info"), | |
| **params, | |
| ) | |
| return models | |
| def render_models(models: list[dict[str, Any]]) -> str: | |
| if not models: | |
| return " []" | |
| return "\n".join(render_yaml(models, indent=2)) | |
| def render_general_settings() -> str: | |
| settings: dict[str, Any] = { | |
| "pass_through_endpoints": PASS_THROUGH_ENDPOINTS, | |
| } | |
| if env("LITELLM_MASTER_KEY"): | |
| settings = {"master_key": "os.environ/LITELLM_MASTER_KEY", **settings} | |
| if env("DATABASE_URL"): | |
| settings["database_url"] = "os.environ/DATABASE_URL" | |
| return "\n".join(render_yaml(settings, indent=2)) | |
| def render_template(template: str, models: list[dict[str, Any]]) -> str: | |
| rendered = template | |
| if "__AUTO_MODEL_LIST__" in rendered: | |
| rendered = rendered.replace("__AUTO_MODEL_LIST__", render_models(models)) | |
| if "__GENERAL_SETTINGS__" in rendered: | |
| rendered = rendered.replace("__GENERAL_SETTINGS__", render_general_settings()) | |
| return rendered | |
| def env_refs(text: str) -> set[str]: | |
| return set(ENV_REF_RE.findall(text)) | |
| def is_api_provider_env_ref(name: str) -> bool: | |
| if name in API_PROVIDER_ENVS: | |
| return True | |
| for base in API_PROVIDER_ENVS: | |
| if not NUMBERED_ENV_SLOT_RE.search(base) and name.startswith(f"{base}_"): | |
| return True | |
| return False | |
| def main() -> int: | |
| parser = argparse.ArgumentParser() | |
| parser.add_argument("template", type=Path) | |
| parser.add_argument("output", type=Path) | |
| parser.add_argument( | |
| "--catalog", | |
| type=Path, | |
| help="Model catalog JSON to expand into model_list. Defaults to config/model-catalog.json beside the template.", | |
| ) | |
| parser.add_argument( | |
| "--usable-models", | |
| type=Path, | |
| default=None, | |
| help="Path to usable-models.json. If not provided, fetches from HF dataset. Use --no-usable-models to disable filtering.", | |
| ) | |
| parser.add_argument( | |
| "--no-usable-models", | |
| action="store_true", | |
| help="Disable usable models filtering (load all models from catalog).", | |
| ) | |
| parser.add_argument( | |
| "--include-legacy-aliases", | |
| action="store_true", | |
| help="Also add the old short aliases such as gemini-flash and openai-fast.", | |
| ) | |
| parser.add_argument( | |
| "--secrets", | |
| type=Path, | |
| default=Path(os.environ["LITELLM_SECRETS_FILE"]) if os.environ.get("LITELLM_SECRETS_FILE") else None, | |
| help="Optional local JSON secret file. Values are loaded only into this process.", | |
| ) | |
| parser.add_argument("--strict-env", action="store_true") | |
| parser.add_argument("--summary-json", action="store_true") | |
| args = parser.parse_args() | |
| secrets_loaded = 0 | |
| if args.secrets: | |
| secrets_loaded = load_secrets(args.secrets) | |
| _load_domain_email_secrets() | |
| # Load usable models filter | |
| usable_ids: set[str] | None = None | |
| if not args.no_usable_models: | |
| usable_ids = load_usable_models(args.usable_models) | |
| catalog_path = args.catalog or default_catalog_path(args.template) | |
| models = load_model_catalog(catalog_path, usable_ids=usable_ids) | |
| if args.include_legacy_aliases: | |
| models.extend(build_legacy_models()) | |
| template = args.template.read_text(encoding="utf-8") | |
| rendered = render_template(template, models) | |
| args.output.parent.mkdir(parents=True, exist_ok=True) | |
| args.output.write_text(rendered, encoding="utf-8") | |
| refs = env_refs(rendered) | |
| present = {name for name in refs if env(name)} | |
| missing_required = sorted(refs - present - OPTIONAL_ENV_REFS) | |
| missing_optional = sorted((refs - present) & OPTIONAL_ENV_REFS) | |
| api_refs = sorted(name for name in refs if is_api_provider_env_ref(name)) | |
| api_present = sorted(set(api_refs) & present) | |
| missing_api_refs = sorted(set(api_refs) - present) | |
| summary = { | |
| "template": str(args.template), | |
| "catalog": str(catalog_path), | |
| "output": str(args.output), | |
| "secretsLoaded": secrets_loaded, | |
| "usableModelsFilter": usable_ids is not None, | |
| "usableModelCount": len(usable_ids) if usable_ids else 0, | |
| "models": len(models), | |
| "apiProviderEnvRefs": len(api_refs), | |
| "apiProviderEnvRefsPresent": len(api_present), | |
| "missingApiProviderEnvRefs": missing_api_refs, | |
| "envRefs": len(refs), | |
| "envRefsPresent": len(present), | |
| "missingRequired": missing_required, | |
| "missingOptional": missing_optional, | |
| } | |
| if args.summary_json: | |
| print(json.dumps(summary, indent=2), file=sys.stderr) | |
| else: | |
| filter_info = f", usable filter={len(usable_ids)}" if usable_ids else "" | |
| print( | |
| "Rendered LiteLLM config " | |
| f"({len(models)} models{filter_info}, {len(api_refs)} API provider env refs, " | |
| f"{len(api_present)} API provider env refs present, {len(refs)} total env refs, " | |
| f"{len(missing_required)} required missing).", | |
| file=sys.stderr, | |
| ) | |
| if args.strict_env and missing_required: | |
| return 1 | |
| return 0 | |
| if __name__ == "__main__": | |
| raise SystemExit(main()) | |