feat: record streaming token usage in TrackedLLMClient.stream_chat
Implement manual generator driving using next()/StopIteration to capture the return value (trailing usage dict) from inner stream_chat() implementations, enabling token tracking for streaming LLM calls. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -56,18 +56,24 @@ class TrackedLLMClient:
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
def stream_chat(self, messages: List[Dict[str, str]], *args: Any, **kwargs: Any):
|
def stream_chat(self, messages: List[Dict[str, str]], *args: Any, **kwargs: Any):
|
||||||
"""Delegate to the wrapped client's stream_chat(), recording call outcome only.
|
"""Delegate to the wrapped client's stream_chat(), recording call outcome and usage.
|
||||||
|
|
||||||
Token usage is NOT recorded here: none of the current provider
|
Drives the inner generator manually (instead of a plain `for` loop) so
|
||||||
stream_chat() implementations parse a trailing usage chunk from the
|
it can capture the generator's return value via StopIteration.value —
|
||||||
gateway (see the design doc's Known Limitations), so accumulating a
|
the trailing token-usage dict the inner client captures from a
|
||||||
token count here would silently be wrong. Only call success/failure
|
stream_options.include_usage chunk, if the gateway sent one.
|
||||||
and latency are tracked for streaming calls.
|
|
||||||
"""
|
"""
|
||||||
start = time.time()
|
start = time.time()
|
||||||
error: Optional[str] = None
|
error: Optional[str] = None
|
||||||
|
usage: Optional[Dict[str, int]] = None
|
||||||
|
gen = self._inner.stream_chat(messages, *args, **kwargs)
|
||||||
try:
|
try:
|
||||||
for chunk in self._inner.stream_chat(messages, *args, **kwargs):
|
while True:
|
||||||
|
try:
|
||||||
|
chunk = next(gen)
|
||||||
|
except StopIteration as stop:
|
||||||
|
usage = stop.value
|
||||||
|
break
|
||||||
yield chunk
|
yield chunk
|
||||||
except Exception as exc: # noqa: BLE001 - report, then re-raise unchanged
|
except Exception as exc: # noqa: BLE001 - report, then re-raise unchanged
|
||||||
error = str(exc)
|
error = str(exc)
|
||||||
@@ -77,6 +83,7 @@ class TrackedLLMClient:
|
|||||||
provider=self._inner.config.provider.value,
|
provider=self._inner.config.provider.value,
|
||||||
model=self._inner.config.model,
|
model=self._inner.config.model,
|
||||||
success=error is None,
|
success=error is None,
|
||||||
|
usage=usage,
|
||||||
latency_ms=int((time.time() - start) * 1000),
|
latency_ms=int((time.time() - start) * 1000),
|
||||||
error=error,
|
error=error,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -83,3 +83,23 @@ def test_stream_chat_records_call_without_token_usage():
|
|||||||
entry = tracker.get("deepseek", "deepseek-v4-flash")
|
entry = tracker.get("deepseek", "deepseek-v4-flash")
|
||||||
assert entry.call_count_ok == 1
|
assert entry.call_count_ok == 1
|
||||||
assert entry.total_tokens == 0
|
assert entry.total_tokens == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_stream_chat_records_usage_from_generator_return_value():
|
||||||
|
"""stream_chat() must forward the inner generator's returned usage dict to record()."""
|
||||||
|
inner = _make_inner()
|
||||||
|
|
||||||
|
def fake_stream(*args, **kwargs):
|
||||||
|
yield "chunk-1"
|
||||||
|
yield "chunk-2"
|
||||||
|
return {"prompt_tokens": 6, "completion_tokens": 2, "total_tokens": 8}
|
||||||
|
|
||||||
|
inner.stream_chat.side_effect = fake_stream
|
||||||
|
tracker = ModelUsageTracker()
|
||||||
|
|
||||||
|
chunks = list(TrackedLLMClient(inner, tracker).stream_chat([{"role": "user", "content": "hi"}]))
|
||||||
|
|
||||||
|
assert chunks == ["chunk-1", "chunk-2"]
|
||||||
|
entry = tracker.get("deepseek", "deepseek-v4-flash")
|
||||||
|
assert entry.total_tokens == 8
|
||||||
|
assert entry.call_count_ok == 1
|
||||||
|
|||||||
Reference in New Issue
Block a user