From 1341e4f77e54136d4e3b847e5abe39595d79c58e Mon Sep 17 00:00:00 2001 From: Stan Date: Thu, 3 Sep 2026 05:02:07 +0500 Subject: [PATCH 1/5] fix(aiml): tag aimlapi.com requests and read the provider's own key env var Chat completions for the aiml provider go out through the OpenAI SDK path, which never calls the provider config's validate_environment, so the provider had no way to put anything on the wire beyond the api base and the key. Every request therefore reached the provider untagged, and the provider could not tell LiteLLM traffic apart from anything else. Merging the headers in the chat dispatch is the only spot on that path where both the provider and the resolved api base are known. The headers are keyed on the request host, not on the provider name, so they cannot ride along to a different backend when someone points AIML_API_BASE at their own gateway, and caller supplied headers still win on a key clash. The provider documents AIMLAPI_API_KEY everywhere else, while LiteLLM has only ever read AIML_API_KEY. Renaming would break existing configs, so AIML_API_KEY stays primary and AIMLAPI_API_KEY joins the AIMLAPI_KEY fallback the image generation config already had. --- litellm/llms/aiml/chat/transformation.py | 11 +- litellm/llms/aiml/common_utils.py | 47 +++++++ .../aiml/image_generation/transformation.py | 18 +-- litellm/main.py | 5 + .../chat/test_aiml_chat_transformation.py | 116 ++++++++++++++++++ .../llms/aiml/test_aiml_common_utils.py | 88 +++++++++++++ 6 files changed, 270 insertions(+), 15 deletions(-) create mode 100644 litellm/llms/aiml/common_utils.py create mode 100644 tests/test_litellm/llms/aiml/chat/test_aiml_chat_transformation.py create mode 100644 tests/test_litellm/llms/aiml/test_aiml_common_utils.py diff --git a/litellm/llms/aiml/chat/transformation.py b/litellm/llms/aiml/chat/transformation.py index 55bd754fd40..1a564b1ac3b 100644 --- a/litellm/llms/aiml/chat/transformation.py +++ b/litellm/llms/aiml/chat/transformation.py @@ -1,7 +1,7 @@ from typing import Final +from litellm.llms.aiml.common_utils import get_aiml_api_base, get_aiml_api_key from litellm.llms.openai.chat.gpt_transformation import OpenAIGPTConfig -from litellm.secret_managers.main import get_secret_str class AIMLChatConfig(OpenAIGPTConfig): @@ -12,9 +12,6 @@ def custom_llm_provider(self) -> str | None: def _get_openai_compatible_provider_info( self, api_base: str | None, api_key: str | None ) -> tuple[str | None, str | None]: - # AIML is openai compatible, we just need to set the api_base - api_base = ( - api_base or get_secret_str("AIML_API_BASE") or "https://api.aimlapi.com/v1" # Default AIML API base URL - ) - dynamic_api_key: Final = api_key or get_secret_str("AIML_API_KEY") - return api_base, dynamic_api_key + resolved_api_base: Final = get_aiml_api_base(api_base) + dynamic_api_key: Final = get_aiml_api_key(api_key) + return resolved_api_base, dynamic_api_key diff --git a/litellm/llms/aiml/common_utils.py b/litellm/llms/aiml/common_utils.py new file mode 100644 index 00000000000..d59b40ba54a --- /dev/null +++ b/litellm/llms/aiml/common_utils.py @@ -0,0 +1,47 @@ +import types +from collections.abc import Mapping +from typing import Final +from urllib.parse import urlparse + +from litellm.secret_managers.main import get_secret_str + +AIML_DEFAULT_API_BASE: Final = "https://api.aimlapi.com/v1" + +AIML_ATTRIBUTION_HOSTS: Final = frozenset({"api.aimlapi.com"}) + +AIML_ATTRIBUTION_HEADERS: Final[Mapping[str, str]] = types.MappingProxyType( + { + "HTTP-Referer": "https://github.com/BerriAI/litellm", + "X-Title": "LiteLLM", + "X-AIMLAPI-Partner-ID": "part_litellm", + "X-AIMLAPI-Source": "agent/litellm", + } +) + +_NO_ATTRIBUTION: Final[Mapping[str, str]] = types.MappingProxyType({}) + + +def get_aiml_api_key(api_key: str | None = None) -> str | None: + return ( + api_key or get_secret_str("AIML_API_KEY") or get_secret_str("AIMLAPI_API_KEY") or get_secret_str("AIMLAPI_KEY") + ) + + +def get_aiml_api_base(api_base: str | None = None) -> str: + return api_base or get_secret_str("AIML_API_BASE") or AIML_DEFAULT_API_BASE + + +def aiml_attribution_headers(api_base: str | None) -> Mapping[str, str]: + host: Final = urlparse(get_aiml_api_base(api_base)).hostname + if host in AIML_ATTRIBUTION_HOSTS: + return AIML_ATTRIBUTION_HEADERS + return _NO_ATTRIBUTION + + +def with_aiml_attribution( + headers: Mapping[str, str] | None, api_base: str | None +) -> dict[str, str]: # mutable-ok: the OpenAI SDK extra_headers slot this feeds is typed as a plain dict + return { # mutable-ok: a fresh dict per request leaves the caller's headers and AIML_ATTRIBUTION_HEADERS unmutated + **aiml_attribution_headers(api_base), + **(headers or _NO_ATTRIBUTION), + } diff --git a/litellm/llms/aiml/image_generation/transformation.py b/litellm/llms/aiml/image_generation/transformation.py index 4f4cd074165..349ea5ce6ec 100644 --- a/litellm/llms/aiml/image_generation/transformation.py +++ b/litellm/llms/aiml/image_generation/transformation.py @@ -2,6 +2,7 @@ import httpx +from litellm.llms.aiml.common_utils import aiml_attribution_headers, get_aiml_api_key from litellm.llms.base_llm.image_generation.transformation import ( BaseImageGenerationConfig, ) @@ -129,15 +130,16 @@ def validate_environment( api_key: str | None = None, api_base: str | None = None, ) -> dict: - final_api_key: Final[str | None] = ( - api_key or get_secret_str("AIML_API_KEY") or get_secret_str("AIMLAPI_KEY") # Alternative name - ) + final_api_key: Final[str | None] = get_aiml_api_key(api_key) if not final_api_key: - raise ValueError("AIML_API_KEY or AIMLAPI_KEY is not set") - - headers["Authorization"] = f"Bearer {final_api_key}" - headers["Content-Type"] = "application/json" - return headers + raise ValueError("AIML_API_KEY, AIMLAPI_API_KEY or AIMLAPI_KEY is not set") + + return { # mutable-ok: a fresh dict leaves the caller's header mapping unmutated; the base signature returns dict + **aiml_attribution_headers(api_base or self.DEFAULT_BASE_URL), + **headers, + "Authorization": f"Bearer {final_api_key}", + "Content-Type": "application/json", + } def transform_image_generation_request( self, diff --git a/litellm/main.py b/litellm/main.py index 01c106adc7c..66fc32a9bca 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -2534,6 +2534,11 @@ def _complete_custom_openai( headers = headers or litellm.headers + if custom_llm_provider == "aiml": + from litellm.llms.aiml.common_utils import with_aiml_attribution + + headers = with_aiml_attribution(headers, api_base) + # Add GitHub Copilot headers (same as /responses endpoint does) if custom_llm_provider == "github_copilot": from litellm.llms.github_copilot.authenticator import Authenticator diff --git a/tests/test_litellm/llms/aiml/chat/test_aiml_chat_transformation.py b/tests/test_litellm/llms/aiml/chat/test_aiml_chat_transformation.py new file mode 100644 index 00000000000..1df8d0bbb01 --- /dev/null +++ b/tests/test_litellm/llms/aiml/chat/test_aiml_chat_transformation.py @@ -0,0 +1,116 @@ +import httpx +import pytest +from openai import OpenAI + +import litellm +from litellm.llms.aiml.chat.transformation import AIMLChatConfig + +ATTRIBUTION_KEYS = ("http-referer", "x-title", "x-aimlapi-partner-id", "x-aimlapi-source") + + +def _recording_client(base_url: str, sent: list[httpx.Request]) -> OpenAI: + def handler(request: httpx.Request) -> httpx.Response: + sent.append(request) + return httpx.Response( + 200, + json={ + "id": "chatcmpl-1", + "object": "chat.completion", + "created": 0, + "model": "openai/gpt-5-5", + "choices": [ + { + "index": 0, + "message": {"role": "assistant", "content": "ok"}, + "finish_reason": "stop", + } + ], + "usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2}, + }, + ) + + return OpenAI( + api_key="sk-test", + base_url=base_url, + http_client=httpx.Client(transport=httpx.MockTransport(handler)), + ) + + +def test_completion_sends_every_attribution_header_on_the_wire(monkeypatch): + monkeypatch.setenv("AIML_API_KEY", "sk-test") + sent: list[httpx.Request] = [] + + litellm.completion( + model="aiml/openai/gpt-5-5", + messages=[{"role": "user", "content": "hi"}], + client=_recording_client("https://api.aimlapi.com/v1", sent), + ) + + assert len(sent) == 1 + headers = sent[0].headers + assert headers["http-referer"] == "https://github.com/BerriAI/litellm" + assert headers["x-title"] == "LiteLLM" + assert headers["x-aimlapi-partner-id"] == "part_litellm" + assert headers["x-aimlapi-source"] == "agent/litellm" + + +def test_completion_keeps_caller_supplied_headers(monkeypatch): + monkeypatch.setenv("AIML_API_KEY", "sk-test") + sent: list[httpx.Request] = [] + + litellm.completion( + model="aiml/openai/gpt-5-5", + messages=[{"role": "user", "content": "hi"}], + extra_headers={"X-Title": "my-app", "X-Custom": "kept"}, + client=_recording_client("https://api.aimlapi.com/v1", sent), + ) + + headers = sent[0].headers + assert headers["x-title"] == "my-app" + assert headers["x-custom"] == "kept" + assert headers["x-aimlapi-partner-id"] == "part_litellm" + + +def test_completion_withholds_attribution_from_a_non_aimlapi_base(monkeypatch): + monkeypatch.setenv("AIML_API_KEY", "sk-test") + monkeypatch.setenv("AIML_API_BASE", "https://gateway.example.com/v1") + sent: list[httpx.Request] = [] + + litellm.completion( + model="aiml/openai/gpt-5-5", + messages=[{"role": "user", "content": "hi"}], + client=_recording_client("https://gateway.example.com/v1", sent), + ) + + headers = sent[0].headers + assert [key for key in ATTRIBUTION_KEYS if key in headers] == [] + + +def test_other_providers_never_receive_aimlapi_attribution(monkeypatch): + monkeypatch.setenv("OPENAI_API_KEY", "sk-test") + sent: list[httpx.Request] = [] + + litellm.completion( + model="openai/gpt-5-5", + messages=[{"role": "user", "content": "hi"}], + client=_recording_client("https://api.openai.com/v1", sent), + ) + + headers = sent[0].headers + assert [key for key in ATTRIBUTION_KEYS if key in headers] == [] + + +@pytest.mark.parametrize( + "env_key, env_value", + [("AIML_API_KEY", "from-aiml"), ("AIMLAPI_API_KEY", "from-aimlapi")], +) +def test_provider_info_reads_both_api_key_env_vars(monkeypatch, env_key, env_value): + monkeypatch.delenv("AIML_API_KEY", raising=False) + monkeypatch.delenv("AIMLAPI_API_KEY", raising=False) + monkeypatch.delenv("AIMLAPI_KEY", raising=False) + monkeypatch.setenv(env_key, env_value) + + api_base, api_key = AIMLChatConfig()._get_openai_compatible_provider_info(None, None) + + assert api_base == "https://api.aimlapi.com/v1" + assert api_key == env_value diff --git a/tests/test_litellm/llms/aiml/test_aiml_common_utils.py b/tests/test_litellm/llms/aiml/test_aiml_common_utils.py new file mode 100644 index 00000000000..0e6298348a9 --- /dev/null +++ b/tests/test_litellm/llms/aiml/test_aiml_common_utils.py @@ -0,0 +1,88 @@ +import re + +import pytest + +from litellm.llms.aiml.common_utils import ( + AIML_ATTRIBUTION_HEADERS, + aiml_attribution_headers, + get_aiml_api_base, + get_aiml_api_key, + with_aiml_attribution, +) + +PARTNER_ID_PATTERN = re.compile(r"^part_[A-Za-z0-9]{1,64}$") +SOURCE_PATTERN = re.compile(r"^(web|agent|mcp)/[a-z0-9-]{1,32}$") + + +def test_partner_id_matches_the_gateway_contract(): + assert PARTNER_ID_PATTERN.match(AIML_ATTRIBUTION_HEADERS["X-AIMLAPI-Partner-ID"]) + + +def test_source_matches_the_gateway_contract(): + assert SOURCE_PATTERN.match(AIML_ATTRIBUTION_HEADERS["X-AIMLAPI-Source"]) + + +def test_referer_and_title_identify_the_calling_project(): + assert AIML_ATTRIBUTION_HEADERS["HTTP-Referer"] == "https://github.com/BerriAI/litellm" + assert AIML_ATTRIBUTION_HEADERS["X-Title"] == "LiteLLM" + + +def test_attribution_is_sent_to_the_default_base(): + assert dict(aiml_attribution_headers(None)) == dict(AIML_ATTRIBUTION_HEADERS) + + +def test_attribution_is_withheld_from_a_proxy_fronting_the_api(): + assert dict(aiml_attribution_headers("https://gateway.example.com/aiml/v1")) == {} + + +def test_caller_headers_win_on_a_key_clash(): + merged = with_aiml_attribution({"X-Title": "my-app"}, None) + + assert merged["X-Title"] == "my-app" + assert merged["X-AIMLAPI-Partner-ID"] == AIML_ATTRIBUTION_HEADERS["X-AIMLAPI-Partner-ID"] + + +def test_merging_never_mutates_the_shared_constant(): + before = dict(AIML_ATTRIBUTION_HEADERS) + + first = with_aiml_attribution({"X-Title": "first"}, None) + first["X-Request-Id"] = "abc" + second = with_aiml_attribution(None, None) + + assert dict(AIML_ATTRIBUTION_HEADERS) == before + assert "X-Request-Id" not in second + + +def test_api_key_falls_back_across_both_env_var_spellings(monkeypatch): + monkeypatch.delenv("AIML_API_KEY", raising=False) + monkeypatch.setenv("AIMLAPI_API_KEY", "from-aimlapi-spelling") + + assert get_aiml_api_key() == "from-aimlapi-spelling" + + +def test_existing_env_var_still_takes_precedence(monkeypatch): + monkeypatch.setenv("AIML_API_KEY", "from-aiml-spelling") + monkeypatch.setenv("AIMLAPI_API_KEY", "from-aimlapi-spelling") + + assert get_aiml_api_key() == "from-aiml-spelling" + + +def test_explicit_api_key_beats_the_environment(monkeypatch): + monkeypatch.setenv("AIML_API_KEY", "from-env") + + assert get_aiml_api_key("explicit") == "explicit" + + +@pytest.mark.parametrize( + "env_base, expected", + [ + (None, "https://api.aimlapi.com/v1"), + ("https://gateway.example.com/v1", "https://gateway.example.com/v1"), + ], +) +def test_api_base_resolution(monkeypatch, env_base, expected): + monkeypatch.delenv("AIML_API_BASE", raising=False) + if env_base is not None: + monkeypatch.setenv("AIML_API_BASE", env_base) + + assert get_aiml_api_base() == expected From 7df5b435604c55a53c8270413c312b4fe01e97d2 Mon Sep 17 00:00:00 2001 From: Stan Date: Thu, 3 Sep 2026 05:02:16 +0500 Subject: [PATCH 2/5] fix(aiml): drop the retired dall-e entries and correct flux pro ultra pricing aiml/dall-e-2 and aiml/dall-e-3 are gone. POSTing either to /v1/images/generations comes back 404 with "Model 'openai/dall-e-3' is no longer available (retired 2026-06-09)", so shipping cost entries for them only tells a user the ids are supported when they are not. flux-pro/v1.1-ultra was priced at $0.063 an image, but a real generation reports usd_spent 0.078, so every ultra call was under counted by a quarter. The provider's catalog now lists that pair under blackforestlabs prefixed ids, which route to the same models at the same prices, so both spellings are carried: the old ones still work and removing them would silently drop existing users to $0 spend. Checked by POSTing each id to /v1/images/generations with no prompt, which answers 400 for a model that exists and 404 for one that does not. Worth knowing for anyone repeating this: flux-pro/v1.1 and flux-pro/v1.1-ultra appear in neither the id nor the aliases list of GET /v1/models, yet both still serve traffic, so the catalog alone is not enough to call an id dead. --- ...odel_prices_and_context_window_backup.json | 20 +++++++------------ .../spend_tracking/budget_reservation.py | 2 +- model_prices_and_context_window.json | 20 +++++++------------ 3 files changed, 15 insertions(+), 27 deletions(-) diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index 2846d12db6e..781b122b62d 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -143,26 +143,20 @@ "output_cost_per_token": 7e-07, "supports_system_messages": true }, - "aiml/dall-e-2": { + "aiml/blackforestlabs/flux-pro-1.1": { "litellm_provider": "aiml", - "metadata": { - "notes": "DALL-E 2 via AI/ML API - Reliable text-to-image generation" - }, "mode": "image_generation", - "output_cost_per_image": 0.026, - "source": "https://docs.aimlapi.com/", + "output_cost_per_image": 0.052, + "source": "https://api.aimlapi.com/v1/models?include=pricing", "supported_endpoints": [ "/v1/images/generations" ] }, - "aiml/dall-e-3": { + "aiml/blackforestlabs/flux-pro-1.1-ultra": { "litellm_provider": "aiml", - "metadata": { - "notes": "DALL-E 3 via AI/ML API - High-quality text-to-image generation" - }, "mode": "image_generation", - "output_cost_per_image": 0.052, - "source": "https://docs.aimlapi.com/", + "output_cost_per_image": 0.078, + "source": "https://api.aimlapi.com/v1/models?include=pricing", "supported_endpoints": [ "/v1/images/generations" ] @@ -190,7 +184,7 @@ "aiml/flux-pro/v1.1-ultra": { "litellm_provider": "aiml", "mode": "image_generation", - "output_cost_per_image": 0.063, + "output_cost_per_image": 0.078, "supported_endpoints": [ "/v1/images/generations" ] diff --git a/litellm/proxy/spend_tracking/budget_reservation.py b/litellm/proxy/spend_tracking/budget_reservation.py index 91d2ece7a51..f02421eb964 100644 --- a/litellm/proxy/spend_tracking/budget_reservation.py +++ b/litellm/proxy/spend_tracking/budget_reservation.py @@ -1202,7 +1202,7 @@ def _estimate_image_generation_cost( The "output" vs "input" cost-per-image naming is inconsistent across providers — OpenAI's dall-e-3 entry uses ``input_cost_per_image`` while - aiml/dall-e-3 uses ``output_cost_per_image`` — so both are summed. + aiml/blackforestlabs/flux-pro-1.1 uses ``output_cost_per_image`` — so both are summed. """ # Gate strictly on `mode`. Several chat and embedding models carry # ``input_cost_per_image`` / ``output_cost_per_image`` to price multimodal diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index 2846d12db6e..781b122b62d 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -143,26 +143,20 @@ "output_cost_per_token": 7e-07, "supports_system_messages": true }, - "aiml/dall-e-2": { + "aiml/blackforestlabs/flux-pro-1.1": { "litellm_provider": "aiml", - "metadata": { - "notes": "DALL-E 2 via AI/ML API - Reliable text-to-image generation" - }, "mode": "image_generation", - "output_cost_per_image": 0.026, - "source": "https://docs.aimlapi.com/", + "output_cost_per_image": 0.052, + "source": "https://api.aimlapi.com/v1/models?include=pricing", "supported_endpoints": [ "/v1/images/generations" ] }, - "aiml/dall-e-3": { + "aiml/blackforestlabs/flux-pro-1.1-ultra": { "litellm_provider": "aiml", - "metadata": { - "notes": "DALL-E 3 via AI/ML API - High-quality text-to-image generation" - }, "mode": "image_generation", - "output_cost_per_image": 0.052, - "source": "https://docs.aimlapi.com/", + "output_cost_per_image": 0.078, + "source": "https://api.aimlapi.com/v1/models?include=pricing", "supported_endpoints": [ "/v1/images/generations" ] @@ -190,7 +184,7 @@ "aiml/flux-pro/v1.1-ultra": { "litellm_provider": "aiml", "mode": "image_generation", - "output_cost_per_image": 0.063, + "output_cost_per_image": 0.078, "supported_endpoints": [ "/v1/images/generations" ] From c8798bd027de0164c368a57202724f897f3106c8 Mon Sep 17 00:00:00 2001 From: Stan Date: Thu, 3 Sep 2026 05:02:26 +0500 Subject: [PATCH 3/5] chore(aiml): show the provider under the name it trades as The provider's product, docs and billing all say aimlapi.com, so "AI/ML API" in the model add form, the credential picker, the public endpoint support response and the README table is a name a user has to translate before recognising it. The machine identifier stays aiml, so nobody's config changes. --- README.md | 2 +- litellm/provider_endpoints_support_backup.json | 2 +- litellm/proxy/public_endpoints/provider_create_fields.json | 2 +- provider_endpoints_support.json | 2 +- .../proxy/public_endpoints/test_public_endpoints.py | 2 +- ui/litellm-dashboard/src/components/provider_info_helpers.tsx | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 92757fcbbc1..b4f96ce6430 100644 --- a/README.md +++ b/README.md @@ -271,7 +271,7 @@ curl -X POST 'http://0.0.0.0:4000/v1/chat/completions' \ | Provider | `/chat/completions` | `/messages` | `/responses` | `/embeddings` | `/image/generations` | `/audio/transcriptions` | `/audio/speech` | `/moderations` | `/batches` | `/rerank` | |-------------------------------------------------------------------------------------|---------------------|-------------|--------------|---------------|----------------------|-------------------------|-----------------|----------------|-----------|-----------| | [Abliteration (`abliteration`)](https://docs.litellm.ai/docs/providers/abliteration) | ✅ | | | | | | | | | | -| [AI/ML API (`aiml`)](https://docs.litellm.ai/docs/providers/aiml) | ✅ | ✅ | ✅ | ✅ | ✅ | | | | | | +| [aimlapi.com (`aiml`)](https://docs.litellm.ai/docs/providers/aiml) | ✅ | ✅ | ✅ | ✅ | ✅ | | | | | | | [AI21 (`ai21`)](https://docs.litellm.ai/docs/providers/ai21) | ✅ | ✅ | ✅ | | | | | | | | | [AI21 Chat (`ai21_chat`)](https://docs.litellm.ai/docs/providers/ai21) | ✅ | ✅ | ✅ | | | | | | | | | [Aleph Alpha](https://docs.litellm.ai/docs/providers/aleph_alpha) | ✅ | ✅ | ✅ | | | | | | | | diff --git a/litellm/provider_endpoints_support_backup.json b/litellm/provider_endpoints_support_backup.json index 9d6b1e18f59..cacc2a40947 100644 --- a/litellm/provider_endpoints_support_backup.json +++ b/litellm/provider_endpoints_support_backup.json @@ -67,7 +67,7 @@ } }, "aiml": { - "display_name": "AI/ML API (`aiml`)", + "display_name": "aimlapi.com (`aiml`)", "url": "https://docs.litellm.ai/docs/providers/aiml", "endpoints": { "chat_completions": true, diff --git a/litellm/proxy/public_endpoints/provider_create_fields.json b/litellm/proxy/public_endpoints/provider_create_fields.json index 66f8c2ea36f..fa0cb9bae58 100644 --- a/litellm/proxy/public_endpoints/provider_create_fields.json +++ b/litellm/proxy/public_endpoints/provider_create_fields.json @@ -1,7 +1,7 @@ [ { "provider": "AIML", - "provider_display_name": "AI/ML API", + "provider_display_name": "aimlapi.com", "litellm_provider": "aiml", "credential_fields": [ { diff --git a/provider_endpoints_support.json b/provider_endpoints_support.json index ebc220b3496..29c7d275af6 100644 --- a/provider_endpoints_support.json +++ b/provider_endpoints_support.json @@ -67,7 +67,7 @@ } }, "aiml": { - "display_name": "AI/ML API (`aiml`)", + "display_name": "aimlapi.com (`aiml`)", "url": "https://docs.litellm.ai/docs/providers/aiml", "endpoints": { "chat_completions": true, diff --git a/tests/test_litellm/proxy/public_endpoints/test_public_endpoints.py b/tests/test_litellm/proxy/public_endpoints/test_public_endpoints.py index 31430da71e8..87ab4a17193 100644 --- a/tests/test_litellm/proxy/public_endpoints/test_public_endpoints.py +++ b/tests/test_litellm/proxy/public_endpoints/test_public_endpoints.py @@ -925,7 +925,7 @@ def test_build_endpoints_empty_providers_returns_empty(): def test_clean_display_name_strips_suffix(): assert _clean_display_name("OpenAI (`openai`)") == "OpenAI" - assert _clean_display_name("AI/ML API (`aiml`)") == "AI/ML API" + assert _clean_display_name("aimlapi.com (`aiml`)") == "aimlapi.com" assert _clean_display_name("A2A (Agent-to-Agent) (`a2a`)") == "A2A (Agent-to-Agent)" diff --git a/ui/litellm-dashboard/src/components/provider_info_helpers.tsx b/ui/litellm-dashboard/src/components/provider_info_helpers.tsx index d01a6a34cbe..7770286d9e7 100644 --- a/ui/litellm-dashboard/src/components/provider_info_helpers.tsx +++ b/ui/litellm-dashboard/src/components/provider_info_helpers.tsx @@ -69,7 +69,7 @@ export enum Providers { A2A_Agent = "A2A Agent", AI21 = "Ai21", AI21_CHAT = "Ai21 Chat", - AIML = "AI/ML API", + AIML = "aimlapi.com", AIOHTTP_OPENAI = "Aiohttp Openai", Anthropic = "Anthropic", ANTHROPIC_TEXT = "Anthropic Text", From 69b1644bcba544aeb354dbf04ab2ecbe76b74b57 Mon Sep 17 00:00:00 2001 From: Stan Date: Thu, 3 Sep 2026 05:02:41 +0500 Subject: [PATCH 4/5] chore(aimlapi): fork-only placement, do not send upstream Puts aimlapi.com first in the hand ordered lists a user actually reads: the Admin UI provider dropdowns, which render in enum declaration order, the provider table in provider_endpoints_support.json, and the README provider table. The provider create form already listed it first, so that file is untouched here. Placement only, no functional change. Drop this commit before sending anything from this branch upstream. --- README.md | 2 +- .../provider_endpoints_support_backup.json | 36 +++++++++---------- provider_endpoints_support.json | 36 +++++++++---------- .../src/components/provider_info_helpers.tsx | 4 +-- 4 files changed, 39 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index b4f96ce6430..a4ce92b2231 100644 --- a/README.md +++ b/README.md @@ -270,8 +270,8 @@ curl -X POST 'http://0.0.0.0:4000/v1/chat/completions' \ | Provider | `/chat/completions` | `/messages` | `/responses` | `/embeddings` | `/image/generations` | `/audio/transcriptions` | `/audio/speech` | `/moderations` | `/batches` | `/rerank` | |-------------------------------------------------------------------------------------|---------------------|-------------|--------------|---------------|----------------------|-------------------------|-----------------|----------------|-----------|-----------| -| [Abliteration (`abliteration`)](https://docs.litellm.ai/docs/providers/abliteration) | ✅ | | | | | | | | | | | [aimlapi.com (`aiml`)](https://docs.litellm.ai/docs/providers/aiml) | ✅ | ✅ | ✅ | ✅ | ✅ | | | | | | +| [Abliteration (`abliteration`)](https://docs.litellm.ai/docs/providers/abliteration) | ✅ | | | | | | | | | | | [AI21 (`ai21`)](https://docs.litellm.ai/docs/providers/ai21) | ✅ | ✅ | ✅ | | | | | | | | | [AI21 Chat (`ai21_chat`)](https://docs.litellm.ai/docs/providers/ai21) | ✅ | ✅ | ✅ | | | | | | | | | [Aleph Alpha](https://docs.litellm.ai/docs/providers/aleph_alpha) | ✅ | ✅ | ✅ | | | | | | | | diff --git a/litellm/provider_endpoints_support_backup.json b/litellm/provider_endpoints_support_backup.json index cacc2a40947..a64dd484ed6 100644 --- a/litellm/provider_endpoints_support_backup.json +++ b/litellm/provider_endpoints_support_backup.json @@ -32,6 +32,24 @@ } }, "providers": { + "aiml": { + "display_name": "aimlapi.com (`aiml`)", + "url": "https://docs.litellm.ai/docs/providers/aiml", + "endpoints": { + "chat_completions": true, + "messages": true, + "responses": true, + "embeddings": true, + "image_generations": true, + "audio_transcriptions": false, + "audio_speech": false, + "moderations": false, + "batches": false, + "rerank": false, + "a2a": true, + "interactions": true + } + }, "a2a": { "display_name": "A2A (Agent-to-Agent) (`a2a`)", "url": "https://docs.litellm.ai/docs/providers/a2a", @@ -66,24 +84,6 @@ "a2a": false } }, - "aiml": { - "display_name": "aimlapi.com (`aiml`)", - "url": "https://docs.litellm.ai/docs/providers/aiml", - "endpoints": { - "chat_completions": true, - "messages": true, - "responses": true, - "embeddings": true, - "image_generations": true, - "audio_transcriptions": false, - "audio_speech": false, - "moderations": false, - "batches": false, - "rerank": false, - "a2a": true, - "interactions": true - } - }, "ai21": { "display_name": "AI21 (`ai21`)", "url": "https://docs.litellm.ai/docs/providers/ai21", diff --git a/provider_endpoints_support.json b/provider_endpoints_support.json index 29c7d275af6..3daab2bf4b8 100644 --- a/provider_endpoints_support.json +++ b/provider_endpoints_support.json @@ -32,6 +32,24 @@ } }, "providers": { + "aiml": { + "display_name": "aimlapi.com (`aiml`)", + "url": "https://docs.litellm.ai/docs/providers/aiml", + "endpoints": { + "chat_completions": true, + "messages": true, + "responses": true, + "embeddings": true, + "image_generations": true, + "audio_transcriptions": false, + "audio_speech": false, + "moderations": false, + "batches": false, + "rerank": false, + "a2a": true, + "interactions": true + } + }, "a2a": { "display_name": "A2A (Agent-to-Agent) (`a2a`)", "url": "https://docs.litellm.ai/docs/providers/a2a", @@ -66,24 +84,6 @@ "a2a": false } }, - "aiml": { - "display_name": "aimlapi.com (`aiml`)", - "url": "https://docs.litellm.ai/docs/providers/aiml", - "endpoints": { - "chat_completions": true, - "messages": true, - "responses": true, - "embeddings": true, - "image_generations": true, - "audio_transcriptions": false, - "audio_speech": false, - "moderations": false, - "batches": false, - "rerank": false, - "a2a": true, - "interactions": true - } - }, "ai21": { "display_name": "AI21 (`ai21`)", "url": "https://docs.litellm.ai/docs/providers/ai21", diff --git a/ui/litellm-dashboard/src/components/provider_info_helpers.tsx b/ui/litellm-dashboard/src/components/provider_info_helpers.tsx index 7770286d9e7..125d19a956b 100644 --- a/ui/litellm-dashboard/src/components/provider_info_helpers.tsx +++ b/ui/litellm-dashboard/src/components/provider_info_helpers.tsx @@ -66,10 +66,10 @@ import xaiLogo from "../../public/assets/logos/xai.svg"; import xinferenceLogo from "../../public/assets/logos/xinference.svg"; export enum Providers { + AIML = "aimlapi.com", A2A_Agent = "A2A Agent", AI21 = "Ai21", AI21_CHAT = "Ai21 Chat", - AIML = "aimlapi.com", AIOHTTP_OPENAI = "Aiohttp Openai", Anthropic = "Anthropic", ANTHROPIC_TEXT = "Anthropic Text", @@ -181,10 +181,10 @@ export enum Providers { } export const provider_map: Record = { + AIML: "aiml", A2A_Agent: "a2a_agent", AI21: "ai21", AI21_CHAT: "ai21_chat", - AIML: "aiml", AIOHTTP_OPENAI: "aiohttp_openai", Anthropic: "anthropic", ANTHROPIC_TEXT: "anthropic_text", From 02114bda8db620cb6eb69ac99fb21427d1619c1d Mon Sep 17 00:00:00 2001 From: Stan Date: Thu, 3 Sep 2026 18:13:13 +0500 Subject: [PATCH 5/5] fix(aimlapi): use the registered partner id The placeholder part_litellm was a readable stand-in chosen before the partner was registered. Registration mints the id server-side, so the real value is part_O0eykPA6gQNIFEYaUBojBbU4. A wrong or unknown partner id is accepted with a 200 and silently not attributed, so this would not have surfaced at runtime. --- litellm/llms/aiml/common_utils.py | 2 +- .../llms/aiml/chat/test_aiml_chat_transformation.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/litellm/llms/aiml/common_utils.py b/litellm/llms/aiml/common_utils.py index d59b40ba54a..83a36913bf7 100644 --- a/litellm/llms/aiml/common_utils.py +++ b/litellm/llms/aiml/common_utils.py @@ -13,7 +13,7 @@ { "HTTP-Referer": "https://github.com/BerriAI/litellm", "X-Title": "LiteLLM", - "X-AIMLAPI-Partner-ID": "part_litellm", + "X-AIMLAPI-Partner-ID": "part_O0eykPA6gQNIFEYaUBojBbU4", "X-AIMLAPI-Source": "agent/litellm", } ) diff --git a/tests/test_litellm/llms/aiml/chat/test_aiml_chat_transformation.py b/tests/test_litellm/llms/aiml/chat/test_aiml_chat_transformation.py index 1df8d0bbb01..f824392fcbc 100644 --- a/tests/test_litellm/llms/aiml/chat/test_aiml_chat_transformation.py +++ b/tests/test_litellm/llms/aiml/chat/test_aiml_chat_transformation.py @@ -50,7 +50,7 @@ def test_completion_sends_every_attribution_header_on_the_wire(monkeypatch): headers = sent[0].headers assert headers["http-referer"] == "https://github.com/BerriAI/litellm" assert headers["x-title"] == "LiteLLM" - assert headers["x-aimlapi-partner-id"] == "part_litellm" + assert headers["x-aimlapi-partner-id"] == "part_O0eykPA6gQNIFEYaUBojBbU4" assert headers["x-aimlapi-source"] == "agent/litellm" @@ -68,7 +68,7 @@ def test_completion_keeps_caller_supplied_headers(monkeypatch): headers = sent[0].headers assert headers["x-title"] == "my-app" assert headers["x-custom"] == "kept" - assert headers["x-aimlapi-partner-id"] == "part_litellm" + assert headers["x-aimlapi-partner-id"] == "part_O0eykPA6gQNIFEYaUBojBbU4" def test_completion_withholds_attribution_from_a_non_aimlapi_base(monkeypatch):