mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-09-27 18:42:20 +02:00
fix(security): close bearer side-effect boundaries
This commit is contained in:
@@ -31,6 +31,7 @@ class RequestCapability:
|
||||
allow_detached_execution: bool
|
||||
allow_message_events: bool
|
||||
allow_auto_naming: bool
|
||||
allow_live_probes: bool
|
||||
|
||||
|
||||
def is_bearer_principal(request: Request) -> bool:
|
||||
@@ -101,6 +102,7 @@ def request_capability(request: Request) -> RequestCapability:
|
||||
allow_detached_execution=not bearer,
|
||||
allow_message_events=not bearer,
|
||||
allow_auto_naming=not bearer,
|
||||
allow_live_probes=not bearer,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -330,12 +330,16 @@ async def maybe_compact(
|
||||
*,
|
||||
persist: bool = True,
|
||||
compaction_state: Optional[Dict[str, Any]] = None,
|
||||
allow_live_probes: bool = True,
|
||||
) -> tuple:
|
||||
"""Check context usage and compact if above threshold.
|
||||
|
||||
Returns (messages, context_length, was_compacted).
|
||||
"""
|
||||
context_length = get_context_length(endpoint_url, model)
|
||||
context_kwargs = {}
|
||||
if not allow_live_probes:
|
||||
context_kwargs["allow_live_probes"] = False
|
||||
context_length = get_context_length(endpoint_url, model, **context_kwargs)
|
||||
used = estimate_tokens(messages)
|
||||
pct = (used / context_length) * 100 if context_length else 0
|
||||
|
||||
@@ -392,6 +396,9 @@ async def maybe_compact(
|
||||
]
|
||||
|
||||
try:
|
||||
summary_kwargs = {}
|
||||
if not allow_live_probes:
|
||||
summary_kwargs["allow_live_probes"] = False
|
||||
summary = await llm_call_async(
|
||||
compact_url,
|
||||
compact_model,
|
||||
@@ -400,6 +407,7 @@ async def maybe_compact(
|
||||
max_tokens=SUMMARY_MAX_TOKENS,
|
||||
headers=compact_headers,
|
||||
timeout=30,
|
||||
**summary_kwargs,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Compaction summary failed: {e}")
|
||||
|
||||
+39
-8
@@ -1903,6 +1903,7 @@ def list_model_ids(
|
||||
*,
|
||||
owner: Optional[str] = None,
|
||||
endpoint_id: Optional[str] = None,
|
||||
allow_live_probes: bool = True,
|
||||
) -> List[str]:
|
||||
"""List available model IDs from an endpoint."""
|
||||
cached = _configured_cached_model_ids(base_chat_url, owner=owner, endpoint_id=endpoint_id)
|
||||
@@ -1911,6 +1912,8 @@ def list_model_ids(
|
||||
provider = _detect_provider(base_chat_url)
|
||||
if provider == "anthropic":
|
||||
return list(ANTHROPIC_MODELS)
|
||||
if not allow_live_probes:
|
||||
return []
|
||||
try:
|
||||
h = {}
|
||||
if headers:
|
||||
@@ -1952,9 +1955,16 @@ def normalize_model_id(
|
||||
*,
|
||||
owner: Optional[str] = None,
|
||||
endpoint_id: Optional[str] = None,
|
||||
allow_live_probes: bool = True,
|
||||
) -> Optional[str]:
|
||||
"""Normalize a model ID to match available models."""
|
||||
avail = list_model_ids(endpoint_url, timeout, owner=owner, endpoint_id=endpoint_id)
|
||||
avail = list_model_ids(
|
||||
endpoint_url,
|
||||
timeout,
|
||||
owner=owner,
|
||||
endpoint_id=endpoint_id,
|
||||
allow_live_probes=allow_live_probes,
|
||||
)
|
||||
if not avail:
|
||||
return None
|
||||
if requested in avail:
|
||||
@@ -1968,7 +1978,8 @@ def normalize_model_id(
|
||||
|
||||
def llm_call(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.DEFAULT_TIMEOUT, prompt_type: Optional[str] = None) -> str:
|
||||
timeout: int = LLMConfig.DEFAULT_TIMEOUT, prompt_type: Optional[str] = None,
|
||||
allow_live_probes: bool = True) -> str:
|
||||
"""Synchronous LLM call with optional prompt type enhancement."""
|
||||
h = _provider_headers(_detect_provider(url))
|
||||
# Tolerate headers that arrive as a JSON string (some sessions stored them
|
||||
@@ -2012,9 +2023,12 @@ def llm_call(url: str, model: str, messages: List[Dict], temperature: float = LL
|
||||
payload = _build_anthropic_payload(model, messages_copy, temperature, max_tokens)
|
||||
elif provider == "ollama":
|
||||
target_url = _normalize_ollama_url(url)
|
||||
context_kwargs = {}
|
||||
if not allow_live_probes:
|
||||
context_kwargs["allow_live_probes"] = False
|
||||
payload = _build_ollama_payload(
|
||||
model, messages_copy, temperature, max_tokens,
|
||||
stream=False, num_ctx=get_context_length(url, model),
|
||||
stream=False, num_ctx=get_context_length(url, model, **context_kwargs),
|
||||
)
|
||||
else:
|
||||
target_url = _normalize_openai_chat_url(url)
|
||||
@@ -2273,6 +2287,7 @@ async def llm_call_async(
|
||||
workload: str = "foreground",
|
||||
availability_only_transport: bool = False,
|
||||
return_model_metadata: bool = False,
|
||||
allow_live_probes: bool = True,
|
||||
) -> str | tuple[str, str]:
|
||||
"""Asynchronous LLM call using httpx with connection pooling, timeout, retry logic, and performance logging."""
|
||||
provider = _detect_provider(url)
|
||||
@@ -2307,6 +2322,9 @@ async def llm_call_async(
|
||||
# Reuse stream_llm's validated Codex SSE path and collect deltas.
|
||||
parts: List[str] = []
|
||||
actual_model = model
|
||||
stream_kwargs = {"workload": workload}
|
||||
if not allow_live_probes:
|
||||
stream_kwargs["allow_live_probes"] = False
|
||||
async for chunk in stream_llm(
|
||||
url,
|
||||
model,
|
||||
@@ -2315,7 +2333,7 @@ async def llm_call_async(
|
||||
max_tokens=max_tokens,
|
||||
headers=headers,
|
||||
timeout=timeout,
|
||||
workload=workload,
|
||||
**stream_kwargs,
|
||||
):
|
||||
event_is_error = False
|
||||
for line in str(chunk).splitlines():
|
||||
@@ -2372,9 +2390,12 @@ async def llm_call_async(
|
||||
h = {"Content-Type": "application/json"}
|
||||
if headers:
|
||||
h.update(headers)
|
||||
context_kwargs = {}
|
||||
if not allow_live_probes:
|
||||
context_kwargs["allow_live_probes"] = False
|
||||
payload = _build_ollama_payload(
|
||||
model, messages_copy, temperature, max_tokens,
|
||||
stream=False, num_ctx=get_context_length(url, model),
|
||||
stream=False, num_ctx=get_context_length(url, model, **context_kwargs),
|
||||
)
|
||||
else:
|
||||
target_url = _normalize_openai_chat_url(url)
|
||||
@@ -2560,9 +2581,13 @@ async def stream_llm(url: str, model: str, messages: List[Dict], temperature: fl
|
||||
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, workload: str = "foreground"):
|
||||
tool_choice_none: bool = False, workload: str = "foreground",
|
||||
allow_live_probes: bool = True):
|
||||
target_url = _stream_target_url(url)
|
||||
async with _local_model_slot(target_url, model, workload):
|
||||
inner_kwargs = {}
|
||||
if not allow_live_probes:
|
||||
inner_kwargs["allow_live_probes"] = False
|
||||
async for chunk in _stream_llm_inner(
|
||||
url,
|
||||
model,
|
||||
@@ -2575,6 +2600,7 @@ async def stream_llm(url: str, model: str, messages: List[Dict], temperature: fl
|
||||
tools=tools,
|
||||
session_id=session_id,
|
||||
tool_choice_none=tool_choice_none,
|
||||
**inner_kwargs,
|
||||
):
|
||||
yield chunk
|
||||
|
||||
@@ -2583,7 +2609,7 @@ async def _stream_llm_inner(url: str, model: str, messages: List[Dict], temperat
|
||||
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, allow_live_probes: bool = True):
|
||||
"""Stream LLM responses with improved error handling.
|
||||
|
||||
Yields SSE chunks:
|
||||
@@ -2618,9 +2644,14 @@ async def _stream_llm_inner(url: str, model: str, messages: List[Dict], temperat
|
||||
h = {"Content-Type": "application/json"}
|
||||
if headers:
|
||||
h.update(headers)
|
||||
context_kwargs = {}
|
||||
if not allow_live_probes:
|
||||
context_kwargs["allow_live_probes"] = False
|
||||
payload = _build_ollama_payload(
|
||||
model, messages_copy, temperature, max_tokens,
|
||||
stream=True, tools=tools, num_ctx=get_context_length(url, model),
|
||||
stream=True,
|
||||
tools=tools,
|
||||
num_ctx=get_context_length(url, model, **context_kwargs),
|
||||
)
|
||||
elif provider == "chatgpt-subscription":
|
||||
target_url = _normalize_chatgpt_subscription_url(url)
|
||||
|
||||
+39
-6
@@ -238,16 +238,31 @@ KNOWN_CONTEXT_WINDOWS = {
|
||||
_context_cache: Dict[Tuple[str, str], Tuple[int, bool]] = {}
|
||||
|
||||
|
||||
def _get_context_length_cached(endpoint_url: str, model: str) -> Tuple[int, bool]:
|
||||
def _get_context_length_cached(
|
||||
endpoint_url: str,
|
||||
model: str,
|
||||
*,
|
||||
allow_live_probes: bool = True,
|
||||
) -> Tuple[int, bool]:
|
||||
"""Return (context_length, known). ``known`` is False only when the value is a
|
||||
bare DEFAULT_CONTEXT fallback (no endpoint report and not in the known table)."""
|
||||
cache_key = (endpoint_url, model)
|
||||
if not allow_live_probes:
|
||||
# A bearer may consume metadata already learned by an interactive or
|
||||
# explicitly privileged refresh, but a context build must not cause a
|
||||
# new /slots, /models, or catalog request or populate those caches.
|
||||
cached = _context_cache.get(cache_key)
|
||||
if cached:
|
||||
return cached
|
||||
known = _lookup_known(model)
|
||||
return (known, True) if known else (DEFAULT_CONTEXT, False)
|
||||
|
||||
configured_kind = _configured_endpoint_kind(endpoint_url)
|
||||
is_local = is_local_endpoint(endpoint_url)
|
||||
# Key on (endpoint_url, model): the same model id can be served by two
|
||||
# different remote endpoints with different real context windows (e.g. a
|
||||
# capped proxy vs. the full provider), so caching by model id alone would
|
||||
# serve one endpoint's window for the other (issue #2603).
|
||||
cache_key = (endpoint_url, model)
|
||||
if not is_local and cache_key in _context_cache:
|
||||
return _context_cache[cache_key]
|
||||
|
||||
@@ -261,23 +276,41 @@ def _get_context_length_cached(endpoint_url: str, model: str) -> Tuple[int, bool
|
||||
return ctx, known
|
||||
|
||||
|
||||
def get_context_length(endpoint_url: str, model: str) -> int:
|
||||
def get_context_length(
|
||||
endpoint_url: str,
|
||||
model: str,
|
||||
*,
|
||||
allow_live_probes: bool = True,
|
||||
) -> int:
|
||||
"""Get the context window size for a model.
|
||||
|
||||
Queries /v1/models on the endpoint and looks for context_length
|
||||
or context_window fields. Caches result per (endpoint, model).
|
||||
Falls back to DEFAULT_CONTEXT if unavailable.
|
||||
"""
|
||||
return _get_context_length_cached(endpoint_url, model)[0]
|
||||
return _get_context_length_cached(
|
||||
endpoint_url,
|
||||
model,
|
||||
allow_live_probes=allow_live_probes,
|
||||
)[0]
|
||||
|
||||
|
||||
def get_context_length_known(endpoint_url: str, model: str) -> Tuple[int, bool]:
|
||||
def get_context_length_known(
|
||||
endpoint_url: str,
|
||||
model: str,
|
||||
*,
|
||||
allow_live_probes: bool = True,
|
||||
) -> Tuple[int, bool]:
|
||||
"""Like ``get_context_length`` but also returns whether the window was actually
|
||||
discovered (endpoint-reported or in the known-models table) rather than the bare
|
||||
DEFAULT_CONTEXT fallback. Callers that *scale* a budget off the window must not
|
||||
trust an unknown value — a fallback 128K isn't proof the model holds 128K
|
||||
(review on #4122)."""
|
||||
return _get_context_length_cached(endpoint_url, model)
|
||||
return _get_context_length_cached(
|
||||
endpoint_url,
|
||||
model,
|
||||
allow_live_probes=allow_live_probes,
|
||||
)
|
||||
|
||||
|
||||
def budget_context_for_model(endpoint_url: str, model: str, *, fallback: int = 0) -> int:
|
||||
|
||||
Reference in New Issue
Block a user