diff --git a/litellm/__init__.py b/litellm/__init__.py index b0761da7f7c..27cb7991ef7 100644 --- a/litellm/__init__.py +++ b/litellm/__init__.py @@ -686,6 +686,7 @@ qwencloud_models: Set = set() qwen_ai_platform_models: Set = set() moonshot_models: Set = set() publicai_models: Set = set() +aiand_models: Set = set() darkbloom_models: Set = set() v0_models: Set = set() morph_models: Set = set() @@ -950,6 +951,8 @@ def _populate_provider_model_sets(model_cost_map: Dict) -> None: moonshot_models.add(key) elif value.get("litellm_provider") == "publicai": publicai_models.add(key) + elif value.get("litellm_provider") == "aiand": + aiand_models.add(key) elif value.get("litellm_provider") == "darkbloom": darkbloom_models.add(key) elif value.get("litellm_provider") == "v0": @@ -1114,6 +1117,7 @@ model_list = list( | qwen_ai_platform_models | moonshot_models | publicai_models + | aiand_models | darkbloom_models | v0_models | morph_models @@ -1226,6 +1230,7 @@ def _build_models_by_provider() -> dict: "modelscope": modelscope_models, "moonshot": moonshot_models, "publicai": publicai_models, + "aiand": aiand_models, "darkbloom": darkbloom_models, "v0": v0_models, "morph": morph_models, diff --git a/litellm/constants.py b/litellm/constants.py index cd40ee22e57..0ced4cfb353 100644 --- a/litellm/constants.py +++ b/litellm/constants.py @@ -1070,6 +1070,7 @@ openai_text_completion_compatible_providers: Final[list] = [ # providers that s "lambda_ai", "hyperbolic", "wandb", + "aiand", ] _openai_like_providers: Final[list] = [ "predibase", diff --git a/litellm/litellm_core_utils/exception_mapping_utils.py b/litellm/litellm_core_utils/exception_mapping_utils.py index 0fdfb301291..5999bb0617d 100644 --- a/litellm/litellm_core_utils/exception_mapping_utils.py +++ b/litellm/litellm_core_utils/exception_mapping_utils.py @@ -1803,6 +1803,71 @@ def _map_together_ai_exception( ) +_AIAND_ERROR_PAYLOAD_KEYS: Final = ("message", "code", "type", "param") + + +def _map_aiand_exception( + *, + model: str, + original_exception: _ProviderHTTPException, + custom_llm_provider: str, + error_str: str, + exception_type: str, + exception_provider: str, + extra_information: str, +) -> None: + error_body: object = getattr(original_exception, "body", None) + if not isinstance(error_body, Mapping): + try: + error_body = json.loads(error_str) + except ValueError: + error_body = None + inner: Final[object] = error_body.get("error") if isinstance(error_body, Mapping) else None + error_payload: Final[Mapping[str, object]] = ( + inner + if isinstance(inner, Mapping) + else error_body + if isinstance(error_body, Mapping) and any(key in error_body for key in _AIAND_ERROR_PAYLOAD_KEYS) + else {} + ) + status_code: Final[int | None] = getattr(original_exception, "status_code", None) + error_message: Final[object] = error_payload.get("message") + message: Final[str] = error_message if isinstance(error_message, str) else error_str + if status_code == 402 and ( + error_payload.get("code") == "insufficient_credits" + or error_payload.get("type") == "billing_error" + or "insufficient_credits" in error_str + or "billing_error" in error_str + ): + raise RateLimitError( + message=f"{exception_provider} - {message}", + llm_provider="aiand", + model=model, + response=getattr(original_exception, "response", None), + litellm_debug_info=extra_information, + ) + elif status_code == 401 and (error_payload.get("code") == "invalid_api_key" or "invalid_api_key" in error_str): + raise AuthenticationError( + message=f"{exception_provider} - {message}", + llm_provider="aiand", + model=model, + response=getattr(original_exception, "response", None), + litellm_debug_info=extra_information, + ) + elif status_code == 404 and ( + error_payload.get("code") == "model_not_found" + or error_payload.get("param") == "model" + or "model_not_found" in error_str + ): + raise NotFoundError( + message=f"{exception_provider} - {message}", + model=model, + llm_provider="aiand", + response=getattr(original_exception, "response", None), + litellm_debug_info=extra_information, + ) + + def _map_aleph_alpha_exception( *, model: str, @@ -2463,7 +2528,17 @@ def exception_type( custom_llm_provider=custom_llm_provider, body=getattr(original_exception, "body", None), ) - if ( + if custom_llm_provider == "aiand": + _map_aiand_exception( + model=model, + original_exception=mappable_exception, + custom_llm_provider=custom_llm_provider, + error_str=error_str, + exception_type=exception_type, + exception_provider=exception_provider, + extra_information=extra_information, + ) + elif ( custom_llm_provider == "openai" or custom_llm_provider == "text-completion-openai" or custom_llm_provider == "custom_openai" diff --git a/litellm/llms/openai_like/providers.json b/litellm/llms/openai_like/providers.json index f0d8f3828ec..847ab731c68 100644 --- a/litellm/llms/openai_like/providers.json +++ b/litellm/llms/openai_like/providers.json @@ -111,7 +111,7 @@ "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"] + "supported_endpoints": ["/v1/chat/completions", "/v1/responses", "/v1/messages", "/v1/completions"] }, "crusoe": { "base_url": "https://managed-inference-api-proxy.crusoecloud.com/v1", diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index dcd7477fa7d..a84397093a8 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -59672,6 +59672,11 @@ "/v1/chat/completions", "/v1/responses", "/v1/messages" + ], + "reasoning_effort_levels": [ + "none", + "high", + "max" ] }, "aiand/deepseek-ai/deepseek-v4-pro": { @@ -59697,6 +59702,11 @@ "/v1/chat/completions", "/v1/responses", "/v1/messages" + ], + "reasoning_effort_levels": [ + "none", + "high", + "max" ] }, "aiand/deepseek-ai/deepseek-v4.1-flash": { @@ -59722,6 +59732,11 @@ "/v1/chat/completions", "/v1/responses", "/v1/messages" + ], + "reasoning_effort_levels": [ + "none", + "high", + "max" ] }, "aiand/google/gemma-4-31b-it": { @@ -59747,6 +59762,10 @@ "/v1/chat/completions", "/v1/responses", "/v1/messages" + ], + "reasoning_effort_levels": [ + "none", + "high" ] }, "aiand/moonshotai/kimi-k2.7-code": { @@ -59772,6 +59791,9 @@ "/v1/chat/completions", "/v1/responses", "/v1/messages" + ], + "reasoning_effort_levels": [ + "high" ] }, "aiand/moonshotai/kimi-k3": { @@ -59797,6 +59819,11 @@ "/v1/chat/completions", "/v1/responses", "/v1/messages" + ], + "reasoning_effort_levels": [ + "low", + "high", + "max" ] }, "aiand/motif-technologies/motif-3": { @@ -59822,6 +59849,10 @@ "/v1/chat/completions", "/v1/responses", "/v1/messages" + ], + "reasoning_effort_levels": [ + "none", + "high" ] }, "aiand/openai/gpt-oss-120b": { @@ -59847,6 +59878,11 @@ "/v1/chat/completions", "/v1/responses", "/v1/messages" + ], + "reasoning_effort_levels": [ + "low", + "medium", + "high" ] }, "aiand/qwen/qwen3.6-27b": { @@ -59872,6 +59908,10 @@ "/v1/chat/completions", "/v1/responses", "/v1/messages" + ], + "reasoning_effort_levels": [ + "none", + "high" ] }, "aiand/qwen/qwen3.8-27b": { @@ -59897,6 +59937,12 @@ "/v1/chat/completions", "/v1/responses", "/v1/messages" + ], + "reasoning_effort_levels": [ + "none", + "low", + "medium", + "xhigh" ] }, "aiand/zai-org/glm-5.2": { @@ -59922,6 +59968,11 @@ "/v1/chat/completions", "/v1/responses", "/v1/messages" + ], + "reasoning_effort_levels": [ + "none", + "high", + "max" ] }, "aiand/zai-org/glm-5.3": { @@ -59947,6 +59998,11 @@ "/v1/chat/completions", "/v1/responses", "/v1/messages" + ], + "reasoning_effort_levels": [ + "low", + "high", + "max" ] }, "aiand/zai-org/glm-5.3-flash": { @@ -59972,6 +60028,11 @@ "/v1/chat/completions", "/v1/responses", "/v1/messages" + ], + "reasoning_effort_levels": [ + "low", + "high", + "max" ] }, "tensormesh/Qwen/Qwen3.5-397B-A17B-FP8": { diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index dcd7477fa7d..a84397093a8 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -59672,6 +59672,11 @@ "/v1/chat/completions", "/v1/responses", "/v1/messages" + ], + "reasoning_effort_levels": [ + "none", + "high", + "max" ] }, "aiand/deepseek-ai/deepseek-v4-pro": { @@ -59697,6 +59702,11 @@ "/v1/chat/completions", "/v1/responses", "/v1/messages" + ], + "reasoning_effort_levels": [ + "none", + "high", + "max" ] }, "aiand/deepseek-ai/deepseek-v4.1-flash": { @@ -59722,6 +59732,11 @@ "/v1/chat/completions", "/v1/responses", "/v1/messages" + ], + "reasoning_effort_levels": [ + "none", + "high", + "max" ] }, "aiand/google/gemma-4-31b-it": { @@ -59747,6 +59762,10 @@ "/v1/chat/completions", "/v1/responses", "/v1/messages" + ], + "reasoning_effort_levels": [ + "none", + "high" ] }, "aiand/moonshotai/kimi-k2.7-code": { @@ -59772,6 +59791,9 @@ "/v1/chat/completions", "/v1/responses", "/v1/messages" + ], + "reasoning_effort_levels": [ + "high" ] }, "aiand/moonshotai/kimi-k3": { @@ -59797,6 +59819,11 @@ "/v1/chat/completions", "/v1/responses", "/v1/messages" + ], + "reasoning_effort_levels": [ + "low", + "high", + "max" ] }, "aiand/motif-technologies/motif-3": { @@ -59822,6 +59849,10 @@ "/v1/chat/completions", "/v1/responses", "/v1/messages" + ], + "reasoning_effort_levels": [ + "none", + "high" ] }, "aiand/openai/gpt-oss-120b": { @@ -59847,6 +59878,11 @@ "/v1/chat/completions", "/v1/responses", "/v1/messages" + ], + "reasoning_effort_levels": [ + "low", + "medium", + "high" ] }, "aiand/qwen/qwen3.6-27b": { @@ -59872,6 +59908,10 @@ "/v1/chat/completions", "/v1/responses", "/v1/messages" + ], + "reasoning_effort_levels": [ + "none", + "high" ] }, "aiand/qwen/qwen3.8-27b": { @@ -59897,6 +59937,12 @@ "/v1/chat/completions", "/v1/responses", "/v1/messages" + ], + "reasoning_effort_levels": [ + "none", + "low", + "medium", + "xhigh" ] }, "aiand/zai-org/glm-5.2": { @@ -59922,6 +59968,11 @@ "/v1/chat/completions", "/v1/responses", "/v1/messages" + ], + "reasoning_effort_levels": [ + "none", + "high", + "max" ] }, "aiand/zai-org/glm-5.3": { @@ -59947,6 +59998,11 @@ "/v1/chat/completions", "/v1/responses", "/v1/messages" + ], + "reasoning_effort_levels": [ + "low", + "high", + "max" ] }, "aiand/zai-org/glm-5.3-flash": { @@ -59972,6 +60028,11 @@ "/v1/chat/completions", "/v1/responses", "/v1/messages" + ], + "reasoning_effort_levels": [ + "low", + "high", + "max" ] }, "tensormesh/Qwen/Qwen3.5-397B-A17B-FP8": { diff --git a/scripts/sync_aiand_models.py b/scripts/sync_aiand_models.py index 4cb3798bf20..1b2b97a4752 100644 --- a/scripts/sync_aiand_models.py +++ b/scripts/sync_aiand_models.py @@ -13,6 +13,7 @@ Policy highlights: the spec. - The spec cannot express endpoint support or caching behavior, so ``supported_endpoints`` and ``supports_prompt_caching`` stay fixed for the whole provider. +- Reasoning effort levels map from the spec's ``effort`` reasoning option, when one is declared. """ import argparse @@ -57,11 +58,17 @@ class SpecModalities(BaseModel): input: list[str] +class SpecReasoningOption(BaseModel): + type: str + values: list[str] + + class SpecModel(BaseModel): id: str name: str family: str reasoning: bool + reasoning_options: list[SpecReasoningOption] = [] tool_call: bool structured_output: bool temperature: bool @@ -99,8 +106,16 @@ def _today() -> str: return datetime.now(tz=timezone.utc).date().isoformat() +def _effort_levels(model: SpecModel) -> tuple[str, ...]: + for option in model.reasoning_options: + if option.type == "effort": + return tuple(option.values) + return () + + def _spec_fields(model: SpecModel) -> RegistryEntry: - return { + effort_levels: Final = _effort_levels(model) + fields: RegistryEntry = { "litellm_provider": PROVIDER, "mode": "chat", "input_cost_per_token": per_token(model.cost.input), @@ -121,6 +136,9 @@ def _spec_fields(model: SpecModel) -> RegistryEntry: "source": SOURCE_URL, "supported_endpoints": list(SUPPORTED_ENDPOINTS), } + if effort_levels: + fields["reasoning_effort_levels"] = list(effort_levels) + return fields def _new_entry(model: SpecModel) -> RegistryEntry: diff --git a/tests/unit/litellm_core_utils/test_exception_mapping_utils.py b/tests/unit/litellm_core_utils/test_exception_mapping_utils.py index 9de768ea47b..c1474f3f72b 100644 --- a/tests/unit/litellm_core_utils/test_exception_mapping_utils.py +++ b/tests/unit/litellm_core_utils/test_exception_mapping_utils.py @@ -1,3 +1,5 @@ +import json + import httpx import openai import pytest @@ -9,6 +11,7 @@ from litellm.litellm_core_utils.exception_mapping_utils import ( ExceptionCheckers, _get_body_error_code, _get_response_headers, + _map_aiand_exception, exception_type, extract_and_raise_litellm_exception, ) @@ -1039,6 +1042,239 @@ def test_an_unmapped_exception_with_no_model_or_provider_message_keeps_traceback assert "Traceback (most recent call last)" in raised.value.message +AIAND_INSUFFICIENT_CREDITS_MESSAGE = ( + "Insufficient credits. Review billing at https://console.aiand.com/settings/billing to continue." +) + + +@pytest.mark.parametrize( + "error_body", + [ + { + "message": AIAND_INSUFFICIENT_CREDITS_MESSAGE, + "type": "billing_error", + "param": None, + "code": "insufficient_credits", + }, + { + "message": "Balance too low to process the request.", + "type": "billing_error", + "param": None, + "code": None, + }, + ], +) +def test_an_aiand_402_billing_error_is_a_rate_limit_error(error_body, quiet_exception_mapping): + from litellm.llms.base_llm.chat.transformation import BaseLLMException + + original_exception = BaseLLMException( + status_code=402, + message=json.dumps({"error": error_body}), + ) + + with pytest.raises(litellm.RateLimitError) as raised: + exception_type( + model="test-model", + original_exception=original_exception, + custom_llm_provider="aiand", + ) + + assert raised.value.status_code == 429 + assert raised.value.llm_provider == "aiand" + assert raised.value.model == "test-model" + assert raised.value.message == f"litellm.RateLimitError: AiandException - {error_body['message']}" + + +def test_an_aiand_402_from_the_response_body_is_a_rate_limit_error(quiet_exception_mapping): + from litellm.llms.base_llm.chat.transformation import BaseLLMException + + original_exception = BaseLLMException( + status_code=402, + message=AIAND_INSUFFICIENT_CREDITS_MESSAGE, + body={ + "error": { + "message": AIAND_INSUFFICIENT_CREDITS_MESSAGE, + "type": "billing_error", + "param": None, + "code": "insufficient_credits", + } + }, + ) + + with pytest.raises(litellm.RateLimitError) as raised: + exception_type( + model="test-model", + original_exception=original_exception, + custom_llm_provider="aiand", + ) + + assert raised.value.llm_provider == "aiand" + assert raised.value.message == f"litellm.RateLimitError: AiandException - {AIAND_INSUFFICIENT_CREDITS_MESSAGE}" + + +def test_an_aiand_402_from_an_unwrapped_body_is_a_rate_limit_error(quiet_exception_mapping): + from litellm.llms.base_llm.chat.transformation import BaseLLMException + + original_exception = BaseLLMException( + status_code=402, + message=( + "Error code: 402 - {'error': {'message': " + f"'{AIAND_INSUFFICIENT_CREDITS_MESSAGE}', " + "'type': 'billing_error', 'param': None, 'code': 'insufficient_credits'}}" + ), + body={ + "message": AIAND_INSUFFICIENT_CREDITS_MESSAGE, + "type": "billing_error", + "param": None, + "code": "insufficient_credits", + }, + ) + + with pytest.raises(litellm.RateLimitError) as raised: + exception_type( + model="test-model", + original_exception=original_exception, + custom_llm_provider="aiand", + ) + + assert raised.value.status_code == 429 + assert raised.value.llm_provider == "aiand" + assert raised.value.message == f"litellm.RateLimitError: AiandException - {AIAND_INSUFFICIENT_CREDITS_MESSAGE}" + + +def test_an_aiand_402_from_a_non_json_error_str_is_a_rate_limit_error(quiet_exception_mapping): + from litellm.llms.base_llm.chat.transformation import BaseLLMException + + error_str = ( + "Error code: 402 - {'error': {'message': " + f"'{AIAND_INSUFFICIENT_CREDITS_MESSAGE}', " + "'type': 'billing_error', 'param': None, 'code': 'insufficient_credits'}}" + ) + original_exception = BaseLLMException(status_code=402, message=error_str) + + with pytest.raises(litellm.RateLimitError) as raised: + exception_type( + model="test-model", + original_exception=original_exception, + custom_llm_provider="aiand", + ) + + assert raised.value.status_code == 429 + assert raised.value.llm_provider == "aiand" + assert raised.value.message == f"litellm.RateLimitError: AiandException - {error_str}" + + +def test_an_aiand_401_invalid_api_key_is_an_authentication_error(quiet_exception_mapping): + from litellm.llms.base_llm.chat.transformation import BaseLLMException + + original_exception = BaseLLMException( + status_code=401, + message=json.dumps( + { + "error": { + "message": "Missing or invalid API key", + "type": "authentication_error", + "param": None, + "code": "invalid_api_key", + } + } + ), + ) + + with pytest.raises(litellm.AuthenticationError) as raised: + exception_type( + model="test-model", + original_exception=original_exception, + custom_llm_provider="aiand", + ) + + assert raised.value.status_code == 401 + assert raised.value.llm_provider == "aiand" + assert raised.value.model == "test-model" + assert raised.value.message == "litellm.AuthenticationError: AiandException - Missing or invalid API key" + + +def test_an_aiand_404_model_not_found_is_a_not_found_error(quiet_exception_mapping): + from litellm.llms.base_llm.chat.transformation import BaseLLMException + + original_exception = BaseLLMException( + status_code=404, + message=json.dumps( + { + "error": { + "message": "Model not found", + "type": "invalid_request_error", + "param": "model", + "code": "model_not_found", + } + } + ), + ) + + with pytest.raises(litellm.NotFoundError) as raised: + exception_type( + model="test-model", + original_exception=original_exception, + custom_llm_provider="aiand", + ) + + assert raised.value.status_code == 404 + assert raised.value.llm_provider == "aiand" + assert raised.value.model == "test-model" + assert raised.value.message == "litellm.NotFoundError: AiandException - Model not found" + + +def test_an_unknown_aiand_error_falls_through_without_raising(): + from litellm.llms.base_llm.chat.transformation import BaseLLMException + + class _AiandUpstreamError(BaseLLMException): + code = "" + llm_provider = "aiand" + + original_exception = _AiandUpstreamError(status_code=418, message="I am a teapot") + + assert ( + _map_aiand_exception( + model="test-model", + original_exception=original_exception, + custom_llm_provider="aiand", + error_str="I am a teapot", + exception_type="HTTPException", + exception_provider="AiandException", + extra_information="", + ) + is None + ) + + +def test_an_unknown_aiand_error_still_maps_by_the_upstream_status(quiet_exception_mapping): + from litellm.llms.base_llm.chat.transformation import BaseLLMException + + original_exception = BaseLLMException( + status_code=400, + message=json.dumps( + { + "error": { + "message": "Something else went wrong", + "type": "invalid_request_error", + "param": None, + "code": "invalid_value", + } + } + ), + ) + + with pytest.raises(litellm.BadRequestError) as raised: + exception_type( + model="test-model", + original_exception=original_exception, + custom_llm_provider="aiand", + ) + + assert raised.value.status_code == 400 + assert raised.value.llm_provider == "aiand" + + CONTEXT_WINDOW_MESSAGE = "This model's maximum context length is 4096 tokens." CONTENT_POLICY_MESSAGE = '{"error": {"type": "invalid_request_error", "code": "content_policy_violation"}}' TIMEOUT_MESSAGE = "Request timed out." diff --git a/tests/unit/llms/openai_like/test_aiand_provider.py b/tests/unit/llms/openai_like/test_aiand_provider.py index c21bbbb5bc4..e91391e133f 100644 --- a/tests/unit/llms/openai_like/test_aiand_provider.py +++ b/tests/unit/llms/openai_like/test_aiand_provider.py @@ -4,13 +4,14 @@ Tests for aiand provider configuration and integration. import json from pathlib import Path -from typing import Final +from typing import Final, get_args import pytest import respx import litellm from litellm.caching.llm_caching_handler import LLMClientCache +from litellm.types.llms.openai import REASONING_EFFORT def test_aiand_provider_resolution(monkeypatch: pytest.MonkeyPatch) -> None: @@ -95,6 +96,16 @@ def test_aiand_model_cost_and_capabilities(model: str) -> None: assert litellm.supports_vision(model) is model_info["supports_vision"] +def test_aiand_entries_declare_reasoning_effort_levels() -> None: + known_efforts: Final = frozenset(get_args(REASONING_EFFORT)) + for model in AIAND_MODELS: + levels = litellm.get_model_info(model)["reasoning_effort_levels"] + assert levels, f"{model} declares no reasoning_effort_levels" + assert set(levels) <= known_efforts, f"{model} declares unknown reasoning efforts" + flash = litellm.get_model_info("aiand/deepseek-ai/deepseek-v4.1-flash") + assert set(flash["reasoning_effort_levels"]) == {"none", "high", "max"} + + def test_aiand_backup_registry_mirrors_cost_map() -> None: package_root = Path(litellm.__file__).parent cost_map = json.loads((package_root.parent / "model_prices_and_context_window.json").read_text()) @@ -107,6 +118,22 @@ def test_aiand_backup_registry_mirrors_cost_map() -> None: assert aiand_entries == {name: backup[name] for name in aiand_entries} +def test_aiand_models_listed_by_provider(monkeypatch: pytest.MonkeyPatch) -> None: + package_root = Path(litellm.__file__).parent + backup = json.loads((package_root / "model_prices_and_context_window_backup.json").read_text()) + aiand_keys = {name for name in backup if name.startswith("aiand/")} + assert len(aiand_keys) == 13 + + monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True") + monkeypatch.setattr(litellm, "model_cost", litellm.get_model_cost_map(url="")) + monkeypatch.setattr(litellm, "models_by_provider", dict(litellm.models_by_provider)) + litellm.add_known_models() + + assert "aiand" in litellm.models_by_provider + assert set(litellm.models_by_provider["aiand"]) == aiand_keys + assert set(litellm.get_valid_models(custom_llm_provider="aiand")) == aiand_keys + + def test_aiand_is_available_in_add_model_form() -> None: fields_path = Path(litellm.__file__).parent / "proxy" / "public_endpoints" / "provider_create_fields.json" providers = json.loads(fields_path.read_text()) @@ -141,6 +168,19 @@ def test_aiand_supported_endpoints() -> None: } +def test_aiand_registered_for_text_completion() -> None: + assert "aiand" in litellm.openai_text_completion_compatible_providers + + +def test_aiand_provider_declares_completions_endpoint() -> None: + from litellm.llms.openai_like.json_loader import JSONProviderRegistry + + provider_config: Final = JSONProviderRegistry.get("aiand") + + assert provider_config is not None + assert "/v1/completions" in provider_config.supported_endpoints + + def test_aiand_chat_completion_request() -> None: with respx.mock() as upstream: route: Final = upstream.post("https://api.aiand.com/v1/chat/completions").respond( @@ -214,6 +254,44 @@ def test_aiand_responses_request() -> None: assert response.output[0].content[0].text == "Hello from aiand" +def test_aiand_text_completion_request(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("AIAND_API_KEY", "aiand-test-key") + + with respx.mock() as upstream: + route: Final = upstream.post("https://api.aiand.com/v1/completions").respond( + 200, + json={ + "id": "cmpl_aiand", + "object": "text_completion", + "created": 1_789_550_000, + "model": "deepseek-ai/deepseek-v4.1-flash", + "choices": [ + { + "text": "Hello from aiand", + "index": 0, + "logprobs": None, + "finish_reason": "stop", + } + ], + "usage": {"prompt_tokens": 2, "completion_tokens": 4, "total_tokens": 6}, + }, + ) + response: Final = litellm.text_completion( + model="aiand/deepseek-ai/deepseek-v4.1-flash", + prompt="Say hello", + ) + + request: Final = route.calls.last.request + body: Final = json.loads(request.content) + assert route.call_count == 1 + assert str(request.url) == "https://api.aiand.com/v1/completions" + assert request.headers["authorization"] == "Bearer aiand-test-key" + assert body["model"] == "deepseek-ai/deepseek-v4.1-flash" + assert body["prompt"] == "Say hello" + assert response.object == "text_completion" + assert response.choices[0].text == "Hello from aiand" + + @pytest.mark.asyncio async def test_aiand_anthropic_messages_request(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(litellm, "disable_aiohttp_transport", True) diff --git a/tests/unit/test_sync_aiand_models.py b/tests/unit/test_sync_aiand_models.py index a4bde9071da..8b5e6051dde 100644 --- a/tests/unit/test_sync_aiand_models.py +++ b/tests/unit/test_sync_aiand_models.py @@ -70,7 +70,7 @@ def test_load_spec_raises_when_id_mismatches_key() -> None: def test_added_model_lands_in_cost_map_with_expected_fields() -> None: - spec = sync.load_spec(_spec_json(_model())) + spec = sync.load_spec(_spec_json(_model(reasoning_options=[{"type": "effort", "values": ["low", "high", "max"]}]))) outcome = sync.compute_sync({}, spec) entry = outcome.cost_map["aiand/acme/chat-1"] assert entry["litellm_provider"] == "aiand" @@ -86,6 +86,7 @@ def test_added_model_lands_in_cost_map_with_expected_fields() -> None: assert entry["supports_tool_choice"] is True assert entry["supports_response_schema"] is True assert entry["supports_reasoning"] is False + assert entry["reasoning_effort_levels"] == ["low", "high", "max"] assert entry["supports_vision"] is False assert entry["source"] == "https://api.aiand.com/v1/api.json" assert entry["supported_endpoints"] == ["/v1/chat/completions", "/v1/responses", "/v1/messages"]