mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-11 03:38:38 +00:00
feat(aiand): reach provider parity with together_ai
- declare per-model reasoning_effort_levels on all 13 catalog entries (root + backup cost maps) and map spec reasoning_options in the sync script so re-runs keep the field - register aiand in models_by_provider so get_valid_models, proxy wildcard aiand/*, and cheapest-model picking behave like together_ai - map aiand error payloads (402 insufficient_credits, 401 invalid key, 404 model_not_found) to precise exceptions, including the openai-SDK unwrapped body shape - register /v1/completions so text_completion(model='aiand/...') routes natively with AIAND_API_KEY auth
This commit is contained in:
parent
d1526c09fc
commit
c77aa2f882
10 changed files with 541 additions and 5 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue