Skip to content

Commit 8228c13

Browse files
pseudotensorclaude
andcommitted
refactor: move the max_tokens resolution into an h2o hook, reverting the core edits
#26 implemented the max_tokens / max_completion_tokens resolution in core litellm — `get_optional_params` plus a `get_preferred_max_tokens_param` method on BaseConfig and four provider configs, a param threaded through `main.py`, entries in `types/utils.py` and `types/router.py`, a constant in `types/llms/azure.py`, and the regenerated dashboard `schema.d.ts` that the types change forced. Eleven upstream files. That is the wrong trade for this fork. `h2o-main` is rebuilt as `<upstream-tag>` + the h2o file delta on every version bump, so each of those eleven is re-applied by hand every time, while a file added under `integrations/h2o/` is additive and costs nothing. Reviewer feedback on #26 was exactly this. So: all eleven core files are reverted to their upstream state, the core-utils module and its tests are removed, and the entire behaviour now lives in `litellm/integrations/h2o/litellm_max_tokens_resolution_hook.py`. Net change against the upstream base is two new files and zero edits. Nothing was given up to do it. Verified against the UNMODIFIED tree that `async_pre_call_deployment_hook` can reach everything the core version used: * `api_version`, `additional_drop_params`, `model_info` and the `use_max_completion_tokens` directive are all in its kwargs for the SELECTED deployment (the directive survives `LiteLLM_Params`' extra="allow"), so a mixed model group is still judged per member; * it runs BEFORE param mapping, so collapsing there still fixes the last-wins ordering rather than only renaming a field — confirmed by driving real `acompletion` and reading the mapped params; * `types/*` was only needed to stop the directive leaking to the provider; the hook pops it instead, on EVERY call type, which removes that need entirely; * reasoning-model detection asks litellm's own `is_o_series_model` / `is_model_gpt_5_model` rather than adding methods to those configs, so there is no hardcoded model-name list here either; * `get_supported_openai_params` is called directly, so a target the provider does not accept is still never forced. The one capability a core implementation has that this does not: the deployment hook dispatch lives only in the `@client` decorator's ASYNC wrapper, so a direct sync in-process `litellm.completion()` bypasses it. Not a capability we use — this hook is registered only in the proxy config and the proxy maps /chat/completions to `acompletion`. The risk that comes back with a hook is the one that broke #25, and it is now gated from evidence rather than guesswork. That dispatch is NOT chat-specific; observed call types reaching it are `acompletion`, `anthropic_messages` and `atext_completion`. `litellm.anthropic_messages` declares `max_tokens` as a REQUIRED parameter, so popping it raises `TypeError: anthropic_messages() missing 1 required positional argument` outside the hook's try/except. Gated on `call_type` in {completion, acompletion}, with tests pinning that /v1/messages returns normally, /v1/completions keeps its `max_tokens`, and the directive is stripped even on the gated types so it cannot leak there. Carries forward every hardening from #27 (which this supersedes): floats coerced the way AnthropicConfig already coerces them, unusable values left exactly as they arrived, a dropped field never used as a target, non-str api_version treated as unrecognizable rather than raising. Verification, all measured: * 136 unit + end-to-end tests in the new file. * The h2ogpte#11992 model matrix through the hook: 214/214, including 182 with-limit/without-limit comparisons across 26 model routes x 7 param sets with 0 perturbed — so function calling, tool_choice, parallel_tool_calls, response_format and every sampling param are untouched. * Per-provider, client 50 against a 64000 ceiling: azure max_completion_tokens 50, anthropic max_tokens 50, bedrock maxTokens 50, gemini max_output_tokens 50, o3/gpt-5 max_completion_tokens 50. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 60f5ee9 commit 8228c13

15 files changed

Lines changed: 963 additions & 875 deletions

File tree

litellm/integrations/h2o/litellm_max_tokens_resolution_hook.py

Lines changed: 390 additions & 0 deletions
Large diffs are not rendered by default.

litellm/litellm_core_utils/max_tokens_params.py

Lines changed: 0 additions & 144 deletions
This file was deleted.

litellm/llms/azure/chat/gpt_5_transformation.py

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Support for Azure OpenAI gpt-5 model family."""
22

3-
from typing import List, Optional
3+
from typing import List
44

55
import litellm
66
from litellm.exceptions import UnsupportedParamsError
@@ -56,17 +56,6 @@ def is_model_gpt_5_model(cls, model: str) -> bool:
5656
_normalized = model.split("/")[-1] # strip provider prefix, e.g. "azure/"
5757
return ("gpt-5" in model and not _normalized.startswith("gpt-5-chat")) or "gpt5_series" in model
5858

59-
def get_preferred_max_tokens_param(
60-
self, model: str, api_version: Optional[str] = None
61-
) -> Optional[str]:
62-
"""Always ``max_completion_tokens``, independent of api_version.
63-
64-
The MRO would otherwise reach ``AzureOpenAIConfig``'s api_version rule
65-
first, which would answer ``max_tokens`` for a pre-2025 version — wrong
66-
for a gpt-5 deployment, whose ``map_openai_params`` renames it anyway.
67-
"""
68-
return "max_completion_tokens"
69-
7059
def get_supported_openai_params(self, model: str) -> List[str]:
7160
"""Get supported parameters for Azure OpenAI GPT-5 models.
7261

litellm/llms/azure/chat/gpt_transformation.py

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
from litellm.llms.base_llm.chat.transformation import BaseLLMException
1010
from litellm.types.llms.azure import (
1111
API_VERSION_MONTH_SUPPORTED_RESPONSE_FORMAT,
12-
API_VERSION_YEAR_REQUIRING_MAX_COMPLETION_TOKENS,
1312
API_VERSION_YEAR_SUPPORTED_RESPONSE_FORMAT,
1413
)
1514
from litellm.types.utils import ModelResponse
@@ -149,34 +148,6 @@ def _is_response_format_supported_api_version(self, api_version_year: str, api_v
149148
else:
150149
return api_month >= supported_month
151150

152-
def get_preferred_max_tokens_param(
153-
self, model: str, api_version: Optional[str] = None
154-
) -> Optional[str]:
155-
"""Azure's output-token field depends on the api_version.
156-
157-
2025+ versions (and the v1 API) reject ``max_tokens`` for ALL chat
158-
models, not just the o-series, so the caller's value has to travel on
159-
``max_completion_tokens``. Older versions predate that field, so it has
160-
to travel on ``max_tokens``.
161-
162-
Returns None when the api_version isn't a recognizable shape, which
163-
leaves the caller's own field untouched.
164-
"""
165-
if api_version is None:
166-
return None
167-
168-
from litellm.llms.azure.common_utils import BaseAzureLLM
169-
170-
if BaseAzureLLM._is_azure_v1_api_version(api_version):
171-
return "max_completion_tokens"
172-
173-
api_version_year = api_version.split("-")[0]
174-
if len(api_version_year) != 4 or not api_version_year.isdigit():
175-
return None
176-
if int(api_version_year) >= API_VERSION_YEAR_REQUIRING_MAX_COMPLETION_TOKENS:
177-
return "max_completion_tokens"
178-
return "max_tokens"
179-
180151
def map_openai_params(
181152
self,
182153
non_default_params: dict,

litellm/llms/base_llm/chat/transformation.py

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -192,22 +192,6 @@ def max_retry_on_unprocessable_entity_error(self) -> int:
192192
def get_supported_openai_params(self, model: str) -> list:
193193
pass
194194

195-
def get_preferred_max_tokens_param(
196-
self, model: str, api_version: Optional[str] = None
197-
) -> Optional[str]:
198-
"""Which output-token field this provider wants on the wire.
199-
200-
The OpenAI schema has two — ``max_tokens`` and its replacement
201-
``max_completion_tokens`` — and a request can arrive carrying both.
202-
Return the one this provider accepts so that only that one is sent;
203-
return None (the default) when the provider handles either and the
204-
caller's choice should stand.
205-
206-
See ``litellm.litellm_core_utils.max_tokens_params`` for the resolution
207-
this feeds.
208-
"""
209-
return None
210-
211195
def _add_response_format_to_tools(
212196
self,
213197
optional_params: dict,

litellm/llms/openai/chat/gpt_5_transformation.py

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -188,16 +188,6 @@ def get_supported_openai_params(self, model: str) -> list:
188188

189189
return [param for param in base_gpt_series_params if param not in non_supported_params]
190190

191-
def get_preferred_max_tokens_param(
192-
self, model: str, api_version: Optional[str] = None
193-
) -> Optional[str]:
194-
"""gpt-5 models take ``max_completion_tokens`` only.
195-
196-
Same reason as the o-series: the rename below writes the key that the
197-
generic mapping overwrites when both fields are present.
198-
"""
199-
return "max_completion_tokens"
200-
201191
def map_openai_params(
202192
self,
203193
non_default_params: dict,

litellm/llms/openai/chat/o_series_transformation.py

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -87,18 +87,6 @@ def get_supported_openai_params(self, model: str) -> list:
8787

8888
return [param for param in all_openai_params if param not in non_supported_params]
8989

90-
def get_preferred_max_tokens_param(
91-
self, model: str, api_version: Optional[str] = None
92-
) -> Optional[str]:
93-
"""O-series models take ``max_completion_tokens`` only.
94-
95-
``map_openai_params`` below already moves ``max_tokens`` across, but it
96-
writes the key that the generic mapping then overwrites when BOTH
97-
fields are present. Declaring the preference is what makes a request
98-
carrying both resolve to one field holding the tighter value.
99-
"""
100-
return "max_completion_tokens"
101-
10290
def map_openai_params(
10391
self,
10492
non_default_params: dict,

litellm/main.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5254,10 +5254,6 @@ def completion( # type: ignore
52545254
"service_tier": service_tier,
52555255
"allowed_openai_params": kwargs.get("allowed_openai_params"),
52565256
"base_model": base_model,
5257-
# Selects WHICH output-token field is sent (max_tokens vs
5258-
# max_completion_tokens), overriding the provider's own detection.
5259-
# Settable per-request or per-deployment via litellm_params.
5260-
"use_max_completion_tokens": kwargs.get("use_max_completion_tokens"),
52615257
}
52625258
optional_params = get_optional_params(**optional_param_args, **non_default_params)
52635259
processed_non_default_params = pre_process_non_default_params(

litellm/types/llms/azure.py

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,2 @@
11
API_VERSION_YEAR_SUPPORTED_RESPONSE_FORMAT = 2024
22
API_VERSION_MONTH_SUPPORTED_RESPONSE_FORMAT = 8
3-
4-
# First api_version year in which Azure chat completions reject `max_tokens`
5-
# in favour of `max_completion_tokens`:
6-
#
7-
# Unsupported parameter: 'max_tokens' is not supported with this model.
8-
# Use 'max_completion_tokens' instead.
9-
#
10-
# Observed on 2025-04-01-preview against a gpt-4o deployment, i.e. for a plain
11-
# chat model and not only for the o-series. The boundary is set at the year
12-
# rather than a specific preview date because Azure rolled the change out
13-
# across the 2025 preview versions and the v1 (`preview` / `latest` / `v1`)
14-
# API, and because `max_completion_tokens` is the field Azure documents as
15-
# current for every 2025 version — so erring on this side of the boundary
16-
# sends the field Azure asks for.
17-
API_VERSION_YEAR_REQUIRING_MAX_COMPLETION_TOKENS = 2025

litellm/types/router.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -245,12 +245,6 @@ class GenericLiteLLMParams(CredentialLiteLLMParams, CustomPricingLiteLLMParams):
245245
default=False,
246246
description="Use stored xAI OAuth credentials when no xAI API key is configured.",
247247
)
248-
# Which output-token field this deployment accepts. True -> send
249-
# `max_completion_tokens`, False -> send `max_tokens`, None (default) ->
250-
# let the provider config decide (for Azure, from the api_version).
251-
# Overrides that detection, so it is the authoritative control when a
252-
# deployment's requirement can't be inferred from provider + api_version.
253-
use_max_completion_tokens: Optional[bool] = None
254248
model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)
255249
merge_reasoning_content_in_choices: Optional[bool] = False
256250
model_info: Optional[Dict] = None

0 commit comments

Comments
 (0)