From 2ef454612416aaaef590bc458edb5721facc624a Mon Sep 17 00:00:00 2001 From: Louisss Date: Thu, 1 Oct 2026 22:02:55 +0800 Subject: [PATCH] fix(llmcore): responses payload parity - temperature passthrough, ultra effort Two silent-config bugs in the OpenAI Responses branch (issue #813, A-4/A-5, root-caused by GiftedScout): 1. temperature: the chat_completions branch and ClaudeSession.raw_ask both send the user-configured value behind `if temperature != 1`; the responses branch never did, so temperature in mykey.py silently no-op'd for every responses endpoint. Official Responses API supports the field (default 1.0), so the chat-branch guard is reused verbatim - the kimi/moonshot force-to-1 and MiniMax clamp upstream of the branch still apply. 2. reasoning_effort 'ultra': BaseSession._enum's whitelist ended at 'max', so GPT-5.6's 'ultra' was dropped with a single WARN line and the payload fell back to the endpoint default. One-line whitelist addition; the Claude-only output_config.effort mapping still warns on ultra there. Tests: frontends/tests/test_responses_payload_parity.py - monkeypatches requests.post to capture the real payload through a real NativeOAISession.raw_ask (canned SSE, no network/credentials): temperature present when configured / omitted at default 1 / omitted under model overrides, ultra survives _enum and reaches payload, high unchanged, bogus still dropped, chat_completions branch shape pinned as regression guard. Full suite: 279 passed, same 3 pre-existing Windows-env failures. Co-Authored-By: GiftedScout --- .../tests/test_responses_payload_parity.py | 151 ++++++++++++++++++ llmcore.py | 5 +- 2 files changed, 155 insertions(+), 1 deletion(-) create mode 100644 frontends/tests/test_responses_payload_parity.py diff --git a/frontends/tests/test_responses_payload_parity.py b/frontends/tests/test_responses_payload_parity.py new file mode 100644 index 000000000..9311ec64a --- /dev/null +++ b/frontends/tests/test_responses_payload_parity.py @@ -0,0 +1,151 @@ +# -*- coding: utf-8 -*- +"""Regression: responses-mode payload parity (upstream issue #813, A-4/A-5). + +Two silent-config bugs in the OpenAI Responses branch of llmcore: + +1. `temperature` — the chat_completions branch sends the user-configured value + (`if temperature != 1`, same pattern as ClaudeSession.raw_ask), the + responses branch never did: temperature in mykey.py silently no-op'd for + every responses endpoint. Official Responses API supports the field + (default 1.0), so the chat-branch guard is reused verbatim. +2. `reasoning_effort` — BaseSession._enum's whitelist lacked 'ultra' + (GPT-5.6 tier), so the value was dropped with a single WARN line and the + payload silently fell back to the endpoint default. + +Verification: monkeypatch llmcore.requests.post to capture the real payload +and return a canned SSE stream; drive real NativeOAISession.raw_ask. No +network, no credentials. +""" +import sys +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parent.parent.parent + +# test_bridge_sessions.py setdefault()s an empty llmcore stub into sys.modules; +# under full-suite collection it can shadow the real module before us. +_stub = sys.modules.get("llmcore") +if _stub is not None and not hasattr(_stub, "NativeOAISession"): + del sys.modules["llmcore"] +sys.path.insert(0, str(ROOT)) + +import llmcore # noqa: E402 + + +_SSE_LINES = [ + 'data: {"type":"response.output_text.delta","delta":"hi"}', + 'data: {"type":"response.completed","response":{"usage":{"input_tokens":1,"output_tokens":1}}}', + 'data: [DONE]', '', +] + +_CHAT_SSE_LINES = [ + 'data: {"choices":[{"delta":{"content":"hi"}}]}', + 'data: {"choices":[{"delta":{},"usage":{"prompt_tokens":1,"completion_tokens":1}}]}', + 'data: [DONE]', '', +] + + +class FakeResponse: + """Upstream calls requests.post as a context manager (TTFT/abort support, + added after GiftedScout's fork point), so the fake supports the protocol + too — a plain object would AttributeError on __enter__.""" + status_code = 200 + + def __init__(self, lines): + self._lines = lines + + def __enter__(self): + return self + + def __exit__(self, *exc): + return False + + def iter_lines(self): + for line in self._lines: + yield line + + def json(self): + return {} + + +def _cfg(**over): + cfg = { + 'apikey': 'sk-test', 'apibase': 'https://api.example.com/v1', + 'model': 'test-model', 'api_mode': 'responses', + 'system': 'S', 'stream': True, 'max_retries': 0, + } + cfg.update(over) + return cfg + + +@pytest.fixture +def capture(monkeypatch): + seen = {} + + def fake_post(url, headers=None, json=None, stream=False, timeout=None, **kw): + seen['url'] = url + seen['payload'] = json + seen['kw'] = kw + lines = _CHAT_SSE_LINES if 'chat/completions' in url else _SSE_LINES + return FakeResponse(lines) + + monkeypatch.setattr(llmcore.requests, 'post', fake_post) + return seen + + +def _ask_payload(capture, **cfgover): + sess = llmcore.NativeOAISession(_cfg(**cfgover)) + list(sess.raw_ask([{'role': 'user', 'content': 'x'}])) + return capture, sess + + +# --- temperature ------------------------------------------------------------ + +def test_responses_payload_includes_configured_temperature(capture): + seen, _ = _ask_payload(capture, temperature=0.5) + assert seen['payload']['temperature'] == 0.5 + + +def test_responses_temperature_default_still_omitted(capture): + seen, _ = _ask_payload(capture, temperature=1) + assert 'temperature' not in seen['payload'] + + +def test_responses_temperature_respects_model_override(capture): + # kimi/moonshot force temperature to 1 before either branch builds a payload + seen, _ = _ask_payload(capture, model='kimi-k2', temperature=0.5) + assert 'temperature' not in seen['payload'] + + +# --- reasoning_effort ------------------------------------------------------- + +def test_reasoning_effort_ultra_survives_enum_and_payload(capture): + seen, sess = _ask_payload(capture, reasoning_effort='ultra') + assert sess.reasoning_effort == 'ultra' + assert seen['payload']['reasoning'] == {'effort': 'ultra'} + + +def test_reasoning_effort_high_unchanged(capture): + seen, sess = _ask_payload(capture, reasoning_effort='high') + assert sess.reasoning_effort == 'high' + assert seen['payload']['reasoning'] == {'effort': 'high'} + + +def test_reasoning_effort_invalid_still_dropped(capture): + seen, sess = _ask_payload(capture, reasoning_effort='bogus') + assert sess.reasoning_effort is None + assert 'reasoning' not in seen['payload'] + + +# --- chat_completions branch regression ------------------------------------- + +def test_chat_branch_temperature_and_effort_shape(capture): + seen, _ = _ask_payload(capture, api_mode='chat_completions', temperature=0.3, + reasoning_effort='ultra', max_tokens=2048) + p = seen['payload'] + assert p['temperature'] == 0.3 + assert p['reasoning_effort'] == 'ultra' # flat key, not the responses shape + assert 'reasoning' not in p + assert p['max_tokens'] == 2048 + assert 'chat/completions' in seen['url'] diff --git a/llmcore.py b/llmcore.py index b9077f5df..83b0f97e9 100644 --- a/llmcore.py +++ b/llmcore.py @@ -528,6 +528,9 @@ def _openai_stream(sess, messages): 'include': ['reasoning.encrypted_content']} if sess.reasoning_effort: payload["reasoning"] = {"effort": sess.reasoning_effort} if sess.max_tokens: payload["max_output_tokens"] = sess.max_tokens + # 对齐 chat 分支:Responses API 官方支持 temperature(默认1.0),此前该分支 + # 漏传,用户在 mykey 里配的 temperature 对 responses 端点静默失效。 + if temperature != 1: payload["temperature"] = temperature else: url = auto_make_url(sess.api_base, "chat/completions") if sess.system: messages = [{"role": "system", "content": sess.system}] + messages @@ -665,7 +668,7 @@ def __init__(self, cfg): def _enum(key, valid): v = cfg.get(key); v = None if v is None else str(v).strip().lower() return v if not v or v in valid else print(f"[WARN] Invalid {key} {v!r}, ignored.") - self.reasoning_effort = _enum('reasoning_effort', {'none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'}) + self.reasoning_effort = _enum('reasoning_effort', {'none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'ultra'}) self.service_tier = _enum('service_tier', {'auto', 'default', 'priority', 'flex'}) self.thinking_type = _enum('thinking_type', {'adaptive', 'enabled', 'disabled'}) self.thinking_budget_tokens = cfg.get('thinking_budget_tokens')