feat(streaming): publish observed local runtime model to SSE and journal
This commit is contained in:
1 parent
3f00824916
commit
c45cab3779
7 files changed
+372
-8
No files matched your search
@@ -4067,6 +4067,7 @@ def _run_journal_live_snapshot(stream_id: str | None, *, handler=None) -> dict |
|
||||
"tool_calls": tool_calls,
|
||||
"last_assistant_text": assistant_text,
|
||||
"last_reasoning_text": reasoning_text,
|
||||
"runtime_model": runtime_model_from_events(session_id, stream_id, events),
|
||||
"activity_burst_anchors": activity_burst_anchors,
|
||||
"current_activity_burst_id": current_activity_burst_id,
|
||||
"current_live_segment_seq": current_live_segment_seq,
|
||||
@@ -10839,6 +10840,7 @@ from api.streaming import (
|
||||
)
|
||||
from api.gateway_chat import _run_gateway_chat_streaming, webui_gateway_chat_enabled
|
||||
from api.run_journal import (
|
||||
runtime_model_from_events,
|
||||
_parse_run_journal_event_id as _shared_parse_run_journal_event_id,
|
||||
_summary_from_events,
|
||||
bound_run_journal_snapshot_args,
|
||||
|
||||
+35
-2
@@ -10,6 +10,7 @@ import os
|
||||
import re
|
||||
import threading
|
||||
import time
|
||||
from copy import deepcopy
|
||||
from collections import OrderedDict
|
||||
from pathlib import Path
|
||||
from typing import Any, Iterable
|
||||
@@ -130,7 +131,7 @@ def _get_cached_summary(path: Path) -> dict | None:
|
||||
_SUMMARY_CACHE.pop(key, None)
|
||||
return None
|
||||
_SUMMARY_CACHE.move_to_end(key)
|
||||
return dict(summary)
|
||||
return deepcopy(summary)
|
||||
|
||||
|
||||
def _cache_summary(
|
||||
@@ -147,7 +148,7 @@ def _cache_summary(
|
||||
return
|
||||
key = str(path)
|
||||
with _SUMMARY_CACHE_LOCK:
|
||||
_SUMMARY_CACHE[key] = (signature, dict(summary))
|
||||
_SUMMARY_CACHE[key] = (signature, deepcopy(summary))
|
||||
_SUMMARY_CACHE.move_to_end(key)
|
||||
while len(_SUMMARY_CACHE) > _SUMMARY_CACHE_MAX_ENTRIES:
|
||||
_SUMMARY_CACHE.popitem(last=False)
|
||||
@@ -534,6 +535,37 @@ def select_authoritative_terminal_event(events: Iterable[dict]) -> dict | None:
|
||||
)
|
||||
|
||||
|
||||
def runtime_model_from_events(session_id: str, stream_id: str, events: Iterable[dict]) -> dict | None:
|
||||
"""Project only observed serving identity for this journal owner.
|
||||
|
||||
A fallback warning or malformed later observation invalidates old evidence;
|
||||
the configured selection and free-text status never supply serving identity.
|
||||
"""
|
||||
observed = None
|
||||
for event in events:
|
||||
if not isinstance(event, dict) or event.get("session_id") != session_id or event.get("run_id") != stream_id:
|
||||
continue
|
||||
payload = event.get("payload")
|
||||
if not isinstance(payload, dict):
|
||||
if event.get("event") == "runtime_model":
|
||||
observed = None
|
||||
continue
|
||||
if event.get("event") == "warning" and payload.get("type") == "fallback":
|
||||
observed = None
|
||||
elif event.get("event") == "runtime_model":
|
||||
observed = None
|
||||
if (payload.get("session_id") != session_id or payload.get("stream_id") != stream_id
|
||||
or not isinstance(payload.get("model"), str) or not payload["model"].strip()
|
||||
or not isinstance(payload.get("fallback_active"), bool)
|
||||
or payload.get("phase") not in ("observed_output", "route_observed")):
|
||||
continue
|
||||
observed = {key: payload[key] for key in (
|
||||
"session_id", "stream_id", "model", "fallback_active", "phase")}
|
||||
if isinstance(payload.get("provider"), str) and payload["provider"].strip():
|
||||
observed["provider"] = payload["provider"]
|
||||
return observed
|
||||
|
||||
|
||||
def _summary_from_events(session_id: str, run_id: str, events: Iterable[dict]) -> dict:
|
||||
ordered = [event for event in events if isinstance(event, dict)]
|
||||
last = ordered[-1] if ordered else None
|
||||
@@ -549,6 +581,7 @@ def _summary_from_events(session_id: str, run_id: str, events: Iterable[dict]) -
|
||||
"terminal": bool(terminal),
|
||||
"terminal_state": status,
|
||||
"last_event": (last or {}).get("event"),
|
||||
"runtime_model": runtime_model_from_events(session_id, run_id, ordered),
|
||||
}
|
||||
|
||||
|
||||
|
||||
+54
-1
@@ -891,6 +891,9 @@ def _is_fallback_lifecycle_message(kind: str, message: str) -> bool:
|
||||
or 'falling back' in m
|
||||
or 'fallback activated' in m
|
||||
or 'trying fallback' in m
|
||||
or 'model fallback:' in m
|
||||
or 'switched to fallback' in m
|
||||
or 'primary model restored:' in m
|
||||
)
|
||||
)
|
||||
|
||||
@@ -9734,6 +9737,37 @@ def _run_agent_streaming(
|
||||
except Exception:
|
||||
logger.debug("Failed to put event to queue")
|
||||
|
||||
_last_runtime_model_identity = None
|
||||
_runtime_model_session_id = session_id
|
||||
|
||||
def _observe_runtime_model():
|
||||
"""Publish this turn's Agent identity at output, never at an attempted route."""
|
||||
nonlocal _last_runtime_model_identity
|
||||
raw_model = getattr(agent, 'model', None)
|
||||
if not isinstance(raw_model, str) or not raw_model.strip():
|
||||
return
|
||||
raw_provider = getattr(agent, 'provider', None)
|
||||
provider = str(raw_provider).strip().lstrip('@').lower() if isinstance(raw_provider, str) else ''
|
||||
# The Agent may carry a provider-qualified routing hint in its model.
|
||||
from api.config import _parse_provider_qualified_model_id
|
||||
parsed = _parse_provider_qualified_model_id(raw_model.strip())
|
||||
model_id = (parsed[0] if parsed else raw_model).strip()
|
||||
if not model_id:
|
||||
return
|
||||
fallback_active = getattr(agent, '_provider_fallback_active', None) is True
|
||||
identity = (provider, model_id.lower(), fallback_active)
|
||||
if identity == _last_runtime_model_identity:
|
||||
return
|
||||
payload = {
|
||||
'session_id': _runtime_model_session_id, 'stream_id': stream_id,
|
||||
'model': model_id, 'fallback_active': fallback_active,
|
||||
'phase': 'observed_output',
|
||||
}
|
||||
if provider:
|
||||
payload['provider'] = provider
|
||||
put('runtime_model', payload)
|
||||
_last_runtime_model_identity = identity
|
||||
|
||||
# #5940: capture a terminal (non-retryable) provider error the Agent emits via
|
||||
# its lifecycle status_callback. The Agent aborts a non-retryable API error
|
||||
# (e.g. HTTP 400 "invalid model / no credentials") with
|
||||
@@ -9754,6 +9788,7 @@ def _run_agent_streaming(
|
||||
turn-completion classifier can report the real cause instead of the
|
||||
generic no_response fallback. All other lifecycle messages are dropped.
|
||||
"""
|
||||
nonlocal _last_runtime_model_identity
|
||||
_message = str(message or '').strip()
|
||||
_kind = str(kind or '').strip().lower()
|
||||
if not _message:
|
||||
@@ -9778,6 +9813,7 @@ def _run_agent_streaming(
|
||||
# show them as warnings via the existing messages.js 'warning' listener.
|
||||
_is_fallback_notice = _is_fallback_lifecycle_message(_kind, _message)
|
||||
if _is_fallback_notice:
|
||||
_last_runtime_model_identity = None
|
||||
put('warning', {'type': 'fallback', 'message': _message})
|
||||
|
||||
# xsession wakeup misroute root fix (Option 1): pre-init so the outer
|
||||
@@ -10325,6 +10361,8 @@ def _run_agent_streaming(
|
||||
nonlocal _token_sent
|
||||
if text is None:
|
||||
return # end-of-stream sentinel
|
||||
if text:
|
||||
_observe_runtime_model()
|
||||
# #4729: visible output is starting — flush any buffered reasoning tail
|
||||
# first so the live Thinking stream is complete before/at the transition.
|
||||
_flush_reasoning_buffer()
|
||||
@@ -10368,6 +10406,8 @@ def _run_agent_streaming(
|
||||
# partial window is not lost when the reasoning phase ends.
|
||||
_flush_reasoning_buffer()
|
||||
return
|
||||
if text:
|
||||
_observe_runtime_model()
|
||||
_tool_boundary_advanced = False
|
||||
reasoning_delta = str(text)
|
||||
# Some runtimes mirror user-visible progress text through the
|
||||
@@ -11570,6 +11610,11 @@ def _run_agent_streaming(
|
||||
if isinstance(_m, dict) and _m.get('role') == 'assistant':
|
||||
_answer = str(_m.get('content', ''))
|
||||
break
|
||||
if (_answer.strip() and not result.get('error')
|
||||
and not _agent_result_terminal_failure(result)
|
||||
and not getattr(agent, '_last_error', None)
|
||||
and not _captured_terminal_error[0]):
|
||||
_observe_runtime_model()
|
||||
# /btw is intentionally non-persistent, but its terminal SSE
|
||||
# payload is still public output. Project the ephemeral
|
||||
# session before enqueueing it so raw Agent ``api_content`` or
|
||||
@@ -12272,6 +12317,8 @@ def _run_agent_streaming(
|
||||
# the catch-all label, hint, and provider details.
|
||||
return # apperror already closes the stream on the client side
|
||||
|
||||
_observe_runtime_model()
|
||||
|
||||
# ── Handle context compression side effects ──
|
||||
# Also detect compression via the result dict or compressor state
|
||||
if not _compressed:
|
||||
@@ -12478,7 +12525,9 @@ def _run_agent_streaming(
|
||||
# mutates agent.model when a fallback fires, so the pre-run
|
||||
# resolved_model would mis-attribute exactly the turns where
|
||||
# attribution matters most.
|
||||
_used_model = getattr(agent, 'model', None) or resolved_model or model
|
||||
# The configured selection is not proof that it served this turn.
|
||||
_observed_model = getattr(agent, 'model', None)
|
||||
_used_model = _observed_model.strip() if isinstance(_observed_model, str) else None
|
||||
if _gateway_routing:
|
||||
s.gateway_routing = _gateway_routing
|
||||
_history = list(getattr(s, 'gateway_routing_history', None) or [])
|
||||
@@ -13364,6 +13413,9 @@ def _run_agent_streaming(
|
||||
if 'credential_pool' in _agent_params:
|
||||
_heal_kwargs['credential_pool'] = _runtime_bundle['credential_pool']
|
||||
_heal_agent = _AIAgent(**_heal_kwargs)
|
||||
# Delta callbacks close over `agent`; the replacement, not
|
||||
# the failed original, owns any successful retry output.
|
||||
agent = _heal_agent
|
||||
_agent_sig = _compute_agent_cache_signature(
|
||||
resolved_model,
|
||||
resolved_api_key,
|
||||
@@ -13491,6 +13543,7 @@ def _run_agent_streaming(
|
||||
s, tool_calls=s.tool_calls
|
||||
)
|
||||
)
|
||||
_observe_runtime_model()
|
||||
if _done_session_payload is not None:
|
||||
put('done', {
|
||||
'session': _done_session_payload,
|
||||
|
||||
Reference in new issue
Block a user