mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-13 23:11:40 +00:00
Merge pull request #35918 from Lee-Si-Yoon/feat/friendli-model-metadata-sync
feat(friendli): auto-sync Friendli model metadata into price registry
This commit is contained in:
commit
b1a61f510c
4 changed files with 706 additions and 28 deletions
|
|
@ -1,6 +1,8 @@
|
|||
import asyncio
|
||||
import aiohttp
|
||||
import json
|
||||
import math
|
||||
from typing import Any
|
||||
|
||||
# Asynchronously fetch data from a given URL
|
||||
async def fetch_data(url):
|
||||
|
|
@ -21,11 +23,157 @@ async def fetch_data(url):
|
|||
print("Error fetching data from URL:", e)
|
||||
return None
|
||||
|
||||
|
||||
FRIENDLI_API_URL = "https://api.friendli.ai/serverless/v1/models"
|
||||
FRIENDLI_PROVIDER = "friendliai"
|
||||
|
||||
INHERITABLE_BASE_KEYS = (
|
||||
"supports_pdf_input",
|
||||
"supports_assistant_prefill",
|
||||
"supports_adaptive_thinking",
|
||||
"supports_output_config",
|
||||
)
|
||||
|
||||
REASONING_EFFORT_LEVEL_ORDER = ("none", "minimal", "low", "medium", "high", "xhigh", "max")
|
||||
|
||||
|
||||
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()
|
||||
if base_model in local_data:
|
||||
return base_model
|
||||
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 _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 _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:
|
||||
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 _modality_flags(input_mods: list) -> dict:
|
||||
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:
|
||||
# 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] = {
|
||||
"litellm_provider": FRIENDLI_PROVIDER,
|
||||
}
|
||||
|
||||
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]
|
||||
|
||||
ctx = model.get("context_length")
|
||||
if ctx is not None:
|
||||
entry["max_input_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)
|
||||
|
||||
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["reasoning_effort_levels"] = _reasoning_effort_levels(
|
||||
model.get("reasoning_options", [])
|
||||
)
|
||||
|
||||
func = model.get("functionality", {})
|
||||
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
|
||||
|
||||
entry.update(_modality_flags(model.get("input_modalities", [])))
|
||||
|
||||
entry["mode"] = model.get("mode", "chat")
|
||||
|
||||
desc = model.get("description")
|
||||
if desc:
|
||||
entry["comment"] = desc
|
||||
|
||||
dep = model.get("deprecation_date")
|
||||
if dep:
|
||||
entry["deprecation_date"] = dep.split("T")[0]
|
||||
|
||||
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):
|
||||
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)):
|
||||
|
|
@ -46,6 +194,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 = {
|
||||
|
|
@ -84,7 +234,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"]),
|
||||
|
|
@ -143,13 +300,16 @@ 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)
|
||||
|
||||
friendli_data = asyncio.run(fetch_data(FRIENDLI_API_URL))
|
||||
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:
|
||||
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.")
|
||||
|
|
|
|||
|
|
@ -23083,58 +23083,206 @@
|
|||
},
|
||||
"friendliai/zai-org/GLM-5.3-Flash": {
|
||||
"litellm_provider": "friendliai",
|
||||
"supports_reasoning": true,
|
||||
"supports_function_calling": true,
|
||||
"max_input_tokens": 1048576,
|
||||
"max_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,
|
||||
"mode": "chat",
|
||||
"comment": "Native multimodal GLM model for efficient coding and long-horizon agent tasks",
|
||||
"source": "https://api.friendli.ai/serverless/v1/models",
|
||||
"supports_vision": true,
|
||||
"supports_image_input": true,
|
||||
"supports_video_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",
|
||||
"supports_reasoning": true,
|
||||
"supports_function_calling": true,
|
||||
"max_input_tokens": 1048576,
|
||||
"max_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",
|
||||
"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_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",
|
||||
|
|
|
|||
|
|
@ -23083,58 +23083,206 @@
|
|||
},
|
||||
"friendliai/zai-org/GLM-5.3-Flash": {
|
||||
"litellm_provider": "friendliai",
|
||||
"supports_reasoning": true,
|
||||
"supports_function_calling": true,
|
||||
"max_input_tokens": 1048576,
|
||||
"max_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,
|
||||
"mode": "chat",
|
||||
"comment": "Native multimodal GLM model for efficient coding and long-horizon agent tasks",
|
||||
"source": "https://api.friendli.ai/serverless/v1/models",
|
||||
"supports_vision": true,
|
||||
"supports_image_input": true,
|
||||
"supports_video_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",
|
||||
"supports_reasoning": true,
|
||||
"supports_function_calling": true,
|
||||
"max_input_tokens": 1048576,
|
||||
"max_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",
|
||||
"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_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",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,222 @@
|
|||
"""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_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([], {}) == {}
|
||||
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_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": {
|
||||
"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
|
||||
Loading…
Add table
Reference in a new issue