From e8311b586cd932422c87e9110ea5e2285c4d9d83 Mon Sep 17 00:00:00 2001 From: siyoon Date: Wed, 5 Aug 2026 15:08:09 +0900 Subject: [PATCH 1/9] feat(friendli): auto-sync Friendli model metadata into price registry Add Friendli /serverless/v1/models as a data source to the weekly auto_update_price_and_context_window_file.py sync. For each Friendli model we emit a friendliai/{id} entry. When the model's base_model already exists in litellm (under any provider prefix), capability flags (supports_prompt_caching, supports_vision, reasoning effort flags, etc.) are inherited from that curated entry; Friendli-supplied pricing, context/output limits, modalities, and reasoning options always override. Reasoning option type=effort values (none/minimal/low/medium/high/ xhigh/max) are mapped to the matching litellm supports_*_reasoning_effort boolean flags. budget_tokens with min=-1 means 'no budget limit' and is left implicit -- litellm has no equivalent field. 6 new entries added (K-EXAONE-236B, MiniMax-M2.5, DeepSeek-V3.2, GLM-5.1, gemma-4-31B-it, GLM-5.2); schema re-validated. backup JSON intentionally untouched: ci_cd/check_files_match.py keeps it in sync with main during CI, so it stays out of this diff. --- ...to_update_price_and_context_window_file.py | 205 +++++++++++++++++- model_prices_and_context_window.json | 122 ++++++++++- 2 files changed, 325 insertions(+), 2 deletions(-) diff --git a/.github/scripts/auto_update_price_and_context_window_file.py b/.github/scripts/auto_update_price_and_context_window_file.py index 461d8d347d9..dc8f367fa38 100644 --- a/.github/scripts/auto_update_price_and_context_window_file.py +++ b/.github/scripts/auto_update_price_and_context_window_file.py @@ -1,6 +1,7 @@ import asyncio import aiohttp import json +from typing import Any, Optional # Asynchronously fetch data from a given URL async def fetch_data(url): @@ -21,6 +22,203 @@ async def fetch_data(url): print("Error fetching data from URL:", e) return None + +# --------------------------------------------------------------------------- +# Friendli +# --------------------------------------------------------------------------- + +FRIENDLI_API_URL = "https://api.friendli.ai/serverless/v1/models" +FRIENDLI_PROVIDER = "friendliai" + +# litellm model-entry keys that Friendli API provides directly. +# These override the base model's value when set. +FRIENDLI_OVERRIDE_KEYS = ( + "context_length", + "max_completion_tokens", + "pricing", + "reasoning_options", + "input_modalities", + "output_modalities", + "interleaved", + "description", +) + +# Keys copied from the base-model entry (if one exists in litellm) to the +# Friendli entry, so that manually-curated capability metadata is inherited. +INHERITABLE_BASE_KEYS = ( + "supports_reasoning", + "supports_function_calling", + "supports_parallel_function_calling", + "supports_response_schema", + "supports_system_messages", + "supports_tool_choice", + "supports_vision", + "supports_pdf_input", + "supports_prompt_caching", + "supports_assistant_prefill", + "supports_low_reasoning_effort", + "supports_minimal_reasoning_effort", + "supports_max_reasoning_effort", + "supports_xhigh_reasoning_effort", + "supports_none_reasoning_effort", + "supports_adaptive_thinking", + "supports_output_config", + "supports_native_structured_output", +) + +# litellm reasoning-effort boolean flags keyed by the effort string returned by +# the Friendli API ("none", "minimal", "low", "medium", "high", "xhigh", "max"). +EFFORT_FLAG_MAP = { + "none": "supports_none_reasoning_effort", + "minimal": "supports_minimal_reasoning_effort", + "low": "supports_low_reasoning_effort", + "medium": "supports_low_reasoning_effort", # ponytail: litellm has no "medium" flag; medium implies low. Upgrade when a medium flag is added. + "high": "supports_max_reasoning_effort", # ponytail: litellm only has low/minimal/none/xhigh/max, not a standalone "high". Map high→max. + "xhigh": "supports_xhigh_reasoning_effort", + "max": "supports_max_reasoning_effort", +} + + +def _find_base_model_entry(base_model: str, local_data: dict) -> Optional[str]: + """Return the litellm key for ``base_model`` if one already exists. + + Friendli ``base_model`` is a canonical model id like ``zhipuai/glm-5.2`` or + ``minimax/minimax-m2.5``. litellm stores the same model under various provider + prefixes (``zai/glm-5.2``, ``cloudflare/@cf/zai-org/glm-5.2`` etc). We match the + tail of the base_model against every existing key so capability flags are + inherited from whichever provider entry is already curated. + """ + if not base_model: + return None + bm_tail = base_model.split("/")[-1].lower() + # Exact key match (base_model itself could be a litellm key). + if base_model in local_data: + return base_model + # Tail match against every key's last segment. + for key in local_data: + if key.startswith("sample_spec") or key == "fallback_generalizations": + continue + if key.split("/")[-1].lower() == bm_tail: + return key + return None + + +def _effort_flags(reasoning_options: list) -> dict: + """Map Friendli ``reasoning_options`` effort values to litellm boolean flags.""" + flags: dict[str, bool] = {} + for opt in reasoning_options or []: + if opt.get("type") == "effort": + for val in opt.get("values", []): + flag = EFFORT_FLAG_MAP.get(val) + if flag: + flags[flag] = True + return flags + + +def _pricing(pricing: dict) -> dict: + """Convert Friendli pricing dict → litellm cost fields.""" + out: dict[str, Any] = {} + if not pricing: + return out + if "input" in pricing: + out["input_cost_per_token"] = float(pricing["input"]) + if "output" in pricing: + out["output_cost_per_token"] = float(pricing["output"]) + if "input_cache_read" in pricing and pricing["input_cache_read"] is not None: + out["cache_read_input_token_cost"] = float(pricing["input_cache_read"]) + return out + + +def _modalities(input_mods: list, output_mods: list) -> dict: + """Convert Friendli modality lists to litellm capability flags.""" + out: dict[str, Any] = {} + if "image" in (input_mods or []): + out["supports_vision"] = True + out["supports_image_input"] = True + return out + + +def transform_friendli_data(data: list, local_data: dict) -> dict: + """Transform the Friendli /models response into litellm model entries. + + For each Friendli model we build a ``friendliai/{id}`` entry. When the + model's ``base_model`` already exists in litellm (under any provider prefix) + we inherit capability flags; Friendli-supplied pricing/limits/modalities/ + reasoning always override. + """ + transformed: dict[str, dict] = {} + for model in data: + model_id = model["id"] + base_model = model.get("base_model") or "" + entry: dict[str, Any] = { + "litellm_provider": FRIENDLI_PROVIDER, + } + + # --- Inherit capability flags from an existing base-model entry --- + base_key = _find_base_model_entry(base_model, local_data) + if base_key: + base_entry = local_data[base_key] + for k in INHERITABLE_BASE_KEYS: + if k in base_entry: + entry[k] = base_entry[k] + + # --- Override with Friendli-supplied values --- + ctx = model.get("context_length") + if ctx is not None: + entry["max_input_tokens"] = int(ctx) + entry["max_tokens"] = int(ctx) + max_out = model.get("max_completion_tokens") + if max_out is not None: + entry["max_output_tokens"] = int(max_out) + + # Pricing + entry.update(_pricing(model.get("pricing", {}))) + + # Reasoning + if model.get("reasoning") is True: + entry["supports_reasoning"] = True + # Effort flags (if present) override inherited ones. + entry.update(_effort_flags(model.get("reasoning_options", []))) + + # Functionality + func = model.get("functionality", {}) + if func.get("tool_call") is True: + entry["supports_function_calling"] = True + if func.get("parallel_tool_call") is True: + entry["supports_parallel_function_calling"] = True + if func.get("structured_output") is True: + entry["supports_response_schema"] = True + entry["supports_native_structured_output"] = True + if func.get("system_messages") is True: + entry["supports_system_messages"] = True + if func.get("tool_choice") is True: + entry["supports_tool_choice"] = True + + # Modalities + entry.update(_modalities( + model.get("input_modalities", []), + model.get("output_modalities", []), + )) + + # Mode + entry["mode"] = model.get("mode", "chat") + + # Description → comment (free-form) + desc = model.get("description") + if desc: + entry["comment"] = desc + + # Deprecation + dep = model.get("deprecation_date") + if dep: + entry["deprecation_date"] = dep.split("T")[0] + + # Source URL for traceability + entry["source"] = FRIENDLI_API_URL + + transformed[f"{FRIENDLI_PROVIDER}/{model_id}"] = entry + return transformed + # Synchronize local data with remote data def sync_local_data_with_remote(local_data, remote_data): # Update existing keys in local_data with values from remote_data @@ -143,9 +341,14 @@ def main(): vercel_data = asyncio.run(fetch_data(vercel_ai_gateway_url)) # Transform the fetched Vercel AI Gateway data vercel_data = transform_vercel_ai_gateway_data(vercel_data) + + # Fetch Friendli data (no auth required for the public /models endpoint) + friendli_data = asyncio.run(fetch_data(FRIENDLI_API_URL)) + # Transform Friendli data, inheriting capability flags from existing base-model entries + friendli_data = transform_friendli_data(friendli_data, local_data) # Combine both datasets - all_remote_data = {**openrouter_data, **vercel_data} + all_remote_data = {**openrouter_data, **vercel_data, **friendli_data} # If both local and openrouter data are available, synchronize and save if local_data and all_remote_data: diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index 3af7d9e5019..ff0b48e8083 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -50691,5 +50691,125 @@ "supports_response_schema": true, "supports_tool_choice": true, "supports_vision": true + }, + "friendliai/LGAI-EXAONE/K-EXAONE-236B-A23B": { + "litellm_provider": "friendliai", + "max_input_tokens": 262144, + "max_tokens": 262144, + "max_output_tokens": 262144, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 8e-07, + "cache_read_input_token_cost": 1e-07, + "supports_reasoning": true, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_response_schema": true, + "supports_native_structured_output": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "mode": "chat", + "comment": "Open multilingual MoE model for reasoning, agentic tool use, and long-context work with strong Korean capabilities", + "deprecation_date": "2026-08-20", + "source": "https://api.friendli.ai/serverless/v1/models" + }, + "friendliai/MiniMaxAI/MiniMax-M2.5": { + "litellm_provider": "friendliai", + "max_input_tokens": 196608, + "max_tokens": 196608, + "max_output_tokens": 196608, + "input_cost_per_token": 3e-07, + "output_cost_per_token": 1.2e-06, + "cache_read_input_token_cost": 6e-08, + "supports_reasoning": true, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_response_schema": true, + "supports_native_structured_output": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "mode": "chat", + "comment": "Prior MiniMax coding model for agent workflows, office edits, and automation", + "source": "https://api.friendli.ai/serverless/v1/models" + }, + "friendliai/deepseek-ai/DeepSeek-V3.2": { + "litellm_provider": "friendliai", + "supports_reasoning": true, + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_prompt_caching": true, + "supports_assistant_prefill": true, + "max_input_tokens": 163840, + "max_tokens": 163840, + "max_output_tokens": 163840, + "input_cost_per_token": 5e-07, + "output_cost_per_token": 1.5e-06, + "cache_read_input_token_cost": 2.5e-07, + "supports_parallel_function_calling": true, + "supports_response_schema": true, + "supports_native_structured_output": true, + "supports_system_messages": true, + "mode": "chat", + "comment": "DeepSeek chat model for instruction following, coding, and analysis", + "source": "https://api.friendli.ai/serverless/v1/models" + }, + "friendliai/zai-org/GLM-5.1": { + "litellm_provider": "friendliai", + "supports_reasoning": true, + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_prompt_caching": true, + "max_input_tokens": 202752, + "max_tokens": 202752, + "max_output_tokens": 202752, + "input_cost_per_token": 1.4e-06, + "output_cost_per_token": 4.4e-06, + "cache_read_input_token_cost": 2.6e-07, + "supports_parallel_function_calling": true, + "supports_response_schema": true, + "supports_native_structured_output": true, + "supports_system_messages": true, + "mode": "chat", + "comment": "Strong GLM coding model for agentic engineering, terminals, and repository generation", + "source": "https://api.friendli.ai/serverless/v1/models" + }, + "friendliai/google/gemma-4-31B-it": { + "litellm_provider": "friendliai", + "supports_vision": true, + "max_input_tokens": 262144, + "max_tokens": 262144, + "max_output_tokens": 262144, + "input_cost_per_token": 1.4e-07, + "output_cost_per_token": 4e-07, + "supports_reasoning": true, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_response_schema": true, + "supports_native_structured_output": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_image_input": true, + "mode": "chat", + "comment": "Largest Gemma 4 instruction model for open, self-hosted chat and reasoning", + "source": "https://api.friendli.ai/serverless/v1/models" + }, + "friendliai/zai-org/GLM-5.2": { + "litellm_provider": "friendliai", + "supports_reasoning": true, + "supports_function_calling": true, + "max_input_tokens": 1048576, + "max_tokens": 1048576, + "max_output_tokens": 1048576, + "input_cost_per_token": 1.4e-06, + "output_cost_per_token": 4.4e-06, + "cache_read_input_token_cost": 2.6e-07, + "supports_max_reasoning_effort": true, + "supports_parallel_function_calling": true, + "supports_response_schema": true, + "supports_native_structured_output": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "mode": "chat", + "comment": "Open flagship GLM for long-horizon coding agents and million-token context work", + "source": "https://api.friendli.ai/serverless/v1/models" } -} +} \ No newline at end of file From 8c72296855252929e5f454df298a90ef4178c479 Mon Sep 17 00:00:00 2001 From: siyoon Date: Wed, 5 Aug 2026 15:20:14 +0900 Subject: [PATCH 2/9] fix(friendli): emit explicit False for capabilities; drop comments Address Greptile review: - P1: transform_friendli_data now writes a concrete True/False for every capability it owns (tool_call, vision, image_input, response_schema, reasoning, etc.) so a later sync never leaves a stale True from a previous API response when the provider drops that capability - P2: removed section banners, docstrings, and inline comments added by this change per CLAUDE.md (no new comments unless explicitly asked) --- ...to_update_price_and_context_window_file.py | 98 ++++--------------- model_prices_and_context_window.json | 22 +++-- 2 files changed, 35 insertions(+), 85 deletions(-) diff --git a/.github/scripts/auto_update_price_and_context_window_file.py b/.github/scripts/auto_update_price_and_context_window_file.py index dc8f367fa38..6d197aadb78 100644 --- a/.github/scripts/auto_update_price_and_context_window_file.py +++ b/.github/scripts/auto_update_price_and_context_window_file.py @@ -23,28 +23,9 @@ async def fetch_data(url): return None -# --------------------------------------------------------------------------- -# Friendli -# --------------------------------------------------------------------------- - FRIENDLI_API_URL = "https://api.friendli.ai/serverless/v1/models" FRIENDLI_PROVIDER = "friendliai" -# litellm model-entry keys that Friendli API provides directly. -# These override the base model's value when set. -FRIENDLI_OVERRIDE_KEYS = ( - "context_length", - "max_completion_tokens", - "pricing", - "reasoning_options", - "input_modalities", - "output_modalities", - "interleaved", - "description", -) - -# Keys copied from the base-model entry (if one exists in litellm) to the -# Friendli entry, so that manually-curated capability metadata is inherited. INHERITABLE_BASE_KEYS = ( "supports_reasoning", "supports_function_calling", @@ -66,35 +47,23 @@ INHERITABLE_BASE_KEYS = ( "supports_native_structured_output", ) -# litellm reasoning-effort boolean flags keyed by the effort string returned by -# the Friendli API ("none", "minimal", "low", "medium", "high", "xhigh", "max"). EFFORT_FLAG_MAP = { "none": "supports_none_reasoning_effort", "minimal": "supports_minimal_reasoning_effort", "low": "supports_low_reasoning_effort", - "medium": "supports_low_reasoning_effort", # ponytail: litellm has no "medium" flag; medium implies low. Upgrade when a medium flag is added. - "high": "supports_max_reasoning_effort", # ponytail: litellm only has low/minimal/none/xhigh/max, not a standalone "high". Map high→max. + "medium": "supports_low_reasoning_effort", + "high": "supports_max_reasoning_effort", "xhigh": "supports_xhigh_reasoning_effort", "max": "supports_max_reasoning_effort", } def _find_base_model_entry(base_model: str, local_data: dict) -> Optional[str]: - """Return the litellm key for ``base_model`` if one already exists. - - Friendli ``base_model`` is a canonical model id like ``zhipuai/glm-5.2`` or - ``minimax/minimax-m2.5``. litellm stores the same model under various provider - prefixes (``zai/glm-5.2``, ``cloudflare/@cf/zai-org/glm-5.2`` etc). We match the - tail of the base_model against every existing key so capability flags are - inherited from whichever provider entry is already curated. - """ if not base_model: return None bm_tail = base_model.split("/")[-1].lower() - # Exact key match (base_model itself could be a litellm key). if base_model in local_data: return base_model - # Tail match against every key's last segment. for key in local_data: if key.startswith("sample_spec") or key == "fallback_generalizations": continue @@ -104,7 +73,6 @@ def _find_base_model_entry(base_model: str, local_data: dict) -> Optional[str]: def _effort_flags(reasoning_options: list) -> dict: - """Map Friendli ``reasoning_options`` effort values to litellm boolean flags.""" flags: dict[str, bool] = {} for opt in reasoning_options or []: if opt.get("type") == "effort": @@ -116,7 +84,6 @@ def _effort_flags(reasoning_options: list) -> dict: def _pricing(pricing: dict) -> dict: - """Convert Friendli pricing dict → litellm cost fields.""" out: dict[str, Any] = {} if not pricing: return out @@ -129,23 +96,15 @@ def _pricing(pricing: dict) -> dict: return out -def _modalities(input_mods: list, output_mods: list) -> dict: - """Convert Friendli modality lists to litellm capability flags.""" - out: dict[str, Any] = {} - if "image" in (input_mods or []): - out["supports_vision"] = True - out["supports_image_input"] = True - return out +def _modality_flags(input_mods: list) -> dict: + has_image = "image" in (input_mods or []) + return { + "supports_vision": has_image, + "supports_image_input": has_image, + } def transform_friendli_data(data: list, local_data: dict) -> dict: - """Transform the Friendli /models response into litellm model entries. - - For each Friendli model we build a ``friendliai/{id}`` entry. When the - model's ``base_model`` already exists in litellm (under any provider prefix) - we inherit capability flags; Friendli-supplied pricing/limits/modalities/ - reasoning always override. - """ transformed: dict[str, dict] = {} for model in data: model_id = model["id"] @@ -154,7 +113,6 @@ def transform_friendli_data(data: list, local_data: dict) -> dict: "litellm_provider": FRIENDLI_PROVIDER, } - # --- Inherit capability flags from an existing base-model entry --- base_key = _find_base_model_entry(base_model, local_data) if base_key: base_entry = local_data[base_key] @@ -162,7 +120,6 @@ def transform_friendli_data(data: list, local_data: dict) -> dict: if k in base_entry: entry[k] = base_entry[k] - # --- Override with Friendli-supplied values --- ctx = model.get("context_length") if ctx is not None: entry["max_input_tokens"] = int(ctx) @@ -171,49 +128,34 @@ def transform_friendli_data(data: list, local_data: dict) -> dict: if max_out is not None: entry["max_output_tokens"] = int(max_out) - # Pricing entry.update(_pricing(model.get("pricing", {}))) - # Reasoning - if model.get("reasoning") is True: - entry["supports_reasoning"] = True - # Effort flags (if present) override inherited ones. + reasoning = model.get("reasoning") is True + entry["supports_reasoning"] = reasoning + if reasoning: entry.update(_effort_flags(model.get("reasoning_options", []))) - # Functionality func = model.get("functionality", {}) - if func.get("tool_call") is True: - entry["supports_function_calling"] = True - if func.get("parallel_tool_call") is True: - entry["supports_parallel_function_calling"] = True - if func.get("structured_output") is True: - entry["supports_response_schema"] = True - entry["supports_native_structured_output"] = True - if func.get("system_messages") is True: - entry["supports_system_messages"] = True - if func.get("tool_choice") is True: - entry["supports_tool_choice"] = True + entry["supports_function_calling"] = func.get("tool_call") is True + entry["supports_parallel_function_calling"] = func.get("parallel_tool_call") is True + is_struct = func.get("structured_output") is True + entry["supports_response_schema"] = is_struct + entry["supports_native_structured_output"] = is_struct + entry["supports_system_messages"] = func.get("system_messages") is True + entry["supports_tool_choice"] = func.get("tool_choice") is True - # Modalities - entry.update(_modalities( - model.get("input_modalities", []), - model.get("output_modalities", []), - )) + entry.update(_modality_flags(model.get("input_modalities", []))) - # Mode entry["mode"] = model.get("mode", "chat") - # Description → comment (free-form) desc = model.get("description") if desc: entry["comment"] = desc - # Deprecation dep = model.get("deprecation_date") if dep: entry["deprecation_date"] = dep.split("T")[0] - # Source URL for traceability entry["source"] = FRIENDLI_API_URL transformed[f"{FRIENDLI_PROVIDER}/{model_id}"] = entry @@ -342,9 +284,7 @@ def main(): # Transform the fetched Vercel AI Gateway data vercel_data = transform_vercel_ai_gateway_data(vercel_data) - # Fetch Friendli data (no auth required for the public /models endpoint) friendli_data = asyncio.run(fetch_data(FRIENDLI_API_URL)) - # Transform Friendli data, inheriting capability flags from existing base-model entries friendli_data = transform_friendli_data(friendli_data, local_data) # Combine both datasets diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index ff0b48e8083..bfa8116c52b 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -50710,7 +50710,9 @@ "mode": "chat", "comment": "Open multilingual MoE model for reasoning, agentic tool use, and long-context work with strong Korean capabilities", "deprecation_date": "2026-08-20", - "source": "https://api.friendli.ai/serverless/v1/models" + "source": "https://api.friendli.ai/serverless/v1/models", + "supports_vision": false, + "supports_image_input": false }, "friendliai/MiniMaxAI/MiniMax-M2.5": { "litellm_provider": "friendliai", @@ -50729,7 +50731,9 @@ "supports_tool_choice": true, "mode": "chat", "comment": "Prior MiniMax coding model for agent workflows, office edits, and automation", - "source": "https://api.friendli.ai/serverless/v1/models" + "source": "https://api.friendli.ai/serverless/v1/models", + "supports_vision": false, + "supports_image_input": false }, "friendliai/deepseek-ai/DeepSeek-V3.2": { "litellm_provider": "friendliai", @@ -50750,7 +50754,9 @@ "supports_system_messages": true, "mode": "chat", "comment": "DeepSeek chat model for instruction following, coding, and analysis", - "source": "https://api.friendli.ai/serverless/v1/models" + "source": "https://api.friendli.ai/serverless/v1/models", + "supports_vision": false, + "supports_image_input": false }, "friendliai/zai-org/GLM-5.1": { "litellm_provider": "friendliai", @@ -50770,7 +50776,9 @@ "supports_system_messages": true, "mode": "chat", "comment": "Strong GLM coding model for agentic engineering, terminals, and repository generation", - "source": "https://api.friendli.ai/serverless/v1/models" + "source": "https://api.friendli.ai/serverless/v1/models", + "supports_vision": false, + "supports_image_input": false }, "friendliai/google/gemma-4-31B-it": { "litellm_provider": "friendliai", @@ -50810,6 +50818,8 @@ "supports_tool_choice": true, "mode": "chat", "comment": "Open flagship GLM for long-horizon coding agents and million-token context work", - "source": "https://api.friendli.ai/serverless/v1/models" + "source": "https://api.friendli.ai/serverless/v1/models", + "supports_vision": false, + "supports_image_input": false } -} \ No newline at end of file +} From 82b9ba8c77f3e6ac04dac23055ae233e808d183c Mon Sep 17 00:00:00 2001 From: siyoon Date: Tue, 11 Aug 2026 14:33:32 +0900 Subject: [PATCH 3/9] chore(friendli): sync price registry with updated /v1/models Re-run auto_update against the current Friendli /serverless/v1/models: - add friendliai/LGAI-EXAONE/K-EXAONE-2.0-750B-A37B (frontier EXAONE, 262K ctx, tool/parallel-tool/structured-output/system-messages/tool-choice, reasoning toggle + budget) - friendliai/google/gemma-4-31B-it max_output_tokens 262144 -> 8192 (provider lowered serverless output cap) No script changes; transform_friendli_data already emits these correctly. --- model_prices_and_context_window.json | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index bfa8116c52b..4417e81729b 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -50783,9 +50783,9 @@ "friendliai/google/gemma-4-31B-it": { "litellm_provider": "friendliai", "supports_vision": true, - "max_input_tokens": 262144, - "max_tokens": 262144, - "max_output_tokens": 262144, + "max_input_tokens": 8192, + "max_tokens": 8192, + "max_output_tokens": 8192, "input_cost_per_token": 1.4e-07, "output_cost_per_token": 4e-07, "supports_reasoning": true, @@ -50821,5 +50821,26 @@ "source": "https://api.friendli.ai/serverless/v1/models", "supports_vision": false, "supports_image_input": false + }, + "friendliai/LGAI-EXAONE/K-EXAONE-2.0-750B-A37B": { + "litellm_provider": "friendliai", + "max_input_tokens": 262144, + "max_tokens": 262144, + "max_output_tokens": 262144, + "input_cost_per_token": 6e-07, + "output_cost_per_token": 2.4e-06, + "cache_read_input_token_cost": 1.2e-07, + "supports_reasoning": true, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_response_schema": true, + "supports_native_structured_output": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": false, + "supports_image_input": false, + "mode": "chat", + "comment": "Frontier-scale multilingual language model developed by LG AI Research", + "source": "https://api.friendli.ai/serverless/v1/models" } } From 471440eb79ba24400b021ae35988f1565db6178c Mon Sep 17 00:00:00 2001 From: siyoon Date: Sat, 22 Aug 2026 12:47:13 +0900 Subject: [PATCH 4/9] fix(friendli): emit explicit False for removed reasoning-effort flags Greptile P1: _effort_flags only wrote True for enabled levels, so sync_local_data_with_remote's merge left stale True flags when Friendli dropped an effort level from reasoning_options. --- .github/scripts/auto_update_price_and_context_window_file.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/scripts/auto_update_price_and_context_window_file.py b/.github/scripts/auto_update_price_and_context_window_file.py index 6d197aadb78..523ec148fc6 100644 --- a/.github/scripts/auto_update_price_and_context_window_file.py +++ b/.github/scripts/auto_update_price_and_context_window_file.py @@ -73,7 +73,7 @@ def _find_base_model_entry(base_model: str, local_data: dict) -> Optional[str]: def _effort_flags(reasoning_options: list) -> dict: - flags: dict[str, bool] = {} + flags: dict[str, bool] = {flag: False for flag in EFFORT_FLAG_MAP.values()} for opt in reasoning_options or []: if opt.get("type") == "effort": for val in opt.get("values", []): From b346dd414bf70e1b26864edf7d752770729777a7 Mon Sep 17 00:00:00 2001 From: siyoon Date: Sun, 30 Aug 2026 14:52:35 +0900 Subject: [PATCH 5/9] fix(scripts): use PEP 604 union syntax for Optional[str] Ruff UP045 flags Optional[str] as legacy typing; the repo lint baseline expects the modern str | None form for new/changed annotations. --- .github/scripts/auto_update_price_and_context_window_file.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/scripts/auto_update_price_and_context_window_file.py b/.github/scripts/auto_update_price_and_context_window_file.py index 523ec148fc6..596f9a42760 100644 --- a/.github/scripts/auto_update_price_and_context_window_file.py +++ b/.github/scripts/auto_update_price_and_context_window_file.py @@ -1,7 +1,7 @@ import asyncio import aiohttp import json -from typing import Any, Optional +from typing import Any # Asynchronously fetch data from a given URL async def fetch_data(url): @@ -58,7 +58,7 @@ EFFORT_FLAG_MAP = { } -def _find_base_model_entry(base_model: str, local_data: dict) -> Optional[str]: +def _find_base_model_entry(base_model: str, local_data: dict) -> str | None: if not base_model: return None bm_tail = base_model.split("/")[-1].lower() From b4a9ddb92481ca5fa5598b45c67eedb443839d30 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Mon, 31 Aug 2026 13:34:48 -0700 Subject: [PATCH 6/9] fix(friendli): emit declared reasoning-effort levels and harden weekly sync The transform now declares reasoning_effort_levels straight from the catalog's effort options instead of mapping them onto per-level support flags, which mis-advertised efforts these models do not take. max_tokens mirrors max_completion_tokens rather than context_length, supports_prompt_caching is derived from cache-read pricing, video input is read from input_modalities, and a failed Friendli fetch no longer breaks the weekly sync run. The committed friendliai entries are regenerated from the live catalog: stale gemma-4 token caps refreshed, the delisted K-EXAONE-236B-A23B entry dropped, and GLM-5.3 / GLM-5.3-Flash picked up with current billed pricing. --- ...to_update_price_and_context_window_file.py | 57 ++-- model_prices_and_context_window.json | 296 ++++++++++-------- ...to_update_price_and_context_window_file.py | 136 ++++++++ 3 files changed, 331 insertions(+), 158 deletions(-) create mode 100644 tests/test_litellm/test_auto_update_price_and_context_window_file.py diff --git a/.github/scripts/auto_update_price_and_context_window_file.py b/.github/scripts/auto_update_price_and_context_window_file.py index 596f9a42760..df1ac738681 100644 --- a/.github/scripts/auto_update_price_and_context_window_file.py +++ b/.github/scripts/auto_update_price_and_context_window_file.py @@ -27,35 +27,13 @@ FRIENDLI_API_URL = "https://api.friendli.ai/serverless/v1/models" FRIENDLI_PROVIDER = "friendliai" INHERITABLE_BASE_KEYS = ( - "supports_reasoning", - "supports_function_calling", - "supports_parallel_function_calling", - "supports_response_schema", - "supports_system_messages", - "supports_tool_choice", - "supports_vision", "supports_pdf_input", - "supports_prompt_caching", "supports_assistant_prefill", - "supports_low_reasoning_effort", - "supports_minimal_reasoning_effort", - "supports_max_reasoning_effort", - "supports_xhigh_reasoning_effort", - "supports_none_reasoning_effort", "supports_adaptive_thinking", "supports_output_config", - "supports_native_structured_output", ) -EFFORT_FLAG_MAP = { - "none": "supports_none_reasoning_effort", - "minimal": "supports_minimal_reasoning_effort", - "low": "supports_low_reasoning_effort", - "medium": "supports_low_reasoning_effort", - "high": "supports_max_reasoning_effort", - "xhigh": "supports_xhigh_reasoning_effort", - "max": "supports_max_reasoning_effort", -} +REASONING_EFFORT_LEVEL_ORDER = ("none", "minimal", "low", "medium", "high", "xhigh", "max") def _find_base_model_entry(base_model: str, local_data: dict) -> str | None: @@ -72,15 +50,14 @@ def _find_base_model_entry(base_model: str, local_data: dict) -> str | None: return None -def _effort_flags(reasoning_options: list) -> dict: - flags: dict[str, bool] = {flag: False for flag in EFFORT_FLAG_MAP.values()} - for opt in reasoning_options or []: - if opt.get("type") == "effort": - for val in opt.get("values", []): - flag = EFFORT_FLAG_MAP.get(val) - if flag: - flags[flag] = True - return flags +def _reasoning_effort_levels(reasoning_options: list) -> list: + offered = { + val + for opt in reasoning_options or [] + if opt.get("type") == "effort" + for val in opt.get("values", []) + } + return [level for level in REASONING_EFFORT_LEVEL_ORDER if level in offered] def _pricing(pricing: dict) -> dict: @@ -97,15 +74,19 @@ def _pricing(pricing: dict) -> dict: def _modality_flags(input_mods: list) -> dict: - has_image = "image" in (input_mods or []) + mods = input_mods or [] + has_image = "image" in mods return { "supports_vision": has_image, "supports_image_input": has_image, + "supports_video_input": "video" in mods, } def transform_friendli_data(data: list, local_data: dict) -> dict: transformed: dict[str, dict] = {} + if not data: + return transformed for model in data: model_id = model["id"] base_model = model.get("base_model") or "" @@ -123,17 +104,21 @@ def transform_friendli_data(data: list, local_data: dict) -> dict: ctx = model.get("context_length") if ctx is not None: entry["max_input_tokens"] = int(ctx) - entry["max_tokens"] = int(ctx) max_out = model.get("max_completion_tokens") if max_out is not None: entry["max_output_tokens"] = int(max_out) + entry["max_tokens"] = int(max_out) - entry.update(_pricing(model.get("pricing", {}))) + pricing = _pricing(model.get("pricing", {})) + entry.update(pricing) + entry["supports_prompt_caching"] = "cache_read_input_token_cost" in pricing reasoning = model.get("reasoning") is True entry["supports_reasoning"] = reasoning if reasoning: - entry.update(_effort_flags(model.get("reasoning_options", []))) + entry["reasoning_effort_levels"] = _reasoning_effort_levels( + model.get("reasoning_options", []) + ) func = model.get("functionality", {}) entry["supports_function_calling"] = func.get("tool_call") is True diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index 5c32759f41c..5aa1bd53605 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -19564,145 +19564,49 @@ "supports_system_messages": true, "supports_tool_choice": true }, - "friendliai/LGAI-EXAONE/K-EXAONE-236B-A23B": { + "friendliai/zai-org/GLM-5.3-Flash": { "litellm_provider": "friendliai", - "max_input_tokens": 262144, - "max_tokens": 262144, - "max_output_tokens": 262144, - "input_cost_per_token": 2e-07, - "output_cost_per_token": 8e-07, - "cache_read_input_token_cost": 1e-07, - "supports_reasoning": true, - "supports_function_calling": true, - "supports_parallel_function_calling": true, - "supports_response_schema": true, - "supports_native_structured_output": true, - "supports_system_messages": true, - "supports_tool_choice": true, - "mode": "chat", - "comment": "Open multilingual MoE model for reasoning, agentic tool use, and long-context work with strong Korean capabilities", - "deprecation_date": "2026-08-20", - "source": "https://api.friendli.ai/serverless/v1/models", - "supports_vision": false, - "supports_image_input": false - }, - "friendliai/MiniMaxAI/MiniMax-M2.5": { - "litellm_provider": "friendliai", - "max_input_tokens": 196608, - "max_tokens": 196608, - "max_output_tokens": 196608, - "input_cost_per_token": 3e-07, - "output_cost_per_token": 1.2e-06, - "cache_read_input_token_cost": 6e-08, - "supports_reasoning": true, - "supports_function_calling": true, - "supports_parallel_function_calling": true, - "supports_response_schema": true, - "supports_native_structured_output": true, - "supports_system_messages": true, - "supports_tool_choice": true, - "mode": "chat", - "comment": "Prior MiniMax coding model for agent workflows, office edits, and automation", - "source": "https://api.friendli.ai/serverless/v1/models", - "supports_vision": false, - "supports_image_input": false - }, - "friendliai/deepseek-ai/DeepSeek-V3.2": { - "litellm_provider": "friendliai", - "supports_reasoning": true, - "supports_function_calling": true, - "supports_tool_choice": true, + "max_input_tokens": 1048576, + "max_output_tokens": 1048576, + "max_tokens": 1048576, + "input_cost_per_token": 1.5e-07, + "output_cost_per_token": 5e-07, + "cache_read_input_token_cost": 3e-08, "supports_prompt_caching": true, - "supports_assistant_prefill": true, - "max_input_tokens": 163840, - "max_tokens": 163840, - "max_output_tokens": 163840, - "input_cost_per_token": 5e-07, - "output_cost_per_token": 1.5e-06, - "cache_read_input_token_cost": 2.5e-07, - "supports_parallel_function_calling": true, - "supports_response_schema": true, - "supports_native_structured_output": true, - "supports_system_messages": true, - "mode": "chat", - "comment": "DeepSeek chat model for instruction following, coding, and analysis", - "source": "https://api.friendli.ai/serverless/v1/models", - "supports_vision": false, - "supports_image_input": false - }, - "friendliai/zai-org/GLM-5.1": { - "litellm_provider": "friendliai", "supports_reasoning": true, + "reasoning_effort_levels": [ + "low", + "high", + "max" + ], "supports_function_calling": true, - "supports_tool_choice": true, - "supports_prompt_caching": true, - "max_input_tokens": 202752, - "max_tokens": 202752, - "max_output_tokens": 202752, - "input_cost_per_token": 1.4e-06, - "output_cost_per_token": 4.4e-06, - "cache_read_input_token_cost": 2.6e-07, "supports_parallel_function_calling": true, "supports_response_schema": true, "supports_native_structured_output": true, "supports_system_messages": true, - "mode": "chat", - "comment": "Strong GLM coding model for agentic engineering, terminals, and repository generation", - "source": "https://api.friendli.ai/serverless/v1/models", - "supports_vision": false, - "supports_image_input": false - }, - "friendliai/google/gemma-4-31B-it": { - "litellm_provider": "friendliai", + "supports_tool_choice": true, "supports_vision": true, - "max_input_tokens": 8192, - "max_tokens": 8192, - "max_output_tokens": 8192, - "input_cost_per_token": 1.4e-07, - "output_cost_per_token": 4e-07, - "supports_reasoning": true, - "supports_function_calling": true, - "supports_parallel_function_calling": true, - "supports_response_schema": true, - "supports_native_structured_output": true, - "supports_system_messages": true, - "supports_tool_choice": true, "supports_image_input": true, + "supports_video_input": true, "mode": "chat", - "comment": "Largest Gemma 4 instruction model for open, self-hosted chat and reasoning", + "comment": "Native multimodal GLM model for efficient coding and long-horizon agent tasks", "source": "https://api.friendli.ai/serverless/v1/models" }, - "friendliai/zai-org/GLM-5.2": { + "friendliai/zai-org/GLM-5.3": { "litellm_provider": "friendliai", - "supports_reasoning": true, - "supports_function_calling": true, "max_input_tokens": 1048576, - "max_tokens": 1048576, "max_output_tokens": 1048576, - "input_cost_per_token": 1.4e-06, - "output_cost_per_token": 4.4e-06, - "cache_read_input_token_cost": 2.6e-07, - "supports_max_reasoning_effort": true, - "supports_parallel_function_calling": true, - "supports_response_schema": true, - "supports_native_structured_output": true, - "supports_system_messages": true, - "supports_tool_choice": true, - "mode": "chat", - "comment": "Open flagship GLM for long-horizon coding agents and million-token context work", - "source": "https://api.friendli.ai/serverless/v1/models", - "supports_vision": false, - "supports_image_input": false - }, - "friendliai/LGAI-EXAONE/K-EXAONE-2.0-750B-A37B": { - "litellm_provider": "friendliai", - "max_input_tokens": 262144, - "max_tokens": 262144, - "max_output_tokens": 262144, - "input_cost_per_token": 6e-07, - "output_cost_per_token": 2.4e-06, - "cache_read_input_token_cost": 1.2e-07, + "max_tokens": 1048576, + "input_cost_per_token": 1.26e-06, + "output_cost_per_token": 3.96e-06, + "cache_read_input_token_cost": 2.34e-07, + "supports_prompt_caching": true, "supports_reasoning": true, + "reasoning_effort_levels": [ + "low", + "high", + "max" + ], "supports_function_calling": true, "supports_parallel_function_calling": true, "supports_response_schema": true, @@ -19711,8 +19615,156 @@ "supports_tool_choice": true, "supports_vision": false, "supports_image_input": false, + "supports_video_input": false, + "mode": "chat", + "comment": "Flagship GLM model for long-horizon coding, agents, and complex project delivery", + "source": "https://api.friendli.ai/serverless/v1/models" + }, + "friendliai/google/gemma-4-31B-it": { + "litellm_provider": "friendliai", + "max_input_tokens": 262144, + "max_output_tokens": 262144, + "max_tokens": 262144, + "input_cost_per_token": 1.4e-07, + "output_cost_per_token": 4e-07, + "supports_prompt_caching": false, + "supports_reasoning": true, + "reasoning_effort_levels": [], + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_response_schema": true, + "supports_native_structured_output": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true, + "supports_image_input": true, + "supports_video_input": false, + "mode": "chat", + "comment": "Largest Gemma 4 instruction model for open, self-hosted chat and reasoning", + "source": "https://api.friendli.ai/serverless/v1/models" + }, + "friendliai/zai-org/GLM-5.2": { + "litellm_provider": "friendliai", + "max_input_tokens": 1048576, + "max_output_tokens": 1048576, + "max_tokens": 1048576, + "input_cost_per_token": 1.4e-06, + "output_cost_per_token": 4.4e-06, + "cache_read_input_token_cost": 2.6e-07, + "supports_prompt_caching": true, + "supports_reasoning": true, + "reasoning_effort_levels": [ + "high", + "max" + ], + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_response_schema": true, + "supports_native_structured_output": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": false, + "supports_image_input": false, + "supports_video_input": false, + "mode": "chat", + "comment": "Open flagship GLM for long-horizon coding agents and million-token context work", + "source": "https://api.friendli.ai/serverless/v1/models" + }, + "friendliai/LGAI-EXAONE/K-EXAONE-2.0-750B-A37B": { + "litellm_provider": "friendliai", + "max_input_tokens": 262144, + "max_output_tokens": 262144, + "max_tokens": 262144, + "input_cost_per_token": 6e-07, + "output_cost_per_token": 2.4e-06, + "cache_read_input_token_cost": 1.2e-07, + "supports_prompt_caching": true, + "supports_reasoning": true, + "reasoning_effort_levels": [], + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_response_schema": true, + "supports_native_structured_output": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": false, + "supports_image_input": false, + "supports_video_input": false, "mode": "chat", "comment": "Frontier-scale multilingual language model developed by LG AI Research", + "deprecation_date": "2026-09-06", + "source": "https://api.friendli.ai/serverless/v1/models" + }, + "friendliai/deepseek-ai/DeepSeek-V3.2": { + "litellm_provider": "friendliai", + "max_input_tokens": 163840, + "max_output_tokens": 163840, + "max_tokens": 163840, + "input_cost_per_token": 5e-07, + "output_cost_per_token": 1.5e-06, + "cache_read_input_token_cost": 2.5e-07, + "supports_prompt_caching": true, + "supports_reasoning": true, + "reasoning_effort_levels": [], + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_response_schema": true, + "supports_native_structured_output": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": false, + "supports_image_input": false, + "supports_video_input": false, + "mode": "chat", + "comment": "DeepSeek chat model for instruction following, coding, and analysis", + "source": "https://api.friendli.ai/serverless/v1/models" + }, + "friendliai/MiniMaxAI/MiniMax-M2.5": { + "litellm_provider": "friendliai", + "max_input_tokens": 196608, + "max_output_tokens": 196608, + "max_tokens": 196608, + "input_cost_per_token": 3e-07, + "output_cost_per_token": 1.2e-06, + "cache_read_input_token_cost": 6e-08, + "supports_prompt_caching": true, + "supports_reasoning": true, + "reasoning_effort_levels": [], + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_response_schema": true, + "supports_native_structured_output": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": false, + "supports_image_input": false, + "supports_video_input": false, + "mode": "chat", + "comment": "Prior MiniMax coding model for agent workflows, office edits, and automation", + "source": "https://api.friendli.ai/serverless/v1/models" + }, + "friendliai/zai-org/GLM-5.1": { + "litellm_provider": "friendliai", + "max_input_tokens": 202752, + "max_output_tokens": 202752, + "max_tokens": 202752, + "input_cost_per_token": 1.4e-06, + "output_cost_per_token": 4.4e-06, + "cache_read_input_token_cost": 2.6e-07, + "supports_prompt_caching": true, + "supports_reasoning": true, + "reasoning_effort_levels": [], + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_response_schema": true, + "supports_native_structured_output": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": false, + "supports_image_input": false, + "supports_video_input": false, + "mode": "chat", + "comment": "Strong GLM coding model for agentic engineering, terminals, and repository generation", "source": "https://api.friendli.ai/serverless/v1/models" }, "ft:babbage-002": { diff --git a/tests/test_litellm/test_auto_update_price_and_context_window_file.py b/tests/test_litellm/test_auto_update_price_and_context_window_file.py new file mode 100644 index 00000000000..7dbd24a1b81 --- /dev/null +++ b/tests/test_litellm/test_auto_update_price_and_context_window_file.py @@ -0,0 +1,136 @@ +"""Unit tests for the Friendli transform in +`.github/scripts/auto_update_price_and_context_window_file.py`.""" + +from __future__ import annotations + +import importlib.util +import sys +from pathlib import Path + +import pytest + +SCRIPT_PATH = ( + Path(__file__).resolve().parents[2] + / ".github" + / "scripts" + / "auto_update_price_and_context_window_file.py" +) + + +@pytest.fixture(scope="module") +def sync_module(): + spec = importlib.util.spec_from_file_location( + "auto_update_price_and_context_window_file", SCRIPT_PATH + ) + assert spec and spec.loader, f"Could not load spec for {SCRIPT_PATH}" + module = importlib.util.module_from_spec(spec) + sys.modules["auto_update_price_and_context_window_file"] = module + spec.loader.exec_module(module) + return module + + +def _reasoning_model(**overrides: object) -> dict: + model = { + "id": "zai-org/GLM-Test", + "base_model": "zhipuai/glm-test", + "context_length": 1048576, + "max_completion_tokens": 131072, + "pricing": {"input": "0.00000015", "output": "0.0000005", "input_cache_read": "0.00000003"}, + "reasoning": True, + "reasoning_options": [{"type": "effort", "values": ["max", "high", "low"]}], + "functionality": { + "tool_call": True, + "parallel_tool_call": True, + "structured_output": True, + "system_messages": True, + "tool_choice": True, + }, + "input_modalities": ["text", "image", "video"], + "mode": "chat", + } + model.update(overrides) + return model + + +def test_transform_emits_declared_effort_levels_in_canonical_order(sync_module): + entry = sync_module.transform_friendli_data([_reasoning_model()], {})[ + "friendliai/zai-org/GLM-Test" + ] + assert entry["supports_reasoning"] is True + assert entry["reasoning_effort_levels"] == ["low", "high", "max"] + assert not any(k.endswith("_reasoning_effort") for k in entry) + + +def test_transform_reasoning_model_without_effort_options_declares_empty_levels(sync_module): + model = _reasoning_model(reasoning_options=[{"type": "budget_tokens", "values": []}]) + entry = sync_module.transform_friendli_data([model], {})["friendliai/zai-org/GLM-Test"] + assert entry["reasoning_effort_levels"] == [] + + +def test_transform_non_reasoning_model_declares_no_levels(sync_module): + model = _reasoning_model(reasoning=False, reasoning_options=[]) + entry = sync_module.transform_friendli_data([model], {})["friendliai/zai-org/GLM-Test"] + assert entry["supports_reasoning"] is False + assert "reasoning_effort_levels" not in entry + + +def test_transform_max_tokens_mirrors_output_cap_not_context(sync_module): + entry = sync_module.transform_friendli_data([_reasoning_model()], {})[ + "friendliai/zai-org/GLM-Test" + ] + assert entry["max_input_tokens"] == 1048576 + assert entry["max_output_tokens"] == 131072 + assert entry["max_tokens"] == entry["max_output_tokens"] + + +def test_transform_prompt_caching_follows_cache_pricing(sync_module): + cached = sync_module.transform_friendli_data([_reasoning_model()], {})[ + "friendliai/zai-org/GLM-Test" + ] + assert cached["supports_prompt_caching"] is True + assert cached["cache_read_input_token_cost"] == 3e-08 + + uncached_model = _reasoning_model(pricing={"input": "0.00000014", "output": "0.0000004"}) + uncached = sync_module.transform_friendli_data([uncached_model], {})[ + "friendliai/zai-org/GLM-Test" + ] + assert uncached["supports_prompt_caching"] is False + assert "cache_read_input_token_cost" not in uncached + + +def test_transform_modalities_set_vision_image_and_video_flags(sync_module): + entry = sync_module.transform_friendli_data([_reasoning_model()], {})[ + "friendliai/zai-org/GLM-Test" + ] + assert entry["supports_vision"] is True + assert entry["supports_image_input"] is True + assert entry["supports_video_input"] is True + + text_only = _reasoning_model(input_modalities=["text"]) + entry_text = sync_module.transform_friendli_data([text_only], {})[ + "friendliai/zai-org/GLM-Test" + ] + assert entry_text["supports_vision"] is False + assert entry_text["supports_image_input"] is False + assert entry_text["supports_video_input"] is False + + +def test_transform_survives_failed_fetch(sync_module): + assert sync_module.transform_friendli_data(None, {}) == {} + assert sync_module.transform_friendli_data([], {}) == {} + + +def test_transform_inherits_allowlisted_keys_from_base_model_entry(sync_module): + local = { + "zhipuai/glm-test": { + "supports_pdf_input": True, + "supports_assistant_prefill": True, + "input_cost_per_token": 9e-06, + } + } + entry = sync_module.transform_friendli_data([_reasoning_model()], local)[ + "friendliai/zai-org/GLM-Test" + ] + assert entry["supports_pdf_input"] is True + assert entry["supports_assistant_prefill"] is True + assert entry["input_cost_per_token"] == 1.5e-07 From 81f9ad322bd28b424269558ec6535cbb007d8fa6 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Mon, 31 Aug 2026 13:39:52 -0700 Subject: [PATCH 7/9] fix(scripts): keep the weekly price sync alive past unrepresentable rows Vercel now lists video and embedding models without token pricing or token limits, and the first of them KeyError'd the whole weekly run before any provider was synced. Those rows are skipped, and a failed catalog fetch for any provider now yields an empty transform instead of a crash. --- ...to_update_price_and_context_window_file.py | 9 +++++ ...to_update_price_and_context_window_file.py | 33 ++++++++++++++++++- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/.github/scripts/auto_update_price_and_context_window_file.py b/.github/scripts/auto_update_price_and_context_window_file.py index df1ac738681..e7d7e465f8f 100644 --- a/.github/scripts/auto_update_price_and_context_window_file.py +++ b/.github/scripts/auto_update_price_and_context_window_file.py @@ -171,6 +171,8 @@ def write_to_file(file_path, data): # Update the existing models and add the missing models for OpenRouter def transform_openrouter_data(data): transformed = {} + if not data: + return transformed for row in data: # Add the fields 'max_tokens' and 'input_cost_per_token' obj = { @@ -209,7 +211,14 @@ def transform_openrouter_data(data): # Update the existing models and add the missing models for Vercel AI Gateway def transform_vercel_ai_gateway_data(data): transformed = {} + if not data: + return transformed for row in data: + # Rows without token pricing or token limits (video/embedding models) previously KeyError'd the whole sync + if any(row.get(k) is None for k in ("context_window", "max_tokens")) or any( + row.get("pricing", {}).get(k) is None for k in ("input", "output") + ): + continue obj = { "max_tokens": row["context_window"], "input_cost_per_token": float(row["pricing"]["input"]), diff --git a/tests/test_litellm/test_auto_update_price_and_context_window_file.py b/tests/test_litellm/test_auto_update_price_and_context_window_file.py index 7dbd24a1b81..8dc43100544 100644 --- a/tests/test_litellm/test_auto_update_price_and_context_window_file.py +++ b/tests/test_litellm/test_auto_update_price_and_context_window_file.py @@ -115,9 +115,40 @@ def test_transform_modalities_set_vision_image_and_video_flags(sync_module): assert entry_text["supports_video_input"] is False -def test_transform_survives_failed_fetch(sync_module): +def test_transforms_survive_failed_fetch(sync_module): assert sync_module.transform_friendli_data(None, {}) == {} assert sync_module.transform_friendli_data([], {}) == {} + assert sync_module.transform_openrouter_data(None) == {} + assert sync_module.transform_vercel_ai_gateway_data(None) == {} + + +def test_vercel_transform_skips_rows_without_token_pricing_or_limits(sync_module): + rows = [ + { + "id": "wan-video", + "pricing": {"video_duration_pricing": [{"resolution": "720p", "cost_per_second": "0.1"}]}, + }, + { + "id": "qwen3-embedding", + "context_window": 32768, + "max_tokens": 32768, + "pricing": {"input": "0.00000001"}, + }, + { + "id": "no-limits-chat", + "pricing": {"input": "0.000001", "output": "0.000002"}, + }, + { + "id": "good-chat", + "context_window": 128000, + "max_tokens": 8192, + "pricing": {"input": "0.000001", "output": "0.000002"}, + }, + ] + transformed = sync_module.transform_vercel_ai_gateway_data(rows) + assert list(transformed) == ["vercel_ai_gateway/good-chat"] + assert transformed["vercel_ai_gateway/good-chat"]["input_cost_per_token"] == 1e-06 + assert transformed["vercel_ai_gateway/good-chat"]["output_cost_per_token"] == 2e-06 def test_transform_inherits_allowlisted_keys_from_base_model_entry(sync_module): From 077cdb5fb9eafabd516472359b167e3ed4b6549b Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Mon, 31 Aug 2026 13:56:54 -0700 Subject: [PATCH 8/9] fix(scripts): replace synced Friendli entries wholesale and seed the backup cost map --- ...to_update_price_and_context_window_file.py | 11 +- ...odel_prices_and_context_window_backup.json | 203 ++++++++++++++++++ ...to_update_price_and_context_window_file.py | 23 ++ 3 files changed, 234 insertions(+), 3 deletions(-) diff --git a/.github/scripts/auto_update_price_and_context_window_file.py b/.github/scripts/auto_update_price_and_context_window_file.py index e7d7e465f8f..928452ee8d7 100644 --- a/.github/scripts/auto_update_price_and_context_window_file.py +++ b/.github/scripts/auto_update_price_and_context_window_file.py @@ -147,10 +147,15 @@ def transform_friendli_data(data: list, local_data: dict) -> dict: return transformed # Synchronize local data with remote data -def sync_local_data_with_remote(local_data, remote_data): +def sync_local_data_with_remote(local_data, remote_data, replace_keys=frozenset()): # Update existing keys in local_data with values from remote_data + # (replace_keys entries are swapped wholesale so a field the remote catalog + # dropped, e.g. cache pricing, cannot survive as a stale value) for key in (set(local_data) & set(remote_data)): - local_data[key].update(remote_data[key]) + if key in replace_keys: + local_data[key] = remote_data[key] + else: + local_data[key].update(remote_data[key]) # Add new keys from remote_data to local_data for key in (set(remote_data) - set(local_data)): @@ -286,7 +291,7 @@ def main(): # If both local and openrouter data are available, synchronize and save if local_data and all_remote_data: - sync_local_data_with_remote(local_data, all_remote_data) + sync_local_data_with_remote(local_data, all_remote_data, replace_keys=frozenset(friendli_data)) write_to_file(local_file_path, local_data) else: print("Failed to fetch model data from either local file or URL.") diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index 05c1cfd3179..5aa1bd53605 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -19564,6 +19564,209 @@ "supports_system_messages": true, "supports_tool_choice": true }, + "friendliai/zai-org/GLM-5.3-Flash": { + "litellm_provider": "friendliai", + "max_input_tokens": 1048576, + "max_output_tokens": 1048576, + "max_tokens": 1048576, + "input_cost_per_token": 1.5e-07, + "output_cost_per_token": 5e-07, + "cache_read_input_token_cost": 3e-08, + "supports_prompt_caching": true, + "supports_reasoning": true, + "reasoning_effort_levels": [ + "low", + "high", + "max" + ], + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_response_schema": true, + "supports_native_structured_output": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true, + "supports_image_input": true, + "supports_video_input": true, + "mode": "chat", + "comment": "Native multimodal GLM model for efficient coding and long-horizon agent tasks", + "source": "https://api.friendli.ai/serverless/v1/models" + }, + "friendliai/zai-org/GLM-5.3": { + "litellm_provider": "friendliai", + "max_input_tokens": 1048576, + "max_output_tokens": 1048576, + "max_tokens": 1048576, + "input_cost_per_token": 1.26e-06, + "output_cost_per_token": 3.96e-06, + "cache_read_input_token_cost": 2.34e-07, + "supports_prompt_caching": true, + "supports_reasoning": true, + "reasoning_effort_levels": [ + "low", + "high", + "max" + ], + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_response_schema": true, + "supports_native_structured_output": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": false, + "supports_image_input": false, + "supports_video_input": false, + "mode": "chat", + "comment": "Flagship GLM model for long-horizon coding, agents, and complex project delivery", + "source": "https://api.friendli.ai/serverless/v1/models" + }, + "friendliai/google/gemma-4-31B-it": { + "litellm_provider": "friendliai", + "max_input_tokens": 262144, + "max_output_tokens": 262144, + "max_tokens": 262144, + "input_cost_per_token": 1.4e-07, + "output_cost_per_token": 4e-07, + "supports_prompt_caching": false, + "supports_reasoning": true, + "reasoning_effort_levels": [], + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_response_schema": true, + "supports_native_structured_output": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true, + "supports_image_input": true, + "supports_video_input": false, + "mode": "chat", + "comment": "Largest Gemma 4 instruction model for open, self-hosted chat and reasoning", + "source": "https://api.friendli.ai/serverless/v1/models" + }, + "friendliai/zai-org/GLM-5.2": { + "litellm_provider": "friendliai", + "max_input_tokens": 1048576, + "max_output_tokens": 1048576, + "max_tokens": 1048576, + "input_cost_per_token": 1.4e-06, + "output_cost_per_token": 4.4e-06, + "cache_read_input_token_cost": 2.6e-07, + "supports_prompt_caching": true, + "supports_reasoning": true, + "reasoning_effort_levels": [ + "high", + "max" + ], + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_response_schema": true, + "supports_native_structured_output": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": false, + "supports_image_input": false, + "supports_video_input": false, + "mode": "chat", + "comment": "Open flagship GLM for long-horizon coding agents and million-token context work", + "source": "https://api.friendli.ai/serverless/v1/models" + }, + "friendliai/LGAI-EXAONE/K-EXAONE-2.0-750B-A37B": { + "litellm_provider": "friendliai", + "max_input_tokens": 262144, + "max_output_tokens": 262144, + "max_tokens": 262144, + "input_cost_per_token": 6e-07, + "output_cost_per_token": 2.4e-06, + "cache_read_input_token_cost": 1.2e-07, + "supports_prompt_caching": true, + "supports_reasoning": true, + "reasoning_effort_levels": [], + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_response_schema": true, + "supports_native_structured_output": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": false, + "supports_image_input": false, + "supports_video_input": false, + "mode": "chat", + "comment": "Frontier-scale multilingual language model developed by LG AI Research", + "deprecation_date": "2026-09-06", + "source": "https://api.friendli.ai/serverless/v1/models" + }, + "friendliai/deepseek-ai/DeepSeek-V3.2": { + "litellm_provider": "friendliai", + "max_input_tokens": 163840, + "max_output_tokens": 163840, + "max_tokens": 163840, + "input_cost_per_token": 5e-07, + "output_cost_per_token": 1.5e-06, + "cache_read_input_token_cost": 2.5e-07, + "supports_prompt_caching": true, + "supports_reasoning": true, + "reasoning_effort_levels": [], + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_response_schema": true, + "supports_native_structured_output": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": false, + "supports_image_input": false, + "supports_video_input": false, + "mode": "chat", + "comment": "DeepSeek chat model for instruction following, coding, and analysis", + "source": "https://api.friendli.ai/serverless/v1/models" + }, + "friendliai/MiniMaxAI/MiniMax-M2.5": { + "litellm_provider": "friendliai", + "max_input_tokens": 196608, + "max_output_tokens": 196608, + "max_tokens": 196608, + "input_cost_per_token": 3e-07, + "output_cost_per_token": 1.2e-06, + "cache_read_input_token_cost": 6e-08, + "supports_prompt_caching": true, + "supports_reasoning": true, + "reasoning_effort_levels": [], + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_response_schema": true, + "supports_native_structured_output": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": false, + "supports_image_input": false, + "supports_video_input": false, + "mode": "chat", + "comment": "Prior MiniMax coding model for agent workflows, office edits, and automation", + "source": "https://api.friendli.ai/serverless/v1/models" + }, + "friendliai/zai-org/GLM-5.1": { + "litellm_provider": "friendliai", + "max_input_tokens": 202752, + "max_output_tokens": 202752, + "max_tokens": 202752, + "input_cost_per_token": 1.4e-06, + "output_cost_per_token": 4.4e-06, + "cache_read_input_token_cost": 2.6e-07, + "supports_prompt_caching": true, + "supports_reasoning": true, + "reasoning_effort_levels": [], + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_response_schema": true, + "supports_native_structured_output": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": false, + "supports_image_input": false, + "supports_video_input": false, + "mode": "chat", + "comment": "Strong GLM coding model for agentic engineering, terminals, and repository generation", + "source": "https://api.friendli.ai/serverless/v1/models" + }, "ft:babbage-002": { "deprecation_date": "2026-10-23", "input_cost_per_token": 1.6e-06, diff --git a/tests/test_litellm/test_auto_update_price_and_context_window_file.py b/tests/test_litellm/test_auto_update_price_and_context_window_file.py index 8dc43100544..9766e54e9ff 100644 --- a/tests/test_litellm/test_auto_update_price_and_context_window_file.py +++ b/tests/test_litellm/test_auto_update_price_and_context_window_file.py @@ -151,6 +151,29 @@ def test_vercel_transform_skips_rows_without_token_pricing_or_limits(sync_module assert transformed["vercel_ai_gateway/good-chat"]["output_cost_per_token"] == 2e-06 +def test_sync_replaces_friendli_entries_so_dropped_cache_pricing_does_not_survive(sync_module): + local = { + "friendliai/zai-org/GLM-Test": { + "litellm_provider": "friendliai", + "cache_read_input_token_cost": 3e-08, + "supports_prompt_caching": True, + } + } + uncached_model = _reasoning_model(pricing={"input": "0.00000014", "output": "0.0000004"}) + remote = sync_module.transform_friendli_data([uncached_model], local) + sync_module.sync_local_data_with_remote(local, remote, replace_keys=frozenset(remote)) + synced = local["friendliai/zai-org/GLM-Test"] + assert "cache_read_input_token_cost" not in synced + assert synced["supports_prompt_caching"] is False + + +def test_sync_still_merges_entries_outside_replace_keys(sync_module): + local = {"openrouter/some-model": {"input_cost_per_token": 1e-06, "supports_vision": True}} + remote = {"openrouter/some-model": {"input_cost_per_token": 2e-06}} + sync_module.sync_local_data_with_remote(local, remote) + assert local["openrouter/some-model"] == {"input_cost_per_token": 2e-06, "supports_vision": True} + + def test_transform_inherits_allowlisted_keys_from_base_model_entry(sync_module): local = { "zhipuai/glm-test": { From 044e6c28bdad165b3948156f4c302eae86698294 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Mon, 31 Aug 2026 16:05:43 -0700 Subject: [PATCH 9/9] fix(price-sync): skip Friendli rows without valid token prices so priced entries never get wiped --- ...to_update_price_and_context_window_file.py | 18 +++++++++++ ...to_update_price_and_context_window_file.py | 32 +++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/.github/scripts/auto_update_price_and_context_window_file.py b/.github/scripts/auto_update_price_and_context_window_file.py index 928452ee8d7..4c9a05bd4f0 100644 --- a/.github/scripts/auto_update_price_and_context_window_file.py +++ b/.github/scripts/auto_update_price_and_context_window_file.py @@ -1,6 +1,7 @@ import asyncio import aiohttp import json +import math from typing import Any # Asynchronously fetch data from a given URL @@ -60,6 +61,19 @@ def _reasoning_effort_levels(reasoning_options: list) -> list: return [level for level in REASONING_EFFORT_LEVEL_ORDER if level in offered] +def _valid_token_price(value: object) -> bool: + try: + price = float(value) # pyright: ignore[reportArgumentType] # non-numeric values are rejected via the except + except (TypeError, ValueError): + return False + return math.isfinite(price) and price >= 0 + + +def _has_valid_token_prices(pricing: dict | None) -> bool: + prices = pricing or {} + return _valid_token_price(prices.get("input")) and _valid_token_price(prices.get("output")) + + def _pricing(pricing: dict) -> dict: out: dict[str, Any] = {} if not pricing: @@ -88,6 +102,10 @@ def transform_friendli_data(data: list, local_data: dict) -> dict: if not data: return transformed for model in data: + # An unpriced row must never wholesale-replace an already priced local entry: + # missing prices cost-calculate as zero, silently zeroing tracked spend + if not _has_valid_token_prices(model.get("pricing")): + continue model_id = model["id"] base_model = model.get("base_model") or "" entry: dict[str, Any] = { diff --git a/tests/test_litellm/test_auto_update_price_and_context_window_file.py b/tests/test_litellm/test_auto_update_price_and_context_window_file.py index 9766e54e9ff..435747e9a09 100644 --- a/tests/test_litellm/test_auto_update_price_and_context_window_file.py +++ b/tests/test_litellm/test_auto_update_price_and_context_window_file.py @@ -115,6 +115,38 @@ def test_transform_modalities_set_vision_image_and_video_flags(sync_module): assert entry_text["supports_video_input"] is False +def test_transform_skips_rows_without_valid_token_prices_so_priced_local_entries_survive(sync_module): + local = { + "friendliai/zai-org/GLM-Test": { + "litellm_provider": "friendliai", + "input_cost_per_token": 1.5e-07, + "output_cost_per_token": 5e-07, + } + } + unpriced_rows = [ + _reasoning_model(pricing={}), + _reasoning_model(pricing=None), + _reasoning_model(pricing={"input": "0.00000015"}), + _reasoning_model(pricing={"output": "0.0000005"}), + _reasoning_model(pricing={"input": "not-a-number", "output": "0.0000005"}), + _reasoning_model(pricing={"input": "-0.00000015", "output": "0.0000005"}), + _reasoning_model(pricing={"input": "inf", "output": "0.0000005"}), + _reasoning_model(pricing={"input": "nan", "output": "0.0000005"}), + ] + remote = sync_module.transform_friendli_data(unpriced_rows, local) + assert remote == {} + sync_module.sync_local_data_with_remote(local, remote, replace_keys=frozenset(remote)) + assert local["friendliai/zai-org/GLM-Test"]["input_cost_per_token"] == 1.5e-07 + assert local["friendliai/zai-org/GLM-Test"]["output_cost_per_token"] == 5e-07 + + +def test_transform_keeps_zero_priced_rows(sync_module): + free_model = _reasoning_model(pricing={"input": "0", "output": "0"}) + entry = sync_module.transform_friendli_data([free_model], {})["friendliai/zai-org/GLM-Test"] + assert entry["input_cost_per_token"] == 0.0 + assert entry["output_cost_per_token"] == 0.0 + + def test_transforms_survive_failed_fetch(sync_module): assert sync_module.transform_friendli_data(None, {}) == {} assert sync_module.transform_friendli_data([], {}) == {}