Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 17 additions & 7 deletions agent/auxiliary_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -4726,22 +4726,32 @@ def _build_call_kwargs(

if max_tokens is not None:
# Codex adapter handles max_tokens internally; OpenRouter/Nous use max_tokens.
# Direct OpenAI api.openai.com with newer models needs max_completion_tokens.
# Direct OpenAI api.openai.com and GitHub Copilot api.githubcopilot.com with
# newer GPT-4o/o-series/GPT-5 models reject max_tokens and require
# max_completion_tokens instead.
# ZAI vision models (glm-4v-flash, glm-4v-plus, etc.) reject max_tokens with
# error code 1210 ("API 调用参数有误") on multimodal requests — skip it.
_model_lower = (model or "").lower()
_skip_max_tokens = (
provider == "zai"
and ("4v" in _model_lower or "5v" in _model_lower or "-v" in _model_lower)
)
# Endpoints that require max_completion_tokens for newer OpenAI models,
# keyed on the resolved request hostname rather than the provider label.
# GitHub Copilot normalises to provider="copilot" (not "custom"), so a
# provider=="custom" gate alone misses it — mirror auxiliary_max_tokens_param,
# which already lists both hosts. See issue #34530.
_resolved_base = base_url
if not _resolved_base and provider == "custom":
_resolved_base = _current_custom_base_url()
_wants_max_completion = base_url_hostname(_resolved_base) in {
"api.openai.com",
"api.githubcopilot.com",
}
if _skip_max_tokens:
pass # ZAI vision models do not accept max_tokens
elif provider == "custom":
custom_base = base_url or _current_custom_base_url()
if base_url_hostname(custom_base) == "api.openai.com":
kwargs["max_completion_tokens"] = max_tokens
else:
kwargs["max_tokens"] = max_tokens
elif _wants_max_completion:
kwargs["max_completion_tokens"] = max_tokens
else:
kwargs["max_tokens"] = max_tokens

Expand Down
81 changes: 81 additions & 0 deletions tests/agent/test_auxiliary_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,87 @@ def test_uses_max_completion_tokens_for_github_copilot_custom_base_path(self):
assert auxiliary_max_tokens_param(2048) == {"max_completion_tokens": 2048}


class TestBuildCallKwargsMaxTokensParam:
"""_build_call_kwargs must emit max_completion_tokens for endpoints that
reject max_tokens on newer OpenAI models, keyed on the resolved request
hostname rather than the provider label.

Regression test for issue #34530: auxiliary context compression configured
with provider=github-copilot (which normalises to provider="copilot", not
"custom") and a GPT-5 model sent max_tokens and failed with HTTP 400
"Unsupported parameter: 'max_tokens' ... Use 'max_completion_tokens'".
"""

def test_github_copilot_provider_uses_max_completion_tokens(self):
# github-copilot normalises to provider="copilot"; the bug was that the
# max_completion_tokens path was gated on provider=="custom".
kwargs = _build_call_kwargs(
provider="copilot",
model="gpt-5.4",
messages=[{"role": "user", "content": "hi"}],
max_tokens=1024,
base_url="https://api.githubcopilot.com",
)
assert kwargs.get("max_completion_tokens") == 1024
assert "max_tokens" not in kwargs

def test_github_copilot_base_with_path_uses_max_completion_tokens(self):
kwargs = _build_call_kwargs(
provider="copilot",
model="gpt-5.5",
messages=[{"role": "user", "content": "hi"}],
max_tokens=512,
base_url="https://api.githubcopilot.com/chat/completions",
)
assert kwargs.get("max_completion_tokens") == 512
assert "max_tokens" not in kwargs

def test_openai_base_still_uses_max_completion_tokens(self):
kwargs = _build_call_kwargs(
provider="openai",
model="gpt-5.4",
messages=[{"role": "user", "content": "hi"}],
max_tokens=2048,
base_url="https://api.openai.com/v1",
)
assert kwargs.get("max_completion_tokens") == 2048
assert "max_tokens" not in kwargs

def test_custom_openai_base_still_uses_max_completion_tokens(self):
# Preserve the pre-existing provider=="custom" + api.openai.com behaviour.
kwargs = _build_call_kwargs(
provider="custom",
model="gpt-5.4",
messages=[{"role": "user", "content": "hi"}],
max_tokens=2048,
base_url="https://api.openai.com/v1",
)
assert kwargs.get("max_completion_tokens") == 2048
assert "max_tokens" not in kwargs

def test_openrouter_still_uses_max_tokens(self):
kwargs = _build_call_kwargs(
provider="openrouter",
model="openai/gpt-5.4",
messages=[{"role": "user", "content": "hi"}],
max_tokens=4096,
base_url="https://openrouter.ai/api/v1",
)
assert kwargs.get("max_tokens") == 4096
assert "max_completion_tokens" not in kwargs

def test_no_base_url_non_custom_provider_uses_max_tokens(self):
# Defensive: an unknown host (empty) must keep max_tokens, not flip.
kwargs = _build_call_kwargs(
provider="deepseek",
model="deepseek-chat",
messages=[{"role": "user", "content": "hi"}],
max_tokens=1000,
)
assert kwargs.get("max_tokens") == 1000
assert "max_completion_tokens" not in kwargs


class TestNormalizeAuxProvider:
def test_maps_github_copilot_aliases(self):
assert _normalize_aux_provider("github") == "copilot"
Expand Down
15 changes: 12 additions & 3 deletions tests/agent/test_unsupported_temperature_retry.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,12 @@ def test_retries_once_without_temperature(self, error_message):
retry_kwargs = client.chat.completions.create.call_args_list[1].kwargs
assert first_kwargs["temperature"] == 0.3
assert "temperature" not in retry_kwargs
# other kwargs preserved
assert retry_kwargs["max_tokens"] == 500
# other kwargs preserved. api.openai.com normalises max_tokens to
# max_completion_tokens (issue #34530), so accept either.
assert (
retry_kwargs.get("max_tokens") == 500
or retry_kwargs.get("max_completion_tokens") == 500
)

def test_non_temperature_400_does_not_retry_as_temperature(self):
"""Unrelated 400s (e.g. bad tool role) must not silently drop temp."""
Expand Down Expand Up @@ -207,7 +211,12 @@ async def test_async_retries_once_without_temperature(self):
retry_kwargs = client.chat.completions.create.call_args_list[1].kwargs
assert first_kwargs["temperature"] == 0.3
assert "temperature" not in retry_kwargs
assert retry_kwargs["max_tokens"] == 500
# api.openai.com normalises max_tokens to max_completion_tokens
# (issue #34530), so accept either form on the retry call.
assert (
retry_kwargs.get("max_tokens") == 500
or retry_kwargs.get("max_completion_tokens") == 500
)

@pytest.mark.asyncio
async def test_async_non_temperature_400_does_not_retry(self):
Expand Down
Loading