mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-04 02:31:27 +00:00
fix(oci): inject default maxTokens so omitted max_tokens doesn't truncate
OCI GenAI applies a tiny server-side maxTokens default (~20 tokens) when the request omits it, so any call that doesn't send max_tokens comes back cut off mid-string with finishReason "length". MLflow judges never send max_tokens, so their JSON responses arrived as unterminated strings and json.loads failed in MLflow's gateway adapter. When no maxTokens/maxCompletionTokens target is set, inject DEFAULT_OCI_CHAT_MAX_TOKENS (env-overridable, defaults 4096), mirroring the Anthropic config's default-max-tokens behaviour. An explicit max_tokens still wins, and reasoning models still route to maxCompletionTokens. Used a fixed default rather than the catalog max_output_tokens because the catalog value is unreliable for some models (grok-4 reports max_output_tokens equal to its context window, not a real output cap, which would risk 400s). Adds TestOCIDefaultMaxTokens covering Cohere and generic injection, the explicit-override case, and the reasoning maxCompletionTokens branch.
This commit is contained in:
parent
9ccda11919
commit
0c6378a320
3 changed files with 49 additions and 0 deletions
|
|
@ -418,6 +418,7 @@ REPLICATE_POLLING_DELAY_SECONDS = float(
|
|||
DEFAULT_ANTHROPIC_CHAT_MAX_TOKENS = int(
|
||||
os.getenv("DEFAULT_ANTHROPIC_CHAT_MAX_TOKENS", 4096)
|
||||
)
|
||||
DEFAULT_OCI_CHAT_MAX_TOKENS = int(os.getenv("DEFAULT_OCI_CHAT_MAX_TOKENS", 4096))
|
||||
TOGETHER_AI_4_B = int(os.getenv("TOGETHER_AI_4_B", 4))
|
||||
TOGETHER_AI_8_B = int(os.getenv("TOGETHER_AI_8_B", 8))
|
||||
TOGETHER_AI_21_B = int(os.getenv("TOGETHER_AI_21_B", 21))
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ from typing import (
|
|||
import httpx
|
||||
|
||||
import litellm
|
||||
from litellm.constants import DEFAULT_OCI_CHAT_MAX_TOKENS
|
||||
from litellm.litellm_core_utils.logging_utils import track_llm_api_timing
|
||||
from litellm.llms.base_llm.chat.transformation import BaseConfig, BaseLLMException
|
||||
from litellm.llms.custom_httpx.http_handler import (
|
||||
|
|
@ -451,6 +452,13 @@ class OCIChatConfig(BaseConfig):
|
|||
elif oci_alias in optional_params:
|
||||
selected_params[target] = optional_params[oci_alias] # type: ignore[index]
|
||||
|
||||
# OCI's server-side default token cap is tiny (~20 tokens), so an
|
||||
# omitted max_tokens silently truncates the response mid-string. Most
|
||||
# callers never send a limit (MLflow judges among them), so inject a
|
||||
# sane default when one is absent, mirroring litellm's Anthropic config.
|
||||
if max_tokens_key not in selected_params:
|
||||
selected_params[max_tokens_key] = DEFAULT_OCI_CHAT_MAX_TOKENS
|
||||
|
||||
# OCI expects uppercase reasoning levels (LOW/MEDIUM/HIGH/NONE); OpenAI
|
||||
# clients send lowercase. OpenAI's "disable" maps to OCI's "NONE".
|
||||
if "reasoningEffort" in selected_params:
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import litellm
|
|||
sys.path.insert(0, os.path.abspath("../../../../.."))
|
||||
|
||||
from litellm import ModelResponse
|
||||
from litellm.constants import DEFAULT_OCI_CHAT_MAX_TOKENS
|
||||
from litellm.llms.oci.chat.transformation import (
|
||||
OCIChatConfig,
|
||||
OCIRequestWrapper,
|
||||
|
|
@ -104,6 +105,7 @@ class TestOCIChatConfig:
|
|||
"chatRequest": {
|
||||
"apiFormat": "GENERIC",
|
||||
"isStream": False,
|
||||
"maxTokens": DEFAULT_OCI_CHAT_MAX_TOKENS,
|
||||
"messages": [
|
||||
{
|
||||
"role": "USER",
|
||||
|
|
@ -956,6 +958,44 @@ class TestOCICohereParamMapping:
|
|||
assert result.get("temperature") == 0.5
|
||||
|
||||
|
||||
class TestOCIDefaultMaxTokens:
|
||||
"""Regression for OCI's tiny server-side token cap (~20 tokens), which
|
||||
silently truncated responses mid-string whenever the caller omitted
|
||||
max_tokens (MLflow judges never send it, so their JSON came back cut off).
|
||||
transform_request injects DEFAULT_OCI_CHAT_MAX_TOKENS when no limit is
|
||||
supplied, and leaves an explicit limit untouched."""
|
||||
|
||||
def _chat_request(self, model: str, optional_params: dict) -> dict:
|
||||
config = OCIChatConfig()
|
||||
body = config.transform_request(
|
||||
model=model,
|
||||
messages=[{"role": "user", "content": "hi"}],
|
||||
optional_params={**BASE_OCI_PARAMS, **optional_params},
|
||||
litellm_params={},
|
||||
headers={},
|
||||
)
|
||||
return body["chatRequest"]
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"model", ["cohere.command-latest", "meta.llama-3.3-70b-instruct"]
|
||||
)
|
||||
def test_default_injected_when_max_tokens_omitted(self, model):
|
||||
chat_request = self._chat_request(model, {})
|
||||
assert chat_request["maxTokens"] == DEFAULT_OCI_CHAT_MAX_TOKENS
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"model", ["cohere.command-latest", "meta.llama-3.3-70b-instruct"]
|
||||
)
|
||||
def test_explicit_max_tokens_not_overridden(self, model):
|
||||
chat_request = self._chat_request(model, {"max_tokens": 256})
|
||||
assert chat_request["maxTokens"] == 256
|
||||
|
||||
def test_reasoning_model_defaults_max_completion_tokens(self):
|
||||
chat_request = self._chat_request("openai.gpt-5", {})
|
||||
assert chat_request["maxCompletionTokens"] == DEFAULT_OCI_CHAT_MAX_TOKENS
|
||||
assert "maxTokens" not in chat_request
|
||||
|
||||
|
||||
class TestOCIReasoningEffort:
|
||||
"""
|
||||
Reasoning-effort handling for GENERIC reasoning models:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue