Checkpoint Odysseus local update

This commit is contained in:
pewdiepie-archdaemon
2026-07-07 00:50:07 +00:00
parent 5f6e6a2c4a
commit 017903de61
66 changed files with 22349 additions and 982 deletions
+248 -5
View File
@@ -8,13 +8,88 @@ import hashlib
import threading
import re
import os
from contextlib import asynccontextmanager
from fastapi import HTTPException
from typing import Optional, Dict, List, Tuple
from src.model_context import get_context_length, DEFAULT_CONTEXT
from src.model_context import get_context_length, DEFAULT_CONTEXT, is_local_endpoint
from urllib.parse import urlparse
logger = logging.getLogger(__name__)
_LOCAL_MODEL_LOCK = asyncio.Lock()
_LOCAL_MODEL_WAITING_FOREGROUND = 0
_LOCAL_MODEL_CURRENT: Dict[str, object] = {}
def _local_model_gate_enabled() -> bool:
return os.getenv("ODYSSEUS_LOCAL_MODEL_GATE", "true").lower() not in {"0", "false", "no", "off"}
def _gate_workload(workload: Optional[str]) -> str:
return "background" if str(workload or "").lower() == "background" else "foreground"
@asynccontextmanager
async def _local_model_slot(target_url: str, model: str, workload: Optional[str] = None):
"""Serialize local model traffic, with foreground chat taking priority.
Most local servers expose one GPU/CPU generation pipe even when their HTTP
API accepts multiple requests. Letting scheduled email/tasks and foreground
chat hit that pipe together creates the user-visible "streams crossed" and
"prompt waited behind a task" failure mode. Cloud providers are left alone.
"""
if not _local_model_gate_enabled() or not is_local_endpoint(target_url):
yield
return
global _LOCAL_MODEL_WAITING_FOREGROUND
kind = _gate_workload(workload)
current_task = asyncio.current_task()
if kind == "foreground":
_LOCAL_MODEL_WAITING_FOREGROUND += 1
current = dict(_LOCAL_MODEL_CURRENT)
if current.get("workload") == "background":
task = current.get("task")
if isinstance(task, asyncio.Task) and not task.done():
logger.info(
"[model-gate] cancelling background local model call for foreground request model=%s",
model,
)
task.cancel()
else:
# Background work should not jump in while the browser/chat is active
# or while a foreground request is waiting to acquire the local model.
try:
from src.interactive_gate import has_foreground_activity
except Exception:
has_foreground_activity = lambda: False # type: ignore
while _LOCAL_MODEL_WAITING_FOREGROUND > 0 or has_foreground_activity():
await asyncio.sleep(0.25)
acquired = False
try:
await _LOCAL_MODEL_LOCK.acquire()
acquired = True
if kind == "foreground":
_LOCAL_MODEL_WAITING_FOREGROUND = max(0, _LOCAL_MODEL_WAITING_FOREGROUND - 1)
_LOCAL_MODEL_CURRENT.clear()
_LOCAL_MODEL_CURRENT.update({
"task": current_task,
"workload": kind,
"url": target_url,
"model": model,
"started": time.time(),
})
yield
finally:
if kind == "foreground":
_LOCAL_MODEL_WAITING_FOREGROUND = max(0, _LOCAL_MODEL_WAITING_FOREGROUND - 1)
if acquired and _LOCAL_MODEL_LOCK.locked():
owner = _LOCAL_MODEL_CURRENT.get("task")
if owner is current_task:
_LOCAL_MODEL_CURRENT.clear()
_LOCAL_MODEL_LOCK.release()
class LLMConfig:
"""Configuration constants for LLM operations."""
DEFAULT_TIMEOUT = 30
@@ -199,6 +274,75 @@ def _stream_delta_event(text: str, *, thinking: bool = False) -> str:
payload["thinking"] = True
return f"data: {json.dumps(payload)}\n\n"
_DEGENERATE_WORD_RE = re.compile(r"[A-Za-z0-9_\u0370-\u03ff\u0400-\u04ff]+")
class _DegenerateStreamGuard:
"""Detect local-model token collapse before it floods the UI.
Some self-hosted models fail by repeating one token forever ("Var Var Var",
"Summer Summer ..."). This is not a useful response and can burn context,
browser memory, and GPU time. Keep the guard conservative: only fire on long
same-token runs or a very dominant repeated token in the recent window.
"""
def __init__(self, model: str):
self.model = model or "model"
self.last_token = ""
self.same_run = 0
self.recent_tokens: List[str] = []
self.total_chars = 0
def check(self, text: str) -> Optional[str]:
if not text:
return None
self.total_chars += len(text)
tokens = [t.lower() for t in _DEGENERATE_WORD_RE.findall(text) if len(t) >= 2]
if not tokens:
return None
for token in tokens:
if token == self.last_token:
self.same_run += 1
else:
self.last_token = token
self.same_run = 1
self.recent_tokens.append(token)
if len(self.recent_tokens) > 96:
self.recent_tokens = self.recent_tokens[-96:]
reason = None
if self.same_run >= 28 and self.total_chars >= 100:
reason = f"repeated '{self.last_token}' {self.same_run} times"
elif len(self.recent_tokens) >= 72:
top = max(set(self.recent_tokens), key=self.recent_tokens.count)
count = self.recent_tokens.count(top)
if count >= 60 and count / max(len(self.recent_tokens), 1) >= 0.78:
reason = f"repeated '{top}' {count}/{len(self.recent_tokens)} recent tokens"
if not reason and len(self.recent_tokens) >= 80:
# Phrase loops are common on some local quantized MLX/MoE models:
# "Also be a software developer mode?" repeated forever will not
# trip the single-token guard above, but it is still a wedged
# generation. Require many repeats of the same 4-gram so normal
# prose/list formatting is not interrupted.
grams = [tuple(self.recent_tokens[i:i + 4]) for i in range(0, len(self.recent_tokens) - 3)]
if grams:
top_gram = max(set(grams), key=grams.count)
gram_count = grams.count(top_gram)
if gram_count >= 10:
reason = f"repeated phrase '{' '.join(top_gram)}' {gram_count} times"
if not reason:
return None
logger.warning("[degenerate-stream] aborting model=%s reason=%s", self.model, reason)
message = (
f"Stopped generation: {self.model} started repeating tokens "
f"({reason}). Try a different model or lower temperature."
)
return f'event: error\ndata: {json.dumps({"status": 502, "text": message, "error": message})}\n\n'
def _model_activity_key(url: str, model: str) -> str:
return f"{(url or '').strip()}|{(model or '').strip()}"
@@ -755,6 +899,52 @@ def _apply_local_cache_affinity(payload: Dict, url: str, session_id: Optional[st
payload.setdefault("cache_prompt", True)
def _is_local_minimax_mlx_request(url: str, model: str) -> bool:
"""Local MLX MiniMax-family endpoints need conservative sampling defaults.
The OpenAI-compatible MLX server accepts repetition/frequency penalties.
Some large quantized MiniMax/MoE ports otherwise fall into visible reasoning
loops ("Also be...", "No.", etc.) even for trivial prompts.
"""
if not model:
return False
m = model.lower()
if "minimax" not in m and "mini-max" not in m:
return False
try:
from src.model_context import is_local_endpoint
return is_local_endpoint(url)
except Exception:
return False
def _apply_local_generation_stability(payload: Dict, url: str, model: str) -> None:
if not _is_local_minimax_mlx_request(url, model):
return
if "temperature" in payload:
try:
# MiniMax MLX quantized ports are very sensitive to chat/agent
# harness size. Character presets can ask for a warmer voice, but
# local MiniMax needs a final compatibility clamp or trivial
# prompts can fall into visible reasoning/repetition loops.
payload["temperature"] = min(float(payload.get("temperature") or 0.2), 0.2)
except (TypeError, ValueError):
payload["temperature"] = 0.2
payload.setdefault("top_p", 0.9)
payload.setdefault("top_k", 20)
payload.setdefault("repetition_penalty", 1.12)
payload.setdefault("repetition_context_size", 256)
payload.setdefault("frequency_penalty", 0.08)
payload.setdefault("frequency_context_size", 256)
payload.setdefault("presence_penalty", 0.02)
payload.setdefault("presence_context_size", 256)
payload.setdefault("stop", ["<|im_end|>", "<|endoftext|>", "</s>"])
# A max_tokens of 0 means "server default/unbounded" for many local
# endpoints. Keep simple chats from running forever when the model loops.
if not payload.get("max_tokens") and not payload.get("max_completion_tokens"):
payload["max_tokens"] = 2048
def _provider_headers(provider: str, headers: Optional[Dict] = None) -> Dict[str, str]:
h = {"Content-Type": "application/json"}
if isinstance(headers, dict):
@@ -1602,6 +1792,7 @@ def llm_call(url: str, model: str, messages: List[Dict], temperature: float = LL
if max_tokens and max_tokens > 0:
tok_key = "max_completion_tokens" if _uses_max_completion_tokens(model) else "max_tokens"
payload[tok_key] = max_tokens
_apply_local_generation_stability(payload, target_url, model)
if provider == "mistral" and _supports_thinking(model):
payload["reasoning_effort"] = _MISTRAL_REASONING_EFFORT
try:
@@ -1711,6 +1902,7 @@ async def llm_call_async(
max_retries: int = LLMConfig.MAX_RETRIES,
prompt_type: Optional[str] = None,
session_id: Optional[str] = None,
workload: str = "foreground",
) -> str:
"""Asynchronous LLM call using httpx with connection pooling, timeout, retry logic, and performance logging."""
provider = _detect_provider(url)
@@ -1748,6 +1940,7 @@ async def llm_call_async(
max_tokens=max_tokens,
headers=headers,
timeout=timeout,
workload=workload,
):
event_is_error = False
for line in str(chunk).splitlines():
@@ -1813,6 +2006,7 @@ async def llm_call_async(
if provider == "mistral" and _supports_thinking(model):
payload["reasoning_effort"] = _MISTRAL_REASONING_EFFORT
_apply_local_cache_affinity(payload, url, session_id)
_apply_local_generation_stability(payload, target_url, model)
if _is_host_dead(target_url):
raise HTTPException(503, f"Upstream {_host_key(target_url)} marked unreachable (cooldown active)")
@@ -1823,9 +2017,10 @@ async def llm_call_async(
attempt += 1
start = time.time()
try:
note_model_activity(target_url, model)
client = _get_http_client()
r = await httpx_post_kimi_aware_async(client, target_url, h, json=payload, timeout=call_timeout)
async with _local_model_slot(target_url, model, workload):
note_model_activity(target_url, model)
client = _get_http_client()
r = await httpx_post_kimi_aware_async(client, target_url, h, json=payload, timeout=call_timeout)
duration = time.time() - start
if not r.is_success:
friendly = _format_upstream_error(r.status_code, r.text, target_url)
@@ -1867,11 +2062,45 @@ async def llm_call_async(
raise HTTPException(502, f"POST {target_url} failed after {max_retries} attempts: {e}")
await asyncio.sleep(LLMConfig.RETRY_DELAY)
def _stream_target_url(url: str) -> str:
provider = _detect_provider(url)
if provider == "anthropic":
return _normalize_anthropic_url(url)
if provider == "ollama":
return _normalize_ollama_url(url)
if provider == "chatgpt-subscription":
return _normalize_chatgpt_subscription_url(url)
return _normalize_openai_chat_url(url)
async def stream_llm(url: str, model: str, messages: List[Dict], temperature: float = LLMConfig.DEFAULT_TEMPERATURE,
max_tokens: int = LLMConfig.DEFAULT_MAX_TOKENS, headers: Optional[Dict] = None,
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):
tool_choice_none: bool = False, workload: str = "foreground"):
target_url = _stream_target_url(url)
async with _local_model_slot(target_url, model, workload):
async for chunk in _stream_llm_inner(
url,
model,
messages,
temperature=temperature,
max_tokens=max_tokens,
headers=headers,
timeout=timeout,
prompt_type=prompt_type,
tools=tools,
session_id=session_id,
tool_choice_none=tool_choice_none,
):
yield chunk
async def _stream_llm_inner(url: str, model: str, messages: List[Dict], temperature: float = LLMConfig.DEFAULT_TEMPERATURE,
max_tokens: int = LLMConfig.DEFAULT_MAX_TOKENS, headers: Optional[Dict] = None,
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):
"""Stream LLM responses with improved error handling.
Yields SSE chunks:
@@ -1945,6 +2174,7 @@ async def stream_llm(url: str, model: str, messages: List[Dict], temperature: fl
if _is_ollama_openai_compat_url(url) and _supports_thinking(model):
payload["think"] = False
_apply_local_cache_affinity(payload, url, session_id)
_apply_local_generation_stability(payload, target_url, model)
h = _provider_headers(provider, headers)
if provider == "copilot":
from src.copilot import apply_request_headers
@@ -1961,6 +2191,7 @@ async def stream_llm(url: str, model: str, messages: List[Dict], temperature: fl
yield f'event: error\ndata: {json.dumps({"error": f"Upstream {_host_key(target_url)} unreachable (cooldown active)", "status": 503})}\n\n'
return
note_model_activity(target_url, model)
degenerate_guard = _DegenerateStreamGuard(model)
# ── ChatGPT Subscription / Codex Responses streaming ──
if provider == "chatgpt-subscription":
@@ -1995,6 +2226,10 @@ async def stream_llm(url: str, model: str, messages: List[Dict], temperature: fl
if evt == "response.output_text.delta":
delta = data.get("delta") or ""
if delta:
_degenerate = degenerate_guard.check(delta)
if _degenerate:
yield _degenerate
return
yield f'data: {json.dumps({"delta": delta})}\n\n'
elif evt == "response.completed":
usage = (data.get("response") or {}).get("usage") or data.get("usage") or {}
@@ -2327,11 +2562,19 @@ async def stream_llm(url: str, model: str, messages: List[Dict], temperature: fl
reasoning = (reasoning + thinking_part) if reasoning else thinking_part
content = text_part
if reasoning:
_degenerate = degenerate_guard.check(reasoning)
if _degenerate:
yield _degenerate
return
yield _stream_delta_event(reasoning, thinking=True)
if content:
content = _strip_visible_chat_template_artifacts(content)
if not content:
continue
_degenerate = degenerate_guard.check(content)
if _degenerate:
yield _degenerate
return
content = re.sub(r"<mm:think(\s+[^>]*)?>", r"<think\1>", content, flags=re.IGNORECASE)
content = re.sub(r"</mm:think>", "</think>", content, flags=re.IGNORECASE)
stripped = content.lstrip()