This commit is contained in:
Jonas Boos 2026-11-07 03:03:05 +00:00 • committed by GitHub
commit bbda486e56
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 407 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) | ✅ | ✅ | ✅ | | | | | | | |
| [Flonno (`flonno`)](https://docs.litellm.ai/docs/providers/flonno) | ✅ | ✅ | | | | | | | | |
| [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

@ -87,6 +87,7 @@ const OPENAI_COMPATIBLE_PROVIDERS: &[&str] = &[
"parasail",
"libertai",
"featherless_ai",
"flonno",
"nscale",
"nebius",
"dashscope",

View file

@ -690,6 +690,7 @@ kyma_models: Final[set[str]] = set() # mutable-ok: populated when the shared mo
streamlake_models: Final[Set[str]] = set()
wally_models: Final[Set[str]] = set()
zerogpu_models: Final[Set[str]] = set()
flonno_models: Final[Set[str]] = set()
v0_models: Set = set()
morph_models: Set = set()
opencode_go_models: Final[set[str]] = set() # mutable-ok: populated when the shared model catalog loads
@ -989,6 +990,8 @@ def _populate_provider_model_sets(model_cost_map: Mapping[str, Mapping[str, obje
wally_models.add(key)
elif value.get("litellm_provider") == "zerogpu":
zerogpu_models.add(key)
elif value.get("litellm_provider") == "flonno":
flonno_models.add(key)
elif value.get("litellm_provider") == "v0":
v0_models.add(key)
elif value.get("litellm_provider") == "morph":
@ -1170,6 +1173,7 @@ model_list = list(
| streamlake_models
| wally_models
| zerogpu_models
| flonno_models
| v0_models
| morph_models
| opencode_go_models
@ -1304,6 +1308,7 @@ def _build_models_by_provider() -> dict:
"streamlake": streamlake_models,
"wally": wally_models,
"zerogpu": zerogpu_models,
"flonno": flonno_models,
"v0": v0_models,
"morph": morph_models,
"opencode_go": opencode_go_models,

View file

@ -1097,6 +1097,7 @@ openai_compatible_endpoints: Final[list[str]] = [
"https://inference.runanywhere.ai/v1",
"https://gigachat.devices.sberbank.ru/api/v1",
"https://api.openinfer.ai/v1",
"https://www.flonno.com/v1",
"https://vanchin.streamlake.ai/api/gateway/v1/endpoints",
]
@ -1147,6 +1148,7 @@ openai_compatible_providers: Final[list[str]] = [
"parasail", # Parasail - JSON-configured provider
"libertai", # LibertAI - JSON-configured provider
"featherless_ai",
"flonno",
"nscale",
"nebius",
"dashscope",

View file

@ -265,6 +265,20 @@
},
"supported_endpoints": ["/v1/chat/completions", "/v1/responses", "/v1/messages"]
},
"flonno": {
"base_url": "https://www.flonno.com/v1",
"api_key_env": "FLONNO_API_KEY",
"api_base_env": "FLONNO_API_BASE",
"provider_info": {
"supports_function_calling": true,
"supports_vision": true,
"supports_reasoning": true
},
"supported_endpoints": [
"/v1/chat/completions",
"/v1/messages"
]
},
"inferenceapis": {
"base_url": "https://api.inferenceapis.com/v1",
"api_key_env": "INFERENCEAPIS_API_KEY",

View file

@ -2797,6 +2797,10 @@ def _complete_custom_openai(
raise litellm.AuthenticationError(
message="Set BIGMODEL_API_KEY or pass api_key", llm_provider="bigmodel", model=model
)
if custom_llm_provider == "flonno" and not api_key:
raise litellm.AuthenticationError(
message="Set FLONNO_API_KEY or pass api_key", llm_provider="flonno", model=model
)
organization = (
organization
or (None if custom_llm_provider == "bigmodel" else litellm.organization or get_secret("OPENAI_ORGANIZATION"))

View file

@ -29168,6 +29168,22 @@
"output_cost_per_token": 0.0,
"source": "https://fireworks.ai/pricing"
},
"flonno/qwen3.8-27b": {
"cache_read_input_token_cost": 3e-08,
"input_cost_per_token": 3e-08,
"litellm_provider": "flonno",
"max_input_tokens": 131072,
"max_output_tokens": 32768,
"max_tokens": 32768,
"mode": "chat",
"output_cost_per_token": 2.5e-07,
"source": "https://www.flonno.com/pricing.json",
"supports_function_calling": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_tool_choice": true,
"supports_vision": true
},
"friendliai/zai-org/GLM-5.3-Flash": {
"litellm_provider": "friendliai",
"max_input_tokens": 1048576,

View file

@ -1686,6 +1686,32 @@
"generateContent": true
}
},
"flonno": {
"display_name": "Flonno (`flonno`)",
"url": "https://docs.litellm.ai/docs/providers/flonno",
"endpoints": {
"chat_completions": true,
"responses": false,
"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
}
},
"firecrawl": {
"display_name": "Firecrawl (`firecrawl`)",
"url": "https://docs.litellm.ai/docs/search/firecrawl",

View file

@ -1701,6 +1701,34 @@
],
"default_model_placeholder": "gpt-3.5-turbo"
},
{
"provider": "Flonno",
"provider_display_name": "Flonno",
"litellm_provider": "flonno",
"credential_fields": [
{
"key": "api_base",
"label": "API Base",
"placeholder": "https://www.flonno.com/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": "flonno/qwen3.8-27b"
},
{
"provider": "FRIENDLIAI",
"provider_display_name": "Friendliai",

View file

@ -4707,6 +4707,7 @@ class LlmProviders(str, Enum):
FIREWORKS_AI = "fireworks_ai"
FRIENDLIAI = "friendliai"
FEATHERLESS_AI = "featherless_ai"
FLONNO = "flonno"
WATSONX = "watsonx"
SCALEDOWN = "scaledown"
WATSONX_TEXT = "watsonx_text"

View file

@ -29168,6 +29168,22 @@
"output_cost_per_token": 0.0,
"source": "https://fireworks.ai/pricing"
},
"flonno/qwen3.8-27b": {
"cache_read_input_token_cost": 3e-08,
"input_cost_per_token": 3e-08,
"litellm_provider": "flonno",
"max_input_tokens": 131072,
"max_output_tokens": 32768,
"max_tokens": 32768,
"mode": "chat",
"output_cost_per_token": 2.5e-07,
"source": "https://www.flonno.com/pricing.json",
"supports_function_calling": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_tool_choice": true,
"supports_vision": true
},
"friendliai/zai-org/GLM-5.3-Flash": {
"litellm_provider": "friendliai",
"max_input_tokens": 1048576,

View file

@ -1686,6 +1686,32 @@
"generateContent": true
}
},
"flonno": {
"display_name": "Flonno (`flonno`)",
"url": "https://docs.litellm.ai/docs/providers/flonno",
"endpoints": {
"chat_completions": true,
"responses": false,
"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
}
},
"firecrawl": {
"display_name": "Firecrawl (`firecrawl`)",
"url": "https://docs.litellm.ai/docs/search/firecrawl",

View file

@ -0,0 +1,238 @@
import json
from collections.abc import Mapping
from pathlib import Path
from typing import Final
import httpx
import pytest
import respx
from openai import OpenAI
import litellm
from litellm.caching.llm_caching_handler import LLMClientCache
from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider
from litellm.llms.openai_like.json_loader import JSONProviderRegistry
from litellm.proxy.auth.model_checks import get_provider_models
from litellm.types.utils import LlmProviders
FLONNO_API_BASE: Final = "https://www.flonno.com/v1"
FLONNO_MODEL: Final = "qwen3.8-27b"
FLONNO_PREFIXED: Final = f"flonno/{FLONNO_MODEL}"
_REPO_ROOT: Final = Path(__file__).resolve().parents[4]
_PRIMARY_PRICES: Final = json.loads((_REPO_ROOT / "model_prices_and_context_window.json").read_text())
_BACKUP_PRICES: Final = json.loads((_REPO_ROOT / "litellm" / "model_prices_and_context_window_backup.json").read_text())
class _RecordingFlonno:
def __init__(self, usage: Mapping[str, object]) -> None:
self.usage: Final = usage
self.requests: tuple[httpx.Request, ...] = ()
def respond(self, request: httpx.Request) -> httpx.Response:
self.requests = (*self.requests, request)
return httpx.Response(
200,
json={
"id": "chatcmpl-flonno",
"object": "chat.completion",
"created": 1234567890,
"model": json.loads(request.content)["model"],
"choices": [
{
"index": 0,
"message": {"role": "assistant", "content": "hi"},
"finish_reason": "stop",
}
],
"usage": dict(self.usage),
},
)
def client(self) -> OpenAI:
return OpenAI(
api_key="flonno-test-key",
base_url=FLONNO_API_BASE,
http_client=httpx.Client(transport=httpx.MockTransport(self.respond)),
)
def test_flonno_in_provider_list_and_json_registry():
assert LlmProviders.FLONNO.value == "flonno"
assert "flonno" in litellm.provider_list
assert JSONProviderRegistry.exists("flonno")
provider = JSONProviderRegistry.get("flonno")
assert provider is not None
assert provider.base_url == FLONNO_API_BASE
assert provider.api_key_env == "FLONNO_API_KEY"
assert "/v1/chat/completions" in provider.supported_endpoints
assert "/v1/messages" in provider.supported_endpoints
assert "/v1/responses" not in provider.supported_endpoints
def test_flonno_in_openai_compatible_providers():
from litellm.constants import openai_compatible_providers
assert "flonno" in openai_compatible_providers
def test_prefixed_model_reaches_flonno_chat_completions_without_the_prefix():
flonno: Final = _RecordingFlonno({"prompt_tokens": 3, "completion_tokens": 1, "total_tokens": 4})
with flonno.client() as client:
litellm.completion(
model=FLONNO_PREFIXED,
messages=[{"role": "user", "content": "hello"}],
api_key="flonno-test-key",
client=client,
)
assert len(flonno.requests) == 1
assert str(flonno.requests[0].url) == f"{FLONNO_API_BASE}/chat/completions"
assert json.loads(flonno.requests[0].content) == {
"model": FLONNO_MODEL,
"messages": [{"role": "user", "content": "hello"}],
}
@pytest.mark.asyncio
async def test_anthropic_messages_request_goes_to_flonno_natively(
monkeypatch: pytest.MonkeyPatch,
):
monkeypatch.setattr(litellm, "disable_aiohttp_transport", True)
monkeypatch.setattr(litellm, "in_memory_llm_clients_cache", LLMClientCache())
with respx.mock() as upstream:
route: Final = upstream.post(f"{FLONNO_API_BASE}/messages").respond(
200,
json={
"id": "msg_flonno",
"type": "message",
"role": "assistant",
"model": FLONNO_MODEL,
"content": [{"type": "text", "text": "Hello from Flonno"}],
"stop_reason": "end_turn",
"stop_sequence": None,
"usage": {"input_tokens": 4, "output_tokens": 3},
},
)
response: Final = await litellm.anthropic.messages.acreate(
model=FLONNO_PREFIXED,
messages=[{"role": "user", "content": "Say hello"}],
max_tokens=32,
api_key="sk-test",
)
request: Final = route.calls.last.request
body: Final = json.loads(request.content)
assert request.headers["authorization"] == "Bearer sk-test"
assert body["model"] == FLONNO_MODEL
assert body["messages"] == [{"role": "user", "content": "Say hello"}]
assert response["content"][0]["text"] == "Hello from Flonno"
def test_flonno_key_and_base_come_from_flonno_env_vars(monkeypatch: pytest.MonkeyPatch):
monkeypatch.setenv("FLONNO_API_KEY", "flonno-env-key")
monkeypatch.delenv("FLONNO_API_BASE", raising=False)
assert get_llm_provider(model=FLONNO_PREFIXED) == (
FLONNO_MODEL,
"flonno",
"flonno-env-key",
FLONNO_API_BASE,
)
monkeypatch.setenv("FLONNO_API_BASE", "https://flonno.internal.example/v1")
assert get_llm_provider(model=FLONNO_PREFIXED)[3] == "https://flonno.internal.example/v1"
def test_flonno_wildcard_lists_every_flonno_catalog_model():
catalog_models: Final = sorted(
key for key, row in litellm.model_cost.items() if row.get("litellm_provider") == "flonno"
)
assert catalog_models == [FLONNO_PREFIXED]
assert sorted(get_provider_models("flonno") or []) == catalog_models
def test_catalog_row_matches_its_backup_and_fits_its_context():
row: Final = _PRIMARY_PRICES[FLONNO_PREFIXED]
assert row == _BACKUP_PRICES[FLONNO_PREFIXED]
assert row["mode"] == "chat"
assert row["max_output_tokens"] <= row["max_input_tokens"]
assert row["input_cost_per_token"] == 3e-08
assert row["output_cost_per_token"] == 2.5e-07
assert litellm.get_model_info(FLONNO_PREFIXED)["litellm_provider"] == "flonno"
def test_flonno_completion_never_borrows_openai_api_key(monkeypatch: pytest.MonkeyPatch):
monkeypatch.delenv("FLONNO_API_KEY", raising=False)
monkeypatch.setenv("OPENAI_API_KEY", "sk-openai-should-not-leak")
monkeypatch.setattr(litellm, "api_key", None)
monkeypatch.setattr(litellm, "openai_key", None)
with respx.mock(assert_all_called=False) as upstream:
route: Final = upstream.post(f"{FLONNO_API_BASE}/chat/completions").respond(
200,
json={
"id": "chatcmpl-flonno",
"object": "chat.completion",
"created": 1,
"model": FLONNO_MODEL,
"choices": [
{
"index": 0,
"message": {"role": "assistant", "content": "hi"},
"finish_reason": "stop",
}
],
"usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2},
},
)
with pytest.raises(litellm.AuthenticationError, match="FLONNO_API_KEY"):
litellm.completion(
model=FLONNO_PREFIXED,
messages=[{"role": "user", "content": "hello"}],
)
assert route.call_count == 0
def test_flonno_completion_uses_flonno_api_key_from_env(monkeypatch: pytest.MonkeyPatch):
monkeypatch.setenv("FLONNO_API_KEY", "flonno-env-key")
monkeypatch.setenv("OPENAI_API_KEY", "sk-openai-should-not-be-used")
monkeypatch.setattr(litellm, "api_key", None)
monkeypatch.setattr(litellm, "openai_key", None)
monkeypatch.setattr(litellm, "disable_aiohttp_transport", True)
monkeypatch.setattr(litellm, "in_memory_llm_clients_cache", LLMClientCache())
with respx.mock() as upstream:
route: Final = upstream.post(f"{FLONNO_API_BASE}/chat/completions").respond(
200,
json={
"id": "chatcmpl-flonno",
"object": "chat.completion",
"created": 1,
"model": FLONNO_MODEL,
"choices": [
{
"index": 0,
"message": {"role": "assistant", "content": "hi"},
"finish_reason": "stop",
}
],
"usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2},
},
)
response: Final = litellm.completion(
model=FLONNO_PREFIXED,
messages=[{"role": "user", "content": "hello"}],
)
request: Final = route.calls.last.request
assert route.call_count == 1
assert str(request.url) == f"{FLONNO_API_BASE}/chat/completions"
assert request.headers["authorization"] == "Bearer flonno-env-key"
assert "sk-openai-should-not-be-used" not in request.headers.get("authorization", "")
assert response.choices[0].message.content == "hi"

View file

@ -449,6 +449,26 @@ def test_zerogpu_add_model_form_defaults_resolve_to_a_priced_zerogpu_model():
assert fields_by_key["api_key"]["field_type"] == "password"
def test_flonno_add_model_form_defaults_resolve_to_a_priced_flonno_model():
import litellm
from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider
app_instance: Final = FastAPI()
app_instance.include_router(router)
providers: Final = TestClient(app_instance).get("/public/providers/fields").json()
flonno: Final = next(p for p in providers if p["litellm_provider"] == "flonno")
fields_by_key: Final = {f["key"]: f for f in flonno["credential_fields"]}
placeholder: Final = flonno["default_model_placeholder"]
_, provider, _, api_base = get_llm_provider(model=placeholder)
assert provider == "flonno"
assert api_base == fields_by_key["api_base"]["placeholder"]
assert litellm.model_cost[placeholder]["litellm_provider"] == "flonno"
assert fields_by_key["api_key"]["required"] is True
assert fields_by_key["api_key"]["field_type"] == "password"
def test_qwen_mainland_provider_fields_carry_the_qianwen_brand():
app_instance = FastAPI()
app_instance.include_router(router)

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View file

@ -422,6 +422,10 @@ describe("provider_info_helpers", () => {
expect(getPlaceholder("ZeroGPU")).toBe("zerogpu/gpt-oss-120b");
});
it("should return flonno/qwen3.8-27b placeholder for Flonno provider", () => {
expect(getPlaceholder("Flonno")).toBe("flonno/qwen3.8-27b");
});
it("should return a chatgpt/ placeholder for the CHATGPT dropdown key", () => {
expect(getPlaceholder("CHATGPT")).toBe("chatgpt/gpt-5.4");
});

View file

@ -31,6 +31,7 @@ import groqLogo from "../../public/assets/logos/groq.svg";
import huggingfaceLogo from "../../public/assets/logos/huggingface.svg";
import hyperbolicLogo from "../../public/assets/logos/hyperbolic.svg";
import hostingerLogo from "../../public/assets/logos/hostinger.png";
import flonnoLogo from "../../public/assets/logos/flonno.png";
import infinityLogo from "../../public/assets/logos/infinity.png";
import jinaLogo from "../../public/assets/logos/jina.png";
import lambdaLogo from "../../public/assets/logos/lambda.svg";
@ -124,6 +125,7 @@ export enum Providers {
FalAI = "Fal AI",
FEATHERLESS_AI = "Featherless Ai",
FireworksAI = "Fireworks AI",
Flonno = "Flonno",
FRIENDLIAI = "Friendliai",
GALADRIEL = "Galadriel",
GIGACHAT = "GigaChat",
@ -260,6 +262,7 @@ export const provider_map: Record<string, string> = {
FalAI: "fal_ai",
FEATHERLESS_AI: "featherless_ai",
FireworksAI: "fireworks_ai",
Flonno: "flonno",
FRIENDLIAI: "friendliai",
GALADRIEL: "galadriel",
GIGACHAT: "gigachat",
@ -392,6 +395,7 @@ export const providerLogoMap: Partial<Record<Providers, string>> = {
[Providers.FalAI]: falAiLogo.src,
[Providers.FEATHERLESS_AI]: featherlessLogo.src,
[Providers.FireworksAI]: fireworksLogo.src,
[Providers.Flonno]: flonnoLogo.src,
[Providers.FRIENDLIAI]: friendliLogo.src,
[Providers.GIGACHAT]: gigachatLogo.src,
[Providers.GITHUB_COPILOT]: githubCopilotLogo.src,
@ -535,6 +539,7 @@ const providerPlaceholderMap: Partial<Record<Providers, string>> = {
[Providers.ZAI]: "zai/glm-4.5",
[Providers.BIGMODEL]: "bigmodel/glm-5.3",
[Providers.ZeroGPU]: "zerogpu/gpt-oss-120b",
[Providers.Flonno]: "flonno/qwen3.8-27b",
};
export const getPlaceholder = (selectedProvider: string): string => {