mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-31 02:02:22 +01:00
fix(runtime): resolve compact-turn context once at the chat route
The first checkpoint removed terminal-metrics discovery, but a normal compact chat turn still ran two context systems: build_chat_context's legacy untyped lookup (directly or inside maybe_compact) and the typed resolver inside stream_preview. Resolve the typed ContextResolution once, at the chat route, before build_chat_context, using the session's provider credentials. The predicate mirrors _clean_v3_preview; every input it needs is known at that point and the native-workspace term cannot veto a requested clean route. The same object then: - sizes legacy history shaping in build_chat_context through a new maybe_compact(context_length=...) override, so no legacy probe runs; an unknown window still shapes with DEFAULT_CONTEXT but gains no provenance; - crosses stream_agent_loop (one new parameter, forwarded only at the compact dispatch) into stream_preview, which reuses it and probes only for callers that arrive without one or with one bound to another route. ContextResolution now records the endpoint and model it describes (endpoint URL excluded from repr and metrics). The bare legacy context_length is never converted into typed evidence. Credential scoping: origins compare with default ports normalized, an empty host is never trusted, and the probe client never follows redirects. Tests cover the configured origin, the server-resolved Tailscale form, scheme/port/lookalike/userinfo/path origins, redirects, and secret-free errors, logs and metrics. The conftest guard now replaces only the resolver's I/O edges (HTTP client and DNS-capable URL building) instead of the whole probe, and exposes a context_probe_ledger fixture, so route integration tests run the real resolver offline and can count metadata requests.
This commit is contained in:
+13
-1
@@ -1027,11 +1027,15 @@ async def build_chat_context(
|
||||
persist_user_message: bool = True,
|
||||
interaction_mode: str | None = None,
|
||||
auto_escalated: bool = False,
|
||||
context_resolution=None,
|
||||
) -> ChatContext:
|
||||
"""Build the full context (preface + messages) for an LLM call.
|
||||
|
||||
This is the shared logic between /chat and /chat_stream — preset extraction,
|
||||
message preprocessing, memory/RAG/web injection, compaction, normalization.
|
||||
|
||||
``context_resolution`` is the turn's already resolved context window. When
|
||||
supplied, history shaping sizes against it instead of probing the endpoint.
|
||||
"""
|
||||
# Preset
|
||||
preset = extract_preset(chat_handler, preset_id)
|
||||
@@ -1225,12 +1229,20 @@ async def build_chat_context(
|
||||
# for every candidate. Running selected-model compaction here would mutate
|
||||
# session history before we know which route can answer and would make a
|
||||
# later larger-context candidate unable to recover discarded history.
|
||||
if context_resolution is not None:
|
||||
prepared_window = {"context_length": context_resolution.shaping_window}
|
||||
else:
|
||||
prepared_window = {}
|
||||
if defer_context_shaping:
|
||||
context_length = get_context_length(sess.endpoint_url, sess.model)
|
||||
context_length = (
|
||||
prepared_window.get("context_length")
|
||||
or get_context_length(sess.endpoint_url, sess.model)
|
||||
)
|
||||
was_compacted = False
|
||||
else:
|
||||
messages, context_length, was_compacted = await maybe_compact(
|
||||
sess, sess.endpoint_url, sess.model, messages, sess.headers, owner=user,
|
||||
**prepared_window,
|
||||
)
|
||||
_before_trim_messages = len(messages)
|
||||
_before_trim_tokens = estimate_tokens(messages)
|
||||
|
||||
@@ -2893,6 +2893,26 @@ def setup_chat_routes(
|
||||
allowed_models=_allowed_models_for_request(request),
|
||||
)
|
||||
|
||||
# A compact (clean v3) turn resolves its typed context window once,
|
||||
# here, with the session's provider credentials. History shaping below
|
||||
# and the compact runtime both reuse this exact object, so the turn
|
||||
# neither probes twice nor mixes the legacy untyped lookup into it.
|
||||
# The predicate mirrors ``_clean_v3_preview`` below; the native
|
||||
# workspace term cannot veto a requested clean route.
|
||||
_compact_context_resolution = None
|
||||
if _clean_v3_route_requested and _turn_contract_enabled(
|
||||
exact_tool_approval=exact_tool_approval,
|
||||
runtime_surface=str((client_runtime_context or {}).get("surface") or ""),
|
||||
native_workspace_contract=False,
|
||||
clean_v3_route=True,
|
||||
full_schema_route=(_effective_tool_schema_mode == "full"),
|
||||
):
|
||||
from src.agent_runtime.context_resolution import resolve_effective_context
|
||||
_compact_context_resolution = await resolve_effective_context(
|
||||
sess.endpoint_url, sess.model, headers=sess.headers,
|
||||
client_runtime_context=client_runtime_context,
|
||||
)
|
||||
|
||||
# Build shared context (stream path uses enhanced_message for context preface)
|
||||
ctx = await build_chat_context(
|
||||
sess, request, chat_handler, chat_processor,
|
||||
@@ -2923,6 +2943,7 @@ def setup_chat_routes(
|
||||
else None
|
||||
),
|
||||
persist_user_message=not tool_approval_continuation and not is_internal_tool_request(request),
|
||||
context_resolution=_compact_context_resolution,
|
||||
interaction_mode=chat_mode,
|
||||
auto_escalated=auto_escalated,
|
||||
)
|
||||
@@ -4459,6 +4480,9 @@ def setup_chat_routes(
|
||||
client_runtime_context=client_runtime_context,
|
||||
thinking_mode=thinking_mode,
|
||||
reasoning_effort=reasoning_effort,
|
||||
context_resolution=(
|
||||
_compact_context_resolution if _clean_v3_preview else None
|
||||
),
|
||||
):
|
||||
if chunk.startswith("data: ") and not chunk.startswith("data: [DONE]"):
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user