mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-11 09:22:21 +02:00
feat(ui): refine subscription model and usage controls
This commit is contained in:
@@ -20364,6 +20364,7 @@ async def stream_agent_loop(
|
||||
force_textual_tool_transport: bool = False,
|
||||
thinking_mode: Optional[str] = None,
|
||||
suppress_skills: bool = False,
|
||||
reasoning_effort: Optional[str] = None,
|
||||
) -> AsyncGenerator[str, None]:
|
||||
"""Streaming agent loop generator.
|
||||
|
||||
@@ -21475,6 +21476,8 @@ async def stream_agent_loop(
|
||||
timeout=int(get_setting("agent_stream_timeout_seconds", 300) or 300),
|
||||
session_id=session_id,
|
||||
workload=workload,
|
||||
thinking_mode=thinking_mode,
|
||||
reasoning_effort=reasoning_effort,
|
||||
fallback_statuses=fallback_statuses,
|
||||
fallback_on_empty=fallback_on_empty,
|
||||
candidate_request_factory=_direct_candidate_request,
|
||||
@@ -25480,6 +25483,7 @@ async def stream_agent_loop(
|
||||
else _requested_temperature
|
||||
),
|
||||
"thinking_mode": state.get("thinking_mode"),
|
||||
"reasoning_effort": reasoning_effort,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -26172,6 +26176,8 @@ async def stream_agent_loop(
|
||||
timeout=agent_stream_timeout,
|
||||
session_id=session_id,
|
||||
workload=workload,
|
||||
thinking_mode=thinking_mode,
|
||||
reasoning_effort=reasoning_effort,
|
||||
fallback_statuses=fallback_statuses,
|
||||
fallback_on_empty=fallback_on_empty,
|
||||
candidate_request_factory=_candidate_request,
|
||||
|
||||
+102
-1
@@ -147,6 +147,99 @@ def labels_conflict(a: str, b: str) -> bool:
|
||||
return bool(a) and bool(b) and a.casefold() == b.casefold()
|
||||
|
||||
|
||||
KNOWN_CODEX_REASONING_LEVELS = [
|
||||
"none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra", "persistent"
|
||||
]
|
||||
|
||||
STANDARD_CODEX_REASONING_LEVELS = [
|
||||
"low", "medium", "high", "xhigh", "max", "ultra"
|
||||
]
|
||||
|
||||
DEFAULT_CHATGPT_MODEL_CATALOG: dict[str, dict[str, Any]] = {
|
||||
"gpt-6-astra": {
|
||||
"default_reasoning_level": "low",
|
||||
"supported_reasoning_levels": ["low", "medium", "high", "xhigh", "max", "ultra"],
|
||||
},
|
||||
"gpt-5.6-sol": {
|
||||
"default_reasoning_level": "low",
|
||||
"supported_reasoning_levels": ["low", "medium", "high", "xhigh", "max", "ultra"],
|
||||
},
|
||||
"gpt-5.6-terra": {
|
||||
"default_reasoning_level": "medium",
|
||||
"supported_reasoning_levels": ["low", "medium", "high", "xhigh", "max", "ultra"],
|
||||
},
|
||||
"gpt-5.6-luna": {
|
||||
"default_reasoning_level": "medium",
|
||||
"supported_reasoning_levels": ["low", "medium", "high", "xhigh", "max", "ultra"],
|
||||
},
|
||||
"gpt-5.5": {
|
||||
"default_reasoning_level": "medium",
|
||||
"supported_reasoning_levels": ["low", "medium", "high", "xhigh", "max", "ultra"],
|
||||
},
|
||||
"gpt-5.4": {
|
||||
"default_reasoning_level": "medium",
|
||||
"supported_reasoning_levels": ["low", "medium", "high", "xhigh", "max", "ultra"],
|
||||
},
|
||||
"codex-auto-review": {
|
||||
"default_reasoning_level": "medium",
|
||||
"supported_reasoning_levels": ["low", "medium", "high", "xhigh", "max", "ultra"],
|
||||
},
|
||||
}
|
||||
|
||||
# Runtime cache of model metadata (updated dynamically whenever models are fetched)
|
||||
CHATGPT_MODEL_CATALOG_CACHE: dict[str, dict[str, Any]] = dict(DEFAULT_CHATGPT_MODEL_CATALOG)
|
||||
|
||||
|
||||
def _extract_reasoning_levels(item: dict) -> list[str]:
|
||||
raw_levels = item.get("supported_reasoning_levels") or item.get("supportedReasoningEfforts")
|
||||
if not isinstance(raw_levels, list):
|
||||
return []
|
||||
levels: list[str] = []
|
||||
for entry in raw_levels:
|
||||
if isinstance(entry, dict):
|
||||
effort = entry.get("effort") or entry.get("level") or entry.get("name")
|
||||
if effort and isinstance(effort, str):
|
||||
levels.append(effort.strip().lower())
|
||||
elif isinstance(entry, str) and entry.strip():
|
||||
levels.append(entry.strip().lower())
|
||||
return levels
|
||||
|
||||
|
||||
def get_chatgpt_model_metadata(slug: str) -> Optional[dict[str, Any]]:
|
||||
slug = (slug or "").strip()
|
||||
if not slug:
|
||||
return None
|
||||
if slug in CHATGPT_MODEL_CATALOG_CACHE:
|
||||
return dict(CHATGPT_MODEL_CATALOG_CACHE[slug])
|
||||
for k, v in CHATGPT_MODEL_CATALOG_CACHE.items():
|
||||
if k.casefold() == slug.casefold():
|
||||
return dict(v)
|
||||
slug_lower = slug.lower()
|
||||
if any(pat in slug_lower for pat in ("gpt-6", "gpt-5.6", "gpt-5.5", "gpt-5.4", "codex")):
|
||||
return {
|
||||
"default_reasoning_level": "medium",
|
||||
"supported_reasoning_levels": list(STANDARD_CODEX_REASONING_LEVELS),
|
||||
}
|
||||
return None
|
||||
|
||||
|
||||
def validate_reasoning_effort(model: str, effort: Optional[str]) -> Optional[str]:
|
||||
"""Validate reasoning effort against model's advertised levels.
|
||||
Returns None if default/empty/unsupported (fail-safe to omitting override)."""
|
||||
if not effort:
|
||||
return None
|
||||
effort_clean = str(effort).strip().lower()
|
||||
if effort_clean in {"", "default"}:
|
||||
return None
|
||||
meta = get_chatgpt_model_metadata(model)
|
||||
if not meta:
|
||||
return None
|
||||
supported = [lvl.lower() for lvl in meta.get("supported_reasoning_levels", [])]
|
||||
if effort_clean in supported:
|
||||
return effort_clean
|
||||
return None
|
||||
|
||||
|
||||
def fetch_available_models(access_token: str, timeout: float = 10.0) -> list[str]:
|
||||
if not access_token:
|
||||
return []
|
||||
@@ -169,12 +262,20 @@ def fetch_available_models(access_token: str, timeout: float = 10.0) -> list[str
|
||||
slug = item.get("slug")
|
||||
if not isinstance(slug, str) or not slug.strip():
|
||||
continue
|
||||
slug_clean = slug.strip()
|
||||
visibility = item.get("visibility", "")
|
||||
if isinstance(visibility, str) and visibility.strip().lower() in {"hide", "hidden"}:
|
||||
continue
|
||||
levels = _extract_reasoning_levels(item)
|
||||
default_lvl = item.get("default_reasoning_level") or item.get("defaultReasoningEffort")
|
||||
if levels:
|
||||
CHATGPT_MODEL_CATALOG_CACHE[slug_clean] = {
|
||||
"default_reasoning_level": str(default_lvl).strip().lower() if default_lvl else (levels[0] if levels else "medium"),
|
||||
"supported_reasoning_levels": levels,
|
||||
}
|
||||
priority = item.get("priority")
|
||||
rank = int(priority) if isinstance(priority, (int, float)) else 10_000
|
||||
sortable.append((rank, slug.strip()))
|
||||
sortable.append((rank, slug_clean))
|
||||
sortable.sort(key=lambda item: (item[0], item[1]))
|
||||
ordered: list[str] = []
|
||||
seen: set[str] = set()
|
||||
|
||||
+18
-5
@@ -254,7 +254,8 @@ def _cache_header_identity(headers) -> str:
|
||||
|
||||
def _get_cache_key(url: str, model: str, messages: List[Dict],
|
||||
temperature: float, max_tokens: int, headers=None,
|
||||
thinking_mode: Optional[str] = None) -> str:
|
||||
thinking_mode: Optional[str] = None,
|
||||
reasoning_effort: Optional[str] = None) -> str:
|
||||
"""Generate a cache key partitioned by endpoint and credential identity."""
|
||||
hashable_messages = []
|
||||
for msg in messages:
|
||||
@@ -268,6 +269,7 @@ def _get_cache_key(url: str, model: str, messages: List[Dict],
|
||||
'temp': temperature,
|
||||
'max_tokens': max_tokens,
|
||||
'thinking_mode': _normalize_thinking_mode(thinking_mode),
|
||||
'reasoning_effort': str(reasoning_effort or "").strip().lower(),
|
||||
# Never put credentials in a cache key or loggable cache payload. The
|
||||
# digest only prevents responses from one configured account/route
|
||||
# being returned under another route with the same URL and model.
|
||||
@@ -1531,6 +1533,7 @@ def _build_chatgpt_responses_payload(
|
||||
*,
|
||||
stream: bool = False,
|
||||
tools: Optional[List[Dict]] = None,
|
||||
reasoning_effort: Optional[str] = None,
|
||||
**_ignored,
|
||||
) -> Dict:
|
||||
"""Build the ChatGPT/Codex Responses request: model inference only.
|
||||
@@ -1555,11 +1558,13 @@ def _build_chatgpt_responses_payload(
|
||||
# ChatGPT Subscription Codex API does not support max_output_tokens —
|
||||
# passing it returns HTTP 400 "Unsupported parameter: max_output_tokens".
|
||||
# Do not include it in the payload.
|
||||
if reasoning_effort and str(reasoning_effort).strip().lower() not in {"", "default"}:
|
||||
payload["reasoning"] = {"effort": str(reasoning_effort).strip().lower()}
|
||||
return _strip_chatgpt_native_tool_surfaces(payload)
|
||||
|
||||
|
||||
CHATGPT_ALLOWED_PAYLOAD_KEYS = frozenset({
|
||||
"model", "instructions", "input", "stream", "store", "temperature",
|
||||
"model", "instructions", "input", "stream", "store", "temperature", "reasoning",
|
||||
})
|
||||
|
||||
|
||||
@@ -2575,6 +2580,7 @@ async def llm_call_async(
|
||||
availability_only_transport: bool = False,
|
||||
return_model_metadata: bool = False,
|
||||
thinking_mode: Optional[str] = None,
|
||||
reasoning_effort: Optional[str] = None,
|
||||
) -> str | tuple[str, str]:
|
||||
"""Asynchronous LLM call using httpx with connection pooling, timeout, retry logic, and performance logging."""
|
||||
provider = _detect_provider(url)
|
||||
@@ -2613,7 +2619,7 @@ async def llm_call_async(
|
||||
|
||||
cache_key = _get_cache_key(
|
||||
url, model, messages_copy, temperature, max_tokens, headers=headers,
|
||||
thinking_mode=thinking_mode,
|
||||
thinking_mode=thinking_mode, reasoning_effort=reasoning_effort,
|
||||
)
|
||||
cached_response = _get_cached_response(cache_key)
|
||||
if cached_response:
|
||||
@@ -2637,6 +2643,8 @@ async def llm_call_async(
|
||||
headers=headers,
|
||||
timeout=timeout,
|
||||
workload=workload,
|
||||
thinking_mode=thinking_mode,
|
||||
reasoning_effort=reasoning_effort,
|
||||
):
|
||||
event_is_error = False
|
||||
for line in str(chunk).splitlines():
|
||||
@@ -2891,7 +2899,7 @@ async def stream_llm(url: str, model: str, messages: List[Dict], temperature: fl
|
||||
timeout: int = LLMConfig.STREAM_TIMEOUT, prompt_type: Optional[str] = None,
|
||||
tools: Optional[List[Dict]] = None, session_id: Optional[str] = None,
|
||||
tool_choice_none: bool = False, workload: str = "foreground",
|
||||
thinking_mode: Optional[str] = None):
|
||||
thinking_mode: Optional[str] = None, reasoning_effort: Optional[str] = None):
|
||||
target_url = _stream_target_url(url)
|
||||
async with _local_model_slot(target_url, model, workload):
|
||||
async for chunk in _stream_llm_inner(
|
||||
@@ -2907,6 +2915,7 @@ async def stream_llm(url: str, model: str, messages: List[Dict], temperature: fl
|
||||
session_id=session_id,
|
||||
tool_choice_none=tool_choice_none,
|
||||
thinking_mode=thinking_mode,
|
||||
reasoning_effort=reasoning_effort,
|
||||
):
|
||||
yield chunk
|
||||
|
||||
@@ -2916,6 +2925,7 @@ async def _stream_llm_inner(url: str, model: str, messages: List[Dict], temperat
|
||||
timeout: int = LLMConfig.STREAM_TIMEOUT, prompt_type: Optional[str] = None,
|
||||
tools: Optional[List[Dict]] = None, session_id: Optional[str] = None,
|
||||
tool_choice_none: bool = False, thinking_mode: Optional[str] = None,
|
||||
reasoning_effort: Optional[str] = None,
|
||||
_retry_silent_local: bool = True):
|
||||
"""Stream LLM responses with improved error handling.
|
||||
|
||||
@@ -2958,7 +2968,10 @@ async def _stream_llm_inner(url: str, model: str, messages: List[Dict], temperat
|
||||
elif provider == "chatgpt-subscription":
|
||||
target_url = _normalize_chatgpt_subscription_url(url)
|
||||
h = _provider_headers(provider, headers)
|
||||
payload = _build_chatgpt_responses_payload(model, messages_copy, temperature, max_tokens, stream=True)
|
||||
payload = _build_chatgpt_responses_payload(
|
||||
model, messages_copy, temperature, max_tokens, stream=True,
|
||||
reasoning_effort=reasoning_effort,
|
||||
)
|
||||
else:
|
||||
target_url = _normalize_openai_chat_url(url)
|
||||
payload = {
|
||||
|
||||
@@ -13,6 +13,7 @@ class ChatRequest(BaseModel):
|
||||
time_filter: Optional[str] = Field(default=None, description="Time filter for search")
|
||||
preset_id: Optional[str] = Field(default=None, description="Preset identifier")
|
||||
selected_endpoint_id: Optional[str] = Field(default=None, description="Selected model endpoint ID")
|
||||
reasoning_effort: Optional[str] = Field(default=None, description="Reasoning effort override")
|
||||
|
||||
@field_validator('message')
|
||||
@classmethod
|
||||
|
||||
Reference in New Issue
Block a user