Skip to content

Commit d8c4a70

Browse files
jsonbaileyclaude
andauthored
feat!: Rename LDAIMetrics.usage and AIGraphMetrics.usage to .tokens (#175)
## Summary Standardizes on `tokens` as the field name for token usage across all AI metric types, aligning with the canonical spec rename. Renames: - `LDAIMetrics.usage` → `LDAIMetrics.tokens` (runner-level single-model metrics) - `AIGraphMetrics.usage` → `AIGraphMetrics.tokens` (runner-level graph metrics) - `LDAIMetricSummary.usage` → `LDAIMetricSummary.tokens` (managed-layer single-model summary) - `AIGraphMetricSummary.usage` → `AIGraphMetricSummary.tokens` (managed-layer graph summary) Updates the type definitions in `ldai/providers/types.py` and `ldai/tracker.py`, all internal usages across the SDK (`tracker.py`, `managed_agent_graph.py`) and provider packages (openai model + agent + agent-graph runners; langchain model + agent + langgraph runners; langgraph callback handler), tests, and README docstrings. The `to_dict()` serialization key on `LDAIMetrics` also changes from `usage` to `tokens`. No backward-compat aliases are kept -- these types are part of the in-progress unreleased SDK iteration after PR #173 (the prior rename batch). ## Coordinated PRs This is one of three coordinated PRs landing this rename together: - **sdk-specs** (canonical name source): launchdarkly/sdk-specs#163 - **python-server-sdk-ai** (this PR): targets `main` - **js-core** (sibling impl): launchdarkly/js-core#1366 (targets `feat/next-ai-release`) ## Test plan - [x] `make test` -- all 325 tests pass (server-ai 196, langchain 85, openai 44) - [x] `make lint` -- mypy, isort, pycodestyle all clean across all three packages - [ ] e2e validation via `hello-python-ai` chat-judge example (sibling agent) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Broad breaking API rename from `usage` to `tokens` across SDK/provider metric types and serialization, which will require downstream code updates and could silently drop token tracking if any call sites were missed. > > **Overview** > Standardizes token accounting field names by renaming **`usage` → `tokens`** across core metric types (`LDAIMetrics`, `AIGraphMetrics`) and their managed summaries, including `LDAIMetrics.to_dict()` now emitting a `tokens` key. > > Updates all affected runners (LangChain, LangGraph, OpenAI models/agents/graphs), tracker integration (`track_tokens`, graph total token tracking), docs, and tests to use the new `tokens` field with no backward-compatible alias. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit ccdc00e. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 583939d commit d8c4a70

23 files changed

Lines changed: 134 additions & 134 deletions

packages/ai-providers/server-ai-langchain/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ model = await ai_client.create_model("ai-config-key", context)
126126
if model:
127127
result = await model.run("Explain feature flags.")
128128
# Metrics are tracked automatically; access them via result.metrics
129-
print(result.metrics.usage)
129+
print(result.metrics.tokens)
130130
```
131131

132132
### Static Utility Methods
@@ -155,7 +155,7 @@ from ldai_langchain.langchain_helper import get_ai_metrics_from_response
155155
# After getting a response from LangChain
156156
metrics = get_ai_metrics_from_response(ai_message)
157157
print(f"Success: {metrics.success}")
158-
print(f"Tokens used: {metrics.usage.total if metrics.usage else 'N/A'}")
158+
print(f"Tokens used: {metrics.tokens.total if metrics.tokens else 'N/A'}")
159159
```
160160

161161
#### Provider Name Mapping

packages/ai-providers/server-ai-langchain/src/ldai_langchain/langchain_agent_runner.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ async def run(
5959
content=output,
6060
metrics=LDAIMetrics(
6161
success=True,
62-
usage=sum_token_usage_from_messages(messages),
62+
tokens=sum_token_usage_from_messages(messages),
6363
tool_calls=tool_calls if tool_calls else None,
6464
),
6565
raw=result,
@@ -68,7 +68,7 @@ async def run(
6868
log.warning(f"LangChain agent run failed: {error}")
6969
return RunnerResult(
7070
content="",
71-
metrics=LDAIMetrics(success=False, usage=None),
71+
metrics=LDAIMetrics(success=False, tokens=None),
7272
)
7373

7474
def get_agent(self) -> Any:

packages/ai-providers/server-ai-langchain/src/ldai_langchain/langchain_helper.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ def get_ai_metrics_from_response(response: Any) -> LDAIMetrics:
217217
:param response: The response from a LangChain model (BaseMessage or similar)
218218
:return: LDAIMetrics with success status and token usage
219219
"""
220-
return LDAIMetrics(success=True, usage=get_ai_usage_from_response(response))
220+
return LDAIMetrics(success=True, tokens=get_ai_usage_from_response(response))
221221

222222

223223
def get_tool_calls_from_response(response: Any) -> List[str]:

packages/ai-providers/server-ai-langchain/src/ldai_langchain/langchain_model_runner.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ async def _run_completion(self, messages: List[BaseMessage]) -> RunnerResult:
8282
)
8383
return RunnerResult(
8484
content='',
85-
metrics=LDAIMetrics(success=False, usage=metrics.usage),
85+
metrics=LDAIMetrics(success=False, tokens=metrics.tokens),
8686
raw=response,
8787
)
8888

@@ -91,7 +91,7 @@ async def _run_completion(self, messages: List[BaseMessage]) -> RunnerResult:
9191
log.warning(f'LangChain model invocation failed: {error}')
9292
return RunnerResult(
9393
content='',
94-
metrics=LDAIMetrics(success=False, usage=None),
94+
metrics=LDAIMetrics(success=False, tokens=None),
9595
)
9696

9797
async def _run_structured(
@@ -107,11 +107,11 @@ async def _run_structured(
107107
log.warning(f'Structured output did not return a dict. Got: {type(response)}')
108108
return RunnerResult(
109109
content='',
110-
metrics=LDAIMetrics(success=False, usage=None),
110+
metrics=LDAIMetrics(success=False, tokens=None),
111111
)
112112

113113
raw_response = response.get('raw')
114-
usage = None
114+
tokens = None
115115
raw_content = ''
116116
if raw_response is not None:
117117
if hasattr(raw_response, 'content'):
@@ -122,26 +122,26 @@ async def _run_structured(
122122
f'Multimodal response not supported in structured mode. '
123123
f'Content type: {type(raw_response.content)}, Content: {raw_response.content}'
124124
)
125-
usage = get_ai_usage_from_response(raw_response)
125+
tokens = get_ai_usage_from_response(raw_response)
126126

127127
if response.get('parsing_error'):
128128
log.warning('LangChain structured model invocation had a parsing error')
129129
return RunnerResult(
130130
content=raw_content,
131-
metrics=LDAIMetrics(success=False, usage=usage),
131+
metrics=LDAIMetrics(success=False, tokens=tokens),
132132
raw=raw_response,
133133
)
134134

135135
parsed = response.get('parsed') or {}
136136
return RunnerResult(
137137
content=raw_content,
138-
metrics=LDAIMetrics(success=True, usage=usage),
138+
metrics=LDAIMetrics(success=True, tokens=tokens),
139139
raw=raw_response,
140140
parsed=parsed,
141141
)
142142
except Exception as error:
143143
log.warning(f'LangChain structured model invocation failed: {error}')
144144
return RunnerResult(
145145
content='',
146-
metrics=LDAIMetrics(success=False, usage=None),
146+
metrics=LDAIMetrics(success=False, tokens=None),
147147
)

packages/ai-providers/server-ai-langchain/src/ldai_langchain/langgraph_agent_graph_runner.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,7 @@ async def run(self, input: Any) -> AgentGraphRunnerResult:
313313
success=True,
314314
path=handler.path,
315315
duration_ms=duration_ms,
316-
usage=total_usage if (total_usage is not None and total_usage.total > 0) else None,
316+
tokens=total_usage if (total_usage is not None and total_usage.total > 0) else None,
317317
node_metrics=node_metrics,
318318
),
319319
)

packages/ai-providers/server-ai-langchain/src/ldai_langchain/langgraph_callback_handler.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -142,11 +142,11 @@ def on_llm_end(
142142
metrics = self._node_metrics.get(node_key)
143143
if metrics is None:
144144
return
145-
existing = metrics.usage
145+
existing = metrics.tokens
146146
if existing is None:
147-
metrics.usage = usage
147+
metrics.tokens = usage
148148
else:
149-
metrics.usage = TokenUsage(
149+
metrics.tokens = TokenUsage(
150150
total=existing.total + usage.total,
151151
input=existing.input + usage.input,
152152
output=existing.output + usage.output,

packages/ai-providers/server-ai-langchain/tests/test_langchain_provider.py

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -95,10 +95,10 @@ def test_creates_metrics_with_success_true_and_token_usage(self):
9595
result = get_ai_metrics_from_response(mock_response)
9696

9797
assert result.success is True
98-
assert result.usage is not None
99-
assert result.usage.total == 100
100-
assert result.usage.input == 50
101-
assert result.usage.output == 50
98+
assert result.tokens is not None
99+
assert result.tokens.total == 100
100+
assert result.tokens.input == 50
101+
assert result.tokens.output == 50
102102

103103
def test_creates_metrics_with_snake_case_token_usage(self):
104104
"""Should create metrics with snake_case token usage keys."""
@@ -114,10 +114,10 @@ def test_creates_metrics_with_snake_case_token_usage(self):
114114
result = get_ai_metrics_from_response(mock_response)
115115

116116
assert result.success is True
117-
assert result.usage is not None
118-
assert result.usage.total == 150
119-
assert result.usage.input == 75
120-
assert result.usage.output == 75
117+
assert result.tokens is not None
118+
assert result.tokens.total == 150
119+
assert result.tokens.input == 75
120+
assert result.tokens.output == 75
121121

122122
def test_creates_metrics_with_success_true_and_no_usage_when_metadata_missing(self):
123123
"""Should create metrics with success=True and no usage when metadata is missing."""
@@ -126,7 +126,7 @@ def test_creates_metrics_with_success_true_and_no_usage_when_metadata_missing(se
126126
result = get_ai_metrics_from_response(mock_response)
127127

128128
assert result.success is True
129-
assert result.usage is None
129+
assert result.tokens is None
130130

131131
def test_usage_metadata_preferred_over_response_metadata(self):
132132
"""usage_metadata should be used when it has non-zero counts."""
@@ -355,7 +355,7 @@ async def test_returns_success_false_when_structured_model_invocation_throws_err
355355
assert result.metrics.success is False
356356
assert result.parsed is None
357357
assert result.raw is None
358-
assert result.metrics.usage is None
358+
assert result.metrics.tokens is None
359359

360360

361361
class TestGetToolCallsFromResponse:
@@ -546,10 +546,10 @@ async def test_aggregates_token_usage_across_messages(self):
546546

547547
assert result.content == "final answer"
548548
assert result.metrics.success is True
549-
assert result.metrics.usage is not None
550-
assert result.metrics.usage.total == 30
551-
assert result.metrics.usage.input == 18
552-
assert result.metrics.usage.output == 12
549+
assert result.metrics.tokens is not None
550+
assert result.metrics.tokens.total == 30
551+
assert result.metrics.tokens.input == 18
552+
assert result.metrics.tokens.output == 12
553553

554554
@pytest.mark.asyncio
555555
async def test_returns_failure_when_exception_thrown(self):

packages/ai-providers/server-ai-langchain/tests/test_langgraph_callback_handler.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ def test_on_llm_end_accumulates_tokens():
148148
result = _llm_result(total=15, prompt=10, completion=5)
149149
handler.on_llm_end(result, run_id=uuid4(), parent_run_id=node_run_id)
150150

151-
usage = handler.node_metrics['root-agent'].usage
151+
usage = handler.node_metrics['root-agent'].tokens
152152
assert usage is not None
153153
assert usage.total == 15
154154
assert usage.input == 10
@@ -166,7 +166,7 @@ def test_on_llm_end_accumulates_across_multiple_calls():
166166
handler.on_llm_end(result1, run_id=uuid4(), parent_run_id=node_run_id)
167167
handler.on_llm_end(result2, run_id=uuid4(), parent_run_id=node_run_id)
168168

169-
usage = handler.node_metrics['root-agent'].usage
169+
usage = handler.node_metrics['root-agent'].tokens
170170
assert usage.total == 16
171171
assert usage.input == 11
172172
assert usage.output == 5
@@ -203,7 +203,7 @@ def test_on_llm_end_camel_case_token_keys():
203203
)
204204
handler.on_llm_end(result, run_id=uuid4(), parent_run_id=node_run_id)
205205

206-
usage = handler.node_metrics['root-agent'].usage
206+
usage = handler.node_metrics['root-agent'].tokens
207207
assert usage is not None
208208
assert usage.total == 20
209209
assert usage.input == 12
@@ -279,10 +279,10 @@ def test_node_metrics_includes_tokens():
279279

280280
assert 'root-agent' in metrics
281281
node = metrics['root-agent']
282-
assert node.usage is not None
283-
assert node.usage.total == 15
284-
assert node.usage.input == 10
285-
assert node.usage.output == 5
282+
assert node.tokens is not None
283+
assert node.tokens.total == 15
284+
assert node.tokens.input == 10
285+
assert node.tokens.output == 5
286286

287287

288288
def test_node_metrics_includes_duration():
@@ -339,8 +339,8 @@ def test_node_metrics_multiple_nodes():
339339

340340
assert 'root-agent' in metrics
341341
assert 'child-agent' in metrics
342-
assert metrics['root-agent'].usage.total == 15
343-
assert metrics['child-agent'].usage.total == 5
342+
assert metrics['root-agent'].tokens.total == 15
343+
assert metrics['child-agent'].tokens.total == 5
344344

345345

346346
def test_node_metrics_no_tool_calls_returns_none():
@@ -364,7 +364,7 @@ def test_node_metrics_no_usage_returns_none():
364364

365365
metrics = handler.node_metrics
366366

367-
assert metrics['root-agent'].usage is None
367+
assert metrics['root-agent'].tokens is None
368368

369369

370370
# ---------------------------------------------------------------------------

packages/ai-providers/server-ai-langchain/tests/test_tracking_langgraph.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -269,10 +269,10 @@ async def test_tracks_node_and_graph_tokens_on_success():
269269
node_metrics = handler.node_metrics
270270
assert 'root-agent' in node_metrics
271271
node = node_metrics['root-agent']
272-
assert node.usage is not None
273-
assert node.usage.total == 15
274-
assert node.usage.input == 10
275-
assert node.usage.output == 5
272+
assert node.tokens is not None
273+
assert node.tokens.total == 15
274+
assert node.tokens.input == 10
275+
assert node.tokens.output == 5
276276
assert node.success is True
277277
assert node.duration_ms is not None
278278

@@ -495,8 +495,8 @@ def model_factory(node_config, **kwargs):
495495
node_metrics = handler.node_metrics
496496

497497
# Per-node token usage is keyed by node key
498-
assert node_metrics['root-agent'].usage.total == 15
499-
assert node_metrics['child-agent'].usage.total == 5
498+
assert node_metrics['root-agent'].tokens.total == 15
499+
assert node_metrics['child-agent'].tokens.total == 5
500500

501501
# Graph-level total from the real runner run
502502
ev = _events(mock_ld_client)

packages/ai-providers/server-ai-openai/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ model = await ai_client.create_model("ai-config-key", context)
112112
if model:
113113
result = await model.run("Explain feature flags.")
114114
# Metrics are tracked automatically; access them via result.metrics
115-
print(result.metrics.usage)
115+
print(result.metrics.tokens)
116116
```
117117

118118
### Static Utility Methods
@@ -141,7 +141,7 @@ from ldai_openai import get_ai_metrics_from_response
141141
# After getting a response from OpenAI
142142
metrics = get_ai_metrics_from_response(response)
143143
print(f"Success: {metrics.success}")
144-
print(f"Tokens used: {metrics.usage.total if metrics.usage else 'N/A'}")
144+
print(f"Tokens used: {metrics.tokens.total if metrics.tokens else 'N/A'}")
145145
```
146146

147147
## Documentation

0 commit comments

Comments
 (0)