Add execution incidents, action bursts and shared Flash history. Expose verification and Explorer settings with streamed check results. Support graceful cancellation, recording recovery and bundled UI resources. Add CI quality checks and clean up comments and documentation.
172 lines
5.6 KiB
Python
172 lines
5.6 KiB
Python
# Copyright 2026 Google LLC
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
"""Tests for the M0 session token metering pipeline (pure recording)."""
|
|
|
|
from unittest.mock import Mock
|
|
from uuid import uuid4
|
|
|
|
from langchain_core.messages import AIMessage
|
|
|
|
from artemis.services.token_meter import (
|
|
SessionTokenMeter,
|
|
extract_usage,
|
|
get_meter,
|
|
record_llm_usage,
|
|
)
|
|
|
|
|
|
def _msg_with_usage(prompt=1000, completion=50, cached=0):
|
|
usage = {
|
|
"input_tokens": prompt,
|
|
"output_tokens": completion,
|
|
"total_tokens": prompt + completion,
|
|
}
|
|
if cached:
|
|
usage["input_token_details"] = {"cache_read": cached}
|
|
return AIMessage(content="ok", usage_metadata=usage)
|
|
|
|
|
|
def test_extract_usage_from_usage_metadata():
|
|
usage = extract_usage(_msg_with_usage(prompt=1200, completion=80, cached=900))
|
|
assert usage == {
|
|
"prompt_tokens": 1200,
|
|
"completion_tokens": 80,
|
|
"total_tokens": 1280,
|
|
"cached_tokens": 900,
|
|
}
|
|
|
|
|
|
def test_extract_usage_from_response_metadata_fallback():
|
|
msg = AIMessage(content="ok")
|
|
msg.response_metadata = {
|
|
"usage_metadata": {
|
|
"prompt_token_count": 500,
|
|
"candidates_token_count": 25,
|
|
"total_token_count": 525,
|
|
"cached_content_token_count": 400,
|
|
}
|
|
}
|
|
usage = extract_usage(msg)
|
|
assert usage == {
|
|
"prompt_tokens": 500,
|
|
"completion_tokens": 25,
|
|
"total_tokens": 525,
|
|
"cached_tokens": 400,
|
|
}
|
|
|
|
|
|
def test_extract_usage_absent_returns_none():
|
|
assert extract_usage(AIMessage(content="no usage")) is None
|
|
assert extract_usage(object()) is None
|
|
|
|
|
|
def test_meter_accumulates_session_totals():
|
|
meter = SessionTokenMeter("s1")
|
|
meter.record({"prompt_tokens": 100, "completion_tokens": 10, "cached_tokens": 0})
|
|
snapshot = meter.record({"prompt_tokens": 300, "completion_tokens": 20, "cached_tokens": 250})
|
|
assert snapshot == {
|
|
"session_llm_calls": 2,
|
|
"session_prompt_tokens": 400,
|
|
"session_completion_tokens": 30,
|
|
"session_cached_tokens": 250,
|
|
"session_cache_hit_calls": 1,
|
|
}
|
|
assert meter.last_prompt_tokens == 300
|
|
|
|
|
|
def test_record_llm_usage_records_trace_with_context_base():
|
|
engine = Mock()
|
|
engine.current_session_id = uuid4()
|
|
engine.current_step_id = uuid4()
|
|
|
|
payload = record_llm_usage(
|
|
engine, _msg_with_usage(prompt=2000, completion=100, cached=1500), source="google:test"
|
|
)
|
|
|
|
assert payload is not None
|
|
assert payload["context_base_tokens"] == 2000
|
|
assert payload["cached_tokens"] == 1500
|
|
assert payload["source"] == "google:test"
|
|
|
|
engine.record_trace.assert_called_once()
|
|
kwargs = engine.record_trace.call_args.kwargs
|
|
assert kwargs["type"] == "llm_call"
|
|
assert kwargs["name"] == "llm_usage"
|
|
assert kwargs["step_id"] == engine.current_step_id
|
|
assert kwargs["payload"]["prompt_tokens"] == 2000
|
|
|
|
|
|
def test_record_llm_usage_accumulates_across_calls_same_session():
|
|
engine = Mock()
|
|
engine.current_session_id = uuid4()
|
|
engine.current_step_id = None
|
|
|
|
record_llm_usage(engine, _msg_with_usage(prompt=100, completion=5))
|
|
payload = record_llm_usage(engine, _msg_with_usage(prompt=250, completion=10, cached=90))
|
|
|
|
assert payload["session_llm_calls"] == 2
|
|
assert payload["session_prompt_tokens"] == 350
|
|
assert payload["session_cached_tokens"] == 90
|
|
assert payload["session_cache_hit_calls"] == 1
|
|
|
|
meter = get_meter(engine.current_session_id)
|
|
assert meter.last_prompt_tokens == 250
|
|
|
|
|
|
def test_record_llm_usage_never_raises_and_skips_gracefully():
|
|
# No engine
|
|
assert record_llm_usage(None, _msg_with_usage()) is None
|
|
|
|
# Engine without a session
|
|
engine = Mock()
|
|
engine.current_session_id = None
|
|
assert record_llm_usage(engine, _msg_with_usage()) is None
|
|
engine.record_trace.assert_not_called()
|
|
|
|
# Response without usage
|
|
engine2 = Mock()
|
|
engine2.current_session_id = uuid4()
|
|
assert record_llm_usage(engine2, AIMessage(content="nope")) is None
|
|
engine2.record_trace.assert_not_called()
|
|
|
|
# record_trace blowing up must not propagate
|
|
engine3 = Mock()
|
|
engine3.current_session_id = uuid4()
|
|
engine3.record_trace.side_effect = RuntimeError("boom")
|
|
assert record_llm_usage(engine3, _msg_with_usage()) is None
|
|
|
|
|
|
def test_record_llm_usage_lens_calls_do_not_touch_last_prompt_tokens():
|
|
"""Background lens calls accumulate totals but never overwrite the live
|
|
context base (last_prompt_tokens) consumed by the compaction thresholds."""
|
|
engine = Mock()
|
|
engine.current_session_id = uuid4()
|
|
engine.current_step_id = None
|
|
|
|
record_llm_usage(engine, _msg_with_usage(prompt=15000))
|
|
meter = get_meter(engine.current_session_id)
|
|
assert meter.last_prompt_tokens == 15000
|
|
|
|
payload = record_llm_usage(
|
|
engine,
|
|
_msg_with_usage(prompt=300, completion=40),
|
|
source="lens:visual_transition:test-model",
|
|
update_last_prompt=False,
|
|
)
|
|
assert payload["source"] == "lens:visual_transition:test-model"
|
|
assert meter.last_prompt_tokens == 15000 # untouched by the lens call
|
|
assert meter.llm_calls == 2
|
|
assert meter.prompt_tokens == 15300
|