This commit is contained in:
Oleg Bantysh 2026-11-07 08:59:03 +04:00 • committed by GitHub
commit 9ed3da4c67
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 362 additions and 0 deletions

View file

@ -404,6 +404,7 @@ Set `LITELLM_PROXY_API_BASE` and `LITELLM_PROXY_API_KEY` and every model call th
| [Fal AI (`fal_ai`)](https://docs.litellm.ai/docs/providers/fal_ai) | ✅ | ✅ | ✅ | | ✅ | | | | | |
| [Featherless AI (`featherless_ai`)](https://docs.litellm.ai/docs/providers/featherless_ai) | ✅ | ✅ | ✅ | | | | | | | |
| [Fireworks AI (`fireworks_ai`)](https://docs.litellm.ai/docs/providers/fireworks_ai) | ✅ | ✅ | ✅ | | | | | | | |
| [Fortytwo (`fortytwo`)](https://docs.litellm.ai/docs/providers/fortytwo) | ✅ | ✅ | ✅ | | | | | | | |
| [FriendliAI (`friendliai`)](https://docs.litellm.ai/docs/providers/friendliai) | ✅ | ✅ | ✅ | | | | | | | |
| [Galadriel (`galadriel`)](https://docs.litellm.ai/docs/providers/galadriel) | ✅ | ✅ | ✅ | | | | | | | |
| [GitHub Copilot (`github_copilot`)](https://docs.litellm.ai/docs/providers/github_copilot) | ✅ | ✅ | ✅ | ✅ | | | | | | |

View file

@ -689,6 +689,7 @@ openinfer_models: Final[Set[str]] = set()
kyma_models: Final[set[str]] = set() # mutable-ok: populated when the shared model catalog loads
streamlake_models: Final[Set[str]] = set()
wally_models: Final[Set[str]] = set()
fortytwo_models: Final[Set[str]] = set()
zerogpu_models: Final[Set[str]] = set()
v0_models: Set = set()
morph_models: Set = set()
@ -987,6 +988,8 @@ def _populate_provider_model_sets(model_cost_map: Mapping[str, Mapping[str, obje
streamlake_models.add(key)
elif value.get("litellm_provider") == "wally":
wally_models.add(key)
elif value.get("litellm_provider") == "fortytwo":
fortytwo_models.add(key)
elif value.get("litellm_provider") == "zerogpu":
zerogpu_models.add(key)
elif value.get("litellm_provider") == "v0":
@ -1169,6 +1172,7 @@ model_list = list(
| kyma_models
| streamlake_models
| wally_models
| fortytwo_models
| zerogpu_models
| v0_models
| morph_models
@ -1303,6 +1307,7 @@ def _build_models_by_provider() -> dict:
"kyma": kyma_models,
"streamlake": streamlake_models,
"wally": wally_models,
"fortytwo": fortytwo_models,
"zerogpu": zerogpu_models,
"v0": v0_models,
"morph": morph_models,

View file

@ -1067,6 +1067,7 @@ openai_compatible_endpoints: Final[list[str]] = [
"https://api-inference.modelscope.cn/v1",
"https://api.moonshot.ai/v1",
"https://api.publicai.co/v1",
"https://api.fortytwo.network/v1",
"https://api.aiand.com/v1",
"https://api.synthetic.new/openai/v1",
"https://serverless.tensormesh.ai/v1",
@ -1137,6 +1138,7 @@ openai_compatible_providers: Final[list[str]] = [
"novita",
"meta_llama",
"publicai", # PublicAI - JSON-configured provider
"fortytwo",
"aiand", # ai& - JSON-configured provider
"synthetic", # Synthetic - JSON-configured provider
"tensormesh", # Tensormesh - JSON-configured provider

View file

@ -11,6 +11,18 @@
"convert_content_list_to_string": true
}
},
"fortytwo": {
"base_url": "https://api.fortytwo.network/v1",
"api_key_env": "FORTYTWO_API_KEY",
"api_base_env": "FORTYTWO_API_BASE",
"provider_info": {
"supports_function_calling": true,
"supports_reasoning": true
},
"supported_endpoints": [
"/v1/chat/completions"
]
},
"helicone": {
"base_url": "https://ai-gateway.helicone.ai/",
"api_key_env": "HELICONE_API_KEY"

View file

@ -29168,6 +29168,22 @@
"output_cost_per_token": 0.0,
"source": "https://fireworks.ai/pricing"
},
"fortytwo/qwen/qwen3.8-27b": {
"cache_read_input_token_cost": 8.5e-08,
"input_cost_per_token": 4.25e-07,
"litellm_provider": "fortytwo",
"max_input_tokens": 262144,
"max_output_tokens": 235929,
"mode": "chat",
"output_cost_per_token": 2.55e-06,
"source": "https://docs.fortytwo.network/docs/models",
"supports_function_calling": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_tool_choice": true,
"supports_vision": true
},
"friendliai/zai-org/GLM-5.3-Flash": {
"litellm_provider": "friendliai",
"max_input_tokens": 1048576,

View file

@ -5509,6 +5509,32 @@
"a2a": true,
"generateContent": true
}
},
"fortytwo": {
"display_name": "Fortytwo (`fortytwo`)",
"url": "https://docs.litellm.ai/docs/providers/fortytwo",
"endpoints": {
"chat_completions": true,
"responses": true,
"text_completion": true,
"embeddings": false,
"image_generations": false,
"image_edits": false,
"audio_transcriptions": false,
"audio_speech": false,
"rerank": false,
"ocr": false,
"search": false,
"moderations": false,
"realtime": false,
"video_generations": false,
"batches": false,
"messages": true,
"interactions": true,
"systemone": false,
"a2a": true,
"generateContent": true
}
}
},
"endpoints": {

View file

@ -27,6 +27,34 @@
],
"default_model_placeholder": "aiand/openai/gpt-oss-120b"
},
{
"provider": "FORTYTWO",
"provider_display_name": "Fortytwo",
"litellm_provider": "fortytwo",
"credential_fields": [
{
"key": "api_base",
"label": "API Base",
"placeholder": "https://api.fortytwo.network/v1",
"tooltip": null,
"required": false,
"field_type": "text",
"options": null,
"default_value": null
},
{
"key": "api_key",
"label": "API Key",
"placeholder": null,
"tooltip": null,
"required": true,
"field_type": "password",
"options": null,
"default_value": null
}
],
"default_model_placeholder": "fortytwo/qwen/qwen3.8-27b"
},
{
"provider": "SCALEDOWN",
"provider_display_name": "ScaleDown",

View file

@ -4773,6 +4773,7 @@ class LlmProviders(str, Enum):
LANGFLOW = "langflow"
MICROSOFT_365_COPILOT = "microsoft_365_copilot"
MINIMAX = "minimax"
FORTYTWO = "fortytwo"
SYNTHETIC = "synthetic"
APERTIS = "apertis"
NANOGPT = "nano-gpt"

View file

@ -29168,6 +29168,22 @@
"output_cost_per_token": 0.0,
"source": "https://fireworks.ai/pricing"
},
"fortytwo/qwen/qwen3.8-27b": {
"cache_read_input_token_cost": 8.5e-08,
"input_cost_per_token": 4.25e-07,
"litellm_provider": "fortytwo",
"max_input_tokens": 262144,
"max_output_tokens": 235929,
"mode": "chat",
"output_cost_per_token": 2.55e-06,
"source": "https://docs.fortytwo.network/docs/models",
"supports_function_calling": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_tool_choice": true,
"supports_vision": true
},
"friendliai/zai-org/GLM-5.3-Flash": {
"litellm_provider": "friendliai",
"max_input_tokens": 1048576,

View file

@ -5509,6 +5509,32 @@
"a2a": true,
"generateContent": true
}
},
"fortytwo": {
"display_name": "Fortytwo (`fortytwo`)",
"url": "https://docs.litellm.ai/docs/providers/fortytwo",
"endpoints": {
"chat_completions": true,
"responses": true,
"text_completion": true,
"embeddings": false,
"image_generations": false,
"image_edits": false,
"audio_transcriptions": false,
"audio_speech": false,
"rerank": false,
"ocr": false,
"search": false,
"moderations": false,
"realtime": false,
"video_generations": false,
"batches": false,
"messages": true,
"interactions": true,
"systemone": false,
"a2a": true,
"generateContent": true
}
}
},
"endpoints": {

View file

@ -0,0 +1,229 @@
import json
from collections.abc import Iterator
from pathlib import Path
from typing import Final
import pytest
import respx
import litellm
from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider
from litellm.proxy.auth.model_checks import get_provider_models
MODEL: Final = "fortytwo/qwen/qwen3.8-27b"
API_BASE: Final = "https://api.fortytwo.network/v1"
MESSAGES: Final = [{"role": "user", "content": "hello"}]
USAGE: Final = {
"prompt_tokens": 1000,
"completion_tokens": 200,
"total_tokens": 1200,
"prompt_tokens_details": {"cached_tokens": 400},
}
@pytest.fixture(autouse=True)
def fortytwo_environment(monkeypatch: pytest.MonkeyPatch) -> Iterator[None]:
monkeypatch.setenv("FORTYTWO_API_KEY", "fortytwo-unit-test-key")
monkeypatch.delenv("FORTYTWO_API_BASE", raising=False)
yield
def _completion_body() -> dict[str, object]:
return {
"id": "chatcmpl-fortytwo-test",
"object": "chat.completion",
"created": 1234567890,
"model": "qwen/qwen3.8-27b",
"choices": [
{
"index": 0,
"message": {"role": "assistant", "content": "answer", "reasoning": "reasoning text"},
"finish_reason": "stop",
}
],
"usage": USAGE,
}
@pytest.mark.parametrize("override", [None, "https://fortytwo-unit.invalid/v1"])
def test_completion_uses_fortytwo_environment_and_strips_only_provider_prefix(
monkeypatch: pytest.MonkeyPatch, override: str | None
) -> None:
if override is not None:
monkeypatch.setenv("FORTYTWO_API_BASE", override)
monkeypatch.setenv("OPENAI_API_KEY", "unrelated-openai-unit-key")
with respx.mock() as upstream:
route: Final = upstream.post(f"{override or API_BASE}/chat/completions").respond(200, json=_completion_body())
response: Final = litellm.completion(model=MODEL, messages=MESSAGES, num_retries=0)
request: Final = route.calls.last.request
assert request.headers["authorization"] == "Bearer fortytwo-unit-test-key"
assert json.loads(request.content) == {"model": "qwen/qwen3.8-27b", "messages": MESSAGES}
assert response.choices[0].message.content == "answer"
assert response.choices[0].message.reasoning_content == "reasoning text"
def test_fortytwo_key_in_generic_openai_environment_is_used_when_own_key_is_absent(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.delenv("FORTYTWO_API_KEY")
monkeypatch.setenv("OPENAI_API_KEY", "fortytwo-key-in-openai-env")
with respx.mock() as upstream:
route: Final = upstream.post(f"{API_BASE}/chat/completions").respond(200, json=_completion_body())
response: Final = litellm.completion(model=MODEL, messages=MESSAGES, num_retries=0)
assert len(route.calls) == 1
assert route.calls.last.request.headers["authorization"] == "Bearer fortytwo-key-in-openai-env"
assert json.loads(route.calls.last.request.content) == {"model": "qwen/qwen3.8-27b", "messages": MESSAGES}
assert response.choices[0].message.content == "answer"
def test_rejected_key_preserves_authentication_failure() -> None:
with respx.mock() as upstream:
route: Final = upstream.post(f"{API_BASE}/chat/completions").respond(
401,
json={"error": {"message": "Invalid API key", "type": "authentication_error", "code": "invalid_api_key"}},
)
with pytest.raises(litellm.AuthenticationError):
litellm.completion(model=MODEL, messages=MESSAGES, num_retries=0)
assert len(route.calls) == 1
@pytest.mark.parametrize("limit_name", ["max_tokens", "max_completion_tokens"])
def test_output_limit_reaches_upstream(limit_name: str) -> None:
with respx.mock() as upstream:
route: Final = upstream.post(f"{API_BASE}/chat/completions").respond(200, json=_completion_body())
litellm.completion(model=MODEL, messages=MESSAGES, num_retries=0, **{limit_name: 17})
assert json.loads(route.calls.last.request.content) == {
"model": "qwen/qwen3.8-27b",
"messages": MESSAGES,
limit_name: 17,
}
def test_stream_preserves_reasoning_answer_and_final_usage() -> None:
events: Final = (
{"choices": [{"index": 0, "delta": {"role": "assistant", "reasoning": "think"}, "finish_reason": None}]},
{"choices": [{"index": 0, "delta": {"content": "answer"}, "finish_reason": None}]},
{"choices": [{"index": 0, "delta": {}, "finish_reason": "stop"}]},
{"choices": [], "usage": USAGE},
)
wire: Final = (
"".join(
"data: "
+ json.dumps(
{
"id": "chatcmpl-fortytwo-test",
"object": "chat.completion.chunk",
"created": 1234567890,
"model": "qwen/qwen3.8-27b",
**event,
}
)
+ "\n\n"
for event in events
)
+ "data: [DONE]\n\n"
)
with respx.mock() as upstream:
upstream.post(f"{API_BASE}/chat/completions").respond(
200, text=wire, headers={"content-type": "text/event-stream"}
)
chunks: Final = tuple(
litellm.completion(
model=MODEL, messages=MESSAGES, stream=True, stream_options={"include_usage": True}, num_retries=0
)
)
assert (
"".join(getattr(chunk.choices[0].delta, "reasoning_content", "") or "" for chunk in chunks if chunk.choices)
== "think"
)
assert "".join(chunk.choices[0].delta.content or "" for chunk in chunks if chunk.choices) == "answer"
assert chunks[-1].usage.prompt_tokens == USAGE["prompt_tokens"]
assert chunks[-1].usage.completion_tokens == USAGE["completion_tokens"]
def test_tool_call_request_and_response_preserve_function_contract() -> None:
tool: Final = {
"type": "function",
"function": {"name": "city", "parameters": {"type": "object", "properties": {"name": {"type": "string"}}}},
}
tool_call: Final = {
"id": "call-city",
"type": "function",
"function": {"name": "city", "arguments": '{"name":"London"}'},
}
body: Final = {
**_completion_body(),
"choices": [
{
"index": 0,
"message": {"role": "assistant", "content": None, "tool_calls": [tool_call]},
"finish_reason": "tool_calls",
}
],
}
with respx.mock() as upstream:
route: Final = upstream.post(f"{API_BASE}/chat/completions").respond(200, json=body)
response: Final = litellm.completion(
model=MODEL, messages=MESSAGES, tools=[tool], tool_choice="required", num_retries=0
)
assert json.loads(route.calls.last.request.content) == {
"model": "qwen/qwen3.8-27b",
"messages": MESSAGES,
"tools": [tool],
"tool_choice": "required",
}
assert response.choices[0].message.tool_calls[0].model_dump() == tool_call
def test_response_cost_uses_cached_and_uncached_catalogue_rates() -> None:
with respx.mock() as upstream:
upstream.post(f"{API_BASE}/chat/completions").respond(200, json=_completion_body())
response: Final = litellm.completion(model=MODEL, messages=MESSAGES, num_retries=0)
rates: Final = litellm.model_cost[MODEL]
expected: Final = (
600 * rates["input_cost_per_token"]
+ 400 * rates["cache_read_input_token_cost"]
+ 200 * rates["output_cost_per_token"]
)
assert response._hidden_params["response_cost"] == pytest.approx(expected)
def test_responses_call_uses_chat_bridge_and_keeps_responses_shape() -> None:
with respx.mock() as upstream:
route: Final = upstream.post(f"{API_BASE}/chat/completions").respond(200, json=_completion_body())
response: Final = litellm.responses(model=MODEL, input="hello", num_retries=0)
assert json.loads(route.calls.last.request.content)["model"] == "qwen/qwen3.8-27b"
assert response.output[-1].content[0].text == "answer"
assert response.usage.input_tokens == USAGE["prompt_tokens"]
assert response.usage.output_tokens == USAGE["completion_tokens"]
def test_messages_call_uses_chat_bridge_and_keeps_messages_shape() -> None:
with respx.mock() as upstream:
route: Final = upstream.post(f"{API_BASE}/chat/completions").respond(200, json=_completion_body())
response: Final = litellm.anthropic.messages.create(model=MODEL, messages=MESSAGES, max_tokens=17)
assert json.loads(route.calls.last.request.content)["model"] == "qwen/qwen3.8-27b"
assert response["content"][-1] == {"type": "text", "text": "answer"}
assert response["usage"]["input_tokens"] + response["usage"]["cache_read_input_tokens"] == USAGE["prompt_tokens"]
assert response["usage"]["output_tokens"] == USAGE["completion_tokens"]
def test_discovery_and_bundled_catalogue_match_runtime_model() -> None:
root: Final = Path(__file__).resolve().parents[4]
cost_map: Final = json.loads((root / "model_prices_and_context_window.json").read_text())
bundled: Final = json.loads((root / "litellm/model_prices_and_context_window_backup.json").read_text())
assert cost_map[MODEL] == bundled[MODEL] == litellm.model_cost[MODEL]
assert MODEL in get_provider_models(provider="fortytwo")
assert MODEL in litellm.model_list
assert MODEL in litellm.models_by_provider["fortytwo"]
assert get_llm_provider("publicai/a-test-model")[0:2] == ("a-test-model", "publicai")
@pytest.mark.parametrize("effort", ["none", "minimal", "low", "medium", "high", "xhigh", "max"])
def test_reasoning_effort_is_forwarded_without_remapping(effort: str) -> None:
with respx.mock() as upstream:
route: Final = upstream.post(f"{API_BASE}/chat/completions").respond(200, json=_completion_body())
litellm.completion(model=MODEL, messages=MESSAGES, reasoning_effort=effort, num_retries=0)
assert json.loads(route.calls.last.request.content)["reasoning_effort"] == effort