Drop Kimi frequency_penalty and gate generic service_tier on opt-in

5/10 reviewers in the last round flagged Kimi forwarding non-default frequency_penalty as a 400 risk for K2.5 / K2.6, mirroring the existing lock on temperature and top_p. Hide the slider on the frontend and add frequency_penalty to Kimi's body_omit so even stale clients have the field stripped before the request hits the wire.

service_tier on the generic OpenAI-compatible branch was forwarding whatever value the dispatcher received, so a stale frontend could send standard_only (Anthropic) or scale to providers like Mistral that do not document the field, producing 400s. Gate the forward on an explicit accepts_service_tier=True provider registry opt-in; Anthropic and OpenAI Responses already handle service_tier inside their own helpers.
This commit is contained in:
Daniel Han 2026-05-24 15:54:26 +00:00
commit e0a9b1d76a
4 changed files with 27 additions and 13 deletions

View file

@ -519,7 +519,10 @@ class ExternalProviderClient:
normalized_stop = _normalize_stop_for_provider(stop, provider_info)
if normalized_stop:
body["stop"] = normalized_stop
if service_tier is not None:
# service_tier is OpenAI Chat-only on the generic OAI-compat
# branch; opt-in via `accepts_service_tier=True` on the registry
# entry. Anthropic and Responses handle it in their own helpers.
if service_tier is not None and provider_info.get("accepts_service_tier", False):
body["service_tier"] = service_tier
if parallel_tool_calls is not None:
body["parallel_tool_calls"] = parallel_tool_calls

View file

@ -157,11 +157,12 @@ PROVIDER_REGISTRY: dict[str, dict[str, Any]] = {
"auth_prefix": "Bearer ",
"notes": "Moonshot API key. China: use base URL https://api.moonshot.cn/v1",
"model_id_allowlist": re.compile(r"^kimi-k2\.[56]$"),
# Both k2.6 and k2.5 are reasoning-class. The API rejects custom
# sampling: "invalid temperature: only 1 is allowed for this model"
# (and the same shape for top_p). Strip both fields from the
# outbound body so the server falls back to its required defaults.
"body_omit": ("temperature", "top_p"),
# Both k2.6 and k2.5 are reasoning-class. The API rejects
# custom sampling: "invalid temperature: only 1 is allowed for
# this model" (and the same shape for top_p). frequency_penalty
# is reported by reviewers to follow the same lock; strip it
# too so non-default values from stale clients do not 400.
"body_omit": ("temperature", "top_p", "frequency_penalty"),
# Kimi accepts at most 5 stop strings (each <= 32 bytes) per
# https://platform.kimi.ai/docs/api/chat
"stop_max": 5,

View file

@ -360,10 +360,16 @@ def test_openai_compat_empty_stop_omitted(monkeypatch):
assert "stop" not in body, body
def test_openai_compat_forwards_service_tier(monkeypatch):
def test_openai_compat_drops_service_tier_by_default(monkeypatch):
"""Generic OAI-compat providers (mistral, deepseek, openrouter, ...)
do not document a `service_tier` field. The dispatcher must drop
it unless the provider registry explicitly opts in with
`accepts_service_tier=True`; otherwise a stale frontend could
smuggle Anthropic/OpenAI-Responses-only values onto unrelated
providers."""
captured = _install_mock(monkeypatch, sse_payload = _oai_done_payload())
body = _drive_openai_compat(captured, service_tier = "flex")
assert body.get("service_tier") == "flex", body
assert "service_tier" not in body, body
def test_openai_compat_forwards_parallel_tool_calls(monkeypatch):
@ -567,14 +573,15 @@ def test_kimi_web_search_bypass_forwards_new_sampling_fields(monkeypatch):
_drive(run())
body = captured["body"]
assert body.get("frequency_penalty") == 1.25, body
# Per-provider drops: Kimi locks frequency_penalty/temperature/top_p.
assert "frequency_penalty" not in body, body
assert "temperature" not in body, body
assert "top_p" not in body, body
# Other knobs forward through the bypass.
assert body.get("seed") == 7, body
assert body.get("stop") == ["END"], body
assert body.get("parallel_tool_calls") is False, body
assert body.get("presence_penalty") == 0.5, body
# body_omit still strips temperature / top_p for Kimi.
assert "temperature" not in body, body
assert "top_p" not in body, body
def test_kimi_web_search_uses_kimi_stop_cap_5(monkeypatch):

View file

@ -454,7 +454,10 @@ const PROVIDER_CAPABILITIES: Record<string, ProviderCapabilities> = {
minP: false,
repetitionPenalty: false,
presencePenalty: true,
frequencyPenalty: true,
// K2.5/K2.6 lock sampling the same way temperature/top_p are
// locked; reviewers report non-default frequency_penalty 400s
// upstream, so hide the slider and strip the field in body_omit.
frequencyPenalty: false,
seed: false,
stop: true,
serviceTier: false,