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:
fenil modi 2026-10-03 13:40:17 +00:00
parent d1526c09fc
commit c77aa2f882
10 changed files with 541 additions and 5 deletions

View file

@ -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,

View file

@ -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",

View file

@ -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"

View file

@ -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",

View file

@ -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": {

View file

@ -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": {

View file

@ -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:

View file

@ -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."

View file

@ -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)

View file

@ -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"]