From f6f49d16cc2422d2a83c93c074e0ac4d6a889cc5 Mon Sep 17 00:00:00 2001 From: islomjon-aiand Date: Wed, 28 Oct 2026 19:15:56 +0900 Subject: [PATCH] feat: add ai& (aiand) provider (#38958) * feat: add aiand provider * feat: expose aiand provider in admin UI * feat(aiand): add ai& model pricing to the cost map * feat(aiand): list ai& models for aiand/* wildcards and mark /v1/messages supported * feat(aiand): send Anthropic /v1/messages to ai& natively * fix(aiand): drop the retired max_tokens field and regenerate endpoint support --------- Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com> --- litellm/__init__.py | 5 + litellm/constants.py | 2 + litellm/llms/openai_like/providers.json | 6 + ...odel_prices_and_context_window_backup.json | 195 ++++++++++++++++++ .../provider_endpoints_support_backup.json | 26 +++ .../provider_create_fields.json | 28 +++ litellm/types/utils.py | 1 + model_prices_and_context_window.json | 195 ++++++++++++++++++ provider_endpoints_support.json | 26 +++ .../llms/openai_like/test_aiand_provider.py | 145 +++++++++++++ 10 files changed, 629 insertions(+) create mode 100644 tests/unit/llms/openai_like/test_aiand_provider.py diff --git a/litellm/__init__.py b/litellm/__init__.py index 41e3fa34323..2cff21bf29a 100644 --- a/litellm/__init__.py +++ b/litellm/__init__.py @@ -710,6 +710,7 @@ transcribe_models: Set = set() gigachat_models: Set = set() llamagate_models: Set = set() coralbricks_models: Set[str] = set() +aiand_models: Set[str] = set() reducto_models: Set = set() bedrock_mantle_models: Set = set() @@ -1022,6 +1023,8 @@ def _populate_provider_model_sets(model_cost_map: Mapping[str, Mapping[str, obje llamagate_models.add(key) elif value.get("litellm_provider") == "coralbricks": coralbricks_models.add(key) + elif value.get("litellm_provider") == "aiand": + aiand_models.add(key) elif value.get("litellm_provider") == "reducto": reducto_models.add(key) elif value.get("litellm_provider") == "bedrock_mantle": @@ -1159,6 +1162,7 @@ model_list = list( | reducto_models | bedrock_mantle_models | coralbricks_models + | aiand_models | set(clarifai_models) ) @@ -1283,6 +1287,7 @@ def _build_models_by_provider() -> dict: "gigachat": gigachat_models, "llamagate": llamagate_models, "coralbricks": coralbricks_models, + "aiand": aiand_models, "reducto": reducto_models, "bedrock_mantle": bedrock_mantle_models, } diff --git a/litellm/constants.py b/litellm/constants.py index 8cc51d0ab5e..0d6dfb5c0d4 100644 --- a/litellm/constants.py +++ b/litellm/constants.py @@ -1035,6 +1035,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.aiand.com/v1", "https://api.synthetic.new/openai/v1", "https://serverless.tensormesh.ai/v1", "https://api.stima.tech/v1", @@ -1100,6 +1101,7 @@ openai_compatible_providers: Final[list[str]] = [ "novita", "meta_llama", "publicai", # PublicAI - JSON-configured provider + "aiand", # ai& - JSON-configured provider "synthetic", # Synthetic - JSON-configured provider "tensormesh", # Tensormesh - JSON-configured provider "apertis", # Apertis - JSON-configured provider diff --git a/litellm/llms/openai_like/providers.json b/litellm/llms/openai_like/providers.json index 115d914d3ac..0034ed2b9d4 100644 --- a/litellm/llms/openai_like/providers.json +++ b/litellm/llms/openai_like/providers.json @@ -107,6 +107,12 @@ "api_key_env": "AIHUBMIX_API_KEY", "api_base_env": "AIHUBMIX_API_BASE" }, + "aiand": { + "base_url": "https://api.aiand.com/v1", + "api_key_env": "AIAND_API_KEY", + "api_base_env": "AIAND_API_BASE", + "supported_endpoints": ["/v1/chat/completions", "/v1/responses", "/v1/messages"] + }, "crusoe": { "base_url": "https://api.inference.crusoecloud.com/v1", "api_key_env": "CRUSOE_API_KEY", diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index d053300e299..8207c3122f9 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -69825,6 +69825,201 @@ "supports_reasoning": true, "supports_vision": true }, + "aiand/deepseek-ai/deepseek-v4-flash": { + "cache_read_input_token_cost": 8e-08, + "input_cost_per_token": 1.5e-07, + "litellm_provider": "aiand", + "max_input_tokens": 1048576, + "max_output_tokens": 384000, + "mode": "chat", + "output_cost_per_token": 2.5e-07, + "source": "https://api.aiand.com/v1/models", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_tool_choice": true, + "supports_vision": false + }, + "aiand/deepseek-ai/deepseek-v4-pro": { + "cache_read_input_token_cost": 2.5e-07, + "input_cost_per_token": 1e-06, + "litellm_provider": "aiand", + "max_input_tokens": 1048576, + "max_output_tokens": 384000, + "mode": "chat", + "output_cost_per_token": 2.5e-06, + "source": "https://api.aiand.com/v1/models", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_tool_choice": true, + "supports_vision": false + }, + "aiand/deepseek-ai/deepseek-v4.1-flash": { + "cache_read_input_token_cost": 2e-08, + "input_cost_per_token": 3e-07, + "litellm_provider": "aiand", + "max_input_tokens": 1048576, + "max_output_tokens": 384000, + "mode": "chat", + "output_cost_per_token": 6e-07, + "source": "https://api.aiand.com/v1/models", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "aiand/google/gemma-4-31b-it": { + "cache_read_input_token_cost": 5e-08, + "input_cost_per_token": 2e-07, + "litellm_provider": "aiand", + "max_input_tokens": 262144, + "max_output_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 5e-07, + "source": "https://api.aiand.com/v1/models", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "aiand/moonshotai/kimi-k2.7-code": { + "cache_read_input_token_cost": 2e-07, + "input_cost_per_token": 7.5e-07, + "litellm_provider": "aiand", + "max_input_tokens": 262144, + "max_output_tokens": 262144, + "mode": "chat", + "output_cost_per_token": 3.5e-06, + "source": "https://api.aiand.com/v1/models", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "aiand/moonshotai/kimi-k3": { + "cache_read_input_token_cost": 5e-07, + "input_cost_per_token": 3e-06, + "litellm_provider": "aiand", + "max_input_tokens": 1048576, + "max_output_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 1.25e-05, + "source": "https://api.aiand.com/v1/models", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "aiand/motif-technologies/motif-3": { + "cache_read_input_token_cost": 2e-07, + "input_cost_per_token": 5e-07, + "litellm_provider": "aiand", + "max_input_tokens": 262144, + "max_output_tokens": 262144, + "mode": "chat", + "output_cost_per_token": 2e-06, + "source": "https://api.aiand.com/v1/models", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_tool_choice": true, + "supports_vision": false + }, + "aiand/openai/gpt-oss-120b": { + "cache_read_input_token_cost": 8e-08, + "input_cost_per_token": 1.5e-07, + "litellm_provider": "aiand", + "max_input_tokens": 131072, + "max_output_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 6e-07, + "source": "https://api.aiand.com/v1/models", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_tool_choice": true, + "supports_vision": false + }, + "aiand/qwen/qwen3.6-27b": { + "cache_read_input_token_cost": 2e-07, + "input_cost_per_token": 3.2e-07, + "litellm_provider": "aiand", + "max_input_tokens": 262144, + "max_output_tokens": 65536, + "mode": "chat", + "output_cost_per_token": 3.2e-06, + "source": "https://api.aiand.com/v1/models", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "aiand/qwen/qwen3.8-27b": { + "cache_read_input_token_cost": 2e-07, + "input_cost_per_token": 4e-07, + "litellm_provider": "aiand", + "max_input_tokens": 262144, + "max_output_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 3e-06, + "source": "https://api.aiand.com/v1/models", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "aiand/zai-org/glm-5.2": { + "cache_read_input_token_cost": 3e-07, + "input_cost_per_token": 1e-06, + "litellm_provider": "aiand", + "max_input_tokens": 1048576, + "max_output_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 4e-06, + "source": "https://api.aiand.com/v1/models", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_tool_choice": true, + "supports_vision": false + }, + "aiand/zai-org/glm-5.3": { + "cache_read_input_token_cost": 3e-07, + "input_cost_per_token": 1e-06, + "litellm_provider": "aiand", + "max_input_tokens": 1048576, + "max_output_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 4e-06, + "source": "https://api.aiand.com/v1/models", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_tool_choice": true, + "supports_vision": false + }, + "aiand/zai-org/glm-5.3-flash": { + "cache_read_input_token_cost": 3e-08, + "input_cost_per_token": 1.5e-07, + "litellm_provider": "aiand", + "max_input_tokens": 1048550, + "max_output_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 5e-07, + "source": "https://api.aiand.com/v1/models", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_tool_choice": true, + "supports_vision": true + }, "bespoke/nimble-latest": { "input_cost_per_token": 0.0, "litellm_provider": "bespoke", diff --git a/litellm/provider_endpoints_support_backup.json b/litellm/provider_endpoints_support_backup.json index 9ba6ed342ea..1ab03fb3fdc 100644 --- a/litellm/provider_endpoints_support_backup.json +++ b/litellm/provider_endpoints_support_backup.json @@ -189,6 +189,32 @@ "generateContent": false } }, + "aiand": { + "display_name": "ai& (`aiand`)", + "url": "https://docs.aiand.com/api/chat-completions/", + "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 + } + }, "amazon_nova": { "display_name": "Amazon Nova (`amazon_nova`)", "url": "https://docs.litellm.ai/docs/providers/amazon_nova", diff --git a/litellm/proxy/public_endpoints/provider_create_fields.json b/litellm/proxy/public_endpoints/provider_create_fields.json index d96f2976508..0dd96c856f8 100644 --- a/litellm/proxy/public_endpoints/provider_create_fields.json +++ b/litellm/proxy/public_endpoints/provider_create_fields.json @@ -1,4 +1,32 @@ [ + { + "provider": "AIAND", + "provider_display_name": "ai&", + "litellm_provider": "aiand", + "credential_fields": [ + { + "key": "api_base", + "label": "API Base", + "placeholder": "https://api.aiand.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": "aiand/openai/gpt-oss-120b" + }, { "provider": "SCALEDOWN", "provider_display_name": "ScaleDown", diff --git a/litellm/types/utils.py b/litellm/types/utils.py index 2869f9b2577..055c18f2e36 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -4573,6 +4573,7 @@ class LlmProviders(str, Enum): POE = "poe" CHUTES = "chutes" NEOSANTARA = "neosantara" + AIAND = "aiand" PARASAIL = "parasail" XIAOMI_MIMO = "xiaomi_mimo" TENSORMESH = "tensormesh" diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index d053300e299..8207c3122f9 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -69825,6 +69825,201 @@ "supports_reasoning": true, "supports_vision": true }, + "aiand/deepseek-ai/deepseek-v4-flash": { + "cache_read_input_token_cost": 8e-08, + "input_cost_per_token": 1.5e-07, + "litellm_provider": "aiand", + "max_input_tokens": 1048576, + "max_output_tokens": 384000, + "mode": "chat", + "output_cost_per_token": 2.5e-07, + "source": "https://api.aiand.com/v1/models", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_tool_choice": true, + "supports_vision": false + }, + "aiand/deepseek-ai/deepseek-v4-pro": { + "cache_read_input_token_cost": 2.5e-07, + "input_cost_per_token": 1e-06, + "litellm_provider": "aiand", + "max_input_tokens": 1048576, + "max_output_tokens": 384000, + "mode": "chat", + "output_cost_per_token": 2.5e-06, + "source": "https://api.aiand.com/v1/models", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_tool_choice": true, + "supports_vision": false + }, + "aiand/deepseek-ai/deepseek-v4.1-flash": { + "cache_read_input_token_cost": 2e-08, + "input_cost_per_token": 3e-07, + "litellm_provider": "aiand", + "max_input_tokens": 1048576, + "max_output_tokens": 384000, + "mode": "chat", + "output_cost_per_token": 6e-07, + "source": "https://api.aiand.com/v1/models", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "aiand/google/gemma-4-31b-it": { + "cache_read_input_token_cost": 5e-08, + "input_cost_per_token": 2e-07, + "litellm_provider": "aiand", + "max_input_tokens": 262144, + "max_output_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 5e-07, + "source": "https://api.aiand.com/v1/models", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "aiand/moonshotai/kimi-k2.7-code": { + "cache_read_input_token_cost": 2e-07, + "input_cost_per_token": 7.5e-07, + "litellm_provider": "aiand", + "max_input_tokens": 262144, + "max_output_tokens": 262144, + "mode": "chat", + "output_cost_per_token": 3.5e-06, + "source": "https://api.aiand.com/v1/models", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "aiand/moonshotai/kimi-k3": { + "cache_read_input_token_cost": 5e-07, + "input_cost_per_token": 3e-06, + "litellm_provider": "aiand", + "max_input_tokens": 1048576, + "max_output_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 1.25e-05, + "source": "https://api.aiand.com/v1/models", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "aiand/motif-technologies/motif-3": { + "cache_read_input_token_cost": 2e-07, + "input_cost_per_token": 5e-07, + "litellm_provider": "aiand", + "max_input_tokens": 262144, + "max_output_tokens": 262144, + "mode": "chat", + "output_cost_per_token": 2e-06, + "source": "https://api.aiand.com/v1/models", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_tool_choice": true, + "supports_vision": false + }, + "aiand/openai/gpt-oss-120b": { + "cache_read_input_token_cost": 8e-08, + "input_cost_per_token": 1.5e-07, + "litellm_provider": "aiand", + "max_input_tokens": 131072, + "max_output_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 6e-07, + "source": "https://api.aiand.com/v1/models", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_tool_choice": true, + "supports_vision": false + }, + "aiand/qwen/qwen3.6-27b": { + "cache_read_input_token_cost": 2e-07, + "input_cost_per_token": 3.2e-07, + "litellm_provider": "aiand", + "max_input_tokens": 262144, + "max_output_tokens": 65536, + "mode": "chat", + "output_cost_per_token": 3.2e-06, + "source": "https://api.aiand.com/v1/models", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "aiand/qwen/qwen3.8-27b": { + "cache_read_input_token_cost": 2e-07, + "input_cost_per_token": 4e-07, + "litellm_provider": "aiand", + "max_input_tokens": 262144, + "max_output_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 3e-06, + "source": "https://api.aiand.com/v1/models", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "aiand/zai-org/glm-5.2": { + "cache_read_input_token_cost": 3e-07, + "input_cost_per_token": 1e-06, + "litellm_provider": "aiand", + "max_input_tokens": 1048576, + "max_output_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 4e-06, + "source": "https://api.aiand.com/v1/models", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_tool_choice": true, + "supports_vision": false + }, + "aiand/zai-org/glm-5.3": { + "cache_read_input_token_cost": 3e-07, + "input_cost_per_token": 1e-06, + "litellm_provider": "aiand", + "max_input_tokens": 1048576, + "max_output_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 4e-06, + "source": "https://api.aiand.com/v1/models", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_tool_choice": true, + "supports_vision": false + }, + "aiand/zai-org/glm-5.3-flash": { + "cache_read_input_token_cost": 3e-08, + "input_cost_per_token": 1.5e-07, + "litellm_provider": "aiand", + "max_input_tokens": 1048550, + "max_output_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 5e-07, + "source": "https://api.aiand.com/v1/models", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_tool_choice": true, + "supports_vision": true + }, "bespoke/nimble-latest": { "input_cost_per_token": 0.0, "litellm_provider": "bespoke", diff --git a/provider_endpoints_support.json b/provider_endpoints_support.json index 9ba6ed342ea..1ab03fb3fdc 100644 --- a/provider_endpoints_support.json +++ b/provider_endpoints_support.json @@ -189,6 +189,32 @@ "generateContent": false } }, + "aiand": { + "display_name": "ai& (`aiand`)", + "url": "https://docs.aiand.com/api/chat-completions/", + "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 + } + }, "amazon_nova": { "display_name": "Amazon Nova (`amazon_nova`)", "url": "https://docs.litellm.ai/docs/providers/amazon_nova", diff --git a/tests/unit/llms/openai_like/test_aiand_provider.py b/tests/unit/llms/openai_like/test_aiand_provider.py new file mode 100644 index 00000000000..804f3a3b6eb --- /dev/null +++ b/tests/unit/llms/openai_like/test_aiand_provider.py @@ -0,0 +1,145 @@ +import json +from typing import Final + +import httpx +import pytest +import respx + +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_known_models_from_wildcard +from litellm.utils import ProviderConfigManager + +AIAND_API_BASE = "https://api.aiand.com/v1" + + +def test_aiand_chat_provider(monkeypatch: pytest.MonkeyPatch): + monkeypatch.setenv("AIAND_API_KEY", "test-key") + + provider_config = JSONProviderRegistry.get("aiand") + assert provider_config is not None + assert litellm.LlmProviders.AIAND.value == "aiand" + assert "aiand" in litellm.openai_compatible_providers + assert provider_config.base_url == AIAND_API_BASE + assert provider_config.api_key_env == "AIAND_API_KEY" + assert provider_config.api_base_env == "AIAND_API_BASE" + + model, provider, api_key, api_base = get_llm_provider("aiand/openai/gpt-oss-120b") + assert model == "openai/gpt-oss-120b" + assert provider == "aiand" + assert api_key == "test-key" + assert api_base == AIAND_API_BASE + + +def test_aiand_completion_request(respx_mock: respx.MockRouter): + messages = [{"role": "user", "content": "Hello"}] + route = respx_mock.post(f"{AIAND_API_BASE}/chat/completions").mock( + return_value=httpx.Response( + 200, + json={ + "id": "chatcmpl-test", + "object": "chat.completion", + "created": 1, + "model": "openai/gpt-oss-120b", + "choices": [ + { + "index": 0, + "message": {"role": "assistant", "content": "Hello"}, + "finish_reason": "stop", + } + ], + "usage": { + "prompt_tokens": 1, + "completion_tokens": 1, + "total_tokens": 2, + }, + }, + ) + ) + + litellm.completion( + model="aiand/openai/gpt-oss-120b", + api_key="test-key", + messages=messages, + ) + + assert route.called + request = route.calls.last.request + assert str(request.url) == f"{AIAND_API_BASE}/chat/completions" + assert request.headers["Authorization"] == "Bearer test-key" + request_body = json.loads(request.read()) + assert request_body["model"] == "openai/gpt-oss-120b" + assert request_body["messages"] == messages + + +def test_aiand_responses_provider(): + config = ProviderConfigManager.get_provider_responses_api_config( + provider="aiand", + model="aiand/openai/gpt-oss-120b", + ) + + assert config is not None + assert config.custom_llm_provider == "aiand" + assert config.get_complete_url(api_base=None, litellm_params={}) == f"{AIAND_API_BASE}/responses" + + +def _aiand_catalog() -> frozenset[str]: + return frozenset(name for name, info in litellm.model_cost.items() if info.get("litellm_provider") == "aiand") + + +def test_aiand_wildcard_lists_its_priced_models(): + catalog: Final = _aiand_catalog() + + assert catalog + assert frozenset(get_known_models_from_wildcard("aiand/*")) == catalog + + +def test_aiand_calls_are_billed_from_their_cost_rows(): + catalog: Final = _aiand_catalog() + assert catalog + + for model in sorted(catalog): + row: Final = litellm.model_cost[model] + prompt_cost, completion_cost = litellm.cost_per_token(model=model, prompt_tokens=1000, completion_tokens=500) + + assert prompt_cost == pytest.approx(1000 * row["input_cost_per_token"]) + assert completion_cost == pytest.approx(500 * row["output_cost_per_token"]) + assert prompt_cost > 0 and completion_cost > 0 + + +@pytest.mark.asyncio +async def test_aiand_anthropic_messages_reach_ai_and_with_the_thinking_budget(monkeypatch: pytest.MonkeyPatch): + monkeypatch.setattr(litellm, "disable_aiohttp_transport", True) + monkeypatch.setattr(litellm, "in_memory_llm_clients_cache", LLMClientCache()) + thinking: Final = {"type": "enabled", "budget_tokens": 1024} + with respx.mock() as upstream: + route: Final = upstream.post(f"{AIAND_API_BASE}/messages").respond( + 200, + json={ + "id": "msg_aiand", + "type": "message", + "role": "assistant", + "model": "zai-org/glm-5.3", + "content": [{"type": "thinking", "thinking": "A greeting", "signature": ""}, {"type": "text", "text": "Hello"}], + "stop_reason": "end_turn", + "stop_sequence": None, + "usage": {"input_tokens": 4, "output_tokens": 3}, + }, + ) + response: Final = await litellm.anthropic.messages.acreate( + model="aiand/zai-org/glm-5.3", + messages=[{"role": "user", "content": "Say hello"}], + max_tokens=2048, + thinking=thinking, + api_key="aiand-test-key", + ) + + request: Final = route.calls.last.request + body: Final = json.loads(request.content) + assert route.call_count == 1 + assert body["model"] == "zai-org/glm-5.3" + assert body["thinking"] == thinking + assert "reasoning_effort" not in body + assert [block["type"] for block in response["content"]] == ["thinking", "text"]