OpenRouter stop cap is 4, GGUF tool-loop final pass forwards new fields

OpenRouter normalises to OpenAI's chat schema and inherits the 4-entry stop cap. The default 16-cap was too permissive; add stop_max=4 on both the backend provider registry and the frontend PROVIDER_STOP_MAX map.

The GGUF tool-iteration final-answer pass at llama_cpp.py:5182 was carrying only the legacy sampling fields. Forward frequency_penalty, seed, and parallel_tool_calls there too so the cap-exhausted path matches the per-iteration loop.

Test pins the OpenRouter 4-cap.
This commit is contained in:
Daniel Han 2026-05-24 15:33:42 +00:00
commit 1d1a205a19
4 changed files with 47 additions and 6 deletions

View file

@ -5202,6 +5202,14 @@ class LlamaCppBackend:
stream_payload["t_max_predict_ms"] = _DEFAULT_T_MAX_PREDICT_MS
if stop:
stream_payload["stop"] = stop
# Match the per-iteration tool loop above so sampling behavior
# stays consistent when the cap-exhausted final-answer pass runs.
if frequency_penalty is not None:
stream_payload["frequency_penalty"] = frequency_penalty
if seed is not None:
stream_payload["seed"] = seed
if parallel_tool_calls is not None:
stream_payload["parallel_tool_calls"] = parallel_tool_calls
stream_payload["stream_options"] = {"include_usage": True}
cumulative = ""

View file

@ -312,6 +312,9 @@ PROVIDER_REGISTRY: dict[str, dict[str, Any]] = {
},
"notes": "Unified gateway to 300+ models across all major providers. HTTP-Referer and X-Title headers sent for attribution.",
"model_list_mode": "curated",
# OpenRouter normalises to OpenAI's chat schema and inherits
# the 4-entry stop cap.
"stop_max": 4,
},
}

View file

@ -607,6 +607,34 @@ def test_kimi_web_search_uses_kimi_stop_cap_5(monkeypatch):
assert body["stop"] == ["S0", "S1", "S2", "S3", "S4"], body
def test_openrouter_stop_cap_is_4(monkeypatch):
"""OpenRouter normalises to OpenAI's chat schema and inherits the
4-entry stop cap; the default 16-cap is too permissive for it."""
captured = _install_mock(monkeypatch, sse_payload = _oai_done_payload())
async def run():
client = ExternalProviderClient(
provider_type = "openrouter",
base_url = "https://openrouter.ai/api/v1",
api_key = "or-test",
)
async for _ in client.stream_chat_completion(
messages = [{"role": "user", "content": "hi"}],
model = "openai/gpt-4o",
temperature = 0.5,
top_p = 0.9,
max_tokens = 64,
stop = [f"S{i}" for i in range(10)],
):
pass
await client.close()
_drive(run())
body = captured["body"]
assert len(body.get("stop", [])) == 4, body
assert body["stop"] == ["S0", "S1", "S2", "S3"], body
def test_kimi_drops_stop_strings_over_32_bytes(monkeypatch):
"""Kimi limits each stop string to <= 32 bytes per
https://platform.kimi.ai/docs/api/chat. Drop overlong entries

View file

@ -64,12 +64,13 @@ export interface ProviderCapabilities {
* Per-provider stop-sequence max count. Resolved by
* `getProviderStopMax(providerType)`. Mirrors the backend's
* `provider_info.stop_max` for the same provider type.
* - openai: 4 (Chat Completions hard cap; Responses drops stop)
* - anthropic: 16 (client-side guard; docs publish no max)
* - kimi: 5 (https://platform.kimi.ai/docs/api/chat)
* - deepseek: 16 (https://api-docs.deepseek.com/api/create-chat-completion)
* - mistral: 16 (no documented max; widen to permissive default)
* - default: 16 (covers ollama, vllm, llama.cpp, openrouter, custom)
* - openai: 4 (Chat Completions hard cap; Responses drops stop)
* - anthropic: 16 (client-side guard; docs publish no max)
* - kimi: 5 (https://platform.kimi.ai/docs/api/chat)
* - deepseek: 16 (https://api-docs.deepseek.com/api/create-chat-completion)
* - mistral: 16 (no documented max; widen to permissive default)
* - openrouter: 4 (normalises to OpenAI's chat schema)
* - default: 16 (covers ollama, vllm, llama.cpp, custom)
*/
const PROVIDER_STOP_MAX: Record<string, number> = {
openai: 4,
@ -77,6 +78,7 @@ const PROVIDER_STOP_MAX: Record<string, number> = {
kimi: 5,
deepseek: 16,
mistral: 16,
openrouter: 4,
};
export function getProviderStopMax(