litellm - 💡(How to fix) Fix JSON providers with base_class: "openai_gpt" don't inherit OpenAI parameter mappings (e.g. max_tokens -> max_completion_tokens) [1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
BerriAI/litellm#25373Fetched 2026-04-09 07:52:26
View on GitHub
Comments
0
Participants
1
Timeline
1
Reactions
0
Participants
Timeline (top)
labeled ×1

Error Message

  1. Observe that max_tokens is sent as-is (not mapped to max_completion_tokens), causing a 400 error from the model.

Root Cause

Actual: max_tokens is sent as-is; reasoning models reject it because they require max_completion_tokens.

Fix Action

Workaround

Use openai/ prefix with api_base override instead of a custom provider name:

- model_name: gpt-5-mini
  litellm_params:
    model: openai/gpt-5-mini
    api_base: https://gateway.example.com/openai/app

This works but defeats the purpose of JSON providers — you lose the ability to have a named provider with its own base URL and key resolution, and you can't use openai/ for both the real OpenAI API and your gateway simultaneously.

Code Example

{
  "mygateway": {
    "base_url": "https://gateway.example.com/openai/app",
    "api_key_env": "MYGATEWAY_API_KEY",
    "base_class": "openai_gpt"
  }
}

---

model_list:
  - model_name: gpt-5-mini
    litellm_params:
      model: mygateway/gpt-5-mini
      api_key: os.environ/MYGATEWAY_API_KEY

---

- model_name: gpt-5-mini
  litellm_params:
    model: openai/gpt-5-mini
    api_base: https://gateway.example.com/openai/app

---

provider_config: Optional[BaseConfig] = None
if custom_llm_provider is not None and custom_llm_provider in [
    provider.value for provider in LlmProviders
]:
    provider_config = ProviderConfigManager.get_provider_chat_config(
        model=model, provider=LlmProviders(custom_llm_provider)
    )

---

def map_openai_params(self, non_default_params, optional_params, model, drop_params):
    supported_params = self.get_supported_openai_params(model)
    for param, value in non_default_params.items():
        if param in provider.param_mappings:
            optional_params[provider.param_mappings[param]] = value
        elif param in supported_params:
            optional_params[param] = value
    # ... temperature constraints ...
    return optional_params

---

{
  "mygateway": {
    "base_url": "https://api.openai.com/v1",
    "api_key_env": "OPENAI_API_KEY",
    "base_class": "openai_gpt"
  }
}

---

model_list:
  - model_name: test-reasoning
    litellm_params:
      model: mygateway/o3-mini
      api_key: os.environ/OPENAI_API_KEY

---

import litellm
response = litellm.completion(
    model="mygateway/o3-mini",
    messages=[{"role": "user", "content": "Say hi"}],
    max_tokens=50,
)

---

if provider_config is None and custom_llm_provider is not None:
    from litellm.llms.openai_like.json_loader import JSONProviderRegistry
    if JSONProviderRegistry.exists(custom_llm_provider):
        provider_config = ProviderConfigManager.get_provider_chat_config(
            model=model, provider=custom_llm_provider
        )

---

def map_openai_params(self, non_default_params, optional_params, model, drop_params):
    # First: apply parent's smart transformations (e.g. max_tokens -> max_completion_tokens)
    optional_params = super().map_openai_params(
        non_default_params=non_default_params,
        optional_params=optional_params,
        model=model,
        drop_params=drop_params,
    )
    # Then: apply JSON-specific param_mappings on top
    for param, value in non_default_params.items():
        if param in provider.param_mappings:
            mapped_key = provider.param_mappings[param]
            optional_params[mapped_key] = value
            # Remove the original if it was set by super()
            if param in optional_params and param != mapped_key:
                del optional_params[param]
    return optional_params
RAW_BUFFERClick to expand / collapse

JSON providers with base_class: "openai_gpt" don't inherit OpenAI parameter mappings (e.g. max_tokens -> max_completion_tokens)

What happened

When using a JSON-based provider (via providers.json) with base_class: "openai_gpt" to proxy an OpenAI-compatible gateway, OpenAI-specific parameter transformations like max_tokens -> max_completion_tokens for reasoning models (o1, o3, gpt-5, etc.) are silently skipped. The raw max_tokens is sent upstream, causing the request to be rejected by the model.

Use case

We run an internal OpenAI-compatible gateway that serves multiple model families (GPT, Claude, Gemini) behind a single base URL. We want to register it as a named provider in providers.json so we can use mygateway/model-name syntax, while inheriting all of OpenAI's parameter handling:

{
  "mygateway": {
    "base_url": "https://gateway.example.com/openai/app",
    "api_key_env": "MYGATEWAY_API_KEY",
    "base_class": "openai_gpt"
  }
}
model_list:
  - model_name: gpt-5-mini
    litellm_params:
      model: mygateway/gpt-5-mini
      api_key: os.environ/MYGATEWAY_API_KEY

Expected: mygateway/gpt-5-mini behaves identically to openai/gpt-5-mini with api_base override — including all parameter transformations.

Actual: max_tokens is sent as-is; reasoning models reject it because they require max_completion_tokens.

Workaround

Use openai/ prefix with api_base override instead of a custom provider name:

- model_name: gpt-5-mini
  litellm_params:
    model: openai/gpt-5-mini
    api_base: https://gateway.example.com/openai/app

This works but defeats the purpose of JSON providers — you lose the ability to have a named provider with its own base URL and key resolution, and you can't use openai/ for both the real OpenAI API and your gateway simultaneously.

Root cause (two-layer bug)

Layer 1: get_optional_params() gates on LlmProviders enum

In litellm/utils.py ~ line 1330:

provider_config: Optional[BaseConfig] = None
if custom_llm_provider is not None and custom_llm_provider in [
    provider.value for provider in LlmProviders
]:
    provider_config = ProviderConfigManager.get_provider_chat_config(
        model=model, provider=LlmProviders(custom_llm_provider)
    )

A JSON provider like "mygateway" is not in the LlmProviders enum, so provider_config stays None. The subsequent if/elif chain has no branch for it, so it falls through to the generic OpenAILikeChatConfig().map_openai_params() — which knows nothing about the JSON provider's config or OpenAI's reasoning-model parameter rules.

Note: Some JSON providers in the repo (e.g. publicai, chutes) are duplicated in the LlmProviders enum, masking this bug for those specific providers. Providers not in the enum (e.g. veniceai, scaleway, gmi, sarvam, or any user-defined provider) are affected.

Layer 2: JSONProviderConfig.map_openai_params() doesn't call super()

Even if Layer 1 were fixed, dynamic_config.py overrides map_openai_params() without calling super():

def map_openai_params(self, non_default_params, optional_params, model, drop_params):
    supported_params = self.get_supported_openai_params(model)
    for param, value in non_default_params.items():
        if param in provider.param_mappings:
            optional_params[provider.param_mappings[param]] = value
        elif param in supported_params:
            optional_params[param] = value
    # ... temperature constraints ...
    return optional_params

This is a "dumb" key rename — it only applies the explicit param_mappings from JSON. The smart transformations from OpenAIGPTConfig.map_openai_params() (like conditionally mapping max_tokens -> max_completion_tokens based on whether the model is a reasoning model) are lost.

Steps to reproduce

  1. Add a custom provider to providers.json:
{
  "mygateway": {
    "base_url": "https://api.openai.com/v1",
    "api_key_env": "OPENAI_API_KEY",
    "base_class": "openai_gpt"
  }
}
  1. Configure a reasoning model:
model_list:
  - model_name: test-reasoning
    litellm_params:
      model: mygateway/o3-mini
      api_key: os.environ/OPENAI_API_KEY
  1. Send a request with max_tokens:
import litellm
response = litellm.completion(
    model="mygateway/o3-mini",
    messages=[{"role": "user", "content": "Say hi"}],
    max_tokens=50,
)
  1. Observe that max_tokens is sent as-is (not mapped to max_completion_tokens), causing a 400 error from the model.

  2. Compare with openai/o3-mini — same request succeeds because OpenAIGPTConfig.map_openai_params() handles the mapping.

Suggested fix

Layer 1: In get_optional_params(), after the LlmProviders enum check, fall back to checking JSONProviderRegistry:

if provider_config is None and custom_llm_provider is not None:
    from litellm.llms.openai_like.json_loader import JSONProviderRegistry
    if JSONProviderRegistry.exists(custom_llm_provider):
        provider_config = ProviderConfigManager.get_provider_chat_config(
            model=model, provider=custom_llm_provider
        )

Layer 2: In JSONProviderConfig.map_openai_params(), call super() first, then apply JSON-specific overrides:

def map_openai_params(self, non_default_params, optional_params, model, drop_params):
    # First: apply parent's smart transformations (e.g. max_tokens -> max_completion_tokens)
    optional_params = super().map_openai_params(
        non_default_params=non_default_params,
        optional_params=optional_params,
        model=model,
        drop_params=drop_params,
    )
    # Then: apply JSON-specific param_mappings on top
    for param, value in non_default_params.items():
        if param in provider.param_mappings:
            mapped_key = provider.param_mappings[param]
            optional_params[mapped_key] = value
            # Remove the original if it was set by super()
            if param in optional_params and param != mapped_key:
                del optional_params[param]
    return optional_params

Environment

  • LiteLLM version: 1.82.3
  • Python: 3.13
  • Deployment: Kubernetes (Helm chart)

Related issues

  • #24366 (JSON providers: 429 errors bypass cooldown — another case of JSON providers not being fully integrated)

extent analysis

TL;DR

The most likely fix involves modifying the get_optional_params() function to check JSONProviderRegistry and updating JSONProviderConfig.map_openai_params() to call super() before applying JSON-specific overrides.

Guidance

  1. Update get_optional_params(): After checking the LlmProviders enum, add a fallback to check JSONProviderRegistry to ensure JSON providers are properly configured.
  2. Modify JSONProviderConfig.map_openai_params(): Call super() to apply parent class transformations (like max_tokens to max_completion_tokens mapping) before applying JSON-specific parameter mappings.
  3. Verify the fix: Test with a custom provider and a reasoning model, sending a request with max_tokens to ensure it's correctly mapped to max_completion_tokens.
  4. Review related issues: Consider fixes for related problems, such as #24366, to ensure full integration of JSON providers.

Example

# In get_optional_params()
if provider_config is None and custom_llm_provider is not None:
    from litellm.llms.openai_like.json_loader import JSONProviderRegistry
    if JSONProviderRegistry.exists(custom_llm_provider):
        provider_config = ProviderConfigManager.get_provider_chat_config(
            model=model, provider=custom_llm_provider
        )

# In JSONProviderConfig.map_openai_params()
def map_openai_params(self, non_default_params, optional_params, model, drop_params):
    optional_params = super().map_openai_params(
        non_default_params=non_default_params,
        optional_params=optional_params,
        model=model,
        drop_params=drop_params,
    )
    # Apply JSON-specific param_mappings
    for param, value in non_default_params.items():
        if param in provider.param_mappings:
            mapped_key = provider.param_mappings[param]
            optional_params[mapped_key] = value
            if param in optional_params and param != mapped_key:
                del optional_params[param]
    return optional_params

Notes

  • These changes assume the JSONProviderRegistry and ProviderConfigManager are correctly implemented and accessible.
  • The fix may require additional adjustments based on the specific LiteLLM version and deployment environment.

Recommendation

Apply the suggested workaround by modifying get_optional_params() and JSONProviderConfig.map_openai_params() as described, to ensure proper handling of JSON providers and parameter transformations.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING