From daa43de167ddfbefb129db7a646129624311563a Mon Sep 17 00:00:00 2001 From: robust-WJL Date: Mon, 27 Jul 2026 15:05:22 +0900 Subject: [PATCH] fix(friendliai): sync serverless catalog with the models API The catalog carried only meta-llama-3.1-70b-instruct and meta-llama-3.1-8b-instruct. Both are retired; neither is served by https://api.friendli.ai/serverless/v1/models anymore, so their pricing and 8k context windows were the only FriendliAI data litellm exposed. Every field is taken from that endpoint, which is public and unauthenticated: pricing.input, pricing.output and pricing.input_cache_read map onto the token costs, context_length onto max_input_tokens, max_completion_tokens onto max_output_tokens and max_tokens, and functionality.tool_call, functionality.parallel_tool_call and functionality.structured_output onto the capability flags. Qwen3-235B-A22B-Instruct-2507 reports a deprecation_date of 2026-08-05 and carries it through. tests/test_litellm/test_friendliai_model_metadata.py pins the lineup, asserts the retired keys stay absent, and asserts the root JSON and the bundled backup never drift apart for this provider --- ...odel_prices_and_context_window_backup.json | 110 ++++++++++++++-- model_prices_and_context_window.json | 110 ++++++++++++++-- .../test_friendliai_model_metadata.py | 122 ++++++++++++++++++ 3 files changed, 318 insertions(+), 24 deletions(-) create mode 100644 tests/test_litellm/test_friendliai_model_metadata.py diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index 2cef600ea32..2ebcceb4bda 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -17198,30 +17198,116 @@ "output_cost_per_token": 0.0, "source": "https://fireworks.ai/pricing" }, - "friendliai/meta-llama-3.1-70b-instruct": { - "input_cost_per_token": 6e-07, + "friendliai/LGAI-EXAONE/K-EXAONE-236B-A23B": { + "cache_read_input_token_cost": 1e-07, + "input_cost_per_token": 2e-07, "litellm_provider": "friendliai", - "max_input_tokens": 8192, - "max_output_tokens": 8192, - "max_tokens": 8192, + "max_input_tokens": 262144, + "max_output_tokens": 262144, + "max_tokens": 262144, "mode": "chat", - "output_cost_per_token": 6e-07, + "output_cost_per_token": 8e-07, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "friendliai/MiniMaxAI/MiniMax-M2.5": { + "cache_read_input_token_cost": 6e-08, + "input_cost_per_token": 3e-07, + "litellm_provider": "friendliai", + "max_input_tokens": 196608, + "max_output_tokens": 196608, + "max_tokens": 196608, + "mode": "chat", + "output_cost_per_token": 1.2e-06, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "friendliai/Qwen/Qwen3-235B-A22B-Instruct-2507": { + "deprecation_date": "2026-08-05", + "input_cost_per_token": 2e-07, + "litellm_provider": "friendliai", + "max_input_tokens": 262144, + "max_output_tokens": 262144, + "max_tokens": 262144, + "mode": "chat", + "output_cost_per_token": 8e-07, "supports_function_calling": true, "supports_parallel_function_calling": true, "supports_response_schema": true, "supports_system_messages": true, "supports_tool_choice": true }, - "friendliai/meta-llama-3.1-8b-instruct": { - "input_cost_per_token": 1e-07, + "friendliai/deepseek-ai/DeepSeek-V3.2": { + "cache_read_input_token_cost": 2.5e-07, + "input_cost_per_token": 5e-07, "litellm_provider": "friendliai", - "max_input_tokens": 8192, - "max_output_tokens": 8192, - "max_tokens": 8192, + "max_input_tokens": 163840, + "max_output_tokens": 163840, + "max_tokens": 163840, "mode": "chat", - "output_cost_per_token": 1e-07, + "output_cost_per_token": 1.5e-06, "supports_function_calling": true, "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "friendliai/google/gemma-4-31B-it": { + "input_cost_per_token": 1.4e-07, + "litellm_provider": "friendliai", + "max_input_tokens": 262144, + "max_output_tokens": 262144, + "max_tokens": 262144, + "mode": "chat", + "output_cost_per_token": 4e-07, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "friendliai/zai-org/GLM-5.1": { + "cache_read_input_token_cost": 2.6e-07, + "input_cost_per_token": 1.4e-06, + "litellm_provider": "friendliai", + "max_input_tokens": 202752, + "max_output_tokens": 202752, + "max_tokens": 202752, + "mode": "chat", + "output_cost_per_token": 4.4e-06, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "friendliai/zai-org/GLM-5.2": { + "cache_read_input_token_cost": 2.6e-07, + "input_cost_per_token": 1.4e-06, + "litellm_provider": "friendliai", + "max_input_tokens": 1048576, + "max_output_tokens": 1048576, + "max_tokens": 1048576, + "mode": "chat", + "output_cost_per_token": 4.4e-06, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, "supports_response_schema": true, "supports_system_messages": true, "supports_tool_choice": true diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index db28118d52b..ad3cd2380cc 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -17198,30 +17198,116 @@ "output_cost_per_token": 0.0, "source": "https://fireworks.ai/pricing" }, - "friendliai/meta-llama-3.1-70b-instruct": { - "input_cost_per_token": 6e-07, + "friendliai/LGAI-EXAONE/K-EXAONE-236B-A23B": { + "cache_read_input_token_cost": 1e-07, + "input_cost_per_token": 2e-07, "litellm_provider": "friendliai", - "max_input_tokens": 8192, - "max_output_tokens": 8192, - "max_tokens": 8192, + "max_input_tokens": 262144, + "max_output_tokens": 262144, + "max_tokens": 262144, "mode": "chat", - "output_cost_per_token": 6e-07, + "output_cost_per_token": 8e-07, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "friendliai/MiniMaxAI/MiniMax-M2.5": { + "cache_read_input_token_cost": 6e-08, + "input_cost_per_token": 3e-07, + "litellm_provider": "friendliai", + "max_input_tokens": 196608, + "max_output_tokens": 196608, + "max_tokens": 196608, + "mode": "chat", + "output_cost_per_token": 1.2e-06, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "friendliai/Qwen/Qwen3-235B-A22B-Instruct-2507": { + "deprecation_date": "2026-08-05", + "input_cost_per_token": 2e-07, + "litellm_provider": "friendliai", + "max_input_tokens": 262144, + "max_output_tokens": 262144, + "max_tokens": 262144, + "mode": "chat", + "output_cost_per_token": 8e-07, "supports_function_calling": true, "supports_parallel_function_calling": true, "supports_response_schema": true, "supports_system_messages": true, "supports_tool_choice": true }, - "friendliai/meta-llama-3.1-8b-instruct": { - "input_cost_per_token": 1e-07, + "friendliai/deepseek-ai/DeepSeek-V3.2": { + "cache_read_input_token_cost": 2.5e-07, + "input_cost_per_token": 5e-07, "litellm_provider": "friendliai", - "max_input_tokens": 8192, - "max_output_tokens": 8192, - "max_tokens": 8192, + "max_input_tokens": 163840, + "max_output_tokens": 163840, + "max_tokens": 163840, "mode": "chat", - "output_cost_per_token": 1e-07, + "output_cost_per_token": 1.5e-06, "supports_function_calling": true, "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "friendliai/google/gemma-4-31B-it": { + "input_cost_per_token": 1.4e-07, + "litellm_provider": "friendliai", + "max_input_tokens": 262144, + "max_output_tokens": 262144, + "max_tokens": 262144, + "mode": "chat", + "output_cost_per_token": 4e-07, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "friendliai/zai-org/GLM-5.1": { + "cache_read_input_token_cost": 2.6e-07, + "input_cost_per_token": 1.4e-06, + "litellm_provider": "friendliai", + "max_input_tokens": 202752, + "max_output_tokens": 202752, + "max_tokens": 202752, + "mode": "chat", + "output_cost_per_token": 4.4e-06, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "friendliai/zai-org/GLM-5.2": { + "cache_read_input_token_cost": 2.6e-07, + "input_cost_per_token": 1.4e-06, + "litellm_provider": "friendliai", + "max_input_tokens": 1048576, + "max_output_tokens": 1048576, + "max_tokens": 1048576, + "mode": "chat", + "output_cost_per_token": 4.4e-06, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, "supports_response_schema": true, "supports_system_messages": true, "supports_tool_choice": true diff --git a/tests/test_litellm/test_friendliai_model_metadata.py b/tests/test_litellm/test_friendliai_model_metadata.py new file mode 100644 index 00000000000..5a68fa8d289 --- /dev/null +++ b/tests/test_litellm/test_friendliai_model_metadata.py @@ -0,0 +1,122 @@ +"""Regression tests for the FriendliAI serverless catalog entries. + +The catalog previously carried only ``friendliai/meta-llama-3.1-70b-instruct`` +and ``friendliai/meta-llama-3.1-8b-instruct``, both retired and no longer +served by ``https://api.friendli.ai/serverless/v1/models``. These tests pin the +current serverless lineup, assert the retired entries stay gone, and assert the +root JSON and the bundled backup JSON never drift apart for this provider. +""" + +import json +from pathlib import Path +from typing import Final, NamedTuple + +import pytest + + +class FriendliModel(NamedTuple): + key: str + input_cost_per_token: float + output_cost_per_token: float + cache_read_input_token_cost: float | None + max_input_tokens: int + max_output_tokens: int + + +EXPECTED_MODELS: Final[tuple[FriendliModel, ...]] = ( + FriendliModel( + "friendliai/LGAI-EXAONE/K-EXAONE-236B-A23B", 2e-07, 8e-07, 1e-07, 262144, 262144 + ), + FriendliModel( + "friendliai/MiniMaxAI/MiniMax-M2.5", 3e-07, 1.2e-06, 6e-08, 196608, 196608 + ), + FriendliModel( + "friendliai/Qwen/Qwen3-235B-A22B-Instruct-2507", 2e-07, 8e-07, None, 262144, 262144 + ), + FriendliModel( + "friendliai/deepseek-ai/DeepSeek-V3.2", 5e-07, 1.5e-06, 2.5e-07, 163840, 163840 + ), + FriendliModel( + "friendliai/google/gemma-4-31B-it", 1.4e-07, 4e-07, None, 262144, 262144 + ), + FriendliModel( + "friendliai/zai-org/GLM-5.1", 1.4e-06, 4.4e-06, 2.6e-07, 202752, 202752 + ), + FriendliModel( + "friendliai/zai-org/GLM-5.2", 1.4e-06, 4.4e-06, 2.6e-07, 1048576, 1048576 + ), +) + +RETIRED_KEYS: Final[tuple[str, ...]] = ( + "friendliai/meta-llama-3.1-70b-instruct", + "friendliai/meta-llama-3.1-8b-instruct", +) + +_REPO_ROOT: Final[Path] = Path(__file__).resolve().parents[2] +ROOT_PATH: Final[Path] = _REPO_ROOT / "model_prices_and_context_window.json" +BACKUP_PATH: Final[Path] = ( + _REPO_ROOT / "litellm" / "model_prices_and_context_window_backup.json" +) +CATALOG_PATHS: Final[tuple[Path, ...]] = (ROOT_PATH, BACKUP_PATH) + + +def _load(path: Path) -> dict[str, dict[str, object]]: + return json.loads(path.read_text(encoding="utf-8")) + + +@pytest.mark.parametrize("path", CATALOG_PATHS, ids=lambda p: p.name) +@pytest.mark.parametrize("expected", EXPECTED_MODELS, ids=lambda m: m.key) +def test_serverless_model_pricing_and_context(path: Path, expected: FriendliModel) -> None: + entry = _load(path).get(expected.key) + assert entry is not None, f"{expected.key} missing from {path.name}" + assert entry["litellm_provider"] == "friendliai" + assert entry["mode"] == "chat" + assert entry["input_cost_per_token"] == expected.input_cost_per_token + assert entry["output_cost_per_token"] == expected.output_cost_per_token + assert entry.get("cache_read_input_token_cost") == expected.cache_read_input_token_cost + assert entry["max_input_tokens"] == expected.max_input_tokens + assert entry["max_output_tokens"] == expected.max_output_tokens + assert entry["max_tokens"] == expected.max_output_tokens + + +@pytest.mark.parametrize("path", CATALOG_PATHS, ids=lambda p: p.name) +@pytest.mark.parametrize("expected", EXPECTED_MODELS, ids=lambda m: m.key) +def test_serverless_models_advertise_tool_calling( + path: Path, expected: FriendliModel +) -> None: + entry = _load(path)[expected.key] + assert entry["supports_function_calling"] is True + assert entry["supports_parallel_function_calling"] is True + assert entry["supports_response_schema"] is True + assert entry["supports_tool_choice"] is True + + +@pytest.mark.parametrize("path", CATALOG_PATHS, ids=lambda p: p.name) +@pytest.mark.parametrize("key", RETIRED_KEYS) +def test_retired_llama_entries_absent(path: Path, key: str) -> None: + assert key not in _load(path), f"{key} is retired and must not be re-added" + + +def test_prompt_caching_flag_tracks_cache_read_cost() -> None: + catalog = _load(ROOT_PATH) + for expected in EXPECTED_MODELS: + entry = catalog[expected.key] + assert entry.get("supports_prompt_caching", False) is ( + expected.cache_read_input_token_cost is not None + ), f"{expected.key} prompt-caching flag disagrees with cache_read cost" + + +def test_deprecating_model_carries_iso_date() -> None: + entry = _load(ROOT_PATH)["friendliai/Qwen/Qwen3-235B-A22B-Instruct-2507"] + assert entry["deprecation_date"] == "2026-08-05" + + +def test_root_and_backup_agree_on_every_friendliai_entry() -> None: + root = {k: v for k, v in _load(ROOT_PATH).items() if k.startswith("friendliai/")} + backup = {k: v for k, v in _load(BACKUP_PATH).items() if k.startswith("friendliai/")} + assert root == backup + + +def test_catalog_covers_exactly_the_expected_lineup() -> None: + keys = {k for k in _load(ROOT_PATH) if k.startswith("friendliai/")} + assert keys == {m.key for m in EXPECTED_MODELS}