BerriAI/litellm · error · Exception
api base needs to be a string. api_base=
Error message
api base needs to be a string. api_base={api_base} What it means
Raised inside get_llm_provider() when the model string starts with a known provider prefix (e.g. 'groq/...', 'vertex_ai/...') and the supplied api_base is not None and not a Python str. LiteLLM type-checks api_base here because every downstream provider handler concatenates it into request URLs, so a list or URL object would fail obscurely later. The raw Exception is caught by get_llm_provider's except block and re-raised as litellm.exceptions.BadRequestError prefixed with 'GetLLMProvider Exception - ...'.
Solutions
- Pass api_base as a plain string URL, e.g. api_base='https://api.groq.com/openai/v1'.
- If you need multiple base URLs, use 'openai/<model>' with an api_base list (the supported form) or define separate Router deployments, one api_base each.
- Coerce non-str objects before the call: str(httpx_url) or secret_str.get_secret_value().
- Check config.yaml indentation so litellm_params.api_base is a scalar string, not a list or nested map.
Example fix
# before litellm.completion(model='groq/llama-3.1-8b-instant', api_base=['https://api.groq.com/openai/v1']) # after litellm.completion(model='groq/llama-3.1-8b-instant', api_base='https://api.groq.com/openai/v1')
Defensive patterns
Strategy: type-guard
Validate before calling
import litellm
api_base = 'https://api.groq.com/openai/v1'
assert api_base is None or isinstance(api_base, str), f'api_base must be a str, got {type(api_base).__name__}'
resp = litellm.completion(model='groq/llama-3.1-8b-instant', api_base=api_base, messages=[{'role': 'user', 'content': 'hi'}]) Type guard
def is_valid_api_base(value: object) -> bool:
return value is None or isinstance(value, str) Try / catch
try:
resp = litellm.completion(model='groq/llama-3.1-8b-instant', api_base=api_base)
except litellm.exceptions.BadRequestError as e:
if 'api base needs to be a string' in str(e):
resp = litellm.completion(model='groq/llama-3.1-8b-instant', api_base=str(api_base))
else:
raise Prevention
- Keep a single str constant for each provider's base URL and reuse it.
- At app startup, assert isinstance on every api_base you forward to litellm.
- Reserve list api_base for 'openai/'-prefixed models only.
- str() httpx.URL / unwrap SecretStr at your config boundary, not at call sites.
When it happens
Trigger: Calling litellm.completion()/acompletion() with model='<provider>/<model>' where the prefix is in litellm.provider_list, plus api_base as a list (['https://a','https://b']), an httpx.URL object, a pydantic SecretStr, a dict, or bytes. Only fires when no earlier OpenAI-compatible branch (which handles list api_base) matched.
Common situations: Copying the multi-base-URL load-balancing pattern (api_base as a YAML list) to a provider that is not routed through the OpenAI-compatible branch; passing httpx.URL or SecretStr objects from application settings; YAML indentation errors that make api_base a mapping instead of a scalar.
Understand the failure class
Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.
Related errors
- api_base is required
- api_base is required for hosted_vllm embeddings
- api_base must be provided for Hosted VLLM rerank
- api_base not set for Hosted VLLM responses API. Set via…
- dynamic_api_key needs to be a string. Got type=
AI-assisted analysis of BerriAI/litellm@77b7c6c40c (2026-08-18).
Data as JSON: /api/errors/d5f945e0dcdfe485.
Report an issue: GitHub.
Appendix: source
Thrown at litellm/litellm_core_utils/get_llm_provider_logic.py:225
if (
model.split("/", 1)[0] in litellm.provider_list
and model.split("/", 1)[0] not in litellm.model_list_set
and len(model.split("/"))
> 1 # handle edge case where user passes in `litellm --model mistral` https://github.com/BerriAI/litellm/issues/1351
):
return _get_openai_compatible_provider_info(
model=model,
api_base=api_base,
api_key=api_key,
dynamic_api_key=dynamic_api_key,
litellm_params=litellm_params,
)
elif model.split("/", 1)[0] in litellm.provider_list:
custom_llm_provider = model.split("/", 1)[0]
model = model.split("/", 1)[1]
if api_base is not None and not isinstance(api_base, str):
raise Exception(f"api base needs to be a string. api_base={api_base}")
if dynamic_api_key is not None and not isinstance(dynamic_api_key, str):
raise Exception(f"dynamic_api_key needs to be a string. Got type={type(dynamic_api_key).__name__}")
return model, custom_llm_provider, dynamic_api_key, api_base
# check if api base is a known openai compatible endpoint
if api_base:
for endpoint in litellm.openai_compatible_endpoints:
if _endpoint_matches_api_base(endpoint, api_base):
if endpoint == "api.perplexity.ai":
custom_llm_provider = "perplexity"
dynamic_api_key = get_secret_str("PERPLEXITYAI_API_KEY")
elif endpoint == "api.endpoints.anyscale.com/v1":
custom_llm_provider = "anyscale"
dynamic_api_key = get_secret_str("ANYSCALE_API_KEY")
elif endpoint == "api.deepinfra.com/v1/openai":
custom_llm_provider = "deepinfra"
dynamic_api_key = get_secret_str("DEEPINFRA_API_KEY")
elif endpoint == "api.mistral.ai/v1":
custom_llm_provider = "mistral"View on GitHub (pinned to 77b7c6c40c)