mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-11-02 10:52:22 +00:00
fix: send supported reasoning parameters to gpt-5.1 and gpt-6 models
This commit is contained in:
@@ -5,6 +5,7 @@ Models are loaded dynamically from the API.
|
|||||||
"""
|
"""
|
||||||
from typing import Optional, List
|
from typing import Optional, List
|
||||||
import json
|
import json
|
||||||
|
import re
|
||||||
import urllib.request
|
import urllib.request
|
||||||
import urllib.error
|
import urllib.error
|
||||||
from .base import AIProvider, AIProviderError
|
from .base import AIProvider, AIProviderError
|
||||||
@@ -36,11 +37,11 @@ class OpenAIProvider(AIProvider):
|
|||||||
]
|
]
|
||||||
|
|
||||||
# Recommended models for chat (in priority order)
|
# Recommended models for chat (in priority order)
|
||||||
RECOMMENDED_PREFIXES = ['gpt-4o-mini', 'gpt-4o', 'gpt-4-turbo', 'gpt-4', 'gpt-3.5-turbo']
|
RECOMMENDED_PREFIXES = ['gpt-6-luna', 'gpt-5.6-luna', 'gpt-5.4-mini', 'gpt-6-sol']
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _is_reasoning_model(model: str) -> bool:
|
def _is_reasoning_model(model: str) -> bool:
|
||||||
"""True for OpenAI reasoning models (o-series + non-chat gpt-5+).
|
"""True for OpenAI reasoning models (o-series + non-chat gpt-5 and later).
|
||||||
|
|
||||||
These use a stricter API contract than chat models:
|
These use a stricter API contract than chat models:
|
||||||
- Must use ``max_completion_tokens`` instead of ``max_tokens``
|
- Must use ``max_completion_tokens`` instead of ``max_tokens``
|
||||||
@@ -54,10 +55,23 @@ class OpenAIProvider(AIProvider):
|
|||||||
# o1, o3, o4, o5 ... (o<digit>...)
|
# o1, o3, o4, o5 ... (o<digit>...)
|
||||||
if len(m) >= 2 and m[0] == 'o' and m[1].isdigit():
|
if len(m) >= 2 and m[0] == 'o' and m[1].isdigit():
|
||||||
return True
|
return True
|
||||||
# gpt-5, gpt-5-mini, gpt-5.1, gpt-5.2-pro ... EXCEPT *-chat-latest
|
# gpt-5, gpt-5-mini, gpt-5.1, gpt-6-luna, gpt-6.1-sol ... EXCEPT *-chat-latest
|
||||||
if m.startswith('gpt-5') and '-chat' not in m:
|
major = re.match(r'gpt-(\d+)', m)
|
||||||
return True
|
return bool(major) and int(major.group(1)) >= 5 and '-chat' not in m
|
||||||
return False
|
|
||||||
|
@staticmethod
|
||||||
|
def _reasoning_effort(model: str) -> str:
|
||||||
|
"""Lowest reasoning_effort the model accepts.
|
||||||
|
|
||||||
|
gpt-5 / gpt-5-mini / gpt-5-nano (and the o-series) accept 'minimal'.
|
||||||
|
gpt-5.1 and later reject 'minimal' with HTTP 400, and gpt-6-astra /
|
||||||
|
gpt-6.1-sol also reject 'none', so 'low' is the value all of them
|
||||||
|
accept. In tests it spent 0-5 reasoning tokens on a notification.
|
||||||
|
"""
|
||||||
|
m = model.lower()
|
||||||
|
if re.match(r'gpt-5(?!\.)', m) or m.startswith('o'):
|
||||||
|
return 'minimal'
|
||||||
|
return 'low'
|
||||||
|
|
||||||
def list_models(self) -> List[str]:
|
def list_models(self) -> List[str]:
|
||||||
"""List available models for chat completions.
|
"""List available models for chat completions.
|
||||||
@@ -189,22 +203,19 @@ class OpenAIProvider(AIProvider):
|
|||||||
# apply model-specific defaults.
|
# apply model-specific defaults.
|
||||||
if self.base_url:
|
if self.base_url:
|
||||||
payload['max_tokens'] = max_tokens
|
payload['max_tokens'] = max_tokens
|
||||||
# Reasoning models (o1/o3/o4/gpt-5*, excluding *-chat-latest) use a
|
# Reasoning models (o1/o3/o4/gpt-5+, excluding *-chat-latest) use a
|
||||||
# different parameter contract: max_completion_tokens instead of
|
# different parameter contract: max_completion_tokens instead of
|
||||||
# max_tokens, and no temperature field. Sending the classic chat
|
# max_tokens, and no temperature field. Sending the classic chat
|
||||||
# parameters to them produces HTTP 400 Bad Request.
|
# parameters to them produces HTTP 400 Bad Request.
|
||||||
#
|
#
|
||||||
# They also spend output budget on internal reasoning by default,
|
# They also spend output budget on internal reasoning by default,
|
||||||
# which empties the user-visible reply when max_tokens is small
|
# which empties the user-visible reply when max_tokens is small
|
||||||
# (like the ~200 we use for notifications). reasoning_effort
|
# (like the ~200 we use for notifications). The lowest
|
||||||
# 'minimal' keeps that internal reasoning to a minimum so the
|
# reasoning_effort the model accepts keeps that internal reasoning
|
||||||
# entire budget is available for the translation, which is
|
# to a minimum so the budget goes to the translation.
|
||||||
# exactly what this pipeline wants. OpenAI documents 'minimal',
|
|
||||||
# 'low', 'medium', 'high' — 'minimal' is the right setting for a
|
|
||||||
# straightforward translate+explain task.
|
|
||||||
elif self._is_reasoning_model(self.model):
|
elif self._is_reasoning_model(self.model):
|
||||||
payload['max_completion_tokens'] = max_tokens
|
payload['max_completion_tokens'] = max_tokens
|
||||||
payload['reasoning_effort'] = 'minimal'
|
payload['reasoning_effort'] = self._reasoning_effort(self.model)
|
||||||
else:
|
else:
|
||||||
payload['max_tokens'] = max_tokens
|
payload['max_tokens'] = max_tokens
|
||||||
payload['temperature'] = 0.3
|
payload['temperature'] = 0.3
|
||||||
|
|||||||
@@ -98,6 +98,19 @@ class OpenAICompatibleModelTests(unittest.TestCase):
|
|||||||
self.assertNotIn("temperature", provider.captured_payload)
|
self.assertNotIn("temperature", provider.captured_payload)
|
||||||
self.assertNotIn("max_tokens", provider.captured_payload)
|
self.assertNotIn("max_tokens", provider.captured_payload)
|
||||||
|
|
||||||
|
def test_newer_reasoning_models_use_low_effort(self):
|
||||||
|
# gpt-5.1+ reject 'minimal'; gpt-6* were sent chat parameters before.
|
||||||
|
for model in ("gpt-5.6-luna", "gpt-6-luna", "gpt-6.1-sol"):
|
||||||
|
with self.subTest(model=model):
|
||||||
|
provider = CapturingOpenAIProvider(api_key="token", model=model)
|
||||||
|
|
||||||
|
self.assertEqual(provider.generate("system", "user", max_tokens=123), "ok")
|
||||||
|
|
||||||
|
self.assertEqual(provider.captured_payload["max_completion_tokens"], 123)
|
||||||
|
self.assertEqual(provider.captured_payload["reasoning_effort"], "low")
|
||||||
|
self.assertNotIn("temperature", provider.captured_payload)
|
||||||
|
self.assertNotIn("max_tokens", provider.captured_payload)
|
||||||
|
|
||||||
def test_runtime_ai_config_prefers_provider_specific_model(self):
|
def test_runtime_ai_config_prefers_provider_specific_model(self):
|
||||||
manager = self._manager({
|
manager = self._manager({
|
||||||
"ai_enabled": "true",
|
"ai_enabled": "true",
|
||||||
|
|||||||
Reference in New Issue
Block a user